@fungi.computer/truffle 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +282 -34
  2. package/dist/auth-cli.d.ts +10 -0
  3. package/dist/auth-cli.js +36 -14
  4. package/dist/fungi-account-command.d.ts +1 -1
  5. package/dist/fungi-account-command.js +6 -3
  6. package/dist/index.d.ts +10 -2
  7. package/dist/index.js +8 -2
  8. package/dist/internal/image-fit-metafile.json +106 -0
  9. package/dist/internal/local-image-fit-worker.mjs +960 -0
  10. package/dist/internal/private-workerd-sandbox-worker.mjs +11 -12
  11. package/dist/internal/sandbox-metafile.json +7 -7
  12. package/dist/local-archive-handoff.d.ts +12 -0
  13. package/dist/local-archive-handoff.js +21 -0
  14. package/dist/local-extensions.d.ts +58 -12
  15. package/dist/local-extensions.js +195 -55
  16. package/dist/local-image-fit.d.ts +30 -0
  17. package/dist/local-image-fit.js +105 -0
  18. package/dist/local-models.d.ts +4 -2
  19. package/dist/local-models.js +11 -6
  20. package/dist/local-session-archives.d.ts +8 -0
  21. package/dist/local-session-archives.js +152 -0
  22. package/dist/local-skill-source.d.ts +18 -3
  23. package/dist/local-skill-source.js +216 -140
  24. package/dist/log-event.d.ts +7 -0
  25. package/dist/log-event.js +8 -4
  26. package/dist/private-bounded-response.d.ts +2 -0
  27. package/dist/private-bounded-response.js +41 -0
  28. package/dist/private-file-credential-store.d.ts +28 -3
  29. package/dist/private-file-credential-store.js +57 -9
  30. package/dist/private-files-patch.d.ts +2 -1
  31. package/dist/private-files-patch.js +21 -3
  32. package/dist/private-fungi-account.d.ts +40 -23
  33. package/dist/private-fungi-account.js +201 -21
  34. package/dist/private-fungi-mcp-connection.d.ts +4 -0
  35. package/dist/private-fungi-mcp-connection.js +25 -6
  36. package/dist/private-fungi-mcp-selection-schema.d.ts +7 -0
  37. package/dist/private-fungi-mcp-selection-schema.js +15 -0
  38. package/dist/private-fungi-mcp-selection.d.ts +6 -6
  39. package/dist/private-fungi-mcp-selection.js +29 -16
  40. package/dist/private-fungi-model-api.js +4 -4
  41. package/dist/private-fungi-model-catalog.d.ts +10 -0
  42. package/dist/private-fungi-model-catalog.js +10 -2
  43. package/dist/private-fungi-model-provider.d.ts +1 -1
  44. package/dist/private-fungi-model-provider.js +6 -1
  45. package/dist/private-fungi-pending-logouts.d.ts +1 -1
  46. package/dist/private-fungi-pending-logouts.js +1 -2
  47. package/dist/private-fungi-service-logout.d.ts +44 -0
  48. package/dist/private-fungi-service-logout.js +59 -0
  49. package/dist/private-invocation-computer.d.ts +24 -3
  50. package/dist/private-invocation-computer.js +101 -62
  51. package/dist/private-invocation-files.d.ts +16 -1
  52. package/dist/private-invocation-files.js +154 -83
  53. package/dist/private-local-acp-stdio.js +3 -0
  54. package/dist/private-local-admission-gate.d.ts +14 -0
  55. package/dist/private-local-admission-gate.js +52 -0
  56. package/dist/private-local-agent-control.d.ts +101 -0
  57. package/dist/private-local-agent-control.js +30 -0
  58. package/dist/private-local-agent-default-model.d.ts +12 -0
  59. package/dist/private-local-agent-default-model.js +31 -0
  60. package/dist/private-local-auth-rpc.d.ts +67 -7
  61. package/dist/private-local-auth-rpc.js +140 -17
  62. package/dist/private-local-bwrap.d.ts +13 -0
  63. package/dist/private-local-bwrap.js +70 -0
  64. package/dist/private-local-client-version.d.ts +5 -0
  65. package/dist/private-local-client-version.js +43 -0
  66. package/dist/private-local-computer-attach.d.ts +3 -3
  67. package/dist/private-local-computer-attach.js +17 -3
  68. package/dist/private-local-computer-processes.d.ts +6 -3
  69. package/dist/private-local-computer-processes.js +55 -31
  70. package/dist/private-local-config-root.d.ts +21 -0
  71. package/dist/private-local-config-root.js +19 -0
  72. package/dist/private-local-containment.d.ts +51 -0
  73. package/dist/private-local-containment.js +161 -0
  74. package/dist/private-local-delegation.d.ts +10 -3
  75. package/dist/private-local-delegation.js +70 -15
  76. package/dist/private-local-effective-model.d.ts +9 -1
  77. package/dist/private-local-effective-model.js +3 -0
  78. package/dist/private-local-exec-environment.d.ts +16 -0
  79. package/dist/private-local-exec-environment.js +11 -0
  80. package/dist/private-local-linux-sandbox.d.ts +8 -0
  81. package/dist/private-local-linux-sandbox.js +67 -0
  82. package/dist/private-local-mail.d.ts +4 -2
  83. package/dist/private-local-mail.js +41 -33
  84. package/dist/private-local-mesh-enrollment.d.ts +34 -0
  85. package/dist/private-local-mesh-enrollment.js +188 -0
  86. package/dist/private-local-mesh-links.d.ts +21 -0
  87. package/dist/private-local-mesh-links.js +229 -0
  88. package/dist/private-local-mesh-record.d.ts +120 -0
  89. package/dist/private-local-mesh-record.js +55 -0
  90. package/dist/private-local-process-identity.d.ts +20 -0
  91. package/dist/private-local-process-identity.js +40 -0
  92. package/dist/private-local-runtime-host.d.ts +64 -13
  93. package/dist/private-local-runtime-host.js +361 -172
  94. package/dist/private-local-sandbox-policy.d.ts +21 -0
  95. package/dist/private-local-sandbox-policy.js +121 -0
  96. package/dist/private-local-sandbox.d.ts +37 -0
  97. package/dist/private-local-sandbox.js +114 -0
  98. package/dist/private-local-seatbelt.d.ts +24 -0
  99. package/dist/private-local-seatbelt.js +68 -0
  100. package/dist/private-local-service-contract.d.ts +91 -11
  101. package/dist/private-local-service-discovery.d.ts +129 -13
  102. package/dist/private-local-service-discovery.js +148 -47
  103. package/dist/private-local-service-lease.d.ts +16 -0
  104. package/dist/private-local-service-lease.js +105 -0
  105. package/dist/private-local-service-listener.d.ts +9 -2
  106. package/dist/private-local-service-listener.js +260 -64
  107. package/dist/private-local-service-log.d.ts +128 -0
  108. package/dist/private-local-service-log.js +321 -0
  109. package/dist/private-local-service-owner.d.ts +87 -1519
  110. package/dist/private-local-service-owner.js +1069 -71
  111. package/dist/private-local-service-process.js +3 -1
  112. package/dist/private-local-service-termination.d.ts +7 -0
  113. package/dist/private-local-service-termination.js +67 -0
  114. package/dist/private-local-service.d.ts +31 -2
  115. package/dist/private-local-service.js +402 -126
  116. package/dist/private-local-session-discovery.d.ts +52 -5
  117. package/dist/private-local-session-discovery.js +132 -11
  118. package/dist/private-local-shiitake-host.js +2 -2
  119. package/dist/private-local-startup-model.d.ts +51 -0
  120. package/dist/private-local-startup-model.js +86 -0
  121. package/dist/private-local-stop-all.d.ts +27 -0
  122. package/dist/private-local-stop-all.js +26 -0
  123. package/dist/private-local-ui-attachment.d.ts +19 -0
  124. package/dist/private-local-ui-attachment.js +71 -0
  125. package/dist/private-local-worker-server.d.ts +1 -0
  126. package/dist/private-local-worker-server.js +13 -5
  127. package/dist/private-mcp-form-contract.d.ts +186 -0
  128. package/dist/private-mcp-form-contract.js +53 -0
  129. package/dist/private-mcp-form-elicitation.d.ts +18 -0
  130. package/dist/private-mcp-form-elicitation.js +97 -0
  131. package/dist/private-mcp-form-session.d.ts +172 -0
  132. package/dist/private-mcp-form-session.js +148 -0
  133. package/dist/private-openai-claims.d.ts +30 -0
  134. package/dist/private-openai-claims.js +69 -0
  135. package/dist/private-oyster-mcp-auth.d.ts +13 -0
  136. package/dist/private-oyster-mcp-auth.js +57 -0
  137. package/dist/private-provider-rejection.d.ts +15 -0
  138. package/dist/private-provider-rejection.js +24 -0
  139. package/dist/private-sqlite-owner.d.ts +34 -2
  140. package/dist/private-sqlite-owner.js +103 -37
  141. package/dist/private-team-computers.d.ts +4 -4
  142. package/dist/private-team-computers.js +11 -2
  143. package/dist/seatbelt/safe-base.sbpl +79 -0
  144. package/dist/subscription-activation.d.ts +9 -0
  145. package/dist/subscription-activation.js +62 -0
  146. package/dist/subscription-identity.d.ts +23 -0
  147. package/dist/subscription-identity.js +124 -0
  148. package/dist/subscription-turn.d.ts +17 -0
  149. package/dist/subscription-turn.js +143 -0
  150. package/dist/team-apps.d.ts +2 -2
  151. package/dist/team-apps.js +7 -2
  152. package/native/bin/LICENSE.bubblewrap +502 -0
  153. package/native/bin/LICENSE.libcap +112 -0
  154. package/native/bin/LICENSE.sandbox-runtime +201 -0
  155. package/native/bin/SOURCE.bubblewrap.tar.xz +0 -0
  156. package/native/bin/bwrap-build-source/Dockerfile +48 -0
  157. package/native/bin/bwrap-build-source/README.md +96 -0
  158. package/native/bin/bwrap-build-source/build.mjs +52 -0
  159. package/native/bin/bwrap-build-source/linux-arm64.ini +14 -0
  160. package/native/bin/bwrap-build-source/pin.mjs +21 -0
  161. package/native/bin/bwrap-build-source/record.mjs +74 -0
  162. package/native/bin/bwrap-build-source/vendor/LICENSE.sandbox-runtime +201 -0
  163. package/native/bin/bwrap-build-source/vendor/README.md +23 -0
  164. package/native/bin/bwrap-build-source/vendor/seccomp-unix-block.c +280 -0
  165. package/native/bin/bwrap-build.json +44 -0
  166. package/native/bin/fungi-bwrap +0 -0
  167. package/native/bin/fungi-bwrap.sha256 +1 -0
  168. package/native/bin/fungi-seccomp.bpf +0 -0
  169. package/native/bin/fungi-seccomp.bpf.sha256 +1 -0
  170. package/package.json +44 -31
  171. package/dist/private-local-model-settings.d.ts +0 -5
  172. package/dist/private-local-model-settings.js +0 -31
  173. package/dist/private-local-subagents.d.ts +0 -27
  174. package/dist/private-local-subagents.js +0 -77
