@tablation/crew 0.79.1 → 0.79.3

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 +64 -18
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -13147,10 +13147,19 @@ async function canMintAgentKeys(route, o = {}) {
13147
13147
 
13148
13148
  // src/seat-read-probe.ts
13149
13149
  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
- async function probeIssuesRead(route, apiKey, o = {}) {
13151
- const resolved = route.resolved;
13152
- if (!resolved?.models?.issues) return { state: "unknown", detail: "route has no resolved Issues table" };
13153
- const url = `${route.baseUrl.replace(/\/+$/, "")}/api/workspaces/${resolved.workspaceId}/data-models/${resolved.models.issues}/records?limit=1`;
13150
+ function probeTables(route) {
13151
+ const m = route.resolved?.models;
13152
+ if (!m) return [];
13153
+ const r = route.resolved;
13154
+ return [
13155
+ { name: "Issues", modelId: m.issues },
13156
+ { name: "Comments", modelId: m.comments },
13157
+ { name: "Crew", modelId: m.crew },
13158
+ { name: "Ships", modelId: r?.shipsModelId ?? "" }
13159
+ ].filter((t) => !!t.modelId);
13160
+ }
13161
+ async function probeTableRead(route, modelId, apiKey, o = {}) {
13162
+ const url = `${route.baseUrl.replace(/\/+$/, "")}/api/data-models/${modelId}/records?limit=1`;
13154
13163
  try {
13155
13164
  const res = await (o.fetchImpl ?? fetch)(url, {
13156
13165
  headers: { Authorization: `Bearer ${apiKey}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
@@ -13168,12 +13177,46 @@ async function probeSeatReads(route, o = {}) {
13168
13177
  for (const role of roles) {
13169
13178
  const id = role === "release" ? await resolveReleaseKey(route, o.getStore) : await resolveSeatKey(route, role, o.getStore);
13170
13179
  if (id.kind !== "agent") continue;
13171
- out.push({ role, ...await probeIssuesRead(route, id.apiKey, o) });
13180
+ const tables = [];
13181
+ for (const t of probeTables(route)) tables.push({ table: t.name, ...await probeTableRead(route, t.modelId, id.apiKey, o) });
13182
+ const bad = tables.find((t) => t.state === "forbidden") ?? tables.find((t) => t.state === "unknown");
13183
+ out.push({ role, state: bad?.state ?? (tables.length ? "ok" : "unknown"), detail: bad?.detail ?? (tables.length ? void 0 : "route has no resolved tables"), tables });
13172
13184
  }
13173
13185
  return out;
13174
13186
  }
13175
- function describeSeatForbidden(role) {
13176
- return `seat read: ERROR ${role} agent key cannot read Issues (403) \u2014 ${SEAT_NO_READ_REMEDY}`;
13187
+ function forbiddenDetail(e, route) {
13188
+ const msg = e instanceof Error ? e.message : String(e);
13189
+ const m = /^([A-Z]+) (\S+) -> 403/.exec(msg);
13190
+ const verb = /No (READ|WRITE) access/i.exec(msg)?.[1]?.toUpperCase();
13191
+ const modelId = /\/data-models\/([\w-]+)/i.exec(m?.[2] ?? "")?.[1];
13192
+ const r = route?.resolved;
13193
+ const known = [
13194
+ [r?.models?.issues, "Issues"],
13195
+ [r?.models?.comments, "Comments"],
13196
+ [r?.models?.crew, "Crew"],
13197
+ [r?.shipsModelId, "Ships"],
13198
+ [r?.locksModelId, "Locks"],
13199
+ [r?.epicsModelId, "Epics"],
13200
+ [r?.reposModelId, "Repos"]
13201
+ ];
13202
+ const table2 = modelId ? known.find(([id]) => id && id === modelId)?.[1] ?? `table ${modelId}` : void 0;
13203
+ return { method: m?.[1], path: m?.[2], table: table2, verb };
13204
+ }
13205
+ function remedyFor(d, roleName = "the agent role") {
13206
+ if (d.table && d.verb && !(d.table === "Issues" && d.verb === "READ")) {
13207
+ return `grant ${d.verb} on ${d.table} to ${roleName}`;
13208
+ }
13209
+ return SEAT_NO_READ_REMEDY;
13210
+ }
13211
+ function describeSeatForbidden(role, tables = []) {
13212
+ const bad = tables.filter((t) => t.state === "forbidden").map((t) => t.table);
13213
+ const what = bad.length ? bad.join(", ") : "Issues";
13214
+ const remedy = bad.length && !(bad.length === 1 && bad[0] === "Issues") ? bad.map((t) => remedyFor({ table: t, verb: "READ" })).join("; ") : SEAT_NO_READ_REMEDY;
13215
+ return `seat read: ERROR ${role} agent key cannot read ${what} (403) \u2014 ${remedy}`;
13216
+ }
13217
+ function describeSeatTables(role, tables) {
13218
+ const cells = tables.map((t) => `${t.table} ${t.state === "ok" ? "ok" : t.state === "forbidden" ? "403" : `error (${t.detail ?? "?"})`}`);
13219
+ return `seat read: ${role}: ${cells.join(", ")}`;
13177
13220
  }
13178
13221
  function isForbiddenError(e) {
13179
13222
  const status2 = e?.status;
@@ -13181,16 +13224,18 @@ function isForbiddenError(e) {
13181
13224
  return /\b403\b/.test(e instanceof Error ? e.message : String(e));
13182
13225
  }
13183
13226
  var SEAT_FORBIDDEN_KEY = "seat_forbidden:release";
13184
- function seatForbiddenItem(now = /* @__PURE__ */ new Date()) {
13227
+ function seatForbiddenItem(now = /* @__PURE__ */ new Date(), e, route) {
13228
+ const d = e === void 0 ? {} : forbiddenDetail(e, route);
13229
+ const what = d.method && d.table ? `${d.method} ${d.table}${d.verb ? ` -> 403 No ${d.verb} access` : " -> 403"}` : "cannot read the Issues table (403)";
13185
13230
  return {
13186
13231
  kind: "seat_forbidden",
13187
13232
  key: SEAT_FORBIDDEN_KEY,
13188
- message: `release seat cannot read the Issues table (403) \u2014 ${SEAT_NO_READ_REMEDY}`,
13233
+ message: `release seat: ${what} \u2014 ${remedyFor(d)}`,
13189
13234
  since: now.toISOString()
13190
13235
  };
13191
13236
  }
13192
13237
  function seatForbiddenRepoLine(scope) {
13193
- return `${scope}: skipped \u2014 release seat cannot read Issues (see attention)`;
13238
+ return `${scope}: skipped \u2014 release seat was refused by the tracker (see attention)`;
13194
13239
  }
13195
13240
 
13196
13241
  // src/cli.ts
@@ -18263,7 +18308,7 @@ ${changed2.map((c) => ` - ${c}
18263
18308
  return summary;
18264
18309
  } catch (e) {
18265
18310
  if (isForbiddenError(e)) {
18266
- await raiseShipAttention({ state, emit: remit, route: c, ship: cfg.ship }, seatForbiddenItem());
18311
+ await raiseShipAttention({ state, emit: remit, route: c, ship: cfg.ship }, seatForbiddenItem(/* @__PURE__ */ new Date(), e, c));
18267
18312
  const line = seatForbiddenRepoLine(scope);
18268
18313
  remit.warn(line, { step: "release" });
18269
18314
  return { scope, tests: "skipped", outcome: "error", detail: line };
@@ -19024,7 +19069,7 @@ ${changed2.map((c) => ` - ${c}
19024
19069
  if (!dryRun) {
19025
19070
  for (const p of await probeSeatReads(target, { userAgent: cfg.ship.userAgent })) {
19026
19071
  if (p.state === "forbidden") {
19027
- process.stdout.write(`${describeSeatForbidden(p.role)}
19072
+ process.stdout.write(`${describeSeatForbidden(p.role, p.tables)}
19028
19073
  `);
19029
19074
  process.exitCode = 1;
19030
19075
  }
@@ -20007,7 +20052,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20007
20052
  for (const o of mintOutcomes) process.stderr.write(`${describeMintOutcome(o)}
20008
20053
  `);
20009
20054
  for (const p of await probeSeatReads({ ...existingRoute, resolved: { ...existingRoute.resolved, seats: found.seats } }, { userAgent: cfg.ship.userAgent })) {
20010
- if (p.state === "forbidden") process.stderr.write(`${describeSeatForbidden(p.role)}
20055
+ if (p.state === "forbidden") process.stderr.write(`${describeSeatForbidden(p.role, p.tables)}
20011
20056
  `);
20012
20057
  }
20013
20058
  } catch (e) {
@@ -20186,7 +20231,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20186
20231
  for (const m of minted) process.stderr.write(` ${describeMintOutcome(m)}
20187
20232
  `);
20188
20233
  for (const p of await probeSeatReads(syncRoute, { userAgent: cfg.ship.userAgent })) {
20189
- if (p.state === "forbidden") process.stderr.write(` ${describeSeatForbidden(p.role)}
20234
+ if (p.state === "forbidden") process.stderr.write(` ${describeSeatForbidden(p.role, p.tables)}
20190
20235
  `);
20191
20236
  }
20192
20237
  if (run2.readOnly) {
@@ -21016,7 +21061,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21016
21061
  baseUrl,
21017
21062
  workspaceId: route.resolved.workspaceId,
21018
21063
  apiKey,
21019
- keyProblem: keychainDown ? `${seatKey.kind === "ship" ? seatKey.reason : ""} \u2014 ${REMEDY["keychain-locked"]}` : void 0,
21064
+ // Only the seat's reason: runSelftest appends the remedy itself, as for every other class.
21065
+ keyProblem: keychainDown && seatKey.kind === "ship" ? seatKey.reason : void 0,
21020
21066
  workspaceName
21021
21067
  }));
21022
21068
  }
@@ -21313,7 +21359,7 @@ watermark: ${state.watermark()}
21313
21359
  if (!flag("offline")) {
21314
21360
  for (const p of await probeSeatReads(route, { userAgent: cfg.ship.userAgent })) {
21315
21361
  if (p.state === "forbidden") {
21316
- problems.push({ severity: "blocker", kind: "seat_cannot_read_issues", route: route.route, message: `${p.role} agent key cannot read Issues (403)`, remedy: SEAT_NO_READ_REMEDY });
21362
+ problems.push({ severity: "blocker", kind: "seat_cannot_read_issues", route: route.route, message: `${p.role} agent key refused on ${p.tables.filter((t) => t.state === "forbidden").map((t) => t.table).join(", ")} (403)`, remedy: describeSeatForbidden(p.role, p.tables).replace(/^.*?— /, "") });
21317
21363
  } else if (p.state === "ok") passed++;
21318
21364
  }
21319
21365
  }
@@ -21459,11 +21505,11 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
21459
21505
  if (!flag("offline")) {
21460
21506
  for (const p of await probeSeatReads(roleRoute, { userAgent: cfg.ship.userAgent })) {
21461
21507
  if (p.state === "forbidden") {
21462
- process.stdout.write(`${describeSeatForbidden(p.role)}
21508
+ process.stdout.write(`${describeSeatForbidden(p.role, p.tables)}
21463
21509
  `);
21464
21510
  process.exitCode = 1;
21465
21511
  } else {
21466
- process.stdout.write(p.state === "ok" ? `seat read: ${p.role} can read Issues
21512
+ process.stdout.write(p.state === "ok" ? `${describeSeatTables(p.role, p.tables)}
21467
21513
  ` : `seat read: ${p.role} could not be checked (${p.detail})
21468
21514
  `);
21469
21515
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.79.1",
3
+ "version": "0.79.3",
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",