@clipboard-health/groundcrew 3.2.0 → 3.3.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.
Files changed (34) hide show
  1. package/README.md +2 -2
  2. package/crew.config.example.ts +19 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +6 -0
  5. package/dist/commands/sandbox/auth.d.ts +3 -0
  6. package/dist/commands/sandbox/auth.d.ts.map +1 -0
  7. package/dist/commands/sandbox/auth.js +227 -0
  8. package/dist/commands/sandbox/index.d.ts +2 -0
  9. package/dist/commands/sandbox/index.d.ts.map +1 -0
  10. package/dist/commands/sandbox/index.js +47 -0
  11. package/dist/commands/sandbox/inspect.d.ts +2 -0
  12. package/dist/commands/sandbox/inspect.d.ts.map +1 -0
  13. package/dist/commands/sandbox/inspect.js +18 -0
  14. package/dist/commands/sandbox/lifecycle.d.ts +7 -0
  15. package/dist/commands/sandbox/lifecycle.d.ts.map +1 -0
  16. package/dist/commands/sandbox/lifecycle.js +68 -0
  17. package/dist/commands/sandbox/model.d.ts +10 -0
  18. package/dist/commands/sandbox/model.d.ts.map +1 -0
  19. package/dist/commands/sandbox/model.js +37 -0
  20. package/dist/commands/sandbox/picker.d.ts +20 -0
  21. package/dist/commands/sandbox/picker.d.ts.map +1 -0
  22. package/dist/commands/sandbox/picker.js +23 -0
  23. package/dist/lib/agentLaunch.d.ts.map +1 -1
  24. package/dist/lib/agentLaunch.js +1 -0
  25. package/dist/lib/config.d.ts +71 -1
  26. package/dist/lib/config.d.ts.map +1 -1
  27. package/dist/lib/config.js +79 -1
  28. package/dist/lib/dockerSandbox.d.ts +12 -8
  29. package/dist/lib/dockerSandbox.d.ts.map +1 -1
  30. package/dist/lib/dockerSandbox.js +33 -22
  31. package/dist/lib/sandboxGitDefaults.d.ts +10 -0
  32. package/dist/lib/sandboxGitDefaults.d.ts.map +1 -0
  33. package/dist/lib/sandboxGitDefaults.js +31 -0
  34. package/package.json +2 -1
package/README.md CHANGED
@@ -522,9 +522,9 @@ Cross-team projects work — the orchestrator caches the in-progress state ID pe
522
522
  </details>
523
523
 
524
524
  <details>
525
- <summary>Claude launches in bypass-permissions mode by default</summary>
525
+ <summary>Claude launches in auto mode by default</summary>
526
526
 
527
- Groundcrew creates isolated per-ticket worktrees for unattended runs, so the shipped `claude` command is `claude --permission-mode bypassPermissions` to avoid workspace-trust and tool-permission prompts blocking automation. Override `models.definitions.claude.cmd` for a stricter mode.
527
+ Groundcrew creates isolated per-ticket worktrees for unattended runs, so the shipped `claude` command is `claude --permission-mode auto` to let Claude proceed without stopping for clarifying questions while keeping its built-in safety prompts intact. Override `models.definitions.claude.cmd` for `bypassPermissions` if you need to suppress tool-permission prompts entirely, or for a stricter mode.
528
528
 
529
529
  </details>
530
530
 
@@ -92,6 +92,25 @@ export default {
92
92
  // // macOS when you need an agent to use Docker safely.
93
93
  // local: { runner: "auto" },
94
94
  //
95
+ // // Additional auth recipes for `crew sandbox auth <model> <tool>`. The
96
+ // // shipped recipes (claude/codex/cursor agents + github tool) are merged
97
+ // // with whatever you declare here; your recipe wins on key collision.
98
+ // // Describe each tool's in-sandbox login + status commands and a regex
99
+ // // that matches its logged-in output. Omit `kind` for cross-cutting
100
+ // // tools that should appear in every sandbox's picker; set
101
+ // // `kind: "agent"` to scope a recipe to a single sbx agent.
102
+ // sandbox: {
103
+ // authRecipes: {
104
+ // gcloud: {
105
+ // displayName: "gcloud",
106
+ // binary: "gcloud",
107
+ // loginArgs: ["auth", "login", "--no-launch-browser"],
108
+ // statusArgs: ["auth", "list", "--filter=status:ACTIVE", "--format=value(account)"],
109
+ // authenticatedPattern: /@/,
110
+ // },
111
+ // },
112
+ // },
113
+ //
95
114
  // prompts: {
96
115
  // // Keep personal workflow instructions next to this config, for example
97
116
  // // `${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/initial-prompt.md`.
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAmKA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BvD"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAyKA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BvD"}
package/dist/cli.js CHANGED
@@ -5,6 +5,7 @@ import { initConfigCli } from "./commands/init.js";
5
5
  import { interruptWorkspaceCli } from "./commands/interruptWorkspace.js";
6
6
  import { orchestrate } from "./commands/orchestrator.js";
7
7
  import { resumeWorkspaceCli } from "./commands/resumeWorkspace.js";
8
+ import { sandboxCli } from "./commands/sandbox/index.js";
8
9
  import { setupReposCli } from "./commands/setupRepos.js";
9
10
  import { setupWorkspaceCli } from "./commands/setupWorkspace.js";
10
11
  import { errorMessage, readTicketArgument, writeError, writeOutput } from "./lib/util.js";
@@ -108,6 +109,11 @@ const SUBCOMMANDS = {
108
109
  usage: "<ticket>",
109
110
  invoke: resumeWorkspaceCli,
110
111
  },
