@fungi.computer/truffle 0.2.2 → 0.3.1

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 (68) hide show
  1. package/dist/private-files-patch.js +3 -17
  2. package/dist/private-invocation-computer.d.ts +224 -12
  3. package/dist/private-invocation-computer.js +162 -22
  4. package/dist/private-invocation-files.d.ts +3 -2
  5. package/dist/private-invocation-files.js +22 -3
  6. package/dist/private-local-agent-control.d.ts +2 -2
  7. package/dist/private-local-bwrap.d.ts +9 -7
  8. package/dist/private-local-bwrap.js +26 -21
  9. package/dist/private-local-computer-processes.d.ts +15 -0
  10. package/dist/private-local-computer-processes.js +13 -0
  11. package/dist/private-local-containment.d.ts +15 -13
  12. package/dist/private-local-containment.js +49 -65
  13. package/dist/private-local-delegation.d.ts +3 -3
  14. package/dist/private-local-delegation.js +32 -19
  15. package/dist/private-local-exec-review.d.ts +12 -0
  16. package/dist/private-local-exec-review.js +62 -0
  17. package/dist/private-local-exec-rules.d.ts +16 -0
  18. package/dist/private-local-exec-rules.js +441 -0
  19. package/dist/private-local-git-exclude.js +25 -15
  20. package/dist/private-local-linux-sandbox.d.ts +5 -5
  21. package/dist/private-local-linux-sandbox.js +63 -30
  22. package/dist/private-local-mail.d.ts +2 -2
  23. package/dist/private-local-mesh-links.d.ts +4 -1
  24. package/dist/private-local-mesh-links.js +50 -3
  25. package/dist/private-local-native-digests.js +1 -1
  26. package/dist/private-local-native-install-error.d.ts +2 -0
  27. package/dist/private-local-native-install-error.js +10 -0
  28. package/dist/private-local-native-staging.d.ts +7 -8
  29. package/dist/private-local-native-staging.js +132 -43
  30. package/dist/private-local-permission-mode.d.ts +10 -0
  31. package/dist/private-local-permission-mode.js +18 -0
  32. package/dist/private-local-runtime-host.d.ts +28 -5
  33. package/dist/private-local-runtime-host.js +404 -66
  34. package/dist/private-local-sandbox-policy.d.ts +30 -11
  35. package/dist/private-local-sandbox-policy.js +223 -373
  36. package/dist/private-local-sandbox.d.ts +30 -25
  37. package/dist/private-local-sandbox.js +190 -100
  38. package/dist/private-local-seatbelt.d.ts +7 -13
  39. package/dist/private-local-seatbelt.js +55 -32
  40. package/dist/private-local-service-contract.d.ts +5 -4
  41. package/dist/private-local-service-discovery.d.ts +19 -7
  42. package/dist/private-local-service-discovery.js +4 -3
  43. package/dist/private-local-service-listener.js +40 -2
  44. package/dist/private-local-service-log.d.ts +18 -0
  45. package/dist/private-local-service-log.js +6 -0
  46. package/dist/private-local-service-owner.d.ts +18 -10
  47. package/dist/private-local-service-owner.js +119 -25
  48. package/dist/private-local-service.js +30 -3
  49. package/dist/private-local-session-discovery.d.ts +2 -1
  50. package/dist/private-local-session-discovery.js +18 -6
  51. package/dist/private-mcp-form-contract.d.ts +10 -1
  52. package/dist/private-mcp-form-contract.js +10 -1
  53. package/dist/private-mcp-form-session.d.ts +39 -3
  54. package/dist/private-mcp-form-session.js +28 -5
  55. package/dist/private-oyster-mcp-auth.d.ts +1 -1
  56. package/dist/private-oyster-mcp-auth.js +139 -7
  57. package/dist/private-provider-rejection.js +1 -1
  58. package/dist/seatbelt/safe-base.sbpl +2 -2
  59. package/native/bin/bwrap-build-source/Dockerfile +2 -0
  60. package/native/bin/bwrap-build-source/README.md +6 -6
  61. package/native/bin/bwrap-build-source/pin.mjs +2 -2
  62. package/native/bin/bwrap-build-source/record.mjs +12 -1
  63. package/native/bin/bwrap-build-source/vendor/seccomp-unix-block.c +10 -7
  64. package/native/bin/bwrap-build.json +3 -2
  65. package/native/bin/fungi-apply-patch +0 -0
  66. package/native/bin/fungi-seccomp-workspace.bpf +0 -0
  67. package/native/bin/fungi-seccomp-workspace.bpf.sha256 +1 -0
  68. package/package.json +23 -17
@@ -4,7 +4,7 @@ export { sandboxStatusSchema } from "./private-local-sandbox.js";
4
4
  * The client-service wire contract. Bump it with any change an older client
5
5
  * cannot read; a running client then asks its window to restart.
6
6
  */
7
- export declare const privateLocalServiceProtocol = 3;
7
+ export declare const privateLocalServiceProtocol = 4;
8
8
  /** This build's identity: the packed CLI's content hash, or `source` for a source run. */
9
9
  export declare function privateLocalServiceVersion(): string;
10
10
  /**
@@ -19,7 +19,7 @@ declare const serviceLifecycleSchema: z.ZodObject<{
19
19
  startedAt: z.ZodString;
20
20
  }, z.core.$loose>;
21
21
  declare const serviceRecordSchema: z.ZodObject<{
22
- protocol: z.ZodLiteral<3>;
22
+ protocol: z.ZodLiteral<4>;
23
23
  baseUrl: z.ZodURL;
24
24
  version: z.ZodString;
25
25
  serviceId: z.ZodString & z.ZodType<import("@fungi.computer/cairn").CairnId<"local-service", "service", 1>, string, z.core.$ZodTypeInternals<import("@fungi.computer/cairn").CairnId<"local-service", "service", 1>, string>>;
@@ -29,7 +29,7 @@ declare const serviceRecordSchema: z.ZodObject<{
29
29
  export type ServiceRecord = z.infer<typeof serviceRecordSchema>;
30
30
  /** Mandatory live diagnostics; never written into the registration pointer. */
31
31
  export declare const liveServiceStatusSchema: z.ZodObject<{
32
- protocol: z.ZodLiteral<3>;
32
+ protocol: z.ZodLiteral<4>;
33
33
  baseUrl: z.ZodURL;
34
34
  version: z.ZodString;
35
35
  serviceId: z.ZodString & z.ZodType<import("@fungi.computer/cairn").CairnId<"local-service", "service", 1>, string, z.core.$ZodTypeInternals<import("@fungi.computer/cairn").CairnId<"local-service", "service", 1>, string>>;
@@ -76,7 +76,11 @@ export declare const selectionSchema: z.ZodObject<{
76
76
  }>>;
77
77
  invocationRoot: z.ZodString;
78
78
  clientPath: z.ZodString;
79
- requireSandbox: z.ZodOptional<z.ZodBoolean>;
79
+ mode: z.ZodOptional<z.ZodEnum<{
80
+ "read-only": "read-only";
81
+ workspace: "workspace";
82
+ "full-access": "full-access";
83
+ }>>;
80
84
  databasePath: z.ZodOptional<z.ZodString>;
81
85
  sessionId: z.ZodOptional<z.ZodString>;
82
86
  fungiMcp: z.ZodOptional<z.ZodObject<{
@@ -91,7 +95,11 @@ export declare const sessionCreationSchema: z.ZodObject<{
91
95
  }>>;
92
96
  invocationRoot: z.ZodString;
93
97
  clientPath: z.ZodString;
94
- requireSandbox: z.ZodOptional<z.ZodBoolean>;
98
+ mode: z.ZodOptional<z.ZodEnum<{
99
+ "read-only": "read-only";
100
+ workspace: "workspace";
101
+ "full-access": "full-access";
102
+ }>>;
95
103
  databasePath: z.ZodString;
96
104
  requestId: z.ZodString;
97
105
  fungiMcp: z.ZodOptional<z.ZodObject<{
@@ -100,13 +108,17 @@ export declare const sessionCreationSchema: z.ZodObject<{
100
108
  }, z.core.$strict>>;
101
109
  }, z.core.$strict>;
102
110
  export declare const sessionInspectionSchema: z.ZodObject<{
111
+ mode: z.ZodOptional<z.ZodEnum<{
112
+ "read-only": "read-only";
113
+ workspace: "workspace";
114
+ "full-access": "full-access";
115
+ }>>;
103
116
  databasePath: z.ZodOptional<z.ZodString>;
104
117
  invocationRoot: z.ZodString;
105
118
  fungiMcp: z.ZodOptional<z.ZodObject<{
106
119
  endpoint: z.ZodPipe<z.ZodURL, z.ZodString>;
107
120
  authentication: z.ZodOptional<z.ZodLiteral<"oyster-key">>;
108
121
  }, z.core.$strict>>;
109
- requireSandbox: z.ZodOptional<z.ZodBoolean>;
110
122
  sessionId: z.ZodString;
111
123
  limit: z.ZodNumber;
112
124
  }, z.core.$strict>;
@@ -131,10 +143,10 @@ export declare const archiveRefusalSchema: z.ZodObject<{
131
143
  reason: z.ZodEnum<{
132
144
  pending: "pending";
133
145
  running: "running";
134
- unreadable: "unreadable";
135
146
  archived: "archived";
136
147
  not_creator: "not_creator";
137
148
  self: "self";
149
+ unreadable: "unreadable";
138
150
  }>;
139
151
  error: z.ZodString;
140
152
  }, z.core.$strict>;
@@ -6,6 +6,7 @@ import { join, resolve } from "node:path";
6
6
  import { homedir } from "node:os";
7
7
  import lockfile from "proper-lockfile";
8
8
  import { z } from "zod";
9
+ import { localPermissionModeSchema } from "./private-local-permission-mode.js";
9
10
  import { localServiceIds } from "./private-local-service-ids.js";
10
11
  import { acquireServiceOwnerLease, readServiceLeaseOwner, } from "./private-local-service-lease.js";
11
12
  import { modelReasoningLevels } from "@fungi.computer/shiitake/model-preference";
@@ -16,7 +17,7 @@ export { sandboxStatusSchema } from "./private-local-sandbox.js";
16
17
  * The client-service wire contract. Bump it with any change an older client
17
18
  * cannot read; a running client then asks its window to restart.
18
19
  */
19
- export const privateLocalServiceProtocol = 3;
20
+ export const privateLocalServiceProtocol = 4;
20
21
  /** This build's identity: the packed CLI's content hash, or `source` for a source run. */
21
22
  export function privateLocalServiceVersion() {
22
23
  return (z
@@ -64,7 +65,7 @@ export const selectionSchema = z.strictObject({
64
65
  uiAttachmentLifetime: localUiAttachmentLifetimeSchema.optional(),
65
66
  invocationRoot: z.string().min(1),
66
67
  clientPath: privateLocalExecPathSchema,
67
- requireSandbox: z.boolean().optional(),
68
+ mode: localPermissionModeSchema.optional(),
68
69
  databasePath: z.string().min(1).optional(),
69
70
  sessionId: z.string().min(1).optional(),
70
71
  fungiMcp: fungiMcpSelectionSchema.optional(),
@@ -73,7 +74,7 @@ export const sessionCreationSchema = z.strictObject({
73
74
  uiAttachmentLifetime: localUiAttachmentLifetimeSchema.optional(),
74
75
  invocationRoot: z.string().min(1),
75
76
  clientPath: privateLocalExecPathSchema,
76
- requireSandbox: z.boolean().optional(),
77
+ mode: localPermissionModeSchema.optional(),
77
78
  databasePath: z.string().min(1),
78
79
  requestId: z.string().min(1),
79
80
  fungiMcp: fungiMcpSelectionSchema.optional(),
@@ -6,6 +6,10 @@ import { createServiceLogLifecycle, } from "./private-local-service-log.js";
6
6
  import { stopAllRequestSchema } from "./private-local-stop-all.js";
7
7
  import { FungiMcpSelectionConflict } from "./private-fungi-mcp-selection.js";
8
8
  import { LocalSandboxUnavailable } from "./private-local-containment.js";
9
+ import { createNativePayloadStaging, } from "./private-local-native-staging.js";
10
+ import { nativePackageStartupError } from "./private-local-native-install-error.js";
11
+ import { LocalBwrapUnavailable } from "./private-local-bwrap.js";
12
+ import { writeFileSync } from "node:fs";
9
13
  import { PrivateLocalAuthTarget } from "./private-local-auth-rpc.js";
10
14
  import { runLocalAuthCli } from "./auth-cli.js";
11
15
  import { capturePrivateLocalConfigRoot, } from "./private-local-config-root.js";
@@ -88,7 +92,12 @@ readRecoveryManifest, root = capturePrivateLocalConfigRoot()) {
88
92
  heap: jsc.heapStats(),
89
93
  }),
90
94
  });
91
- const owners = createLocalServiceOwners(openRuntime, directory, undefined, idleSweepMs, readRecoveryManifest, diagnostics.recoveryDisabled, root);
95
+ let native;
96
+ const owners = createLocalServiceOwners(openRuntime, directory, undefined, idleSweepMs, readRecoveryManifest, diagnostics.recoveryDisabled, root, undefined, diagnostics.ruleMatch, () => {
97
+ if (native === undefined)
98
+ throw new Error("Local service native staging is not ready");
99
+ return native;
100
+ }, diagnostics.approvalDecision);
92
101
  const prefix = `/service/${serviceId}`;
93
102
  const releaseAccountOwner = registerFungiAccountOwner(root.credentialPath, () => owners.refreshMesh());
94
103
  const app = new Hono();
@@ -182,7 +191,11 @@ readRecoveryManifest, root = capturePrivateLocalConfigRoot()) {
182
191
  };
183
192
  };
184
193
  const shutdown = createPrivateLocalServiceShutdown({
185
- closeOwners: owners.close,
194
+ closeOwners: async () => {
195
+ await owners.close();
196
+ // No Agent, including one reopened during recovery, still uses the copy.
197
+ native?.close();
198
+ },
186
199
  closeServer: async () => {
187
200
  await server?.close();
188
201
  },
@@ -233,6 +246,31 @@ readRecoveryManifest, root = capturePrivateLocalConfigRoot()) {
233
246
  const isLocalRequest = (request) => !request.headers.has("origin") &&
234
247
  new URL(request.url).hostname === "127.0.0.1";
235
248
  try {
249
+ // Verify and stage before publishing discovery, even without a selected Agent.
250
+ try {
251
+ native = createNativePayloadStaging(directory);
252
+ }
253
+ catch (cause) {
254
+ if (cause instanceof LocalBwrapUnavailable) {
255
+ // A detached child has no stderr. Only this classified startup refusal
256
+ // crosses to the starting client; never export arbitrary exception text.
257
+ const marker = process.env.FUNGI_NATIVE_STARTUP_REFUSAL;
258
+ if (marker !== undefined) {
259
+ try {
260
+ writeFileSync(marker, "native-package", {
261
+ flag: "wx",
262
+ mode: 0o600,
263
+ });
264
+ }
265
+ catch (reportFailure) {
266
+ // A broken report channel cannot replace the native refusal.
267
+ logEvent("service-listener.native-refusal-report", reportFailure);
268
+ }
269
+ }
270
+ throw nativePackageStartupError(cause);
271
+ }
272
+ throw cause;
273
+ }
236
274
  app.use("*", async (context, next) => {
237
275
  const request = context.req.raw;
238
276
  // Local client only: no browser-origin admission or DNS rebinding authority.
@@ -50,6 +50,18 @@ export type ServiceLogEvent = Readonly<{
50
50
  }> | Readonly<{
51
51
  event: "recovery-persistence-disabled";
52
52
  code: RecoveryPersistenceFailureCode;
53
+ }> | Readonly<{
54
+ event: "exec-rule-match";
55
+ agentId: string;
56
+ executable: string;
57
+ ruleId: string;
58
+ }> | Readonly<{
59
+ event: "exec-approval-decision";
60
+ agentId: string;
61
+ recordId: string;
62
+ decision: "approve" | "deny";
63
+ scope?: "once" | "always";
64
+ ruleId?: string;
53
65
  }> | Readonly<{
54
66
  event: "memory";
55
67
  rss: number;
@@ -80,6 +92,12 @@ export type ServiceLogLifecycle = Readonly<{
80
92
  ready: (serviceId: string) => void;
81
93
  stopping: (reason: ServiceLogShutdownReason) => void;
82
94
  failure: (failure: FailureMetadata) => void;
95
+ ruleMatch: (match: Omit<Extract<ServiceLogEvent, {
96
+ event: "exec-rule-match";
97
+ }>, "event">) => void;
98
+ approvalDecision: (decision: Omit<Extract<ServiceLogEvent, {
99
+ event: "exec-approval-decision";
100
+ }>, "event">) => void;
83
101
  recoveryDisabled: (code: RecoveryPersistenceFailureCode) => void;
84
102
  /** Synchronous, so it lands before the bounded drain exits the process. */
85
103
  drainTimeout: (drain: Readonly<{
@@ -287,6 +287,12 @@ export function createServiceLogLifecycle(input) {
287
287
  failure(failure) {
288
288
  emit({ event: "failure", failure });
289
289
  },
290
+ ruleMatch(match) {
291
+ emit({ event: "exec-rule-match", ...match });
292
+ },
293
+ approvalDecision(decision) {
294
+ emit({ event: "exec-approval-decision", ...decision });
295
+ },
290
296
  recoveryDisabled(code) {
291
297
  if (closed || recovery !== undefined)
292
298
  return;
@@ -6,7 +6,8 @@ import { type LocalExtensions } from "./local-extensions.js";
6
6
  import type { PrivateLocalServiceSelection, PrivateLocalSessionCreation } from "./private-local-service-contract.js";
7
7
  import { openPrivateLocalRuntimeHost, type PrivateLocalRuntimeHost } from "./private-local-runtime-host.js";
8
8
  import type { LocalUiAttachment } from "./private-local-ui-attachment.js";
9
- export type LocalServiceRuntime = Pick<PrivateLocalRuntimeHost, "databasePath" | "agentId" | "name" | "getSession" | "effectiveModel" | "setModel" | "setArchived" | "setRequireSandbox" | "sandboxStatus" | "pendingDelegation" | "assertModelReady" | "listSessions" | "createSession" | "deleteSession" | "extensions" | "prepareReload" | "bindSessionPath" | "acceptMail" | "readSettlement" | "idle" | "close">;
9
+ import { createLocalMeshLinks } from "./private-local-mesh-links.js";
10
+ export type LocalServiceRuntime = Pick<PrivateLocalRuntimeHost, "databasePath" | "agentId" | "name" | "getSession" | "effectiveModel" | "setModel" | "setArchived" | "setMode" | "sandboxStatus" | "pendingDelegation" | "assertModelReady" | "listSessions" | "createSession" | "deleteSession" | "extensions" | "prepareReload" | "bindSessionPath" | "acceptMail" | "readSettlement" | "idle" | "close">;
10
11
  export declare class LocalSessionPathConflict extends Error {
11
12
  readonly name = "LocalSessionPathConflict";
12
13
  constructor();
@@ -35,7 +36,9 @@ idleSweepMs?: number,
35
36
  /** Read the recovery manifest; tests can hold this I/O independently of opens. */
36
37
  readRecoveryManifest?: (path: string) => Promise<string>,
37
38
  /** Report only the classification, never the manifest or exception text. */
38
- onRecoveryDisabled?: ServiceLogLifecycle["recoveryDisabled"], configRoot?: PrivateLocalConfigRoot): {
39
+ onRecoveryDisabled?: ServiceLogLifecycle["recoveryDisabled"], configRoot?: PrivateLocalConfigRoot, meshLinksFactory?: typeof createLocalMeshLinks, onExecRuleMatch?: NonNullable<Parameters<typeof openPrivateLocalRuntimeHost>[0]["onExecRuleMatch"]>,
40
+ /** Supplied by the listener after startup; individual Agents never close it. */
41
+ serviceNative?: () => NonNullable<Parameters<typeof openPrivateLocalRuntimeHost>[0]["native"]>, onExecApprovalDecision?: NonNullable<Parameters<typeof openPrivateLocalRuntimeHost>[0]["onExecApprovalDecision"]>): {
39
42
  refreshMesh: () => Promise<void>;
40
43
  createSession: (creation: PrivateLocalSessionCreation) => Promise<{
41
44
  agentId: string;
@@ -59,6 +62,10 @@ onRecoveryDisabled?: ServiceLogLifecycle["recoveryDisabled"], configRoot?: Priva
59
62
  set: (preference: import("@fungi.computer/shiitake").ModelPreference | null) => Promise<void>;
60
63
  }>;
61
64
  read: (input?: import("@fungi.computer/shiitake").ReadOptions) => Promise<import("@fungi.computer/shiitake").SessionSnapshot>;
65
+ readSettlement: (commandId: string, options?: Readonly<{
66
+ timeoutMs?: number;
67
+ signal?: AbortSignal;
68
+ }>) => Promise<import("@fungi.computer/shiitake").CommandSettlement | undefined>;
62
69
  subscribe: (input: import("@fungi.computer/shiitake").SubscribeOptions) => AsyncIterable<import("@fungi.computer/shiitake").SessionFrame>;
63
70
  watch: (input?: import("@fungi.computer/shiitake").WatchOptions) => Promise<import("@fungi.computer/shiitake").SessionWatch>;
64
71
  }>;
@@ -87,6 +94,10 @@ onRecoveryDisabled?: ServiceLogLifecycle["recoveryDisabled"], configRoot?: Priva
87
94
  set: (preference: import("@fungi.computer/shiitake").ModelPreference | null) => Promise<void>;
88
95
  }>;
89
96
  read: (input?: import("@fungi.computer/shiitake").ReadOptions) => Promise<import("@fungi.computer/shiitake").SessionSnapshot>;
97
+ readSettlement: (commandId: string, options?: Readonly<{
98
+ timeoutMs?: number;
99
+ signal?: AbortSignal;
100
+ }>) => Promise<import("@fungi.computer/shiitake").CommandSettlement | undefined>;
90
101
  subscribe: (input: import("@fungi.computer/shiitake").SubscribeOptions) => AsyncIterable<import("@fungi.computer/shiitake").SessionFrame>;
91
102
  watch: (input?: import("@fungi.computer/shiitake").WatchOptions) => Promise<import("@fungi.computer/shiitake").SessionWatch>;
92
103
  }>;
@@ -108,15 +119,12 @@ onRecoveryDisabled?: ServiceLogLifecycle["recoveryDisabled"], configRoot?: Priva
108
119
  reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | undefined;
109
120
  }>;
110
121
  sandboxStatus: (selection: Pick<PrivateLocalServiceSelection, "invocationRoot" | "databasePath" | "fungiMcp">, signal: AbortSignal) => Promise<{
111
- state: "available";
112
- requireSandbox: boolean;
113
- } | {
114
- state: "unavailable";
115
- requireSandbox: boolean;
116
- reason: {
117
- kind: "missing" | "missing-artifact" | "invalid-digest" | "digest-mismatch" | "unreadable" | "not-executable" | "staging-failed" | "unsupported-platform" | "invalid-folder" | "unsafe-native-payload" | "unsafe-host-code" | "startup-refused" | "backend-refused";
122
+ mode: "read-only" | "workspace" | "full-access";
123
+ available: boolean;
124
+ reason?: {
125
+ kind: "install" | "unsupported" | "folder" | "startup-refused";
118
126
  message: string;
119
- };
127
+ } | undefined;
120
128
  }>;
121
129
  sweepIdle: () => Promise<void>;
122
130
  archive: (request: Readonly<{
@@ -1,5 +1,7 @@
1
1
  import * as Effect from "effect/Effect";
2
+ import { parseDelegationReceipt } from "@fungi.computer/shiitake/client";
2
3
  import { logEvent } from "./log-event.js";
4
+ import { modeWithinCeiling } from "./private-local-permission-mode.js";
3
5
  import { createPrivateModels } from "./local-models.js";
4
6
  import { capturePrivateLocalConfigRoot, } from "./private-local-config-root.js";
5
7
  import { AdmissionGate } from "./private-local-admission-gate.js";
@@ -19,7 +21,6 @@ import { createPrivateLocalSessionRequestId, openPrivateLocalRuntimeHost, resolv
19
21
  import { parsePrivateLocalExecPath } from "./private-local-exec-path.js";
20
22
  import { createLocalMeshLinks, localMeshLinkKey, } from "./private-local-mesh-links.js";
21
23
  import { agentMailAddressSchema, machineAgentRequestSchema, machineDirectorySchema, } from "@fungi.computer/seam/machine-link-protocol";
22
- import { parseAccepted } from "@fungi.computer/shiitake/client";
23
24
  import { commandSettlementSchema } from "@fungi.computer/shiitake/fleet";
24
25
  import { BirdDogMailSchema } from "@fungi.computer/bird-dog/effect";
25
26
  export class LocalSessionPathConflict extends Error {
@@ -44,7 +45,10 @@ export class LocalAgentSelectionAmbiguous extends Error {
44
45
  // Reopen inputs only. Credentials remain with Local's account custodian.
45
46
  const recoveryOwnerSchema = selectionSchema
46
47
  .omit({ clientPath: true, sessionId: true, uiAttachmentLifetime: true })
47
- .extend({ databasePath: z.string().min(1) });
48
+ .extend({
49
+ databasePath: z.string().min(1),
50
+ wakeAt: z.number().optional(),
51
+ });
48
52
  const decodeRecoveryFile = Schema.decodeUnknownResult(Schema.fromJsonString(Schema.Array(Schema.Unknown)));
49
53
  class LocalRecoveryPersistenceDisabled extends Error {
50
54
  reason;
@@ -65,12 +69,40 @@ idleSweepMs = 60_000,
65
69
  /** Read the recovery manifest; tests can hold this I/O independently of opens. */
66
70
  readRecoveryManifest = (path) => readFile(path, "utf8"),
67
71
  /** Report only the classification, never the manifest or exception text. */
68
- onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot()) {
72
+ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot(), meshLinksFactory = createLocalMeshLinks, onExecRuleMatch = () => undefined,
73
+ /** Supplied by the listener after startup; individual Agents never close it. */
74
+ serviceNative, onExecApprovalDecision = () => undefined) {
69
75
  const extensions = suppliedExtensions ?? localExtensions({ home: configRoot.home });
70
76
  const recoveryPath = recoveryDirectory === undefined
71
77
  ? undefined
72
78
  : resolve(recoveryDirectory, "owners.json");
73
79
  const recovery = new Map();
80
+ const wakes = new Map();
81
+ let wakeTimer;
82
+ function scheduleWakes() {
83
+ if (wakeTimer !== undefined)
84
+ clearTimeout(wakeTimer);
85
+ wakeTimer = undefined;
86
+ if (stopping || wakes.size === 0)
87
+ return;
88
+ let earliest = Infinity;
89
+ for (const entry of wakes.values())
90
+ earliest = Math.min(earliest, entry.wakeAt ?? Infinity);
91
+ wakeTimer = setTimeout(() => {
92
+ wakeTimer = undefined;
93
+ if (stopping)
94
+ return;
95
+ const due = [...wakes.values()].filter((entry) => entry.wakeAt !== undefined && entry.wakeAt <= Date.now());
96
+ for (const entry of due) {
97
+ wakes.delete(entry.databasePath);
98
+ void ownerFor(entry).catch((cause) => {
99
+ logEvent("service-owner.wake", cause);
100
+ });
101
+ }
102
+ scheduleWakes();
103
+ }, Math.min(2_147_483_647, Math.max(0, earliest - Date.now())));
104
+ wakeTimer.unref();
105
+ }
74
106
  let loadingRecovery;
75
107
  let saving = Promise.resolve();
76
108
  const emptySessions = new Map();
@@ -87,7 +119,7 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
87
119
  }
88
120
  return undefined;
89
121
  }
90
- const mesh = createLocalMeshLinks({
122
+ const mesh = meshLinksFactory({
91
123
  credentialPath: configRoot.credentialPath,
92
124
  receive: async (record, envelope) => {
93
125
  if (envelope.kind === "machineDirectory") {
@@ -142,13 +174,18 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
142
174
  })(envelope.event);
143
175
  if (envelope.kind !== "agentMail" ||
144
176
  envelope.key !== mail.key ||
145
- mail.to.agentId !== address.agentId ||
146
- new TextEncoder().encode(JSON.stringify(mail)).byteLength > 256 * 1024)
177
+ mail.to.agentId !== address.agentId)
147
178
  return "refused";
148
- // Only select the physical store here. Never rewrite frozen addresses
149
- // or bypass that runtime's own recipient/sender admission.
150
- const receipt = await admitted(owner, () => owner.acceptMail(mail));
151
- return { status: "accepted", receipt };
179
+ if (mail.from.sessionId === undefined &&
180
+ !(mail.replyTo.length === 0 && mail.reply?.outcome === "undeliverable"))
181
+ return "refused";
182
+ const sessionlessAcceptance = mail.to.sessionId === undefined;
183
+ // Only select the physical store here. Never bypass that runtime's
184
+ // recipient/sender admission; request keys make acceptance replay-safe.
185
+ const command = await admitted(owner, () => owner.acceptMail(mail));
186
+ if (sessionlessAcceptance && command.sessionId === undefined)
187
+ throw new Error("Sessionless mail acceptance returned no Session");
188
+ return { status: "accepted", receipt: command };
152
189
  }
153
190
  finally {
154
191
  release();
@@ -295,6 +332,8 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
295
332
  });
296
333
  pending = pending.then(async (owner) => {
297
334
  roots.set(owner.runtime.databasePath, invocationRoot);
335
+ if (wakes.delete(owner.runtime.databasePath))
336
+ scheduleWakes();
298
337
  if (recoveryPath !== undefined) {
299
338
  const reference = {
300
339
  invocationRoot,
@@ -523,9 +562,12 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
523
562
  }
524
563
  let runtime;
525
564
  let idle;
565
+ let wakeAt;
526
566
  try {
527
567
  ({ runtime } = await pending);
528
- idle = await runtime.idle();
568
+ const state = await runtime.idle();
569
+ idle = state.idle;
570
+ wakeAt = state.wakeAt;
529
571
  }
530
572
  catch (cause) {
531
573
  // One unreadable Agent stays open; the sweep still checks the rest.
@@ -542,7 +584,7 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
542
584
  idleCandidates.add(databasePath);
543
585
  continue;
544
586
  }
545
- closed.push(releaseStore(databasePath, pending, runtime));
587
+ closed.push(releaseStore(databasePath, pending, runtime, wakeAt));
546
588
  }
547
589
  await Promise.all(closed);
548
590
  })().finally(() => {
@@ -550,7 +592,18 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
550
592
  });
551
593
  return sweeping;
552
594
  }
553
- function releaseStore(databasePath, pending, runtime) {
595
+ /** Forget empty Agents; retain a future wake for the next service owner. */
596
+ function rememberIdleWake(databasePath, wakeAt) {
597
+ const reference = recovery.get(databasePath);
598
+ if (wakeAt === null || reference === undefined) {
599
+ recovery.delete(databasePath);
600
+ return undefined;
601
+ }
602
+ const scheduled = { ...reference, wakeAt };
603
+ recovery.set(databasePath, scheduled);
604
+ return scheduled;
605
+ }
606
+ function releaseStore(databasePath, pending, runtime, wakeAt) {
554
607
  idleCandidates.delete(databasePath);
555
608
  released.add(runtime);
556
609
  for (const [key, value] of owners)
@@ -558,7 +611,7 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
558
611
  owners.delete(key);
559
612
  emptySessions.delete(databasePath);
560
613
  roots.delete(databasePath);
561
- recovery.delete(databasePath);
614
+ const scheduled = rememberIdleWake(databasePath, wakeAt);
562
615
  const opened = openedByStore.get(databasePath);
563
616
  openedByStore.delete(databasePath);
564
617
  if (opened !== undefined)
@@ -570,6 +623,10 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
570
623
  catch (cause) {
571
624
  logEvent("service-owner.release-runtime", cause);
572
625
  }
626
+ if (scheduled !== undefined && !stopping) {
627
+ wakes.set(databasePath, scheduled);
628
+ scheduleWakes();
629
+ }
573
630
  await saveRecovery();
574
631
  })();
575
632
  releasing.set(databasePath, closed);
@@ -627,6 +684,16 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
627
684
  const invocationRoot = await realpath(selection.invocationRoot);
628
685
  const databasePath = await selectedDatabasePath(invocationRoot, selection.databasePath);
629
686
  ensureRunning();
687
+ if (selection.mode !== undefined && databasePath !== undefined) {
688
+ const child = readPrivateLocalAgentProfile(databasePath);
689
+ if (child.createdBy !== null) {
690
+ const stores = await listPrivateLocalAgentStores(invocationRoot);
691
+ const creatorPath = stores.find((path) => basename(dirname(path)) === child.createdBy);
692
+ if (creatorPath === undefined ||
693
+ !modeWithinCeiling(selection.mode, readPrivateLocalAgentProfile(creatorPath).mode))
694
+ throw new AgentCreateRefused("exceeds_creator", "a created Agent cannot exceed its creator's permission mode");
695
+ }
696
+ }
630
697
  const fungiMcp = selection.fungiMcp === undefined
631
698
  ? undefined
632
699
  : readFungiMcpSelection(selection.fungiMcp, {
@@ -662,11 +729,15 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
662
729
  invocationRoot,
663
730
  databasePath,
664
731
  // The host must persist/admit this BEFORE Shiitake can recover work.
665
- requireSandbox: selection.requireSandbox,
732
+ mode: selection.mode,
666
733
  agentControl: projectControl ? directory(invocationRoot) : undefined,
667
734
  mail: projectControl ? mailRouting(invocationRoot) : undefined,
668
735
  meshAdmission,
736
+ onExecRuleMatch,
737
+ onExecApprovalDecision,
669
738
  };
739
+ if (serviceNative !== undefined)
740
+ options = { ...options, native: serviceNative() };
670
741
  if (fungiMcp !== undefined)
671
742
  options = { ...options, fungiMcp };
672
743
  const captured = configuration;
@@ -683,8 +754,8 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
683
754
  fungiMcp !== undefined &&
684
755
  !sameFungiMcpConnection(owner.fungiMcp, fungiMcp))
685
756
  throw new FungiMcpSelectionConflict();
686
- if (!openingHere && selection.requireSandbox !== undefined)
687
- owner.runtime.setRequireSandbox(selection.requireSandbox);
757
+ if (!openingHere && selection.mode !== undefined)
758
+ owner.runtime.setMode(selection.mode);
688
759
  return owner.runtime;
689
760
  }
690
761
  async function select(selection, createRequestId) {
@@ -750,11 +821,15 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
750
821
  !("receipt" in result) ||
751
822
  result.status === "refused")
752
823
  throw new Error("Mesh recipient did not return command acceptance");
753
- return parseAccepted(JSON.stringify(result.receipt));
824
+ return parseDelegationReceipt(JSON.stringify(result.receipt));
754
825
  }
755
826
  const { owner, release } = await control.owner(mail.to.agentId, new AbortController().signal);
756
827
  try {
757
- return await admitted(owner, () => owner.acceptMail(mail));
828
+ const receipt = await admitted(owner, () => owner.acceptMail(mail));
829
+ if (mail.to.sessionId === undefined &&
830
+ receipt.sessionId === undefined)
831
+ throw new Error("Sessionless mail acceptance returned no Session");
832
+ return receipt;
758
833
  }
759
834
  finally {
760
835
  release();
@@ -935,7 +1010,7 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
935
1010
  if (creatorDatabase === undefined)
936
1011
  throw new AgentCreateRefused("no_creator", "the creating Agent is not in this Local project");
937
1012
  const creatorStore = dirname(creatorDatabase);
938
- const sandbox = readPrivateLocalAgentProfile(creatorDatabase);
1013
+ const creatorProfile = readPrivateLocalAgentProfile(creatorDatabase);
939
1014
  try {
940
1015
  await extensions.assertHeldByAgent(creatorStore, requested);
941
1016
  }
@@ -970,11 +1045,15 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
970
1045
  createRequestId: requestId,
971
1046
  name,
972
1047
  createdBy,
973
- requireSandbox: sandbox.requireSandbox,
974
- folder: sandbox.folder,
1048
+ mode: creatorProfile.mode,
1049
+ folder: creatorProfile.folder,
975
1050
  agentControl: directory(invocationRoot),
976
1051
  mail: mailRouting(invocationRoot),
1052
+ onExecRuleMatch,
1053
+ onExecApprovalDecision,
977
1054
  };
1055
+ if (serviceNative !== undefined)
1056
+ options = { ...options, native: serviceNative() };
978
1057
  if (model !== undefined)
979
1058
  options = {
980
1059
  ...options,
@@ -1028,7 +1107,8 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
1028
1107
  checkModel: async ({ agentId, sessionId }, signal) => {
1029
1108
  const { owner, release } = await admittedOwner(agentId, signal);
1030
1109
  try {
1031
- await owner.assertModelReady(sessionId, signal);
1110
+ if (sessionId !== undefined)
1111
+ await owner.assertModelReady(sessionId, signal);
1032
1112
  }
1033
1113
  finally {
1034
1114
  release();
@@ -1060,6 +1140,8 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
1060
1140
  function close() {
1061
1141
  stopping = true;
1062
1142
  clearInterval(sweepTimer);
1143
+ if (wakeTimer !== undefined)
1144
+ clearTimeout(wakeTimer);
1063
1145
  closing ??= (async () => {
1064
1146
  const meshResults = await Promise.allSettled([mesh.close()]);
1065
1147
  // End our observers before deciding which Agents have work to resume.
@@ -1098,8 +1180,11 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
1098
1180
  const databasePath = owner.runtime.databasePath;
1099
1181
  const unused = () => !uses.has(databasePath) && !gates.has(databasePath);
1100
1182
  try {
1101
- if (unused() && (await owner.runtime.idle()) && unused())
1102
- recovery.delete(databasePath);
1183
+ if (unused()) {
1184
+ const state = await owner.runtime.idle();
1185
+ if (state.idle && unused())
1186
+ rememberIdleWake(databasePath, state.wakeAt);
1187
+ }
1103
1188
  }
1104
1189
  catch (cause) {
1105
1190
  logEvent("service-owner.read-idle", cause);
@@ -1184,6 +1269,14 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
1184
1269
  for (const selection of loaded.references) {
1185
1270
  if (stopping)
1186
1271
  break;
1272
+ if (selection.wakeAt !== undefined && selection.wakeAt > Date.now()) {
1273
+ // A client may already have opened this store while the manifest was
1274
+ // loading. Its runtime now owns the wake; do not arm a second one.
1275
+ await Promise.allSettled(pendingOpenings);
1276
+ if (!owners.has(`store:${selection.databasePath}`))
1277
+ wakes.set(selection.databasePath, selection);
1278
+ continue;
1279
+ }
1187
1280
  try {
1188
1281
  await ownerFor(selection);
1189
1282
  }
@@ -1199,6 +1292,7 @@ onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot
1199
1292
  recovery.delete(selection.databasePath);
1200
1293
  }
1201
1294
  }
1295
+ scheduleWakes();
1202
1296
  await saveRecovery();
1203
1297
  }
1204
1298
  /** A person archives or restores any Agent of one project folder. */