@clipboard-health/groundcrew 4.48.0 → 4.48.2

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.
Files changed (54) hide show
  1. package/clearance-allow-hosts +1 -0
  2. package/crew.config.example.ts +2 -2
  3. package/dist/commands/doctor.js +0 -25
  4. package/dist/commands/openWorkspace.d.ts.map +1 -1
  5. package/dist/commands/openWorkspace.js +14 -17
  6. package/dist/commands/resumeWorkspace.d.ts.map +1 -1
  7. package/dist/commands/resumeWorkspace.js +7 -15
  8. package/dist/commands/setupWorkspace.d.ts.map +1 -1
  9. package/dist/commands/setupWorkspace.js +16 -18
  10. package/dist/lib/agentLaunch.d.ts +13 -11
  11. package/dist/lib/agentLaunch.d.ts.map +1 -1
  12. package/dist/lib/agentLaunch.js +102 -52
  13. package/dist/lib/cmuxAgentHookInstall.d.ts +17 -0
  14. package/dist/lib/cmuxAgentHookInstall.d.ts.map +1 -0
  15. package/dist/lib/cmuxAgentHookInstall.js +42 -0
  16. package/dist/lib/codexConfigRelocation.d.ts +60 -0
  17. package/dist/lib/codexConfigRelocation.d.ts.map +1 -0
  18. package/dist/lib/codexConfigRelocation.js +70 -0
  19. package/dist/lib/commandRunner.d.ts +9 -0
  20. package/dist/lib/commandRunner.d.ts.map +1 -1
  21. package/dist/lib/commandRunner.js +1 -0
  22. package/dist/lib/config.d.ts +8 -12
  23. package/dist/lib/config.d.ts.map +1 -1
  24. package/dist/lib/config.js +1 -2
  25. package/dist/lib/gitCommonDir.d.ts +7 -0
  26. package/dist/lib/gitCommonDir.d.ts.map +1 -0
  27. package/dist/lib/gitCommonDir.js +15 -0
  28. package/dist/lib/host.d.ts +0 -14
  29. package/dist/lib/host.d.ts.map +1 -1
  30. package/dist/lib/host.js +1 -8
  31. package/dist/lib/launchCommand.d.ts +32 -52
  32. package/dist/lib/launchCommand.d.ts.map +1 -1
  33. package/dist/lib/launchCommand.js +96 -187
  34. package/dist/lib/localRunner.d.ts +4 -5
  35. package/dist/lib/localRunner.d.ts.map +1 -1
  36. package/dist/lib/localRunner.js +4 -23
  37. package/docs/configuration.md +3 -3
  38. package/docs/runners.md +7 -50
  39. package/docs/setup-hooks.md +1 -1
  40. package/docs/spec/e2e-scenario-catalog.md +1 -1
  41. package/docs/spec/v2-design.md +7 -7
  42. package/docs/task-sources.md +3 -3
  43. package/package.json +2 -5
  44. package/static/demo.tape +0 -2
  45. package/static/render-demo.sh +0 -1
  46. package/dist/lib/clearanceHosts.d.ts +0 -31
  47. package/dist/lib/clearanceHosts.d.ts.map +0 -1
  48. package/dist/lib/clearanceHosts.js +0 -96
  49. package/dist/lib/srtLaunch.d.ts +0 -61
  50. package/dist/lib/srtLaunch.d.ts.map +0 -1
  51. package/dist/lib/srtLaunch.js +0 -107
  52. package/dist/lib/srtPolicy.d.ts +0 -118
  53. package/dist/lib/srtPolicy.d.ts.map +0 -1
  54. package/dist/lib/srtPolicy.js +0 -371
