@fungi.computer/truffle 0.0.0-stage → 0.2.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 (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +542 -2
  3. package/computerd-loopback.cjs +16 -0
  4. package/dist/auth-cli.d.ts +46 -0
  5. package/dist/auth-cli.js +373 -0
  6. package/dist/fungi-account-command.d.ts +18 -0
  7. package/dist/fungi-account-command.js +95 -0
  8. package/dist/index.d.ts +27 -0
  9. package/dist/index.js +23 -0
  10. package/dist/internal/image-fit-metafile.json +106 -0
  11. package/dist/internal/local-image-fit-worker.mjs +960 -0
  12. package/dist/internal/private-workerd-sandbox-worker.mjs +27487 -0
  13. package/dist/internal/sandbox-metafile.json +6726 -0
  14. package/dist/linear-auth.d.ts +4 -0
  15. package/dist/linear-auth.js +72 -0
  16. package/dist/local-archive-handoff.d.ts +12 -0
  17. package/dist/local-archive-handoff.js +21 -0
  18. package/dist/local-extensions.d.ts +143 -0
  19. package/dist/local-extensions.js +624 -0
  20. package/dist/local-image-fit.d.ts +30 -0
  21. package/dist/local-image-fit.js +105 -0
  22. package/dist/local-login.d.ts +55 -0
  23. package/dist/local-login.js +167 -0
  24. package/dist/local-models.d.ts +16 -0
  25. package/dist/local-models.js +82 -0
  26. package/dist/local-session-archives.d.ts +8 -0
  27. package/dist/local-session-archives.js +152 -0
  28. package/dist/local-skill-source.d.ts +21 -0
  29. package/dist/local-skill-source.js +433 -0
  30. package/dist/log-event.d.ts +9 -0
  31. package/dist/log-event.js +13 -0
  32. package/dist/private-bounded-response.d.ts +2 -0
  33. package/dist/private-bounded-response.js +41 -0
  34. package/dist/private-bun-shiitake-host.d.ts +11 -0
  35. package/dist/private-bun-shiitake-host.js +66 -0
  36. package/dist/private-file-credential-store.d.ts +58 -0
  37. package/dist/private-file-credential-store.js +331 -0
  38. package/dist/private-files-patch.d.ts +4 -0
  39. package/dist/private-files-patch.js +104 -0
  40. package/dist/private-fungi-account.d.ts +252 -0
  41. package/dist/private-fungi-account.js +1329 -0
  42. package/dist/private-fungi-auth-error.d.ts +5 -0
  43. package/dist/private-fungi-auth-error.js +7 -0
  44. package/dist/private-fungi-mcp-connection.d.ts +26 -0
  45. package/dist/private-fungi-mcp-connection.js +162 -0
  46. package/dist/private-fungi-mcp-endpoint.d.ts +2 -0
  47. package/dist/private-fungi-mcp-endpoint.js +15 -0
  48. package/dist/private-fungi-mcp-selection-schema.d.ts +7 -0
  49. package/dist/private-fungi-mcp-selection-schema.js +15 -0
  50. package/dist/private-fungi-mcp-selection.d.ts +14 -0
  51. package/dist/private-fungi-mcp-selection.js +49 -0
  52. package/dist/private-fungi-model-api.d.ts +6 -0
  53. package/dist/private-fungi-model-api.js +125 -0
  54. package/dist/private-fungi-model-catalog.d.ts +16 -0
  55. package/dist/private-fungi-model-catalog.js +86 -0
  56. package/dist/private-fungi-model-provider.d.ts +6 -0
  57. package/dist/private-fungi-model-provider.js +51 -0
  58. package/dist/private-fungi-pending-logouts.d.ts +34 -0
  59. package/dist/private-fungi-pending-logouts.js +135 -0
  60. package/dist/private-fungi-service-logout.d.ts +44 -0
  61. package/dist/private-fungi-service-logout.js +59 -0
  62. package/dist/private-invocation-computer.d.ts +58 -0
  63. package/dist/private-invocation-computer.js +498 -0
  64. package/dist/private-invocation-files.d.ts +30 -0
  65. package/dist/private-invocation-files.js +520 -0
  66. package/dist/private-local-acp-stdio.d.ts +17 -0
  67. package/dist/private-local-acp-stdio.js +145 -0
  68. package/dist/private-local-admission-gate.d.ts +14 -0
  69. package/dist/private-local-admission-gate.js +52 -0
  70. package/dist/private-local-agent-control.d.ts +101 -0
  71. package/dist/private-local-agent-control.js +30 -0
  72. package/dist/private-local-agent-default-model.d.ts +12 -0
  73. package/dist/private-local-agent-default-model.js +31 -0
  74. package/dist/private-local-auth-rpc.d.ts +129 -0
  75. package/dist/private-local-auth-rpc.js +396 -0
  76. package/dist/private-local-bwrap.d.ts +13 -0
  77. package/dist/private-local-bwrap.js +70 -0
  78. package/dist/private-local-client-version.d.ts +5 -0
  79. package/dist/private-local-client-version.js +43 -0
  80. package/dist/private-local-computer-attach.d.ts +49 -0
  81. package/dist/private-local-computer-attach.js +230 -0
  82. package/dist/private-local-computer-backend.d.ts +282 -0
  83. package/dist/private-local-computer-backend.js +402 -0
  84. package/dist/private-local-computer-processes.d.ts +202 -0
  85. package/dist/private-local-computer-processes.js +613 -0
  86. package/dist/private-local-config-root.d.ts +21 -0
  87. package/dist/private-local-config-root.js +19 -0
  88. package/dist/private-local-containment.d.ts +51 -0
  89. package/dist/private-local-containment.js +161 -0
  90. package/dist/private-local-default-model.d.ts +11 -0
  91. package/dist/private-local-default-model.js +45 -0
  92. package/dist/private-local-delegation.d.ts +36 -0
  93. package/dist/private-local-delegation.js +133 -0
  94. package/dist/private-local-effective-model.d.ts +27 -0
  95. package/dist/private-local-effective-model.js +17 -0
  96. package/dist/private-local-exec-environment.d.ts +16 -0
  97. package/dist/private-local-exec-environment.js +11 -0
  98. package/dist/private-local-exec-launch.d.ts +41 -0
  99. package/dist/private-local-exec-launch.js +51 -0
  100. package/dist/private-local-exec-path.d.ts +6 -0
  101. package/dist/private-local-exec-path.js +40 -0
  102. package/dist/private-local-git-exclude.d.ts +2 -0
  103. package/dist/private-local-git-exclude.js +33 -0
  104. package/dist/private-local-http-dispatch.d.ts +5 -0
  105. package/dist/private-local-http-dispatch.js +14 -0
  106. package/dist/private-local-linux-sandbox.d.ts +8 -0
  107. package/dist/private-local-linux-sandbox.js +67 -0
  108. package/dist/private-local-mail.d.ts +13 -0
  109. package/dist/private-local-mail.js +77 -0
  110. package/dist/private-local-mesh-enrollment.d.ts +34 -0
  111. package/dist/private-local-mesh-enrollment.js +188 -0
  112. package/dist/private-local-mesh-links.d.ts +21 -0
  113. package/dist/private-local-mesh-links.js +229 -0
  114. package/dist/private-local-mesh-record.d.ts +120 -0
  115. package/dist/private-local-mesh-record.js +55 -0
  116. package/dist/private-local-process-identity.d.ts +20 -0
  117. package/dist/private-local-process-identity.js +40 -0
  118. package/dist/private-local-runtime-host.d.ts +192 -0
  119. package/dist/private-local-runtime-host.js +1177 -0
  120. package/dist/private-local-sandbox-policy.d.ts +21 -0
  121. package/dist/private-local-sandbox-policy.js +121 -0
  122. package/dist/private-local-sandbox.d.ts +37 -0
  123. package/dist/private-local-sandbox.js +114 -0
  124. package/dist/private-local-seatbelt.d.ts +24 -0
  125. package/dist/private-local-seatbelt.js +68 -0
  126. package/dist/private-local-service-contract.d.ts +170 -0
  127. package/dist/private-local-service-contract.js +1 -0
  128. package/dist/private-local-service-discovery.d.ts +217 -0
  129. package/dist/private-local-service-discovery.js +322 -0
  130. package/dist/private-local-service-ids.d.ts +3 -0
  131. package/dist/private-local-service-ids.js +8 -0
  132. package/dist/private-local-service-lease.d.ts +16 -0
  133. package/dist/private-local-service-lease.js +105 -0
  134. package/dist/private-local-service-listener.d.ts +22 -0
  135. package/dist/private-local-service-listener.js +407 -0
  136. package/dist/private-local-service-log.d.ts +128 -0
  137. package/dist/private-local-service-log.js +321 -0
  138. package/dist/private-local-service-owner.d.ts +135 -0
  139. package/dist/private-local-service-owner.js +1369 -0
  140. package/dist/private-local-service-process.d.ts +1 -0
  141. package/dist/private-local-service-process.js +8 -0
  142. package/dist/private-local-service-termination.d.ts +7 -0
  143. package/dist/private-local-service-termination.js +67 -0
  144. package/dist/private-local-service.d.ts +46 -0
  145. package/dist/private-local-service.js +606 -0
  146. package/dist/private-local-session-discovery.d.ts +66 -0
  147. package/dist/private-local-session-discovery.js +278 -0
  148. package/dist/private-local-shiitake-host.d.ts +44 -0
  149. package/dist/private-local-shiitake-host.js +143 -0
  150. package/dist/private-local-startup-model.d.ts +51 -0
  151. package/dist/private-local-startup-model.js +86 -0
  152. package/dist/private-local-stop-all.d.ts +27 -0
  153. package/dist/private-local-stop-all.js +26 -0
  154. package/dist/private-local-ui-attachment.d.ts +19 -0
  155. package/dist/private-local-ui-attachment.js +71 -0
  156. package/dist/private-local-worker-server.d.ts +16 -0
  157. package/dist/private-local-worker-server.js +144 -0
  158. package/dist/private-mcp-form-contract.d.ts +186 -0
  159. package/dist/private-mcp-form-contract.js +53 -0
  160. package/dist/private-mcp-form-elicitation.d.ts +18 -0
  161. package/dist/private-mcp-form-elicitation.js +97 -0
  162. package/dist/private-mcp-form-session.d.ts +172 -0
  163. package/dist/private-mcp-form-session.js +148 -0
  164. package/dist/private-openai-claims.d.ts +30 -0
  165. package/dist/private-openai-claims.js +69 -0
  166. package/dist/private-oyster-mcp-auth.d.ts +13 -0
  167. package/dist/private-oyster-mcp-auth.js +57 -0
  168. package/dist/private-provider-rejection.d.ts +15 -0
  169. package/dist/private-provider-rejection.js +24 -0
  170. package/dist/private-sqlite-owner.d.ts +39 -0
  171. package/dist/private-sqlite-owner.js +169 -0
  172. package/dist/private-team-computers.d.ts +38 -0
  173. package/dist/private-team-computers.js +110 -0
  174. package/dist/private-workerd-sandbox.d.ts +3 -0
  175. package/dist/private-workerd-sandbox.js +347 -0
  176. package/dist/seatbelt/safe-base.sbpl +79 -0
  177. package/dist/subscription-activation.d.ts +9 -0
  178. package/dist/subscription-activation.js +62 -0
  179. package/dist/subscription-identity.d.ts +23 -0
  180. package/dist/subscription-identity.js +124 -0
  181. package/dist/subscription-turn.d.ts +17 -0
  182. package/dist/subscription-turn.js +143 -0
  183. package/dist/team-apps.d.ts +24 -0
  184. package/dist/team-apps.js +128 -0
  185. package/dist/tracker-git-credential.d.ts +15 -0
  186. package/dist/tracker-git-credential.js +87 -0
  187. package/native/bin/LICENSE.bubblewrap +502 -0
  188. package/native/bin/LICENSE.libcap +112 -0
  189. package/native/bin/LICENSE.sandbox-runtime +201 -0
  190. package/native/bin/SOURCE.bubblewrap.tar.xz +0 -0
  191. package/native/bin/build.json +4037 -0
  192. package/native/bin/bwrap-build-source/Dockerfile +48 -0
  193. package/native/bin/bwrap-build-source/README.md +96 -0
  194. package/native/bin/bwrap-build-source/build.mjs +52 -0
  195. package/native/bin/bwrap-build-source/linux-arm64.ini +14 -0
  196. package/native/bin/bwrap-build-source/pin.mjs +21 -0
  197. package/native/bin/bwrap-build-source/record.mjs +74 -0
  198. package/native/bin/bwrap-build-source/vendor/LICENSE.sandbox-runtime +201 -0
  199. package/native/bin/bwrap-build-source/vendor/README.md +23 -0
  200. package/native/bin/bwrap-build-source/vendor/seccomp-unix-block.c +280 -0
  201. package/native/bin/bwrap-build.json +44 -0
  202. package/native/bin/fungi-apply-patch +0 -0
  203. package/native/bin/fungi-bwrap +0 -0
  204. package/native/bin/fungi-bwrap.sha256 +1 -0
  205. package/native/bin/fungi-seccomp.bpf +0 -0
  206. package/native/bin/fungi-seccomp.bpf.sha256 +1 -0
  207. package/package.json +161 -3
@@ -0,0 +1,1177 @@
1
+ import { createAssistantMessageEventStream } from "@earendil-works/pi-ai/utils/event-stream";
2
+ import { observeProviderStatus } from "@fungi.computer/mule";
3
+ import { modelFailureMessage, modelFailureRetryable, } from "./private-provider-rejection.js";
4
+ import { logEvent } from "./log-event.js";
5
+ import { localArchiveHandoff } from "./local-archive-handoff.js";
6
+ import { createMcpFormSession } from "./private-mcp-form-session.js";
7
+ import { localExtensions, } from "./local-extensions.js";
8
+ import { lstat, mkdir, realpath, stat } from "node:fs/promises";
9
+ import { lstatSync, mkdirSync, realpathSync } from "node:fs";
10
+ import { basename, dirname, isAbsolute, join, relative, resolve, } from "node:path";
11
+ import { createHash } from "node:crypto";
12
+ import { z } from "zod";
13
+ import { cleanupSessionResources, } from "@earendil-works/pi-ai";
14
+ import { parseModelPreference, } from "@fungi.computer/shiitake/model-preference";
15
+ import { createAgentControlSource, } from "@fungi.computer/shiitake/agent-control";
16
+ import { defineIdFamily } from "@fungi.computer/cairn";
17
+ import { words } from "@fungi.computer/cairn/words";
18
+ import { botanicalExtensionsWithSkills } from "@fungi.computer/shiitake-extensions/roster";
19
+ import { homeExtension, homeGrant, } from "@fungi.computer/shiitake-extensions/home/host";
20
+ import { Knapsack } from "@fungi.computer/knapsack";
21
+ import { makeHost, Shiitake, ShiitakeError, } from "@fungi.computer/shiitake";
22
+ import { createPrivateModels } from "./local-models.js";
23
+ import { capturePrivateLocalConfigRoot, } from "./private-local-config-root.js";
24
+ import { acquireSubscriptionTurn } from "./subscription-turn.js";
25
+ import { listOfferedLocalModels } from "./private-local-default-model.js";
26
+ import { ensureAgentDefaultModelTable, readAgentDefaultModel, seedAgentDefaultModel, writeAgentDefaultModel, } from "./private-local-agent-default-model.js";
27
+ import { defaultLocalReasoning, resolveKnownLocalStartupChoice, resolveLocalStartupChoice, startupReasoning, validateLocalReasoning, } from "./private-local-startup-model.js";
28
+ import { assertFungiModelsEnabled } from "./private-fungi-model-api.js";
29
+ import { privateLocalEffectiveModelSchema, } from "./private-local-effective-model.js";
30
+ import { createPrivateInvocationComputer, } from "./private-invocation-computer.js";
31
+ import { createPrivateLocalComputerProcesses, } from "./private-local-computer-processes.js";
32
+ import { capturePrivateLocalExecPath, parsePrivateLocalExecPath, } from "./private-local-exec-path.js";
33
+ import { ensurePrivateLocalGitExclude } from "./private-local-git-exclude.js";
34
+ import { createLocalSandboxControls, } from "./private-local-sandbox.js";
35
+ import { localAgentFolderSchema, resolveAgentFolder, } from "./private-local-sandbox-policy.js";
36
+ import { LocalStoreSchemaError } from "./private-local-session-discovery.js";
37
+ import { acquireNodeStoreLease, nodeRuntimeHost, } from "@fungi.computer/shiitake/node";
38
+ import { createPrivateInvocationFilesPort, } from "./private-invocation-files.js";
39
+ import { createLocalSkillSource } from "./local-skill-source.js";
40
+ import { createPrivateWorkerdSandbox } from "./private-workerd-sandbox.js";
41
+ import { createPrivateFungiMcpConnection, } from "./private-fungi-mcp-connection.js";
42
+ import { createPrivateSqliteOwner, openPrivateWritableSqliteDatabase, } from "./private-sqlite-owner.js";
43
+ import { createPrivateLocalMailRoom, hasPrivateLocalPendingMail, } from "./private-local-mail.js";
44
+ import { createPrivateLocalDelegation, } from "./private-local-delegation.js";
45
+ import { createPrivateLocalAgentControl, } from "./private-local-agent-control.js";
46
+ import { subagentsExtension, subagentsGrant, } from "@fungi.computer/shiitake-extensions/subagents/host";
47
+ const initialSessionId = "main";
48
+ const maximumInstructionReadBytes = 128 * 1_024;
49
+ const maximumInstructionSnapshotBytes = 32_000;
50
+ const instructionRevisionVersion = "local-host-instructions-v3";
51
+ const authoredInstructions = "You are the local coding agent. Explain durable changes clearly. After a run fails, each new user message is a new instruction: retry or continue the failed task only when the user explicitly asks, and ask what they want when the message is ambiguous.";
52
+ const utf8 = new TextEncoder();
53
+ const localHostIds = defineIdFamily({
54
+ family: "local-host",
55
+ version: 1,
56
+ kinds: {
57
+ store: { prefix: "local_store" },
58
+ "session-create-request": { prefix: "local_session_create_request" },
59
+ },
60
+ });
61
+ function isErrnoException(error) {
62
+ return "code" in error;
63
+ }
64
+ function sidecarPathFor(databasePath) {
65
+ return `${databasePath}.owner.sqlite`;
66
+ }
67
+ function ensureLocalAgentId(database, databasePath, pathAgentId, name, createdBy, folder, requireSandbox) {
68
+ database.exec("BEGIN IMMEDIATE");
69
+ try {
70
+ database.exec(`
71
+ CREATE TABLE IF NOT EXISTS private_local_identity (
72
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
73
+ agent_id TEXT NOT NULL UNIQUE,
74
+ name TEXT,
75
+ created_by TEXT,
76
+ archived INTEGER NOT NULL DEFAULT 0 CHECK (archived IN (0, 1)),
77
+ folder TEXT NOT NULL DEFAULT '.',
78
+ require_sandbox INTEGER NOT NULL DEFAULT 0 CHECK (require_sandbox IN (0, 1))
79
+ )
80
+ `);
81
+ const columns = database
82
+ .prepare("PRAGMA table_info(private_local_identity)")
83
+ .all();
84
+ if (!["name", "created_by", "archived", "folder", "require_sandbox"].every((required) => columns.some((column) => column.name === required)))
85
+ throw new LocalStoreSchemaError(databasePath);
86
+ const row = database
87
+ .prepare("SELECT agent_id, created_by, folder FROM private_local_identity WHERE singleton = 1")
88
+ .get();
89
+ if (row !== undefined) {
90
+ const parsed = localHostIds.store.schema.safeParse(row.agent_id);
91
+ if (!parsed.success ||
92
+ (pathAgentId !== undefined && parsed.data !== pathAgentId))
93
+ throw new Error("Local Agent identity does not match its store");
94
+ if (createdBy !== undefined && row.created_by !== createdBy)
95
+ throw new Error("Local Agent creation identity conflicts with its store");
96
+ const storedFolder = localAgentFolderSchema.parse(row.folder);
97
+ if (folder !== undefined &&
98
+ localAgentFolderSchema.parse(folder) !== storedFolder)
99
+ throw new Error("Local Agent folder conflicts with its store");
100
+ if (requireSandbox !== undefined)
101
+ database
102
+ .prepare("UPDATE private_local_identity SET require_sandbox = ? WHERE singleton = 1")
103
+ .run(requireSandbox ? 1 : 0);
104
+ database.exec("COMMIT");
105
+ return parsed.data;
106
+ }
107
+ const agentId = pathAgentId ?? localHostIds.store.generate();
108
+ database
109
+ .prepare("INSERT INTO private_local_identity (singleton, agent_id, name, created_by, folder, require_sandbox) VALUES (1, ?, ?, ?, ?, ?)")
110
+ // An unnamed Agent gets a Cairn candidate word name so it never shows its id.
111
+ .run(agentId, name ?? words(), createdBy ?? null, localAgentFolderSchema.parse(folder ?? "."), requireSandbox === true ? 1 : 0);
112
+ database.exec("COMMIT");
113
+ return agentId;
114
+ }
115
+ catch (cause) {
116
+ if (database.isTransaction) {
117
+ try {
118
+ database.exec("ROLLBACK");
119
+ }
120
+ catch (rollbackCause) {
121
+ throw new AggregateError([cause, rollbackCause], "Local transaction and rollback failed", { cause: rollbackCause });
122
+ }
123
+ }
124
+ throw cause;
125
+ }
126
+ }
127
+ /** Local-owned id for a new Session creation request. */
128
+ export function createPrivateLocalSessionRequestId() {
129
+ return localHostIds["session-create-request"].generate();
130
+ }
131
+ const localCreateReceiptFile = "local-agent-create-receipts.sqlite";
132
+ const localCreateReceiptTable = "local_agent_create_receipts";
133
+ const maximumReceiptIdAttempts = 3;
134
+ const receiptBusyTimeoutMs = 5_000;
135
+ async function ensurePrivateLocalDirectory(path) {
136
+ try {
137
+ await mkdir(path);
138
+ }
139
+ catch (cause) {
140
+ if (!(cause instanceof Error) ||
141
+ !isErrnoException(cause) ||
142
+ cause.code !== "EEXIST")
143
+ throw cause;
144
+ }
145
+ const metadata = await lstat(path);
146
+ const canonical = await realpath(path);
147
+ if (!metadata.isDirectory() ||
148
+ metadata.isSymbolicLink() ||
149
+ canonical !== path)
150
+ throw new Error("Local Agent create directory is not project owned");
151
+ }
152
+ async function assertPrivateLocalReceiptFile(path, kind) {
153
+ // A concurrent create's commit deletes the rollback journal at any point
154
+ // between these reads; a file that is gone has nothing left to check.
155
+ try {
156
+ const metadata = await lstat(path);
157
+ // SQLite can unlink the journal between the kernel's lookup and getattr.
158
+ // Its now-unlinked regular inode is gone, just as with ENOENT below.
159
+ if (kind === "journal" && metadata.isFile() && metadata.nlink === 0)
160
+ return;
161
+ if (!metadata.isFile() ||
162
+ metadata.isSymbolicLink() ||
163
+ metadata.nlink !== 1 ||
164
+ (await realpath(path)) !== path)
165
+ throw new Error("Local Agent create receipt is not a unique project file");
166
+ }
167
+ catch (cause) {
168
+ if (cause instanceof Error &&
169
+ isErrnoException(cause) &&
170
+ cause.code === "ENOENT")
171
+ return;
172
+ throw cause;
173
+ }
174
+ }
175
+ /** Resolve an Agent create request through its durable Local receipt. */
176
+ export async function resolvePrivateLocalCreateStore(root, requestId) {
177
+ if (requestId.length === 0 || requestId.length > 256)
178
+ throw new Error("Local Agent create request id is invalid");
179
+ await ensurePrivateLocalGitExclude(root);
180
+ const fungiDirectory = join(root, ".fungi");
181
+ const agentsDirectory = join(fungiDirectory, "agents");
182
+ await ensurePrivateLocalDirectory(fungiDirectory);
183
+ await ensurePrivateLocalDirectory(agentsDirectory);
184
+ const receiptPath = join(agentsDirectory, localCreateReceiptFile);
185
+ await assertPrivateLocalReceiptFile(receiptPath, "receipt");
186
+ await assertPrivateLocalReceiptFile(`${receiptPath}-journal`, "journal");
187
+ const database = openPrivateWritableSqliteDatabase(receiptPath, receiptBusyTimeoutMs);
188
+ try {
189
+ database.exec(`PRAGMA busy_timeout = ${String(receiptBusyTimeoutMs)}`);
190
+ database.exec("BEGIN IMMEDIATE");
191
+ database.exec(`
192
+ CREATE TABLE IF NOT EXISTS ${localCreateReceiptTable} (
193
+ request_id TEXT PRIMARY KEY NOT NULL,
194
+ agent_id TEXT NOT NULL UNIQUE,
195
+ name TEXT
196
+ ) WITHOUT ROWID
197
+ `);
198
+ const existing = database
199
+ .prepare(`SELECT agent_id FROM ${localCreateReceiptTable} WHERE request_id = ?`)
200
+ .get(requestId);
201
+ if (existing !== undefined) {
202
+ const parsed = localHostIds.store.schema.safeParse(existing.agent_id);
203
+ if (!parsed.success)
204
+ throw new Error("Local Agent create receipt is invalid");
205
+ const agentId = parsed.data;
206
+ database.exec("COMMIT");
207
+ await ensurePrivateLocalDirectory(join(agentsDirectory, agentId));
208
+ return {
209
+ agentId,
210
+ databasePath: join(agentsDirectory, agentId, "agent.sqlite"),
211
+ };
212
+ }
213
+ let agentId;
214
+ const insert = database.prepare(`INSERT OR IGNORE INTO ${localCreateReceiptTable} (request_id, agent_id) VALUES (?, ?)`);
215
+ for (let attempt = 0; attempt < maximumReceiptIdAttempts; attempt += 1) {
216
+ const candidate = localHostIds.store.generate();
217
+ const candidateDirectory = join(agentsDirectory, candidate);
218
+ try {
219
+ // Keep the SQLite write transaction synchronous. Awaiting filesystem
220
+ // work here lets a concurrent same-process retry block the event loop
221
+ // in BEGIN IMMEDIATE, preventing this mkdir continuation from running.
222
+ mkdirSync(candidateDirectory);
223
+ }
224
+ catch (cause) {
225
+ if (cause instanceof Error &&
226
+ isErrnoException(cause) &&
227
+ cause.code === "EEXIST")
228
+ continue;
229
+ throw cause;
230
+ }
231
+ const metadata = lstatSync(candidateDirectory);
232
+ if (!metadata.isDirectory() ||
233
+ metadata.isSymbolicLink() ||
234
+ realpathSync(candidateDirectory) !== candidateDirectory)
235
+ throw new Error("Local Agent create directory is not project owned");
236
+ if (insert.run(requestId, candidate).changes > 0) {
237
+ agentId = candidate;
238
+ break;
239
+ }
240
+ }
241
+ if (agentId === undefined)
242
+ throw new Error("Could not allocate a unique Local Agent identity");
243
+ database.exec("COMMIT");
244
+ return {
245
+ agentId,
246
+ databasePath: join(agentsDirectory, agentId, "agent.sqlite"),
247
+ };
248
+ }
249
+ catch (cause) {
250
+ if (database.isTransaction) {
251
+ try {
252
+ database.exec("ROLLBACK");
253
+ }
254
+ catch (rollbackCause) {
255
+ throw new AggregateError([cause, rollbackCause], "Local transaction and rollback failed", { cause: rollbackCause });
256
+ }
257
+ }
258
+ throw cause;
259
+ }
260
+ finally {
261
+ database.close();
262
+ }
263
+ }
264
+ async function localDatabasePaths(root, requested, createRequestId) {
265
+ if (requested !== undefined && createRequestId !== undefined)
266
+ throw new Error("Local Agent database and create request are mutually exclusive");
267
+ if (requested !== undefined) {
268
+ const existing = await realpath(resolve(root, requested));
269
+ const metadata = await stat(existing);
270
+ if (!metadata.isFile())
271
+ throw new Error(`Local Agent database is not a file: ${requested}`);
272
+ const parent = await realpath(dirname(existing));
273
+ const databasePath = join(parent, basename(existing));
274
+ const parsedAgentId = localHostIds.store.parse(basename(parent));
275
+ const agentId = parsedAgentId.ok ? parsedAgentId.value : undefined;
276
+ return { agentId, databasePath, sidecarPath: sidecarPathFor(databasePath) };
277
+ }
278
+ const createStore = createRequestId === undefined
279
+ ? undefined
280
+ : await resolvePrivateLocalCreateStore(root, createRequestId);
281
+ const createDatabasePath = createStore?.databasePath;
282
+ const generatedAgentId = createStore?.agentId ?? localHostIds.store.generate();
283
+ const selectedParent = createDatabasePath === undefined
284
+ ? join(root, ".fungi", "agents", generatedAgentId)
285
+ : dirname(createDatabasePath);
286
+ let parent;
287
+ if (createRequestId === undefined) {
288
+ await mkdir(selectedParent, { recursive: true });
289
+ parent = await realpath(selectedParent);
290
+ }
291
+ else {
292
+ for (const path of [
293
+ join(root, ".fungi"),
294
+ join(root, ".fungi", "agents"),
295
+ selectedParent,
296
+ ]) {
297
+ try {
298
+ await mkdir(path);
299
+ }
300
+ catch (cause) {
301
+ if (!(cause instanceof Error) ||
302
+ !isErrnoException(cause) ||
303
+ cause.code !== "EEXIST")
304
+ throw cause;
305
+ }
306
+ const metadata = await lstat(path);
307
+ const canonical = await realpath(path);
308
+ if (!metadata.isDirectory() ||
309
+ metadata.isSymbolicLink() ||
310
+ canonical !== path)
311
+ throw new Error("Local Agent create directory is not project owned");
312
+ }
313
+ parent = await realpath(selectedParent);
314
+ const withinProject = relative(root, parent);
315
+ if (isAbsolute(withinProject) ||
316
+ withinProject === ".." ||
317
+ withinProject.startsWith("../"))
318
+ throw new Error("Local Agent create directory is outside this project");
319
+ }
320
+ const databasePath = createDatabasePath ?? join(parent, "agent.sqlite");
321
+ if (createRequestId !== undefined) {
322
+ for (const path of [databasePath, sidecarPathFor(databasePath)]) {
323
+ let metadata;
324
+ try {
325
+ metadata = await lstat(path);
326
+ }
327
+ catch (cause) {
328
+ if (!(cause instanceof Error) ||
329
+ !isErrnoException(cause) ||
330
+ cause.code !== "ENOENT")
331
+ throw cause;
332
+ continue;
333
+ }
334
+ if (!metadata.isFile() ||
335
+ metadata.isSymbolicLink() ||
336
+ metadata.nlink !== 1)
337
+ throw new Error("Local Agent create store is not a unique project file");
338
+ if ((await realpath(path)) !== path)
339
+ throw new Error("Local Agent create store is not a unique project file");
340
+ }
341
+ }
342
+ return {
343
+ agentId: generatedAgentId,
344
+ databasePath,
345
+ sidecarPath: sidecarPathFor(databasePath),
346
+ };
347
+ }
348
+ function frame(parts) {
349
+ const total = parts.reduce((size, part) => size + 4 + part.byteLength, 0);
350
+ const framed = new Uint8Array(total);
351
+ const view = new DataView(framed.buffer);
352
+ let offset = 0;
353
+ for (const part of parts) {
354
+ view.setUint32(offset, part.byteLength);
355
+ offset += 4;
356
+ framed.set(part, offset);
357
+ offset += part.byteLength;
358
+ }
359
+ return framed;
360
+ }
361
+ async function instructionRevision(authored, agents) {
362
+ const digest = await crypto.subtle.digest("SHA-256", frame([
363
+ utf8.encode(instructionRevisionVersion),
364
+ utf8.encode(authored),
365
+ Uint8Array.of(agents === undefined ? 0 : 1),
366
+ utf8.encode(agents ?? ""),
367
+ ]));
368
+ return `local-instructions-${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
369
+ }
370
+ /** Build the fixed source read by Shiitake once for each local run. */
371
+ export function createPrivateLocalInstructionSource(port) {
372
+ return Object.freeze({
373
+ load: async ({ sessionId, signal }) => {
374
+ signal.throwIfAborted();
375
+ let agents;
376
+ try {
377
+ const page = await port.read({ path: "AGENTS.md", offset: 0, limit: maximumInstructionReadBytes }, {
378
+ executionId: `local-instructions/${sessionId}`,
379
+ deadlineAt: null,
380
+ signal,
381
+ });
382
+ if ("image" in page)
383
+ throw new Error("Root AGENTS.md is an image, not instructions");
384
+ if (page.truncated)
385
+ throw new Error("Root AGENTS.md exceeds the 128 KiB instruction bound");
386
+ agents = page.content;
387
+ }
388
+ catch (cause) {
389
+ if (cause instanceof Error &&
390
+ isErrnoException(cause) &&
391
+ cause.code === "ENOENT") {
392
+ agents = undefined;
393
+ }
394
+ else
395
+ throw cause;
396
+ }
397
+ signal.throwIfAborted();
398
+ const text = agents === undefined
399
+ ? authoredInstructions
400
+ : `${authoredInstructions}\n\n${agents}`;
401
+ if (utf8.encode(text).byteLength > maximumInstructionSnapshotBytes)
402
+ throw new Error(`Local instructions exceed Shiitake's ${String(maximumInstructionSnapshotBytes)}-byte snapshot bound`);
403
+ return Object.freeze({
404
+ revision: await instructionRevision(authoredInstructions, agents),
405
+ text,
406
+ });
407
+ },
408
+ });
409
+ }
410
+ export function createPrivateLocalModelSource(models, providerId, modelId, providerCacheKey, selectedOwnerReasoning, defaults) {
411
+ const model = modelId === undefined || providerId === undefined
412
+ ? undefined
413
+ : models.getModel(providerId, modelId);
414
+ if (providerId !== undefined && providerId !== "fungi" && model === undefined)
415
+ throw new ShiitakeError("model", "rejected", `Local model is not registered: ${providerId}/${modelId ?? "select with --model"}`);
416
+ if (model !== undefined)
417
+ validateLocalReasoning(model, selectedOwnerReasoning);
418
+ const ownerReasoning = selectedOwnerReasoning ??
419
+ (model === undefined ? undefined : defaultLocalReasoning(model));
420
+ const agentPreference = () => defaults?.agent?.() ??
421
+ (providerId === undefined
422
+ ? null
423
+ : {
424
+ providerId,
425
+ modelId: modelId ?? "@fungi/luna",
426
+ });
427
+ const prepareSelection = async (preference, signal) => {
428
+ const startup = await resolveLocalStartupChoice({
429
+ models,
430
+ session: preference,
431
+ agent: agentPreference(),
432
+ signal,
433
+ });
434
+ if (startup === null)
435
+ throw new ShiitakeError("model", "rejected", "No signed-in model provider is available. Run /login, then choose a model with /model.");
436
+ const choice = startup.preference;
437
+ const selectedProvider = choice.providerId;
438
+ if (selectedProvider === "fungi") {
439
+ try {
440
+ assertFungiModelsEnabled();
441
+ }
442
+ catch (cause) {
443
+ throw new ShiitakeError("model", "rejected", "Fungi models are paused. Try again later.", undefined, cause);
444
+ }
445
+ await assertAuthenticated("fungi", signal);
446
+ // Catalog facts choose a model; every stream rechecks live Hub authority.
447
+ // A failed catalog refresh must not hide a dispatch refusal from the transcript.
448
+ if (models.getModels("fungi").length === 0) {
449
+ const result = await models.refresh({ providers: ["fungi"], signal });
450
+ if (result.aborted)
451
+ throw new Error("Fungi model catalog refresh was cancelled");
452
+ const failure = result.errors.get("fungi");
453
+ if (failure !== undefined)
454
+ throw new ShiitakeError("model", "rejected", "Fungi models are unavailable. Sign in with /login or choose another provider with /model.", undefined, failure);
455
+ }
456
+ }
457
+ const selectedId = choice.modelId;
458
+ const selected = models.getModel(selectedProvider, selectedId);
459
+ if (selected === undefined)
460
+ throw new ShiitakeError("model", "rejected", `Local model is not registered: ${selectedProvider}/${selectedId}`);
461
+ const reasoning = startupReasoning(selected, choice, ownerReasoning);
462
+ validateLocalReasoning(selected, reasoning);
463
+ return { selected, reasoning };
464
+ };
465
+ const assertAuthenticated = async (provider, signal) => {
466
+ const message = `${provider} is not signed in. Run /login or choose another model with /model.`;
467
+ try {
468
+ if ((await models.checkAuth(provider, { signal })) === undefined)
469
+ throw new ShiitakeError("model", "rejected", message);
470
+ signal.throwIfAborted();
471
+ }
472
+ catch (cause) {
473
+ signal.throwIfAborted();
474
+ throw new ShiitakeError("model", "rejected", message, undefined, cause);
475
+ }
476
+ };
477
+ return Object.freeze({
478
+ assertReady: async (preference, signal) => {
479
+ const { selected } = await prepareSelection(preference, signal);
480
+ await assertAuthenticated(selected.provider, signal);
481
+ },
482
+ effective: async (preference, signal) => {
483
+ signal.throwIfAborted();
484
+ let selection;
485
+ try {
486
+ selection = await prepareSelection(preference, signal);
487
+ }
488
+ catch (cause) {
489
+ if (signal.aborted ||
490
+ (preference?.providerId ?? providerId) !== "fungi")
491
+ throw cause;
492
+ logEvent("fungi-model.status", cause);
493
+ const source = cause instanceof ShiitakeError
494
+ ? cause.message
495
+ : "Fungi is unreachable or unavailable. Retry or sign in again.";
496
+ return privateLocalEffectiveModelSchema.parse({
497
+ agentName: "Shiitake",
498
+ providerId: "fungi",
499
+ providerName: "Fungi",
500
+ modelId: preference?.modelId ?? modelId ?? "@fungi/luna",
501
+ modelName: "Fungi",
502
+ credential: { kind: "missing", source },
503
+ label: `Shiitake · Fungi · ${source}`,
504
+ });
505
+ }
506
+ const { selected, reasoning } = selection;
507
+ const provider = models.getProvider(selected.provider);
508
+ if (provider === undefined)
509
+ throw new Error("Local model provider is not registered");
510
+ const auth = await models.checkAuth(provider.id, { signal });
511
+ signal.throwIfAborted();
512
+ const source = auth === undefined
513
+ ? "Sign in required"
514
+ : auth.source === undefined || auth.source === "OAuth"
515
+ ? auth.type === "oauth"
516
+ ? `${provider.name} login`
517
+ : "API key"
518
+ : auth.source;
519
+ return privateLocalEffectiveModelSchema.parse({
520
+ agentName: "Shiitake",
521
+ providerId: provider.id,
522
+ providerName: provider.name,
523
+ modelId: selected.id,
524
+ modelName: selected.name,
525
+ credential: { kind: auth?.type ?? "missing", source },
526
+ reasoning,
527
+ label: `Shiitake · ${selected.name} (${reasoning ?? "no reasoning"}) · ${source}`,
528
+ });
529
+ },
530
+ // Observe the same explicit layers against last-known registry facts.
531
+ // Catalog presence alone cannot identify the signed-in fallback: Pi's
532
+ // availability queries check auth. Leave that choice unknown here.
533
+ describe: ({ signal, preference }) => {
534
+ signal.throwIfAborted();
535
+ const choice = resolveKnownLocalStartupChoice({
536
+ session: preference,
537
+ agent: agentPreference(),
538
+ });
539
+ const selected = choice === null
540
+ ? undefined
541
+ : models.getModel(choice.preference.providerId, choice.preference.modelId);
542
+ const contextWindow = selected?.contextWindow;
543
+ return Promise.resolve({
544
+ contextWindow: contextWindow !== undefined && contextWindow > 0
545
+ ? contextWindow
546
+ : null,
547
+ });
548
+ },
549
+ acquire: async ({ signal, preference }) => {
550
+ signal.throwIfAborted();
551
+ const { selected, reasoning: selectedReasoning } = await prepareSelection(preference, signal);
552
+ await assertAuthenticated(selected.provider, signal);
553
+ const turn = await acquireSubscriptionTurn(models, selected.provider, signal);
554
+ // One acquired door owns at most its latest failure. Identity ties the
555
+ // description to the exact returned message, not to a provider name or
556
+ // prose that any SDK can supply.
557
+ let modelFailure;
558
+ return Promise.resolve(Object.freeze({
559
+ model: selected,
560
+ retryable: (message) => !signal.aborted &&
561
+ message.stopReason === "error" &&
562
+ modelFailure?.message === message &&
563
+ modelFailure.retryable,
564
+ failureReason: (message) => modelFailure?.message === message ? modelFailure.reason : undefined,
565
+ stream: (selected, context, options) => {
566
+ modelFailure = undefined;
567
+ const streamOptions = { ...options };
568
+ const reasoning = options?.reasoning ?? selectedReasoning;
569
+ if (reasoning !== undefined)
570
+ streamOptions.reasoning = reasoning;
571
+ const provider = observeProviderStatus(streamOptions);
572
+ const events = createAssistantMessageEventStream();
573
+ const failure = (usage, aborted, upstream) => {
574
+ const evidence = {
575
+ refusal: turn.refusal(),
576
+ upstream,
577
+ rejection: provider.rejection(),
578
+ };
579
+ const message = modelFailureMessage(selected.provider, evidence);
580
+ const result = {
581
+ role: "assistant",
582
+ content: [],
583
+ api: selected.api,
584
+ provider: selected.provider,
585
+ model: selected.id,
586
+ usage,
587
+ stopReason: aborted ? "aborted" : "error",
588
+ timestamp: Date.now(),
589
+ errorMessage: aborted ? "Run cancelled" : message,
590
+ };
591
+ if (!aborted)
592
+ modelFailure = {
593
+ message: result,
594
+ reason: message,
595
+ retryable: modelFailureRetryable(evidence),
596
+ };
597
+ return result;
598
+ };
599
+ void (async () => {
600
+ try {
601
+ const upstream = turn.models.streamSimple(selected, context, {
602
+ ...streamOptions,
603
+ fetch: provider.fetch,
604
+ onProviderStreamEvent: provider.onProviderStreamEvent,
605
+ sessionId: providerCacheKey,
606
+ transport: options?.transport ?? "auto",
607
+ });
608
+ for await (const event of upstream) {
609
+ if (event.type === "error") {
610
+ const error = failure(event.error.usage, event.reason === "aborted", event.error);
611
+ events.push({ ...event, error });
612
+ events.end(error);
613
+ return;
614
+ }
615
+ events.push(event);
616
+ }
617
+ events.end(await upstream.result());
618
+ }
619
+ catch {
620
+ const aborted = (options?.signal ?? signal).aborted;
621
+ const error = failure({
622
+ input: 0,
623
+ output: 0,
624
+ cacheRead: 0,
625
+ cacheWrite: 0,
626
+ totalTokens: 0,
627
+ cost: {
628
+ input: 0,
629
+ output: 0,
630
+ cacheRead: 0,
631
+ cacheWrite: 0,
632
+ total: 0,
633
+ },
634
+ }, aborted);
635
+ events.push({
636
+ type: "error",
637
+ reason: aborted ? "aborted" : "error",
638
+ error,
639
+ });
640
+ events.end(error);
641
+ }
642
+ })();
643
+ return events;
644
+ },
645
+ release: turn.release,
646
+ }));
647
+ },
648
+ });
649
+ }
650
+ /** One local Computer source, resolving its PATH and process scope per Session. */
651
+ function createPrivateLocalComputerSource(root, port, sessionPath, processes, home, sandbox) {
652
+ const id = "local-computer";
653
+ return {
654
+ id,
655
+ acquire: async (context) => {
656
+ const sessionId = context.session?.sessionId;
657
+ if (sessionId === undefined)
658
+ throw new Error("Local Computer requires a Session");
659
+ const path = sessionPath(sessionId) ??
660
+ capturePrivateLocalExecPath(process.env.PATH ?? "/usr/bin:/bin", process.cwd());
661
+ const selected = await createPrivateInvocationComputer(root, path, processes.scope(sessionId), home, sandbox);
662
+ return Knapsack.computer({
663
+ id,
664
+ name: "local",
665
+ namespace: ["computers"],
666
+ files: {
667
+ port,
668
+ pathInstructions: localPathInstructions(root),
669
+ },
670
+ computer: selected,
671
+ }).acquire(context);
672
+ },
673
+ };
674
+ }
675
+ function localPathInstructions(root) {
676
+ return `Files root: ${root}. Relative paths resolve from this root; absolute paths must remain inside it.`;
677
+ }
678
+ /** Assemble the existing Files, Computer, and workerd owners once per host. */
679
+ export async function createPrivateLocalTools(invocationRoot, fungiMcp, agentControl, sessionPath,
680
+ /** This Agent's store; its owner-enabled installed extensions join the roster. */
681
+ storeDirectory,
682
+ /**
683
+ * This Agent's sub-agent record: its Session-bound Agent-control tools and
684
+ * the view's facts. Present when it can direct other Agents.
685
+ */
686
+ subagents, configuration, configRoot = capturePrivateLocalConfigRoot(), sandbox) {
687
+ const forms = createMcpFormSession();
688
+ const root = await realpath(resolve(invocationRoot));
689
+ const port = await createPrivateInvocationFilesPort(root, undefined, sandbox);
690
+ const processes = createPrivateLocalComputerProcesses(root, configRoot.home);
691
+ const computerSources = sessionPath === undefined
692
+ ? [
693
+ Knapsack.computer({
694
+ id: "local-computer",
695
+ name: "local",
696
+ namespace: ["computers"],
697
+ files: {
698
+ port,
699
+ pathInstructions: localPathInstructions(root),
700
+ },
701
+ computer: await createPrivateInvocationComputer(root, undefined, processes.scope("invocation"), configRoot.home, sandbox?.launch),
702
+ }),
703
+ ]
704
+ : [
705
+ createPrivateLocalComputerSource(root, port, sessionPath, processes, configRoot.home, sandbox?.launch),
706
+ ];
707
+ const installed = configuration?.installed ??
708
+ (storeDirectory === undefined
709
+ ? { roster: [], grants: [], skills: [], diagnostics: [] }
710
+ : await localExtensions({ home: configRoot.home }).agent(storeDirectory, root));
711
+ function configure(installed, connection, clientPath) {
712
+ const base = botanicalExtensionsWithSkills(createLocalSkillSource({
713
+ cwd: root,
714
+ home: configRoot.home,
715
+ extensions: installed.skills,
716
+ }));
717
+ for (const diagnostic of installed.diagnostics)
718
+ logEvent("local-extension-load", diagnostic.error);
719
+ const archive = storeDirectory === undefined
720
+ ? { roster: [], grants: [] }
721
+ : localArchiveHandoff(storeDirectory, installed.grants);
722
+ const extensions = {
723
+ roster: [
724
+ ...base.roster,
725
+ forms.extension,
726
+ homeExtension({
727
+ computers: {
728
+ read: () => [
729
+ { name: "local", attachmentId: "local-computer", port },
730
+ ],
731
+ },
732
+ }),
733
+ ...(subagents === undefined
734
+ ? []
735
+ : [subagentsExtension({ source: subagents.source })]),
736
+ ...archive.roster,
737
+ ...installed.roster,
738
+ ],
739
+ grants: [
740
+ ...base.grants,
741
+ {
742
+ id: forms.extension.manifest.id,
743
+ capabilities: forms.extension.manifest.requests,
744
+ },
745
+ homeGrant,
746
+ ...(subagents === undefined ? [] : [subagentsGrant]),
747
+ ...archive.grants,
748
+ ...installed.grants,
749
+ ],
750
+ };
751
+ return Object.freeze({
752
+ extensions,
753
+ tools: Object.freeze({
754
+ execution: Object.freeze({ timeoutMs: 900_000 }),
755
+ sources: Object.freeze([
756
+ ...(agentControl === undefined
757
+ ? []
758
+ : [subagents?.control ?? createAgentControlSource(agentControl)]),
759
+ ...(clientPath === undefined
760
+ ? computerSources
761
+ : [
762
+ createPrivateLocalComputerSource(root, port, () => clientPath, processes, configRoot.home, sandbox?.launch),
763
+ ]),
764
+ ...(connection === undefined
765
+ ? []
766
+ : [createPrivateFungiMcpConnection(connection)]),
767
+ ]),
768
+ sandbox: createPrivateWorkerdSandbox(),
769
+ }),
770
+ });
771
+ }
772
+ const initial = configure(installed, configuration === undefined ? fungiMcp : configuration.fungiMcp, configuration?.clientPath);
773
+ if (initial.tools === undefined)
774
+ throw new Error("Local tools are unavailable");
775
+ return {
776
+ ...initial,
777
+ tools: initial.tools,
778
+ port,
779
+ processes,
780
+ configure,
781
+ bindSession: forms.bindSession,
782
+ presentApproval: forms.presentApproval,
783
+ };
784
+ }
785
+ function localRuntimeOwner() {
786
+ const host = nodeRuntimeHost((cause) => {
787
+ logEvent("shiitake_reconciliation_failed", cause);
788
+ });
789
+ let pendingWork;
790
+ return Object.freeze({
791
+ ...host,
792
+ runtime: Object.freeze({
793
+ bind: (input) => {
794
+ pendingWork = input.pendingWork;
795
+ return host.runtime.bind(input);
796
+ },
797
+ }),
798
+ settled: async () => {
799
+ if (pendingWork === undefined)
800
+ return false;
801
+ const work = await pendingWork();
802
+ return !work.pending && work.nextWakeAt === null;
803
+ },
804
+ });
805
+ }
806
+ /** Event-loop adaptation for the one local Shiitake owner; it has no queue policy. */
807
+ export function createPrivateLocalRuntimeHost() {
808
+ return localRuntimeOwner().runtime;
809
+ }
810
+ function asLocalFailure(cause, message) {
811
+ return cause instanceof Error ? cause : new Error(message, { cause });
812
+ }
813
+ function aggregateLocalFailures(failures, message) {
814
+ if (failures.length === 1) {
815
+ const [failure] = failures;
816
+ if (failure === undefined)
817
+ throw new Error("Missing local close failure");
818
+ return failure;
819
+ }
820
+ return new AggregateError(failures, message);
821
+ }
822
+ async function captureCloseFailure(close, message) {
823
+ try {
824
+ await close();
825
+ return undefined;
826
+ }
827
+ catch (cause) {
828
+ return asLocalFailure(cause, message);
829
+ }
830
+ }
831
+ /**
832
+ * Close the fixed local owner in dependency order without surrendering its
833
+ * sidecar before runtime drain and main-database close have succeeded.
834
+ *
835
+ * This source-local seam is intentionally not re-exported by the package; the
836
+ * focused tests use it to falsify injected failure ordering.
837
+ */
838
+ export async function closePrivateLocalResources(resources, initiatingFailure) {
839
+ const failures = initiatingFailure === undefined ? [] : [initiatingFailure];
840
+ if (resources.agent !== undefined) {
841
+ const failure = await captureCloseFailure(resources.agent.close, "Local Agent close failed");
842
+ if (failure !== undefined)
843
+ failures.push(failure);
844
+ }
845
+ // No tool call runs after the Agent closed: stop its background processes.
846
+ if (resources.processes !== undefined) {
847
+ const failure = await captureCloseFailure(resources.processes.close, "Local Computer processes close failed");
848
+ if (failure !== undefined)
849
+ failures.push(failure);
850
+ }
851
+ if (resources.sandbox !== undefined) {
852
+ const failure = await captureCloseFailure(() => Promise.resolve().then(() => resources.sandbox?.close()), "Local sandbox close failed");
853
+ if (failure !== undefined)
854
+ failures.push(failure);
855
+ }
856
+ const runtimeFailure = await captureCloseFailure(resources.runtime.close, "Local runtime close failed");
857
+ if (runtimeFailure !== undefined) {
858
+ failures.push(runtimeFailure);
859
+ throw aggregateLocalFailures(failures, "Local host close failed");
860
+ }
861
+ const providerCacheKey = resources.providerCacheKey;
862
+ if (providerCacheKey !== undefined) {
863
+ const modelFailure = await captureCloseFailure(() => Promise.resolve().then(() => {
864
+ cleanupSessionResources(providerCacheKey);
865
+ }), "Local provider resources close failed");
866
+ if (modelFailure !== undefined)
867
+ failures.push(modelFailure);
868
+ }
869
+ if (resources.database !== undefined) {
870
+ const databaseFailure = await captureCloseFailure(() => Promise.resolve().then(() => resources.database?.close()), "Local Agent database close failed");
871
+ if (databaseFailure !== undefined) {
872
+ failures.push(databaseFailure);
873
+ throw aggregateLocalFailures(failures, "Local host close failed");
874
+ }
875
+ }
876
+ if (resources.lease !== undefined) {
877
+ const leaseFailure = await captureCloseFailure(() => Promise.resolve().then(() => resources.lease?.release()), "Local ownership sidecar release failed");
878
+ if (leaseFailure !== undefined)
879
+ failures.push(leaseFailure);
880
+ }
881
+ if (failures.length > 0)
882
+ throw aggregateLocalFailures(failures, "Local host close failed");
883
+ }
884
+ /** Adapt Core's store refusal without reclassifying other opening failures. */
885
+ function localHostOpeningFailure(cause, databasePath) {
886
+ if (cause instanceof ShiitakeError &&
887
+ cause.boundary === "store" &&
888
+ cause.cause === "incompatible")
889
+ return new LocalStoreSchemaError(databasePath, { cause });
890
+ return asLocalFailure(cause, "Local host opening failed");
891
+ }
892
+ /**
893
+ * Open one private file-backed local Shiitake host and its fixed Session.
894
+ *
895
+ * The returned close owns agent, main database, and sidecar release in that
896
+ * order. It is the only lifecycle the current terminal caller needs to learn.
897
+ */
898
+ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
899
+ const configRoot = input.configRoot ?? capturePrivateLocalConfigRoot();
900
+ const models = suppliedModels ?? createPrivateModels(configRoot.credentialPath);
901
+ const root = await realpath(resolve(input.invocationRoot));
902
+ await ensurePrivateLocalGitExclude(root);
903
+ const selectedProviderId = input.providerId;
904
+ const selectedModelId = input.modelId;
905
+ if (selectedProviderId !== undefined && selectedProviderId !== "fungi") {
906
+ const model = selectedModelId === undefined
907
+ ? undefined
908
+ : models.getModel(selectedProviderId, selectedModelId);
909
+ if (model === undefined)
910
+ throw new ShiitakeError("model", "rejected", `Local model is not registered: ${selectedProviderId}/${selectedModelId ?? "select with --model"}`);
911
+ validateLocalReasoning(model, input.reasoning);
912
+ }
913
+ const paths = await localDatabasePaths(root, input.databasePath, input.createRequestId);
914
+ const runtime = localRuntimeOwner();
915
+ let lease;
916
+ let providerCacheKey;
917
+ let database;
918
+ let agent;
919
+ let processes;
920
+ let sandbox;
921
+ const sessionPaths = new Map();
922
+ let usesServiceConfiguration = input.configuration !== undefined;
923
+ try {
924
+ lease = acquireNodeStoreLease(paths.sidecarPath);
925
+ // Claim only after the lease: a rejected opener must not clean up its live owner.
926
+ providerCacheKey = createHash("sha256")
927
+ .update(JSON.stringify(["fungi-local", paths.databasePath, initialSessionId]))
928
+ .digest("hex");
929
+ database = openPrivateWritableSqliteDatabase(paths.databasePath, 0);
930
+ database.exec("PRAGMA foreign_keys = ON");
931
+ const agentId = ensureLocalAgentId(database, paths.databasePath, paths.agentId, input.name, input.createdBy, input.folder, input.requireSandbox);
932
+ const sandboxDatabase = database;
933
+ const readSandboxRequirement = () => z
934
+ .union([z.literal(0), z.literal(1)])
935
+ .parse(sandboxDatabase
936
+ .prepare("SELECT require_sandbox FROM private_local_identity WHERE singleton = 1")
937
+ .get()?.require_sandbox) === 1;
938
+ const selectedFolder = resolveAgentFolder(root, localAgentFolderSchema.parse(database
939
+ .prepare("SELECT folder FROM private_local_identity WHERE singleton = 1")
940
+ .get()?.folder));
941
+ const sandboxOwner = createLocalSandboxControls({
942
+ project: root,
943
+ folder: selectedFolder.folder,
944
+ required: readSandboxRequirement,
945
+ });
946
+ sandbox = sandboxOwner;
947
+ // Both constructors below can resume stored commands before returning.
948
+ // Admission must precede tools, configuration hooks and Agent construction.
949
+ const name = z
950
+ .string()
951
+ .nullable()
952
+ .parse(database
953
+ .prepare("SELECT name FROM private_local_identity WHERE singleton = 1")
954
+ .get()?.name) ?? agentId;
955
+ const modelDatabase = database;
956
+ ensureAgentDefaultModelTable(modelDatabase);
957
+ let initialModel = selectedProviderId === undefined || selectedModelId === undefined
958
+ ? null
959
+ : parseModelPreference({
960
+ providerId: selectedProviderId,
961
+ modelId: selectedModelId,
962
+ });
963
+ if (initialModel !== null && input.reasoning !== undefined)
964
+ initialModel = parseModelPreference({
965
+ ...initialModel,
966
+ reasoning: input.reasoning,
967
+ });
968
+ seedAgentDefaultModel(modelDatabase, initialModel);
969
+ const readAgentModel = () => readAgentDefaultModel(modelDatabase);
970
+ const store = createPrivateSqliteOwner(database);
971
+ let acceptMail;
972
+ let readSettlement;
973
+ if (input.mail !== undefined && input.agentControl === undefined)
974
+ throw new Error("Local delegation requires the project Agent directory");
975
+ const delegation = input.mail === undefined || input.agentControl === undefined
976
+ ? undefined
977
+ : createPrivateLocalDelegation({
978
+ agentId,
979
+ storage: store,
980
+ directory: input.agentControl,
981
+ routing: input.mail,
982
+ meshAdmission: input.meshAdmission === undefined
983
+ ? undefined
984
+ : async (recipientAgentId) => input.meshAdmission?.(agentId, recipientAgentId),
985
+ accept: (mail, meta, reply) => {
986
+ if (acceptMail === undefined)
987
+ return Promise.reject(new Error("Local mail ingress is unavailable"));
988
+ return acceptMail(mail, meta, reply);
989
+ },
990
+ });
991
+ let projectDirectory;
992
+ if (input.agentControl !== undefined) {
993
+ projectDirectory = {
994
+ ...input.agentControl,
995
+ models: (signal) => listOfferedLocalModels(models, signal),
996
+ };
997
+ if (delegation !== undefined)
998
+ projectDirectory = {
999
+ ...projectDirectory,
1000
+ send: delegation.send,
1001
+ recall: delegation.recall,
1002
+ };
1003
+ }
1004
+ const subagents = projectDirectory === undefined
1005
+ ? undefined
1006
+ : createPrivateLocalAgentControl({
1007
+ agentId,
1008
+ directory: projectDirectory,
1009
+ storage: store,
1010
+ });
1011
+ const localTools = await createPrivateLocalTools(selectedFolder.absolute, input.fungiMcp, subagents?.directory, (sessionId) => sessionPaths.get(sessionId), dirname(paths.databasePath), subagents, await input.configuration?.(dirname(paths.databasePath)), configRoot, sandboxOwner);
1012
+ processes = localTools.processes;
1013
+ const sessionProcesses = processes;
1014
+ const modelSource = createPrivateLocalModelSource(models, selectedProviderId, selectedModelId, providerCacheKey, input.reasoning, { agent: readAgentModel });
1015
+ const hostOptions = {
1016
+ store,
1017
+ runtime: runtime.runtime,
1018
+ extensions: localTools.extensions,
1019
+ model: modelSource,
1020
+ instructions: createPrivateLocalInstructionSource(localTools.port),
1021
+ tools: localTools.tools,
1022
+ };
1023
+ if (input.mail === undefined) {
1024
+ agent = await Shiitake.make(hostOptions);
1025
+ }
1026
+ else {
1027
+ const mailRoom = createPrivateLocalMailRoom(database, {
1028
+ deliver: input.mail.deliver,
1029
+ scheduleWake: runtime.scheduleWake,
1030
+ });
1031
+ const host = await makeHost(hostOptions, mailRoom);
1032
+ agent = host.agent;
1033
+ acceptMail = host.acceptMail;
1034
+ readSettlement = host.readSettlement;
1035
+ }
1036
+ const sessionOwner = agent;
1037
+ const setAgentModel = async (input, signal) => {
1038
+ const preference = parseModelPreference(input);
1039
+ if (preference === null)
1040
+ throw new ShiitakeError("model", "invalid", "An Agent model is required");
1041
+ await modelSource.assertReady(preference, signal);
1042
+ signal.throwIfAborted();
1043
+ writeAgentDefaultModel(modelDatabase, preference);
1044
+ };
1045
+ // /model in a chat sets this Agent's default and its thinking level; a
1046
+ // Session override is cleared so the running Session follows the default.
1047
+ const getSession = (sessionId, attachment) => {
1048
+ const session = localTools.bindSession(sessionId, sessionOwner.session(sessionId), attachment);
1049
+ return {
1050
+ ...session,
1051
+ modelPreference: {
1052
+ read: async () => (await session.modelPreference.read()) ?? readAgentModel(),
1053
+ set: async (input) => {
1054
+ const preference = parseModelPreference(input);
1055
+ if (preference !== null)
1056
+ await setAgentModel(preference, new AbortController().signal);
1057
+ await session.modelPreference.set(null);
1058
+ },
1059
+ },
1060
+ };
1061
+ };
1062
+ const session = getSession(initialSessionId);
1063
+ let closing;
1064
+ return Object.freeze({
1065
+ agentId,
1066
+ name,
1067
+ databasePath: paths.databasePath,
1068
+ session,
1069
+ getSession,
1070
+ presentApproval: localTools.presentApproval,
1071
+ setModel: setAgentModel,
1072
+ setRequireSandbox: (required) => {
1073
+ sandboxDatabase
1074
+ .prepare("UPDATE private_local_identity SET require_sandbox = ? WHERE singleton = 1")
1075
+ .run(required ? 1 : 0);
1076
+ sandboxOwner.assertReady();
1077
+ },
1078
+ sandboxStatus: sandboxOwner.status,
1079
+ setArchived: (archived) => {
1080
+ modelDatabase
1081
+ .prepare("UPDATE private_local_identity SET archived = ? WHERE singleton = 1")
1082
+ .run(archived ? 1 : 0);
1083
+ },
1084
+ assertModelReady: async (sessionId, signal) => {
1085
+ const preference = await sessionOwner
1086
+ .session(sessionId)
1087
+ .modelPreference.read();
1088
+ await modelSource.assertReady(preference, signal);
1089
+ },
1090
+ bindSessionPath: (sessionId, path) => {
1091
+ const selected = parsePrivateLocalExecPath(path);
1092
+ if (usesServiceConfiguration)
1093
+ return true;
1094
+ const previous = sessionPaths.get(sessionId);
1095
+ if (previous !== undefined && previous !== selected)
1096
+ return false;
1097
+ sessionPaths.set(sessionId, selected);
1098
+ return true;
1099
+ },
1100
+ effectiveModel: async (sessionId, signal) => {
1101
+ signal.throwIfAborted();
1102
+ const preference = await sessionOwner
1103
+ .session(sessionId)
1104
+ .modelPreference.read();
1105
+ return modelSource.effective(preference, signal);
1106
+ },
1107
+ listSessions: (after) => sessionOwner.listSessions(after),
1108
+ createSession: (requestId) => sessionOwner.createSession(requestId),
1109
+ deleteSession: async (sessionId) => {
1110
+ const deleted = await sessionOwner.deleteSession(sessionId);
1111
+ // The Session ended: its background processes and logs go with it.
1112
+ await sessionProcesses.disposeScope(sessionId);
1113
+ return deleted;
1114
+ },
1115
+ extensions: sessionOwner.extensions,
1116
+ prepareReload: async (configuration) => {
1117
+ const prepared = await sessionOwner.prepareConfiguration(localTools.configure(configuration.installed, configuration.fungiMcp, configuration.clientPath));
1118
+ return {
1119
+ discard: prepared.discard,
1120
+ commit: () => {
1121
+ prepared.commit();
1122
+ usesServiceConfiguration = true;
1123
+ },
1124
+ };
1125
+ },
1126
+ acceptMail: (mail) => delegation === undefined
1127
+ ? Promise.reject(new Error("Local mail ingress is unavailable"))
1128
+ : delegation.receive(mail),
1129
+ pendingDelegation: () => delegation === undefined
1130
+ ? Promise.resolve(undefined)
1131
+ : delegation.outstanding(),
1132
+ readSettlement: (sessionId, commandId) => readSettlement === undefined
1133
+ ? Promise.reject(new Error("Local settlement reads are unavailable"))
1134
+ : readSettlement(sessionId, commandId),
1135
+ idle: async () => {
1136
+ if (sessionProcesses.running())
1137
+ return false;
1138
+ if (!(await runtime.settled()))
1139
+ return false;
1140
+ // Delegated work still owed to this Agent is not read here: that read
1141
+ // reopens each child, and the child's reply mail reopens this Agent.
1142
+ let after;
1143
+ do {
1144
+ const page = await sessionOwner.listSessions(after);
1145
+ for (const row of page.sessions)
1146
+ if (row.state === "running" || row.state === "needs_you")
1147
+ return false;
1148
+ after = page.next;
1149
+ } while (after !== undefined);
1150
+ // Reconciliation enqueues replies before advancing its settled cursor.
1151
+ // Observe mail last: an earlier empty outbox can become retryable mail
1152
+ // while we await settlement or Session reads, especially during stop
1153
+ // when delivery cannot reopen a parent. The owner rechecks admissions
1154
+ // and uses after this observation before forgetting the Agent.
1155
+ return (input.mail === undefined ||
1156
+ !(await hasPrivateLocalPendingMail(modelDatabase)));
1157
+ },
1158
+ close: () => {
1159
+ closing ??= closePrivateLocalResources({
1160
+ agent,
1161
+ processes,
1162
+ sandbox,
1163
+ runtime,
1164
+ database,
1165
+ lease,
1166
+ providerCacheKey,
1167
+ });
1168
+ return closing;
1169
+ },
1170
+ });
1171
+ }
1172
+ catch (cause) {
1173
+ const openingFailure = localHostOpeningFailure(cause, paths.databasePath);
1174
+ await closePrivateLocalResources({ agent, processes, sandbox, runtime, database, lease, providerCacheKey }, openingFailure);
1175
+ throw openingFailure;
1176
+ }
1177
+ }