@clipboard-health/groundcrew 4.31.0 → 4.32.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/README.md CHANGED
@@ -50,8 +50,8 @@ crew init --global --project-dir ~/dev --repo OWNER/REPO --agent claude
50
50
 
51
51
  # 3. Run the clone commands printed by `crew init`.
52
52
 
53
- # 4. Set the clearance egress proxy allowlist.
54
- export CLEARANCE_ALLOW_HOSTS_FILES="$(npm root -g)/@clipboard-health/groundcrew/clearance-allow-hosts"
53
+ # 4. Safehouse runs use groundcrew's bundled clearance allowlist automatically.
54
+ # Add extra hosts later via CLEARANCE_ALLOW_HOSTS or CLEARANCE_ALLOW_HOSTS_FILES.
55
55
 
56
56
  # 5. Using Linear? Export your API key. (Jira and other trackers: see Task Pickup.)
57
57
  export GROUNDCREW_LINEAR_API_KEY="lin_api_..."
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA+KH,wBAAsB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CAmF/C"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAyLH,wBAAsB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CAmF/C"}
@@ -7,6 +7,7 @@ import { createBoard } from "../lib/board.js";
7
7
  import { buildSources, sourcesFromConfig } from "../lib/buildSources.js";
8
8
  import { loadConfigWithSource, worktreeBaseDir, } from "../lib/config.js";
9
9
  import { detectHostCapabilities, which } from "../lib/host.js";
10
+ import { isEnvironmentAssignment } from "../lib/launchCommand.js";
10
11
  import { resolveLocalRunner } from "../lib/localRunner.js";
11
12
  import { gatedAgents } from "../lib/usage.js";
12
13
  import { errorMessage, writeOutput } from "../lib/util.js";