@@ -1,371 +0,0 @@
1
- /**
2
- * Generate the srt (Anthropic sandbox-runtime) settings object for a single
3
- * agent launch. srt is only a sandbox engine — it has no groundcrew- or
4
- * agent-aware policy of its own — so this module owns the policy ergonomics
5
- * that safehouse previously supplied via its bundled `.sb` profiles.
6
- *
7
- * The shape, in one place:
8
- *
9
- * - **Reads** start broad and are clamped: `denyRead` masks the whole home
10
- * region (`/Users` on macOS, `/home` + `/root` on Linux) so the agent cannot
11
- * read `~/.ssh`, `~/.aws`, shell history, or unrelated repos; `allowRead`
12
- * then re-opens exactly the worktree, the repo's git metadata, the language
13
- * toolchains needed to *run* the agent, the agent's own config/credential
14
- * dirs, and — on macOS, for keychain-authenticated agents (claude) — the user
15
- * keychain dir (`~/Library/Keychains`) so the agent can authenticate under
16
- * the home mask. srt skips non-existent allow/deny paths, so listing a path
17
- * that isn't present (a Linux keychain dir, an uninstalled toolchain) is
18
- * harmless.
19
- * - **Writes** are allow-only in srt. Two STAFF-1305 structural fixes shape it:
20
- * 1. **Agent state (work item 1).** The host-CLI persistence vector (planting
21
- * hooks, `mcpServers`, commands, plugins, … that execute on the user's
22
- * next host run) is closed per agent. **claude** runs with a writable
23
- * `~/.claude` (its Bash tool creates `session-env`/scratch state there) but
24
- * every fixed-path executable/instruction surface — including
25
- * `~/.claude.json` (`mcpServers`) and the bundled `chrome` binary — is
26
- * denied; claude tolerates those write denials. **codex** hard-fails with a
27
- * read-only home, so it is pointed at a relocated, per-launch writable
28
- * config dir (`CODEX_HOME`, see {@link agentConfigRelocation}) and its real
29
- * `~/.codex` is never write-granted at all. **cursor-agent** keeps a
30
- * writable `~/.cursor` (chats/projects/resume) but denies mcp.json,
31
- * hooks, rules, skills, plugins, and commands — the host-RCE surfaces.
32
- * 2. **Git (work item 2).** The git common dir is granted write as a **narrow
33
- * allowlist** of exactly the paths `status/diff/add/commit/push/gc` write —
34
- * never wholesale — so the per-worktree gitdir redirection files, sibling
35
- * worktree gitdirs, and the repo `config`/`hooks` stay unwritable.
36
- * `denyWrite` is belt-and-suspenders over global toolchain bins and the agent
37
- * homes; it uses **literal paths only** because bubblewrap silently ignores
38
- * globs on Linux.
39
- * - **Network** is allow-only and sourced from the existing clearance
40
- * allowlist (see {@link ./clearanceHosts.ts}); local binding and unix sockets
41
- * stay off (the docker socket and the DNS-exfil vector in srt#88).
42
- *
43
- * `allowPty` is on because the agent runs interactively under tmux;
44
- * `allowGitConfig` stays off so the agent cannot rewrite `~/.gitconfig` or
45
- * `.git/config` (both readable, just not writable).
46
- */
47
- import os from "node:os";
48
- import path from "node:path";
49
- import process from "node:process";
50
- import { SandboxRuntimeConfigSchema, } from "@anthropic-ai/sandbox-runtime";
51
- /**
52
- * Return the config-relocation spec for an agent, or `undefined` when the agent
53
- * runs with a writable real home + deny-list and does not relocate (claude,
54
- * unknown agents).
55
- */
56
- export function agentConfigRelocation(agent) {
57
- return AGENT_CONFIG_RELOCATIONS[agent.toLowerCase()];
58
- }
59
- /**
60
- * Per-agent credential/config profiles. Deliberately narrow — no blanket
61
- * `~/.config`, which would re-expose unrelated apps' secrets. An unknown agent
62
- * gets no extra home access and must be granted paths explicitly.
63
- *
64
- * claude keeps a writable home (`writePaths`) with the executable surfaces
65
- * re-closed (`denyPaths`) because its macOS keychain credential is bound to the
66
- * default config dir, so it cannot be relocated. codex has no `writePaths`: it
67
- * relocates (see `AGENT_CONFIG_RELOCATIONS`), so its real `~/.codex` is
68
- * read-only and no per-surface deny-list is needed there.
69
- */
70
- const AGENT_SRT_PROFILES = {
71
- claude: {
72
- readPaths: [".claude", ".claude.json"],
73
- writePaths: [".claude"],
74
- denyPaths: [
75
- // The mcpServers config — claude spawns these commands on every startup,
76
- // the sharpest host-RCE persistence vector. claude tolerates this being
77
- // read-only (validated live; it does not write it during a task).
78
- ".claude.json",
79
- ".claude/settings.json",
80
- ".claude/settings.local.json",
81
- ".claude/commands",
82
- ".claude/agents",
83
- ".claude/plugins",
84
- ".claude/skills",
85
- ".claude/hooks",
86
- ".claude/statusline.sh",
87
- ".claude/CLAUDE.md",
88
- // Bundled browser binary — replaceable with a malicious one that runs when
89
- // claude next drives a browser on the host.
90
- ".claude/chrome",
91
- // ~/.claude is itself a git repo; deny the executable surfaces within its
92
- // gitdir (commits, if any, still write objects/refs).
93
- ".claude/.git/hooks",
94
- ".claude/.git/config",
95
- ],
96
- usesMacosKeychain: true,
97
- },
98
- codex: {
99
- // Read-only: codex relocates its writable home (CODEX_HOME), so the real
100
- // ~/.codex never needs write and no per-surface deny-list is required.
101
- readPaths: [".codex"],
102
- writePaths: [],
103
- denyPaths: [],
104
- },
105
- "cursor-agent": {
106
- // Writable state for chats/projects/resume (`--continue`), matching the
107
- // paths Safehouse's cursor-agent profile re-opens. Persistence surfaces
108
- // that would execute on the user's next host run are carved back out.
109
- readPaths: [".cursor", ".config/cursor", ".local/share/cursor-agent"],
110
- writePaths: [
111
- ".cursor",
112
- ".config/cursor",
113
- ".cache/cursor-compile-cache",
114
- "Library/Caches/cursor-compile-cache",
115
- ],
116
- denyPaths: [
117
- // mcpServers — the sharpest host-RCE vector (same class as claude's
118
- // ~/.claude.json mcpServers).
119
- ".cursor/mcp.json",
120
- // Lifecycle hooks run on the user's next Cursor launch.
121
- ".cursor/hooks.json",
122
- ".cursor/hooks",
123
- // Global rules can inject instructions into future Cursor sessions.
124
- ".cursor/rules",
125
- ".cursor/skills-cursor",
126
- ".cursor/plugins",
127
- ".cursor/commands",
128
- ],
129
- usesMacosKeychain: true,
130
- },
131
- };
132
- const AGENT_CONFIG_RELOCATIONS = {
133
- codex: {
134
- configDirEnv: "CODEX_HOME",
135
- sourceHomeRelativeDir: ".codex",
136
- // auth.json carries the ChatGPT OAuth tokens (codex reads creds from a file,
137
- // not the keychain); config.toml preserves the user's codex configuration.
138
- seedFiles: ["auth.json", "config.toml"],
139
- },
140
- };
141
- /** macOS user keychain dir, re-opened read-only for keychain-authenticated agents. */
142
- const MACOS_KEYCHAIN_READ_PATH = "Library/Keychains";
143
- /**
144
- * Language toolchains and version managers re-opened read-only so the agent's
145
- * runtime (and any installed CLIs) can execute even though they live under the
146
- * home deny-read mask.
147
- *
148
- * srt's `allowRead` takes precedence over `denyRead`, so a credential carve-out
149
- * is impossible once a parent is re-opened — the roots themselves must be
150
- * narrow. Pure version-manager dirs (no credentials) are kept whole so version
151
- * resolution (`nvm use`, etc.) works; multi-purpose homes are narrowed to their
152
- * executable + dependency-cache subpaths so credential/config files (e.g.
153
- * `~/.cargo/credentials.toml`) and unrelated app state (`~/.local/share`) stay
154
- * masked. The node runtime itself is re-opened separately via `nodePrefix`.
155
- * (Polyglot coverage is best-effort + user-extensible; validated in STAFF-1305.)
156
- */
157
- const TOOLCHAIN_READ_ROOTS = [
158
- ".nvm",
159
- ".rustup",
160
- ".asdf",
161
- ".volta",
162
- ".pyenv",
163
- ".rbenv",
164
- ".npm", // npm cache; the ~/.npmrc credential file lives at $HOME and stays denied
165
- ".local/bin",
166
- ".local/lib",
167
- ".cargo/bin",
168
- ".cargo/registry",
169
- ".cargo/git",
170
- ".bun/bin",
171
- ".bun/install",
172
- ".deno/bin",
173
- "go/bin",
174
- "go/pkg",
175
- ];
176
- /**
177
- * The git common dir is granted write only at these relative subpaths — never
178
- * wholesale — so the agent's `status/diff/add/commit/push` + `gc`/`pack-refs`
179
- * work while the persistence/tamper surfaces stay unwritable by *omission*
180
- * (macOS Seatbelt is deny-beats-allow, so a denied parent cannot be re-allowed
181
- * for a child — the allowlist is the only correct shape). Closed by not being
182
- * listed: `config`, `hooks`, `modules`, and **sibling** worktree gitdirs under
183
- * `worktrees/<other>` (cross-task tamper). This worktree's own gitdir is
184
- * granted separately and its redirection files carved back out (see
185
- * `gitCommonWriteDenies`).
186
- *
187
- * Enumerated against a live run under srt (STAFF-1305): `objects` (loose +
188
- * packs + commit-graph), `refs` + `logs` (branch + remote-tracking refs and
189
- * their reflogs — granted whole because `gc` packs/deletes refs across *all*
190
- * branches, so scoping to the current branch would break `gc`), `packed-refs`
191
- * (+ `.lock`/`.new` temps), `info` (`update-server-info`), and the root-level
192
- * `gc.pid`/`HEAD`/`ORIG_HEAD`/`FETCH_HEAD` (+ `.lock` temps) that `gc`'s
193
- * repo-global reflog expiry touches. None are code-execution surfaces.
194
- */
195
- const GIT_COMMON_WRITE_PATHS = [
196
- "objects",
197
- "refs",
198
- "logs",
199
- "info",
200
- "packed-refs",
201
- "packed-refs.lock",
202
- "packed-refs.new",
203
- "gc.pid",
204
- "gc.pid.lock",
205
- "HEAD",
206
- "HEAD.lock",
207
- "ORIG_HEAD",
208
- "FETCH_HEAD",
209
- ];
210
- /**
211
- * Every agent credential/state home dir. A profile that does NOT own one of
212
- * these (it isn't in the profile's `writePaths`) must deny writes to it — both
213
- * as cross-agent defense (the codex profile shouldn't write `~/.claude`) and to
214
- * override srt's hardcoded default write path `~/.claude/debug`, which
215
- * `getDefaultWritePaths()` adds to every policy. Without this, that default
216
- * re-opens `~/.claude/debug` (and, on Linux, makes it readable via the write
217
- * bind) even under the profile-neutral prepare policy and the relocating codex
218
- * profile. `denyWrite` wins over `allowWrite`, so denying the home dir overrides
219
- * the default.
220
- */
221
- const ALL_AGENT_HOME_DIRS = [".claude", ".codex", ".cursor"];
222
- /** Git identity/config the agent reads (never writes — see `allowGitConfig`). */
223
- const GIT_READ_PATHS = [".gitconfig", ".config/git"];
224
- /**
225
- * Global toolchain bin/module locations writes are denied to, to close the
226
- * agent-safehouse#102 persistence vector (modifying a globally-installed CLI
227
- * that the user later runs outside the sandbox). Home-relative literals.
228
- *
229
- * `.npm/_npx` is denied even though `~/.npm` is writable for the npm cache:
230
- * `npx` stores downloaded tools there as ready-to-run binaries, so an agent
231
- * that poisons that cache would get host execution the next time the user runs
232
- * `npx <tool>` outside the sandbox — the same vector as the bin dirs above.
233
- */
234
- const TOOLCHAIN_WRITE_DENY = [
235
- ".cargo/bin",
236
- "go/bin",
237
- ".bun/install/global",
238
- ".deno/bin",
239
- ".local/bin",
240
- ".npm-global",
241
- ".npm/_npx",
242
- ".npmrc",
243
- ];
244
- export function buildSrtSettings(input) {
245
- const platform = input.platform ?? process.platform;
246
- const homeDir = input.homeDir ?? os.homedir();
247
- const nodeExecPath = input.nodeExecPath ?? process.execPath;
248
- const profile = AGENT_SRT_PROFILES[input.agent.toLowerCase()] ?? {
249
- readPaths: [],
250
- writePaths: [],
251
- denyPaths: [],
252
- };
253
- const underHome = (relativePath) => path.join(homeDir, relativePath);
254
- const underGit = (relativePath) => path.join(input.gitCommonDir, relativePath);
255
- const worktreeBasename = path.basename(input.worktreeDir);
256
- // `<nodeBin>/../` is the node prefix; nvm/Volta-managed nodes keep their
257
- // global modules at `<prefix>/lib/node_modules` and shims at `<prefix>/bin`.
258
- const nodePrefix = path.dirname(path.dirname(nodeExecPath));
259
- const nodeGlobalModules = path.join(nodePrefix, "lib", "node_modules");
260
- const nodeBinDir = path.join(nodePrefix, "bin");
261
- // `/mnt` masks WSL's Windows drive mounts (e.g. `/mnt/c/Users/<user>/.aws`,
262
- // `.ssh`) — the Windows profile is readable from WSL and would otherwise
263
- // bypass the home mask on a documented-supported platform. Harmless on native
264
- // Linux (the worktree, if it lives under /mnt, is re-allowed below since
265
- // allowRead wins over denyRead).
266
- const denyRead = platform === "darwin" ? ["/Users"] : ["/home", "/root", "/mnt"];
267
- // macOS keychain re-open for keychain-authenticated agents. Home-relative so
268
- // it is a no-op on Linux (the path does not exist there; srt skips it), where
269
- // these agents read credentials from a file under their (readable) home.
270
- const keychainRead = platform === "darwin" && profile.usesMacosKeychain === true
271
- ? [underHome(MACOS_KEYCHAIN_READ_PATH)]
272
- : [];
273
- const allowRead = unique([
274
- input.worktreeDir,
275
- input.gitCommonDir,
276
- nodePrefix,
277
- ...TOOLCHAIN_READ_ROOTS.map(underHome),
278
- ...GIT_READ_PATHS.map(underHome),
279
- ...profile.readPaths.map(underHome),
280
- ...(input.readOnlyDirs ?? []),
281
- ...keychainRead,
282
- ...(input.relocatedConfigDir === undefined ? [] : [input.relocatedConfigDir]),
283
- ...(input.taskSourceWritePaths ?? []),
284
- ]);
285
- const allowWrite = unique([
286
- input.worktreeDir,
287
- underHome(".npm"),
288
- // Narrow git allowlist — never the whole common dir (see GIT_COMMON_WRITE_PATHS).
289
- ...GIT_COMMON_WRITE_PATHS.map(underGit),
290
- underGit(path.join("worktrees", worktreeBasename)),
291
- ...profile.writePaths.map(underHome),
292
- // The agent's relocated, writable config home (codex). Absent for agents
293
- // that write their real home behind a deny-list (claude).
294
- ...(input.relocatedConfigDir === undefined ? [] : [input.relocatedConfigDir]),
295
- ...(input.taskSourceWritePaths ?? []),
296
- ]);
297
- const denyWrite = unique([
298
- nodeGlobalModules,
299
- nodeBinDir,
300
- ...TOOLCHAIN_WRITE_DENY.map(underHome),
301
- // Carve the agent's executable/config surfaces back out of its writable
302
- // state dir so a prompted agent can't plant a hook/command/plugin/mcpServer
303
- // that runs on the user's next host invocation (denyWrite wins over
304
- // allowWrite).
305
- ...profile.denyPaths.map(underHome),
306
- // Deny agent home dirs this profile does not own — counters srt's default
307
- // `~/.claude/debug` write path for the neutral prepare policy and the
308
- // relocating codex profile, and keeps profiles from writing each other's
309
- // credentials.
310
- ...ALL_AGENT_HOME_DIRS.filter((dir) => !profile.writePaths.includes(dir)).map(underHome),
311
- // Carve the per-worktree git redirection + config files back out of the
312
- // granted worktree gitdir: `commondir`/`gitdir` redirect git to a fake
313
- // common dir with its own hooks/config, and `config.worktree` can set
314
- // `core.*` hooks that run when git next operates here on the host. git
315
- // writes these once at worktree creation, never during a task.
316
- ...gitCommonWriteDenies(input.gitCommonDir, worktreeBasename),
317
- // The worktree's `.git` is a pointer *file* (`gitdir: …`). Deny writing it so
318
- // the agent can't redirect the gitdir to a writable fake with its own
319
- // config/hooks. git sets this pointer once at creation.
320
- path.join(input.worktreeDir, ".git"),
321
- ]);
322
- const settings = {
323
- network: {
324
- allowedDomains: [...input.allowedDomains],
325
- deniedDomains: [],
326
- allowLocalBinding: false,
327
- allowUnixSockets: [],
328
- allowAllUnixSockets: false,
329
- },
330
- filesystem: {
331
- denyRead,
332
- allowRead,
333
- allowWrite,
334
- denyWrite,
335
- allowGitConfig: false,
336
- },
337
- allowPty: true,
338
- };
339
- // Fail closed: validate with srt's own schema before this is staged. srt's
340
- // `loadConfig` `safeParse`s the settings file and, on ANY failure, returns
341
- // null — at which point the CLI silently falls back to a default config with
342
- // `denyRead: []`, disabling the home read mask for the launch. A single
343
- // malformed `allowedDomains` entry (e.g. a URL or host:port that slipped
344
- // through `collectAllowedDomains`) would otherwise trip that fail-open. Throw
345
- // here instead so the launch aborts loudly rather than running unsandboxed.
346
- const validation = SandboxRuntimeConfigSchema.safeParse(settings);
347
- if (!validation.success) {
348
- const detail = validation.error.issues
349
- .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
350
- .join("; ");
351
- throw new Error(`Generated srt settings failed validation (refusing to launch unsandboxed): ${detail}`);
352
- }
353
- return settings;
354
- }
355
- /**
356
- * Files inside this worktree's granted gitdir that must stay unwritable: the
357
- * `commondir`/`gitdir` redirection pointers and the per-worktree
358
- * `config.worktree`. Returned as denies so they override the gitdir's
359
- * `allowWrite` grant.
360
- */
361
- function gitCommonWriteDenies(gitCommonDir, worktreeBasename) {
362
- const worktreeGitDir = path.join(gitCommonDir, "worktrees", worktreeBasename);
363
- return [
364
- path.join(worktreeGitDir, "commondir"),
365
- path.join(worktreeGitDir, "gitdir"),
366
- path.join(worktreeGitDir, "config.worktree"),
367
- ];
368
- }
369
- function unique(values) {
370
- return [...new Set(values)];
371
- }