Skip to content

Develop - #499

Open
toderian wants to merge 2 commits into
mainfrom
develop
Open

toderian wants to merge 2 commits into
mainfrom
develop

Conversation

@toderian

Copy link
Copy Markdown
Contributor

No description provided.

* test: pin the RedMesh plugin's source default of TUNNEL_ENGINE_ENABLED=False

What changed:
- New tests/test_plugin_config.py asserts PENTESTER_API_01's _CONFIG explicitly sets
  TUNNEL_ENGINE_ENABLED to False (the framework default is True) and leaves PORT to the
  framework (None -> a port drawn from 30000-32500 on every start).

Why:
RM-075 Phase 1: the plugin HTTP API is a private-network service and the node's network
policy is its security boundary (hub ADR 0001). That boundary assumes the plugin never
opens a tunnel on its own. This pins the source default; a deploy-time instance config can
still override any _CONFIG key, so the deployed value is verified by the
redmesh-boundary-check runbook, not by this test.

Checks:
- .venv/bin/python -m pytest extensions/business/cybersec/red_mesh/tests/test_plugin_config.py:
  2 passed.
- Mutation check: flipping the source default to True fails 1 of 2; restored.

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* feat(auth): require and verify the channel token on every RedMesh endpoint; resolve launch actors server-side

What changed:
- tenancy/channel.py: @channel_token_required, a functools.wraps-based innermost decorator
  that runs validate_backend_token before the body. The framework's require_token=True
  only rejects a *missing* bearer and binds the value to `token`; it never compares it
  (basic_server.j2 get_bearer_token). The comparison now happens once, here, for all 52
  endpoints instead of inline in two.
- pentester_api_01.py: every endpoint carries require_token=True and the decorator; 30
  bare `@BasePlugin.endpoint` uses become the call form; 50 endpoints gain `token: str`
  as their first parameter (the framework raises ValueError at plugin init otherwise);
  the two inline token checks and their import are gone.
- Launch attribution: the four launch_* endpoints accept `actor: {account_id}` and resolve
  it through a new _resolve_launch_actor seam -> tenancy/identity.resolve_actor over a
  minimal cstore-auth reader (tenancy/adapters/cstore_identity.py, needs only
  R1EN_CSTORE_AUTH_HKEY; never reads passwords). created_by_name/created_by_id are
  derived from the resolved account and the request's values are ignored. Missing,
  unknown, tombstoned ('null') and non-active accounts get the contract's unified
  not-found denial; a store failure fails closed as 503. Records without schemaVersion
  are accepted as version 0 (Navigator writes none); an unrecognised present value is
  rejected.
- Tests: conftest's fake endpoint decorator now mirrors the real attributes
  (__endpoint__, __http_method__, __require_token__) and an autouse fixture sets
  REDMESH_BACKEND_TOKEN; TEST_CHANNEL_TOKEN threaded through 77 positional call sites
  in 7 files; test_authz_surface.py enumerates the plugin the framework's way and pins
  count, require_token, token-first, the decorator marker, per-endpoint wrong-token
  denial with the body untouched, and launch actor denial/derivation;
  test_tenancy_channel.py and test_tenancy_identity.py cover the pieces; the native-IPC
  harness renders analyze_job with require_token like the real server.

Why:
RM-075 Phase 2, edge-node side. On a private network the shared credential is the
mitigation for other workloads on the same segment - but only if its value is checked,
and only if every endpoint checks it. Launch attribution had been an unverified body
string since the beginning.

