@fungi.computer/truffle 0.3.1 → 0.5.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.
Files changed (41) hide show
  1. package/dist/local-board-agents.d.ts +46 -0
  2. package/dist/local-board-agents.js +138 -0
  3. package/dist/local-board-shipping.d.ts +129 -0
  4. package/dist/local-board-shipping.js +325 -0
  5. package/dist/local-extensions.d.ts +2 -0
  6. package/dist/local-extensions.js +121 -36
  7. package/dist/local-login.js +33 -19
  8. package/dist/local-upgrade.d.ts +63 -0
  9. package/dist/local-upgrade.js +358 -0
  10. package/dist/private-local-agent-control.d.ts +8 -1
  11. package/dist/private-local-agent-control.js +16 -2
  12. package/dist/private-local-auth-rpc.d.ts +5 -0
  13. package/dist/private-local-auth-rpc.js +46 -4
  14. package/dist/private-local-client-version.js +3 -1
  15. package/dist/private-local-delegation.d.ts +18 -0
  16. package/dist/private-local-delegation.js +82 -21
  17. package/dist/private-local-exec-review.js +45 -8
  18. package/dist/private-local-extension-archive-notice.d.ts +8 -0
  19. package/dist/private-local-extension-archive-notice.js +108 -0
  20. package/dist/private-local-extension-records.d.ts +25 -0
  21. package/dist/private-local-extension-records.js +51 -0
  22. package/dist/private-local-process-identity.d.ts +14 -0
  23. package/dist/private-local-process-identity.js +51 -0
  24. package/dist/private-local-runtime-host.d.ts +7 -1
  25. package/dist/private-local-runtime-host.js +88 -31
  26. package/dist/private-local-service-contract.d.ts +1 -0
  27. package/dist/private-local-service-discovery.js +2 -3
  28. package/dist/private-local-service-owner.js +80 -9
  29. package/dist/private-local-service-termination.js +12 -11
  30. package/dist/private-local-service.js +19 -1
  31. package/dist/private-local-session-discovery.d.ts +16 -4
  32. package/dist/private-local-session-discovery.js +68 -4
  33. package/dist/private-mcp-form-contract.d.ts +13 -0
  34. package/dist/private-mcp-form-contract.js +6 -1
  35. package/dist/private-mcp-form-session.d.ts +26 -0
  36. package/dist/private-mcp-form-session.js +8 -5
  37. package/dist/private-provider-rejection.d.ts +1 -1
  38. package/dist/private-provider-rejection.js +28 -8
  39. package/dist/private-sqlite-owner.d.ts +7 -1
  40. package/dist/private-sqlite-owner.js +14 -2
  41. package/package.json +32 -16
