@tablation/crew 0.51.1 → 0.52.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 +104 -7
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -11608,7 +11608,8 @@ function keyIsAdmin(route) {
11608
11608
  return role === void 0 || role === "WORKSPACE_ADMIN" || role === "PLATFORM_ADMIN";
11609
11609
  }
11610
11610
  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";
11611
- var MEMBER_NO_KEY = "no agent key for this seat \u2014 minting needs a workspace admin key";
11611
+ var MEMBER_NO_KEY = 'no agent key for this seat \u2014 run `crew agents sync` (needs the "Mint agent keys" capability on your role)';
11612
+ 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)';
11612
11613
  function seatKeySlug(workspaceSlug, agentId) {
11613
11614
  return `${workspaceSlug}#agent:${agentId}`;
11614
11615
  }
@@ -11712,6 +11713,10 @@ async function mintMissingSeatKeys(route, o) {
11712
11713
  },
11713
11714
  body: JSON.stringify({ name: `crew ${o.shipName ?? "ship"} ${role}` })
11714
11715
  });
11716
+ if (res.status === 403) {
11717
+ out.push({ role, action: "forbidden" });
11718
+ break;
11719
+ }
11715
11720
  if (!res.ok) {
11716
11721
  out.push({ role, action: "failed", detail: `${res.status} ${res.statusText}` });
11717
11722
  continue;
@@ -11722,13 +11727,15 @@ async function mintMissingSeatKeys(route, o) {
11722
11727
  continue;
11723
11728
  }
11724
11729
  try {
11725
- await store.set(host, slug, {
11730
+ const session = {
11726
11731
  apiKey: body.key,
11727
11732
  shipId: o.shipName ?? "",
11728
11733
  workspaceId: resolved.workspaceId,
11729
11734
  identityId: agentId,
11730
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
11731
- });
11735
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
11736
+ ...body.id ? { keyId: body.id } : {}
11737
+ };
11738
+ await store.set(host, slug, session);
11732
11739
  } catch (e) {
11733
11740
  let revoked = false;
11734
11741
  if (body.id) {
@@ -11764,10 +11771,92 @@ function describeMintOutcome(o) {
11764
11771
  return `${o.role}: agent key already stored`;
11765
11772
  case "no_agent":
11766
11773
  return `${o.role}: no linked Agents row \u2014 writes as the ship until \`crew agents sync\` links one`;
11774
+ case "forbidden":
11775
+ return FORBIDDEN_LINE;
11767
11776
  case "failed":
11768
11777
  return `${o.role}: could not mint an agent key (${o.detail ?? "unknown"}) \u2014 writes as the ship`;
11769
11778
  }
11770
11779
  }
11780
+ async function revokeSeatKeys(route, o) {
11781
+ const out = [];
11782
+ if (!route.resolved) return out;
11783
+ const resolved = route.resolved;
11784
+ const doFetch = o.fetchImpl ?? fetch;
11785
+ const targets = [
11786
+ ...SEAT_KEY_ROLES.filter((r) => resolved.seats[r]).map((role) => ({ role, agentId: seatAgentId(route, role) })),
11787
+ ...resolved.releaseSeat ? [{ role: "release", agentId: resolved.releaseAgentId }] : []
11788
+ ];
11789
+ let store;
11790
+ try {
11791
+ store = (o.getStore ?? getSessionStore2)();
11792
+ } catch (e) {
11793
+ return targets.map(({ role }) => ({ role, action: "failed", detail: describeStoreError(e) }));
11794
+ }
11795
+ const host = hostFromUrl2(route.baseUrl);
11796
+ const base = route.baseUrl.replace(/\/+$/, "");
11797
+ for (const { role, agentId } of targets) {
11798
+ if (!agentId) {
11799
+ out.push({ role, action: "no_agent" });
11800
+ continue;
11801
+ }
11802
+ const slug = slugFor(route, agentId);
11803
+ try {
11804
+ const session = await store.get(host, slug);
11805
+ if (!session?.apiKey) {
11806
+ out.push({ role, action: "not_stored" });
11807
+ continue;
11808
+ }
11809
+ if (!session.keyId) {
11810
+ 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" });
11811
+ continue;
11812
+ }
11813
+ if (o.dryRun) {
11814
+ out.push({ role, action: "revoked", detail: "dry run" });
11815
+ continue;
11816
+ }
11817
+ const del2 = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys/${session.keyId}`, {
11818
+ method: "DELETE",
11819
+ headers: { Authorization: `Bearer ${resolveApiKey(route)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
11820
+ });
11821
+ if (!del2.ok && del2.status !== 404) {
11822
+ out.push({ role, action: "failed", detail: `${del2.status} ${del2.statusText}` });
11823
+ continue;
11824
+ }
11825
+ await store.clear(host, slug);
11826
+ out.push({ role, action: "revoked", detail: del2.ok ? void 0 : "server already had no such key" });
11827
+ } catch (e) {
11828
+ out.push({ role, action: "failed", detail: /Command failed|security|sk_|-w /i.test(e.message) ? describeStoreError(e) : e.message });
11829
+ }
11830
+ }
11831
+ return out;
11832
+ }
11833
+ function describeRevokeOutcome(o) {
11834
+ switch (o.action) {
11835
+ case "revoked":
11836
+ return `${o.role}: agent key revoked and cleared from the keychain${o.detail ? ` (${o.detail})` : ""}`;
11837
+ case "not_stored":
11838
+ return `${o.role}: no agent key stored on this ship`;
11839
+ case "no_agent":
11840
+ return `${o.role}: no linked Agents row`;
11841
+ case "failed":
11842
+ return `${o.role}: could not revoke the agent key (${o.detail ?? "unknown"}) \u2014 left in the keychain`;
11843
+ }
11844
+ }
11845
+ async function canMintAgentKeys(route, o = {}) {
11846
+ const resolved = route.resolved;
11847
+ if (!resolved) return void 0;
11848
+ const agentId = seatAgentId(route, "dev") ?? SEAT_KEY_ROLES.map((r) => seatAgentId(route, r)).find(Boolean);
11849
+ if (!agentId) return void 0;
11850
+ try {
11851
+ const res = await (o.fetchImpl ?? fetch)(`${route.baseUrl.replace(/\/+$/, "")}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys`, {
11852
+ headers: { Authorization: `Bearer ${resolveApiKey(route)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
11853
+ });
11854
+ if (res.ok) return true;
11855
+ return res.status === 403 ? false : void 0;
11856
+ } catch {
11857
+ return void 0;
11858
+ }
11859
+ }
11771
11860
 
11772
11861
  // src/logbook.ts
11773
11862
  import { createHash as createHash3 } from "crypto";
@@ -13666,7 +13755,7 @@ function usage() {
13666
13755
  crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
13667
13756
  crew connect resolve a workspace's ids into a crew.yaml block (--no-mcp skips registering the Tablation MCP server with Claude Code; --mcp replace overwrites a different one)
13668
13757
  crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
13669
- crew agents sync [route] [--force] push crew's personas AND skill files into the workspace Agents/Agent Skills tables (--force overwrites even a diverged persona or skill; never one marked Customize prompt)
13758
+ crew agents sync [route] [--force] [--revoke-seat-keys] push crew's personas AND skill files into the workspace Agents/Agent Skills tables (--force overwrites even a diverged persona or skill; never one marked Customize prompt)
13670
13759
  crew agents prompt R [route] print one persona's current prompt (e.g. R=pair, for a SessionStart hook; --harness H adds which CLI it serves)
13671
13760
  crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
13672
13761
  crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
@@ -15187,7 +15276,7 @@ ${changed2.map((c) => ` - ${c}
15187
15276
  break;
15188
15277
  }
15189
15278
  if (sub !== "sync") {
15190
- process.stderr.write("crew agents sync [route] [--force] push crew's personas (Developer/Design/QA/Triage/Pair) AND skill files (e.g. grill-me) into the workspace Agents/Agent Skills tables (--force overwrites even a diverged persona or skill; never one marked Customize prompt)\n");
15279
+ process.stderr.write("crew agents sync [route] [--force] push crew's personas (Developer/Design/QA/Triage/Pair) AND skill files (e.g. grill-me) into the workspace Agents/Agent Skills tables (--force overwrites even a diverged persona or skill; never one marked Customize prompt; --revoke-seat-keys deletes this ship's seat keys server-side and from the keychain, so the next sync mints fresh ones)\n");
15191
15280
  process.stderr.write(`crew agents prompt <role> [route] print a persona's current prompt (roles: ${ROLE_NAMES.join(", ")})
15192
15281
  `);
15193
15282
  process.exit(2);
@@ -15270,7 +15359,12 @@ ${changed2.map((c) => ` - ${c}
15270
15359
  }
15271
15360
  if (readOnly) {
15272
15361
  process.stdout.write("personas and skills: not synced (this key is not a workspace admin; the workspace's rows are used as they are)\n");
15273
- process.stdout.write("seat keys: not minted (needs a workspace admin key)\n");
15362
+ }
15363
+ if (flag("revoke-seat-keys")) {
15364
+ for (const o of await revokeSeatKeys(target, { userAgent: cfg.ship.userAgent, dryRun })) {
15365
+ process.stdout.write(`${describeRevokeOutcome(o)}
15366
+ `);
15367
+ }
15274
15368
  } else {
15275
15369
  for (const o of await mintMissingSeatKeys(target, { userAgent: cfg.ship.userAgent, dryRun, shipName: cfg.ship.name })) {
15276
15370
  process.stdout.write(`${describeMintOutcome(o)}
@@ -16675,6 +16769,9 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
16675
16769
  const keyRole = route.resolved?.role ?? await fetchKeyRole(route, cfg.ship.userAgent);
16676
16770
  const roleRoute = route.resolved && keyRole ? { ...route, resolved: { ...route.resolved, role: keyRole } } : route;
16677
16771
  process.stdout.write(`api key role: ${keyRole ?? "unknown"}
16772
+ `);
16773
+ const canMint = await canMintAgentKeys(roleRoute, { userAgent: cfg.ship.userAgent });
16774
+ process.stdout.write(`can mint agent keys: ${canMint === void 0 ? "unknown" : canMint ? "yes" : "no"}
16678
16775
  `);
16679
16776
  for (const role of SEAT_KEY_ROLES) {
16680
16777
  if (!route.resolved?.seats[role]) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.51.1",
3
+ "version": "0.52.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",