@tablation/crew 0.51.0 → 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 +154 -13
- 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
|
}
|
|
@@ -11644,6 +11645,13 @@ async function resolveSeatKey(route, role, getStore = getSessionStore2) {
|
|
|
11644
11645
|
return ship("OS keychain unavailable");
|
|
11645
11646
|
}
|
|
11646
11647
|
}
|
|
11648
|
+
function describeStoreError(e) {
|
|
11649
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
11650
|
+
if (/User interaction is not allowed/i.test(msg)) {
|
|
11651
|
+
return "keychain unavailable (User interaction is not allowed \u2014 run from a logged-in session, or `security unlock-keychain` first)";
|
|
11652
|
+
}
|
|
11653
|
+
return "keychain unavailable (could not write to the OS keychain)";
|
|
11654
|
+
}
|
|
11647
11655
|
async function mintMissingSeatKeys(route, o) {
|
|
11648
11656
|
const out = [];
|
|
11649
11657
|
if (!route.resolved) return out;
|
|
@@ -11660,11 +11668,27 @@ async function mintMissingSeatKeys(route, o) {
|
|
|
11660
11668
|
return targets.map(({ role }) => ({
|
|
11661
11669
|
role,
|
|
11662
11670
|
action: "failed",
|
|
11663
|
-
detail:
|
|
11671
|
+
detail: describeStoreError(e)
|
|
11664
11672
|
}));
|
|
11665
11673
|
}
|
|
11666
11674
|
const host = hostFromUrl2(route.baseUrl);
|
|
11667
11675
|
const base = route.baseUrl.replace(/\/+$/, "");
|
|
11676
|
+
if (!o.dryRun) {
|
|
11677
|
+
const probeSlug = seatKeySlug(splitRoute(route.route)[0], "_probe");
|
|
11678
|
+
try {
|
|
11679
|
+
await store.set(host, probeSlug, {
|
|
11680
|
+
apiKey: "probe",
|
|
11681
|
+
shipId: "",
|
|
11682
|
+
workspaceId: resolved.workspaceId,
|
|
11683
|
+
identityId: "_probe",
|
|
11684
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11685
|
+
});
|
|
11686
|
+
await store.clear(host, probeSlug);
|
|
11687
|
+
} catch (e) {
|
|
11688
|
+
const detail = `${describeStoreError(e)}; no keys minted`;
|
|
11689
|
+
return targets.map(({ role, agentId }) => agentId ? { role, action: "failed", detail } : { role, action: "no_agent" });
|
|
11690
|
+
}
|
|
11691
|
+
}
|
|
11668
11692
|
for (const { role, agentId } of targets) {
|
|
11669
11693
|
if (!agentId) {
|
|
11670
11694
|
out.push({ role, action: "no_agent" });
|
|
@@ -11689,6 +11713,10 @@ async function mintMissingSeatKeys(route, o) {
|
|
|
11689
11713
|
},
|
|
11690
11714
|
body: JSON.stringify({ name: `crew ${o.shipName ?? "ship"} ${role}` })
|
|
11691
11715
|
});
|
|
11716
|
+
if (res.status === 403) {
|
|
11717
|
+
out.push({ role, action: "forbidden" });
|
|
11718
|
+
break;
|
|
11719
|
+
}
|
|
11692
11720
|
if (!res.ok) {
|
|
11693
11721
|
out.push({ role, action: "failed", detail: `${res.status} ${res.statusText}` });
|
|
11694
11722
|
continue;
|
|
@@ -11698,16 +11726,39 @@ async function mintMissingSeatKeys(route, o) {
|
|
|
11698
11726
|
out.push({ role, action: "failed", detail: "response carried no key" });
|
|
11699
11727
|
continue;
|
|
11700
11728
|
}
|
|
11701
|
-
|
|
11702
|
-
|
|
11703
|
-
|
|
11704
|
-
|
|
11705
|
-
|
|
11706
|
-
|
|
11707
|
-
|
|
11729
|
+
try {
|
|
11730
|
+
const session = {
|
|
11731
|
+
apiKey: body.key,
|
|
11732
|
+
shipId: o.shipName ?? "",
|
|
11733
|
+
workspaceId: resolved.workspaceId,
|
|
11734
|
+
identityId: agentId,
|
|
11735
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11736
|
+
...body.id ? { keyId: body.id } : {}
|
|
11737
|
+
};
|
|
11738
|
+
await store.set(host, slug, session);
|
|
11739
|
+
} catch (e) {
|
|
11740
|
+
let revoked = false;
|
|
11741
|
+
if (body.id) {
|
|
11742
|
+
try {
|
|
11743
|
+
const del2 = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys/${body.id}`, {
|
|
11744
|
+
method: "DELETE",
|
|
11745
|
+
headers: { Authorization: `Bearer ${resolveApiKey(route)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
11746
|
+
});
|
|
11747
|
+
revoked = del2.ok;
|
|
11748
|
+
} catch {
|
|
11749
|
+
revoked = false;
|
|
11750
|
+
}
|
|
11751
|
+
}
|
|
11752
|
+
out.push({
|
|
11753
|
+
role,
|
|
11754
|
+
action: "failed",
|
|
11755
|
+
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`
|
|
11756
|
+
});
|
|
11757
|
+
continue;
|
|
11758
|
+
}
|
|
11708
11759
|
out.push({ role, action: "minted" });
|
|
11709
11760
|
} catch (e) {
|
|
11710
|
-
out.push({ role, action: "failed", detail: e.message });
|
|
11761
|
+
out.push({ role, action: "failed", detail: /Command failed|security|sk_|-w /i.test(e.message) ? describeStoreError(e) : e.message });
|
|
11711
11762
|
}
|
|
11712
11763
|
}
|
|
11713
11764
|
return out;
|
|
@@ -11720,10 +11771,92 @@ function describeMintOutcome(o) {
|
|
|
11720
11771
|
return `${o.role}: agent key already stored`;
|
|
11721
11772
|
case "no_agent":
|
|
11722
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;
|
|
11723
11776
|
case "failed":
|
|
11724
11777
|
return `${o.role}: could not mint an agent key (${o.detail ?? "unknown"}) \u2014 writes as the ship`;
|
|
11725
11778
|
}
|
|
11726
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
|
+
}
|
|
11727
11860
|
|
|
11728
11861
|
// src/logbook.ts
|
|
11729
11862
|
import { createHash as createHash3 } from "crypto";
|
|
@@ -13622,7 +13755,7 @@ function usage() {
|
|
|
13622
13755
|
crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
|
|
13623
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)
|
|
13624
13757
|
crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
|
|
13625
|
-
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)
|
|
13626
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)
|
|
13627
13760
|
crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
|
|
13628
13761
|
crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
|
|
@@ -15143,7 +15276,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15143
15276
|
break;
|
|
15144
15277
|
}
|
|
15145
15278
|
if (sub !== "sync") {
|
|
15146
|
-
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");
|
|
15147
15280
|
process.stderr.write(`crew agents prompt <role> [route] print a persona's current prompt (roles: ${ROLE_NAMES.join(", ")})
|
|
15148
15281
|
`);
|
|
15149
15282
|
process.exit(2);
|
|
@@ -15226,7 +15359,12 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15226
15359
|
}
|
|
15227
15360
|
if (readOnly) {
|
|
15228
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");
|
|
15229
|
-
|
|
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
|
+
}
|
|
15230
15368
|
} else {
|
|
15231
15369
|
for (const o of await mintMissingSeatKeys(target, { userAgent: cfg.ship.userAgent, dryRun, shipName: cfg.ship.name })) {
|
|
15232
15370
|
process.stdout.write(`${describeMintOutcome(o)}
|
|
@@ -16631,6 +16769,9 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
16631
16769
|
const keyRole = route.resolved?.role ?? await fetchKeyRole(route, cfg.ship.userAgent);
|
|
16632
16770
|
const roleRoute = route.resolved && keyRole ? { ...route, resolved: { ...route.resolved, role: keyRole } } : route;
|
|
16633
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"}
|
|
16634
16775
|
`);
|
|
16635
16776
|
for (const role of SEAT_KEY_ROLES) {
|
|
16636
16777
|
if (!route.resolved?.seats[role]) continue;
|
package/package.json
CHANGED