@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 +3 -0
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/agent/aliases.d.ts +19 -0
- package/dist/agent/extensions/providers.d.ts +1 -0
- package/dist/agent/folder.d.ts +76 -0
- package/dist/agent/launch.d.ts +108 -0
- package/dist/agent/packages.d.ts +28 -0
- package/dist/agent/state.d.ts +10 -0
- package/dist/cli.d.ts +2 -0
- package/dist/commands/doctor.d.ts +25 -0
- package/dist/commands/start.d.ts +94 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +95 -0
- package/dist/nono/linked-git-dir.d.ts +11 -0
- package/dist/nono/profiles.d.ts +66 -0
- package/dist/setup/install.d.ts +22 -0
- package/dist/setup/utils.d.ts +24 -0
- package/dist/util/proc.d.ts +21 -0
- package/dist/util/style.d.ts +2 -0
- package/dist/util/which.d.ts +19 -0
- package/package.json +233 -0
package/CHANGELOG.md
ADDED
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
|
+

|
|
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,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 {};
|
package/dist/index.d.ts
ADDED