How a bound command runs¶
The Cobra binding never calls a command's RunE in the server's own process.
Every invocation is a subprocess of the binary the binding was given: the
stored command path, one --name=value token per encoded flag value, a --
separator, then the positional arguments.
Why a subprocess¶
A Cobra tree is mutable shared state. Flags are bound to variables when the tree is built, and running two invocations of the same command concurrently in one process means two callers writing the same variables. Ophis ran each call in a child for this reason, and the module keeps that isolation deliberately (spec 0001 D6: shared mutable trees are not a supported concurrency strategy). It also means a command's own pre-runs, config loading and middleware run exactly as they would from a terminal, so what a client gets is what a person would get.
The cost is a process start per call and the loss of in-process access to the command's return value. The module recovers what it can from the child: stdout, stderr and the exit code are the operation's result, in the shape ophis established, so a consumer's tooling keeps reading it.
What bounds a run¶
One binding owns one supervisor. By default it holds one execution slot, and a
second call while that slot is taken fails at once with a retryable busy;
nothing queues, because a queued call would run later than the caller expects
with no way to say so. Search and inspection never take a slot, so discovery
stays available while a command runs.
A run has five minutes (shortened by any earlier deadline the caller set), a five-second cleanup budget, and one MiB of retained output across both streams. Output beyond the budget is drained and discarded rather than left in the pipe, where it would block the child; the result reports the truncation.
Stopping a run, and everything it started¶
A cancelled or timed-out command is not just killed. Its descendants have to
go too, or a sleep 600 a script spawned keeps running after the client has
moved on. The child is therefore started inside a containment unit: a new
process group on Unix, a job object on Windows that is assigned before the
child's first instruction (the child starts suspended and is resumed only once
assignment succeeds). Cleanup asks the unit to terminate, forces it halfway
through the budget, then polls until the unit reports no live process.
The boundary is the unit. A process that deliberately leaves it (a new session on Unix) is outside what the module claims to clean up, and the documentation says so rather than pretending otherwise.
When cleanup cannot be confirmed¶
Exhausting the wait is not evidence that the work stopped. If the unit still
reports a live process at the end of the budget, the call returns
cleanup_failed, the execution slot stays taken, and Binding.Status()
reports the failure. The binding keeps asking, and releases the slot only when
the unit is seen to be empty. A host that exposes Status() in its health
report therefore shows a stuck process rather than silently admitting new work
beside it.
The distinct endings¶
Timeout, cancellation, a non-zero exit and an unconfirmed cleanup are four
different failure codes, and the first three still carry the child's output
as diagnostics. A run that could not start at all is unavailable. The
failure reference lists them.
Platform acceptance¶
Cross-compilation proves the containment code builds for every platform. It does not prove a job object contains a grandchild, or that a process group is reaped within budget on a particular kernel: only a native run does. The Linux suite runs on every pipeline; native macOS and Windows jobs are wired and armed by a CI variable once runners exist. Until they have run, the platform claim for those two is "compiles and is designed to", not "proven".