112
+ sandbox: {
113
+ summary: "Manage Docker Sandboxes (sbx) for configured models",
114
+ usage: "<list|ensure|regenerate|auth|rm> [...args]",
115
+ invoke: sandboxCli,
116
+ },
111
117
  setup: {
112
118
  summary: "Project-level setup commands (currently: repos)",
113
119
  usage: "repos [--dry-run] [<repo>...]",
@@ -0,0 +1,3 @@
1
+ import type { ResolvedConfig } from "../../lib/config.ts";
2
+ export declare function runAuth(config: ResolvedConfig, argv: string[]): Promise<void>;
3
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/commands/sandbox/auth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAc,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAuNtE,wBAAsB,OAAO,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAcnF"}
@@ -0,0 +1,227 @@
1
+ import { runCommandAsync } from "../../lib/commandRunner.js";
2
+ import { writeOutput } from "../../lib/util.js";
3
+ import { ensureOne } from "./lifecycle.js";
4
+ import { resolveModel, sandboxModels } from "./model.js";
5
+ import { pickTools } from "./picker.js";
6
+ /**
7
+ * Built-in recipes shipped with crew. Users register additional tools
8
+ * by adding entries under `sandbox.authRecipes` in `crew.config.ts`;
9
+ * a user recipe under the same key overrides the built-in.
10
+ *
11
+ * `kind: "agent"` recipes only appear in the picker when the current
12
+ * sandbox's agent matches the recipe key. `kind: "tool"` (the default
13
+ * for user recipes) is cross-cutting and always appears.
14
+ */
15
+ const BUILTIN_AUTH_RECIPES = {
16
+ claude: {
17
+ displayName: "Claude",
18
+ loginArgs: ["auth", "login"],
19
+ statusArgs: ["auth", "status"],
20
+ authenticatedPattern: /"loggedIn"\s*:\s*true/,
21
+ kind: "agent",
22
+ },
23
+ codex: {
24
+ displayName: "Codex",
25
+ // `--device-auth` keeps the OAuth flow headless: codex prints a URL
26
+ // and a code instead of trying to open a browser inside the sandbox.
27
+ loginArgs: ["login", "--device-auth"],
28
+ statusArgs: ["login", "status"],
29
+ // Match "Logged in using …" but not a hypothetical "Not logged in".
30
+ authenticatedPattern: /(^|\W)Logged in using\b/i,
31
+ kind: "agent",
32
+ },
33
+ cursor: {
34
+ displayName: "Cursor",
35
+ binary: "cursor-agent",
36
+ loginArgs: ["login"],
37
+ statusArgs: ["status"],
38
+ // Authenticated output is "✓ Logged in as <email>"; the unauthenticated
39
+ // output is "Not logged in", which a loose /Logged in/i would falsely
40
+ // match.
41
+ authenticatedPattern: /Logged in as\b/i,
42
+ kind: "agent",
43
+ // cursor-agent tries to open a browser by default and silently
44
+ // writes a partial auth file when xdg-open fails; this env var
45
+ // switches it to a device-code flow that works without a browser.
46
+ env: { NO_OPEN_BROWSER: "1" },
47
+ },
48
+ github: {
49
+ displayName: "GitHub CLI",
50
+ binary: "gh",
51
+ loginArgs: ["auth", "login"],
52
+ statusArgs: ["auth", "status"],
53
+ authenticatedPattern: /Logged in to github\.com/i,
54
+ kind: "tool",
55
+ },
56
+ };
57
+ function binaryFor(toolKey, recipe) {
58
+ return recipe.binary ?? toolKey;
59
+ }
60
+ function envFlags(recipe) {
61
+ const entries = Object.entries(recipe.env ?? {});
62
+ return entries.flatMap(([key, value]) => ["-e", `${key}=${value}`]);
63
+ }
64
+ // User-supplied recipes can carry arbitrary tokens; wrap each in single
65
+ // quotes so spaces and shell metacharacters can't change how the in-sandbox
66
+ // shell tokenizes the status command.
67
+ function shellQuote(value) {
68
+ return `'${value.replaceAll("'", `'\\''`)}'`;
69
+ }
70
+ async function probeAuthStatus(sandboxName, toolKey, recipe) {
71
+ // Some CLIs print status to stderr instead of stdout (codex does
72
+ // this). Fold stderr into stdout via the in-sandbox shell so the
73
+ // pattern match sees the message regardless of which stream it
74
+ // landed on.
75
+ const innerCommand = `${[binaryFor(toolKey, recipe), ...recipe.statusArgs]
76
+ .map(shellQuote)
77
+ .join(" ")} 2>&1`;
78
+ try {
79
+ const output = await runCommandAsync("sbx", [
80
+ "exec",
81
+ ...envFlags(recipe),
82
+ sandboxName,
83
+ "sh",
84
+ "-c",
85
+ innerCommand,
86
+ ]);
87
+ // Reset lastIndex so a /g or /y user recipe doesn't carry state
88
+ // across probes and return a false negative.
89
+ recipe.authenticatedPattern.lastIndex = 0;
90
+ return recipe.authenticatedPattern.test(output);
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ }
96
+ async function loginAndVerify(input) {
97
+ const { sandboxName, toolKey, recipe, modelName, gitDefaults } = input;
98
+ const binary = binaryFor(toolKey, recipe);
99
+ writeOutput(`${sandboxName}: launching '${recipe.displayName}' login...`);
100
+ writeOutput("Complete the login flow in the prompts/browser, then return here.");
101
+ await runCommandAsync("sbx", ["exec", "-it", ...envFlags(recipe), sandboxName, binary, ...recipe.loginArgs], { stdio: "inherit" });
102
+ writeOutput("");
103
+ writeOutput(`${sandboxName}: verifying '${recipe.displayName}' authentication...`);
104
+ const authenticated = await probeAuthStatus(sandboxName, toolKey, recipe);
105
+ if (authenticated) {
106
+ writeOutput(`${sandboxName}: '${recipe.displayName}' authenticated.`);
107
+ if (gitDefaults && toolKey === "github" && binary === "gh") {
108
+ await runGhSetupGit(sandboxName);
109
+ }
110
+ return;
111
+ }
112
+ writeOutput(`${sandboxName}: could not confirm '${recipe.displayName}' authentication — re-run 'crew sandbox auth ${modelName}' to retry.`);
113
+ }
114
+ /**
115
+ * Register `gh` as git's credential helper inside the sandbox so HTTPS
116
+ * pushes succeed without prompting. Best-effort — a failure here doesn't
117
+ * undo the login itself, so we warn and move on.
118
+ */
119
+ async function runGhSetupGit(sandboxName) {
120
+ writeOutput(`${sandboxName}: wiring 'gh' as git credential helper...`);
121
+ try {
122
+ await runCommandAsync("sbx", ["exec", sandboxName, "gh", "auth", "setup-git"]);
123
+ writeOutput(`${sandboxName}: 'gh auth setup-git' done.`);
124
+ }
125
+ catch (error) {
126
+ writeOutput(`${sandboxName}: warning — 'gh auth setup-git' failed: ${String(error)}`);
127
+ }
128
+ }
129
+ function availableRecipes(config) {
130
+ const merged = {
131
+ ...BUILTIN_AUTH_RECIPES,
132
+ ...config.sandbox.authRecipes,
133
+ };
134
+ return Object.entries(merged)
135
+ .map(([key, recipe]) => ({ key, recipe }))
136
+ .toSorted((a, b) => a.key.localeCompare(b.key));
137
+ }
138
+ function shouldShowInPicker(entry, currentAgent) {
139
+ // Tools (the default) appear in every sandbox. Agent recipes only
140
+ // appear when they match the current sandbox's agent, so opening
141
+ // 'crew sandbox auth codex' doesn't list Claude or Cursor.
142
+ const kind = entry.recipe.kind ?? "tool";
143
+ return kind === "tool" || entry.key === currentAgent;
144
+ }
145
+ const AUTH_USAGE = "Usage: crew sandbox auth <model> | --all";
146
+ function parseAuthArgs(config, argv) {
147
+ const positionals = [];
148
+ let all = false;
149
+ for (const argument of argv) {
150
+ if (argument === "--all") {
151
+ all = true;
152
+ continue;
153
+ }
154
+ if (argument.startsWith("-")) {
155
+ throw new Error(`crew sandbox auth: unknown option '${argument}'`);
156
+ }
157
+ positionals.push(argument);
158
+ }
159
+ if (all && positionals.length > 0) {
160
+ throw new Error("crew sandbox auth: --all cannot be combined with a model name");
161
+ }
162
+ if (all) {
163
+ const models = sandboxModels(config);
164
+ if (models.length === 0) {
165
+ throw new Error("crew sandbox auth --all: no sandbox-bearing models configured");
166
+ }
167
+ return { models: models.map((model) => ({ modelName: model.modelName, model })) };
168
+ }
169
+ const [modelName, ...extras] = positionals;
170
+ if (modelName === undefined || extras.length > 0) {
171
+ throw new Error(AUTH_USAGE);
172
+ }
173
+ return { models: [{ modelName, model: resolveModel(config, modelName) }] };
174
+ }
175
+ export async function runAuth(config, argv) {
176
+ const { models } = parseAuthArgs(config, argv);
177
+ for (const [index, { modelName, model }] of models.entries()) {
178
+ if (models.length > 1) {
179
+ writeOutput("");
180
+ writeOutput(`════ ${modelName} (${index + 1}/${models.length}) ════`);
181
+ }
182
+ writeOutput(`${model.sandboxName}: ensuring sandbox is up...`);
183
+ // oxlint-disable-next-line no-await-in-loop -- each sandbox is interactive; running them sequentially keeps the prompts coherent
184
+ await ensureOne(config, model);
185
+ writeOutput("");
186
+ // oxlint-disable-next-line no-await-in-loop -- intentionally sequential, see above
187
+ await runAuthInteractive(config, model, modelName);
188
+ }
189
+ }
190
+ async function runAuthInteractive(config, model, modelName) {
191
+ const recipes = availableRecipes(config).filter((entry) => shouldShowInPicker(entry, model.sandbox.agent));
192
+ writeOutput(`${model.sandboxName}: probing authentication status for ${recipes.length} tools...`);
193
+ const statuses = await Promise.all(recipes.map(async ({ key, recipe }) => ({
194
+ key,
195
+ recipe,
196
+ authenticated: await probeAuthStatus(model.sandboxName, key, recipe),
197
+ })));
198
+ const choices = statuses.map(({ key, recipe, authenticated }) => ({
199
+ key,
200
+ label: `${recipe.displayName} (${key})`,
201
+ authenticated,
202
+ }));
203
+ writeOutput("");
204
+ const selectedKeys = await pickTools(choices);
205
+ if (selectedKeys.length === 0) {
206
+ writeOutput("Nothing selected. Exiting.");
207
+ return;
208
+ }
209
+ const selectedRecipes = new Map(statuses.map((entry) => [entry.key, entry.recipe]));
210
+ for (const key of selectedKeys) {
211
+ const recipe = selectedRecipes.get(key);
212
+ /* v8 ignore next 3 @preserve - defensive; selectedKeys come from the same map */
213
+ if (recipe === undefined) {
214
+ continue;
215
+ }
216
+ writeOutput("");
217
+ writeOutput(`── ${recipe.displayName} ──`);
218
+ // oxlint-disable-next-line no-await-in-loop -- each login is interactive; running them sequentially keeps the prompts coherent
219
+ await loginAndVerify({
220
+ sandboxName: model.sandboxName,
221
+ toolKey: key,
222
+ recipe,
223
+ modelName,
224
+ gitDefaults: config.sandbox.gitDefaults,
225
+ });
226
+ }
227
+ }
@@ -0,0 +1,2 @@
1
+ export declare function sandboxCli(argv: string[]): Promise<void>;
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/sandbox/index.ts"],"names":[],"mappings":"AAkBA,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA8B9D"}
@@ -0,0 +1,47 @@
1
+ import { loadConfig } from "../../lib/config.js";
2
+ import { runAuth } from "./auth.js";
3
+ import { runList } from "./inspect.js";
4
+ import { runEnsure, runRegenerate, runRemove } from "./lifecycle.js";
5
+ const USAGE = [
6
+ "Usage: crew sandbox <verb> [...args]",
7
+ "",
8
+ "Verbs:",
9
+ " list Show every groundcrew-owned sandbox known to sbx",
10
+ " ensure [<model>] Provision the sandbox for one model, or all when omitted",
11
+ " regenerate <model>|--all Tear down and recreate from current template/kits",
12
+ " auth <model>|--all Open a checkbox picker of every tool available in <model>'s",
13
+ " sandbox and run the login flow for each one you select;",
14
+ " --all loops through every configured sandbox in turn",
15
+ " rm <model> Remove the sandbox for a model",
16
+ ].join("\n");
17
+ export async function sandboxCli(argv) {
18
+ const [verb, ...rest] = argv;
19
+ if (verb === undefined) {
20
+ throw new Error(USAGE);
21
+ }
22
+ switch (verb) {
23
+ case "list": {
24
+ await runList();
25
+ return;
26
+ }
27
+ case "ensure": {
28
+ await runEnsure(await loadConfig(), rest);
29
+ return;
30
+ }
31
+ case "regenerate": {
32
+ await runRegenerate(await loadConfig(), rest);
33
+ return;
34
+ }
35
+ case "auth": {
36
+ await runAuth(await loadConfig(), rest);
37
+ return;
38
+ }
39
+ case "rm": {
40
+ await runRemove(await loadConfig(), rest);
41
+ return;
42
+ }
43
+ default: {
44
+ throw new Error(`Unknown sandbox sub-verb: ${verb}\n${USAGE}`);
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,2 @@
1
+ export declare function runList(): Promise<void>;
2
+ //# sourceMappingURL=inspect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../../../src/commands/sandbox/inspect.ts"],"names":[],"mappings":"AAKA,wBAAsB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAc7C"}
@@ -0,0 +1,18 @@
1
+ import { runCommandAsync } from "../../lib/commandRunner.js";
2
+ import { writeOutput } from "../../lib/util.js";
3
+ const SANDBOX_NAME_PREFIX = "groundcrew-";
4
+ export async function runList() {
5
+ const output = await runCommandAsync("sbx", ["ls"]);
6
+ const names = output
7
+ .split("\n")
8
+ .map((line) => line.trim().split(/\s+/)[0])
9
+ .filter((name) => name !== undefined && name.startsWith(SANDBOX_NAME_PREFIX))
10
+ .map((name) => name.slice(SANDBOX_NAME_PREFIX.length));
11
+ if (names.length === 0) {
12
+ writeOutput("(none)");
13
+ return;
14
+ }
15
+ for (const name of names) {
16
+ writeOutput(name);
17
+ }
18
+ }
@@ -0,0 +1,7 @@
1
+ import type { ResolvedConfig } from "../../lib/config.ts";
2
+ import { type SandboxModel } from "./model.ts";
3
+ export declare function ensureOne(config: ResolvedConfig, model: SandboxModel, alreadyExists?: boolean): Promise<void>;
4
+ export declare function runEnsure(config: ResolvedConfig, argv: string[]): Promise<void>;
5
+ export declare function runRegenerate(config: ResolvedConfig, argv: string[]): Promise<void>;
6
+ export declare function runRemove(config: ResolvedConfig, argv: string[]): Promise<void>;
7
+ //# sourceMappingURL=lifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../src/commands/sandbox/lifecycle.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAG1D,OAAO,EAAsC,KAAK,YAAY,EAAiB,MAAM,YAAY,CAAC;AAElG,wBAAsB,SAAS,CAC7B,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,YAAY,EACnB,aAAa,CAAC,EAAE,OAAO,GACtB,OAAO,CAAC,IAAI,CAAC,CAQf;AAMD,wBAAsB,SAAS,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBrF;AAUD,wBAAsB,aAAa,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBzF;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAMrF"}
@@ -0,0 +1,68 @@
1
+ import { resolve } from "node:path";
2
+ import { runCommandAsync } from "../../lib/commandRunner.js";
3
+ import { ensureSandbox, sandboxExists } from "../../lib/dockerSandbox.js";
4
+ import { writeOutput } from "../../lib/util.js";
5
+ import { requireOnePositional, resolveModel, sandboxModels } from "./model.js";
6
+ export async function ensureOne(config, model, alreadyExists) {
7
+ await ensureSandbox({
8
+ sandboxName: model.sandboxName,
9
+ sandbox: model.sandbox,
10
+ mountPath: resolve(config.workspace.projectDir),
11
+ gitDefaults: config.sandbox.gitDefaults,
12
+ ...(alreadyExists === undefined ? {} : { alreadyExists }),
13
+ });
14
+ }
15
+ async function removeOne(model) {
16
+ await runCommandAsync("sbx", ["rm", "--force", model.sandboxName]);
17
+ }
18
+ export async function runEnsure(config, argv) {
19
+ const targets = argv.length === 0
20
+ ? sandboxModels(config)
21
+ : [resolveModel(config, requireOnePositional(argv, "Usage: crew sandbox ensure [<model>]"))];
22
+ if (targets.length === 0) {
23
+ writeOutput("No sandbox models configured.");
24
+ return;
25
+ }
26
+ for (const model of targets) {
27
+ // oxlint-disable-next-line no-await-in-loop -- one sandbox at a time; probe then ensure
28
+ const existed = await sandboxExists(model.sandboxName);
29
+ writeOutput(existed
30
+ ? `${model.sandboxName}: already exists`
31
+ : `${model.sandboxName}: creating (agent=${model.sandbox.agent}, template=${model.sandbox.template ?? "default"})`);
32
+ // oxlint-disable-next-line no-await-in-loop -- sbx create is intentionally sequential
33
+ await ensureOne(config, model, existed);
34
+ if (!existed) {
35
+ writeOutput(`${model.sandboxName}: created`);
36
+ }
37
+ }
38
+ }
39
+ function regenerateTargets(config, argv) {
40
+ const target = requireOnePositional(argv, "Usage: crew sandbox regenerate <model>|--all");
41
+ if (target === "--all") {
42
+ return sandboxModels(config);
43
+ }
44
+ return [resolveModel(config, target)];
45
+ }
46
+ export async function runRegenerate(config, argv) {
47
+ const targets = regenerateTargets(config, argv);
48
+ if (targets.length === 0) {
49
+ writeOutput("No sandbox models configured.");
50
+ return;
51
+ }
52
+ for (const model of targets) {
53
+ writeOutput(`${model.sandboxName}: removing existing sandbox...`);
54
+ // oxlint-disable-next-line no-await-in-loop -- sbx rm/create are intentionally sequential
55
+ await removeOne(model);
56
+ writeOutput(`${model.sandboxName}: creating (agent=${model.sandbox.agent}, template=${model.sandbox.template ?? "default"})`);
57
+ // oxlint-disable-next-line no-await-in-loop -- sbx rm/create are intentionally sequential
58
+ await ensureOne(config, model, false);
59
+ writeOutput(`${model.sandboxName}: regenerated`);
60
+ }
61
+ }
62
+ export async function runRemove(config, argv) {
63
+ const modelName = requireOnePositional(argv, "Usage: crew sandbox rm <model>");
64
+ const model = resolveModel(config, modelName);
65
+ writeOutput(`${model.sandboxName}: removing...`);
66
+ await removeOne(model);
67
+ writeOutput(`${model.sandboxName}: removed`);
68
+ }
@@ -0,0 +1,10 @@
1
+ import type { ResolvedConfig, SandboxDefinition } from "../../lib/config.ts";
2
+ export interface SandboxModel {
3
+ modelName: string;
4
+ sandbox: SandboxDefinition;
5
+ sandboxName: string;
6
+ }
7
+ export declare function sandboxModels(config: ResolvedConfig): SandboxModel[];
8
+ export declare function resolveModel(config: ResolvedConfig, modelName: string): SandboxModel;
9
+ export declare function requireOnePositional(argv: string[], usage: string): string;
10
+ //# sourceMappingURL=model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../../src/commands/sandbox/model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAG7E,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,iBAAiB,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,YAAY,EAAE,CAcpE;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY,CAapF;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAM1E"}
@@ -0,0 +1,37 @@
1
+ import { sandboxNameFor } from "../../lib/dockerSandbox.js";
2
+ export function sandboxModels(config) {
3
+ const models = [];
4
+ for (const [modelName, definition] of Object.entries(config.models.definitions)) {
5
+ const { sandbox } = definition;
6
+ if (sandbox === undefined) {
7
+ continue;
8
+ }
9
+ models.push({
10
+ modelName,
11
+ sandbox,
12
+ sandboxName: sandboxNameFor({ agent: sandbox.agent }),
13
+ });
14
+ }
15
+ return models;
16
+ }
17
+ export function resolveModel(config, modelName) {
18
+ const definition = config.models.definitions[modelName];
19
+ if (definition === undefined) {
20
+ throw new Error(`crew sandbox: unknown model '${modelName}'`);
21
+ }
22
+ if (definition.sandbox === undefined) {
23
+ throw new Error(`crew sandbox: model '${modelName}' has no sandbox config`);
24
+ }
25
+ return {
26
+ modelName,
27
+ sandbox: definition.sandbox,
28
+ sandboxName: sandboxNameFor({ agent: definition.sandbox.agent }),
29
+ };
30
+ }
31
+ export function requireOnePositional(argv, usage) {
32
+ const [first, ...rest] = argv;
33
+ if (first === undefined || rest.length > 0) {
34
+ throw new Error(usage);
35
+ }
36
+ return first;
37
+ }
@@ -0,0 +1,20 @@
1
+ export interface ToolChoice {
2
+ /** Recipe key (e.g. "claude", "github"). Returned in the selection. */
3
+ key: string;
4
+ /** Human-friendly label shown in the prompt. */
5
+ label: string;
6
+ /** Auth status decoration: ✓ when authenticated, ○ otherwise. */
7
+ authenticated: boolean;
8
+ }
9
+ /**
10
+ * Show an interactive checkbox picker so the engineer chooses which
11
+ * tools to authenticate. Items marked `authenticated` start unchecked
12
+ * (no need to re-auth); unauthed items start checked (default action
13
+ * is "auth what's missing"). The returned array is the list of `key`
14
+ * values that the engineer left checked when they confirmed.
15
+ *
16
+ * Extracted to its own module so tests can vi.mock it and skip stdin
17
+ * interaction; the real implementation pulls @inquirer/checkbox.
18
+ */
19
+ export declare function pickTools(choices: readonly ToolChoice[]): Promise<readonly string[]>;
20
+ //# sourceMappingURL=picker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"picker.d.ts","sourceRoot":"","sources":["../../../src/commands/sandbox/picker.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,uEAAuE;IACvE,GAAG,EAAE,MAAM,CAAC;IACZ,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,aAAa,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAW1F"}
@@ -0,0 +1,23 @@
1
+ import checkbox from "@inquirer/checkbox";
2
+ /**
3
+ * Show an interactive checkbox picker so the engineer chooses which
4
+ * tools to authenticate. Items marked `authenticated` start unchecked
5
+ * (no need to re-auth); unauthed items start checked (default action
6
+ * is "auth what's missing"). The returned array is the list of `key`
7
+ * values that the engineer left checked when they confirmed.
8
+ *
9
+ * Extracted to its own module so tests can vi.mock it and skip stdin
10
+ * interaction; the real implementation pulls @inquirer/checkbox.
11
+ */
12
+ export async function pickTools(choices) {
13
+ const selected = await checkbox({
14
+ message: "Select tools to authenticate (space to toggle, enter to confirm):",
15
+ choices: choices.map((choice) => ({
16
+ name: `${choice.authenticated ? "✓" : "○"} ${choice.label}`,
17
+ value: choice.key,
18
+ checked: !choice.authenticated,
19
+ })),
20
+ pageSize: Math.max(choices.length, 1),
21
+ });
22
+ return selected;
23
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"agentLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/agentLaunch.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAOhF,UAAU,mBAAmB;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;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,CA6B/B;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,UAAU,EAAE,eAAe,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhB;AAED,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":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAOhF,UAAU,mBAAmB;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;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,CA6B/B;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,UAAU,EAAE,eAAe,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhB;AAED,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"}
@@ -37,6 +37,7 @@ export async function ensureAgentSandbox(input) {
37
37
  sandboxName: input.sandboxName,
38
38
  sandbox: input.definition.sandbox,
39
39
  mountPath: resolve(input.config.workspace.projectDir),
40
+ gitDefaults: input.config.sandbox.gitDefaults,
40
41
  }, input.signal);
41
42
  }
42
43
  }
@@ -58,13 +58,49 @@ export interface SandboxDefinition {
58
58
  */
59
59
  setupCommand?: string;
60
60
  }
61
+ /**
62
+ * Recipe used by `crew sandbox auth <model>` to drive an interactive
63
+ * login flow inside a sbx sandbox and then verify it. The flow is
64
+ * picker-driven — no positional `<tool>` argument; the picker lists
65
+ * every recipe visible to the current sandbox.
66
+ *
67
+ * `binary` defaults to the recipe key (typically the agent or CLI name).
68
+ * `authenticatedPattern` matches against combined stdout+stderr from
69
+ * `statusArgs` — exit code alone isn't reliable because some CLIs
70
+ * report "not logged in" while still exiting 0.
71
+ * `kind` controls visibility in the interactive picker: `"agent"`
72
+ * recipes are scoped to a specific sbx agent and only appear when you
73
+ * `auth` against that agent's sandbox; `"tool"` recipes (default)
74
+ * appear in every sandbox's picker because they're cross-cutting
75
+ * (github, npm, gcloud, …). Defaults to `"tool"` when omitted.
76
+ *
77
+ * Ship-side recipes for `claude`, `codex`, and `cursor` live in
78
+ * `src/commands/sandbox/auth.ts`; users register additional tools
79
+ * under `sandbox.authRecipes` in their config.
80
+ */
81
+ export interface AuthRecipe {
82
+ displayName: string;
83
+ binary?: string;
84
+ loginArgs: readonly string[];
85
+ statusArgs: readonly string[];
86
+ authenticatedPattern: RegExp;
87
+ kind?: "agent" | "tool";
88
+ /**
89
+ * Environment variables passed to `sbx exec` for both the login and
90
+ * status calls. Use this for CLIs whose default flow assumes a
91
+ * browser or other host-only feature — e.g. cursor-agent wants
92
+ * `NO_OPEN_BROWSER=1` to print a device code instead of trying to
93
+ * launch a browser inside the sandbox.
94
+ */
95
+ env?: Record<string, string>;
96
+ }
61
97
  export interface ModelDefinition {
62
98
  /**
63
99
  * Shell command launched for the model. Wrapped with Safehouse/clearance
64
100
  * for execution. The rendered prompt is appended as a single quoted
65
101
  * positional argument. `{{worktree}}` is replaced before launch.
66
102
  *
67
- * Keep this agent-native (e.g., `claude --permission-mode bypassPermissions`).
103
+ * Keep this agent-native (e.g., `claude --permission-mode auto`).
68
104
  * Groundcrew adds the Safehouse wrapper.
69
105
  */
70
106
  cmd: string;
@@ -192,6 +228,32 @@ export interface Config {
192
228
  local?: {
193
229
  runner?: LocalRunnerSetting;
194
230
  };
231
+ /**
232
+ * Sandbox-wide settings. `authRecipes` lets users register additional
233
+ * tools (github, npm, gcloud, …) for `crew sandbox auth <model>` to
234
+ * authenticate inside the sandbox. The auth flow is picker-driven —
235
+ * registered recipes show up in the picker alongside the shipped ones,
236
+ * and a user recipe under the same key (e.g. "claude") overrides the
237
+ * shipped one.
238
+ */
239
+ sandbox?: {
240
+ authRecipes?: Record<string, AuthRecipe>;
241
+ /**
242
+ * When true (default), every `crew sandbox ensure` / `auth` run applies
243
+ * a small set of git defaults inside the sandbox so robot commits push
244
+ * over `gh`-managed HTTPS regardless of how the user cloned the repo:
245
+ *
246
+ * - disable GPG signing for commits and tags
247
+ * - rewrite `git@github.com:` and `ssh://git@github.com/` URLs to
248
+ * `https://github.com/` so push uses gh's credential helper
249
+ * - after a successful `github` auth recipe login, run
250
+ * `gh auth setup-git` inside the sandbox
251
+ *
252
+ * Set `false` to skip both the git-config block and the post-login
253
+ * `gh auth setup-git` step.
254
+ */
255
+ gitDefaults?: boolean;
256
+ };
195
257
  logging?: {
196
258
  /**
197
259
  * Append-mode log file destination. `log()` and `logEvent()` tee here
@@ -261,6 +323,14 @@ export interface ResolvedConfig {
261
323
  local: {
262
324
  runner: LocalRunnerSetting;
263
325
  };
326
+ /**
327
+ * Sandbox-wide settings. Always present after defaults; `authRecipes`
328
+ * is `{}` when the user provides none.
329
+ */
330
+ sandbox: {
331
+ authRecipes: Record<string, AuthRecipe>;
332
+ gitDefaults: boolean;
333
+ };
264
334
  logging: {
265
335
  file: string;
266
336
  };
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
@@ -42,7 +42,7 @@ const DEFAULT_ORCHESTRATOR = {
42
42
  };
43
43
  const DEFAULT_MODEL_DEFINITIONS = {
44
44
  claude: {
45
- cmd: "claude --permission-mode bypassPermissions",
45
+ cmd: "claude --permission-mode auto",
46
46
  color: "#C15F3C",
47
47
  usage: { codexbar: { provider: "claude" } },
48
48
  },
@@ -132,6 +132,15 @@ function normalizeOptionalString(value, path) {
132
132
  }
133
133
  return value.trim();
134
134
  }
135
+ function normalizeOptionalBoolean(value, path) {
136
+ if (value === undefined) {
137
+ return undefined;
138
+ }
139
+ if (typeof value !== "boolean") {
140
+ fail(`${path} must be a boolean`);
141
+ }
142
+ return value;
143
+ }
135
144
  function normalizeOptionalStringArray(value, path) {
136
145
  if (value === undefined) {
137
146
  return undefined;
@@ -325,6 +334,11 @@ function requireObject(value, path) {
325
334
  fail(`${path} must be an object (got ${JSON.stringify(value)})`);
326
335
  }
327
336
  }
337
+ function requireOptionalObject(value, path) {
338
+ if (value !== undefined && !isPlainObject(value)) {
339
+ fail(`${path} must be an object`);
340
+ }
341
+ }
328
342
  function normalizeProject(value, index) {
329
343
  const path = `linear.projects[${index}]`;
330
344
  if (!isPlainObject(value)) {
@@ -429,6 +443,65 @@ function normalizeProjects(linear) {
429
443
  });
430
444
  return resolved;
431
445
  }
446
+ function normalizeAuthRecipes(value, path) {
447
+ if (value === undefined) {
448
+ return {};
449
+ }
450
+ if (!isPlainObject(value)) {
451
+ fail(`${path} must be an object`);
452
+ }
453
+ const recipes = {};
454
+ for (const [key, raw] of Object.entries(value)) {
455
+ const recipePath = `${path}.${key}`;
456
+ if (!isPlainObject(raw)) {
457
+ fail(`${recipePath} must be an object`);
458
+ }
459
+ const { displayName, binary, loginArgs, statusArgs, authenticatedPattern, kind, env } = raw;
460
+ requireString(displayName, `${recipePath}.displayName`);
461
+ const loginArray = normalizeOptionalStringArray(loginArgs, `${recipePath}.loginArgs`);
462
+ const statusArray = normalizeOptionalStringArray(statusArgs, `${recipePath}.statusArgs`);
463
+ if (loginArray === undefined) {
464
+ fail(`${recipePath}.loginArgs is required`);
465
+ }
466
+ if (statusArray === undefined) {
467
+ fail(`${recipePath}.statusArgs is required`);
468
+ }
469
+ if (!(authenticatedPattern instanceof RegExp)) {
470
+ fail(`${recipePath}.authenticatedPattern must be a RegExp`);
471
+ }
472
+ const recipe = {
473
+ displayName,
474
+ loginArgs: loginArray,
475
+ statusArgs: statusArray,
476
+ authenticatedPattern,
477
+ };
478
+ const binaryString = normalizeOptionalString(binary, `${recipePath}.binary`);
479
+ if (binaryString !== undefined) {
480
+ recipe.binary = binaryString;
481
+ }
482
+ if (kind !== undefined) {
483
+ if (kind !== "agent" && kind !== "tool") {
484
+ fail(`${recipePath}.kind must be "agent" or "tool"`);
485
+ }
486
+ recipe.kind = kind;
487
+ }
488
+ if (env !== undefined) {
489
+ if (!isPlainObject(env)) {
490
+ fail(`${recipePath}.env must be an object`);
491
+ }
492
+ const normalizedEnv = {};
493
+ for (const [envKey, envValue] of Object.entries(env)) {
494
+ if (typeof envValue !== "string") {
495
+ fail(`${recipePath}.env.${envKey} must be a string`);
496
+ }
497
+ normalizedEnv[envKey] = envValue;
498
+ }
499
+ recipe.env = normalizedEnv;
500
+ }
501
+ recipes[key] = recipe;
502
+ }
503
+ return recipes;
504
+ }
432
505
  function applyDefaults(user) {
433
506
  // Guard the top-level shape before reading nested fields, so a
434
507
  // malformed runtime config produces a `groundcrew config: ...` error
@@ -443,6 +516,7 @@ function applyDefaults(user) {
443
516
  if (Object.hasOwn(user, "remote")) {
444
517
  fail("remote is no longer supported: groundcrew runs locally via safehouse/sdx/none; remove the remote block from your config");
445
518
  }
519
+ requireOptionalObject(user.sandbox, "sandbox");
446
520
  const userLocal = user.local;
447
521
  if (userLocal !== undefined && !isPlainObject(userLocal)) {
448
522
  fail("local must be an object");
@@ -470,6 +544,10 @@ function applyDefaults(user) {
470
544
  local: {
471
545
  runner: normalizeLocalRunner(userLocal?.runner, "local.runner") ?? "auto",
472
546
  },
547
+ sandbox: {
548
+ authRecipes: normalizeAuthRecipes(user.sandbox?.authRecipes, "sandbox.authRecipes"),
549
+ gitDefaults: normalizeOptionalBoolean(user.sandbox?.gitDefaults, "sandbox.gitDefaults") ?? true,
550
+ },
473
551
  logging: {
474
552
  file: expandHome(normalizeOptionalString(user.logging?.file, "logging.file") ?? defaultLogFile()),
475
553
  },
@@ -25,15 +25,19 @@ interface EnsureSandboxArguments {
25
25
  * clone) are visible to `sbx exec -w <worktreeDir>` after creation.
26
26
  */
27
27
  mountPath: string;
28
+ /**
29
+ * When true, apply the standard git defaults inside the sandbox after
30
+ * it exists (idempotent, runs whether the sandbox was just created or
31
+ * already there). See `sandboxGitDefaults.ts` for what gets set.
32
+ */
33
+ gitDefaults: boolean;
34
+ /**
35
+ * Result of an earlier `sandboxExists` probe by the caller, used to
36
+ * skip the initial `sbx ls` here. Leave undefined to let this function
37
+ * probe on its own.
38
+ */
39
+ alreadyExists?: boolean;
28
40
  }
29
- /**
30
- * Idempotent guard: ensure a Docker Sandboxes container exists for the
31
- * given repository + model. Probes `sbx ls`; if `sandboxName` is missing,
32
- * calls `sbx create --name <name> [--template <t>] [--kit <k>]... <agent>
33
- * <mountPath>` to provision it. First-time agent auth still happens inside
34
- * the sandbox the first time `sbx exec` runs the agent — `create` only
35
- * provisions the container, it does not attach.
36
- */
37
41
  export declare function ensureSandbox(arguments_: EnsureSandboxArguments, signal?: AbortSignal): Promise<void>;
38
42
  export {};
39
43
  //# sourceMappingURL=dockerSandbox.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dockerSandbox.d.ts","sourceRoot":"","sources":["../../src/lib/dockerSandbox.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAMpE;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAM/F;AAED,UAAU,sBAAsB;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,iBAAiB,CAAC;IAC3B;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,UAAU,EAAE,sBAAsB,EAClC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAqBf"}
1
+ {"version":3,"file":"dockerSandbox.d.ts","sourceRoot":"","sources":["../../src/lib/dockerSandbox.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAGrD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAMpE;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAM/F;AAED,UAAU,sBAAsB;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,iBAAiB,CAAC;IAC3B;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,WAAW,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAsBD,wBAAsB,aAAa,CACjC,UAAU,EAAE,sBAAsB,EAClC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAuBf"}
@@ -1,4 +1,5 @@
1
1
  import { runCommandAsync } from "./commandRunner.js";
2
+ import { applyGitDefaults } from "./sandboxGitDefaults.js";
2
3
  /**
3
4
  * Derive a deterministic sbx sandbox name from the sbx agent so every
4
5
  * groundcrew model that targets the same agent reuses one sandbox across
@@ -29,30 +30,40 @@ export async function sandboxExists(sandboxName, signal) {
29
30
  * Idempotent guard: ensure a Docker Sandboxes container exists for the
30
31
  * given repository + model. Probes `sbx ls`; if `sandboxName` is missing,
31
32
  * calls `sbx create --name <name> [--template <t>] [--kit <k>]... <agent>
32
- * <mountPath>` to provision it. First-time agent auth still happens inside
33
- * the sandbox the first time `sbx exec` runs the agent — `create` only
34
- * provisions the container, it does not attach.
33
+ * <mountPath>` to provision it. Once the container exists (newly created
34
+ * or pre-existing), applies the standard git defaults when enabled.
35
+ * First-time agent auth still happens inside the sandbox the first time
36
+ * `sbx exec` runs the agent — `create` only provisions the container, it
37
+ * does not attach.
35
38
  */
36
- export async function ensureSandbox(arguments_, signal) {
37
- if (await sandboxExists(arguments_.sandboxName, signal)) {
38
- return;
39
- }
40
- const createArguments = ["create", "--name", arguments_.sandboxName];
41
- if (arguments_.sandbox.template !== undefined) {
42
- createArguments.push("--template", arguments_.sandbox.template);
43
- }
44
- for (const kit of arguments_.sandbox.kits ?? []) {
45
- createArguments.push("--kit", kit);
39
+ async function resolveExistence(arguments_, signal) {
40
+ if (arguments_.alreadyExists === undefined) {
41
+ return await sandboxExists(arguments_.sandboxName, signal);
46
42
  }
47
- createArguments.push(arguments_.sandbox.agent, arguments_.mountPath);
48
- const options = signal === undefined ? {} : { signal };
49
- try {
50
- await runCommandAsync("sbx", createArguments, options);
51
- }
52
- catch (error) {
53
- if (await sandboxExists(arguments_.sandboxName, signal)) {
54
- return;
43
+ return arguments_.alreadyExists;
44
+ }
45
+ export async function ensureSandbox(arguments_, signal) {
46
+ const existed = await resolveExistence(arguments_, signal);
47
+ if (!existed) {
48
+ const createArguments = ["create", "--name", arguments_.sandboxName];
49
+ if (arguments_.sandbox.template !== undefined) {
50
+ createArguments.push("--template", arguments_.sandbox.template);
51
+ }
52
+ for (const kit of arguments_.sandbox.kits ?? []) {
53
+ createArguments.push("--kit", kit);
54
+ }
55
+ createArguments.push(arguments_.sandbox.agent, arguments_.mountPath);
56
+ const options = signal === undefined ? {} : { signal };
57
+ try {
58
+ await runCommandAsync("sbx", createArguments, options);
55
59
  }
56
- throw error;
60
+ catch (error) {
61
+ if (!(await sandboxExists(arguments_.sandboxName, signal))) {
62
+ throw error;
63
+ }
64
+ }
65
+ }
66
+ if (arguments_.gitDefaults) {
67
+ await applyGitDefaults({ sandboxName: arguments_.sandboxName }, signal);
57
68
  }
58
69
  }
@@ -0,0 +1,10 @@
1
+ interface ApplyGitDefaultsArguments {
2
+ sandboxName: string;
3
+ }
4
+ /**
5
+ * Apply the standard git defaults inside `sandboxName`. Idempotent —
6
+ * safe to call on every `ensure`/`auth` run to repair drift.
7
+ */
8
+ export declare function applyGitDefaults(arguments_: ApplyGitDefaultsArguments, signal?: AbortSignal): Promise<void>;
9
+ export {};
10
+ //# sourceMappingURL=sandboxGitDefaults.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandboxGitDefaults.d.ts","sourceRoot":"","sources":["../../src/lib/sandboxGitDefaults.ts"],"names":[],"mappings":"AAyBA,UAAU,yBAAyB;IACjC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,yBAAyB,EACrC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAOf"}
@@ -0,0 +1,31 @@
1
+ import { runCommandAsync } from "./commandRunner.js";
2
+ /**
3
+ * Git defaults applied inside every sandbox when `sandbox.gitDefaults`
4
+ * is enabled (the default).
5
+ *
6
+ * - Disable GPG signing — robot commits inside the sandbox have no key
7
+ * and would otherwise fail or end up unsigned silently.
8
+ * - Rewrite GitHub SSH URLs to HTTPS so push/fetch go through the `gh`
9
+ * credential helper (wired by `gh auth setup-git` after a successful
10
+ * `crew sandbox auth` github login), regardless of how the user
11
+ * originally cloned the repo on the host.
12
+ *
13
+ * `url.<base>.insteadOf` is multi-valued in git; `--unset-all` before
14
+ * `--add` keeps the set identical across repeated runs instead of
15
+ * appending duplicates.
16
+ */
17
+ const GIT_DEFAULT_COMMANDS = [
18
+ "git config --global commit.gpgsign false",
19
+ "git config --global tag.gpgsign false",
20
+ '(git config --global --unset-all url."https://github.com/".insteadOf 2>/dev/null || true)',
21
+ 'git config --global --add url."https://github.com/".insteadOf "git@github.com:"',
22
+ 'git config --global --add url."https://github.com/".insteadOf "ssh://git@github.com/"',
23
+ ].join(" && ");
24
+ /**
25
+ * Apply the standard git defaults inside `sandboxName`. Idempotent —
26
+ * safe to call on every `ensure`/`auth` run to repair drift.
27
+ */
28
+ export async function applyGitDefaults(arguments_, signal) {
29
+ const options = signal === undefined ? {} : { signal };
30
+ await runCommandAsync("sbx", ["exec", arguments_.sandboxName, "sh", "-c", GIT_DEFAULT_COMMANDS], options);
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/groundcrew",
3
- "version": "3.2.0",
3
+ "version": "3.3.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",
@@ -68,6 +68,7 @@
68
68
  },
69
69
  "dependencies": {
70
70
  "@clipboard-health/clearance": "1.0.8",
71
+ "@inquirer/checkbox": "5.1.5",
71
72
  "@linear/sdk": "85.0.0",
72
73
  "cosmiconfig": "9.0.1",
73
74
  "tslib": "2.8.1",