@@ -1,53 +1,107 @@
1
+ import { localFailureMetadata, logEvent } from "./log-event.js";
2
+ import { bodyLimit } from "hono/body-limit";
3
+ import { enrollPrivateLocalMesh, localMeshJoinSchema, } from "./private-local-mesh-enrollment.js";
4
+ import { createLocalMachineEnrollmentAccount, registerFungiAccountOwner, signOutOwnedFungiAccount, } from "./private-fungi-account.js";
5
+ import { createServiceLogLifecycle, } from "./private-local-service-log.js";
6
+ import { stopAllRequestSchema } from "./private-local-stop-all.js";
1
7
  import { FungiMcpSelectionConflict } from "./private-fungi-mcp-selection.js";
8
+ import { LocalSandboxUnavailable } from "./private-local-containment.js";
2
9
  import { PrivateLocalAuthTarget } from "./private-local-auth-rpc.js";
3
10
  import { runLocalAuthCli } from "./auth-cli.js";
11
+ import { capturePrivateLocalConfigRoot, } from "./private-local-config-root.js";
4
12
  import { Hono } from "hono";
13
+ import { correlationKey } from "@fungi.computer/cairn";
14
+ import { createLocalUiAttachment, } from "./private-local-ui-attachment.js";
5
15
  import { localServiceIds } from "./private-local-service-ids.js";
