@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
@@ -23,7 +23,7 @@ Locked constraints, not to be re-litigated during implementation:
23
23
 
24
24
  - The locked decisions kill or fundamentally reshape most of v1 (~21K source lines, ~38K test lines): `config.ts` dies with the TS-config format, the reviewer PR-polling loop dies, clearance/safehouse/sbx die, several commands die, and the core nouns are redefined. Every external surface breaks: config, CLI, source contract, sandbox posture, completion model.
25
25
  - Evolve's safety-net premise was stale: the acceptance suite never runs against v1 (green-on-v1 dropped), so in-place evolution gets zero protection from it. "Evolve" would be the same rewrite executed as a thousand PRs fighting 38K test lines that assert dying behavior.
26
- - Roughly 15–25% of v1 survives recognizably (srtPolicy/srtLaunch, the cleaner, tmux/git plumbing, doctor pieces, usage, completions). Surviving code is **ported file-by-file with its unit tests**; `git mv` preserves history.
26
+ - Roughly 15–25% of v1 survives recognizably (sandbox policy and launch composition, the cleaner, tmux/git plumbing, doctor pieces, usage, completions). Surviving code is **ported file-by-file with its unit tests**; `git mv` preserves history.
27
27
 
28
28
  **Repo strategy: same repo, isolated `v2/` workspace.** v2 grows on `main` under a top-level `v2/` with its own `package.json`, flat `src/<module>` layout, and its own `CLAUDE.md`/`AGENTS.md`/`CONTEXT.md` carrying the v2 ubiquitous language. v1 and v2 use the same nouns with contradictory meanings ("Workspace" means a terminal pane in v1 and a per-task directory of worktrees in v2), so sharing one `src/` was rejected — it would contaminate agent-driven work. dependency-cruiser forbids cross-tree imports in both directions. **Flip condition:** if v2-building agents keep tripping over v1 despite the boundary, escalate mid-build to a new repo initialized from a full clone, then rename old → `groundcrew-v1`, new → `groundcrew`.
29
29
 
@@ -96,14 +96,14 @@ Single-repo tasks stay dead simple as the degenerate case: one designated repo,
96
96
  **"Plugin" means exactly one thing in v2: a task source.** Everything else that looked pluggable is config or core:
97
97
 
98
98
  - **Agent harnesses are declarative config**, not plugins: `cmd`, resume args, sandbox preferences, plus first-class `model`/`effort` fields. `cmd`-points-at-your-script is the escape hatch for odd agents.
99
- - **Sandbox runners are core-only**, explicitly not pluggable — a pluggable sandbox is a contradiction under sandboxed-by-default. **v2 ships exactly one runner: srt** (`sandbox-exec` on macOS, bubblewrap on Linux). Safehouse, clearance, and sbx die, including the `crew-clearance-ensure` bin.
99
+ - **Sandbox runners are core-only**, explicitly not pluggable — a pluggable sandbox is a contradiction under sandboxed-by-default. The implementation selects the supported platform runner and keeps its mechanics behind the core sandbox boundary.
100
100
  - **No in-process plugin class exists.** The internal `TaskSource` interface survives only as core's private representation of a discovered manifest.
101
101
 
102
102
  **Packaging:** a plugin is a **directory bundle** — `source.json` plus scripts — discovered under `~/.config/groundcrew/task-sources/<name>/` (user) or bundled in the package (first-party). The directory is the plugin: language-agnostic, no runtime loading. Distribution is deliberately not groundcrew's problem (git clone, `curl | tar`, npm postinstall). No registry or catalog in v2.0; provenance tiers collapse to `package | user`. Review isolation is met structurally: third-party code never enters the core repo and never runs in the core process.
103
103
 
104
104
  **Versioning:** `source.json` carries a required integer `protocolVersion: 1` naming the contract generation, bumped only on breaking change — capability-by-omission carries all additive change (no version sniffing: `update` absent means read-only). Parseable-but-unsupported version → explicit actionable error in discovery/doctor/status, never a silent skip; unparseable manifests keep skip-plus-warn. Core declares a supported set; v2.0 ships `{1}`. No v1 command aliases.
105
105
 
