@tablation/crew 0.102.0 → 0.103.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 +42 -9
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -12387,7 +12387,17 @@ async function runProvision(o, deps, write, human) {
12387
12387
  emit("problem", message, data);
12388
12388
  return { code, summary: { ...summary, ok: false, state: "failed" } };
12389
12389
  };
12390
- if (!o.key) return fail(PROVISION_EXIT.problem, "no connect key: pass --key or set CREW_CONNECT_KEY", { stage: "input" });
12390
+ const keyFor = (route2) => {
12391
+ const ws = route2.split("/")[0].toLowerCase();
12392
+ const hit = Object.entries(o.keys ?? {}).find(([k, v]) => k.toLowerCase() === ws && v);
12393
+ return hit?.[1] || o.key || void 0;
12394
+ };
12395
+ for (const route2 of o.routes) {
12396
+ if (!keyFor(route2)) {
12397
+ return fail(PROVISION_EXIT.problem, `no connect key for ${route2}: pass --key or set CREW_CONNECT_KEY_${envKey(route2.split("/")[0])} or CREW_CONNECT_KEY`, { stage: "input" });
12398
+ }
12399
+ }
12400
+ if (o.routes.length === 0 && !o.key) return fail(PROVISION_EXIT.problem, "no connect key: pass --key or set CREW_CONNECT_KEY", { stage: "input" });
12391
12401
  if (o.routes.length === 0) {
12392
12402
  return fail(PROVISION_EXIT.problem, "no routes to serve: pass --route WORKSPACE[/PROJECT] (repeatable) or set CREW_PROVISION_ROUTES", { stage: "input" });
12393
12403
  }
@@ -12413,7 +12423,7 @@ async function runProvision(o, deps, write, human) {
12413
12423
  ...o.dryRun ? ["--dry-run"] : [],
12414
12424
  ...o.choices.flatMap((c) => ["--choose", c])
12415
12425
  ];
12416
- const res = deps.crew(args, { env: { CREW_CONNECT_KEY: o.key } });
12426
+ const res = deps.crew(args, { env: { CREW_CONNECT_KEY: keyFor(route2) } });
12417
12427
  const events = parseEvents(res.stdout);
12418
12428
  for (const e of events) if (o.json) write(`${JSON.stringify({ ...e, phase: "connect" })}
12419
12429
  `);
@@ -12503,6 +12513,15 @@ function tokensFromEnv(hosts, env) {
12503
12513
  }
12504
12514
  return out;
12505
12515
  }
