@tablation/crew 0.45.3 → 0.46.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 CHANGED
@@ -4822,7 +4822,12 @@ var Tracker = class {
4822
4822
  authorship;
4823
4823
  attachments;
4824
4824
  stateDir;
4825
- constructor(route, ship) {
4825
+ /**
4826
+ * `opts.apiKey` overrides the route's ship key (CREW-1603): a lane's own
4827
+ * writes pass its seat's agent key so the event log and rule context see an
4828
+ * agent, not the ship's human principal.
4829
+ */
4830
+ constructor(route, ship, opts = {}) {
4826
4831
  this.route = route;
4827
4832
  if (!route.resolved) {
4828
4833
  throw new ConfigError(
@@ -4836,7 +4841,7 @@ var Tracker = class {
4836
4841
  this.stateDir = ship.stateDir;
4837
4842
  this.client = new TablationClient({
4838
4843
  baseUrl: `${route.baseUrl}/api`,
4839
- apiKey: resolveApiKey(route),
4844
+ apiKey: opts.apiKey ?? resolveApiKey(route),
4840
4845
  // Cloudflare 403s (error 1010) default agents on this host.
4841
4846
  headers: { "User-Agent": ship.userAgent }
4842
4847
  });
@@ -10799,13 +10804,118 @@ ${opts.hostPassengers ? " hostPassengers: true\n" : ""}${apiKeyFileLine}${api
10799
10804
 
10800
10805
  // src/cli.ts
10801
10806
  import {
10802
- getSessionStore as getSessionStore2,
10803
- hostFromUrl as hostFromUrl2,
10807
+ getSessionStore as getSessionStore3,
10808
+ hostFromUrl as hostFromUrl3,
10804
10809
  loginWithDeviceCode,
10805
10810
  getOrCreateShipId,
10806
10811
  DeviceLoginError
10807
10812
  } from "@tablation/client";
10808
10813
 
10814
+ // src/seat-keys.ts
10815
+ import { getSessionStore as getSessionStore2, hostFromUrl as hostFromUrl2 } from "@tablation/client";
10816
+ var SEAT_KEY_ROLES = ROLE_NAMES.filter((r) => r !== "pair");
10817
+ function seatKeySlug(workspaceSlug, agentId) {
10818
+ return `${workspaceSlug}#agent:${agentId}`;
10819
+ }
10820
+ function slugFor(route, agentId) {
10821
+ const [workspaceSlug] = splitRoute(route.route);
10822
+ return seatKeySlug(workspaceSlug, agentId);
10823
+ }
10824
+ function seatAgentId(route, role) {
10825
+ return route.resolved?.agentPersonas?.[role]?.agentId;
10826
+ }
10827
+ async function resolveSeatKey(route, role, getStore = getSessionStore2) {
10828
+ const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route), reason });
10829
+ if (!SEAT_KEY_ROLES.includes(role)) return ship(`${role} always writes as the ship`);
10830
+ const agentId = seatAgentId(route, role);
10831
+ if (!agentId) return ship("seat has no linked Agents row (run `crew agents sync`)");
10832
+ try {
10833
+ const session = await getStore().get(hostFromUrl2(route.baseUrl), slugFor(route, agentId));
10834
+ if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
10835
+ return ship("no agent key stored for this seat (run `crew agents sync`)");
10836
+ } catch {
10837
+ return ship("OS keychain unavailable");
10838
+ }
10839
+ }
10840
+ async function mintMissingSeatKeys(route, o) {
10841
+ const out = [];
10842
+ if (!route.resolved) return out;
10843
+ const resolved = route.resolved;
10844
+ const doFetch = o.fetchImpl ?? fetch;
10845
+ const seated = SEAT_KEY_ROLES.filter((r) => resolved.seats[r]);
10846
+ let store;
10847
+ try {
10848
+ store = (o.getStore ?? getSessionStore2)();
10849
+ } catch (e) {
10850
+ return seated.map((role) => ({
10851
+ role,
10852
+ action: "failed",
10853
+ detail: `OS keychain unavailable: ${e.message}`
10854
+ }));
10855
+ }
10856
+ const host = hostFromUrl2(route.baseUrl);
10857
+ const base = route.baseUrl.replace(/\/+$/, "");
10858
+ for (const role of seated) {
10859
+ const agentId = seatAgentId(route, role);
10860
+ if (!agentId) {
10861
+ out.push({ role, action: "no_agent" });
10862
+ continue;
10863
+ }
10864
+ const slug = slugFor(route, agentId);
10865
+ try {
10866
+ if ((await store.get(host, slug))?.apiKey) {
10867
+ out.push({ role, action: "already_stored" });
10868
+ continue;
10869
+ }
10870
+ if (o.dryRun) {
10871
+ out.push({ role, action: "minted", detail: "dry run" });
10872
+ continue;
10873
+ }
10874
+ const res = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys`, {
10875
+ method: "POST",
10876
+ headers: {
10877
+ Authorization: `Bearer ${resolveApiKey(route)}`,
10878
+ "Content-Type": "application/json",
10879
+ "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0"
10880
+ },
10881
+ body: JSON.stringify({ name: `crew ${o.shipName ?? "ship"} ${role}` })
10882
+ });
10883
+ if (!res.ok) {
10884
+ out.push({ role, action: "failed", detail: `${res.status} ${res.statusText}` });
10885
+ continue;
10886
+ }
10887
+ const body = await res.json();
10888
+ if (!body.key) {
10889
+ out.push({ role, action: "failed", detail: "response carried no key" });
10890
+ continue;
10891
+ }
10892
+ await store.set(host, slug, {
10893
+ apiKey: body.key,
10894
+ shipId: o.shipName ?? "",
10895
+ workspaceId: resolved.workspaceId,
10896
+ identityId: agentId,
10897
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
10898
+ });
10899
+ out.push({ role, action: "minted" });
10900
+ } catch (e) {
10901
+ out.push({ role, action: "failed", detail: e.message });
10902
+ }
10903
+ }
10904
+ return out;
10905
+ }
10906
+ function describeMintOutcome(o) {
10907
+ switch (o.action) {
10908
+ case "minted":
10909
+ return `${o.role}: agent key minted${o.detail ? ` (${o.detail})` : ""}`;
10910
+ case "already_stored":
10911
+ return `${o.role}: agent key already stored`;
10912
+ case "no_agent":
10913
+ return `${o.role}: no linked Agents row \u2014 writes as the ship until \`crew agents sync\` links one`;
10914
+ case "failed":
10915
+ return `${o.role}: could not mint an agent key (${o.detail ?? "unknown"}) \u2014 writes as the ship`;
10916
+ }
10917
+ }
10918
+
10809
10919
  // src/logbook.ts
10810
10920
  import { createHash as createHash3 } from "crypto";
10811
10921
  import { TablationClient as TablationClient4 } from "@tablation/client";
@@ -12159,7 +12269,7 @@ async function runOnePass(o) {
12159
12269
  const decide = o.decide ?? decideCycle;
12160
12270
  const spawn5 = o.spawnRoleAgent ?? runRoleAgent;
12161
12271
  const breaker = o.spawnBreaker ?? new SpawnBreaker();
12162
- const ctx = { route: o.route, ship: o.ship, state: o.state, emit };
12272
+ const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
12163
12273
  const decision = await decide(ctx);
12164
12274
  o.state.attention(o.route.route).persist(decision.attention);
12165
12275
  if (decision.sweep.length) {
@@ -12259,7 +12369,13 @@ async function publishShipAttention(o) {
12259
12369
  }
12260
12370
  async function runRoleAgent(role, decision, ctx) {
12261
12371
  const { route, ship, emit } = ctx;
12262
- const tracker = new Tracker(route, ship);
12372
+ const identity = await resolveSeatKey(route, role, ctx.getSessionStore);
12373
+ if (identity.kind === "ship" && SEAT_KEY_ROLES.includes(role)) {
12374
+ emit.warn(`${role} writes as the ship, not its own agent: ${identity.reason}`, { step: "agent", role });
12375
+ } else if (identity.kind === "agent") {
12376
+ emit.emit(`${role} writes as its own agent principal`, { step: "agent", role });
12377
+ }
12378
+ const tracker = new Tracker(route, ship, { apiKey: identity.apiKey });
12263
12379
  let ticketHint = decision.actionable.top?.issue_id;
12264
12380
  let ticketRepo = repoTargetFor(route, decision.actionable.top?.repo_id)?.name;
12265
12381
  let workingId = decision.actionable.top?.id ?? null;
@@ -12336,7 +12452,7 @@ async function runRoleAgent(role, decision, ctx) {
12336
12452
  stateDir: ship.stateDir,
12337
12453
  roster: rosterFor(decision, route, role),
12338
12454
  environment: await buildEnvironment(route, ship, ticketHint),
12339
- apiKey: resolveApiKey(route),
12455
+ apiKey: identity.apiKey,
12340
12456
  cycle: emit.cycle,
12341
12457
  ticket: ticketHint,
12342
12458
  repo: ticketRepo,
@@ -14066,6 +14182,13 @@ ${changed2.map((c) => ` - ${c}
14066
14182
  raw.agentSkills = skillResult.agentSkills;
14067
14183
  mkdirSync13(dirOf(resolvedPath), { recursive: true });
14068
14184
  writeFileSync13(resolvedPath, `${JSON.stringify(raw, null, 2)}
14185
+ `);
14186
+ }
14187
+ if (!dryRun) {
14188
+ target.resolved.agentPersonas = personaResult.agentPersonas;
14189
+ }
14190
+ for (const o of await mintMissingSeatKeys(target, { userAgent: cfg.ship.userAgent, dryRun, shipName: cfg.ship.name })) {
14191
+ process.stdout.write(`${describeMintOutcome(o)}
14069
14192
  `);
14070
14193
  }
14071
14194
  if (personaResult.outcomes.some((o) => o.action === "diverged") || skillResult.outcomes.some((o) => o.action === "diverged")) {
@@ -14332,18 +14455,18 @@ ${reconstruction.prompt}
14332
14455
  let apiKey = value("key") ?? process.env.CREW_CONNECT_KEY ?? cfg.routes.find((c) => c.apiKey)?.apiKey;
14333
14456
  let store;
14334
14457
  try {
14335
- store = getSessionStore2();
14458
+ store = getSessionStore3();
14336
14459
  } catch {
14337
14460
  store = void 0;
14338
14461
  }
14339
14462
  let keychainBacked = false;
14340
14463
  if (!apiKey && workspace && store) {
14341
14464
  try {
14342
- const existing = await store.get(hostFromUrl2(baseUrl), workspace);
14465
+ const existing = await store.get(hostFromUrl3(baseUrl), workspace);
14343
14466
  if (existing?.apiKey) {
14344
14467
  apiKey = existing.apiKey;
14345
14468
  keychainBacked = true;
14346
- process.stderr.write(`Reusing the existing keychain session for ${hostFromUrl2(baseUrl)}/${workspace}.
14469
+ process.stderr.write(`Reusing the existing keychain session for ${hostFromUrl3(baseUrl)}/${workspace}.
14347
14470
  `);
14348
14471
  }
14349
14472
  } catch {
@@ -14397,7 +14520,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
14397
14520
  if (!workspace) workspace = result.workspace.slug;
14398
14521
  if (store) {
14399
14522
  try {
14400
- await store.set(hostFromUrl2(baseUrl), result.workspace.slug, {
14523
+ await store.set(hostFromUrl3(baseUrl), result.workspace.slug, {
14401
14524
  apiKey: result.apiKey.key,
14402
14525
  shipId: getOrCreateShipId(),
14403
14526
  workspaceId: result.workspace.id,
@@ -14406,7 +14529,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
14406
14529
  });
14407
14530
  keychainBacked = true;
14408
14531
  process.stderr.write(
14409
- `API key stored in the OS keychain for ${hostFromUrl2(baseUrl)}/${result.workspace.slug}.
14532
+ `API key stored in the OS keychain for ${hostFromUrl3(baseUrl)}/${result.workspace.slug}.
14410
14533
  `
14411
14534
  );
14412
14535
  } catch (e) {
@@ -14543,6 +14666,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
14543
14666
  process.stderr.write(`Resolved ids written to ${resolvedPath}
14544
14667
  `);
14545
14668
  }
14669
+ if (!dryRun && previous?.agentPersonas && existingRoute?.resolved) {
14670
+ try {
14671
+ for (const o of await mintMissingSeatKeys({ ...existingRoute, resolved: { ...existingRoute.resolved, seats: found.seats } }, { userAgent: cfg.ship.userAgent, shipName: cfg.ship.name })) {
14672
+ process.stderr.write(`${describeMintOutcome(o)}
14673
+ `);
14674
+ }
14675
+ } catch (e) {
14676
+ process.stderr.write(`seat keys: skipped (${e.message})
14677
+ `);
14678
+ }
14679
+ }
14546
14680
  let apiKeyFile;
14547
14681
  const apiKeyVar = "CREW_API_KEY";
14548
14682
  if (mintedFromDevice && !keychainBacked) {
@@ -15417,6 +15551,14 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
15417
15551
  process.stdout.write(
15418
15552
  route.resolved?.releaseSeat ? "release seat: this ship has its own Release Crew row\n" : "release seat: none \u2014 release comments are credited to the dev seat; run `crew connect` once the Crew table has this ship's row\n"
15419
15553
  );
15554
+ for (const role of SEAT_KEY_ROLES) {
15555
+ if (!route.resolved?.seats[role]) continue;
15556
+ const id = await resolveSeatKey(route, role);
15557
+ process.stdout.write(
15558
+ `seat identity: ${role} writes as ${id.kind === "agent" ? `its own agent (${id.agentId})` : `the ship (${id.reason})`}
15559
+ `
15560
+ );
15561
+ }
15420
15562
  const [ships, crewRows] = await Promise.all([tracker.shipRows(), tracker.crewRows()]);
15421
15563
  if (ships.length === 0) {
15422
15564
  process.stdout.write("ship record: this workspace has no Ships table (fine)\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.45.3",
3
+ "version": "0.46.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",
@@ -45,6 +45,13 @@ request — a default client user agent is rejected before it reaches the API,
45
45
  and the failure looks like a network problem. Do not guess column names; the
46
46
  Environment section lists them for this workspace.
47
47
 
48
+ A write to a record can be refused by a rule on the table. The response is a
49
+ 422 whose `message` says what is missing and whose `fieldErrors` names the
50
+ field. Do not retry the same write. If the missing value is yours to supply
51
+ (a resolution note when closing, a duplicate target), supply it and write
52
+ once more; otherwise leave the ticket where it is and say so in your
53
+ hand-off comment.
54
+
48
55
  Your identity is the Crew row named for your seat in the `## Your crew`
49
56
  roster at the top of this prompt. Use its id as the assignee and as the
50
57
  comment author. Every agent and every person who touches this tracker has
@@ -46,6 +46,13 @@ request — a default client user agent is rejected before it reaches the API,
46
46
  and the failure looks like a network problem. Do not guess column names; the
47
47
  Environment section lists them for this workspace.
48
48
 
49
+ A write to a record can be refused by a rule on the table. The response is a
50
+ 422 whose `message` says what is missing and whose `fieldErrors` names the
51
+ field. Do not retry the same write. If the missing value is yours to supply
52
+ (a resolution note when closing, a duplicate target), supply it and write
53
+ once more; otherwise leave the ticket where it is and say so in your
54
+ hand-off comment.
55
+
49
56
  Your identity is the Crew row named for your seat in the `## Your crew`
50
57
  roster at the top of this prompt. Use its id as the assignee and as the
51
58
  comment author. Every agent and every person who touches this tracker has