@fungi.computer/truffle 0.2.0 → 0.2.2
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 +38 -21
- package/dist/private-invocation-computer.d.ts +9 -2
- package/dist/private-local-bwrap.d.ts +13 -5
- package/dist/private-local-bwrap.js +65 -30
- package/dist/private-local-containment.d.ts +10 -2
- package/dist/private-local-containment.js +64 -23
- package/dist/private-local-linux-sandbox.d.ts +4 -2
- package/dist/private-local-linux-sandbox.js +16 -3
- package/dist/private-local-native-digests.js +2 -0
- package/dist/private-local-native-staging.d.ts +17 -0
- package/dist/private-local-native-staging.js +49 -0
- package/dist/private-local-runtime-host.js +13 -6
- package/dist/private-local-sandbox-policy.d.ts +21 -0
- package/dist/private-local-sandbox-policy.js +439 -10
- package/dist/private-local-sandbox.d.ts +11 -0
- package/dist/private-local-sandbox.js +93 -18
- package/dist/private-local-service-discovery.d.ts +1 -1
- package/dist/private-local-service-owner.d.ts +1 -1
- package/dist/private-local-service.js +13 -6
- package/dist/private-local-session-discovery.d.ts +1 -1
- package/native/bin/bwrap-build-source/Dockerfile +1 -1
- package/package.json +11 -11
|
@@ -6,6 +6,7 @@ import * as ChildProcess from "effect/process/ChildProcess";
|
|
|
6
6
|
import { parsePatch } from "diff";
|
|
7
7
|
import * as Stream from "effect/Stream";
|
|
8
8
|
import { runPrivateInvocationProcess, } from "./private-invocation-computer.js";
|
|
9
|
+
import { createNativePayloadStaging } from "./private-local-native-staging.js";
|
|
9
10
|
function patchExecutable() {
|
|
10
11
|
if (process.env.FUNGI_BUNDLED_CLI) {
|
|
11
12
|
const name = `@fungi.computer/spore-native-${process.platform}-${process.arch}`;
|
|
@@ -51,7 +52,9 @@ async function unifiedDiffContext(patch, read, context) {
|
|
|
51
52
|
export async function createPrivateFilesPatch(root, read, sandbox) {
|
|
52
53
|
const executable = patchExecutable();
|
|
53
54
|
try {
|
|
54
|
-
|
|
55
|
+
// Advertise the installed helper if present; only the verified private
|
|
56
|
+
// copy is executed, so its source does not need an execute bit.
|
|
57
|
+
await access(executable, constants.F_OK);
|
|
55
58
|
}
|
|
56
59
|
catch (cause) {
|
|
57
60
|
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
|
|
@@ -63,27 +66,41 @@ export async function createPrivateFilesPatch(root, read, sandbox) {
|
|
|
63
66
|
const bytes = new TextEncoder().encode(patch);
|
|
64
67
|
if (bytes.byteLength > maximumPatchBytes)
|
|
65
68
|
throw new Error("Patch exceeds the 128 KiB input bound");
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
69
|
+
let result;
|
|
70
|
+
let standalone;
|
|
71
|
+
try {
|
|
72
|
+
const launch = sandbox?.(root, [executable], executable);
|
|
73
|
+
// Without controls, this one Files invocation owns and closes its copy.
|
|
74
|
+
if (launch === undefined)
|
|
75
|
+
standalone = createNativePayloadStaging(root);
|
|
76
|
+
const privateExecutable = standalone?.patch(executable);
|
|
77
|
+
const executableToRun = launch?.executable ?? privateExecutable;
|
|
78
|
+
if (executableToRun === undefined)
|
|
79
|
+
throw new Error("Native Files helper was not staged");
|
|
80
|
+
const env = { ...process.env, ...launch?.environment };
|
|
81
|
+
if (launch !== undefined && !("bareNative" in launch)) {
|
|
82
|
+
delete env.XDG_RUNTIME_DIR;
|
|
83
|
+
delete env.DBUS_SESSION_BUS_ADDRESS;
|
|
84
|
+
}
|
|
85
|
+
const command = ChildProcess.make(executableToRun, launch?.args ?? [], {
|
|
86
|
+
cwd: root,
|
|
87
|
+
env,
|
|
88
|
+
extendEnv: false,
|
|
89
|
+
stdin: Stream.make(bytes),
|
|
90
|
+
additionalFds: launch?.seccompFilter === undefined
|
|
91
|
+
? undefined
|
|
92
|
+
: {
|
|
93
|
+
fd3: {
|
|
94
|
+
type: "input",
|
|
95
|
+
stream: Stream.make(launch.seccompFilter),
|
|
96
|
+
},
|
|
83
97
|
},
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
98
|
+
});
|
|
99
|
+
result = await runPrivateInvocationProcess(command, context.signal, 30_000);
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
standalone?.close();
|
|
103
|
+
}
|
|
87
104
|
if (result.status === "deadline_exceeded")
|
|
88
105
|
return {
|
|
89
106
|
status: "failed",
|
|
@@ -3,8 +3,15 @@ import type { KnapsackComputer } from "@fungi.computer/knapsack";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { type PrivateLocalProcessScope } from "./private-local-computer-processes.js";
|
|
5
5
|
import type { SandboxCommand } from "./private-local-containment.js";
|
|
6
|
-
/**
|
|
7
|
-
export type
|
|
6
|
+
/** A bare fallback for the native Files engine still names only staged bytes. */
|
|
7
|
+
export type BareNativeCommand = SandboxCommand & Readonly<{
|
|
8
|
+
bareNative: true;
|
|
9
|
+
}>;
|
|
10
|
+
/** Only Files supplies a helper; generic exec can still fall back to its argv. */
|
|
11
|
+
export type LocalSandboxLaunch = {
|
|
12
|
+
(cwd: string, argv: readonly [string, ...string[]]): SandboxCommand | undefined;
|
|
13
|
+
(cwd: string, argv: readonly [string, ...string[]], readOnlyHelperFile: string): SandboxCommand | BareNativeCommand;
|
|
14
|
+
};
|
|
8
15
|
declare const execOutput: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
9
16
|
exitCode: z.ZodNumber;
|
|
10
17
|
stdout: z.ZodString;
|
|
@@ -1,13 +1,21 @@
|
|
|
1
|
+
import { type LocalSandboxPolicy } from "./private-local-sandbox-policy.js";
|
|
1
2
|
export declare class LocalBwrapUnavailable extends Error {
|
|
2
|
-
readonly reason: "missing" | "invalid-digest" | "digest-mismatch" | "not-executable";
|
|
3
|
+
readonly reason: "missing" | "missing-artifact" | "invalid-digest" | "digest-mismatch" | "unreadable" | "not-executable" | "staging-failed";
|
|
3
4
|
readonly name = "LocalBwrapUnavailable";
|
|
4
|
-
constructor(reason: "missing" | "invalid-digest" | "digest-mismatch" | "not-executable", message: string, options?: ErrorOptions);
|
|
5
|
+
constructor(reason: "missing" | "missing-artifact" | "invalid-digest" | "digest-mismatch" | "unreadable" | "not-executable" | "staging-failed", message: string, options?: ErrorOptions);
|
|
5
6
|
}
|
|
6
7
|
export type LinuxSandboxPayload = {
|
|
7
|
-
readonly
|
|
8
|
+
readonly executableBytes: Uint8Array;
|
|
8
9
|
readonly seccompFilter: Uint8Array;
|
|
9
10
|
};
|
|
11
|
+
export type LinuxSandboxDigests = Readonly<{
|
|
12
|
+
bwrap: string;
|
|
13
|
+
seccomp: string;
|
|
14
|
+
}>;
|
|
15
|
+
/** Packaged patch has no sidecar; the built module owns its digest. */
|
|
16
|
+
export type NativeSourceReader = (path: string) => Buffer;
|
|
17
|
+
export declare function verifiedNativePatchHelper(path: string, project: string, readNativeSource?: NativeSourceReader): Uint8Array;
|
|
10
18
|
/** Check both packaged artifacts on every use; never cache verification. */
|
|
11
|
-
export declare function verifyLocalLinuxSandboxPayload(executable: string): LinuxSandboxPayload;
|
|
19
|
+
export declare function verifyLocalLinuxSandboxPayload(executable: string, policy: LocalSandboxPolicy, expected: LinuxSandboxDigests): LinuxSandboxPayload;
|
|
12
20
|
/** Same optional native-package lookup as apply_patch, with no PATH fallback. */
|
|
13
|
-
export declare function localLinuxSandboxPayload(): LinuxSandboxPayload;
|
|
21
|
+
export declare function localLinuxSandboxPayload(policy: LocalSandboxPolicy): LinuxSandboxPayload;
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { lstatSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
.string()
|
|
9
|
-
.trim()
|
|
10
|
-
.regex(/^[a-f0-9]{64}$/u);
|
|
6
|
+
import { nativeDigests } from "./private-local-native-digests.js";
|
|
7
|
+
import { assertHostOwnedNativeFile, UnsafeNativePayload, } from "./private-local-sandbox-policy.js";
|
|
11
8
|
export class LocalBwrapUnavailable extends Error {
|
|
12
9
|
reason;
|
|
13
10
|
name = "LocalBwrapUnavailable";
|
|
@@ -16,55 +13,93 @@ export class LocalBwrapUnavailable extends Error {
|
|
|
16
13
|
this.reason = reason;
|
|
17
14
|
}
|
|
18
15
|
}
|
|
16
|
+
export function verifiedNativePatchHelper(path, project, readNativeSource = readFileSync) {
|
|
17
|
+
const expected = nativeDigests[`${process.platform}-${process.arch}`]?.patch;
|
|
18
|
+
if (expected === undefined)
|
|
19
|
+
throw new LocalBwrapUnavailable("invalid-digest", "No built native patch digest for this platform; reinstall Fungi.");
|
|
20
|
+
return verifiedBytes(path, "fungi-apply-patch", project, expected, readNativeSource);
|
|
21
|
+
}
|
|
19
22
|
/** Check both packaged artifacts on every use; never cache verification. */
|
|
20
|
-
export function verifyLocalLinuxSandboxPayload(executable) {
|
|
21
|
-
verifiedBytes(executable, "fungi-bwrap");
|
|
22
|
-
try {
|
|
23
|
-
accessSync(executable, constants.X_OK);
|
|
24
|
-
}
|
|
25
|
-
catch (cause) {
|
|
26
|
-
throw new LocalBwrapUnavailable("not-executable", "Packaged fungi-bwrap cannot be executed; reinstall Fungi.", { cause });
|
|
27
|
-
}
|
|
23
|
+
export function verifyLocalLinuxSandboxPayload(executable, policy, expected) {
|
|
24
|
+
const executableBytes = verifiedBytes(executable, "fungi-bwrap", policy.projectRoot, expected.bwrap);
|
|
28
25
|
return {
|
|
29
|
-
|
|
30
|
-
seccompFilter: verifiedBytes(join(dirname(executable), "fungi-seccomp.bpf"), "fungi-seccomp.bpf"),
|
|
26
|
+
executableBytes,
|
|
27
|
+
seccompFilter: verifiedBytes(join(dirname(executable), "fungi-seccomp.bpf"), "fungi-seccomp.bpf", policy.projectRoot, expected.seccomp),
|
|
31
28
|
};
|
|
32
29
|
}
|
|
33
|
-
function verifiedBytes(path, label) {
|
|
30
|
+
function verifiedBytes(path, label, project, expected, readNativeSource = readFileSync) {
|
|
34
31
|
let bytes;
|
|
35
|
-
let digest;
|
|
36
32
|
try {
|
|
33
|
+
assertHostOwnedNativeFile(project, path);
|
|
37
34
|
if (!statSync(path).isFile())
|
|
38
35
|
throw new LocalBwrapUnavailable("not-executable", `Packaged ${label} is not a regular file; reinstall Fungi.`);
|
|
39
|
-
bytes =
|
|
40
|
-
digest = readFileSync(`${path}.sha256`, "utf8");
|
|
36
|
+
bytes = readNativeSource(path);
|
|
41
37
|
}
|
|
42
38
|
catch (cause) {
|
|
43
39
|
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
|
|
44
|
-
throw new LocalBwrapUnavailable("missing", `Packaged ${label}
|
|
45
|
-
|
|
40
|
+
throw new LocalBwrapUnavailable("missing-artifact", `Packaged ${label} is missing; reinstall Fungi with optional dependencies enabled.`, { cause });
|
|
41
|
+
if (cause instanceof UnsafeNativePayload ||
|
|
42
|
+
cause instanceof LocalBwrapUnavailable)
|
|
43
|
+
throw cause;
|
|
44
|
+
throw new LocalBwrapUnavailable("unreadable", `Packaged ${label} cannot be read; reinstall Fungi. No command was started.`, { cause });
|
|
46
45
|
}
|
|
47
|
-
|
|
48
|
-
if (!expected.success)
|
|
49
|
-
throw new LocalBwrapUnavailable("invalid-digest", `Packaged ${label} has an invalid SHA-256 record; reinstall Fungi.`);
|
|
50
|
-
if (createHash("sha256").update(bytes).digest("hex") !== expected.data)
|
|
46
|
+
if (createHash("sha256").update(bytes).digest("hex") !== expected)
|
|
51
47
|
throw new LocalBwrapUnavailable("digest-mismatch", `Packaged ${label} digest mismatch; reinstall Fungi. No command was started.`);
|
|
52
48
|
return bytes;
|
|
53
49
|
}
|
|
54
50
|
/** Same optional native-package lookup as apply_patch, with no PATH fallback. */
|
|
55
|
-
export function localLinuxSandboxPayload() {
|
|
51
|
+
export function localLinuxSandboxPayload(policy) {
|
|
52
|
+
const digest = nativeDigests[`${process.platform}-${process.arch}`];
|
|
53
|
+
if (digest?.bwrap === undefined || digest.seccomp === undefined)
|
|
54
|
+
throw new LocalBwrapUnavailable("invalid-digest", "No built native Linux payload digest for this platform; reinstall Fungi.");
|
|
56
55
|
let executable;
|
|
57
56
|
if (process.env.FUNGI_BUNDLED_CLI) {
|
|
58
57
|
const name = `@fungi.computer/spore-native-${process.platform}-${process.arch}`;
|
|
58
|
+
const message = `Packaged ${name}/bin/fungi-bwrap cannot be read; reinstall Fungi.`;
|
|
59
|
+
let directories;
|
|
60
|
+
try {
|
|
61
|
+
directories = createRequire(process.env.FUNGI_BUNDLED_CLI).resolve.paths(name);
|
|
62
|
+
}
|
|
63
|
+
catch (cause) {
|
|
64
|
+
throw new LocalBwrapUnavailable("missing-artifact", message, { cause });
|
|
65
|
+
}
|
|
66
|
+
if (directories === null)
|
|
67
|
+
throw new LocalBwrapUnavailable("missing-artifact", message);
|
|
68
|
+
let packageRoot;
|
|
69
|
+
for (const directory of directories) {
|
|
70
|
+
const candidate = join(directory, name);
|
|
71
|
+
try {
|
|
72
|
+
if (lstatSync(candidate, { throwIfNoEntry: false }) !== undefined) {
|
|
73
|
+
packageRoot = candidate;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (cause) {
|
|
78
|
+
throw new LocalBwrapUnavailable("missing-artifact", message, { cause });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (packageRoot === undefined)
|
|
82
|
+
throw new LocalBwrapUnavailable("missing", `Missing ${name}; reinstall Fungi with optional dependencies enabled.`);
|
|
83
|
+
// Package metadata (including exports) is not an authority for whether
|
|
84
|
+
// native bytes are absent. Check the package's location before its bin.
|
|
59
85
|
try {
|
|
60
|
-
|
|
86
|
+
assertHostOwnedNativeFile(policy.projectRoot, packageRoot);
|
|
87
|
+
if (!statSync(packageRoot).isDirectory())
|
|
88
|
+
throw new LocalBwrapUnavailable("missing-artifact", message);
|
|
61
89
|
}
|
|
62
90
|
catch (cause) {
|
|
63
|
-
|
|
91
|
+
if (cause instanceof UnsafeNativePayload ||
|
|
92
|
+
cause instanceof LocalBwrapUnavailable)
|
|
93
|
+
throw cause;
|
|
94
|
+
throw new LocalBwrapUnavailable("missing-artifact", message, { cause });
|
|
64
95
|
}
|
|
96
|
+
executable = join(packageRoot, "bin", "fungi-bwrap");
|
|
65
97
|
}
|
|
66
98
|
else {
|
|
67
99
|
executable = fileURLToPath(new URL("../native/bin/fungi-bwrap", import.meta.url));
|
|
68
100
|
}
|
|
69
|
-
return verifyLocalLinuxSandboxPayload(executable
|
|
101
|
+
return verifyLocalLinuxSandboxPayload(executable, policy, {
|
|
102
|
+
bwrap: digest.bwrap,
|
|
103
|
+
seccomp: digest.seccomp,
|
|
104
|
+
});
|
|
70
105
|
}
|
|
@@ -1,12 +1,19 @@
|
|
|
1
|
+
import { type NativePayloadStaging } from "./private-local-native-staging.js";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
export declare const sandboxUnavailableReasonSchema: z.ZodObject<{
|
|
3
4
|
kind: z.ZodEnum<{
|
|
4
5
|
missing: "missing";
|
|
6
|
+
"missing-artifact": "missing-artifact";
|
|
5
7
|
"invalid-digest": "invalid-digest";
|
|
6
8
|
"digest-mismatch": "digest-mismatch";
|
|
9
|
+
unreadable: "unreadable";
|
|
7
10
|
"not-executable": "not-executable";
|
|
11
|
+
"staging-failed": "staging-failed";
|
|
8
12
|
"unsupported-platform": "unsupported-platform";
|
|
9
13
|
"invalid-folder": "invalid-folder";
|
|
14
|
+
"unsafe-native-payload": "unsafe-native-payload";
|
|
15
|
+
"unsafe-host-code": "unsafe-host-code";
|
|
16
|
+
"startup-refused": "startup-refused";
|
|
10
17
|
"backend-refused": "backend-refused";
|
|
11
18
|
}>;
|
|
12
19
|
message: z.ZodString;
|
|
@@ -28,7 +35,7 @@ export type SandboxCommand = Readonly<{
|
|
|
28
35
|
}>;
|
|
29
36
|
/** Existing foreground/background owners retain process and cancellation lifetime. */
|
|
30
37
|
export type LocalSandbox = Readonly<{
|
|
31
|
-
command(cwd: string, argv: readonly [string, ...string[]]): SandboxCommand;
|
|
38
|
+
command(cwd: string, argv: readonly [string, ...string[]], readOnlyHelperFile?: string): SandboxCommand;
|
|
32
39
|
close(): void;
|
|
33
40
|
}>;
|
|
34
41
|
export type SandboxAdmission = Readonly<{
|
|
@@ -38,6 +45,7 @@ export type SandboxAdmission = Readonly<{
|
|
|
38
45
|
state: "unavailable";
|
|
39
46
|
reason: SandboxUnavailableReason;
|
|
40
47
|
}>;
|
|
48
|
+
export declare function sandboxUnavailableReason(cause: unknown): SandboxUnavailableReason;
|
|
41
49
|
/**
|
|
42
50
|
* The caller already owns the person's auto-approve choice. This module has no
|
|
43
51
|
* mode, setting, bare-command branch, PATH fallback or permission to widen it.
|
|
@@ -48,4 +56,4 @@ export declare function establishLocalSandbox(input: Readonly<{
|
|
|
48
56
|
/** Absolute folder selected by the Local owner. */
|
|
49
57
|
folder: string;
|
|
50
58
|
platform?: string;
|
|
51
|
-
}
|
|
59
|
+
}>, staging?: NativePayloadStaging): SandboxAdmission;
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { closeSync, mkdtempSync, openSync, realpathSync, rmSync,
|
|
2
|
+
import { closeSync, mkdtempSync, openSync, realpathSync, rmSync, } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { LocalBwrapUnavailable
|
|
5
|
+
import { LocalBwrapUnavailable } from "./private-local-bwrap.js";
|
|
6
|
+
import { createNativePayloadStaging, } from "./private-local-native-staging.js";
|
|
6
7
|
import { linuxSandboxCommand } from "./private-local-linux-sandbox.js";
|
|
7
8
|
import { seatbeltCommand } from "./private-local-seatbelt.js";
|
|
8
|
-
import { sandboxPolicy } from "./private-local-sandbox-policy.js";
|
|
9
|
+
import { sandboxPolicy, UnsafeHostCode, UnsafeNativePayload, } from "./private-local-sandbox-policy.js";
|
|
9
10
|
import { z } from "zod";
|
|
10
11
|
export const sandboxUnavailableReasonSchema = z.strictObject({
|
|
11
12
|
kind: z.enum([
|
|
12
13
|
"unsupported-platform",
|
|
13
14
|
"invalid-folder",
|
|
14
15
|
"missing",
|
|
16
|
+
"missing-artifact",
|
|
15
17
|
"invalid-digest",
|
|
16
18
|
"digest-mismatch",
|
|
19
|
+
"unreadable",
|
|
17
20
|
"not-executable",
|
|
21
|
+
"staging-failed",
|
|
22
|
+
"unsafe-native-payload",
|
|
23
|
+
"unsafe-host-code",
|
|
24
|
+
"startup-refused",
|
|
18
25
|
"backend-refused",
|
|
19
26
|
]),
|
|
20
27
|
message: z.string(),
|
|
@@ -27,7 +34,11 @@ export class LocalSandboxUnavailable extends Error {
|
|
|
27
34
|
this.reason = reason;
|
|
28
35
|
}
|
|
29
36
|
}
|
|
30
|
-
function
|
|
37
|
+
export function sandboxUnavailableReason(cause) {
|
|
38
|
+
if (cause instanceof UnsafeHostCode)
|
|
39
|
+
return { kind: "unsafe-host-code", message: cause.message };
|
|
40
|
+
if (cause instanceof UnsafeNativePayload)
|
|
41
|
+
return { kind: "unsafe-native-payload", message: cause.message };
|
|
31
42
|
if (cause instanceof LocalBwrapUnavailable)
|
|
32
43
|
return { kind: cause.reason, message: cause.message };
|
|
33
44
|
return {
|
|
@@ -40,7 +51,7 @@ function unavailable(cause) {
|
|
|
40
51
|
* mode, setting, bare-command branch, PATH fallback or permission to widen it.
|
|
41
52
|
* A ready result proves only that the backend can start, not the escape matrix.
|
|
42
53
|
*/
|
|
43
|
-
export function establishLocalSandbox(input) {
|
|
54
|
+
export function establishLocalSandbox(input, staging) {
|
|
44
55
|
try {
|
|
45
56
|
sandboxPolicy(input.project, input.folder);
|
|
46
57
|
}
|
|
@@ -62,14 +73,17 @@ export function establishLocalSandbox(input) {
|
|
|
62
73
|
message: `Local sandbox is unavailable on ${platform}.`,
|
|
63
74
|
},
|
|
64
75
|
};
|
|
65
|
-
|
|
76
|
+
const native = staging ?? createNativePayloadStaging(input.project);
|
|
77
|
+
let agentTemporaryDirectory;
|
|
66
78
|
try {
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
79
|
+
// Native staging is never granted as an Agent-writable temporary root.
|
|
80
|
+
if (platform === "darwin")
|
|
81
|
+
agentTemporaryDirectory = realpathSync(mkdtempSync(join(tmpdir(), "fungi-sandbox-")));
|
|
82
|
+
const agentTemporary = agentTemporaryDirectory;
|
|
83
|
+
const environmentTmp = agentTemporary ?? "/tmp";
|
|
70
84
|
let closed = false;
|
|
71
85
|
const sandbox = {
|
|
72
|
-
command: (cwd, argv) => {
|
|
86
|
+
command: (cwd, argv, readOnlyHelperFile) => {
|
|
73
87
|
if (closed)
|
|
74
88
|
throw new LocalSandboxUnavailable({
|
|
75
89
|
kind: "backend-refused",
|
|
@@ -79,23 +93,45 @@ export function establishLocalSandbox(input) {
|
|
|
79
93
|
// it must not turn into an authorized unsandboxed retry.
|
|
80
94
|
const policy = sandboxPolicy(input.project, input.folder);
|
|
81
95
|
try {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
96
|
+
const linuxPayload = platform === "linux" ? native.linux(policy) : undefined;
|
|
97
|
+
const stagedHelper = readOnlyHelperFile === undefined
|
|
98
|
+
? undefined
|
|
99
|
+
: native.patch(readOnlyHelperFile);
|
|
100
|
+
const stagedArgv = stagedHelper === undefined
|
|
101
|
+
? argv
|
|
102
|
+
: [
|
|
103
|
+
argv[0] === readOnlyHelperFile ? stagedHelper : argv[0],
|
|
104
|
+
...argv
|
|
105
|
+
.slice(1)
|
|
106
|
+
.map((argument) => argument === readOnlyHelperFile ? stagedHelper : argument),
|
|
107
|
+
];
|
|
108
|
+
const launch = linuxPayload !== undefined
|
|
109
|
+
? linuxSandboxCommand(policy, linuxPayload, cwd, stagedArgv, undefined, stagedHelper)
|
|
110
|
+
: seatbeltCommand({
|
|
111
|
+
...policy,
|
|
112
|
+
temporaryDirectory: environmentTmp,
|
|
113
|
+
}, stagedArgv);
|
|
85
114
|
return {
|
|
86
115
|
...launch,
|
|
87
|
-
environment: { TMPDIR:
|
|
116
|
+
environment: { TMPDIR: environmentTmp },
|
|
88
117
|
};
|
|
89
118
|
}
|
|
90
119
|
catch (cause) {
|
|
91
|
-
throw new LocalSandboxUnavailable(
|
|
120
|
+
throw new LocalSandboxUnavailable(sandboxUnavailableReason(cause));
|
|
92
121
|
}
|
|
93
122
|
},
|
|
94
123
|
close: () => {
|
|
95
124
|
if (closed)
|
|
96
125
|
return;
|
|
97
126
|
closed = true;
|
|
98
|
-
|
|
127
|
+
try {
|
|
128
|
+
if (staging === undefined)
|
|
129
|
+
native.close();
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
if (agentTemporary !== undefined)
|
|
133
|
+
rmSync(agentTemporary, { recursive: true });
|
|
134
|
+
}
|
|
99
135
|
},
|
|
100
136
|
};
|
|
101
137
|
const command = sandbox.command(realpathSync(input.folder), [
|
|
@@ -104,8 +140,7 @@ export function establishLocalSandbox(input) {
|
|
|
104
140
|
let filterDescriptor;
|
|
105
141
|
const probe = () => {
|
|
106
142
|
if (command.seccompFilter !== undefined) {
|
|
107
|
-
const path =
|
|
108
|
-
writeFileSync(path, command.seccompFilter, { mode: 0o600 });
|
|
143
|
+
const path = native.probeFilter(command.seccompFilter);
|
|
109
144
|
filterDescriptor = openSync(path, "r");
|
|
110
145
|
}
|
|
111
146
|
return spawnSync(command.executable, [...command.args], {
|
|
@@ -134,11 +169,11 @@ export function establishLocalSandbox(input) {
|
|
|
134
169
|
})();
|
|
135
170
|
if (result.status !== 0) {
|
|
136
171
|
sandbox.close();
|
|
137
|
-
const stderr = result.stderr.trim();
|
|
172
|
+
const stderr = result.error === undefined ? result.stderr.trim() : "";
|
|
138
173
|
return {
|
|
139
174
|
state: "unavailable",
|
|
140
175
|
reason: {
|
|
141
|
-
kind: "
|
|
176
|
+
kind: "startup-refused",
|
|
142
177
|
message: result.error?.message ??
|
|
143
178
|
(stderr.length > 0
|
|
144
179
|
? stderr
|
|
@@ -149,13 +184,19 @@ export function establishLocalSandbox(input) {
|
|
|
149
184
|
return { state: "ready", sandbox };
|
|
150
185
|
}
|
|
151
186
|
catch (cause) {
|
|
152
|
-
|
|
153
|
-
|
|
187
|
+
try {
|
|
188
|
+
if (staging === undefined)
|
|
189
|
+
native.close();
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
if (agentTemporaryDirectory !== undefined)
|
|
193
|
+
rmSync(agentTemporaryDirectory, { recursive: true });
|
|
194
|
+
}
|
|
154
195
|
return {
|
|
155
196
|
state: "unavailable",
|
|
156
197
|
reason: cause instanceof LocalSandboxUnavailable
|
|
157
198
|
? cause.reason
|
|
158
|
-
:
|
|
199
|
+
: sandboxUnavailableReason(cause),
|
|
159
200
|
};
|
|
160
201
|
}
|
|
161
202
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { type LocalSandboxPolicy } from "./private-local-sandbox-policy.js";
|
|
2
2
|
import type { LinuxSandboxPayload } from "./private-local-bwrap.js";
|
|
3
3
|
/** Ordinary upstream bwrap argv. There is no approval or bare-command path. */
|
|
4
|
-
export declare function linuxSandboxCommand(policy: LocalSandboxPolicy, payload: LinuxSandboxPayload,
|
|
5
|
-
args: string[];
|
|
4
|
+
export declare function linuxSandboxCommand(policy: LocalSandboxPolicy, payload: Omit<LinuxSandboxPayload, "executableBytes"> & {
|
|
6
5
|
executable: string;
|
|
6
|
+
}, cwd: string, argv: readonly [string, ...string[]], host?: NodeJS.ProcessEnv, readOnlyHelperFile?: string): {
|
|
7
|
+
args: string[];
|
|
7
8
|
seccompFilter: Uint8Array;
|
|
9
|
+
executable: string;
|
|
8
10
|
};
|
|
@@ -2,15 +2,21 @@ import { existsSync, mkdirSync, realpathSync, statSync, writeFileSync, } from "n
|
|
|
2
2
|
import { isAbsolute, join } 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
|
+
const reserved = new Set(policy.readOnly);
|
|
9
10
|
for (const git of policy.immovable) {
|
|
10
|
-
|
|
11
|
+
if (reserved.has(join(git, "hooks")))
|
|
12
|
+
mkdirSync(join(git, "hooks"), { recursive: true });
|
|
11
13
|
for (const config of ["config", "config.worktree"])
|
|
12
|
-
if (!existsSync(join(git, config)))
|
|
14
|
+
if (reserved.has(join(git, config)) && !existsSync(join(git, config)))
|
|
13
15
|
writeFileSync(join(git, config), "", { flag: "wx" });
|
|
16
|
+
// Git reads a present commondir and rejects an empty one; "." names the
|
|
17
|
+
// Git directory itself, which is what an absent commondir means.
|
|
18
|
+
if (!existsSync(join(git, "commondir")))
|
|
19
|
+
writeFileSync(join(git, "commondir"), ".", { flag: "wx" });
|
|
14
20
|
}
|
|
15
21
|
const args = [
|
|
16
22
|
"--unshare-all",
|
|
@@ -62,6 +68,13 @@ export function linuxSandboxCommand(policy, payload, cwd, argv, host = process.e
|
|
|
62
68
|
existsSync(agentSocket) &&
|
|
63
69
|
statSync(agentSocket).isSocket())
|
|
64
70
|
args.push("--ro-bind", "/dev/null", realpathSync(agentSocket));
|
|
71
|
+
// Native Files passes its resolved helper, never an untrusted exec argv[0].
|
|
72
|
+
// Re-expose only that file after private mounts; its siblings stay hidden.
|
|
73
|
+
if (readOnlyHelperFile !== undefined) {
|
|
74
|
+
if (!statSync(readOnlyHelperFile).isFile())
|
|
75
|
+
throw new Error("Local native helper must be a regular file");
|
|
76
|
+
args.push("--ro-bind", readOnlyHelperFile, readOnlyHelperFile);
|
|
77
|
+
}
|
|
65
78
|
args.push("--chdir", cwd, "--", ...argv);
|
|
66
79
|
return { ...payload, args };
|
|
67
80
|
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// Generated from native build output; not a runtime sidecar.
|
|
2
|
+
export const nativeDigests = Object.freeze({"linux-x64":{"patch":"df8cca6e61abec1438f96b74cf415a1609caef3868ad2e0ebe0a8c20bd571b87","bwrap":"035ac5ddfc70dd7d4a9b575b351944d47aa9d0bbafeb433fb72b2afb02cef7f4","seccomp":"38112f9cf994bed2ed962fae63e803c41874732ab032088b2c1ea3d713e51c11"}});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type NativeSourceReader } from "./private-local-bwrap.js";
|
|
2
|
+
import { type LocalSandboxPolicy, type NativeRunLivenessProbe } from "./private-local-sandbox-policy.js";
|
|
3
|
+
/**
|
|
4
|
+
* One invocation owner for pinned native bytes. Verify the installed source on
|
|
5
|
+
* every admission, but execute/bind only the first private copy of those
|
|
6
|
+
* verified bytes. The built digest pins each label to a single payload.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createNativePayloadStaging(project: string, readNativePatchSource?: NativeSourceReader, probeNativeRunLiveness?: NativeRunLivenessProbe): {
|
|
9
|
+
patch: (path: string) => string;
|
|
10
|
+
linux: (policy: LocalSandboxPolicy) => {
|
|
11
|
+
executable: string;
|
|
12
|
+
seccompFilter: Uint8Array<ArrayBufferLike>;
|
|
13
|
+
};
|
|
14
|
+
probeFilter: (bytes: Uint8Array) => string;
|
|
15
|
+
close: () => void;
|
|
16
|
+
};
|
|
17
|
+
export type NativePayloadStaging = ReturnType<typeof createNativePayloadStaging>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { rmSync } from "node:fs";
|
|
2
|
+
import { localLinuxSandboxPayload, verifiedNativePatchHelper, LocalBwrapUnavailable, } from "./private-local-bwrap.js";
|
|
3
|
+
import { privateNativeDirectory, stageNativeBytes, UnsafeNativePayload, } from "./private-local-sandbox-policy.js";
|
|
4
|
+
/**
|
|
5
|
+
* One invocation owner for pinned native bytes. Verify the installed source on
|
|
6
|
+
* every admission, but execute/bind only the first private copy of those
|
|
7
|
+
* verified bytes. The built digest pins each label to a single payload.
|
|
8
|
+
*/
|
|
9
|
+
export function createNativePayloadStaging(project, readNativePatchSource, probeNativeRunLiveness) {
|
|
10
|
+
let directory;
|
|
11
|
+
let closed = false;
|
|
12
|
+
const staged = new Map();
|
|
13
|
+
const stage = (label, bytes) => {
|
|
14
|
+
if (closed)
|
|
15
|
+
throw new Error("Native staging owner is closed");
|
|
16
|
+
const existing = staged.get(label);
|
|
17
|
+
if (existing !== undefined)
|
|
18
|
+
return existing;
|
|
19
|
+
try {
|
|
20
|
+
directory ??= privateNativeDirectory(project, probeNativeRunLiveness);
|
|
21
|
+
const path = stageNativeBytes(directory, label, bytes);
|
|
22
|
+
staged.set(label, path);
|
|
23
|
+
return path;
|
|
24
|
+
}
|
|
25
|
+
catch (cause) {
|
|
26
|
+
if (cause instanceof UnsafeNativePayload)
|
|
27
|
+
throw cause;
|
|
28
|
+
throw new LocalBwrapUnavailable("staging-failed", `Cannot stage verified ${label} in a private native directory; no command was started.`, { cause });
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
patch: (path) => stage("fungi-apply-patch", verifiedNativePatchHelper(path, project, readNativePatchSource)),
|
|
33
|
+
linux: (policy) => {
|
|
34
|
+
const payload = localLinuxSandboxPayload(policy);
|
|
35
|
+
return {
|
|
36
|
+
executable: stage("fungi-bwrap", payload.executableBytes),
|
|
37
|
+
seccompFilter: payload.seccompFilter,
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
probeFilter: (bytes) => stage("fungi-seccomp.bpf", bytes),
|
|
41
|
+
close: () => {
|
|
42
|
+
if (closed)
|
|
43
|
+
return;
|
|
44
|
+
closed = true;
|
|
45
|
+
if (directory !== undefined)
|
|
46
|
+
rmSync(directory, { recursive: true });
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createAssistantMessageEventStream } from "@earendil-works/pi-ai/utils/event-stream";
|
|
2
|
-
import { observeProviderStatus } from "@fungi.computer/mule";
|
|
2
|
+
import { observeProviderStatus, normalizeModel, decodeProviderData, } from "@fungi.computer/mule";
|
|
3
|
+
import { normalizeContext } from "@earendil-works/pi-ai/utils/transcript";
|
|
3
4
|
import { modelFailureMessage, modelFailureRetryable, } from "./private-provider-rejection.js";
|
|
4
5
|
import { logEvent } from "./log-event.js";
|
|
5
6
|
import { localArchiveHandoff } from "./local-archive-handoff.js";
|
|
@@ -556,7 +557,7 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
|
|
|
556
557
|
// prose that any SDK can supply.
|
|
557
558
|
let modelFailure;
|
|
558
559
|
return Promise.resolve(Object.freeze({
|
|
559
|
-
model: selected,
|
|
560
|
+
model: normalizeModel(selected),
|
|
560
561
|
retryable: (message) => !signal.aborted &&
|
|
561
562
|
message.stopReason === "error" &&
|
|
562
563
|
modelFailure?.message === message &&
|
|
@@ -598,13 +599,19 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
|
|
|
598
599
|
};
|
|
599
600
|
void (async () => {
|
|
600
601
|
try {
|
|
601
|
-
const
|
|
602
|
-
|
|
602
|
+
const { onPayload, onResponse, ...rest } = streamOptions;
|
|
603
|
+
const requestOptions = {
|
|
604
|
+
...rest,
|
|
603
605
|
fetch: provider.fetch,
|
|
604
|
-
onProviderStreamEvent: provider.onProviderStreamEvent,
|
|
606
|
+
onProviderStreamEvent: (data, model) => provider.onProviderStreamEvent(decodeProviderData(data), normalizeModel(model)),
|
|
605
607
|
sessionId: providerCacheKey,
|
|
606
608
|
transport: options?.transport ?? "auto",
|
|
607
|
-
}
|
|
609
|
+
};
|
|
610
|
+
if (onPayload !== undefined)
|
|
611
|
+
requestOptions.onPayload = (data, model) => onPayload(decodeProviderData(data), normalizeModel(model));
|
|
612
|
+
if (onResponse !== undefined)
|
|
613
|
+
requestOptions.onResponse = (response, model) => onResponse(response, normalizeModel(model));
|
|
614
|
+
const upstream = turn.models.streamSimple(selected, normalizeContext(context), requestOptions);
|
|
608
615
|
for await (const event of upstream) {
|
|
609
616
|
if (event.type === "error") {
|
|
610
617
|
const error = failure(event.error.usage, event.reason === "aborted", event.error);
|