@tablation/crew 0.45.4 → 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 +154 -12
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4822,7 +4822,12 @@ var Tracker = class {
|
|
|
4822
4822
|
authorship;
|
|
4823
4823
|
attachments;
|
|
4824
4824
|
stateDir;
|
|
4825
|
-
|
|
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
|
|
10803
|
-
hostFromUrl as
|
|
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
|
|
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:
|
|
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 =
|
|
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(
|
|
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 ${
|
|
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(
|
|
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 ${
|
|
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