@tablation/crew 0.126.4 → 0.126.5

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 +36 -0
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -15097,6 +15097,12 @@ function sshArgsFor(o) {
15097
15097
  "ServerAliveCountMax=3",
15098
15098
  "-o",
15099
15099
  "ExitOnForwardFailure=yes",
15100
+ // Offer only the ship key. Without this OpenSSH offers every ssh-agent key
15101
+ // before the `-i` one, the relay hashes whichever it sees first, and a
15102
+ // slug owned by the ship key's fingerprint is rejected as a conflict
15103
+ // (CREW-2932).
15104
+ "-o",
15105
+ "IdentitiesOnly=yes",
15100
15106
  "-i",
15101
15107
  o.privateKeyPath,
15102
15108
  "-p",
@@ -15173,6 +15179,7 @@ function isPidAlive(pid) {
15173
15179
  }
15174
15180
  }
15175
15181
  var CONTROL_ONLY_HASH = "control-only";
15182
+ var TUNNEL_REJECTED_AFTER = 2;
15176
15183
  function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, stateDir, mcpSecrets = /* @__PURE__ */ new Map(), deps = {}) {
15177
15184
  const alive = deps.isPidAlive ?? isPidAlive;
15178
15185
  const plannedIds = new Set(plans.map((p) => p.workspaceId));
@@ -15203,6 +15210,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
15203
15210
  deps.onStatus?.(plan.workspaceId, "disconnected", existing);
15204
15211
  continue;
15205
15212
  }
15213
+ const diedQuickly = existing !== void 0 && existing.status === "connecting" && !alive(existing.pid) && controlUnchanged && secretUnchanged && slugUnchanged && existing.mountsHash === mountsHash;
15214
+ const quickExits = diedQuickly ? (existing.quickExits ?? 0) + 1 : 0;
15206
15215
  let settledStatus = "connecting";
15207
15216
  const tunnel = startTunnel({
15208
15217
  relayHost,
@@ -15230,6 +15239,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
15230
15239
  mcpSecret: currentSecret,
15231
15240
  slug: slug2,
15232
15241
  liveSlug: slug2,
15242
+ quickExits,
15233
15243
  endpointId: existing?.endpointId,
15234
15244
  registeredKey: existing?.registeredKey
15235
15245
  };
@@ -15254,6 +15264,18 @@ function readAllPersistedWorkspaceIds(stateDir) {
15254
15264
  if (!existsSync35(dir)) return [];
15255
15265
  return readdirSync13(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
15256
15266
  }
15267
+ var TUNNEL_REJECTED_KEY = "tunnel_rejected";
15268
+ function tunnelRejectedMessage(workspaceId, quickExits) {
15269
+ 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`;
15270
+ }
15271
+ function rejectedTunnels(stateDir) {
15272
+ const out = [];
15273
+ for (const workspaceId of readAllPersistedWorkspaceIds(stateDir)) {
15274
+ const q2 = readPersistedTunnel(stateDir, workspaceId)?.quickExits ?? 0;
15275
+ if (q2 >= TUNNEL_REJECTED_AFTER) out.push({ workspaceId, quickExits: q2 });
15276
+ }
15277
+ return out;
15278
+ }
15257
15279
 
15258
15280
  // src/pool-member-host.ts
15259
15281
  function loadShip(crewHome) {
@@ -25442,6 +25464,15 @@ if (command === "passenger-sync-daemon") {
25442
25464
  })
25443
25465
  );
25444
25466
  if (status2 === "disconnected") pending.push(unregisterEndpoint(workspaceId, info, reconcile));
25467
+ const rejectedKey = `${TUNNEL_REJECTED_KEY}:${workspaceId}`;
25468
+ if ((info?.quickExits ?? 0) >= TUNNEL_REJECTED_AFTER) {
25469
+ pending.push(raiseShipAttention({ state, emit, route: owningRoute, ship: cfg.ship }, {
25470
+ kind: TUNNEL_REJECTED_KEY,
25471
+ key: rejectedKey,
25472
+ message: tunnelRejectedMessage(workspaceId, info.quickExits),
25473
+ since: (/* @__PURE__ */ new Date()).toISOString()
25474
+ }).then(() => void 0).catch(() => void 0));
25475
+ } else if (status2 === "connected") clearShipAttention(state, rejectedKey);
25445
25476
  }
25446
25477
  });
25447
25478
  for (const plan of containerPlans) pending.push(reconcileEndpoint(plan.workspaceId, reconcile));
@@ -30122,6 +30153,9 @@ watermark: ${state.watermark()}
30122
30153
  if (!flag("offline")) {
30123
30154
  for (const message of managedRuntimeWarnings(await doctorShipKind(route2, cfg.ship), cfg.ship)) problems.push({ severity: "attention", kind: "managed_runtime_default", message });
30124
30155
  }
30156
+ for (const t of rejectedTunnels(cfg.ship.stateDir)) {
30157
+ problems.push({ severity: "attention", kind: TUNNEL_REJECTED_KEY, message: tunnelRejectedMessage(t.workspaceId, t.quickExits), remedy: "rotate the slug with `crew rotate-passenger-url`" });
30158
+ }
30125
30159
  const previewProblem2 = await previewHostProblem(cfg);
30126
30160
  if (previewProblem2) problems.push(previewHostFinding(previewProblem2));
30127
30161
  if (cfg.routes.some((x) => x.route === route2.route && x.enabled && x.hostPeriscopes)) {
@@ -30142,6 +30176,8 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
30142
30176
  `
30143
30177
  );
30144
30178
  for (const w of roleCapWarnings(cfg.ship)) process.stdout.write(`role concurrency: WARN ${w}
30179
+ `);
30180
+ for (const t of rejectedTunnels(cfg.ship.stateDir)) process.stdout.write(`relay tunnel: ATTENTION ${tunnelRejectedMessage(t.workspaceId, t.quickExits)}
30145
30181
  `);
30146
30182
  process.stdout.write(`periscopes: ${periscopeSummary(cfg)}
30147
30183
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.126.4",
3
+ "version": "0.126.5",
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",