@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.
- package/dist/cli.js +288 -262
- 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)
|
|
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
|
|
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) => `${
|
|
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
|
-
|
|
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