106
- **Sources are sandboxed by default**, under the same srt machinery as agents: deny-by-default; a source gets its install dir (read), declared secret files, declared env, stdout/stderr, and a per-source scratch dir. The manifest gains a **network egress allowlist** (e.g. `network: ["api.linear.app"]`) alongside `secrets`/`env`/`prerequisites`. Uniform across origins — bundled first-party sources run under the same policy. Per-source opt-out (`sandbox: false`) is loud in status/doctor.
106
+ **Sources are sandboxed by default**, under the same core sandbox boundary as agents: deny-by-default; a source gets its install dir (read), declared secret files, declared env, stdout/stderr, and a per-source scratch dir. The manifest gains a **network egress allowlist** (e.g. `network: ["api.linear.app"]`) alongside `secrets`/`env`/`prerequisites`. Uniform across origins — bundled first-party sources run under the same policy. Per-source opt-out (`sandbox: false`) is loud in status/doctor.
107
107
 
108
108
  **Batteries included, kernel-pure code paths:** `linear`, `jira`, and `todo-txt` ship as manifest bundles crossing the same process boundary as third-party sources — first-party sources are the protocol's permanent conformance tests. The generic `shell` config-block adapter dies: bring-your-own-scripts is just a user-dir manifest.
109
109
 
@@ -190,7 +190,7 @@ Consequences: the session presenter presents sessions, not runs; `status` render
190
190
  | **Run** | The run record: state machine, **reported layer** (`artifact add`/`done` intake), outcomes, writeback point | the **Writeback port** (consumer-owned, defined in `src/run/`) |
191
191
  | **Workspace** | Worktrees, branches, **observed layer** (credential-free git facts), `.groundcrew/task.json` and the task-identity resolver; typed errors that Shell maps to exit codes 2/3 | the workspace marker-file format |
192
192
  | **Session** | Harness profiles (declarative), launch composition (presenter → sandbox → agent), pause/resume and session-id capture; cmux/tmux/zellij adapters in-core | the **presenter contract seam** |
193
- | **Sandbox** | Pure library: `wrap(command, policy) → command`; srt mechanics hidden; no lifecycle ownership | nothing pluggable — core-only by decision |
193
+ | **Sandbox** | Pure library: `wrap(command, policy) → command`; runner mechanics hidden; no lifecycle ownership | nothing pluggable — core-only by decision |
194
194
  | **Shell** | commander wiring, routing (`repo add` → Workspace; `artifact add`/`done` → Run), rendering, error→exit-code mapping; `status` = read model joining Run (reported) and Workspace (observed) | — |
195
195
 
196
196
  The flow model's central invariant gets one owner per layer — **observed = Workspace, reported = Run** — so a lie or missing link is always attributable to exactly one module. Sandbox has two real callers (Session wraps agents, Acquisition wraps sources): a real seam, and the one place that keeps "sandbox runners are not pluggable" enforceable.
@@ -279,7 +279,7 @@ The file sink gets everything, always. Console: `info`+ by default, human-render
279
279
 
280
280
  ### 10.5 Reconcile and reaping
281
281
 
282
- **Active sweep yes, TTL no.** srt sandboxes are process-scoped — nothing standing to reap except a live agent process, and the interactive baseline forbids killing those on a timer.
282
+ **Active sweep yes, TTL no.** Agent launches are process-scoped, and the interactive baseline forbids killing live agents on a timer. Persistent runner environments remain externally managed.
283
283
 
284
284
  - **Reconcile is an idempotent library routine with two callers**: startup (the crash-safety guarantee — on-disk git/tmux/sandbox state is the source of truth, compared against expected task state; `using`/signal handlers are the graceful path only) and the dispatcher tick (every Nth poll cycle).
285
285
  - Auto-GC only the provably dead: presenter surfaces whose process exited, stale run records, clean worktrees of source-terminal tasks (the cleaner, section 5).
