@fungi.computer/truffle 0.1.1 → 0.2.1

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 (174) hide show
  1. package/README.md +282 -34
  2. package/dist/auth-cli.d.ts +10 -0
  3. package/dist/auth-cli.js +36 -14
  4. package/dist/fungi-account-command.d.ts +1 -1
  5. package/dist/fungi-account-command.js +6 -3
  6. package/dist/index.d.ts +10 -2
  7. package/dist/index.js +8 -2
  8. package/dist/internal/image-fit-metafile.json +106 -0
  9. package/dist/internal/local-image-fit-worker.mjs +960 -0
  10. package/dist/internal/private-workerd-sandbox-worker.mjs +11 -12
  11. package/dist/internal/sandbox-metafile.json +7 -7
  12. package/dist/local-archive-handoff.d.ts +12 -0
  13. package/dist/local-archive-handoff.js +21 -0
  14. package/dist/local-extensions.d.ts +58 -12
  15. package/dist/local-extensions.js +195 -55
  16. package/dist/local-image-fit.d.ts +30 -0
  17. package/dist/local-image-fit.js +105 -0
  18. package/dist/local-models.d.ts +4 -2
  19. package/dist/local-models.js +11 -6
  20. package/dist/local-session-archives.d.ts +8 -0
  21. package/dist/local-session-archives.js +152 -0
  22. package/dist/local-skill-source.d.ts +18 -3
  23. package/dist/local-skill-source.js +216 -140
  24. package/dist/log-event.d.ts +7 -0
  25. package/dist/log-event.js +8 -4
  26. package/dist/private-bounded-response.d.ts +2 -0
  27. package/dist/private-bounded-response.js +41 -0
  28. package/dist/private-file-credential-store.d.ts +28 -3
  29. package/dist/private-file-credential-store.js +57 -9
  30. package/dist/private-files-patch.d.ts +2 -1
  31. package/dist/private-files-patch.js +21 -3
  32. package/dist/private-fungi-account.d.ts +40 -23
  33. package/dist/private-fungi-account.js +201 -21
  34. package/dist/private-fungi-mcp-connection.d.ts +4 -0
  35. package/dist/private-fungi-mcp-connection.js +25 -6
  36. package/dist/private-fungi-mcp-selection-schema.d.ts +7 -0
  37. package/dist/private-fungi-mcp-selection-schema.js +15 -0
  38. package/dist/private-fungi-mcp-selection.d.ts +6 -6
  39. package/dist/private-fungi-mcp-selection.js +29 -16
  40. package/dist/private-fungi-model-api.js +4 -4
  41. package/dist/private-fungi-model-catalog.d.ts +10 -0
  42. package/dist/private-fungi-model-catalog.js +10 -2
  43. package/dist/private-fungi-model-provider.d.ts +1 -1
  44. package/dist/private-fungi-model-provider.js +6 -1
  45. package/dist/private-fungi-pending-logouts.d.ts +1 -1
  46. package/dist/private-fungi-pending-logouts.js +1 -2
  47. package/dist/private-fungi-service-logout.d.ts +44 -0
  48. package/dist/private-fungi-service-logout.js +59 -0
  49. package/dist/private-invocation-computer.d.ts +24 -3
  50. package/dist/private-invocation-computer.js +101 -62
  51. package/dist/private-invocation-files.d.ts +16 -1
  52. package/dist/private-invocation-files.js +154 -83
  53. package/dist/private-local-acp-stdio.js +3 -0
  54. package/dist/private-local-admission-gate.d.ts +14 -0
  55. package/dist/private-local-admission-gate.js +52 -0
  56. package/dist/private-local-agent-control.d.ts +101 -0
  57. package/dist/private-local-agent-control.js +30 -0
  58. package/dist/private-local-agent-default-model.d.ts +12 -0
  59. package/dist/private-local-agent-default-model.js +31 -0
  60. package/dist/private-local-auth-rpc.d.ts +67 -7
  61. package/dist/private-local-auth-rpc.js +140 -17
  62. package/dist/private-local-bwrap.d.ts +13 -0
  63. package/dist/private-local-bwrap.js +70 -0
  64. package/dist/private-local-client-version.d.ts +5 -0
  65. package/dist/private-local-client-version.js +43 -0
  66. package/dist/private-local-computer-attach.d.ts +3 -3
  67. package/dist/private-local-computer-attach.js +17 -3
  68. package/dist/private-local-computer-processes.d.ts +6 -3
  69. package/dist/private-local-computer-processes.js +55 -31
  70. package/dist/private-local-config-root.d.ts +21 -0
  71. package/dist/private-local-config-root.js +19 -0
  72. package/dist/private-local-containment.d.ts +51 -0
  73. package/dist/private-local-containment.js +161 -0
  74. package/dist/private-local-delegation.d.ts +10 -3
  75. package/dist/private-local-delegation.js +70 -15
  76. package/dist/private-local-effective-model.d.ts +9 -1
  77. package/dist/private-local-effective-model.js +3 -0
  78. package/dist/private-local-exec-environment.d.ts +16 -0
  79. package/dist/private-local-exec-environment.js +11 -0
  80. package/dist/private-local-linux-sandbox.d.ts +8 -0
  81. package/dist/private-local-linux-sandbox.js +67 -0
  82. package/dist/private-local-mail.d.ts +4 -2
  83. package/dist/private-local-mail.js +41 -33
  84. package/dist/private-local-mesh-enrollment.d.ts +34 -0
  85. package/dist/private-local-mesh-enrollment.js +188 -0
  86. package/dist/private-local-mesh-links.d.ts +21 -0
  87. package/dist/private-local-mesh-links.js +229 -0
  88. package/dist/private-local-mesh-record.d.ts +120 -0
  89. package/dist/private-local-mesh-record.js +55 -0
  90. package/dist/private-local-process-identity.d.ts +20 -0
  91. package/dist/private-local-process-identity.js +40 -0
  92. package/dist/private-local-runtime-host.d.ts +64 -13
  93. package/dist/private-local-runtime-host.js +361 -172
  94. package/dist/private-local-sandbox-policy.d.ts +21 -0
  95. package/dist/private-local-sandbox-policy.js +121 -0
  96. package/dist/private-local-sandbox.d.ts +37 -0
  97. package/dist/private-local-sandbox.js +114 -0
  98. package/dist/private-local-seatbelt.d.ts +24 -0
  99. package/dist/private-local-seatbelt.js +68 -0
  100. package/dist/private-local-service-contract.d.ts +91 -11
  101. package/dist/private-local-service-discovery.d.ts +129 -13
  102. package/dist/private-local-service-discovery.js +148 -47
  103. package/dist/private-local-service-lease.d.ts +16 -0
  104. package/dist/private-local-service-lease.js +105 -0
  105. package/dist/private-local-service-listener.d.ts +9 -2
  106. package/dist/private-local-service-listener.js +260 -64
  107. package/dist/private-local-service-log.d.ts +128 -0
  108. package/dist/private-local-service-log.js +321 -0
  109. package/dist/private-local-service-owner.d.ts +87 -1519
  110. package/dist/private-local-service-owner.js +1069 -71
  111. package/dist/private-local-service-process.js +3 -1
  112. package/dist/private-local-service-termination.d.ts +7 -0
  113. package/dist/private-local-service-termination.js +67 -0
  114. package/dist/private-local-service.d.ts +31 -2
  115. package/dist/private-local-service.js +402 -126
  116. package/dist/private-local-session-discovery.d.ts +52 -5
  117. package/dist/private-local-session-discovery.js +132 -11
  118. package/dist/private-local-shiitake-host.js +2 -2
  119. package/dist/private-local-startup-model.d.ts +51 -0
  120. package/dist/private-local-startup-model.js +86 -0
  121. package/dist/private-local-stop-all.d.ts +27 -0
  122. package/dist/private-local-stop-all.js +26 -0
  123. package/dist/private-local-ui-attachment.d.ts +19 -0
  124. package/dist/private-local-ui-attachment.js +71 -0
  125. package/dist/private-local-worker-server.d.ts +1 -0
  126. package/dist/private-local-worker-server.js +13 -5
  127. package/dist/private-mcp-form-contract.d.ts +186 -0
  128. package/dist/private-mcp-form-contract.js +53 -0
  129. package/dist/private-mcp-form-elicitation.d.ts +18 -0
  130. package/dist/private-mcp-form-elicitation.js +97 -0
  131. package/dist/private-mcp-form-session.d.ts +172 -0
  132. package/dist/private-mcp-form-session.js +148 -0
  133. package/dist/private-openai-claims.d.ts +30 -0
  134. package/dist/private-openai-claims.js +69 -0
  135. package/dist/private-oyster-mcp-auth.d.ts +13 -0
  136. package/dist/private-oyster-mcp-auth.js +57 -0
  137. package/dist/private-provider-rejection.d.ts +15 -0
  138. package/dist/private-provider-rejection.js +24 -0
  139. package/dist/private-sqlite-owner.d.ts +34 -2
  140. package/dist/private-sqlite-owner.js +103 -37
  141. package/dist/private-team-computers.d.ts +4 -4
  142. package/dist/private-team-computers.js +11 -2
  143. package/dist/seatbelt/safe-base.sbpl +79 -0
  144. package/dist/subscription-activation.d.ts +9 -0
  145. package/dist/subscription-activation.js +62 -0
  146. package/dist/subscription-identity.d.ts +23 -0
  147. package/dist/subscription-identity.js +124 -0
  148. package/dist/subscription-turn.d.ts +17 -0
  149. package/dist/subscription-turn.js +143 -0
  150. package/dist/team-apps.d.ts +2 -2
  151. package/dist/team-apps.js +7 -2
  152. package/native/bin/LICENSE.bubblewrap +502 -0
  153. package/native/bin/LICENSE.libcap +112 -0
  154. package/native/bin/LICENSE.sandbox-runtime +201 -0
  155. package/native/bin/SOURCE.bubblewrap.tar.xz +0 -0
  156. package/native/bin/bwrap-build-source/Dockerfile +48 -0
  157. package/native/bin/bwrap-build-source/README.md +96 -0
  158. package/native/bin/bwrap-build-source/build.mjs +52 -0
  159. package/native/bin/bwrap-build-source/linux-arm64.ini +14 -0
  160. package/native/bin/bwrap-build-source/pin.mjs +21 -0
  161. package/native/bin/bwrap-build-source/record.mjs +74 -0
  162. package/native/bin/bwrap-build-source/vendor/LICENSE.sandbox-runtime +201 -0
  163. package/native/bin/bwrap-build-source/vendor/README.md +23 -0
  164. package/native/bin/bwrap-build-source/vendor/seccomp-unix-block.c +280 -0
  165. package/native/bin/bwrap-build.json +44 -0
  166. package/native/bin/fungi-bwrap +0 -0
  167. package/native/bin/fungi-bwrap.sha256 +1 -0
  168. package/native/bin/fungi-seccomp.bpf +0 -0
  169. package/native/bin/fungi-seccomp.bpf.sha256 +1 -0
  170. package/package.json +44 -31
  171. package/dist/private-local-model-settings.d.ts +0 -5
  172. package/dist/private-local-model-settings.js +0 -31
  173. package/dist/private-local-subagents.d.ts +0 -27
  174. package/dist/private-local-subagents.js +0 -77
