@clipboard-health/groundcrew 4.8.0 → 4.10.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 +23 -9
- package/clearance-allow-hosts +5 -1
- package/crew.config.example.ts +9 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +10 -3
- package/dist/commands/setupWorkspace.d.ts.map +1 -1
- package/dist/commands/setupWorkspace.js +7 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/buildSecrets.d.ts +2 -2
- package/dist/lib/buildSecrets.js +2 -2
- package/dist/lib/config.d.ts +20 -7
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +48 -15
- package/dist/lib/launchCommand.d.ts +7 -6
- package/dist/lib/launchCommand.d.ts.map +1 -1
- package/dist/lib/launchCommand.js +38 -30
- package/dist/lib/repositoryHooks.d.ts +8 -0
- package/dist/lib/repositoryHooks.d.ts.map +1 -0
- package/dist/lib/repositoryHooks.js +71 -0
- package/docs/adr/0001-groundcrew-uses-but-does-not-provision-sandboxes.md +1 -1
- package/docs/configuration.md +21 -1
- package/docs/credentials.md +6 -6
- package/docs/setup-hook-agent-prompt.md +46 -49
- package/docs/setup-hooks.md +64 -29
- package/package.json +2 -2
- package/static/demo-fixture.sh +111 -0
- package/static/demo.gif +0 -0
- package/static/demo.tape +42 -0
- package/static/render-demo.sh +40 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
9
|
-
Dispatch your ticket backlog to AI coding agents. One git worktree per ticket, sandboxed by default.
|
|
9
|
+
Dispatch your ticket backlog to local, interactive AI coding agents. One git worktree per ticket, sandboxed by default.
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
@@ -17,16 +17,22 @@
|
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
19
|
<p align="center">
|
|
20
|
-
<img alt="Groundcrew
|
|
20
|
+
<a href="./static/demo.tape"><img alt="Groundcrew dispatching tickets into tmux panes with coding agents running in parallel" src="./static/demo.gif" width="800"></a>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
<p align="center">
|
|
24
|
+
VHS source: <a href="./static/demo.tape">static/demo.tape</a>.
|
|
21
25
|
</p>
|
|
22
26
|
|
|
23
27
|
Groundcrew watches assigned tickets, creates isolated worktrees, launches agent CLIs in dedicated terminals, and leaves each ticket's work on its own PR-ready branch. For the backstory, read _[Tickets to pull requests while you sleep](https://www.clipboardworks.com/resources/blog/tickets-to-pull-requests-while-you-sleep)_.
|
|
24
28
|
|
|
25
29
|
## Why
|
|
26
30
|
|
|
31
|
+
- **Local.** Agents run on your machine with your tools, shell, and credentials. That makes them more steerable than remote agents, and easy to nudge when they drift.
|
|
32
|
+
- **Interactive.** Each ticket launches the real `claude` or `codex` CLI in its own terminal pane, not a wrapper that approximates it. Watch any session live and take over when you need to.
|
|
27
33
|
- **One worktree per ticket.** Agents work in parallel without stepping on each other.
|
|
34
|
+
- **Sandboxed by default.** Safehouse or Docker Sandboxes isolate each agent on the host; `none` is an explicit escape hatch.
|
|
28
35
|
- **Pluggable ticket sources.** Linear by default; Jira and local files via [ticket sources](./docs/ticket-sources.md).
|
|
29
|
-
- **Local-first isolation.** Safehouse, Docker Sandboxes, or an explicit `none` escape hatch.
|
|
30
36
|
- **Multi-agent routing.** Ships `claude` and `codex` presets; bring your own CLI in config.
|
|
31
37
|
|
|
32
38
|
## Prerequisites
|
|
@@ -83,8 +89,7 @@ crew init [--global | --local] [--force] [--dry-run] # create a crew.config.
|
|
|
83
89
|
[--runner <auto|safehouse|sdx|none>] [--model <claude|codex>]
|
|
84
90
|
crew doctor # check setup
|
|
85
91
|
crew status [<TICKET>] # inspect current state or one ticket
|
|
86
|
-
crew run
|
|
87
|
-
crew run --watch # poll forever
|
|
92
|
+
crew run [--watch] # one-shot or --watch forever
|
|
88
93
|
crew start <TICKET> # provision + launch one ticket now
|
|
89
94
|
crew stop <TICKET> [--reason <text>] # stop workspace, keep worktree
|
|
90
95
|
crew resume <TICKET> # reopen a paused ticket
|
|
@@ -106,15 +111,18 @@ export default {
|
|
|
106
111
|
projectDir: "~/dev",
|
|
107
112
|
knownRepositories: ["OWNER/REPO"],
|
|
108
113
|
},
|
|
109
|
-
local: {
|
|
110
|
-
runner: "auto",
|
|
111
|
-
},
|
|
112
114
|
models: {
|
|
113
115
|
default: "claude",
|
|
114
116
|
definitions: {
|
|
115
117
|
claude: {},
|
|
116
118
|
},
|
|
117
119
|
},
|
|
120
|
+
defaults: {
|
|
121
|
+
hooks: {
|
|
122
|
+
// No-op placeholder; replace with your repo's setup, e.g. "npm ci".
|
|
123
|
+
prepareWorktree: "true",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
118
126
|
} satisfies Config;
|
|
119
127
|
```
|
|
120
128
|
|
|
@@ -125,7 +133,7 @@ There is no `linear` config block. Groundcrew reads `GROUNDCREW_LINEAR_API_KEY`
|
|
|
125
133
|
- [Configuration](./docs/configuration.md): discovery order, repo layout, full config table, prompt customization.
|
|
126
134
|
- [Runners](./docs/runners.md): Safehouse, Docker Sandboxes, and the `none` escape hatch.
|
|
127
135
|
- [Credentials](./docs/credentials.md): Linear API keys, 1Password, build secrets, and `preLaunch`.
|
|
128
|
-
- [
|
|
136
|
+
- [Prepare worktree hooks](./docs/setup-hooks.md): `.groundcrew/config.json` `hooks.prepareWorktree` for per-repo dependency setup.
|
|
129
137
|
- [Ticket sources](./docs/ticket-sources.md): custom shell/Jira/local-plan adapters.
|
|
130
138
|
- [Troubleshooting](./docs/troubleshooting.md): common operational pitfalls and fixes.
|
|
131
139
|
|
|
@@ -143,6 +151,12 @@ node --run crew:op -- run --watch
|
|
|
143
151
|
|
|
144
152
|
Both forms discover config through cosmiconfig. Source edits in `src/**` are picked up on the next invocation. Requires Node >= 24.
|
|
145
153
|
|
|
154
|
+
Regenerate the README demo with VHS:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
./static/render-demo.sh
|
|
158
|
+
```
|
|
159
|
+
|
|
146
160
|
## License
|
|
147
161
|
|
|
148
162
|
[MIT](./LICENSE)
|
package/clearance-allow-hosts
CHANGED
|
@@ -64,8 +64,9 @@ raw.githubusercontent.com
|
|
|
64
64
|
release-assets.githubusercontent.com
|
|
65
65
|
results-receiver.actions.githubusercontent.com
|
|
66
66
|
|
|
67
|
-
# npm
|
|
67
|
+
# npm/Conda registries + package websites
|
|
68
68
|
api.npmjs.org
|
|
69
|
+
conda.anaconda.org
|
|
69
70
|
registry.npmjs.org
|
|
70
71
|
www.npmjs.com
|
|
71
72
|
|
|
@@ -83,6 +84,9 @@ formulae.brew.sh
|
|
|
83
84
|
hub.docker.com
|
|
84
85
|
index.docker.io
|
|
85
86
|
json.schemastore.org
|
|
87
|
+
mise-versions.jdx.dev
|
|
86
88
|
nx.dev
|
|
89
|
+
playwright.azureedge.net
|
|
90
|
+
registry.terraform.io
|
|
87
91
|
sourcegraph.com
|
|
88
92
|
vitest.dev
|
package/crew.config.example.ts
CHANGED
|
@@ -35,6 +35,15 @@ export default {
|
|
|
35
35
|
// },
|
|
36
36
|
},
|
|
37
37
|
},
|
|
38
|
+
// Repo-preparation hook: runs after each worktree is created and before the
|
|
39
|
+
// agent launches. The default below is a no-op placeholder. Replace it with
|
|
40
|
+
// your repo's setup, e.g. "npm ci" or "uv sync --dev --frozen". A repo-local
|
|
41
|
+
// `.groundcrew/config.json` hooks.prepareWorktree overrides this per repo.
|
|
42
|
+
defaults: {
|
|
43
|
+
hooks: {
|
|
44
|
+
prepareWorktree: "true",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
38
47
|
// Everything below is optional — defaults shown for reference. Uncomment
|
|
39
48
|
// and edit to override.
|
|
40
49
|
//
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA8KH,wBAAsB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CAgF/C"}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { existsSync, statSync } from "node:fs";
|
|
6
6
|
import { createBoard } from "../lib/board.js";
|
|
7
7
|
import { buildSources, sourcesFromConfig } from "../lib/buildSources.js";
|
|
8
|
-
import {
|
|
8
|
+
import { loadConfigWithSource, } from "../lib/config.js";
|
|
9
9
|
import { detectHostCapabilities, which } from "../lib/host.js";
|
|
10
10
|
import { resolveLocalRunner } from "../lib/localRunner.js";
|
|
11
11
|
import { gatedModels } from "../lib/usage.js";
|
|
@@ -15,6 +15,11 @@ import { resolveWorkspaceKind } from "../lib/workspaces.js";
|
|
|
15
15
|
// catch wrapper + wrapped CLI commands like `safehouse claude --foo`.
|
|
16
16
|
const MAX_TOKENS_PER_CMD = 2;
|
|
17
17
|
const BUILT_IN_MODEL_NAMES = ["claude", "codex"];
|
|
18
|
+
const CONFIG_SOURCE_LABELS = {
|
|
19
|
+
env: "GROUNDCREW_CONFIG",
|
|
20
|
+
project: "project",
|
|
21
|
+
xdg: "global XDG",
|
|
22
|
+
};
|
|
18
23
|
async function checkCmd(cmd, required, hint) {
|
|
19
24
|
const path = await which(cmd);
|
|
20
25
|
const resolvedHint = path ?? hint;
|
|
@@ -148,8 +153,10 @@ export async function doctor() {
|
|
|
148
153
|
writeOutput("=================");
|
|
149
154
|
let config;
|
|
150
155
|
try {
|
|
151
|
-
config = await
|
|
152
|
-
|
|
156
|
+
const { config: loadedConfig, source } = await loadConfigWithSource();
|
|
157
|
+
config = loadedConfig;
|
|
158
|
+
const sourceLabel = CONFIG_SOURCE_LABELS[source.kind];
|
|
159
|
+
writeOutput(`[ok] config loaded — ${source.filepath} (${sourceLabel})`);
|
|
153
160
|
}
|
|
154
161
|
catch (error) {
|
|
155
162
|
writeOutput(`[--] config: ${errorMessage(error)}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAkBnE,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,aAAa,CAAC;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAuBD,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,CAqHf;AAyID,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CA4Cf"}
|
|
@@ -4,6 +4,7 @@ import { openAgentWorkspace, prepareAgentLaunch } from "../lib/agentLaunch.js";
|
|
|
4
4
|
import { createBoard } from "../lib/board.js";
|
|
5
5
|
import { buildSources, sourcesFromConfig } from "../lib/buildSources.js";
|
|
6
6
|
import { buildLaunchCommand } from "../lib/launchCommand.js";
|
|
7
|
+
import { resolvePrepareWorktreeCommand } from "../lib/repositoryHooks.js";
|
|
7
8
|
import { recordRunState } from "../lib/runState.js";
|
|
8
9
|
import { stageBuildSecrets, stagePromptFromTemplate, stageWorkspaceLaunchCommand, } from "../lib/stagedLaunch.js";
|
|
9
10
|
import { naturalIdFromCanonical } from "../lib/ticketSource.js";
|
|
@@ -73,12 +74,17 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
73
74
|
workspaceContinuationInstruction: renderWorkspaceContinuationInstruction(accessHint),
|
|
74
75
|
});
|
|
75
76
|
promptDir = stagedPrompt.directory;
|
|
76
|
-
const
|
|
77
|
+
const prepareWorktreeCommand = resolvePrepareWorktreeCommand({
|
|
78
|
+
worktreeDir: launchDir,
|
|
79
|
+
defaultHooks: config.defaults.hooks,
|
|
80
|
+
});
|
|
81
|
+
const secretsFile = prepareWorktreeCommand === undefined ? undefined : stageBuildSecrets(promptDir);
|
|
77
82
|
const launchCommand = buildLaunchCommand({
|
|
78
83
|
definition,
|
|
79
84
|
promptFile: stagedPrompt.file,
|
|
80
85
|
worktreeDir: launchDir,
|
|
81
86
|
secretsFile,
|
|
87
|
+
prepareWorktreeCommand,
|
|
82
88
|
runner,
|
|
83
89
|
sandboxName,
|
|
84
90
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { orchestrate, type OrchestratorOptions } from "./commands/orchestrator.t
|
|
|
6
6
|
export { resumeWorkspace, type ResumeWorkspaceOptions } from "./commands/resumeWorkspace.ts";
|
|
7
7
|
export { setupWorkspace, type SetupWorkspaceOptions } from "./commands/setupWorkspace.ts";
|
|
8
8
|
export { status, type StatusOptions } from "./commands/status.ts";
|
|
9
|
-
export type { Config, ModelDefinition, ResolvedConfig, SourceConfig } from "./lib/config.ts";
|
|
9
|
+
export type { Config, HookCommands, ModelDefinition, ResolvedConfig, SourceConfig, } from "./lib/config.ts";
|
|
10
10
|
export { loadConfig } from "./lib/config.ts";
|
|
11
11
|
export { readRunState, recordRunState, removeRunState, runStateDirectory, runStatePath, updateRunState, type RunLifecycleState, type RunState, } from "./lib/runState.ts";
|
|
12
12
|
export { fetchBlockersForTicket, fetchInProgressIssueCount, fetchRawLinearIssue, fetchResolvedIssue, isIssueInProgress, isIssueTodo, isTerminalStateType, isTerminalStatusForBlocker, isTerminalStatusForIssue, type RawLinearIssue, } from "./lib/adapters/linear/fetch.ts";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAChG,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,GAC/B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,KAAK,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACnF,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAC7F,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAC1F,OAAO,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAClE,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAChG,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,GAC/B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,KAAK,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACnF,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAC7F,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAC1F,OAAO,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAClE,YAAY,EACV,MAAM,EACN,YAAY,EACZ,eAAe,EACf,cAAc,EACd,YAAY,GACb,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EACL,YAAY,EACZ,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,KAAK,iBAAiB,EACtB,KAAK,QAAQ,GACd,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,EACX,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,EACxB,KAAK,cAAc,GACpB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,oBAAoB,GAC1B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,KAAK,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACvE,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AACpF,OAAO,EACL,eAAe,EACf,KAAK,aAAa,EAClB,aAAa,EACb,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,oBAAoB,EACpB,KAAK,OAAO,IAAI,gBAAgB,EAChC,KAAK,UAAU,IAAI,mBAAmB,EACtC,KAAK,eAAe,EACpB,KAAK,eAAe,IAAI,wBAAwB,EAChD,KAAK,KAAK,IAAI,cAAc,EAC5B,iBAAiB,IAAI,0BAA0B,EAC/C,KAAK,UAAU,IAAI,mBAAmB,EACtC,KAAK,YAAY,GAClB,MAAM,uBAAuB,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Build-time secrets we shuttle into
|
|
3
|
-
* process must not inherit these values.
|
|
2
|
+
* Build-time secrets we shuttle into prepareWorktree phases only. The launched
|
|
3
|
+
* agent process must not inherit these values.
|
|
4
4
|
*/
|
|
5
5
|
export declare const BUILD_SECRET_NAMES: readonly ["NPM_TOKEN", "BUF_TOKEN"];
|
|
6
6
|
//# sourceMappingURL=buildSecrets.d.ts.map
|
package/dist/lib/buildSecrets.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Build-time secrets we shuttle into
|
|
3
|
-
* process must not inherit these values.
|
|
2
|
+
* Build-time secrets we shuttle into prepareWorktree phases only. The launched
|
|
3
|
+
* agent process must not inherit these values.
|
|
4
4
|
*/
|
|
5
5
|
export const BUILD_SECRET_NAMES = ["NPM_TOKEN", "BUF_TOKEN"];
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -8,6 +8,9 @@ export { BUILD_SECRET_NAMES } from "./buildSecrets.ts";
|
|
|
8
8
|
* adapter's `schema.ts` and runs at `buildSources` time, not here.
|
|
9
9
|
*/
|
|
10
10
|
export type SourceConfig = LinearAdapterConfig | ShellAdapterConfig;
|
|
11
|
+
export interface HookCommands {
|
|
12
|
+
prepareWorktree?: string;
|
|
13
|
+
}
|
|
11
14
|
/**
|
|
12
15
|
* Reserved model name. A ticket labeled `agent-any` resolves at runtime
|
|
13
16
|
* to the configured model with the most available session capacity, so
|
|
@@ -47,12 +50,6 @@ export declare const LOCAL_RUNNER_SETTINGS: readonly LocalRunnerSetting[];
|
|
|
47
50
|
export interface SandboxDefinition {
|
|
48
51
|
/** sbx agent name (e.g. "claude", "codex"). */
|
|
49
52
|
agent: string;
|
|
50
|
-
/**
|
|
51
|
-
* Setup command run **inside** the sandbox before the agent exec.
|
|
52
|
-
* Defaults to the shared `.groundcrew/setup.sh --deps-only` convention
|
|
53
|
-
* (see `launchCommand.ts`) when omitted.
|
|
54
|
-
*/
|
|
55
|
-
setupCommand?: string;
|
|
56
53
|
}
|
|
57
54
|
export interface ModelDefinition {
|
|
58
55
|
/**
|
|
@@ -69,7 +66,7 @@ export interface ModelDefinition {
|
|
|
69
66
|
* exec'd and **outside** Safehouse/sdx. Use to mint short-lived credentials
|
|
70
67
|
* (e.g. `export SESSION_TOKEN=...`) that the wrapped `cmd` inherits via
|
|
71
68
|
* the process environment. `{{worktree}}` is replaced before launch.
|
|
72
|
-
* Failures abort launch (unlike
|
|
69
|
+
* Failures abort launch (unlike prepareWorktree, which logs and continues).
|
|
73
70
|
* Not supported for `local.runner` `sdx` in v1.
|
|
74
71
|
*/
|
|
75
72
|
preLaunch?: string;
|
|
@@ -148,6 +145,9 @@ export interface Config {
|
|
|
148
145
|
projectDir: string;
|
|
149
146
|
knownRepositories: string[];
|
|
150
147
|
};
|
|
148
|
+
defaults?: {
|
|
149
|
+
hooks?: HookCommands;
|
|
150
|
+
};
|
|
151
151
|
orchestrator?: {
|
|
152
152
|
maximumInProgress?: number;
|
|
153
153
|
pollIntervalMilliseconds?: number;
|
|
@@ -209,6 +209,9 @@ export interface ResolvedConfig {
|
|
|
209
209
|
projectDir: string;
|
|
210
210
|
knownRepositories: string[];
|
|
211
211
|
};
|
|
212
|
+
defaults: {
|
|
213
|
+
hooks: HookCommands;
|
|
214
|
+
};
|
|
212
215
|
orchestrator: {
|
|
213
216
|
maximumInProgress: number;
|
|
214
217
|
pollIntervalMilliseconds: number;
|
|
@@ -238,6 +241,15 @@ export interface ResolvedConfig {
|
|
|
238
241
|
file: string;
|
|
239
242
|
};
|
|
240
243
|
}
|
|
244
|
+
export type ConfigSourceKind = "env" | "project" | "xdg";
|
|
245
|
+
export interface ConfigSource {
|
|
246
|
+
kind: ConfigSourceKind;
|
|
247
|
+
filepath: string;
|
|
248
|
+
}
|
|
249
|
+
export interface LoadedConfig {
|
|
250
|
+
config: Readonly<ResolvedConfig>;
|
|
251
|
+
source: Readonly<ConfigSource>;
|
|
252
|
+
}
|
|
241
253
|
/**
|
|
242
254
|
* Single source of truth for "is preLaunchEnv asking us to forward anything?"
|
|
243
255
|
*
|
|
@@ -255,5 +267,6 @@ export declare function hasPreLaunchEnv(definition: Pick<ModelDefinition, "preLa
|
|
|
255
267
|
* not enabled from an arbitrary unknown label like `agent-typo`.
|
|
256
268
|
*/
|
|
257
269
|
export declare function isBuiltInModelNotEnabled(config: Pick<ResolvedConfig, "models">, name: string): boolean;
|
|
270
|
+
export declare function loadConfigWithSource(): Promise<Readonly<LoadedConfig>>;
|
|
258
271
|
export declare function loadConfig(): Promise<Readonly<ResolvedConfig>>;
|
|
259
272
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/lib/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAMrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,mBAAmB,GAAG,kBAAkB,CAAC;AAEpE;;;;;GAKG;AACH,eAAO,MAAM,eAAe,QAAQ,CAAC;AAErC;;;;;;GAMG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5D,eAAO,MAAM,uBAAuB,EAAE,SAAS,oBAAoB,EAIzD,CAAC;AAEX;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,KAAK,GAAG,MAAM,CAAC;AAEvD;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,MAAM,CAAC;AAEtD,eAAO,MAAM,qBAAqB,EAAE,SAAS,kBAAkB,EAKrD,CAAC;AAEX;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/config.js
CHANGED
|
@@ -152,6 +152,31 @@ function normalizeOptionalString(value, configKey) {
|
|
|
152
152
|
}
|
|
153
153
|
return value.trim();
|
|
154
154
|
}
|
|
155
|
+
function normalizeHookCommands(value, configKey) {
|
|
156
|
+
if (value === undefined) {
|
|
157
|
+
return {};
|
|
158
|
+
}
|
|
159
|
+
if (!isPlainObject(value)) {
|
|
160
|
+
fail(`${configKey} must be an object`);
|
|
161
|
+
}
|
|
162
|
+
const hooks = {};
|
|
163
|
+
const prepareWorktree = normalizeOptionalString(value["prepareWorktree"], `${configKey}.prepareWorktree`);
|
|
164
|
+
if (prepareWorktree !== undefined) {
|
|
165
|
+
hooks.prepareWorktree = prepareWorktree;
|
|
166
|
+
}
|
|
167
|
+
return hooks;
|
|
168
|
+
}
|
|
169
|
+
function normalizeDefaults(value) {
|
|
170
|
+
if (value === undefined) {
|
|
171
|
+
return { hooks: {} };
|
|
172
|
+
}
|
|
173
|
+
if (!isPlainObject(value)) {
|
|
174
|
+
fail("defaults must be an object");
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
hooks: normalizeHookCommands(value["hooks"], "defaults.hooks"),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
155
180
|
const ENV_VAR_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
156
181
|
function validatePreLaunchEnv(modelName, value) {
|
|
157
182
|
const configPath = `models.definitions.${modelName}.preLaunchEnv`;
|
|
@@ -163,7 +188,7 @@ function validatePreLaunchEnv(modelName, value) {
|
|
|
163
188
|
fail(`${configPath}[${index}] must be a POSIX env var name matching ${ENV_VAR_NAME_PATTERN.source} (got ${JSON.stringify(entry)})`);
|
|
164
189
|
}
|
|
165
190
|
// Build secrets are sourced into the host launch shell, forwarded only to
|
|
166
|
-
// the Safehouse *
|
|
191
|
+
// the Safehouse *prepareWorktree* wrap, and `unset` on the host before the agent
|
|
167
192
|
// wrap is exec'd. Listing one here would silently never reach the agent —
|
|
168
193
|
// fail loudly so the operator picks a different name (or removes the
|
|
169
194
|
// entry) instead of debugging a missing env var at runtime.
|
|
@@ -220,22 +245,20 @@ function normalizeSandbox(value, configKey) {
|
|
|
220
245
|
if (Object.hasOwn(value, "kits")) {
|
|
221
246
|
failRemovedConfigKey(`${configKey}.kits`, "Groundcrew no longer creates sdx sandboxes or applies sandbox kits.");
|
|
222
247
|
}
|
|
223
|
-
|
|
248
|
+
if (Object.hasOwn(value, "setupCommand")) {
|
|
249
|
+
fail(`${configKey}.setupCommand is no longer supported: use repo-local \`.groundcrew/config.json\` \`hooks.prepareWorktree\`, or \`defaults.hooks.prepareWorktree\` in crew.config.ts when you need a fallback for repos without their own hook.`);
|
|
250
|
+
}
|
|
251
|
+
const { agent } = value;
|
|
224
252
|
requireString(agent, `${configKey}.agent`);
|
|
225
253
|
const trimmedAgent = agent.trim();
|
|
226
254
|
if (trimmedAgent.length === 0) {
|
|
227
255
|
fail(`${configKey}.agent must be a non-empty string (got ${JSON.stringify(agent)})`);
|
|
228
256
|
}
|
|
229
|
-
|
|
230
|
-
const normalizedSetup = normalizeOptionalString(setupCommand, `${configKey}.setupCommand`);
|
|
231
|
-
if (normalizedSetup !== undefined) {
|
|
232
|
-
sandbox.setupCommand = normalizedSetup;
|
|
233
|
-
}
|
|
234
|
-
return sandbox;
|
|
257
|
+
return { agent: trimmedAgent };
|
|
235
258
|
}
|
|
236
259
|
function failRemovedConfigKey(configKey, reason) {
|
|
237
260
|
fail(`${configKey} is no longer supported: ${reason} ` +
|
|
238
|
-
"Provision and manage the sandbox yourself with `sbx` (for example `sbx create --name groundcrew-<agent> <agent> <projectDir>`), then keep only `models.definitions.<model>.sandbox.agent`
|
|
261
|
+
"Provision and manage the sandbox yourself with `sbx` (for example `sbx create --name groundcrew-<agent> <agent> <projectDir>`), then keep only `models.definitions.<model>.sandbox.agent` in crew.config.ts.");
|
|
239
262
|
}
|
|
240
263
|
function failIfLegacyModelKeys(name, override) {
|
|
241
264
|
if (!isPlainObject(override)) {
|
|
@@ -423,6 +446,7 @@ function applyDefaults(user) {
|
|
|
423
446
|
projectDir: expandHome(user.workspace.projectDir),
|
|
424
447
|
knownRepositories: user.workspace.knownRepositories,
|
|
425
448
|
},
|
|
449
|
+
defaults: normalizeDefaults(user.defaults),
|
|
426
450
|
orchestrator: { ...DEFAULT_ORCHESTRATOR, ...user.orchestrator },
|
|
427
451
|
models: {
|
|
428
452
|
default: user.models?.default ?? "claude",
|
|
@@ -582,26 +606,28 @@ async function discoverUserConfig() {
|
|
|
582
606
|
if (!existsSync(overridePath)) {
|
|
583
607
|
fail(`GROUNDCREW_CONFIG=${overridePath} not found`);
|
|
584
608
|
}
|
|
585
|
-
|
|
609
|
+
const result = await loadAt(overridePath);
|
|
610
|
+
return { result, source: { kind: "env", filepath: result.filepath } };
|
|
586
611
|
}
|
|
587
612
|
const project = await explorer.search(process.cwd());
|
|
588
613
|
if (project !== null && project.isEmpty !== true) {
|
|
589
|
-
return project;
|
|
614
|
+
return { result: project, source: { kind: "project", filepath: project.filepath } };
|
|
590
615
|
}
|
|
591
616
|
const xdgPath = findXdgConfigFile();
|
|
592
617
|
if (xdgPath !== undefined) {
|
|
593
|
-
|
|
618
|
+
const result = await loadAt(xdgPath);
|
|
619
|
+
return { result, source: { kind: "xdg", filepath: result.filepath } };
|
|
594
620
|
}
|
|
595
621
|
// Throw directly so oxlint's `consistent-return` rule sees a
|
|
596
622
|
// terminating statement; it doesn't track `fail()`'s `never` return.
|
|
597
623
|
throw new Error(`groundcrew config: no crew config found. Create crew.config.ts in your project root, or ${xdgConfigPath("groundcrew", "crew.config.ts")}, or set GROUNDCREW_CONFIG.`);
|
|
598
624
|
}
|
|
599
625
|
let cached;
|
|
600
|
-
export async function
|
|
626
|
+
export async function loadConfigWithSource() {
|
|
601
627
|
if (cached) {
|
|
602
628
|
return cached;
|
|
603
629
|
}
|
|
604
|
-
const result = await discoverUserConfig();
|
|
630
|
+
const { result, source } = await discoverUserConfig();
|
|
605
631
|
const { filepath, isEmpty } = result;
|
|
606
632
|
const userConfig = result.config;
|
|
607
633
|
if (isEmpty === true || !isPlainObject(userConfig)) {
|
|
@@ -612,6 +638,13 @@ export async function loadConfig() {
|
|
|
612
638
|
const resolved = applyDefaults(userConfig);
|
|
613
639
|
validate(resolved);
|
|
614
640
|
setLogFile(resolved.logging.file);
|
|
615
|
-
cached = Object.freeze(
|
|
641
|
+
cached = Object.freeze({
|
|
642
|
+
config: Object.freeze(resolved),
|
|
643
|
+
source: Object.freeze(source),
|
|
644
|
+
});
|
|
616
645
|
return cached;
|
|
617
646
|
}
|
|
647
|
+
export async function loadConfig() {
|
|
648
|
+
const loadedConfig = await loadConfigWithSource();
|
|
649
|
+
return loadedConfig.config;
|
|
650
|
+
}
|
|
@@ -11,11 +11,6 @@ export { shellSingleQuote } from "./shell.ts";
|
|
|
11
11
|
* exercise the catch branch.
|
|
12
12
|
*/
|
|
13
13
|
export declare function resolveSafehouseClearancePath(baseUrl?: string): string;
|
|
14
|
-
/**
|
|
15
|
-
* Per-repo setup hook: if `.groundcrew/setup.sh` exists, run it with
|
|
16
|
-
* `--deps-only`; otherwise no-op.
|
|
17
|
-
*/
|
|
18
|
-
export declare const SETUP_COMMAND = "if [ -f .groundcrew/setup.sh ]; then bash .groundcrew/setup.sh --deps-only; fi";
|
|
19
14
|
interface LaunchCommandArguments {
|
|
20
15
|
definition: ModelDefinition;
|
|
21
16
|
promptFile: string;
|
|
@@ -23,11 +18,17 @@ interface LaunchCommandArguments {
|
|
|
23
18
|
/**
|
|
24
19
|
* Optional path to a `KEY='value'` env file containing build-time
|
|
25
20
|
* secrets (see `BUILD_SECRET_NAMES`). Sourced on the host shell before
|
|
26
|
-
*
|
|
21
|
+
* prepareWorktree; for the sdx runner the names are propagated into the sandbox
|
|
27
22
|
* via `sbx exec -e KEY`. Always unset before exec'ing the agent so the
|
|
28
23
|
* agent process never inherits them.
|
|
29
24
|
*/
|
|
30
25
|
secretsFile?: string | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Optional repo-preparation hook resolved by the caller from the freshly
|
|
28
|
+
* created worktree's `.groundcrew/config.json`, falling back to
|
|
29
|
+
* `defaults.hooks.prepareWorktree` from crew.config.ts.
|
|
30
|
+
*/
|
|
31
|
+
prepareWorktreeCommand?: string | undefined;
|
|
31
32
|
/**
|
|
32
33
|
* Concrete local isolation backend chosen for this launch. Resolved
|
|
33
34
|
* from `config.local.runner` via `resolveLocalRunner` before this
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"launchCommand.d.ts","sourceRoot":"","sources":["../../src/lib/launchCommand.ts"],"names":[],"mappings":"AAGA,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;
|
|
1
|
+
{"version":3,"file":"launchCommand.d.ts","sourceRoot":"","sources":["../../src/lib/launchCommand.ts"],"names":[],"mappings":"AAGA,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;AA2KD,UAAU,sBAAsB;IAC9B,UAAU,EAAE,eAAe,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;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;CAClC;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,sBAAsB,GAAG,MAAM,CA0B7E"}
|
|
@@ -25,11 +25,6 @@ export function resolveSafehouseClearancePath(baseUrl = import.meta.url) {
|
|
|
25
25
|
return path.resolve(path.dirname(clearancePackageJson), "safehouse", "safehouse-clearance");
|
|
26
26
|
}
|
|
27
27
|
const SAFEHOUSE_CLEARANCE_WRAPPER_PATH = resolveSafehouseClearancePath();
|
|
28
|
-
/**
|
|
29
|
-
* Per-repo setup hook: if `.groundcrew/setup.sh` exists, run it with
|
|
30
|
-
* `--deps-only`; otherwise no-op.
|
|
31
|
-
*/
|
|
32
|
-
export const SETUP_COMMAND = "if [ -f .groundcrew/setup.sh ]; then bash .groundcrew/setup.sh --deps-only; fi";
|
|
33
28
|
function renderAgentCommand(arguments_) {
|
|
34
29
|
return arguments_.agentCmd
|
|
35
30
|
.replaceAll("{{worktree}}", shellSingleQuote(arguments_.worktreeDir))
|
|
@@ -38,17 +33,17 @@ function renderAgentCommand(arguments_) {
|
|
|
38
33
|
function renderPreLaunch(preLaunch, worktreeDir) {
|
|
39
34
|
return preLaunch.replaceAll("{{worktree}}", shellSingleQuote(worktreeDir));
|
|
40
35
|
}
|
|
41
|
-
function
|
|
36
|
+
function prepareWorktreeWithStatusReporting(prepareWorktreeCommand) {
|
|
42
37
|
return [
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
'if [ "$
|
|
38
|
+
`(${prepareWorktreeCommand})`,
|
|
39
|
+
"prepare_status=$?",
|
|
40
|
+
'if [ "$prepare_status" -ne 0 ]; then echo "groundcrew prepareWorktree hook exited with status $prepare_status; continuing to agent." >&2; fi',
|
|
46
41
|
].join("; ");
|
|
47
42
|
}
|
|
48
43
|
/**
|
|
49
44
|
* Source a `KEY='value'` file with auto-export so build-time secrets land
|
|
50
|
-
* in the shell env before
|
|
51
|
-
* the file disappeared between staging and launch.
|
|
45
|
+
* in the shell env before prepareWorktree runs. The `-f` guard keeps it a
|
|
46
|
+
* no-op if the file disappeared between staging and launch.
|
|
52
47
|
*/
|
|
53
48
|
function sourceSecretsLine(secretsFile) {
|
|
54
49
|
return `if [ -f ${shellSingleQuote(secretsFile)} ]; then set -a && . ${shellSingleQuote(secretsFile)} && set +a; fi`;
|
|
@@ -196,8 +191,9 @@ export function buildLaunchCommand(arguments_) {
|
|
|
196
191
|
/**
|
|
197
192
|
* The Safehouse wrap applies only when `runner === "safehouse"` and `cmd` does
|
|
198
193
|
* not already invoke `safehouse` itself. A `safehouse …` cmd owns its own
|
|
199
|
-
* sandbox flags, and we can't splice
|
|
200
|
-
* those (and the `none` runner) fall through to the unwrapped host
|
|
194
|
+
* sandbox flags, and we can't splice prepareWorktree into a command we don't
|
|
195
|
+
* control, so those (and the `none` runner) fall through to the unwrapped host
|
|
196
|
+
* path.
|
|
201
197
|
*/
|
|
202
198
|
function shouldWrapWithSafehouse(arguments_) {
|
|
203
199
|
if (arguments_.runner !== "safehouse") {
|
|
@@ -207,8 +203,9 @@ function shouldWrapWithSafehouse(arguments_) {
|
|
|
207
203
|
}
|
|
208
204
|
/**
|
|
209
205
|
* Unsandboxed host launch (`runner === "none"`, or a `safehouse …` cmd that
|
|
210
|
-
* brings its own wrap).
|
|
211
|
-
* host shell because there is no groundcrew-managed sandbox to run them
|
|
206
|
+
* brings its own wrap). prepareWorktree, secret sourcing, and the agent all run
|
|
207
|
+
* on the host shell because there is no groundcrew-managed sandbox to run them
|
|
208
|
+
* inside.
|
|
212
209
|
*/
|
|
213
210
|
function buildUnwrappedHostLaunchCommand(arguments_) {
|
|
214
211
|
const promptDir = path.dirname(arguments_.promptFile);
|
|
@@ -220,8 +217,10 @@ function buildUnwrappedHostLaunchCommand(arguments_) {
|
|
|
220
217
|
const lines = [
|
|
221
218
|
...hostTrapAndCd({ worktreeDir: arguments_.worktreeDir, promptDir }),
|
|
222
219
|
...hostSourceSecrets(arguments_.secretsFile),
|
|
223
|
-
setupWithStatusReporting(SETUP_COMMAND),
|
|
224
220
|
];
|
|
221
|
+
if (arguments_.prepareWorktreeCommand !== undefined) {
|
|
222
|
+
lines.push(prepareWorktreeWithStatusReporting(arguments_.prepareWorktreeCommand));
|
|
223
|
+
}
|
|
225
224
|
if (arguments_.secretsFile !== undefined) {
|
|
226
225
|
lines.push(unsetSecretsLine());
|
|
227
226
|
}
|
|
@@ -237,9 +236,11 @@ function buildUnwrappedHostLaunchCommand(arguments_) {
|
|
|
237
236
|
/**
|
|
238
237
|
* Safehouse launch. Two Safehouse wraps, by design:
|
|
239
238
|
*
|
|
240
|
-
* 1. **
|
|
241
|
-
*
|
|
242
|
-
* egress-restricted,
|
|
239
|
+
* 1. **prepareWorktree wrap**: plain
|
|
240
|
+
* `safehouse-clearance ... sh -c '<prepareWorktree>'`. Runs the repo
|
|
241
|
+
* preparation hook filesystem-isolated and egress-restricted,
|
|
242
|
+
* **without** inheriting agent-profile grants. Omitted entirely when no
|
|
243
|
+
* hook command is configured.
|
|
243
244
|
* 2. **Agent wrap**: `safehouse-clearance "$shim" -c '<exec agent>' sh "$_p"`
|
|
244
245
|
* where `$shim` is a `mktemp`-d symlink to `/bin/sh` named after the
|
|
245
246
|
* agent (e.g. `claude`). Safehouse selects the matching agent profile
|
|
@@ -253,14 +254,14 @@ function buildUnwrappedHostLaunchCommand(arguments_) {
|
|
|
253
254
|
* from which `stageBuildSecrets` reads them) nor file-sourced values — and keeps
|
|
254
255
|
* stale same-named ambient credentials from being forwarded. `secrets.env` is
|
|
255
256
|
* then sourced into the host launch shell so Safehouse can forward build secrets
|
|
256
|
-
* into the **
|
|
257
|
-
* them otherwise). After
|
|
257
|
+
* into the **prepareWorktree wrap** via `--env-pass=` (Safehouse's `--env=FILE` mode strips
|
|
258
|
+
* them otherwise). After prepareWorktree returns, `BUILD_SECRET_NAMES` are `unset` again
|
|
258
259
|
* on the host so they cannot reach the agent wrap.
|
|
259
260
|
*
|
|
260
261
|
* `--env-pass` composition is split per wrap (deliberate, post PR #128):
|
|
261
|
-
* -
|
|
262
|
+
* - prepareWorktree wrap forwards build secrets only.
|
|
262
263
|
* - Agent wrap forwards `preLaunchEnv` names only. preLaunch credentials never
|
|
263
|
-
* reach the profile-neutral
|
|
264
|
+
* reach the profile-neutral prepare phase.
|
|
264
265
|
*/
|
|
265
266
|
function buildSafehouseLaunchCommand(arguments_) {
|
|
266
267
|
const promptDir = path.dirname(arguments_.promptFile);
|
|
@@ -270,15 +271,17 @@ function buildSafehouseLaunchCommand(arguments_) {
|
|
|
270
271
|
worktreeDir: arguments_.worktreeDir,
|
|
271
272
|
sandboxName: "",
|
|
272
273
|
});
|
|
273
|
-
const
|
|
274
|
+
const prepareWorktreeCommand = arguments_.prepareWorktreeCommand === undefined
|
|
275
|
+
? undefined
|
|
276
|
+
: prepareWorktreeWithStatusReporting(arguments_.prepareWorktreeCommand);
|
|
274
277
|
const agentCommand = `exec ${agentCmd} "$@"`;
|
|
275
|
-
// Split --env-pass per wrap: the
|
|
278
|
+
// Split --env-pass per wrap: the prepareWorktree wrap only needs build secrets (so
|
|
276
279
|
// `npm install` etc. can authenticate); the agent wrap only needs the
|
|
277
280
|
// user's preLaunchEnv (build secrets are `unset` on the host between the
|
|
278
281
|
// two wraps, so forwarding them here would silently no-op). Keeps preLaunch
|
|
279
|
-
// credentials out of the profile-neutral
|
|
282
|
+
// credentials out of the profile-neutral prepare phase — see PR #128.
|
|
280
283
|
// Trailing space keeps each flag separated from the next argv token.
|
|
281
|
-
const
|
|
284
|
+
const prepareWorktreeEnvPassFlag = arguments_.secretsFile === undefined ? "" : `--env-pass=${BUILD_SECRET_NAMES.join(",")} `;
|
|
282
285
|
const preLaunchEnvNames = arguments_.definition.preLaunchEnv ?? [];
|
|
283
286
|
const agentEnvPassFlag = preLaunchEnvNames.length === 0 ? "" : `--env-pass=${preLaunchEnvNames.join(",")} `;
|
|
284
287
|
const safehouseWrapper = shellSingleQuote(SAFEHOUSE_CLEARANCE_WRAPPER_PATH);
|
|
@@ -298,7 +301,10 @@ function buildSafehouseLaunchCommand(arguments_) {
|
|
|
298
301
|
lines.push(unsetEnvironmentLine([...BUILD_SECRET_NAMES, ...preLaunchEnvNames]));
|
|
299
302
|
lines.push(renderPreLaunch(arguments_.definition.preLaunch, arguments_.worktreeDir));
|
|
300
303
|
}
|
|
301
|
-
lines.push(...hostSourceSecrets(arguments_.secretsFile), `_p=$(cat ${shellSingleQuote(arguments_.promptFile)})`, `rm -rf ${shellSingleQuote(promptDir)}
|
|
304
|
+
lines.push(...hostSourceSecrets(arguments_.secretsFile), `_p=$(cat ${shellSingleQuote(arguments_.promptFile)})`, `rm -rf ${shellSingleQuote(promptDir)}`);
|
|
305
|
+
if (prepareWorktreeCommand !== undefined) {
|
|
306
|
+
lines.push(`${safehouseWrapper} ${prepareWorktreeEnvPassFlag}sh -c ${shellSingleQuote(prepareWorktreeCommand)}`);
|
|
307
|
+
}
|
|
302
308
|
if (arguments_.secretsFile !== undefined) {
|
|
303
309
|
lines.push(unsetSecretsLine());
|
|
304
310
|
}
|
|
@@ -321,8 +327,10 @@ function buildSdxLaunchCommand(arguments_) {
|
|
|
321
327
|
worktreeDir: arguments_.worktreeDir,
|
|
322
328
|
sandboxName: arguments_.sandboxName,
|
|
323
329
|
});
|
|
324
|
-
const
|
|
325
|
-
|
|
330
|
+
const innerParts = [];
|
|
331
|
+
if (arguments_.prepareWorktreeCommand !== undefined) {
|
|
332
|
+
innerParts.push(prepareWorktreeWithStatusReporting(arguments_.prepareWorktreeCommand));
|
|
333
|
+
}
|
|
326
334
|
if (arguments_.secretsFile !== undefined) {
|
|
327
335
|
innerParts.push(unsetSecretsLine());
|
|
328
336
|
}
|