Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 67 additions & 33 deletions docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -603,12 +603,12 @@ omits it.

## 7. Wire contract

The wire contract is a typed, JSON-serialised envelope shared by the TypeScript SDK,
`mxc_ffi`, and the executor CLI. The SDK passes the envelope to `mxc_ffi`; direct CLI
callers can provide the same envelope through `--config-base64`. Rust parses both paths
into the same request types (§9.1). The only open content is at the leaves of
`ErrorEnvelope.details`; every other field, including the error envelope's named
structured fields, is statically typed.
The raw exact wire contract is a typed, JSON-serialised envelope shared by the
TypeScript SDK and `mxc_ffi`. Direct executor calls carry lifecycle routing in CLI
arguments while retaining the same phase-specific exact contracts internally. Rust
normalizes both paths into the same request types (§9.1). The only open content is at
Comment thread
MGudgin marked this conversation as resolved.
the leaves of `ErrorEnvelope.details`; every other field, including the error
envelope's named structured fields, is statically typed.

### 7.1 Request envelope

Expand Down Expand Up @@ -688,6 +688,25 @@ State-aware-only fields:
| `phase` | `Phase` member | Yes | Discriminator. Absence means a one-shot request. |
| `process` | `ProcessConfig` | Required for `exec`; absent otherwise. | Cross-backend execution fields. |

#### `wxc-exec` lifecycle transport

Raw exact SDK and FFI calls retain `phase` and `sandboxId` in JSON. Direct
`wxc-exec` lifecycle calls instead remove those routing fields from the supplied JSON
and pass them as command-line arguments:

```text
wxc-exec.exe policy.json --operation provision
wxc-exec.exe policy.json --operation start --sandbox-id iso:abc
wxc-exec.exe policy.json --operation exec --sandbox-id iso:abc -- command arg
wxc-exec.exe policy.json --operation stop --sandbox-id iso:abc
wxc-exec.exe policy.json --operation deprovision --sandbox-id iso:abc
```

Provision JSON still contains `containment`; later operations route from the
`--sandbox-id` prefix. Supplying `phase` or `sandboxId` in CLI JSON is rejected rather
than overriding the command-line routing. Without `--operation`, `wxc-exec` accepts
only one-shot JSON.

Cross-cutting fields available to state-aware (state-aware-only at top level — backends
declare which phases honor them, see §10.3):

Expand Down Expand Up @@ -757,23 +776,20 @@ state-aware mode so `stdout` remains parseable without sentinels. (One-shot disp
keeps its existing `stdout` logger behaviour — the stricter routing applies to
state-aware only.)

Configuration parse-phase failures that occur **after** the request is
discriminated as state-aware (i.e. the `phase` field was recognized) follow the
state-aware contract: the typed `{error}` envelope is the only primary output,
while the human-readable actionable parse diagnostic is written only to
configured auxiliary sinks (`--log-file` and the Windows diagnostic console). It
is not duplicated to the logger's primary console/buffer output, so such a parse
failure does not add stderr noise even with `--debug`. Dispatch-time failures,
including typed per-backend configuration errors, use the same auxiliary-only
diagnostic routing before the executor emits their typed `{error}` envelope.

CLI failures that occur **before** discrimination is possible — malformed base64,
non-UTF-8 bytes, or JSON so malformed that the `phase` field cannot be read —
cannot be attributed to the state-aware path. The diagnostic is written to the
primary output (stderr) and **no**
`{error}` envelope is emitted. Callers that require an envelope even for
unparseable input should validate that the payload is well-formed JSON before
invoking `wxc-exec`.
When `--operation` is present, the executor selects the lifecycle contract before
reading or decoding the configuration source. Every failure from that point onward —
an unreadable file, malformed base64, non-UTF-8 bytes, malformed JSON, an invalid
version, exact-contract rejection, or dispatch failure — emits a typed `{error}`
envelope as the only primary output. The human-readable diagnostic is written only to
configured auxiliary sinks (`--log-file` and the Windows diagnostic console), and the
rejection is recorded through the same `ConfigRejected` audit path. It is not
duplicated to stdout or stderr even with `--debug`.

Without `--operation`, `wxc-exec` accepts only one-shot requests. Input-source and
pre-parse failures on that path retain the legacy primary diagnostic on stderr and do
not emit a lifecycle envelope. Raw SDK and FFI lifecycle calls do not use this CLI
stream protocol; they return their status and error data through their binding result
surfaces.

For exec specifically, MXC diagnostic output mixes with the script's own stderr when
`--debug` is passed. This is a small amount of pre- and post-dispatch noise; consumers
Expand Down Expand Up @@ -856,9 +872,11 @@ codes.

