@tablation/crew 0.126.4 → 0.126.6

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 +48 -7
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -690,9 +690,9 @@ function urlPort(url) {
690
690
  return void 0;
691
691
  }
692
692
  }
693
- function portListening(port, timeoutMs = 500) {
693
+ function hostListening(port, host, timeoutMs) {
694
694
  return new Promise((resolve8) => {
695
- const s = connect({ port, host: "127.0.0.1" });
695
+ const s = connect({ port, host });
696
696
  const done = (ok) => {
697
697
  s.destroy();
698
698
  resolve8(ok);
@@ -702,6 +702,10 @@ function portListening(port, timeoutMs = 500) {
702
702
  s.once("error", () => done(false));
703
703
  });
704
704
  }
705
+ async function portListening(port, timeoutMs = 500) {
706
+ if (await hostListening(port, "127.0.0.1", timeoutMs)) return true;
707
+ return hostListening(port, "::1", timeoutMs);
708
+ }
705
709
  async function periscopesForStatus(stateDir, probe = portListening) {
706
710
  return Promise.all(readPeriscopeSidecars(stateDir).map(async (sc) => {
707
711
  const port = urlPort(sc.url);
@@ -15097,6 +15101,12 @@ function sshArgsFor(o) {
15097
15101
  "ServerAliveCountMax=3",
15098
15102
  "-o",
15099
15103
  "ExitOnForwardFailure=yes",
15104
+ // Offer only the ship key. Without this OpenSSH offers every ssh-agent key
15105
+ // before the `-i` one, the relay hashes whichever it sees first, and a
15106
+ // slug owned by the ship key's fingerprint is rejected as a conflict
15107
+ // (CREW-2932).
15108
+ "-o",
15109
+ "IdentitiesOnly=yes",
15100
15110
  "-i",
15101
15111
  o.privateKeyPath,
15102
15112
  "-p",
@@ -15173,6 +15183,7 @@ function isPidAlive(pid) {
15173
15183
  }
15174
15184
  }
15175
15185
  var CONTROL_ONLY_HASH = "control-only";
15186
+ var TUNNEL_REJECTED_AFTER = 2;
15176
15187
  function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, stateDir, mcpSecrets = /* @__PURE__ */ new Map(), deps = {}) {
15177
15188
  const alive = deps.isPidAlive ?? isPidAlive;
15178
15189
  const plannedIds = new Set(plans.map((p) => p.workspaceId));
@@ -15203,6 +15214,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
15203
15214
  deps.onStatus?.(plan.workspaceId, "disconnected", existing);
15204
15215
  continue;
15205
15216
  }
15217
+ const diedQuickly = existing !== void 0 && existing.status === "connecting" && !alive(existing.pid) && controlUnchanged && secretUnchanged && slugUnchanged && existing.mountsHash === mountsHash;
15218
+ const quickExits = diedQuickly ? (existing.quickExits ?? 0) + 1 : 0;
15206
15219
  let settledStatus = "connecting";
15207
15220
  const tunnel = startTunnel({
15208
15221
  relayHost,
@@ -15230,6 +15243,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
15230
15243
  mcpSecret: currentSecret,
15231
15244
  slug: slug2,
15232
15245
  liveSlug: slug2,
15246
+ quickExits,
15233
15247
  endpointId: existing?.endpointId,
15234
15248
  registeredKey: existing?.registeredKey
15235
15249
  };
@@ -15254,6 +15268,18 @@ function readAllPersistedWorkspaceIds(stateDir) {
15254
15268
  if (!existsSync35(dir)) return [];
15255
15269
  return readdirSync13(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
15256
15270
  }
15271
+ var TUNNEL_REJECTED_KEY = "tunnel_rejected";
15272
+ function tunnelRejectedMessage(workspaceId, quickExits) {
15273
+ return `the relay tunnel for workspace ${workspaceId} exited before settling on ${quickExits} consecutive cycles, which is how a refused claim looks (the relay logs "slug already owned by another workspace/ssh key") \u2014 rotate the slug with \`crew rotate-passenger-url\` or check the relay journal`;
15274
+ }
15275
+ function rejectedTunnels(stateDir) {
15276
+ const out = [];
15277
+ for (const workspaceId of readAllPersistedWorkspaceIds(stateDir)) {
15278
+ const q2 = readPersistedTunnel(stateDir, workspaceId)?.quickExits ?? 0;
15279
+ if (q2 >= TUNNEL_REJECTED_AFTER) out.push({ workspaceId, quickExits: q2 });
15280
+ }
15281
+ return out;
15282
+ }
15257
15283
 
15258
15284
  // src/pool-member-host.ts
15259
15285
  function loadShip(crewHome) {
@@ -25442,6 +25468,15 @@ if (command === "passenger-sync-daemon") {
25442
25468
  })
25443
25469
  );
25444
25470
  if (status2 === "disconnected") pending.push(unregisterEndpoint(workspaceId, info, reconcile));
25471
+ const rejectedKey = `${TUNNEL_REJECTED_KEY}:${workspaceId}`;
25472
+ if ((info?.quickExits ?? 0) >= TUNNEL_REJECTED_AFTER) {
25473
+ pending.push(raiseShipAttention({ state, emit, route: owningRoute, ship: cfg.ship }, {
25474
+ kind: TUNNEL_REJECTED_KEY,
25475
+ key: rejectedKey,
25476
+ message: tunnelRejectedMessage(workspaceId, info.quickExits),
25477
+ since: (/* @__PURE__ */ new Date()).toISOString()
25478
+ }).then(() => void 0).catch(() => void 0));
25479
+ } else if (status2 === "connected") clearShipAttention(state, rejectedKey);
25445
25480
  }
25446
25481
  });
25447
25482
  for (const plan of containerPlans) pending.push(reconcileEndpoint(plan.workspaceId, reconcile));
@@ -28920,11 +28955,12 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
28920
28955
  });
28921
28956
  };
28922
28957
  if (subP === "list") {
28923
- const entries = periscopeListEntries(
28924
- (await Promise.all(routesP.map(gather))).flat().map((r) => ({ ...r, pointedAt: readPointedAt(r.worktree) })),
28925
- /* @__PURE__ */ new Date(),
28926
- (r) => pidsInWorktree(r.worktree, Number(r.number)).length > 0
28927
- );
28958
+ const listRows = (await Promise.all(routesP.map(gather))).flat().map((r) => ({ ...r, pointedAt: readPointedAt(r.worktree) }));
28959
+ const serving = new Map(await Promise.all(listRows.map(async (r) => {
28960
+ const port = urlPort(r.url);
28961
+ return [r.id, port === void 0 ? false : await portListening(port)];
28962
+ })));
28963
+ const entries = periscopeListEntries(listRows, /* @__PURE__ */ new Date(), (r) => serving.get(r.id) ?? false);
28928
28964
  process.stdout.write(jsonP ? `${JSON.stringify(entries)}
28929
28965
  ` : `${formatPeriscopeList(entries)}
28930
28966
  `);
@@ -30122,6 +30158,9 @@ watermark: ${state.watermark()}
30122
30158
  if (!flag("offline")) {
30123
30159
  for (const message of managedRuntimeWarnings(await doctorShipKind(route2, cfg.ship), cfg.ship)) problems.push({ severity: "attention", kind: "managed_runtime_default", message });
30124
30160
  }
30161
+ for (const t of rejectedTunnels(cfg.ship.stateDir)) {
30162
+ problems.push({ severity: "attention", kind: TUNNEL_REJECTED_KEY, message: tunnelRejectedMessage(t.workspaceId, t.quickExits), remedy: "rotate the slug with `crew rotate-passenger-url`" });
30163
+ }
30125
30164
  const previewProblem2 = await previewHostProblem(cfg);
30126
30165
  if (previewProblem2) problems.push(previewHostFinding(previewProblem2));
30127
30166
  if (cfg.routes.some((x) => x.route === route2.route && x.enabled && x.hostPeriscopes)) {
@@ -30142,6 +30181,8 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
30142
30181
  `
30143
30182
  );
30144
30183
  for (const w of roleCapWarnings(cfg.ship)) process.stdout.write(`role concurrency: WARN ${w}
30184
+ `);
30185
+ for (const t of rejectedTunnels(cfg.ship.stateDir)) process.stdout.write(`relay tunnel: ATTENTION ${tunnelRejectedMessage(t.workspaceId, t.quickExits)}
30145
30186
  `);
30146
30187
  process.stdout.write(`periscopes: ${periscopeSummary(cfg)}
30147
30188
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.126.4",
3
+ "version": "0.126.6",
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",