@@ -1,15 +1,62 @@
1
1
  import { type StoredSessionSummary } from "@fungi.computer/shiitake/effect";
2
- import type { PrivateLocalServiceManager, PrivateLocalServiceRequestOptions, PrivateLocalSessionDiscovery } from "./private-local-service-contract.js";
2
+ import type { PrivateLocalServiceManager, PrivateLocalDiscoveryOptions, PrivateLocalSessionDiscovery } from "./private-local-service-contract.js";
3
3
  export type LocalSummary = StoredSessionSummary & Readonly<{
4
4
  databasePath: string;
5
5
  sessionId: string;
6
6
  }>;
7
- export declare function listPrivateLocalAgentStores(root: string): Promise<readonly string[]>;
7
+ /**
8
+ * Which Agent stores a listing returns: the ones in use, only the archived
9
+ * ones (to restore them), or both (to find one by id).
10
+ */
11
+ export type LocalStoreVisibility = "active" | "archived" | "all";
12
+ /** The one plain phrase for a store written before the current schema, with how to reset it. */
13
+ export declare function unreadableStoreReason(agentId: string): string;
14
+ /**
15
+ * Whether a store's Agent is archived. A store this cannot read counts as
16
+ * not archived, so discovery still reports it as a problem.
17
+ */
18
+ export declare function isArchivedStore(path: string): boolean;
19
+ export declare function listPrivateLocalAgentStores(root: string, visibility?: LocalStoreVisibility): Promise<readonly string[]>;
20
+ /** A Local or Woodstock store outside the current schema; Local never migrates it. */
21
+ export declare class LocalStoreSchemaError extends Error {
22
+ readonly databasePath: string;
23
+ readonly name = "LocalStoreSchemaError";
24
+ constructor(databasePath: string, options?: ErrorOptions);
25
+ }
26
+ /**
27
+ * One Agent store's chosen display name, default model, whether it is
28
+ * archived and the Agent that created it (absent when a human made it), read
29
+ * without opening a host.
30
+ */
31
+ export declare function readPrivateLocalAgentProfile(path: string): {
32
+ name: string | undefined;
33
+ createdBy: string | null;
34
+ archived: boolean;
35
+ requireSandbox: boolean;
36
+ folder: string;
37
+ model: {
38
+ readonly providerId: string;
39
+ readonly modelId: string;
40
+ readonly reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | undefined;
41
+ } | null;
42
+ };
8
43
  export declare function readPrivateLocalSessionSummaries(path: string): Promise<readonly LocalSummary[]>;
