@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,606 @@
1
+ import { FungiMcpSelectionConflict } from "./private-fungi-mcp-selection.js";
2
+ import { logEvent } from "./log-event.js";
3
+ import { readBoundedResponse } from "./private-bounded-response.js";
4
+ import { LocalSandboxUnavailable, sandboxUnavailableReasonSchema, } from "./private-local-containment.js";
5
+ export { LocalSandboxUnavailable } from "./private-local-containment.js";
6
+ import { capturePrivateLocalConfigRoot } from "./private-local-config-root.js";
7
+ import { stopAllResultSchema } from "./private-local-stop-all.js";
8
+ import { LocalAgentSelectionAmbiguous, LocalSessionPathConflict, } from "./private-local-service-owner.js";
9
+ export { LocalAgentSelectionAmbiguous, LocalSessionPathConflict, } from "./private-local-service-owner.js";
10
+ import { EventSource } from "eventsource";
11
+ import { AgentArchiveRefused } from "@fungi.computer/shiitake/agent-control";
12
+ export { AgentArchiveRefused } from "@fungi.computer/shiitake/agent-control";
13
+ import { newWebSocketRpcSession } from "@fungi.computer/capnweb";
14
+ import { runPrivateLocalAuthRemote, } from "./private-local-auth-rpc.js";
15
+ import { spawn } from "node:child_process";
16
+ import { fileURLToPath } from "node:url";
17
+ import { setTimeout as delay } from "node:timers/promises";
18
+ import { readInstalledLocalClientBuildId } from "./private-local-client-version.js";
19
+ export { readInstalledLocalClientBuildId } from "./private-local-client-version.js";
20
+ export { privateLocalServiceVersion } from "./private-local-service-discovery.js";
21
+ import { createShiitakeClient, parseSessionSnapshot, } from "@fungi.computer/shiitake/client";
22
+ import { acquireServiceLock, assertNoUntrackedServiceOwner, clearAbandonedServiceOwner, modelChoicesSchema, privateLocalServiceVersion, readLiveServiceStatus, readServiceRegistration, removeOwnedServiceRecord, removeUnidentifiedServiceRecord, selectedSchema, sandboxStatusSchema, sessionDiscoverySchema, archiveRefusalSchema, archiveRequestSchema, discoverRequestSchema, sessionInspectionSchema, serviceDirectory, serviceDirectoryPath, serviceOwnerIsLive, } from "./private-local-service-discovery.js";
23
+ import { terminatePrivateLocalService } from "./private-local-service-termination.js";
24
+ import { readServiceLeaseOwner } from "./private-local-service-lease.js";
25
+ import { privateLocalEffectiveModelSchema } from "./private-local-effective-model.js";
26
+ import { LocalStoreSchemaError, projectPrivateLocalSessionDiscovery, } from "./private-local-session-discovery.js";
27
+ export { LocalStoreSchemaError } from "./private-local-session-discovery.js";
28
+ import { capturePrivateLocalExecPath, parsePrivateLocalExecPath, } from "./private-local-exec-path.js";
29
+ import { z } from "zod";
30
+ import { serviceDiagnosticsStatusSchema } from "./private-local-service-log.js";
31
+ export { runPrivateLocalWorkerServer, } from "./private-local-worker-server.js";
32
+ /** The 409 answers a select can give; each is one case of a closed union. */
33
+ const selectionConflictSchema = z.discriminatedUnion("code", [
34
+ z.strictObject({
35
+ code: z.literal("sandbox_unavailable"),
36
+ reason: sandboxUnavailableReasonSchema,
37
+ }),
38
+ z.object({ code: z.literal("session_path_conflict") }),
39
+ z.object({ code: z.literal("store_in_use") }),
40
+ z.object({
41
+ code: z.literal("store_schema_incompatible"),
42
+ databasePath: z.string().min(1),
43
+ error: z.string().min(1),
44
+ }),
45
+ z.object({
46
+ code: z.literal("agent_selection_ambiguous"),
47
+ agentIds: z.array(z.string()),
48
+ }),
49
+ ]);
50
+ /** Selection, creation and inspection share the listener's typed refusals. */
51
+ async function rejectSelectionConflict(response) {
52
+ const body = await response.json().catch((cause) => {
53
+ console.error("Local service response unavailable", cause);
54
+ return undefined;
55
+ });
56
+ const conflict = selectionConflictSchema.safeParse(body);
57
+ if (conflict.success) {
58
+ if (conflict.data.code === "store_schema_incompatible")
59
+ throw new LocalStoreSchemaError(conflict.data.databasePath);
60
+ if (conflict.data.code === "sandbox_unavailable")
61
+ throw new LocalSandboxUnavailable(conflict.data.reason);
62
+ if (conflict.data.code === "session_path_conflict")
63
+ throw new LocalSessionPathConflict();
64
+ if (conflict.data.code === "agent_selection_ambiguous")
65
+ throw new LocalAgentSelectionAmbiguous(conflict.data.agentIds);
66
+ throw new Error("This Agent store is held by another Fungi service. Stop the older service process, then retry; keep the Agent store files.");
67
+ }
68
+ // An Agent-selection answer without its Agents is a broken service, not
69
+ // an MCP connection conflict.
70
+ if (z.object({ code: z.literal("agent_selection_ambiguous") }).safeParse(body)
71
+ .success)
72
+ throw new Error("Local service sent an unreadable Agent selection");
73
+ throw new FungiMcpSelectionConflict();
74
+ }
75
+ /** The listener's body for a failure it did not expect. */
76
+ const serviceFailureSchema = z.object({
77
+ code: z.literal("internal_error"),
78
+ error: z.string(),
79
+ });
80
+ /** The service answered a selection with a failure status and no readable failure body. */
81
+ export class LocalServiceUnreadableResponse extends Error {
82
+ name = "LocalServiceUnreadableResponse";
83
+ status;
84
+ constructor(status, cause) {
85
+ super(`Local service selection failed (${String(status)}) without a readable failure body`, { cause });
86
+ this.status = status;
87
+ }
88
+ }
89
+ /**
90
+ * A newer client replaced the service this running client reached with one
91
+ * that speaks another protocol. Only restarting the window can reconnect it.
92
+ */
93
+ export class LocalServiceUpdated extends Error {
94
+ name = "LocalServiceUpdated";
95
+ constructor() {
96
+ super("The Fungi service was updated. Restart this window.");
97
+ }
98
+ }
99
+ /** Construction does not start a service or open a store. Call start/connect explicitly. */
100
+ export function createPrivateLocalServiceManager(options = {}) {
101
+ const root = capturePrivateLocalConfigRoot(options.home);
102
+ const selectedDirectory = serviceDirectoryPath(options.directory ?? root.serviceDirectory);
103
+ const reportCleanupFailure = options.reportCleanupFailure ??
104
+ ((cause) => {
105
+ process.stderr.write(`Local service cleanup failed (${cause instanceof Error ? cause.name : "NonError"}).\n`);
106
+ });
107
+ const clientPath = options.executionPath === undefined
108
+ ? capturePrivateLocalExecPath(process.env.PATH ?? "/usr/bin:/bin", process.cwd())
109
+ : parsePrivateLocalExecPath(options.executionPath);
110
+ const expectedVersion = privateLocalServiceVersion();
111
+ /**
112
+ * Set once this client has reached a service. Only a starting client
113
+ * replaces a service speaking another protocol. Same-protocol build changes
114
+ * still require the installed-client check; reaching a service is not proof
115
+ * that this window remains the installed client.
116
+ */
117
+ let reachedService = false;
118
+ function accept(service) {
119
+ reachedService = true;
120
+ return service;
121
+ }
122
+ const maximumInspectionResponseBytes = 2_097_152;
123
+ async function status(timeoutMs) {
124
+ const directory = selectedDirectory;
125
+ const registration = await readServiceRegistration(directory);
126
+ if (registration?.kind === "other-protocol" &&
127
+ (await serviceOwnerIsLive(directory)))
128
+ throw new LocalServiceUpdated();
129
+ if (registration?.kind !== "current")
130
+ return undefined;
131
+ const probe = await readLiveServiceStatus(registration.record, timeoutMs);
132
+ return probe.kind === "running" ? probe.service : undefined;
133
+ }
134
+ /**
135
+ * A loaded machine can delay a live service past the short handshake. Its
136
+ * lease proves it is alive, so wait for it outside the startup lock instead
137
+ * of spawning a second service that can only fail on that lease.
138
+ */
139
+ async function awaitBusyService(request) {
140
+ const waitMs = options.busyServiceWaitMs ?? 60_000;
141
+ const deadline = Date.now() + waitMs;
142
+ while (Date.now() < deadline) {
143
+ request.signal?.throwIfAborted();
144
+ const ready = await status(Math.min(10_000, waitMs));
145
+ if (ready) {
146
+ if (ready.version === expectedVersion)
147
+ return accept(ready);
148
+ // It answered at last, as another version: replace it under the lock.
149
+ const started = await startUnderLock(request);
150
+ if (started !== "busy")
151
+ return started;
152
+ }
153
+ await delay(250, undefined, { signal: request.signal });
154
+ }
155
+ throw new Error(`The Fungi service is running but has not answered for ${String(Math.round(waitMs / 1000))} seconds; the machine may be overloaded. Retry, or run fungi stop after its turns finish.`);
156
+ }
157
+ async function start(request = {}) {
158
+ const started = await startUnderLock(request);
159
+ return started === "busy" ? awaitBusyService(request) : started;
160
+ }
161
+ /** `fungi stop`'s work, under the startup lock that serializes it with starts. */
162
+ async function stopUnderLock(directory, expectedServiceId) {
163
+ const registration = await readServiceRegistration(directory);
164
+ if (registration === undefined)
165
+ return { kind: "not-running", recordCleaned: false };
166
+ if (registration.kind === "unidentified") {
167
+ if (await serviceOwnerIsLive(directory))
168
+ throw new Error(`A Fungi service this client cannot identify owns ${directory}. End that Fungi service process, then retry; saved Sessions are kept.`);
169
+ await removeUnidentifiedServiceRecord(directory);
170
+ return { kind: "not-running", recordCleaned: true };
171
+ }
172
+ const lifecycle = registration.kind === "current"
173
+ ? registration.record
174
+ : registration.lifecycle;
175
+ if (expectedServiceId !== undefined &&
176
+ lifecycle.serviceId !== expectedServiceId)
177
+ return { kind: "replaced" };
178
+ const owner = await readServiceLeaseOwner(directory);
179
+ if (owner === undefined) {
180
+ await removeOwnedServiceRecord(directory, lifecycle.serviceId);
181
+ return { kind: "not-running", recordCleaned: true };
182
+ }
183
+ // The file points at a claim; it cannot supply signal authority itself.
184
+ if (owner.pid !== lifecycle.pid || owner.startedAt !== lifecycle.startedAt)
185
+ throw new Error(`The Fungi lease owner in ${directory} does not match service.json; retry after the owner registers itself. No process was signalled.`);
186
+ const outcome = await terminatePrivateLocalService(directory, owner);
187
+ // A killed process cannot remove its own record or lifetime lock.
188
+ await removeOwnedServiceRecord(directory, lifecycle.serviceId);
189
+ await clearAbandonedServiceOwner(directory);
190
+ return { kind: outcome };
191
+ }
192
+ /**
193
+ * Reuse the registered service when this client can, otherwise stop it so a
194
+ * service of this version can start; undefined means start one.
195
+ */
196
+ async function reuseOrReplace(directory, registration) {
197
+ if (registration.kind === "current") {
198
+ const probe = await readLiveServiceStatus(registration.record);
199
+ // Not answering: the lifetime lease decides between busy and gone.
200
+ if (probe.kind !== "running")
201
+ return undefined;
202
+ if (probe.service.version === expectedVersion)
203
+ return probe.service;
204
+ // Replace only for the installed client. A stale window (or an
205
+ // uninstalled source run) must not stop the service; it restarts.
206
+ if (expectedVersion !== (await readInstalledLocalClientBuildId()))
207
+ throw new LocalServiceUpdated();
208
+ }
209
+ else if (registration.kind === "other-protocol" && reachedService) {
210
+ if (await serviceOwnerIsLive(directory))
211
+ throw new LocalServiceUpdated();
212
+ }
213
+ if (registration.kind === "other-protocol" &&
214
+ expectedVersion !== (await readInstalledLocalClientBuildId()))
215
+ throw new LocalServiceUpdated();
216
+ await stopUnderLock(directory);
217
+ return undefined;
218
+ }
219
+ async function startUnderLock(request) {
220
+ request.signal?.throwIfAborted();
221
+ const directory = await serviceDirectory(selectedDirectory);
222
+ const release = await acquireServiceLock(directory, "startup");
223
+ try {
224
+ const registration = await readServiceRegistration(directory);
225
+ const existing = registration === undefined
226
+ ? undefined
227
+ : await reuseOrReplace(directory, registration);
228
+ request.signal?.throwIfAborted();
229
+ if (existing)
230
+ return accept(existing);
231
+ await assertNoUntrackedServiceOwner(directory);
232
+ if (await serviceOwnerIsLive(directory))
233
+ return "busy";
234
+ const bundledCli = process.env.FUNGI_BUNDLED_CLI;
235
+ const child = spawn(options.executable ?? process.execPath, bundledCli === undefined
236
+ ? [
237
+ fileURLToPath(new URL("./private-local-service-process.js", import.meta.url)),
238
+ directory,
239
+ ]
240
+ : [bundledCli, "--internal-service", directory], {
241
+ detached: true,
242
+ stdio: "ignore",
243
+ // Service startup retains its existing inherited inputs. This is
244
+ // not a Computer exec child: only its selected host HOME is pinned.
245
+ env: { ...process.env, HOME: root.home },
246
+ });
247
+ let failure;
248
+ let exited = false;
249
+ const readFailure = () => failure;
250
+ const hasExited = () => exited;
251
+ child.once("error", (cause) => {
252
+ failure = cause;
253
+ });
254
+ child.once("exit", () => {
255
+ exited = true;
256
+ });
257
+ child.unref();
258
+ for (let attempt = 0; attempt < 100; attempt += 1) {
259
+ const childFailure = readFailure();
260
+ if (childFailure !== undefined)
261
+ throw childFailure;
262
+ const ready = await status();
263
+ if (ready) {
264
+ if (ready.version !== expectedVersion)
265
+ throw new Error(`The started Local service reports version ${ready.version}, not this client's ${expectedVersion}`);
266
+ return accept(ready);
267
+ }
268
+ if (hasExited())
269
+ throw new Error("Local service exited before its handshake");
270
+ await delay(100, undefined, { signal: request.signal });
271
+ }
272
+ child.kill("SIGTERM");
273
+ for (let attempt = 0; attempt < 20 && !hasExited(); attempt += 1)
274
+ await delay(100);
275
+ throw new Error(hasExited()
276
+ ? "Local service did not become ready and its owned child was stopped"
277
+ : `Local service did not become ready; child ${String(child.pid)} is still running`);
278
+ }
279
+ finally {
280
+ await release();
281
+ }
282
+ }
283
+ /** POST one selection to `service`; every refusal is a typed failure. */
284
+ const postSelection = async (endpoint, input, service, signal) => {
285
+ const init = {
286
+ method: "POST",
287
+ headers: { "Content-Type": "application/json" },
288
+ body: JSON.stringify({ ...input, clientPath }),
289
+ redirect: "error",
290
+ };
291
+ if (signal !== undefined)
292
+ init.signal = signal;
293
+ const response = await fetch(`${service.baseUrl}/service/${service.serviceId}/${endpoint}`, init);
294
+ if (response.status === 409)
295
+ await rejectSelectionConflict(response);
296
+ if (!response.ok) {
297
+ // The service names its own failure in a JSON body; one without it is
298
+ // a broken response, not a status to guess from.
299
+ const failure = serviceFailureSchema.safeParse(await response.json().catch((cause) => {
300
+ throw new LocalServiceUnreadableResponse(response.status, cause);
301
+ }));
302
+ if (!failure.success)
303
+ throw new LocalServiceUnreadableResponse(response.status, failure.error);
304
+ throw new Error(`Local service selection failed (${String(response.status)}): ${failure.data.error}`);
305
+ }
306
+ return selectedSchema.parse(await response.json());
307
+ };
308
+ const routeBase = (service, selected) => `${service.baseUrl}/service/${service.serviceId}${selected.routePath}`;
309
+ const openConnection = async (endpoint, input, request = {}) => {
310
+ request.signal?.throwIfAborted();
311
+ const service = await start(request);
312
+ const selected = await postSelection(endpoint, input, service, request.signal);
313
+ const firstBase = routeBase(service, selected);
314
+ /**
315
+ * Where this connection's client currently talks. A restarted service
316
+ * comes back on a new port with a new id, so the client's requests are
317
+ * redirected here rather than the client rebuilt: its Session address
318
+ * and everything the TUI keyed on it stay the same.
319
+ */
320
+ let route = { service, base: firstBase };
321
+ let closing;
322
+ const isClosing = () => closing !== undefined;
323
+ const reroute = (url) => url === firstBase || url.startsWith(`${firstBase}/`)
324
+ ? route.base + url.slice(firstBase.length)
325
+ : url;
326
+ const transport = globalThis.fetch;
327
+ const releaseRoute = async (base) => {
328
+ try {
329
+ const response = await transport(`${base}/`, {
330
+ method: "DELETE",
331
+ redirect: "error",
332
+ signal: AbortSignal.timeout(5_000),
333
+ });
334
+ if (!response.ok && response.status !== 404)
335
+ throw new Error(`Local client release failed (${String(response.status)})`);
336
+ }
337
+ catch (error) {
338
+ if (options.reportCleanupFailure === undefined)
339
+ logEvent("service-client.release", error);
340
+ else
341
+ options.reportCleanupFailure(error);
342
+ }
343
+ };
344
+ const reroutedFetch = Object.assign((target, init) => {
345
+ if (target instanceof Request)
346
+ throw new TypeError("A Local service connection fetches by URL");
347
+ return transport(reroute(String(target)), init);
348
+ }, { preconnect: transport.preconnect });
349
+ const Source = options.eventSource ?? EventSource;
350
+ class ReroutedEventSource extends Source {
351
+ constructor(url) {
352
+ super(reroute(url));
353
+ // The library would retry the address it was built with, forever and
354
+ // silently. A dropped stream is surfaced instead, so the owner of the
355
+ // connection can rebind it and reopen the observation.
356
+ this.addEventListener("error", () => {
357
+ this.close();
358
+ });
359
+ }
360
+ }
361
+ const client = createShiitakeClient({
362
+ baseUrl: firstBase,
363
+ fetch: reroutedFetch,
364
+ eventSource: ReroutedEventSource,
365
+ });
366
+ return {
367
+ get service() {
368
+ return route.service;
369
+ },
370
+ agentId: selected.agentId,
371
+ databasePath: selected.databasePath,
372
+ sessionId: selected.sessionId,
373
+ client,
374
+ session: client.session(selected.sessionId),
375
+ sandboxStatus: async (request = {}) => {
376
+ request.signal?.throwIfAborted();
377
+ const init = { redirect: "error" };
378
+ if (request.signal !== undefined)
379
+ init.signal = request.signal;
380
+ const response = await transport(`${route.base}/sandbox`, init);
381
+ if (!response.ok)
382
+ throw new Error(`Local sandbox status read failed (${String(response.status)})`);
383
+ return sandboxStatusSchema.parse(await response.json());
384
+ },
385
+ stopAll: async () => {
386
+ const response = await fetch(`${route.service.baseUrl}/service/${route.service.serviceId}/stop-all`, {
387
+ method: "POST",
388
+ headers: { "Content-Type": "application/json" },
389
+ body: JSON.stringify({
390
+ invocationRoot: input.invocationRoot,
391
+ agentId: selected.agentId,
392
+ }),
393
+ redirect: "error",
394
+ });
395
+ if (!response.ok)
396
+ throw new Error(`Stop all failed (${String(response.status)})`);
397
+ return stopAllResultSchema.parse(await response.json());
398
+ },
399
+ effectiveModel: async (request = {}) => {
400
+ request.signal?.throwIfAborted();
401
+ const init = { redirect: "error" };
402
+ if (request.signal !== undefined)
403
+ init.signal = request.signal;
404
+ const response = await transport(`${route.base}/sessions/${encodeURIComponent(selected.sessionId)}/effective-model`, init);
405
+ if (!response.ok)
406
+ throw new Error(`Local effective model read failed (${String(response.status)})`);
407
+ return privateLocalEffectiveModelSchema.parse(await response.json());
408
+ },
409
+ rebind: async (rebindRequest = {}) => {
410
+ if (isClosing())
411
+ throw new Error("Local service connection is closed");
412
+ rebindRequest.signal?.throwIfAborted();
413
+ // A healthy probe does not guarantee that selection will answer.
414
+ // Bound each attempt independently of the pane's lifetime.
415
+ const deadline = AbortSignal.timeout(5_000);
416
+ const signal = rebindRequest.signal === undefined
417
+ ? deadline
418
+ : AbortSignal.any([rebindRequest.signal, deadline]);
419
+ // Read service.json again, and never start a service: whoever
420
+ // stopped it decides when it returns.
421
+ const ready = await status();
422
+ if (ready === undefined)
423
+ return { kind: "unavailable" };
424
+ accept(ready);
425
+ const again = await postSelection("select", {
426
+ invocationRoot: input.invocationRoot,
427
+ databasePath: selected.databasePath,
428
+ sessionId: selected.sessionId,
429
+ fungiMcp: input.fungiMcp,
430
+ uiAttachmentLifetime: input.uiAttachmentLifetime,
431
+ }, ready, signal);
432
+ if (again.agentId !== selected.agentId ||
433
+ again.sessionId !== selected.sessionId)
434
+ throw new Error("The restarted Local service selected a different Session");
435
+ const nextBase = routeBase(ready, again);
436
+ if (isClosing()) {
437
+ await releaseRoute(nextBase);
438
+ throw new Error("Local service connection is closed");
439
+ }
440
+ const previous = route;
441
+ route = { service: ready, base: nextBase };
442
+ if (previous.service.serviceId === ready.serviceId)
443
+ await releaseRoute(previous.base);
444
+ return { kind: "rebound", service: ready };
445
+ },
446
+ close: () => {
447
+ closing ??= (async () => {
448
+ const base = route.base;
449
+ // Stop local observations first, including when the service is gone.
450
+ await client.close();
451
+ await releaseRoute(base);
452
+ })();
453
+ return closing;
454
+ },
455
+ };
456
+ };
457
+ const connect = (selection, request) => openConnection("select", selection, request);
458
+ const inspectResponseSchema = z.strictObject({
459
+ diagnostics: serviceDiagnosticsStatusSchema,
460
+ selected: selectedSchema,
461
+ snapshot: z.unknown(),
462
+ });
463
+ return {
464
+ status,
465
+ start,
466
+ createSession: (creation, request) => openConnection("sessions", creation, request),
467
+ connect,
468
+ async discover(invocationRoot, request = {}) {
469
+ request.signal?.throwIfAborted();
470
+ const service = await start(request);
471
+ const init = {
472
+ method: "POST",
473
+ headers: { "Content-Type": "application/json" },
474
+ body: JSON.stringify(discoverRequestSchema.parse({
475
+ invocationRoot,
476
+ archived: request.archived,
477
+ })),
478
+ redirect: "error",
479
+ };
480
+ if (request.signal !== undefined)
481
+ init.signal = request.signal;
482
+ const response = await fetch(`${service.baseUrl}/service/${service.serviceId}/discover`, init);
483
+ if (!response.ok)
484
+ throw new Error(`Local session discovery failed (${String(response.status)})`);
485
+ return projectPrivateLocalSessionDiscovery(sessionDiscoverySchema.parse(await response.json()), connect);
486
+ },
487
+ async archive(archival, request = {}) {
488
+ request.signal?.throwIfAborted();
489
+ const service = await start(request);
490
+ const init = {
491
+ method: "POST",
492
+ headers: { "Content-Type": "application/json" },
493
+ body: JSON.stringify(archiveRequestSchema.parse(archival)),
494
+ redirect: "error",
495
+ };
496
+ if (request.signal !== undefined)
497
+ init.signal = request.signal;
498
+ const response = await fetch(`${service.baseUrl}/service/${service.serviceId}/archive`, init);
499
+ if (response.status === 409) {
500
+ const refusal = archiveRefusalSchema.safeParse(await response.json().catch((cause) => {
501
+ throw new LocalServiceUnreadableResponse(response.status, cause);
502
+ }));
503
+ if (refusal.success)
504
+ throw new AgentArchiveRefused(refusal.data.reason, refusal.data.error);
505
+ }
506
+ if (!response.ok) {
507
+ const failure = serviceFailureSchema.safeParse(await response.json().catch((cause) => {
508
+ throw new LocalServiceUnreadableResponse(response.status, cause);
509
+ }));
510
+ throw new Error(`Local Agent archive failed (${String(response.status)})${failure.success ? `: ${failure.data.error}` : ""}`);
511
+ }
512
+ },
513
+ async inspect(selection, request = {}) {
514
+ request.signal?.throwIfAborted();
515
+ const service = await start(request);
516
+ const input = sessionInspectionSchema.parse({
517
+ ...selection,
518
+ limit: request.limit ?? 100,
519
+ });
520
+ const init = {
521
+ method: "POST",
522
+ headers: { "Content-Type": "application/json" },
523
+ body: JSON.stringify(input),
524
+ redirect: "error",
525
+ };
526
+ if (request.signal !== undefined)
527
+ init.signal = request.signal;
528
+ const response = await fetch(`${service.baseUrl}/service/${service.serviceId}/inspect`, init);
529
+ if (response.status === 409)
530
+ await rejectSelectionConflict(response);
531
+ if (!response.ok)
532
+ throw new Error(`Local Session inspection failed (${String(response.status)})`);
533
+ const decoded = inspectResponseSchema.parse(JSON.parse(await readBoundedResponse(response, maximumInspectionResponseBytes, "Local Session inspection")));
534
+ const baseUrl = `${service.baseUrl}/service/${service.serviceId}${decoded.selected.routePath}`;
535
+ const client = createShiitakeClient({
536
+ baseUrl,
537
+ eventSource: options.eventSource ?? EventSource,
538
+ });
539
+ return {
540
+ diagnostics: decoded.diagnostics,
541
+ session: client.session(decoded.selected.sessionId),
542
+ snapshot: parseSessionSnapshot(JSON.stringify(decoded.snapshot)),
543
+ close: () => client.close(),
544
+ };
545
+ },
546
+ async auth(request, interaction) {
547
+ interaction.signal?.throwIfAborted();
548
+ const service = await start(interaction.signal === undefined ? {} : { signal: interaction.signal });
549
+ interaction.signal?.throwIfAborted();
550
+ const url = new URL(`${service.baseUrl}/service/${service.serviceId}/auth`);
551
+ url.protocol = "ws:";
552
+ const socket = new WebSocket(url);
553
+ const remote = newWebSocketRpcSession(socket);
554
+ try {
555
+ return await runPrivateLocalAuthRemote(remote, request, interaction, reportCleanupFailure);
556
+ }
557
+ finally {
558
+ socket.close();
559
+ }
560
+ },
561
+ async models(request = {}) {
562
+ request.signal?.throwIfAborted();
563
+ const service = await start(request);
564
+ const init = { redirect: "error" };
565
+ if (request.signal !== undefined)
566
+ init.signal = request.signal;
567
+ const response = await fetch(`${service.baseUrl}/service/${service.serviceId}/models`, init);
568
+ if (!response.ok)
569
+ throw new Error(`Local service model catalogue failed (${String(response.status)})`);
570
+ return modelChoicesSchema.parse(await response.json());
571
+ },
572
+ async reload(invocationRoot) {
573
+ const service = await status();
574
+ if (service === undefined)
575
+ throw new Error("Local service is not running");
576
+ const response = await fetch(`${service.baseUrl}/service/${service.serviceId}/reload`, {
577
+ method: "POST",
578
+ headers: { "Content-Type": "application/json" },
579
+ body: JSON.stringify({ invocationRoot, clientPath }),
580
+ redirect: "error",
581
+ signal: AbortSignal.timeout(30_000),
582
+ });
583
+ const result = z
584
+ .union([
585
+ z.strictObject({ message: z.string() }),
586
+ z.strictObject({ error: z.string() }),
587
+ ])
588
+ .parse(await response.json());
589
+ if ("error" in result)
590
+ throw new Error(result.error);
591
+ if (!response.ok)
592
+ throw new Error(`Configuration reload failed (${String(response.status)})`);
593
+ return result.message;
594
+ },
595
+ async stop(expectedServiceId) {
596
+ const directory = await serviceDirectory(selectedDirectory);
597
+ const release = await acquireServiceLock(directory, "startup");
598
+ try {
599
+ return await stopUnderLock(directory, expectedServiceId);
600
+ }
601
+ finally {
602
+ await release();
603
+ }
604
+ },
605
+ };
606
+ }
@@ -0,0 +1,66 @@
1
+ import { type StoredSessionSummary } from "@fungi.computer/shiitake/effect";
2
+ import type { PrivateLocalServiceManager, PrivateLocalDiscoveryOptions, PrivateLocalSessionDiscovery } from "./private-local-service-contract.js";
3
+ export type LocalSummary = StoredSessionSummary & Readonly<{
4
+ databasePath: string;
5
+ sessionId: string;
6
+ }>;
7
+ /**
8
+ * Which Agent stores a listing returns: the ones in use, only the archived
9
+ * ones (to restore them), or both (to find one by id).
10
+ */
11
+ export type LocalStoreVisibility = "active" | "archived" | "all";
12
+ /** The one plain phrase for a store written before the current schema, with how to reset it. */
13
+ export declare function unreadableStoreReason(agentId: string): string;
14
+ /**
15
+ * Whether a store's Agent is archived. A store this cannot read counts as
16
+ * not archived, so discovery still reports it as a problem.
17
+ */
18
+ export declare function isArchivedStore(path: string): boolean;
19
+ export declare function listPrivateLocalAgentStores(root: string, visibility?: LocalStoreVisibility): Promise<readonly string[]>;
20
+ /** A Local or Woodstock store outside the current schema; Local never migrates it. */
21
+ export declare class LocalStoreSchemaError extends Error {
22
+ readonly databasePath: string;
23
+ readonly name = "LocalStoreSchemaError";
24
+ constructor(databasePath: string, options?: ErrorOptions);
25
+ }
26
+ /**
27
+ * One Agent store's chosen display name, default model, whether it is
28
+ * archived and the Agent that created it (absent when a human made it), read
29
+ * without opening a host.
30
+ */
31
+ export declare function readPrivateLocalAgentProfile(path: string): {
32
+ name: string | undefined;
33
+ createdBy: string | null;
34
+ archived: boolean;
35
+ requireSandbox: boolean;
36
+ folder: string;
37
+ model: {
38
+ readonly providerId: string;
39
+ readonly modelId: string;
40
+ readonly reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | undefined;
41
+ } | null;
42
+ };
43
+ export declare function readPrivateLocalSessionSummaries(path: string): Promise<readonly LocalSummary[]>;
44
+ /**
45
+ * Discover existing folder sessions without opening or reconciling a host.
46
+ * Archived Agents and their Sessions are left out, so no picker or "newest
47
+ * Session" choice can land on one; `archived` lists only those instead.
48
+ */
49
+ export declare function readPrivateLocalSessionDiscovery(invocationRoot: string, request?: PrivateLocalDiscoveryOptions): Promise<{
50
+ root: string;
51
+ agents: Readonly<{
52
+ agentId: string;
53
+ unreadable?: string | undefined;
54
+ name?: string | undefined;
55
+ createdBy?: string | null | undefined;
56
+ databasePath: string;
57
+ sessionCount: number;
58
+ updatedAt?: number | undefined;
59
+ }>[];
60
+ sessions: LocalSummary[];
61
+ problems: Readonly<{
62
+ kind: "corrupt" | "in_use" | "unreadable";
63
+ }>[];
64
+ }>;
65
+ /** Project the service-owned discovery into choices that resume through it. */
66
+ export declare function projectPrivateLocalSessionDiscovery(discovered: Awaited<ReturnType<typeof readPrivateLocalSessionDiscovery>>, connect: PrivateLocalServiceManager["connect"]): PrivateLocalSessionDiscovery;