@fungi.computer/truffle 0.5.0 → 0.7.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.
Files changed (39) hide show
  1. package/README.md +27 -4
  2. package/dist/internal/private-workerd-sandbox-worker.mjs +243 -273
  3. package/dist/internal/sandbox-metafile.json +35 -30
  4. package/dist/local-extensions.d.ts +2 -2
  5. package/dist/local-extensions.js +43 -19
  6. package/dist/log-event.d.ts +3 -0
  7. package/dist/log-event.js +19 -1
  8. package/dist/private-fungi-mcp-connection.js +6 -1
  9. package/dist/private-invocation-computer.d.ts +6 -2
  10. package/dist/private-invocation-computer.js +20 -13
  11. package/dist/private-local-auth-rpc.d.ts +1 -1
  12. package/dist/private-local-computer-processes.d.ts +7 -2
  13. package/dist/private-local-computer-processes.js +47 -23
  14. package/dist/private-local-delegation.js +15 -11
  15. package/dist/private-local-exec-environment.d.ts +5 -2
  16. package/dist/private-local-exec-environment.js +35 -1
  17. package/dist/private-local-exec-review.js +14 -7
  18. package/dist/private-local-exec-rules.d.ts +7 -0
  19. package/dist/private-local-exec-rules.js +23 -6
  20. package/dist/private-local-extension-records.d.ts +1 -1
  21. package/dist/private-local-runtime-host.js +2 -2
  22. package/dist/private-local-sandbox-policy.js +4 -0
  23. package/dist/private-local-sandbox.d.ts +3 -1
  24. package/dist/private-local-sandbox.js +2 -2
  25. package/dist/private-local-schedule-client.d.ts +12 -0
  26. package/dist/private-local-schedule-client.js +41 -0
  27. package/dist/private-local-schedules.d.ts +84 -0
  28. package/dist/private-local-schedules.js +66 -0
  29. package/dist/private-local-service-contract.d.ts +3 -0
  30. package/dist/private-local-service-discovery.d.ts +4 -4
  31. package/dist/private-local-service-discovery.js +1 -1
  32. package/dist/private-local-service-listener.js +9 -0
  33. package/dist/private-local-service-owner.d.ts +130 -0
  34. package/dist/private-local-service-owner.js +29 -0
  35. package/dist/private-local-service.d.ts +3 -0
  36. package/dist/private-local-service.js +2 -0
  37. package/dist/private-mcp-form-session.d.ts +2 -2
  38. package/dist/private-openai-claims.d.ts +1 -1
  39. package/package.json +18 -14
@@ -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
- if ("command" in request) {
170
- const spec = { kind: "shell", command: request.command };
171
- return request.stdin === undefined
172
- ? spec
173
- : { ...spec, stdin: request.stdin };
174
- }
175
- const spec = {
176
- kind: "direct",
177
- executable: request.executable,
178
- args: request.args,
179
- };
180
- return request.stdin === undefined ? spec : { ...spec, stdin: request.stdin };
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
- const append = (chunk) => {
304
- this.#append(chunk);
305
- };
306
- this.#child.stdout?.on("data", append);
307
- this.#child.stderr?.on("data", append);
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)).timedOut;
631
+ timedOut = (await scope.wait(id, request.timeoutMs, signal, sink))
632
+ .timedOut;
609
633
  break;
610
634
  case "kill":
611
635
  id = request.id;
@@ -1,6 +1,6 @@
1
1
  import * as Effect from "effect/Effect";
2
2
  import { AgentArchiveRefused } from "@fungi.computer/shiitake/agent-control";
3
- import { agentDelegationRequestKey, createFleetDelegation, delegationProvenance, delegationReplyMeta, delegationSendMeta, } from "@fungi.computer/shiitake/fleet";
3
+ import { agentDelegationRequestKey, createFleetDelegation, delegationProvenance, delegationReplyMeta, delegationSendMeta, isOneWayAncestorNotice, } from "@fungi.computer/shiitake/fleet";
4
4
  /** How long one Local delegation grant stays valid while it is admitted. */
5
5
  const grantLifetimeMs = 60_000;