12516
+ function keysFromEnv(routes, env) {
12517
+ const out = {};
12518
+ for (const r of routes) {
12519
+ const ws = r.split("/")[0];
12520
+ const v = env[`CREW_CONNECT_KEY_${envKey(ws)}`]?.trim();
12521
+ if (v) out[ws] = v;
12522
+ }
12523
+ return out;
12524
+ }
12506
12525
  function repeated(argv2, name) {
12507
12526
  const out = [];
12508
12527
  for (let i = 0; i < argv2.length; i++) {
@@ -12523,9 +12542,11 @@ function provisionOptionsFrom(argv2, env) {
12523
12542
  const forgeFlags = repeated(argv2, "forge-host");
12524
12543
  const tokenAvailable = !!env.CREW_FORGE_TOKEN?.trim() || Object.keys(env).some((k) => k.startsWith("CREW_FORGE_TOKEN_") && env[k]?.trim());
12525
12544
  const forgeHosts = (forgeFlags.length ? forgeFlags : tokenAvailable ? splitList(env.CREW_FORGE_HOSTS ?? "github.com") : []).map((h) => h.toLowerCase());
12545
+ const routeList = routes.length ? routes : splitList(env.CREW_PROVISION_ROUTES);
12526
12546
  return {
12527
12547
  key: one("key") ?? env.CREW_CONNECT_KEY,
12528
- routes: routes.length ? routes : splitList(env.CREW_PROVISION_ROUTES),
12548
+ keys: keysFromEnv(routeList, env),
12549
+ routes: routeList,
12529
12550
  shipName: one("ship-name") ?? env.CREW_SHIP_NAME,
12530
12551
  shipKind: one("ship-kind"),
12531
12552
  instanceClass: one("instance-class"),
@@ -12546,6 +12567,7 @@ function realProvisionDeps(crewHome) {
12546
12567
  const [bin, ...pre] = crewInvocation(crewHome);
12547
12568
  const env = { ...process.env, ...o.env };
12548
12569
  delete env.CREW_FORGE_TOKEN;
12570
+ for (const k of Object.keys(env)) if (k.startsWith("CREW_CONNECT_KEY_")) delete env[k];
12549
12571
  const res = spawnSync7(bin, [...pre, ...args], { cwd: crewHome, encoding: "utf8", env, input: o.input, maxBuffer: 64 * 1024 * 1024 });
12550
12572
  return { status: res.status ?? 1, stdout: res.stdout ?? "", stderr: `${res.stderr ?? ""}${res.error ? String(res.error) : ""}` };
12551
12573
  };
@@ -12851,6 +12873,7 @@ function realPoolMemberDeps(o) {
12851
12873
  const lines2 = [];
12852
12874
  const outcome = await runProvision({
12853
12875
  key: lease.connectKey,
12876
+ keys: Object.fromEntries((lease.connectKeys ?? []).map((k) => [k.workspace, k.key])),
12854
12877
  routes: lease.routes.map((r) => `${r.workspace}/${r.project}`),
12855
12878
  shipName: lease.shipName,
12856
12879
  shipKind,
@@ -19493,6 +19516,8 @@ async function call(api, method, url, body) {
19493
19516
  }
19494
19517
  function bodyFor(d, forPatch) {
19495
19518
  const body = { label: d.label, url: d.url };
19519
+ if (d.controlUrl !== void 0) body.controlUrl = d.controlUrl;
19520
+ if (d.discoverable !== void 0) body.discoverable = d.discoverable;
19496
19521
  if (d.mcpSecret) body.authorizationHeader = `Bearer ${d.mcpSecret}`;
19497
19522
  else if (forPatch) body.authorizationHeader = null;
19498
19523
  return body;
@@ -19541,15 +19566,17 @@ async function probeEndpointAccess(api, workspaceId) {
19541
19566
  }
19542
19567
  }
19543
19568
  var REGISTER_MCP_ENDPOINTS_HINT = "give this key's role the REGISTER_MCP_ENDPOINTS capability, e.g. via the group that holds the Crew project role";
19544
- function registrationKey(url, mcpSecret) {
19569
+ function registrationKey(url, mcpSecret, controlUrl) {
19545
19570
  return createHash8("sha256").update(`${url}
19546
- ${mcpSecret ?? ""}`).digest("hex").slice(0, 16);
19571
+ ${mcpSecret ?? ""}
19572
+ ${controlUrl ?? ""}`).digest("hex").slice(0, 16);
19547
19573
  }
19548
19574
  async function reconcileEndpoint(workspaceId, d) {
19549
19575
  const st = readPersistedTunnel(d.stateDir, workspaceId);
19550
19576
  if (!st || st.status !== "connected" || !st.slug) return;
19551
19577
  const url = d.urlFor(st.slug);
19552
- const key = registrationKey(url, st.mcpSecret);
19578
+ const controlUrl = st.controlSocket && d.controlUrlFor ? d.controlUrlFor(st.slug) : null;
19579
+ const key = registrationKey(url, st.mcpSecret, controlUrl);
19553
19580
  if (st.endpointId && st.registeredKey === key) return;
19554
19581
  const api = d.apiFor(workspaceId);
19555
19582
  if (!api) {
@@ -19559,7 +19586,8 @@ async function reconcileEndpoint(workspaceId, d) {
19559
19586
  const out = await registerEndpoint(api, workspaceId, st.endpointId, {
19560
19587
  label: `Ship: ${d.shipName}`,
19561
19588
  url,
19562
- mcpSecret: st.mcpSecret
19589
+ mcpSecret: st.mcpSecret,
19590
+ controlUrl
19563
19591
  });
19564
19592
  if (out.kind === "ok") {
19565
19593
  updatePersistedTunnel(d.stateDir, workspaceId, { endpointId: out.endpointId, registeredKey: key });
@@ -21232,7 +21260,8 @@ async function registerPeriscopeEndpoint(d, p) {
21232
21260
  const out = await registerEndpoint(api, p.workspaceId, stored?.endpointId, {
21233
21261
  label: `Periscope: ${d.shipName} ${p.ticket}`,
21234
21262
  url,
21235
- mcpSecret: secret
21263
+ mcpSecret: secret,
21264
+ discoverable: false
21236
21265
  });
21237
21266
  if (out.kind !== "ok") {
21238
21267
  return { ok: false, message: out.kind === "forbidden" ? "registering the endpoint was refused (403): the route key lacks REGISTER_MCP_ENDPOINTS" : out.message };
@@ -22047,6 +22076,7 @@ if (command === "passenger-sync-daemon") {
22047
22076
  shipName: cfg.ship.name,
22048
22077
  apiFor: endpointApiFor(routes),
22049
22078
  urlFor: (slug) => publicUrlFor(slug, relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain),
22079
+ controlUrlFor: (slug) => `${relayBaseUrlFor(slug, relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain)}/control`,
22050
22080
  warn: (m) => emit.warn(m, { step: "passengers" })
22051
22081
  };
22052
22082
  }, releaseRefused = function() {
@@ -27122,6 +27152,9 @@ ${foreign.join("\n")}
27122
27152
  ` ${r.route} url: ${slug ? publicUrlFor(slug, cfg.ship.relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain) : "not minted yet (first tunnel cycle creates it)"}
27123
27153
  `
27124
27154
  );
27155
+ const controlUrl = slug && readPersistedTunnel(cfg.ship.stateDir, ws)?.controlSocket ? `${relayBaseUrlFor(slug, cfg.ship.relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain)}/control` : void 0;
27156
+ process.stdout.write(` ${r.route} control url: ${controlUrl ?? "none (the tunnel carries passengers only)"}
27157
+ `);
27125
27158
  const api = endpointApiFor([r])(ws);
27126
27159
  if (!api) continue;
27127
27160
  const probe = await probeEndpointAccess(api, ws);
@@ -27140,7 +27173,7 @@ ${foreign.join("\n")}
27140
27173
  }
27141
27174
  for (const e of listPeriscopeEndpoints(cfg.ship.stateDir)) {
27142
27175
  const secret = readPeriscopeSecret(cfg.ship.stateDir, e.ticketRecordId) ? "live" : "ended, awaiting removal";
27143
- process.stdout.write(`periscope mcp: ${e.ticket} ${e.route} ${e.url} (${secret})
27176
+ process.stdout.write(`periscope mcp: ${e.ticket} ${e.route} ${e.url} (${secret}, attached only)
27144
27177
  `);
27145
27178
  }
27146
27179
  if (cfg.ship.relayHost && !flag("offline")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.102.0",
3
+ "version": "0.103.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",