@fungi.computer/truffle 0.2.2 → 0.3.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/dist/private-files-patch.js +3 -17
- package/dist/private-invocation-computer.d.ts +224 -12
- package/dist/private-invocation-computer.js +162 -22
- package/dist/private-invocation-files.d.ts +3 -2
- package/dist/private-invocation-files.js +22 -3
- package/dist/private-local-agent-control.d.ts +2 -2
- package/dist/private-local-bwrap.d.ts +9 -7
- package/dist/private-local-bwrap.js +26 -21
- package/dist/private-local-computer-processes.d.ts +15 -0
- package/dist/private-local-computer-processes.js +13 -0
- package/dist/private-local-containment.d.ts +15 -13
- package/dist/private-local-containment.js +49 -65
- package/dist/private-local-delegation.d.ts +3 -3
- package/dist/private-local-delegation.js +32 -19
- package/dist/private-local-exec-review.d.ts +12 -0
- package/dist/private-local-exec-review.js +62 -0
- package/dist/private-local-exec-rules.d.ts +16 -0
- package/dist/private-local-exec-rules.js +441 -0
- package/dist/private-local-git-exclude.js +25 -15
- package/dist/private-local-linux-sandbox.d.ts +5 -5
- package/dist/private-local-linux-sandbox.js +63 -30
- package/dist/private-local-mail.d.ts +2 -2
- package/dist/private-local-mesh-links.d.ts +4 -1
- package/dist/private-local-mesh-links.js +50 -3
- package/dist/private-local-native-digests.js +1 -1
- package/dist/private-local-native-install-error.d.ts +2 -0
- package/dist/private-local-native-install-error.js +10 -0
- package/dist/private-local-native-staging.d.ts +7 -8
- package/dist/private-local-native-staging.js +132 -43
- package/dist/private-local-permission-mode.d.ts +10 -0
- package/dist/private-local-permission-mode.js +18 -0
- package/dist/private-local-runtime-host.d.ts +28 -5
- package/dist/private-local-runtime-host.js +404 -66
- package/dist/private-local-sandbox-policy.d.ts +30 -11
- package/dist/private-local-sandbox-policy.js +223 -373
- package/dist/private-local-sandbox.d.ts +30 -25
- package/dist/private-local-sandbox.js +190 -100
- package/dist/private-local-seatbelt.d.ts +7 -13
- package/dist/private-local-seatbelt.js +55 -32
- package/dist/private-local-service-contract.d.ts +5 -4
- package/dist/private-local-service-discovery.d.ts +19 -7
- package/dist/private-local-service-discovery.js +4 -3
- package/dist/private-local-service-listener.js +40 -2
- package/dist/private-local-service-log.d.ts +18 -0
- package/dist/private-local-service-log.js +6 -0
- package/dist/private-local-service-owner.d.ts +18 -10
- package/dist/private-local-service-owner.js +119 -25
- package/dist/private-local-service.js +30 -3
- package/dist/private-local-session-discovery.d.ts +2 -1
- package/dist/private-local-session-discovery.js +18 -6
- package/dist/private-mcp-form-contract.d.ts +10 -1
- package/dist/private-mcp-form-contract.js +10 -1
- package/dist/private-mcp-form-session.d.ts +39 -3
- package/dist/private-mcp-form-session.js +28 -5
- package/dist/private-oyster-mcp-auth.d.ts +1 -1
- package/dist/private-oyster-mcp-auth.js +139 -7
- package/dist/private-provider-rejection.js +1 -1
- package/dist/seatbelt/safe-base.sbpl +2 -2
- package/native/bin/bwrap-build-source/Dockerfile +2 -0
- package/native/bin/bwrap-build-source/README.md +6 -6
- package/native/bin/bwrap-build-source/pin.mjs +2 -2
- package/native/bin/bwrap-build-source/record.mjs +12 -1
- package/native/bin/bwrap-build-source/vendor/seccomp-unix-block.c +10 -7
- package/native/bin/bwrap-build.json +3 -2
- package/native/bin/fungi-apply-patch +0 -0
- package/native/bin/fungi-seccomp-workspace.bpf +0 -0
- package/native/bin/fungi-seccomp-workspace.bpf.sha256 +1 -0
- package/package.json +23 -17
|
@@ -4,7 +4,7 @@ export { sandboxStatusSchema } from "./private-local-sandbox.js";
|
|
|
4
4
|
* The client-service wire contract. Bump it with any change an older client
|
|
5
5
|
* cannot read; a running client then asks its window to restart.
|
|
6
6
|
*/
|
|
7
|
-
export declare const privateLocalServiceProtocol =
|
|
7
|
+
export declare const privateLocalServiceProtocol = 4;
|
|
8
8
|
/** This build's identity: the packed CLI's content hash, or `source` for a source run. */
|
|
9
9
|
export declare function privateLocalServiceVersion(): string;
|
|
10
10
|
/**
|
|
@@ -19,7 +19,7 @@ declare const serviceLifecycleSchema: z.ZodObject<{
|
|
|
19
19
|
startedAt: z.ZodString;
|
|
20
20
|
}, z.core.$loose>;
|
|
21
21
|
declare const serviceRecordSchema: z.ZodObject<{
|
|
22
|
-
protocol: z.ZodLiteral<
|
|
22
|
+
protocol: z.ZodLiteral<4>;
|
|
23
23
|
baseUrl: z.ZodURL;
|
|
24
24
|
version: z.ZodString;
|
|
25
25
|
serviceId: z.ZodString & z.ZodType<import("@fungi.computer/cairn").CairnId<"local-service", "service", 1>, string, z.core.$ZodTypeInternals<import("@fungi.computer/cairn").CairnId<"local-service", "service", 1>, string>>;
|
|
@@ -29,7 +29,7 @@ declare const serviceRecordSchema: z.ZodObject<{
|
|
|
29
29
|
export type ServiceRecord = z.infer<typeof serviceRecordSchema>;
|
|
30
30
|
/** Mandatory live diagnostics; never written into the registration pointer. */
|
|
31
31
|
export declare const liveServiceStatusSchema: z.ZodObject<{
|
|
32
|
-
protocol: z.ZodLiteral<
|
|
32
|
+
protocol: z.ZodLiteral<4>;
|
|
33
33
|
baseUrl: z.ZodURL;
|
|
34
34
|
version: z.ZodString;
|
|
35
35
|
serviceId: z.ZodString & z.ZodType<import("@fungi.computer/cairn").CairnId<"local-service", "service", 1>, string, z.core.$ZodTypeInternals<import("@fungi.computer/cairn").CairnId<"local-service", "service", 1>, string>>;
|
|
@@ -76,7 +76,11 @@ export declare const selectionSchema: z.ZodObject<{
|
|
|
76
76
|
}>>;
|
|
77
77
|
invocationRoot: z.ZodString;
|
|
78
78
|
clientPath: z.ZodString;
|
|
79
|
-
|
|
79
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
80
|
+
"read-only": "read-only";
|
|
81
|
+
workspace: "workspace";
|
|
82
|
+
"full-access": "full-access";
|
|
83
|
+
}>>;
|
|
80
84
|
databasePath: z.ZodOptional<z.ZodString>;
|
|
81
85
|
sessionId: z.ZodOptional<z.ZodString>;
|
|
82
86
|
fungiMcp: z.ZodOptional<z.ZodObject<{
|
|
@@ -91,7 +95,11 @@ export declare const sessionCreationSchema: z.ZodObject<{
|
|
|
91
95
|
}>>;
|
|
92
96
|
invocationRoot: z.ZodString;
|
|
93
97
|
clientPath: z.ZodString;
|
|
94
|
-
|
|
98
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
99
|
+
"read-only": "read-only";
|
|
100
|
+
workspace: "workspace";
|
|
101
|
+
"full-access": "full-access";
|
|
102
|
+
}>>;
|
|
95
103
|
databasePath: z.ZodString;
|
|
96
104
|
requestId: z.ZodString;
|
|
97
105
|
fungiMcp: z.ZodOptional<z.ZodObject<{
|
|
@@ -100,13 +108,17 @@ export declare const sessionCreationSchema: z.ZodObject<{
|
|
|
100
108
|
}, z.core.$strict>>;
|
|
101
109
|
}, z.core.$strict>;
|
|
102
110
|
export declare const sessionInspectionSchema: z.ZodObject<{
|
|
111
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
112
|
+
"read-only": "read-only";
|
|
113
|
+
workspace: "workspace";
|
|
114
|
+
"full-access": "full-access";
|
|
115
|
+
}>>;
|
|
103
116
|
databasePath: z.ZodOptional<z.ZodString>;
|
|
104
117
|
invocationRoot: z.ZodString;
|
|
105
118
|
fungiMcp: z.ZodOptional<z.ZodObject<{
|
|
106
119
|
endpoint: z.ZodPipe<z.ZodURL, z.ZodString>;
|
|
107
120
|
authentication: z.ZodOptional<z.ZodLiteral<"oyster-key">>;
|
|
108
121
|
}, z.core.$strict>>;
|
|
109
|
-
requireSandbox: z.ZodOptional<z.ZodBoolean>;
|
|
110
122
|
sessionId: z.ZodString;
|
|
111
123
|
limit: z.ZodNumber;
|
|
112
124
|
}, z.core.$strict>;
|
|
@@ -131,10 +143,10 @@ export declare const archiveRefusalSchema: z.ZodObject<{
|
|
|
131
143
|
reason: z.ZodEnum<{
|
|
132
144
|
pending: "pending";
|
|
133
145
|
running: "running";
|
|
134
|
-
unreadable: "unreadable";
|
|
135
146
|
archived: "archived";
|
|
136
147
|
not_creator: "not_creator";
|
|
137
148
|
self: "self";
|
|
149
|
+
unreadable: "unreadable";
|
|
138
150
|
}>;
|
|
139
151
|
error: z.ZodString;
|
|
140
152
|
}, z.core.$strict>;
|
|
@@ -6,6 +6,7 @@ import { join, resolve } from "node:path";
|
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import lockfile from "proper-lockfile";
|
|
8
8
|
import { z } from "zod";
|
|
9
|
+
import { localPermissionModeSchema } from "./private-local-permission-mode.js";
|
|
9
10
|
import { localServiceIds } from "./private-local-service-ids.js";
|
|
10
11
|
import { acquireServiceOwnerLease, readServiceLeaseOwner, } from "./private-local-service-lease.js";
|
|
11
12
|
import { modelReasoningLevels } from "@fungi.computer/shiitake/model-preference";
|
|
@@ -16,7 +17,7 @@ export { sandboxStatusSchema } from "./private-local-sandbox.js";
|
|
|
16
17
|
* The client-service wire contract. Bump it with any change an older client
|
|
17
18
|
* cannot read; a running client then asks its window to restart.
|
|
18
19
|
*/
|
|
19
|
-
export const privateLocalServiceProtocol =
|
|
20
|
+
export const privateLocalServiceProtocol = 4;
|
|
20
21
|
/** This build's identity: the packed CLI's content hash, or `source` for a source run. */
|
|
21
22
|
export function privateLocalServiceVersion() {
|
|
22
23
|
return (z
|
|
@@ -64,7 +65,7 @@ export const selectionSchema = z.strictObject({
|
|
|
64
65
|
uiAttachmentLifetime: localUiAttachmentLifetimeSchema.optional(),
|
|
65
66
|
invocationRoot: z.string().min(1),
|
|
66
67
|
clientPath: privateLocalExecPathSchema,
|
|
67
|
-
|
|
68
|
+
mode: localPermissionModeSchema.optional(),
|
|
68
69
|
databasePath: z.string().min(1).optional(),
|
|
69
70
|
sessionId: z.string().min(1).optional(),
|
|
70
71
|
fungiMcp: fungiMcpSelectionSchema.optional(),
|
|
@@ -73,7 +74,7 @@ export const sessionCreationSchema = z.strictObject({
|
|
|
73
74
|
uiAttachmentLifetime: localUiAttachmentLifetimeSchema.optional(),
|
|
74
75
|
invocationRoot: z.string().min(1),
|
|
75
76
|
clientPath: privateLocalExecPathSchema,
|
|
76
|
-
|
|
77
|
+
mode: localPermissionModeSchema.optional(),
|
|
77
78
|
databasePath: z.string().min(1),
|
|
78
79
|
requestId: z.string().min(1),
|
|
79
80
|
fungiMcp: fungiMcpSelectionSchema.optional(),
|
|
@@ -6,6 +6,10 @@ import { createServiceLogLifecycle, } from "./private-local-service-log.js";
|
|
|
6
6
|
import { stopAllRequestSchema } from "./private-local-stop-all.js";
|
|
7
7
|
import { FungiMcpSelectionConflict } from "./private-fungi-mcp-selection.js";
|
|
8
8
|
import { LocalSandboxUnavailable } from "./private-local-containment.js";
|
|
9
|
+
import { createNativePayloadStaging, } from "./private-local-native-staging.js";
|
|
10
|
+
import { nativePackageStartupError } from "./private-local-native-install-error.js";
|
|
11
|
+
import { LocalBwrapUnavailable } from "./private-local-bwrap.js";
|
|
12
|
+
import { writeFileSync } from "node:fs";
|
|
9
13
|
import { PrivateLocalAuthTarget } from "./private-local-auth-rpc.js";
|
|
10
14
|
import { runLocalAuthCli } from "./auth-cli.js";
|
|
11
15
|
import { capturePrivateLocalConfigRoot, } from "./private-local-config-root.js";
|
|
@@ -88,7 +92,12 @@ readRecoveryManifest, root = capturePrivateLocalConfigRoot()) {
|
|
|
88
92
|
heap: jsc.heapStats(),
|
|
89
93
|
}),
|
|
90
94
|
});
|
|
91
|
-
|
|
95
|
+
let native;
|
|
96
|
+
const owners = createLocalServiceOwners(openRuntime, directory, undefined, idleSweepMs, readRecoveryManifest, diagnostics.recoveryDisabled, root, undefined, diagnostics.ruleMatch, () => {
|
|
97
|
+
if (native === undefined)
|
|
98
|
+
throw new Error("Local service native staging is not ready");
|
|
99
|
+
return native;
|
|
100
|
+
}, diagnostics.approvalDecision);
|
|
92
101
|
const prefix = `/service/${serviceId}`;
|
|
93
102
|
const releaseAccountOwner = registerFungiAccountOwner(root.credentialPath, () => owners.refreshMesh());
|
|
94
103
|
const app = new Hono();
|
|
@@ -182,7 +191,11 @@ readRecoveryManifest, root = capturePrivateLocalConfigRoot()) {
|
|
|
182
191
|
};
|
|
183
192
|
};
|
|
184
193
|
const shutdown = createPrivateLocalServiceShutdown({
|
|
185
|
-
closeOwners:
|
|
194
|
+
closeOwners: async () => {
|
|
195
|
+
await owners.close();
|
|
196
|
+
// No Agent, including one reopened during recovery, still uses the copy.
|
|
197
|
+
native?.close();
|
|
198
|
+
},
|
|
186
199
|
closeServer: async () => {
|
|
187
200
|
await server?.close();
|
|
188
201
|
},
|
|
@@ -233,6 +246,31 @@ readRecoveryManifest, root = capturePrivateLocalConfigRoot()) {
|
|
|
233
246
|
const isLocalRequest = (request) => !request.headers.has("origin") &&
|
|
234
247
|
new URL(request.url).hostname === "127.0.0.1";
|
|
235
248
|
try {
|
|
249
|
+
// Verify and stage before publishing discovery, even without a selected Agent.
|
|
250
|
+
try {
|
|
251
|
+
native = createNativePayloadStaging(directory);
|
|
252
|
+
}
|
|
253
|
+
catch (cause) {
|
|
254
|
+
if (cause instanceof LocalBwrapUnavailable) {
|
|
255
|
+
// A detached child has no stderr. Only this classified startup refusal
|
|
256
|
+
// crosses to the starting client; never export arbitrary exception text.
|
|
257
|
+
const marker = process.env.FUNGI_NATIVE_STARTUP_REFUSAL;
|
|
258
|
+
if (marker !== undefined) {
|
|
259
|
+
try {
|
|
260
|
+
writeFileSync(marker, "native-package", {
|
|
261
|
+
flag: "wx",
|
|
262
|
+
mode: 0o600,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
catch (reportFailure) {
|
|
266
|
+
// A broken report channel cannot replace the native refusal.
|
|
267
|
+
logEvent("service-listener.native-refusal-report", reportFailure);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
throw nativePackageStartupError(cause);
|
|
271
|
+
}
|
|
272
|
+
throw cause;
|
|
273
|
+
}
|
|
236
274
|
app.use("*", async (context, next) => {
|
|
237
275
|
const request = context.req.raw;
|
|
238
276
|
// Local client only: no browser-origin admission or DNS rebinding authority.
|
|
@@ -50,6 +50,18 @@ export type ServiceLogEvent = Readonly<{
|
|
|
50
50
|
}> | Readonly<{
|
|
51
51
|
event: "recovery-persistence-disabled";
|
|
52
52
|
code: RecoveryPersistenceFailureCode;
|
|
53
|
+
}> | Readonly<{
|
|
54
|
+
event: "exec-rule-match";
|
|
55
|
+
agentId: string;
|
|
56
|
+
executable: string;
|
|
57
|
+
ruleId: string;
|
|
58
|
+
}> | Readonly<{
|
|
59
|
+
event: "exec-approval-decision";
|
|
60
|
+
agentId: string;
|
|
61
|
+
recordId: string;
|
|
62
|
+
decision: "approve" | "deny";
|
|
63
|
+
scope?: "once" | "always";
|
|
64
|
+
ruleId?: string;
|
|
53
65
|
}> | Readonly<{
|
|
54
66
|
event: "memory";
|
|
55
67
|
rss: number;
|
|
@@ -80,6 +92,12 @@ export type ServiceLogLifecycle = Readonly<{
|
|
|
80
92
|
ready: (serviceId: string) => void;
|
|
81
93
|
stopping: (reason: ServiceLogShutdownReason) => void;
|
|
82
94
|
failure: (failure: FailureMetadata) => void;
|
|
95
|
+
ruleMatch: (match: Omit<Extract<ServiceLogEvent, {
|
|
96
|
+
event: "exec-rule-match";
|
|
97
|
+
}>, "event">) => void;
|
|
98
|
+
approvalDecision: (decision: Omit<Extract<ServiceLogEvent, {
|
|
99
|
+
event: "exec-approval-decision";
|
|
100
|
+
}>, "event">) => void;
|
|
83
101
|
recoveryDisabled: (code: RecoveryPersistenceFailureCode) => void;
|
|
84
102
|
/** Synchronous, so it lands before the bounded drain exits the process. */
|
|
85
103
|
drainTimeout: (drain: Readonly<{
|
|
@@ -287,6 +287,12 @@ export function createServiceLogLifecycle(input) {
|
|
|
287
287
|
failure(failure) {
|
|
288
288
|
emit({ event: "failure", failure });
|
|
289
289
|
},
|
|
290
|
+
ruleMatch(match) {
|
|
291
|
+
emit({ event: "exec-rule-match", ...match });
|
|
292
|
+
},
|
|
293
|
+
approvalDecision(decision) {
|
|
294
|
+
emit({ event: "exec-approval-decision", ...decision });
|
|
295
|
+
},
|
|
290
296
|
recoveryDisabled(code) {
|
|
291
297
|
if (closed || recovery !== undefined)
|
|
292
298
|
return;
|
|
@@ -6,7 +6,8 @@ import { type LocalExtensions } from "./local-extensions.js";
|
|
|
6
6
|
import type { PrivateLocalServiceSelection, PrivateLocalSessionCreation } from "./private-local-service-contract.js";
|
|
7
7
|
import { openPrivateLocalRuntimeHost, type PrivateLocalRuntimeHost } from "./private-local-runtime-host.js";
|
|
8
8
|
import type { LocalUiAttachment } from "./private-local-ui-attachment.js";
|
|
9
|
-
|
|
9
|
+
import { createLocalMeshLinks } from "./private-local-mesh-links.js";
|
|
10
|
+
export type LocalServiceRuntime = Pick<PrivateLocalRuntimeHost, "databasePath" | "agentId" | "name" | "getSession" | "effectiveModel" | "setModel" | "setArchived" | "setMode" | "sandboxStatus" | "pendingDelegation" | "assertModelReady" | "listSessions" | "createSession" | "deleteSession" | "extensions" | "prepareReload" | "bindSessionPath" | "acceptMail" | "readSettlement" | "idle" | "close">;
|
|
10
11
|
export declare class LocalSessionPathConflict extends Error {
|
|
11
12
|
readonly name = "LocalSessionPathConflict";
|
|
12
13
|
constructor();
|
|
@@ -35,7 +36,9 @@ idleSweepMs?: number,
|
|
|
35
36
|
/** Read the recovery manifest; tests can hold this I/O independently of opens. */
|
|
36
37
|
readRecoveryManifest?: (path: string) => Promise<string>,
|
|
37
38
|
/** Report only the classification, never the manifest or exception text. */
|
|
38
|
-
onRecoveryDisabled?: ServiceLogLifecycle["recoveryDisabled"], configRoot?: PrivateLocalConfigRoot
|
|
39
|
+
onRecoveryDisabled?: ServiceLogLifecycle["recoveryDisabled"], configRoot?: PrivateLocalConfigRoot, meshLinksFactory?: typeof createLocalMeshLinks, onExecRuleMatch?: NonNullable<Parameters<typeof openPrivateLocalRuntimeHost>[0]["onExecRuleMatch"]>,
|
|
40
|
+
/** Supplied by the listener after startup; individual Agents never close it. */
|
|
41
|
+
serviceNative?: () => NonNullable<Parameters<typeof openPrivateLocalRuntimeHost>[0]["native"]>, onExecApprovalDecision?: NonNullable<Parameters<typeof openPrivateLocalRuntimeHost>[0]["onExecApprovalDecision"]>): {
|
|
39
42
|
refreshMesh: () => Promise<void>;
|
|
40
43
|
createSession: (creation: PrivateLocalSessionCreation) => Promise<{
|
|
41
44
|
agentId: string;
|
|
@@ -59,6 +62,10 @@ onRecoveryDisabled?: ServiceLogLifecycle["recoveryDisabled"], configRoot?: Priva
|
|
|
59
62
|
set: (preference: import("@fungi.computer/shiitake").ModelPreference | null) => Promise<void>;
|
|
60
63
|
}>;
|
|
61
64
|
read: (input?: import("@fungi.computer/shiitake").ReadOptions) => Promise<import("@fungi.computer/shiitake").SessionSnapshot>;
|
|
65
|
+
readSettlement: (commandId: string, options?: Readonly<{
|
|
66
|
+
timeoutMs?: number;
|
|
67
|
+
signal?: AbortSignal;
|
|
68
|
+
}>) => Promise<import("@fungi.computer/shiitake").CommandSettlement | undefined>;
|
|
62
69
|
subscribe: (input: import("@fungi.computer/shiitake").SubscribeOptions) => AsyncIterable<import("@fungi.computer/shiitake").SessionFrame>;
|
|
63
70
|
watch: (input?: import("@fungi.computer/shiitake").WatchOptions) => Promise<import("@fungi.computer/shiitake").SessionWatch>;
|
|
64
71
|
}>;
|
|
@@ -87,6 +94,10 @@ onRecoveryDisabled?: ServiceLogLifecycle["recoveryDisabled"], configRoot?: Priva
|
|
|
87
94
|
set: (preference: import("@fungi.computer/shiitake").ModelPreference | null) => Promise<void>;
|
|
88
95
|
}>;
|
|
89
96
|
read: (input?: import("@fungi.computer/shiitake").ReadOptions) => Promise<import("@fungi.computer/shiitake").SessionSnapshot>;
|
|
97
|
+
readSettlement: (commandId: string, options?: Readonly<{
|
|
98
|
+
timeoutMs?: number;
|
|
99
|
+
signal?: AbortSignal;
|
|
100
|
+
}>) => Promise<import("@fungi.computer/shiitake").CommandSettlement | undefined>;
|
|
90
101
|
subscribe: (input: import("@fungi.computer/shiitake").SubscribeOptions) => AsyncIterable<import("@fungi.computer/shiitake").SessionFrame>;
|
|
91
102
|
watch: (input?: import("@fungi.computer/shiitake").WatchOptions) => Promise<import("@fungi.computer/shiitake").SessionWatch>;
|
|
92
103
|
}>;
|
|
@@ -108,15 +119,12 @@ onRecoveryDisabled?: ServiceLogLifecycle["recoveryDisabled"], configRoot?: Priva
|
|
|
108
119
|
reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | undefined;
|
|
109
120
|
}>;
|
|
110
121
|
sandboxStatus: (selection: Pick<PrivateLocalServiceSelection, "invocationRoot" | "databasePath" | "fungiMcp">, signal: AbortSignal) => Promise<{
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
requireSandbox: boolean;
|
|
116
|
-
reason: {
|
|
117
|
-
kind: "missing" | "missing-artifact" | "invalid-digest" | "digest-mismatch" | "unreadable" | "not-executable" | "staging-failed" | "unsupported-platform" | "invalid-folder" | "unsafe-native-payload" | "unsafe-host-code" | "startup-refused" | "backend-refused";
|
|
122
|
+
mode: "read-only" | "workspace" | "full-access";
|
|
123
|
+
available: boolean;
|
|
124
|
+
reason?: {
|
|
125
|
+
kind: "install" | "unsupported" | "folder" | "startup-refused";
|
|
118
126
|
message: string;
|
|
119
|
-
};
|
|
127
|
+
} | undefined;
|
|
120
128
|
}>;
|
|
121
129
|
sweepIdle: () => Promise<void>;
|
|
122
130
|
archive: (request: Readonly<{
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
|
+
import { parseDelegationReceipt } from "@fungi.computer/shiitake/client";
|
|
2
3
|
import { logEvent } from "./log-event.js";
|
|
4
|
+
import { modeWithinCeiling } from "./private-local-permission-mode.js";
|
|
3
5
|
import { createPrivateModels } from "./local-models.js";
|
|
4
6
|
import { capturePrivateLocalConfigRoot, } from "./private-local-config-root.js";
|
|
5
7
|
import { AdmissionGate } from "./private-local-admission-gate.js";
|
|
@@ -19,7 +21,6 @@ import { createPrivateLocalSessionRequestId, openPrivateLocalRuntimeHost, resolv
|
|
|
19
21
|
import { parsePrivateLocalExecPath } from "./private-local-exec-path.js";
|
|
20
22
|
import { createLocalMeshLinks, localMeshLinkKey, } from "./private-local-mesh-links.js";
|
|
21
23
|
import { agentMailAddressSchema, machineAgentRequestSchema, machineDirectorySchema, } from "@fungi.computer/seam/machine-link-protocol";
|
|
22
|
-
import { parseAccepted } from "@fungi.computer/shiitake/client";
|
|
23
24
|
import { commandSettlementSchema } from "@fungi.computer/shiitake/fleet";
|
|
24
25
|
import { BirdDogMailSchema } from "@fungi.computer/bird-dog/effect";
|
|
25
26
|
export class LocalSessionPathConflict extends Error {
|
|
@@ -44,7 +45,10 @@ export class LocalAgentSelectionAmbiguous extends Error {
|
|
|
44
45
|
// Reopen inputs only. Credentials remain with Local's account custodian.
|
|
45
46
|
const recoveryOwnerSchema = selectionSchema
|
|
46
47
|
.omit({ clientPath: true, sessionId: true, uiAttachmentLifetime: true })
|
|
47
|
-
.extend({
|
|
48
|
+
.extend({
|
|
49
|
+
databasePath: z.string().min(1),
|
|
50
|
+
wakeAt: z.number().optional(),
|
|
51
|
+
});
|
|
48
52
|
const decodeRecoveryFile = Schema.decodeUnknownResult(Schema.fromJsonString(Schema.Array(Schema.Unknown)));
|
|
49
53
|
class LocalRecoveryPersistenceDisabled extends Error {
|
|
50
54
|
reason;
|
|
@@ -65,12 +69,40 @@ idleSweepMs = 60_000,
|
|
|
65
69
|
/** Read the recovery manifest; tests can hold this I/O independently of opens. */
|
|
66
70
|
readRecoveryManifest = (path) => readFile(path, "utf8"),
|
|
67
71
|
/** Report only the classification, never the manifest or exception text. */
|
|
68
|
-
onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot())
|
|
72
|
+
onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot(), meshLinksFactory = createLocalMeshLinks, onExecRuleMatch = () => undefined,
|
|
73
|
+
/** Supplied by the listener after startup; individual Agents never close it. */
|
|
74
|
+
serviceNative, onExecApprovalDecision = () => undefined) {
|
|
69
75
|
const extensions = suppliedExtensions ?? localExtensions({ home: configRoot.home });
|
|
70
76
|
const recoveryPath = recoveryDirectory === undefined
|
|
71
77
|
? undefined
|
|
72
78
|
: resolve(recoveryDirectory, "owners.json");
|
|
73
79
|
const recovery = new Map();
|
|
80
|
+
const wakes = new Map();
|
|
81
|
+
let wakeTimer;
|
|
82
|
+
function scheduleWakes() {
|
|
83
|
+
if (wakeTimer !== undefined)
|
|
84
|
+
clearTimeout(wakeTimer);
|
|
85
|
+
wakeTimer = undefined;
|
|
86
|
+
if (stopping || wakes.size === 0)
|
|
87
|
+
return;
|
|
88
|
+
let earliest = Infinity;
|
|
89
|
+
for (const entry of wakes.values())
|
|
90
|
+
earliest = Math.min(earliest, entry.wakeAt ?? Infinity);
|
|
91
|
+
wakeTimer = setTimeout(() => {
|
|
92
|
+
wakeTimer = undefined;
|
|
93
|
+
if (stopping)
|
|
94
|
+
return;
|
|
95
|
+
const due = [...wakes.values()].filter((entry) => entry.wakeAt !== undefined && entry.wakeAt <= Date.now());
|
|
96
|
+
for (const entry of due) {
|
|
97
|
+
wakes.delete(entry.databasePath);
|
|
98
|
+
void ownerFor(entry).catch((cause) => {
|
|
99
|
+
logEvent("service-owner.wake", cause);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
scheduleWakes();
|
|
103
|
+
}, Math.min(2_147_483_647, Math.max(0, earliest - Date.now())));
|
|
104
|
+
wakeTimer.unref();
|
|
105
|
+
}
|
|
74
106
|
let loadingRecovery;
|
|
75
107
|
let saving = Promise.resolve();
|
|
76
108
|
const emptySessions = new Map();
|
|
@@ -87,7 +119,7 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
87
119
|
}
|
|
88
120
|
return undefined;
|
|
89
121
|
}
|
|
90
|
-
const mesh =
|
|
122
|
+
const mesh = meshLinksFactory({
|
|
91
123
|
credentialPath: configRoot.credentialPath,
|
|
92
124
|
receive: async (record, envelope) => {
|
|
93
125
|
if (envelope.kind === "machineDirectory") {
|
|
@@ -142,13 +174,18 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
142
174
|
})(envelope.event);
|
|
143
175
|
if (envelope.kind !== "agentMail" ||
|
|
144
176
|
envelope.key !== mail.key ||
|
|
145
|
-
mail.to.agentId !== address.agentId
|
|
146
|
-
new TextEncoder().encode(JSON.stringify(mail)).byteLength > 256 * 1024)
|
|
177
|
+
mail.to.agentId !== address.agentId)
|
|
147
178
|
return "refused";
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
179
|
+
if (mail.from.sessionId === undefined &&
|
|
180
|
+
!(mail.replyTo.length === 0 && mail.reply?.outcome === "undeliverable"))
|
|
181
|
+
return "refused";
|
|
182
|
+
const sessionlessAcceptance = mail.to.sessionId === undefined;
|
|
183
|
+
// Only select the physical store here. Never bypass that runtime's
|
|
184
|
+
// recipient/sender admission; request keys make acceptance replay-safe.
|
|
185
|
+
const command = await admitted(owner, () => owner.acceptMail(mail));
|
|
186
|
+
if (sessionlessAcceptance && command.sessionId === undefined)
|
|
187
|
+
throw new Error("Sessionless mail acceptance returned no Session");
|
|
188
|
+
return { status: "accepted", receipt: command };
|
|
152
189
|
}
|
|
153
190
|
finally {
|
|
154
191
|
release();
|
|
@@ -295,6 +332,8 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
295
332
|
});
|
|
296
333
|
pending = pending.then(async (owner) => {
|
|
297
334
|
roots.set(owner.runtime.databasePath, invocationRoot);
|
|
335
|
+
if (wakes.delete(owner.runtime.databasePath))
|
|
336
|
+
scheduleWakes();
|
|
298
337
|
if (recoveryPath !== undefined) {
|
|
299
338
|
const reference = {
|
|
300
339
|
invocationRoot,
|
|
@@ -523,9 +562,12 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
523
562
|
}
|
|
524
563
|
let runtime;
|
|
525
564
|
let idle;
|
|
565
|
+
let wakeAt;
|
|
526
566
|
try {
|
|
527
567
|
({ runtime } = await pending);
|
|
528
|
-
|
|
568
|
+
const state = await runtime.idle();
|
|
569
|
+
idle = state.idle;
|
|
570
|
+
wakeAt = state.wakeAt;
|
|
529
571
|
}
|
|
530
572
|
catch (cause) {
|
|
531
573
|
// One unreadable Agent stays open; the sweep still checks the rest.
|
|
@@ -542,7 +584,7 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
542
584
|
idleCandidates.add(databasePath);
|
|
543
585
|
continue;
|
|
544
586
|
}
|
|
545
|
-
closed.push(releaseStore(databasePath, pending, runtime));
|
|
587
|
+
closed.push(releaseStore(databasePath, pending, runtime, wakeAt));
|
|
546
588
|
}
|
|
547
589
|
await Promise.all(closed);
|
|
548
590
|
})().finally(() => {
|
|
@@ -550,7 +592,18 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
550
592
|
});
|
|
551
593
|
return sweeping;
|
|
552
594
|
}
|
|
553
|
-
|
|
595
|
+
/** Forget empty Agents; retain a future wake for the next service owner. */
|
|
596
|
+
function rememberIdleWake(databasePath, wakeAt) {
|
|
597
|
+
const reference = recovery.get(databasePath);
|
|
598
|
+
if (wakeAt === null || reference === undefined) {
|
|
599
|
+
recovery.delete(databasePath);
|
|
600
|
+
return undefined;
|
|
601
|
+
}
|
|
602
|
+
const scheduled = { ...reference, wakeAt };
|
|
603
|
+
recovery.set(databasePath, scheduled);
|
|
604
|
+
return scheduled;
|
|
605
|
+
}
|
|
606
|
+
function releaseStore(databasePath, pending, runtime, wakeAt) {
|
|
554
607
|
idleCandidates.delete(databasePath);
|
|
555
608
|
released.add(runtime);
|
|
556
609
|
for (const [key, value] of owners)
|
|
@@ -558,7 +611,7 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
558
611
|
owners.delete(key);
|
|
559
612
|
emptySessions.delete(databasePath);
|
|
560
613
|
roots.delete(databasePath);
|
|
561
|
-
|
|
614
|
+
const scheduled = rememberIdleWake(databasePath, wakeAt);
|
|
562
615
|
const opened = openedByStore.get(databasePath);
|
|
563
616
|
openedByStore.delete(databasePath);
|
|
564
617
|
if (opened !== undefined)
|
|
@@ -570,6 +623,10 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
570
623
|
catch (cause) {
|
|
571
624
|
logEvent("service-owner.release-runtime", cause);
|
|
572
625
|
}
|
|
626
|
+
if (scheduled !== undefined && !stopping) {
|
|
627
|
+
wakes.set(databasePath, scheduled);
|
|
628
|
+
scheduleWakes();
|
|
629
|
+
}
|
|
573
630
|
await saveRecovery();
|
|
574
631
|
})();
|
|
575
632
|
releasing.set(databasePath, closed);
|
|
@@ -627,6 +684,16 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
627
684
|
const invocationRoot = await realpath(selection.invocationRoot);
|
|
628
685
|
const databasePath = await selectedDatabasePath(invocationRoot, selection.databasePath);
|
|
629
686
|
ensureRunning();
|
|
687
|
+
if (selection.mode !== undefined && databasePath !== undefined) {
|
|
688
|
+
const child = readPrivateLocalAgentProfile(databasePath);
|
|
689
|
+
if (child.createdBy !== null) {
|
|
690
|
+
const stores = await listPrivateLocalAgentStores(invocationRoot);
|
|
691
|
+
const creatorPath = stores.find((path) => basename(dirname(path)) === child.createdBy);
|
|
692
|
+
if (creatorPath === undefined ||
|
|
693
|
+
!modeWithinCeiling(selection.mode, readPrivateLocalAgentProfile(creatorPath).mode))
|
|
694
|
+
throw new AgentCreateRefused("exceeds_creator", "a created Agent cannot exceed its creator's permission mode");
|
|
695
|
+
}
|
|
696
|
+
}
|
|
630
697
|
const fungiMcp = selection.fungiMcp === undefined
|
|
631
698
|
? undefined
|
|
632
699
|
: readFungiMcpSelection(selection.fungiMcp, {
|
|
@@ -662,11 +729,15 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
662
729
|
invocationRoot,
|
|
663
730
|
databasePath,
|
|
664
731
|
// The host must persist/admit this BEFORE Shiitake can recover work.
|
|
665
|
-
|
|
732
|
+
mode: selection.mode,
|
|
666
733
|
agentControl: projectControl ? directory(invocationRoot) : undefined,
|
|
667
734
|
mail: projectControl ? mailRouting(invocationRoot) : undefined,
|
|
668
735
|
meshAdmission,
|
|
736
|
+
onExecRuleMatch,
|
|
737
|
+
onExecApprovalDecision,
|
|
669
738
|
};
|
|
739
|
+
if (serviceNative !== undefined)
|
|
740
|
+
options = { ...options, native: serviceNative() };
|
|
670
741
|
if (fungiMcp !== undefined)
|
|
671
742
|
options = { ...options, fungiMcp };
|
|
672
743
|
const captured = configuration;
|
|
@@ -683,8 +754,8 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
683
754
|
fungiMcp !== undefined &&
|
|
684
755
|
!sameFungiMcpConnection(owner.fungiMcp, fungiMcp))
|
|
685
756
|
throw new FungiMcpSelectionConflict();
|
|
686
|
-
if (!openingHere && selection.
|
|
687
|
-
owner.runtime.
|
|
757
|
+
if (!openingHere && selection.mode !== undefined)
|
|
758
|
+
owner.runtime.setMode(selection.mode);
|
|
688
759
|
return owner.runtime;
|
|
689
760
|
}
|
|
690
761
|
async function select(selection, createRequestId) {
|
|
@@ -750,11 +821,15 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
750
821
|
!("receipt" in result) ||
|
|
751
822
|
result.status === "refused")
|
|
752
823
|
throw new Error("Mesh recipient did not return command acceptance");
|
|
753
|
-
return
|
|
824
|
+
return parseDelegationReceipt(JSON.stringify(result.receipt));
|
|
754
825
|
}
|
|
755
826
|
const { owner, release } = await control.owner(mail.to.agentId, new AbortController().signal);
|
|
756
827
|
try {
|
|
757
|
-
|
|
828
|
+
const receipt = await admitted(owner, () => owner.acceptMail(mail));
|
|
829
|
+
if (mail.to.sessionId === undefined &&
|
|
830
|
+
receipt.sessionId === undefined)
|
|
831
|
+
throw new Error("Sessionless mail acceptance returned no Session");
|
|
832
|
+
return receipt;
|
|
758
833
|
}
|
|
759
834
|
finally {
|
|
760
835
|
release();
|
|
@@ -935,7 +1010,7 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
935
1010
|
if (creatorDatabase === undefined)
|
|
936
1011
|
throw new AgentCreateRefused("no_creator", "the creating Agent is not in this Local project");
|
|
937
1012
|
const creatorStore = dirname(creatorDatabase);
|
|
938
|
-
const
|
|
1013
|
+
const creatorProfile = readPrivateLocalAgentProfile(creatorDatabase);
|
|
939
1014
|
try {
|
|
940
1015
|
await extensions.assertHeldByAgent(creatorStore, requested);
|
|
941
1016
|
}
|
|
@@ -970,11 +1045,15 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
970
1045
|
createRequestId: requestId,
|
|
971
1046
|
name,
|
|
972
1047
|
createdBy,
|
|
973
|
-
|
|
974
|
-
folder:
|
|
1048
|
+
mode: creatorProfile.mode,
|
|
1049
|
+
folder: creatorProfile.folder,
|
|
975
1050
|
agentControl: directory(invocationRoot),
|
|
976
1051
|
mail: mailRouting(invocationRoot),
|
|
1052
|
+
onExecRuleMatch,
|
|
1053
|
+
onExecApprovalDecision,
|
|
977
1054
|
};
|
|
1055
|
+
if (serviceNative !== undefined)
|
|
1056
|
+
options = { ...options, native: serviceNative() };
|
|
978
1057
|
if (model !== undefined)
|
|
979
1058
|
options = {
|
|
980
1059
|
...options,
|
|
@@ -1028,7 +1107,8 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
1028
1107
|
checkModel: async ({ agentId, sessionId }, signal) => {
|
|
1029
1108
|
const { owner, release } = await admittedOwner(agentId, signal);
|
|
1030
1109
|
try {
|
|
1031
|
-
|
|
1110
|
+
if (sessionId !== undefined)
|
|
1111
|
+
await owner.assertModelReady(sessionId, signal);
|
|
1032
1112
|
}
|
|
1033
1113
|
finally {
|
|
1034
1114
|
release();
|
|
@@ -1060,6 +1140,8 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
1060
1140
|
function close() {
|
|
1061
1141
|
stopping = true;
|
|
1062
1142
|
clearInterval(sweepTimer);
|
|
1143
|
+
if (wakeTimer !== undefined)
|
|
1144
|
+
clearTimeout(wakeTimer);
|
|
1063
1145
|
closing ??= (async () => {
|
|
1064
1146
|
const meshResults = await Promise.allSettled([mesh.close()]);
|
|
1065
1147
|
// End our observers before deciding which Agents have work to resume.
|
|
@@ -1098,8 +1180,11 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
1098
1180
|
const databasePath = owner.runtime.databasePath;
|
|
1099
1181
|
const unused = () => !uses.has(databasePath) && !gates.has(databasePath);
|
|
1100
1182
|
try {
|
|
1101
|
-
if (unused()
|
|
1102
|
-
|
|
1183
|
+
if (unused()) {
|
|
1184
|
+
const state = await owner.runtime.idle();
|
|
1185
|
+
if (state.idle && unused())
|
|
1186
|
+
rememberIdleWake(databasePath, state.wakeAt);
|
|
1187
|
+
}
|
|
1103
1188
|
}
|
|
1104
1189
|
catch (cause) {
|
|
1105
1190
|
logEvent("service-owner.read-idle", cause);
|
|
@@ -1184,6 +1269,14 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
1184
1269
|
for (const selection of loaded.references) {
|
|
1185
1270
|
if (stopping)
|
|
1186
1271
|
break;
|
|
1272
|
+
if (selection.wakeAt !== undefined && selection.wakeAt > Date.now()) {
|
|
1273
|
+
// A client may already have opened this store while the manifest was
|
|
1274
|
+
// loading. Its runtime now owns the wake; do not arm a second one.
|
|
1275
|
+
await Promise.allSettled(pendingOpenings);
|
|
1276
|
+
if (!owners.has(`store:${selection.databasePath}`))
|
|
1277
|
+
wakes.set(selection.databasePath, selection);
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1187
1280
|
try {
|
|
1188
1281
|
await ownerFor(selection);
|
|
1189
1282
|
}
|
|
@@ -1199,6 +1292,7 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
1199
1292
|
recovery.delete(selection.databasePath);
|
|
1200
1293
|
}
|
|
1201
1294
|
}
|
|
1295
|
+
scheduleWakes();
|
|
1202
1296
|
await saveRecovery();
|
|
1203
1297
|
}
|
|
1204
1298
|
/** A person archives or restores any Agent of one project folder. */
|