fix(engine): scope-confine the co-resident deny set at the ContainNode actuation boundary - #334
Merged
thejefflarson merged 2 commits intoAug 8, 2026
Conversation
…e actuation boundary Add co_resident_denies_in_scope(graph, host, scope) beside the unfiltered co_resident_denies: unscoped -> full set (historical meaning), scoped -> retain only denies ActuationScope::in_scope accepts (the same namespace-OR-label match the webhook's EnforceScope uses). Wrap the result in a ScopedDenies newtype constructible only by that function, and change NodeContainmentActuator::apply to accept &ScopedDenies by type -- the only door into apply now demands a scope-confined set. Revert keeps &[Mitigation] (the full set) unchanged, since lifting a deny protector never placed under the current scope is a harmless no-op, but filtering revert would orphan an out-of-scope deny placed under an earlier, wider scope. Rebase contain_node_in_scope on the same function (scope.is_unscoped() || !subset.is_empty()) so proposal-side eligibility and the apply-side subset share one scope-match source instead of two implementations that could drift apart. Behavior-identical to the prior direct any-match -- existing tests pass unchanged. Closes the enforceScope escape: applying the co-resident sweep unfiltered would write default-deny NetworkPolicies into namespaces the operator never authorized (the ADR-0021 enforce-everywhere escape). Design settled in the ADR-0040 addendum (2026-08-08): protector never grows an in-product approve->apply path for node containment, so the filter lives at the actuator boundary rather than a hypothetical apply call-site. Tests: unscoped returns the full set; scoped retains the correct subset; scoped with no in-scope co-resident is empty; label-axis match; a compile_fail doctest plus a runtime witness for ScopedDenies' non-constructibility outside co_resident_denies_in_scope; contain_node_in_scope's existing fixtures pass unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP
…sident_denies-set-at-the-future
thejefflarson
deleted the
thejefflarson/jef-767-scope-filter-the-co_resident_denies-set-at-the-future
branch
August 8, 2026 20:19
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
Closes the
ContainNodeenforceScopeescape at the actuation boundary.co_resident_deniesreturns one default-deny per labelled co-resident pod on a contained node, unfiltered byenforceScope— applying it unfiltered would writeNetworkPolicys into namespaces the operator never authorized (the ADR-0021 enforce-everywhere escape).Design is settled in the ADR-0040 addendum (2026-08-08,
docs/adr/0040-node-scoped-containment-mechanism-escalation.mdonbacklog-drain-ideas/PR #331) and the accompanying brief (docs/ideas/containnode-scope-confinement.md): protector never grows an in-product approve→apply path for node containment (the durable act stays out-of-band — cordon + runbook), so the filter lives at the actuator boundary rather than a hypothetical apply call-site.What changed
co_resident_denies_in_scope(graph, host, scope)beside the unfilteredco_resident_denies: unscoped → full set (historical meaning); scoped → retain only deniesActuationScope::in_scopeitself accepts (the same namespace-OR-label match the webhook'sEnforceScopeuses).ScopedDeniesnewtype constructible only by that function — the private innerVecmeans no code outside the module can build one directly (proved by acompile_faildoctest plus a runtime witness test).NodeContainmentActuator::apply(node_containment/live.rs) to accept&ScopedDeniesby type, so the only door intoapplydemands a scope-confined set.applystill has no call site inEngine(ADR-0040 §5 / addendum — propose-first by construction, no in-product approve→apply flow at all).&[Mitigation]/ the full set, unchanged — the sharedco_resident_denieshelper is untouched, so the revert seam still lifts every deny protector could ever have placed under any historical scope.contain_node_in_scopeonco_resident_denies_in_scope(scope.is_unscoped() || !subset.is_empty()) — behavior-identical to the prior direct any-match, so eligibility and the apply subset now share one scope-match source instead of two implementations that could drift apart.Invariants preserved
ContainNodestays propose-only — no approve→apply flow added.node_containment.rsis 440 lines,tests.rs596,live.rs166 — all under the 1,000-line cap.Testing
node_containment/tests.rs: unscoped → full set; scoped → correct subset; scoped with no in-scope co-resident → empty; label-axis match;ScopedDeniesnon-constructibility (acompile_faildoctest on the type itself, since#[cfg(test)]modules are never compiled by rustdoc, plus a runtime witness test).contain_node_in_scopefixtures pass unchanged (equivalence with the prior direct implementation).cargo fmt --check,cargo clippy --all-targets -- -D warnings,cargo nextest run(1254 passed, 2 skipped — includes the repo'sfile_size_guardandself_containment_guardtests) all green.soundcheck:pr-reviewmanually against the diff (Agent tool unavailable in this context) — no Critical/High findings; the change is a pure in-process scope-filter with no new attack surface. Ran/simplifymanually as well — code was already clean on all four angles, no changes needed.Closes JEF-767
🤖 Generated with Claude Code