@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
|
@@ -1,19 +1,25 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, realpathSync, statSync
|
|
2
|
-
import { isAbsolute, join } from "node:path";
|
|
1
|
+
import { existsSync, mkdirSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, relative, sep } from "node:path";
|
|
3
3
|
import { protectedSandboxPaths, } from "./private-local-sandbox-policy.js";
|
|
4
4
|
/** Ordinary upstream bwrap argv. There is no approval or bare-command path. */
|
|
5
|
-
export function linuxSandboxCommand(policy, payload, cwd, argv, host = process.env) {
|
|
5
|
+
export function linuxSandboxCommand(policy, payload, cwd, argv, host = process.env, readOnlyHelperFile) {
|
|
6
6
|
const root = policy.writableRoot;
|
|
7
7
|
// Bind sources must exist. Only the trusted host prepares the reserved paths.
|
|
8
8
|
mkdirSync(join(root, ".fungi"), { recursive: true });
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
writeFileSync(join(git, config), "", { flag: "wx" });
|
|
14
|
-
}
|
|
9
|
+
// A missing carve-out would be skipped by the existing-path ro-bind loop,
|
|
10
|
+
// allowing Workspace to seed self-attested native-patch cache receipts.
|
|
11
|
+
if (policy.fungiCache !== undefined)
|
|
12
|
+
mkdirSync(policy.fungiCache, { recursive: true, mode: 0o700 });
|
|
15
13
|
const args = [
|
|
16
|
-
"
|
|
14
|
+
...(policy.mode === "read-only"
|
|
15
|
+
? ["--unshare-all"]
|
|
16
|
+
: [
|
|
17
|
+
"--unshare-user",
|
|
18
|
+
"--unshare-pid",
|
|
19
|
+
"--unshare-ipc",
|
|
20
|
+
"--unshare-uts",
|
|
21
|
+
"--unshare-cgroup-try",
|
|
22
|
+
]),
|
|
17
23
|
"--die-with-parent",
|
|
18
24
|
"--new-session",
|
|
19
25
|
"--seccomp",
|
|
@@ -22,27 +28,43 @@ export function linuxSandboxCommand(policy, payload, cwd, argv, host = process.e
|
|
|
22
28
|
"/",
|
|
23
29
|
"/",
|
|
24
30
|
// Private system mounts precede the folder bind so /tmp projects survive.
|
|
25
|
-
"--
|
|
31
|
+
"--bind",
|
|
32
|
+
policy.temporaryDirectory,
|
|
26
33
|
"/tmp",
|
|
27
34
|
"--tmpfs",
|
|
28
35
|
"/run",
|
|
36
|
+
...(existsSync("/var/run") && realpathSync("/var/run") !== "/run"
|
|
37
|
+
? ["--tmpfs", realpathSync("/var/run")]
|
|
38
|
+
: []),
|
|
29
39
|
"--dev",
|
|
30
40
|
"/dev",
|
|
31
41
|
"--proc",
|
|
32
42
|
"/proc",
|
|
33
|
-
"--bind",
|
|
34
|
-
root,
|
|
35
|
-
root,
|
|
36
43
|
];
|
|
37
|
-
|
|
44
|
+
// A selected folder under /tmp disappears behind the private /tmp bind.
|
|
45
|
+
// Restore it read-only for Read-only; Workspace will bind it writable below.
|
|
46
|
+
if (policy.mode === "read-only")
|
|
47
|
+
args.push("--ro-bind", root, root);
|
|
48
|
+
// System masks precede every writable grant. Git is rebound read-only after.
|
|
49
|
+
for (const path of policy.writable)
|
|
38
50
|
args.push("--bind", path, path);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
|
|
51
|
+
// External Git metadata is never a writable grant. Missing paths do not
|
|
52
|
+
// become host-side bwrap mountpoints.
|
|
53
|
+
if (policy.mode === "workspace")
|
|
54
|
+
for (const path of protectedSandboxPaths(policy.readOnly))
|
|
55
|
+
if (existsSync(path))
|
|
56
|
+
args.push("--ro-bind", path, path);
|
|
43
57
|
const runtime = host.XDG_RUNTIME_DIR;
|
|
44
58
|
if (runtime && isAbsolute(runtime) && existsSync(runtime))
|
|
45
59
|
args.push("--tmpfs", realpathSync(runtime));
|
|
60
|
+
// systemd's /etc/resolv.conf is often a symlink into the masked /run tree.
|
|
61
|
+
// Restore only its target file in networked Workspace, never the resolver's
|
|
62
|
+
// runtime directory or user bus.
|
|
63
|
+
if (policy.mode === "workspace" && existsSync("/etc/resolv.conf")) {
|
|
64
|
+
const resolver = realpathSync("/etc/resolv.conf");
|
|
65
|
+
if (resolver.startsWith("/run/"))
|
|
66
|
+
args.push("--ro-bind", resolver, resolver);
|
|
67
|
+
}
|
|
46
68
|
// Stopgap masks, not an inventory of all possible host IPC. /run also
|
|
47
69
|
// covers the usual /var/run alias. These follow folder/protected binds so
|
|
48
70
|
// a selected socket inside the project is not exposed again by a later bind.
|
|
@@ -56,12 +78,36 @@ export function linuxSandboxCommand(policy, payload, cwd, argv, host = process.e
|
|
|
56
78
|
if (existsSync(path) && statSync(path).isDirectory())
|
|
57
79
|
args.push("--tmpfs", realpathSync(path));
|
|
58
80
|
}
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
81
|
+
for (const path of policy.hidden) {
|
|
82
|
+
// Host /tmp is already replaced. An external fixture HOME beneath it has
|
|
83
|
+
// no mountpoint in the guest; restoring it would reveal host data.
|
|
84
|
+
const underTmp = relative("/tmp", path);
|
|
85
|
+
const underRoot = relative(root, path);
|
|
86
|
+
if (underTmp !== ".." &&
|
|
87
|
+
!underTmp.startsWith(`..${sep}`) &&
|
|
88
|
+
(underRoot === ".." || underRoot.startsWith(`..${sep}`)))
|
|
89
|
+
continue;
|
|
90
|
+
// Absent records cannot be read; never create a user's credential paths.
|
|
91
|
+
if (existsSync(path) && statSync(path).isDirectory())
|
|
92
|
+
args.push("--tmpfs", path);
|
|
93
|
+
}
|
|
94
|
+
if (policy.sshAgentSocket !== undefined)
|
|
95
|
+
args.push("--bind", policy.sshAgentSocket, policy.sshAgentSocket);
|
|
96
|
+
if (policy.sshAgentMask !== undefined)
|
|
97
|
+
args.push("--ro-bind", "/dev/null", policy.sshAgentMask);
|
|
98
|
+
// Native Files passes its resolved helper, never an untrusted exec argv[0].
|
|
99
|
+
// Re-expose only that file after private mounts; its siblings stay hidden.
|
|
100
|
+
if (readOnlyHelperFile !== undefined) {
|
|
101
|
+
if (!statSync(readOnlyHelperFile).isFile())
|
|
102
|
+
throw new Error("Local native helper must be a regular file");
|
|
103
|
+
args.push("--ro-bind", readOnlyHelperFile, readOnlyHelperFile);
|
|
104
|
+
}
|
|
105
|
+
// R is a bind mount: let sandboxed Git reads discover a parent repository
|
|
106
|
+
// when the selected folder has no .git of its own.
|
|
107
|
+
args.push("--setenv", "GIT_DISCOVERY_ACROSS_FILESYSTEM", "1");
|
|
108
|
+
args.push("--setenv", "FUNGI_SANDBOX", policy.mode);
|
|
109
|
+
if (policy.mode === "read-only")
|
|
110
|
+
args.push("--setenv", "FUNGI_SANDBOX_NETWORK_DISABLED", "1");
|
|
65
111
|
args.push("--chdir", cwd, "--", ...argv);
|
|
66
112
|
return { ...payload, args };
|
|
67
113
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { BirdDogMail } from "@fungi.computer/bird-dog";
|
|
2
2
|
import { type BirdDogEffect } from "@fungi.computer/bird-dog/effect";
|
|
3
3
|
import type { PrivateSqliteDatabase } from "./private-sqlite-owner.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { DelegationReceipt } from "@fungi.computer/shiitake";
|
|
5
5
|
export type PrivateLocalMailOptions = Readonly<{
|
|
6
6
|
/** Admit one reply at its addressed Agent; a rejection retries later. */
|
|
7
|
-
deliver: (mail: BirdDogMail) => Promise<
|
|
7
|
+
deliver: (mail: BirdDogMail) => Promise<DelegationReceipt>;
|
|
8
8
|
scheduleWake?: (at: number) => void;
|
|
9
9
|
now?: () => number;
|
|
10
10
|
}>;
|
|
@@ -13,7 +13,10 @@ export declare const localMeshLinkKey: (record: EnrolledLocalMachine) => string;
|
|
|
13
13
|
export declare function createLocalMeshLinks(options: {
|
|
14
14
|
credentialPath: string;
|
|
15
15
|
receive: (record: EnrolledLocalMachine, envelope: PostEnvelope) => Promise<PostDeliveryResult>;
|
|
16
|
-
}
|
|
16
|
+
}, timing?: Readonly<{
|
|
17
|
+
wait: (ms: number, signal: AbortSignal) => Promise<void>;
|
|
18
|
+
random: () => number;
|
|
19
|
+
}>): {
|
|
17
20
|
refresh: () => Promise<void>;
|
|
18
21
|
isCurrent: (record: EnrolledLocalMachine) => Promise<boolean>;
|
|
19
22
|
deliver: (record: EnrolledLocalMachine, envelope: PostEnvelope) => Promise<PostDeliveryResult>;
|
|
@@ -14,7 +14,10 @@ export const localMeshLinkKey = (record) => JSON.stringify([
|
|
|
14
14
|
* Service-owned native I/O only. The account owns pins, Carrier owns retry
|
|
15
15
|
* custody, and the addressed runtime owns acceptance. No mail is queued here.
|
|
16
16
|
*/
|
|
17
|
-
export function createLocalMeshLinks(options
|
|
17
|
+
export function createLocalMeshLinks(options, timing = {
|
|
18
|
+
wait: (ms, signal) => delay(ms, undefined, { signal }),
|
|
19
|
+
random: Math.random,
|
|
20
|
+
}) {
|
|
18
21
|
const account = createLocalMachineEnrollmentAccount(options.credentialPath);
|
|
19
22
|
const links = new Map();
|
|
20
23
|
let stopping = false;
|
|
@@ -33,6 +36,31 @@ export function createLocalMeshLinks(options) {
|
|
|
33
36
|
!link.stop.signal.aborted &&
|
|
34
37
|
(await current(link.record)));
|
|
35
38
|
}
|
|
39
|
+
// WebSocket's error event does not expose the HTTP upgrade response. A
|
|
40
|
+
// freshly signed plain GET can surface the receiver's upgrade hint. The
|
|
41
|
+
// response is NOT signed: this is diagnostic only, never authority to stop.
|
|
42
|
+
async function upgradeAdvertised(record, signal) {
|
|
43
|
+
try {
|
|
44
|
+
const url = machineLinkDialUrl(record.authority.origin, record.scopeId, record.deviceId);
|
|
45
|
+
const signer = await importSeamSigner(`${record.deviceId}:${record.privateKey}`);
|
|
46
|
+
const signature = await signSeamRequest(signer, machineLinkAudience(record.scopeId, record.deviceId), "GET", url.href, unsignedPayload);
|
|
47
|
+
const response = await fetch(url, {
|
|
48
|
+
headers: { [seamHeader]: signature },
|
|
49
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(10_000)]),
|
|
50
|
+
});
|
|
51
|
+
await response.body?.cancel();
|
|
52
|
+
return (response.status === 426 &&
|
|
53
|
+
response.headers.get("x-fungi-machine-link-action") === "upgrade fungi");
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Equal jitter avoids synchronized immediate retries even at the 15 s cap. */
|
|
60
|
+
const retryMs = (failures, random) => {
|
|
61
|
+
const ceiling = Math.min(15_000, 500 * 2 ** Math.min(failures, 5));
|
|
62
|
+
return Math.floor((ceiling * (1 + random)) / 2);
|
|
63
|
+
};
|
|
36
64
|
async function connection(record, signal, publish) {
|
|
37
65
|
const url = machineLinkDialUrl(record.authority.origin, record.scopeId, record.deviceId);
|
|
38
66
|
const signer = await importSeamSigner(`${record.deviceId}:${record.privateKey}`);
|
|
@@ -161,21 +189,39 @@ export function createLocalMeshLinks(options) {
|
|
|
161
189
|
stop,
|
|
162
190
|
channel: undefined,
|
|
163
191
|
done: (async () => {
|
|
192
|
+
let failures = 0;
|
|
193
|
+
let upgradeNotified = false;
|
|
164
194
|
while (!stopped()) {
|
|
165
195
|
try {
|
|
166
196
|
if (!(await current(record)))
|
|
167
197
|
return "stopped";
|
|
168
198
|
await connection(record, stop.signal, (channel) => {
|
|
169
199
|
link.channel = channel;
|
|
200
|
+
if (channel !== undefined) {
|
|
201
|
+
failures = 0;
|
|
202
|
+
upgradeNotified = false;
|
|
203
|
+
}
|
|
170
204
|
});
|
|
171
205
|
}
|
|
172
206
|
catch (cause) {
|
|
173
207
|
if (stopped())
|
|
174
208
|
return "stopped";
|
|
175
209
|
logEvent("mesh-link.connection", cause);
|
|
210
|
+
const advertised = await upgradeAdvertised(record, stop.signal);
|
|
211
|
+
if (stopped())
|
|
212
|
+
return "stopped";
|
|
213
|
+
if (advertised) {
|
|
214
|
+
// A forged refusal can delay a link, not permanently kill it.
|
|
215
|
+
// Emit once per outage and retry at the bounded cap.
|
|
216
|
+
if (!upgradeNotified)
|
|
217
|
+
console.error("upgrade fungi");
|
|
218
|
+
upgradeNotified = true;
|
|
219
|
+
failures = 5;
|
|
220
|
+
}
|
|
176
221
|
}
|
|
177
222
|
try {
|
|
178
|
-
await
|
|
223
|
+
await timing.wait(retryMs(failures, timing.random()), stop.signal);
|
|
224
|
+
failures = Math.min(failures + 1, 5);
|
|
179
225
|
}
|
|
180
226
|
catch (cause) {
|
|
181
227
|
if (stopped())
|
|
@@ -204,7 +250,8 @@ export function createLocalMeshLinks(options) {
|
|
|
204
250
|
const link = links.get(localMeshLinkKey(record));
|
|
205
251
|
const channel = link?.channel;
|
|
206
252
|
if (channel === undefined || !(await isCurrent(record))) {
|
|
207
|
-
|
|
253
|
+
// An unavailable send is Carrier's retry, not authority to retire a
|
|
254
|
+
// service-owned link while its redial is waiting.
|
|
208
255
|
throw new MachineLinkPostUnavailable({ reason: "closed" });
|
|
209
256
|
}
|
|
210
257
|
return await channel.deliver(envelope);
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// Generated from native build output; not a runtime sidecar.
|
|
2
|
+
export const nativeDigests = Object.freeze({"linux-x64":{"patch":"93d17ead57b33d2295cd3960603220b1c418084ab39bc8450a8fd90a258c7608","bwrap":"035ac5ddfc70dd7d4a9b575b351944d47aa9d0bbafeb433fb72b2afb02cef7f4","seccomp":"38112f9cf994bed2ed962fae63e803c41874732ab032088b2c1ea3d713e51c11","seccompWorkspace":"e46f1045f85fcf14f91fae8654bba6a0c87f57e3b92f60bfc16008dbf7dc87c8"}});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** The Spore caller owns the release version; Local cannot infer it from its own build ID. */
|
|
3
|
+
export function nativePackageStartupError(cause) {
|
|
4
|
+
const release = z.string().min(1).safeParse(process.env.FUNGI_SPORE_VERSION);
|
|
5
|
+
if (!release.success)
|
|
6
|
+
return new Error("Fungi CLI release version is unavailable; restart Fungi.", {
|
|
7
|
+
cause,
|
|
8
|
+
});
|
|
9
|
+
return new Error(`Fungi's native package for ${process.platform}-${process.arch} is missing or doesn't match this version. Reinstall: npm i -g @fungi.computer/spore@${release.data}`, { cause });
|
|
10
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type NativeSourceReader } from "./private-local-bwrap.js";
|
|
2
|
+
import { type LinuxSandboxProfile } from "./private-local-sandbox-policy.js";
|
|
3
|
+
/** The same metadata-independent package path selects the verified source and Files caller. */
|
|
4
|
+
export declare function packagedPatchPath(): string;
|
|
5
|
+
/** One verified private copy for the service's lifetime, not for each command. */
|
|
6
|
+
export declare function createNativePayloadStaging(project: string, readNativeSource?: NativeSourceReader): {
|
|
7
|
+
admitProject: (nextProject: string) => void;
|
|
8
|
+
patch: (path: string) => string;
|
|
9
|
+
linux: (profile: LinuxSandboxProfile) => {
|
|
10
|
+
executable: string;
|
|
11
|
+
seccompFilter: Uint8Array<ArrayBufferLike>;
|
|
12
|
+
};
|
|
13
|
+
probeFilter: (bytes: Uint8Array) => string;
|
|
14
|
+
close: () => void;
|
|
15
|
+
};
|
|
16
|
+
export type NativePayloadStaging = ReturnType<typeof createNativePayloadStaging>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { userInfo } from "node:os";
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { localLinuxSandboxPayload, verifiedNativePatchHelper, LocalBwrapUnavailable, } from "./private-local-bwrap.js";
|
|
7
|
+
import { assertHostOwnedNativeFile, UnsafeNativePayload, } from "./private-local-sandbox-policy.js";
|
|
8
|
+
function within(root, path) {
|
|
9
|
+
const tail = relative(root, path);
|
|
10
|
+
return !isAbsolute(tail) && tail !== ".." && !tail.startsWith(`..${sep}`);
|
|
11
|
+
}
|
|
12
|
+
/** The same metadata-independent package path selects the verified source and Files caller. */
|
|
13
|
+
export function packagedPatchPath() {
|
|
14
|
+
if (process.env.FUNGI_BUNDLED_CLI) {
|
|
15
|
+
const name = `@fungi.computer/spore-native-${process.platform}-${process.arch}`;
|
|
16
|
+
try {
|
|
17
|
+
const paths = createRequire(process.env.FUNGI_BUNDLED_CLI).resolve.paths(name);
|
|
18
|
+
const packageRoot = paths
|
|
19
|
+
?.map((directory) => join(directory, name))
|
|
20
|
+
.find((candidate) => lstatSync(candidate, { throwIfNoEntry: false }));
|
|
21
|
+
if (packageRoot !== undefined)
|
|
22
|
+
return join(packageRoot, "bin", "fungi-apply-patch");
|
|
23
|
+
throw new Error("Native package not installed");
|
|
24
|
+
}
|
|
25
|
+
catch (cause) {
|
|
26
|
+
throw new LocalBwrapUnavailable("install", `Missing ${name}/bin/fungi-apply-patch; reinstall Fungi.`, { cause });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return fileURLToPath(new URL("../native/bin/fungi-apply-patch", import.meta.url));
|
|
30
|
+
}
|
|
31
|
+
/** One verified private copy for the service's lifetime, not for each command. */
|
|
32
|
+
export function createNativePayloadStaging(project, readNativeSource = readFileSync) {
|
|
33
|
+
const patchSource = packagedPatchPath();
|
|
34
|
+
assertHostOwnedNativeFile(project, dirname(dirname(patchSource)));
|
|
35
|
+
const patchBytes = verifiedNativePatchHelper(patchSource, project, readNativeSource);
|
|
36
|
+
const linux = process.platform === "linux"
|
|
37
|
+
? localLinuxSandboxPayload(project, readNativeSource)
|
|
38
|
+
: undefined;
|
|
39
|
+
const installDirectories = [
|
|
40
|
+
dirname(patchSource),
|
|
41
|
+
...(linux === undefined ? [] : [dirname(linux.sourcePath)]),
|
|
42
|
+
].flatMap((source) => [resolve(source), realpathSync(source)]);
|
|
43
|
+
const runtime = process.env.XDG_RUNTIME_DIR;
|
|
44
|
+
const base = runtime ?? join(userInfo().homedir, ".local", "state");
|
|
45
|
+
const uid = process.getuid?.();
|
|
46
|
+
if (uid === undefined || !isAbsolute(base))
|
|
47
|
+
throw new UnsafeNativePayload("Native staging needs an absolute runtime directory and POSIX user.");
|
|
48
|
+
if (runtime !== undefined) {
|
|
49
|
+
const metadata = lstatSync(runtime, { throwIfNoEntry: false });
|
|
50
|
+
if (metadata === undefined ||
|
|
51
|
+
!metadata.isDirectory() ||
|
|
52
|
+
metadata.uid !== uid ||
|
|
53
|
+
(metadata.mode & 0o077) !== 0)
|
|
54
|
+
throw new UnsafeNativePayload("XDG_RUNTIME_DIR must be a private directory owned by this user.");
|
|
55
|
+
}
|
|
56
|
+
const root = join(base, "fungi");
|
|
57
|
+
const projectRoot = realpathSync(project);
|
|
58
|
+
if (within(projectRoot, resolve(root)))
|
|
59
|
+
throw new UnsafeNativePayload("Host native staging cannot live inside the Agent project.");
|
|
60
|
+
let directory;
|
|
61
|
+
try {
|
|
62
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
63
|
+
const actual = realpathSync(root);
|
|
64
|
+
if (within(projectRoot, actual))
|
|
65
|
+
throw new UnsafeNativePayload("Host native staging resolves inside the Agent project.");
|
|
66
|
+
const metadata = lstatSync(actual);
|
|
67
|
+
if (!metadata.isDirectory() ||
|
|
68
|
+
metadata.uid !== uid ||
|
|
69
|
+
(metadata.mode & 0o077) !== 0)
|
|
70
|
+
throw new UnsafeNativePayload("Host native staging root must be private and owned by this user.");
|
|
71
|
+
const staged = mkdtempSync(join(actual, "native-"));
|
|
72
|
+
directory = staged;
|
|
73
|
+
const boundaries = [staged, ...installDirectories];
|
|
74
|
+
const stage = (label, bytes, mode) => {
|
|
75
|
+
const target = join(staged, label);
|
|
76
|
+
writeFileSync(target, bytes, { flag: "wx", mode });
|
|
77
|
+
return target;
|
|
78
|
+
};
|
|
79
|
+
const patch = stage("fungi-apply-patch", patchBytes, 0o500);
|
|
80
|
+
const bwrap = linux && stage("fungi-bwrap", linux.executableBytes, 0o500);
|
|
81
|
+
const seccomp = linux && stage("fungi-seccomp.bpf", linux.seccomp, 0o400);
|
|
82
|
+
const workspace = linux &&
|
|
83
|
+
stage("fungi-seccomp-workspace.bpf", linux.seccompWorkspace, 0o400);
|
|
84
|
+
let closed = false;
|
|
85
|
+
return {
|
|
86
|
+
admitProject: (nextProject) => {
|
|
87
|
+
if (closed)
|
|
88
|
+
throw new Error("Native staging owner is closed");
|
|
89
|
+
const next = realpathSync(nextProject);
|
|
90
|
+
for (const source of boundaries)
|
|
91
|
+
if (within(next, source))
|
|
92
|
+
throw new UnsafeNativePayload("Native files must be outside the Agent project.");
|
|
93
|
+
},
|
|
94
|
+
patch: (path) => {
|
|
95
|
+
if (closed)
|
|
96
|
+
throw new Error("Native staging owner is closed");
|
|
97
|
+
if (resolve(path) !== resolve(patchSource))
|
|
98
|
+
throw new LocalBwrapUnavailable("install", "Native patch helper changed; restart Fungi.");
|
|
99
|
+
return patch;
|
|
100
|
+
},
|
|
101
|
+
linux: (profile) => {
|
|
102
|
+
if (closed)
|
|
103
|
+
throw new Error("Native staging owner is closed");
|
|
104
|
+
if (linux === undefined || bwrap === undefined)
|
|
105
|
+
throw new LocalBwrapUnavailable("install", "No packaged Linux sandbox; reinstall Fungi.");
|
|
106
|
+
return {
|
|
107
|
+
executable: bwrap,
|
|
108
|
+
seccompFilter: profile.mode === "workspace"
|
|
109
|
+
? linux.seccompWorkspace
|
|
110
|
+
: linux.seccomp,
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
probeFilter: (bytes) => {
|
|
114
|
+
if (closed)
|
|
115
|
+
throw new Error("Native staging owner is closed");
|
|
116
|
+
if (bytes === linux?.seccomp && seccomp !== undefined)
|
|
117
|
+
return seccomp;
|
|
118
|
+
if (bytes === linux?.seccompWorkspace && workspace !== undefined)
|
|
119
|
+
return workspace;
|
|
120
|
+
throw new LocalBwrapUnavailable("install", "Unknown native filter; restart Fungi.");
|
|
121
|
+
},
|
|
122
|
+
close: () => {
|
|
123
|
+
if (closed)
|
|
124
|
+
return;
|
|
125
|
+
closed = true;
|
|
126
|
+
rmSync(staged, { recursive: true });
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
catch (cause) {
|
|
131
|
+
if (directory !== undefined)
|
|
132
|
+
rmSync(directory, { recursive: true });
|
|
133
|
+
if (cause instanceof UnsafeNativePayload ||
|
|
134
|
+
cause instanceof LocalBwrapUnavailable)
|
|
135
|
+
throw cause;
|
|
136
|
+
throw new LocalBwrapUnavailable("install", "Cannot copy verified native files into the private runtime directory; reinstall Fungi.", { cause });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const localPermissionModeSchema: z.ZodEnum<{
|
|
3
|
+
"read-only": "read-only";
|
|
4
|
+
workspace: "workspace";
|
|
5
|
+
"full-access": "full-access";
|
|
6
|
+
}>;
|
|
7
|
+
export type LocalPermissionMode = z.output<typeof localPermissionModeSchema>;
|
|
8
|
+
export declare function modeWithinCeiling(candidate: LocalPermissionMode, creator: LocalPermissionMode): boolean;
|
|
9
|
+
/** Levi's 2026-10-25 Q1 decision: default Workspace when the host sandbox probe passes, else Full access. */
|
|
10
|
+
export declare function defaultLocalPermissionMode(sandboxAvailable: boolean): LocalPermissionMode;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const localPermissionModeSchema = z.enum([
|
|
3
|
+
"read-only",
|
|
4
|
+
"workspace",
|
|
5
|
+
"full-access",
|
|
6
|
+
]);
|
|
7
|
+
const rank = {
|
|
8
|
+
"read-only": 0,
|
|
9
|
+
workspace: 1,
|
|
10
|
+
"full-access": 2,
|
|
11
|
+
};
|
|
12
|
+
export function modeWithinCeiling(candidate, creator) {
|
|
13
|
+
return rank[candidate] <= rank[creator];
|
|
14
|
+
}
|
|
15
|
+
/** Levi's 2026-10-25 Q1 decision: default Workspace when the host sandbox probe passes, else Full access. */
|
|
16
|
+
export function defaultLocalPermissionMode(sandboxAvailable) {
|
|
17
|
+
return sandboxAvailable ? "workspace" : "full-access";
|
|
18
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { OutstandingDelegation } from "@fungi.computer/shiitake/fleet";
|
|
|
2
2
|
import { createMcpFormSession } from "./private-mcp-form-session.js";
|
|
3
3
|
import type { LocalUiAttachment } from "./private-local-ui-attachment.js";
|
|
4
4
|
import { type LocalAgentExtensions } from "./local-extensions.js";
|
|
5
|
+
import { type LocalPermissionMode } from "./private-local-permission-mode.js";
|
|
5
6
|
import { type Models, type ThinkingLevel } from "@earendil-works/pi-ai";
|
|
6
7
|
import type { CapabilitySource } from "@fungi.computer/mycelium";
|
|
7
8
|
import type { BirdDogMail } from "@fungi.computer/bird-dog";
|
|
@@ -12,8 +13,10 @@ import type { ExtensionRosterV0 } from "@fungi.computer/shiitake/extensions";
|
|
|
12
13
|
import { type InstructionSource, type ModelSource, Shiitake, type RuntimeHost, type ShiitakeAgent, type ShiitakeSession, type ShiitakeOptions } from "@fungi.computer/shiitake";
|
|
13
14
|
import { type PrivateLocalConfigRoot } from "./private-local-config-root.js";
|
|
14
15
|
import { type PrivateLocalEffectiveModel } from "./private-local-effective-model.js";
|
|
16
|
+
import { type LocalExecEscapeStage, type LocalExecPolicyRun } from "./private-invocation-computer.js";
|
|
15
17
|
import { type PrivateLocalComputerProcesses } from "./private-local-computer-processes.js";
|
|
16
18
|
import { type LocalSandboxControls, type LocalSandboxStatus } from "./private-local-sandbox.js";
|
|
19
|
+
import type { NativePayloadStaging } from "./private-local-native-staging.js";
|
|
17
20
|
import { type NodeRuntimeHost, type NodeStoreLease } from "@fungi.computer/shiitake/node";
|
|
18
21
|
import { createPrivateInvocationFilesPort, type LocalSandboxWrites } from "./private-invocation-files.js";
|
|
19
22
|
import { type PrivateFungiMcpConnectionOptions } from "./private-fungi-mcp-connection.js";
|
|
@@ -49,9 +52,17 @@ export type PrivateLocalTools = Readonly<{
|
|
|
49
52
|
export type PrivateLocalRuntimeHostOptions = Readonly<{
|
|
50
53
|
/** Service-selected host configuration, independent of this Agent's Files root. */
|
|
51
54
|
configRoot?: PrivateLocalConfigRoot;
|
|
55
|
+
/** Verified service copy; the service, not the Agent, closes it. */
|
|
56
|
+
native?: NativePayloadStaging | undefined;
|
|
52
57
|
configuration?: (storeDirectory: string) => Promise<LocalRuntimeConfiguration>;
|
|
53
58
|
/** Invocation root captured before selecting a fresh local database. */
|
|
54
59
|
invocationRoot: string;
|
|
60
|
+
/** Service-owned nonsecret audit sink, never a command or environment logger. */
|
|
61
|
+
onExecRuleMatch?: (match: Readonly<{
|
|
62
|
+
agentId: string;
|
|
63
|
+
executable: string;
|
|
64
|
+
ruleId: string;
|
|
65
|
+
}>) => void;
|
|
55
66
|
/** Existing Agent database to reopen instead of creating a fresh local store. */
|
|
56
67
|
databasePath?: string | undefined;
|
|
57
68
|
/** Stable Local store identity for replaying one Agent create request. */
|
|
@@ -63,7 +74,7 @@ export type PrivateLocalRuntimeHostOptions = Readonly<{
|
|
|
63
74
|
/** Immutable project-relative folder authority, inherited by child Agents. */
|
|
64
75
|
folder?: string | undefined;
|
|
65
76
|
/** Persist before constructing the Agent, which can recover durable work. */
|
|
66
|
-
|
|
77
|
+
mode?: LocalPermissionMode | undefined;
|
|
67
78
|
/** Explicit Agent model; otherwise the Agent's stored default, then the available login. */
|
|
68
79
|
providerId?: string | undefined;
|
|
69
80
|
modelId?: string | undefined;
|
|
@@ -107,8 +118,8 @@ export type PrivateLocalRuntimeHost = Readonly<{
|
|
|
107
118
|
* flag changes; the store, its Sessions and files stay.
|
|
108
119
|
*/
|
|
109
120
|
setArchived: (archived: boolean) => void;
|
|
110
|
-
/** Persist
|
|
111
|
-
|
|
121
|
+
/** Persist a person's mode before another affected launch is admitted. */
|
|
122
|
+
setMode: (mode: LocalPermissionMode) => void;
|
|
112
123
|
sandboxStatus: () => LocalSandboxStatus;
|
|
113
124
|
/** Set the persistent Agent default, thinking level included, without changing Session overrides. */
|
|
114
125
|
setModel: (model: ModelPreference, signal: AbortSignal) => Promise<void>;
|
|
@@ -135,7 +146,10 @@ export type PrivateLocalRuntimeHost = Readonly<{
|
|
|
135
146
|
* owed a delivery attempt or running background process. Work it delegated
|
|
136
147
|
* is owed by the child, whose reply mail reopens this Agent.
|
|
137
148
|
*/
|
|
138
|
-
idle: () => Promise<
|
|
149
|
+
idle: () => Promise<{
|
|
150
|
+
idle: boolean;
|
|
151
|
+
wakeAt: number | null;
|
|
152
|
+
}>;
|
|
139
153
|
close: () => Promise<void>;
|
|
140
154
|
}>;
|
|
141
155
|
export type LocalRuntimeConfiguration = Readonly<{
|
|
@@ -171,7 +185,7 @@ storeDirectory?: string,
|
|
|
171
185
|
subagents?: Readonly<{
|
|
172
186
|
control: CapabilitySource;
|
|
173
187
|
source: SubagentSource;
|
|
174
|
-
}>, configuration?: LocalRuntimeConfiguration, configRoot?: PrivateLocalConfigRoot, sandbox?: LocalSandboxWrites): Promise<PrivateLocalTools>;
|
|
188
|
+
}>, configuration?: LocalRuntimeConfiguration, configRoot?: PrivateLocalConfigRoot, sandbox?: LocalSandboxWrites, currentMode?: () => LocalPermissionMode, escapeForSession?: (sessionId: string) => LocalExecEscapeStage, policyForSession?: (sessionId: string) => LocalExecPolicyRun): Promise<PrivateLocalTools>;
|
|
175
189
|
/** Event-loop adaptation for the one local Shiitake owner; it has no queue policy. */
|
|
176
190
|
export declare function createPrivateLocalRuntimeHost(): RuntimeHost;
|
|
177
191
|
/**
|