@clipboard-health/groundcrew 2.1.1 → 2.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.
- package/README.md +15 -8
- package/configExample.ts +9 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +39 -12
- package/dist/commands/setupWorkspace.d.ts.map +1 -1
- package/dist/commands/setupWorkspace.js +68 -8
- package/dist/lib/config.d.ts +63 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +75 -7
- package/dist/lib/dockerSandbox.d.ts +40 -0
- package/dist/lib/dockerSandbox.d.ts.map +1 -0
- package/dist/lib/dockerSandbox.js +58 -0
- package/dist/lib/host.d.ts +10 -0
- package/dist/lib/host.d.ts.map +1 -1
- package/dist/lib/host.js +8 -3
- package/dist/lib/launchCommand.d.ts +17 -3
- package/dist/lib/launchCommand.d.ts.map +1 -1
- package/dist/lib/launchCommand.js +66 -8
- package/dist/lib/localRunner.d.ts +22 -1
- package/dist/lib/localRunner.d.ts.map +1 -1
- package/dist/lib/localRunner.js +48 -5
- package/dist/lib/workspaces.d.ts +5 -0
- package/dist/lib/workspaces.d.ts.map +1 -1
- package/dist/lib/workspaces.js +156 -42
- package/dist/lib/worktrees.d.ts +5 -0
- package/dist/lib/worktrees.d.ts.map +1 -1
- package/dist/lib/worktrees.js +14 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,12 @@ This installs the `crew` binary. `@clipboard-health/clearance` is pulled in tran
|
|
|
15
15
|
|
|
16
16
|
## Quickstart
|
|
17
17
|
|
|
18
|
-
1. **Install prereqs.** Node 24, `git`, `cmux` _or_ `tmux`, and the agent CLIs themselves (`claude`, `codex`, `cursor-agent`, ...).
|
|
18
|
+
1. **Install prereqs.** Node 24, `git`, `cmux` _or_ `tmux`, and the agent CLIs themselves (`claude`, `codex`, `cursor-agent`, ...). Pick a local isolation backend below depending on platform and what the agent needs to do. Optional: `codexbar` for session-usage gating. The `workspaceKind` config key picks the workspace backend (`auto` resolves to cmux when installed, else tmux).
|
|
19
|
+
- **`safehouse`** (macOS default) — [Safehouse](https://agent-safehouse.dev/) on `PATH`. The fastest local backend; cannot safely give the agent Docker.
|
|
20
|
+
- **`sdx`** (Linux default, macOS opt-in) — [Docker Sandboxes](https://docs.docker.com/sandboxes/) (`sbx`) on `PATH`. Required when a ticket needs the agent to use Docker (`docker build`, `docker run`, integration tests). Each model that should run under sdx needs a `sandbox: { agent: "<sbx-agent>" }` block in `config.ts` so groundcrew knows which sbx agent to address.
|
|
21
|
+
- **`none`** — explicit unsandboxed escape hatch. Never picked implicitly; `crew doctor` warns when it is configured.
|
|
22
|
+
|
|
23
|
+
Groundcrew resolves `local.runner` per platform: macOS → `safehouse`, Linux/WSL → `sdx`. Set `local.runner` to `"safehouse" | "sdx" | "none"` to override; leave at `"auto"` for the platform default.
|
|
19
24
|
|
|
20
25
|
2. **Create a Linear project to scope your work.** Any team works — make a project inside it and drop tickets in. The orchestrator polls by project, not by team, so you don't need a dedicated team.
|
|
21
26
|
|
|
@@ -63,11 +68,11 @@ This installs the `crew` binary. `@clipboard-health/clearance` is pulled in tran
|
|
|
63
68
|
|
|
64
69
|
`LINEAR_API_KEY` continues to work for existing setups; if both variables are set, `GROUNDCREW_LINEAR_API_KEY` wins.
|
|
65
70
|
|
|
66
|
-
5. **Prepare the runner and agent auth.** Groundcrew
|
|
71
|
+
5. **Prepare the runner and agent auth.** Groundcrew uses a `cmux` or `tmux` workspace hosting the resolved local backend (`safehouse`, `sdx`, or `none`) plus locally authenticated agent CLIs. Setup fails fast when the resolved backend's binary or platform requirement is missing — `safehouse` requires macOS + `safehouse` on PATH; `sdx` requires macOS or Linux + `sbx` on PATH. `models.isolation` and per-model `isolation` are legacy keys and fail config validation. Per-model `sandbox` blocks are accepted again and used by the `sdx` runner.
|
|
67
72
|
|
|
68
|
-
|
|
73
|
+
For sdx, sandboxes are created automatically on first launch. Groundcrew names them deterministically as `groundcrew-<repository>-<model>`, probes `sbx ls`, and runs `sbx create [--template ...] [--kit ...] <agent> <projectDir>` when missing — `projectDir` is the mount so every per-ticket worktree (created as a sibling of the bare clone) is visible inside the sandbox. First-time agent auth happens inside the sandbox the first time the agent runs via `sbx exec`. To bootstrap manually instead, run `sbx create --name groundcrew-<repo>-<model> <agent> <projectDir>` once.
|
|
69
74
|
|
|
70
|
-
6. **Set the clearance allowlist for local
|
|
75
|
+
6. **Set the clearance allowlist for local Safehouse runs.** Only applies when `local.runner` resolves to `safehouse`. Groundcrew starts `clearance` from `@clipboard-health/clearance` on `http://127.0.0.1:19999` (skipping the launch if something is already listening) and runs the agent through the bundled `safehouse-clearance` wrapper. Clearance refuses to start without an allowlist — see [its README](https://github.com/ClipboardHealth/core-utils/tree/main/packages/clearance) for the proxy's env vars, log paths, and DNS rules. The shortest path is to set the env before `crew run`:
|
|
71
76
|
|
|
72
77
|
```bash
|
|
73
78
|
CLEARANCE_ALLOW_HOSTS="api.openai.com,auth.openai.com,api.anthropic.com,mcp.linear.app,api.linear.app" \
|
|
@@ -112,12 +117,14 @@ Required fields are marked **required**; everything else has a default and can b
|
|
|
112
117
|
| `orchestrator.sessionLimitPercentage` | `85` | Number in `(0, 100]`. A model whose codexbar session window exceeds this percentage is skipped that tick. |
|
|
113
118
|
| `models.default` | `"claude"` | Tiebreak for `agent-any` resolution and fallback for explicit but unknown `agent-*` labels. Also used by `crew run --ticket <TICKET>` for unlabeled tickets. `crew run` without `--ticket` ignores unlabeled tickets and does not apply this default. Must exist in `models.definitions`. |
|
|
114
119
|
| `models.definitions` | `{ claude, codex }` | Agent definitions. Additive merge with shipped defaults. |
|
|
115
|
-
| `models.definitions.<name>.cmd` | — | Shell command launched for the model. Runs in the worktree through
|
|
120
|
+
| `models.definitions.<name>.cmd` | — | Shell command launched for the model. Runs in the worktree through the resolved `local.runner`. `{{worktree}}` is replaced before launch; `{{sandbox}}` expands to the sbx sandbox name under the sdx runner and an empty string otherwise. |
|
|
116
121
|
| `models.definitions.<name>.color` | — | Color for the workspace status pill (cmux only; tmux silently drops it). |
|
|
117
122
|
| `models.definitions.<name>.usage` | optional | If set, codexbar usage is fetched for this model and gated by `sessionLimitPercentage`. Omit to never gate. When `usage.codexbar.source` is omitted, groundcrew uses `auto` on macOS and `cli` elsewhere. |
|
|
123
|
+
| `models.definitions.<name>.sandbox` | optional | Docker Sandboxes binding for the model. Required at launch when `local.runner` resolves to `sdx`. Fields: `agent` (required sbx agent name), `template`, `kits`, `setupCommand` (override for the inside-sandbox setup script). |
|
|
118
124
|
| `models.definitions.<name>.disabled` | optional | When set to exactly `true`, drops the named shipped default (`claude` or `codex`). Doctor skips probing it; `agent-<name>` labels fall back to `models.default` with a warning. See "Disabling a shipped default" below. |
|
|
119
125
|
| `prompts.initial` | (template) | First message sent to the agent. Placeholders: `{{ticket}}`, `{{worktree}}`, `{{title}}`, `{{description}}`. |
|
|
120
126
|
| `workspaceKind` | `"auto"` | Terminal session manager. `"auto"` picks `cmux` when on PATH, else `tmux`. Set to `"cmux"` or `"tmux"` to fail loudly when the chosen backend is missing. tmux windows live in a dedicated `groundcrew` session. |
|
|
127
|
+
| `local.runner` | `"auto"` | Local isolation backend. `"auto"` resolves to `safehouse` on macOS and `sdx` on Linux/WSL. Explicit values: `"safehouse"`, `"sdx"`, `"none"`. `"none"` is never picked implicitly — doctor warns when it is configured. |
|
|
121
128
|
| `logging.file` | XDG state path | Append-mode log file destination. `log()` / `logEvent()` tee here in addition to stdout, so a vanished workspace doesn't take the evidence with it. Defaults to `${XDG_STATE_HOME:-$HOME/.local/state}/groundcrew/groundcrew.log`. |
|
|
122
129
|
|
|
123
130
|
The branch prefix (`<prefix>-<TICKET>`) is derived from your OS username (`os.userInfo().username`), not configured. Agent selection looks for a top-level Linear label named `agent-<model>` (e.g. `agent-claude`, `agent-codex`). **`crew run` without `--ticket` only fetches tickets with an `agent-*` label** — the GraphQL query filters them server-side, so unlabeled tickets are never returned by Linear's API and do not appear in the rendered board. Use `crew run --ticket <TICKET>` to provision an unlabeled ticket on demand (manual setup falls back to `models.default`). The reserved label `agent-any` routes the ticket to the configured model with the most available session capacity (lowest codexbar session-used percent), skipping any model already over `sessionLimitPercentage`. With no usage data, `agent-any` resolves to `models.default`. The name `any` cannot be used in `models.definitions`. Todo tickets blocked by Linear issues that are not in `linear.statuses.terminal` are skipped until their blockers reach a terminal status.
|
|
@@ -164,16 +171,16 @@ crew cleanup <TICKET>
|
|
|
164
171
|
|
|
165
172
|
## Gotchas
|
|
166
173
|
|
|
167
|
-
- **
|
|
174
|
+
- **Local execution picks one of safehouse/sdx/none.** `local.runner: "auto"` resolves to `safehouse` on macOS and `sdx` (Docker Sandboxes) on Linux/WSL. Override with `local.runner: "safehouse" | "sdx" | "none"`. There is no per-model `isolation` knob anymore — the runner is global. `sdx` requires a per-model `sandbox: { agent }` block so groundcrew can map the model to an sbx agent.
|
|
168
175
|
- **Safehouse-already-wrapped commands are not re-wrapped.** If a `models.definitions.<name>.cmd` already starts with `safehouse`, groundcrew assumes that command owns its Safehouse flags and does not add the `safehouse-clearance` wrapper a second time. Changing the proxy's allowlist after it's running requires killing the PID in `${XDG_CACHE_HOME:-$HOME/.cache}/clearance/clearance.pid` so the next launch picks up the new env.
|
|
169
|
-
- **
|
|
176
|
+
- **Sandbox lifecycle is create-only.** Groundcrew auto-creates the sandbox for a `<repository, model>` pair when missing, but never deletes one — sandboxes persist across tickets and across `crew cleanup`. Auth state lives inside the sandbox, so deleting it forces a re-login. Inspect or remove them manually with `sbx ls` / `sbx rm`.
|
|
170
177
|
- **Dead tmux windows vanish by default.** When a wrapped agent command fails (e.g. `safehouse-clearance` not found, `npm install` crash), the tmux window closes immediately and the error scrolls into the void. Set `GROUNDCREW_KEEP_DEAD_WINDOWS=1` (any non-empty value works) in the env you launch `crew` from to flip the per-window `remain-on-exit` to `on`; the window stays open with the error visible. Close it manually with `tmux kill-window -t groundcrew:<ticket>` after diagnosis. tmux backend only.
|
|
171
178
|
- **Status names matter.** If your team uses `Started` instead of `In Progress`, set `linear.statuses.inProgress = "Started"`.
|
|
172
179
|
- **Leaf-only.** Parent issues with children are ignored — sub-issues are the work items.
|
|
173
180
|
- **Tickets stay in the in-progress status until something else moves them.** Groundcrew sets a ticket to `inProgress` when it provisions a workspace and never advances it. The next transition (typically "in review" when a PR opens) is left to your team's Linear automation rules.
|
|
174
181
|
- **Project must be on a single Linear team in practice.** Cross-team projects work — the orchestrator caches the in-progress state ID per team — but every team in the project must use the same status name for `linear.statuses.inProgress`.
|
|
175
182
|
- **Claude launches in bypass-permissions mode by default.** 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` if you want a stricter mode.
|
|
176
|
-
- **Doctor's command introspection is shallow.** Doctor reports
|
|
183
|
+
- **Doctor's command introspection is shallow.** Doctor reports the resolved local runner (safehouse / sdx / none) and whether its prerequisites are met, then tokenizes model `cmd` and checks the first two non-flag tokens against PATH (so `safehouse claude --foo` checks both `safehouse` and `claude`). Boolean flags without values, env-var assignments (`FOO=1`), shell pipelines, and subshells are not parsed — verify those manually. In particular, `npx -y claude` and `env FOO=1 claude` only check the wrapper, not the wrapped CLI. When `local.runner` is `"none"`, doctor surfaces a single WARNING line so the unsandboxed launch is visible at a glance.
|
|
177
184
|
- **Doctor checks every enabled model, including shipped defaults you didn't disable.** `models.definitions` includes both shipped defaults (`claude`, `codex`) by default via additive merge. If you only intend to label tickets `agent-claude` and don't have `codex` installed, set `models.definitions.codex: { disabled: true }` (see "Disabling a shipped default" under "Config reference"). Without that, doctor exits non-zero on a missing `codex` binary even though `crew run` would never route to it.
|
|
178
185
|
- **Switch to tmux if cmux is misbehaving.** Set `workspaceKind: "tmux"` to force the tmux backend when cmux's CLI/socket bridge is flaky (symptoms: `cmux --json list-workspaces` returning `Failed to write to socket (Broken pipe)` or `Socket not found at ...cmux.sock` on every tick). tmux is more reliable — just a unix socket, no GUI app — at the cost of losing cmux's status pills, notifications, and vertical-tab sidebar.
|
|
179
186
|
- **Agent CLI must accept a positional prompt.** The handoff is `<your cmd> "<prompt>"`. `claude`, `codex`, and `cursor-agent` all support this.
|
package/configExample.ts
CHANGED
|
@@ -44,9 +44,18 @@ export const config: Config = {
|
|
|
44
44
|
// cmd: "cursor-agent",
|
|
45
45
|
// color: "#929292",
|
|
46
46
|
// },
|
|
47
|
+
// // To run a model under the sdx (Docker Sandboxes) runner, bind it to
|
|
48
|
+
// // an sbx agent. Required when `local.runner` resolves to `sdx`.
|
|
49
|
+
// // claude: { sandbox: { agent: "claude" } },
|
|
47
50
|
// },
|
|
48
51
|
// },
|
|
49
52
|
//
|
|
53
|
+
// // Local isolation backend. Defaults to `"auto"` — macOS → safehouse,
|
|
54
|
+
// // Linux → sdx (Docker Sandboxes). `"none"` is an explicit unsandboxed
|
|
55
|
+
// // escape hatch and is never picked implicitly. Switch to `"sdx"` on
|
|
56
|
+
// // macOS when you need an agent to use Docker safely.
|
|
57
|
+
// local: { runner: "auto" },
|
|
58
|
+
//
|
|
50
59
|
// prompts: {
|
|
51
60
|
// initial: [
|
|
52
61
|
// "Begin work on {{ticket}} ({{title}}) in the {{worktree}} wt subdirectory.",
|
|
@@ -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;AA8IH,wBAAsB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CAkE/C"}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* Returns true if every required check passes; false otherwise.
|
|
4
4
|
*/
|
|
5
5
|
import { existsSync, statSync } from "node:fs";
|
|
6
|
-
import { loadConfig } from "../lib/config.js";
|
|
6
|
+
import { loadConfig, } from "../lib/config.js";
|
|
7
7
|
import { detectHostCapabilities, which } from "../lib/host.js";
|
|
8
|
+
import { resolveLocalRunner } from "../lib/localRunner.js";
|
|
8
9
|
import { errorMessage, resolveLinearApiKey, writeOutput } from "../lib/util.js";
|
|
9
10
|
import { resolveWorkspaceKind } from "../lib/workspaces.js";
|
|
10
11
|
// Tokenization stops after this many non-flag tokens. Two is enough to
|
|
@@ -140,8 +141,13 @@ export async function doctor() {
|
|
|
140
141
|
writeOutput(`[--] host: ${errorMessage(error)}`);
|
|
141
142
|
return false;
|
|
142
143
|
}
|
|
143
|
-
const
|
|
144
|
-
|
|
144
|
+
const resolvedRunner = resolveLocalRunner(config.local.runner, host);
|
|
145
|
+
const localCapability = localCapabilityCheck(host, resolvedRunner);
|
|
146
|
+
reportLocalCapability({
|
|
147
|
+
check: localCapability,
|
|
148
|
+
setting: config.local.runner,
|
|
149
|
+
resolved: resolvedRunner,
|
|
150
|
+
});
|
|
145
151
|
const workspaceOutcome = resolveWorkspaceOutcome(config, host);
|
|
146
152
|
reportWorkspaceKind(config, workspaceOutcome);
|
|
147
153
|
const checks = [
|
|
@@ -176,22 +182,43 @@ export async function doctor() {
|
|
|
176
182
|
writeOutput("All required checks passed.");
|
|
177
183
|
return true;
|
|
178
184
|
}
|
|
179
|
-
function localCapabilityCheck(host) {
|
|
180
|
-
|
|
185
|
+
function localCapabilityCheck(host, resolved) {
|
|
186
|
+
if (resolved === "safehouse") {
|
|
187
|
+
const ok = host.isSafehouseSupported && host.hasSafehouse;
|
|
188
|
+
return {
|
|
189
|
+
name: "local runner (safehouse)",
|
|
190
|
+
ok,
|
|
191
|
+
required: false,
|
|
192
|
+
hint: ok
|
|
193
|
+
? "ready"
|
|
194
|
+
: "safehouse runner requires macOS with `safehouse` on PATH (install from https://agent-safehouse.dev/)",
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if (resolved === "sdx") {
|
|
198
|
+
const ok = host.isSdxSupported && host.hasSbx;
|
|
199
|
+
return {
|
|
200
|
+
name: "local runner (sdx)",
|
|
201
|
+
ok,
|
|
202
|
+
required: false,
|
|
203
|
+
hint: ok
|
|
204
|
+
? "ready"
|
|
205
|
+
: "sdx runner requires `sbx` (Docker Sandboxes) on PATH (install from https://docs.docker.com/sandboxes/)",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
// resolved === "none"
|
|
181
209
|
return {
|
|
182
|
-
name: "local runner (
|
|
183
|
-
ok:
|
|
210
|
+
name: "local runner (none)",
|
|
211
|
+
ok: true,
|
|
184
212
|
required: false,
|
|
185
|
-
hint:
|
|
186
|
-
? "ready"
|
|
187
|
-
: "groundcrew requires macOS with Safehouse on PATH (install from https://agent-safehouse.dev/)",
|
|
213
|
+
hint: "WARNING: local.runner='none' — agent runs unsandboxed on the host. Only use this when you understand the implications.",
|
|
188
214
|
};
|
|
189
215
|
}
|
|
190
|
-
function reportLocalCapability(
|
|
216
|
+
function reportLocalCapability(arguments_) {
|
|
191
217
|
writeOutput();
|
|
192
218
|
writeOutput("Local runner");
|
|
193
219
|
writeOutput("------------");
|
|
194
|
-
writeOutput(
|
|
220
|
+
writeOutput(`requested: ${arguments_.setting} → resolved: ${arguments_.resolved}`);
|
|
221
|
+
writeOutput(format(arguments_.check));
|
|
195
222
|
}
|
|
196
223
|
function resolveWorkspaceOutcome(config, host) {
|
|
197
224
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AAOA,OAAO,EAAkC,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AAOA,OAAO,EAAkC,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAUvF,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAgBD,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAmED,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,CAoGf;AA0FD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CAoBf"}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
4
|
import { ensureClearance } from "@clipboard-health/clearance";
|
|
5
5
|
import { fetchResolvedIssue } from "../lib/boardSource.js";
|
|
6
6
|
import { BUILD_SECRET_NAMES, loadConfig } from "../lib/config.js";
|
|
7
|
+
import { ensureSandbox, sandboxNameFor } from "../lib/dockerSandbox.js";
|
|
7
8
|
import { detectHostCapabilities } from "../lib/host.js";
|
|
8
9
|
import { buildLaunchCommand, shellSingleQuote } from "../lib/launchCommand.js";
|
|
9
10
|
import { createLinearIssueStatusUpdater } from "../lib/linearIssueStatus.js";
|
|
10
|
-
import { assertLocalRunnerRequirements } from "../lib/localRunner.js";
|
|
11
|
+
import { assertLocalRunnerRequirements, resolveLocalRunner } from "../lib/localRunner.js";
|
|
11
12
|
import { errorMessage, getLinearClient, log, readEnvironmentVariable } from "../lib/util.js";
|
|
12
13
|
import { workspaces } from "../lib/workspaces.js";
|
|
13
|
-
import { worktrees } from "../lib/worktrees.js";
|
|
14
|
+
import { isWorktreeAlreadyExistsError, worktrees } from "../lib/worktrees.js";
|
|
14
15
|
async function fetchTicket(ticket) {
|
|
15
16
|
const client = getLinearClient();
|
|
16
17
|
const issue = await client.issue(ticket.toUpperCase());
|
|
@@ -75,12 +76,30 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
75
76
|
if (!definition) {
|
|
76
77
|
throw new Error(`Unknown model: ${model}`);
|
|
77
78
|
}
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
const host = await detectHostCapabilities(signal);
|
|
80
|
+
const runner = resolveLocalRunner(config.local.runner, host);
|
|
81
|
+
assertLocalRunnerRequirements(host, runner);
|
|
82
|
+
if (runner === "safehouse") {
|
|
83
|
+
await ensureClearance({ logger: log });
|
|
84
|
+
}
|
|
85
|
+
if (runner === "sdx" && definition.sandbox === undefined) {
|
|
86
|
+
throw new Error(`Local groundcrew runs with the sdx runner require a sandbox config on model '${model}'. ` +
|
|
87
|
+
"Add `sandbox: { agent: '<sbx-agent-name>' }` to the model in your config.ts.");
|
|
88
|
+
}
|
|
80
89
|
const spec = { repository, ticket };
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
90
|
+
let created;
|
|
91
|
+
try {
|
|
92
|
+
created =
|
|
93
|
+
signal === undefined
|
|
94
|
+
? await worktrees.create(config, spec)
|
|
95
|
+
: await worktrees.create(config, spec, signal);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (isWorktreeAlreadyExistsError(error)) {
|
|
99
|
+
await logAccessHintForExistingWorkspace({ config, ticket, signal });
|
|
100
|
+
}
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
84
103
|
const { branchName, dir: launchDir } = created;
|
|
85
104
|
const worktreeName = `${repository}-${ticket}`;
|
|
86
105
|
// Anything that fails after the worktree is on disk must roll it back
|
|
@@ -103,11 +122,21 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
103
122
|
const stagedPrompt = stagePrompt({ config, ticket, ticketDetails, worktreeName });
|
|
104
123
|
promptDir = stagedPrompt.directory;
|
|
105
124
|
const secretsFile = stageBuildSecrets(promptDir);
|
|
125
|
+
const sandboxName = runner === "sdx" ? sandboxNameFor({ repository, model }) : undefined;
|
|
126
|
+
if (runner === "sdx" && sandboxName !== undefined && definition.sandbox !== undefined) {
|
|
127
|
+
await ensureSandbox({
|
|
128
|
+
sandboxName,
|
|
129
|
+
sandbox: definition.sandbox,
|
|
130
|
+
mountPath: resolve(config.workspace.projectDir),
|
|
131
|
+
}, signal);
|
|
132
|
+
}
|
|
106
133
|
const launchCommand = buildLaunchCommand({
|
|
107
134
|
definition,
|
|
108
135
|
promptFile: stagedPrompt.file,
|
|
109
136
|
worktreeDir: launchDir,
|
|
110
137
|
secretsFile,
|
|
138
|
+
runner,
|
|
139
|
+
sandboxName,
|
|
111
140
|
});
|
|
112
141
|
const launchCmd = stageWorkspaceLaunchCommand(promptDir, launchCommand);
|
|
113
142
|
log("Opening workspace...");
|
|
@@ -120,12 +149,43 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
120
149
|
log(`Workspace "${ticket}" launched (${model})`);
|
|
121
150
|
log(` Worktree: ${launchDir}`);
|
|
122
151
|
log(` Branch: ${branchName}`);
|
|
152
|
+
await logWorkspaceAccessHint({ config, ticket, signal });
|
|
123
153
|
}
|
|
124
154
|
catch (error) {
|
|
125
155
|
await rollbackWorktree({ config, entry: created, promptDir });
|
|
126
156
|
throw error;
|
|
127
157
|
}
|
|
128
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Probe the workspace backend and, if a workspace for `ticket` is still
|
|
161
|
+
* live, log the access hint. Used on the pre-launch error path (e.g. the
|
|
162
|
+
* worktree already exists from a prior run) so the user can find the
|
|
163
|
+
* still-running session instead of being told only that the worktree is
|
|
164
|
+
* in the way. Silent when the probe is unavailable or the workspace is
|
|
165
|
+
* gone — we don't want to point at a window that doesn't exist.
|
|
166
|
+
*/
|
|
167
|
+
async function logAccessHintForExistingWorkspace(arguments_) {
|
|
168
|
+
const { config, ticket, signal } = arguments_;
|
|
169
|
+
const accessHint = await workspaces.accessHint(config, ticket, signal);
|
|
170
|
+
if (accessHint === undefined) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const probe = await workspaces.probe(config, signal);
|
|
174
|
+
if (probe.kind !== "ok" || !probe.names.has(ticket)) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
logAccessHint(accessHint);
|
|
178
|
+
}
|
|
179
|
+
async function logWorkspaceAccessHint(arguments_) {
|
|
180
|
+
const accessHint = await workspaces.accessHint(arguments_.config, arguments_.ticket, arguments_.signal);
|
|
181
|
+
if (accessHint === undefined) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
logAccessHint(accessHint);
|
|
185
|
+
}
|
|
186
|
+
function logAccessHint(accessHint) {
|
|
187
|
+
log(` Attach: ${accessHint.command}`);
|
|
188
|
+
}
|
|
129
189
|
async function rollbackWorktree(arguments_) {
|
|
130
190
|
log(`Setup failed; rolling back worktree ${arguments_.entry.repository}-${arguments_.entry.ticket}...`);
|
|
131
191
|
let result;
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -15,6 +15,39 @@ export declare const AGENT_ANY_MODEL = "any";
|
|
|
15
15
|
*/
|
|
16
16
|
export type WorkspaceKindSetting = "auto" | "cmux" | "tmux";
|
|
17
17
|
export declare const WORKSPACE_KIND_SETTINGS: readonly WorkspaceKindSetting[];
|
|
18
|
+
/**
|
|
19
|
+
* Concrete local isolation backend selected for a launch. `safehouse` is
|
|
20
|
+
* macOS-only (clearance HTTP-egress + sandbox profile); `sdx` is Docker
|
|
21
|
+
* Sandboxes (`sbx` CLI) — works on Linux and macOS and is the only known
|
|
22
|
+
* option that lets the agent use Docker safely without exposing the host
|
|
23
|
+
* socket; `none` is an explicit unsandboxed escape hatch.
|
|
24
|
+
*/
|
|
25
|
+
export type LocalRunner = "safehouse" | "sdx" | "none";
|
|
26
|
+
/**
|
|
27
|
+
* User-facing local runner setting. `auto` resolves at launch time:
|
|
28
|
+
* macOS picks `safehouse`, Linux picks `sdx`. `none` is never picked
|
|
29
|
+
* implicitly.
|
|
30
|
+
*/
|
|
31
|
+
export type LocalRunnerSetting = LocalRunner | "auto";
|
|
32
|
+
export declare const LOCAL_RUNNER_SETTINGS: readonly LocalRunnerSetting[];
|
|
33
|
+
/**
|
|
34
|
+
* Per-model Docker Sandboxes (sdx) binding. Required at launch when
|
|
35
|
+
* `local.runner` resolves to `sdx` so groundcrew knows which sbx agent
|
|
36
|
+
* to address and how to seed the sandbox.
|
|
37
|
+
*/
|
|
38
|
+
export interface SandboxDefinition {
|
|
39
|
+
/** sbx agent name (e.g. "claude", "codex"). */
|
|
40
|
+
agent: string;
|
|
41
|
+
/** Optional `sbx run --template` value. */
|
|
42
|
+
template?: string;
|
|
43
|
+
/** Optional `sbx run --kit` values (each passed as a separate flag). */
|
|
44
|
+
kits?: string[];
|
|
45
|
+
/**
|
|
46
|
+
* Setup command run **inside** the sandbox before the agent exec.
|
|
47
|
+
* Defaults to `DEFAULT_SANDBOX_SETUP_COMMAND` when omitted.
|
|
48
|
+
*/
|
|
49
|
+
setupCommand?: string;
|
|
50
|
+
}
|
|
18
51
|
export interface ModelDefinition {
|
|
19
52
|
/**
|
|
20
53
|
* Shell command launched for the model. Wrapped with Safehouse/clearance
|
|
@@ -32,6 +65,12 @@ export interface ModelDefinition {
|
|
|
32
65
|
source?: string;
|
|
33
66
|
};
|
|
34
67
|
};
|
|
68
|
+
/**
|
|
69
|
+
* Docker Sandboxes binding. Required when `local.runner` resolves to
|
|
70
|
+
* `sdx` — pure additive: omitted models can still run under `safehouse`
|
|
71
|
+
* or `none` without surprise.
|
|
72
|
+
*/
|
|
73
|
+
sandbox?: SandboxDefinition;
|
|
35
74
|
}
|
|
36
75
|
/**
|
|
37
76
|
* User-facing model entry shape. Discriminated union so the type system
|
|
@@ -51,6 +90,14 @@ type UserModelDefinition = EnabledUserModelDefinition | DisabledUserModelDefinit
|
|
|
51
90
|
* assumed to already have the right Node and npm versions.
|
|
52
91
|
*/
|
|
53
92
|
export declare const DEFAULT_HOST_SETUP_COMMAND = "if [ -x .claude/setup.sh ]; then ./.claude/setup.sh --deps-only; elif [ -f .claude/setup.sh ] && command -v bash >/dev/null 2>&1; then bash .claude/setup.sh --deps-only; else npm clean-install; fi";
|
|
93
|
+
/**
|
|
94
|
+
* Setup command run inside an sdx (Docker Sandboxes) sandbox before the
|
|
95
|
+
* agent process exec. Independent of the host setup — sandboxes typically
|
|
96
|
+
* lack Node tooling on first start, so we keep the recipe scoped to the
|
|
97
|
+
* common case of an npm-managed repo while still letting per-model
|
|
98
|
+
* `sandbox.setupCommand` override it for languages outside that path.
|
|
99
|
+
*/
|
|
100
|
+
export declare const DEFAULT_SANDBOX_SETUP_COMMAND = "if [ -x .claude/setup.sh ]; then ./.claude/setup.sh --deps-only; elif [ -f .claude/setup.sh ] && command -v bash >/dev/null 2>&1; then bash .claude/setup.sh --deps-only; else npm clean-install; fi";
|
|
54
101
|
/**
|
|
55
102
|
* Loose user-facing shape — what a `config.ts` file declares.
|
|
56
103
|
* Fields with defaults are optional; only `linear.projectSlug` and the
|
|
@@ -107,6 +154,14 @@ export interface Config {
|
|
|
107
154
|
* to fail loudly when the chosen backend is missing.
|
|
108
155
|
*/
|
|
109
156
|
workspaceKind?: WorkspaceKindSetting;
|
|
157
|
+
/**
|
|
158
|
+
* Local isolation backend selector. Defaults to `"auto"` (macOS →
|
|
159
|
+
* safehouse, Linux → sdx). `"none"` is an explicit unsandboxed escape
|
|
160
|
+
* hatch — never selected implicitly.
|
|
161
|
+
*/
|
|
162
|
+
local?: {
|
|
163
|
+
runner?: LocalRunnerSetting;
|
|
164
|
+
};
|
|
110
165
|
logging?: {
|
|
111
166
|
/**
|
|
112
167
|
* Append-mode log file destination. `log()` and `logEvent()` tee here
|
|
@@ -158,6 +213,14 @@ export interface ResolvedConfig {
|
|
|
158
213
|
* `auto` resolves to cmux when installed, else tmux.
|
|
159
214
|
*/
|
|
160
215
|
workspaceKind: WorkspaceKindSetting;
|
|
216
|
+
/**
|
|
217
|
+
* Local isolation selection. The user-facing `auto` is preserved here
|
|
218
|
+
* so `localRunner.resolve()` can pick the platform default later — the
|
|
219
|
+
* resolver is the only place that knows the host capabilities.
|
|
220
|
+
*/
|
|
221
|
+
local: {
|
|
222
|
+
runner: LocalRunnerSetting;
|
|
223
|
+
};
|
|
161
224
|
logging: {
|
|
162
225
|
file: string;
|
|
163
226
|
};
|
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,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD;;;;;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,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE;QACN,QAAQ,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KACjD,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
|
@@ -17,11 +17,25 @@ export const WORKSPACE_KIND_SETTINGS = [
|
|
|
17
17
|
"cmux",
|
|
18
18
|
"tmux",
|
|
19
19
|
];
|
|
20
|
+
export const LOCAL_RUNNER_SETTINGS = [
|
|
21
|
+
"auto",
|
|
22
|
+
"safehouse",
|
|
23
|
+
"sdx",
|
|
24
|
+
"none",
|
|
25
|
+
];
|
|
20
26
|
/**
|
|
21
27
|
* Setup command run inside sibling worktrees on the host. The host is
|
|
22
28
|
* assumed to already have the right Node and npm versions.
|
|
23
29
|
*/
|
|
24
30
|
export const DEFAULT_HOST_SETUP_COMMAND = "if [ -x .claude/setup.sh ]; then ./.claude/setup.sh --deps-only; elif [ -f .claude/setup.sh ] && command -v bash >/dev/null 2>&1; then bash .claude/setup.sh --deps-only; else npm clean-install; fi";
|
|
31
|
+
/**
|
|
32
|
+
* Setup command run inside an sdx (Docker Sandboxes) sandbox before the
|
|
33
|
+
* agent process exec. Independent of the host setup — sandboxes typically
|
|
34
|
+
* lack Node tooling on first start, so we keep the recipe scoped to the
|
|
35
|
+
* common case of an npm-managed repo while still letting per-model
|
|
36
|
+
* `sandbox.setupCommand` override it for languages outside that path.
|
|
37
|
+
*/
|
|
38
|
+
export const DEFAULT_SANDBOX_SETUP_COMMAND = "if [ -x .claude/setup.sh ]; then ./.claude/setup.sh --deps-only; elif [ -f .claude/setup.sh ] && command -v bash >/dev/null 2>&1; then bash .claude/setup.sh --deps-only; else npm clean-install; fi";
|
|
25
39
|
const DEFAULT_STATUSES = {
|
|
26
40
|
todo: "Todo",
|
|
27
41
|
inProgress: "In Progress",
|
|
@@ -194,6 +208,43 @@ function normalizeWorkspaceKind(value, path) {
|
|
|
194
208
|
}
|
|
195
209
|
return value;
|
|
196
210
|
}
|
|
211
|
+
function isLocalRunnerSetting(value) {
|
|
212
|
+
return typeof value === "string" && LOCAL_RUNNER_SETTINGS.includes(value);
|
|
213
|
+
}
|
|
214
|
+
function normalizeLocalRunner(value, path) {
|
|
215
|
+
if (value === undefined) {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
if (!isLocalRunnerSetting(value)) {
|
|
219
|
+
fail(`${path} must be one of ${LOCAL_RUNNER_SETTINGS.join(", ")} (got ${JSON.stringify(value)})`);
|
|
220
|
+
}
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
function normalizeSandbox(value, path) {
|
|
224
|
+
if (!isPlainObject(value)) {
|
|
225
|
+
fail(`${path} must be an object`);
|
|
226
|
+
}
|
|
227
|
+
const { agent, template, kits, setupCommand } = value;
|
|
228
|
+
requireString(agent, `${path}.agent`);
|
|
229
|
+
const trimmedAgent = agent.trim();
|
|
230
|
+
if (trimmedAgent.length === 0) {
|
|
231
|
+
fail(`${path}.agent must be a non-empty string (got ${JSON.stringify(agent)})`);
|
|
232
|
+
}
|
|
233
|
+
const sandbox = { agent: trimmedAgent };
|
|
234
|
+
const normalizedTemplate = normalizeOptionalString(template, `${path}.template`);
|
|
235
|
+
if (normalizedTemplate !== undefined) {
|
|
236
|
+
sandbox.template = normalizedTemplate;
|
|
237
|
+
}
|
|
238
|
+
const normalizedKits = normalizeOptionalStringArray(kits, `${path}.kits`);
|
|
239
|
+
if (normalizedKits !== undefined) {
|
|
240
|
+
sandbox.kits = normalizedKits;
|
|
241
|
+
}
|
|
242
|
+
const normalizedSetup = normalizeOptionalString(setupCommand, `${path}.setupCommand`);
|
|
243
|
+
if (normalizedSetup !== undefined) {
|
|
244
|
+
sandbox.setupCommand = normalizedSetup;
|
|
245
|
+
}
|
|
246
|
+
return sandbox;
|
|
247
|
+
}
|
|
197
248
|
function failIfLegacyModelKeys(name, override) {
|
|
198
249
|
if (!isPlainObject(override)) {
|
|
199
250
|
fail(`models.definitions.${name} must be an object`);
|
|
@@ -201,14 +252,11 @@ function failIfLegacyModelKeys(name, override) {
|
|
|
201
252
|
if (Object.hasOwn(override, "isolation")) {
|
|
202
253
|
fail(`models.definitions.${name}.isolation is no longer supported: per-model isolation is no longer supported`);
|
|
203
254
|
}
|
|
204
|
-
if (Object.hasOwn(override, "sandbox")) {
|
|
205
|
-
fail(`models.definitions.${name}.sandbox is no longer supported: Docker Sandboxes are no longer supported`);
|
|
206
|
-
}
|
|
207
255
|
if (Object.hasOwn(override, "disabled")) {
|
|
208
256
|
if (override["disabled"] !== true) {
|
|
209
257
|
fail(`models.definitions.${name}.disabled must be exactly \`true\` when set (got ${JSON.stringify(override["disabled"])})`);
|
|
210
258
|
}
|
|
211
|
-
const conflicting = ["cmd", "color", "usage"].filter((key) => Object.hasOwn(override, key));
|
|
259
|
+
const conflicting = ["cmd", "color", "usage", "sandbox"].filter((key) => Object.hasOwn(override, key));
|
|
212
260
|
if (conflicting.length > 0) {
|
|
213
261
|
fail(`models.definitions.${name}: cannot combine \`disabled: true\` with other fields (${conflicting.join(", ")}). Either disable the model or override its fields, not both.`);
|
|
214
262
|
}
|
|
@@ -258,7 +306,10 @@ function mergeDefinitions(user) {
|
|
|
258
306
|
if (override.usage !== undefined) {
|
|
259
307
|
candidate.usage = override.usage;
|
|
260
308
|
}
|
|
261
|
-
|
|
309
|
+
if (override.sandbox !== undefined) {
|
|
310
|
+
candidate.sandbox = normalizeSandbox(override.sandbox, `models.definitions.${name}.sandbox`);
|
|
311
|
+
}
|
|
312
|
+
const { cmd, color, usage, sandbox } = candidate;
|
|
262
313
|
if (typeof cmd !== "string" || cmd.length === 0) {
|
|
263
314
|
fail(`models.definitions.${name}.cmd must be a non-empty string`);
|
|
264
315
|
}
|
|
@@ -269,6 +320,9 @@ function mergeDefinitions(user) {
|
|
|
269
320
|
if (usage !== undefined) {
|
|
270
321
|
definition.usage = usage;
|
|
271
322
|
}
|
|
323
|
+
if (sandbox !== undefined) {
|
|
324
|
+
definition.sandbox = sandbox;
|
|
325
|
+
}
|
|
272
326
|
merged[name] = definition;
|
|
273
327
|
}
|
|
274
328
|
return merged;
|
|
@@ -294,10 +348,14 @@ function applyDefaults(user) {
|
|
|
294
348
|
requireString(user.linear.projectSlug, "linear.projectSlug");
|
|
295
349
|
requireObject(user.workspace, "workspace");
|
|
296
350
|
if (isPlainObject(user.models) && Object.hasOwn(user.models, "isolation")) {
|
|
297
|
-
fail("models.isolation is no longer supported: local
|
|
351
|
+
fail("models.isolation is no longer supported: set `local.runner` ('safehouse' | 'sdx' | 'none' | 'auto') instead");
|
|
298
352
|
}
|
|
299
353
|
if (Object.hasOwn(user, "remote")) {
|
|
300
|
-
fail("remote is no longer supported: groundcrew
|
|
354
|
+
fail("remote is no longer supported: groundcrew runs locally via safehouse/sdx/none; remove the remote block from your config");
|
|
355
|
+
}
|
|
356
|
+
const userLocal = user.local;
|
|
357
|
+
if (userLocal !== undefined && !isPlainObject(userLocal)) {
|
|
358
|
+
fail("local must be an object");
|
|
301
359
|
}
|
|
302
360
|
const slugId = extractSlugId(user.linear.projectSlug);
|
|
303
361
|
if (slugId === undefined) {
|
|
@@ -323,6 +381,9 @@ function applyDefaults(user) {
|
|
|
323
381
|
initial: user.prompts?.initial ?? DEFAULT_PROMPT_INITIAL,
|
|
324
382
|
},
|
|
325
383
|
workspaceKind: normalizeWorkspaceKind(user.workspaceKind, "workspaceKind") ?? "auto",
|
|
384
|
+
local: {
|
|
385
|
+
runner: normalizeLocalRunner(userLocal?.runner, "local.runner") ?? "auto",
|
|
386
|
+
},
|
|
326
387
|
logging: {
|
|
327
388
|
file: expandHome(normalizeOptionalString(user.logging?.file, "logging.file") ?? defaultLogFile()),
|
|
328
389
|
},
|
|
@@ -379,6 +440,13 @@ function validate(config) {
|
|
|
379
440
|
}
|
|
380
441
|
requireString(codexbar.provider, `${usagePath}.codexbar.provider`);
|
|
381
442
|
}
|
|
443
|
+
if (definition.sandbox !== undefined) {
|
|
444
|
+
requireString(definition.sandbox.agent, `models.definitions.${name}.sandbox.agent`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/* v8 ignore next 5 @preserve -- normalizeLocalRunner rejects invalid strings before validate() runs; this is a belt-and-suspenders guard */
|
|
448
|
+
if (!LOCAL_RUNNER_SETTINGS.includes(config.local.runner)) {
|
|
449
|
+
fail(`local.runner must be one of ${LOCAL_RUNNER_SETTINGS.join(", ")} (got ${JSON.stringify(config.local.runner)})`);
|
|
382
450
|
}
|
|
383
451
|
// Disabled-default check must run before the generic "not a key" check so
|
|
384
452
|
// the user gets the specific "is disabled" message instead of a stale-list
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { SandboxDefinition } from "./config.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Derive a deterministic sbx sandbox name from the repository + model
|
|
4
|
+
* tuple so `crew sandbox auth <repo>` and the subsequent `crew local`
|
|
5
|
+
* launch agree on which sandbox to target. Lowercased and reduced to the
|
|
6
|
+
* sbx-safe charset (`a-z0-9.+-`) so unusual repo names still round-trip
|
|
7
|
+
* cleanly. Keep the prefix stable — doctor and teardown use it to
|
|
8
|
+
* identify groundcrew-owned sandboxes.
|
|
9
|
+
*/
|
|
10
|
+
export declare function sandboxNameFor(arguments_: {
|
|
11
|
+
repository: string;
|
|
12
|
+
model: string;
|
|
13
|
+
}): string;
|
|
14
|
+
/**
|
|
15
|
+
* Probe `sbx ls` to see whether a sandbox with `sandboxName` already
|
|
16
|
+
* exists. Used by `crew sandbox auth` to switch between create vs reuse
|
|
17
|
+
* branches without surfacing the raw sbx error on first run.
|
|
18
|
+
*/
|
|
19
|
+
export declare function sandboxExists(sandboxName: string, signal?: AbortSignal): Promise<boolean>;
|
|
20
|
+
interface EnsureSandboxArguments {
|
|
21
|
+
sandboxName: string;
|
|
22
|
+
sandbox: SandboxDefinition;
|
|
23
|
+
/**
|
|
24
|
+
* Host path bound into the sandbox at the same path. Pass the workspace
|
|
25
|
+
* `projectDir` so all per-ticket worktrees (siblings of the bare repo
|
|
26
|
+
* clone) are visible to `sbx exec -w <worktreeDir>` after creation.
|
|
27
|
+
*/
|
|
28
|
+
mountPath: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Idempotent guard: ensure a Docker Sandboxes container exists for the
|
|
32
|
+
* given repository + model. Probes `sbx ls`; if `sandboxName` is missing,
|
|
33
|
+
* calls `sbx create --name <name> [--template <t>] [--kit <k>]... <agent>
|
|
34
|
+
* <mountPath>` to provision it. First-time agent auth still happens inside
|
|
35
|
+
* the sandbox the first time `sbx exec` runs the agent — `create` only
|
|
36
|
+
* provisions the container, it does not attach.
|
|
37
|
+
*/
|
|
38
|
+
export declare function ensureSandbox(arguments_: EnsureSandboxArguments, signal?: AbortSignal): Promise<void>;
|
|
39
|
+
export {};
|
|
40
|
+
//# sourceMappingURL=dockerSandbox.d.ts.map
|
|
@@ -0,0 +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,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAMxF;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"}
|