6
6
  /**
@@ -113,19 +113,22 @@ export function createPrivateLocalDelegation(input) {
113
113
  sessionId: sourceSessionId,
114
114
  });
115
115
  signal.throwIfAborted();
116
+ let delegation = {
117
+ agentId: task.agentId,
118
+ sessionId: task.sessionId,
119
+ delivery: task.delivery,
120
+ text: task.text,
121
+ requestId,
122
+ };
123
+ if (task.delivery === "followUp" && task.goal !== undefined)
124
+ delegation = { ...delegation, goal: task.goal };
116
125
  return fleet.start({
117
126
  agents: agents.map(({ agentId }) => agentId),
118
127
  actions: ["delegate"],
119
128
  expiresAt: Date.now() + grantLifetimeMs,
120
129
  provenance,
121
130
  meta,
122
- }, {
123
- agentId: task.agentId,
124
- sessionId: task.sessionId,
125
- delivery: task.delivery,
126
- text: task.text,
127
- requestId,
128
- });
131
+ }, delegation);
129
132
  }
130
133
  /** Resolve only this sender's receipt, then let the target own the pending guard. */
131
134
  async function recall(task, sourceSessionId, signal) {
@@ -157,9 +160,8 @@ export function createPrivateLocalDelegation(input) {
157
160
  }
158
161
  }
159
162
  /**
160
- * Admit one mail addressed to this Agent. New work must name its sender as
161
- * its only reply destination; a reply-less mail is admitted only as an
162
- * authenticated reply. Everything else is refused.
163
+ * Admit delegated work, one-way ancestor notices and authenticated replies.
164
+ * Other reply-less mail is refused.
163
165
  */
