Skip to content

Commit 69a05eb

Browse files
authored
fix(sandbox): complete successful main processes (#2884)
1 parent 57c7f74 commit 69a05eb

60 files changed

Lines changed: 2686 additions & 832 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/openshell-cli/SKILL.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ This creates a sandbox whose canonical main process is `/bin/bash -l` and
7474
attaches your terminal to that retained process. Add `--detach` to return after
7575
the sandbox becomes ready without attaching.
7676

77+
An explicit trailing command is foreground even when stdin or stdout is not a
78+
terminal. The CLI streams its stdout and stderr and returns its exact exit
79+
status. Exit code 0 leaves a retained sandbox in `Completed`; nonzero leaves it
80+
in `Error` with `MainProcessFailed`. Use `--no-keep` to delete either result
81+
after output drains, or `--detach` for a long-running service.
82+
7783
When supplying `--name`, use a portable DNS-1123 label: at most 63 lowercase alphanumeric or `-` characters, beginning and ending with an alphanumeric character. The Kubernetes driver rejects uppercase letters, underscores, dots, and other names that cannot become Kubernetes resource labels.
7884

7985
**Shortcut for known tools**: When the trailing command is a recognized tool, the CLI auto-creates the required provider from local credentials:
@@ -248,11 +254,16 @@ Key flags:
248254
- `--approval-mode manual|auto`: Control handling of agent-authored policy proposals; `manual` is the default
249255
- `--upload <PATH>[:<DEST>]`: Upload local files into the container working directory or an explicit destination
250256
- `--no-git-ignore`: Disable `.gitignore` filtering for uploads
251-
- `--no-keep`: Delete the sandbox after the initial command or shell exits
257+
- `--no-keep`: Delete the sandbox after main output and the exit result drain
252258
- `--detach`: Start the canonical main process without attaching
253259
- `--forward [BIND_ADDRESS:]PORT`: Forward a local port and keep the sandbox alive
254260
- `--editor vscode|cursor`: Open a remote editor after creation and keep the sandbox alive
255261

262+
`--detach` adds no attachment grace period. When the canonical process exits,
263+
its terminal phase is reported immediately. A foreground create declares one
264+
expected main-process SSH attachment; cleanup finalizes after that connection
265+
closes naturally.
266+
256267
Do not combine `--upload` with a trailing main command. Uploads currently finish
257268
after the canonical process starts; create a scratch sandbox and use
258269
`sandbox exec`, or build the files into the image.
@@ -363,7 +374,10 @@ openshell sandbox start [name]
363374
Both commands default to the last-used sandbox. Stop stops background
364375
forwards and waits for `Stopped`; start waits for `Ready`. Connect, exec,
365376
file transfer, forwarding, and exposed services are unavailable while
366-
stopped. Delete remains the operation that removes retained state.
377+
stopped or completed. Starting a retained `Completed` or
378+
`Error/MainProcessFailed` sandbox launches a fresh canonical-main instance and
379+
invalidates SSH sessions from the previous runtime generation. Delete remains
380+
the operation that removes retained state.
367381

368382
---
369383

.agents/skills/openshell-cli/cli-reference.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -207,18 +207,28 @@ identity provider. Requires an authenticated gateway connection.
207207
Create a sandbox through the selected gateway and launch its canonical main
208208
process. By default, the CLI attaches to that retained process after the
209209
sandbox becomes ready. A trailing command defines the canonical main process;
210-
without one, the default is `/bin/bash -l` with a PTY.
210+
without one, the default is `/bin/bash -l` with a PTY. Explicit commands remain
211+
foreground in non-interactive automation: stdout and stderr stream to the
212+
caller and the CLI returns the command's exact status. Exit 0 leaves
213+
`Completed`; nonzero leaves `Error/MainProcessFailed`.
214+
Starting either retained terminal result invalidates SSH sessions from the
215+
previous runtime generation.
211216

212217
| Flag | Description |
213218
|------|-------------|
214219
| `--name <NAME>` | Sandbox name (auto-generated if omitted) |
215220
| `--from <SOURCE>` | Community name, Dockerfile path, directory, or image reference (BYOC) |
216-
| `--no-keep` | Delete the sandbox after the initial command or shell exits |
221+
| `--no-keep` | Delete the sandbox after main output and the result drain |
217222
| `--detach` | Start the canonical main process without attaching |
218223
| `--editor vscode|cursor` | Launch a remote editor and keep the sandbox alive |
219224
| `--gpu [COUNT]` | Request the driver's default GPU selection or a specific count |
220225
| `--cpu <QUANTITY>` | CPU limit (for example: `500m`, `1`, `2.5`) |
221226
| `--memory <QUANTITY>` | Memory limit (for example: `512Mi`, `4Gi`, `8G`) |
227+
228+
`--detach` adds no attachment grace period: the sandbox reports the canonical
229+
process result immediately when it exits. Foreground creation declares one
230+
expected main-process SSH attachment; cleanup finalizes after that connection
231+
drains and closes naturally.
222232
| `--driver-config-json <JSON>` | Experimental driver-keyed configuration object |
223233
| `--provider <NAME>` | Provider to attach (repeatable) |
224234
| `--policy <PATH>` | Custom policy YAML; overrides the built-in default and `OPENSHELL_SANDBOX_POLICY` |
@@ -269,8 +279,9 @@ and waits for the `Stopped` phase.
269279

270280
### `openshell sandbox start [name]`
271281

272-
Start a stopped sandbox and wait for `Ready`. The name defaults to the
273-
last-used sandbox.
282+
Start a stopped, failed, or completed sandbox and wait for `Ready`. This
283+
launches a fresh canonical-main instance. The name defaults to the last-used
284+
sandbox.
274285

275286
### `openshell sandbox exec [OPTIONS] -- COMMAND...`
276287

architecture/compute-runtimes.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,14 @@ The gateway persists lifecycle intent before mutating compute:
137137
Ready -> Stopping -> Stopped -> Starting -> Ready
138138
```
139139

140+
A canonical main process that exits successfully follows `Ready -> Completed`.
141+
A nonzero or signal-normalized result follows `Ready -> Error` with a
142+
`MainProcessFailed` condition. Both retained results may be started explicitly,
143+
which creates a fresh main-process instance. Drivers must not automatically
144+
restart a completed or failed canonical process. Before an explicit restart,
145+
the gateway disconnects the prior supervisor session and deletes its SSH
146+
sessions so credentials cannot cross runtime generations.
147+
140148
`StopSandbox` and `StartSandbox` are idempotent driver operations. Stop
141149
retains the driver resource and its persistent workspace boundary while making
142150
exec, SSH, forwarding, and exposed services unavailable. Start reactivates the

architecture/gateway.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ workloads.
1616
- Coordinate supervisor relay sessions for connect, exec, file sync, and
1717
service forwarding.
1818
- Persist the canonical main-process instance ID and normalized exit code on
19-
sandbox status. Any main process exit transitions the sandbox to `Error`,
20-
including exit code zero.
19+
sandbox status. Exit code zero transitions the sandbox to `Completed`;
20+
nonzero results transition it to `Error/MainProcessFailed`. Infrastructure
21+
failures also use `Error`, with a distinct reason and no fabricated command
22+
result.
2123

2224
The gateway does not enforce agent network policy at request time. That happens
2325
inside each sandbox, where the supervisor and proxy can observe local process
@@ -26,7 +28,15 @@ identity.
2628
The live supervisor session is the readiness authority for its main-process
2729
instance. The supervisor reports its normalized result through the
2830
sandbox-authenticated `ReportMainProcessExit` RPC, and the gateway rejects
29-
results from stale instance IDs.
31+
results from stale instance IDs. Foreground creation carries a one-shot
32+
attachment intent to the process supervisor. The supervisor durably reports the
33+
result immediately, accepts that declared SSH attachment even when the process
34+
has already exited, sends the retained output and exit status, and waits for the
35+
peer's channel close before finalizing the result for ephemeral cleanup.
36+
Detached commands carry no attachment intent, so they finalize and exit
37+
immediately without a grace period. Finalization is persisted separately from
38+
the exit result; the gateway deletes an ephemeral sandbox only after the
39+
finalized supervisor session disconnects.
3040

3141
## Protocol and Auth
3242

architecture/sandbox.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,15 @@ engine with a gateway policy revision.
481481
re-evaluate.
482482
- If the supervisor relay drops, the sandbox can keep running, but connect and
483483
exec operations fail until the supervisor registers again.
484-
- If the canonical main process exits, including with code 0, the supervisor
485-
reports its normalized exit code before shutdown. The gateway persists the
486-
code on sandbox status, records `MainProcessExited`, and makes the sandbox
487-
terminal `Error`; runtime restart policies must not replace the process.
484+
- If the canonical main process exits, the supervisor durably reports the
485+
normalized result immediately. A foreground create declares a one-shot main
486+
attachment, so the supervisor accepts it even after a fast process exits,
487+
sends the retained output and SSH exit status, waits for the peer's channel
488+
close, and then finalizes ephemeral cleanup. With no declared or active
489+
attachment, it finalizes and exits without a grace period. The gateway waits
490+
for that finalized supervisor session to disconnect before deleting an
491+
ephemeral sandbox. Exit code 0 records
492+
`Completed/MainProcessCompleted`; nonzero and signal-normalized exits record
493+
`Error/MainProcessFailed`. Infrastructure failures also use `Error`, with a
494+
distinct condition reason and no fabricated canonical-process result. Runtime
495+
restart policies must not replace the canonical process.

crates/openshell-cli/src/commands/common.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ pub fn phase_name(phase: i32) -> &'static str {
6565
Ok(SandboxPhase::Stopping) => "Stopping",
6666
Ok(SandboxPhase::Stopped) => "Stopped",
6767
Ok(SandboxPhase::Starting) => "Starting",
68+
Ok(SandboxPhase::Completed) => "Completed",
6869
Ok(SandboxPhase::Unknown) | Err(_) => "Unknown",
6970
}
7071
}

crates/openshell-cli/src/main.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3071,7 +3071,7 @@ async fn run_async() -> Result<()> {
30713071
let endpoint = &ctx.endpoint;
30723072
let mut tls = tls.with_gateway_name(&ctx.name);
30733073
apply_auth(&mut tls, &ctx.name);
3074-
Box::pin(run::sandbox_create(
3074+
let exit_code = Box::pin(run::sandbox_create(
30753075
endpoint,
30763076
&ctx.name,
30773077
run::SandboxCreateConfig {
@@ -3100,6 +3100,9 @@ async fn run_async() -> Result<()> {
31003100
&tls,
31013101
))
31023102
.await?;
3103+
if exit_code != 0 {
3104+
std::process::exit(exit_code);
3105+
}
31033106
}
31043107
SandboxCommands::Upload {
31053108
name,
@@ -3225,7 +3228,12 @@ async fn run_async() -> Result<()> {
32253228
)
32263229
.await?;
32273230
} else {
3228-
run::sandbox_connect(endpoint, &name, &tls, &cli.workspace).await?;
3231+
let exit_code =
3232+
run::sandbox_connect(endpoint, &name, &tls, &cli.workspace)
3233+
.await?;
3234+
if exit_code != 0 {
3235+
std::process::exit(exit_code);
3236+
}
32293237
}
32303238
let _ = save_last_sandbox(&ctx.name, &cli.workspace, &name);
32313239
}

0 commit comments

Comments
 (0)