@tablation/crew 0.112.0 → 0.114.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 (2) hide show
  1. package/dist/cli.js +117 -20
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -3030,7 +3030,9 @@ var SHIP_KEYS = /* @__PURE__ */ new Set([
3030
3030
  "maxPeriscopes",
3031
3031
  "periscopeIdleMinutes",
3032
3032
  "secretStore",
3033
- "env"
3033
+ "env",
3034
+ "runner",
3035
+ "sandbox"
3034
3036
  ]);
3035
3037
  var ROUTE_KEYS = /* @__PURE__ */ new Set([
3036
3038
  "route",
@@ -11784,7 +11786,13 @@ function syncAllPassengerCheckouts(cfg) {
11784
11786
 
11785
11787
  // src/sandbox-image.ts
11786
11788
  var SANDBOX_BASE_IMAGE = "nvcr.io/nvidia/base/ubuntu:24.04";
11787
- var SANDBOX_HARNESS_VERSION = "0.1.6";
11789
+ var SANDBOX_OPENSHELL_VERSION = "0.1.6";
11790
+ var SANDBOX_CLAUDE_VERSION = "2.1.309";
11791
+ var SANDBOX_CLAUDE_DIST = "https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases";
11792
+ var SANDBOX_CLAUDE_SHA256 = {
11793
+ x86_64: { platform: "linux-x64", sha256: "4d704a8aecbe038affa3000f498ae17c1b4f753add8facb8764120affc99b658" },
11794
+ aarch64: { platform: "linux-arm64", sha256: "9796dd26899337f687739a0b39a7353b639f062a3436a7b941fcf19a1b757a9b" }
11795
+ };
11788
11796
  var SANDBOX_UID = 1e3;
11789
11797
  var CREW_PREFIX = "/opt/crew";
11790
11798
  var SANDBOX_RETRY_AFTER_MS = 60 * 60 * 1e3;
@@ -11808,15 +11816,28 @@ var CREW_ENV = {
11808
11816
  XDG_CACHE_HOME: `${CREW_PREFIX}/cache`,
11809
11817
  npm_config_cache: `${CREW_PREFIX}/cache/npm`,
11810
11818
  npm_config_store_dir: `${CREW_PREFIX}/pnpm-store`,
11811
- PNPM_HOME: `${CREW_PREFIX}/pnpm`
11819
+ PNPM_HOME: `${CREW_PREFIX}/pnpm`,
11820
+ // A run cannot reach the download host, so an in-run update attempt is only noise.
11821
+ DISABLE_AUTOUPDATER: "1"
11812
11822
  };
11823
+ function claudeInstallCommand() {
11824
+ const cases = Object.entries(SANDBOX_CLAUDE_SHA256).map(([arch, { platform: platform2, sha256: sha2562 }]) => `${arch}) p=${platform2}; sum=${sha2562};;`).join(" ");
11825
+ return [
11826
+ `case "$(uname -m)" in ${cases} *) echo "no pinned Claude Code binary for $(uname -m)" >&2; exit 1;; esac`,
11827
+ `mkdir -p ${CREW_PREFIX}/bin`,
11828
+ `curl -fsSL --retry 3 -o ${CREW_PREFIX}/bin/claude "${SANDBOX_CLAUDE_DIST}/${SANDBOX_CLAUDE_VERSION}/$p/claude"`,
11829
+ `echo "$sum ${CREW_PREFIX}/bin/claude" | sha256sum -c -`,
11830
+ `chmod 0755 ${CREW_PREFIX}/bin/claude`,
11831
+ `${CREW_PREFIX}/bin/claude --version | grep -F "${SANDBOX_CLAUDE_VERSION}"`
11832
+ ].join("; ");
11833
+ }
11813
11834
  function baseLayer() {
11814
11835
  const dockerfile = [
11815
11836
  `FROM ${SANDBOX_BASE_IMAGE}`,
11816
- `LABEL io.crew.sandbox.layer="base" io.crew.sandbox.harness="${SANDBOX_HARNESS_VERSION}"`,
11837
+ `LABEL io.crew.sandbox.layer="base" io.crew.sandbox.openshell="${SANDBOX_OPENSHELL_VERSION}"`,
11817
11838
  ""
11818
11839
  ].join("\n");
11819
- return { name: "base", tag: `crew-sandbox-base:${hash({ SANDBOX_BASE_IMAGE, SANDBOX_HARNESS_VERSION })}`, dockerfile, files: {} };
11840
+ return { name: "base", tag: `crew-sandbox-base:${hash({ SANDBOX_BASE_IMAGE, SANDBOX_OPENSHELL_VERSION })}`, dockerfile, files: {} };
11820
11841
  }
11821
11842
  function crewLayer(base) {
11822
11843
  const files = { "credential-helper": CREDENTIAL_HELPER, "mcp.json": MCP_CONFIG };
@@ -11827,10 +11848,11 @@ function crewLayer(base) {
11827
11848
  run2("apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git && rm -rf /var/lib/apt/lists/*"),
11828
11849
  `COPY credential-helper ${CREW_PREFIX}/bin/credential-helper`,
11829
11850
  `COPY mcp.json ${CREW_PREFIX}/mcp/mcp.json`,
11851
+ run2(claudeInstallCommand()),
11830
11852
  run2(`chmod 0755 ${CREW_PREFIX}/bin/credential-helper && git config --system credential.helper ${CREW_PREFIX}/bin/credential-helper`),
11831
11853
  run2(`mkdir -p ${CREW_PREFIX}/cache ${CREW_PREFIX}/pnpm-store ${CREW_PREFIX}/pnpm /sandbox && chown -R ${SANDBOX_UID}:${SANDBOX_UID} ${CREW_PREFIX} /sandbox`),
11832
11854
  `ENV ${env} PATH=${CREW_PREFIX}/pnpm:${CREW_PREFIX}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,
11833
- `LABEL io.crew.sandbox.layer="crew"`,
11855
+ `LABEL io.crew.sandbox.layer="crew" io.crew.sandbox.claude="${SANDBOX_CLAUDE_VERSION}"`,
11834
11856
  ""
11835
11857
  ].join("\n");
11836
11858
  return { name: "crew", tag: `crew-sandbox-crew:${hash({ base: base.tag, dockerfile, files })}`, dockerfile, files };
@@ -11891,7 +11913,7 @@ function planSandbox(repo, decl, warm = {}) {
11891
11913
  const base = baseLayer();
11892
11914
  const crew = crewLayer(base);
11893
11915
  const layer = repoLayer(repo, decl, crew, warm);
11894
- return { repo, tag: layer.tag, layers: [base, crew, layer] };
11916
+ return { repo, tag: layer.tag, claude: SANDBOX_CLAUDE_VERSION, layers: [base, crew, layer] };
11895
11917
  }
11896
11918
  var sandboxStatePath = (stateDir) => join26(stateDir, "sandbox-images.json");
11897
11919
  function readSandboxState(stateDir) {
@@ -11916,8 +11938,8 @@ function sandboxImageStatus(state, plan) {
11916
11938
  return { status: "failed", detail: `last build of ${plan.tag} failed at ${s.failure.at}; log ${s.failure.logPath}${current ? `; running ${current.tag}` : ""}` };
11917
11939
  }
11918
11940
  if (!current) return { status: "missing", detail: `no image built yet (wants ${plan.tag}); run \`crew sandbox build\`` };
11919
- if (current.tag !== plan.tag) return { status: "stale", detail: `running ${current.tag}, declaration now builds ${plan.tag}` };
11920
- return { status: "ok", detail: `${current.tag} built ${current.builtAt}` };
11941
+ if (current.tag !== plan.tag) return { status: "stale", detail: `running ${current.tag} (claude ${current.claude ?? "unknown"}), declaration now builds ${plan.tag} (claude ${plan.claude})` };
11942
+ return { status: "ok", detail: `${current.tag} built ${current.builtAt}, claude ${current.claude ?? "unknown"}` };
11921
11943
  }
11922
11944
  var spawnBuildRunner = (engine = "docker") => (args, logPath) => new Promise((resolve7) => {
11923
11945
  const child = spawn4(engine, args, { stdio: ["ignore", "pipe", "pipe"] });
@@ -11980,7 +12002,7 @@ async function buildSandboxImage(plan, deps) {
11980
12002
  rmSync13(ctx, { recursive: true, force: true });
11981
12003
  }
11982
12004
  }
11983
- const tags = [{ tag: plan.tag, builtAt: now().toISOString() }, ...entry.tags.filter((t) => t.tag !== plan.tag)];
12005
+ const tags = [{ tag: plan.tag, builtAt: now().toISOString(), claude: plan.claude }, ...entry.tags.filter((t) => t.tag !== plan.tag)];
11984
12006
  const dropped = tags.slice(KEEP_TAGS);
11985
12007
  state[plan.repo] = { tags: tags.slice(0, KEEP_TAGS) };
11986
12008
  writeSandboxState(deps.stateDir, state);
@@ -12144,6 +12166,17 @@ function roleCapWarnings(ship) {
12144
12166
  if (!ship.maxConcurrentAgentsSet) return [];
12145
12167
  return Object.entries(ship.roleConcurrency).filter(([, n]) => (n ?? 0) > ship.maxConcurrentAgents).map(([role, n]) => `ship.roleConcurrency.${role} is ${n}, above ship.maxConcurrentAgents (${ship.maxConcurrentAgents}); the ship-wide cap wins`);
12146
12168
  }
12169
+ function managedRuntimeWarnings(kind, ship) {
12170
+ if (kind !== "managed_instance" && kind !== "pooled_instance" && kind !== "pooled") return [];
12171
+ const out = [];
12172
+ const explicit = ship.toolchains ?? [];
12173
+ const effectiveToolchains = explicit.some((t) => t.name === "claude") ? explicit : [{ name: "claude", harness: "claude" }, ...explicit];
12174
+ if ((ship.runner ?? "host") !== "sandbox") out.push(`ship.runner is "${ship.runner ?? "host"}" on a ${kind} ship; a managed ship should run agents in the sandbox (set ship.runner: sandbox)`);
12175
+ for (const t of effectiveToolchains) {
12176
+ if (t.harness === "claude" && t.auth !== "workspace") out.push(`ship.toolchains.${t.name}.auth is "${t.auth ?? "inherit"}" on a ${kind} ship; a managed ship reaches a model only through the workspace endpoint (set auth: workspace)`);
12177
+ }
12178
+ return out;
12179
+ }
12147
12180
 
12148
12181
  // src/selftest.ts
12149
12182
  import { spawn as spawn5 } from "child_process";
@@ -13228,7 +13261,7 @@ function matchShipRows(ships, shipName2, machineId) {
13228
13261
  import { spawnSync as spawnSync7 } from "child_process";
13229
13262
  import { existsSync as existsSync28, readFileSync as readFileSync27, writeFileSync as writeFileSync21 } from "fs";
13230
13263
  import { hostname as hostname2 } from "os";
13231
- import { parseDocument as parseDocument3 } from "yaml";
13264
+ import { parseDocument as parseDocument3, isMap as isMap2, isScalar } from "yaml";
13232
13265
 
13233
13266
  // src/doctor-fix.ts
13234
13267
  import { parseDocument as parseDocument2 } from "yaml";
@@ -13664,6 +13697,15 @@ async function runProvision(o, deps, write, human) {
13664
13697
  emit("connect", `set ship.${key}`, { setting: key });
13665
13698
  }
13666
13699
  }
13700
+ if (o.runner === "host") {
13701
+ emit("connect", "runner: host chosen explicitly (--runner host); not applying the managed sandbox defaults", { setting: "ship.runner", value: "host" });
13702
+ if (deps.setShipSetting("runner", "host")) summary.changed = true;
13703
+ } else if (o.runner === "sandbox" || isManagedShipKind(o.shipKind)) {
13704
+ for (const key of deps.applyManagedDefaults()) {
13705
+ summary.changed = true;
13706
+ emit("connect", `set ${key}`, { setting: key });
13707
+ }
13708
+ }
13667
13709
  for (const host of o.forgeHosts) {
13668
13710
  const token = o.tokens[host];
13669
13711
  const stored = deps.storedToken(host);
@@ -13707,6 +13749,38 @@ async function runProvision(o, deps, write, human) {
13707
13749
  emit("done", summary.state, { summary });
13708
13750
  return { code: PROVISION_EXIT.ok, summary };
13709
13751
  }
13752
+ function isManagedShipKind(kind) {
13753
+ return kind === "managed_instance" || kind === "pooled_instance" || kind === "pooled";
13754
+ }
13755
+ var MANAGED_CACHE_VOLUME = "crew-cache";
13756
+ function applyManagedDefaultsToDoc(doc) {
13757
+ const wrote = [];
13758
+ if (doc.getIn(["ship", "runner"]) === void 0) {
13759
+ doc.setIn(["ship", "runner"], "sandbox");
13760
+ wrote.push("ship.runner");
13761
+ }
13762
+ if (doc.getIn(["ship", "sandbox", "cacheVolume"]) === void 0) {
13763
+ doc.setIn(["ship", "sandbox", "cacheVolume"], MANAGED_CACHE_VOLUME);
13764
+ wrote.push("ship.sandbox.cacheVolume");
13765
+ }
13766
+ const existing = doc.getIn(["ship", "toolchains"]);
13767
+ const hasClaude = isMap2(existing) && existing.items.some((i) => (isScalar(i.key) ? i.key.value : i.key) === "claude");
13768
+ if (!hasClaude) {
13769
+ doc.setIn(["ship", "toolchains", "claude", "harness"], "claude");
13770
+ for (const k of ["bin", "model", "maxThinkingTokens"]) {
13771
+ const v = doc.getIn(["ship", "agent", k]);
13772
+ if (v !== void 0) doc.setIn(["ship", "toolchains", "claude", k], v);
13773
+ }
13774
+ }
13775
+ const toolchains = doc.getIn(["ship", "toolchains"]);
13776
+ for (const name of isMap2(toolchains) ? toolchains.items.map((i) => String(isScalar(i.key) ? i.key.value : i.key)) : []) {
13777
+ if (doc.getIn(["ship", "toolchains", name, "harness"]) !== "claude") continue;
13778
+ if (doc.getIn(["ship", "toolchains", name, "auth"]) !== void 0) continue;
13779
+ doc.setIn(["ship", "toolchains", name, "auth"], "workspace");
13780
+ wrote.push(`ship.toolchains.${name}.auth`);
13781
+ }
13782
+ return wrote;
13783
+ }
13710
13784
  function envKey(host) {
13711
13785
  return host.toUpperCase().replace(/[^A-Z0-9]/g, "_");
13712
13786
  }
@@ -13758,6 +13832,7 @@ function provisionOptionsFrom(argv2, env) {
13758
13832
  relayHost: one("relay-host"),
13759
13833
  maxConcurrentAgents: Number.isInteger(Number(one("max-concurrent-agents"))) && Number(one("max-concurrent-agents")) > 0 ? Number(one("max-concurrent-agents")) : void 0,
13760
13834
  agentBin: one("agent-bin"),
13835
+ runner: one("runner") === "host" || one("runner") === "sandbox" ? one("runner") : void 0,
13761
13836
  extraPath: one("extra-path"),
13762
13837
  baseUrl: one("base-url"),
13763
13838
  forgeHosts,
@@ -13842,6 +13917,14 @@ function realProvisionDeps(crewHome) {
13842
13917
  writeFileSync21(file, doc.toString());
13843
13918
  return true;
13844
13919
  },
13920
+ applyManagedDefaults() {
13921
+ const file = configFile();
13922
+ if (!file) return [];
13923
+ const doc = parseDocument3(readFileSync27(file, "utf8"));
13924
+ const wrote = applyManagedDefaultsToDoc(doc);
13925
+ if (wrote.length) writeFileSync21(file, doc.toString());
13926
+ return wrote;
13927
+ },
13845
13928
  storedToken(host) {
13846
13929
  const res = spawnCrew(["credential", "get"], { input: `protocol=https
13847
13930
  host=${host}
@@ -19351,7 +19434,7 @@ function renderRepoCheck(r) {
19351
19434
  }
19352
19435
 
19353
19436
  // src/repos-hooks.ts
19354
- import { isMap as isMap2, parseDocument as parseDocument6, YAMLMap } from "yaml";
19437
+ import { isMap as isMap3, parseDocument as parseDocument6, YAMLMap } from "yaml";
19355
19438
  function packageManager(f) {
19356
19439
  if (f.exists("pnpm-lock.yaml")) return "pnpm";
19357
19440
  if (f.exists("yarn.lock")) return "yarn";
@@ -19497,9 +19580,9 @@ function planRouteHooks(text2, route2, name, hooks, opts = {}) {
19497
19580
  const m = routeMapOf(doc, route2);
19498
19581
  const repos = m.get("repos", true);
19499
19582
  let target = m;
19500
- if (isMap2(repos)) {
19583
+ if (isMap3(repos)) {
19501
19584
  const entry = repos.get(name, true);
19502
- if (isMap2(entry)) target = entry;
19585
+ if (isMap3(entry)) target = entry;
19503
19586
  else {
19504
19587
  const created = new YAMLMap();
19505
19588
  created.set("dir", String(repos.get(name)));
@@ -19509,7 +19592,7 @@ function planRouteHooks(text2, route2, name, hooks, opts = {}) {
19509
19592
  }
19510
19593
  const current = target.get("hooks", true);
19511
19594
  let hooksMap;
19512
- if (isMap2(current)) hooksMap = current;
19595
+ if (isMap3(current)) hooksMap = current;
19513
19596
  else {
19514
19597
  hooksMap = new YAMLMap();
19515
19598
  target.set("hooks", hooksMap);
@@ -19534,13 +19617,13 @@ function clearRouteHooks(text2, route2, name) {
19534
19617
  const m = routeMapOf(doc, route2);
19535
19618
  const repos = m.get("repos", true);
19536
19619
  let target = m;
19537
- if (isMap2(repos)) {
19620
+ if (isMap3(repos)) {
19538
19621
  const entry = repos.get(name, true);
19539
- if (!isMap2(entry)) return { text: text2, removed: [] };
19622
+ if (!isMap3(entry)) return { text: text2, removed: [] };
19540
19623
  target = entry;
19541
19624
  }
19542
19625
  const current = target.get("hooks", true);
19543
- if (!isMap2(current)) return { text: text2, removed: [] };
19626
+ if (!isMap3(current)) return { text: text2, removed: [] };
19544
19627
  const removed = current.items.map((i) => String(i.key.value ?? i.key));
19545
19628
  target.delete("hooks");
19546
19629
  return { text: String(doc), removed };
@@ -23861,6 +23944,15 @@ if (command === "passenger-sync-daemon") {
23861
23944
  }
23862
23945
  return { problems, readiness };
23863
23946
  }
23947
+ async function doctorShipKind(route3, ship) {
23948
+ try {
23949
+ const rows = await new Tracker(route3, ship).shipRows();
23950
+ const mine = matchShipRows(rows, ship.name, doctorMachineId(ship.stateDir)).rows;
23951
+ return mine.length === 1 ? mine[0].kind ?? null : null;
23952
+ } catch {
23953
+ return null;
23954
+ }
23955
+ }
23864
23956
  async function doctorFix(o) {
23865
23957
  const routes = await Promise.all(routesInScope(cfg.routes, named).map((c) => gatherHealth({ route: c.route, enabled: c.enabled, hasRepos: reposOf(c).length > 0 }, () => routeProblems(c))));
23866
23958
  const unitExists = (p) => existsSync45(p);
@@ -26307,7 +26399,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
26307
26399
  }
26308
26400
  let apiKeyFile;
26309
26401
  const apiKeyVar = "CREW_API_KEY";
26310
- if (firstRunFromFlags && agentBinSearched && !dryRun) {
26402
+ if (firstRunFromFlags && agentBinSearched && !dryRun && !isManagedShipKind(value("ship-kind"))) {
26311
26403
  const msg = `no agent binary: "claude" was not found on PATH or in ${agentBinSearched.join(", ")} \u2014 pass --agent-bin /absolute/path`;
26312
26404
  process.stderr.write(`crew connect: ${msg}
26313
26405
  `);
@@ -26683,7 +26775,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
26683
26775
  `);
26684
26776
  for (const t of r.tags) {
26685
26777
  const ageH = Math.max(0, Math.round((Date.now() - Date.parse(t.builtAt)) / 36e5));
26686
- process.stdout.write(` ${t.tag} built ${t.builtAt} (${ageH}h ago)
26778
+ process.stdout.write(` ${t.tag} built ${t.builtAt} (${ageH}h ago), claude ${t.claude ?? "unknown"}
26687
26779
  `);
26688
26780
  }
26689
26781
  }
@@ -28214,6 +28306,9 @@ watermark: ${state.watermark()}
28214
28306
  }
28215
28307
  }
28216
28308
  for (const message of roleCapWarnings(cfg.ship)) problems.push({ severity: "note", kind: "role_cap_above_ship_cap", message });
28309
+ if (!flag("offline")) {
28310
+ for (const message of managedRuntimeWarnings(await doctorShipKind(route2, cfg.ship), cfg.ship)) problems.push({ severity: "attention", kind: "managed_runtime_default", message });
28311
+ }
28217
28312
  const previewProblem2 = await previewHostProblem(cfg);
28218
28313
  if (previewProblem2) problems.push(previewHostFinding(previewProblem2));
28219
28314
  if (cfg.routes.some((x) => x.route === route2.route && x.enabled && x.hostPeriscopes)) {
@@ -28452,6 +28547,8 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
28452
28547
  crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "none"}
28453
28548
  `
28454
28549
  );
28550
+ for (const w of managedRuntimeWarnings(row.kind, cfg.ship)) process.stdout.write(`managed runtime: WARN ${w}
28551
+ `);
28455
28552
  }
28456
28553
  }
28457
28554
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.112.0",
3
+ "version": "0.114.0",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",