@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.
- package/dist/cli.js +104 -7
- 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 =
|
|
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
|
-
|
|
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
|
-
|
|
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