164
166
  async function receive(mail) {
165
167
  const mesh = await input.meshAdmission?.(mail.to.agentId);
@@ -187,6 +189,8 @@ export function createPrivateLocalDelegation(input) {
187
189
  throw new Error("Local delegated work must reply to its sender");
188
190
  return input.accept(mail, mail.meta);
189
191
  }
192
+ if (isOneWayAncestorNotice(mail))
193
+ return input.accept(mail, mail.meta);
190
194
  if (mail.reply?.outcome === "accepted") {
191
195
  const receipt = fleet.recordAcceptedNotice(mail);
192
196
  if (receipt === undefined)
@@ -1,3 +1,6 @@
1
+ import { z } from "zod";
2
+ /** Shared admission for shell/direct run and managed start. */
3
+ export declare const privateLocalExecEnvironmentInput: z.ZodRecord<z.ZodString, z.ZodString>;
1
4
  /**
2
5
  * The only host variables a Local exec child receives besides its selected
3
6
  * PATH and HOME. Exec runs as the host's own user, so that user's session
@@ -5,7 +8,7 @@
5
8
  * `loginctl` and other user-service tools need to find the user manager.
6
9
  * Every other host variable, credentials included, stays out.
7
10
  */
8
- export type PrivateLocalExecEnvironment = {
11
+ export type PrivateLocalExecEnvironment = Record<string, string> & {
9
12
  PATH: string;
10
13
  HOME: string;
11
14
  XDG_RUNTIME_DIR?: string;
@@ -13,4 +16,4 @@ export type PrivateLocalExecEnvironment = {
13
16
  };
14
17
  export declare function privateLocalExecEnvironment(executionPath: string, host: NodeJS.ProcessEnv & {
15
18
  HOME: string;
16
- }): PrivateLocalExecEnvironment;
19
+ }, extra?: Readonly<Record<string, string>>): PrivateLocalExecEnvironment;
@@ -1,5 +1,39 @@
1
- export function privateLocalExecEnvironment(executionPath, host) {
1
+ import { z } from "zod";
2
+ // Extras reach the native sandbox launcher too; its loader controls stay owned
3
+ // by the host rather than loading model-selected code before containment.
4
+ const hostOwnedNames = new Set([
5
+ "PATH",
6
+ "HOME",
7
+ "XDG_RUNTIME_DIR",
8
+ "DBUS_SESSION_BUS_ADDRESS",
9
+ "TMPDIR",
10
+ "SSH_AUTH_SOCK",
11
+ "GIT_DISCOVERY_ACROSS_FILESYSTEM",
12
+ "FUNGI_SANDBOX",
13
+ "FUNGI_SANDBOX_NETWORK_DISABLED",
14
+ "GLIBC_TUNABLES",
15
+ "GCONV_PATH",
16
+ "LOCPATH",
17
+ ]);
18
+ const environmentName = z
19
+ .string()
20
+ .regex(/^[A-Z_][A-Z0-9_]*$/u)
21
+ .refine((name) => !hostOwnedNames.has(name) &&
22
+ !name.startsWith("LD_") &&
23
+ !name.startsWith("DYLD_"), "Host-owned environment variable");
24
+ /** Shared admission for shell/direct run and managed start. */
25
+ export const privateLocalExecEnvironmentInput = z
26
+ .record(environmentName, z
27
+ .string()
28
+ .max(1_024)
29
+ .regex(/^[^\0]*$/u, "Environment values cannot contain NUL"))
30
+ .refine((environment) => Object.keys(environment).length <= 64, {
31
+ message: "Exec env accepts at most 64 variables",
32
+ })
33
+ .describe("Up to 64 extra string variables, each at most 1024 UTF-16 code units, with uppercase ASCII names (letters, digits and underscores; no leading digit). Host-owned names, including HOME, PATH, session/sandbox locators and dynamic-loader controls, are refused.");
34
+ export function privateLocalExecEnvironment(executionPath, host, extra = {}) {
2
35
  const environment = {
36
+ ...extra,
3
37
  PATH: executionPath,
4
38
  HOME: host.HOME,
5
39
  };
@@ -1,5 +1,6 @@
1
- import { gitRuleCoverage, rulePrefix } from "./private-local-exec-rules.js";
2
- import { quote } from "shell-quote";
1
+ import * as Result from "effect/Result";
2
+ import { gitRuleCoverage, hasExtensionInstallRisk, rulePrefix, } from "./private-local-exec-rules.js";
3
+ import { parse, quote } from "shell-quote";
3
4
  const gitHookGrantSubcommands = new Set([
4
5
  "add",
5
6
  "branch",
@@ -25,6 +26,12 @@ export function localExecEscapeReview(request, cwd, gitGrant) {
25
26
  ? undefined
26
27
  : [request.executable, ...(request.args ?? [])];
27
28
  const command = request.command ?? quote(argv ?? []);
29
+ // Literal shell tokens only add the extension risk note. They never turn
30
+ // shell text into argv, establish command identity, or permit a saved rule.
31
+ // Opaque shell text keeps its existing once-only generic review.
32
+ const words = argv ??
33
+ Result.getOrElse(Result.try(() => parse(command, {})), () => []);
34
+ const extensionInstall = hasExtensionInstallRisk(words, argv === undefined ? "shell" : "direct");
28
35
  const prefix = argv === undefined || (argv[0] === "git" && gitGrant === undefined)
29
36
  ? undefined
30
37
  : rulePrefix(argv, gitGrant?.configuredRemotes);
@@ -33,9 +40,7 @@ export function localExecEscapeReview(request, cwd, gitGrant) {
33
40
  cwd,
34
41
  justification: request.escape.justification,
35
42
  form: argv === undefined ? "shell" : "direct",
36
- kind: argv?.[0] === "fungi" && argv[1] === "extension" && argv[2] === "add"
37
- ? "extension-install"
38
- : "exec",
43
+ kind: extensionInstall ? "extension-install" : "exec",
39
44
  };
40
45
  if (argv !== undefined)
41
46
  review.argv = argv;
@@ -44,7 +49,7 @@ export function localExecEscapeReview(request, cwd, gitGrant) {
44
49
  review.alwaysAllowCoverage =
45
50
  prefix[0] === "git"
46
51
  ? gitRuleCoverage(prefix[1] ?? "")
47
- : `Direct argv commands beginning ${JSON.stringify(prefix)} in this project; shell commands still ask.`;
52
+ : `Direct argv commands beginning ${JSON.stringify(prefix)} in this project; shell commands${prefix[0] === "fungi" ? " and extension installs" : ""} still ask.`;
48
53
  if (prefix[0] === "git") {
49
54
  const hookCapable = gitHookGrantSubcommands.has(prefix[1] ?? "");
50
55
  const summary = (hookCapable
@@ -68,7 +73,9 @@ export function localExecEscapeReview(request, cwd, gitGrant) {
68
73
  review.risk = { summary, detail };
69
74
  }
70
75
  else {
71
- const summary = `Every \`${prefix.join(" ")} …\` command will run outside the sandbox, as you.`;
76
+ const summary = prefix[0] === "fungi"
77
+ ? `Matching \`${prefix.join(" ")} …\` commands run outside the sandbox, as you. Extension installs still ask.`
78
+ : `Every \`${prefix.join(" ")} …\` command will run outside the sandbox, as you.`;
72
79
  review.risk = { summary, detail: summary };
73
80
  }
74
81
  }
@@ -1,3 +1,10 @@
1
+ import type { ParseEntry } from "shell-quote";
2
+ /**
3
+ * Conservative literal risk, not CLI parsing or executable identity. Global
4
+ * options can occur among positionals. Shell tokens may disclose risk, but
5
+ * only actual direct argv ever reaches the reusable-grant matcher.
6
+ */
7
+ export declare function hasExtensionInstallRisk(words: readonly ParseEntry[], form: "direct" | "shell"): boolean;
1
8
  /** Offer only direct commands that this same matcher can authorize. */
2
9
  export declare function rulePrefix(argv: readonly string[], configuredRemotes?: ReadonlySet<string>): string[] | undefined;
3
10
  /** Text for the same Git option profile that gates matching, not a second list. */
@@ -2,6 +2,27 @@ import { lstatSync, mkdirSync, readFileSync, renameSync, rmdirSync, writeFileSyn
2
2
  import { basename, join } from "node:path";
3
3
  import { correlationKey, defineIdFamily } from "@fungi.computer/cairn";
4
4
  import { z } from "zod";
5
+ function fungiWord(word) {
6
+ const parsed = z.string().safeParse(word);
7
+ return parsed.success && basename(parsed.data) === "fungi";
8
+ }
9
+ /**
10
+ * Conservative literal risk, not CLI parsing or executable identity. Global
11
+ * options can occur among positionals. Shell tokens may disclose risk, but
12
+ * only actual direct argv ever reaches the reusable-grant matcher.
13
+ */
14
+ export function hasExtensionInstallRisk(words, form) {
15
+ const executable = form === "direct"
16
+ ? fungiWord(words[0])
17
+ ? 0
18
+ : -1
19
+ : words.findIndex(fungiWord);
20
+ if (executable < 0)
21
+ return false;
22
+ const extension = words.findIndex((word, index) => index > executable && word === "extension");
23
+ return (extension >= 0 &&
24
+ words.some((word, index) => index > extension && word === "add"));
25
+ }
5
26
  const prefixSchema = z.array(z.string().min(1).max(4096)).min(1).max(32);
6
27
  const projectRuleIds = defineIdFamily({
7
28
  family: "local-exec-rules",
@@ -264,9 +285,7 @@ export function rulePrefix(argv, configuredRemotes) {
264
285
  ? prefix
265
286
  : undefined;
266
287
  }
267
- if (basename(argv[0] ?? "") === "fungi" &&
268
- argv[1] === "extension" &&
269
- argv[2] === "add")
288
+ if (hasExtensionInstallRisk(argv, "direct"))
270
289
  return undefined;
271
290
  const parsed = prefixSchema.safeParse(argv.slice(0, 2));
272
291
  return parsed.success && validPrefix(parsed.data) ? parsed.data : undefined;
@@ -416,9 +435,7 @@ export function localExecRules(project) {
416
435
  };
417
436
  const match = (argv, configuredRemotes) => {
418
437
  if (unsafeExecutable(argv[0] ?? "") ||
419
- (basename(argv[0] ?? "") === "fungi" &&
420
- argv[1] === "extension" &&
421
- argv[2] === "add"))
438
+ hasExtensionInstallRisk(argv, "direct"))
422
439
  return undefined;
423
440
  if (argv[0] === "git" && !safeGitCommand(argv, configuredRemotes))
424
441
  return undefined;
@@ -7,6 +7,7 @@ export declare const activationsSchema: z.ZodObject<{
7
7
  revision: z.ZodString;
8
8
  capabilities: z.ZodArray<z.ZodEnum<{
9
9
  state: "state";
10
+ prompt: "prompt";
10
11
  attention: "attention";
11
12
  settled: "settled";
12
13
  send: "send";
@@ -15,7 +16,6 @@ export declare const activationsSchema: z.ZodObject<{
15
16
  projection: "projection";
16
17
  run_tools: "run_tools";
17
18
  files: "files";
18
- prompt: "prompt";
19
19
  compaction_handoff: "compaction_handoff";
20
20
  }>>;
21
21
  skills: z.ZodBoolean;
@@ -1348,7 +1348,7 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
1348
1348
  throw new Error("Local escape approval could not be presented");
1349
1349
  return pending;
1350
1350
  };
1351
- const policyForSession = (sessionId) => async ({ request, cwd, mode, executionPath }) => {
1351
+ const policyForSession = (sessionId) => async ({ request, cwd, mode, executionPath, context }) => {
1352
1352
  if (mode !== "workspace" || !("executable" in request))
1353
1353
  return undefined;
1354
1354
  let configuredRemotes;
@@ -1365,7 +1365,7 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
1365
1365
  executable: request.executable,
1366
1366
  ruleId: matched.ruleId,
1367
1367
  });
1368
- return shellOutput.parse(await dispatchApprovedLocalExec(request, cwd, executionPath, configRoot.home, localTools.processes.scope(sessionId), mode));
1368
+ return shellOutput.parse(await dispatchApprovedLocalExec(request, cwd, executionPath, configRoot.home, localTools.processes.scope(sessionId), mode, context.output));
1369
1369
  };
1370
1370
  const localTools = await createPrivateLocalTools(selectedFolder.absolute, input.fungiMcp, subagents?.directory, (sessionId) => sessionPaths.get(sessionId), dirname(paths.databasePath), subagents, await input.configuration?.(dirname(paths.databasePath)), configRoot, sandboxOwner, () => sandboxOwner.status().mode, escapeForSession, policyForSession);
1371
1371
  processes = localTools.processes;
@@ -90,6 +90,10 @@ osHome, platform = "linux") {
90
90
  mode,
91
91
  temporaryDirectory,
92
92
  fungiCache: protectedFungiCache,
93
+ // Adding PATH lookup directories would invalidate the current name-based
94
+ // extension-install risk classification; revisit private-local-exec-rules.ts
95
+ // before widening this set. Guard: ../test/private-local-containment.test.ts,
96
+ // "keeps every captured PATH directory outside Workspace writable roots".
93
97
  writable: mode === "workspace"
94
98
  ? [...new Set([policy.writableRoot, ...writableCaches])]
95
99
  : [],
@@ -1,6 +1,6 @@
1
1
  import type { NativeSourceReader } from "./private-local-bwrap.js";
2
2
  import { z } from "zod";
3
- import { sandboxUnavailableReasonSchema, type SandboxUnavailableReason } from "./private-local-containment.js";
3
+ import { establishLocalSandbox, sandboxUnavailableReasonSchema, type SandboxUnavailableReason } from "./private-local-containment.js";
4
4
  import type { LocalPermissionMode } from "./private-local-permission-mode.js";
5
5
  import type { LocalSandboxWrites } from "./private-invocation-files.js";
6
6
  import type { NativePayloadStaging } from "./private-local-native-staging.js";
@@ -48,6 +48,8 @@ export declare function createLocalSandboxControls(input: Readonly<{
48
48
  platform?: string;
49
49
  readNativePatchSource?: NativeSourceReader;
50
50
  native?: NativePayloadStaging | undefined;
51
+ /** Test-only hook for inspecting probe launches without native execution. */
52
+ runProbe?: Parameters<typeof establishLocalSandbox>[2];
51
53
  }>): LocalSandboxControls;
52
54
  /** Probe a fresh Agent before deciding its mode; never treat an unavailable backend as a sandboxed run. */
53
55
  export declare function probeLocalSandbox(project: string, folder: string, native?: NativePayloadStaging): SandboxUnavailableReason | undefined;
@@ -97,7 +97,7 @@ export function createLocalSandboxControls(input) {
97
97
  temporaryDirectory: temporaryDirectory ??
98
98
  agentSandboxTemporaryDirectory(temporaryOwner, input.project),
99
99
  ...platform,
100
- }, native);
100
+ }, native, input.runProbe);
101
101
  }
102
102
  }
103
103
  catch (cause) {
@@ -167,7 +167,7 @@ export function createLocalSandboxControls(input) {
167
167
  mode: nextMode,
168
168
  temporaryDirectory,
169
169
  ...platform,
170
- }, native);
170
+ }, native, input.runProbe);
171
171
  if (admission.state === "ready") {
172
172
  sandbox = admission.sandbox;
173
173
  preparedMode = nextMode;
@@ -0,0 +1,12 @@
1
+ import type { z } from "zod";
2
+ import { type AgentScheduleRow, type AgentScheduleTarget } from "@fungi.computer/shiitake-extensions/schedules";
3
+ import { localScheduleListInputSchema, type LocalSchedulePage } from "./private-local-schedules.js";
4
+ import type { PrivateLocalServiceRequestOptions, PrivateLocalServiceStatus } from "./private-local-service-contract.js";
5
+ /** A private, folder-bound owner capability; it contains no public URL or grant. */
6
+ export type PrivateLocalScheduleCapability = Readonly<{
7
+ list(input?: Omit<z.input<typeof localScheduleListInputSchema>, "invocationRoot">, options?: PrivateLocalServiceRequestOptions): Promise<LocalSchedulePage>;
8
+ pause(target: AgentScheduleTarget, options?: PrivateLocalServiceRequestOptions): Promise<AgentScheduleRow>;
9
+ delete(target: AgentScheduleTarget, options?: PrivateLocalServiceRequestOptions): Promise<AgentScheduleRow>;
10
+ }>;
11
+ /** Only a native private service client creates this capability for its person. */
12
+ export declare function createPrivateLocalScheduleCapability(invocationRoot: string, start: (options?: PrivateLocalServiceRequestOptions) => Promise<PrivateLocalServiceStatus>): PrivateLocalScheduleCapability;
@@ -0,0 +1,41 @@
1
+ import * as Schema from "effect/Schema";
2
+ import { parseShiitakeError } from "@fungi.computer/shiitake/client";
3
+ import { agentScheduleRowSchema, } from "@fungi.computer/shiitake-extensions/schedules";
4
+ import { readBoundedResponse } from "./private-bounded-response.js";
5
+ import { localScheduleControlInputSchema, localScheduleListInputSchema, localSchedulePageSchema, } from "./private-local-schedules.js";
6
+ // A page has at most 64 prompts of 256 KiB, including JSON escaping and metadata.
7
+ // This is a wire budget, not a limit on retained records or future occurrences.
8
+ const maximumResponseBytes = 128 * 1024 * 1024;
9
+ /** Only a native private service client creates this capability for its person. */
10
+ export function createPrivateLocalScheduleCapability(invocationRoot, start) {
11
+ async function call(operation, input, output, options = {}) {
12
+ options.signal?.throwIfAborted();
13
+ const service = await start(options);
14
+ const request = {
15
+ method: "POST",
16
+ headers: { "Content-Type": "application/json" },
17
+ body: JSON.stringify(input),
18
+ redirect: "error",
19
+ };
20
+ if (options.signal !== undefined)
21
+ request.signal = options.signal;
22
+ const response = await fetch(`${service.baseUrl}/service/${service.serviceId}/schedules/${operation}`, request);
23
+ const body = await readBoundedResponse(response, maximumResponseBytes, `Private Local schedule ${operation}`);
24
+ if (!response.ok) {
25
+ let failure;
26
+ try {
27
+ failure = parseShiitakeError(body);
28
+ }
29
+ catch (cause) {
30
+ throw new Error(`Private Local schedule ${operation} failed (${String(response.status)})`, { cause });
31
+ }
32
+ throw failure;
33
+ }
34
+ return Schema.decodeUnknownSync(output, { onExcessProperty: "error" })(JSON.parse(body));
35
+ }
36
+ return {
37
+ list: (input = {}, options) => call("list", localScheduleListInputSchema.parse({ ...input, invocationRoot }), localSchedulePageSchema, options),
38
+ pause: (target, options) => call("pause", localScheduleControlInputSchema.parse({ invocationRoot, target }), agentScheduleRowSchema, options),
39
+ delete: (target, options) => call("delete", localScheduleControlInputSchema.parse({ invocationRoot, target }), agentScheduleRowSchema, options),
40
+ };
41
+ }
@@ -0,0 +1,84 @@
1
+ import * as Schema from "effect/Schema";
2
+ import { z } from "zod";
3
+ import type { ShiitakeAgent } from "@fungi.computer/shiitake";
4
+ /** Private service ingress, never a Team/member or machine-link request. */
5
+ export declare const localScheduleListInputSchema: z.ZodObject<{
6
+ after: z.ZodOptional<z.ZodObject<{
7
+ agentId: z.ZodString;
8
+ kind: z.ZodUnion<readonly [z.ZodLiteral<"almanac">, z.ZodLiteral<"loop">]>;
9
+ after: z.ZodOptional<z.ZodString>;
10
+ }, z.core.$strict>>;
11
+ limit: z.ZodDefault<z.ZodNumber>;
12
+ invocationRoot: z.ZodString;
13
+ }, z.core.$strict>;
14
+ /** Private person-only mutation, distinct from the finite list request. */
15
+ export declare const localScheduleControlInputSchema: z.ZodObject<{
16
+ invocationRoot: z.ZodString;
17
+ target: z.ZodDiscriminatedUnion<[z.ZodObject<{
18
+ kind: z.ZodLiteral<"almanac">;
19
+ agentId: z.ZodString;
20
+ scheduleId: z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>;
21
+ }, z.core.$strict>, z.ZodObject<{
22
+ kind: z.ZodLiteral<"loop">;
23
+ agentId: z.ZodString;
24
+ sessionId: z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>;
25
+ loopId: z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>;
26
+ }, z.core.$strict>], "kind">;
27
+ }, z.core.$strict>;
28
+ /** Local directory pages reuse the schedule owner's decoder and retained state. */
29
+ export declare const localSchedulePageSchema: Schema.Struct<{
30
+ readonly items: Schema.$Array<Schema.Union<readonly [Schema.Struct<{
31
+ readonly agentId: Schema.String;
32
+ readonly kind: Schema.Literal<"almanac">;
33
+ readonly schedule: Schema.Struct<{
34
+ readonly id: Schema.String;
35
+ readonly sessionId: Schema.String;
36
+ readonly prompt: Schema.String;
37
+ readonly cron: Schema.NullOr<Schema.Struct<{
38
+ readonly expression: Schema.String;
39
+ readonly timeZone: Schema.String;
40
+ }>>;
41
+ readonly nextDueAt: Schema.Int;
42
+ readonly state: Schema.Literals<readonly ["active", "paused", "cancelled", "finished"]>;
43
+ readonly lastCommandId: Schema.NullOr<Schema.String>;
44
+ readonly lastError: Schema.NullOr<Schema.String>;
45
+ readonly attempt: Schema.Int;
46
+ }>;
47
+ }>, Schema.Struct<{
48
+ readonly agentId: Schema.String;
49
+ readonly kind: Schema.Literal<"loop">;
50
+ readonly schedule: Schema.Struct<{
51
+ readonly sessionId: Schema.String;
52
+ readonly loopId: Schema.String;
53
+ readonly origin: Schema.Literals<readonly ["human", "model"]>;
54
+ readonly expiresAt: Schema.NullOr<Schema.Int>;
55
+ readonly prompt: Schema.String;
56
+ readonly mode: Schema.Literals<readonly ["self_paced", "fixed_interval"]>;
57
+ readonly cron: Schema.NullOr<Schema.Struct<{
58
+ readonly expression: Schema.String;
59
+ readonly timeZone: Schema.String;
60
+ }>>;
61
+ readonly state: Schema.Literals<readonly ["active", "paused", "finished", "failed", "cancelled"]>;
62
+ readonly commandId: Schema.String;
63
+ readonly reason: Schema.NullOr<Schema.String>;
64
+ readonly nextDueAt: Schema.NullOr<Schema.Int>;
65
+ readonly lastError: Schema.NullOr<Schema.String>;
66
+ readonly consecutiveFailures: Schema.Int;
67
+ }>;
68
+ }>]>>;
69
+ readonly unavailable: Schema.$Array<Schema.Struct<{
70
+ readonly agentId: Schema.String;
71
+ readonly reason: Schema.Literal<"owner_unavailable">;
72
+ }>>;
73
+ readonly next: Schema.NullOr<Schema.Struct<{
74
+ readonly agentId: Schema.String;
75
+ readonly kind: Schema.Literals<readonly ["almanac", "loop"]>;
76
+ readonly after: Schema.optionalKey<Schema.String>;
77
+ }>>;
78
+ }>;
79
+ export type LocalSchedulePage = typeof localSchedulePageSchema.Type;
80
+ /** One bounded read, borrowing existing admitted Agent owners one at a time. */
81
+ export declare function readLocalSchedulePage(agentIds: readonly string[], open: (agentId: string, signal: AbortSignal) => Promise<{
82
+ owner: Pick<ShiitakeAgent, "extensions">;
83
+ release: () => void;
84
+ }>, input: z.output<typeof localScheduleListInputSchema>, signal: AbortSignal): Promise<LocalSchedulePage>;