@tablation/crew 0.79.2 → 0.79.4

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 +288 -262
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -8942,6 +8942,285 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
8942
8942
  return { adopted, ...interrupted ? { interrupted } : {}, ms: ms2, ticket };
8943
8943
  }
8944
8944
 
8945
+ // src/seat-keys.ts
8946
+ import { getSessionStore as getSessionStore2, hostFromUrl as hostFromUrl2 } from "@tablation/client";
8947
+ var SEAT_KEY_ROLES = ROLE_NAMES.filter((r) => r !== "pair");
8948
+ function keyIsAdmin(route) {
8949
+ const role = route.resolved?.role;
8950
+ return role === void 0 || role === "WORKSPACE_ADMIN" || role === "PLATFORM_ADMIN";
8951
+ }
8952
+ var MEMBER_NO_ROW = "has no linked Agents row \u2014 a workspace admin's `crew agents sync` creates the persona rows; run it here afterwards to link this ship's seats";
8953
+ var MEMBER_NO_KEY = 'no agent key for this seat \u2014 run `crew agents sync` (needs the "Mint agent keys" capability on your role)';
8954
+ var FORBIDDEN_LINE = 'seat keys: not minted \u2014 your role does not grant "Mint agent keys"; ask a workspace admin to add it to your project role (or run sync with an admin key)';
8955
+ function seatKeySlug(workspaceSlug, agentId) {
8956
+ return `${workspaceSlug}#agent:${agentId}`;
8957
+ }
8958
+ function slugFor(route, agentId) {
8959
+ const [workspaceSlug] = splitRoute(route.route);
8960
+ return seatKeySlug(workspaceSlug, agentId);
8961
+ }
8962
+ function seatAgentId(route, role) {
8963
+ return route.resolved?.agentPersonas?.[role]?.agentId;
8964
+ }
8965
+ async function resolveReleaseKey(route, getStore = getSessionStore2) {
8966
+ const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route), reason });
8967
+ const agentId = route.resolved?.releaseAgentId;
8968
+ if (!agentId) return ship(keyIsAdmin(route) ? "release seat has no linked Agents row (run `crew agents sync`)" : `release seat ${MEMBER_NO_ROW}`);
8969
+ try {
8970
+ const session = await getStore().get(hostFromUrl2(route.baseUrl), slugFor(route, agentId));
8971
+ if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
8972
+ return ship(keyIsAdmin(route) ? "no agent key stored for the release seat (run `crew agents sync`)" : MEMBER_NO_KEY);
8973
+ } catch (e) {
8974
+ return ship(describeReadError(e));
8975
+ }
8976
+ }
8977
+ async function resolveSeatKey(route, role, getStore = getSessionStore2) {
8978
+ const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route), reason });
8979
+ if (!SEAT_KEY_ROLES.includes(role)) return ship(`${role} always writes as the ship`);
8980
+ const agentId = seatAgentId(route, role);
8981
+ if (!agentId) return ship(keyIsAdmin(route) ? "seat has no linked Agents row (run `crew agents sync`)" : `seat ${MEMBER_NO_ROW}`);
8982
+ try {
8983
+ const session = await getStore().get(hostFromUrl2(route.baseUrl), slugFor(route, agentId));
8984
+ if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
8985
+ return ship(keyIsAdmin(route) ? "no agent key stored for this seat (run `crew agents sync`)" : MEMBER_NO_KEY);
8986
+ } catch (e) {
8987
+ return ship(describeReadError(e));
8988
+ }
8989
+ }
8990
+ function safeStderrLine(e) {
8991
+ const err = e;
8992
+ const raw = typeof err?.stderr === "string" && err.stderr.trim() ? err.stderr : typeof err?.message === "string" ? err.message : "";
8993
+ for (const line of raw.split("\n")) {
8994
+ const t = line.trim();
8995
+ if (!t || /^Command failed:/i.test(t) || /(^|\s)-w(\s|$)/.test(t)) continue;
8996
+ return t.slice(0, 120);
8997
+ }
8998
+ return void 0;
8999
+ }
9000
+ function describeReadError(e) {
9001
+ const code = e?.code;
9002
+ const line = safeStderrLine(e);
9003
+ const what = typeof code === "number" ? `security exited ${code}` : typeof code === "string" ? `security failed: ${code}` : void 0;
9004
+ const detail = [what, line].filter(Boolean).join(what && line ? ": " : "");
9005
+ return detail ? `OS keychain unavailable (${detail})` : "OS keychain unavailable";
9006
+ }
9007
+ function keychainExitCode(reason) {
9008
+ const m = /security exited (\d+)/.exec(reason);
9009
+ return m ? Number(m[1]) : void 0;
9010
+ }
9011
+ function describeStoreError(e) {
9012
+ const msg = e instanceof Error ? e.message : String(e);
9013
+ if (/User interaction is not allowed/i.test(msg)) {
9014
+ return "keychain unavailable (User interaction is not allowed \u2014 run from a logged-in session, or `security unlock-keychain` first)";
9015
+ }
9016
+ return "keychain unavailable (could not write to the OS keychain)";
9017
+ }
9018
+ async function mintMissingSeatKeys(route, o) {
9019
+ const out = [];
9020
+ if (!route.resolved) return out;
9021
+ const resolved = route.resolved;
9022
+ const doFetch = o.fetchImpl ?? fetch;
9023
+ const targets = [
9024
+ ...SEAT_KEY_ROLES.filter((r) => resolved.seats[r]).map((role) => ({ role, agentId: seatAgentId(route, role) })),
9025
+ ...resolved.releaseSeat ? [{ role: "release", agentId: resolved.releaseAgentId }] : []
9026
+ ];
9027
+ let store;
9028
+ try {
9029
+ store = (o.getStore ?? getSessionStore2)();
9030
+ } catch (e) {
9031
+ return targets.map(({ role }) => ({
9032
+ role,
9033
+ action: "failed",
9034
+ detail: describeStoreError(e)
9035
+ }));
9036
+ }
9037
+ const host = hostFromUrl2(route.baseUrl);
9038
+ const base = route.baseUrl.replace(/\/+$/, "");
9039
+ if (!o.dryRun) {
9040
+ const probeSlug = seatKeySlug(splitRoute(route.route)[0], "_probe");
9041
+ try {
9042
+ await store.set(host, probeSlug, {
9043
+ apiKey: "probe",
9044
+ shipId: "",
9045
+ workspaceId: resolved.workspaceId,
9046
+ identityId: "_probe",
9047
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
9048
+ });
9049
+ await store.clear(host, probeSlug);
9050
+ } catch (e) {
9051
+ const detail = `${describeStoreError(e)}; no keys minted`;
9052
+ return targets.map(({ role, agentId }) => agentId ? { role, action: "failed", detail } : { role, action: "no_agent" });
9053
+ }
9054
+ }
9055
+ for (const { role, agentId } of targets) {
9056
+ if (!agentId) {
9057
+ out.push({ role, action: "no_agent" });
9058
+ continue;
9059
+ }
9060
+ const slug = slugFor(route, agentId);
9061
+ try {
9062
+ if ((await store.get(host, slug))?.apiKey) {
9063
+ out.push({ role, action: "already_stored" });
9064
+ continue;
9065
+ }
9066
+ if (o.dryRun) {
9067
+ out.push({ role, action: "minted", detail: "dry run" });
9068
+ continue;
9069
+ }
9070
+ const res = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys`, {
9071
+ method: "POST",
9072
+ headers: {
9073
+ Authorization: `Bearer ${resolveApiKey(route)}`,
9074
+ "Content-Type": "application/json",
9075
+ "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0"
9076
+ },
9077
+ body: JSON.stringify({ name: `crew ${o.shipName ?? "ship"} ${role}` })
9078
+ });
9079
+ if (res.status === 403) {
9080
+ out.push({ role, action: "forbidden" });
9081
+ break;
9082
+ }
9083
+ if (!res.ok) {
9084
+ out.push({ role, action: "failed", detail: `${res.status} ${res.statusText}` });
9085
+ continue;
9086
+ }
9087
+ const body = await res.json();
9088
+ if (!body.key) {
9089
+ out.push({ role, action: "failed", detail: "response carried no key" });
9090
+ continue;
9091
+ }
9092
+ try {
9093
+ const session = {
9094
+ apiKey: body.key,
9095
+ shipId: o.shipName ?? "",
9096
+ workspaceId: resolved.workspaceId,
9097
+ identityId: agentId,
9098
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
9099
+ ...body.id ? { keyId: body.id } : {}
9100
+ };
9101
+ await store.set(host, slug, session);
9102
+ } catch (e) {
9103
+ let revoked = false;
9104
+ if (body.id) {
9105
+ try {
9106
+ const del2 = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys/${body.id}`, {
9107
+ method: "DELETE",
9108
+ headers: { Authorization: `Bearer ${resolveApiKey(route)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
9109
+ });
9110
+ revoked = del2.ok;
9111
+ } catch {
9112
+ revoked = false;
9113
+ }
9114
+ }
9115
+ out.push({
9116
+ role,
9117
+ action: "failed",
9118
+ detail: revoked ? `minted but could not be stored (${describeStoreError(e)}); revoked` : `a key for ${role} is live on the server and not stored (${describeStoreError(e)}) \u2014 revoke it from that seat's Agents row`
9119
+ });
9120
+ continue;
9121
+ }
9122
+ out.push({ role, action: "minted" });
9123
+ } catch (e) {
9124
+ out.push({ role, action: "failed", detail: /Command failed|security|sk_|-w /i.test(e.message) ? describeStoreError(e) : e.message });
9125
+ }
9126
+ }
9127
+ return out;
9128
+ }
9129
+ function describeMintOutcome(o) {
9130
+ switch (o.action) {
9131
+ case "minted":
9132
+ return `${o.role}: agent key minted${o.detail ? ` (${o.detail})` : ""}`;
9133
+ case "already_stored":
9134
+ return `${o.role}: agent key already stored`;
9135
+ case "no_agent":
9136
+ return `${o.role}: no linked Agents row \u2014 writes as the ship until \`crew agents sync\` links one`;
9137
+ case "forbidden":
9138
+ return FORBIDDEN_LINE;
9139
+ case "failed":
9140
+ return `${o.role}: could not mint an agent key (${o.detail ?? "unknown"}) \u2014 writes as the ship`;
9141
+ }
9142
+ }
9143
+ async function revokeSeatKeys(route, o) {
9144
+ const out = [];
9145
+ if (!route.resolved) return out;
9146
+ const resolved = route.resolved;
9147
+ const doFetch = o.fetchImpl ?? fetch;
9148
+ const targets = [
9149
+ ...SEAT_KEY_ROLES.filter((r) => resolved.seats[r]).map((role) => ({ role, agentId: seatAgentId(route, role) })),
9150
+ ...resolved.releaseSeat ? [{ role: "release", agentId: resolved.releaseAgentId }] : []
9151
+ ];
9152
+ let store;
9153
+ try {
9154
+ store = (o.getStore ?? getSessionStore2)();
9155
+ } catch (e) {
9156
+ return targets.map(({ role }) => ({ role, action: "failed", detail: describeStoreError(e) }));
9157
+ }
9158
+ const host = hostFromUrl2(route.baseUrl);
9159
+ const base = route.baseUrl.replace(/\/+$/, "");
9160
+ for (const { role, agentId } of targets) {
9161
+ if (!agentId) {
9162
+ out.push({ role, action: "no_agent" });
9163
+ continue;
9164
+ }
9165
+ const slug = slugFor(route, agentId);
9166
+ try {
9167
+ const session = await store.get(host, slug);
9168
+ if (!session?.apiKey) {
9169
+ out.push({ role, action: "not_stored" });
9170
+ continue;
9171
+ }
9172
+ if (!session.keyId) {
9173
+ out.push({ role, action: "failed", detail: "no key id was recorded when this key was minted \u2014 revoke it from the seat's Agents row" });
9174
+ continue;
9175
+ }
9176
+ if (o.dryRun) {
9177
+ out.push({ role, action: "revoked", detail: "dry run" });
9178
+ continue;
9179
+ }
9180
+ const del2 = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys/${session.keyId}`, {
9181
+ method: "DELETE",
9182
+ headers: { Authorization: `Bearer ${resolveApiKey(route)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
9183
+ });
9184
+ if (!del2.ok && del2.status !== 404) {
9185
+ out.push({ role, action: "failed", detail: `${del2.status} ${del2.statusText}` });
9186
+ continue;
9187
+ }
9188
+ await store.clear(host, slug);
9189
+ out.push({ role, action: "revoked", detail: del2.ok ? void 0 : "server already had no such key" });
9190
+ } catch (e) {
9191
+ out.push({ role, action: "failed", detail: /Command failed|security|sk_|-w /i.test(e.message) ? describeStoreError(e) : e.message });
9192
+ }
9193
+ }
9194
+ return out;
9195
+ }
9196
+ function describeRevokeOutcome(o) {
9197
+ switch (o.action) {
9198
+ case "revoked":
9199
+ return `${o.role}: agent key revoked and cleared from the keychain${o.detail ? ` (${o.detail})` : ""}`;
9200
+ case "not_stored":
9201
+ return `${o.role}: no agent key stored on this ship`;
9202
+ case "no_agent":
9203
+ return `${o.role}: no linked Agents row`;
9204
+ case "failed":
9205
+ return `${o.role}: could not revoke the agent key (${o.detail ?? "unknown"}) \u2014 left in the keychain`;
9206
+ }
9207
+ }
9208
+ async function canMintAgentKeys(route, o = {}) {
9209
+ const resolved = route.resolved;
9210
+ if (!resolved) return void 0;
9211
+ const agentId = seatAgentId(route, "dev") ?? SEAT_KEY_ROLES.map((r) => seatAgentId(route, r)).find(Boolean);
9212
+ if (!agentId) return void 0;
9213
+ try {
9214
+ const res = await (o.fetchImpl ?? fetch)(`${route.baseUrl.replace(/\/+$/, "")}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys`, {
9215
+ headers: { Authorization: `Bearer ${resolveApiKey(route)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
9216
+ });
9217
+ if (res.ok) return true;
9218
+ return res.status === 403 ? false : void 0;
9219
+ } catch {
9220
+ return void 0;
9221
+ }
9222
+ }
9223
+
8945
9224
  // src/selftest.ts
8946
9225
  var SELFTEST_TIMEOUT_MS = 12e4;
8947
9226
  var KEY_VAR = "CREW_API_KEY";
@@ -9052,6 +9331,7 @@ var REMEDY = {
9052
9331
  "mcp-unavailable": "register the tracker MCP server (`crew doctor` shows how), then re-run `crew selftest`",
9053
9332
  "key-refused": "the dev seat's key was refused \u2014 ask a workspace admin to mint a fresh one (`crew agents sync`)",
9054
9333
  "keychain-locked": "unlock the OS keychain from a logged-in session (`security unlock-keychain`), then re-run",
9334
+ "keychain-error": "run `security find-generic-password -s com.tablation.cli -g` by hand, and check `security list-keychains -d user` and `security default-keychain -d user`",
9055
9335
  timeout: "the harness did not answer in time \u2014 run it by hand to see where it stalls",
9056
9336
  "wrong-answer": "the agent answered but did not name the workspace \u2014 run it by hand to see what it said"
9057
9337
  };
@@ -9085,7 +9365,10 @@ async function runSelftest(i, deps = realDeps) {
9085
9365
  const base = { toolchain: spec.name, harness: spec.harness, model: spec.model };
9086
9366
  const finish = (r) => ({ ...base, ...r, ms: now() - started, ranAt: new Date(started).toISOString() });
9087
9367
  const failed = (failure, what) => finish({ ok: false, failure, detail: `${what} \u2014 ${REMEDY[failure]}` });
9088
- if (!i.apiKey) return failed("keychain-locked", i.keyProblem ?? "no seat key could be read");
9368
+ if (!i.apiKey) {
9369
+ const locked = keychainExitCode(i.keyProblem ?? "") === 36;
9370
+ return failed(locked ? "keychain-locked" : "keychain-error", i.keyProblem ?? "no seat key could be read");
9371
+ }
9089
9372
  const adapter = adapterFor(spec.harness);
9090
9373
  const invocation = adapter.invocation(spec, { role: "dev", model: spec.model, mcp: { url: `${i.baseUrl.replace(/\/+$/, "")}/api/mcp`, keyVar: KEY_VAR } });
9091
9374
  const env = {
@@ -9412,7 +9695,7 @@ async function signOutSessions(o) {
9412
9695
  // src/reset.ts
9413
9696
  import { existsSync as existsSync20, mkdirSync as mkdirSync12, readFileSync as readFileSync19, readdirSync as readdirSync7, rmSync as rmSync11, statSync as statSync6, writeFileSync as writeFileSync13 } from "fs";
9414
9697
  import { dirname as dirname9, join as join22 } from "path";
9415
- import { hostFromUrl as hostFromUrl2 } from "@tablation/client";
9698
+ import { hostFromUrl as hostFromUrl3 } from "@tablation/client";
9416
9699
  function clonedCheckoutsFile(stateDir) {
9417
9700
  return join22(stateDir, "cloned-checkouts.json");
9418
9701
  }
@@ -9458,7 +9741,7 @@ async function runReset(o, fx) {
9458
9741
  steps.push({ step: "credential", target: route.route, action: "failed", detail: e.message });
9459
9742
  }
9460
9743
  }
9461
- const covered = new Set([...o.routes, ...o.droppedRoutes ?? []].map((r) => `${hostFromUrl2(r.baseUrl)}/${splitRoute(r.route)[0]}`));
9744
+ const covered = new Set([...o.routes, ...o.droppedRoutes ?? []].map((r) => `${hostFromUrl3(r.baseUrl)}/${splitRoute(r.route)[0]}`));
9462
9745
  try {
9463
9746
  for (const s of await fx.hostSessions(o.stateDir)) {
9464
9747
  const target = `${s.host}/${s.workspace}`;
@@ -12887,264 +13170,6 @@ function formatWhoami(report) {
12887
13170
  `;
12888
13171
  }
12889
13172
 
12890
- // src/seat-keys.ts
12891
- import { getSessionStore as getSessionStore2, hostFromUrl as hostFromUrl3 } from "@tablation/client";
12892
- var SEAT_KEY_ROLES = ROLE_NAMES.filter((r) => r !== "pair");
12893
- function keyIsAdmin(route) {
12894
- const role = route.resolved?.role;
12895
- return role === void 0 || role === "WORKSPACE_ADMIN" || role === "PLATFORM_ADMIN";
12896
- }
12897
- var MEMBER_NO_ROW = "has no linked Agents row \u2014 a workspace admin's `crew agents sync` creates the persona rows; run it here afterwards to link this ship's seats";
12898
- var MEMBER_NO_KEY = 'no agent key for this seat \u2014 run `crew agents sync` (needs the "Mint agent keys" capability on your role)';
12899
- var FORBIDDEN_LINE = 'seat keys: not minted \u2014 your role does not grant "Mint agent keys"; ask a workspace admin to add it to your project role (or run sync with an admin key)';
12900
- function seatKeySlug(workspaceSlug, agentId) {
12901
- return `${workspaceSlug}#agent:${agentId}`;
12902
- }
12903
- function slugFor(route, agentId) {
12904
- const [workspaceSlug] = splitRoute(route.route);
12905
- return seatKeySlug(workspaceSlug, agentId);
12906
- }
12907
- function seatAgentId(route, role) {
12908
- return route.resolved?.agentPersonas?.[role]?.agentId;
12909
- }
12910
- async function resolveReleaseKey(route, getStore = getSessionStore2) {
12911
- const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route), reason });
12912
- const agentId = route.resolved?.releaseAgentId;
12913
- if (!agentId) return ship(keyIsAdmin(route) ? "release seat has no linked Agents row (run `crew agents sync`)" : `release seat ${MEMBER_NO_ROW}`);
12914
- try {
12915
- const session = await getStore().get(hostFromUrl3(route.baseUrl), slugFor(route, agentId));
12916
- if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
12917
- return ship(keyIsAdmin(route) ? "no agent key stored for the release seat (run `crew agents sync`)" : MEMBER_NO_KEY);
12918
- } catch {
12919
- return ship("OS keychain unavailable");
12920
- }
12921
- }
12922
- async function resolveSeatKey(route, role, getStore = getSessionStore2) {
12923
- const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route), reason });
12924
- if (!SEAT_KEY_ROLES.includes(role)) return ship(`${role} always writes as the ship`);
12925
- const agentId = seatAgentId(route, role);
12926
- if (!agentId) return ship(keyIsAdmin(route) ? "seat has no linked Agents row (run `crew agents sync`)" : `seat ${MEMBER_NO_ROW}`);
12927
- try {
12928
- const session = await getStore().get(hostFromUrl3(route.baseUrl), slugFor(route, agentId));
12929
- if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
12930
- return ship(keyIsAdmin(route) ? "no agent key stored for this seat (run `crew agents sync`)" : MEMBER_NO_KEY);
12931
- } catch {
12932
- return ship("OS keychain unavailable");
12933
- }
12934
- }
12935
- function describeStoreError(e) {
12936
- const msg = e instanceof Error ? e.message : String(e);
12937
- if (/User interaction is not allowed/i.test(msg)) {
12938
- return "keychain unavailable (User interaction is not allowed \u2014 run from a logged-in session, or `security unlock-keychain` first)";
12939
- }
12940
- return "keychain unavailable (could not write to the OS keychain)";
12941
- }
12942
- async function mintMissingSeatKeys(route, o) {
12943
- const out = [];
12944
- if (!route.resolved) return out;
12945
- const resolved = route.resolved;
12946
- const doFetch = o.fetchImpl ?? fetch;
12947
- const targets = [
12948
- ...SEAT_KEY_ROLES.filter((r) => resolved.seats[r]).map((role) => ({ role, agentId: seatAgentId(route, role) })),
12949
- ...resolved.releaseSeat ? [{ role: "release", agentId: resolved.releaseAgentId }] : []
12950
- ];
12951
- let store;
12952
- try {
12953
- store = (o.getStore ?? getSessionStore2)();
12954
- } catch (e) {
12955
- return targets.map(({ role }) => ({
12956
- role,
12957
- action: "failed",
12958
- detail: describeStoreError(e)
12959
- }));
12960
- }
12961
- const host = hostFromUrl3(route.baseUrl);
12962
- const base = route.baseUrl.replace(/\/+$/, "");
12963
- if (!o.dryRun) {
12964
- const probeSlug = seatKeySlug(splitRoute(route.route)[0], "_probe");
12965
- try {
12966
- await store.set(host, probeSlug, {
12967
- apiKey: "probe",
12968
- shipId: "",
12969
- workspaceId: resolved.workspaceId,
12970
- identityId: "_probe",
12971
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
12972
- });
12973
- await store.clear(host, probeSlug);
12974
- } catch (e) {
12975
- const detail = `${describeStoreError(e)}; no keys minted`;
12976
- return targets.map(({ role, agentId }) => agentId ? { role, action: "failed", detail } : { role, action: "no_agent" });
12977
- }
12978
- }
12979
- for (const { role, agentId } of targets) {
12980
- if (!agentId) {
12981
- out.push({ role, action: "no_agent" });
12982
- continue;
12983
- }
12984
- const slug = slugFor(route, agentId);
12985
- try {
12986
- if ((await store.get(host, slug))?.apiKey) {
12987
- out.push({ role, action: "already_stored" });
12988
- continue;
12989
- }
12990
- if (o.dryRun) {
12991
- out.push({ role, action: "minted", detail: "dry run" });
12992
- continue;
12993
- }
12994
- const res = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys`, {
12995
- method: "POST",
12996
- headers: {
12997
- Authorization: `Bearer ${resolveApiKey(route)}`,
12998
- "Content-Type": "application/json",
12999
- "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0"
13000
- },
13001
- body: JSON.stringify({ name: `crew ${o.shipName ?? "ship"} ${role}` })
13002
- });
13003
- if (res.status === 403) {
13004
- out.push({ role, action: "forbidden" });
13005
- break;
13006
- }
13007
- if (!res.ok) {
13008
- out.push({ role, action: "failed", detail: `${res.status} ${res.statusText}` });
13009
- continue;
13010
- }
13011
- const body = await res.json();
13012
- if (!body.key) {
13013
- out.push({ role, action: "failed", detail: "response carried no key" });
13014
- continue;
13015
- }
13016
- try {
13017
- const session = {
13018
- apiKey: body.key,
13019
- shipId: o.shipName ?? "",
13020
- workspaceId: resolved.workspaceId,
13021
- identityId: agentId,
13022
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
13023
- ...body.id ? { keyId: body.id } : {}
13024
- };
13025
- await store.set(host, slug, session);
13026
- } catch (e) {
13027
- let revoked = false;
13028
- if (body.id) {
13029
- try {
13030
- const del2 = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys/${body.id}`, {
13031
- method: "DELETE",
13032
- headers: { Authorization: `Bearer ${resolveApiKey(route)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
13033
- });
13034
- revoked = del2.ok;
13035
- } catch {
13036
- revoked = false;
13037
- }
13038
- }
13039
- out.push({
13040
- role,
13041
- action: "failed",
13042
- detail: revoked ? `minted but could not be stored (${describeStoreError(e)}); revoked` : `a key for ${role} is live on the server and not stored (${describeStoreError(e)}) \u2014 revoke it from that seat's Agents row`
13043
- });
13044
- continue;
13045
- }
13046
- out.push({ role, action: "minted" });
13047
- } catch (e) {
13048
- out.push({ role, action: "failed", detail: /Command failed|security|sk_|-w /i.test(e.message) ? describeStoreError(e) : e.message });
13049
- }
13050
- }
13051
- return out;
13052
- }
13053
- function describeMintOutcome(o) {
13054
- switch (o.action) {
13055
- case "minted":
13056
- return `${o.role}: agent key minted${o.detail ? ` (${o.detail})` : ""}`;
13057
- case "already_stored":
13058
- return `${o.role}: agent key already stored`;
13059
- case "no_agent":
13060
- return `${o.role}: no linked Agents row \u2014 writes as the ship until \`crew agents sync\` links one`;
13061
- case "forbidden":
13062
- return FORBIDDEN_LINE;
13063
- case "failed":
13064
- return `${o.role}: could not mint an agent key (${o.detail ?? "unknown"}) \u2014 writes as the ship`;
13065
- }
13066
- }
13067
- async function revokeSeatKeys(route, o) {
13068
- const out = [];
13069
- if (!route.resolved) return out;
13070
- const resolved = route.resolved;
13071
- const doFetch = o.fetchImpl ?? fetch;
13072
- const targets = [
13073
- ...SEAT_KEY_ROLES.filter((r) => resolved.seats[r]).map((role) => ({ role, agentId: seatAgentId(route, role) })),
13074
- ...resolved.releaseSeat ? [{ role: "release", agentId: resolved.releaseAgentId }] : []
13075
- ];
13076
- let store;
13077
- try {
13078
- store = (o.getStore ?? getSessionStore2)();
13079
- } catch (e) {
13080
- return targets.map(({ role }) => ({ role, action: "failed", detail: describeStoreError(e) }));
13081
- }
13082
- const host = hostFromUrl3(route.baseUrl);
13083
- const base = route.baseUrl.replace(/\/+$/, "");
13084
- for (const { role, agentId } of targets) {
13085
- if (!agentId) {
13086
- out.push({ role, action: "no_agent" });
13087
- continue;
13088
- }
13089
- const slug = slugFor(route, agentId);
13090
- try {
13091
- const session = await store.get(host, slug);
13092
- if (!session?.apiKey) {
13093
- out.push({ role, action: "not_stored" });
13094
- continue;
13095
- }
13096
- if (!session.keyId) {
13097
- out.push({ role, action: "failed", detail: "no key id was recorded when this key was minted \u2014 revoke it from the seat's Agents row" });
13098
- continue;
13099
- }
13100
- if (o.dryRun) {
13101
- out.push({ role, action: "revoked", detail: "dry run" });
13102
- continue;
13103
- }
13104
- const del2 = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys/${session.keyId}`, {
13105
- method: "DELETE",
13106
- headers: { Authorization: `Bearer ${resolveApiKey(route)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
13107
- });
13108
- if (!del2.ok && del2.status !== 404) {
13109
- out.push({ role, action: "failed", detail: `${del2.status} ${del2.statusText}` });
13110
- continue;
13111
- }
13112
- await store.clear(host, slug);
13113
- out.push({ role, action: "revoked", detail: del2.ok ? void 0 : "server already had no such key" });
13114
- } catch (e) {
13115
- out.push({ role, action: "failed", detail: /Command failed|security|sk_|-w /i.test(e.message) ? describeStoreError(e) : e.message });
13116
- }
13117
- }
13118
- return out;
13119
- }
13120
- function describeRevokeOutcome(o) {
13121
- switch (o.action) {
13122
- case "revoked":
13123
- return `${o.role}: agent key revoked and cleared from the keychain${o.detail ? ` (${o.detail})` : ""}`;
13124
- case "not_stored":
13125
- return `${o.role}: no agent key stored on this ship`;
13126
- case "no_agent":
13127
- return `${o.role}: no linked Agents row`;
13128
- case "failed":
13129
- return `${o.role}: could not revoke the agent key (${o.detail ?? "unknown"}) \u2014 left in the keychain`;
13130
- }
13131
- }
13132
- async function canMintAgentKeys(route, o = {}) {
13133
- const resolved = route.resolved;
13134
- if (!resolved) return void 0;
13135
- const agentId = seatAgentId(route, "dev") ?? SEAT_KEY_ROLES.map((r) => seatAgentId(route, r)).find(Boolean);
13136
- if (!agentId) return void 0;
13137
- try {
13138
- const res = await (o.fetchImpl ?? fetch)(`${route.baseUrl.replace(/\/+$/, "")}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys`, {
13139
- headers: { Authorization: `Bearer ${resolveApiKey(route)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
13140
- });
13141
- if (res.ok) return true;
13142
- return res.status === 403 ? false : void 0;
13143
- } catch {
13144
- return void 0;
13145
- }
13146
- }
13147
-
13148
13173
  // src/seat-read-probe.ts
13149
13174
  var SEAT_NO_READ_REMEDY = "the project has no default role for agent keys \u2014 update the Issues app's template, then re-mint with `crew agents sync --revoke-seat-keys`, or assign the Crew seat role to the agent members";
13150
13175
  function probeTables(route) {
@@ -21061,7 +21086,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21061
21086
  baseUrl,
21062
21087
  workspaceId: route.resolved.workspaceId,
21063
21088
  apiKey,
21064
- keyProblem: keychainDown ? `${seatKey.kind === "ship" ? seatKey.reason : ""} \u2014 ${REMEDY["keychain-locked"]}` : void 0,
21089
+ // Only the seat's reason: runSelftest appends the remedy itself, as for every other class.
21090
+ keyProblem: keychainDown && seatKey.kind === "ship" ? seatKey.reason : void 0,
21065
21091
  workspaceName
21066
21092
  }));
21067
21093
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.79.2",
3
+ "version": "0.79.4",
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",