feat: develop the 3 deferred hardening items (Rule-of-Two on main paths, suspend auto-reattach+GC, opt-in hard egress)#102
Merged
RNT56 merged 4 commits intoJul 10, 2026
Conversation
…serve/swarm) The lethal trifecta (untrusted web input ∧ private repo data ∧ open egress) was evaluated only on browse/desktop; the main agent paths never checked it. Add a shared enforceRuleOfTwo choke point evaluated once at each front door's startup (where egress/web/repo are known), reusing capguard.Evaluate: - chat/tui/run (attended): a trifecta prompts once at the console. - serve/swarm (headless): a trifecta with no human FAILS CLOSED (Refuse) — the exact combination the Rule of Two exists to refuse; a graduated-auto-approval envelope may still auto-proceed inside its blast fence. Default egress is deny-all/narrow, so the verdict is Allow and every normal run is byte-identical; only a wide/wildcard egress config trips it. The audit event is metadata-only (verdict + axes + labels, never the host list — I3/I7). Enforced by default; NILCORE_RULE_OF_TWO=0 is the documented operator opt-out. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… suspend/ refs The audit fixed the sleep/suspend DATA-LOSS (committed work is pinned to a durable suspend/<id> ref instead of being branch -D'd), but nothing read it back (the wake re-drove from a fresh HEAD worktree) and the refs leaked. AUTO-REATTACH: on a drive whose session has a suspended predecessor with a preserved branch (correlated by the stable <conversation> prefix of the task id; only serve drives can sleep, so run tasks never collide), the orchestrator now bases the resumed worktree on that committed work via worktree.CreateFrom instead of HEAD, retires the predecessor, deletes the consumed ref, and logs task_resumed. A stale/unresolvable ref falls back to HEAD (today's behavior — no regression, no data loss); no predecessor ⇒ byte-identical. GC: Checkpoint.SweepSuspended (wired best-effort on serve boot) prunes suspend/ refs whose task is resolved and caps the survivors to the most-recent N, closing the branch leak. New worktree.ListBranches helper. Backend contract I1 untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…l network + gateway The container backend's egress allowlist was COOPERATIVE (--network bridge + HTTP(S)_PROXY env) — bypassable by a command that ignores the proxy (curl --noproxy, raw sockets, /dev/tcp). Add an OPT-IN hard mode (NILCORE_EGRESS_HARD): - a per-run --internal docker/podman network (NO default route) to which the sandbox attaches ONLY, so the proxy is the sole egress path — unbypassable at --cap-drop=ALL, no host root/NET_ADMIN; - the allowlist proxy runs as a dual-homed GATEWAY container via a new hidden "nilcore egress-gateway" verb (reuses policy.EgressProxy — no new module, I6); - the sandbox's --dns points at the gateway to blackhole in-sandbox DNS. Fails CLOSED to --network none on any setup error (never cooperative-fallback); idempotent teardown + a label-scoped reaper on serve boot reclaim orphans. HONEST residuals (documented in docs/ARCHITECTURE.md + comments): DNS-tunnel exfil is only mitigated, hard mode needs the nilcore/sandbox image (not bare debian), and the lifecycle is Linux-container CI-validated (only the arg assembly + decision seam + gateway proxy are unit-tested on the host). The namespace backend's empty netns stays the recommended hard boundary; NILCORE_EGRESS_STRICT stays the fail-closed cooperative refusal. Default (opt-out) path is byte-identical. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- orchestrator.go: drop the dead resumeBranch/resumedFrom reset on the resume-fallback path (wt stays nil ⇒ HEAD fallback runs; the vars are not read again) — ineffassign. - ruleoftwo.go: lowercase the leading word of two error strings — ST1005. Both are behavior-preserving. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Stacked on #100 (base = its branch, not main, since #100 isn't merged yet). Develops the three "deferred (documented, not broken claims)" follow-ups from the 2026-07-10 audit. All three gates green:
make verify,make test-race(0 races),make tui-verify. Designed via a design+critique pass before implementation.1. Rule-of-Two on the main paths — a shared
enforceRuleOfTwochoke point evaluates the lethal trifecta (untrusted web ∧ private repo ∧ open egress) once at each front door's startup, reusingcapguard.Evaluate. chat (attended → one prompt); serve + swarm (headless → fail-closed Refuse — an unattended wide-egress daemon IS the forbidden config). Default deny-all/narrow egress ⇒ Allow ⇒ byte-identical; only a wide/wildcard egress trips it. Enforced by default;NILCORE_RULE_OF_TWO=0opts out. Audit event is metadata-only (never the host list).2. Sleep/suspend auto-reattach + GC — a woken drive resumes FROM its preserved
suspend/<id>work (worktree.CreateFrom) instead of a fresh HEAD, retires the predecessor, deletes the consumed ref, logstask_resumed; a stale ref falls back to HEAD (no regression/data-loss).SweepSuspendedon serve boot closes the ref leak.backend.Task(I1) untouched.3. Container egress HARD boundary (opt-in) —
NILCORE_EGRESS_HARD: a per-run--internalnetwork (no default route) the sandbox attaches to only, with the allowlist proxy as a dual-homed gateway container via a new hiddennilcore egress-gatewayverb (reusespolicy.EgressProxy, no new module). Fails closed to--network none; reaper cleans orphans.Honest caveats (in code + docs/ARCHITECTURE.md): egress hard mode is opt-in + Linux-container CI-only (only the arg assembly, decision seam, and gateway proxy are unit-tested on the host), has a residual DNS-tunnel gap (mitigated via
--dns, not fully closed), and needs the nilcore/sandbox image — the namespace backend's empty netns stays the recommended hard boundary. Two default-behavior changes: a wide/wildcard-egress unattended serve/swarm now refuses to start by default (opt out withNILCORE_RULE_OF_TWO=0); egress hard mode is off unless opted in.🤖 Generated with Claude Code