6
16
  import writeFileAtomic from "write-file-atomic";
7
17
  import { z } from "zod";
18
+ import { privateLocalExecPathSchema } from "./private-local-exec-path.js";
8
19
  import { listPrivateModels } from "./local-models.js";
9
20
  import { privateSqliteFailureCode } from "./private-sqlite-owner.js";
10
21
  import { openPrivateLocalBunServer } from "./private-bun-shiitake-host.js";
11
- import { readPrivateLocalSessionDiscovery } from "./private-local-session-discovery.js";
12
- import { createLocalServiceOwners, LocalSessionPathConflict, } from "./private-local-service-owner.js";
13
- import { acquireServiceLock, recordPath, removeOwnedServiceRecord, selectionSchema, sessionCreationSchema, sessionInspectionSchema, } from "./private-local-service-discovery.js";
22
+ import { readOwnProcessStart } from "./private-local-process-identity.js";
23
+ import { LocalStoreSchemaError, readPrivateLocalSessionDiscovery, } from "./private-local-session-discovery.js";
24
+ import { AgentArchiveRefused } from "@fungi.computer/shiitake/agent-control";
25
+ import { createLocalServiceOwners, LocalAgentSelectionAmbiguous, LocalSessionPathConflict, } from "./private-local-service-owner.js";
26
+ import { acquireServiceLock, archiveRequestSchema, discoverRequestSchema, privateLocalServiceProtocol, privateLocalServiceVersion, recordPath, removeOwnedServiceRecord, selectionSchema, sessionCreationSchema, sessionInspectionSchema, sandboxStatusSchema, } from "./private-local-service-discovery.js";
14
27
  export function createPrivateLocalServiceShutdown(input) {
15
28
  let draining;
16
29
  let finishing;
17
- const drain = () => {
18
- draining ??= input.closeOwners();
19
- return draining;
20
- };
30
+ let phase = "active";
21
31
  const finish = () => {
22
32
  finishing ??= (async () => {
23
- // Failed owner drain leaves the live listener, discovery, and owner lock
24
- // intact. A caller must never observe this service as stopped.
25
- await drain();
33
+ // Keep discovery and the lease until owners settle. The process owner
34
+ // bounds this whole sequence, including a hung owner or listener.
35
+ phase = "owners";
36
+ draining ??= input.closeOwners();
37
+ await draining;
38
+ phase = "listener";
26
39
  await input.closeServer();
40
+ // Stop diagnostics before releasing ownership. Its resource latches I/O
41
+ // faults and disables writes; a diagnostic fault cannot strand a service
42
+ // registration/lease or make normal shutdown reject.
43
+ try {
44
+ input.closeDiagnostics?.();
45
+ }
46
+ catch (cause) {
47
+ // Diagnostics cannot strand registration/lease or fail a normal stop.
48
+ // The owned resource latches its typed fault; this protects cleanup
49
+ // against an unexpected disposal callback failure as well.
50
+ logEvent("service-log.dispose", cause);
51
+ }
27
52
  try {
53
+ phase = "record";
28
54
  await input.removeRecord();
29
55
  }
30
56
  finally {
57
+ phase = "lease";
31
58
  await input.releaseOwner();
32
59
  }
60
+ phase = "closed";
33
61
  })();
34
62
  return finishing;
35
63
  };
36
- return { drain, finish, isDraining: () => draining !== undefined };
64
+ return {
65
+ finish,
66
+ isDraining: () => draining !== undefined,
67
+ phase: () => phase,
68
+ };
37
69
  }
