User Story
As an operator running autonomous coding agents through openshell sandbox exec from a CI runner (fullsend),
I want to stop a command I started with sandbox exec, together with every process it spawned,
so that a hung, timed-out or superseded agent run does not keep running inside a sandbox I still need.
Problem Statement
sandbox exec is fire-and-forget with respect to termination. Once the command is running, nothing on the client side can end it: the exec stream reports only stdout, stderr and an exit code, the CLI has no command to signal it, and neither closing the client, the request timeout, nor an SSH signal on the channel reaches the process inside the sandbox. Processes the command left detached (nohup ... &, a dev server, a watcher) are likewise unreachable.
Impact / Why This Matters
A runner that keeps a sandbox alive across more than one command has to do process hygiene itself. Today fullsend runs a shell snippet through another sandbox exec that lists every process of the sandbox user, spares its own exec channel by walking parent pids, spares the sandbox main process by matching its argv, then sends TERM and KILL to the rest (fullsend-ai/fullsend#6753). It needs the same workaround to interrupt a running agent before resuming its session (fullsend-ai/fullsend#6959).
That workaround is racy by construction (pid reuse, processes forked during the sweep, argv collisions with the main process), it has to be re-verified against every sandbox image, and a reviewer's summary was blunt: process enumeration is what cgroups exist to avoid. A kill that also reaches an agent's detached children is something the sandbox runtime can guarantee and the caller cannot.
Without this, users of long-lived sandboxes either accept leaked processes between commands, or maintain their own enumeration sweep, or tear down and recreate the sandbox for every command and lose the workspace state that made keeping it alive worthwhile.
Proposed Design
Externally observable behaviour, implementation left open:
- Timeout terminates. When
sandbox exec --timeout N expires, the command and everything it spawned are terminated in the sandbox, not just reported as exit 124 to the client. Ideally TERM first, then KILL after a grace period.
- Disconnect terminates (opt-in or default, maintainers' call). When the client goes away, the command and its descendants are terminated the same way, so a killed CLI does not leave the sandbox busy.
- Explicit signal. A way to signal a running exec from the client: for example
openshell sandbox exec --signal TERM <exec-id> (or sandbox kill), with the exec id returned when the command starts. Signals reach the command and its descendants, including processes that detached themselves from the original process group.
- Scope is one exec. None of the above touches the sandbox main process, other concurrent execs, or the supervisor.
A per-exec cgroup would give the descendant guarantee for free and matches what the compute drivers already rely on at the container level; a per-exec process group would cover the common case but not setsid children. Either is fine from the user's side as long as the behaviour above holds.
Acceptance Criteria
Alternatives Considered
- Caller-side sweep via a second
sandbox exec (what fullsend does today): works, but is racy, must spare the caller's own exec channel by pid ancestry, and cannot tell an agent-started sleep infinity from the keep-alive main process.
- Recreate the sandbox for every command: gives clean kernel semantics but discards workspace state, bootstrap and credentials, and multiplies provisioning time for workflows that intentionally continue in one sandbox.
- Run the agent as the sandbox main process so the existing SSH signal handler reaches it: only one command per sandbox, and the main process exiting makes the sandbox terminal, so it cannot be restarted or interrupted and resumed.
- Hold exec stdin open and rely on EOF: only helps commands that exit on stdin EOF, and does nothing for detached children.
Agent Investigation
Read from the v0.0.116 source (tag v0.0.116, d1155aa); not a live probe, but it matches what fullsend observed on 0.0.115 and 0.0.116.
- The exec request carries command, workdir, env, timeout, stdin and tty size; events carry stdout, stderr and
exit_code only. No pid, exec id, signal or kill RPC: ExecSandboxRequest, ExecSandboxEvent.
- The supervisor spawns the command as
/bin/bash -c with no process group or cgroup of its own; its pid is registered only for reaping: spawn_pipe_exec, managed_children::register, is_managed in run.rs.
- Channel close and stdin EOF drop the input sender and abort output tasks; neither signals the child:
channel_close, channel_eof.
- The gateway timeout sends
exit_code: 124 to the client and returns without telling the supervisor anything: stream_exec_over_relay, and the interactive variant at L2081.
- The SSH
signal handler returns early unless the channel is attached to the main process, and then signals only the main process group: signal, signal_group. So the plumbing for delivering a signal into the sandbox exists; it is scoped to the main process today.
- The
sandbox exec CLI exposes --timeout, --tty/--no-tty, --workdir, --env; there is no kill or signal subcommand.
Checklist
User Story
As an operator running autonomous coding agents through
openshell sandbox execfrom a CI runner (fullsend),I want to stop a command I started with
sandbox exec, together with every process it spawned,so that a hung, timed-out or superseded agent run does not keep running inside a sandbox I still need.
Problem Statement
sandbox execis fire-and-forget with respect to termination. Once the command is running, nothing on the client side can end it: the exec stream reports only stdout, stderr and an exit code, the CLI has no command to signal it, and neither closing the client, the request timeout, nor an SSH signal on the channel reaches the process inside the sandbox. Processes the command left detached (nohup ... &, a dev server, a watcher) are likewise unreachable.Impact / Why This Matters
A runner that keeps a sandbox alive across more than one command has to do process hygiene itself. Today fullsend runs a shell snippet through another
sandbox execthat lists every process of the sandbox user, spares its own exec channel by walking parent pids, spares the sandbox main process by matching its argv, then sends TERM and KILL to the rest (fullsend-ai/fullsend#6753). It needs the same workaround to interrupt a running agent before resuming its session (fullsend-ai/fullsend#6959).That workaround is racy by construction (pid reuse, processes forked during the sweep, argv collisions with the main process), it has to be re-verified against every sandbox image, and a reviewer's summary was blunt: process enumeration is what cgroups exist to avoid. A kill that also reaches an agent's detached children is something the sandbox runtime can guarantee and the caller cannot.
Without this, users of long-lived sandboxes either accept leaked processes between commands, or maintain their own enumeration sweep, or tear down and recreate the sandbox for every command and lose the workspace state that made keeping it alive worthwhile.
Proposed Design
Externally observable behaviour, implementation left open:
sandbox exec --timeout Nexpires, the command and everything it spawned are terminated in the sandbox, not just reported as exit 124 to the client. Ideally TERM first, then KILL after a grace period.openshell sandbox exec --signal TERM <exec-id>(orsandbox kill), with the exec id returned when the command starts. Signals reach the command and its descendants, including processes that detached themselves from the original process group.A per-exec cgroup would give the descendant guarantee for free and matches what the compute drivers already rely on at the container level; a per-exec process group would cover the common case but not
setsidchildren. Either is fine from the user's side as long as the behaviour above holds.Acceptance Criteria
sandbox exec --timeout Nthat ignores SIGTERM is gone from the sandbox process table within the grace period after N seconds, along with any child it backgrounded withnohup ... &.openshell sandbox execclient process ends the remote command and its descendants (if disconnect termination is opt-in, with the flag set).setsid.sandbox execreference page.Alternatives Considered
sandbox exec(what fullsend does today): works, but is racy, must spare the caller's own exec channel by pid ancestry, and cannot tell an agent-startedsleep infinityfrom the keep-alive main process.Agent Investigation
Read from the v0.0.116 source (tag
v0.0.116,d1155aa); not a live probe, but it matches what fullsend observed on 0.0.115 and 0.0.116.exit_codeonly. No pid, exec id, signal or kill RPC:ExecSandboxRequest,ExecSandboxEvent./bin/bash -cwith no process group or cgroup of its own; its pid is registered only for reaping:spawn_pipe_exec,managed_children::register,is_managedinrun.rs.channel_close,channel_eof.exit_code: 124to the client and returns without telling the supervisor anything:stream_exec_over_relay, and the interactive variant at L2081.signalhandler returns early unless the channel is attached to the main process, and then signals only the main process group:signal,signal_group. So the plumbing for delivering a signal into the sandbox exists; it is scoped to the main process today.sandbox execCLI exposes--timeout,--tty/--no-tty,--workdir,--env; there is no kill or signal subcommand.Checklist