@@ -326,7 +326,7 @@ If Effect is ever reconsidered: gate on v4-stable, follow the opencode pattern (
326
326
  1. **Build the acceptance suite red-first from the [E2E scenario catalog](./e2e-scenario-catalog.md)** — the `e2e/` package, fixture source, scripted agent, fake `gh`, harness self-tests. This is the entry point; no v2 code before it.
327
327
  2. Fix `main`'s release automation (path/scope filter) so v2 commits don't trigger 4.x publishes (section 2).
328
328
  3. Scaffold the `v2/` workspace: seven-module skeleton, dependency-cruiser rules (section 9.6) proven to bite, `CONTEXT.md` from the glossary seed, ADRs seeded from the wayfinder resolutions.
329
- 4. Bring the suite green module by module, porting the surviving v1 code (srtPolicy/srtLaunch, cleaner, tmux/git plumbing, doctor pieces) file-by-file with its tests.
329
+ 4. Bring the suite green module by module, porting the surviving v1 code (sandbox policy and launch composition, cleaner, tmux/git plumbing, doctor pieces) file-by-file with its tests.
330
330
  5. Before publishing 5.0.0: resolve the `crew upgrade` ambush (section 11) and execute the cutover (section 2).
331
331
 
332
332
  Post-v2 handoff backlog (deferred by decision): migration easing detail (section 11); presenter bundles and their trust posture (section 8); `crew progress` (section 10.6); an optional repo allowlist (section 4).
@@ -343,7 +343,7 @@ Every map decision, in resolution order. Full rationale lives in the linked tick
343
343
  | [Multi-repo task model](https://linear.app/clipboardhealth/issue/DEVOP-5967) | One session per task over a workspace of worktrees | 4 |
344
344
  | [Flow model](https://linear.app/clipboardhealth/issue/DEVOP-5968) | Sinks and flows dissolve; `list`/`get`/`update` source contract; run-state machine | 3 |
345
345
  | [Effect vs plain TypeScript](https://linear.app/clipboardhealth/issue/DEVOP-5972) | Plain TypeScript; exceptions inside, result-shaped protocol at the boundary; reconcile-on-startup | 12.1 |
346
- | [Plugin packaging and review isolation](https://linear.app/clipboardhealth/issue/DEVOP-5973) | Task sources are the only plugin kind; directory bundles; `protocolVersion`; sandboxed by default; srt only | 6 |
346
+ | [Plugin packaging and review isolation](https://linear.app/clipboardhealth/issue/DEVOP-5973) | Task sources are the only plugin kind; directory bundles; `protocolVersion`; sandboxed by default; core runners | 6 |
347
347
  | [CLI surface](https://linear.app/clipboardhealth/issue/DEVOP-5975) | 14 commands; `crew.config.jsonc`; install.sh → `crew init` | 7 |
348
348
  | [E2E scenario catalog](https://linear.app/clipboardhealth/issue/DEVOP-5974) | v2-only red-first suite; harness self-tests; completion model codified (amends the destination) | 5 |
349
349
  | [Headless extension point](https://linear.app/clipboardhealth/issue/DEVOP-5976) | Session-presenter seam; headless = future detached presenter (amends CLI config: `multiplexer` → `presenter`) | 8 |
@@ -52,9 +52,9 @@ expected or explicitly allowed.
52
52
  Set `sandboxWritePaths` when the shell source owns local files the agent must
53
53
  read and update in place, such as a plan directory. Each path may be absolute,
54
54
  relative to the agent working directory, or `~`-prefixed. Under the `safehouse`
55
- and `srt` runners, groundcrew opens those directories for read and write only
56
- for tasks from that source; an unqualified task id grants every eligible
57
- source's declared paths. The `sdx` runner does not mount these host paths.
55
+ runner, groundcrew opens those directories for read and write only for tasks
56
+ from that source; an unqualified task id grants every eligible source's
57
+ declared paths. The `sdx` runner does not mount these host paths.
58
58
 
59
59
  ```json
60
60
  [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/groundcrew",
3
- "version": "4.48.0",
3
+ "version": "4.48.2",
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",
@@ -63,14 +63,12 @@
63
63
  "markdown:lint": "markdownlint-cli2 --config config/markdownlint-cli2.jsonc",
64
64
  "prepack": "node --run build",
65
65
  "prepare": "husky",
66
- "spell:check": "cspell --config config/cspell.json --no-summary --no-progress --no-must-find-files",
67
66
  "sync-ai-rules": "node ./node_modules/@clipboard-health/ai-rules/scripts/sync.js common",
68
67
  "syncpack:lint": "syncpack lint --config config/syncpack.config.ts",
69
68
  "test": "vitest run --coverage",
70
69
  "verify": "node scripts/verifyAll.mts"
71
70
  },
72
71
  "dependencies": {
73
- "@anthropic-ai/sandbox-runtime": "0.0.65",
74
72
  "@clipboard-health/clearance": "1.4.7",
75
73
  "@linear/sdk": "88.1.0",
76
74
  "agent-trust": "1.0.0",
@@ -79,7 +77,7 @@
79
77
  "zod": "4.4.3"
80
78
  },
81
79
  "devDependencies": {
82
- "@clipboard-health/ai-rules": "2.37.1",
80
+ "@clipboard-health/ai-rules": "2.43.4",
83
81
  "@clipboard-health/oxlint-config": "1.12.9",
84
82
  "@nx/js": "22.7.7",
85
83
  "@tsconfig/node24": "24.0.4",
@@ -87,7 +85,6 @@
87
85
  "@types/node": "25.9.5",
88
86
  "@typescript/native-preview": "7.0.0-dev.20260707.2",
89
87
  "@vitest/coverage-v8": "4.1.10",
90
- "cspell": "10.0.1",
91
88
  "dependency-cruiser": "18.1.0",
92
89
  "husky": "9.1.7",
93
90
  "jscpd": "5.0.12",
package/static/demo.tape CHANGED
@@ -1,5 +1,3 @@
1
- # cspell:ignore noprofile norc ttyd
2
-
3
1
  Output static/demo.gif
4
2
  Require bash
5
3
  Require ffmpeg
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env bash
2
- # cspell:ignore ttyd
3
2
  set -euo pipefail
4
3
 
5
4
  script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
@@ -1,31 +0,0 @@
1
- /**
2
- * Translate the existing clearance egress allowlist into srt
3
- * `network.allowedDomains`. groundcrew keeps one source of truth for which
4
- * hosts an agent may reach: the `CLEARANCE_ALLOW_HOSTS` env var and the
5
- * newline-delimited files named by `CLEARANCE_ALLOW_HOSTS_FILES` (the shipped
6
- * `clearance-allow-hosts` starter plus any personal files).
7
- *
8
- * clearance and srt share host-matching semantics — a bare `example.com` is an
9
- * exact match and `*.example.com` matches subdomains — so the translation is
10
- * almost an identity. The only real work is parsing (comments, blanks,
11
- * comma/whitespace/newline separators), de-duplication, and dropping entries
12
- * srt's domain schema would reject (`*.com`, `*`, leading/trailing dots, bare
13
- * tokens without a dot like `localhost`) so the generated settings file always
14
- * validates.
15
- */
16
- export interface CollectAllowedDomainsInput {
17
- /** Raw `CLEARANCE_ALLOW_HOSTS` value (comma/whitespace separated). */
18
- hosts?: string | undefined;
19
- /**
20
- * Raw `CLEARANCE_ALLOW_HOSTS_FILES` value: a list of file paths joined by
21
- * the platform path delimiter (`:` on POSIX), each a newline-delimited host
22
- * list. Missing or unreadable files are skipped with a debug line.
23
- */
24
- files?: string | undefined;
25
- }
26
- /**
27
- * Parse and validate clearance allow-host sources into a de-duplicated list of
28
- * srt domain patterns, preserving first-seen order.
29
- */
30
- export declare function collectAllowedDomains(input: CollectAllowedDomainsInput): string[];
31
- //# sourceMappingURL=clearanceHosts.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"clearanceHosts.d.ts","sourceRoot":"","sources":["../../src/lib/clearanceHosts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAOH,MAAM,WAAW,0BAA0B;IACzC,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,0BAA0B,GAAG,MAAM,EAAE,CA8BjF"}
@@ -1,96 +0,0 @@
1
- /**
2
- * Translate the existing clearance egress allowlist into srt
3
- * `network.allowedDomains`. groundcrew keeps one source of truth for which
4
- * hosts an agent may reach: the `CLEARANCE_ALLOW_HOSTS` env var and the
5
- * newline-delimited files named by `CLEARANCE_ALLOW_HOSTS_FILES` (the shipped
6
- * `clearance-allow-hosts` starter plus any personal files).
7
- *
8
- * clearance and srt share host-matching semantics — a bare `example.com` is an
9
- * exact match and `*.example.com` matches subdomains — so the translation is
10
- * almost an identity. The only real work is parsing (comments, blanks,
11
- * comma/whitespace/newline separators), de-duplication, and dropping entries
12
- * srt's domain schema would reject (`*.com`, `*`, leading/trailing dots, bare
13
- * tokens without a dot like `localhost`) so the generated settings file always
14
- * validates.
15
- */
16
- import { readFileSync } from "node:fs";
17
- import { splitPathList } from "./pathList.js";
18
- import { debug } from "./util.js";
19
- /**
20
- * Parse and validate clearance allow-host sources into a de-duplicated list of
21
- * srt domain patterns, preserving first-seen order.
22
- */
23
- export function collectAllowedDomains(input) {
24
- const texts = [];
25
- for (const file of splitPathList(input.files)) {
26
- try {
27
- texts.push(readFileSync(file, "utf8"));
28
- }
29
- catch (error) {
30
- debug(`Skipping unreadable CLEARANCE_ALLOW_HOSTS_FILES entry ${file}: ${String(error)}`);
31
- }
32
- }
33
- if (input.hosts !== undefined) {
34
- texts.push(input.hosts);
35
- }
36
- const seen = new Set();
37
- const domains = [];
38
- for (const text of texts) {
39
- for (const raw of tokenize(text)) {
40
- const domain = normalizeDomain(raw);
41
- if (domain === undefined) {
42
- continue;
43
- }
44
- const key = domain.toLowerCase();
45
- if (seen.has(key)) {
46
- continue;
47
- }
48
- seen.add(key);
49
- domains.push(domain);
50
- }
51
- }
52
- return domains;
53
- }
54
- /**
55
- * Split a host source into candidate tokens. Handles env-style
56
- * comma/whitespace separators and file-style newline lists with `#` comments
57
- * (full-line or trailing — hostnames never contain `#`).
58
- */
59
- function tokenize(text) {
60
- return text
61
- .split(/\r?\n/)
62
- .flatMap((line) => line.replace(/#.*$/, "").split(/[\s,]+/))
63
- .map((token) => token.trim())
64
- .filter((token) => token.length > 0);
65
- }
66
- /**
67
- * Normalize a raw host token into a valid srt domain pattern, or `undefined`
68
- * if srt would reject it. Mirrors srt's `DomainPatternSchema`: a wildcard must
69
- * be `*.<domain-with-a-dot>`; a bare host must contain a dot and no `*`. A
70
- * leading-dot suffix form (`.example.com`) is rewritten to `*.example.com`.
71
- */
72
- function normalizeDomain(token) {
73
- const candidate = token.startsWith(".") ? `*${token}` : token;
74
- if (candidate.startsWith("*.")) {
75
- const base = candidate.slice(2);
76
- return isPlainDomain(base) ? candidate : undefined;
77
- }
78
- return isPlainDomain(candidate) ? candidate : undefined;
79
- }
80
- function isPlainDomain(value) {
81
- return (value.length > 0 &&
82
- !value.includes("*") &&
83
- // Mirror srt's DomainPatternSchema, which rejects scheme/path/port tokens.
84
- // This matters for safety, not just correctness: a token srt rejects (e.g.
85
- // `https://api.github.com`, `api.github.com:443`, `github.com/path`) fails
86
- // the whole settings file's schema validation, and srt's `loadConfig` then
87
- // returns null → the CLI silently falls back to a config with no read mask.
88
- // Dropping such tokens here keeps the generated settings valid (fail closed
89
- // for that host, never fail open for the launch).
90
- !value.includes("://") &&
91
- !value.includes("/") &&
92
- !value.includes(":") &&
93
- value.includes(".") &&
94
- !value.startsWith(".") &&
95
- !value.endsWith("."));
96
- }
@@ -1,61 +0,0 @@
1
- import type { AgentDefinition } from "./config.ts";
2
- export interface StagedSrtLaunch {
3
- /** Dedicated temp dir holding the settings files (and any relocated config home). */
4
- directory: string;
5
- /** Profile-neutral policy for the prepareWorktree wrap (no agent credentials). */
6
- prepareFile: string;
7
- /** Full agent policy for the agent wrap. */
8
- agentFile: string;
9
- /**
10
- * Env var pointing the agent at its relocated, writable config home (codex's
11
- * `CODEX_HOME`). Threaded into the agent wrap by `buildLaunchCommand`.
12
- * Undefined for read-only agents (claude), which run with a read-only home.
13
- */
14
- agentConfigDirEnv?: {
15
- name: string;
16
- value: string;
17
- };
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;
31
- /**
32
- * Generate the srt policies for a launch and stage them, plus — for agents that
33
- * cannot run with a read-only config home (codex) — a relocated, writable
34
- * config dir seeded with the minimal files the agent needs to authenticate and
35
- * keep its config. Shared by `setupWorkspace` (fresh runs) and `resumeWorkspace`
36
- * (resumes) so both behave identically under the srt runner.
37
- *
38
- * Two policies, distinct surfaces:
39
- * - the `prepare` policy is profile-neutral (empty agent → no `~/.claude` /
40
- * `~/.codex` grants, no relocated home) so the repo-controlled prepareWorktree
41
- * hook can't touch the agent's credentials;
42
- * - the `agent` policy carries the agent's read-only config profile and, when
43
- * the agent relocates, the writable relocated home.
44
- *
45
- * The relocated home lives **inside** the settings dir but is the only thing
46
- * under it granted to the sandbox — the settings JSON siblings are never
47
- * read-granted, so the agent can't read or rewrite its own policy. The launch
48
- * command tears the whole dir down after srt exits.
49
- */
50
- export declare function buildAndStageSrtLaunch(input: {
51
- task: string;
52
- worktreeDir: string;
53
- definition: AgentDefinition;
54
- /** Local task-source directories the agent needs for self-completion writeback. */
55
- taskSourceWritePaths?: readonly string[] | undefined;
56
- /** Absolute host dirs re-opened read-only (`config.local.readOnlyDirs`). */
57
- readOnlyDirs?: readonly string[] | undefined;
58
- /** Defaults to `os.homedir()`. Injected in tests to seed from a fixture home. */
59
- homeDir?: string;
60
- }): StagedSrtLaunch;
61
- //# sourceMappingURL=srtLaunch.d.ts.map
@@ -1 +0,0 @@
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,mFAAmF;IACnF,oBAAoB,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACrD,4EAA4E;IAC5E,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IAC7C,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,eAAe,CA+ClB"}
@@ -1,107 +0,0 @@
1
- import { copyFileSync, existsSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { clearanceAllowHostsFilesFromEnvironment } from "./clearanceAllowlist.js";
5
- import { collectAllowedDomains } from "./clearanceHosts.js";
6
- import { runCommand } from "./commandRunner.js";
7
- import { inferAgentCommandName } from "./launchCommand.js";
8
- import { agentConfigRelocation, buildSrtSettings } from "./srtPolicy.js";
9
- import { readEnvironmentVariable } from "./util.js";
10
- /**
11
- * Resolve the worktree's real git common dir — the shared `.git` the srt policy
12
- * fences off (read-grant + narrow write-allow + write-denies). Derived from the
13
- * worktree itself rather than assuming a `<projectDir>/<repo>/.git` clone, so
14
- * scripted/sparse-checkout worktrees — whose checkout is owned by an external
15
- * provisioner and whose `repo` is just an alias with no clone on disk — get the
16
- * correct dir instead of a phantom path that breaks git access and leaves the
17
- * real common dir unfenced. For native worktrees this returns the same
18
- * `<projectDir>/<repo>/.git` as before. `--path-format=absolute` keeps the path
19
- * absolute regardless of git version or cwd.
20
- */
21
- export function resolveGitCommonDir(worktreeDir) {
22
- return runCommand("git", [
23
- "-C",
24
- worktreeDir,
25
- "rev-parse",
26
- "--path-format=absolute",
27
- "--git-common-dir",
28
- ]);
29
- }
30
- /**
31
- * Generate the srt policies for a launch and stage them, plus — for agents that
32
- * cannot run with a read-only config home (codex) — a relocated, writable
33
- * config dir seeded with the minimal files the agent needs to authenticate and
34
- * keep its config. Shared by `setupWorkspace` (fresh runs) and `resumeWorkspace`
35
- * (resumes) so both behave identically under the srt runner.
36
- *
37
- * Two policies, distinct surfaces:
38
- * - the `prepare` policy is profile-neutral (empty agent → no `~/.claude` /
39
- * `~/.codex` grants, no relocated home) so the repo-controlled prepareWorktree
40
- * hook can't touch the agent's credentials;
41
- * - the `agent` policy carries the agent's read-only config profile and, when
42
- * the agent relocates, the writable relocated home.
43
- *
44
- * The relocated home lives **inside** the settings dir but is the only thing
45
- * under it granted to the sandbox — the settings JSON siblings are never
46
- * read-granted, so the agent can't read or rewrite its own policy. The launch
47
- * command tears the whole dir down after srt exits.
48
- */
49
- export function buildAndStageSrtLaunch(input) {
50
- const agent = inferAgentCommandName(input.definition.cmd);
51
- const homeDir = input.homeDir ?? os.homedir();
52
- const base = {
53
- worktreeDir: input.worktreeDir,
54
- gitCommonDir: resolveGitCommonDir(input.worktreeDir),
55
- allowedDomains: collectAllowedDomains({
56
- hosts: readEnvironmentVariable("CLEARANCE_ALLOW_HOSTS"),
57
- files: clearanceAllowHostsFilesFromEnvironment(),
58
- }),
59
- readOnlyDirs: input.readOnlyDirs ?? [],
60
- };
61
- const directory = mkdtempSync(path.join(os.tmpdir(), `groundcrew-srt-${input.task}-`));
62
- const relocation = agentConfigRelocation(agent);
63
- let relocatedConfigDir;
64
- let agentConfigDirEnv;
65
- if (relocation !== undefined) {
66
- relocatedConfigDir = path.join(directory, `${agent}-home`);
67
- mkdirSync(relocatedConfigDir, { recursive: true });
68
- seedRelocatedConfigDir({
69
- sourceDir: path.join(homeDir, relocation.sourceHomeRelativeDir),
70
- seedFiles: relocation.seedFiles,
71
- relocatedConfigDir,
72
- });
73
- agentConfigDirEnv = { name: relocation.configDirEnv, value: relocatedConfigDir };
74
- }
75
- const prepare = buildSrtSettings({ ...base, agent: "" });
76
- const agentSettings = buildSrtSettings({
77
- ...base,
78
- agent,
79
- taskSourceWritePaths: input.taskSourceWritePaths ?? [],
80
- ...(relocatedConfigDir === undefined ? {} : { relocatedConfigDir }),
81
- });
82
- const prepareFile = path.join(directory, "prepare-settings.json");
83
- const agentFile = path.join(directory, "agent-settings.json");
84
- writeFileSync(prepareFile, `${JSON.stringify(prepare, undefined, 2)}\n`);
85
- writeFileSync(agentFile, `${JSON.stringify(agentSettings, undefined, 2)}\n`);
86
- return {
87
- directory,
88
- prepareFile,
89
- agentFile,
90
- ...(agentConfigDirEnv === undefined ? {} : { agentConfigDirEnv }),
91
- };
92
- }
93
- /**
94
- * Copy the agent's minimal credential/config files into the relocated home.
95
- * Best-effort per file: a missing source (e.g. the user isn't logged into the
96
- * agent, or has no config) is skipped rather than aborting the launch — the
97
- * agent then reports its own "not logged in" state, which is the correct signal.
98
- */
99
- function seedRelocatedConfigDir(input) {
100
- for (const file of input.seedFiles) {
101
- const source = path.join(input.sourceDir, file);
102
- if (!existsSync(source)) {
103
- continue;
104
- }
105
- copyFileSync(source, path.join(input.relocatedConfigDir, file));
106
- }
107
- }
@@ -1,118 +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 { type SandboxRuntimeConfig } from "@anthropic-ai/sandbox-runtime";
48
- export interface BuildSrtSettingsInput {
49
- /** Absolute worktree directory the agent runs in (read + write). */
50
- worktreeDir: string;
51
- /**
52
- * Absolute path to the repo's git common dir (the parent clone's `.git`).
53
- * Granted read wholesale, but write only as a narrow allowlist of the paths
54
- * the git workflow actually touches (see `GIT_COMMON_WRITE_PATHS`).
55
- */
56
- gitCommonDir: string;
57
- /** Agent identity (e.g. "claude", "codex") used to pick the credential profile. */
58
- agent: string;
59
- /** srt `network.allowedDomains`, already translated from the clearance allowlist. */
60
- allowedDomains: readonly string[];
61
- /**
62
- * Absolute path to the agent's relocated, writable config/state home for this
63
- * launch (codex's `CODEX_HOME`). When set it is granted read + write so the
64
- * agent persists session state there instead of its real home, which stays
65
- * read-only. claude does not relocate (its macOS keychain credential is bound
66
- * to the default config dir), so this is omitted for it.
67
- */
68
- relocatedConfigDir?: string;
69
- /**
70
- * Local task-source directories the worker needs for self-completion
71
- * writeback, such as a todo-txt file's parent directory and task prompt dir.
72
- */
73
- taskSourceWritePaths?: readonly string[];
74
- /** Defaults to `process.platform`. Injected in tests to exercise both deny-read roots. */
75
- platform?: NodeJS.Platform;
76
- /** Defaults to `os.homedir()`. Injected in tests. */
77
- homeDir?: string;
78
- /** Defaults to `process.execPath`; used to locate the global node_modules to deny writes to. */
79
- nodeExecPath?: string;
80
- /**
81
- * Absolute host dirs re-opened read-only from `config.local.readOnlyDirs`
82
- * (toolchains the profile masks but doesn't re-open, e.g. tfenv). srt skips
83
- * any that don't exist, so passing an absent path is harmless.
84
- */
85
- readOnlyDirs?: readonly string[];
86
- }
87
- /**
88
- * How an agent that cannot run with a read-only config home is pointed at a
89
- * relocated, per-launch writable home instead. The launch stages a temp dir,
90
- * seeds it with the minimal files the agent needs to authenticate + keep its
91
- * config, exports `configDirEnv` to that dir, and grants it write — so the real
92
- * home (which holds the persistence surfaces) is never written.
93
- *
94
- * Empirically (STAFF-1305 live validation, macOS): codex hard-fails to launch
95
- * with a read-only `~/.codex` ("failed to initialize in-process app-server
96
- * client") and authenticates from a file (`auth.json`), so relocating
97
- * `CODEX_HOME` + seeding `auth.json`/`config.toml` both unblocks it and closes
98
- * persistence. claude is deliberately absent: its macOS keychain credential is
99
- * bound to the default config dir, so relocating `CLAUDE_CONFIG_DIR` breaks
100
- * auth — claude instead runs with a writable home whose executable surfaces are
101
- * denied (see `AGENT_SRT_PROFILES`).
102
- */
103
- export interface AgentConfigRelocation {
104
- /** Env var that points the agent at a relocated config home. */
105
- configDirEnv: string;
106
- /** Home-relative dir the seed files are copied from (the agent's real home). */
107
- sourceHomeRelativeDir: string;
108
- /** Files (relative to `sourceHomeRelativeDir`) seeded into the relocated home. */
109
- seedFiles: readonly string[];
110
- }
111
- /**
112
- * Return the config-relocation spec for an agent, or `undefined` when the agent
113
- * runs with a writable real home + deny-list and does not relocate (claude,
114
- * unknown agents).
115
- */
116
- export declare function agentConfigRelocation(agent: string): AgentConfigRelocation | undefined;
117
- export declare function buildSrtSettings(input: BuildSrtSettingsInput): SandboxRuntimeConfig;
118
- //# sourceMappingURL=srtPolicy.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"srtPolicy.d.ts","sourceRoot":"","sources":["../../src/lib/srtPolicy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAMH,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,+BAA+B,CAAC;AAEvC,MAAM,WAAW,qBAAqB;IACpC,oEAAoE;IACpE,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB,mFAAmF;IACnF,KAAK,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gGAAgG;IAChG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,YAAY,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,qBAAqB,EAAE,MAAM,CAAC;IAC9B,kFAAkF;IAClF,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAEtF;AAgOD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,oBAAoB,CA0HnF"}