Skip to content

security: tenant + actor authority binding, replayed in branch order (2ad70f66..33ec4ed1) - #686

Merged
CueCrux-Myles merged 7 commits into
mainfrom
redsteel/replay-a
Aug 11, 2026
Merged

CueCrux-Myles merged 7 commits into
mainfrom
redsteel/replay-a

Conversation

@CueCrux-Myles

Copy link
Copy Markdown
Contributor

Continues the red-steel replay (#630), now in branch order rather than by theme. Stacks on #683.

Why the order changed

The plan sliced the branch thematically and measured conflicts by cherry-picking each commit in isolation onto main — 21 of 26 conflicting. That is the wrong measurement for a linear stack. Replayed in order:

commit alone vs main in order
f731d3c0 (#683) 2 files / 3 hunks —
2ad70f66 — 1 file / 1 hunk
bf308b51 — 1 file / 1 hunk
ba642868 7 files / 13 hunks 0
33ec4ed1 0 0

33ec4ed1 is pulled forward out of order deliberately: it is the one-line test fix for ba642868's tenant-scoped delete, and the build is red without it.

The two real conflicts

ci.yml — a union, not a side. main added the GRPC_PORT per-job allocation (concurrent Test jobs were colliding on the default 4007); red-steel added CORECRUXD_ROUTE_AUTH=enforce. Both kept.

work.rs — the one judgement call, and it is wider than the original commit. bf308b51 stops trusting the caller-supplied ?tenant_id=. Since it was written, main extracted the lookup into kanban_items_for_query, now called from two places. A literal cherry-pick fixes GET /v1/work and silently leaves GET /v1/attention/summary passing a default query — tenant_id: None, so it counts every tenant's work items into one roll-up. Holding admin:read says you may read a summary, not whose.

Taking a side there would have dropped the fix on one surface while looking complete — the failure mode the WIP branch flagged for workspace-scan. So the shared helper now requires an authenticated tenant as a parameter instead of reading q.tenant_id, which makes omitting it a compile error rather than a silent leak. attention.rs resolves one through the same path /v1/work uses, and its execplan_items_for_query / list_pending_gates calls are scoped for the same reason.

Behaviour changes, and the tests that asserted otherwise

main had grown tests asserting the bypasses these commits close. Each migration is documented at the site.

Unknown-canonical undo returned 400, not 404 Red-steel checks source_fact_ids.is_empty() before the existence lookup. Both guards are right; the order was not. Fixed the cherry-pick, restoring main's 404.
console_actor_from_headers removed It read the actor off x-corecrux-passport-id, so an unauthenticated caller could attribute a review mutation to themselves. Test replaced, not deleted — it now pins that a bare header does not authorise one.
Console actor is operator:unverified:console The prefix is the provenance signal; a bare console made an unauthenticated caller indistinguishable from an authenticated one in the audit trail.
Auth-off work transitions queue for review The integration test asserted the bypass: naming an ungated passport applied a transition for anyone who could reach the port.
MCP work mutations need an authenticated agent, claim must match That test gets its own token daemon. Tokenising the shared one would 401 ~20 other MCP calls in the file that legitimately test the unauthenticated read surface.

Repo gates

Both fired correctly and are addressed in-PR, not worked around:

  • Mutator audit — registers all four of set_horizon[_for_tenant] / reverify[_for_tenant]. Red-steel adds the tenant-scoped variants rather than renaming, so both names are live.
  • unwrap ratchet — crux-mcp 152 → 172. All 21 additions are inside #[cfg(test)] mod t1_regression (lib.rs:65), so this is the rebaseline the ratchet explicitly sanctions.

Verification

  • cargo check --workspace --all-targets --locked — clean
  • cargo test --workspace --locked — 7618 passed
  • clippy — 0 under CI's gate (--workspace -- -D warnings), matching main. (--all-targets reports 350 on main too and is not what CI runs.)
  • check-licence-headers.sh, unwrap-ratchet.sh, check_workflow_policy.py — pass

Operational note for reviewers

Local tooling driving PATCH /v1/work with by_passport against an auth-off daemon will now get 202 queued instead of 200 applied. That is the intended fix, but it is a live behavioural change, not test-only.

Plan and decision log: PlanCrux/.agent/execplans/redsteel-remediation-replay-2026-08-07.md.

🤖 Generated with Claude Code

CueCrux-Myles and others added 7 commits August 10, 2026 22:52
Require admin:write for structural governance mutations, derive secure route-auth defaults from listener and auth posture, preserve semantic read/capability reachability, and pin enforce mode across shipped packaging.\n\nagent:codex-work
Cherry-picked from red-steel `bf308b51`, resolved against current `main`.

Work reads and mutations now answer for the *authenticated* tenant rather
than whatever `?tenant_id=` the caller supplied.

**Extended beyond the original commit, deliberately.** Since red-steel was
written, `main` extracted the kanban lookup into
`work::kanban_items_for_query`, now called from two places. A literal
cherry-pick fixes `GET /v1/work` and silently leaves the second caller —
`GET /v1/attention/summary` — passing a default `ListWorkQuery`, so its
`tenant_id` is `None` and `list_work` counts every tenant's items into one
roll-up. Holding `admin:read` says you may read a summary, not whose.

Taking a side of the diff here would have dropped the fix on one of the two
surfaces while looking complete — the same failure mode the WIP branch
flagged for workspace-scan. So the shared helper now *requires* an
authenticated tenant as a parameter instead of reading `q.tenant_id`, which
makes the omission a compile error rather than a silent leak, and
`attention.rs` resolves one through the same `work_scope_context` +
`resolve_authorized_tenant` path `/v1/work` uses. Its `execplan_items_for_query`
and `list_pending_gates` calls are tenant-scoped for the same reason.

Verified: `cargo check --workspace --all-targets --locked` clean.

Refs #630. Ordered replay of redsteel-remediation-replay-2026-08-07.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Update the eviction test helper for the tenant-aware fact-store API so the integrated workspace suite compiles and exercises the intended default tenant.

Co-Authored-By: OpenAI Codex <codex@openai.com>
…anged

Four commits landed above (`2ad70f66`, `bf308b51`, `ba642868`, `33ec4ed1`).
Each closed a real bypass; `main` had since grown tests that asserted the
bypass. Migrating them, with the reasoning, so a later reader can tell a
deliberate contract change from a regression.

- Unknown-canonical undo returned **400 instead of 404**: red-steel checked
  `source_fact_ids.is_empty()` before the existence lookup. Both guards are
  correct, the order was not — existence now wins, restoring `main`'s 404.
  This one is a fix to the cherry-pick, not to a test.
- `console_actor_from_headers` is gone: it read the actor straight off
  `x-corecrux-passport-id`, so an unauthenticated caller could name themselves
  on a review mutation. Its test is replaced rather than deleted, pinning that
  a bare header no longer authorises one.
- Console actor is now `operator:unverified:console`, not `console`. The
  prefix is the whole provenance signal.
- Work transitions from auth-off identities **queue for review** instead of
  applying. The integration test asserted the bypass directly: naming an
  ungated passport applied a transition to anyone who could reach the port.
- MCP work mutations require an authenticated agent, and the claimed
  passport/tenant must match it. That test gets its own token daemon —
  tokenising the shared one would 401 ~20 other MCP calls in the file that
  legitimately exercise the unauthenticated read surface.

Two repo gates caught the rest, which is them working:

- The `FactStore` mutator audit registers all four of
  `set_horizon[_for_tenant]` / `reverify[_for_tenant]` — red-steel *adds* the
  tenant-scoped variants rather than renaming, so both names are live.
- unwrap ratchet: `crux-mcp` 152 -> 172. All 21 additions are inside
  `#[cfg(test)] mod t1_regression` (lib.rs:65), so this is the rebaseline the
  ratchet sanctions, not an erosion of it.

Verified: `cargo check --workspace --all-targets --locked` clean;
`cargo test --workspace --locked` 7618 passed; clippy 0 under CI's gate
(`--workspace -- -D warnings`), matching `main`.

Refs #630. Ordered replay of redsteel-remediation-replay-2026-08-07.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Update stale actor-attribution fixtures for the tenant partition introduced before M17. Work collaborators remain mutually visible inside the work tenant, while legacy default-tenant facts remain isolated and retain a null actor.

Co-Authored-By: OpenAI Codex <codex@openai.com>
…t it

`2ad70f66` added a case asserting that a MISSPELLED route-auth mode falls
back to Enforce rather than silently disabling route auth. The misspelling
is the input under test, and the Lint job's `typos` check fails on the
literal.

Assembled from fragments rather than exempted in `_typos.toml`: a global
extend-word would suppress a genuine instance of the same misspelling
anywhere else in the tree, which is a worse trade for one test.
`spellchecker:disable-line` is not honoured by the pinned typos version —
tried first, and it flagged the comment too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@CueCrux-Myles
CueCrux-Myles added this pull request to the merge queue Aug 11, 2026
Merged via the queue into main with commit a9751b5 Aug 11, 2026
25 checks passed
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