@@ -78,12 +79,14 @@ function checkDir(path, label) {
78
79
  * the executable name (first non-flag token), and any subsequent
79
80
  * non-flag, non-flag-value token until a flag is hit. Flag tokens are
80
81
  * dropped along with the token immediately following them (treated as
81
- * the flag's value).
82
+ * the flag's value). `env VAR=val` assignments are skipped (they are not
83
+ * binaries), so the wrapped command is still found.
82
84
  *
83
85
  * Examples:
84
86
  * "safehouse claude --permission-mode auto" → ["safehouse", "claude"]
85
87
  * "claude" → ["claude"]
86
88
  * "node --inspect script.ts" → ["node"] (script.ts skipped — flag value)
89
+ * "env GIT_CONFIG_COUNT=1 claude --foo" → ["env", "claude"] (assignment skipped)
87
90
  */
88
91
  function commandTokensToCheck(cmd) {
89
92
  const parts = cmd.trim().split(/\s+/);
@@ -101,6 +104,13 @@ function commandTokensToCheck(cmd) {
101
104
  index += 2;
102
105
  continue;
103
106
  }
107
+ if (isEnvironmentAssignment(token)) {
108
+ // An `env VAR=val …` prefix: the assignment is not a binary, so don't
109
+ // probe it on PATH. Mirrors the launch path's inferAgentCommandName,
110
+ // which skips assignments to find the real agent command.
111
+ index += 1;
112
+ continue;
113
+ }
104
114
  result.push(token);
105
115
  if (result.length >= MAX_TOKENS_PER_CMD) {
106
116
  break;
@@ -1 +1 @@
1
- {"version":3,"file":"agentLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/agentLaunch.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,aAAa,CAAC;AASrB;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,eAAe,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,sBAAsB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC,GAAG;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAwBhE;AAED,UAAU,mBAAmB;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,eAAe,CAAC;IAC5B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA+C/B;AAqBD,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAUhB"}
1
+ {"version":3,"file":"agentLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/agentLaunch.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,aAAa,CAAC;AASrB;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,eAAe,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,sBAAsB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC,GAAG;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CA0BhE;AAsBD,UAAU,mBAAmB;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,eAAe,CAAC;IAC5B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA+C/B;AAwBD,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAUhB"}
@@ -1,10 +1,11 @@
1
1
  import { ensureClearance } from "@clipboard-health/clearance";
2
+ import { clearanceAllowHostsFilesFromEnvironment } from "./clearanceAllowlist.js";
2
3
  import { hasPreLaunchEnv, } from "./config.js";
3
4
  import { detectHostCapabilities } from "./host.js";
4
5
  import { buildLaunchCommand } from "./launchCommand.js";
5
6
  import { assertLocalRunnerRequirements, resolveLocalRunner } from "./localRunner.js";
6
7
  import { sandboxNameFor } from "./sandboxName.js";
7
- import { buildAndStageSrtLaunch } from "./srtLaunch.js";
8
+ import { buildAndStageSrtLaunch, resolveGitCommonDir } from "./srtLaunch.js";
8
9
  import { debug, sleep } from "./util.js";
9
10
  import { workspaces } from "./workspaces.js";
10
11
  /**
@@ -35,9 +36,29 @@ export function composeAgentLaunch(input) {
35
36
  srtAgentSettingsFile: staged?.agentFile,
36
37
  srtSettingsDir: staged?.directory,
37
38
  srtAgentConfigDirEnv: staged?.agentConfigDirEnv,
39
+ safehouseAddDirs: input.runner === "safehouse" ? resolveSafehouseAddDirs(input.worktreeDir) : undefined,
38
40
  });
39
41
  return { launchCommand, srtSettingsDir: staged?.directory };
40
42
  }
43
+ /**
44
+ * Filesystem paths the safehouse sandbox must be granted (read/write) beyond
45
+ * its automatic cwd grant, so git works for every worktree shape:
46
+ *
47
+ * - `worktreeDir` — the checkout root. A `workdir` subproject cwd's into a
48
+ * subdir, so without this the worktree-root `.git` gitfile is unreachable.
49
+ * - the **git common dir** — resolved from the worktree itself (not assumed to
50
+ * be `<projectDir>/<repo>/.git`), so a scripted/sparse-checkout worktree
51
+ * whose store lives outside the worktree tree (e.g. graft's `~/carrot/.git`)
52
+ * gets git access. This is the path the bare cwd grant fundamentally cannot
53
+ * cover, and the reason this resolution exists.
54
+ * Gated to the safehouse runner at the call site (srt fences its own equivalent
55
+ * surface — worktree root + git common dir — through its settings file; sdx/none
56
+ * don't use it). Deduped defensively in case git resolves either path to the
57
+ * same directory in an unusual checkout shape.
58
+ */
59
+ function resolveSafehouseAddDirs(worktreeDir) {
60
+ return [...new Set([worktreeDir, resolveGitCommonDir(worktreeDir)])];
61
+ }
41
62
  export async function prepareAgentLaunch(input) {
42
63
  const host = await detectHostCapabilities(input.signal);
43
64
  const runner = resolveLocalRunner(input.config.local.runner, host);
@@ -77,6 +98,9 @@ async function alreadyReady() {
77
98
  }
78
99
  async function ensureSafehouseClearance(signal) {
79
100
  await ensureClearance({
101
+ envOverrides: {
102
+ CLEARANCE_ALLOW_HOSTS_FILES: clearanceAllowHostsFilesFromEnvironment(),
103
+ },
80
104
  logger: debug,
81
105
  ...(signal === undefined
82
106
  ? {}
@@ -0,0 +1,8 @@
1
+ interface ClearanceAllowHostsFilesInput {
2
+ defaultFile?: string | undefined;
3
+ existingFiles?: string | undefined;
4
+ }
5
+ export declare function clearanceAllowHostsFilesValue(input?: ClearanceAllowHostsFilesInput): string;
6
+ export declare function clearanceAllowHostsFilesFromEnvironment(): string;
7
+ export {};
8
+ //# sourceMappingURL=clearanceAllowlist.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clearanceAllowlist.d.ts","sourceRoot":"","sources":["../../src/lib/clearanceAllowlist.ts"],"names":[],"mappings":"AAOA,UAAU,6BAA6B;IACrC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACpC;AAMD,wBAAgB,6BAA6B,CAAC,KAAK,GAAE,6BAAkC,GAAG,MAAM,CAa/F;AAED,wBAAgB,uCAAuC,IAAI,MAAM,CAIhE"}
@@ -0,0 +1,26 @@
1
+ import path from "node:path";
2
+ import { splitPathList } from "./pathList.js";
3
+ import { readEnvironmentVariable } from "./util.js";
4
+ const CLEARANCE_ALLOW_HOSTS_FILES = "CLEARANCE_ALLOW_HOSTS_FILES";
5
+ function groundcrewClearanceAllowHostsFile() {
6
+ return path.resolve(import.meta.dirname, "..", "..", "clearance-allow-hosts");
7
+ }
8
+ export function clearanceAllowHostsFilesValue(input = {}) {
9
+ const defaultFile = input.defaultFile ?? groundcrewClearanceAllowHostsFile();
10
+ const files = [defaultFile, ...splitPathList(input.existingFiles)];
11
+ const seen = new Set();
12
+ const uniqueFiles = [];
13
+ for (const file of files) {
14
+ if (seen.has(file)) {
15
+ continue;
16
+ }
17
+ seen.add(file);
18
+ uniqueFiles.push(file);
19
+ }
20
+ return uniqueFiles.join(path.delimiter);
21
+ }
22
+ export function clearanceAllowHostsFilesFromEnvironment() {
23
+ return clearanceAllowHostsFilesValue({
24
+ existingFiles: readEnvironmentVariable(CLEARANCE_ALLOW_HOSTS_FILES),
25
+ });
26
+ }
@@ -14,7 +14,7 @@
14
14
  * validates.
15
15
  */
16
16
  import { readFileSync } from "node:fs";
17
- import path from "node:path";
17
+ import { splitPathList } from "./pathList.js";
18
18
  import { debug } from "./util.js";
19
19
  /**
20
20
  * Parse and validate clearance allow-host sources into a de-duplicated list of
@@ -51,15 +51,6 @@ export function collectAllowedDomains(input) {
51
51
  }
52
52
  return domains;
53
53
  }
54
- function splitPathList(value) {
55
- if (value === undefined || value.length === 0) {
56
- return [];
57
- }
58
- return value
59
- .split(path.delimiter)
60
- .map((entry) => entry.trim())
61
- .filter((entry) => entry.length > 0);
62
- }
63
54
  /**
64
55
  * Split a host source into candidate tokens. Handles env-style
65
56
  * comma/whitespace separators and file-style newline lists with `#` comments
@@ -30,6 +30,7 @@ export declare function resolveSrtBinPath(baseUrl?: string): string;
30
30
  export declare function srtBinEntry(manifest: {
31
31
  bin?: string | Record<string, string>;
32
32
  }): string;
33
+ export declare function isEnvironmentAssignment(token: string): boolean;
33
34
  /**
34
35
  * Infer the agent's command basename from a agent `cmd` (skipping a leading
35
36
  * `env`/`KEY=val` prefix). Safehouse uses it to pick the matching `.sb`
@@ -102,6 +103,14 @@ interface LaunchCommandArguments {
102
103
  name: string;
103
104
  value: string;
104
105
  } | undefined;
106
+ /**
107
+ * Extra filesystem paths granted read/write to the safehouse sandbox via
108
+ * `--add-dirs`, beyond safehouse's automatic cwd grant. Resolved (and deduped)
109
+ * by `composeAgentLaunch`'s `resolveSafehouseAddDirs` — see there for which
110
+ * paths and why git needs them. Empty/undefined → no `--add-dirs` flag (the
111
+ * pre-existing behavior). Only consumed by the safehouse wrap.
112
+ */
113
+ safehouseAddDirs?: readonly string[] | undefined;
105
114
  }
106
115
  /**
107
116
  * Build the shell command that runs inside the workspace. The prompt is
@@ -1 +1 @@
1
- {"version":3,"file":"launchCommand.d.ts","sourceRoot":"","sources":["../../src/lib/launchCommand.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,GAAE,MAAwB,GAAG,MAAM,CAcvF;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,MAAwB,GAAG,MAAM,CAgB3E;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,MAAM,CAMvF;AAsMD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA8B9D;AAED,UAAU,sBAAsB;IAC9B,UAAU,EAAE,eAAe,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C;;;;OAIG;IACH,MAAM,EAAE,WAAW,CAAC;IACpB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;CACpE;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,sBAAsB,GAAG,MAAM,CA6B7E"}
1
+ {"version":3,"file":"launchCommand.d.ts","sourceRoot":"","sources":["../../src/lib/launchCommand.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAIrB,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,GAAE,MAAwB,GAAG,MAAM,CAcvF;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,MAAwB,GAAG,MAAM,CAgB3E;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,MAAM,CAMvF;AAsMD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE9D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA8B9D;AAED,UAAU,sBAAsB;IAC9B,UAAU,EAAE,eAAe,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C;;;;OAIG;IACH,MAAM,EAAE,WAAW,CAAC;IACpB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IACnE;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;CAClD;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,sBAAsB,GAAG,MAAM,CA6B7E"}
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import path from "node:path";
4
4
  import { BUILD_SECRET_NAMES, hasPreLaunchEnv, } from "./config.js";
5
+ import { clearanceAllowHostsFilesFromEnvironment } from "./clearanceAllowlist.js";
5
6
  import { shellSingleQuote } from "./shell.js";
6
7
  export { shellSingleQuote } from "./shell.js";
7
8
  /**
@@ -100,6 +101,9 @@ function unsetEnvironmentLine(names) {
100
101
  function unsetSecretsLine() {
101
102
  return unsetEnvironmentLine(BUILD_SECRET_NAMES);
102
103
  }
104
+ function safehouseClearanceWrapperCommand() {
105
+ return `CLEARANCE_ALLOW_HOSTS_FILES=${shellSingleQuote(clearanceAllowHostsFilesFromEnvironment())} ${shellSingleQuote(SAFEHOUSE_CLEARANCE_WRAPPER_PATH)}`;
106
+ }
103
107
  function trapCleanupLine(promptDir) {
104
108
  const cleanupCmd = `rm -rf ${shellSingleQuote(promptDir)}`;
105
109
  return `trap ${shellSingleQuote(cleanupCmd)} EXIT`;
@@ -209,7 +213,7 @@ function tokenizeShellPrefix(command) {
209
213
  }
210
214
  return tokens;
211
215
  }
212
- function isEnvironmentAssignment(token) {
216
+ export function isEnvironmentAssignment(token) {
213
217
  return /^[A-Za-z_][A-Za-z0-9_]*=/.test(token);
214
218
  }
215
219
  /**
@@ -363,7 +367,14 @@ function buildSafehouseLaunchCommand(arguments_) {
363
367
  const prepareWorktreeEnvPassFlag = arguments_.secretsFile === undefined ? "" : `--env-pass=${BUILD_SECRET_NAMES.join(",")} `;
364
368
  const preLaunchEnvNames = arguments_.definition.preLaunchEnv ?? [];
365
369
  const agentEnvPassFlag = preLaunchEnvNames.length === 0 ? "" : `--env-pass=${preLaunchEnvNames.join(",")} `;
366
- const safehouseWrapper = shellSingleQuote(SAFEHOUSE_CLEARANCE_WRAPPER_PATH);
370
+ // safehouse reads colon-separated paths from `--add-dirs`; both wraps get the
371
+ // same grant so the prepareWorktree hook and the agent can each reach git.
372
+ // Quote the whole value so shell-special chars survive; the trailing space
373
+ // separates it from the next argv token. See `resolveSafehouseAddDirs` for
374
+ // which paths these are and why.
375
+ const addDirs = arguments_.safehouseAddDirs ?? [];
376
+ const safehouseAddDirsFlag = addDirs.length === 0 ? "" : `--add-dirs=${shellSingleQuote(addDirs.join(":"))} `;
377
+ const safehouseWrapper = safehouseClearanceWrapperCommand();
367
378
  // Defensive shim+promptDir trap: by the time we arm it, `rm -rf <promptDir>`
368
379
  // has already run (line below) so the promptDir wipe is a no-op on the happy
369
380
  // path. Keeps the failure-window between shim creation and the explicit
@@ -382,7 +393,7 @@ function buildSafehouseLaunchCommand(arguments_) {
382
393
  secretsFile: arguments_.secretsFile,
383
394
  }));
384
395
  if (prepareWorktreeCommand !== undefined) {
385
- lines.push(`${safehouseWrapper} ${prepareWorktreeEnvPassFlag}sh -c ${shellSingleQuote(prepareWorktreeCommand)}`);
396
+ lines.push(`${safehouseWrapper} ${safehouseAddDirsFlag}${prepareWorktreeEnvPassFlag}sh -c ${shellSingleQuote(prepareWorktreeCommand)}`);
386
397
  }
387
398
  if (arguments_.secretsFile !== undefined) {
388
399
  lines.push(unsetSecretsLine());
@@ -392,7 +403,7 @@ function buildSafehouseLaunchCommand(arguments_) {
392
403
  // Running the real launch chain as `sh -c` would make it see `sh`, so use
393
404
  // an agent-named symlink to /bin/sh. This preserves per-agent profile
394
405
  // selection without enabling every agent profile.
395
- `{ ${safehouseWrapper} ${agentEnvPassFlag}"$_safehouse_shim" -c ${shellSingleQuote(agentCommand)} sh "$_p"; _safehouse_status=$?; rm -rf "$_safehouse_shim_dir"; trap - EXIT; exit "$_safehouse_status"; }`);
406
+ `{ ${safehouseWrapper} ${safehouseAddDirsFlag}${agentEnvPassFlag}"$_safehouse_shim" -c ${shellSingleQuote(agentCommand)} sh "$_p"; _safehouse_status=$?; rm -rf "$_safehouse_shim_dir"; trap - EXIT; exit "$_safehouse_status"; }`);
396
407
  return lines.join(" && ");
397
408
  }
398
409
  /**
@@ -0,0 +1,2 @@
1
+ export declare function splitPathList(value: string | undefined): string[];
2
+ //# sourceMappingURL=pathList.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pathList.d.ts","sourceRoot":"","sources":["../../src/lib/pathList.ts"],"names":[],"mappings":"AAEA,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CASjE"}
@@ -0,0 +1,11 @@
1
+ import path from "node:path";
2
+ export function splitPathList(value) {
3
+ const paths = [];
4
+ for (const entry of value?.split(path.delimiter) ?? []) {
5
+ const pathEntry = entry.trim();
6
+ if (pathEntry.length > 0) {
7
+ paths.push(pathEntry);
8
+ }
9
+ }
10
+ return paths;
11
+ }
@@ -16,6 +16,18 @@ export interface StagedSrtLaunch {
16
16
  value: string;
17
17
  };
18
18
  }
19
+ /**
20
+ * Resolve the worktree's real git common dir — the shared `.git` the srt policy
21
+ * fences off (read-grant + narrow write-allow + write-denies). Derived from the
22
+ * worktree itself rather than assuming a `<projectDir>/<repo>/.git` clone, so
23
+ * scripted/sparse-checkout worktrees — whose checkout is owned by an external
24
+ * provisioner and whose `repo` is just an alias with no clone on disk — get the
25
+ * correct dir instead of a phantom path that breaks git access and leaves the
26
+ * real common dir unfenced. For native worktrees this returns the same
27
+ * `<projectDir>/<repo>/.git` as before. `--path-format=absolute` keeps the path
28
+ * absolute regardless of git version or cwd.
29
+ */
30
+ export declare function resolveGitCommonDir(worktreeDir: string): string;
19
31
  /**
20
32
  * Generate the srt policies for a launch and stage them, plus — for agents that
21
33
  * cannot run with a read-only config home (codex) — a relocated, writable
@@ -1 +1 @@
1
- {"version":3,"file":"srtLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/srtLaunch.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAKnD,MAAM,WAAW,eAAe;IAC9B,qFAAqF;IACrF,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACrD;AAuBD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,eAAe,CAAC;IAC5B,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,eAAe,CA6ClB"}
1
+ {"version":3,"file":"srtLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/srtLaunch.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAKnD,MAAM,WAAW,eAAe;IAC9B,qFAAqF;IACrF,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACrD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAQ/D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,eAAe,CAAC;IAC5B,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,eAAe,CA6ClB"}
@@ -1,6 +1,7 @@
1
1
  import { copyFileSync, existsSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import { clearanceAllowHostsFilesFromEnvironment } from "./clearanceAllowlist.js";
4
5
  import { collectAllowedDomains } from "./clearanceHosts.js";
5
6
  import { runCommand } from "./commandRunner.js";
6
7
  import { inferAgentCommandName } from "./launchCommand.js";
@@ -17,7 +18,7 @@ import { readEnvironmentVariable } from "./util.js";
17
18
  * `<projectDir>/<repo>/.git` as before. `--path-format=absolute` keeps the path
18
19
  * absolute regardless of git version or cwd.
19
20
  */
20
- function resolveGitCommonDir(worktreeDir) {
21
+ export function resolveGitCommonDir(worktreeDir) {
21
22
  return runCommand("git", [
22
23
  "-C",
23
24
  worktreeDir,
@@ -53,7 +54,7 @@ export function buildAndStageSrtLaunch(input) {
53
54
  gitCommonDir: resolveGitCommonDir(input.worktreeDir),
54
55
  allowedDomains: collectAllowedDomains({
55
56
  hosts: readEnvironmentVariable("CLEARANCE_ALLOW_HOSTS"),
56
- files: readEnvironmentVariable("CLEARANCE_ALLOW_HOSTS_FILES"),
57
+ files: clearanceAllowHostsFilesFromEnvironment(),
57
58
  }),
58
59
  };
59
60
  const directory = mkdtempSync(path.join(os.tmpdir(), `groundcrew-srt-${input.task}-`));
package/docs/runners.md CHANGED
@@ -13,19 +13,21 @@
13
13
 
14
14
  ## Safehouse Clearance Allowlist
15
15
 
16
- Only applies when `local.runner` resolves to `safehouse`. Groundcrew starts `clearance` on `http://127.0.0.1:19999` and runs the agent through the bundled `safehouse-clearance` wrapper. Clearance refuses to start without an allowlist.
16
+ Only applies when `local.runner` resolves to `safehouse`. Groundcrew starts `clearance` on `http://127.0.0.1:19999` and runs the agent through the bundled `safehouse-clearance` wrapper. Groundcrew automatically points clearance at its shipped starter allowlist, so a fresh install does not need a `CLEARANCE_ALLOW_HOSTS_FILES` export.
17
17
 
18
- Shortest path:
18
+ Groundcrew ships that starter file at `$(npm root -g)/@clipboard-health/groundcrew/clearance-allow-hosts`, covering model APIs, Linear, Notion, Slack, Datadog, GitHub, npm, and common dev tooling.
19
+
20
+ To add ad hoc hosts for one run, use `CLEARANCE_ALLOW_HOSTS`:
19
21
 
20
22
  ```bash
21
23
  CLEARANCE_ALLOW_HOSTS="api.openai.com,auth.openai.com,api.anthropic.com,mcp.linear.app,api.linear.app" \
22
24
  crew run --watch
23
25
  ```
24
26
 
25
- Groundcrew ships a starter file covering model APIs, Linear, Notion, Slack, Datadog, GitHub, npm, and common dev tooling at `$(npm root -g)/@clipboard-health/groundcrew/clearance-allow-hosts`. Point clearance at it, optionally with a personal file:
27
+ To keep personal hosts in a file, set `CLEARANCE_ALLOW_HOSTS_FILES` to only the additional files. Groundcrew prepends its shipped file automatically:
26
28
 
27
29
  ```bash
28
- CLEARANCE_ALLOW_HOSTS_FILES="$(npm root -g)/@clipboard-health/groundcrew/clearance-allow-hosts:$HOME/.config/clearance/personal-allow-hosts" \
30
+ CLEARANCE_ALLOW_HOSTS_FILES="$HOME/.config/clearance/personal-allow-hosts" \
29
31
  crew run --watch
30
32
  ```
31
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/groundcrew",
3
- "version": "4.31.0",
3
+ "version": "4.32.0",
4
4
  "description": "Linear-driven orchestrator that launches AI coding agents in git worktrees, with workspace lifecycle and usage tracking.",
5
5
  "keywords": [
6
6
  "agent",
@@ -69,7 +69,7 @@
69
69
  },
70
70
  "dependencies": {
71
71
  "@anthropic-ai/sandbox-runtime": "0.0.52",
72
- "@clipboard-health/clearance": "1.1.14",
72
+ "@clipboard-health/clearance": "1.2.0",
73
73
  "@linear/sdk": "86.0.0",
74
74
  "cosmiconfig": "9.0.1",
75
75
  "tslib": "2.8.1",