You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found during an architecture review of main (6257211). internal/cli/start.go is the highest-churn file in the repo (20 of the last 120 commits).
runLaunch is a 769-line single function that holds an entire process lifecycle in local variables. Because no object owns the acquired resources, teardown is written twice — and the two versions disagree. The code's own comment asserts they agree.
Refs #174 (the 123-line arming phase inside runLaunch), #173 (the facade-wiring phase). Both shrink this function; doing them first makes this extraction much smaller.
allSecrets[i].Zero() — plaintext secrets are not wiped
start.go:376-380
os.RemoveAll(sandboxTmp) — temp dir leaked
:649
cacheScope.Close() — releases a flock (toolcache/cache.go:131)
:668
auditor.Close(), cancel()
:637, :700
The comment at start.go:606-607 states the opposite of what the code does:
"Under --audit-strict a failure to open/write is fatal: fatalTeardown runs the same cleanup the deferred teardowns do and exits non-zero."
That is exactly the drift an owning object prevents.
Related: the secret-zeroing discipline is already defeated
start.go:611-616 copies every resolved secret into secretValues []string via sec.ExposeString() to seed the audit redactor, and keeps it for the whole run. Go strings are immutable, so Secret.Zero() (internal/secrets/secret.go:45-50) can never reach those copies — the redactor's seed permanently defeats the zeroing the rest of the file maintains. Feeding the redactor Secret values (or hashes/lengths) fixes it.
Two more symptoms of the same cause
fatalTeardown is forward-declared (start.go:620, body assigned 280 lines later at :900) purely because a closure must reference locals that do not exist yet.
createRuntimeDir (start.go:1288-1303) does os.RemoveAll(dir) on a path derived only from sha256(workdir) (:1291-1295) and is untested. Two concurrent omac start in one workdir: the second deletes the first's rtDir, unlinking the live bridge.sock (:601). createRuntimeDirServe (serve.go:1790-1803) is the same 14 lines with a different salt.
Suggested fix / Ask
Give the launch an object that owns what it acquires.
Add a launch.Session (own package or internal/cli) owning rtDir, auditor, cache scope, supervisor, facade, control plane, sandbox tmp dir and the resolved secrets: go func Open(env Env, plan Plan) (*Session, error) // phases 4–7 func (s *Session) Argv(plan Plan) ([]string, error) func (s *Session) Run(argv []string, extra map[string]string) (int, error) func (s *Session) Close() error // the ONLY teardown
Replace fatalTeardown with s.Close(); return ExitIOError — no os.Exit mid-pipeline, so secret zeroing, tmp removal and the flock release cannot be skipped.
Seed the audit redactor from secrets.Secret values (or hashes) rather than []string, so Zero() is meaningful.
Reduce runLaunch to flag parsing + plan building + Open/Argv/Run (target ≲ 100 lines).
Reuse the same Session from runServe so the two entry points stop re-deriving the sequence.
Make createRuntimeDir concurrency-safe (or fail loudly on an in-use rtDir) and unify it with createRuntimeDirServe; add a test for the concurrent-launch case.
Acceptance criteria
Exactly one teardown path; a strict-audit write failure runs the same cleanup as a normal exit (regression test asserting secrets zeroed, sandbox tmp removed, cache flock released).
No os.Exit inside the launch pipeline.
Audit redaction no longer requires immutable copies of secret values.
Phases 4–7 exercisable in tests without execing a harness.
A second omac start in the same workdir cannot delete the first's runtime dir.
runLaunch and runServe share the sequence rather than duplicating it.
Non-goals
Changing CLI flags, exit codes or any user-visible launch behaviour.
Duplicate check
Context / Summary
Found during an architecture review of
main(6257211).internal/cli/start.gois the highest-churn file in the repo (20 of the last 120 commits).runLaunchis a 769-line single function that holds an entire process lifecycle in local variables. Because no object owns the acquired resources, teardown is written twice — and the two versions disagree. The code's own comment asserts they agree.Refs #174 (the 123-line arming phase inside
runLaunch), #173 (the facade-wiring phase). Both shrink this function; doing them first makes this extraction much smaller.Problem / What
internal/cli/runLaunch—internal/cli/start.go:180-948::205, 224, 231, 247, 278, 316, 351, 592, 603, 673, 707, 729, 758, 775, 848, 896, 910)return Exit*sites, 8defers (:376, 637, 649, 668, 675, 700, 751, 770), 18 rebinds of one sharederrsandbox.ExecWithReadycall (:936)optsinto 9 shadow locals (:181-189), so bothopts.noSandboxandnoSandboxstay live for 750 linesinternal/cli/runServe(serve.go:48-530, 483 lines) re-derives much of the same sequenceThe concrete defect: the strict-audit abort skips four cleanups
fatalTeardown(start.go:900-908) ends inos.Exit, which skips everydefer:Diffed against the deferred stack, it omits:
allSecrets[i].Zero()— plaintext secrets are not wipedstart.go:376-380os.RemoveAll(sandboxTmp)— temp dir leaked:649cacheScope.Close()— releases a flock (toolcache/cache.go:131):668auditor.Close(),cancel():637,:700The comment at
start.go:606-607states the opposite of what the code does:That is exactly the drift an owning object prevents.
Related: the secret-zeroing discipline is already defeated
start.go:611-616copies every resolved secret intosecretValues []stringviasec.ExposeString()to seed the audit redactor, and keeps it for the whole run. Go strings are immutable, soSecret.Zero()(internal/secrets/secret.go:45-50) can never reach those copies — the redactor's seed permanently defeats the zeroing the rest of the file maintains. Feeding the redactorSecretvalues (or hashes/lengths) fixes it.Two more symptoms of the same cause
fatalTeardownis forward-declared (start.go:620, body assigned 280 lines later at:900) purely because a closure must reference locals that do not exist yet.createRuntimeDir(start.go:1288-1303) doesos.RemoveAll(dir)on a path derived only fromsha256(workdir)(:1291-1295) and is untested. Two concurrentomac startin one workdir: the second deletes the first'srtDir, unlinking the livebridge.sock(:601).createRuntimeDirServe(serve.go:1790-1803) is the same 14 lines with a different salt.Suggested fix / Ask
Give the launch an object that owns what it acquires.
launch.Session(own package orinternal/cli) owningrtDir, auditor, cache scope, supervisor, facade, control plane, sandbox tmp dir and the resolved secrets:go func Open(env Env, plan Plan) (*Session, error) // phases 4–7 func (s *Session) Argv(plan Plan) ([]string, error) func (s *Session) Run(argv []string, extra map[string]string) (int, error) func (s *Session) Close() error // the ONLY teardownfatalTeardownwiths.Close(); return ExitIOError— noos.Exitmid-pipeline, so secret zeroing, tmp removal and the flock release cannot be skipped.secrets.Secretvalues (or hashes) rather than[]string, soZero()is meaningful.runLaunchto flag parsing + plan building +Open/Argv/Run(target ≲ 100 lines).SessionfromrunServeso the two entry points stop re-deriving the sequence.createRuntimeDirconcurrency-safe (or fail loudly on an in-usertDir) and unify it withcreateRuntimeDirServe; add a test for the concurrent-launch case.Acceptance criteria
os.Exitinside the launch pipeline.execing a harness.omac startin the same workdir cannot delete the first's runtime dir.runLaunchandrunServeshare the sequence rather than duplicating it.Non-goals
omac startmode #70).Evidence / Environment
main@6257211, Linux.🤖 Filed by an agent during an architecture review of
main.