Repository navigation
Harden profile ceilings, task-token scoping, and result evidence - #40
Merged
Merged
Conversation
profile_ceilings remapped the effective profile unconditionally, with no check that the ceiling was weaker. A mapping pointing at a stronger profile silently escalated every request naming the keyed profile, and the runner that executed was resolved from the requested profile name, so it could belong to a profile the worker never enforced. - Reject escalating and undefined ceilings at config load; keep rank <= so an equal-risk cap (the documented system-change -> repo-dev) still loads. - Re-run the runner checks against the ceiling in checkProfile, and expose Decision.EffectiveRunner so the mesh server dispatches the runner that belongs to the profile policy actually granted. - Move the require_explicit/require_lease invariant into ValidateSemantics keyed on capabilities instead of profile name, so a custom name granting edit_repo can no longer skip it. Validate the normalized capability set so legacy claims-only profiles are covered too. - Convert ValidateSemantics to a params struct; four adjacent bools made positional call sites an ordering hazard. - Add require_lease to the profile_validate request structs, tool schemas, and the profile validate CLI flags so callers can satisfy the invariant.
A per-task token authenticated a worker's runner reaching back into the gateway during subdelegation, but the five task-scoped read/cancel paths never consulted the token's provenance -- only delegate did. A worker's runner, whose output the design treats as untrusted, could therefore read the result, transcript, or artifact of, or cancel, any task this origin had delegated. Both the task context file backing a token and peer_tasks already carry origin_session_id, so the token's session is the ownership key: it covers the token's own task and anything that task delegated onward, and nothing else. No migration needed. - Gate status/result/transcript/artifact/cancel through authorizeTaskAccess. - Filter list_tasks to the token's session and withhold local task history. - Leave host-token callers unscoped; that is this machine's own agent, and origin-restart recovery depends on those lookups reaching every task. - Tasks predating this scoping have no mirrored session and stay reachable only by the host token.
… repairs Three evidence-integrity fixes: - commitGit discarded DiffStat and ChangedFiles errors, so a git failure produced an empty diffstat and empty file list -- and because the bundle is only built when files are non-empty, a real code change could be reported with no diff and no artifact. Both errors now fail the task. - RunProgram checked truncation before exit status, so a run that both overflowed the output limit and exited non-zero surfaced as result_too_large and masked worker_failed. Failure now wins, and truncation is reported on CompletedProcess so it is still visible. - repair_peers rewrote peer trust in the config but returned requires_daemon_restart: true, because reloadConfig replaced only the gateway's engine pointer while the mesh RPC server -- which enforces inbound peer policy -- kept the old one. policy.Engine.Reload now swaps config in place under a mutex so every holder of the shared engine sees it, and repair_peers reports the reload outcome honestly instead of claiming success it could not deliver.
Docs: - CLAUDE.md said the gateway exposes ~41 MCP tools; it is 31 (plus 22 on the stdio setup surface), verified against both the advertised lists and the dispatch switches. - Reword the high-risk profile invariant to match the capability-keyed enforcement, and document the ceiling-as-cap rule and task-token scoping. - docs/configuration.md's minimal-shape [profiles.dev] granted edit_repo with neither flag, so the documented config no longer validated. Fixed, along with the ceilings paragraph and the ladder's name-based framing. - repair_peers docs no longer tell users a restart is required. Hygiene: - Audit directory is 0700, matching the store; the log file was already 0600 but sat in a 0755 directory. - Chmod the SQLite db before migrations run, closing the window where task history was world-readable during the first writes. The -wal/-shm sidecars do not exist until then, so they stay in the post-migration pass, which already tolerates a missing file. - Prune leases that expired or were revoked longer than 30 days ago; the file was append-only and grew without bound. Validation still refuses an expired or revoked lease, so pruning cannot turn a denial into an accept. - Cycle detection in CheckInbound is case-insensitive, matching validateDelegationChain on the mesh RPC path. - Unify StdoutRef/StderrRef on the log: prefix via an exported runner.LogRef; the shell adapter emitted a bare path. - Factor the shared task-read prologue into fetchOwnedTask so the ownership check cannot be omitted from a future read path.
Follow-ups from review of the preceding commits: - CommandOutput.Truncated now carries output-limit truncation through the result envelope, the task mirror, and result.v1.json (whose additionalProperties:false would otherwise reject the field). Marking the process struct alone left the origin unable to tell that a worker_failed task also had incomplete stdout/stderr -- the half of the truncation fix that actually reaches the caller. - delegate derived the origin session from the request body before falling back to token provenance. Since that session is now the ownership key for task-token reads, a worker's runner could mint children under a session of its choosing and later read that session's tasks. The session now comes only from the token's stored context when one is present, and a token whose context carries no session is refused rather than falling through to the shared gateway-default session. - docs/configuration.md's [profiles.interactive] example granted four high-risk capabilities with require_explicit but no require_lease, so the documented config no longer validated. Verified, no change needed: a profile allowed by a hot-reloaded policy whose runner is absent from the startup-built registry fails cleanly with runner_unavailable; and revRange substitutes HEAD for an empty base, so failing on DiffStat errors cannot break a task in a repo without commits.
somoore
force-pushed
the
harden/policy-ceilings-and-evidence
branch
from
July 31, 2026 18:13
25b6d2c to
5082747
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 25b6d2c892
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
somoore
added a commit
that referenced
this pull request
Oct 28, 2026
…rungs The ceiling subset check rejected capping an agent profile such as repo-dev or system-change to query, because no agent profile lists the probe capabilities. A config using such a cap would then fail to load. Unrestricted shell already covers those read-only inspection commands, so treat them as implied by shell=allow. A shell=deny profile such as readonly-agent still cannot cap to them. Addresses review feedback on #40.
somoore
added a commit
that referenced
this pull request
Oct 28, 2026
Addresses review feedback on #40.
somoore
added a commit
that referenced
this pull request
Oct 28, 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.
Summary
Five commits of security hardening and evidence-integrity fixes across policy, the gateway, and the result pipeline:
checkProfilere-checks rank and runner constraints against the ceiling at request time, and callers dispatchDecision.EffectiveRunnerso the executed runner belongs to the profile policy actually granted. Therequire_explicit/require_leaseinvariant now keys on capabilities rather than profile name, so a custom-named profile granting high-risk capabilities cannot skip it.status/result/transcript/artifact/cancel) now authorize against the token's origin session,list_tasksis filtered to the token's session, anddelegatederives the session only from the token's stored context — closing a hole where a subdelegating worker's runner could read or cancel unrelated tasks, or mint children under a session of its choosing.commitGitfails the task on DiffStat/ChangedFiles errors instead of silently reporting a code change with no diff; a run that both overflows output limits and exits non-zero surfaces asworker_failed(failure outranks truncation) with truncation still carried through the result envelope, task mirror, andresult.v1.json.policy.Engine.Reloadswaps config in place under a mutex so the mesh RPC server sees repaired peer trust;repair_peersno longer claims a restart is required (or success it couldn't deliver).Test plan
make test(fmt-check, mod-check, supply-chain, workflow-lint, dup, vuln, unit, build) green in CI