kortixd update/rollback are now context-aware. In-sandbox they DRIVE the
existing entrypoint supervisor (stage → exit 75 → supervisor swaps); standalone
they keep the safe self-swap. The supervisor's staged-swap / crash-loop-rollback
machinery is UNCHANGED — kortixd stages, the supervisor swaps.
Branch: p2/kortixd-supervisor-interface (off origin/main). Committed, not
pushed. 3 files changed, +382/-18.
apps/kortix-sandbox-agent-server/src/cli.ts— context-aware update/rollback.apps/sandbox/entrypoint.sh— setKORTIX_SUPERVISED=1(+ export state dir).apps/kortix-sandbox-agent-server/src/__tests__/cli-update.test.ts— new tests.
The ONLY entrypoint change is two exported env vars before the supervisor loop
(no change to the swap/rollback/crash-loop logic, no kortixd install added):
mkdir -p "${AGENT_STATE_DIR}" 2>/dev/null || true
+# Tell the daemon (and any `kortixd` invocation that inherits this env) that a
+# supervisor owns the binary swap. `kortixd update` then STAGES ${AGENT_NEXT}
+# and exits ${SWAP_CODE} for this loop to install, instead of self-swapping its
+# own running binary — which is unsafe and which warm-fork/resume/restart would
+# not re-run anyway. Export the resolved state dir so it stages into the exact
+# slot select_agent/promote_staged_agent read. See apps/kortix-sandbox-agent-server/src/cli.ts.
+export KORTIX_SUPERVISED=1
+export KORTIX_AGENT_STATE_DIR="${AGENT_STATE_DIR}"
+
COMPILED_RUNTIME_PATH=""KORTIX_SUPERVISED=1 is only read by kortixd's management verbs
(update/rollback). The daemon's normal serve path never reads it, so boot is
byte-for-byte unchanged. No kortixd install was added to boot: the baked
binary is already the immutable floor and select_agent already resolves it, so
an extra boot-time install would add risk for zero benefit.
Detection (detectSupervised): supervised iff KORTIX_SUPERVISED=1 (primary,
set by the entrypoint), OR — fallback for an older baked entrypoint — the running
binary is a supervisor-managed path (<state>/agent.current or the baked floor)
and the state dir exists. A standalone kortixd on a normal machine matches
neither. --standalone / --supervised force either path (testing/recovery).
SUPERVISED (kortixd update, in-sandbox):
- Resolve target (manifest /
--from). No-op if the running binary already matches, or ifagent.next.sha256already equals the target (idempotent). - Download → verify content digest → smoke-test the candidate
(
version+--health-check). - Stage into
<state-dir>using the EXACT contract ofruntime-assets.ts:stageAgentBinaryandentrypoint.sh:promote_staged_agent: writeagent.next.sha256(content"<sha>\n") FIRST via atomic rename, then rename the verified binary intoagent.next. Never touches the live binary. - Return exit code 75 (
AGENT_SWAP_EXIT_CODE). Under the supervisor loop this triggers the atomic swap + health-supervision + crash-loop rollback. Run from a shell, the exit is harmless and the stage persists for next boot.
STANDALONE (kortixd update, off-sandbox): unchanged — download → verify →
smoke-test → atomic self-swap (keep <name>.prev) → post-swap health →
auto-rollback to .prev on failure.
ROLLBACK: supervised → performSupervisorRollback mirrors
entrypoint.sh:rollback_agent exactly (restore agent.prev→agent.current, or
drop the override to fall back to the baked floor; latch agent.pinned; discard
any staged agent.next). Standalone → consume <name>.prev (unchanged).
One extra hardening in realRun: a non-executable smoke-test candidate can throw
ENOEXEC synchronously from child_process.spawn, escaping the 'error'
handler. It is now caught and mapped to exit 126, so a bad candidate is a clean
"candidate failed — kept current binary" with temp-file cleanup, never an
uncaught throw. This fixes both the supervised and standalone paths.
$ bun tsc --noEmit
(no output, exit 0)
New tests: supervised staging (exit 75, live binary untouched, agent.next +
matching sha256), supervised no-op, supervised broken-candidate refusal,
supervised already-staged re-run, detectSupervised, performSupervisorRollback
(with prev / no prev / nothing-to-roll-back). Plus the original 9 standalone
tests (self-swap, digest mismatch, pre/post-swap smoke failure, auto-rollback,
best-effort, digest cache).
19 pass 0 fail 58 expect() calls
runtime-assets.test.ts + runtime-convergence.test.ts also green (77 pass / 0
fail together with cli-update).
The supervisor still swaps a staged agent.next and rolls back a crash-looper.
The staged-file contract is UNCHANGED, so the harness needed no edits.
PASS swap: staged binary promoted, relaunched, installed as current
PASS bad digest: staged binary discarded, live binary kept running
PASS crash-looping update rolls back to the baked binary and pins
PASS update installs beside the baked binary and never overwrites it
PASS first bad update with no predecessor falls back to the baked binary
PASS pinned box refuses staged updates
... (12 passed, 0 failed)
- A. Supervised staging (
KORTIX_SUPERVISED=1 kortixd update):exit=75;sha256(agent.next) == agent.next.sha256(so the supervisor's independent re-verification accepts it); LIVE BINARY UNTOUCHED; target NOT self-swapped; no.prev. - B. Standalone (
--standalone):exit=0; target self-swapped to the build;.prevkept. - C. Supervised rollback (prev present): restores
agent.prev, latchesagent.pinned, consumes prev, discards stagedagent.next;exit=0. - D. Supervised rollback (no prev): removes
agent.current→ drops to the baked floor, latches pin;exit=0. - E. Broken staged candidate: smoke test fails (
version exited 126);exit=1; nothing staged; no leaked temp file — the supervisor never sees a bad build.
- Immutable baked floor. The root-owned baked binary is never written by
kortixd; supervised rollback with no predecessor drops back to it by removing
the
agent.currentoverride (harness 6b/6c still green). - Independent re-verification. The supervisor re-hashes
agent.nextagainstagent.next.sha256before promoting. kortixd writes the identical"<sha>\n"+ verified-binary contract (proven: keystone A shows the two match). - Atomic, no partial binary. kortixd stages via same-filesystem
rename(2), side-car first then binary — the exact orderstageAgentBinaryuses; any interruption leaves a state the supervisor already refuses. - Crash-loop rollback + pin. Untouched. A kortixd-staged binary that dies fast still rolls back and pins (harness test 6 green).
agent.prevrollback target. Untouched; supervised CLI rollback follows the same restore-prev / drop-to-floor + pin logic.- Pinned box refuses staged updates. Untouched (harness test 7 green).
- No self-overwrite of a running binary. kortixd never self-swaps in-sandbox — it stages and exits 75, exactly the case the supervisor exists to handle for warm-fork/resume/restart.
- Failure-biased. A bad artifact, bad digest, or unrunnable candidate leaves a working box: kortixd smoke-tests before staging, and the supervisor re-verifies before promoting.
Highest-risk change in the epic; kept minimal and reviewable. Not pushed — the coordinator reviews the diff with the user before merge, since this touches the sandbox boot for every box.