38
70
  /** Native Bun listener retained from TUI; service owns both listener and stores. */
39
- export async function runPrivateLocalService(directory, openRuntime, listModels = listPrivateModels, runAuth = runLocalAuthCli) {
71
+ export async function runPrivateLocalService(directory, openRuntime, listModels = listPrivateModels, runAuth = runLocalAuthCli,
72
+ /** How often the service releases Agents nothing uses. */
73
+ idleSweepMs,
74
+ /** Narrow manifest I/O port, independent of the Agent-runtime opener. */
75
+ readRecoveryManifest, root = capturePrivateLocalConfigRoot()) {
76
+ // Publish the same discriminator in the pointer and the acquired owner row.
77
+ const startedAt = await readOwnProcessStart();
40
78
  const release = await acquireServiceLock(directory, "owner");
41
- const owners = createLocalServiceOwners(openRuntime, directory);
42
79
  const serviceId = localServiceIds.service.generate();
43
- const buildId = z
44
- .string()
45
- .regex(/^[a-f0-9]{64}$/u)
46
- .optional()
47
- .parse(process.env.FUNGI_BUNDLED_BUILD_ID);
80
+ const version = privateLocalServiceVersion();
81
+ const jsc = await import("bun:jsc");
82
+ const diagnostics = createServiceLogLifecycle({
83
+ directory,
84
+ pid: process.pid,
85
+ version,
86
+ readMemory: () => ({
87
+ rss: process.memoryUsage().rss,
88
+ heap: jsc.heapStats(),
89
+ }),
90
+ });
91
+ const owners = createLocalServiceOwners(openRuntime, directory, undefined, idleSweepMs, readRecoveryManifest, diagnostics.recoveryDisabled, root);
48
92
  const prefix = `/service/${serviceId}`;
93
+ const releaseAccountOwner = registerFungiAccountOwner(root.credentialPath, () => owners.refreshMesh());
49
94
  const app = new Hono();
50
95
  app.onError((error, context) => {
96
+ diagnostics.failure(localFailureMetadata("service.request", error));
97
+ if (error instanceof LocalStoreSchemaError)
98
+ return context.json({
99
+ code: "store_schema_incompatible",
100
+ databasePath: error.databasePath,
101
+ error: error.message,
102
+ }, 409);
103
+ if (error instanceof LocalSandboxUnavailable)
104
+ return context.json({ code: "sandbox_unavailable", reason: error.reason }, 409);
51
105
  if (error instanceof z.ZodError)
52
106
  return context.json({
53
107
  code: "invalid_selection",
@@ -58,38 +112,72 @@ export async function runPrivateLocalService(directory, openRuntime, listModels
58
112
  }, 400);
59
113
  if (error instanceof LocalSessionPathConflict)
60
114
  return context.json({ code: "session_path_conflict", error: error.message }, 409);
115
+ if (error instanceof LocalAgentSelectionAmbiguous)
116
+ return context.json({ code: "agent_selection_ambiguous", agentIds: error.agentIds }, 409);
61
117
  if (error instanceof FungiMcpSelectionConflict)
62
118
  return context.json({ error: error.message }, 409);
119
+ if (error instanceof AgentArchiveRefused)
120
+ return context.json({
121
+ code: "agent_archive_refused",
122
+ reason: error.reason,
123
+ error: error.message,
124
+ }, 409);
63
125
  const sqliteFailure = privateSqliteFailureCode(error);
64
126
  if (sqliteFailure === "busy" || sqliteFailure === "locked")
65
127
  return context.json({ code: "store_in_use" }, 409);
66
- console.error(error);
67
- return context.text("Internal Server Error", 500);
128
+ logEvent("service-listener.request", error);
129
+ return context.json({
130
+ code: "internal_error",
131
+ error: error instanceof Error ? error.message : "Unknown failure",
132
+ }, 500);
68
133
  });
69
134
  const routes = new Map();
70
135
  const stopped = Promise.withResolvers();
71
136
  let server;
137
+ /** Identity-only pointer; live routes add current diagnostics separately. */
138
+ let record;
72
139
  let recordWritten = false;
73
- const mount = (selected, sessionId) => {
74
- let mounted = routes.get(selected.databasePath);
140
+ /**
141
+ * Mount an Agent's route by store, not by its open runtime: a request on a
142
+ * route reopens an Agent the service released while it was idle.
143
+ */
144
+ const mount = (selected, fungiMcp, attachment) => {
145
+ // Inspectors share a read route; each UI gets a private host-issued route.
146
+ let mounted = attachment === undefined ? routes.get(selected.databasePath) : undefined;
75
147
  if (!mounted) {
76
- const path = `/agents/${String(routes.size)}`;
148
+ const path = `/agents/${correlationKey()}`;
77
149
  const routePrefix = prefix + path;
150
+ const target = {
151
+ invocationRoot: selected.invocationRoot,
152
+ databasePath: selected.databasePath,
153
+ fungiMcp,
154
+ };
78
155
  mounted = {
79
156
  path,
80
157
  fetch: (request) => {
81
158
  const url = new URL(request.url);
82
159
  url.pathname = url.pathname.slice(routePrefix.length) || "/";
83
- return selected.routes.fetch(new Request(url.toString(), request));
160
+ if (request.method === "DELETE" &&
161
+ url.pathname === "/" &&
162
+ attachment !== undefined) {
163
+ attachment.close();
164
+ return new Response(null, { status: 204 });
165
+ }
166
+ return owners.serve(target, new Request(url.toString(), request), attachment);
84
167
  },
85
- effectiveModel: selected.effectiveModel,
168
+ effectiveModel: (sessionId, signal) => owners.effectiveModel(target, sessionId, signal),
169
+ sandboxStatus: (signal) => owners.sandboxStatus(target, signal),
86
170
  };
87
- routes.set(selected.databasePath, mounted);
171
+ const key = attachment === undefined ? selected.databasePath : path;
172
+ routes.set(key, mounted);
173
+ attachment?.signal.addEventListener("abort", () => {
174
+ routes.delete(key);
175
+ }, { once: true });
88
176
  }
89
177
  return {
90
178
  agentId: selected.agentId,
91
179
  databasePath: selected.databasePath,
92
- sessionId,
180
+ sessionId: selected.sessionId,
93
181
  routePath: mounted.path,
94
182
  };
95
183
  };
@@ -103,86 +191,157 @@ export async function runPrivateLocalService(directory, openRuntime, listModels
103
191
  await removeOwnedServiceRecord(directory, serviceId);
104
192
  },
105
193
  releaseOwner: release,
194
+ closeDiagnostics: diagnostics.close,
106
195
  });
196
+ const drainDeadlineMs = 4_000;
197
+ let deadline;
107
198
  const finish = () => {
199
+ // Leave headroom before the maintained client's 5-second SIGKILL fallback.
200
+ // Never reset the overall budget for another Agent or another signal.
201
+ deadline ??= setTimeout(() => {
202
+ // The background service has no stderr (spawned with stdio "ignore");
203
+ // the private service log is its only durable diagnostic record.
204
+ diagnostics.drainTimeout({
205
+ deadlineMs: drainDeadlineMs,
206
+ phase: shutdown.phase(),
207
+ unfinishedOwners: owners.unfinishedOwners(),
208
+ });
209
+ // A Promise race cannot interrupt Agent.close's uninterruptible work.
210
+ // Committed SQLite facts and recovery references remain for the next
211
+ // owner; do not pretend those unfinished Agents closed successfully.
212
+ process.exit(1);
213
+ }, drainDeadlineMs);
108
214
  const finishing = shutdown.finish();
109
215
  void finishing.then(() => {
216
+ clearTimeout(deadline);
110
217
  stopped.resolve(undefined);
111
218
  }, (cause) => {
112
219
  stopped.reject(cause);
113
220
  });
114
221
  return finishing;
115
222
  };
116
- const onSignal = () => {
223
+ const onSignal = (reason) => {
224
+ diagnostics.stopping(reason);
117
225
  void finish();
118
226
  };
227
+ const onInterrupt = () => {
228
+ onSignal("SIGINT");
229
+ };
230
+ const onTerminate = () => {
231
+ onSignal("SIGTERM");
232
+ };
119
233
  const isLocalRequest = (request) => !request.headers.has("origin") &&
120
234
  new URL(request.url).hostname === "127.0.0.1";
121
235
  try {
122
- await owners.restore();
123
236
  app.use("*", async (context, next) => {
124
237
  const request = context.req.raw;
125
238
  // Local client only: no browser-origin admission or DNS rebinding authority.
126
239
  if (!isLocalRequest(request))
127
240
  return context.body(null, 403);
128
241
  const path = new URL(request.url).pathname;
129
- if (shutdown.isDraining() &&
130
- path !== `${prefix}/status` &&
131
- path !== `${prefix}/stop`)
242
+ // While SIGTERM drains its owners, only the handshake answers, so
243
+ // `fungi stop` can still confirm the process it is waiting on.
244
+ if (shutdown.isDraining() && path !== `${prefix}/status`)
132
245
  return context.body(null, 503);
133
246
  await next();
134
247
  return;
135
248
  });
136
- app.get(`${prefix}/status`, (context) => context.json({ serviceId, baseUrl: server?.baseUrl, buildId }));
137
- app.get(`${prefix}/models`, async (context) => context.json(await listModels()));
249
+ app.get(`${prefix}/status`, (context) => record === undefined
250
+ ? context.body(null, 503)
251
+ : context.json({
252
+ ...record,
253
+ diagnostics: diagnostics.getStatus(),
254
+ }));
255
+ app.get(`${prefix}/models`, async (context) => context.json(await listModels(root.credentialPath)));
256
+ app.post(`${prefix}/mesh/join`, bodyLimit({ maxSize: 512 * 1024 }), async (context) => {
257
+ const enrolled = await enrollPrivateLocalMesh(localMeshJoinSchema.parse(await context.req.json()), {
258
+ credentialPath: root.credentialPath,
259
+ signal: context.req.raw.signal,
260
+ account: createLocalMachineEnrollmentAccount(root.credentialPath),
261
+ });
262
+ await owners.refreshMesh();
263
+ // Enrollment is not a confirmed shared connection or command receipt.
264
+ return context.json(enrolled, 202);
265
+ });
138
266
  app.post(`${prefix}/discover`, async (context) => {
139
- const input = selectionSchema
140
- .pick({ invocationRoot: true })
141
- .parse(await context.req.json());
267
+ const input = discoverRequestSchema.parse(await context.req.json());
142
268
  return context.json(await readPrivateLocalSessionDiscovery(input.invocationRoot, {
143
269
  signal: context.req.raw.signal,
270
+ archived: input.archived,
144
271
  }));
145
272
  });
273
+ app.post(`${prefix}/reload`, async (context) => {
274
+ try {
275
+ const input = z
276
+ .strictObject({
277
+ invocationRoot: z.string().min(1),
278
+ clientPath: privateLocalExecPathSchema,
279
+ })
280
+ .parse(await context.req.json());
281
+ await owners.reload(input.invocationRoot, input.clientPath);
282
+ return context.json({
283
+ message: "Configuration reloaded; applies from the next turn",
284
+ });
285
+ }
286
+ catch (cause) {
287
+ return context.json({
288
+ error: `${cause instanceof Error ? cause.message : String(cause)}\nOld configuration kept.`,
289
+ }, 400);
290
+ }
291
+ });
292
+ app.post(`${prefix}/stop-all`, async (context) => context.json(await owners.stopAll(stopAllRequestSchema.parse(await context.req.json()))));
293
+ app.post(`${prefix}/archive`, async (context) => {
294
+ await owners.archive(archiveRequestSchema.parse(await context.req.json()), context.req.raw.signal);
295
+ return context.body(null, 204);
296
+ });
146
297
  app.get(`${prefix}/agents/:route/sessions/:sessionId/effective-model`, async (context) => {
147
298
  const route = [...routes.values()].find((entry) => entry.path === `/agents/${context.req.param("route")}`);
148
299
  if (route === undefined)
149
300
  return context.body(null, 404);
150
301
  return context.json(await route.effectiveModel(context.req.param("sessionId"), context.req.raw.signal));
151
302
  });
303
+ app.get(`${prefix}/agents/:route/sandbox`, async (context) => {
304
+ const route = [...routes.values()].find((entry) => entry.path === `/agents/${context.req.param("route")}`);
305
+ if (route === undefined)
306
+ return context.body(null, 404);
307
+ return context.json(sandboxStatusSchema.parse(await route.sandboxStatus(context.req.raw.signal)));
308
+ });
152
309
  app.post(`${prefix}/sessions`, async (context) => {
153
310
  const input = sessionCreationSchema.parse(await context.req.json());
154
311
  const selected = await owners.createSession(input);
155
- return context.json(mount(selected, selected.sessionId));
312
+ try {
313
+ return context.json(mount(selected, input.fungiMcp, createLocalUiAttachment(selected.sessionId, input.uiAttachmentLifetime)));
314
+ }
315
+ finally {
316
+ selected.release();
317
+ }
156
318
  });
157
319
  app.post(`${prefix}/select`, async (context) => {
158
320
  const input = selectionSchema.parse(await context.req.json());
159
321
  const selected = await owners.select(input);
160
- return context.json(mount(selected, selected.sessionId));
322
+ try {
323
+ return context.json(mount(selected, input.fungiMcp, createLocalUiAttachment(selected.sessionId, input.uiAttachmentLifetime)));
324
+ }
325
+ finally {
326
+ selected.release();
327
+ }
161
328
  });
162
329
  app.post(`${prefix}/inspect`, async (context) => {
163
330
  const input = sessionInspectionSchema.parse(await context.req.json());
164
331
  const selected = await owners.select(input);
165
- return context.json({
166
- selected: mount(selected, selected.sessionId),
167
- snapshot: await selected.session.read({
168
- limit: input.limit,
169
- includeSandboxFailures: true,
170
- }),
171
- });
172
- });
173
- app.post(`${prefix}/stop`, async (context) => {
174
332
  try {
175
- await shutdown.drain();
333
+ return context.json({
334
+ diagnostics: diagnostics.getStatus(),
335
+ selected: mount(selected, input.fungiMcp, undefined),
336
+ snapshot: await selected.session.read({
337
+ limit: input.limit,
338
+ includeSandboxFailures: true,
339
+ }),
340
+ });
176
341
  }
177
- catch {
178
- // Preserve failed owner drain as an observable service failure. The
179
- // listener remains discoverable for explicit diagnosis.
180
- return context.json({ error: "Local service resources failed to close" }, 500);
342
+ finally {
343
+ selected.release();
181
344
  }
182
- setTimeout(() => {
183
- void finish();
184
- }, 0);
185
- return context.body(null, 202);
186
345
  });
187
346
  app.all(`${prefix}/agents/*`, (context) => {
188
347
  const path = new URL(context.req.url).pathname;
@@ -195,17 +354,54 @@ export async function runPrivateLocalService(directory, openRuntime, listModels
195
354
  server = openPrivateLocalBunServer((request) => app.fetch(request), undefined, {
196
355
  path: `${prefix}/auth`,
197
356
  admit: (request) => isLocalRequest(request) && !shutdown.isDraining(),
198
- createTarget: () => new PrivateLocalAuthTarget(runAuth),
357
+ createTarget: () => new PrivateLocalAuthTarget((argv, options) => runAuth(argv, {
358
+ ...options,
359
+ credentialPath: root.credentialPath,
360
+ }), async (request) => {
361
+ if (request.credentialPath !== root.credentialPath)
362
+ throw new Error("Fungi account belongs to another Local home");
363
+ return await signOutOwnedFungiAccount(request.endpoint, {
364
+ credentialPath: root.credentialPath,
365
+ });
366
+ }),
367
+ });
368
+ record = {
369
+ protocol: privateLocalServiceProtocol,
370
+ version,
371
+ serviceId,
372
+ pid: process.pid,
373
+ startedAt,
374
+ baseUrl: server.baseUrl,
375
+ };
376
+ await writeFileAtomic(recordPath(directory), JSON.stringify(record), {
377
+ mode: 0o600,
199
378
  });
200
- await writeFileAtomic(recordPath(directory), JSON.stringify({ serviceId, baseUrl: server.baseUrl }), { mode: 0o600 });
201
379
  recordWritten = true;
202
- process.on("SIGINT", onSignal);
203
- process.on("SIGTERM", onSignal);
380
+ diagnostics.ready(serviceId);
381
+ process.on("SIGINT", onInterrupt);
382
+ process.on("SIGTERM", onTerminate);
383
+ // Health and shutdown must answer before any saved Agent is reopened.
384
+ // Owner shutdown tracks this task; requests share its ordinary ownerFor.
385
+ void owners.restore();
386
+ void owners.refreshMesh().catch((cause) => {
387
+ logEvent("service-listener.mesh-start", cause);
388
+ });
204
389
  await stopped.promise;
205
390
  }
391
+ catch (cause) {
392
+ logEvent("service-listener.lifecycle", cause);
393
+ diagnostics.failure(localFailureMetadata("service-listener.lifecycle", cause));
394
+ diagnostics.stopping(recordWritten ? "runtime-failure" : "startup-failure");
395
+ throw cause;
396
+ }
206
397
  finally {
207
- process.off("SIGINT", onSignal);
208
- process.off("SIGTERM", onSignal);
209
- await finish();
398
+ process.off("SIGINT", onInterrupt);
399
+ process.off("SIGTERM", onTerminate);
400
+ try {
401
+ await finish();
402
+ }
403
+ finally {
404
+ releaseAccountOwner();
405
+ }
210
406
  }
211
407
  }
@@ -0,0 +1,128 @@
1
+ import type { heapStats } from "bun:jsc";
2
+ import { localFailureMetadata } from "./log-event.js";
3
+ import { z } from "zod";
4
+ export declare const serviceLogRecordBytes: number;
5
+ export declare const serviceLogSampleIntervalMs: number;
6
+ export type HeapStatistics = ReturnType<typeof heapStats>;
7
+ export type FailureMetadata = ReturnType<typeof localFailureMetadata>;
8
+ export declare const serviceDiagnosticsStatusSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
9
+ state: z.ZodLiteral<"active">;
10
+ }, z.core.$strict>, z.ZodObject<{
11
+ state: z.ZodLiteral<"failed">;
12
+ code: z.ZodEnum<{
13
+ "sink-open-failed": "sink-open-failed";
14
+ "sink-write-failed": "sink-write-failed";
15
+ "sink-close-failed": "sink-close-failed";
16
+ }>;
17
+ since: z.ZodISODateTime;
18
+ }, z.core.$strict>, z.ZodObject<{
19
+ state: z.ZodLiteral<"recovery-disabled">;
20
+ code: z.ZodEnum<{
21
+ "read-failed": "read-failed";
22
+ "decode-failed": "decode-failed";
23
+ }>;
24
+ since: z.ZodISODateTime;
25
+ }, z.core.$strict>], "state">;
26
+ export type ServiceDiagnosticsStatus = z.infer<typeof serviceDiagnosticsStatusSchema>;
27
+ export type ServiceDiagnosticsFailureCode = Extract<ServiceDiagnosticsStatus, {
28
+ state: "failed";
29
+ }>["code"];
30
+ export type RecoveryPersistenceFailureCode = Extract<ServiceDiagnosticsStatus, {
31
+ state: "recovery-disabled";
32
+ }>["code"];
33
+ export type ServiceLogWriter = Readonly<{
34
+ writeLine: (line: string) => void;
35
+ close: () => void;
36
+ }>;
37
+ export type OpenServiceLogWriter = (directory: string) => ServiceLogWriter;
38
+ /** Catchable observations, not an inferred cause for a missing final record. */
39
+ export type ServiceLogEvent = Readonly<{
40
+ event: "starting";
41
+ }> | Readonly<{
42
+ event: "ready";
43
+ serviceId: string;
44
+ }> | Readonly<{
45
+ event: "stopping" | "stopped";
46
+ reason: "SIGINT" | "SIGTERM" | "startup-failure" | "runtime-failure";
47
+ }> | Readonly<{
48
+ event: "failure";
49
+ failure: FailureMetadata;
50
+ }> | Readonly<{
51
+ event: "recovery-persistence-disabled";
52
+ code: RecoveryPersistenceFailureCode;
53
+ }> | Readonly<{
54
+ event: "memory";
55
+ rss: number;
56
+ heap: HeapStatistics;
57
+ }> | Readonly<{
58
+ event: "memory-unavailable";
59
+ failure: FailureMetadata;
60
+ }> | Readonly<{
61
+ event: "drain-timeout";
62
+ deadlineMs: number;
63
+ phase: ServiceDrainPhase;
64
+ unfinishedOwners: readonly string[];
65
+ unfinishedOwnerCount: number;
66
+ }>;
67
+ /** Where SIGTERM drain stood; owners settle before listener, record, lease. */
68
+ export type ServiceDrainPhase = "active" | "owners" | "listener" | "record" | "lease" | "closed";
69
+ export type ServiceLogRecord = ServiceLogEvent & Readonly<{
70
+ source: "local-service";
71
+ timestamp: string;
72
+ pid: number;
73
+ version: string;
74
+ }>;
75
+ export type ServiceLogShutdownReason = Extract<ServiceLogEvent, {
76
+ event: "stopping" | "stopped";
77
+ }>["reason"];
78
+ export type ServiceLogLifecycle = Readonly<{
79
+ getStatus: () => ServiceDiagnosticsStatus;
80
+ ready: (serviceId: string) => void;
81
+ stopping: (reason: ServiceLogShutdownReason) => void;
82
+ failure: (failure: FailureMetadata) => void;
83
+ recoveryDisabled: (code: RecoveryPersistenceFailureCode) => void;
84
+ /** Synchronous, so it lands before the bounded drain exits the process. */
85
+ drainTimeout: (drain: Readonly<{
86
+ deadlineMs: number;
87
+ phase: ServiceDrainPhase;
88
+ unfinishedOwners: readonly string[];
89
+ }>) => void;
90
+ close: () => void;
91
+ }>;
92
+ /**
93
+ * Preserve ALL maintained count entries, in their producer order, when they fit.
94
+ * If they do not, explicitly omit the maps as a whole. No ranking/top-N policy,
95
+ * count substitution, exception serialization or byte-sliced JSON.
96
+ */
97
+ export declare function encodeServiceLogRecord(record: ServiceLogRecord): string;
98
+ /**
99
+ * One timer per service lifetime. The listener must close it before disposing
100
+ * the writer, and dispose the writer before releasing its existing owner lease.
101
+ * Both entry paths use this same owned resource.
102
+ */
103
+ export declare function createServiceLogSampler(input: Readonly<{
104
+ readMemory: () => Readonly<{
105
+ rss: number;
106
+ heap: HeapStatistics;
107
+ }>;
108
+ emit: (event: ServiceLogEvent) => void;
109
+ }>): {
110
+ start(): void;
111
+ close(): void;
112
+ };
113
+ /**
114
+ * Diagnostics resources only: no service lock, signal handler or runtime drain.
115
+ * The existing listener remains the owner. Native file I/O stays in this module.
116
+ * Writer faults latch a typed live failure, stop sampling and close once.
117
+ * They never escape into a request, turn, signal handler or timer callback.
118
+ */
119
+ export declare function createServiceLogLifecycle(input: Readonly<{
120
+ pid: number;
121
+ version: string;
122
+ directory: string;
123
+ openWriter?: OpenServiceLogWriter;
124
+ readMemory: () => Readonly<{
125
+ rss: number;
126
+ heap: HeapStatistics;
127
+ }>;
128
+ }>): ServiceLogLifecycle;