# Remote deny-gate (box-side guard parity) — design

audience: AI coding agents first. Implementer: codex (`-m gpt-5.6-terra -c model_reasoning_effort=medium`).
Owner intent (2026-08-15 ruling): no `pi_spawn:remote` factory runs until a box-side shim
layer exists and the owner clears it. This spec is that shim layer. It is the named
precondition for flipping factory to the k3s/remote preset.

## Context

On the workstation, agents are fenced by a guard chain: a `git` PATH shim blocks
destructive verbs against the shared main checkout and reroutes `push` into the land
queue; hooks deny writes into protected paths. Inside the box-side execution surfaces —
`sandbox-run` containers (image `modules/sandbox/image/`) and harness seats
(`modules/harness/seat/`) — none of that exists: a contained agent holding staged codex or
claude credentials (`modules/workstation/claude/lib/agent-credentials.sh` path) has a raw
`git` and could push anywhere the credential reaches. The sandbox image already has a
PATH-interception seam (`etc/toolgap.bash`, `bin/sandbox-toolgap`) and a fail-closed
entrypoint (`bin/sandbox-egress-init`).

## Contract

1. **`sandbox-git-shim`** — new `modules/sandbox/image/bin/git`, installed ahead of
   `/usr/bin/git` on the image PATH. POSIX-sh dispatcher, decision before exec:
   - DENY always: `push --force`/`-f`, any `--no-verify`, `push` to a ref matching
     `refs/heads/main` or `refs/heads/master` on ANY remote.
   - ALLOW `push` only to result-lane refs: `refs/cdx/*`, `refs/harness-seat/*`,
     `refs/heads/factory-result/*`, `refs/heads/wip/*`. Everything else is DENY.
   - DENY destructive verbs (`reset --hard`, `clean -f*`, `checkout --`/`checkout .`,
     `restore`, `stash drop|clear`, `gc --prune=now`, `update-ref -d`, `branch -D`)
     when the resolved git dir lives OUTSIDE the container workspace
     (`/sandbox/workspaces/*`) — the disposable workspace stays fully usable.
   - Everything else execs `/usr/bin/git` unchanged. A denial prints one line naming the
     verb, the target, and this spec's filename, exits 86, and appends a JSONL record to
     the toolgap file (`SANDBOX_TOOLGAP_FILE`) with `{kind:"git-deny",argv,cwd,ts}`.
2. **Fail-closed presence check.** `sandbox-egress-init` (already the mandatory
   entrypoint) additionally verifies `command -v git` resolves to the shim path and that
   the shim's self-test (`git --deny-gate-selftest`, exercising one DENY and one ALLOW
   decision without touching a repo) exits 0 — otherwise refuse launch with exit 12,
   same pattern as the egress rules check.
3. **Seat parity.** The seat provisioning path (`modules/harness/seat/`) mounts or bakes
   the same shim for seat containers; a seat that cannot prove the shim refuses to start.
   Trace where seats set PATH; do not fork a second shim implementation — one file, two
   installers.
4. **Clearance receipt for the owner.** A proof script
   (`modules/sandbox/test/deny-gate-proofs.sh`, mirroring `denial-proofs.sh`) runs INSIDE
   a live container on each box and shows every DENY rule actually blocking (RED proof)
   and the ALLOW lanes passing, emitting one machine-readable receipt per box. Factory
   remote stays off until the owner reads these receipts and clears — this spec does not
   flip anything.

## Out of scope

Flipping `pi_spawn` to remote; any change to `sssf.config.yaml`; the k3s Factory pod path
(it already has controller-only publication and no in-pod credential — this gate is for
credentialed sandbox/seat surfaces); workstation-side shims (already exist).

## Acceptance

- Image unit tests: shim decision table covered case by case, including the boundary
  cases `push origin HEAD:refs/heads/main` (DENY), `push mirror sha:refs/cdx/x/out`
  (ALLOW), `reset --hard` inside `/sandbox/workspaces/x` (ALLOW), same outside (DENY).
- `sandbox-egress-init` refuses launch when the shim is absent or self-test fails
  (test by masking the shim in a scratch image layer or PATH override).
- `bash modules/sandbox/host/test/*.test.sh` and the image build stay green.
- Live: `deny-gate-proofs.sh` receipt from each of debian1/2/3 attached to the plan doc.
