@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,4 +1,3 @@
1
- import { readFileSync } from "node:fs";
2
1
  import { createRequire } from "node:module";
3
2
  import path from "node:path";
4
3
  import { BUILD_SECRET_NAMES, hasPreLaunchEnv, } from "./config.js";
@@ -38,51 +37,6 @@ export function resolveSafehouseClearancePath(baseUrl = import.meta.url) {
38
37
  return path.resolve(path.dirname(clearancePackageJson), "safehouse", "safehouse-clearance");
39
38
  }
40
39
  const SAFEHOUSE_CLEARANCE_WRAPPER_PATH = resolveSafehouseClearancePath();
41
- /**
42
- * Resolve the `srt` CLI shipped by `@anthropic-ai/sandbox-runtime` (a pinned
43
- * groundcrew dependency) via Node's module resolution, reading the package's
44
- * `bin` field so the path survives version bumps that move the entry point.
45
- * The resolved `dist/cli.js` carries a `#!/usr/bin/env node` shebang and npm
46
- * marks it executable, so it is exec'd directly like the safehouse wrapper.
47
- *
48
- * @param baseUrl - **Test-only seam.** Production callers must omit this so the
49
- * helper resolves from this module's URL. Tests pass an invalid value to
50
- * exercise the catch branch.
51
- */
52
- export function resolveSrtBinPath(baseUrl = import.meta.url) {
53
- let packageJsonPath;
54
- try {
55
- packageJsonPath = createRequire(baseUrl).resolve("@anthropic-ai/sandbox-runtime/package.json");
56
- }
57
- catch (error) {
58
- throw new Error("@anthropic-ai/sandbox-runtime is required by @clipboard-health/groundcrew for the srt runner but could not be resolved. " +
59
- "Reinstall groundcrew's dependencies (`npm install`), or set local.runner to 'safehouse' or 'sdx'.", { cause: error });
60
- }
61
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- reading our pinned dependency's well-known `bin` field
62
- const manifest = JSON.parse(readFileSync(packageJsonPath, "utf8"));
63
- return path.resolve(path.dirname(packageJsonPath), srtBinEntry(manifest));
64
- }
65
- /**
66
- * Extract the `srt` entry from a package manifest's `bin` field, which npm
67
- * allows as either a bare string (single-bin packages) or a name→path map.
68
- */
69
- export function srtBinEntry(manifest) {
70
- const binEntry = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.["srt"];
71
- if (binEntry === undefined) {
72
- throw new Error("@anthropic-ai/sandbox-runtime package.json is missing the `srt` bin entry.");
73
- }
74
- return binEntry;
75
- }
76
- /**
77
- * Lazily resolved + memoized srt bin path. Unlike the safehouse wrapper (eager
78
- * at module load), this defers the `package.json` read until an srt launch is
79
- * actually built — srt is opt-in, so non-srt runs never pay for it.
80
- */
81
- let srtBinPathCache;
82
- function srtBinPath() {
83
- srtBinPathCache ??= resolveSrtBinPath();
84
- return srtBinPathCache;
85
- }
86
40
  function renderAgentCommand(arguments_) {
87
41
  return arguments_.agentCmd
88
42
  .replaceAll("{{worktree}}", shellSingleQuote(arguments_.worktreeDir))
@@ -137,20 +91,15 @@ function safehouseClearanceWrapperCommand() {
137
91
  function safehouseWrapperCommand(networkEgress) {
138
92
  return networkEgress === "allowlisted" ? safehouseClearanceWrapperCommand() : "safehouse";
139
93
  }
140
- function trapCleanupLine(promptDir) {
141
- const cleanupCmd = `rm -rf ${shellSingleQuote(promptDir)}`;
94
+ function trapCleanupLine({ promptDir, additionalCleanupCommands = [], }) {
95
+ const cleanupCmd = [`rm -rf ${shellSingleQuote(promptDir)}`, ...additionalCleanupCommands].join("; ");
142
96
  return `trap ${shellSingleQuote(cleanupCmd)} EXIT`;
143
97
  }
144
- /**
145
- * Shared head of every host-shell `&&` chain: arm the `EXIT` trap that wipes
146
- * `promptDir` (must come before any link that can fail, including the `cd`),
147
- * then `cd` into the working directory (the worktree root, or its `workdir`
148
- * subproject). Kept separate from secret sourcing so the safehouse path can
149
- * splice `preLaunch` between the `cd` and the secrets source — preLaunch must
150
- * never see build-time secrets in env.
151
- */
152
- function hostTrapAndCd(arguments_) {
153
- return [trapCleanupLine(arguments_.promptDir), `cd ${shellSingleQuote(arguments_.workingDir)}`];
98
+ function hostTrapAndCd({ workingDir, promptDir, additionalCleanupCommands = [], }) {
99
+ return [
100
+ trapCleanupLine({ promptDir, additionalCleanupCommands }),
101
+ `cd ${shellSingleQuote(workingDir)}`,
102
+ ];
154
103
  }
155
104
  /**
156
105
  * Optional source-of-secrets line. Returns `[]` when no `secretsFile` is
@@ -173,9 +122,9 @@ function preLaunchPromptAndExec(arguments_) {
173
122
  return lines;
174
123
  }
175
124
  /**
176
- * Shared by the safehouse, srt, and sdx builders: render the `exec <agent> "$@"`
125
+ * Shared by the safehouse and sdx builders: render the `exec <agent> "$@"`
177
126
  * inner command and the optional status-reported prepareWorktree hook. The
178
- * `{{sandbox}}` template is filled from `sandboxName` (empty for safehouse/srt).
127
+ * `{{sandbox}}` template is filled from `sandboxName` (empty for safehouse).
179
128
  */
180
129
  function renderPrepareAndAgentCommand(arguments_) {
181
130
  const agentCmd = renderAgentCommand({
@@ -193,7 +142,7 @@ function renderPrepareAndAgentCommand(arguments_) {
193
142
  };
194
143
  }
195
144
  /**
196
- * Shared host-shell prologue for the in-sandbox runners (safehouse, srt): when a
145
+ * Shared host-shell prologue for the safehouse runner: when a
197
146
  * `preLaunch` hook is present, scrub build-secret and `preLaunchEnv` names so it
198
147
  * cannot see them; then source build secrets, read the staged prompt into `$_p`,
199
148
  * and wipe the prompt dir. The caller arms the EXIT trap and `cd`s first.
@@ -254,7 +203,7 @@ export function isEnvironmentAssignment(token) {
254
203
  /**
255
204
  * Infer the agent's command basename from a agent `cmd` (skipping a leading
256
205
  * `env`/`KEY=val` prefix). Safehouse uses it to pick the matching `.sb`
257
- * profile; srt uses it to pick the agent's credential profile in `srtPolicy`.
206
+ * profile.
258
207
  */
259
208
  export function inferAgentCommandName(agentCmd) {
260
209
  const tokens = tokenizeShellPrefix(agentCmd);
@@ -331,9 +280,6 @@ function promptPositional(omitPromptArgument) {
331
280
  * prompt in hand.
332
281
  */
333
282
  export function buildLaunchCommand(arguments_) {
334
- if (arguments_.runner === "srt") {
335
- return buildSrtLaunchCommand(arguments_);
336
- }
337
283
  if (arguments_.runner === "sdx") {
338
284
  if (arguments_.definition.preLaunch !== undefined) {
339
285
  throw new Error("preLaunch is not yet supported for runner='sdx'. Set local.runner to 'safehouse' or 'none', or open an issue for sdx support.");
@@ -342,7 +288,7 @@ export function buildLaunchCommand(arguments_) {
342
288
  throw new Error("preLaunchEnv is not yet supported for runner='sdx'. Set local.runner to 'safehouse' or 'none', or open an issue for sdx support.");
343
289
  }
344
290
  if (arguments_.prepareWorktreeUnsandboxedCommand !== undefined) {
345
- throw new Error("unsandboxedHooks.prepareWorktree is not supported for runner='sdx': the sdx container has no host to run it on. Remove unsandboxedHooks for this repo, or use runner 'safehouse', 'srt', or 'none'.");
291
+ throw new Error("unsandboxedHooks.prepareWorktree is not supported for runner='sdx': the sdx container has no host to run it on. Remove unsandboxedHooks for this repo, or use runner 'safehouse' or 'none'.");
346
292
  }
347
293
  return buildSdxLaunchCommand(arguments_);
348
294
  }
@@ -465,10 +411,11 @@ function buildSafehouseLaunchCommand(arguments_) {
465
411
  // separates it from the next argv token. See `resolveSafehouseAddDirs` for
466
412
  // which paths these are and why.
467
413
  const safehousePrepareAddDirs = arguments_.safehouseAddDirs ?? [];
468
- const safehouseAgentAddDirs = uniqueStrings([
469
- ...safehousePrepareAddDirs,
470
- ...(arguments_.safehouseAgentAddDirs ?? []),
471
- ]);
414
+ const safehouseAgentAddDirs = resolveSafehouseAgentAddDirs({
415
+ prepareAddDirs: safehousePrepareAddDirs,
416
+ agentAddDirs: arguments_.safehouseAgentAddDirs,
417
+ integrationAddDirs: safehouseAgentIntegration?.addDirs,
418
+ });
472
419
  const safehouseAddDirsFlag = safehousePathListFlag("--add-dirs", safehousePrepareAddDirs);
473
420
  const safehouseAgentAddDirsFlag = safehousePathListFlag("--add-dirs", safehouseAgentAddDirs);
474
421
  const safehouseAgentAddDirsReadOnlyFlag = safehousePathListFlag("--add-dirs-ro", uniqueStrings([
@@ -479,13 +426,26 @@ function buildSafehouseLaunchCommand(arguments_) {
479
426
  // profile only — the repo-controlled prepareWorktree hook never needs them.
480
427
  const safehouseEnableFlag = safehouseEnableFeaturesFlag(arguments_.safehouseEnableFeatures ?? []);
481
428
  const safehouseWrapper = safehouseWrapperCommand(arguments_.networkEgress);
429
+ // Extra paths an integration staged for the agent wrap (e.g. codex's
430
+ // relocated CODEX_HOME) and wants removed alongside the shim dir. Rendered
431
+ // once and reused by both the trap (failure path) and the explicit
432
+ // post-wrap cleanup (success path) below, so the two stay in lockstep.
433
+ const { teardownCleanupCmd, writeBackCmd } = safehouseIntegrationCleanupCommands(safehouseAgentIntegration);
482
434
  // Defensive shim+promptDir trap: by the time we arm it, `rm -rf <promptDir>`
483
435
  // has already run (line below) so the promptDir wipe is a no-op on the happy
484
436
  // path. Keeps the failure-window between shim creation and the explicit
485
437
  // post-wrap cleanup covered for both targets without an unarmed window.
486
- const shimAndPromptCleanup = `rm -rf "$_safehouse_shim_dir"; rm -rf ${shellSingleQuote(promptDir)}`;
438
+ const shimAndPromptCleanup = safehouseShimAndPromptCleanup({
439
+ promptDir,
440
+ writeBackCmd,
441
+ teardownCleanupCmd,
442
+ });
487
443
  const shimAndPromptTrap = `trap ${shellSingleQuote(shimAndPromptCleanup)} EXIT`;
488
- const lines = hostTrapAndCd({ workingDir: arguments_.workingDir, promptDir });
444
+ const lines = hostTrapAndCd({
445
+ workingDir: arguments_.workingDir,
446
+ promptDir,
447
+ additionalCleanupCommands: teardownCleanupCmd === "" ? [] : [teardownCleanupCmd],
448
+ });
489
449
  // Scrub inherited env before preLaunch (build secrets are copied out of
490
450
  // `process.env`, which the launch shell inherits), then source secrets and
491
451
  // read the staged prompt. See `hostPreLaunchSourceAndReadPrompt`.
@@ -510,130 +470,79 @@ function buildSafehouseLaunchCommand(arguments_) {
510
470
  // Running the real launch chain as `sh -c` would make it see `sh`, so use
511
471
  // an agent-named symlink to /bin/sh. This preserves per-agent profile
512
472
  // selection without enabling every agent profile.
513
- `{ ${safehouseWrapper} ${safehouseAgentAddDirsFlag}${safehouseAgentAddDirsReadOnlyFlag}${safehouseEnableFlag}${agentEnvPassFlag}"$_safehouse_shim" -c ${shellSingleQuote(agentCommand)} sh${promptPositional(arguments_.omitPromptArgument)}; _safehouse_status=$?; rm -rf "$_safehouse_shim_dir"; trap - EXIT; exit "$_safehouse_status"; }`);
473
+ `{ ${safehouseWrapper} ${safehouseAgentAddDirsFlag}${safehouseAgentAddDirsReadOnlyFlag}${safehouseEnableFlag}${agentEnvPassFlag}"$_safehouse_shim" -c ${shellSingleQuote(agentCommand)} sh${promptPositional(arguments_.omitPromptArgument)}; _safehouse_status=$?; rm -rf "$_safehouse_shim_dir";${successPathCleanupSegment(writeBackCmd, teardownCleanupCmd)} trap - EXIT; exit "$_safehouse_status"; }`);
514
474
  return lines.join(" && ");
515
475
  }
516
- function safehousePathListFlag(flagName, paths) {
517
- return paths.length === 0 ? "" : `${flagName}=${shellSingleQuote(paths.join(":"))} `;
476
+ /**
477
+ * Extra paths an integration staged for the agent wrap (e.g. codex's
478
+ * relocated `CODEX_HOME`) rendered as a single `rm -rf` command, or `""` when
479
+ * the integration staged nothing (Claude) — the empty string keeps every
480
+ * caller's string interpolation a no-op rather than branching itself.
481
+ */
482
+ function safehouseTeardownCleanupCommand(teardownPaths) {
483
+ return teardownPaths.length === 0
484
+ ? ""
485
+ : `rm -rf ${teardownPaths.map((teardownPath) => shellSingleQuote(teardownPath)).join(" ")}`;
518
486
  }
519
- function safehouseEnableFeaturesFlag(features) {
520
- return features.length === 0 ? "" : `--enable=${shellSingleQuote(features.join(","))} `;
487
+ function safehouseIntegrationCleanupCommands(integration) {
488
+ return {
489
+ teardownCleanupCmd: safehouseTeardownCleanupCommand(uniqueStrings(integration?.teardownPaths ?? [])),
490
+ writeBackCmd: safehouseWriteBackCommand(integration?.writeBackFiles ?? []),
491
+ };
521
492
  }
522
- function uniqueStrings(values) {
523
- return [...new Set(values)];
493
+ function safehouseWriteBackCommand(writeBackFiles) {
494
+ return writeBackFiles
495
+ .map(({ baselinePath, stagedPath, sourcePath }) => {
496
+ const temporaryTemplate = shellSingleQuote(`${sourcePath}.groundcrew.XXXXXX`);
497
+ return [
498
+ `_groundcrew_write_back_tmp=$(/usr/bin/mktemp ${temporaryTemplate})`,
499
+ `if [ -n "$_groundcrew_write_back_tmp" ]; then if [ ! -L ${shellSingleQuote(stagedPath)} ] && [ -f ${shellSingleQuote(stagedPath)} ] && /bin/cp -P ${shellSingleQuote(stagedPath)} "$_groundcrew_write_back_tmp" && [ ! -L "$_groundcrew_write_back_tmp" ] && [ -f "$_groundcrew_write_back_tmp" ] && /bin/chmod 600 "$_groundcrew_write_back_tmp"; then if /usr/bin/cmp -s "$_groundcrew_write_back_tmp" ${shellSingleQuote(baselinePath)}; then :; elif ! /usr/bin/cmp -s ${shellSingleQuote(sourcePath)} ${shellSingleQuote(baselinePath)}; then echo 'groundcrew: skipped staged agent config write-back because the source changed concurrently' >&2; elif ! /bin/mv -f "$_groundcrew_write_back_tmp" ${shellSingleQuote(sourcePath)}; then echo 'groundcrew: failed to persist staged agent config' >&2; fi; else echo 'groundcrew: refused unsafe staged agent config write-back' >&2; fi; /bin/rm -f "$_groundcrew_write_back_tmp"; else echo 'groundcrew: failed to create staged agent config write-back file' >&2; fi`,
500
+ "unset _groundcrew_write_back_tmp",
501
+ ].join("; ");
502
+ })
503
+ .join("; ");
524
504
  }
525
505
  /**
526
- * Benign baseline env the srt wraps run under (via `env -i`). This is an
527
- * allowlist on purpose: srt's CLI spawns its child with the *inherited* host
528
- * env, so — unlike safehouse (`--env=FILE` sanitized baseline) and sdx (clean
529
- * container env) — ambient secrets in the launch shell would otherwise reach
530
- * the agent and bypass the filesystem read mask. A denylist can't enumerate
531
- * every secret var, so we clear the env and re-add only these known-benign
532
- * names plus per-wrap forwarded vars (build secrets / `preLaunchEnv`). Values
533
- * are read from the host shell at runtime. Vars unset on the host become empty
534
- * (harmless for these). srt injects proxy + TMPDIR vars into the child itself.
506
+ * The EXIT-trap command that wipes the profile-shim dir, the prompt dir, and
507
+ * any integration teardown paths — the failure-path cleanup. Shared with the
508
+ * explicit success-path cleanup below it (`successPathTeardownSegment`) so
509
+ * both paths remove the same set of staged paths.
535
510
  */
536
- const SRT_ENV_BASELINE = [
537
- "PATH",
538
- "HOME",
539
- "USER",
540
- "LOGNAME",
541
- "SHELL",
542
- "TERM",
543
- "COLORTERM",
544
- "LANG",
545
- "LC_ALL",
546
- "LC_CTYPE",
547
- "TZ",
548
- "PWD",
549
- ];
550
- /** Render `env -i` forwarded assignments: ` NAME="$NAME"` per name (value taken from the host shell at runtime). */
551
- function srtForwardedEnv(names) {
552
- return names.map((name) => ` ${name}="$${name}"`).join("");
511
+ function safehouseShimAndPromptCleanup(input) {
512
+ return [
513
+ `rm -rf "$_safehouse_shim_dir"`,
514
+ `rm -rf ${shellSingleQuote(input.promptDir)}`,
515
+ ...(input.writeBackCmd === "" ? [] : [input.writeBackCmd]),
516
+ ...(input.teardownCleanupCmd === "" ? [] : [input.teardownCleanupCmd]),
517
+ ].join("; ");
518
+ }
519
+ /** Success-path counterpart to the EXIT-trap integration cleanup. */
520
+ function successPathCleanupSegment(writeBackCmd, teardownCleanupCmd) {
521
+ const commands = [writeBackCmd, teardownCleanupCmd].filter((command) => command !== "");
522
+ return commands.length === 0 ? "" : ` ${commands.join("; ")};`;
553
523
  }
554
524
  /**
555
- * srt launch. Two srt wraps mirror the safehouse two-wrap design, so the
556
- * dependency/build phase is sandboxed too — not just the agent:
557
- *
558
- * 1. **prepareWorktree wrap**: `env -i <baseline+secrets> srt --settings <file> sh -c '<hook>'`.
559
- * 2. **Agent wrap**: `env -i <baseline+preLaunchEnv> srt --settings <file> sh -c 'exec <agent> "$@"' sh "$_p"`.
560
- *
561
- * Unlike safehouse there is no profile-by-basename selection, so no symlink
562
- * shim is needed — srt takes the policy as an explicit `--settings` file. Each
563
- * srt invocation runs under `env -i` (see `SRT_ENV_BASELINE`) so the agent gets
564
- * a sanitized env instead of the inherited host env: the prepareWorktree wrap
565
- * additionally forwards build secrets (for `npm ci` auth) and the agent wrap
566
- * forwards the user's opt-in `preLaunchEnv` names — matching safehouse's
567
- * sanitized baseline plus explicit pass-list posture. `preLaunch` and secret
568
- * sourcing still run on the host shell (full env), exactly as under safehouse.
569
- *
570
- * The settings file lives in `srtSettingsDir` (a dedicated temp dir, never the
571
- * prompt dir, which is wiped before the agent execs). The EXIT trap covers both
572
- * the settings dir and the prompt dir for every failure window; the happy path
573
- * removes the prompt dir before the agent wrap and the settings dir after it.
525
+ * Writable `--add-dirs` for the agent wrap: the shared prepare grants plus the
526
+ * task-source write paths plus any integration-staged dirs (e.g. codex's
527
+ * relocated `CODEX_HOME`), deduped. Kept out of `buildSafehouseLaunchCommand`
528
+ * so its per-source nullish handling doesn't count against that function's
529
+ * complexity budget.
574
530
  */
575
- function buildSrtLaunchCommand(arguments_) {
576
- if (arguments_.srtPrepareSettingsFile === undefined ||
577
- arguments_.srtAgentSettingsFile === undefined ||
578
- arguments_.srtSettingsDir === undefined) {
579
- throw new Error("buildLaunchCommand: runner='srt' requires srtPrepareSettingsFile, srtAgentSettingsFile, and srtSettingsDir (generate them with buildSrtSettings + stageSrtSettings before calling).");
580
- }
581
- const promptDir = path.dirname(arguments_.promptFile);
582
- const { agentCommand, prepareWorktreeCommand } = renderPrepareAndAgentCommand(arguments_);
583
- const srtBin = shellSingleQuote(srtBinPath());
584
- // Distinct policies per wrap: the prepareWorktree hook is repo-controlled, so
585
- // it runs under the profile-neutral settings (no agent credential grants);
586
- // only the agent wrap gets the full agent policy.
587
- //
588
- // The trailing `--` is load-bearing: srt's CLI (commander) has its own `-c`,
589
- // `--settings`, `--debug`, and `--control-fd` options. Without `--`, srt would
590
- // capture the child's `sh -c '<cmd>'` as its OWN `-c` (dropping the trailing
591
- // prompt positionals entirely), and an option-looking prompt/hook value could
592
- // mutate srt's options (e.g. redirect `--settings`). `--` ends srt option
593
- // parsing so everything after is the child argv.
594
- const prepareTarget = `${srtBin} --settings ${shellSingleQuote(arguments_.srtPrepareSettingsFile)} --`;
595
- const agentTarget = `${srtBin} --settings ${shellSingleQuote(arguments_.srtAgentSettingsFile)} --`;
596
- // `env -i <baseline>` drops the inherited host env; each wrap re-adds only the
597
- // benign baseline plus its forwarded names (`VAR="$VAR"` — value from the host
598
- // shell at runtime; the names are safe identifiers, validated for
599
- // preLaunchEnv). env -i isolates the agent wrap from build secrets, so no
600
- // `unset` dance between wraps is needed.
601
- const baseline = SRT_ENV_BASELINE.map((name) => `${name}="$${name}"`).join(" ");
602
- const prepareForward = arguments_.secretsFile === undefined ? "" : srtForwardedEnv(BUILD_SECRET_NAMES);
603
- const prepareWrap = `env -i ${baseline}${prepareForward} ${prepareTarget}`;
604
- // The relocated config-home env (e.g. CODEX_HOME) is an explicit value, not a
605
- // `VAR="$VAR"` host passthrough — groundcrew computed the staged path, it is
606
- // not in the launch shell's env. The name is a fixed identifier; the value is
607
- // single-quoted. Only the agent wrap gets it.
608
- const agentConfigDirAssignment = arguments_.srtAgentConfigDirEnv === undefined
609
- ? ""
610
- : ` ${arguments_.srtAgentConfigDirEnv.name}=${shellSingleQuote(arguments_.srtAgentConfigDirEnv.value)}`;
611
- const agentWrap = `env -i ${baseline}${agentConfigDirAssignment}${srtForwardedEnv([
612
- ...(arguments_.definition.preLaunchEnv ?? []),
613
- ...workerEnvironmentNames(arguments_.workerEnvironment),
614
- ])} ${agentTarget}`;
615
- // One EXIT trap wipes both the settings dir and the prompt dir, covering
616
- // every failure window between here and the post-wrap cleanup.
617
- const cleanup = `rm -rf ${shellSingleQuote(arguments_.srtSettingsDir)}; rm -rf ${shellSingleQuote(promptDir)}`;
618
- const lines = [
619
- `trap ${shellSingleQuote(cleanup)} EXIT`,
620
- `cd ${shellSingleQuote(arguments_.workingDir)}`,
621
- ...hostPreLaunchSourceAndReadPrompt({
622
- definition: arguments_.definition,
623
- worktreeDir: arguments_.worktreeDir,
624
- promptFile: arguments_.promptFile,
625
- promptDir,
626
- secretsFile: arguments_.secretsFile,
627
- }),
628
- ];
629
- if (arguments_.prepareWorktreeUnsandboxedCommand !== undefined) {
630
- lines.push(hostPrepareWorktreeLine(arguments_.prepareWorktreeUnsandboxedCommand, arguments_.definition.preLaunchEnv ?? []));
631
- }
632
- if (prepareWorktreeCommand !== undefined) {
633
- lines.push(`${prepareWrap} sh -c ${shellSingleQuote(prepareWorktreeCommand)}`);
634
- }
635
- lines.push(...workerEnvironmentExports(arguments_.workerEnvironment), `{ ${agentWrap} sh -c ${shellSingleQuote(agentCommand)} sh${promptPositional(arguments_.omitPromptArgument)}; _srt_status=$?; rm -rf ${shellSingleQuote(arguments_.srtSettingsDir)}; trap - EXIT; exit "$_srt_status"; }`);
636
- return lines.join(" && ");
531
+ function resolveSafehouseAgentAddDirs(input) {
532
+ return uniqueStrings([
533
+ ...input.prepareAddDirs,
534
+ ...(input.agentAddDirs ?? []),
535
+ ...(input.integrationAddDirs ?? []),
536
+ ]);
537
+ }
538
+ function safehousePathListFlag(flagName, paths) {
539
+ return paths.length === 0 ? "" : `${flagName}=${shellSingleQuote(paths.join(":"))} `;
540
+ }
541
+ function safehouseEnableFeaturesFlag(features) {
542
+ return features.length === 0 ? "" : `--enable=${shellSingleQuote(features.join(","))} `;
543
+ }
544
+ function uniqueStrings(values) {
545
+ return [...new Set(values)];
637
546
  }
638
547
  function buildSdxLaunchCommand(arguments_) {
639
548
  /* v8 ignore next 5 @preserve -- setupWorkspace passes sandboxName + sandbox config when picking sdx; missing fields are programmer errors */
@@ -659,7 +568,7 @@ function buildSdxLaunchCommand(arguments_) {
659
568
  const sbxEnvironmentFlags = sbxEnvironmentNames.length === 0
660
569
  ? ""
661
570
  : `${sbxEnvironmentNames.map((name) => `-e ${name}`).join(" ")} `;
662
- const lines = [trapCleanupLine(promptDir)];
571
+ const lines = [trapCleanupLine({ promptDir })];
663
572
  if (arguments_.secretsFile !== undefined) {
664
573
  lines.push(sourceSecretsLine(arguments_.secretsFile));
665
574
  }
@@ -3,11 +3,10 @@ import type { HostCapabilities } from "./host.ts";
3
3
  /**
4
4
  * Resolve `local.runner` from config + host capabilities into a concrete
5
5
  * backend. `auto` defaults to safehouse on macOS and sdx on Linux — both
6
- * are the deny-first paths for their platform. `srt`, `none`, and the
7
- * explicit names pass through unchanged so users always get exactly what
8
- * they asked for; `srt` is therefore opt-in only and never picked by
9
- * `auto` in this phase. Pure: takes everything it needs as arguments so
10
- * the dispatcher can test platform pivots without touching real hosts.
6
+ * are the deny-first paths for their platform. `none` and the explicit names
7
+ * pass through unchanged so users always get exactly what they asked for.
8
+ * Pure: takes everything it needs as arguments so the dispatcher can test
9
+ * platform pivots without touching real hosts.
11
10
  */
12
11
  export declare function resolveLocalRunner(setting: LocalRunnerSetting, host: HostCapabilities): LocalRunner;
13
12
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"localRunner.d.ts","sourceRoot":"","sources":["../../src/lib/localRunner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAGlD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,gBAAgB,GACrB,WAAW,CAQb;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI,CAqD/F"}
1
+ {"version":3,"file":"localRunner.d.ts","sourceRoot":"","sources":["../../src/lib/localRunner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAGlD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,gBAAgB,GACrB,WAAW,CAQb;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI,CA+B/F"}
@@ -2,11 +2,10 @@ import { log, styleWarning } from "./util.js";
2
2
  /**
3
3
  * Resolve `local.runner` from config + host capabilities into a concrete
4
4
  * backend. `auto` defaults to safehouse on macOS and sdx on Linux — both
5
- * are the deny-first paths for their platform. `srt`, `none`, and the
6
- * explicit names pass through unchanged so users always get exactly what
7
- * they asked for; `srt` is therefore opt-in only and never picked by
8
- * `auto` in this phase. Pure: takes everything it needs as arguments so
9
- * the dispatcher can test platform pivots without touching real hosts.
5
+ * are the deny-first paths for their platform. `none` and the explicit names
6
+ * pass through unchanged so users always get exactly what they asked for.
7
+ * Pure: takes everything it needs as arguments so the dispatcher can test
8
+ * platform pivots without touching real hosts.
10
9
  */
11
10
  export function resolveLocalRunner(setting, host) {
12
11
  if (setting !== "auto") {
@@ -38,24 +37,6 @@ export function assertLocalRunnerRequirements(host, runner) {
38
37
  }
39
38
  return;
40
39
  }
41
- if (runner === "srt") {
42
- if (!host.isSrtSupported) {
43
- throw new Error("Local groundcrew runs with the srt runner require macOS or Linux/WSL. Set local.runner to 'auto' to pick the platform default.");
44
- }
45
- if (host.isLinux) {
46
- const missing = [
47
- host.hasBubblewrap ? undefined : "bubblewrap",
48
- host.hasSocat ? undefined : "socat",
49
- host.hasRipgrep ? undefined : "ripgrep (rg)",
50
- ].filter((name) => name !== undefined);
51
- if (missing.length > 0) {
52
- throw new Error(`Local groundcrew runs with the srt runner on Linux require ${missing.join(", ")} on PATH. ` +
53
- "Install the missing dependencies (Debian/Ubuntu: `apt install bubblewrap socat ripgrep`). " +
54
- "On Ubuntu 24.04+ you may also need `sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0` to allow unprivileged user namespaces.");
55
- }
56
- }
57
- return;
58
- }
59
40
  if (runner === "sdx") {
60
41
  if (!host.isSdxSupported) {
61
42
  throw new Error("Local groundcrew runs with the sdx runner require macOS or Linux.");
@@ -314,7 +314,7 @@ and hook contract.
314
314
  | Key | Default | What it does |
315
315
  | ----------------------------------------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
316
316
  | `sources` | `[]` | Additional pluggable task sources, dispatched alongside the built-in Linear adapter. Built-in kinds: `shell`, `linear`. Declare `{ kind: "linear", team: "ENG", statuses: { ... } }` to configure Linear task creation's default team and/or override Linear status names used for `in-progress` / `in-review` disambiguation. Disable the implicit Linear source with `{ kind: "linear", enabled: false }` (no API key required) — useful for shell-only setups. |
317
- | `sources[].sandboxWritePaths` | optional | Shell sources only. Local directories that the `safehouse` and `srt` sandbox runners open for read and write while an agent works a task from that source. Use this when a shell-backed task store lives outside the worktree, such as `~/plans`. Paths may be absolute, relative to the agent working directory, or `~`-prefixed. Not supported by the `sdx` runner. |
317
+ | `sources[].sandboxWritePaths` | optional | Shell sources only. Local directories that the `safehouse` sandbox runner opens for read and write while an agent works a task from that source. Use this when a shell-backed task store lives outside the worktree, such as `~/plans`. Paths may be absolute, relative to the agent working directory, or `~`-prefixed. Not supported by the `sdx` runner. |
318
318
  | `git.remote` | `"origin"` | Remote used for `fetch` and as the worktree base ref. |
319
319
  | `git.defaultBranch` | `"main"` | Branch fetched from `git.remote` and used as the worktree base. |
320
320
  | `git.branchPrefix` | OS username | Prefix groundcrew puts before the task id when naming a worktree branch (`<branchPrefix>-<task>`). Must be a slash-free slug of letters, digits, `.`, `_`, or `-`. Defaults to the OS account username. Changing it only affects newly created worktrees; existing local branches keep their original names until cleaned up. Prefer a per-user config for personal prefixes — a committed `git.branchPrefix` gives every contributor the same branch prefix. |
@@ -340,6 +340,6 @@ and hook contract.
340
340
  | `prompts.promptFile` | optional | Path to a UTF-8 file whose contents become `prompts.initial`, read at load time. Resolved relative to the config file's directory; `~` is expanded and absolute paths are used as-is. The JSON-friendly alternative to inlining a large prompt or `readFileSync`. Mutually exclusive with `prompts.initial`. |
341
341
  | `workspaceKind` | `"auto"` | Terminal session manager. `"auto"` picks `cmux` when on PATH, else `tmux`. Set to `"cmux"`, `"tmux"`, or `"zellij"` to fail loudly when the chosen backend is missing. |
342
342
  | `local.runner` | `"auto"` | Local isolation backend. `"auto"` uses `safehouse` on macOS and `sdx` on Linux/WSL. Explicit: `"safehouse"`, `"sdx"`, `"none"`. `"none"` is never picked implicitly. |
343
- | `local.networkEgress` | `"allowlisted"` | Network egress posture. With `local.runner: "safehouse"`, `"allowlisted"` wraps with Clearance and `"open"` keeps the Safehouse filesystem sandbox while opening network egress. Ignored by `srt`, `sdx`, and `none`; `srt` keeps enforcing its own allowlist. |
344
- | `local.readOnlyDirs` | `~/.config/tfenv` | Host directories re-opened read-only inside the sandbox (`safehouse` + `srt`), for toolchains the sandbox profile masks but does not re-open. `~` is expanded. Defaults to tfenv's config root so `terraform`/`tfenv` work in the sandbox; set your own list to add or replace entries. Ignored by `sdx` and `none`. |
343
+ | `local.networkEgress` | `"allowlisted"` | Network egress posture. With `local.runner: "safehouse"`, `"allowlisted"` wraps with Clearance and `"open"` keeps the Safehouse filesystem sandbox while opening network egress. Ignored by `sdx` and `none`. |
344
+ | `local.readOnlyDirs` | `~/.config/tfenv` | Host directories re-opened read-only inside the Safehouse sandbox, for toolchains the profile masks but does not re-open. `~` is expanded. Defaults to tfenv's config root so `terraform`/`tfenv` work in the sandbox; set your own list to add or replace entries. Ignored by `sdx` and `none`. |
345
345
  | `logging.file` | XDG state path | Append-mode log file. `log()` / `logEvent()` tee here in addition to stdout. Defaults to `${XDG_STATE_HOME:-$HOME/.local/state}/groundcrew/groundcrew.log`. |
package/docs/runners.md CHANGED
@@ -2,14 +2,11 @@
2
2
 
3
3
  `local.runner` picks the local isolation backend. `auto` resolves per platform.
4
4
 
5
- | Runner | Default on | Backend |
6
- | ----------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7
- | `safehouse` | macOS | [Safehouse](https://agent-safehouse.dev/) — fastest local; cannot safely give the agent Docker. |
8
- | `srt` | — (opt-in) | [Anthropic sandbox-runtime](https://github.com/anthropic-experimental/sandbox-runtime) — fast, non-Docker, on macOS **and** Linux/WSL. Built-in network + filesystem policy. |
9
- | `sdx` | Linux / WSL | [Docker Sandboxes](https://docs.docker.com/sandboxes/) (`sbx`) — required when the agent needs `docker`. |
10
- | `none` | — | Unsandboxed escape hatch. Never picked implicitly; doctor warns when configured. |
11
-
12
- `auto` never resolves to `srt` — opt in explicitly with `local.runner: "srt"`.
5
+ | Runner | Default on | Backend |
6
+ | ----------- | ----------- | -------------------------------------------------------------------------------------------------------- |
7
+ | `safehouse` | macOS | [Safehouse](https://agent-safehouse.dev/) — fastest local; cannot safely give the agent Docker. |
8
+ | `sdx` | Linux / WSL | [Docker Sandboxes](https://docs.docker.com/sandboxes/) (`sbx`) — required when the agent needs `docker`. |
9
+ | `none` | — | Unsandboxed escape hatch. Never picked implicitly; doctor warns when configured. |
13
10
 
14
11
  ## Safehouse Clearance Allowlist
15
12
 
@@ -52,54 +49,14 @@ This is for the case where the allowlist is more friction than the egress restri
52
49
  Scope and limits:
53
50
 
54
51
  - **safehouse only.** It applies to both groundcrew-composed Safehouse wraps (the `prepareWorktree` wrap and the agent wrap).
55
- - **Ignored by `srt` / `sdx` / `none`.** The other runners ignore it, so you can leave `networkEgress` set while switching `local.runner`. Note that `srt` enforces its own network allowlist (`CLEARANCE_ALLOW_HOSTS` / `allowedDomains`) regardless: to actually open egress, use the `safehouse` runner.
52
+ - **Ignored by `sdx` / `none`.** The other runners ignore it, so you can leave `networkEgress` set while switching `local.runner`.
56
53
  - **No additional effect when `cmd` already starts with `safehouse`:** that command owns its own wrap, so groundcrew injects nothing. Groundcrew-managed setup/resume launches still reject cmd-owned Safehouse wraps because worker self-completion env cannot be injected.
57
54
 
58
- ## srt (Anthropic sandbox-runtime)
59
-
60
- `srt` is the fast, non-Docker option that works on **both macOS and Linux/WSL** — the gap `safehouse` (macOS-only) and `sdx` (Docker) leave. It replaces both Safehouse and Clearance in one tool: a sandbox engine (`sandbox-exec` on macOS, `bubblewrap` on Linux) plus a built-in proxy network allowlist. It is opt-in while it stabilizes:
61
-
62
- ```bash
63
- crew run --watch # with local.runner: "srt" in crew.config.ts
64
- ```
65
-
66
- > **Beta.** `@anthropic-ai/sandbox-runtime` is an Anthropic research preview; its config format may evolve. groundcrew pins the version it ships with, so upgrades are deliberate.
67
-
68
- Groundcrew generates a per-launch policy itself (Safehouse's `.sb` profiles have no equivalent here):
69
-
70
- - **Reads**: the home region (`/Users` on macOS, `/home`+`/root`+`/mnt` on Linux — `/mnt` covers WSL's Windows drive mounts) is denied, then the worktree, the repo's git metadata, the language toolchains needed to run the agent, and the agent's own config dirs (`~/.claude`, `~/.codex`, …) are re-opened. On macOS the user keychain dir (`~/Library/Keychains`) is also re-opened read-only so keychain-authenticated agents (claude) can sign in under the home mask. The agent cannot read `~/.ssh`, `~/.aws`, shell history, or unrelated repos.
71
- - **Writes**: allow-only, and the host-CLI persistence vector (planting hooks, `mcpServers`, `commands/`, `plugins/`, … that run on the user's next host invocation) is closed per agent. **claude** keeps a writable `~/.claude` (its Bash tool needs scratch/session state there) but every fixed-path executable/instruction surface — `~/.claude.json` (`mcpServers`), `settings.json` and its hooks, `commands/`, `agents/`, `plugins/`, `skills/`, `statusline.sh`, `CLAUDE.md`, the bundled `chrome` binary, `.git/{hooks,config}` — is denied; claude tolerates those write denials. **codex** hard-fails with a read-only home, so it is pointed at a per-launch relocated config dir (`CODEX_HOME`) seeded with its credentials, leaving the real `~/.codex` entirely unwritten. The git common dir is granted as a **narrow allowlist** of only what `status/diff/add/commit/push/gc` write (`objects`, `refs`, `logs`, `packed-refs`, this worktree's gitdir, …) — never wholesale, so the repo `config`/`hooks`, the per-worktree gitdir redirection files, and **sibling worktree gitdirs** stay unwritable. Global toolchain bins (`~/.cargo/bin`, global `node_modules`, the npx cache, …) are never writable either.
72
- - **Environment**: each `srt` invocation runs under a sanitized env (`env -i` + a benign baseline). Unlike safehouse and sdx, the `srt` CLI inherits the host env, so without this an ambient `AWS_*`, `GITHUB_TOKEN`, etc. would reach the agent and bypass the read mask. Credentials the agent legitimately needs from the environment must be forwarded explicitly via the agent's `preLaunchEnv` (the same opt-in pass-list safehouse uses).
73
- - **Network**: allow-only, **reused from the same Clearance allowlist** (`CLEARANCE_ALLOW_HOSTS` / `CLEARANCE_ALLOW_HOSTS_FILES`, including the shipped `clearance-allow-hosts`) so there is one source of truth. Local binding and unix sockets stay off (never the Docker socket).
74
- - **Built-in credential profiles**: the per-agent read/write/deny profiles above ship for `claude`, `codex`, and `cursor-agent` (the `cursor` / `cursor-grok` presets). Unknown agents get no extra home access.
75
-
76
- ### Linux / WSL prerequisites
77
-
78
- Install the srt runtime dependencies:
79
-
80
- ```bash
81
- # Debian / Ubuntu
82
- sudo apt install bubblewrap socat ripgrep
83
- ```
84
-
85
- On **Ubuntu 24.04+**, unprivileged user namespaces are restricted by AppArmor; enable them once:
86
-
87
- ```bash
88
- sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
89
- ```
90
-
91
- `crew doctor` checks these and prints what is missing. macOS needs no extra dependencies — the `srt` CLI ships with groundcrew.
92
-
93
- ### Debugging sandbox denials
94
-
95
- - **macOS**: `log stream --predicate 'process == "sandbox-exec"' --style syslog`
96
- - **Linux**: bubblewrap has no built-in violation log; trace the failing command with `strace -f -e trace=file <cmd>` to find the blocked path, then widen the policy.
97
-
98
55
  ## Docker Sandboxes Setup
99
56
 
100
57
  `sdx` does not support `unsandboxedHooks`. The sdx container has no
101
58
  host to run it on; configuring it for an `sdx`-runner repo is a launch-time
102
- config error. Use `safehouse`, `srt`, or `none` if you need host-side setup.
59
+ config error. Use `safehouse` or `none` if you need host-side setup.
103
60
 
104
61
  Each agent that runs under `sdx` needs a `sandbox: { agent: "<sbx-agent>" }` block in `crew.config.ts`. Groundcrew addresses the sandbox as `groundcrew-<agent>` and reuses one existing sandbox per agent across repos and tasks.
105
62
 
@@ -127,7 +127,7 @@ Grant it only to repositories you trust to run arbitrary code on the host.
127
127
  (top-level or under `hooks`) is a hard config error.
128
128
  - **Per-repository only.** There is no `defaults.unsandboxedHooks`;
129
129
  host execution is never a fleet-wide default.
130
- - **Runner support.** Runs on the host for `safehouse`, `srt`, and `none`. The
130
+ - **Runner support.** Runs on the host for `safehouse` and `none`. The
131
131
  `sdx` runner rejects it at launch — a container has no host to run it on.
132
132
  - **Credentials.** Build secrets are available so `npm`/`bundle` can
133
133
  authenticate; the agent's `preLaunchEnv` names are scrubbed so the command
@@ -58,7 +58,7 @@ With no working implementation to validate against (green-on-v1 dropped), the ha
58
58
  ### 1.6 Lanes
59
59
 
60
60
  - **Core lane**: hermetic, no sandbox runner, runs anywhere including CI.
61
- - **Sandbox lane**: real srt (macOS `sandbox-exec` / Linux bubblewrap), platform-gated; asserts denial behavior that can't be faked honestly. v2-only by definition — v1's posture keeps its existing unit-level coverage.
61
+ - **Sandbox lane**: real platform runner, platform-gated; asserts denial behavior that can't be faked honestly. v2-only by definition — v1's posture keeps its existing unit-level coverage.
62
62
 
63
63
  ### 1.7 Suite packaging
64
64