@fungi.computer/truffle 0.1.1 → 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 (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,45 +1,54 @@
1
1
  import { createAssistantMessageEventStream } from "@earendil-works/pi-ai/utils/event-stream";
2
- import { isContextOverflow } from "@earendil-works/pi-ai/utils/overflow";
3
- import { isRetryableAssistantError } from "@earendil-works/pi-ai/utils/retry";
2
+ import { observeProviderStatus } from "@fungi.computer/mule";
3
+ import { modelFailureMessage, modelFailureRetryable, } from "./private-provider-rejection.js";
4
4
  import { logEvent } from "./log-event.js";
5
- import { localExtensions } from "./local-extensions.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";
6
8
  import { lstat, mkdir, realpath, stat } from "node:fs/promises";
7
9
  import { lstatSync, mkdirSync, realpathSync } from "node:fs";
8
10
  import { basename, dirname, isAbsolute, join, relative, resolve, } from "node:path";
9
- import { DatabaseSync } from "node:sqlite";
10
11
  import { createHash } from "node:crypto";
11
- import { cleanupSessionResources, clampThinkingLevel, getSupportedThinkingLevels, } from "@earendil-works/pi-ai";
12
- import { parseModelPreference, parseModelPreferenceJson, resolveModelPreference, } from "@fungi.computer/shiitake/model-preference";
12
+ import { z } from "zod";
13
+ import { cleanupSessionResources, } from "@earendil-works/pi-ai";
14
+ import { parseModelPreference, } from "@fungi.computer/shiitake/model-preference";
13
15
  import { createAgentControlSource, } from "@fungi.computer/shiitake/agent-control";
14
16
  import { defineIdFamily } from "@fungi.computer/cairn";
17
+ import { words } from "@fungi.computer/cairn/words";
15
18
  import { botanicalExtensionsWithSkills } from "@fungi.computer/shiitake-extensions/roster";
16
19
  import { homeExtension, homeGrant, } from "@fungi.computer/shiitake-extensions/home/host";
17
20
  import { Knapsack } from "@fungi.computer/knapsack";
18
21
  import { makeHost, Shiitake, ShiitakeError, } from "@fungi.computer/shiitake";
19
22
  import { createPrivateModels } from "./local-models.js";
20
- import { defaultLocalModel, listOfferedLocalModels, } from "./private-local-default-model.js";
21
- import { createLocalModelSettings } from "./private-local-model-settings.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";
22
28
  import { assertFungiModelsEnabled } from "./private-fungi-model-api.js";
23
29
  import { privateLocalEffectiveModelSchema, } from "./private-local-effective-model.js";
24
- import { createPrivateInvocationComputer } from "./private-invocation-computer.js";
30
+ import { createPrivateInvocationComputer, } from "./private-invocation-computer.js";
25
31
  import { createPrivateLocalComputerProcesses, } from "./private-local-computer-processes.js";
26
32
  import { capturePrivateLocalExecPath, parsePrivateLocalExecPath, } from "./private-local-exec-path.js";
27
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";
28
37
  import { acquireNodeStoreLease, nodeRuntimeHost, } from "@fungi.computer/shiitake/node";
29
- import { createPrivateInvocationFilesPort } from "./private-invocation-files.js";
38
+ import { createPrivateInvocationFilesPort, } from "./private-invocation-files.js";
30
39
  import { createLocalSkillSource } from "./local-skill-source.js";
31
40
  import { createPrivateWorkerdSandbox } from "./private-workerd-sandbox.js";
32
41
  import { createPrivateFungiMcpConnection, } from "./private-fungi-mcp-connection.js";
33
- import { createPrivateSqliteOwner } from "./private-sqlite-owner.js";
34
- import { createPrivateLocalMailRoom } from "./private-local-mail.js";
42
+ import { createPrivateSqliteOwner, openPrivateWritableSqliteDatabase, } from "./private-sqlite-owner.js";
43
+ import { createPrivateLocalMailRoom, hasPrivateLocalPendingMail, } from "./private-local-mail.js";
35
44
  import { createPrivateLocalDelegation, } from "./private-local-delegation.js";
36
- import { createPrivateLocalSubagents } from "./private-local-subagents.js";
45
+ import { createPrivateLocalAgentControl, } from "./private-local-agent-control.js";
37
46
  import { subagentsExtension, subagentsGrant, } from "@fungi.computer/shiitake-extensions/subagents/host";
38
47
  const initialSessionId = "main";
39
48
  const maximumInstructionReadBytes = 128 * 1_024;
40
49
  const maximumInstructionSnapshotBytes = 32_000;
41
- const instructionRevisionVersion = "local-host-instructions-v2";
42
- const authoredInstructions = "You are the local coding agent. Explain durable changes clearly. Discover available operations through forage.search and inspect their input with forage.describe; call only the exact paths they return. Extension prompt sections describe the Session's tools and controls. After a run fails, each new user message is a new instruction. Retry or continue the previous task only when the user explicitly asks; if the new message is ambiguous, ask what they want before acting.";
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.";
43
52
  const utf8 = new TextEncoder();
44
53
  const localHostIds = defineIdFamily({
45
54
  family: "local-host",
@@ -55,30 +64,51 @@ function isErrnoException(error) {
55
64
  function sidecarPathFor(databasePath) {
56
65
  return `${databasePath}.owner.sqlite`;
57
66
  }
58
- function ensureLocalAgentId(database, pathAgentId) {
67
+ function ensureLocalAgentId(database, databasePath, pathAgentId, name, createdBy, folder, requireSandbox) {
59
68
  database.exec("BEGIN IMMEDIATE");
60
69
  try {
61
70
  database.exec(`
62
71
  CREATE TABLE IF NOT EXISTS private_local_identity (
63
72
  singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
64
- agent_id TEXT NOT NULL UNIQUE
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))
65
79
  )
66
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);
67
86
  const row = database
68
- .prepare("SELECT agent_id FROM private_local_identity WHERE singleton = 1")
87
+ .prepare("SELECT agent_id, created_by, folder FROM private_local_identity WHERE singleton = 1")
69
88
  .get();
70
89
  if (row !== undefined) {
71
90
  const parsed = localHostIds.store.schema.safeParse(row.agent_id);
72
91
  if (!parsed.success ||
73
92
  (pathAgentId !== undefined && parsed.data !== pathAgentId))
74
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);
75
104
  database.exec("COMMIT");
76
105
  return parsed.data;
77
106
  }
78
107
  const agentId = pathAgentId ?? localHostIds.store.generate();
79
108
  database
80
- .prepare("INSERT INTO private_local_identity (singleton, agent_id) VALUES (1, ?)")
81
- .run(agentId);
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);
82
112
  database.exec("COMMIT");
83
113
  return agentId;
84
114
  }
@@ -119,11 +149,15 @@ async function ensurePrivateLocalDirectory(path) {
119
149
  canonical !== path)
120
150
  throw new Error("Local Agent create directory is not project owned");
121
151
  }
122
- async function assertPrivateLocalReceiptFile(path) {
152
+ async function assertPrivateLocalReceiptFile(path, kind) {
123
153
  // A concurrent create's commit deletes the rollback journal at any point
124
154
  // between these reads; a file that is gone has nothing left to check.
125
155
  try {
126
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;
127
161
  if (!metadata.isFile() ||
128
162
  metadata.isSymbolicLink() ||
129
163
  metadata.nlink !== 1 ||
@@ -148,18 +182,17 @@ export async function resolvePrivateLocalCreateStore(root, requestId) {
148
182
  await ensurePrivateLocalDirectory(fungiDirectory);
149
183
  await ensurePrivateLocalDirectory(agentsDirectory);
150
184
  const receiptPath = join(agentsDirectory, localCreateReceiptFile);
151
- await assertPrivateLocalReceiptFile(receiptPath);
152
- await assertPrivateLocalReceiptFile(`${receiptPath}-journal`);
153
- const database = new DatabaseSync(receiptPath, {
154
- timeout: receiptBusyTimeoutMs,
155
- });
185
+ await assertPrivateLocalReceiptFile(receiptPath, "receipt");
186
+ await assertPrivateLocalReceiptFile(`${receiptPath}-journal`, "journal");
187
+ const database = openPrivateWritableSqliteDatabase(receiptPath, receiptBusyTimeoutMs);
156
188
  try {
157
189
  database.exec(`PRAGMA busy_timeout = ${String(receiptBusyTimeoutMs)}`);
158
190
  database.exec("BEGIN IMMEDIATE");
159
191
  database.exec(`
160
192
  CREATE TABLE IF NOT EXISTS ${localCreateReceiptTable} (
161
193
  request_id TEXT PRIMARY KEY NOT NULL,
162
- agent_id TEXT NOT NULL UNIQUE
194
+ agent_id TEXT NOT NULL UNIQUE,
195
+ name TEXT
163
196
  ) WITHOUT ROWID
164
197
  `);
165
198
  const existing = database
@@ -346,6 +379,8 @@ export function createPrivateLocalInstructionSource(port) {
346
379
  deadlineAt: null,
347
380
  signal,
348
381
  });
382
+ if ("image" in page)
383
+ throw new Error("Root AGENTS.md is an image, not instructions");
349
384
  if (page.truncated)
350
385
  throw new Error("Root AGENTS.md exceeds the 128 KiB instruction bound");
351
386
  agents = page.content;
@@ -372,26 +407,6 @@ export function createPrivateLocalInstructionSource(port) {
372
407
  },
373
408
  });
374
409
  }
375
- /** Validate the selected registered model before resolving credentials. */
376
- function validateLocalReasoning(model, requested) {
377
- if (requested === undefined)
378
- return;
379
- const supported = getSupportedThinkingLevels(model).filter((level) => level !== "off");
380
- if (!supported.includes(requested))
381
- throw new Error("Reasoning level " +
382
- requested +
383
- " is not supported by " +
384
- model.provider +
385
- "/" +
386
- model.id +
387
- "; supported levels: " +
388
- (supported.join(", ") || "none"));
389
- }
390
- /** Local's default is medium, clamped through Pi's model capabilities. */
391
- function defaultLocalReasoning(model) {
392
- const level = clampThinkingLevel(model, "medium");
393
- return level === "off" ? undefined : level;
394
- }
395
410
  export function createPrivateLocalModelSource(models, providerId, modelId, providerCacheKey, selectedOwnerReasoning, defaults) {
396
411
  const model = modelId === undefined || providerId === undefined
397
412
  ? undefined
@@ -402,25 +417,23 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
402
417
  validateLocalReasoning(model, selectedOwnerReasoning);
403
418
  const ownerReasoning = selectedOwnerReasoning ??
404
419
  (model === undefined ? undefined : defaultLocalReasoning(model));
405
- const select = async (preference, signal) => {
406
- const agent = defaults?.agent?.() ??
407
- (providerId === undefined
408
- ? null
409
- : {
410
- providerId,
411
- modelId: modelId ?? "@fungi/luna",
412
- });
413
- const explicit = resolveModelPreference({
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,
414
430
  session: preference,
415
- agent,
416
- manual: preference === null && agent === null ? await defaults?.manual() : null,
431
+ agent: agentPreference(),
432
+ signal,
417
433
  });
418
- const choice = resolveModelPreference({
419
- session: explicit,
420
- available: explicit === null ? await defaultLocalModel(models, signal) : null,
421
- });
422
- if (choice === null)
434
+ if (startup === null)
423
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;
424
437
  const selectedProvider = choice.providerId;
425
438
  if (selectedProvider === "fungi") {
426
439
  try {
@@ -445,11 +458,7 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
445
458
  const selected = models.getModel(selectedProvider, selectedId);
446
459
  if (selected === undefined)
447
460
  throw new ShiitakeError("model", "rejected", `Local model is not registered: ${selectedProvider}/${selectedId}`);
448
- const reasoning = choice.reasoning ??
449
- (ownerReasoning !== undefined &&
450
- getSupportedThinkingLevels(selected).includes(ownerReasoning)
451
- ? ownerReasoning
452
- : defaultLocalReasoning(selected));
461
+ const reasoning = startupReasoning(selected, choice, ownerReasoning);
453
462
  validateLocalReasoning(selected, reasoning);
454
463
  return { selected, reasoning };
455
464
  };
@@ -467,14 +476,14 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
467
476
  };
468
477
  return Object.freeze({
469
478
  assertReady: async (preference, signal) => {
470
- const { selected } = await select(preference, signal);
479
+ const { selected } = await prepareSelection(preference, signal);
471
480
  await assertAuthenticated(selected.provider, signal);
472
481
  },
473
482
  effective: async (preference, signal) => {
474
483
  signal.throwIfAborted();
475
484
  let selection;
476
485
  try {
477
- selection = await select(preference, signal);
486
+ selection = await prepareSelection(preference, signal);
478
487
  }
479
488
  catch (cause) {
480
489
  if (signal.aborted ||
@@ -514,38 +523,61 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
514
523
  modelId: selected.id,
515
524
  modelName: selected.name,
516
525
  credential: { kind: auth?.type ?? "missing", source },
526
+ reasoning,
517
527
  label: `Shiitake · ${selected.name} (${reasoning ?? "no reasoning"}) · ${source}`,
518
528
  });
519
529
  },
520
- // The window comes from the same registry entry a run would acquire.
521
- describe: async ({ signal, preference }) => {
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 }) => {
522
534
  signal.throwIfAborted();
523
- const { selected } = await select(preference, signal);
524
- return {
525
- contextWindow: selected.contextWindow > 0 ? selected.contextWindow : null,
526
- };
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
+ });
527
548
  },
528
549
  acquire: async ({ signal, preference }) => {
529
550
  signal.throwIfAborted();
530
- const { selected, reasoning: selectedReasoning } = await select(preference, signal);
551
+ const { selected, reasoning: selectedReasoning } = await prepareSelection(preference, signal);
531
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;
532
558
  return Promise.resolve(Object.freeze({
533
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,
534
565
  stream: (selected, context, options) => {
566
+ modelFailure = undefined;
535
567
  const streamOptions = { ...options };
536
568
  const reasoning = options?.reasoning ?? selectedReasoning;
537
569
  if (reasoning !== undefined)
538
570
  streamOptions.reasoning = reasoning;
571
+ const provider = observeProviderStatus(streamOptions);
539
572
  const events = createAssistantMessageEventStream();
540
573
  const failure = (usage, aborted, upstream) => {
541
- let message = `${selected.provider} rejected the model request. Check /login or choose another model with /model.`;
542
- if (upstream !== undefined) {
543
- if (isContextOverflow(upstream))
544
- message = `${selected.provider} request exceeds the context window. Compact the Session or choose another model.`;
545
- else if (isRetryableAssistantError(upstream))
546
- message = `${selected.provider} service unavailable. Try again shortly.`;
547
- }
548
- return {
574
+ const evidence = {
575
+ refusal: turn.refusal(),
576
+ upstream,
577
+ rejection: provider.rejection(),
578
+ };
579
+ const message = modelFailureMessage(selected.provider, evidence);
580
+ const result = {
549
581
  role: "assistant",
550
582
  content: [],
551
583
  api: selected.api,
@@ -556,11 +588,20 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
556
588
  timestamp: Date.now(),
557
589
  errorMessage: aborted ? "Run cancelled" : message,
558
590
  };
591
+ if (!aborted)
592
+ modelFailure = {
593
+ message: result,
594
+ reason: message,
595
+ retryable: modelFailureRetryable(evidence),
596
+ };
597
+ return result;
559
598
  };
560
599
  void (async () => {
561
600
  try {
562
- const upstream = models.streamSimple(selected, context, {
601
+ const upstream = turn.models.streamSimple(selected, context, {
563
602
  ...streamOptions,
603
+ fetch: provider.fetch,
604
+ onProviderStreamEvent: provider.onProviderStreamEvent,
564
605
  sessionId: providerCacheKey,
565
606
  transport: options?.transport ?? "auto",
566
607
  });
@@ -601,13 +642,13 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
601
642
  })();
602
643
  return events;
603
644
  },
604
- release: () => Promise.resolve(),
645
+ release: turn.release,
605
646
  }));
606
647
  },
607
648
  });
608
649
  }
609
650
  /** One local Computer source, resolving its PATH and process scope per Session. */
610
- function createPrivateLocalComputerSource(root, port, sessionPath, processes) {
651
+ function createPrivateLocalComputerSource(root, port, sessionPath, processes, home, sandbox) {
611
652
  const id = "local-computer";
612
653
  return {
613
654
  id,
@@ -617,7 +658,7 @@ function createPrivateLocalComputerSource(root, port, sessionPath, processes) {
617
658
  throw new Error("Local Computer requires a Session");
618
659
  const path = sessionPath(sessionId) ??
619
660
  capturePrivateLocalExecPath(process.env.PATH ?? "/usr/bin:/bin", process.cwd());
620
- const selected = await createPrivateInvocationComputer(root, path, processes.scope(sessionId));
661
+ const selected = await createPrivateInvocationComputer(root, path, processes.scope(sessionId), home, sandbox);
621
662
  return Knapsack.computer({
622
663
  id,
623
664
  name: "local",
@@ -642,10 +683,11 @@ storeDirectory,
642
683
  * This Agent's sub-agent record: its Session-bound Agent-control tools and
643
684
  * the view's facts. Present when it can direct other Agents.
644
685
  */
645
- subagents) {
686
+ subagents, configuration, configRoot = capturePrivateLocalConfigRoot(), sandbox) {
687
+ const forms = createMcpFormSession();
646
688
  const root = await realpath(resolve(invocationRoot));
647
- const port = await createPrivateInvocationFilesPort(root);
648
- const processes = createPrivateLocalComputerProcesses(root);
689
+ const port = await createPrivateInvocationFilesPort(root, undefined, sandbox);
690
+ const processes = createPrivateLocalComputerProcesses(root, configRoot.home);
649
691
  const computerSources = sessionPath === undefined
650
692
  ? [
651
693
  Knapsack.computer({
@@ -656,60 +698,110 @@ subagents) {
656
698
  port,
657
699
  pathInstructions: localPathInstructions(root),
658
700
  },
659
- computer: await createPrivateInvocationComputer(root, undefined, processes.scope("invocation")),
701
+ computer: await createPrivateInvocationComputer(root, undefined, processes.scope("invocation"), configRoot.home, sandbox?.launch),
660
702
  }),
661
703
  ]
662
- : [createPrivateLocalComputerSource(root, port, sessionPath, processes)];
663
- const base = botanicalExtensionsWithSkills(createLocalSkillSource({ cwd: root }));
664
- const installed = storeDirectory === undefined
665
- ? { roster: [], grants: [], diagnostics: [] }
666
- : await localExtensions().agent(storeDirectory, root);
667
- for (const diagnostic of installed.diagnostics)
668
- logEvent("local-extension-load", diagnostic.error);
669
- const extensions = {
670
- roster: [
671
- ...base.roster,
672
- homeExtension({
673
- computers: {
674
- read: () => [{ name: "local", attachmentId: "local-computer", port }],
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,
675
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(),
676
769
  }),
677
- ...(subagents === undefined
678
- ? []
679
- : [subagentsExtension({ source: subagents.source })]),
680
- ...installed.roster,
681
- ],
682
- grants: [
683
- ...base.grants,
684
- homeGrant,
685
- ...(subagents === undefined ? [] : [subagentsGrant]),
686
- ...installed.grants,
687
- ],
688
- };
689
- return Object.freeze({
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,
690
778
  port,
691
- extensions,
692
779
  processes,
693
- tools: Object.freeze({
694
- execution: Object.freeze({ timeoutMs: 900_000 }),
695
- sources: Object.freeze([
696
- ...(agentControl === undefined
697
- ? []
698
- : [subagents?.control ?? createAgentControlSource(agentControl)]),
699
- ...computerSources,
700
- ...(fungiMcp === undefined
701
- ? []
702
- : [createPrivateFungiMcpConnection(fungiMcp)]),
703
- ]),
704
- sandbox: createPrivateWorkerdSandbox(),
705
- }),
706
- });
780
+ configure,
781
+ bindSession: forms.bindSession,
782
+ presentApproval: forms.presentApproval,
783
+ };
707
784
  }
708
- /** Shiitake's public Node runtime host, reporting failed passes to local logs. */
709
785
  function localRuntimeOwner() {
710
- return nodeRuntimeHost((cause) => {
786
+ const host = nodeRuntimeHost((cause) => {
711
787
  logEvent("shiitake_reconciliation_failed", cause);
712
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
+ });
713
805
  }
714
806
  /** Event-loop adaptation for the one local Shiitake owner; it has no queue policy. */
715
807
  export function createPrivateLocalRuntimeHost() {
@@ -756,6 +848,11 @@ export async function closePrivateLocalResources(resources, initiatingFailure) {
756
848
  if (failure !== undefined)
757
849
  failures.push(failure);
758
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
+ }
759
856
  const runtimeFailure = await captureCloseFailure(resources.runtime.close, "Local runtime close failed");
760
857
  if (runtimeFailure !== undefined) {
761
858
  failures.push(runtimeFailure);
@@ -784,13 +881,23 @@ export async function closePrivateLocalResources(resources, initiatingFailure) {
784
881
  if (failures.length > 0)
785
882
  throw aggregateLocalFailures(failures, "Local host close failed");
786
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
+ }
787
892
  /**
788
893
  * Open one private file-backed local Shiitake host and its fixed Session.
789
894
  *
790
895
  * The returned close owns agent, main database, and sidecar release in that
791
896
  * order. It is the only lifecycle the current terminal caller needs to learn.
792
897
  */
793
- export async function openPrivateLocalRuntimeHost(input, models = createPrivateModels()) {
898
+ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
899
+ const configRoot = input.configRoot ?? capturePrivateLocalConfigRoot();
900
+ const models = suppliedModels ?? createPrivateModels(configRoot.credentialPath);
794
901
  const root = await realpath(resolve(input.invocationRoot));
795
902
  await ensurePrivateLocalGitExclude(root);
796
903
  const selectedProviderId = input.providerId;
@@ -810,18 +917,43 @@ export async function openPrivateLocalRuntimeHost(input, models = createPrivateM
810
917
  let database;
811
918
  let agent;
812
919
  let processes;
920
+ let sandbox;
813
921
  const sessionPaths = new Map();
922
+ let usesServiceConfiguration = input.configuration !== undefined;
814
923
  try {
815
924
  lease = acquireNodeStoreLease(paths.sidecarPath);
816
925
  // Claim only after the lease: a rejected opener must not clean up its live owner.
817
926
  providerCacheKey = createHash("sha256")
818
927
  .update(JSON.stringify(["fungi-local", paths.databasePath, initialSessionId]))
819
928
  .digest("hex");
820
- database = new DatabaseSync(paths.databasePath, { timeout: 0 });
929
+ database = openPrivateWritableSqliteDatabase(paths.databasePath, 0);
821
930
  database.exec("PRAGMA foreign_keys = ON");
822
- const agentId = ensureLocalAgentId(database, paths.agentId);
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;
823
955
  const modelDatabase = database;
824
- modelDatabase.exec("CREATE TABLE IF NOT EXISTS private_local_model_settings (singleton INTEGER PRIMARY KEY CHECK(singleton = 1), model TEXT NOT NULL) STRICT");
956
+ ensureAgentDefaultModelTable(modelDatabase);
825
957
  let initialModel = selectedProviderId === undefined || selectedModelId === undefined
826
958
  ? null
827
959
  : parseModelPreference({
@@ -833,16 +965,8 @@ export async function openPrivateLocalRuntimeHost(input, models = createPrivateM
833
965
  ...initialModel,
834
966
  reasoning: input.reasoning,
835
967
  });
836
- modelDatabase
837
- .prepare("INSERT OR IGNORE INTO private_local_model_settings VALUES (1, ?)")
838
- .run(JSON.stringify(initialModel));
839
- const readAgentModel = () => {
840
- const row = modelDatabase
841
- .prepare("SELECT model FROM private_local_model_settings WHERE singleton = 1")
842
- .get();
843
- return parseModelPreferenceJson(row?.model);
844
- };
845
- const modelSettings = createLocalModelSettings(input.modelSettingsPath);
968
+ seedAgentDefaultModel(modelDatabase, initialModel);
969
+ const readAgentModel = () => readAgentDefaultModel(modelDatabase);
846
970
  const store = createPrivateSqliteOwner(database);
847
971
  let acceptMail;
848
972
  let readSettlement;
@@ -855,28 +979,39 @@ export async function openPrivateLocalRuntimeHost(input, models = createPrivateM
855
979
  storage: store,
856
980
  directory: input.agentControl,
857
981
  routing: input.mail,
982
+ meshAdmission: input.meshAdmission === undefined
983
+ ? undefined
984
+ : async (recipientAgentId) => input.meshAdmission?.(agentId, recipientAgentId),
858
985
  accept: (mail, meta, reply) => {
859
986
  if (acceptMail === undefined)
860
987
  return Promise.reject(new Error("Local mail ingress is unavailable"));
861
988
  return acceptMail(mail, meta, reply);
862
989
  },
863
990
  });
864
- let agentControl;
991
+ let projectDirectory;
865
992
  if (input.agentControl !== undefined) {
866
- agentControl = {
993
+ projectDirectory = {
867
994
  ...input.agentControl,
868
995
  models: (signal) => listOfferedLocalModels(models, signal),
869
996
  };
870
997
  if (delegation !== undefined)
871
- agentControl = { ...agentControl, send: delegation.send };
998
+ projectDirectory = {
999
+ ...projectDirectory,
1000
+ send: delegation.send,
1001
+ recall: delegation.recall,
1002
+ };
872
1003
  }
873
- const subagents = agentControl === undefined
1004
+ const subagents = projectDirectory === undefined
874
1005
  ? undefined
875
- : createPrivateLocalSubagents({ database, directory: agentControl });
876
- const localTools = await createPrivateLocalTools(root, input.fungiMcp, agentControl, (sessionId) => sessionPaths.get(sessionId), dirname(paths.databasePath), subagents);
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);
877
1012
  processes = localTools.processes;
878
1013
  const sessionProcesses = processes;
879
- const modelSource = createPrivateLocalModelSource(models, selectedProviderId, selectedModelId, providerCacheKey, input.reasoning, { agent: readAgentModel, manual: modelSettings.read });
1014
+ const modelSource = createPrivateLocalModelSource(models, selectedProviderId, selectedModelId, providerCacheKey, input.reasoning, { agent: readAgentModel });
880
1015
  const hostOptions = {
881
1016
  store,
882
1017
  runtime: runtime.runtime,
@@ -899,17 +1034,27 @@ export async function openPrivateLocalRuntimeHost(input, models = createPrivateM
899
1034
  readSettlement = host.readSettlement;
900
1035
  }
901
1036
  const sessionOwner = agent;
902
- const getSession = (sessionId) => {
903
- const session = sessionOwner.session(sessionId);
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);
904
1049
  return {
905
1050
  ...session,
906
1051
  modelPreference: {
907
- read: session.modelPreference.read,
1052
+ read: async () => (await session.modelPreference.read()) ?? readAgentModel(),
908
1053
  set: async (input) => {
909
1054
  const preference = parseModelPreference(input);
910
1055
  if (preference !== null)
911
- await modelSource.assertReady(preference, new AbortController().signal);
912
- await session.modelPreference.set(preference);
1056
+ await setAgentModel(preference, new AbortController().signal);
1057
+ await session.modelPreference.set(null);
913
1058
  },
914
1059
  },
915
1060
  };
@@ -918,18 +1063,23 @@ export async function openPrivateLocalRuntimeHost(input, models = createPrivateM
918
1063
  let closing;
919
1064
  return Object.freeze({
920
1065
  agentId,
1066
+ name,
921
1067
  databasePath: paths.databasePath,
922
1068
  session,
923
1069
  getSession,
924
- setModel: async (input, signal) => {
925
- const preference = parseModelPreference(input);
926
- if (preference === null)
927
- throw new ShiitakeError("model", "invalid", "An Agent model is required");
928
- await modelSource.assertReady(preference, signal);
929
- signal.throwIfAborted();
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) => {
930
1080
  modelDatabase
931
- .prepare("UPDATE private_local_model_settings SET model = ? WHERE singleton = 1")
932
- .run(JSON.stringify(preference));
1081
+ .prepare("UPDATE private_local_identity SET archived = ? WHERE singleton = 1")
1082
+ .run(archived ? 1 : 0);
933
1083
  },
934
1084
  assertModelReady: async (sessionId, signal) => {
935
1085
  const preference = await sessionOwner
@@ -939,6 +1089,8 @@ export async function openPrivateLocalRuntimeHost(input, models = createPrivateM
939
1089
  },
940
1090
  bindSessionPath: (sessionId, path) => {
941
1091
  const selected = parsePrivateLocalExecPath(path);
1092
+ if (usesServiceConfiguration)
1093
+ return true;
942
1094
  const previous = sessionPaths.get(sessionId);
943
1095
  if (previous !== undefined && previous !== selected)
944
1096
  return false;
@@ -961,16 +1113,53 @@ export async function openPrivateLocalRuntimeHost(input, models = createPrivateM
961
1113
  return deleted;
962
1114
  },
963
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
+ },
964
1126
  acceptMail: (mail) => delegation === undefined
965
1127
  ? Promise.reject(new Error("Local mail ingress is unavailable"))
966
1128
  : delegation.receive(mail),
1129
+ pendingDelegation: () => delegation === undefined
1130
+ ? Promise.resolve(undefined)
1131
+ : delegation.outstanding(),
967
1132
  readSettlement: (sessionId, commandId) => readSettlement === undefined
968
1133
  ? Promise.reject(new Error("Local settlement reads are unavailable"))
969
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
+ },
970
1158
  close: () => {
971
1159
  closing ??= closePrivateLocalResources({
972
1160
  agent,
973
1161
  processes,
1162
+ sandbox,
974
1163
  runtime,
975
1164
  database,
976
1165
  lease,
@@ -981,8 +1170,8 @@ export async function openPrivateLocalRuntimeHost(input, models = createPrivateM
981
1170
  });
982
1171
  }
983
1172
  catch (cause) {
984
- const openingFailure = asLocalFailure(cause, "Local host opening failed");
985
- await closePrivateLocalResources({ agent, processes, runtime, database, lease, providerCacheKey }, openingFailure);
1173
+ const openingFailure = localHostOpeningFailure(cause, paths.databasePath);
1174
+ await closePrivateLocalResources({ agent, processes, sandbox, runtime, database, lease, providerCacheKey }, openingFailure);
986
1175
  throw openingFailure;
987
1176
  }
988
1177
  }