@@ -0,0 +1,358 @@
1
+ import { lstat, mkdir, readFile, rename, unlink } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import lockfile from "proper-lockfile";
5
+ import semver from "semver";
6
+ import writeFileAtomic from "write-file-atomic";
7
+ import { z } from "zod";
8
+ import { logEvent } from "./log-event.js";
9
+ import { incompatibleLocalExtensionActivations } from "./private-local-extension-records.js";
10
+ import { createPrivateLocalServiceManager, LocalServiceUpdated, } from "./private-local-service.js";
11
+ import { readInstalledLocalClientBuildId } from "./private-local-client-version.js";
12
+ import { LocalStoreSchemaError, probePrivateLocalAgentStoreEntries, readPrivateLocalAgentProfile, } from "./private-local-session-discovery.js";
13
+ import { assertNoUntrackedServiceOwner, privateLocalServiceVersion, readServiceRegistration, serviceOwnerIsLive, } from "./private-local-service-discovery.js";
14
+ const version = z
15
+ .string()
16
+ .refine((value) => semver.valid(value) !== null, "Invalid semver version");
17
+ const versionMarkerSchema = z.strictObject({
18
+ version: z.literal(1),
19
+ release: version,
20
+ spore: version,
21
+ build: z.union([z.literal("source"), z.string().regex(/^[a-f0-9]{64}$/u)]),
22
+ });
23
+ const newsSchema = z.strictObject({
24
+ release: versionMarkerSchema.shape.release,
25
+ lines: z.array(z.string().max(120)).max(3),
26
+ });
27
+ const markerPath = (home) => join(home, ".fungi", "version.json");
28
+ const pendingPath = (home) => join(home, ".fungi", ".upgrade-in-progress.json");
29
+ const pendingSchema = z.strictObject({
30
+ version: z.literal(1),
31
+ prior: z.discriminatedUnion("kind", [
32
+ z.strictObject({
33
+ kind: z.literal("recorded"),
34
+ release: version,
35
+ spore: version,
36
+ }),
37
+ z.strictObject({
38
+ kind: z.literal("protocol"),
39
+ protocol: z.number().int().nonnegative(),
40
+ build: z.string().min(1),
41
+ }),
42
+ z.strictObject({ kind: z.literal("unrecorded") }),
43
+ ]),
44
+ });
45
+ /** Persist prior identity before stop removes the only protocol-3 record. */
46
+ async function readPendingUpgrade(home) {
47
+ const path = pendingPath(home);
48
+ let text;
49
+ try {
50
+ text = await readFile(path, "utf8");
51
+ }
52
+ catch (cause) {
53
+ if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
54
+ return undefined;
55
+ throw new Error(`Cannot read the in-progress Fungi upgrade at ${path}`, {
56
+ cause,
57
+ });
58
+ }
59
+ try {
60
+ return pendingSchema.parse(JSON.parse(text)).prior;
61
+ }
62
+ catch (cause) {
63
+ throw new Error(`Cannot resume the Fungi upgrade at ${path}: inspect or move this marker aside before retrying`, { cause });
64
+ }
65
+ }
66
+ async function writePendingUpgrade(home, prior) {
67
+ await writeFileAtomic(pendingPath(home), `${JSON.stringify(pendingSchema.parse({ version: 1, prior }))}\n`, { mode: 0o600 });
68
+ }
69
+ /** A completed version marker supersedes a pending pass left after its write. */
70
+ function pendingIsComplete(recorded, pending) {
71
+ return (recorded.kind === "recorded" &&
72
+ (pending.kind !== "recorded" ||
73
+ recorded.release !== pending.release ||
74
+ recorded.spore !== pending.spore));
75
+ }
76
+ /** Version marker is a label, not permission to accept an older store schema. */
77
+ export async function readPriorInstall(home) {
78
+ const path = markerPath(home);
79
+ let text;
80
+ try {
81
+ text = await readFile(path, "utf8");
82
+ }
83
+ catch (cause) {
84
+ if (!(cause instanceof Error && "code" in cause && cause.code === "ENOENT"))
85
+ throw cause;
86
+ }
87
+ if (text !== undefined) {
88
+ try {
89
+ const marker = versionMarkerSchema.parse(JSON.parse(text));
90
+ return { kind: "recorded", release: marker.release, spore: marker.spore };
91
+ }
92
+ catch (cause) {
93
+ throw new Error(`Cannot read ${path}: unsupported Fungi version marker`, {
94
+ cause,
95
+ });
96
+ }
97
+ }
98
+ const registration = await readServiceRegistration(join(home, ".fungi", "service"));
99
+ if (registration?.kind === "other-protocol")
100
+ return {
101
+ kind: "protocol",
102
+ protocol: registration.lifecycle.protocol,
103
+ build: registration.lifecycle.version,
104
+ };
105
+ return { kind: "unrecorded" };
106
+ }
107
+ /** Only the completed upgrade pass calls this writer; never write it early. */
108
+ export async function writeLocalVersionMarker(home, marker) {
109
+ const path = markerPath(home);
110
+ const record = versionMarkerSchema.parse({ version: 1, ...marker });
111
+ await mkdir(join(home, ".fungi"), { recursive: true, mode: 0o700 });
112
+ await writeFileAtomic(path, `${JSON.stringify(record)}\n`, { mode: 0o600 });
113
+ }
114
+ /** Invalid news lines are optional; a missing release label is not safe to mark current. */
115
+ export async function readLocalUpgradeNews(path) {
116
+ const value = JSON.parse(await readFile(path, "utf8"));
117
+ const release = z
118
+ .looseObject({ release: versionMarkerSchema.shape.release })
119
+ .parse(value).release;
120
+ return {
121
+ release,
122
+ lines: newsSchema.safeParse(value).data?.lines ?? [],
123
+ };
124
+ }
125
+ async function exists(path) {
126
+ try {
127
+ await lstat(path);
128
+ return true;
129
+ }
130
+ catch (cause) {
131
+ if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
132
+ return false;
133
+ throw cause;
134
+ }
135
+ }
136
+ function archiveSlug(prior) {
137
+ if (prior.kind === "recorded")
138
+ return prior.release;
139
+ if (prior.kind === "unrecorded")
140
+ return "unversioned";
141
+ const build = prior.build.slice(0, 12);
142
+ return `protocol-${String(prior.protocol)}-${/^[a-f0-9]{12}$/u.test(build) ? build : "source"}`;
143
+ }
144
+ async function surveyIncompatibleLocalHome(home) {
145
+ const incompatible = [];
146
+ for (const store of await probePrivateLocalAgentStoreEntries(home)) {
147
+ try {
148
+ readPrivateLocalAgentProfile(store.databasePath);
149
+ }
150
+ catch (cause) {
151
+ if (cause instanceof LocalStoreSchemaError)
152
+ incompatible.push({
153
+ kind: "agent-store",
154
+ from: store.entry,
155
+ name: store.agentId,
156
+ folder: "agents",
157
+ });
158
+ else
159
+ logEvent("upgrade-agent-store-survey", cause);
160
+ }
161
+ }
162
+ for (const path of await incompatibleLocalExtensionActivations(home))
163
+ incompatible.push({
164
+ kind: "extension-activations",
165
+ from: path,
166
+ name: "activations.json",
167
+ folder: "extensions",
168
+ });
169
+ return incompatible;
170
+ }
171
+ /** Upgrade → service startup → extensions owner: this lock is always first. */
172
+ export async function upgradeLocalHome(options) {
173
+ const home = options.home ?? homedir();
174
+ const spore = versionMarkerSchema.shape.spore.parse(options.spore);
175
+ // A new empty home has no need to read a release artifact to use ordinary
176
+ // commands. A recorded or running older installation needs its label.
177
+ const recorded = await readPriorInstall(home);
178
+ let news = recorded.kind === "unrecorded"
179
+ ? undefined
180
+ : await readLocalUpgradeNews(options.newsPath);
181
+ const current = (prior, release) => prior.kind === "recorded" &&
182
+ release !== undefined &&
183
+ (semver.gt(prior.release, release) ||
184
+ (semver.eq(prior.release, release) && semver.gte(prior.spore, spore)));
185
+ if (current(recorded, news?.release))
186
+ return { kind: "current" };
187
+ const root = join(home, ".fungi");
188
+ await mkdir(root, { recursive: true, mode: 0o700 });
189
+ const release = await lockfile.lock(root, {
190
+ lockfilePath: join(root, ".upgrade.lock"),
191
+ realpath: false,
192
+ stale: 30_000,
193
+ update: 10_000,
194
+ retries: { retries: 60, factor: 1.2, minTimeout: 25, maxTimeout: 250 },
195
+ });
196
+ try {
197
+ options.signal?.throwIfAborted();
198
+ const observed = await readPriorInstall(home);
199
+ const pending = await readPendingUpgrade(home);
200
+ const prior = pending !== undefined && !pendingIsComplete(observed, pending)
201
+ ? pending
202
+ : observed;
203
+ if (prior.kind === "recorded") {
204
+ news ??= await readLocalUpgradeNews(options.newsPath);
205
+ }
206
+ if (current(prior, news?.release))
207
+ return { kind: "current" };
208
+ const directory = options.directory ?? join(root, "service");
209
+ const registration = await readServiceRegistration(directory);
210
+ if (prior.kind === "unrecorded" &&
211
+ registration?.kind === "current" &&
212
+ registration.record.version === privateLocalServiceVersion() &&
213
+ (await surveyIncompatibleLocalHome(home)).length === 0)
214
+ return { kind: "current" };
215
+ if (prior.kind === "unrecorded" && registration === undefined) {
216
+ const archive = join(root, "archive", archiveSlug(prior));
217
+ if ((await surveyIncompatibleLocalHome(home)).length === 0 &&
218
+ !(await exists(join(archive, "agents"))) &&
219
+ !(await exists(join(archive, "extensions", "activations.json"))))
220
+ return { kind: "current" };
221
+ }
222
+ // The service stop removes service.json. Record its prior identity while
223
+ // the upgrade lock still holds it, before any stop or archive rename.
224
+ await writePendingUpgrade(home, prior);
225
+ let service = "absent";
226
+ if (registration !== undefined) {
227
+ // The upgrade is no bypass around the service's installed-client check.
228
+ // An uninstalled or stale window cannot signal a different live build.
229
+ if ((await exists(join(directory, "owner.sqlite"))) &&
230
+ (await serviceOwnerIsLive(directory)) &&
231
+ privateLocalServiceVersion() !==
232
+ (await (options.installedClientBuildId ?? readInstalledLocalClientBuildId)()))
233
+ throw new LocalServiceUpdated();
234
+ const expectedId = registration.kind === "current"
235
+ ? registration.record.serviceId
236
+ : registration.kind === "other-protocol"
237
+ ? registration.lifecycle.serviceId
238
+ : undefined;
239
+ const stop = options.stopService ??
240
+ createPrivateLocalServiceManager({ home, directory }).stop;
241
+ const stopped = await stop(expectedId);
242
+ if (stopped.kind === "replaced")
243
+ throw new Error(`The Fungi service in ${directory} changed during upgrade; retry before archiving`);
244
+ if (stopped.kind === "stopped" || stopped.kind === "killed")
245
+ service = stopped.kind;
246
+ }
247
+ if ((await readServiceRegistration(directory)) !== undefined ||
248
+ ((await exists(join(directory, "owner.sqlite"))) &&
249
+ (await serviceOwnerIsLive(directory))))
250
+ throw new Error(`A Fungi service still owns ${directory}; no files were archived`);
251
+ await assertNoUntrackedServiceOwner(directory);
252
+ const archive = join(root, "archive", archiveSlug(prior));
253
+ const incompatible = await surveyIncompatibleLocalHome(home);
254
+ // The installed-registry owner also uses archive/unversioned. Only our
255
+ // own entries indicate an interrupted home upgrade to resume.
256
+ const existingArchive = (await exists(join(archive, "agents"))) ||
257
+ (await exists(join(archive, "extensions", "activations.json")));
258
+ if (incompatible.length === 0 &&
259
+ !existingArchive &&
260
+ service === "absent" &&
261
+ prior.kind === "unrecorded") {
262
+ await unlink(pendingPath(home));
263
+ return { kind: "current" };
264
+ }
265
+ news ??= await readLocalUpgradeNews(options.newsPath);
266
+ if (incompatible.length > 0) {
267
+ await mkdir(join(archive, "agents"), { recursive: true, mode: 0o700 });
268
+ await mkdir(join(archive, "extensions"), {
269
+ recursive: true,
270
+ mode: 0o700,
271
+ });
272
+ }
273
+ const archived = [];
274
+ for (const item of incompatible) {
275
+ options.signal?.throwIfAborted();
276
+ let destination;
277
+ for (const suffix of [
278
+ "",
279
+ "-2",
280
+ "-3",
281
+ "-4",
282
+ "-5",
283
+ "-6",
284
+ "-7",
285
+ "-8",
286
+ "-9",
287
+ ]) {
288
+ const candidate = join(archive, item.folder, `${item.name}${suffix}`);
289
+ if (!(await exists(candidate))) {
290
+ destination = candidate;
291
+ break;
292
+ }
293
+ }
294
+ if (destination === undefined)
295
+ throw new Error(`Cannot archive ${item.from} under ${join(archive, item.folder)}: all destinations exist`);
296
+ try {
297
+ await rename(item.from, destination);
298
+ }
299
+ catch (cause) {
300
+ throw new Error(`Cannot archive ${item.from} to ${destination}`, {
301
+ cause,
302
+ });
303
+ }
304
+ archived.push({ kind: item.kind, from: item.from, to: destination });
305
+ }
306
+ await writeLocalVersionMarker(home, {
307
+ release: news.release,
308
+ spore,
309
+ build: privateLocalServiceVersion(),
310
+ });
311
+ await unlink(pendingPath(home));
312
+ return {
313
+ kind: "upgraded",
314
+ report: {
315
+ from: prior,
316
+ to: news.release,
317
+ service,
318
+ archive: incompatible.length > 0 || existingArchive ? archive : undefined,
319
+ archived,
320
+ whatsNew: news.lines,
321
+ },
322
+ };
323
+ }
324
+ finally {
325
+ await release();
326
+ }
327
+ }
328
+ export function formatLocalUpgradeNotice(report) {
329
+ const from = report.from.kind === "recorded"
330
+ ? report.from.release
331
+ : report.from.kind === "protocol"
332
+ ? `an earlier Fungi (protocol ${String(report.from.protocol)})`
333
+ : "an earlier Fungi";
334
+ const lines = [`Upgraded ${from} → ${report.to}`];
335
+ if (report.service === "stopped")
336
+ lines.push("Stopped the old Fungi service.");
337
+ if (report.service === "killed")
338
+ lines.push("Stopped the old Fungi service after its exit deadline.");
339
+ if (report.archive !== undefined) {
340
+ const agents = report.archived.filter((item) => item.kind === "agent-store").length;
341
+ const activations = report.archived.find((item) => item.kind === "extension-activations");
342
+ const contents = [
343
+ ...(agents > 0
344
+ ? [`${String(agents)} Agent ${agents === 1 ? "store" : "stores"}`]
345
+ : []),
346
+ ...(activations !== undefined ? ["extension activations"] : []),
347
+ ].join(" and ");
348
+ lines.push(report.archived.length === 0
349
+ ? "The earlier archive is still at"
350
+ : `Archived ${contents} to`, ` ${report.archive} (nothing was deleted)`);
351
+ if (activations !== undefined)
352
+ lines.push(` Extension activations: ${activations.to}`);
353
+ }
354
+ const news = report.whatsNew.slice(0, 3);
355
+ if (news.length > 0)
356
+ lines.push("What's new:", ...news.map((line) => ` • ${line}`));
357
+ return lines;
358
+ }
@@ -4,6 +4,8 @@ import { createConversationAgents } from "@fungi.computer/shiitake-extensions/ro
4
4
  export type LocalCreateRequest = Parameters<NonNullable<AgentControlDirectory["create"]>>[0] & Readonly<{
5
5
  /** The Agent creating, resolved by the creator's own host, never by the caller. */
6
6
  createdBy: string;
7
+ /** The creating Session, captured by the Local Agent-control host. */
8
+ createdBySessionId: string;
7
9
  }>;
