@fungi.computer/truffle 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +542 -2
- package/computerd-loopback.cjs +16 -0
- package/dist/auth-cli.d.ts +46 -0
- package/dist/auth-cli.js +373 -0
- package/dist/fungi-account-command.d.ts +18 -0
- package/dist/fungi-account-command.js +95 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +23 -0
- package/dist/internal/image-fit-metafile.json +106 -0
- package/dist/internal/local-image-fit-worker.mjs +960 -0
- package/dist/internal/private-workerd-sandbox-worker.mjs +27487 -0
- package/dist/internal/sandbox-metafile.json +6726 -0
- package/dist/linear-auth.d.ts +4 -0
- package/dist/linear-auth.js +72 -0
- package/dist/local-archive-handoff.d.ts +12 -0
- package/dist/local-archive-handoff.js +21 -0
- package/dist/local-extensions.d.ts +143 -0
- package/dist/local-extensions.js +624 -0
- package/dist/local-image-fit.d.ts +30 -0
- package/dist/local-image-fit.js +105 -0
- package/dist/local-login.d.ts +55 -0
- package/dist/local-login.js +167 -0
- package/dist/local-models.d.ts +16 -0
- package/dist/local-models.js +82 -0
- package/dist/local-session-archives.d.ts +8 -0
- package/dist/local-session-archives.js +152 -0
- package/dist/local-skill-source.d.ts +21 -0
- package/dist/local-skill-source.js +433 -0
- package/dist/log-event.d.ts +9 -0
- package/dist/log-event.js +13 -0
- package/dist/private-bounded-response.d.ts +2 -0
- package/dist/private-bounded-response.js +41 -0
- package/dist/private-bun-shiitake-host.d.ts +11 -0
- package/dist/private-bun-shiitake-host.js +66 -0
- package/dist/private-file-credential-store.d.ts +58 -0
- package/dist/private-file-credential-store.js +331 -0
- package/dist/private-files-patch.d.ts +4 -0
- package/dist/private-files-patch.js +104 -0
- package/dist/private-fungi-account.d.ts +252 -0
- package/dist/private-fungi-account.js +1329 -0
- package/dist/private-fungi-auth-error.d.ts +5 -0
- package/dist/private-fungi-auth-error.js +7 -0
- package/dist/private-fungi-mcp-connection.d.ts +26 -0
- package/dist/private-fungi-mcp-connection.js +162 -0
- package/dist/private-fungi-mcp-endpoint.d.ts +2 -0
- package/dist/private-fungi-mcp-endpoint.js +15 -0
- package/dist/private-fungi-mcp-selection-schema.d.ts +7 -0
- package/dist/private-fungi-mcp-selection-schema.js +15 -0
- package/dist/private-fungi-mcp-selection.d.ts +14 -0
- package/dist/private-fungi-mcp-selection.js +49 -0
- package/dist/private-fungi-model-api.d.ts +6 -0
- package/dist/private-fungi-model-api.js +125 -0
- package/dist/private-fungi-model-catalog.d.ts +16 -0
- package/dist/private-fungi-model-catalog.js +86 -0
- package/dist/private-fungi-model-provider.d.ts +6 -0
- package/dist/private-fungi-model-provider.js +51 -0
- package/dist/private-fungi-pending-logouts.d.ts +34 -0
- package/dist/private-fungi-pending-logouts.js +135 -0
- package/dist/private-fungi-service-logout.d.ts +44 -0
- package/dist/private-fungi-service-logout.js +59 -0
- package/dist/private-invocation-computer.d.ts +58 -0
- package/dist/private-invocation-computer.js +498 -0
- package/dist/private-invocation-files.d.ts +30 -0
- package/dist/private-invocation-files.js +520 -0
- package/dist/private-local-acp-stdio.d.ts +17 -0
- package/dist/private-local-acp-stdio.js +145 -0
- package/dist/private-local-admission-gate.d.ts +14 -0
- package/dist/private-local-admission-gate.js +52 -0
- package/dist/private-local-agent-control.d.ts +101 -0
- package/dist/private-local-agent-control.js +30 -0
- package/dist/private-local-agent-default-model.d.ts +12 -0
- package/dist/private-local-agent-default-model.js +31 -0
- package/dist/private-local-auth-rpc.d.ts +129 -0
- package/dist/private-local-auth-rpc.js +396 -0
- package/dist/private-local-bwrap.d.ts +13 -0
- package/dist/private-local-bwrap.js +70 -0
- package/dist/private-local-client-version.d.ts +5 -0
- package/dist/private-local-client-version.js +43 -0
- package/dist/private-local-computer-attach.d.ts +49 -0
- package/dist/private-local-computer-attach.js +230 -0
- package/dist/private-local-computer-backend.d.ts +282 -0
- package/dist/private-local-computer-backend.js +402 -0
- package/dist/private-local-computer-processes.d.ts +202 -0
- package/dist/private-local-computer-processes.js +613 -0
- package/dist/private-local-config-root.d.ts +21 -0
- package/dist/private-local-config-root.js +19 -0
- package/dist/private-local-containment.d.ts +51 -0
- package/dist/private-local-containment.js +161 -0
- package/dist/private-local-default-model.d.ts +11 -0
- package/dist/private-local-default-model.js +45 -0
- package/dist/private-local-delegation.d.ts +36 -0
- package/dist/private-local-delegation.js +133 -0
- package/dist/private-local-effective-model.d.ts +27 -0
- package/dist/private-local-effective-model.js +17 -0
- package/dist/private-local-exec-environment.d.ts +16 -0
- package/dist/private-local-exec-environment.js +11 -0
- package/dist/private-local-exec-launch.d.ts +41 -0
- package/dist/private-local-exec-launch.js +51 -0
- package/dist/private-local-exec-path.d.ts +6 -0
- package/dist/private-local-exec-path.js +40 -0
- package/dist/private-local-git-exclude.d.ts +2 -0
- package/dist/private-local-git-exclude.js +33 -0
- package/dist/private-local-http-dispatch.d.ts +5 -0
- package/dist/private-local-http-dispatch.js +14 -0
- package/dist/private-local-linux-sandbox.d.ts +8 -0
- package/dist/private-local-linux-sandbox.js +67 -0
- package/dist/private-local-mail.d.ts +13 -0
- package/dist/private-local-mail.js +77 -0
- package/dist/private-local-mesh-enrollment.d.ts +34 -0
- package/dist/private-local-mesh-enrollment.js +188 -0
- package/dist/private-local-mesh-links.d.ts +21 -0
- package/dist/private-local-mesh-links.js +229 -0
- package/dist/private-local-mesh-record.d.ts +120 -0
- package/dist/private-local-mesh-record.js +55 -0
- package/dist/private-local-process-identity.d.ts +20 -0
- package/dist/private-local-process-identity.js +40 -0
- package/dist/private-local-runtime-host.d.ts +192 -0
- package/dist/private-local-runtime-host.js +1177 -0
- package/dist/private-local-sandbox-policy.d.ts +21 -0
- package/dist/private-local-sandbox-policy.js +121 -0
- package/dist/private-local-sandbox.d.ts +37 -0
- package/dist/private-local-sandbox.js +114 -0
- package/dist/private-local-seatbelt.d.ts +24 -0
- package/dist/private-local-seatbelt.js +68 -0
- package/dist/private-local-service-contract.d.ts +170 -0
- package/dist/private-local-service-contract.js +1 -0
- package/dist/private-local-service-discovery.d.ts +217 -0
- package/dist/private-local-service-discovery.js +322 -0
- package/dist/private-local-service-ids.d.ts +3 -0
- package/dist/private-local-service-ids.js +8 -0
- package/dist/private-local-service-lease.d.ts +16 -0
- package/dist/private-local-service-lease.js +105 -0
- package/dist/private-local-service-listener.d.ts +22 -0
- package/dist/private-local-service-listener.js +407 -0
- package/dist/private-local-service-log.d.ts +128 -0
- package/dist/private-local-service-log.js +321 -0
- package/dist/private-local-service-owner.d.ts +135 -0
- package/dist/private-local-service-owner.js +1369 -0
- package/dist/private-local-service-process.d.ts +1 -0
- package/dist/private-local-service-process.js +8 -0
- package/dist/private-local-service-termination.d.ts +7 -0
- package/dist/private-local-service-termination.js +67 -0
- package/dist/private-local-service.d.ts +46 -0
- package/dist/private-local-service.js +606 -0
- package/dist/private-local-session-discovery.d.ts +66 -0
- package/dist/private-local-session-discovery.js +278 -0
- package/dist/private-local-shiitake-host.d.ts +44 -0
- package/dist/private-local-shiitake-host.js +143 -0
- package/dist/private-local-startup-model.d.ts +51 -0
- package/dist/private-local-startup-model.js +86 -0
- package/dist/private-local-stop-all.d.ts +27 -0
- package/dist/private-local-stop-all.js +26 -0
- package/dist/private-local-ui-attachment.d.ts +19 -0
- package/dist/private-local-ui-attachment.js +71 -0
- package/dist/private-local-worker-server.d.ts +16 -0
- package/dist/private-local-worker-server.js +144 -0
- package/dist/private-mcp-form-contract.d.ts +186 -0
- package/dist/private-mcp-form-contract.js +53 -0
- package/dist/private-mcp-form-elicitation.d.ts +18 -0
- package/dist/private-mcp-form-elicitation.js +97 -0
- package/dist/private-mcp-form-session.d.ts +172 -0
- package/dist/private-mcp-form-session.js +148 -0
- package/dist/private-openai-claims.d.ts +30 -0
- package/dist/private-openai-claims.js +69 -0
- package/dist/private-oyster-mcp-auth.d.ts +13 -0
- package/dist/private-oyster-mcp-auth.js +57 -0
- package/dist/private-provider-rejection.d.ts +15 -0
- package/dist/private-provider-rejection.js +24 -0
- package/dist/private-sqlite-owner.d.ts +39 -0
- package/dist/private-sqlite-owner.js +169 -0
- package/dist/private-team-computers.d.ts +38 -0
- package/dist/private-team-computers.js +110 -0
- package/dist/private-workerd-sandbox.d.ts +3 -0
- package/dist/private-workerd-sandbox.js +347 -0
- package/dist/seatbelt/safe-base.sbpl +79 -0
- package/dist/subscription-activation.d.ts +9 -0
- package/dist/subscription-activation.js +62 -0
- package/dist/subscription-identity.d.ts +23 -0
- package/dist/subscription-identity.js +124 -0
- package/dist/subscription-turn.d.ts +17 -0
- package/dist/subscription-turn.js +143 -0
- package/dist/team-apps.d.ts +24 -0
- package/dist/team-apps.js +128 -0
- package/dist/tracker-git-credential.d.ts +15 -0
- package/dist/tracker-git-credential.js +87 -0
- package/native/bin/LICENSE.bubblewrap +502 -0
- package/native/bin/LICENSE.libcap +112 -0
- package/native/bin/LICENSE.sandbox-runtime +201 -0
- package/native/bin/SOURCE.bubblewrap.tar.xz +0 -0
- package/native/bin/build.json +4037 -0
- package/native/bin/bwrap-build-source/Dockerfile +48 -0
- package/native/bin/bwrap-build-source/README.md +96 -0
- package/native/bin/bwrap-build-source/build.mjs +52 -0
- package/native/bin/bwrap-build-source/linux-arm64.ini +14 -0
- package/native/bin/bwrap-build-source/pin.mjs +21 -0
- package/native/bin/bwrap-build-source/record.mjs +74 -0
- package/native/bin/bwrap-build-source/vendor/LICENSE.sandbox-runtime +201 -0
- package/native/bin/bwrap-build-source/vendor/README.md +23 -0
- package/native/bin/bwrap-build-source/vendor/seccomp-unix-block.c +280 -0
- package/native/bin/bwrap-build.json +44 -0
- package/native/bin/fungi-apply-patch +0 -0
- package/native/bin/fungi-bwrap +0 -0
- package/native/bin/fungi-bwrap.sha256 +1 -0
- package/native/bin/fungi-seccomp.bpf +0 -0
- package/native/bin/fungi-seccomp.bpf.sha256 +1 -0
- package/package.json +161 -3
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const sandboxUnavailableReasonSchema: z.ZodObject<{
|
|
3
|
+
kind: z.ZodEnum<{
|
|
4
|
+
missing: "missing";
|
|
5
|
+
"invalid-digest": "invalid-digest";
|
|
6
|
+
"digest-mismatch": "digest-mismatch";
|
|
7
|
+
"not-executable": "not-executable";
|
|
8
|
+
"unsupported-platform": "unsupported-platform";
|
|
9
|
+
"invalid-folder": "invalid-folder";
|
|
10
|
+
"backend-refused": "backend-refused";
|
|
11
|
+
}>;
|
|
12
|
+
message: z.ZodString;
|
|
13
|
+
}, z.core.$strict>;
|
|
14
|
+
export type SandboxUnavailableReason = z.output<typeof sandboxUnavailableReasonSchema>;
|
|
15
|
+
export declare class LocalSandboxUnavailable extends Error {
|
|
16
|
+
readonly reason: SandboxUnavailableReason;
|
|
17
|
+
readonly name = "LocalSandboxUnavailable";
|
|
18
|
+
constructor(reason: SandboxUnavailableReason);
|
|
19
|
+
}
|
|
20
|
+
export type SandboxCommand = Readonly<{
|
|
21
|
+
executable: string;
|
|
22
|
+
args: readonly string[];
|
|
23
|
+
environment: Readonly<{
|
|
24
|
+
TMPDIR: string;
|
|
25
|
+
}>;
|
|
26
|
+
/** Linux only: feed these verified bytes to fd3; never inherit arbitrary fds. */
|
|
27
|
+
seccompFilter?: Uint8Array;
|
|
28
|
+
}>;
|
|
29
|
+
/** Existing foreground/background owners retain process and cancellation lifetime. */
|
|
30
|
+
export type LocalSandbox = Readonly<{
|
|
31
|
+
command(cwd: string, argv: readonly [string, ...string[]]): SandboxCommand;
|
|
32
|
+
close(): void;
|
|
33
|
+
}>;
|
|
34
|
+
export type SandboxAdmission = Readonly<{
|
|
35
|
+
state: "ready";
|
|
36
|
+
sandbox: LocalSandbox;
|
|
37
|
+
}> | Readonly<{
|
|
38
|
+
state: "unavailable";
|
|
39
|
+
reason: SandboxUnavailableReason;
|
|
40
|
+
}>;
|
|
41
|
+
/**
|
|
42
|
+
* The caller already owns the person's auto-approve choice. This module has no
|
|
43
|
+
* mode, setting, bare-command branch, PATH fallback or permission to widen it.
|
|
44
|
+
* A ready result proves only that the backend can start, not the escape matrix.
|
|
45
|
+
*/
|
|
46
|
+
export declare function establishLocalSandbox(input: Readonly<{
|
|
47
|
+
project: string;
|
|
48
|
+
/** Absolute folder selected by the Local owner. */
|
|
49
|
+
folder: string;
|
|
50
|
+
platform?: string;
|
|
51
|
+
}>): SandboxAdmission;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { closeSync, mkdtempSync, openSync, realpathSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { LocalBwrapUnavailable, localLinuxSandboxPayload, } from "./private-local-bwrap.js";
|
|
6
|
+
import { linuxSandboxCommand } from "./private-local-linux-sandbox.js";
|
|
7
|
+
import { seatbeltCommand } from "./private-local-seatbelt.js";
|
|
8
|
+
import { sandboxPolicy } from "./private-local-sandbox-policy.js";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
export const sandboxUnavailableReasonSchema = z.strictObject({
|
|
11
|
+
kind: z.enum([
|
|
12
|
+
"unsupported-platform",
|
|
13
|
+
"invalid-folder",
|
|
14
|
+
"missing",
|
|
15
|
+
"invalid-digest",
|
|
16
|
+
"digest-mismatch",
|
|
17
|
+
"not-executable",
|
|
18
|
+
"backend-refused",
|
|
19
|
+
]),
|
|
20
|
+
message: z.string(),
|
|
21
|
+
});
|
|
22
|
+
export class LocalSandboxUnavailable extends Error {
|
|
23
|
+
reason;
|
|
24
|
+
name = "LocalSandboxUnavailable";
|
|
25
|
+
constructor(reason) {
|
|
26
|
+
super(`Local sandbox unavailable: ${reason.message}`);
|
|
27
|
+
this.reason = reason;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function unavailable(cause) {
|
|
31
|
+
if (cause instanceof LocalBwrapUnavailable)
|
|
32
|
+
return { kind: cause.reason, message: cause.message };
|
|
33
|
+
return {
|
|
34
|
+
kind: "backend-refused",
|
|
35
|
+
message: cause instanceof Error ? cause.message : "Sandbox preparation failed.",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The caller already owns the person's auto-approve choice. This module has no
|
|
40
|
+
* mode, setting, bare-command branch, PATH fallback or permission to widen it.
|
|
41
|
+
* A ready result proves only that the backend can start, not the escape matrix.
|
|
42
|
+
*/
|
|
43
|
+
export function establishLocalSandbox(input) {
|
|
44
|
+
try {
|
|
45
|
+
sandboxPolicy(input.project, input.folder);
|
|
46
|
+
}
|
|
47
|
+
catch (cause) {
|
|
48
|
+
return {
|
|
49
|
+
state: "unavailable",
|
|
50
|
+
reason: {
|
|
51
|
+
kind: "invalid-folder",
|
|
52
|
+
message: cause instanceof Error ? cause.message : "Invalid sandbox folder.",
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const platform = input.platform ?? process.platform;
|
|
57
|
+
if (platform !== "linux" && platform !== "darwin")
|
|
58
|
+
return {
|
|
59
|
+
state: "unavailable",
|
|
60
|
+
reason: {
|
|
61
|
+
kind: "unsupported-platform",
|
|
62
|
+
message: `Local sandbox is unavailable on ${platform}.`,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
let temporaryDirectory;
|
|
66
|
+
try {
|
|
67
|
+
// A sandbox owns only its own temporary directory, never the host's HOME.
|
|
68
|
+
temporaryDirectory = realpathSync(mkdtempSync(join(tmpdir(), "fungi-sandbox-")));
|
|
69
|
+
const temporary = temporaryDirectory;
|
|
70
|
+
let closed = false;
|
|
71
|
+
const sandbox = {
|
|
72
|
+
command: (cwd, argv) => {
|
|
73
|
+
if (closed)
|
|
74
|
+
throw new LocalSandboxUnavailable({
|
|
75
|
+
kind: "backend-refused",
|
|
76
|
+
message: "Sandbox has been closed.",
|
|
77
|
+
});
|
|
78
|
+
// Failed/incomplete authority discovery is not an unavailable backend:
|
|
79
|
+
// it must not turn into an authorized unsandboxed retry.
|
|
80
|
+
const policy = sandboxPolicy(input.project, input.folder);
|
|
81
|
+
try {
|
|
82
|
+
const launch = platform === "linux"
|
|
83
|
+
? linuxSandboxCommand(policy, localLinuxSandboxPayload(), cwd, argv)
|
|
84
|
+
: seatbeltCommand({ ...policy, temporaryDirectory: temporary }, argv);
|
|
85
|
+
return {
|
|
86
|
+
...launch,
|
|
87
|
+
environment: { TMPDIR: platform === "linux" ? "/tmp" : temporary },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
catch (cause) {
|
|
91
|
+
throw new LocalSandboxUnavailable(unavailable(cause));
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
close: () => {
|
|
95
|
+
if (closed)
|
|
96
|
+
return;
|
|
97
|
+
closed = true;
|
|
98
|
+
rmSync(temporary, { recursive: true });
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
const command = sandbox.command(realpathSync(input.folder), [
|
|
102
|
+
"/usr/bin/true",
|
|
103
|
+
]);
|
|
104
|
+
let filterDescriptor;
|
|
105
|
+
const probe = () => {
|
|
106
|
+
if (command.seccompFilter !== undefined) {
|
|
107
|
+
const path = join(temporary, "seccomp.bpf");
|
|
108
|
+
writeFileSync(path, command.seccompFilter, { mode: 0o600 });
|
|
109
|
+
filterDescriptor = openSync(path, "r");
|
|
110
|
+
}
|
|
111
|
+
return spawnSync(command.executable, [...command.args], {
|
|
112
|
+
cwd: input.folder,
|
|
113
|
+
env: { ...command.environment },
|
|
114
|
+
encoding: "utf8",
|
|
115
|
+
timeout: 5_000,
|
|
116
|
+
// Node's maintained spawn allowlist closes every other inherited fd.
|
|
117
|
+
// bwrap loads and closes fd3 before exec (seccomp_program_new).
|
|
118
|
+
stdio: [
|
|
119
|
+
"ignore",
|
|
120
|
+
"pipe",
|
|
121
|
+
"pipe",
|
|
122
|
+
...(filterDescriptor === undefined ? [] : [filterDescriptor]),
|
|
123
|
+
],
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
const result = (() => {
|
|
127
|
+
try {
|
|
128
|
+
return probe();
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
if (filterDescriptor !== undefined)
|
|
132
|
+
closeSync(filterDescriptor);
|
|
133
|
+
}
|
|
134
|
+
})();
|
|
135
|
+
if (result.status !== 0) {
|
|
136
|
+
sandbox.close();
|
|
137
|
+
const stderr = result.stderr.trim();
|
|
138
|
+
return {
|
|
139
|
+
state: "unavailable",
|
|
140
|
+
reason: {
|
|
141
|
+
kind: "backend-refused",
|
|
142
|
+
message: result.error?.message ??
|
|
143
|
+
(stderr.length > 0
|
|
144
|
+
? stderr
|
|
145
|
+
: `Sandbox startup exited ${String(result.status)} (${String(result.signal)}).`),
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return { state: "ready", sandbox };
|
|
150
|
+
}
|
|
151
|
+
catch (cause) {
|
|
152
|
+
if (temporaryDirectory !== undefined)
|
|
153
|
+
rmSync(temporaryDirectory, { recursive: true });
|
|
154
|
+
return {
|
|
155
|
+
state: "unavailable",
|
|
156
|
+
reason: cause instanceof LocalSandboxUnavailable
|
|
157
|
+
? cause.reason
|
|
158
|
+
: unavailable(cause),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Models } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ModelPreference } from "@fungi.computer/shiitake/model-preference";
|
|
3
|
+
/** One model offered by a signed-in or configured provider, as a provider/model address. */
|
|
4
|
+
export type OfferedLocalModel = Readonly<{
|
|
5
|
+
address: string;
|
|
6
|
+
name: string;
|
|
7
|
+
}>;
|
|
8
|
+
/** The provider registry and its auth owner determine the available fallback. */
|
|
9
|
+
export declare function defaultLocalModel(models: Models, signal: AbortSignal): Promise<ModelPreference | null>;
|
|
10
|
+
/** List models from signed-in or configured providers only, in registry order. */
|
|
11
|
+
export declare function listOfferedLocalModels(models: Models, signal: AbortSignal): Promise<readonly OfferedLocalModel[]>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { logEvent } from "./log-event.js";
|
|
2
|
+
/** Return a provider's catalog only when its auth owner admits it. */
|
|
3
|
+
async function offeredCatalog(models, providerId, signal) {
|
|
4
|
+
signal.throwIfAborted();
|
|
5
|
+
try {
|
|
6
|
+
if ((await models.checkAuth(providerId, { signal })) === undefined)
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
catch (cause) {
|
|
10
|
+
// A provider whose login cannot be resolved offers no models here.
|
|
11
|
+
// Explicit selections report their own safe, provider-specific refusal.
|
|
12
|
+
signal.throwIfAborted();
|
|
13
|
+
logEvent("model-default.check-auth", cause);
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
if (models.getModels(providerId).length === 0) {
|
|
17
|
+
const refreshed = await models.refresh({
|
|
18
|
+
providers: [providerId],
|
|
19
|
+
signal,
|
|
20
|
+
});
|
|
21
|
+
signal.throwIfAborted();
|
|
22
|
+
if (refreshed.aborted || refreshed.errors.has(providerId))
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
return models.getModels(providerId);
|
|
26
|
+
}
|
|
27
|
+
/** The provider registry and its auth owner determine the available fallback. */
|
|
28
|
+
export async function defaultLocalModel(models, signal) {
|
|
29
|
+
for (const provider of models.getProviders()) {
|
|
30
|
+
const catalog = await offeredCatalog(models, provider.id, signal);
|
|
31
|
+
const model = catalog.find((entry) => "default" in entry && entry.default === true) ??
|
|
32
|
+
catalog[0];
|
|
33
|
+
if (model !== undefined)
|
|
34
|
+
return { providerId: provider.id, modelId: model.id };
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
/** List models from signed-in or configured providers only, in registry order. */
|
|
39
|
+
export async function listOfferedLocalModels(models, signal) {
|
|
40
|
+
const offered = [];
|
|
41
|
+
for (const provider of models.getProviders())
|
|
42
|
+
for (const model of await offeredCatalog(models, provider.id, signal))
|
|
43
|
+
offered.push({ address: `${provider.id}/${model.id}`, name: model.name });
|
|
44
|
+
return offered;
|
|
45
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { BirdDogMail } from "@fungi.computer/bird-dog";
|
|
2
|
+
import type { Accepted, CancelResult, CommandSettlement, VerifiedAgentReply } from "@fungi.computer/shiitake";
|
|
3
|
+
import type { AgentControlDirectory, AgentDelegationInput, AgentDelegationInvocation, AgentRecallInput } from "@fungi.computer/shiitake/agent-control";
|
|
4
|
+
import { createFleetDelegation } from "@fungi.computer/shiitake/fleet";
|
|
5
|
+
/**
|
|
6
|
+
* The Local service's project mail routing, shared by every Agent it owns.
|
|
7
|
+
* `deliver` admits frozen mail at its addressed Agent, opening that Agent's
|
|
8
|
+
* owner when it is idle; `readSettlement` reads one exact command there.
|
|
9
|
+
*/
|
|
10
|
+
export type PrivateLocalMailRouting = Readonly<{
|
|
11
|
+
deliver: (mail: BirdDogMail) => Promise<Accepted>;
|
|
12
|
+
readSettlement: (agentId: string, sessionId: string, commandId: string) => Promise<CommandSettlement | undefined>;
|
|
13
|
+
}>;
|
|
14
|
+
/**
|
|
15
|
+
* One Local Agent's side of delegation, over the same fleet contract as the
|
|
16
|
+
* Hub: a `followUp` send freezes BirdDog mail in this Agent's store, and an
|
|
17
|
+
* arriving reply is admitted only after `fleet.authenticateReply` checks it
|
|
18
|
+
* against that frozen mail. The replying child authors the reply line.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createPrivateLocalDelegation(input: Readonly<{
|
|
21
|
+
agentId: string;
|
|
22
|
+
storage: Parameters<typeof createFleetDelegation>[0]["storage"];
|
|
23
|
+
directory: Pick<AgentControlDirectory, "list" | "open">;
|
|
24
|
+
routing: PrivateLocalMailRouting;
|
|
25
|
+
/** Current Team role of this physical store, projected by its enrolled link. */
|
|
26
|
+
meshAdmission?: ((recipientAgentId: string) => Promise<readonly {
|
|
27
|
+
agentId: string;
|
|
28
|
+
name: string;
|
|
29
|
+
}[] | undefined>) | undefined;
|
|
30
|
+
accept: (mail: BirdDogMail, meta?: BirdDogMail["meta"], reply?: VerifiedAgentReply) => Promise<Accepted>;
|
|
31
|
+
}>): {
|
|
32
|
+
send: (task: AgentDelegationInput, sourceSessionId: string, signal: AbortSignal, invocation?: AgentDelegationInvocation) => Promise<Accepted>;
|
|
33
|
+
recall: (task: AgentRecallInput, sourceSessionId: string, signal: AbortSignal) => Promise<CancelResult>;
|
|
34
|
+
receive: (mail: BirdDogMail) => Promise<Accepted>;
|
|
35
|
+
outstanding: () => Promise<import("@fungi.computer/shiitake/fleet").OutstandingDelegation | undefined>;
|
|
36
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { deterministicReplayKey } from "@fungi.computer/cairn";
|
|
2
|
+
import { AgentArchiveRefused } from "@fungi.computer/shiitake/agent-control";
|
|
3
|
+
import { createFleetDelegation, delegationProvenance, delegationReplyMeta, delegationSendMeta, } from "@fungi.computer/shiitake/fleet";
|
|
4
|
+
/** How long one Local delegation grant stays valid while it is admitted. */
|
|
5
|
+
const grantLifetimeMs = 60_000;
|
|
6
|
+
/**
|
|
7
|
+
* One Local Agent's side of delegation, over the same fleet contract as the
|
|
8
|
+
* Hub: a `followUp` send freezes BirdDog mail in this Agent's store, and an
|
|
9
|
+
* arriving reply is admitted only after `fleet.authenticateReply` checks it
|
|
10
|
+
* against that frozen mail. The replying child authors the reply line.
|
|
11
|
+
*/
|
|
12
|
+
export function createPrivateLocalDelegation(input) {
|
|
13
|
+
const fleet = createFleetDelegation({
|
|
14
|
+
storage: input.storage,
|
|
15
|
+
deliver: input.routing.deliver,
|
|
16
|
+
readSettlement: input.routing.readSettlement,
|
|
17
|
+
open: (agentId) => input.directory.open(agentId, new AbortController().signal),
|
|
18
|
+
});
|
|
19
|
+
async function members(signal) {
|
|
20
|
+
return (await input.directory.list(signal)).map(({ agentId, name }) => ({
|
|
21
|
+
agentId,
|
|
22
|
+
name,
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
async function authorizedMembers(agentId, signal) {
|
|
26
|
+
const agents = await members(signal);
|
|
27
|
+
if (!agents.some(({ agentId: memberId }) => memberId === agentId)) {
|
|
28
|
+
const archived = await input.directory.list(signal, { archived: true });
|
|
29
|
+
if (archived.some(({ agentId: memberId }) => memberId === agentId))
|
|
30
|
+
throw new AgentArchiveRefused("archived", `Agent ${agentId} is archived; unarchive it to send it work`);
|
|
31
|
+
throw new Error("Agent is outside this Local project");
|
|
32
|
+
}
|
|
33
|
+
return agents;
|
|
34
|
+
}
|
|
35
|
+
function requestKey(task, sourceSessionId) {
|
|
36
|
+
return deterministicReplayKey("local-agent-delegation", [
|
|
37
|
+
input.agentId,
|
|
38
|
+
sourceSessionId,
|
|
39
|
+
task.agentId,
|
|
40
|
+
task.sessionId,
|
|
41
|
+
task.requestId,
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
/** Send with one caller-owned identity; follow-up replies return here. */
|
|
45
|
+
async function send(task, sourceSessionId, signal, invocation) {
|
|
46
|
+
if (invocation === undefined)
|
|
47
|
+
throw new Error("Delegation requires a tool invocation");
|
|
48
|
+
const agents = await authorizedMembers(task.agentId, signal);
|
|
49
|
+
const requestId = await requestKey(task, sourceSessionId);
|
|
50
|
+
signal.throwIfAborted();
|
|
51
|
+
if (task.delivery === "steer") {
|
|
52
|
+
const opened = await input.directory.open(task.agentId, signal);
|
|
53
|
+
try {
|
|
54
|
+
return await opened.agent.session(task.sessionId).send(task.text, {
|
|
55
|
+
requestId,
|
|
56
|
+
delivery: "steer",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
await opened.release();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const meta = delegationSendMeta({ id: input.agentId, name: input.agentId }, invocation.meta);
|
|
64
|
+
// The caller's request key owns replay, not the current model program's bytes.
|
|
65
|
+
const provenance = await delegationProvenance({
|
|
66
|
+
agentId: input.agentId,
|
|
67
|
+
sessionId: sourceSessionId,
|
|
68
|
+
});
|
|
69
|
+
signal.throwIfAborted();
|
|
70
|
+
return fleet.start({
|
|
71
|
+
agents: agents.map(({ agentId }) => agentId),
|
|
72
|
+
actions: ["delegate"],
|
|
73
|
+
expiresAt: Date.now() + grantLifetimeMs,
|
|
74
|
+
provenance,
|
|
75
|
+
meta,
|
|
76
|
+
}, {
|
|
77
|
+
agentId: task.agentId,
|
|
78
|
+
sessionId: task.sessionId,
|
|
79
|
+
text: task.text,
|
|
80
|
+
requestId,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** Resolve only this sender's receipt, then let the target own the pending guard. */
|
|
84
|
+
async function recall(task, sourceSessionId, signal) {
|
|
85
|
+
await authorizedMembers(task.agentId, signal);
|
|
86
|
+
const requestId = await requestKey(task, sourceSessionId);
|
|
87
|
+
signal.throwIfAborted();
|
|
88
|
+
const opened = await input.directory.open(task.agentId, signal);
|
|
89
|
+
try {
|
|
90
|
+
const session = opened.agent.session(task.sessionId);
|
|
91
|
+
const admitted = fleet.receipt(requestId) ?? (await session.withdraw(requestId));
|
|
92
|
+
return admitted.status === "accepted"
|
|
93
|
+
? await session.cancel({
|
|
94
|
+
commandId: admitted.commandId,
|
|
95
|
+
onlyIfPending: true,
|
|
96
|
+
})
|
|
97
|
+
: admitted;
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
await opened.release();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Admit one mail addressed to this Agent. New work must name its sender as
|
|
105
|
+
* its only reply destination; a reply-less mail is admitted only as an
|
|
106
|
+
* authenticated reply. Everything else is refused.
|
|
107
|
+
*/
|
|
108
|
+
async function receive(mail) {
|
|
109
|
+
const mesh = await input.meshAdmission?.(mail.to.agentId);
|
|
110
|
+
if (mail.to.agentId !== input.agentId && mesh === undefined)
|
|
111
|
+
throw new Error("Local addressed mail recipient is not authorized");
|
|
112
|
+
const agents = mesh ?? (await members(new AbortController().signal));
|
|
113
|
+
if (!agents.some(({ agentId }) => agentId === mail.from.agentId))
|
|
114
|
+
throw new Error("Local mail sender is outside this project");
|
|
115
|
+
if (mail.replyTo.length > 0) {
|
|
116
|
+
const [destination] = mail.replyTo;
|
|
117
|
+
if (mail.replyTo.length !== 1 ||
|
|
118
|
+
destination?.agentId !== mail.from.agentId ||
|
|
119
|
+
destination.sessionId !== mail.from.sessionId)
|
|
120
|
+
throw new Error("Local delegated work must reply to its sender");
|
|
121
|
+
return input.accept(mail, mail.meta);
|
|
122
|
+
}
|
|
123
|
+
const delegation = fleet.authenticateReply(mail);
|
|
124
|
+
if (delegation === undefined)
|
|
125
|
+
throw new Error("Local reply does not match work this Agent sent");
|
|
126
|
+
const replier = agents.find(({ agentId }) => agentId === delegation.reply.agentId);
|
|
127
|
+
if (replier === undefined)
|
|
128
|
+
throw new Error("Local reply sender is outside this project");
|
|
129
|
+
const meta = delegationReplyMeta(delegation, replier.name);
|
|
130
|
+
return input.accept({ ...mail, meta }, meta, delegation.reply);
|
|
131
|
+
}
|
|
132
|
+
return { send, recall, receive, outstanding: fleet.outstanding };
|
|
133
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Secret-free model status published by the Local owner for one Session. */
|
|
3
|
+
export declare const privateLocalEffectiveModelSchema: z.ZodObject<{
|
|
4
|
+
agentName: z.ZodLiteral<"Shiitake">;
|
|
5
|
+
providerId: z.ZodString;
|
|
6
|
+
providerName: z.ZodString;
|
|
7
|
+
modelId: z.ZodString;
|
|
8
|
+
modelName: z.ZodString;
|
|
9
|
+
credential: z.ZodObject<{
|
|
10
|
+
kind: z.ZodEnum<{
|
|
11
|
+
oauth: "oauth";
|
|
12
|
+
api_key: "api_key";
|
|
13
|
+
missing: "missing";
|
|
14
|
+
}>;
|
|
15
|
+
source: z.ZodString;
|
|
16
|
+
}, z.core.$strict>;
|
|
17
|
+
reasoning: z.ZodOptional<z.ZodEnum<{
|
|
18
|
+
minimal: "minimal";
|
|
19
|
+
low: "low";
|
|
20
|
+
medium: "medium";
|
|
21
|
+
high: "high";
|
|
22
|
+
xhigh: "xhigh";
|
|
23
|
+
max: "max";
|
|
24
|
+
}>>;
|
|
25
|
+
label: z.ZodString;
|
|
26
|
+
}, z.core.$strict>;
|
|
27
|
+
export type PrivateLocalEffectiveModel = z.output<typeof privateLocalEffectiveModelSchema>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { thinkingLevelSchema } from "./private-fungi-model-catalog.js";
|
|
3
|
+
/** Secret-free model status published by the Local owner for one Session. */
|
|
4
|
+
export const privateLocalEffectiveModelSchema = z.strictObject({
|
|
5
|
+
agentName: z.literal("Shiitake"),
|
|
6
|
+
providerId: z.string().min(1).max(160),
|
|
7
|
+
providerName: z.string().min(1).max(256),
|
|
8
|
+
modelId: z.string().min(1).max(160),
|
|
9
|
+
modelName: z.string().min(1).max(256),
|
|
10
|
+
credential: z.strictObject({
|
|
11
|
+
kind: z.enum(["oauth", "api_key", "missing"]),
|
|
12
|
+
source: z.string().min(1).max(160),
|
|
13
|
+
}),
|
|
14
|
+
/** The resolved thinking level a run would use; absent when the model has none. */
|
|
15
|
+
reasoning: thinkingLevelSchema.optional(),
|
|
16
|
+
label: z.string().min(1).max(640),
|
|
17
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The only host variables a Local exec child receives besides its selected
|
|
3
|
+
* PATH and HOME. Exec runs as the host's own user, so that user's session
|
|
4
|
+
* locator variables are not a secret and are what `systemd-run --user`,
|
|
5
|
+
* `loginctl` and other user-service tools need to find the user manager.
|
|
6
|
+
* Every other host variable, credentials included, stays out.
|
|
7
|
+
*/
|
|
8
|
+
export type PrivateLocalExecEnvironment = {
|
|
9
|
+
PATH: string;
|
|
10
|
+
HOME: string;
|
|
11
|
+
XDG_RUNTIME_DIR?: string;
|
|
12
|
+
DBUS_SESSION_BUS_ADDRESS?: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function privateLocalExecEnvironment(executionPath: string, host: NodeJS.ProcessEnv & {
|
|
15
|
+
HOME: string;
|
|
16
|
+
}): PrivateLocalExecEnvironment;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function privateLocalExecEnvironment(executionPath, host) {
|
|
2
|
+
const environment = {
|
|
3
|
+
PATH: executionPath,
|
|
4
|
+
HOME: host.HOME,
|
|
5
|
+
};
|
|
6
|
+
if (host.XDG_RUNTIME_DIR)
|
|
7
|
+
environment.XDG_RUNTIME_DIR = host.XDG_RUNTIME_DIR;
|
|
8
|
+
if (host.DBUS_SESSION_BUS_ADDRESS)
|
|
9
|
+
environment.DBUS_SESSION_BUS_ADDRESS = host.DBUS_SESSION_BUS_ADDRESS;
|
|
10
|
+
return environment;
|
|
11
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* One exec launch's own text ceiling: 16384, shared by the shell form's
|
|
4
|
+
* `command` and the direct form's combined `executable`+`args`, so neither
|
|
5
|
+
* gets a materially larger budget. This is exec's own bound, not borrowed
|
|
6
|
+
* from any other owner's limit.
|
|
7
|
+
*
|
|
8
|
+
* The two forms count against it in different units, which the name
|
|
9
|
+
* deliberately leaves open rather than mis-claiming one: `command` is
|
|
10
|
+
* bounded by `z.string().max()`, zod's native UTF-16 code-unit count, the
|
|
11
|
+
* same as the field already was before the direct form existed. The direct
|
|
12
|
+
* form's `combinedArgvBytes` below counts real UTF-8 bytes, because argv is
|
|
13
|
+
* a byte protocol with no code-unit concept. For multi-byte text the two
|
|
14
|
+
* measures diverge; both stay well inside this ceiling for any launch a
|
|
15
|
+
* model would actually send.
|
|
16
|
+
*/
|
|
17
|
+
export declare const maximumLaunchTextLength: number;
|
|
18
|
+
/**
|
|
19
|
+
* One exec launch's own stdin bound. A launch's stdin is a finite payload
|
|
20
|
+
* held fully in memory and written to the child once, then closed; it is
|
|
21
|
+
* never streamed, paginated or interactive. 128 KiB keeps that one-shot
|
|
22
|
+
* payload bounded while still fitting an ordinary small script or data blob.
|
|
23
|
+
* This is exec's own bound, not borrowed from any other owner's limit (in
|
|
24
|
+
* particular, not Files' write bound, which governs a different resource:
|
|
25
|
+
* durable file content, read and written in pages).
|
|
26
|
+
*/
|
|
27
|
+
export declare const maximumLaunchStdinBytes: number;
|
|
28
|
+
/**
|
|
29
|
+
* Finite UTF-8 text delivered to a launched child once, then closed; never
|
|
30
|
+
* an interactive protocol. A lone surrogate is refused rather than silently
|
|
31
|
+
* becoming U+FFFD, so byte-identity is a real guarantee, not a best effort.
|
|
32
|
+
*/
|
|
33
|
+
export declare const launchStdinSchema: z.ZodString;
|
|
34
|
+
/** Combined UTF-8 bytes of a direct form's executable and its args. */
|
|
35
|
+
export declare function combinedArgvBytes(executable: string, args: readonly string[]): number;
|
|
36
|
+
/**
|
|
37
|
+
* One launch's requested `cwd`: absolute, bounded, no NUL. The caller still
|
|
38
|
+
* resolves and contains it under the invocation root; this schema only
|
|
39
|
+
* shapes the raw string.
|
|
40
|
+
*/
|
|
41
|
+
export declare const launchCwdSchema: z.ZodString;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* One exec launch's own text ceiling: 16384, shared by the shell form's
|
|
4
|
+
* `command` and the direct form's combined `executable`+`args`, so neither
|
|
5
|
+
* gets a materially larger budget. This is exec's own bound, not borrowed
|
|
6
|
+
* from any other owner's limit.
|
|
7
|
+
*
|
|
8
|
+
* The two forms count against it in different units, which the name
|
|
9
|
+
* deliberately leaves open rather than mis-claiming one: `command` is
|
|
10
|
+
* bounded by `z.string().max()`, zod's native UTF-16 code-unit count, the
|
|
11
|
+
* same as the field already was before the direct form existed. The direct
|
|
12
|
+
* form's `combinedArgvBytes` below counts real UTF-8 bytes, because argv is
|
|
13
|
+
* a byte protocol with no code-unit concept. For multi-byte text the two
|
|
14
|
+
* measures diverge; both stay well inside this ceiling for any launch a
|
|
15
|
+
* model would actually send.
|
|
16
|
+
*/
|
|
17
|
+
export const maximumLaunchTextLength = 16 * 1_024;
|
|
18
|
+
/**
|
|
19
|
+
* One exec launch's own stdin bound. A launch's stdin is a finite payload
|
|
20
|
+
* held fully in memory and written to the child once, then closed; it is
|
|
21
|
+
* never streamed, paginated or interactive. 128 KiB keeps that one-shot
|
|
22
|
+
* payload bounded while still fitting an ordinary small script or data blob.
|
|
23
|
+
* This is exec's own bound, not borrowed from any other owner's limit (in
|
|
24
|
+
* particular, not Files' write bound, which governs a different resource:
|
|
25
|
+
* durable file content, read and written in pages).
|
|
26
|
+
*/
|
|
27
|
+
export const maximumLaunchStdinBytes = 128 * 1_024;
|
|
28
|
+
/**
|
|
29
|
+
* Finite UTF-8 text delivered to a launched child once, then closed; never
|
|
30
|
+
* an interactive protocol. A lone surrogate is refused rather than silently
|
|
31
|
+
* becoming U+FFFD, so byte-identity is a real guarantee, not a best effort.
|
|
32
|
+
*/
|
|
33
|
+
export const launchStdinSchema = z
|
|
34
|
+
.string()
|
|
35
|
+
.refine((value) => value.isWellFormed(), "Computer exec stdin must be well-formed UTF-16 text (no lone surrogates)")
|
|
36
|
+
.refine((value) => Buffer.byteLength(value, "utf8") <= maximumLaunchStdinBytes, `Computer exec stdin exceeds the ${String(maximumLaunchStdinBytes)}-byte bound`);
|
|
37
|
+
/** Combined UTF-8 bytes of a direct form's executable and its args. */
|
|
38
|
+
export function combinedArgvBytes(executable, args) {
|
|
39
|
+
return [executable, ...args].reduce((total, value) => total + Buffer.byteLength(value, "utf8"), 0);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* One launch's requested `cwd`: absolute, bounded, no NUL. The caller still
|
|
43
|
+
* resolves and contains it under the invocation root; this schema only
|
|
44
|
+
* shapes the raw string.
|
|
45
|
+
*/
|
|
46
|
+
export const launchCwdSchema = z
|
|
47
|
+
.string()
|
|
48
|
+
.min(1)
|
|
49
|
+
.max(4_096)
|
|
50
|
+
.startsWith("/")
|
|
51
|
+
.refine((path) => !path.includes("\0"), "Path cannot contain null bytes");
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Only the invoking client's bounded PATH crosses the local service boundary. */
|
|
3
|
+
export declare const privateLocalExecPathSchema: z.ZodString;
|
|
4
|
+
export declare function parsePrivateLocalExecPath(value: string): string;
|
|
5
|
+
/** Resolve shell-relative client entries before the absolute-only RPC boundary. */
|
|
6
|
+
export declare function capturePrivateLocalExecPath(value: string, clientDirectory: string): string;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from "node:path";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
const maximumPathCharacters = 8_192;
|
|
4
|
+
const maximumPathDirectories = 64;
|
|
5
|
+
/** Only the invoking client's bounded PATH crosses the local service boundary. */
|
|
6
|
+
export const privateLocalExecPathSchema = z
|
|
7
|
+
.string()
|
|
8
|
+
.min(1)
|
|
9
|
+
.max(maximumPathCharacters)
|
|
10
|
+
.superRefine((path, context) => {
|
|
11
|
+
const directories = path.split(":");
|
|
12
|
+
if (directories.length > maximumPathDirectories)
|
|
13
|
+
context.addIssue({
|
|
14
|
+
code: "custom",
|
|
15
|
+
message: "PATH has too many directories",
|
|
16
|
+
});
|
|
17
|
+
for (const directory of directories) {
|
|
18
|
+
if (directory === "" ||
|
|
19
|
+
!isAbsolute(directory) ||
|
|
20
|
+
/[\p{Cc}\p{Cf}]/u.test(directory)) {
|
|
21
|
+
context.addIssue({
|
|
22
|
+
code: "custom",
|
|
23
|
+
message: "PATH must contain only nonempty absolute directories",
|
|
24
|
+
});
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
export function parsePrivateLocalExecPath(value) {
|
|
30
|
+
return privateLocalExecPathSchema.parse(value);
|
|
31
|
+
}
|
|
32
|
+
/** Resolve shell-relative client entries before the absolute-only RPC boundary. */
|
|
33
|
+
export function capturePrivateLocalExecPath(value, clientDirectory) {
|
|
34
|
+
const absolute = value
|
|
35
|
+
.split(":")
|
|
36
|
+
.map((directory) => directory === "" || isAbsolute(directory)
|
|
37
|
+
? directory
|
|
38
|
+
: resolve(clientDirectory, directory));
|
|
39
|
+
return parsePrivateLocalExecPath(absolute.join(":"));
|
|
40
|
+
}
|