@fungi.computer/truffle 0.5.0 → 0.6.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/README.md +27 -4
- package/dist/internal/private-workerd-sandbox-worker.mjs +243 -273
- package/dist/internal/sandbox-metafile.json +35 -30
- package/dist/local-extensions.d.ts +2 -2
- package/dist/local-extensions.js +43 -19
- package/dist/private-fungi-mcp-connection.js +6 -1
- package/dist/private-invocation-computer.d.ts +6 -2
- package/dist/private-invocation-computer.js +20 -13
- package/dist/private-local-computer-processes.d.ts +7 -2
- package/dist/private-local-computer-processes.js +47 -23
- package/dist/private-local-delegation.js +15 -11
- package/dist/private-local-exec-environment.d.ts +5 -2
- package/dist/private-local-exec-environment.js +35 -1
- package/dist/private-local-exec-review.js +14 -7
- package/dist/private-local-exec-rules.d.ts +7 -0
- package/dist/private-local-exec-rules.js +23 -6
- package/dist/private-local-extension-records.d.ts +1 -1
- package/dist/private-local-runtime-host.js +2 -2
- package/dist/private-local-sandbox-policy.js +4 -0
- package/dist/private-local-sandbox.d.ts +3 -1
- package/dist/private-local-sandbox.js +2 -2
- package/dist/private-local-schedule-client.d.ts +12 -0
- package/dist/private-local-schedule-client.js +41 -0
- package/dist/private-local-schedules.d.ts +84 -0
- package/dist/private-local-schedules.js +65 -0
- package/dist/private-local-service-contract.d.ts +3 -0
- package/dist/private-local-service-discovery.d.ts +3 -3
- package/dist/private-local-service-discovery.js +1 -1
- package/dist/private-local-service-listener.js +9 -0
- package/dist/private-local-service-owner.d.ts +130 -0
- package/dist/private-local-service-owner.js +29 -0
- package/dist/private-local-service.d.ts +3 -0
- package/dist/private-local-service.js +2 -0
- package/package.json +17 -13
|
@@ -1934,8 +1934,8 @@
|
|
|
1934
1934
|
],
|
|
1935
1935
|
"format": "esm"
|
|
1936
1936
|
},
|
|
1937
|
-
"../../packages/forage/dist/
|
|
1938
|
-
"bytes":
|
|
1937
|
+
"../../packages/forage/dist/normalization.js": {
|
|
1938
|
+
"bytes": 3520,
|
|
1939
1939
|
"imports": [
|
|
1940
1940
|
{
|
|
1941
1941
|
"path": "../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/index.js",
|
|
@@ -1945,24 +1945,29 @@
|
|
|
1945
1945
|
],
|
|
1946
1946
|
"format": "esm"
|
|
1947
1947
|
},
|
|
1948
|
-
"../../packages/forage/dist/
|
|
1949
|
-
"bytes":
|
|
1950
|
-
"imports": [],
|
|
1951
|
-
"format": "esm"
|
|
1952
|
-
},
|
|
1953
|
-
"../../packages/forage/dist/normalization.js": {
|
|
1954
|
-
"bytes": 3477,
|
|
1948
|
+
"../../packages/forage/dist/schemas.js": {
|
|
1949
|
+
"bytes": 5698,
|
|
1955
1950
|
"imports": [
|
|
1956
1951
|
{
|
|
1957
1952
|
"path": "../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/index.js",
|
|
1958
1953
|
"kind": "import-statement",
|
|
1959
1954
|
"original": "zod"
|
|
1955
|
+
},
|
|
1956
|
+
{
|
|
1957
|
+
"path": "../../packages/forage/dist/normalization.js",
|
|
1958
|
+
"kind": "import-statement",
|
|
1959
|
+
"original": "./normalization.js"
|
|
1960
1960
|
}
|
|
1961
1961
|
],
|
|
1962
1962
|
"format": "esm"
|
|
1963
1963
|
},
|
|
1964
|
+
"../../packages/forage/dist/errors.js": {
|
|
1965
|
+
"bytes": 396,
|
|
1966
|
+
"imports": [],
|
|
1967
|
+
"format": "esm"
|
|
1968
|
+
},
|
|
1964
1969
|
"../../packages/forage/dist/rendering.js": {
|
|
1965
|
-
"bytes":
|
|
1970
|
+
"bytes": 11613,
|
|
1966
1971
|
"imports": [
|
|
1967
1972
|
{
|
|
1968
1973
|
"path": "../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/index.js",
|
|
@@ -1978,17 +1983,12 @@
|
|
|
1978
1983
|
"path": "../../packages/forage/dist/normalization.js",
|
|
1979
1984
|
"kind": "import-statement",
|
|
1980
1985
|
"original": "./normalization.js"
|
|
1981
|
-
},
|
|
1982
|
-
{
|
|
1983
|
-
"path": "../../packages/forage/dist/schemas.js",
|
|
1984
|
-
"kind": "import-statement",
|
|
1985
|
-
"original": "./schemas.js"
|
|
1986
1986
|
}
|
|
1987
1987
|
],
|
|
1988
1988
|
"format": "esm"
|
|
1989
1989
|
},
|
|
1990
1990
|
"../../packages/forage/dist/describe.js": {
|
|
1991
|
-
"bytes":
|
|
1991
|
+
"bytes": 3157,
|
|
1992
1992
|
"imports": [
|
|
1993
1993
|
{
|
|
1994
1994
|
"path": "../../packages/forage/dist/schemas.js",
|
|
@@ -2014,7 +2014,7 @@
|
|
|
2014
2014
|
"format": "esm"
|
|
2015
2015
|
},
|
|
2016
2016
|
"../../packages/forage/dist/search.js": {
|
|
2017
|
-
"bytes":
|
|
2017
|
+
"bytes": 9656,
|
|
2018
2018
|
"imports": [
|
|
2019
2019
|
{
|
|
2020
2020
|
"path": "../../packages/forage/dist/schemas.js",
|
|
@@ -2066,8 +2066,13 @@
|
|
|
2066
2066
|
"format": "esm"
|
|
2067
2067
|
},
|
|
2068
2068
|
"../../packages/forage/dist/catalog.js": {
|
|
2069
|
-
"bytes":
|
|
2069
|
+
"bytes": 6091,
|
|
2070
2070
|
"imports": [
|
|
2071
|
+
{
|
|
2072
|
+
"path": "../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/index.js",
|
|
2073
|
+
"kind": "import-statement",
|
|
2074
|
+
"original": "zod"
|
|
2075
|
+
},
|
|
2071
2076
|
{
|
|
2072
2077
|
"path": "../../packages/forage/dist/errors.js",
|
|
2073
2078
|
"kind": "import-statement",
|
|
@@ -2107,7 +2112,7 @@
|
|
|
2107
2112
|
"format": "esm"
|
|
2108
2113
|
},
|
|
2109
2114
|
"../../packages/forage/dist/app-api.js": {
|
|
2110
|
-
"bytes":
|
|
2115
|
+
"bytes": 6114,
|
|
2111
2116
|
"imports": [
|
|
2112
2117
|
{
|
|
2113
2118
|
"path": "../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/index.js",
|
|
@@ -2129,7 +2134,7 @@
|
|
|
2129
2134
|
"format": "esm"
|
|
2130
2135
|
},
|
|
2131
2136
|
"../../packages/forage/dist/index.js": {
|
|
2132
|
-
"bytes":
|
|
2137
|
+
"bytes": 996,
|
|
2133
2138
|
"imports": [
|
|
2134
2139
|
{
|
|
2135
2140
|
"path": "../../packages/forage/dist/schemas.js",
|
|
@@ -6533,8 +6538,11 @@
|
|
|
6533
6538
|
"../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/index.js": {
|
|
6534
6539
|
"bytesInOutput": 0
|
|
6535
6540
|
},
|
|
6541
|
+
"../../packages/forage/dist/normalization.js": {
|
|
6542
|
+
"bytesInOutput": 2663
|
|
6543
|
+
},
|
|
6536
6544
|
"../../packages/forage/dist/schemas.js": {
|
|
6537
|
-
"bytesInOutput":
|
|
6545
|
+
"bytesInOutput": 5531
|
|
6538
6546
|
},
|
|
6539
6547
|
"../../packages/forage/dist/index.js": {
|
|
6540
6548
|
"bytesInOutput": 127
|
|
@@ -6542,26 +6550,23 @@
|
|
|
6542
6550
|
"../../packages/forage/dist/errors.js": {
|
|
6543
6551
|
"bytesInOutput": 299
|
|
6544
6552
|
},
|
|
6545
|
-
"../../packages/forage/dist/normalization.js": {
|
|
6546
|
-
"bytesInOutput": 2924
|
|
6547
|
-
},
|
|
6548
6553
|
"../../packages/forage/dist/rendering.js": {
|
|
6549
|
-
"bytesInOutput":
|
|
6554
|
+
"bytesInOutput": 9821
|
|
6550
6555
|
},
|
|
6551
6556
|
"../../packages/forage/dist/describe.js": {
|
|
6552
|
-
"bytesInOutput":
|
|
6557
|
+
"bytesInOutput": 2570
|
|
6553
6558
|
},
|
|
6554
6559
|
"../../packages/forage/dist/search.js": {
|
|
6555
|
-
"bytesInOutput":
|
|
6560
|
+
"bytesInOutput": 8262
|
|
6556
6561
|
},
|
|
6557
6562
|
"../../packages/forage/dist/list.js": {
|
|
6558
6563
|
"bytesInOutput": 1793
|
|
6559
6564
|
},
|
|
6560
6565
|
"../../packages/forage/dist/catalog.js": {
|
|
6561
|
-
"bytesInOutput":
|
|
6566
|
+
"bytesInOutput": 4697
|
|
6562
6567
|
},
|
|
6563
6568
|
"../../packages/forage/dist/app-api.js": {
|
|
6564
|
-
"bytesInOutput":
|
|
6569
|
+
"bytesInOutput": 1723
|
|
6565
6570
|
},
|
|
6566
6571
|
"../../packages/forage/dist/definitions.js": {
|
|
6567
6572
|
"bytesInOutput": 1100
|
|
@@ -6720,7 +6725,7 @@
|
|
|
6720
6725
|
"bytesInOutput": 4648
|
|
6721
6726
|
}
|
|
6722
6727
|
},
|
|
6723
|
-
"bytes":
|
|
6728
|
+
"bytes": 993272
|
|
6724
6729
|
}
|
|
6725
6730
|
}
|
|
6726
6731
|
}
|
|
@@ -78,7 +78,7 @@ export declare function localExtensions(options: LocalExtensionsOptions): {
|
|
|
78
78
|
* is pinned to the commit Bun's lockfile resolved; updating is another
|
|
79
79
|
* explicit `add`. Missing declared artifacts are a refusal.
|
|
80
80
|
*/
|
|
81
|
-
add(source: string): Promise<LocalInstalledExtension>;
|
|
81
|
+
add(source: string, cwd: string): Promise<LocalInstalledExtension>;
|
|
82
82
|
/** Remove the package and every activation that references it. */
|
|
83
83
|
remove(id: string): Promise<boolean>;
|
|
84
84
|
list(): Promise<readonly LocalExtensionSummary[]>;
|
|
@@ -101,7 +101,7 @@ export declare function localExtensions(options: LocalExtensionsOptions): {
|
|
|
101
101
|
agentStoreDirectory: string;
|
|
102
102
|
}>): Promise<{
|
|
103
103
|
revision: string;
|
|
104
|
-
capabilities: ("state" | "attention" | "settled" | "send" | "timers" | "commands" | "projection" | "run_tools" | "files" | "
|
|
104
|
+
capabilities: ("state" | "prompt" | "attention" | "settled" | "send" | "timers" | "commands" | "projection" | "run_tools" | "files" | "compaction_handoff")[];
|
|
105
105
|
skills: boolean;
|
|
106
106
|
}>;
|
|
107
107
|
/**
|
package/dist/local-extensions.js
CHANGED
|
@@ -6,6 +6,7 @@ import { Mycelium } from "@fungi.computer/mycelium";
|
|
|
6
6
|
import { defineExtension, ExtensionId, } from "@fungi.computer/shiitake/extensions";
|
|
7
7
|
import * as Predicate from "effect/Predicate";
|
|
8
8
|
import * as Schema from "effect/Schema";
|
|
9
|
+
import npa from "npm-package-arg";
|
|
9
10
|
import writeFileAtomic from "write-file-atomic";
|
|
10
11
|
import { z } from "zod";
|
|
11
12
|
import { describeExtensionSkills, } from "./local-skill-source.js";
|
|
@@ -337,29 +338,52 @@ export function localExtensions(options) {
|
|
|
337
338
|
* is pinned to the commit Bun's lockfile resolved; updating is another
|
|
338
339
|
* explicit `add`. Missing declared artifacts are a refusal.
|
|
339
340
|
*/
|
|
340
|
-
async add(source) {
|
|
341
|
+
async add(source, cwd) {
|
|
342
|
+
if (source.length === 0)
|
|
343
|
+
throw new LocalExtensionError("install", "An extension source is required");
|
|
344
|
+
// Bun installs in private staging, not the caller's project. Let the
|
|
345
|
+
// maintained package-spec parser resolve only local sources there first;
|
|
346
|
+
// leave Git and registry specifications to Bun unchanged.
|
|
347
|
+
const parsed = npa(source, cwd);
|
|
348
|
+
const installSource = parsed.type === "directory" || parsed.type === "file"
|
|
349
|
+
? z.string().parse(parsed.fetchSpec)
|
|
350
|
+
: source;
|
|
341
351
|
await mkdir(join(root, "staging"), { recursive: true, mode: 0o700 });
|
|
342
352
|
const staging = await mkdtemp(join(root, "staging", "install-"));
|
|
343
353
|
try {
|
|
354
|
+
const runBun = async (args, directory) => {
|
|
355
|
+
const child = Bun.spawn([process.execPath, ...args], {
|
|
356
|
+
cwd: directory,
|
|
357
|
+
// A private cache resolves the named ref afresh instead of a
|
|
358
|
+
// cached checkout of an earlier commit.
|
|
359
|
+
env: {
|
|
360
|
+
...process.env,
|
|
361
|
+
BUN_BE_BUN: "1",
|
|
362
|
+
BUN_INSTALL_CACHE_DIR: join(staging, ".install-cache"),
|
|
363
|
+
},
|
|
364
|
+
stdout: "ignore",
|
|
365
|
+
stderr: "pipe",
|
|
366
|
+
});
|
|
367
|
+
const [status, stderr] = await Promise.all([
|
|
368
|
+
child.exited,
|
|
369
|
+
new Response(child.stderr).text(),
|
|
370
|
+
]);
|
|
371
|
+
if (status !== 0)
|
|
372
|
+
throw new LocalExtensionError("install", `Could not install ${source}: ${stderr.trim().slice(0, 2_000)}`);
|
|
373
|
+
};
|
|
374
|
+
// Bun links a directory dependency's files back to its source, even
|
|
375
|
+
// with --backend=copyfile. Its ordinary pack operation supplies an
|
|
376
|
+
// isolated package snapshot and the install lock supplies its integrity.
|
|
377
|
+
// Both operations keep lifecycle scripts disabled.
|
|
378
|
+
const archive = join(staging, "source.tgz");
|
|
379
|
+
if (parsed.type === "directory")
|
|
380
|
+
await runBun(["pm", "pack", "--ignore-scripts", "--filename", archive], installSource);
|
|
344
381
|
await writeFile(join(staging, "package.json"), `${JSON.stringify({ private: true })}\n`);
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
...process.env,
|
|
351
|
-
BUN_BE_BUN: "1",
|
|
352
|
-
BUN_INSTALL_CACHE_DIR: join(staging, ".install-cache"),
|
|
353
|
-
},
|
|
354
|
-
stdout: "pipe",
|
|
355
|
-
stderr: "pipe",
|
|
356
|
-
});
|
|
357
|
-
const [status, stderr] = await Promise.all([
|
|
358
|
-
child.exited,
|
|
359
|
-
new Response(child.stderr).text(),
|
|
360
|
-
]);
|
|
361
|
-
if (status !== 0)
|
|
362
|
-
throw new LocalExtensionError("install", `Could not install ${source}: ${stderr.trim().slice(0, 2_000)}`);
|
|
382
|
+
await runBun([
|
|
383
|
+
"add",
|
|
384
|
+
"--ignore-scripts",
|
|
385
|
+
parsed.type === "directory" ? archive : installSource,
|
|
386
|
+
], staging);
|
|
363
387
|
const lock = lockSchema.parse(Bun.JSONC.parse(await readFile(join(staging, "bun.lock"), "utf8")));
|
|
364
388
|
const dependencies = Object.keys(z
|
|
365
389
|
.looseObject({ dependencies: z.record(z.string(), z.string()) })
|
|
@@ -2,6 +2,7 @@ import { FungiSignInRequiredError } from "./private-fungi-auth-error.js";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { Client, StreamableHTTPClientTransport, UnauthorizedError, } from "@modelcontextprotocol/client";
|
|
4
4
|
import { Mycelium } from "@fungi.computer/mycelium";
|
|
5
|
+
import { forageDiscoveryInstructions } from "@fungi.computer/mycelium/effect";
|
|
5
6
|
import { z } from "zod";
|
|
6
7
|
import { parseFungiMcpEndpoint } from "./private-fungi-mcp-endpoint.js";
|
|
7
8
|
import { registerMcpFormElicitation, McpFormApprovalError, } from "./private-mcp-form-elicitation.js";
|
|
@@ -83,7 +84,11 @@ export function createPrivateFungiMcpConnection(options) {
|
|
|
83
84
|
operations: {
|
|
84
85
|
execute: Mycelium.operation({
|
|
85
86
|
replay: "unsafe",
|
|
86
|
-
description:
|
|
87
|
+
description: [
|
|
88
|
+
"This operation sends its code to the connected Fungi MCP server's execute tool. The following discovery instructions apply inside that remote call, not the outer Local call.",
|
|
89
|
+
forageDiscoveryInstructions,
|
|
90
|
+
"Explicitly return the result. Returns the original MCP content and isError; it does not run on the local machine.",
|
|
91
|
+
].join(" "),
|
|
87
92
|
input: z.strictObject({ code: z.string() }),
|
|
88
93
|
execute: async (input, context) => {
|
|
89
94
|
try {
|
|
@@ -21,6 +21,7 @@ declare const execInput: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
21
21
|
justification: z.ZodString;
|
|
22
22
|
}, z.core.$strict>>;
|
|
23
23
|
cwd: z.ZodOptional<z.ZodString>;
|
|
24
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
24
25
|
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
25
26
|
}, z.core.$strict>, z.ZodObject<{
|
|
26
27
|
executable: z.ZodString;
|
|
@@ -31,6 +32,7 @@ declare const execInput: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
31
32
|
justification: z.ZodString;
|
|
32
33
|
}, z.core.$strict>>;
|
|
33
34
|
cwd: z.ZodOptional<z.ZodString>;
|
|
35
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
34
36
|
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
35
37
|
}, z.core.$strict>]>;
|
|
36
38
|
declare const completedOutput: z.ZodObject<{
|
|
@@ -247,7 +249,9 @@ export type LocalExecPolicyRun = (input: Parameters<LocalExecEscapeStage>[0]) =>
|
|
|
247
249
|
/** Run the recorded command through the normal bounded process door, without a sandbox.
|
|
248
250
|
* The host supplies the captured PATH and canonical cwd; the record supplies the
|
|
249
251
|
* exact original request. No model invocation can call this door directly. */
|
|
250
|
-
export declare function dispatchApprovedLocalExec(input: unknown, cwd: string, executionPath: string, home: string, processes: PrivateLocalProcessScope, mode: LocalPermissionMode
|
|
252
|
+
export declare function dispatchApprovedLocalExec(input: unknown, cwd: string, executionPath: string, home: string, processes: PrivateLocalProcessScope, mode: LocalPermissionMode,
|
|
253
|
+
/** Only inline rule matches have a live execute sink; ledger dispatch does not. */
|
|
254
|
+
sink?: InvocationContext["output"]): Promise<ExecOutput | z.output<typeof backgroundExecOutput>>;
|
|
251
255
|
export type ExecOutput = z.output<typeof execOutput>;
|
|
252
256
|
export type ProcessOutput = z.output<typeof completedOutput> | z.output<typeof deadlineOutput>;
|
|
253
257
|
/**
|
|
@@ -262,7 +266,7 @@ export declare function runPrivateInvocationProcess(processCommand: ChildProcess
|
|
|
262
266
|
requestedMs?: number;
|
|
263
267
|
/** The real command text, for the timeout message; omitted when not meaningful (e.g. a native helper). */
|
|
264
268
|
describeCommand?: () => string;
|
|
265
|
-
}
|
|
269
|
+
}>, output?: InvocationContext["output"]): Promise<ProcessOutput>;
|
|
266
270
|
/**
|
|
267
271
|
* Construct one private invocation-root Computer: bounded shell exec, plus the
|
|
268
272
|
* background-process operations of `processes` when the host supplies a scope.
|
|
@@ -17,7 +17,7 @@ import { z } from "zod";
|
|
|
17
17
|
import { backgroundExecInput, backgroundExecOutput, callBackgroundExec, } from "./private-local-computer-processes.js";
|
|
18
18
|
import { capturePrivateLocalExecPath, parsePrivateLocalExecPath, } from "./private-local-exec-path.js";
|
|
19
19
|
import { combinedArgvBytes, launchCwdSchema, launchStdinSchema, maximumLaunchTextLength, } from "./private-local-exec-launch.js";
|
|
20
|
-
import { privateLocalExecEnvironment } from "./private-local-exec-environment.js";
|
|
20
|
+
import { privateLocalExecEnvironment, privateLocalExecEnvironmentInput, } from "./private-local-exec-environment.js";
|
|
21
21
|
import { capturePrivateLocalConfigRoot } from "./private-local-config-root.js";
|
|
22
22
|
const maximumStreamBytes = 64 * 1_024;
|
|
23
23
|
/** Room inside the stream cap for the visible truncation marker. */
|
|
@@ -50,6 +50,7 @@ const runCommonFields = {
|
|
|
50
50
|
.strictObject({ justification: z.string().min(1).max(2048) })
|
|
51
51
|
.optional(),
|
|
52
52
|
cwd: launchCwdSchema.optional(),
|
|
53
|
+
env: privateLocalExecEnvironmentInput.optional(),
|
|
53
54
|
// Accepted up to the full invocation deadline and silently clamped to
|
|
54
55
|
// maximumCommandTimeoutMs (see runCommandAtInvocationBoundary); rejecting
|
|
55
56
|
// outright here would just trade one confusing failure for another.
|
|
@@ -139,10 +140,12 @@ export const shellOutput = z.union([
|
|
|
139
140
|
/** Run the recorded command through the normal bounded process door, without a sandbox.
|
|
140
141
|
* The host supplies the captured PATH and canonical cwd; the record supplies the
|
|
141
142
|
* exact original request. No model invocation can call this door directly. */
|
|
142
|
-
export async function dispatchApprovedLocalExec(input, cwd, executionPath, home, processes, mode
|
|
143
|
+
export async function dispatchApprovedLocalExec(input, cwd, executionPath, home, processes, mode,
|
|
144
|
+
/** Only inline rule matches have a live execute sink; ledger dispatch does not. */
|
|
145
|
+
sink) {
|
|
143
146
|
const request = shellInput.parse(input);
|
|
144
147
|
if (request.operation === "run") {
|
|
145
|
-
const result = await runCommandAtInvocationBoundary(request, cwd, executionPath, home, new AbortController().signal);
|
|
148
|
+
const result = await runCommandAtInvocationBoundary(request, cwd, executionPath, home, new AbortController().signal, undefined, sink);
|
|
146
149
|
return execOutput.parse({
|
|
147
150
|
...result,
|
|
148
151
|
sandbox: { mode, likelyDenied: false, ranOutsideSandbox: true },
|
|
@@ -316,9 +319,13 @@ function nativeGroupFinalizer(process) {
|
|
|
316
319
|
})));
|
|
317
320
|
})));
|
|
318
321
|
}
|
|
319
|
-
function captureBoundedOutput(stream) {
|
|
322
|
+
function captureBoundedOutput(stream, name, output) {
|
|
320
323
|
const initial = { chunks: [], byteLength: 0, totalBytes: 0 };
|
|
321
|
-
return Stream.runFold(
|
|
324
|
+
return Stream.runFold(output === undefined
|
|
325
|
+
? stream
|
|
326
|
+
: Stream.tap(stream, (bytes) => Effect.sync(() => {
|
|
327
|
+
output.write(name, bytes);
|
|
328
|
+
})), () => initial, (captured, chunk) => {
|
|
322
329
|
const remaining = keptStreamBytes - captured.byteLength;
|
|
323
330
|
const totalBytes = captured.totalBytes + chunk.byteLength;
|
|
324
331
|
if (remaining <= 0)
|
|
@@ -406,7 +413,7 @@ function buildRunCommand(request, cwd, executionPath, home, sandbox) {
|
|
|
406
413
|
HOME: home,
|
|
407
414
|
XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR,
|
|
408
415
|
DBUS_SESSION_BUS_ADDRESS: process.env.DBUS_SESSION_BUS_ADDRESS,
|
|
409
|
-
});
|
|
416
|
+
}, request.env);
|
|
410
417
|
// Effect's own default stdin is an open "pipe" that nothing closes, so an
|
|
411
418
|
// executable that reads stdin (`cat`, `python3 -`) would otherwise hang
|
|
412
419
|
// for the full timeout. "ignore" gives it immediate EOF, matching the
|
|
@@ -446,14 +453,14 @@ function isMissingExecutableFailure(cause) {
|
|
|
446
453
|
(cause.reason._tag === "NotFound" ||
|
|
447
454
|
cause.reason._tag === "PermissionDenied"));
|
|
448
455
|
}
|
|
449
|
-
async function runCommandAtInvocationBoundary(request, cwd, executionPath, home, signal, sandbox) {
|
|
456
|
+
async function runCommandAtInvocationBoundary(request, cwd, executionPath, home, signal, sandbox, output) {
|
|
450
457
|
const processCommand = buildRunCommand(request, cwd, executionPath, home, sandbox);
|
|
451
458
|
const timeoutMs = Math.min(request.timeoutMs, maximumCommandTimeoutMs);
|
|
452
459
|
try {
|
|
453
460
|
return await runPrivateInvocationProcess(processCommand, signal, timeoutMs, {
|
|
454
461
|
requestedMs: request.timeoutMs,
|
|
455
462
|
describeCommand: () => describeRunRequest(request),
|
|
456
|
-
});
|
|
463
|
+
}, output);
|
|
457
464
|
}
|
|
458
465
|
catch (cause) {
|
|
459
466
|
// Spawning the direct form's named executable can fail at the OS level
|
|
@@ -475,7 +482,7 @@ async function runCommandAtInvocationBoundary(request, cwd, executionPath, home,
|
|
|
475
482
|
}
|
|
476
483
|
}
|
|
477
484
|
/** Shared bounded native process owner; callers supply argv/stdin, never shell interpolation. */
|
|
478
|
-
export function runPrivateInvocationProcess(processCommand, signal, timeoutMs, describe) {
|
|
485
|
+
export function runPrivateInvocationProcess(processCommand, signal, timeoutMs, describe, output) {
|
|
479
486
|
return Effect.runPromise(Effect.scoped(Effect.gen(function* () {
|
|
480
487
|
const startedAt = yield* Clock.currentTimeMillis;
|
|
481
488
|
const executor = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
@@ -483,8 +490,8 @@ export function runPrivateInvocationProcess(processCommand, signal, timeoutMs, d
|
|
|
483
490
|
const process = yield* Effect.acquireRelease(executor.spawn(processCommand), nativeGroupFinalizer).pipe(Effect.provideService(Scope.Scope, processScope));
|
|
484
491
|
// Keep both readers alive when the exit wait times out. Killing and
|
|
485
492
|
// reaping the owned group closes the pipes, preserving bounded output.
|
|
486
|
-
const stdoutFiber = yield* Effect.forkChild(captureBoundedOutput(process.stdout));
|
|
487
|
-
const stderrFiber = yield* Effect.forkChild(captureBoundedOutput(process.stderr));
|
|
493
|
+
const stdoutFiber = yield* Effect.forkChild(captureBoundedOutput(process.stdout, "stdout", output));
|
|
494
|
+
const stderrFiber = yield* Effect.forkChild(captureBoundedOutput(process.stderr, "stderr", output));
|
|
488
495
|
const exited = yield* timeoutMs === undefined
|
|
489
496
|
? Effect.map(process.exitCode, Option.some)
|
|
490
497
|
: Effect.timeoutOption(process.exitCode, timeoutMs);
|
|
@@ -519,7 +526,7 @@ async function execute(root, executionPath, home, request, context, sandbox, mod
|
|
|
519
526
|
context.signal.throwIfAborted();
|
|
520
527
|
const cwd = await resolveCwd(root, request.cwd);
|
|
521
528
|
context.signal.throwIfAborted();
|
|
522
|
-
const result = await runCommandAtInvocationBoundary(request, cwd, executionPath, home, context.signal, sandbox);
|
|
529
|
+
const result = await runCommandAtInvocationBoundary(request, cwd, executionPath, home, context.signal, sandbox, context.output);
|
|
523
530
|
return execOutput.parse({
|
|
524
531
|
...result,
|
|
525
532
|
sandbox: {
|
|
@@ -614,7 +621,7 @@ currentMode = () => "full-access", stageEscape, runPolicy) {
|
|
|
614
621
|
return result;
|
|
615
622
|
}
|
|
616
623
|
if (processes !== undefined) {
|
|
617
|
-
const output = await callBackgroundExec(processes, request, (requested) => resolveCwd(root, requested), path, context.signal, sandbox);
|
|
624
|
+
const output = await callBackgroundExec(processes, request, (requested) => resolveCwd(root, requested), path, context.signal, sandbox, context.output);
|
|
618
625
|
return jsonValueSchema.parse("text" in output
|
|
619
626
|
? {
|
|
620
627
|
...output,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { LocalSandboxLaunch } from "./private-invocation-computer.js";
|
|
3
|
+
import type { InvocationContext } from "@fungi.computer/mycelium";
|
|
3
4
|
declare const processSchema: z.ZodObject<{
|
|
4
5
|
id: z.ZodString & z.ZodType<import("@fungi.computer/cairn").CairnId<"local-computer-process", "process", 1>, string, z.core.$ZodTypeInternals<import("@fungi.computer/cairn").CairnId<"local-computer-process", "process", 1>, string>>;
|
|
5
6
|
command: z.ZodString;
|
|
@@ -92,11 +93,13 @@ export type PrivateLocalProcessCommandSpec = Readonly<{
|
|
|
92
93
|
readonly kind: "shell";
|
|
93
94
|
readonly command: string;
|
|
94
95
|
readonly stdin?: string;
|
|
96
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
95
97
|
} | {
|
|
96
98
|
readonly kind: "direct";
|
|
97
99
|
readonly executable: string;
|
|
98
100
|
readonly args: readonly string[];
|
|
99
101
|
readonly stdin?: string;
|
|
102
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
100
103
|
}>;
|
|
101
104
|
/** One Session's (or invocation's) view of the host's background processes. */
|
|
102
105
|
export type PrivateLocalProcessScope = Readonly<{
|
|
@@ -104,7 +107,7 @@ export type PrivateLocalProcessScope = Readonly<{
|
|
|
104
107
|
status: (id: string) => PrivateLocalProcess;
|
|
105
108
|
list: () => readonly PrivateLocalProcess[];
|
|
106
109
|
read: (id: string, offset: number, maxBytes: number) => Promise<z.output<typeof readOutput>>;
|
|
107
|
-
wait: (id: string, timeoutMs: number, signal: AbortSignal) => Promise<z.output<typeof waitOutput>>;
|
|
110
|
+
wait: (id: string, timeoutMs: number, signal: AbortSignal, output?: InvocationContext["output"]) => Promise<z.output<typeof waitOutput>>;
|
|
108
111
|
kill: (id: string) => Promise<z.output<typeof killOutput>>;
|
|
109
112
|
}>;
|
|
110
113
|
/** The host's one background-process owner. */
|
|
@@ -133,6 +136,7 @@ export declare const backgroundExecInput: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
133
136
|
}, z.core.$strict>>;
|
|
134
137
|
command: z.ZodString;
|
|
135
138
|
stdin: z.ZodOptional<z.ZodString>;
|
|
139
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
136
140
|
cwd: z.ZodOptional<z.ZodString>;
|
|
137
141
|
}, z.core.$strict>, z.ZodObject<{
|
|
138
142
|
operation: z.ZodLiteral<"start">;
|
|
@@ -142,6 +146,7 @@ export declare const backgroundExecInput: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
142
146
|
executable: z.ZodString;
|
|
143
147
|
args: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
144
148
|
stdin: z.ZodOptional<z.ZodString>;
|
|
149
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
145
150
|
cwd: z.ZodOptional<z.ZodString>;
|
|
146
151
|
}, z.core.$strict>, z.ZodObject<{
|
|
147
152
|
id: z.ZodString & z.ZodType<import("@fungi.computer/cairn").CairnId<"local-computer-process", "process", 1>, string, z.core.$ZodTypeInternals<import("@fungi.computer/cairn").CairnId<"local-computer-process", "process", 1>, string>>;
|
|
@@ -213,5 +218,5 @@ export declare const backgroundExecOutput: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
213
218
|
}, z.core.$strict>>;
|
|
214
219
|
}, z.core.$strict>]>;
|
|
215
220
|
/** Reuse Session ownership and logs; a wait deadline never cancels the command. */
|
|
216
|
-
export declare function callBackgroundExec(scope: PrivateLocalProcessScope, request: z.output<typeof backgroundExecInput>, resolveCwd: (requested: string | undefined) => Promise<string>, executionPath: string, signal: AbortSignal, sandbox?: LocalSandboxLaunch): Promise<z.output<typeof backgroundExecOutput>>;
|
|
221
|
+
export declare function callBackgroundExec(scope: PrivateLocalProcessScope, request: z.output<typeof backgroundExecInput>, resolveCwd: (requested: string | undefined) => Promise<string>, executionPath: string, signal: AbortSignal, sandbox?: LocalSandboxLaunch, sink?: InvocationContext["output"]): Promise<z.output<typeof backgroundExecOutput>>;
|
|
217
222
|
export {};
|
|
@@ -7,7 +7,7 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
7
7
|
import { defineIdFamily } from "@fungi.computer/cairn";
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { combinedArgvBytes, launchCwdSchema, launchStdinSchema, maximumLaunchTextLength, } from "./private-local-exec-launch.js";
|
|
10
|
-
import { privateLocalExecEnvironment } from "./private-local-exec-environment.js";
|
|
10
|
+
import { privateLocalExecEnvironment, privateLocalExecEnvironmentInput, } from "./private-local-exec-environment.js";
|
|
11
11
|
import { capturePrivateLocalConfigRoot } from "./private-local-config-root.js";
|
|
12
12
|
import { Writable } from "node:stream";
|
|
13
13
|
/**
|
|
@@ -70,6 +70,7 @@ const shellStartInput = z.strictObject({
|
|
|
70
70
|
.optional(),
|
|
71
71
|
command: z.string().min(1).max(maximumLaunchTextLength),
|
|
72
72
|
stdin: launchStdinSchema.optional(),
|
|
73
|
+
env: privateLocalExecEnvironmentInput.optional(),
|
|
73
74
|
cwd: launchCwdSchema.optional(),
|
|
74
75
|
});
|
|
75
76
|
/** The direct form of background `start`: argv run with no shell interpretation. */
|
|
@@ -82,6 +83,7 @@ const directStartInput = z
|
|
|
82
83
|
executable: z.string().min(1),
|
|
83
84
|
args: z.array(z.string()).default([]),
|
|
84
85
|
stdin: launchStdinSchema.optional(),
|
|
86
|
+
env: privateLocalExecEnvironmentInput.optional(),
|
|
85
87
|
cwd: launchCwdSchema.optional(),
|
|
86
88
|
})
|
|
87
89
|
.refine((value) => combinedArgvBytes(value.executable, value.args) <=
|
|
@@ -166,18 +168,18 @@ function groupAlive(groupId) {
|
|
|
166
168
|
}
|
|
167
169
|
}
|
|
168
170
|
function toCommandSpec(request) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
return
|
|
171
|
+
let spec = "command" in request
|
|
172
|
+
? { kind: "shell", command: request.command }
|
|
173
|
+
: {
|
|
174
|
+
kind: "direct",
|
|
175
|
+
executable: request.executable,
|
|
176
|
+
args: request.args,
|
|
177
|
+
};
|
|
178
|
+
if (request.stdin !== undefined)
|
|
179
|
+
spec = { ...spec, stdin: request.stdin };
|
|
180
|
+
if (request.env !== undefined)
|
|
181
|
+
spec = { ...spec, env: request.env };
|
|
182
|
+
return spec;
|
|
181
183
|
}
|
|
182
184
|
/**
|
|
183
185
|
* A one-line, human-readable label for a spec; never used for execution.
|
|
@@ -221,6 +223,7 @@ class ManagedProcess {
|
|
|
221
223
|
#signal = null;
|
|
222
224
|
#endedAt = null;
|
|
223
225
|
#leaderExited = false;
|
|
226
|
+
#output = new Map();
|
|
224
227
|
command;
|
|
225
228
|
constructor(spec, cwd, logDirectory, executionPath, home, launch) {
|
|
226
229
|
this.spec = spec;
|
|
@@ -234,7 +237,7 @@ class ManagedProcess {
|
|
|
234
237
|
HOME: home,
|
|
235
238
|
XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR,
|
|
236
239
|
DBUS_SESSION_BUS_ADDRESS: process.env.DBUS_SESSION_BUS_ADDRESS,
|
|
237
|
-
});
|
|
240
|
+
}, spec.env);
|
|
238
241
|
if (launch !== undefined) {
|
|
239
242
|
delete env.XDG_RUNTIME_DIR;
|
|
240
243
|
delete env.DBUS_SESSION_BUS_ADDRESS;
|
|
@@ -300,11 +303,12 @@ class ManagedProcess {
|
|
|
300
303
|
pid !== undefined && Number.isSafeInteger(pid) && pid > 1
|
|
301
304
|
? pid
|
|
302
305
|
: undefined;
|
|
303
|
-
|
|
304
|
-
this.#append(chunk);
|
|
305
|
-
};
|
|
306
|
-
this.#child.
|
|
307
|
-
|
|
306
|
+
this.#child.stdout?.on("data", (chunk) => {
|
|
307
|
+
this.#append(chunk, "stdout");
|
|
308
|
+
});
|
|
309
|
+
this.#child.stderr?.on("data", (chunk) => {
|
|
310
|
+
this.#append(chunk, "stderr");
|
|
311
|
+
});
|
|
308
312
|
this.#child.on("error", (error) => {
|
|
309
313
|
this.#append(Buffer.from(`\n[fungi: process failed: ${error.message}]\n`));
|
|
310
314
|
this.#finish(closed.resolve);
|
|
@@ -327,7 +331,7 @@ class ManagedProcess {
|
|
|
327
331
|
get running() {
|
|
328
332
|
return this.#endedAt === null;
|
|
329
333
|
}
|
|
330
|
-
#append(chunk) {
|
|
334
|
+
#append(chunk, stream = "stderr") {
|
|
331
335
|
const fd = this.#fd;
|
|
332
336
|
if (fd === undefined)
|
|
333
337
|
return;
|
|
@@ -342,6 +346,22 @@ class ManagedProcess {
|
|
|
342
346
|
writeSync(fd, kept, first, kept.byteLength - first, 0);
|
|
343
347
|
this.#logBytes += chunk.byteLength;
|
|
344
348
|
this.#logTruncated = this.#logBytes > maximumLogBytes;
|
|
349
|
+
for (const output of this.#output.keys())
|
|
350
|
+
output.write(stream, chunk);
|
|
351
|
+
}
|
|
352
|
+
/** Observe only new chunks during this wait; no extra pipe reader or replay. */
|
|
353
|
+
observeOutput(output) {
|
|
354
|
+
if (output !== undefined)
|
|
355
|
+
this.#output.set(output, (this.#output.get(output) ?? 0) + 1);
|
|
356
|
+
return () => {
|
|
357
|
+
if (output === undefined)
|
|
358
|
+
return;
|
|
359
|
+
const remaining = (this.#output.get(output) ?? 1) - 1;
|
|
360
|
+
if (remaining === 0)
|
|
361
|
+
this.#output.delete(output);
|
|
362
|
+
else
|
|
363
|
+
this.#output.set(output, remaining);
|
|
364
|
+
};
|
|
345
365
|
}
|
|
346
366
|
/** Read atomically with appends, including a wrap in circular storage. */
|
|
347
367
|
read(offset, maxBytes) {
|
|
@@ -504,8 +524,10 @@ export function createPrivateLocalComputerProcesses(root, home = capturePrivateL
|
|
|
504
524
|
status: (id) => find(id).snapshot(),
|
|
505
525
|
list: () => [...(scopes.get(key)?.values() ?? [])].map((r) => r.snapshot()),
|
|
506
526
|
read: (id, offset, maxBytes) => Promise.resolve(find(id).read(offset, maxBytes)),
|
|
507
|
-
wait: async (id, timeoutMs, signal) => {
|
|
527
|
+
wait: async (id, timeoutMs, signal, output) => {
|
|
528
|
+
signal.throwIfAborted();
|
|
508
529
|
const record = find(id);
|
|
530
|
+
const stopOutput = record.observeOutput(output);
|
|
509
531
|
const controller = new AbortController();
|
|
510
532
|
const abandon = () => {
|
|
511
533
|
controller.abort();
|
|
@@ -519,6 +541,7 @@ export function createPrivateLocalComputerProcesses(root, home = capturePrivateL
|
|
|
519
541
|
return waitOutput.parse({ process: record.snapshot(), timedOut });
|
|
520
542
|
}
|
|
521
543
|
finally {
|
|
544
|
+
stopOutput();
|
|
522
545
|
signal.removeEventListener("abort", abandon);
|
|
523
546
|
controller.abort();
|
|
524
547
|
}
|
|
@@ -589,7 +612,7 @@ const backgroundResult = readOutput.extend({
|
|
|
589
612
|
});
|
|
590
613
|
export const backgroundExecOutput = z.union([backgroundResult, listOutput]);
|
|
591
614
|
/** Reuse Session ownership and logs; a wait deadline never cancels the command. */
|
|
592
|
-
export async function callBackgroundExec(scope, request, resolveCwd, executionPath, signal, sandbox) {
|
|
615
|
+
export async function callBackgroundExec(scope, request, resolveCwd, executionPath, signal, sandbox, sink) {
|
|
593
616
|
signal.throwIfAborted();
|
|
594
617
|
if (request.operation === "list")
|
|
595
618
|
return { processes: [...scope.list()] };
|
|
@@ -605,7 +628,8 @@ export async function callBackgroundExec(scope, request, resolveCwd, executionPa
|
|
|
605
628
|
}
|
|
606
629
|
case "wait":
|
|
607
630
|
id = request.id;
|
|
608
|
-
timedOut = (await scope.wait(id, request.timeoutMs, signal))
|
|
631
|
+
timedOut = (await scope.wait(id, request.timeoutMs, signal, sink))
|
|
632
|
+
.timedOut;
|
|
609
633
|
break;
|
|
610
634
|
case "kill":
|
|
611
635
|
id = request.id;
|