8
10
  /**
9
11
  * One ordinary Agent as the project's directory lists it. `unreadable` is
@@ -34,11 +36,13 @@ export type LocalArchiveRequest = Readonly<{
34
36
  * creating Agent, so the directory owner can hold the created Agent to what
35
37
  * the creator holds; its archive names who archives.
36
38
  */
37
- export type LocalAgentControlDirectory = Omit<AgentControlDirectory, "create" | "list" | "archive" | "unarchive"> & Readonly<{
39
+ export type LocalAgentControlDirectory = Omit<AgentControlDirectory, "create" | "list" | "archive" | "unarchive" | "answer" | "escalate"> & Readonly<{
38
40
  list: (signal: AbortSignal, options?: Readonly<{
39
41
  archived?: boolean;
40
42
  }>) => Promise<readonly LocalAgentControlAgent[]>;
41
43
  create: (input: LocalCreateRequest, signal: AbortSignal) => Promise<AgentControlAgent>;
44
+ answer: (input: Parameters<NonNullable<AgentControlDirectory["answer"]>>[0], senderAgentId: string, senderSessionId: string, signal: AbortSignal) => ReturnType<NonNullable<AgentControlDirectory["answer"]>>;
45
+ escalate: (input: Parameters<NonNullable<AgentControlDirectory["escalate"]>>[0], senderAgentId: string, senderSessionId: string, signal: AbortSignal) => ReturnType<NonNullable<AgentControlDirectory["escalate"]>>;
42
46
  archive: (input: LocalArchiveRequest, signal: AbortSignal) => Promise<void>;
43
47
  unarchive: (input: LocalArchiveRequest, signal: AbortSignal) => Promise<void>;
44
48
  }>;
@@ -57,6 +61,7 @@ export declare function createPrivateLocalAgentControl(input: Readonly<{
57
61
  acquire: (context: import("@fungi.computer/mycelium").CapabilitySourceContext) => Promise<import("@fungi.computer/mycelium").CapabilitySourceAcquisition>;
58
62
  }>;
59
63
  source: Readonly<{
64
+ hasLiveChildCommand: (sessionId: string) => boolean;
60
65
  read: (sessionId: string, limit: number) => Promise<Readonly<{
61
66
  agents: readonly import("@fungi.computer/shiitake-extensions/subagents/host").SubagentRecord[];
62
67
  more: number;
@@ -72,6 +77,8 @@ export declare function createPrivateLocalAgentControl(input: Readonly<{
72
77
  }>) => Promise<readonly AgentControlAgent[]>;
73
78
  send?: (input: import("@fungi.computer/shiitake/agent-control").AgentDelegationInput, sourceSessionId: string, signal: AbortSignal, invocation?: import("@fungi.computer/shiitake/agent-control").AgentDelegationInvocation) => Promise<import("@fungi.computer/shiitake").Accepted | import("@fungi.computer/shiitake").Held>;
74
79
  recall?: (input: import("@fungi.computer/shiitake/agent-control").AgentRecallInput, sourceSessionId: string, signal: AbortSignal) => Promise<import("@fungi.computer/shiitake").CancelResult>;
80
+ answer?: (input: import("@fungi.computer/shiitake/agent-control").AgentControlAnswerInput, senderSessionId: string, signal: AbortSignal) => Promise<import("@fungi.computer/shiitake/agent-control").AgentControlAnswerOutput>;
81
+ escalate?: (input: import("@fungi.computer/shiitake/agent-control").AgentControlEscalateInput, senderSessionId: string, signal: AbortSignal) => Promise<import("@fungi.computer/shiitake/agent-control").AgentControlEscalateOutput>;
75
82
  create?: (input: Readonly<{
76
83
  requestId: string;
77
84
  name?: string;
@@ -8,7 +8,7 @@ import { createConversationAgents } from "@fungi.computer/shiitake-extensions/ro
8
8
  export function createPrivateLocalAgentControl(input) {
9
9
  const conversations = createConversationAgents(input.storage);
10
10
  function bound(parentSessionId) {
11
- const { create, archive, unarchive } = input.directory;
11
+ const { create, archive, unarchive, answer, escalate } = input.directory;
12
12
  const by = { kind: "agent", agentId: input.agentId };
13
13
  return {
14
14
  ...input.directory,
@@ -17,8 +17,22 @@ export function createPrivateLocalAgentControl(input) {
17
17
  create: async (request, signal) => {
18
18
  if (parentSessionId === undefined)
19
19
  throw new AgentCreateRefused("no_creator", "creation requires an invoking Session");
20
- return create({ ...request, createdBy: input.agentId }, signal);
20
+ return create({
21
+ ...request,
22
+ createdBy: input.agentId,
23
+ createdBySessionId: parentSessionId,
24
+ }, signal);
21
25
  },
26
+ answer: (request, sourceSessionId, signal) => parentSessionId === undefined
27
+ ? Promise.resolve({ status: "refused", reason: "no_provenance" })
28
+ : sourceSessionId !== parentSessionId
29
+ ? Promise.resolve({ status: "refused", reason: "not_sender" })
30
+ : answer(request, input.agentId, parentSessionId, signal),
31
+ escalate: (request, sourceSessionId, signal) => parentSessionId === undefined
32
+ ? Promise.resolve({ status: "refused", reason: "no_provenance" })
33
+ : sourceSessionId !== parentSessionId
34
+ ? Promise.resolve({ status: "refused", reason: "not_sender" })
35
+ : escalate(request, input.agentId, parentSessionId, signal),
22
36
  };
23
37
  }
24
38
  const control = conversations.control(bound);
@@ -87,8 +87,13 @@ declare const wireResultSchema: z.ZodUnion<readonly [z.ZodDiscriminatedUnion<[z.
87
87
  category: z.ZodLiteral<"cancelled">;
88
88
  }, z.core.$strict>, z.ZodObject<{
89
89
  category: z.ZodLiteral<"no-key-entered">;
90
+ }, z.core.$strict>, z.ZodObject<{
91
+ category: z.ZodLiteral<"prompt-failed">;
92
+ }, z.core.$strict>, z.ZodObject<{
93
+ category: z.ZodLiteral<"missing-authorization-code">;
90
94
  }, z.core.$strict>, z.ZodObject<{
91
95
  category: z.ZodLiteral<"unknown">;
96
+ errorName: z.ZodString;
92
97
  }, z.core.$strict>], "category">;
93
98
  }, z.core.$strict>]>;
94
99
  declare class AuthPromptTarget extends RpcTarget {
@@ -80,6 +80,24 @@ const eventSchema = z.discriminatedUnion("type", [
80
80
  }),
81
81
  ]);
82
82
  const answerSchema = z.string().max(localAuthInputLimit);
83
+ const errorNameSchema = z.string().regex(/^[A-Za-z][A-Za-z0-9_]{0,63}$/u);
84
+ /** A class name only: anything else (or a non-Error) is named generically. */
85
+ function errorNameOf(cause) {
86
+ if (!(cause instanceof Error))
87
+ return "NonError";
88
+ const name = errorNameSchema.safeParse(cause.name);
89
+ return name.success ? name.data : "Error";
90
+ }
91
+ /**
92
+ * The person's own dialog could not show or answer a provider question (for
93
+ * example the prompt failed the renderer's contract). Fixed text, no cause.
94
+ */
95
+ class LocalAuthPromptFailed extends Error {
96
+ constructor() {
97
+ super("Authentication prompt failed");
98
+ this.name = "LocalAuthPromptFailed";
99
+ }
100
+ }
83
101
  /**
84
102
  * The closed set of failures that cross the service. Provider errors can
85
103
  * contain credential material, so no free text, cause or provider message
@@ -98,7 +116,13 @@ const localAuthFailureSchema = z.discriminatedUnion("category", [
98
116
  }),
99
117
  z.strictObject({ category: z.literal("cancelled") }),
100
118
  z.strictObject({ category: z.literal("no-key-entered") }),
101
- z.strictObject({ category: z.literal("unknown") }),
119
+ z.strictObject({ category: z.literal("prompt-failed") }),
120
+ z.strictObject({ category: z.literal("missing-authorization-code") }),
121
+ z.strictObject({
122
+ category: z.literal("unknown"),
123
+ /** The thrown value's class name only, e.g. `TypeError`; never its message. */
124
+ errorName: errorNameSchema,
125
+ }),
102
126
  ]);
103
127
  const wireResultSchema = z.union([
104
128
  authResultSchema,
@@ -110,6 +134,7 @@ const wireResultSchema = z.union([
110
134
  /** Pi 1.3.0's fixed error prefixes; never retain a provider body suffix. */
111
135
  const callbackPortInUse = /^Port \d+ is in use\b/u;
112
136
  const oauthStateMismatch = "OAuth state mismatch";
137
+ const missingAuthorizationCode = "Missing authorization code";
113
138
  const openAITokenRequestFailed = /^OpenAI OAuth token request failed \(([1-5]\d{2})\)/u;
114
139
  function classifyFailure(cause, aborted) {
115
140
  if (aborted)
@@ -133,7 +158,14 @@ function classifyFailure(cause, aborted) {
133
158
  return { category: "callback-port-in-use" };
134
159
  if (cause instanceof Error && cause.message === oauthStateMismatch)
135
160
  return { category: "oauth-state-mismatch" };
136
- return { category: "unknown" };
161
+ if (cause instanceof LocalAuthPromptFailed)
162
+ return { category: "prompt-failed" };
163
+ if (cause instanceof Error && cause.message === missingAuthorizationCode)
164
+ return { category: "missing-authorization-code" };
165
+ return {
166
+ category: "unknown",
167
+ errorName: errorNameOf(cause),
168
+ };
137
169
  }
138
170
  /** Fixed wording per category; the dialog shows exactly this. */
139
171
  function failureText(failure) {
@@ -164,8 +196,12 @@ function failureText(failure) {
164
196
  return "Authentication cancelled";
165
197
  case "no-key-entered":
166
198
  return new LocalAuthRefusal().message;
199
+ case "prompt-failed":
200
+ return "The sign-in dialog could not show the provider's question (prompt-failed). Nothing was saved.";
201
+ case "missing-authorization-code":
202
+ return "No authorization code was found in what was pasted. Paste the code or the full redirect URL.";
167
203
  case "unknown":
168
- return "Local authentication failed";
204
+ return `Local authentication failed (unknown: ${failure.errorName}). Nothing was saved.`;
169
205
  }
170
206
  }
171
207
  /** The client-side error for one typed failure; its message is fixed text. */
@@ -333,7 +369,13 @@ export class PrivateLocalAuthTarget extends RpcTarget {
333
369
  try {
334
370
  if (promptSignal.aborted)
335
371
  cancel();
336
- return answerSchema.parse(await pending.result());
372
+ const answer = await pending.result().then((value) => ({ value }), (cause) => ({ cause }));
373
+ if ("cause" in answer) {
374
+ promptSignal.throwIfAborted();
375
+ // The presenter's error text is not trusted to be secret-free.
376
+ throw new LocalAuthPromptFailed();
377
+ }
378
+ return answerSchema.parse(answer.value);
337
379
  }
338
380
  finally {
339
381
  promptSignal.removeEventListener("abort", cancel);
@@ -5,7 +5,9 @@ import { basename, dirname, join } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import { z } from "zod";
7
7
  const execute = promisify(execFile);
8
- const queryOptions = { timeout: 2_000, maxBuffer: 4_096 };
8
+ // A compiled launcher cold-starts a Bun process; 2s sat at twice its quiet-box
9
+ // time and killed the query on a loaded box.
10
+ const queryOptions = { timeout: 10_000, maxBuffer: 4_096 };
9
11
  const buildId = z.string().regex(/^[a-f0-9]{64}$/u);
10
12
  function installedLauncherOnPath() {
11
13
  return new Promise((resolve, reject) => {
@@ -28,9 +28,27 @@ export declare function createPrivateLocalDelegation(input: Readonly<{
28
28
  name: string;
29
29
  }[] | undefined>) | undefined;
30
30
  accept: (mail: BirdDogMail, meta?: BirdDogMail["meta"], reply?: VerifiedAgentReply) => Promise<Accepted>;
31
+ /**
32
+ * What happens to items a child left waiting on this Agent when the work
33
+ * that was told about them ends. Without it a host leaves them to expire.
34
+ */
35
+ unattended?: Readonly<{
36
+ /** Exact settled facts of one command of this Agent's own Sessions. */
37
+ readSettlement: (sessionId: string, commandId: string) => Promise<CommandSettlement | undefined>;
38
+ /**
39
+ * Pass one child item on to a person, as this Agent's Session that was
40
+ * told about it. A child that already moved on answers that and nothing changes.
41
+ */
42
+ escalate: (item: Readonly<{
43
+ agentId: string;
44
+ sessionId: string;
45
+ itemId: string;
46
+ }>, receiverSessionId: string) => Promise<void>;
47
+ }>;
31
48
  }>): {
32
49
  send: (task: AgentDelegationInput, sourceSessionId: string, signal: AbortSignal, invocation?: AgentDelegationInvocation) => Promise<DelegationReceipt>;
33
50
  recall: (task: AgentRecallInput, sourceSessionId: string, signal: AbortSignal) => Promise<CancelResult>;
34
51
  receive: (mail: BirdDogMail) => Promise<Accepted>;
52
+ close: () => Promise<void>;
35
53
  outstanding: () => Promise<import("@fungi.computer/shiitake/fleet").OutstandingDelegation | undefined>;
36
54
  };