security: tenant + actor authority binding, replayed in branch order (2ad70f66..33ec4ed1) - #686
Merged
Merged
Conversation
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>
agent: Codex
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>
This was referenced Aug 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:mainf731d3c0(#683)2ad70f66bf308b51ba64286833ec4ed133ec4ed1is pulled forward out of order deliberately: it is the one-line test fix forba642868's tenant-scopeddelete, and the build is red without it.The two real conflicts
ci.yml— a union, not a side.mainadded theGRPC_PORTper-job allocation (concurrent Test jobs were colliding on the default 4007); red-steel addedCORECRUXD_ROUTE_AUTH=enforce. Both kept.work.rs— the one judgement call, and it is wider than the original commit.bf308b51stops trusting the caller-supplied?tenant_id=. Since it was written,mainextracted the lookup intokanban_items_for_query, now called from two places. A literal cherry-pick fixesGET /v1/workand silently leavesGET /v1/attention/summarypassing a default query —tenant_id: None, so it counts every tenant's work items into one roll-up. Holdingadmin:readsays 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.rsresolves one through the same path/v1/workuses, and itsexecplan_items_for_query/list_pending_gatescalls are scoped for the same reason.Behaviour changes, and the tests that asserted otherwise
mainhad grown tests asserting the bypasses these commits close. Each migration is documented at the site.source_fact_ids.is_empty()before the existence lookup. Both guards are right; the order was not. Fixed the cherry-pick, restoringmain's 404.console_actor_from_headersremovedx-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.operator:unverified:consoleconsolemade an unauthenticated caller indistinguishable from an authenticated one in the audit trail.Repo gates
Both fired correctly and are addressed in-PR, not worked around:
set_horizon[_for_tenant]/reverify[_for_tenant]. Red-steel adds the tenant-scoped variants rather than renaming, so both names are live.crux-mcp152 → 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— cleancargo test --workspace --locked— 7618 passed--workspace -- -D warnings), matchingmain. (--all-targetsreports 350 onmaintoo and is not what CI runs.)check-licence-headers.sh,unwrap-ratchet.sh,check_workflow_policy.py— passOperational note for reviewers
Local tooling driving
PATCH /v1/workwithby_passportagainst an auth-off daemon will now get202 queuedinstead of200 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