@coryrylan/cradle 0.0.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 ADDED
@@ -0,0 +1,3 @@
1
+ # Changelog
2
+
3
+ ## 0.0.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cory Rylan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # cradle
2
+
3
+ ![CI Build](https://github.com/coryrylan/cradle/actions/workflows/pull-request.yml/badge.svg)
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).
6
+
7
+ ## Dependencies
8
+
9
+ `cradle` drives external tools rather than bundling them. Run `cradle doctor` to check what's on your PATH.
10
+
11
+ | Tool | Status | Why |
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. |
15
+ | [`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
+ ## Installation
18
+
19
+ Install the standalone binary to `~/.local/bin/cradle` (macOS and Linux; no Bun required to run it):
20
+
21
+ ```bash
22
+ curl -fsSL https://coryrylan.github.io/cradle/install.sh | bash
23
+ ```
24
+
25
+ Or install the npm package — the installed command is still `cradle`, and [Bun](https://bun.sh) must be on your `PATH` to run it:
26
+
27
+ ```bash
28
+ npm install -g @coryrylan/cradle
29
+ ```
30
+
31
+ The install script and binaries are statically deployed with the [docs site](https://coryrylan.github.io/cradle/); from a clone, `bun run install:local` in `projects/cli/` builds and installs the same binary locally.
32
+
33
+ ## Development
34
+
35
+ Clone the repo and install dependencies:
36
+
37
+ ```bash
38
+ bun install
39
+ ```
40
+
41
+ Run the CLI directly:
42
+
43
+ ```bash
44
+ bun start
45
+ ```
46
+
47
+ ## Commands
48
+
49
+ | Command | Description |
50
+ | ------------------------- | --------------------------------------------------------------- |
51
+ | `bun start` | Run the CLI via Bun |
52
+ | `bun run build` | Build ESM bundle, type declarations, and platform binaries |
53
+ | `bun run test` | Run tests (no coverage) |
54
+ | `bun run test:coverage` | Run tests with coverage; enforces thresholds from `bunfig.toml` |
55
+ | `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
+ | `bun run ci` | Run lint + build + test:coverage (used in CI) |
59
+ | `bun run install:local` | Build and install binary to `~/.local/bin` |
60
+ | `bun run uninstall:local` | Remove locally installed binary |
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`.
63
+
64
+ ## CLI Usage
65
+
66
+ ```bash
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
75
+ ```
76
+
77
+ 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
+ ### Global agent aliases (`~/.cradle/settings.json`)
80
+
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
+
83
+ ```json
84
+ {
85
+ "agents": {
86
+ "my-agent": { "path": "~/dev/agents/my-agent/" }
87
+ }
88
+ }
89
+ ```
90
+
91
+ 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
+ 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` 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
+
95
+ ```json
96
+ {
97
+ "filesystem": {
98
+ "allow": ["~/.some-tool"]
99
+ }
100
+ }
101
+ ```
102
+
103
+ ### Network policy (`network`)
104
+
105
+ Outbound network is **open by default within a sandboxed run**. A `network` block in `sandbox/nono.json` tightens it — the keys mirror nono's canonical [`network`](https://nono.sh/docs) profile fields, folded into the generated profile and enforced by nono (a local CONNECT/credential proxy that Seatbelt forces all egress through). Verified enforced on macOS Seatbelt:
106
+
107
+ ```json
108
+ {
109
+ "network": {
110
+ "block": false,
111
+ "allow_domain": ["api.z.ai", "localhost"],
112
+ "open_port": [11434],
113
+ "listen_port": [8080]
114
+ }
115
+ }
116
+ ```
117
+
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`). |
125
+
126
+ 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
+
128
+ ### macOS Seatbelt escape hatch (`unsafe_macos_seatbelt_rules`)
129
+
130
+ 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
+
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:
133
+
134
+ ```json
135
+ {
136
+ "filesystem": {
137
+ "allow": ["~/.agent-browser"]
138
+ },
139
+ "unsafe_macos_seatbelt_rules": ["(allow mach-register)", "(allow iokit-open)"]
140
+ }
141
+ ```
142
+
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. |
148
+
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.
150
+
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:
152
+
153
+ ```ts
154
+ // extensions/browser-sandbox.ts — set the flag before agent-browser launches Chrome
155
+ process.env.AGENT_BROWSER_ARGS ??= '--no-sandbox';
156
+ ```
157
+
158
+ (Or pass `agent-browser --args --no-sandbox` on each call.) See [`examples/browser`](../../examples/browser) for the complete folder.
159
+
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.
161
+
162
+ ## Build Targets
163
+
164
+ The build produces platform-specific standalone binaries in `dist/`:
165
+
166
+ - `cradle-macos-arm64`
167
+ - `cradle-macos-x64`
168
+ - `cradle-linux-x64`
169
+ - `cradle-linux-arm64`
170
+ - `cradle-windows-x64.exe`
171
+
172
+ ## License
173
+
174
+ MIT
@@ -0,0 +1,19 @@
1
+ export interface ResolveRefDeps {
2
+ readonly home: string;
3
+ readonly cwd: string;
4
+ }
5
+ /**
6
+ * Resolve a `cradle start` ref into a folder path `loadAgentFolder` can load.
7
+ * Path-shaped refs pass through untouched (`loadAgentFolder`'s own `resolve()`
8
+ * handles them, and they never consult `deps.cwd`); a bare name resolves
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
11
+ * hello` from `examples/` keeps working) — and throwing only when neither
12
+ * resolves. Every bare-name branch resolves against the injected `deps.cwd`,
13
+ * never `process.cwd()`, so the dep is the single source of truth for what the
14
+ * lookup tested.
15
+ */
16
+ export declare function resolveAgentRef(ref: string, deps: ResolveRefDeps): Promise<{
17
+ dir: string;
18
+ warnings: readonly string[];
19
+ }>;
@@ -0,0 +1 @@
1
+ export declare function emitProvidersExtension(providersJson: string): string;
@@ -0,0 +1,76 @@
1
+ import { type NpmPackageSpec } from './packages.js';
2
+ export type ThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
3
+ export interface AgentSettings {
4
+ readonly defaultProvider?: string;
5
+ readonly defaultModel?: string;
6
+ readonly defaultThinkingLevel?: ThinkingLevel;
7
+ /** Parsed `npm:` package sources from settings.json's `packages` key — see `./packages.js`. */
8
+ readonly packages?: readonly NpmPackageSpec[];
9
+ /**
10
+ * Installer argv from settings.json's `npmCommand`; `["npm"]` when absent.
11
+ * Always exactly one of `npm`/`pnpm`/`yarn`/`bun` — see `readNpmCommand`.
12
+ */
13
+ readonly npmCommand?: readonly string[];
14
+ }
15
+ export interface AgentSandboxGrants {
16
+ readonly read: readonly string[];
17
+ readonly write: readonly string[];
18
+ readonly allow: readonly string[];
19
+ }
20
+ /**
21
+ * The agent's declared network posture — a curated subset of nono's profile
22
+ * `network` block, using nono's CANONICAL key names (not the `proxy_allow` /
23
+ * `port_allow` legacy aliases). Folded verbatim into the generated per-agent
24
+ * profile by `nono/profiles.ts`, where nono is the enforcement authority:
25
+ * `allow_domain` presence flips nono to default-deny proxy filtering, and nono
26
+ * fails closed (refuses to run) on an unenforceable platform or a bad key —
27
+ * cradle never has to synthesize a fallback. Absent entirely ⇒ nono default
28
+ * (open network), preserving the old `net: "allow"` behavior.
29
+ */
30
+ export interface AgentNetwork {
31
+ /** Deny all outbound (replaces the old `net: "block"`). */
32
+ readonly block?: boolean;
33
+ /** Named nono network-policy profile (host `network-policy.json`); opaque pass-through. */
34
+ readonly networkProfile?: string;
35
+ /** Outbound host allowlist — hostnames/IPs. Presence ⇒ nono default-denies unlisted hosts. */
36
+ readonly allowDomain?: readonly string[];
37
+ /** localhost TCP ports the child may connect+bind (local IPC / dev servers). */
38
+ readonly openPort?: readonly number[];
39
+ /** TCP ports the child may listen on. */
40
+ readonly listenPort?: readonly number[];
41
+ }
42
+ export type AgentSandboxPosture = 'unconfigured' | 'enabled' | 'disabled';
43
+ export interface AgentSandbox {
44
+ /**
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'`).
48
+ */
49
+ readonly posture: AgentSandboxPosture;
50
+ readonly network?: AgentNetwork;
51
+ readonly filesystem: AgentSandboxGrants;
52
+ /**
53
+ * Raw macOS Seatbelt s-expression rules from `sandbox/nono.json`'s
54
+ * `unsafe_macos_seatbelt_rules`, merged verbatim after the base profile's
55
+ * rules (see nono/profiles.ts). The escape hatch a browser agent uses to let
56
+ * Chrome register its crashpad Mach service and open IOKit — capabilities the
57
+ * default profile denies. Named "unsafe" because each rule widens the OS
58
+ * sandbox; nono's startup banner does not list seatbelt rules, so review a
59
+ * folder's `sandbox/nono.json` or the generated profile to audit them.
60
+ */
61
+ readonly unsafeMacosSeatbeltRules: readonly string[];
62
+ }
63
+ export interface AgentFolder {
64
+ readonly dir: string;
65
+ /** Abs path of APPEND_SYSTEM.md — appended to pi's system prompt via `--append-system-prompt`. */
66
+ readonly appendSystemFilePath: string;
67
+ readonly settings: AgentSettings;
68
+ /** Serialized `providers` object from models.json, `null` when absent. */
69
+ readonly providersJson: string | null;
70
+ readonly skillsDir: string | null;
71
+ /** pi-native extensions (top-level `extensions/*.ts` plus each subdir's `index.ts`), absolute paths. */
72
+ readonly extensionFiles: readonly string[];
73
+ readonly sandbox: AgentSandbox;
74
+ readonly warnings: readonly string[];
75
+ }
76
+ export declare function loadAgentFolder(dir: string): Promise<AgentFolder>;
@@ -0,0 +1,108 @@
1
+ import type { AgentFolder } from './folder.js';
2
+ export interface LaunchSpec {
3
+ readonly folder: AgentFolder;
4
+ readonly stateDir: string;
5
+ /**
6
+ * Where the generated providers extension is written (see
7
+ * `folder.providersJson`) and where `--session-dir` points, sourced from the
8
+ * plan's own `extensionsDir`/`sessionsDir` (`commands/start.ts` — the single
9
+ * derivation site; re-deriving from `stateDir` here risks silently
10
+ * disagreeing with a plan whose dirs were overridden after `planStart`).
11
+ */
12
+ readonly extensionsDir: string;
13
+ readonly sessionsDir: string;
14
+ /**
15
+ * Where sandboxed runs point `MISE_CACHE_DIR` (see `composeEnv`), sourced
16
+ * from the plan's `statePaths` derivation like `extensionsDir`/`sessionsDir`.
17
+ */
18
+ readonly miseCacheDir: string;
19
+ readonly sandbox: boolean;
20
+ readonly passthrough: readonly string[];
21
+ /** Resolved path, or bare `nono` for dry-run previews. */
22
+ readonly nonoBin: string;
23
+ /**
24
+ * Resolved path (mise-shim fallback included, see `util/which.ts`), or bare
25
+ * `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.
31
+ */
32
+ readonly piBin: string;
33
+ /** Abs path of the generated per-agent nono profile; used only when `sandbox` is true. */
34
+ readonly profilePath: string;
35
+ /**
36
+ * When true, nono runs without `--silent` so its capabilities banner
37
+ * (effective grants + network disclosure) prints; default is silent — the
38
+ * generated profile file and `--verbose` are the audit surfaces.
39
+ */
40
+ readonly verbose?: boolean;
41
+ /**
42
+ * Abs paths of the pi extension entry files resolved from settings.json's
43
+ * `packages` (see `./packages.js`), installed into `<stateDir>/npm` by
44
+ * `commands/start.ts`. Absent/empty on the argv-only preview used for
45
+ * `--dry-run` (packages resolve at install time, after the preview is
46
+ * printed) and on folders that declare no packages.
47
+ */
48
+ readonly packageEntries?: readonly string[];
49
+ }
50
+ /**
51
+ * Build the bare pi argv. `passthrough` lands last so user-passed flags win
52
+ * 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.
57
+ */
58
+ export declare function composePiArgv(spec: LaunchSpec): string[];
59
+ /**
60
+ * Compose the spawn env for a sandboxed run: `{ MISE_CACHE_DIR: spec.miseCacheDir }`;
61
+ * `{}` when unsandboxed.
62
+ *
63
+ * The generated profile denies the shared `~/Library/Caches/mise` on purpose
64
+ * — a poisoned `bin_paths` cache would redirect which binaries the user's
65
+ * later UNSANDBOXED mise execs resolve to, invisibly and machine-globally.
66
+ * Without this override, every sandboxed `mise exec` spams `mise WARN failed
67
+ * to write cache file` because it can't write there. Pointing `MISE_CACHE_DIR`
68
+ * at a private cache inside the already-granted state dir instead lets mise
69
+ * cache writes succeed (no warnings, working cache) while the shared host
70
+ * cache stays untouched; mise creates the directory itself, cradle never
71
+ * pre-creates or wipes it. Unsandboxed runs return `{}` and keep the shared
72
+ * host cache — no override needed since there's no sandbox denial to work
73
+ * around.
74
+ *
75
+ * This deliberately overrides any user-set `MISE_CACHE_DIR` for sandboxed
76
+ * runs — the profile wouldn't grant a custom location either, so honoring one
77
+ * would just trade the warning spam for a silent cache-write failure. It is
78
+ * also cradle's single exception to argv-only composition (see the module
79
+ * header and `ARCHITECTURE.md`'s "Why argv" section): this configures mise,
80
+ * not pi, so it doesn't touch the argv-survives-sandboxing rationale that
81
+ * motivates keeping pi's own configuration on argv. The failure mode if this
82
+ * env var were ever stripped is benign — mise falls back to the shared cache
83
+ * and the warnings return, nothing breaks.
84
+ */
85
+ export declare function composeEnv(spec: LaunchSpec): Record<string, string>;
86
+ /**
87
+ * Build the argv for `nono run … -- pi …`, or the bare pi argv when
88
+ * sandboxing is disabled (`--no-sandbox`).
89
+ *
90
+ * All filesystem grants (cwd, agent dir, state dir, and the agent's own
91
+ * `sandbox/nono.json` entries) AND the network posture live inside the
92
+ * generated per-agent profile at `spec.profilePath` — see `nono/profiles.ts`.
93
+ * The wrapper is `nono run --silent --profile <file>` by default (silent mode
94
+ * suppresses nono's startup banner); `--verbose` omits `--silent` to show the
95
+ * capabilities banner, and cradle prints `🔒 Sandbox Active` on silent runs.
96
+ * No per-flag network posture — it all lives in the profile.
97
+ *
98
+ * Both `spec.nonoBin` and `spec.piBin` are resolved paths (mise-shim fallback
99
+ * included), never bare names: cradle spawns `nonoBin` directly, and a
100
+ * freshly mise-installed nono may not be on PATH yet; `piBin` is passed
101
+ * through to nono as plain argv (`-- <piBin> …`), and nono execs it *inside*
102
+ * the sandbox, where the resolved absolute path resolves fine against the
103
+ * mise install tree the base profile already grants read on (see
104
+ * `nono/cradle-pi.json`) — no PATH lookup needed either side of the sandbox
105
+ * boundary. nono guarantees child processes inherit the sandbox, so pi
106
+ * spawning its own subprocesses is covered too.
107
+ */
108
+ export declare function composeArgv(spec: LaunchSpec): string[];
@@ -0,0 +1,28 @@
1
+ import { type JsonValue } from '../setup/utils.js';
2
+ /** A parsed `npm:<name>[@<version>]` package source from settings.json's `packages` key. */
3
+ export interface NpmPackageSpec {
4
+ readonly name: string;
5
+ readonly version: string;
6
+ }
7
+ /**
8
+ * Parse a settings.json record's `packages` key into validated npm: package
9
+ * specs. Only `npm:` sources are supported — pi also accepts `git:`,
10
+ * `https://`, `ssh://`, and local paths, all warned and dropped here, since
11
+ * cradle installs the package itself rather than delegating to pi's loader.
12
+ */
13
+ export declare function readPackageSpecs(record: {
14
+ readonly [key: string]: JsonValue;
15
+ }, path: string, warnings: string[]): readonly NpmPackageSpec[];
16
+ /** Deterministic npm manifest for a per-agent packages install, dependencies sorted by name. */
17
+ export declare function emitPackagesManifest(specs: readonly NpmPackageSpec[]): string;
18
+ /**
19
+ * Resolve each spec's installed package into pi extension entry files, ready
20
+ * to pass as explicit `-e` flags. Every failure (not installed, malformed
21
+ * manifest, an entry that escapes the package directory, a missing entry
22
+ * file, no declared extensions) is a warning + skip — packages are
23
+ * warn-don't-throw like every other agent-folder input.
24
+ */
25
+ export declare function resolvePackageEntries(npmDir: string, specs: readonly NpmPackageSpec[]): Promise<{
26
+ entries: string[];
27
+ warnings: string[];
28
+ }>;
@@ -0,0 +1,10 @@
1
+ export interface StatePaths {
2
+ readonly extensionsDir: string;
3
+ readonly sessionsDir: string;
4
+ readonly miseCacheDir: string;
5
+ }
6
+ /** Stable, readable id for an agent folder: sanitized basename + 8-hex path hash. */
7
+ export declare function agentId(absDir: string): string;
8
+ /** Where an agent's runtime state lives. Override the root via `CRADLE_STATE_DIR` (or the `stateRoot` param). */
9
+ export declare function stateDirFor(absDir: string, home?: string, stateRoot?: string | undefined): string;
10
+ export declare function statePaths(stateDir: string): StatePaths;
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ export {};
@@ -0,0 +1,25 @@
1
+ import { type WhichFn } from '../util/which.js';
2
+ export interface DoctorCheck {
3
+ readonly name: string;
4
+ readonly bin: string;
5
+ readonly required: boolean;
6
+ readonly found: string | null;
7
+ readonly version: string | null;
8
+ readonly note?: string;
9
+ }
10
+ interface DoctorDeps {
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>;
14
+ }
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).
20
+ */
21
+ export declare function runDoctor(deps?: DoctorDeps): Promise<DoctorCheck[]>;
22
+ export declare function formatDoctorReport(checks: readonly DoctorCheck[]): string;
23
+ /** Exit non-zero when any required dependency is missing. */
24
+ export declare function doctorExitCode(checks: readonly DoctorCheck[]): number;
25
+ export {};
@@ -0,0 +1,94 @@
1
+ import { type LaunchSpec } from '../agent/launch.js';
2
+ import { type NpmPackageSpec } from '../agent/packages.js';
3
+ import { type TreeFile } from '../setup/install.js';
4
+ import { type WhichFn } from '../util/which.js';
5
+ export interface StartFlags {
6
+ readonly dir: string;
7
+ /** `--offline` → full network block. Overrides the folder network posture and forces the sandbox on (unless --no-sandbox). */
8
+ readonly offline?: boolean;
9
+ /** `--allow-host` (repeatable) → network host allowlist. Overrides the folder allowlist and forces the sandbox on (unless --no-sandbox). */
10
+ readonly allowHost?: readonly string[];
11
+ /** Explicit CLI choice: `--no-sandbox` → true, `--sandbox` → false. Absent = defer to the folder. */
12
+ readonly noSandbox?: boolean;
13
+ readonly dryRun?: boolean;
14
+ readonly passthrough?: readonly string[];
15
+ /** `--verbose` → show nono's full sandbox capabilities banner instead of the one-line status. */
16
+ readonly verbose?: boolean;
17
+ }
18
+ interface StartDeps {
19
+ readonly cwd?: string;
20
+ readonly home?: string;
21
+ readonly which?: WhichFn;
22
+ }
23
+ /**
24
+ * The agent's settings.json `packages` resolved into a per-agent npm install
25
+ * plan. `npmDir` is a private npm project under the agent's state dir — never
26
+ * the folder itself, so the install stays out of the portable, committable
27
+ * agent folder.
28
+ */
29
+ export interface PackagesPlan {
30
+ readonly npmDir: string;
31
+ /** `emitPackagesManifest` output — the package.json cradle writes into `npmDir`. */
32
+ readonly manifest: string;
33
+ readonly specs: readonly NpmPackageSpec[];
34
+ /**
35
+ * `[...(settings.npmCommand ?? ['npm']), 'install', '--ignore-scripts']`.
36
+ * This install runs on the host, unsandboxed, before the sandbox spawns —
37
+ * `--ignore-scripts` stops a folder-declared package's postinstall from
38
+ * running arbitrary host code (`npmCommand` is validated upstream to a
39
+ * single-element allowlist of `npm`/`pnpm`/`yarn`/`bun`, all of which accept
40
+ * this flag).
41
+ */
42
+ readonly installCommand: readonly string[];
43
+ }
44
+ export interface StartPlan {
45
+ /** Generated extensions, relative to `extensionsDir`. */
46
+ readonly files: readonly TreeFile[];
47
+ /**
48
+ * Authoritative — the single derivation site for where generated extensions
49
+ * and sessions live. `materializeStart` re-asserts these onto `launch`
50
+ * before composing the final argv, so overriding either here (as tests do)
51
+ * changes the composed argv accordingly instead of silently disagreeing
52
+ * with whatever `launch` had baked in at `planStart` time.
53
+ */
54
+ readonly extensionsDir: string;
55
+ readonly sessionsDir: string;
56
+ readonly warnings: readonly string[];
57
+ /** The generated per-agent nono profile to write before spawning; `null` on unsandboxed runs (no profile needed). */
58
+ readonly profile: {
59
+ readonly path: string;
60
+ readonly content: string;
61
+ } | null;
62
+ /** Settings.json `packages` resolved into an install plan; `null` when the folder declares none. */
63
+ readonly packages: PackagesPlan | null;
64
+ /** The single argv source: `composeArgv(plan.launch)` — package-entry-free until `materializeStart` recomposes it with resolved package entries. */
65
+ readonly launch: LaunchSpec;
66
+ readonly dryRun: boolean;
67
+ }
68
+ /**
69
+ * Resolve the ref (bare alias name or path — see `../agent/aliases.js`), load
70
+ * the agent folder, and compose the launch. Returns a plan; the caller
71
+ * decides whether to print it (`--dry-run`) or materialize + spawn it.
72
+ *
73
+ * `--dry-run` only previews, so it deliberately skips the bin checks — you can
74
+ * compose a command before nono/pi are installed.
75
+ */
76
+ export declare function planStart(flags: StartFlags, deps?: StartDeps): Promise<StartPlan>;
77
+ export interface MaterializeDeps {
78
+ /** Runs a package install (e.g. `npm install`) in `cwd`; `cli.ts` always passes `runInstall` from `util/proc.js`. */
79
+ readonly install?: (command: readonly string[], cwd: string) => Promise<void>;
80
+ }
81
+ /**
82
+ * 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.
89
+ */
90
+ export declare function materializeStart(plan: StartPlan, deps?: MaterializeDeps): Promise<{
91
+ argv: string[];
92
+ warnings: string[];
93
+ }>;
94
+ export {};
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ import './cli.js';