Skip to content

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".