@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.
- package/README.md +282 -34
- package/dist/auth-cli.d.ts +10 -0
- package/dist/auth-cli.js +36 -14
- package/dist/fungi-account-command.d.ts +1 -1
- package/dist/fungi-account-command.js +6 -3
- package/dist/index.d.ts +10 -2
- package/dist/index.js +8 -2
- package/dist/internal/image-fit-metafile.json +106 -0
- package/dist/internal/local-image-fit-worker.mjs +960 -0
- package/dist/internal/private-workerd-sandbox-worker.mjs +11 -12
- package/dist/internal/sandbox-metafile.json +7 -7
- package/dist/local-archive-handoff.d.ts +12 -0
- package/dist/local-archive-handoff.js +21 -0
- package/dist/local-extensions.d.ts +58 -12
- package/dist/local-extensions.js +195 -55
- package/dist/local-image-fit.d.ts +30 -0
- package/dist/local-image-fit.js +105 -0
- package/dist/local-models.d.ts +4 -2
- package/dist/local-models.js +11 -6
- package/dist/local-session-archives.d.ts +8 -0
- package/dist/local-session-archives.js +152 -0
- package/dist/local-skill-source.d.ts +18 -3
- package/dist/local-skill-source.js +216 -140
- package/dist/log-event.d.ts +7 -0
- package/dist/log-event.js +8 -4
- package/dist/private-bounded-response.d.ts +2 -0
- package/dist/private-bounded-response.js +41 -0
- package/dist/private-file-credential-store.d.ts +28 -3
- package/dist/private-file-credential-store.js +57 -9
- package/dist/private-files-patch.d.ts +2 -1
- package/dist/private-files-patch.js +21 -3
- package/dist/private-fungi-account.d.ts +40 -23
- package/dist/private-fungi-account.js +201 -21
- package/dist/private-fungi-mcp-connection.d.ts +4 -0
- package/dist/private-fungi-mcp-connection.js +25 -6
- package/dist/private-fungi-mcp-selection-schema.d.ts +7 -0
- package/dist/private-fungi-mcp-selection-schema.js +15 -0
- package/dist/private-fungi-mcp-selection.d.ts +6 -6
- package/dist/private-fungi-mcp-selection.js +29 -16
- package/dist/private-fungi-model-api.js +4 -4
- package/dist/private-fungi-model-catalog.d.ts +10 -0
- package/dist/private-fungi-model-catalog.js +10 -2
- package/dist/private-fungi-model-provider.d.ts +1 -1
- package/dist/private-fungi-model-provider.js +6 -1
- package/dist/private-fungi-pending-logouts.d.ts +1 -1
- package/dist/private-fungi-pending-logouts.js +1 -2
- package/dist/private-fungi-service-logout.d.ts +44 -0
- package/dist/private-fungi-service-logout.js +59 -0
- package/dist/private-invocation-computer.d.ts +24 -3
- package/dist/private-invocation-computer.js +101 -62
- package/dist/private-invocation-files.d.ts +16 -1
- package/dist/private-invocation-files.js +154 -83
- package/dist/private-local-acp-stdio.js +3 -0
- package/dist/private-local-admission-gate.d.ts +14 -0
- package/dist/private-local-admission-gate.js +52 -0
- package/dist/private-local-agent-control.d.ts +101 -0
- package/dist/private-local-agent-control.js +30 -0
- package/dist/private-local-agent-default-model.d.ts +12 -0
- package/dist/private-local-agent-default-model.js +31 -0
- package/dist/private-local-auth-rpc.d.ts +67 -7
- package/dist/private-local-auth-rpc.js +140 -17
- package/dist/private-local-bwrap.d.ts +13 -0
- package/dist/private-local-bwrap.js +70 -0
- package/dist/private-local-client-version.d.ts +5 -0
- package/dist/private-local-client-version.js +43 -0
- package/dist/private-local-computer-attach.d.ts +3 -3
- package/dist/private-local-computer-attach.js +17 -3
- package/dist/private-local-computer-processes.d.ts +6 -3
- package/dist/private-local-computer-processes.js +55 -31
- package/dist/private-local-config-root.d.ts +21 -0
- package/dist/private-local-config-root.js +19 -0
- package/dist/private-local-containment.d.ts +51 -0
- package/dist/private-local-containment.js +161 -0
- package/dist/private-local-delegation.d.ts +10 -3
- package/dist/private-local-delegation.js +70 -15
- package/dist/private-local-effective-model.d.ts +9 -1
- package/dist/private-local-effective-model.js +3 -0
- package/dist/private-local-exec-environment.d.ts +16 -0
- package/dist/private-local-exec-environment.js +11 -0
- package/dist/private-local-linux-sandbox.d.ts +8 -0
- package/dist/private-local-linux-sandbox.js +67 -0
- package/dist/private-local-mail.d.ts +4 -2
- package/dist/private-local-mail.js +41 -33
- package/dist/private-local-mesh-enrollment.d.ts +34 -0
- package/dist/private-local-mesh-enrollment.js +188 -0
- package/dist/private-local-mesh-links.d.ts +21 -0
- package/dist/private-local-mesh-links.js +229 -0
- package/dist/private-local-mesh-record.d.ts +120 -0
- package/dist/private-local-mesh-record.js +55 -0
- package/dist/private-local-process-identity.d.ts +20 -0
- package/dist/private-local-process-identity.js +40 -0
- package/dist/private-local-runtime-host.d.ts +64 -13
- package/dist/private-local-runtime-host.js +361 -172
- package/dist/private-local-sandbox-policy.d.ts +21 -0
- package/dist/private-local-sandbox-policy.js +121 -0
- package/dist/private-local-sandbox.d.ts +37 -0
- package/dist/private-local-sandbox.js +114 -0
- package/dist/private-local-seatbelt.d.ts +24 -0
- package/dist/private-local-seatbelt.js +68 -0
- package/dist/private-local-service-contract.d.ts +91 -11
- package/dist/private-local-service-discovery.d.ts +129 -13
- package/dist/private-local-service-discovery.js +148 -47
- package/dist/private-local-service-lease.d.ts +16 -0
- package/dist/private-local-service-lease.js +105 -0
- package/dist/private-local-service-listener.d.ts +9 -2
- package/dist/private-local-service-listener.js +260 -64
- package/dist/private-local-service-log.d.ts +128 -0
- package/dist/private-local-service-log.js +321 -0
- package/dist/private-local-service-owner.d.ts +87 -1519
- package/dist/private-local-service-owner.js +1069 -71
- package/dist/private-local-service-process.js +3 -1
- package/dist/private-local-service-termination.d.ts +7 -0
- package/dist/private-local-service-termination.js +67 -0
- package/dist/private-local-service.d.ts +31 -2
- package/dist/private-local-service.js +402 -126
- package/dist/private-local-session-discovery.d.ts +52 -5
- package/dist/private-local-session-discovery.js +132 -11
- package/dist/private-local-shiitake-host.js +2 -2
- package/dist/private-local-startup-model.d.ts +51 -0
- package/dist/private-local-startup-model.js +86 -0
- package/dist/private-local-stop-all.d.ts +27 -0
- package/dist/private-local-stop-all.js +26 -0
- package/dist/private-local-ui-attachment.d.ts +19 -0
- package/dist/private-local-ui-attachment.js +71 -0
- package/dist/private-local-worker-server.d.ts +1 -0
- package/dist/private-local-worker-server.js +13 -5
- package/dist/private-mcp-form-contract.d.ts +186 -0
- package/dist/private-mcp-form-contract.js +53 -0
- package/dist/private-mcp-form-elicitation.d.ts +18 -0
- package/dist/private-mcp-form-elicitation.js +97 -0
- package/dist/private-mcp-form-session.d.ts +172 -0
- package/dist/private-mcp-form-session.js +148 -0
- package/dist/private-openai-claims.d.ts +30 -0
- package/dist/private-openai-claims.js +69 -0
- package/dist/private-oyster-mcp-auth.d.ts +13 -0
- package/dist/private-oyster-mcp-auth.js +57 -0
- package/dist/private-provider-rejection.d.ts +15 -0
- package/dist/private-provider-rejection.js +24 -0
- package/dist/private-sqlite-owner.d.ts +34 -2
- package/dist/private-sqlite-owner.js +103 -37
- package/dist/private-team-computers.d.ts +4 -4
- package/dist/private-team-computers.js +11 -2
- package/dist/seatbelt/safe-base.sbpl +79 -0
- package/dist/subscription-activation.d.ts +9 -0
- package/dist/subscription-activation.js +62 -0
- package/dist/subscription-identity.d.ts +23 -0
- package/dist/subscription-identity.js +124 -0
- package/dist/subscription-turn.d.ts +17 -0
- package/dist/subscription-turn.js +143 -0
- package/dist/team-apps.d.ts +2 -2
- package/dist/team-apps.js +7 -2
- package/native/bin/LICENSE.bubblewrap +502 -0
- package/native/bin/LICENSE.libcap +112 -0
- package/native/bin/LICENSE.sandbox-runtime +201 -0
- package/native/bin/SOURCE.bubblewrap.tar.xz +0 -0
- package/native/bin/bwrap-build-source/Dockerfile +48 -0
- package/native/bin/bwrap-build-source/README.md +96 -0
- package/native/bin/bwrap-build-source/build.mjs +52 -0
- package/native/bin/bwrap-build-source/linux-arm64.ini +14 -0
- package/native/bin/bwrap-build-source/pin.mjs +21 -0
- package/native/bin/bwrap-build-source/record.mjs +74 -0
- package/native/bin/bwrap-build-source/vendor/LICENSE.sandbox-runtime +201 -0
- package/native/bin/bwrap-build-source/vendor/README.md +23 -0
- package/native/bin/bwrap-build-source/vendor/seccomp-unix-block.c +280 -0
- package/native/bin/bwrap-build.json +44 -0
- package/native/bin/fungi-bwrap +0 -0
- package/native/bin/fungi-bwrap.sha256 +1 -0
- package/native/bin/fungi-seccomp.bpf +0 -0
- package/native/bin/fungi-seccomp.bpf.sha256 +1 -0
- package/package.json +44 -31
- package/dist/private-local-model-settings.d.ts +0 -5
- package/dist/private-local-model-settings.js +0 -31
- package/dist/private-local-subagents.d.ts +0 -27
- 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 {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
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
|
-
|
|
18
|
-
draining ??= input.closeOwners();
|
|
19
|
-
return draining;
|
|
20
|
-
};
|
|
30
|
+
let phase = "active";
|
|
21
31
|
const finish = () => {
|
|
22
32
|
finishing ??= (async () => {
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
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 {
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
67
|
-
return context.
|
|
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
|
-
|
|
74
|
-
|
|
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/${
|
|
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
|
-
|
|
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:
|
|
168
|
+
effectiveModel: (sessionId, signal) => owners.effectiveModel(target, sessionId, signal),
|
|
169
|
+
sandboxStatus: (signal) => owners.sandboxStatus(target, signal),
|
|
86
170
|
};
|
|
87
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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) =>
|
|
137
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
203
|
-
process.on("
|
|
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",
|
|
208
|
-
process.off("SIGTERM",
|
|
209
|
-
|
|
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;
|