@coryrylan/cradle 0.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,35 @@
1
+ ## [1.2.0](https://github.com/coryrylan/cradle/compare/cradle-v1.1.0...cradle-v1.2.0) (2026-07-26)
2
+
3
+ ### Features
4
+
5
+ * **cli:** enhance sandbox configuration with docker support ([542cd79](https://github.com/coryrylan/cradle/commit/542cd79445cfc275ca8fa49f9b8e046a4fb23c9e))
6
+
7
+ ### Bug Fixes
8
+
9
+ * **cli:** rename start command to run and update related documentation ([7324f82](https://github.com/coryrylan/cradle/commit/7324f823ad821b25e7d8e3db07b6cb585acfdd4e))
10
+
11
+ ## [1.1.0](https://github.com/coryrylan/cradle/compare/cradle-v1.0.1...cradle-v1.1.0) (2026-07-20)
12
+
13
+ ### Features
14
+
15
+ * **cli:** enhance sandbox configuration for agent-browser ([363e4b1](https://github.com/coryrylan/cradle/commit/363e4b1b8b7896001ac459c9c508f8a1176868e2))
16
+
17
+ ## [1.0.1](https://github.com/coryrylan/cradle/compare/cradle-v1.0.0...cradle-v1.0.1) (2026-07-19)
18
+
19
+ ### Bug Fixes
20
+
21
+ * **cli:** update agent folder specifications to include system.md ([52f8aa9](https://github.com/coryrylan/cradle/commit/52f8aa9a1aad73d34baf58f647b4ecbedf0dfd5a))
22
+
23
+ ## 1.0.0 (2026-07-19)
24
+
25
+ ### Features
26
+
27
+ * init ([74684ae](https://github.com/coryrylan/cradle/commit/74684ae89643d062d906e1c9d311501ab18b6db6))
28
+
29
+ ### Bug Fixes
30
+
31
+ * **cli:** mise cache paths ([a13f78b](https://github.com/coryrylan/cradle/commit/a13f78b9f65f427ee982bbcb78fa9f1335e285af))
32
+
1
33
  # Changelog
2
34
 
3
35
  ## 0.0.0
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ![CI Build](https://github.com/coryrylan/cradle/actions/workflows/pull-request.yml/badge.svg)
4
4
 
5
- A runtime for portable agents defined as folders. `cradle start <dir>` reads an agent folder — an `APPEND_SYSTEM.md` plus optional pi-native config, skills, extensions, and sandbox posture — and launches the [pi](https://github.com/earendil-works/pi-mono) coding agent configured from it. An agent declaring `sandbox/nono.json` runs inside the [nono](https://github.com/always-further/nono) filesystem sandbox. See [ARCHITECTURE.md](../../ARCHITECTURE.md) for the folder format and [`examples/hello`](../../examples/hello) for a minimal agent. Built with Bun and TypeScript; install the standalone binary via `install.sh` (primary) or the npm package [`@coryrylan/cradle`](https://www.npmjs.com/package/@coryrylan/cradle) (alternative).
5
+ A runtime for portable agents defined as folders. `cradle run <dir>` reads an agent folder — a `SYSTEM.md` or `APPEND_SYSTEM.md` (at least one) plus optional pi-native config, skills, extensions, and sandbox posture — and launches the [pi](https://github.com/earendil-works/pi-mono) coding agent configured from it. An agent declaring `sandbox/nono.json` or `sandbox/sbx.json` runs sandboxed — inside the [nono](https://github.com/always-further/nono) filesystem sandbox or the [Docker Sandboxes](https://docs.docker.com/ai/sandboxes/) microVM, respectively. See [ARCHITECTURE.md](../../ARCHITECTURE.md) for the folder format and [`examples/hello`](../../examples/hello) for a minimal agent. Built with Bun and TypeScript; install the standalone binary via `install.sh` (primary) or the npm package [`@coryrylan/cradle`](https://www.npmjs.com/package/@coryrylan/cradle) (alternative).
6
6
 
7
7
  ## Dependencies
8
8
 
@@ -10,8 +10,9 @@ A runtime for portable agents defined as folders. `cradle start <dir>` reads an
10
10
 
11
11
  | Tool | Status | Why |
12
12
  | ------------------------------------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
- | [`pi`](https://github.com/earendil-works/pi-mono) | **Required** | The coding agent cradle launches. Every `cradle start` spawns it. |
14
- | [`nono`](https://github.com/always-further/nono) | **Recommended** | The filesystem sandbox pi runs inside for a sandboxed run — a folder declaring `sandbox/nono.json`, or `--sandbox`/`--offline`/`--allow-host`; required for that run, not needed otherwise. |
13
+ | [`pi`](https://github.com/earendil-works/pi-mono) | **Required** | The coding agent cradle launches. Every `cradle run` spawns it. |
14
+ | [`nono`](https://github.com/always-further/nono) | **Recommended** | The filesystem sandbox pi runs inside for a nono-backend run — a folder declaring `sandbox/nono.json`, or `--sandbox`/`--sandbox-backend nono`/`--offline`/`--allow-host` with no `sandbox/sbx.json`; required for that run, not needed otherwise. |
15
+ | [`sbx`](https://docs.docker.com/ai/sandboxes/) | **Recommended** | The Docker Sandboxes microVM pi runs inside for an sbx-backend run — a folder declaring `sandbox/sbx.json`, or `--sandbox-backend sbx`; required for that run, not needed otherwise. |
15
16
  | [`mise`](https://mise.jdx.dev) | **Recommended** | The supported way to install and manage `pi` and `nono`. cradle doesn't invoke mise directly, but it falls back to mise's shims when resolving the tools, and the generated sandbox profile grants mise's trees so a sandboxed pi finds its runtime. |
16
17
 
17
18
  ## Installation
@@ -53,32 +54,32 @@ bun start
53
54
  | `bun run test` | Run tests (no coverage) |
54
55
  | `bun run test:coverage` | Run tests with coverage; enforces thresholds from `bunfig.toml` |
55
56
  | `bun run lint` | Lint this package with ESLint (typescript-eslint strict) |
56
- | `bun run format` | Check formatting with Prettier |
57
- | `bun run format:fix` | Auto-fix formatting |
58
57
  | `bun run ci` | Run lint + build + test:coverage (used in CI) |
58
+ | `bun run ci:nocache` | Clean `dist/` then run `ci` (useful for cache debugging) |
59
59
  | `bun run install:local` | Build and install binary to `~/.local/bin` |
60
60
  | `bun run uninstall:local` | Remove locally installed binary |
61
61
 
62
- > Knip (unused files / deps / exports) is configured at the monorepo root and runs across all workspaces. From the repo root, run `bun run lint:knip`.
62
+ > Formatting (Prettier) and Knip (unused files / deps / exports) run at the monorepo root, not per-package. From the repo root: `bun run format` / `bun run format:fix` and `bun run lint:knip`.
63
63
 
64
64
  ## CLI Usage
65
65
 
66
66
  ```bash
67
67
  cradle --version
68
- cradle doctor # check pi (required), nono/mise (recommended) on PATH, with versions
69
- cradle start ./my-agent # run an agent folder with pi; sandboxed when sandbox/nono.json exists
70
- cradle start my-agent # run a name from ~/.cradle/settings.json instead of a path
71
- cradle start . --offline # block all outbound network (exfil protection)
72
- cradle start . --allow-host api.z.ai # restrict network to these hosts (repeatable)
73
- cradle start . --no-sandbox # run pi directly (debug)
74
- cradle start . --dry-run -- --resume # print the write plan + command; forward `--resume` to pi
68
+ cradle doctor # check pi (required), nono/sbx/mise (recommended) on PATH, with versions
69
+ cradle run ./my-agent # run an agent folder with pi; sandboxed when sandbox/nono.json or sandbox/sbx.json exists
70
+ cradle run my-agent # run a name from ~/.cradle/settings.json instead of a path
71
+ cradle run . --offline # block all outbound network (exfil protection)
72
+ cradle run . --allow-host api.example.com # restrict network to these hosts (repeatable)
73
+ cradle run . --sandbox-backend sbx # run under the sbx Docker Sandboxes microVM instead of nono
74
+ cradle run . --no-sandbox # run pi directly (debug)
75
+ cradle run . --dry-run -- --resume # print the write plan + command; forward `--resume` to pi
75
76
  ```
76
77
 
77
78
  The agent runs in _your_ working directory; the agent folder is a parameter (default `.`). Everything after `--` is forwarded verbatim to pi. `--dry-run` prints the generated-extension write plan and the composed command without spawning (and without requiring the bins to be installed). Per-agent state (generated extensions + session history) lives under `~/.cradle/agents/<name>-<hash>/`.
78
79
 
79
80
  ### Global agent aliases (`~/.cradle/settings.json`)
80
81
 
81
- The `dir` positional accepts a bare name instead of a path — `cradle start my-agent` resolves against a global name → folder map, so agent folders you keep far from any project don't need a full path from every cwd:
82
+ The `dir` positional accepts a bare name instead of a path — `cradle run my-agent` resolves against a global name → folder map, so agent folders you keep far from any project don't need a full path from every cwd:
82
83
 
83
84
  ```json
84
85
  {
@@ -90,7 +91,9 @@ The `dir` positional accepts a bare name instead of a path — `cradle start my-
90
91
 
91
92
  A bare name (no `/`, not `.`/`~`-led) checks the alias table first, falling back to the cwd-relative path (`./my-agent`) when no alias is defined — anything already path-shaped (`./x`, `../x`, `/abs/x`, `~/x`, `.`) is never looked up as an alias. See [ARCHITECTURE.md](../../ARCHITECTURE.md#global-agent-aliases) for the full resolution rules.
92
93
 
93
- Each sandboxed run generates its own nono profile at `~/.cradle/agents/<id>/nono-profile.json` the built-in base merged with that agent's `sandbox/nono.json` grantsand points `nono run --profile` at it. There's no shared global profile and no separate setup step: an agent's whole sandbox posture lives in its own directory. To widen it (e.g. granting a tool's data dir), add a `sandbox/` folder to the agent with a `nono.json`:
94
+ An agent folder can declare a sandbox posture for either backend: `sandbox/nono.json` (the OS-policy sandbox covered in this section) or `sandbox/sbx.json` (the Docker Sandboxes microVM see [Docker Sandboxes backend](#docker-sandboxes-backend-sandboxsbxjson) below), or both. `--sandbox-backend <nono|sbx>` picks the backend explicitly and forces it on; bare `--sandbox` uses whichever backend the folder declares, defaulting to nono, and nono wins when a folder declares both (with a warning naming `--sandbox-backend sbx` as the override).
95
+
96
+ Each sandboxed nono run generates its own profile at `~/.cradle/agents/<id>/nono-profile.json` — the built-in base merged with that agent's `sandbox/nono.json` grants — and points `nono run --profile` at it. There's no shared global profile and no separate setup step: an agent's whole sandbox posture lives in its own directory. To widen it (e.g. granting a tool's data dir), add a `sandbox/` folder to the agent with a `nono.json`:
94
97
 
95
98
  ```json
96
99
  {
@@ -108,20 +111,20 @@ Outbound network is **open by default within a sandboxed run**. A `network` bloc
108
111
  {
109
112
  "network": {
110
113
  "block": false,
111
- "allow_domain": ["api.z.ai", "localhost"],
114
+ "allow_domain": ["api.example.com", "localhost"],
112
115
  "open_port": [11434],
113
116
  "listen_port": [8080]
114
117
  }
115
118
  }
116
119
  ```
117
120
 
118
- | Key | Effect |
119
- | ----------------- | ----------------------------------------------------------------------------------------------- |
120
- | `block` | `true` denies **all** outbound (full offline). |
121
- | `allow_domain` | Host allowlist. **Presence flips nono to default-deny** — unlisted hosts are refused (403). |
122
- | `open_port` | localhost TCP ports the agent may connect/bind (e.g. a local model or dev server). |
123
- | `listen_port` | TCP ports the agent may listen on. |
124
- | `network_profile` | Named nono network-policy profile (opaque pass-through; requires a host `network-policy.json`). |
121
+ | Key | Effect |
122
+ | ----------------- | ------------------------------------------------------------------------------------------------ |
123
+ | `block` | `true` denies **all** outbound (full offline). |
124
+ | `allow_domain` | Host allowlist. **Presence flips nono to default-deny** — unlisted hosts are refused (403). |
125
+ | `open_port` | localhost TCP ports the agent may connect/bind; `0` allows any outbound localhost port on macOS. |
126
+ | `listen_port` | TCP ports the agent may listen on. |
127
+ | `network_profile` | Named nono network-policy profile (opaque pass-through; requires a host `network-policy.json`). |
125
128
 
126
129
  An `allow_domain` allowlist blocks localhost too, so a local-model agent must list `localhost`/`127.0.0.1` **and** open its port (see [`examples/hello`](../../examples/hello), locked to Ollama on `localhost:11434`). CLI flags override the folder: `--offline` (full block) and repeatable `--allow-host <host>` (allowlist). Precedence: `--offline` > `--allow-host` > `sandbox/nono.json` > open. Requesting a network policy only means something inside the sandbox, so `--offline`/`--allow-host` force the sandbox on (same as `--sandbox`) unless `--no-sandbox` is passed explicitly, in which case cradle warns `network policy has no effect without the sandbox — pi runs with no network isolation (--sandbox to enforce it)` and nothing is enforced. cradle doesn't echo the resolved posture itself — run with `--verbose` to drop nono's `--silent` flag and see its own capabilities banner (grants + network mode), or read the generated per-agent profile (`nono-profile.json` in the agent's state dir). nono **fails closed** — a malformed `network` key or a platform that can't enforce proxy filtering makes the run refuse to start rather than silently ship an unenforced allowlist.
127
130
 
@@ -129,35 +132,58 @@ An `allow_domain` allowlist blocks localhost too, so a local-model agent must li
129
132
 
130
133
  Some tools need OS capabilities the conservative base profile denies. `sandbox/nono.json` can append raw macOS Seatbelt rules — s-expressions merged verbatim after the base's rules (nono validates the syntax at load). Each one widens the OS sandbox, so audit them where they live: the folder's `sandbox/nono.json` or the generated per-agent profile (`nono-profile.json` in the agent's state dir) — nono's own capabilities banner (shown with `--verbose`) never lists seatbelt rules. Ignored on Linux.
131
134
 
132
- **Browser automation is the motivating case.** [agent-browser](https://agent-browser.dev/) + Chrome for Testing runs sandboxed under nono with exactly two rules plus Chrome's own `--no-sandbox` flag:
135
+ **Browser automation is the motivating case.** [agent-browser](https://agent-browser.dev/) + Chrome for Testing runs sandboxed under nono with a directory grant, a direct-child Unix socket grant, exactly two macOS rules, and Chrome's own `--no-sandbox` flag:
133
136
 
134
137
  ```json
135
138
  {
136
139
  "filesystem": {
137
- "allow": ["~/.agent-browser"]
140
+ "allow": ["~/.agent-browser"],
141
+ "unix_socket_dir_bind": ["~/.agent-browser"]
142
+ },
143
+ "network": {
144
+ "open_port": [0]
138
145
  },
139
146
  "unsafe_macos_seatbelt_rules": ["(allow mach-register)", "(allow iokit-open)"]
140
147
  }
141
148
  ```
142
149
 
143
- | Entry | Why it's needed |
144
- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
145
- | `allow ~/.agent-browser` | agent-browser's own state dir — daemon socket, downloaded Chrome, config. Without it the daemon can't create its socket (`Operation not permitted`). |
146
- | `(allow mach-register)` | Chrome's Crashpad handler registers a Mach service (`bootstrap_check_in org.chromium.crashpad.*`); the base profile denies it, so the browser process aborts. |
147
- | `(allow iokit-open)` | Chrome opens IOKit user clients during startup even headless; without it the browser process crashes before serving CDP. |
150
+ | Entry | Why it's needed |
151
+ | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
152
+ | `allow ~/.agent-browser` | agent-browser's state dir — downloaded Chrome, config, and daemon socket files. |
153
+ | `unix_socket_dir_bind ~/.agent-browser` | Lets the CLI and daemon connect to and bind direct-child Unix sockets; without it, `connect()` returns `EPERM`. |
154
+ | `open_port 0` | Lets the daemon connect to Chrome's random localhost DevTools port on macOS; without it, the CDP WebSocket returns `EPERM`. |
155
+ | `(allow mach-register)` | Chrome's Crashpad handler registers a Mach service (`bootstrap_check_in org.chromium.crashpad.*`); the base profile denies it, so the browser process aborts. |
156
+ | `(allow iokit-open)` | Chrome opens IOKit user clients during startup even headless; without it the browser process crashes before serving CDP. |
157
+
158
+ `unix_socket_dir_bind` is non-recursive. Point it only at a dedicated socket directory, never a broad parent such as `~` or `/tmp`. Port `0` is nono's macOS-only outbound localhost wildcard; Linux requires explicit ports. The two macOS rules are IPC/IOKit capabilities only — the filesystem and network boundaries stay intact, so the sandbox still denies an ungranted path (verified: a granted read succeeds, `~/some-secret` returns `Operation not permitted`). No grant for Chrome's own `~/Library/…/Chrome for Testing` dir is needed — its Crashpad-database `stat` failure under the sandbox is non-fatal noise.
148
159
 
149
- The two rules are IPC/IOKit capabilities only the filesystem and network boundaries stay intact, so the sandbox still denies an ungranted path (verified: a granted read succeeds, `~/some-secret` returns `Operation not permitted`). No grant for Chrome's own `~/Library/…/Chrome for Testing` dir is needed its Crashpad-database `stat` failure under the sandbox is non-fatal noise.
160
+ Chrome's **own** nested sandbox can't initialize inside nono's seatbelt (macOS forbids nesting), so its child processes need `--no-sandbox`. On every sandboxed run, cradle generates and loads `agent-browser-nono-fallback.ts` before package and agent extensions. On macOS it appends `--no-sandbox` to `AGENT_BROWSER_ARGS`; on every platform it maps nono's dynamically injected `HTTPS_PROXY`/`HTTP_PROXY` to `AGENT_BROWSER_PROXY`. Explicit agent-browser proxy configuration still wins. Unsandboxed runs do not load the fallback, so Chrome keeps its own sandbox. See [`examples/browser`](../../examples/browser) for the complete folder.
150
161
 
151
- Chrome's **own** nested sandbox can't initialize inside nono's seatbelt (macOS forbids nesting), so its child processes need `--no-sandbox`. Deliver it through a pi extension rather than a shell env var an extension travels with the agent folder and pi propagates it into every tool's env regardless of how the extension itself set it. The simplest is a one-line agent extension:
162
+ An agent that still cannot run sandboxed can declare `{ "sandbox": false }` in either sandbox file to opt that backend out. cradle then runs pi bare, warns loudly on every run, and an explicit `--sandbox`/`--sandbox-backend` flag always forces isolation back on.
152
163
 
153
- ```ts
154
- // extensions/browser-sandbox.ts — set the flag before agent-browser launches Chrome
155
- process.env.AGENT_BROWSER_ARGS ??= '--no-sandbox';
164
+ ### Docker Sandboxes backend (`sandbox/sbx.json`)
165
+
166
+ A folder can opt into the [Docker Sandboxes](https://docs.docker.com/ai/sandboxes/) (`sbx`) microVM instead of (or alongside) nono by declaring `sandbox/sbx.json` — a sibling file with a restricted subset of `sandbox/nono.json`'s schema: `sandbox: false` to opt out, `filesystem` `read`/`write`/`allow` only, `network` `block`/`allow_domain` only. Keys with no VM-boundary equivalent — `unix_socket_dir_bind`, `network_profile`, `open_port`, `listen_port`, `unsafe_macos_seatbelt_rules` — are warned and dropped rather than silently accepted. Force it explicitly with `--sandbox-backend sbx`; see [ARCHITECTURE.md](../../ARCHITECTURE.md#sandboxsbxjson) for the full schema, backend precedence, and run model.
167
+
168
+ ```json
169
+ {
170
+ "network": { "allow_domain": ["api.example.com"] },
171
+ "filesystem": {
172
+ "allow": ["~/scratch"]
173
+ }
174
+ }
156
175
  ```
157
176
 
158
- (Or pass `agent-browser --args --no-sandbox` on each call.) See [`examples/browser`](../../examples/browser) for the complete folder.
177
+ An sbx run mounts your cwd (rw), the agent folder (ro), the state dir (rw), and `~/.pi/agent` (rw, so a host `pi login` and its OAuth refreshes carry over) into a sandbox named `cradle-<agentId>-<hash8>` — the hash keys the mount set, so a changed grant gets a fresh sandbox rather than attaching to stale mounts (clean those up with `sbx rm`). cradle then provisions `@earendil-works/pi-coding-agent` in-guest, pinned to the host's own `pi` version, and runs `sbx exec` with your host paths preserved verbatim, so the composed pi command is identical to a nono or unsandboxed run.
178
+
179
+ Network policy differs from nono's: `block: true` denies all outbound; `allow_domain` allows the listed hosts (expanded to cover subdomains, with `localhost`/`127.0.0.1` rewritten to `host.docker.internal` — guest loopback is the microVM itself, not the host). Unlike nono's proxy, a per-sandbox `allow` rule only **adds** to your machine's global `sbx` policy and cannot subtract from it — cradle warns on every allowlist run. For strict, nono-style allowlist semantics, run `sbx policy init deny-all` once. One-time machine setup before the first sandboxed `sbx` run:
180
+
181
+ ```bash
182
+ sbx login
183
+ sbx policy init deny-all # or allow-all / balanced — see sbx's own docs
184
+ ```
159
185
 
160
- An agent that still cannot run sandboxed can declare `{ "sandbox": false }` instead. cradle then runs pi bare, warns loudly on every run, and an explicit `--sandbox` flag always forces isolation back on.
186
+ `cradle doctor` checks `sbx` alongside `pi`/`nono`/`mise`.
161
187
 
162
188
  ## Build Targets
163
189
 
@@ -3,11 +3,11 @@ export interface ResolveRefDeps {
3
3
  readonly cwd: string;
4
4
  }
5
5
  /**
6
- * Resolve a `cradle start` ref into a folder path `loadAgentFolder` can load.
6
+ * Resolve a `cradle run` ref into a folder path `loadAgentFolder` can load.
7
7
  * Path-shaped refs pass through untouched (`loadAgentFolder`'s own `resolve()`
8
8
  * handles them, and they never consult `deps.cwd`); a bare name resolves
9
9
  * against the global alias table into an ABSOLUTE, normalized path, falling
10
- * back to the cwd-relative folder when no alias matches (so `cradle start
10
+ * back to the cwd-relative folder when no alias matches (so `cradle run
11
11
  * hello` from `examples/` keeps working) — and throwing only when neither
12
12
  * resolves. Every bare-name branch resolves against the injected `deps.cwd`,
13
13
  * never `process.cwd()`, so the dep is the single source of truth for what the
@@ -0,0 +1,3 @@
1
+ export declare const AGENT_BROWSER_NONO_FALLBACK_EXTENSION_FILE = "agent-browser-nono-fallback.ts";
2
+ /** Emit the sandbox-only environment bridge agent-browser needs when launched as a pi subprocess. */
3
+ export declare function emitAgentBrowserNonoFallbackExtension(): string;
@@ -1 +1,12 @@
1
- export declare function emitProvidersExtension(providersJson: string): string;
1
+ /** Options for `emitProvidersExtension` see `commands/run.ts`'s `emitExtensionFiles`, the single call site. */
2
+ export interface ProvidersEmitOptions {
3
+ /**
4
+ * Rewrite `localhost`/`127.0.0.1` provider baseUrls to the sbx host gateway
5
+ * (see `sbx/compose.ts`'s `rewriteLocalhostBaseUrl`): guest loopback is the
6
+ * microVM itself, so a host-bound local-model endpoint is unreachable under
7
+ * its own name inside the sbx backend. Off (byte-identical output) for nono
8
+ * and unsandboxed runs.
9
+ */
10
+ readonly rewriteLocalhostBaseUrls?: boolean;
11
+ }
12
+ export declare function emitProvidersExtension(providersJson: string, options?: ProvidersEmitOptions): string;
@@ -16,6 +16,8 @@ export interface AgentSandboxGrants {
16
16
  readonly read: readonly string[];
17
17
  readonly write: readonly string[];
18
18
  readonly allow: readonly string[];
19
+ /** Direct-child Unix socket paths the process may connect to and bind. */
20
+ readonly unixSocketDirBind: readonly string[];
19
21
  }
20
22
  /**
21
23
  * The agent's declared network posture — a curated subset of nono's profile
@@ -40,11 +42,20 @@ export interface AgentNetwork {
40
42
  readonly listenPort?: readonly number[];
41
43
  }
42
44
  export type AgentSandboxPosture = 'unconfigured' | 'enabled' | 'disabled';
45
+ /**
46
+ * The sandbox enforcement backend a folder can declare a posture for: `nono`
47
+ * (Seatbelt/bubblewrap, `sandbox/nono.json`) or `sbx` (Docker Sandboxes
48
+ * microVM, `sandbox/sbx.json`). Backend resolution — which one actually runs
49
+ * — is a `commands/run.ts` concern; this module only reads each file's
50
+ * declared posture.
51
+ */
52
+ export type SandboxBackend = 'nono' | 'sbx';
43
53
  export interface AgentSandbox {
44
54
  /**
45
- * The folder's declared sandbox posture. `'unconfigured'` when the folder has
46
- * no `sandbox/nono.json`; otherwise mirrors the file's `sandbox` key
47
- * (absent `'enabled'`).
55
+ * The declaring file's sandbox posture. `'unconfigured'` when the folder
56
+ * has no `sandbox/nono.json` (for the `sandbox` field) or no
57
+ * `sandbox/sbx.json` (for the `sbx` field); otherwise mirrors the file's
58
+ * `sandbox` key (absent ⇒ `'enabled'`).
48
59
  */
49
60
  readonly posture: AgentSandboxPosture;
50
61
  readonly network?: AgentNetwork;
@@ -62,15 +73,30 @@ export interface AgentSandbox {
62
73
  }
63
74
  export interface AgentFolder {
64
75
  readonly dir: string;
65
- /** Abs path of APPEND_SYSTEM.md — appended to pi's system prompt via `--append-system-prompt`. */
66
- readonly appendSystemFilePath: string;
76
+ /**
77
+ * Abs path of SYSTEM.md — REPLACES pi's default system prompt via
78
+ * `--system-prompt`; `null` when the folder has no SYSTEM.md. `loadAgentFolder`
79
+ * guarantees at least one of `systemFilePath`/`appendSystemFilePath` is non-null.
80
+ */
81
+ readonly systemFilePath: string | null;
82
+ /** Abs path of APPEND_SYSTEM.md — appended to pi's system prompt via `--append-system-prompt`; `null` when absent. */
83
+ readonly appendSystemFilePath: string | null;
67
84
  readonly settings: AgentSettings;
68
85
  /** Serialized `providers` object from models.json, `null` when absent. */
69
86
  readonly providersJson: string | null;
70
87
  readonly skillsDir: string | null;
71
88
  /** pi-native extensions (top-level `extensions/*.ts` plus each subdir's `index.ts`), absolute paths. */
72
89
  readonly extensionFiles: readonly string[];
90
+ /** `sandbox/nono.json`'s declared posture — the Seatbelt/bubblewrap backend. */
73
91
  readonly sandbox: AgentSandbox;
92
+ /**
93
+ * `sandbox/sbx.json`'s declared posture — the Docker Sandboxes microVM
94
+ * backend. A restricted subset of nono's schema: no Unix-socket grants, no
95
+ * named network-profile pass-through, no Seatbelt escape hatch — the VM
96
+ * boundary has no equivalent for them, so those keys are warned-and-dropped
97
+ * rather than silently accepted. See `readSbxSandbox`.
98
+ */
99
+ readonly sbx: AgentSandbox;
74
100
  readonly warnings: readonly string[];
75
101
  }
76
102
  export declare function loadAgentFolder(dir: string): Promise<AgentFolder>;
@@ -1,13 +1,13 @@
1
- import type { AgentFolder } from './folder.js';
1
+ import type { AgentFolder, SandboxBackend } from './folder.js';
2
2
  export interface LaunchSpec {
3
3
  readonly folder: AgentFolder;
4
4
  readonly stateDir: string;
5
5
  /**
6
6
  * Where the generated providers extension is written (see
7
7
  * `folder.providersJson`) and where `--session-dir` points, sourced from the
8
- * plan's own `extensionsDir`/`sessionsDir` (`commands/start.ts` — the single
8
+ * plan's own `extensionsDir`/`sessionsDir` (`commands/run.ts` — the single
9
9
  * derivation site; re-deriving from `stateDir` here risks silently
10
- * disagreeing with a plan whose dirs were overridden after `planStart`).
10
+ * disagreeing with a plan whose dirs were overridden after `planRun`).
11
11
  */
12
12
  readonly extensionsDir: string;
13
13
  readonly sessionsDir: string;
@@ -16,21 +16,30 @@ export interface LaunchSpec {
16
16
  * from the plan's `statePaths` derivation like `extensionsDir`/`sessionsDir`.
17
17
  */
18
18
  readonly miseCacheDir: string;
19
- readonly sandbox: boolean;
19
+ /**
20
+ * Which sandbox backend wraps this run, or `null` for unsandboxed. `'nono'`
21
+ * wraps the argv here — see `composeArgv`. `'sbx'` does NOT wrap here: its
22
+ * `sbx exec` wrapper is composed at materialization in `commands/run.ts`,
23
+ * so `composeArgv` returns the bare pi argv for both `'sbx'` and `null`.
24
+ * For `'sbx'`, `piBin` is the literal `pi` — resolved on the guest's PATH,
25
+ * since host paths (mise-shim included) are meaningless inside the
26
+ * microVM.
27
+ */
28
+ readonly backend: SandboxBackend | null;
20
29
  readonly passthrough: readonly string[];
21
30
  /** Resolved path, or bare `nono` for dry-run previews. */
22
31
  readonly nonoBin: string;
23
32
  /**
24
33
  * Resolved path (mise-shim fallback included, see `util/which.ts`), or bare
25
34
  * `pi` for dry-run previews. Used consistently whether or not the run is
26
- * sandboxed — the generated profile already grants read on the mise install
27
- * tree (see `nono/cradle-pi.json`), so the absolute path resolves inside the
28
- * sandbox too; a bare `pi` only resolves on whatever PATH the spawning
29
- * process happens to have, which is exactly the gap a fresh mise-only
30
- * install falls into.
35
+ * nono-sandboxed — the generated profile already grants read on the mise
36
+ * install tree (see `nono/cradle-pi.json`), so the absolute path resolves
37
+ * inside the sandbox too; a bare `pi` only resolves on whatever PATH the
38
+ * spawning process happens to have, which is exactly the gap a fresh
39
+ * mise-only install falls into. See `backend` for the `'sbx'` exception.
31
40
  */
32
41
  readonly piBin: string;
33
- /** Abs path of the generated per-agent nono profile; used only when `sandbox` is true. */
42
+ /** Abs path of the generated per-agent nono profile; used only when `backend` is `'nono'`. */
34
43
  readonly profilePath: string;
35
44
  /**
36
45
  * When true, nono runs without `--silent` so its capabilities banner
@@ -41,7 +50,7 @@ export interface LaunchSpec {
41
50
  /**
42
51
  * Abs paths of the pi extension entry files resolved from settings.json's
43
52
  * `packages` (see `./packages.js`), installed into `<stateDir>/npm` by
44
- * `commands/start.ts`. Absent/empty on the argv-only preview used for
53
+ * `commands/run.ts`. Absent/empty on the argv-only preview used for
45
54
  * `--dry-run` (packages resolve at install time, after the preview is
46
55
  * printed) and on folders that declare no packages.
47
56
  */
@@ -50,15 +59,16 @@ export interface LaunchSpec {
50
59
  /**
51
60
  * Build the bare pi argv. `passthrough` lands last so user-passed flags win
52
61
  * under pi's last-wins parsing. The `-e` order is load-bearing: the generated
53
- * providers extension goes first, then the resolved package entries, then the
54
- * agent's own `extensions/` files which load with the agent's providers
55
- * registered and any package-provided tools already available, since the
56
- * agent's own extensions may depend on them.
62
+ * providers extension goes first, followed by the nono-only agent-browser
63
+ * host-socket fallback, the resolved package entries, then the agent's own
64
+ * `extensions/` files which load with the agent's providers registered,
65
+ * nono subprocess compatibility configured, and any package-provided tools
66
+ * already available, since the agent's own extensions may depend on them.
57
67
  */
58
68
  export declare function composePiArgv(spec: LaunchSpec): string[];
59
69
  /**
60
- * Compose the spawn env for a sandboxed run: `{ MISE_CACHE_DIR: spec.miseCacheDir }`;
61
- * `{}` when unsandboxed.
70
+ * Compose the spawn env for a nono-sandboxed run: `{ MISE_CACHE_DIR: spec.miseCacheDir }`;
71
+ * `{}` for `'sbx'` and unsandboxed runs alike.
62
72
  *
63
73
  * The generated profile denies the shared `~/Library/Caches/mise` on purpose
64
74
  * — a poisoned `bin_paths` cache would redirect which binaries the user's
@@ -68,9 +78,11 @@ export declare function composePiArgv(spec: LaunchSpec): string[];
68
78
  * at a private cache inside the already-granted state dir instead lets mise
69
79
  * cache writes succeed (no warnings, working cache) while the shared host
70
80
  * cache stays untouched; mise creates the directory itself, cradle never
71
- * pre-creates or wipes it. Unsandboxed runs return `{}` and keep the shared
81
+ * pre-creates or wipes it. Unsandboxed runs produce `{}` and keep the shared
72
82
  * host cache — no override needed since there's no sandbox denial to work
73
- * around.
83
+ * around; the sbx guest has no mise at all, and its HOME override rides the
84
+ * `sbx exec` argv composed in `commands/run.ts` rather than env, so the
85
+ * argv-only rule holds there too.
74
86
  *
75
87
  * This deliberately overrides any user-set `MISE_CACHE_DIR` for sandboxed
76
88
  * runs — the profile wouldn't grant a custom location either, so honoring one
@@ -84,8 +96,10 @@ export declare function composePiArgv(spec: LaunchSpec): string[];
84
96
  */
85
97
  export declare function composeEnv(spec: LaunchSpec): Record<string, string>;
86
98
  /**
87
- * Build the argv for `nono run … -- pi …`, or the bare pi argv when
88
- * sandboxing is disabled (`--no-sandbox`).
99
+ * Build the argv for `nono run … -- pi …` when `spec.backend` is `'nono'`, or
100
+ * the bare pi argv otherwise (`'sbx'` or `null`) — the `'sbx'` backend's own
101
+ * `sbx exec` wrapper is composed later, at materialization in
102
+ * `commands/run.ts`, not here (see `LaunchSpec.backend`).
89
103
  *
90
104
  * All filesystem grants (cwd, agent dir, state dir, and the agent's own
91
105
  * `sandbox/nono.json` entries) AND the network posture live inside the
@@ -9,14 +9,13 @@ export interface DoctorCheck {
9
9
  }
10
10
  interface DoctorDeps {
11
11
  readonly which?: WhichFn;
12
- /** Read a bin's `--version` output; injected so in-process tests never spawn. */
13
- readonly readVersion?: (binPath: string) => Promise<string | null>;
12
+ /** Read a bin's version output (default argv `--version`; sbx passes `['version']`); injected so in-process tests never spawn. */
13
+ readonly readVersion?: (binPath: string, args?: readonly string[]) => Promise<string | null>;
14
14
  }
15
15
  /**
16
- * Probe the environment cradle depends on: `pi` (always required), `nono`
17
- * (required for sandboxed runs — a folder declaring sandbox/nono.json, or
18
- * --sandbox/--offline/--allow-host; not needed otherwise), and `mise`
19
- * (recommended toolchain manager).
16
+ * Probe the environment cradle depends on: `pi` (always required), `nono` and
17
+ * `sbx` (each required only for runs on its backend see the notes), and
18
+ * `mise` (recommended toolchain manager).
20
19
  */
21
20
  export declare function runDoctor(deps?: DoctorDeps): Promise<DoctorCheck[]>;
22
21
  export declare function formatDoctorReport(checks: readonly DoctorCheck[]): string;
@@ -1,8 +1,10 @@
1
+ import { type SandboxBackend } from '../agent/folder.js';
1
2
  import { type LaunchSpec } from '../agent/launch.js';
2
3
  import { type NpmPackageSpec } from '../agent/packages.js';
4
+ import { type SbxSpec } from '../sbx/compose.js';
3
5
  import { type TreeFile } from '../setup/install.js';
4
6
  import { type WhichFn } from '../util/which.js';
5
- export interface StartFlags {
7
+ export interface RunFlags {
6
8
  readonly dir: string;
7
9
  /** `--offline` → full network block. Overrides the folder network posture and forces the sandbox on (unless --no-sandbox). */
8
10
  readonly offline?: boolean;
@@ -10,15 +12,19 @@ export interface StartFlags {
10
12
  readonly allowHost?: readonly string[];
11
13
  /** Explicit CLI choice: `--no-sandbox` → true, `--sandbox` → false. Absent = defer to the folder. */
12
14
  readonly noSandbox?: boolean;
15
+ /** `--sandbox-backend` → explicit backend choice; implies the sandbox is on (still beaten by `--no-sandbox`). */
16
+ readonly sandboxBackend?: SandboxBackend;
13
17
  readonly dryRun?: boolean;
14
18
  readonly passthrough?: readonly string[];
15
19
  /** `--verbose` → show nono's full sandbox capabilities banner instead of the one-line status. */
16
20
  readonly verbose?: boolean;
17
21
  }
18
- interface StartDeps {
22
+ interface RunDeps {
19
23
  readonly cwd?: string;
20
24
  readonly home?: string;
21
25
  readonly which?: WhichFn;
26
+ /** Whether stdout is a TTY (`cli.ts` passes `process.stdout.isTTY`) — rides the sbx exec argv as `-t`; default false. */
27
+ readonly tty?: boolean;
22
28
  }
23
29
  /**
24
30
  * The agent's settings.json `packages` resolved into a per-agent npm install
@@ -41,27 +47,46 @@ export interface PackagesPlan {
41
47
  */
42
48
  readonly installCommand: readonly string[];
43
49
  }
44
- export interface StartPlan {
50
+ /**
51
+ * The sbx backend's materialization plan: the setup argvs `materializeRun`
52
+ * runs before the final `sbx exec` (composed there, wrapping the pi argv).
53
+ * `spec.piVersion` is null at plan time — the provision argv is recomposed at
54
+ * materialize with the host pi version (see `MaterializeDeps.readPiVersion`)
55
+ * so the guest install pins to it; the plan-time argv is the unpinned preview
56
+ * dry-run prints, the same precedent as package `-e` entries, which also
57
+ * resolve at materialize.
58
+ */
59
+ export interface SbxRunPlan {
60
+ readonly spec: SbxSpec;
61
+ /** Resolved host pi path — the version-pin source for guest provisioning; never spawned in-guest. */
62
+ readonly hostPiBin: string;
63
+ readonly createArgv: readonly string[];
64
+ readonly policyArgvs: ReadonlyArray<readonly string[]>;
65
+ readonly provisionArgv: readonly string[];
66
+ }
67
+ export interface RunPlan {
45
68
  /** Generated extensions, relative to `extensionsDir`. */
46
69
  readonly files: readonly TreeFile[];
47
70
  /**
48
71
  * Authoritative — the single derivation site for where generated extensions
49
- * and sessions live. `materializeStart` re-asserts these onto `launch`
72
+ * and sessions live. `materializeRun` re-asserts these onto `launch`
50
73
  * before composing the final argv, so overriding either here (as tests do)
51
74
  * changes the composed argv accordingly instead of silently disagreeing
52
- * with whatever `launch` had baked in at `planStart` time.
75
+ * with whatever `launch` had baked in at `planRun` time.
53
76
  */
54
77
  readonly extensionsDir: string;
55
78
  readonly sessionsDir: string;
56
79
  readonly warnings: readonly string[];
57
- /** The generated per-agent nono profile to write before spawning; `null` on unsandboxed runs (no profile needed). */
80
+ /** The generated per-agent nono profile to write before spawning; `null` unless the resolved backend is `'nono'`. */
58
81
  readonly profile: {
59
82
  readonly path: string;
60
83
  readonly content: string;
61
84
  } | null;
85
+ /** The sbx setup plan (see `SbxRunPlan`); `null` unless the resolved backend is `'sbx'`. */
86
+ readonly sbx: SbxRunPlan | null;
62
87
  /** Settings.json `packages` resolved into an install plan; `null` when the folder declares none. */
63
88
  readonly packages: PackagesPlan | null;
64
- /** The single argv source: `composeArgv(plan.launch)` — package-entry-free until `materializeStart` recomposes it with resolved package entries. */
89
+ /** The single argv source: `composeArgv(plan.launch)` — package-entry-free until `materializeRun` recomposes it with resolved package entries. */
65
90
  readonly launch: LaunchSpec;
66
91
  readonly dryRun: boolean;
67
92
  }
@@ -71,23 +96,32 @@ export interface StartPlan {
71
96
  * decides whether to print it (`--dry-run`) or materialize + spawn it.
72
97
  *
73
98
  * `--dry-run` only previews, so it deliberately skips the bin checks — you can
74
- * compose a command before nono/pi are installed.
99
+ * compose a command before nono/sbx/pi are installed.
75
100
  */
76
- export declare function planStart(flags: StartFlags, deps?: StartDeps): Promise<StartPlan>;
101
+ export declare function planRun(flags: RunFlags, deps?: RunDeps): Promise<RunPlan>;
102
+ export interface SbxRunResult {
103
+ readonly exitCode: number;
104
+ readonly stderr: string;
105
+ }
77
106
  export interface MaterializeDeps {
78
107
  /** Runs a package install (e.g. `npm install`) in `cwd`; `cli.ts` always passes `runInstall` from `util/proc.js`. */
79
108
  readonly install?: (command: readonly string[], cwd: string) => Promise<void>;
109
+ /** Runs one sbx setup command (create/policy/provision), capturing stderr; `cli.ts` passes `runCapture` from `util/proc.js`. */
110
+ readonly run?: (argv: readonly string[]) => Promise<SbxRunResult>;
111
+ /** Reads the host pi version (`cli.ts` passes its `--version` reader) to pin the guest install; absent → unpinned provisioning. */
112
+ readonly readPiVersion?: (piBin: string) => Promise<string | null>;
80
113
  }
81
114
  /**
82
115
  * Write the generated extensions (replacing stale ones), ensure the sessions
83
- * dir, and — on sandboxed runs — (re)write the generated per-agent nono profile
84
- * that `nono run --profile` points at. When the folder declares `packages`,
85
- * also (re)install the per-agent npm project and resolve each package's pi
86
- * extension entries, returning the final argv with those entries appended as
87
- * `-e` flags plus any resolution warnings; otherwise the plan's argv is
88
- * already final.
116
+ * dir, and — per backend — (re)write the generated per-agent nono profile
117
+ * that `nono run --profile` points at, or run the sbx setup sequence
118
+ * (create/policy/provision) and wrap the final argv in `sbx exec`. When the
119
+ * folder declares `packages`, also (re)install the per-agent npm project and
120
+ * resolve each package's pi extension entries, returning the final argv with
121
+ * those entries appended as `-e` flags plus any resolution warnings;
122
+ * otherwise the plan's argv is already final.
89
123
  */
90
- export declare function materializeStart(plan: StartPlan, deps?: MaterializeDeps): Promise<{
124
+ export declare function materializeRun(plan: RunPlan, deps?: MaterializeDeps): Promise<{
91
125
  argv: string[];
92
126
  warnings: string[];
93
127
  }>;