9
- /** Discover existing folder sessions without opening or reconciling a host. */
10
- export declare function readPrivateLocalSessionDiscovery(invocationRoot: string, request?: PrivateLocalServiceRequestOptions): Promise<{
44
+ /**
45
+ * Discover existing folder sessions without opening or reconciling a host.
46
+ * Archived Agents and their Sessions are left out, so no picker or "newest
47
+ * Session" choice can land on one; `archived` lists only those instead.
48
+ */
49
+ export declare function readPrivateLocalSessionDiscovery(invocationRoot: string, request?: PrivateLocalDiscoveryOptions): Promise<{
11
50
  root: string;
12
- agentCount: number;
51
+ agents: Readonly<{
52
+ agentId: string;
53
+ unreadable?: string | undefined;
54
+ name?: string | undefined;
55
+ createdBy?: string | null | undefined;
56
+ databasePath: string;
57
+ sessionCount: number;
58
+ updatedAt?: number | undefined;
59
+ }>[];
13
60
  sessions: LocalSummary[];
14
61
  problems: Readonly<{
15
62
  kind: "corrupt" | "in_use" | "unreadable";
@@ -1,9 +1,13 @@
1
1
  import { readdir, realpath, stat } from "node:fs/promises";
2
- import { join, resolve } from "node:path";
3
- import { DatabaseSync } from "node:sqlite";
2
+ import { basename, dirname, join, resolve } from "node:path";
4
3
  import * as Effect from "effect/Effect";
4
+ import { parseModelPreferenceJson } from "@fungi.computer/shiitake/model-preference";
5
+ import { z } from "zod";
6
+ import { readAgentDefaultModelJson } from "./private-local-agent-default-model.js";
7
+ import { localAgentFolderSchema, resolveAgentFolder, } from "./private-local-sandbox-policy.js";
8
+ import { logEvent } from "./log-event.js";
5
9
  import { readSessionSummaries, } from "@fungi.computer/shiitake/effect";
6
- import { createPrivateSqliteOwner, privateSqliteFailureCode, } from "./private-sqlite-owner.js";
10
+ import { createPrivateSqliteOwner, openPrivateReadOnlySqliteDatabase, privateSqliteFailureCode, } from "./private-sqlite-owner.js";
7
11
  function compareSummaries(left, right) {
8
12
  return (right.updatedAt - left.updatedAt ||
9
13
  right.createdAt - left.createdAt ||
@@ -61,12 +65,33 @@ async function immediateStoreCandidates(root, relative) {
61
65
  }
62
66
  })).then((values) => values.filter((value) => value !== undefined));
63
67
  }
64
- export async function listPrivateLocalAgentStores(root) {
68
+ /** The one plain phrase for a store written before the current schema, with how to reset it. */
69
+ export function unreadableStoreReason(agentId) {
70
+ return `predates the current schema; move .fungi/agents/${agentId} aside to reset it`;
71
+ }
72
+ /**
73
+ * Whether a store's Agent is archived. A store this cannot read counts as
74
+ * not archived, so discovery still reports it as a problem.
75
+ */
76
+ export function isArchivedStore(path) {
77
+ try {
78
+ return readPrivateLocalAgentProfile(path).archived;
79
+ }
80
+ catch (cause) {
81
+ if (!(cause instanceof LocalStoreSchemaError))
82
+ logEvent("store-archived", cause);
83
+ return false;
84
+ }
85
+ }
86
+ export async function listPrivateLocalAgentStores(root, visibility = "active") {
65
87
  const candidates = await immediateStoreCandidates(root, ".fungi/agents");
66
88
  const paths = new Set();
67
89
  for (const candidate of candidates) {
68
90
  try {
69
- paths.add(await realpath(candidate.path));
91
+ const path = await realpath(candidate.path);
92
+ if (visibility === "all" ||
93
+ isArchivedStore(path) === (visibility === "archived"))
94
+ paths.add(path);
70
95
  }
71
96
  catch (cause) {
72
97
  if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
@@ -76,8 +101,62 @@ export async function listPrivateLocalAgentStores(root) {
76
101
  }
77
102
  return [...paths];
78
103
  }
104
+ const storedProfileSchema = z.strictObject({
105
+ name: z.string().nullable(),
106
+ createdBy: z.string().nullable(),
107
+ archived: z.union([z.literal(0), z.literal(1)]),
108
+ requireSandbox: z.union([z.literal(0), z.literal(1)]),
109
+ folder: localAgentFolderSchema,
110
+ model: z.string(),
111
+ });
112
+ /** A Local or Woodstock store outside the current schema; Local never migrates it. */
113
+ export class LocalStoreSchemaError extends Error {
114
+ databasePath;
115
+ name = "LocalStoreSchemaError";
116
+ constructor(databasePath, options) {
117
+ super(`Agent store ${databasePath} is incompatible with this version of Fungi. To reset, stop the owning Fungi service, then move this database, its .owner.sqlite sidecar and any associated SQLite journal files aside together. Create a fresh Agent; existing Sessions are not migrated. Keep the moved files if you need their data.`, options);
118
+ this.databasePath = databasePath;
119
+ }
120
+ }
121
+ /**
122
+ * One Agent store's chosen display name, default model, whether it is
123
+ * archived and the Agent that created it (absent when a human made it), read
124
+ * without opening a host.
125
+ */
126
+ export function readPrivateLocalAgentProfile(path) {
127
+ const database = openPrivateReadOnlySqliteDatabase(path);
128
+ try {
129
+ const columns = database
130
+ .prepare("PRAGMA table_info(private_local_identity)")
131
+ .all();
132
+ if (!["name", "created_by", "archived", "require_sandbox", "folder"].every((required) => columns.some((column) => column.name === required)))
133
+ throw new LocalStoreSchemaError(path);
134
+ const identity = database
135
+ .prepare("SELECT name, created_by, archived, require_sandbox, folder FROM private_local_identity WHERE singleton = 1")
136
+ .get();
137
+ const stored = storedProfileSchema.parse({
138
+ name: identity?.name,
139
+ createdBy: identity?.created_by,
140
+ archived: identity?.archived,
141
+ requireSandbox: identity?.require_sandbox,
142
+ folder: identity?.folder,
143
+ model: readAgentDefaultModelJson(database),
144
+ });
145
+ return {
146
+ name: stored.name ?? undefined,
147
+ createdBy: stored.createdBy,
148
+ archived: stored.archived === 1,
149
+ requireSandbox: stored.requireSandbox === 1,
150
+ folder: stored.folder,
151
+ model: parseModelPreferenceJson(stored.model),
152
+ };
153
+ }
154
+ finally {
155
+ database.close();
156
+ }
157
+ }
79
158
  export async function readPrivateLocalSessionSummaries(path) {
80
- const database = new DatabaseSync(path, { readOnly: true, timeout: 0 });
159
+ const database = openPrivateReadOnlySqliteDatabase(path);
81
160
  try {
82
161
  database.exec("PRAGMA foreign_keys = ON");
83
162
  let sqliteFailure;
@@ -117,24 +196,65 @@ export async function readPrivateLocalSessionSummaries(path) {
117
196
  database.close();
118
197
  }
119
198
  }
120
- /** Discover existing folder sessions without opening or reconciling a host. */
199
+ /**
200
+ * Discover existing folder sessions without opening or reconciling a host.
201
+ * Archived Agents and their Sessions are left out, so no picker or "newest
202
+ * Session" choice can land on one; `archived` lists only those instead.
203
+ */
121
204
  export async function readPrivateLocalSessionDiscovery(invocationRoot, request = {}) {
122
205
  request.signal?.throwIfAborted();
123
206
  const root = await realpath(resolve(invocationRoot));
124
- const paths = await listPrivateLocalAgentStores(root);
207
+ const paths = await listPrivateLocalAgentStores(root, request.archived === true ? "archived" : "active");
125
208
  const discovered = [];
209
+ const agents = [];
210
+ const profiles = new Map();
126
211
  const problems = [];
127
212
  for (const path of paths) {
128
213
  request.signal?.throwIfAborted();
214
+ const agentId = basename(dirname(path));
129
215
  try {
130
- discovered.push(...(await readPrivateLocalSessionSummaries(path)));
216
+ const summaries = await readPrivateLocalSessionSummaries(path);
217
+ discovered.push(...summaries);
218
+ let profile;
219
+ let unreadable;
220
+ try {
221
+ profile = readPrivateLocalAgentProfile(path);
222
+ resolveAgentFolder(root, profile.folder);
223
+ profiles.set(agentId, profile);
224
+ }
225
+ catch (cause) {
226
+ // Shown on the row so the user knows to reset the store; opening it
227
+ // refuses with the same error.
228
+ if (!(cause instanceof LocalStoreSchemaError))
229
+ throw cause;
230
+ unreadable = unreadableStoreReason(agentId);
231
+ }
232
+ agents.push({
233
+ agentId,
234
+ name: profile?.name,
235
+ unreadable,
236
+ databasePath: path,
237
+ sessionCount: summaries.length,
238
+ updatedAt: summaries[0]?.updatedAt,
239
+ });
131
240
  }
132
241
  catch (cause) {
133
242
  problems.push({ kind: problemKind(cause) });
243
+ agents.push({ agentId, databasePath: path, sessionCount: 0 });
134
244
  }
135
245
  }
136
246
  discovered.sort(compareSummaries);
137
- return { root, agentCount: paths.length, sessions: discovered, problems };
247
+ // The wire carries identity; clients derive the “made by” label from names.
248
+ const labelled = agents.map((agent) => {
249
+ const createdBy = profiles.get(agent.agentId)?.createdBy;
250
+ if (createdBy === undefined)
251
+ return agent;
252
+ return {
253
+ ...agent,
254
+ createdBy,
255
+ };
256
+ });
257
+ return { root, agents: labelled, sessions: discovered, problems };
138
258
  }
139
259
  /** Project the service-owned discovery into choices that resume through it. */
140
260
  export function projectPrivateLocalSessionDiscovery(discovered, connect) {
@@ -147,10 +267,11 @@ export function projectPrivateLocalSessionDiscovery(discovered, connect) {
147
267
  databasePath,
148
268
  sessionId,
149
269
  fungiMcp: resumeRequest?.fungiMcp,
270
+ requireSandbox: resumeRequest?.requireSandbox,
150
271
  }, resumeRequest),
151
272
  }));
152
273
  return Object.freeze({
153
- agentCount: discovered.agentCount,
274
+ agents: Object.freeze(discovered.agents),
154
275
  sessions: Object.freeze(sessions),
155
276
  problems: Object.freeze(discovered.problems),
156
277
  });
@@ -35,7 +35,7 @@ async function closePrivateShiitakeResources(failures, client, server, owner) {
35
35
  /** Open one bounded private local Hono host on a loopback-only ephemeral port. */
36
36
  export async function openPrivateLocalShiitakeHost(input, openServer, openHost = openPrivateLocalRuntimeHost, eventSource) {
37
37
  const owner = await openHost(input);
38
- const routes = createShiitakeHttpRoutes(owner.getSession, owner.listSessions, owner.createSession, owner.deleteSession, () => owner.extensions);
38
+ const routes = createShiitakeHttpRoutes((sessionId) => owner.getSession(sessionId), owner.listSessions, owner.createSession, owner.deleteSession, () => owner.extensions);
39
39
  const fetch = (request) => routes.fetch(request);
40
40
  let server;
41
41
  let client;
@@ -107,7 +107,7 @@ export async function openPrivateLocalShiitakeWorkerBatch(inputs, openServer, op
107
107
  for (const input of validated) {
108
108
  const owner = await openHost(input.owner);
109
109
  owners.push({ name: input.name, owner });
110
- const routes = createShiitakeHttpRoutes(owner.getSession, owner.listSessions, owner.createSession, owner.deleteSession, () => owner.extensions);
110
+ const routes = createShiitakeHttpRoutes((sessionId) => owner.getSession(sessionId), owner.listSessions, owner.createSession, owner.deleteSession, () => owner.extensions);
111
111
  const routePath = "/workers/" + input.name;
112
112
  app.route(routePath, routes);
113
113
  workers.push(Object.freeze({
@@ -0,0 +1,51 @@
1
+ import { getSupportedThinkingLevels, type Api, type Model, type Models, type ThinkingLevel } from "@earendil-works/pi-ai";
2
+ import { type ModelPreference } from "@fungi.computer/shiitake/model-preference";
3
+ /** Which layer decided a startup model: a Session override, the Agent default, or the available login. */
4
+ export type LocalStartupModelSource = "session" | "agent" | "available";
5
+ /** The one model choice a run or a new chat starts from, before it is checked against its provider. */
6
+ export type LocalStartupChoice = Readonly<{
7
+ preference: ModelPreference;
8
+ source: LocalStartupModelSource;
9
+ }>;
10
+ /** The effective model a new chat of one Agent starts with. */
11
+ export type LocalStartupModel = Readonly<{
12
+ providerId: string;
13
+ modelId: string;
14
+ reasoning: ThinkingLevel | undefined;
15
+ source: LocalStartupModelSource;
16
+ }>;
17
+ /** Validate a requested thinking level against the registered model. */
18
+ export declare function validateLocalReasoning(model: Parameters<typeof getSupportedThinkingLevels>[0], requested: ThinkingLevel | undefined): void;
19
+ /** Local's default is medium, clamped through Pi's model capabilities. */
20
+ export declare function defaultLocalReasoning(model: Parameters<typeof getSupportedThinkingLevels>[0]): ThinkingLevel | undefined;
21
+ /**
22
+ * The thinking level a chosen model runs with: the choice's own level, else
23
+ * the opener's level when this model supports it, else Local's default.
24
+ */
25
+ export declare function startupReasoning(selected: Model<Api>, preference: ModelPreference, ownerReasoning?: ThinkingLevel): ThinkingLevel | undefined;
26
+ /** Resolve explicit layers without checking logins or discovering models. */
27
+ export declare function resolveKnownLocalStartupChoice(input: Readonly<{
28
+ session: ModelPreference | null;
29
+ agent: ModelPreference | null;
30
+ }>): LocalStartupChoice | null;
31
+ /**
32
+ * Resolve the explicit choice, or discover the available login. An explicit
33
+ * layer never falls through on an auth failure; the caller reports it.
34
+ */
35
+ export declare function resolveLocalStartupChoice(input: Readonly<{
36
+ models: Models;
37
+ session: ModelPreference | null;
38
+ agent: ModelPreference | null;
39
+ signal: AbortSignal;
40
+ }>): Promise<LocalStartupChoice | null>;
41
+ /**
42
+ * The model a new chat of the Agent at `databasePath` starts with; no store
43
+ * means a new Agent, which starts from the available login. A store that
44
+ * predates the current schema fails loudly. The landing and a
45
+ * fresh Session both resolve through this chain.
46
+ */
47
+ export declare function resolveAgentStartupModel(input: Readonly<{
48
+ models: Models;
49
+ databasePath: string | undefined;
50
+ signal: AbortSignal;
51
+ }>): Promise<LocalStartupModel | null>;
@@ -0,0 +1,86 @@
1
+ import { clampThinkingLevel, getSupportedThinkingLevels, } from "@earendil-works/pi-ai";
2
+ import {} from "@fungi.computer/shiitake/model-preference";
3
+ import { readPrivateLocalAgentProfile } from "./private-local-session-discovery.js";
4
+ import { defaultLocalModel } from "./private-local-default-model.js";
5
+ /** Validate a requested thinking level against the registered model. */
6
+ export function validateLocalReasoning(model, requested) {
7
+ if (requested === undefined)
8
+ return;
9
+ const supported = getSupportedThinkingLevels(model).filter((level) => level !== "off");
10
+ if (!supported.includes(requested))
11
+ throw new Error("Reasoning level " +
12
+ requested +
13
+ " is not supported by " +
14
+ model.provider +
15
+ "/" +
16
+ model.id +
17
+ "; supported levels: " +
18
+ (supported.join(", ") || "none"));
19
+ }
20
+ /** Local's default is medium, clamped through Pi's model capabilities. */
21
+ export function defaultLocalReasoning(model) {
22
+ const level = clampThinkingLevel(model, "medium");
23
+ return level === "off" ? undefined : level;
24
+ }
25
+ /**
26
+ * The thinking level a chosen model runs with: the choice's own level, else
27
+ * the opener's level when this model supports it, else Local's default.
28
+ */
29
+ export function startupReasoning(selected, preference, ownerReasoning) {
30
+ return (preference.reasoning ??
31
+ (ownerReasoning !== undefined &&
32
+ getSupportedThinkingLevels(selected).includes(ownerReasoning)
33
+ ? ownerReasoning
34
+ : defaultLocalReasoning(selected)));
35
+ }
36
+ /** Resolve explicit layers without checking logins or discovering models. */
37
+ export function resolveKnownLocalStartupChoice(input) {
38
+ if (input.session !== null)
39
+ return { preference: input.session, source: "session" };
40
+ if (input.agent !== null)
41
+ return { preference: input.agent, source: "agent" };
42
+ return null;
43
+ }
44
+ /**
45
+ * Resolve the explicit choice, or discover the available login. An explicit
46
+ * layer never falls through on an auth failure; the caller reports it.
47
+ */
48
+ export async function resolveLocalStartupChoice(input) {
49
+ const known = resolveKnownLocalStartupChoice(input);
50
+ if (known !== null)
51
+ return known;
52
+ const available = await defaultLocalModel(input.models, input.signal);
53
+ return available === null
54
+ ? null
55
+ : { preference: available, source: "available" };
56
+ }
57
+ /**
58
+ * The model a new chat of the Agent at `databasePath` starts with; no store
59
+ * means a new Agent, which starts from the available login. A store that
60
+ * predates the current schema fails loudly. The landing and a
61
+ * fresh Session both resolve through this chain.
62
+ */
63
+ export async function resolveAgentStartupModel(input) {
64
+ const agent = input.databasePath === undefined
65
+ ? null
66
+ : readPrivateLocalAgentProfile(input.databasePath).model;
67
+ const choice = await resolveLocalStartupChoice({
68
+ models: input.models,
69
+ session: null,
70
+ agent,
71
+ signal: input.signal,
72
+ });
73
+ if (choice === null)
74
+ return null;
75
+ const { providerId, modelId } = choice.preference;
76
+ const selected = input.models.getModel(providerId, modelId);
77
+ return {
78
+ providerId,
79
+ modelId,
80
+ // A catalog not loaded yet cannot name a level; the run resolves it later.
81
+ reasoning: selected === undefined
82
+ ? choice.preference.reasoning
83
+ : startupReasoning(selected, choice.preference),
84
+ source: choice.source,
85
+ };
86
+ }
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+ /** The service reports each attempted Agent, including partial failures. */
3
+ export declare const stopAllResultSchema: z.ZodObject<{
4
+ rootAgentId: z.ZodString;
5
+ agents: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
6
+ agentId: z.ZodString;
7
+ name: z.ZodString;
8
+ outcome: z.ZodEnum<{
9
+ stopped: "stopped";
10
+ idle: "idle";
11
+ }>;
12
+ }, z.core.$strict>, z.ZodObject<{
13
+ agentId: z.ZodString;
14
+ name: z.ZodString;
15
+ outcome: z.ZodLiteral<"failed">;
16
+ reason: z.ZodString;
17
+ }, z.core.$strict>], "outcome">>;
18
+ discoveryFailures: z.ZodArray<z.ZodObject<{
19
+ agentId: z.ZodString;
20
+ reason: z.ZodString;
21
+ }, z.core.$strict>>;
22
+ }, z.core.$strict>;
23
+ export type StopAllResult = z.infer<typeof stopAllResultSchema>;
24
+ export declare const stopAllRequestSchema: z.ZodObject<{
25
+ invocationRoot: z.ZodString;
26
+ agentId: z.ZodString;
27
+ }, z.core.$strict>;
@@ -0,0 +1,26 @@
1
+ import { z } from "zod";
2
+ /** The service reports each attempted Agent, including partial failures. */
3
+ export const stopAllResultSchema = z.strictObject({
4
+ rootAgentId: z.string(),
5
+ agents: z.array(z.discriminatedUnion("outcome", [
6
+ z.strictObject({
7
+ agentId: z.string(),
8
+ name: z.string(),
9
+ outcome: z.enum(["stopped", "idle"]),
10
+ }),
11
+ z.strictObject({
12
+ agentId: z.string(),
13
+ name: z.string(),
14
+ outcome: z.literal("failed"),
15
+ reason: z.string(),
16
+ }),
17
+ ])),
18
+ discoveryFailures: z.array(z.strictObject({
19
+ agentId: z.string(),
20
+ reason: z.string(),
21
+ })),
22
+ });
23
+ export const stopAllRequestSchema = z.strictObject({
24
+ invocationRoot: z.string().min(1),
25
+ agentId: z.string().min(1),
26
+ });
@@ -0,0 +1,19 @@
1
+ import type { Json } from "@fungi.computer/shiitake/extensions";
2
+ import { z } from "zod";
3
+ export declare const localUiAttachmentLifetimeSchema: z.ZodEnum<{
4
+ watch: "watch";
5
+ connection: "connection";
6
+ }>;
7
+ export type LocalUiAttachmentLifetime = z.infer<typeof localUiAttachmentLifetimeSchema>;
8
+ /** Host-held authority; a JSON identity or pending id cannot create one. */
9
+ export type LocalUiAttachment = Readonly<{
10
+ sessionId: string;
11
+ signal: AbortSignal;
12
+ close: () => void;
13
+ }>;
14
+ export declare function createLocalUiAttachment(sessionId: string, lifetime?: LocalUiAttachmentLifetime): LocalUiAttachment;
15
+ export declare function startLocalUiWatch(attachment: LocalUiAttachment, sessionId: string): (() => void) | undefined;
16
+ /** Authority lasts for one command, not model work that might wake later. */
17
+ export declare function runLocalUiCommand(attachment: LocalUiAttachment, sessionId: string, body: () => Promise<Json>): Promise<Json>;
18
+ /** Consume host authority before registering the post-commit continuation. */
19
+ export declare function consumeLocalUiCommand(sessionId: string): ((complete: (committed: boolean) => Promise<Json>) => void) | undefined;
@@ -0,0 +1,71 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { z } from "zod";
3
+ export const localUiAttachmentLifetimeSchema = z.enum(["watch", "connection"]);
4
+ const attachments = new WeakMap();
5
+ export function createLocalUiAttachment(sessionId, lifetime = "watch") {
6
+ const controller = new AbortController();
7
+ const attachment = {
8
+ sessionId,
9
+ signal: controller.signal,
10
+ close: () => {
11
+ attachments.delete(attachment);
12
+ controller.abort();
13
+ },
14
+ };
15
+ attachments.set(attachment, { watches: 0, lifetime });
16
+ return attachment;
17
+ }
18
+ export function startLocalUiWatch(attachment, sessionId) {
19
+ const state = attachments.get(attachment);
20
+ if (state === undefined || attachment.sessionId !== sessionId)
21
+ return undefined;
22
+ state.watches += 1;
23
+ let watching = true;
24
+ return () => {
25
+ if (!watching)
26
+ return;
27
+ watching = false;
28
+ state.watches -= 1;
29
+ // TUI/form authority is watch-scoped. ACP closes each prompt's watch,
30
+ // but its host-issued route belongs to the retained connection binding.
31
+ if (state.lifetime === "watch")
32
+ attachment.close();
33
+ };
34
+ }
35
+ const commands = new AsyncLocalStorage();
36
+ function isAttached(command) {
37
+ return (command.attachment.sessionId === command.sessionId &&
38
+ (attachments.get(command.attachment)?.watches ?? 0) > 0);
39
+ }
40
+ /** Authority lasts for one command, not model work that might wake later. */
41
+ export function runLocalUiCommand(attachment, sessionId, body) {
42
+ const command = { attachment, sessionId, available: true };
43
+ return commands.run(command, async () => {
44
+ let result;
45
+ try {
46
+ result = await body();
47
+ }
48
+ catch (cause) {
49
+ command.available = false;
50
+ await command.complete?.(false);
51
+ throw cause;
52
+ }
53
+ command.available = false;
54
+ return command.complete === undefined
55
+ ? result
56
+ : command.complete(isAttached(command));
57
+ });
58
+ }
59
+ /** Consume host authority before registering the post-commit continuation. */
60
+ export function consumeLocalUiCommand(sessionId) {
61
+ const command = commands.getStore();
62
+ if (command === undefined ||
63
+ !command.available ||
64
+ command.sessionId !== sessionId ||
65
+ !isAttached(command))
66
+ return undefined;
67
+ command.available = false;
68
+ return (complete) => {
69
+ command.complete = complete;
70
+ };
71
+ }
@@ -8,6 +8,7 @@ export type Output = Pick<NodeJS.WritableStream, "write">;
8
8
  export type OpenWorkerBatch = typeof openPrivateLocalBunShiitakeWorkerBatch;
9
9
  export type PrivateLocalWorkerServerIo = Readonly<{
10
10
  cwd: string;
11
+ home?: string;
11
12
  stdout?: Output | undefined;
12
13
  signals?: Signals | undefined;
13
14
  }>;
@@ -1,5 +1,7 @@
1
1
  import { logEvent } from "./log-event.js";
2
- import { fungiMcpSelectionSchema, readFungiMcpSelection, } from "./private-fungi-mcp-selection.js";
2
+ import { capturePrivateLocalConfigRoot, } from "./private-local-config-root.js";
3
+ import { readFungiMcpSelection } from "./private-fungi-mcp-selection.js";
4
+ import { fungiMcpSelectionSchema } from "./private-fungi-mcp-selection-schema.js";
3
5
  import { readFile } from "node:fs/promises";
4
6
  import process from "node:process";
5
7
  import { dirname, resolve } from "node:path";
@@ -37,7 +39,7 @@ function parseWorkers(value, filePath) {
37
39
  },
38
40
  }));
39
41
  }
40
- async function readWorkers(filePath, cwd) {
42
+ async function readWorkers(filePath, cwd, configRoot) {
41
43
  const absolutePath = resolve(cwd, filePath);
42
44
  let value;
43
45
  try {
@@ -49,11 +51,16 @@ async function readWorkers(filePath, cwd) {
49
51
  const workers = [];
50
52
  for (const { name, owner } of parseWorkers(value, absolutePath)) {
51
53
  const { fungiMcp, ...local } = owner;
52
- let configured = local;
54
+ let configured = {
55
+ ...local,
56
+ configRoot,
57
+ };
53
58
  if (fungiMcp !== undefined)
54
59
  configured = {
55
60
  ...configured,
56
- fungiMcp: readFungiMcpSelection(fungiMcp),
61
+ fungiMcp: readFungiMcpSelection(fungiMcp, {
62
+ credentialPath: configRoot.credentialPath,
63
+ }),
57
64
  };
58
65
  workers.push({ name, owner: configured });
59
66
  }
@@ -78,7 +85,8 @@ export async function runPrivateLocalWorkerServer(workersFile, listen, io, openB
78
85
  const { openPrivateLocalBunShiitakeWorkerBatch } = await import("./private-bun-shiitake-host.js");
79
86
  return openPrivateLocalBunShiitakeWorkerBatch(...args);
80
87
  }) {
81
- const workers = await readWorkers(workersFile, io.cwd);
88
+ const root = capturePrivateLocalConfigRoot(io.home);
89
+ const workers = await readWorkers(workersFile, io.cwd, root);
82
90
  const stdout = io.stdout ?? process.stdout;
83
91
  const signals = io.signals ?? process;
84
92
  const stopped = Promise.withResolvers();