fix(sec): a derived policy inherits what the child leaves unsaid - #8
Merged
Conversation
Patch bump; the `StyleSheet` seam bee relies on is unchanged. Comments that pinned the prose to 0.3.8 now name the version they actually describe. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Attenuation validated only the rules a child *stated*, and `derive` returned the request verbatim. But every downstream layer reads absence as "nothing to enforce": an empty `[policy.network]` leaves FLAG_NET_ENFORCED clear, an empty `[policy.exec]` installs no EXEC_ALLOW entries, a policy with no write grant never sets FLAG_FS_WRITE_DEFAULT_DENY, and a dropped `!` inode pin degrades to path matching. Intentional for a root policy the operator wrote; a widening channel for a derived one, where an untrusted child could gain authority purely by omission. Silence now inherits, it does not reset. `derive` returns the *effective* child policy: an omitted filesystem/exec/network dimension is copied from the parent, a non-empty child filesystem map re-absorbs every parent `deny`, and a child exec entry whose parent counterpart is inode-pinned is re-pinned. Inheritance is trivially a subset — every inherited rule *is* a parent rule — so the FR-005 containment property is preserved by construction. Separately, the FR-008 protected defaults are injected during compilation, after attenuation, and a more-specific rule out-ranks them at load — so a child under a broad parent grant could name `~/.ssh` precisely and out-rank the protection. The table moves to a shared const in authoring tokens (one table, one truth) and attenuation refuses any child grant landing inside a protected region unless the parent named a containing region explicitly. Closes triage f001, f002, f012, f014, f025 (+ absorbed f006, f032) — five findings, one root cause. Decision recorded as research R15; the seven new tests in crates/core/tests/attenuation.rs each fail without this change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Jul 26, 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.
Closes five MEDIUM triage findings that share one root cause: attenuation validated only the rules a child stated, so an untrusted child could widen its authority purely by omission.
The hole
Every downstream layer reads absence as "nothing to enforce":
[policy.network]FLAG_NET_ENFORCEDstays clear — egress unrestricted (f012)[policy.exec]EXEC_ALLOWentries installed — any binary runs (f014)denyrulewritegrantFLAG_FS_WRITE_DEFAULT_DENYnever set (f001)!inode pinIntentional for a root policy the operator authored. Fatal for a derived one.
Separately (f002): the FR-008 protected defaults (
.git,.bee,~/.ssh,~/.aws) are injected during compilation — after attenuation — and a more-specific rule out-ranks them at load. A child under a broad parent grant could name~/.sshprecisely and out-rank the protection.The fix
Silence inherits, it does not reset.
Policy::derivenow returns the effective child policy rather than the request:filesystem/exec.allow/network.allowis copied from the parent;deny;Inheritance is trivially ⊆ the parent — every inherited rule is a parent rule — so the FR-005 containment property is preserved by construction, and the whole fix lives in the core validator where the property tests already run. It also keeps a child usable: a subagent narrowing only the filesystem does not silently lose its toolchain.
For f002, the protected-defaults table moves to a shared
PROTECTED_DEFAULTSconst held in authoring tokens (one table, one truth — the compiler resolves it, attenuation reasons over it), and attenuation refuses any child grant landing inside a protected region unless the parent named a containing region explicitly.Rejected alternative: treating an empty child list as deny-all. The backend cannot express "enforced but zero destinations" today (
plan.rsderives the enforcement flags from rule presence), so it would need a backend change to mean anything, and it makes exec unusable for a partial child. Recorded in full as research R15.Verification
crates/core/tests/attenuation.rs; with the source change stashed, exactly those 7 fail and the 11 pre-existing pass.cargo test --workspace --no-fail-fast: green exceptrepl_command::no_provider_at_all_reports_what_is_missing, which fails identically onmain(this build lacks--features enforce) — pre-existing and unrelated.cargo clippy --workspace --all-targets -- -D warningsclean, also with--features mcp;cargo fmtapplied.Docs updated: research R15,
contracts/library-api.md,contracts/policy.schema.md, and a remediation log inTRIAGE.md/TRIAGE.json(which also records the six HIGHs already closed by 8e2cdbb + 3225d44).🤖 Generated with Claude Code