### 7.4 Worked example: IsolationSession end-to-end

A complete state-aware lifecycle, threading TS call → JSON the SDK serialises and passes
to the executor via `--config-base64` → Rust trait method that dispatches → response
shape, across all five phases.
A complete state-aware lifecycle, threading TS call → exact JSON the SDK passes through
`mxc_ffi` → Rust trait method that dispatches → response shape, across all five phases.
Each phase also identifies the equivalent direct `wxc-exec` routing; that transport
removes `phase` and `sandboxId` from the shown exact JSON and supplies them as CLI
arguments.

#### Phase 1 — provision

Expand All @@ -879,15 +897,17 @@ const { sandboxId } = await provisionSandbox(
```json
{
"version": "0.9.0-alpha",
"containment": "isolation_session",
"phase": "provision",
"containment": "isolation_session",
Comment thread
MGudgin marked this conversation as resolved.
"network": {
"egress": { "default": "allow" },
"ingress": { "default": "allow", "hostLoopback": "allow" }
}
}
```

Direct executor routing: remove `phase` and pass `--operation provision`.

```rust
// Exact adaptation carries the all-allow network policy on the request. After
// checked binding and backend validation, the dispatcher calls:
Expand Down Expand Up @@ -923,6 +943,9 @@ await startSandbox(
}
```

Direct executor routing: remove `phase` and `sandboxId`, then pass
`--operation start --sandbox-id <id>`.

```rust
// `start` carries no per-phase config for this backend — its StartConfig is
// `()`, so the envelope above has no backend-specific section and the
Expand Down Expand Up @@ -958,6 +981,9 @@ const r = await execInSandboxAsync(
}
```

Direct executor routing: remove `phase` and `sandboxId`, then pass
`--operation exec --sandbox-id <id>`.

```rust
// Parser populates request.script_code = "echo hello", request.script_timeout =
// 5000 from the wire-format `process` block (same path as one-shot). The
Expand Down Expand Up @@ -988,6 +1014,9 @@ await stopSandbox(sandboxId, {});
}
```

Direct executor routing: remove `phase` and `sandboxId`, then pass
`--operation stop --sandbox-id <id>`.

```rust
backend.stop("iso:eyJ2ZXJzaW9uIjoxLCJhZ2VudFVzZXJOYW1lIjoiX2lzb19hYmNfMTIzIn0", &request, /* config */ None)
// returns Ok(StopResult { metadata: None })
Expand All @@ -1011,6 +1040,9 @@ await deprovisionSandbox(sandboxId, {});
}
```

Direct executor routing: remove `phase` and `sandboxId`, then pass
`--operation deprovision --sandbox-id <id>`.

```rust
backend.deprovision("iso:eyJ2ZXJzaW9uIjoxLCJhZ2VudFVzZXJOYW1lIjoiX2lzb19hYmNfMTIzIn0", &request, /* config */ None)
// returns Ok(DeprovisionResult { metadata: None })
Expand All @@ -1028,12 +1060,14 @@ section when serialising state-aware calls — consumers write `appId` directly
fields (`filesystem` / `network` / `runtimeConfig` / `ui`) on a per-(backend, phase) Config map directly
to top-level wire fields — they are already wire-format-aligned in the Config, so the
SDK passes them through unchanged. Cross-backend exec fields (`commandLine`, `cwd`,
`env`, `timeout`) flow through the top-level `process` block. The typed SDK requires `commandLine`. The executor CLI can complete an `exec`
template from arguments after `--`; it sets `process.commandLine` before parsing.
Trailing commands are rejected for every non-exec phase. The Node SDK receives
owned response data and native process streams through `mxc_ffi`. Responses unwrap
any `result` envelope at the SDK boundary so the caller sees a plain `ProvisionResult` /
`StartResult` / `ExecResult` / `StopResult` / `DeprovisionResult`.
`env`, `timeout`) flow through the top-level `process` block. The typed SDK requires
`commandLine`. The executor CLI supplies operation and existing sandbox identity through
`--operation` and `--sandbox-id`; it can complete an `exec` template from arguments
after `--` by setting `process.commandLine` before parsing. Trailing commands are
rejected for every non-exec operation. The Node SDK receives owned response data and
native process streams through `mxc_ffi`. Responses unwrap any `result` envelope at the
SDK boundary so the caller sees a plain `ProvisionResult` / `StartResult` /
`ExecResult` / `StopResult` / `DeprovisionResult`.

## 8. Error model

Expand Down
Loading
Loading