@fungi.computer/truffle 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/private-files-patch.js +39 -36
- package/dist/private-invocation-computer.d.ts +224 -5
- 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 +17 -7
- package/dist/private-local-bwrap.js +73 -33
- 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 +18 -8
- package/dist/private-local-containment.js +70 -45
- 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 +10 -0
- package/dist/private-local-exec-review.js +50 -0
- package/dist/private-local-exec-rules.d.ts +14 -0
- package/dist/private-local-exec-rules.js +420 -0
- package/dist/private-local-git-exclude.js +25 -15
- package/dist/private-local-linux-sandbox.d.ts +5 -3
- package/dist/private-local-linux-sandbox.js +71 -25
- 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 +2 -0
- 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 +16 -0
- package/dist/private-local-native-staging.js +138 -0
- 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 +19 -5
- package/dist/private-local-runtime-host.js +391 -71
- package/dist/private-local-sandbox-policy.d.ts +46 -6
- package/dist/private-local-sandbox-policy.js +350 -71
- package/dist/private-local-sandbox.d.ts +34 -18
- package/dist/private-local-sandbox.js +225 -60
- 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 +18 -6
- 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 +8 -0
- package/dist/private-local-service-log.js +3 -0
- package/dist/private-local-service-owner.d.ts +18 -10
- package/dist/private-local-service-owner.js +117 -25
- package/dist/private-local-service.js +43 -9
- package/dist/private-local-session-discovery.d.ts +3 -2
- package/dist/private-local-session-discovery.js +18 -6
- package/dist/private-mcp-form-contract.d.ts +8 -1
- package/dist/private-mcp-form-contract.js +8 -1
- package/dist/private-mcp-form-session.d.ts +33 -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 +3 -1
- 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 +25 -19
|
@@ -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
|
+
});
|
|
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,11 @@ 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;
|
|
53
58
|
}> | Readonly<{
|
|
54
59
|
event: "memory";
|
|
55
60
|
rss: number;
|
|
@@ -80,6 +85,9 @@ export type ServiceLogLifecycle = Readonly<{
|
|
|
80
85
|
ready: (serviceId: string) => void;
|
|
81
86
|
stopping: (reason: ServiceLogShutdownReason) => void;
|
|
82
87
|
failure: (failure: FailureMetadata) => void;
|
|
88
|
+
ruleMatch: (match: Omit<Extract<ServiceLogEvent, {
|
|
89
|
+
event: "exec-rule-match";
|
|
90
|
+
}>, "event">) => void;
|
|
83
91
|
recoveryDisabled: (code: RecoveryPersistenceFailureCode) => void;
|
|
84
92
|
/** Synchronous, so it lands before the bounded drain exits the process. */
|
|
85
93
|
drainTimeout: (drain: Readonly<{
|
|
@@ -287,6 +287,9 @@ 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
|
+
},
|
|
290
293
|
recoveryDisabled(code) {
|
|
291
294
|
if (closed || recovery !== undefined)
|
|
292
295
|
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"]>): {
|
|
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" | "invalid-digest" | "digest-mismatch" | "not-executable" | "unsupported-platform" | "invalid-folder" | "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) {
|
|
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,14 @@ 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,
|
|
669
737
|
};
|
|
738
|
+
if (serviceNative !== undefined)
|
|
739
|
+
options = { ...options, native: serviceNative() };
|
|
670
740
|
if (fungiMcp !== undefined)
|
|
671
741
|
options = { ...options, fungiMcp };
|
|
672
742
|
const captured = configuration;
|
|
@@ -683,8 +753,8 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
683
753
|
fungiMcp !== undefined &&
|
|
684
754
|
!sameFungiMcpConnection(owner.fungiMcp, fungiMcp))
|
|
685
755
|
throw new FungiMcpSelectionConflict();
|
|
686
|
-
if (!openingHere && selection.
|
|
687
|
-
owner.runtime.
|
|
756
|
+
if (!openingHere && selection.mode !== undefined)
|
|
757
|
+
owner.runtime.setMode(selection.mode);
|
|
688
758
|
return owner.runtime;
|
|
689
759
|
}
|
|
690
760
|
async function select(selection, createRequestId) {
|
|
@@ -750,11 +820,15 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
750
820
|
!("receipt" in result) ||
|
|
751
821
|
result.status === "refused")
|
|
752
822
|
throw new Error("Mesh recipient did not return command acceptance");
|
|
753
|
-
return
|
|
823
|
+
return parseDelegationReceipt(JSON.stringify(result.receipt));
|
|
754
824
|
}
|
|
755
825
|
const { owner, release } = await control.owner(mail.to.agentId, new AbortController().signal);
|
|
756
826
|
try {
|
|
757
|
-
|
|
827
|
+
const receipt = await admitted(owner, () => owner.acceptMail(mail));
|
|
828
|
+
if (mail.to.sessionId === undefined &&
|
|
829
|
+
receipt.sessionId === undefined)
|
|
830
|
+
throw new Error("Sessionless mail acceptance returned no Session");
|
|
831
|
+
return receipt;
|
|
758
832
|
}
|
|
759
833
|
finally {
|
|
760
834
|
release();
|
|
@@ -935,7 +1009,7 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
935
1009
|
if (creatorDatabase === undefined)
|
|
936
1010
|
throw new AgentCreateRefused("no_creator", "the creating Agent is not in this Local project");
|
|
937
1011
|
const creatorStore = dirname(creatorDatabase);
|
|
938
|
-
const
|
|
1012
|
+
const creatorProfile = readPrivateLocalAgentProfile(creatorDatabase);
|
|
939
1013
|
try {
|
|
940
1014
|
await extensions.assertHeldByAgent(creatorStore, requested);
|
|
941
1015
|
}
|
|
@@ -970,11 +1044,14 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
970
1044
|
createRequestId: requestId,
|
|
971
1045
|
name,
|
|
972
1046
|
createdBy,
|
|
973
|
-
|
|
974
|
-
folder:
|
|
1047
|
+
mode: creatorProfile.mode,
|
|
1048
|
+
folder: creatorProfile.folder,
|
|
975
1049
|
agentControl: directory(invocationRoot),
|
|
976
1050
|
mail: mailRouting(invocationRoot),
|
|
1051
|
+
onExecRuleMatch,
|
|
977
1052
|
};
|
|
1053
|
+
if (serviceNative !== undefined)
|
|
1054
|
+
options = { ...options, native: serviceNative() };
|
|
978
1055
|
if (model !== undefined)
|
|
979
1056
|
options = {
|
|
980
1057
|
...options,
|
|
@@ -1028,7 +1105,8 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
1028
1105
|
checkModel: async ({ agentId, sessionId }, signal) => {
|
|
1029
1106
|
const { owner, release } = await admittedOwner(agentId, signal);
|
|
1030
1107
|
try {
|
|
1031
|
-
|
|
1108
|
+
if (sessionId !== undefined)
|
|
1109
|
+
await owner.assertModelReady(sessionId, signal);
|
|
1032
1110
|
}
|
|
1033
1111
|
finally {
|
|
1034
1112
|
release();
|
|
@@ -1060,6 +1138,8 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
1060
1138
|
function close() {
|
|
1061
1139
|
stopping = true;
|
|
1062
1140
|
clearInterval(sweepTimer);
|
|
1141
|
+
if (wakeTimer !== undefined)
|
|
1142
|
+
clearTimeout(wakeTimer);
|
|
1063
1143
|
closing ??= (async () => {
|
|
1064
1144
|
const meshResults = await Promise.allSettled([mesh.close()]);
|
|
1065
1145
|
// End our observers before deciding which Agents have work to resume.
|
|
@@ -1098,8 +1178,11 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
1098
1178
|
const databasePath = owner.runtime.databasePath;
|
|
1099
1179
|
const unused = () => !uses.has(databasePath) && !gates.has(databasePath);
|
|
1100
1180
|
try {
|
|
1101
|
-
if (unused()
|
|
1102
|
-
|
|
1181
|
+
if (unused()) {
|
|
1182
|
+
const state = await owner.runtime.idle();
|
|
1183
|
+
if (state.idle && unused())
|
|
1184
|
+
rememberIdleWake(databasePath, state.wakeAt);
|
|
1185
|
+
}
|
|
1103
1186
|
}
|
|
1104
1187
|
catch (cause) {
|
|
1105
1188
|
logEvent("service-owner.read-idle", cause);
|
|
@@ -1184,6 +1267,14 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
1184
1267
|
for (const selection of loaded.references) {
|
|
1185
1268
|
if (stopping)
|
|
1186
1269
|
break;
|
|
1270
|
+
if (selection.wakeAt !== undefined && selection.wakeAt > Date.now()) {
|
|
1271
|
+
// A client may already have opened this store while the manifest was
|
|
1272
|
+
// loading. Its runtime now owns the wake; do not arm a second one.
|
|
1273
|
+
await Promise.allSettled(pendingOpenings);
|
|
1274
|
+
if (!owners.has(`store:${selection.databasePath}`))
|
|
1275
|
+
wakes.set(selection.databasePath, selection);
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1187
1278
|
try {
|
|
1188
1279
|
await ownerFor(selection);
|
|
1189
1280
|
}
|
|
@@ -1199,6 +1290,7 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
|
|
|
1199
1290
|
recovery.delete(selection.databasePath);
|
|
1200
1291
|
}
|
|
1201
1292
|
}
|
|
1293
|
+
scheduleWakes();
|
|
1202
1294
|
await saveRecovery();
|
|
1203
1295
|
}
|
|
1204
1296
|
/** A person archives or restores any Agent of one project folder. */
|
|
@@ -13,9 +13,12 @@ export { AgentArchiveRefused } from "@fungi.computer/shiitake/agent-control";
|
|
|
13
13
|
import { newWebSocketRpcSession } from "@fungi.computer/capnweb";
|
|
14
14
|
import { runPrivateLocalAuthRemote, } from "./private-local-auth-rpc.js";
|
|
15
15
|
import { spawn } from "node:child_process";
|
|
16
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
17
|
+
import { join } from "node:path";
|
|
16
18
|
import { fileURLToPath } from "node:url";
|
|
17
19
|
import { setTimeout as delay } from "node:timers/promises";
|
|
18
20
|
import { readInstalledLocalClientBuildId } from "./private-local-client-version.js";
|
|
21
|
+
import { nativePackageStartupError } from "./private-local-native-install-error.js";
|
|
19
22
|
export { readInstalledLocalClientBuildId } from "./private-local-client-version.js";
|
|
20
23
|
export { privateLocalServiceVersion } from "./private-local-service-discovery.js";
|
|
21
24
|
import { createShiitakeClient, parseSessionSnapshot, } from "@fungi.computer/shiitake/client";
|
|
@@ -220,6 +223,7 @@ export function createPrivateLocalServiceManager(options = {}) {
|
|
|
220
223
|
request.signal?.throwIfAborted();
|
|
221
224
|
const directory = await serviceDirectory(selectedDirectory);
|
|
222
225
|
const release = await acquireServiceLock(directory, "startup");
|
|
226
|
+
let nativeRefusalDirectory;
|
|
223
227
|
try {
|
|
224
228
|
const registration = await readServiceRegistration(directory);
|
|
225
229
|
const existing = registration === undefined
|
|
@@ -232,17 +236,30 @@ export function createPrivateLocalServiceManager(options = {}) {
|
|
|
232
236
|
if (await serviceOwnerIsLive(directory))
|
|
233
237
|
return "busy";
|
|
234
238
|
const bundledCli = process.env.FUNGI_BUNDLED_CLI;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
]
|
|
240
|
-
|
|
239
|
+
// A detached Bun service also starts in the invoking project's cwd.
|
|
240
|
+
// Keep the host process from importing Agent-authored bunfig or .env.
|
|
241
|
+
const bunFlags = options.executable === undefined && "bun" in process.versions
|
|
242
|
+
? ["--config=/dev/null", "--no-env-file"]
|
|
243
|
+
: [];
|
|
244
|
+
const serviceEntry = bundledCli ??
|
|
245
|
+
fileURLToPath(new URL("./private-local-service-process.js", import.meta.url));
|
|
246
|
+
nativeRefusalDirectory = await mkdtemp(join(directory, ".native-startup-refusal-"));
|
|
247
|
+
const nativeRefusal = join(nativeRefusalDirectory, "native-package");
|
|
248
|
+
const child = spawn(options.executable ?? process.execPath, [
|
|
249
|
+
...bunFlags,
|
|
250
|
+
serviceEntry,
|
|
251
|
+
...(bundledCli === undefined ? [] : ["--internal-service"]),
|
|
252
|
+
directory,
|
|
253
|
+
], {
|
|
241
254
|
detached: true,
|
|
242
255
|
stdio: "ignore",
|
|
243
256
|
// Service startup retains its existing inherited inputs. This is
|
|
244
257
|
// not a Computer exec child: only its selected host HOME is pinned.
|
|
245
|
-
env: {
|
|
258
|
+
env: {
|
|
259
|
+
...process.env,
|
|
260
|
+
HOME: root.home,
|
|
261
|
+
FUNGI_NATIVE_STARTUP_REFUSAL: nativeRefusal,
|
|
262
|
+
},
|
|
246
263
|
});
|
|
247
264
|
let failure;
|
|
248
265
|
let exited = false;
|
|
@@ -265,8 +282,18 @@ export function createPrivateLocalServiceManager(options = {}) {
|
|
|
265
282
|
throw new Error(`The started Local service reports version ${ready.version}, not this client's ${expectedVersion}`);
|
|
266
283
|
return accept(ready);
|
|
267
284
|
}
|
|
268
|
-
if (hasExited())
|
|
285
|
+
if (hasExited()) {
|
|
286
|
+
const refusal = await readFile(nativeRefusal, "utf8").catch((cause) => {
|
|
287
|
+
if (cause instanceof Error &&
|
|
288
|
+
"code" in cause &&
|
|
289
|
+
cause.code === "ENOENT")
|
|
290
|
+
return undefined;
|
|
291
|
+
throw cause;
|
|
292
|
+
});
|
|
293
|
+
if (refusal === "native-package")
|
|
294
|
+
throw nativePackageStartupError();
|
|
269
295
|
throw new Error("Local service exited before its handshake");
|
|
296
|
+
}
|
|
270
297
|
await delay(100, undefined, { signal: request.signal });
|
|
271
298
|
}
|
|
272
299
|
child.kill("SIGTERM");
|
|
@@ -277,7 +304,14 @@ export function createPrivateLocalServiceManager(options = {}) {
|
|
|
277
304
|
: `Local service did not become ready; child ${String(child.pid)} is still running`);
|
|
278
305
|
}
|
|
279
306
|
finally {
|
|
280
|
-
|
|
307
|
+
// The token is per spawn and cannot survive a failed or successful start.
|
|
308
|
+
try {
|
|
309
|
+
if (nativeRefusalDirectory !== undefined)
|
|
310
|
+
await rm(nativeRefusalDirectory, { recursive: true, force: true });
|
|
311
|
+
}
|
|
312
|
+
finally {
|
|
313
|
+
await release();
|
|
314
|
+
}
|
|
281
315
|
}
|
|
282
316
|
}
|
|
283
317
|
/** POST one selection to `service`; every refusal is a typed failure. */
|
|
@@ -32,12 +32,13 @@ export declare function readPrivateLocalAgentProfile(path: string): {
|
|
|
32
32
|
name: string | undefined;
|
|
33
33
|
createdBy: string | null;
|
|
34
34
|
archived: boolean;
|
|
35
|
-
|
|
35
|
+
mode: "read-only" | "workspace" | "full-access";
|
|
36
|
+
writableFolders: string[];
|
|
36
37
|
folder: string;
|
|
37
38
|
model: {
|
|
38
39
|
readonly providerId: string;
|
|
39
40
|
readonly modelId: string;
|
|
40
|
-
readonly reasoning?: "
|
|
41
|
+
readonly reasoning?: "high" | "low" | "max" | "medium" | "minimal" | "xhigh" | undefined;
|
|
41
42
|
} | null;
|
|
42
43
|
};
|
|
43
44
|
export declare function readPrivateLocalSessionSummaries(path: string): Promise<readonly LocalSummary[]>;
|