Checks:
- pytest extensions/business/cybersec/red_mesh/tests: 2489 passed, 3 skipped (was 2416/3).
- python -m py_compile pentester_api_01.py: compiles.
- Mutation checks on test_authz_surface.py: dropping @channel_token_required from one
  endpoint fails 2 (guard + that endpoint's wrong-token subtest); dropping
  require_token=True from one endpoint fails 1. Restored.

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* fix: address execute-plan review round 1 - internal callers must not re-enter guarded endpoints

What changed:
- services/control.py: stop_and_delete_job and purge_all called owner.purge_job(job_id) /
  owner.stop_and_delete_job(job_id) - the endpoint methods, which since the sweep take
  `token` first and run the channel guard. The job id landed in the token slot, the guard
  returned a 403 dict, and the job was stopped but never purged; purge_all counted every job
  through that error. They now call the module-level purge_job / stop_and_delete_job.
- services/launch_api.py: the launch_test compatibility shim called owner.launch_webapp_scan
  / owner.launch_network_scan with keyword arguments only - a TypeError (HTTP 500) on every
  /launch_test, and no actor forwarded even if it had not been. It now calls the module-level
  launchers; the guard and actor resolution already ran once at the launch_test endpoint and
  the derived created_by_* are forwarded.
- model_testing/security.py: the deployed token is trimmed before comparison; Navigator trims
  its copy, so a trailing newline in the secret would have been a 403 on every call with no
  diagnostic.
- tests: test_authz_surface.py gains a caller-side check - no code under pentester_api_01.py,
  services/, mixins/, api_mixins/ or worker/ may call an endpoint method on owner/self/plugin
  - and a routing test that drives launch_test through the real shim into a patched
  module launcher and asserts derived attribution arrives. test_integration.py's
  stop_and_delete_job test patched the module seam (it had been changed to assert the broken
  one-argument endpoint call); TestPurgeAllJobs and the launch_test routing test route the
  module seams to the MagicMock attributes they stub, in the historical shape, so their
  assertions stay meaningful. test_tenancy_channel.py covers the trailing-newline token.

Why:
Both independent reviewers found the same class of defect the surface test cannot see: the
plugin re-entering its own endpoints. The suite had been green because the harness injected
the token at exactly that seam. The caller-side test makes the next such regression fail.

Checks:
- pytest extensions/business/cybersec/red_mesh/tests: 2492 passed, 3 skipped.

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* test: caller-side re-entry check tolerates whitespace before the call paren

What changed:
- TestInternalCallersDoNotReenterEndpoints matches `owner.<endpoint> (` as well as
  `owner.<endpoint>(`, so a spaced call form cannot slip past the check.

Why:
Round-2 verification (main-thread) of the round-1 fix: the package-wide search found no
remaining re-entry - the only same-named hit, repositories/cstore.py:334
self.get_rulebook_review, is JobStateRepository's own method - but the regex had one gap.

Checks:
- pytest tests/test_authz_surface.py: 11 passed, 56 subtests.

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* fix: reject explicitly inactive launch actors

What changed:
- Distinguish absent account state from explicit null in the CStore identity reader.
- Exercise all four launch endpoints through the real account reader for inactive states.

Why:
- RM-075 independent review found that null state allowed launches although Navigator rejects it.

Checks:
- Regression reproduced four null-state failures before the fix.
- Focused identity and endpoint tests: 23 passed, 84 subtests passed.
- Full red_mesh suite: 2493 passed, 3 skipped, 391 subtests passed.
- Independent patch review: PASS; task closure remains pending.

* fix: retain launch identity while deferring universal channel tokens

What changed:
Restore the original 52 endpoint signatures and declarations, adding only actor on four launches. Keep the original two model token guards ahead of account lookup, account-derived attribution, tunnel pin, and internal module calls. Remove obsolete channel guards and global token fixtures. Reject malformed stored schema versions uniformly.

Why:
Complete the approved paired RM-075 backend delivery while deferring new universal shared-token requirements and preserving existing model-operation protections.

Checks:
Focused checks: 430 passed, 2 skipped, 83 subtests. Schema regression red reproduced, then 32 passed with 112 subtests. Final full RedMesh suite: 2492 passed, 3 skipped, 4 warnings, 419 subtests in 147.60s. Full signature/decorator comparison against pre-RM-075 baseline passed. git diff --check and changed production py_compile passed. Native Ratio1 runtime source fixture remains unavailable; no live verification.

* feat: preserve tenant membership scope in account resolution

What changed:
- Extend account views with immutable tenant role and scope pairs.
- Reject malformed metadata and memberships without restoring legacy admin authority.
- Preserve active-account attribution and existing endpoint token behavior.

Why:
- Establish RM-026 identity integrity before tenant policy and persistence depend on it.

Checks:
- Full backend: 2498 passed, 3 skipped, 446 subtests.
- Focused identity/surface/config: 38 passed, 139 subtests.
- Two independent implementation reviewers: PASS after paired SDK-boundary correction.
- Staged diff check passed.

* feat: add tenant-scoped named-role policy evaluator

What changed:
- Added a pure existing-active-tenant policy with immutable decisions and scoped role composition.
- Added role matrix, fail-closed context, task ownership, pentesting gate and real-reader regression tests.

Why:
- Advance RM-026 policy foundations without prematurely wiring endpoints or claiming persistent tenant isolation.
- Keep capability exceptions, lifecycle, storage and deployment changes outside this slice.

Checks:
- Focused policy/identity/surface/config: 50 passed, 308 subtests.
- Full backend: 2510 passed, 615 subtests, 3 existing skips, 4 warnings.
- Two independent implementation reviews: PASS; staged whitespace check: PASS.

* feat: resolve stored tenant authorization facts

What changed:
Add read-only scoped tenant and asset projections, a storage reader port, and an authorization resolver. Reuse shared policy prechecks and add boundary regression tests.

Why:
Connect stored identity and resource ownership to policy without trusting caller authority or enabling live endpoints before publication and deployment prerequisites exist.

Checks:
69 focused tests and 402 subtests; full backend 2529 tests and 709 subtests, 3 existing skips, 4 warnings. Two independent reviews PASS. Policy baseline comparisons and staged whitespace checks pass.

* feat: persist tenant administration with verified publication

What changed:
Added seven disabled-by-default tenant administration endpoints, namespace-bound receipts and reservations, verified activation, scoped reads and membership approvals. Added account incarnation projections and regression coverage.

Why:
RM-026 needs persistent administration while retaining identity-provider membership ownership, safe creation retries and the owner's explicitly accepted CStore consistency limitations.

Checks:
Full backend pytest: 2561 passed, 769 subtests, 3 existing skips, 4 warnings. Both independent paired source/spec reviews PASS after publication-binding and Navigator integration corrections. Original policy tests and two model-token gates unchanged. git diff --check passed. Navigator final aggregate verification remains paired phase evidence, not a deployment claim.

* feat: persist scoped tenant pentester policy changes

What changed:
- Add backend-authorized desired-state Allow Pentester updates and capability hints.
- Preserve publication metadata, no-op attribution, and strict JSON boolean input.

Why:
- Deliver the settled RM-026 policy control without enabling unfinished tenant execution.

Checks:
- Focused administration/store/policy/surface: 66 passed, 357 subtests.
- Full RedMesh: 2570 passed, 798 subtests, 3 existing skips, 4 warnings.
- Two independent xhigh source/spec reviews passed; git diff --check passed.

* feat: persist scoped tenant node assignments

What changed:
- Added STA-only desired-state assignment APIs and tenant-filtered node views.
- Added shared record/address validation, lazy peer eligibility and verified per-node writes.
- Tested plugin JSON transport, stored scope, corruption, retries and uncertain writes.

Why:
- Establish basic CStore subfleet bindings without enabling unfinished tenant execution paths.

Checks:
- Focused: 81 tests and 460 subtests passed.
- Full backend: 2585 tests and 901 subtests passed; 3 existing skips and 4 warnings.
- Independent xhigh source review and independent Navigator-agent cross-review passed.
- Staged whitespace check passed; second explicit-xhigh reviewer substitution awaits owner approval.

* test: isolate fingerprint deadline timing from prior garbage

What changed:
- Collect accumulated cyclic garbage before the existing deadline stopwatch and verify shutdown afterward.

Why:
- A reproduced full-suite failure measured a 144.9 ms generation-2 collection inside the timed call; connection cleanup took less than 1 ms.
- Keep GC enabled, the real reader, 200 ms delayed stream, 10 ms budget and less-than-100 ms assertion unchanged. This isolates the fixture, not production scheduling.

Checks:
- Exact deadline test and independent reciprocal/xhigh reviews pass.
- Fingerprint suite: 74 tests and 60 subtests pass, including real sockets.
- Full backend suite with pending asset slice: 2614 tests and 1164 subtests pass; 3 existing skips, 4 warnings.
- git diff --check passes.

* feat: persist and authorize tenant preset assets

What changed:
- Add scoped create/list/detail/update/deactivate APIs for network, web/API and model presets.
- Validate canonical targets and complete CStore records; preserve creation intent, replay, target digests and stale-update guards.
- Add service/store/plugin JSON transport regression coverage and subsystem memory.

Why:
- Supply the approved tenant preset prerequisite using stored identity and scoped STA/SP authority.
- Preserve fixed network IP/per-job ports, explicit web path prefixes and full model endpoint bindings without target probes or new credentials.
- This is administration only, not tenant launch/worker enforcement or serving cutover.

Checks:
- Focused: 102 tests and 695 subtests pass; independent source/spec/security reviews pass.
- Full backend: 2614 tests and 1164 subtests pass; 3 existing skips and 4 warnings.
- Actual Python/TypeScript normalization agrees on 1782 URL combinations and 20 Unicode/name cases.
- Paired Navigator 8c76f9d: 159 suites/1720 tests, typecheck, lint and build pass.
- Fingerprint timing fixture independently stabilized in 948cb555; assertions preserved.
- git diff --check passes. No push, deployment or live scan.

* feat: persist tenant node failure policy

What changed:
- Add strict stop/continue administration endpoint and current-actor capability.
- Allow scoped Super-Tenant Admins and Tenant Admins to update the future-job preference.
- Preserve publication, attribution and verified-write semantics with transport/service tests.

Why:
- Establish the approved tenant setting before launch snapshots and lifecycle enforcement.
- No running-job behavior or stronger coordination is enabled by this slice.

Checks:
- Focused: 74 tests, 430 subtests passed.
- Full unchanged rerun: 2622 tests, 1213 subtests passed; 3 skips and 4 warnings.
- First full run had a native IPC timeout; exact test and full rerun passed unchanged, cause unproven.
- Two independent spec/quality reviews and git diff --check passed.

* feat: add tenant execution admission and binding values

What changed: Resolve current stored tenant, asset, actor and eligible node facts; preserve strict immutable binding values through scan/model config and archive serializers and CStore models. Reject corrupt explicit identity generation and archive binding mutations.

Why: Establish the reviewed prerequisite for tenant-bound launch and worker enforcement without activating endpoints or execution.

Checks: RED/GREEN admission, serialization and review regressions; two independent repeat reviews PASS; focused215 tests/633 subtests; full2637 tests/1309 subtests, 3 skips/4 warnings; diff check PASS. No activation or deployment.

* feat: enforce tenant-bound job execution

What changed:
- Complete RM-026 B2 paired launch, worker, destination, native transport and archive boundaries on the approved 026 branch.
- Reject cached foreign archive identities, malformed counters and stale state observed by the commit writer; retain finalized-stub idempotency.
- Add permanent production-path regressions and component/agent boundary documentation.

Why:
- Saved tenant assets and subfleet assignments must govern executable effects and preserve original attribution through finalization.
- Close the owner-approved review findings without adding schemas, dependencies or distributed coordination. Tenant execution remains source-disabled.

Checks:
- Full RedMesh pytest suite: 2819 passed, 1730 subtests passed, 3 existing skips, 4 warnings.
- Focused archive/model/legacy: 128 passed, 267 subtests passed; native semaphore contract: 4 passed.
- RED reproductions preceded fixes; compilation and staged diff checks passed.
- Resumed review round 2: three cross-assigned reviewers PASS, two non-author approvals per slice; documentation reviews PASS.
- No Navigator/core edits, live target/provider calls, deployment, migration or activation.

* feat: expose tenant execution display hints

What changed:
Add same-account active-asset canLaunchJobs and a strict source configuration capability hint, with policy and projection regressions.

Why:
Support RM-026 B3.1 transport without granting authorization or enabling execution.

Checks:
2823 backend tests and 1765 subtests passed; four native semaphore checks passed; two independent final reviewers passed; git diff --cached --check passed.

* feat: authorize tenant job read snapshots

What changed:
- Resolve current readers before tenant job access and return strictly validated detached snapshots.
- Preserve stable enumeration and sanitize storage failures with permanent race regressions.

Why:
- Establish the RM-026 I1a.1 foundation without reusing launch authority or enabling endpoints.

Checks:
- Full backend: 2847 tests and 1871 subtests passed; 3 existing skips and 4 warnings.
- Focused: 20 tests and 106 subtests passed; baseline and adjacent suites passed.
- Compilation, diff checks, and two independent cumulative reviews passed.

* feat: bind tenant queries and artifacts to checked jobs

What changed:
- Add checked-snapshot query projections, assigned progress reads and archive-owned triage state.
- Validate explicit artifact associations, inline analysis and bounded traversal before publication.
- Document dormant endpoint boundary and preserve legacy omission paths.

Why:
- Deliver RM-026 I1a.2 without global recovery, arbitrary CID access or reports-to-audit escalation.

Checks:
- Full backend: 2892 tests and 2022 subtests passed; 3 existing skips and 4 warnings.
- New suites: 45 tests and 151 subtests passed; adjacent and actual-finalizer checks passed.
- Compilation, whitespace checks and both independent cumulative reviews passed.

* feat: add checked legacy read compatibility

What changed:
- Resolve fresh stored legacy provenance and current rollout before detached job snapshots.
- Extend checked query and typed artifact projections with explicit unbound compatibility mode.
- Add race, alias, mode and partial-model-config regressions and boundary documentation.

Why:
- Establish I1a.3a compatibility inputs before paired requester transport without permitting bound-data fallback.

Checks:
- Full backend: 2929 tests and 2296 subtests passed; 3 existing skips and 4 warnings.
- Combined read suites: 106 tests and 531 subtests; adjacent legacy suites: 372 and 117.
- Compilation and diff checks passed; two independent cumulative round-2 reviews passed.

* feat: bind native ordinary reads to current requester authority

What changed:
Wire ten ordinary POST reads through checked tenant or proven legacy snapshots, associated reports and filtered audit. Validate generated HTTP requests and preserve legal list aliases through the supported response hook.

Why:
Prevent unscoped or stale-authority reads without changing native core or token policy. This is native local delivery; paired Navigator transport and deferred admission remain pending.

Checks:
Full backend: 3343 tests and 2296 subtests passed (3 skips, 4 existing warnings). Focused native 118, root 170/142; compilation and diff checks passed. Two independent cumulative round-2 reviews passed after permanent alias-collision and malformed-stream regressions.

* feat: authorize legacy correlation status reads

What changed:
- Admit current legacy readers and project checked detached correlation status.
- Pair strict native POST/no-store and sanitized model errors with Navigator.
- Cover native fixture lifetime and document the boundary.

Why:
- Close RM-026 I1a.3c.1 without enabling tenant integrations or read effects.

Checks:
- Canonical backend: 3422 passed, 2310 subtests, 3 skips, 4 warnings.
- Navigator: 170 suites / 2682 tests; build, typecheck, lint pass.
- Compilation, diff checks and both cumulative independent reviews pass.

* feat: authorize legacy export status reads

What changed:
- Protect four export-status endpoints with current legacy admission and detached metadata.
- Reject identity/control collisions; preserve producer dry-run semantics.
- Pair strict native POST/no-store with Navigator and document corrected inventory.

Why:
- Deliver RM-026 I1a.3c.2 without changing export mutations or tenant activation.

Checks:
- Full backend: 3738 tests / 2310 subtests pass; 3 skips, 4 warnings.
- Navigator: 171 suites / 2986 tests plus build/typecheck/lint pass.
- Both cumulative independent reviews, real producer wire replays, compilation/diff pass.

* feat: authorize and validate legacy rulebook reads

What changed:
- Pair two checked requester-bearing POST reads with exact native manifests and typed errors.
- Validate copied metadata, review, audit and submission records before publication; reuse checked staleness traversal.
- Add native producer, corruption, alias, null and finite-number regression coverage.

Why:
- Close direct native requester and record-association bypasses without authorizing tenant effects or changing mutations.

Checks:
- Full RedMesh plus native semaphore: 4270 tests, 2310 subtests passed (3 skips, 4 existing warnings).
- Focused 534 passed; compilation and diff checks passed.
- Two independent cumulative xhigh spec, quality and security reviews passed; paired Navigator replays passed.

* feat: authorize legacy MISP configuration status

What changed:
- Require requester-bearing POST and fresh legacy identity/rollout admission.
- Validate and detach the exact four-field public configuration projection before native wrapping.
- Cover roles, flags, severities, malformed transport, producer corruption and zero-domain-effect denials.

Why:
- Close actorless configuration reads without granting tenant integration or export authority.

Checks:
- Canonical backend 4530 tests / 2310 subtests passed (3 skips, 4 existing warnings).
- Broad focused 739 tests / 102 subtests and expanded configuration suite 244 passed.
- Compilation/diff and two independent cumulative spec, quality and security reviews passed.

* fix: contain unscoped public LLM diagnostics

What changed:
Make llm_health an exact actor-only POST that returns static unavailable without domain diagnostics. Preserve internal health helpers and document the intentional public compatibility break.

Why:
No scoped diagnostic permission or safe provider response contract is approved; ordinary read access must not authorize provider testing.

Checks:
TDD direct and real RAW/WRAPPED native regressions; 795 focused tests and 102 subtests; full backend 4582 tests and 2310 subtests, 3 existing skips and 4 warnings; compile/diff checks; two independent spec, quality and security reviews PASS.

* fix: keep public finding triage unavailable

What changed:
Replace unscoped triage mutation with exact actor-only POST denial; reject legacy mutation bodies and preserve checked reads, internal persistence and submission fences. Document future UI ownership in RM-076.

Why:
The owner deferred triage until its tenant UI, role authority, audit-return and SOC effects have an approved paired contract.

Checks:
TDD direct/native/persistence/inventory cases; 1057 focused tests and 104 subtests; canonical RedMesh backend 4614 tests and 2310 subtests with 3 existing skips and 4 warnings; two isolated spec, quality and security reviews PASS; explicit negative-control probes, compile and staged whitespace checks.

* feat: authorize legacy MISP JSON download

What changed:
- export_misp_json becomes a checked POST (job_id, pass_nr, request_actor),
  admitted through _read_operation with the new reports:export operation
- legacy read admission maps stored role literally: role=admin wins, else
  app_role=pentester, else user; memberships-present still denies
- explicit pass selection takes the first match, omitted takes the last,
  matching the existing export resolver rather than analysis_pass()
- disabled returns {status:"disabled"} after admission and before the
  scan-family check; enabled model jobs return typed 400 unsupported
- an existing selected running pass with a null/empty CID now fails closed
  as 503 corrupt-storage instead of 404
- consumed config/pass/aggregate payloads are checked for numeric
  finiteness before PyMISP stringifies risk/CVSS values
- internal config accessor stays bound-checked and is excluded from report()

Why:
- RM-026 slice I1a.3c.7: the endpoint previously accepted a job ID with no
  requester and then read global job/config/pass/aggregate data. Preserves
  the legacy download while placing it behind a checked requester and
  artifact-ownership edges. Tenant export activation remains I1b.

Checks:
- pytest red_mesh/tests + native semaphore contract: 4790 passed, 3 skipped,
  2337 subtests (pre-slice baseline 4782)
- focused envelope suites: 790 passed, 308 subtests
- two independent non-author round-2 reviews: PASS, no blocking findings

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* feat: serve configuration-only legacy integration readiness

What changed:
- get_integration_status becomes a checked actor-only POST admitted through
  the legacy seam: fresh stored actor, literal membership-key absence and
  current rollout, with all three legacy roles allowed
- new get_public_integration_config rebuilds from the six existing base
  builders and never calls _load_status_record or _merge_record, so no
  persisted delivery history can reach the public view
- the projection publishes exactly eleven pinned keys per integration and
  fails closed on any builder-contract deviation, including an error class
  outside the six configuration codes
- status is derived from the booleans rather than passed through, so a
  delivery-health lifecycle string cannot transit
- the historical producer is retained, documented and still under test as
  F2 groundwork; record writes and cooldown policy are unchanged

Why:
- RM-026 slice I1a.3c.8. The endpoint was unauthenticated and returned
  persisted last_event_id/last_artifact_cid aggregated across jobs the
  caller may not own. The owner chose to omit unowned history while
  retaining safe configuration/readiness status.

Checks:
- pytest red_mesh/tests + native semaphore contract: 4836 passed,
  3 skipped, 2337 subtests (4790 before this slice)
- new suite is verified RED: 11 of 30 fail against the old history
  producer, and all 18 denial cases fail if the projection is moved
  above the admission block
- two independent non-author reviews: spec compliance PASS, no blocking
  findings; the mis-targeted ordering guard they found is fixed here

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* feat: add effect state and the effect operation seam

What changed:
- new tenancy/effects.py: EffectState (none/persisted/delivered), a
  monotonic EffectLedger, and classify_effect_failure
- extract _admitted_snapshot so _read_operation and the new
  _effect_operation share exactly one admission implementation
- _effect_operation classifies failure on the ledger rather than on the
  caught exception, so a partially completed effect is never reported
  as unavailable
- a denial raised after an effect landed reports the incomplete effect,
  not a clean denial

Why:
- RM-026 I1b contract 6. _read_operation ends in a blanket
  except Exception -> 503 "unavailable", which asserts that nothing
  happened. That is true for a read and false for an effect that may
  have already written a bundle or delivered it off-node; a caller that
  retries on 503 would duplicate it.
- the state cannot ride the return value: a service that raises after
  persisting returns nothing. The ledger is therefore passed into the
  service and stays readable after a raise, which the tests pin.

Checks:
- new focused suite: 7 passed, written RED first (module absent)
- existing read admission unchanged: 486 passed across
  test_tenant_read_native and test_legacy_read_access
- pytest red_mesh/tests + native semaphore contract: 4843 passed,
  3 skipped, 2337 subtests (4836 before)

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* feat: admit and scope the persisted-artifact effects (B1)

What changed:
- dry_run_opencti_export, dry_run_taxii_export, export_stix_bundle and
  test_event_export now carry request_actor and run through
  _effect_operation (test_event_export via _admit_actor_only, since it
  has no job to scope against)
- extract _admit_actor_only from the two endpoints that inlined it, and
  give it the reports:export role gate so a job-less effect cannot be
  reached by an account a job-bearing one would refuse
- replace the unscoped global job lookup on these paths: the checked
  snapshot now threads through _prepare_*_export, build_stix_bundle and
  _resolve_pass_data
- record EffectState.PERSISTED once a bundle is on disk
- add public_effect_result: the framework maps any dict carrying an
  `error` key to HTTP 503, so a plain "integration disabled" outcome
  would reach the caller as a server failure. Results now use `status`
  and `configuration_error`, matching export_misp_json's precedent
- declare the four routes in _EFFECT_FIELDS rather than widening what
  _READ_FIELDS means; they share the read guard's strict transport

Why:
- RM-026 I1b B1. These endpoints accepted a job id with no requester and
  read the job globally. Two also wrote integration status records on
  their denial paths, so probing job ids polluted the failure counters
  that feed cooldown policy.

Checks:
- new B1 suite: 36 passed, including denial-path canaries asserting
  record_integration_status is never called on any of 7 fault classes
- pytest red_mesh/tests + native semaphore contract: 4879 passed,
  3 skipped, 2337 subtests (4843 before)
- fixed a cross-module sentinel bug found by the suite: each service
  defines its own _UNSET, so forwarding one into build_stix_bundle made
  it compare against a different object and treat the sentinel as a job

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* fix: address B1 review round 1

What changed:
- record EffectState.PERSISTED before the job-record mutation regardless
  of `persist`, and EffectState.DELIVERED after the SOC emission. With
  persist=False a packet could leave the node and a later failure still
  reported "unavailable"
- public_effect_result becomes a whitelist and validates
  configuration_error against the same set the readiness view uses. It
  previously stripped only `error`, so probe_opencti/probe_taxii's
  detail: str(exc) - carrying exception prose, the configured host and
  an env var name - travelled to the browser at HTTP 200
- implement contract 4: EffectLedger.checkpoint() re-resolves the
  requester and re-checks the job binding, called immediately before
  each persist and before the emission. A no-op for internal callers
- _effect_operation refuses a tenant_id rather than handing a
  tenant_bound snapshot raw to services that would bypass
  checked_job_snapshot and TenantJobArtifacts
- make the effect signature assertions live: the loop filtered on
  ROUTES + READ_ROUTES, so the new branch was dead code
- fix the vacuous scoping canary: the fixture's integrations resolve to
  disabled, so _prepare_* returned before the job read and the canary
  passed without exercising the scoping it names

Why:
- two independent non-author reviews both reported FAIL on contracts 4,
  6 and 7, and both found the dead transport branch.

Checks:
- pytest red_mesh/tests + native semaphore contract: 4884 passed,
  3 skipped, 2337 subtests (4879 before)
- B1 suite 41 passed, including a persist=False test proving a delivered
  event reports effect_incomplete/delivered rather than unavailable
- signature branch proven live: corrupting an expectation now fails

Corrected claim: effect routes are NOT part of the generated read API.
Adding them to the install-time model validator broke 261 tests because
no model is generated for them. Listing them in _READ_PATHS only shapes
transport errors as JSON with no-store; it does not give them the
generated routes' field validation. The comment now says so.

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* fix: address B1 review round 2

What changed:
- let a typed 500 effect_incomplete survive the read guard. Listing the
  effect paths in _READ_PATHS put them under protected_http, which
  coerces any status outside its map to 503 "unavailable" - so over real
  HTTP the entire effect-state mechanism reported the exact falsehood it
  exists to prevent, and Navigator's effect_incomplete was unreachable.
  An unrecognised effect_state still collapses to 503
- cover the dry-run job-record seam in both OpenCTI and TAXII: the
  round-1 fix had landed only in export_stix_bundle, so both still wrote
  the job record with the ledger at NONE when _persist_bundle returned
  falsy
- gate DELIVERED on the emission result. emit_export_status_event never
  raises and returns {"status": "skipped"} when SOC export is disabled -
  the common configuration - so recording it unconditionally claimed a
  delivery that never happened
- thread a ledger through test_event_export and bring the service call
  inside the guard: deliver_redmesh_event performs a real send (dry_run
  only affects the status stamp) and a raise previously escaped to the
  framework, which prints the exception string
- restore stix_bundle and last_exported_at to the whitelist. Dropping
  them broke the STIX manual Download button outright
- add the config codes these services actually emit (missing_url,
  missing_server_url, missing_collection_id, unsupported_mode). The
  whitelist held none of them, so every real misconfiguration published
  configuration_error null and the UI read "unknown_error"

Why:
- both independent reviewers returned FAIL. Three of the six round-1
  fixes were real code but not load-bearing.

Checks:
- pytest red_mesh/tests + native semaphore contract: 4890 passed,
  3 skipped, 2337 subtests (4884 before)
- B1 suite 47 passed, and the controls are now mutation-proven:
  neutering checkpoint() fails a test (previously 48/48 passed), and
  removing the new config codes fails the drift guard
- transport passthrough tested directly, including that a spoofed
  effect_state still collapses to 503

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* fix: address B1 review round 3

What changed:
- handle the RAW detail shape in protected_http. The typed passthrough
  worked only in WRAPPED: RAW unwraps the plugin dict, so the framework
  reduces the detail to the bare error string and destroys effect_state,
  and the guard fell through to 503 "unavailable". RAW now publishes
  effect_state "unknown" - the persisted/delivered distinction is lost
  there, but "something landed, do not blindly retry" survives
- register effect routes in the rendered native harness and the test
  scheduler, so they have end-to-end HTTP coverage at all. Neither the
  round-2 nor the round-3 defect was visible to tests that call plugin
  methods directly
- add an end-to-end test parametrized over RAW and WRAPPED; removing the
  raw branch fails RAW and leaves WRAPPED passing
- gate test_event_export's DELIVERED on status == "sent".
  deliver_redmesh_event returns "sent" | "disabled" | "error" and never
  "skipped", so the previous gate recorded a delivery when the
  integration was disabled and nothing left the node
- pin the whitelist against its UI consumers, so dropping a field they
  read fails a test rather than breaking a button silently
- correct the _READ_PATHS comment: it understated the guard, which does
  enforce POST-only, no query string and exact field names and types

Why:
- round 3 returned PROCEED from the implementation reviewer and REVISE
  from the security auditor, who reproduced the RAW collapse end to end.

Checks:
- pytest red_mesh/tests + native semaphore contract: 4894 passed,
  3 skipped, 2337 subtests (4890 before)
- B1 suite 51 passed; the RAW branch, the checkpoint, the DELIVERED gate
  and the config-code drift guard are each mutation-proven

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* feat: admit and instrument the OpenCTI and TAXII pushes (B2 half 1)

What changed:
- push_to_opencti and publish_to_taxii carry request_actor and run
  through _effect_operation with the checked snapshot and a ledger
- implement the returned-failure mechanism: services return delivery
  failures rather than raising, so the except arms never saw them and a
  failed push after a successful persist carried no effect_state at all.
  The predicate discriminates on the failure class, not the ledger
  alone - a typed configuration code keeps its code and gains
  effect_state, while an untypable failure with a landed effect becomes
  effect_incomplete
- record PERSISTED after each persist, checkpoint immediately before the
  outbound call, and record DELIVERED only on acceptance evidence:
  OpenCTI needs 2xx, no GraphQL errors array and an upload id, since
  GraphQL returns 200 with errors; TAXII accepts 200/201/202, because
  202 means the objects left the node
- register both routes in _EFFECT_FIELDS and EFFECT_ROUTES so they have
  end-to-end HTTP coverage

Why:
- RM-026 I1b B2. These endpoints put data on third-party systems, where
  reporting "nothing happened" after a landed effect invites a retry
  that duplicates it on someone else's server.

Checks:
- pytest red_mesh/tests + native semaphore contract: 4902 passed,
  3 skipped, 2337 subtests (4894 before)
- new B2 suite 8 passed, written RED first: the four over-the-wire
  delivery tests failed before the mechanism existed
- both halves of the predicate mutation-proven: removing it fails the
  delivery tests, removing the typed-code carve-out fails the
  configuration tests

export_misp is deliberately not registered yet. It is still a bare GET
decorator, and the native renderer raises on one; it joins in B2's MISP
phase with its GET->POST conversion, per the plan's fallback split.

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* feat: admit the MISP push and delete the swallowing mixin (B2 half 2)

What changed:
- export_misp becomes a checked POST carrying request_actor, running
  through _effect_operation, and is registered in _EFFECT_FIELDS and
  EFFECT_ROUTES now that it is no longer a bare GET decorator
- push_to_misp gains checked_job/snapshot_mode/ledger, which its two
  siblings already had, and both its scoping seams are closed: the entry
  read and build_misp_event, which reaches the store again through
  _resolve_pass_data
- delete _export_to_misp and its now-dead import. Its blanket
  except Exception -> {"error": str(exc)} both leaked exception prose to
  the response and the log and destroyed the raise the ledger depends on
- replace MISP's prose errors with typed codes: missing_credentials,
  connection_failed, api_error. The connection error text can carry the
  configured MISP URL
- the disabled path no longer emits. Routing through _effect_operation
  made that branch live for the first time, and emitting a SOC event
  plus a job-record write on a disabled integration would be behaviour
  OpenCTI and TAXII do not have
- record DELIVERED at the first accepted add_object, and on a MISPEvent
  with a uuid. PyMISP has no status code, so that is the only acceptance
  evidence; the re-export loop means the remote can hold part of an
  export before the update completes

Why:
- RM-026 I1b B2, MISP phase. A probe could previously name a real job it
  did not own and cause a SOC emission plus a job-record write.

Checks:
- pytest red_mesh/tests + native semaphore contract: 4904 passed,
  3 skipped, 2337 subtests (4902 before)
- test_misp_export, native execution and authz surface: 139 passed

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* fix: address B2 security review

What changed:
- add the MISP checkpoints contract 4 required and B2 shipped without.
  The review demonstrated a revoked requester's payload still reaching
  the MISP server, while the identical race against OpenCTI stopped
  before the outbound call. Checkpoints now sit before the transport is
  constructed and around the job-document mutation in
  _record_export_status, which also records PERSISTED
- fix vacuous acceptance evidence on the re-export branch: add_object
  and update_event results were never inspected and response_event was
  assigned the locally held event, so a fully rejected re-export
  reported status "ok" with a DELIVERED ledger
- give MISP the denial matrix, contract-4 test, transport-failure and
  rejected-re-export tests it had none of. B2 shipped its highest-risk
  endpoint with zero coverage, which is the failure mode this slice's
  own plan opens by warning about
- type OpenCTI's connection fault: it returned type(exc).__name__, which
  nothing could publish, so every connection failure showed the generic
  fallback
- extend the config-code drift guard to the delivery producers, not just
  _config_error. It immediately caught push_failed and the denial codes
- replace the last MISP prose with a typed push_failed
- prune the dead _export_to_misp reference from the config-read canary

Why:
- the security review returned FAIL on two High findings: contract 4 was
  absent on the least recoverable endpoint, and that endpoint had no
  tests at all.

Checks:
- pytest red_mesh/tests + native semaphore contract: 4914 passed,
  3 skipped, 2337 subtests (4904 before)
- B2 suite 20 passed, B1 suite 51; the MISP checkpoint is mutation-proven
  (removing it reproduces the demonstrated defect) and so is the extended
  drift guard

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* fix: address B2 implementation review

What changed:
- carry configuration_error through the transport guard. _read_error_response
  rebuilt the body from scratch, so the typed code never reached the wire:
  for OpenCTI and TAXII the persist always precedes the outbound call, so
  every delivery failure was post-persist and lost its reason. Prose is
  still refused by a conservative shape check
- stop the not-configured branch emitting. It is the twin of the disabled
  branch and was equally dead before this slice, so it was shipping the
  exact behaviour change the plan rejected for its twin - a SOC event and
  a job-record write on a misconfigured integration
- require a uuid, not just isinstance, before recording MISP DELIVERED:
  str(None) would otherwise publish "None" as an event id
- publish a typed disabled_reason so the MISP panel can say why, instead
  of showing a generic failure
- add the tests mutation testing proved were missing: the pre-outbound
  checkpoint on both HTTP services, both MISP scoping seams, MISP
  delivery-on-acceptance, and the misconfigured-path effect freedom

Why:
- the implementation review found the published-delivery-code decision
  inert over HTTP, and six controls surviving mutation with a green suite.

Checks:
- pytest red_mesh/tests + native semaphore contract: 4920 passed,
  3 skipped, 2337 subtests (4914 before)
- the three previously-surviving mutations now die: reverting MISP seam
  one, deleting MISP's DELIVERED record, and deleting OpenCTI's
  pre-outbound checkpoint each fail a named test

This is the third time a control has been live in the plugin and inert
over the wire - B1 round 2, B1 round 3, and now the configuration_error
rebuild. Every later slice should assert the response at the HTTP
boundary, not from a direct plugin call.

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* feat: admit the ingest endpoints (B3)

What changed:
- correlate_suricata_eve and upload_authorization carry request_actor;
  correlate runs through _effect_operation with the checked snapshot,
  upload through _admit_actor_only since it has no job
- close the unscoped read that fed a cross-tenant write: an unadmitted
  caller could overwrite the detection_correlation field of any job id
  it could guess
- checkpoints and ledger records at all three seams: the artifact
  persist, the job-record write, and the R1FS document write
- stop reporting ok when _write_job_record returns None without writing.
  The binding guard can trip, and the caller was told the opposite of
  what happened
- record the derived account as uploaded_by in the document envelope, so
  RM-078's deferral is a stored fact rather than a plan sentence. Never
  caller-supplied
- publish parse failures through an allowlist

Why:
- RM-026 I1b B3. Both endpoints were unauthenticated, and the correlate
  denial path wrote a node-global status record routed through the
  cooldown policy, so probing job ids was an availability effect for
  every tenant.

Checks:
- canonical backend suite: 4936 passed, 3 skipped, 2337 subtests
  (4920 before)
- new B3 suite 16 passed; the scoping fix and the no-op-write fix are
  each mutation-proven

Correction carried into the code: earlier revisions called the parse
failure a plain exception-string leak. _parse_eve_jsonl raises only
eve_jsonl_too_large or invalid_jsonl_line_<n>, neither carrying caller
content, and the line number is useful diagnostics. The allowlist keeps
it while ensuring a future raise that does carry prose cannot publish.

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* fix: address B3 security review

What changed:
- publish `correlation` and the parse/write outcome codes. Routing
  correlate through public_effect_result dropped the payload the
  operator reads, so over the wire a rejected EVE upload and a
  successful zero-match correlation were both HTTP 200 with nothing to
  tell them apart - and the UI checks only res.ok, so a truncated last
  line in a live EVE log reported a clean zero-match result
- add the over-the-wire tests the plan required first and the slice
  shipped without. The omission was not harmless: they are exactly what
  would have caught the above
- record PERSISTED only after _write_job_record actually returns. It was
  recorded before the attempt, so a refused write reported effect_state
  "persisted" when nothing had landed
- route the upload result and its failures through the projection. A
  bare `error` key makes the framework raise, which diverged RAW (503)
  from WRAPPED (R1FS exception prose verbatim at HTTP 200)
- fix a vacuous canary: the upload denial test patched the service
  module while the plugin imports the symbol into its own namespace, so
  assert_not_called bound nothing. Same trap as an earlier slice
- replace a tautological assertion that passed on the broken behaviour
- assert the parse allowlist at its real sink, the node-global status
  record, rather than at a response the projection filters anyway
- assert the bound-job refusal the plan listed as an obligation

Why:
- the security review returned spec-compliance FAIL with two High
  findings, and demonstrated the operator-facing consequence rather than
  asserting it.

Checks:
- canonical backend suite: 4946 passed, 3 skipped, 2337 subtests
  (4936 before)
- B3 suite 26 passed; both halves of the blocking defect are
  mutation-proven across RAW and WRAPPED

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* feat: admit the rulebook review mutations (B4)

What changed:
- all four mutations carry request_actor and admit through a new
  _review_operation, which reuses _admitted_snapshot rather than
  _effect_operation. The effect wrapper rewrites any status:error dict
  to effect_incomplete once the ledger is non-NONE, which would collapse
  the seventeen typed conflict codes whose retryable flag and fence
  detail are the revision-fence UX
- the signer is derived from the admitted account at the parameter
  boundary, so all twelve storage sites follow. A rulebook review is an
  attestation and the backend previously accepted whatever name it was
  handed
- admission precedes the caller-keyed submission lock, so probing job
  ids can no longer grow an unbounded module-global dict
- the checked snapshot replaces the unscoped job read in all four
- narrow update_rulebook_review to {draft, reviewed} as defence in
  depth: the only caller already clamps to these, so this closes nothing
  reachable but aligns the backend with the BFF
- carry typed conflict codes as error_code. Registering these endpoints
  made the framework map their `error` key to a plugin error, which the
  read guard collapsed to 503 - destroying the codes this slice exists
  to protect

Why:
- RM-026 I1b B4. These are the four endpoints the I1a exit gate recorded
  when it noted that a parameter named actor is not evidence of
  admission.

Checks:
- canonical backend suite: 4988 passed, 3 skipped, 2337 subtests
  (4946 before)
- boundary tests written FIRST and RED: all 42 failed before the
  implementation, including the payload-survives-the-wire cases that
  caught the 503 collapse before it was committed

The 503 collapse is the fifth instance of a control live in the plugin
and inert over the wire. It is the first one caught before commit,
because the boundary test existed first.

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* fix: address B4 security review

What changed:
- checkpoint and record the ledger at BOTH write helpers. The ledger was
  threaded into all four service signatures and referenced nowhere in
  their bodies, so a landed review write whose audit append failed was
  reported as unavailable - "no trace" - while the attestation's state
  had already changed. The plan named this twin as the single most
  likely place to add a control in one helper and miss the other, and
  the first attempt missed it at both
- replace the signer test. It was vacuous three ways and substituting
  the literal "attacker" for the derived account left all 42 tests
  green. It now asserts the stored reviewer end to end where the write
  is reachable, and the derived signer at the endpoint-to-service
  boundary for the rest; the mutation now fails 5 tests
- drop the interpolated validation message. _validate_review_answers
  raises f-strings embedding question_id and answer_value from the
  request body, and both endpoints passed str(exc) into the response,
  which the UI prefers for display. The typed code survives

Why:
- the security review returned two HIGH findings, both failures to
  implement things this slice's own plan specified.

Checks:
- canonical backend suite: 4990 passed, 3 skipped, 2337 subtests
- B4 suite 44 passed; the signer control is now mutation-proven

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* feat: admit rulebook assessment generation (B5)

What changed:
- generate_rulebook_assessment carries request_actor and admits through
  _review_operation, keeping its own rich response shape like the B4 four
- close the seam B4 budgeted and did not reach: the checked snapshot now
  threads through build_rulebook_assessment into _resolve_scan_context,
  which read the job from the store directly
- checkpoint and record the ledger before the content-addressed artifact
  persist

Why:
- RM-026 I1b B5. The endpoint was unauthenticated, built an assessment
  from an unscoped job read, persisted an artifact and wrote assessment
  metadata three times.

Checks:
- canonical backend suite: 5001 passed, 3 skipped, 2337 subtests
  (4990 before)
- boundary tests written FIRST and RED: all 11 failed before the
  implementation

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* feat: admit manual analysis and revalidate at completion (B6)

What changed:
- analyze_job takes request_actor and resolves one fresh admission decision before the busy check
  and the drain, so an unadmitted caller can neither observe nor disturb the global analysis slot.
- The checked snapshot is threaded into _prepare_manual_analysis instead of a second unscoped
  _get_job_from_cstore read.
- The requester travels on the analysis state, and _finalize_manual_analysis re-checks it before any
  completion effect. _ManualAnalysisWork is frozen and holds no plugin reference, so the worker
  cannot revalidate; the finalize step on the serialized plugin loop is the only seam that can.
- analyze_job joins the strict read transport (_EFFECT_FIELDS, EFFECT_ROUTES). Without it the
  framework mapped the denial dict to a plugin error and answered 503, so a 403/404 admission
  decision reached the caller as a generic outage.
- Three existing suites stand admission up explicitly rather than inheriting it: the native IPC
  scheduler bypass is scoped to the stub owner by identity so the real get_job_status admission in
  the same test stays live.

Why:
- RM-026 I1b B6. Legacy half only; the owner also granted manual analysis to scoped STA/SP and
  Tenant Pentesters, and that half is RM-078.
- The boundary test was written before the implementation it guards, across RAW and WRAPPED. It
  found the sixth control in this series that was live in the plugin and inert over the wire --
  this time before the commit rather than after.

Checks:
- pytest extensions/business/cybersec/red_mesh/tests: 5011 passed, 3 skipped, 2337 subtests
- pytest tests/test_manual_analysis_b6.py: 14 passed
- mutation: neutering submit-time admission fails 12 of 14; removing the completion revalidation
  fails the revalidation test (canary bound on the instance, not the class)

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* fix: address B6 review findings

What changed:
- analyze_job comes back off the strict read transport. Putting it there collapsed its typed
  operational codes -- analysis_busy 409, analysis_state_changed 409 retryable,
  analysis_request_unavailable 410, analysis_input_too_large 413, analysis_input_invalid 422,
  analysis_timeout 504 -- to a generic 503, because _read_error_response allowlists only
  {400,401,403,404,405,503}. The generated server already honours a plugin's own status_code, and
  that is what carries the admission 403/404 to the caller.
- New UNGUARDED_EFFECT_ROUTES renders analyze_job in the native fixture with the guard installed,
  so a wire test can see the collapse. The suite that asserts 409 over HTTP renders main.py without
  install_generated_read_api, which is why a green suite could not.
- checked_job is now required on _prepare_manual_analysis. Deleting it at the call site previously
  left all 5,011 tests green: the only test for that control asserted the parameter name was in the
  signature, which cannot see the call site.
- _finalize_manual_analysis reuses the snapshot its revalidation just produced instead of issuing a
  second unscoped read of the same record.
- The admission stubs in the amended suites no longer return None. Every happy path was running the
  removed checked_job fallback, so no test anywhere exercised a real snapshot reaching the
  preparation step; that is now asserted by object identity.
- The stale comment claiming I1 had not yet supplied admission is corrected, and the
  execution_binding branch is documented as the fail-closed residue for RM-078.

Why:
- Two independent reviews. Security: PASS, no blocking findings. Implementation:
  DONE_WITH_CONCERNS, spec compliance PASS, no blocking findings. Both named the wire collapse; the
  implementation review also produced the surviving mutation (M1) that this commit closes.

Checks:
- pytest, 15 file groups covering all 143 test files: 5,004 passed, 3 skipped, ~2,300 subtests
- 6 failures in one group are identical at HEAD -- pre-existing order dependence in
  test_tenant_execution_native.py, which passes alone (78) and with its usual neighbours (583)
- mutation: re-adding analyze_job to _EFFECT_FIELDS fails the new wire test in RAW and WRAPPED;
  dropping checked_job= at the call site fails the new threading test

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* feat: admit restricted raw model-test evidence (B7)

What changed:
- get_raw_model_test_evidence takes request_actor and routes through _read_operation with
  operation="reports:export". It was the last GET on the surface, took only a job_id, and returned
  the decrypted restricted artifact -- the raw prompts and model responses a model test captured.
- GET becomes POST, so a job id no longer travels in a URL into proxy logs, browser history and
  referrer headers for the most restricted payload the plugin serves.
- The job-type check now runs on the admitted snapshot instead of a second unscoped
  _get_job_from_cstore read, and a denial outranks it: unsupported_job_type distinguished a real job
  from an absent one for a caller with no authority at all.
- The pre-existing happy-path test now runs through admission with a real legacy reader rather than
  around it.

Why:
- RM-026 I1b B7. The gate decides itself rather than needing an owner call: audit:view requires a
  super_tenant_admin membership, which the legacy seam forbids outright, so it is unreachable in
  this half; reports:export is the strictest gate that can be satisfied. RM-078 owns the
  tenant-scoped half.
- Deliberately NOT on the strict read transport, and the tests say so. The guard rebuilds error
  bodies and allowlists only {400,401,403,404,405,503}, so it would collapse unsupported_job_type
  and the three raw_evidence_* codes -- the regression B6 had to revert. The generated server
  already honours the plugin's own status_code, which is what carries the 403/404.
- Contracts 4 and 6 are vacuous here: a read lands no effect and has nothing to revalidate at
  completion, so the slice gets admission, POST and a wire assertion and no further ceremony.

Checks:
- pytest, 16 file groups covering all 144 test files: 5,018 passed, 3 skipped, ~2,300 subtests
- 6 failures in one group are identical at HEAD (pre-existing order dependence in
  test_tenant_execution_native.py)
- mutation: reverting the endpoint body to its unscoped read fails 13 of the 14 new tests

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* fix: address B7 review findings

What changed:
- Cache-Control: no-store now covers the two admitted endpoints that stay off the strict read
  transport. _NO_STORE_PATHS stamps the header on http.response.start without entering
  _read_error_response, so the typed codes survive and the decrypted restricted artifact is no
  longer the one response on this surface a caching intermediary could store.
- The transport rationale is corrected. It was written as if RAW and WRAPPED behaved alike. They do
  not: in RAW a top-level `error` becomes a 500 the guard would rewrite to `unavailable`; in WRAPPED
  on_response nests the dict under `result`, no HTTPException is raised, and the guard never sees
  it. Both formats are now asserted instead of assumed symmetric.

Why:
- Two independent reviews of 4f5b2b09. Security: PASS, no blocking findings, with no-store as its
  one substantive concern. Implementation: spec compliance FAIL on the missing Navigator pairing
  (fixed in the redmesh-navigator repo), plus the half-true transport claim and the same no-store
  gap.

Checks:
- pytest, 16 file groups covering all 144 test files: 5,022 passed, 3 skipped, ~2,300 subtests
- 6 failures in one group are identical at HEAD (pre-existing order dependence in
  test_tenant_execution_native.py)
- mutation: emptying _NO_STORE_PATHS fails 6 tests across RAW and WRAPPED

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* feat: admit GDPR engagement deletion and derive its audit actor (B8)

What changed:
- delete_job_engagement takes request_actor and is admitted at reports:export before it reads the
  job, loads the JobConfig, or deletes anything. The redaction moved to _redact_job_engagement,
  which consumes the admitted snapshot instead of its own unscoped state-repository read.
- requested_by is gone. It was a caller-supplied string written straight into the deletion audit
  record as the attributed actor (services/engagement_deletion.py:315-316); the attribution now
  comes from the resolved account and nothing else.
- _review_operation takes an optional projection. B8 passes a pass-through: its typed codes must
  keep travelling under `error`, which is what its only client reads, and it reports explicit
  per-stage counts rather than one code, so the rename has nothing to protect.
- The ledger is recorded at the two irreversible seams -- PERSISTED once the sanitized JobConfig is
  on disk, DELIVERED per document actually deleted -- so an unexpected raise after either cannot
  reach the caller as "nothing happened".
- Four failure branches no longer interpolate the exception into the returned message; the prose
  stays in the node log.

Why:
- RM-026 I1b B8. This is the B4 defect class on a destructive operation: a parameter named after an
  identity is not evidence of one, and here it decided who a GDPR deletion was attributed to.
- Not _effect_operation: it rewrites any `status: "error"` dict to effect_incomplete once the ledger
  is non-NONE, which would collapse the per-stage counts. A partial redaction has to say which
  documents went and which did not.
- Not on the strict read transport, for the same reason: the guard rebuilds the body and would
  discard those counts along with the typed code.

Checks:
- pytest, 16 file groups covering all 145 test files: 5,036 passed, 3 skipped, ~2,300 subtests
- 6 failures in one group are identical at HEAD (pre-existing order dependence in
  test_tenant_execution_native.py)
- mutation: substituting the attributed actor fails test_the_audit_record_carries_the_resolved_account
  -- the same mutation that was vacuous three ways in B4

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* fix: address B8 review findings

What changed:
- The response uses the review projection after all. The pass-through kept the typed code under
  `error`, and the framework maps any dict carrying `error` to a plugin error -- in RAW the template
  then reduces the whole body to the bare code string, discarding documents_deleted,
  documents_failed and new_job_config_cid on exactly the partial outcomes that exist to report them.
  Under the projection the response stays 2xx and carries every field in both formats.
- ledger.checkpoint() before the irreversible delete loop. `record` does not run the checkpoint
  callback -- only `checkpoint` does -- so authority was established once, before three storage
  round-trips, and never rechecked, unlike services/authorization_upload.py:218.
- Admission narrows to role == "admin" via a new _review_operation(require_admin=True). The shared
  reports:export gate admits an admin OR an app_role pentester, which is right for the rulebook
  mutations and broader than the owner's recorded B8 decision, on an endpoint that irreversibly
  deletes authorization documents. Deny-by-default until the owner says otherwise.
- The `project` parameter added by the previous commit is gone; it had no caller and was used as a
  boolean, so a real projection passed later would have been silently ignored.
- Two more {exc} interpolations removed, in services/engagement_deletion.py.
- /delete_job_engagement joins _NO_STORE_PATHS. The comment above UNGUARDED_ROUTES claimed all its
  entries got no-store, which was false for this one.
- The pre-admission `if not job_id` short-circuit is gone, so a blank job id answers
  400 invalid_request from admission like every other endpoint.

Why:
- Two independent reviews. Security: PASS, no blocking findings. Implementation: spec compliance
  FAIL on the RAW count loss and the service-layer prose, plus the missing checkpoint and no-store.
- Four tests were vacuous or absent, and the reviews proved it by mutation rather than assertion:
  test_failures_do_not_publish_exception_prose patched a symbol called outside any try, so the
  wrapper's generic handler satisfied it and none of the four de-prosed branches was covered; and
  the delivered-state test …
… MISP floor (#500)

* fix: redact credential pairs in NIS2 rulebook evidence refs

What changed:
- services/rulebook_assessment.py: `_safe_text` now applies the shared
  `redact_credential_text` rule before its own keyword/PEM/bearer patterns.
- tests/test_rulebook_assessment.py: generated assessment with a
  default-credential finding title carries `user:***`, never the secret.

Why:
- RM-064 Phase 1 (reopened 2026-09-03): the delivered PDF's §3.7.3 printed both
  default-credential pairs in cleartext, twice each. This module carried its own
  sanitiser whose patterns require a `password=`-style prefix and never matched a
  bare `user:secret` pair in a finding title. Reusing the shared rule instead of
  adding a fourth regime.

Checks:
- pytest tests/test_rulebook_assessment.py: 35 passed
- client job 6cc55610 titles through `_safe_text`: 25 pairs -> 0 (scratchpad, by path only)

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* fix: close three credential-redaction regex defects

What changed:
- credential_redaction.py: post-colon `(?![\s/])` guard so the `with`/`accepts`
  leads no longer destroy URLs; a continuation rule masks every pair in a
  comma/semicolon/and-separated list, not only the first; comments record why
  there is deliberately no port guard on this rule.
- mixins/report.py `_CRED_RE`: secret quantifier `{3,64}` -> `{3,}` so a secret
  longer than 64 characters no longer publishes its tail; cross-reference to
  the five-digit port concession and to the blackbox path.
- tests/test_credential_redaction.py (new): direct unit tests pinning every
  probe phrasing, the URL guard, list masking, `admin:1234`.
- tests/test_normalization.py: 100-character secret masked to the end.

Why:
- RM-064 Phase 1 items found 2026-09-04: (a) `with https://host:8080/x`
  became `with https:***`; (b) `admin:admin, admin:password` left the second
  pair intact; (c) `user:***AAAAAA` for long secrets. The architect review
  proposed a port guard on the ambiguous leads; rejected because the HTTP
  Basic evidence reads `GET … with admin:1234 -> HTTP 200` and that pair is in
  the default list — a port guard there would publish it.

Checks:
- pytest test_credential_redaction.py test_normalization.py
  test_rulebook_assessment.py test_event_redaction.py: 86 passed
- old `{3,64}` rule reproduced the tail leak on a 100-char secret before the fix

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* fix: redact credential pairs inside the LLM input builders

What changed:
- llm_input_builder.py `_sanitize`: applies `redact_credential_text` to every
  string that flows through `build_llm_input`, so all three callers
  (pentester_api_01, services/llm_structured, redmesh_llm_agent_api) are
  covered without each having to redact first.
- mixins/redmesh_llm_agent.py `_sanitize_untrusted_text` and the graybox
  `top_vulnerable_titles` summary: same rule. This mixin hand-builds its
  payload without `build_llm_input`.
- tests: default-credential pair never reaches the LLM input payload; the
  agent sanitizer masks a pair.

Why:
- RM-064 Phase 1 `[~]` item (found 2026-09-04 in the #491 review): only one
  of three `build_llm_input` call sites redacted the report first, and the
  narrative the model writes is persisted separately from the findings, so a
  finding-level fix never reached it.

Checks:
- pytest test_llm_input_isolation.py test_llm_agent_injection.py
  test_llm_agent_validator.py test_hardening.py test_secret_isolation.py
  test_credential_redaction.py: 87 passed
- both new tests red before the change

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* fix: deny-by-default credential redaction across the three scrubbers

What changed:
- credential_redaction.py: `redact_credential_strings(obj)` walks dicts,
  lists and tuples and applies the phrasing rule to every string leaf except
  under `IDENTITY_KEYS` (hashes, ids, enums).
- Wired as the final step of `_scrub_flat_finding` (graybox storage
  boundary), `_redact_report` (whole redacted report, after the enumerated
  shape-based walks) and `build_finding_event` (SIEM payload).
- Tests: a pair under a key none of the three scrubbers enumerate is masked
  on each path; whole-report data without credential phrasing is untouched.

Why:
- RM-064 Phase 1 (found 2026-09-04): all three scrubbers were field
  allowlists, and each carried a comment naming a field that leaked because
  it was not on the list (`evidence_items`, `affected_assets`,
  `vulnerabilities`, `accepted`, `web_tests_info`). The next field added
  leaked by default. Walking every string is safe because the rule is
  phrasing-anchored; the URL guard from the previous commit is what makes
  that hold, so this commit depends on it.

Checks:
- pytest test_findings_redaction.py test_normalization.py
  test_event_redaction.py test_credential_redaction.py: 97 passed
- full red_mesh suite with the hooks in place: 5518 passed, 3 skipped, the
  single failure being this commit's own graybox test before its fixture
  was corrected (`error` never reaches the flat contract)

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* feat: derive CVSS base scores and withhold contradicting probe templates

What changed:
- cvss.py (new): CVSS v3.1 base score from a vector and the qualitative band,
  tested against published scores.
- findings.py `enrich_finding_for_probe`: populates `cvss_score` from the
  finding's vector when the probe supplied none (NVD scores on CVE findings
  are kept); a probe `cvss_template` is now attached only when its band
  agrees with the finding's severity label.
- tests: scored template, withheld contradicting template, probe-supplied
  vector scored, supplied score not overwritten.

Why:
- RM-064 item 3 / Phase 3 `[~]`: every finding on the client job carried a
  vector with `cvss_score` null, and 31 of 61 carried a vector whose band
  contradicted the label. An AST scan of `worker/` on 2026-09-21 found 91 of
  168 statically resolvable `Finding(...)` sites in a different band from
  their probe's template — the template is the probe's worst case, the label
  is per finding. Re-labelling 91 sites is a product decision; withholding a
  vector that contradicts the label removes the contradiction now, and any
  probe that wants a vector on such a finding sets its own.
- The score is set before identity stamping, so `finding_signature` moves
  once for findings that gain a score (content change); `finding_id` is
  unaffected.

Checks:
- pytest test_cvss.py test_probe_registry.py test_finding_identity_contract.py
  test_probes.py: 317 passed

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* feat: record the MISP severity floor and counts on the export record

What changed:
- services/misp_export.py: `build_misp_event` returns the `MIN_SEVERITY`
  floor it applied plus exported/total finding counts; `push_to_misp` stores
  them on the job's `misp_export` record; `get_misp_export_status` surfaces
  them (None on records written before this).
- tests: stored record after a successful push carries the floor; status
  surfaces it, and reports None rather than a default for older records.

Why:
- RM-064 item 7: the export intentionally omits findings below `MIN_SEVERITY`
  (56 objects against 61 findings on job 6cc55610) and the report never said
  so, because nothing recorded the floor at export time. The Navigator
  already reads this status; the report can now disclose the floor from it.

Checks:
- pytest test_misp_export.py test_tenant_export_status.py
  test_integration_status.py: 327 passed

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* feat: gate default-credential findings on the negative control (RM-069)

What changed:
- worker/service/common.py: `_default_credential_findings` builds the
  SSH/FTP/Telnet default-credential findings after each probe's existing
  random-pair test. Random pair rejected -> CRITICAL, `certain` when an
  authenticated action completed (`id` over SSH, `PWD` over FTP, `id`/`uname`
  over Telnet), `firm` on a bare handshake. Random pair accepted -> INFO,
  `tentative`, title marked inconclusive, pointing at the "accepts arbitrary
  credentials" finding. `raw_data.auth_control` records the control outcome.
- tests/test_default_credential_control.py (new): verdict in both
  directions, proof gating, and the SSH probe wired end to end with paramiko
  mocked.
- tests/test_credential_redaction.py: the new title and evidence shapes are
  masked.

Why:
- RM-069, promised in the 2026-08-26 client reply: a host accepting
  arbitrary credentials also produced CRITICAL default-credential findings
  asserting a genuine weakness, and a successful handshake was the entire
  evidence. The control already ran in every probe; its outcome never
  reached the verdict. Execution order is unchanged, so the number of
  authentication attempts per host is unchanged.

Checks:
- pytest test_default_credential_control.py test_credential_redaction.py
  test_probes.py test_normalization.py: 308 passed

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* feat: package-aware CVE matching with explicit backport status (RM-070)

What changed:
- cve_db.py: `DistroPackage` + `parse_distro_package` read the distribution
  suffix a banner announces (`OpenSSH_8.9p1 Ubuntu-3ubuntu0.10`,
  `Debian-2+deb12u2`); `_compare_debian_revision` orders package revisions
  dpkg-style (`0.3 < 0.10`, `~` before release); `BACKPORT_FIXES` is a small
  static advisory table (USN-6859-1 rows for CVE-2024-6387 on 22.04/24.04);
  `check_cves(..., package=)` drops matches the package revision has fixed
  and states `backport_status` (`not_fixed` -> `firm`, `unknown` ->
  `tentative`, with the reason in the description) on the rest.
- findings.py: `Finding.backport_status` field.
- worker/service/common.py: the SSH probe passes the parsed package to
  `check_cves` and records `raw_data.ssh_package`; `_ssh_identify_library`
  is unchanged.
- tests/test_cve_backport.py (new): real banner shapes, revision ordering,
  status resolution, the client job banner no longer reporting regreSSHion,
  callers without a package unchanged.

Why:
- RM-070, promised in the 2026-08-26 client reply. The matcher compared the
  bare upstream version, so a package that had the fix backported was
  reported vulnerable with only a prose disclaimer. Decision: a static table
  now, a USN/DSA/OVAL feed as the recorded follow-up; anything the table does
  not cover is `unknown`, never silently fixed. Confidence stays `tentative`
  on unknown, so the risk score already weights it at half.

Checks:
- pytest test_cve_backport.py test_check_cves_enrichment.py test_probes.py
  test_finding_identity_contract.py test_default_credential_control.py:
  340 passed

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* fix: profile the SSH proof-action timeout through the resolver

What changed:
- `_ssh_authenticated_action` takes its timeout from the caller's
  `_target_timeout(3)` instead of a literal.

Why:
- The full suite's timeout call-site audit
  (`test_network_worker_waits_use_resolver_except_timing_probe`) allows one
  literal network wait in `worker/`; the RM-069 proof action added a second.

Checks:
- pytest test_timeout_profile.py test_default_credential_control.py: 15 passed
- full red_mesh suite before this fix: 5548 passed, 3 skipped, this one
  failure

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* fix: address execute-plan review round 1 (backend)

What changed:
- credential_redaction.py: an empty secret is masked too (`\S*?`), so
  `Auth OK for root:` — `("root", "")` is a live MySQL default — no longer
  discloses that the password is blank; the walker docstring records the
  `with X:Y` over-masking cost.
- worker/service/common.py: the negative control is tri-state
  (`rejected` | `accepted` | `not_run`); a control whose attempt failed
  before the server answered caps the default-credential finding at `firm`
  and says so, instead of reading as a passed control. Telnet cannot
  distinguish the two and is documented as such.
- services/misp_export.py: the duplicated `findings_exported` /
  `findings_total` keys in `build_misp_event`'s result are gone (the counts
  were already there; only `min_severity` was new).

Why:
- Reviewer findings 2, 5 and 8 from the independent implementation review of
  `feat/rm-064-closeout`; none blocking, all cheap, two of them
  credential-verdict honesty items worth landing before the client rerun.

Checks:
- pytest test_default_credential_control.py test_credential_redaction.py
  test_misp_export.py test_normalization.py test_event_redaction.py
  test_rulebook_assessment.py test_llm_input_isolation.py
  test_findings_redaction.py test_probes.py test_timeout_profile.py:
  470 passed

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* fix: accept an empty credential secret only where probes print one

What changed:
- _PAIR_TEMPLATE: the secret is one or more characters, or empty only at the
  end of the text or before " (" (the `root:` and `ftp: (anonymous)` shapes).
- test_credential_redaction pins four benign "with/for <word>: <text>" labels.

Why:
- The review-round-1 empty-secret allowance turned every
  `with <word>: <text>` into `<word>:*** <text>`. No leak, but the
  deny-by-default walker runs the rule over every string, so finding prose
  was corrupted. Found by the closeout e2e on a graybox report line.

Checks:
- pytest red_mesh/tests: 5553 passed, 3 skipped
- live archives 267a1f40 / 9b6f4928 and both rendered PDFs: 0 unmasked pairs
- git diff --check: clean

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* fix: fail a graybox job whose first pass aborted before any probe ran

What changed:
- graybox worker records abort_reason_class on its report; the aggregation
  rules and AggregatedScanData carry the abort state (the archive round-trip
  dropped it before).
- PassReport carries aborted / abort_reason / abort_phase / abort_reason_class.
- finalization: _pass_abort_state derives an "empty abort" (aggregated.aborted
  and no probe attempted). On pass 1 it is a FAILED job via _mark_scan_aborted
  (failure_class scan_aborted, redacted message naming the phase, timeline
  scan_aborted, lifecycle redmesh.job.failed, archive kept); later passes emit
  pass_aborted and continue. An empty abort skips risk scoring and the LLM
  stage; an abort after probes ran finalizes as before with the fields set.

Why:
- A scan that failed to log in finalized as a completed job with a risk score
  computed from its own "Scan aborted" finding and four minutes of analysis
  of nothing; the console, the PDF and the e2e harness all read it as a
  successful scan (RM-085, found by the closeout live e2e).

Checks:
- pytest test_api.py test_finalization_aggregation.py test_worker.py: 279 passed
- pytest red_mesh/tests: 5557 passed, 3 skipped
- live graybox a5fb8293 against the 500-on-login fixture: FAILED in 17 s,
  failure_class scan_aborted, no ANALYZING stage
- git diff --check: clean

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* fix: address review of the scan-aborted finalization rule

What changed:
- A later monitoring pass that aborted with no probe attempted no longer
  overwrites the job's headline risk_score (the pass record still says 0).
- An empty abort submits no terminal attestation and is not counted as a
  failed required attestation; the job fails as scan_aborted instead.
- _abort_reason_text: credential redaction, control bytes stripped, 240-char
  cap, used for failure_message and both abort timeline labels (the same
  treatment the LLM boundary gives abort_reason).
- Tests: risk_score preserved on pass 2; no attestation call on an empty
  abort; the sanitizer's bounds.

Why:
- Independent review of 623b029 (RM-085): readers take pass_reports[-1]
  as the job's score, an on-chain "0 vulnerabilities" record for a scan that
  never logged in is a worse artifact than none, and two _abort callers
  embed exception text or the operator's target URL.

Checks:
- pytest test_api.py test_finalization_aggregation.py test_worker.py: 281 passed
- pytest red_mesh/tests: full suite result recorded in RM-085
- git diff --check: clean

Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC

* chore: increment version
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant