Skip to content

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 into
claude/nilcore-codebase-audit-ec39d8from
feat/deferred-hardening
Jul 10, 2026
Merged

feat: develop the 3 deferred hardening items (Rule-of-Two on main paths, suspend auto-reattach+GC, opt-in hard egress)#102
RNT56 merged 4 commits into
claude/nilcore-codebase-audit-ec39d8from
feat/deferred-hardening

Conversation

@RNT56

@RNT56 RNT56 commented Jul 10, 2026

Copy link
Copy Markdown
Owner

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 enforceRuleOfTwo choke point evaluates the lethal trifecta (untrusted web ∧ private repo ∧ open egress) once at each front door's startup, reusing capguard.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=0 opts 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, logs task_resumed; a stale ref falls back to HEAD (no regression/data-loss). SweepSuspended on serve boot closes the ref leak. backend.Task (I1) untouched.

3. Container egress HARD boundary (opt-in)NILCORE_EGRESS_HARD: a per-run --internal network (no default route) the sandbox attaches to only, with the allowlist proxy as a dual-homed gateway container via a new hidden nilcore egress-gateway verb (reuses policy.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 with NILCORE_RULE_OF_TWO=0); egress hard mode is off unless opted in.

🤖 Generated with Claude Code

RNT56 and others added 4 commits July 10, 2026 18:56
…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>
@RNT56 RNT56 merged commit ceae630 into claude/nilcore-codebase-audit-ec39d8 Jul 10, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant