Skip to content

feat(sandbox): let the caller terminate a sandbox exec command and everything it spawned #3159

Description

@waynesun09

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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

  • A command started with sandbox exec --timeout N that ignores SIGTERM is gone from the sandbox process table within the grace period after N seconds, along with any child it backgrounded with nohup ... &.
  • Killing the openshell sandbox exec client process ends the remote command and its descendants (if disconnect termination is opt-in, with the flag set).
  • The client can send TERM and KILL to a running exec by id, and the signal reaches descendants that called setsid.
  • The sandbox main process and other running execs are unaffected by any of the above.
  • The behaviour is documented on the sandbox exec reference page.

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

  • I've reviewed existing issues and the architecture docs
  • This is a design proposal, not a "please build this" request

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    state:triage-neededOpened without agent diagnostics and needs triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions