@fungi.computer/truffle 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +282 -34
  2. package/dist/auth-cli.d.ts +10 -0
  3. package/dist/auth-cli.js +36 -14
  4. package/dist/fungi-account-command.d.ts +1 -1
  5. package/dist/fungi-account-command.js +6 -3
  6. package/dist/index.d.ts +10 -2
  7. package/dist/index.js +8 -2
  8. package/dist/internal/image-fit-metafile.json +106 -0
  9. package/dist/internal/local-image-fit-worker.mjs +960 -0
  10. package/dist/internal/private-workerd-sandbox-worker.mjs +11 -12
  11. package/dist/internal/sandbox-metafile.json +7 -7
  12. package/dist/local-archive-handoff.d.ts +12 -0
  13. package/dist/local-archive-handoff.js +21 -0
  14. package/dist/local-extensions.d.ts +58 -12
  15. package/dist/local-extensions.js +195 -55
  16. package/dist/local-image-fit.d.ts +30 -0
  17. package/dist/local-image-fit.js +105 -0
  18. package/dist/local-models.d.ts +4 -2
  19. package/dist/local-models.js +11 -6
  20. package/dist/local-session-archives.d.ts +8 -0
  21. package/dist/local-session-archives.js +152 -0
  22. package/dist/local-skill-source.d.ts +18 -3
  23. package/dist/local-skill-source.js +216 -140
  24. package/dist/log-event.d.ts +7 -0
  25. package/dist/log-event.js +8 -4
  26. package/dist/private-bounded-response.d.ts +2 -0
  27. package/dist/private-bounded-response.js +41 -0
  28. package/dist/private-file-credential-store.d.ts +28 -3
  29. package/dist/private-file-credential-store.js +57 -9
  30. package/dist/private-files-patch.d.ts +2 -1
  31. package/dist/private-files-patch.js +21 -3
  32. package/dist/private-fungi-account.d.ts +40 -23
  33. package/dist/private-fungi-account.js +201 -21
  34. package/dist/private-fungi-mcp-connection.d.ts +4 -0
  35. package/dist/private-fungi-mcp-connection.js +25 -6
  36. package/dist/private-fungi-mcp-selection-schema.d.ts +7 -0
  37. package/dist/private-fungi-mcp-selection-schema.js +15 -0
  38. package/dist/private-fungi-mcp-selection.d.ts +6 -6
  39. package/dist/private-fungi-mcp-selection.js +29 -16
  40. package/dist/private-fungi-model-api.js +4 -4
  41. package/dist/private-fungi-model-catalog.d.ts +10 -0
  42. package/dist/private-fungi-model-catalog.js +10 -2
  43. package/dist/private-fungi-model-provider.d.ts +1 -1
  44. package/dist/private-fungi-model-provider.js +6 -1
  45. package/dist/private-fungi-pending-logouts.d.ts +1 -1
  46. package/dist/private-fungi-pending-logouts.js +1 -2
  47. package/dist/private-fungi-service-logout.d.ts +44 -0
  48. package/dist/private-fungi-service-logout.js +59 -0
  49. package/dist/private-invocation-computer.d.ts +24 -3
  50. package/dist/private-invocation-computer.js +101 -62
  51. package/dist/private-invocation-files.d.ts +16 -1
  52. package/dist/private-invocation-files.js +154 -83
  53. package/dist/private-local-acp-stdio.js +3 -0
  54. package/dist/private-local-admission-gate.d.ts +14 -0
  55. package/dist/private-local-admission-gate.js +52 -0
  56. package/dist/private-local-agent-control.d.ts +101 -0
  57. package/dist/private-local-agent-control.js +30 -0
  58. package/dist/private-local-agent-default-model.d.ts +12 -0
  59. package/dist/private-local-agent-default-model.js +31 -0
  60. package/dist/private-local-auth-rpc.d.ts +67 -7
  61. package/dist/private-local-auth-rpc.js +140 -17
  62. package/dist/private-local-bwrap.d.ts +13 -0
  63. package/dist/private-local-bwrap.js +70 -0
  64. package/dist/private-local-client-version.d.ts +5 -0
  65. package/dist/private-local-client-version.js +43 -0
  66. package/dist/private-local-computer-attach.d.ts +3 -3
  67. package/dist/private-local-computer-attach.js +17 -3
  68. package/dist/private-local-computer-processes.d.ts +6 -3
  69. package/dist/private-local-computer-processes.js +55 -31
  70. package/dist/private-local-config-root.d.ts +21 -0
  71. package/dist/private-local-config-root.js +19 -0
  72. package/dist/private-local-containment.d.ts +51 -0
  73. package/dist/private-local-containment.js +161 -0
  74. package/dist/private-local-delegation.d.ts +10 -3
  75. package/dist/private-local-delegation.js +70 -15
  76. package/dist/private-local-effective-model.d.ts +9 -1
  77. package/dist/private-local-effective-model.js +3 -0
  78. package/dist/private-local-exec-environment.d.ts +16 -0
  79. package/dist/private-local-exec-environment.js +11 -0
  80. package/dist/private-local-linux-sandbox.d.ts +8 -0
  81. package/dist/private-local-linux-sandbox.js +67 -0
  82. package/dist/private-local-mail.d.ts +4 -2
  83. package/dist/private-local-mail.js +41 -33
  84. package/dist/private-local-mesh-enrollment.d.ts +34 -0
  85. package/dist/private-local-mesh-enrollment.js +188 -0
  86. package/dist/private-local-mesh-links.d.ts +21 -0
  87. package/dist/private-local-mesh-links.js +229 -0
  88. package/dist/private-local-mesh-record.d.ts +120 -0
  89. package/dist/private-local-mesh-record.js +55 -0
  90. package/dist/private-local-process-identity.d.ts +20 -0
  91. package/dist/private-local-process-identity.js +40 -0
  92. package/dist/private-local-runtime-host.d.ts +64 -13
  93. package/dist/private-local-runtime-host.js +361 -172
  94. package/dist/private-local-sandbox-policy.d.ts +21 -0
  95. package/dist/private-local-sandbox-policy.js +121 -0
  96. package/dist/private-local-sandbox.d.ts +37 -0
  97. package/dist/private-local-sandbox.js +114 -0
  98. package/dist/private-local-seatbelt.d.ts +24 -0
  99. package/dist/private-local-seatbelt.js +68 -0
  100. package/dist/private-local-service-contract.d.ts +91 -11
  101. package/dist/private-local-service-discovery.d.ts +129 -13
  102. package/dist/private-local-service-discovery.js +148 -47
  103. package/dist/private-local-service-lease.d.ts +16 -0
  104. package/dist/private-local-service-lease.js +105 -0
  105. package/dist/private-local-service-listener.d.ts +9 -2
  106. package/dist/private-local-service-listener.js +260 -64
  107. package/dist/private-local-service-log.d.ts +128 -0
  108. package/dist/private-local-service-log.js +321 -0
  109. package/dist/private-local-service-owner.d.ts +87 -1519
  110. package/dist/private-local-service-owner.js +1069 -71
  111. package/dist/private-local-service-process.js +3 -1
  112. package/dist/private-local-service-termination.d.ts +7 -0
  113. package/dist/private-local-service-termination.js +67 -0
  114. package/dist/private-local-service.d.ts +31 -2
  115. package/dist/private-local-service.js +402 -126
  116. package/dist/private-local-session-discovery.d.ts +52 -5
  117. package/dist/private-local-session-discovery.js +132 -11
  118. package/dist/private-local-shiitake-host.js +2 -2
  119. package/dist/private-local-startup-model.d.ts +51 -0
  120. package/dist/private-local-startup-model.js +86 -0
  121. package/dist/private-local-stop-all.d.ts +27 -0
  122. package/dist/private-local-stop-all.js +26 -0
  123. package/dist/private-local-ui-attachment.d.ts +19 -0
  124. package/dist/private-local-ui-attachment.js +71 -0
  125. package/dist/private-local-worker-server.d.ts +1 -0
  126. package/dist/private-local-worker-server.js +13 -5
  127. package/dist/private-mcp-form-contract.d.ts +186 -0
  128. package/dist/private-mcp-form-contract.js +53 -0
  129. package/dist/private-mcp-form-elicitation.d.ts +18 -0
  130. package/dist/private-mcp-form-elicitation.js +97 -0
  131. package/dist/private-mcp-form-session.d.ts +172 -0
  132. package/dist/private-mcp-form-session.js +148 -0
  133. package/dist/private-openai-claims.d.ts +30 -0
  134. package/dist/private-openai-claims.js +69 -0
  135. package/dist/private-oyster-mcp-auth.d.ts +13 -0
  136. package/dist/private-oyster-mcp-auth.js +57 -0
  137. package/dist/private-provider-rejection.d.ts +15 -0
  138. package/dist/private-provider-rejection.js +24 -0
  139. package/dist/private-sqlite-owner.d.ts +34 -2
  140. package/dist/private-sqlite-owner.js +103 -37
  141. package/dist/private-team-computers.d.ts +4 -4
  142. package/dist/private-team-computers.js +11 -2
  143. package/dist/seatbelt/safe-base.sbpl +79 -0
  144. package/dist/subscription-activation.d.ts +9 -0
  145. package/dist/subscription-activation.js +62 -0
  146. package/dist/subscription-identity.d.ts +23 -0
  147. package/dist/subscription-identity.js +124 -0
  148. package/dist/subscription-turn.d.ts +17 -0
  149. package/dist/subscription-turn.js +143 -0
  150. package/dist/team-apps.d.ts +2 -2
  151. package/dist/team-apps.js +7 -2
  152. package/native/bin/LICENSE.bubblewrap +502 -0
  153. package/native/bin/LICENSE.libcap +112 -0
  154. package/native/bin/LICENSE.sandbox-runtime +201 -0
  155. package/native/bin/SOURCE.bubblewrap.tar.xz +0 -0
  156. package/native/bin/bwrap-build-source/Dockerfile +48 -0
  157. package/native/bin/bwrap-build-source/README.md +96 -0
  158. package/native/bin/bwrap-build-source/build.mjs +52 -0
  159. package/native/bin/bwrap-build-source/linux-arm64.ini +14 -0
  160. package/native/bin/bwrap-build-source/pin.mjs +21 -0
  161. package/native/bin/bwrap-build-source/record.mjs +74 -0
  162. package/native/bin/bwrap-build-source/vendor/LICENSE.sandbox-runtime +201 -0
  163. package/native/bin/bwrap-build-source/vendor/README.md +23 -0
  164. package/native/bin/bwrap-build-source/vendor/seccomp-unix-block.c +280 -0
  165. package/native/bin/bwrap-build.json +44 -0
  166. package/native/bin/fungi-bwrap +0 -0
  167. package/native/bin/fungi-bwrap.sha256 +1 -0
  168. package/native/bin/fungi-seccomp.bpf +0 -0
  169. package/native/bin/fungi-seccomp.bpf.sha256 +1 -0
  170. package/package.json +44 -31
  171. package/dist/private-local-model-settings.d.ts +0 -5
  172. package/dist/private-local-model-settings.js +0 -31
  173. package/dist/private-local-subagents.d.ts +0 -27
  174. package/dist/private-local-subagents.js +0 -77
@@ -0,0 +1,21 @@
1
+ import { z } from "zod";
2
+ /** Retained POSIX folder grammar; no separate safe/YOLO representation. */
3
+ export declare const localAgentFolderSchema: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
4
+ /** Resolve stored folder authority before opening work, not after acquisition. */
5
+ export declare function resolveAgentFolder(project: string, folder: string): {
6
+ folder: string;
7
+ absolute: string;
8
+ };
9
+ /** Containment authority, not an approval setting or an execution mode. */
10
+ export type LocalSandboxPolicy = Readonly<{
11
+ writableRoot: string;
12
+ readOnly: readonly string[];
13
+ immovable: readonly string[];
14
+ }>;
15
+ /** One logical/resolved authority for Files, bwrap and Seatbelt. */
16
+ export declare function protectedSandboxPaths(paths: readonly string[]): string[];
17
+ /**
18
+ * Recompute for every command: the previous command may have initialized Git.
19
+ * An internal caller may tighten the directory budget, never widen the ceiling.
20
+ */
21
+ export declare function sandboxPolicy(project: string, folder: string, directoryLimit?: number): LocalSandboxPolicy;
@@ -0,0 +1,121 @@
1
+ import { lstatSync, readdirSync, realpathSync } from "node:fs";
2
+ import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep, } from "node:path";
3
+ import { z } from "zod";
4
+ /** Retained POSIX folder grammar; no separate safe/YOLO representation. */
5
+ export const localAgentFolderSchema = z
6
+ .string()
7
+ .min(1)
8
+ .refine((value) => !posix.isAbsolute(value) &&
9
+ !value.includes("\\") &&
10
+ !value.includes("\0") &&
11
+ !value.split("/").includes(".."), "Agent folder must be a project-relative POSIX path without '..'")
12
+ .transform((value) => posix.normalize(value));
13
+ /** Resolve stored folder authority before opening work, not after acquisition. */
14
+ export function resolveAgentFolder(project, folder) {
15
+ const root = realpathSync(project);
16
+ const absolute = realpathSync(resolve(root, localAgentFolderSchema.parse(folder)));
17
+ const within = relative(root, absolute);
18
+ if (isAbsolute(within) || within === ".." || within.startsWith(`..${sep}`))
19
+ throw new Error("Agent folder resolves outside the project");
20
+ if (!lstatSync(absolute).isDirectory())
21
+ throw new Error("Agent folder must be a directory");
22
+ return { folder: within.split(sep).join("/") || ".", absolute };
23
+ }
24
+ /** Missing reserved names resolve through their nearest existing ancestor. */
25
+ function canonicalPath(path) {
26
+ if (lstatSync(path, { throwIfNoEntry: false }) !== undefined)
27
+ return realpathSync(path);
28
+ return join(canonicalPath(dirname(path)), basename(path));
29
+ }
30
+ /** One logical/resolved authority for Files, bwrap and Seatbelt. */
31
+ export function protectedSandboxPaths(paths) {
32
+ return [
33
+ ...new Set(paths.flatMap((path) => {
34
+ if (!isAbsolute(path))
35
+ throw new Error("Sandbox protected paths must be absolute");
36
+ const logical = resolve(path);
37
+ return [logical, canonicalPath(logical)];
38
+ })),
39
+ ];
40
+ }
41
+ // A project tree is untrusted input. Refuse incomplete discovery rather than
42
+ // silently protecting only a prefix or allowing a bare command to bypass it.
43
+ // Bound directory traversal, not ordinary dependency-file payloads.
44
+ const maximumDiscoveryDirectories = 100_000;
45
+ const discoveryDirectoryLimit = z
46
+ .number()
47
+ .int()
48
+ .min(1)
49
+ .max(maximumDiscoveryDirectories);
50
+ function gitProtection(root, directoryLimit) {
51
+ const pending = [
52
+ { path: root, kind: "tree" },
53
+ ];
54
+ const readOnly = [];
55
+ const immovable = [];
56
+ let discoveredDirectories = 1; // The writable root itself.
57
+ for (let directory = pending.pop(); directory !== undefined; directory = pending.pop()) {
58
+ const entries = readdirSync(directory.path, { withFileTypes: true });
59
+ const has = (name) => entries.some((entry) => entry.name === name);
60
+ const git = directory.kind === "git" ||
61
+ basename(directory.path) === ".git" ||
62
+ (has("HEAD") && (has("objects") || has("commondir"))) ||
63
+ (directory.kind === "modules" &&
64
+ (has("HEAD") || has("config") || has("hooks")));
65
+ // A nested Git-file checkout is a repository boundary, not another copy of
66
+ // the selected project's payload to walk. The normal root scan owns common
67
+ // administration; never follow the pointer. Never prune the selected root or
68
+ // directories already recognized as Git administration.
69
+ if (directory.path !== root &&
70
+ !git &&
71
+ entries.some((entry) => entry.name === ".git" && entry.isFile())) {
72
+ readOnly.push(join(directory.path, ".git"));
73
+ continue;
74
+ }
75
+ if (git) {
76
+ immovable.push(directory.path);
77
+ readOnly.push(join(directory.path, "hooks"), join(directory.path, "config"), join(directory.path, "config.worktree"));
78
+ }
79
+ for (const entry of entries) {
80
+ const path = join(directory.path, entry.name);
81
+ if (entry.name === ".git" && !entry.isDirectory())
82
+ readOnly.push(path); // Never parse a Git pointer or grant an external dir.
83
+ if (!entry.isDirectory())
84
+ continue; // No symlink cycles or outside walks.
85
+ discoveredDirectories += 1;
86
+ if (discoveredDirectories > directoryLimit)
87
+ throw new Error(`Sandbox Git discovery exceeds ${String(directoryLimit)} directories`);
88
+ let kind = "tree";
89
+ if (git && entry.name === "modules")
90
+ kind = "modules";
91
+ else if (git && entry.name === "worktrees")
92
+ kind = "worktrees";
93
+ else if (!git && directory.kind === "modules")
94
+ kind = "modules";
95
+ else if (!git && directory.kind === "worktrees")
96
+ kind = "git";
97
+ pending.push({ path, kind });
98
+ }
99
+ }
100
+ return { readOnly, immovable };
101
+ }
102
+ /**
103
+ * Recompute for every command: the previous command may have initialized Git.
104
+ * An internal caller may tighten the directory budget, never widen the ceiling.
105
+ */
106
+ export function sandboxPolicy(project, folder, directoryLimit = maximumDiscoveryDirectories) {
107
+ const limit = discoveryDirectoryLimit.parse(directoryLimit);
108
+ const root = realpathSync(project);
109
+ const writableRoot = realpathSync(folder);
110
+ const within = relative(root, writableRoot);
111
+ if (isAbsolute(within) || within === ".." || within.startsWith(`..${sep}`))
112
+ throw new Error("Sandbox folder resolves outside the project.");
113
+ if (!lstatSync(writableRoot).isDirectory())
114
+ throw new Error("Sandbox folder must be a directory.");
115
+ const git = gitProtection(writableRoot, limit);
116
+ return {
117
+ writableRoot,
118
+ readOnly: [join(writableRoot, ".fungi"), ...git.readOnly],
119
+ immovable: git.immovable,
120
+ };
121
+ }
@@ -0,0 +1,37 @@
1
+ import { z } from "zod";
2
+ import type { LocalSandboxWrites } from "./private-invocation-files.js";
3
+ /** Availability is not a hostile-program security or an approval status. */
4
+ export declare const sandboxStatusSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
5
+ state: z.ZodLiteral<"available">;
6
+ requireSandbox: z.ZodBoolean;
7
+ }, z.core.$strict>, z.ZodObject<{
8
+ state: z.ZodLiteral<"unavailable">;
9
+ requireSandbox: z.ZodBoolean;
10
+ reason: z.ZodObject<{
11
+ kind: z.ZodEnum<{
12
+ missing: "missing";
13
+ "invalid-digest": "invalid-digest";
14
+ "digest-mismatch": "digest-mismatch";
15
+ "not-executable": "not-executable";
16
+ "unsupported-platform": "unsupported-platform";
17
+ "invalid-folder": "invalid-folder";
18
+ "backend-refused": "backend-refused";
19
+ }>;
20
+ message: z.ZodString;
21
+ }, z.core.$strict>;
22
+ }, z.core.$strict>], "state">;
23
+ export type LocalSandboxStatus = z.output<typeof sandboxStatusSchema>;
24
+ export type LocalSandboxControls = LocalSandboxWrites & Readonly<{
25
+ assertReady(): void;
26
+ status(): LocalSandboxStatus;
27
+ close(): void;
28
+ }>;
29
+ /**
30
+ * Native Local owns the live requirement; the adapter owns only preparation.
31
+ * Every consumer calls this authority immediately before launching or writing.
32
+ */
33
+ export declare function createLocalSandboxControls(input: Readonly<{
34
+ project: string;
35
+ folder: string;
36
+ required(): boolean;
37
+ }>): LocalSandboxControls;
@@ -0,0 +1,114 @@
1
+ import { isAbsolute, relative, resolve, sep } from "node:path";
2
+ import { z } from "zod";
3
+ import { establishLocalSandbox, LocalSandboxUnavailable, sandboxUnavailableReasonSchema, } from "./private-local-containment.js";
4
+ import { protectedSandboxPaths, resolveAgentFolder, sandboxPolicy, } from "./private-local-sandbox-policy.js";
5
+ /** Availability is not a hostile-program security or an approval status. */
6
+ export const sandboxStatusSchema = z.discriminatedUnion("state", [
7
+ z.strictObject({
8
+ state: z.literal("available"),
9
+ requireSandbox: z.boolean(),
10
+ }),
11
+ z.strictObject({
12
+ state: z.literal("unavailable"),
13
+ requireSandbox: z.boolean(),
14
+ reason: sandboxUnavailableReasonSchema,
15
+ }),
16
+ ]);
17
+ function contains(root, path) {
18
+ const within = relative(root, path);
19
+ return (!isAbsolute(within) && within !== ".." && !within.startsWith(`..${sep}`));
20
+ }
21
+ /**
22
+ * Native Local owns the live requirement; the adapter owns only preparation.
23
+ * Every consumer calls this authority immediately before launching or writing.
24
+ */
25
+ export function createLocalSandboxControls(input) {
26
+ const required = input.required();
27
+ const folder = resolveAgentFolder(input.project, input.folder);
28
+ const initial = establishLocalSandbox({
29
+ project: input.project,
30
+ folder: folder.absolute,
31
+ });
32
+ let sandbox = initial.state === "ready" ? initial.sandbox : undefined;
33
+ let unavailable = initial.state === "unavailable" ? initial.reason : undefined;
34
+ if (unavailable !== undefined) {
35
+ if (unavailable.kind === "invalid-folder")
36
+ throw new Error(unavailable.message);
37
+ if (required)
38
+ throw new LocalSandboxUnavailable(unavailable);
39
+ }
40
+ let closed = false;
41
+ const assertFolder = () => {
42
+ if (closed)
43
+ throw new Error("Local sandbox owner is closed");
44
+ const current = resolveAgentFolder(input.project, input.folder);
45
+ if (current.absolute !== folder.absolute)
46
+ throw new Error("Local Agent folder authority changed while open");
47
+ };
48
+ const launch = (cwd, argv) => {
49
+ assertFolder();
50
+ if (sandbox === undefined) {
51
+ const admission = establishLocalSandbox({
52
+ project: input.project,
53
+ folder: folder.absolute,
54
+ });
55
+ if (admission.state === "ready")
56
+ sandbox = admission.sandbox;
57
+ else
58
+ unavailable = admission.reason;
59
+ }
60
+ if (sandbox !== undefined) {
61
+ try {
62
+ const command = sandbox.command(cwd, argv);
63
+ unavailable = undefined;
64
+ return command;
65
+ }
66
+ catch (cause) {
67
+ if (!(cause instanceof LocalSandboxUnavailable))
68
+ throw cause;
69
+ unavailable = cause.reason;
70
+ }
71
+ }
72
+ if (unavailable === undefined)
73
+ throw new Error("Local sandbox preparation produced no result");
74
+ if (unavailable.kind === "invalid-folder")
75
+ throw new Error(unavailable.message);
76
+ if (input.required())
77
+ throw new LocalSandboxUnavailable(unavailable);
78
+ // No executable has started. Only this owner admits the bare fallback.
79
+ return undefined;
80
+ };
81
+ const assertReady = () => {
82
+ launch(folder.absolute, ["/usr/bin/true"]);
83
+ };
84
+ return {
85
+ launch,
86
+ assertReady,
87
+ assertWrite: (path) => {
88
+ assertReady();
89
+ const target = resolve(path);
90
+ const policy = sandboxPolicy(input.project, folder.absolute);
91
+ if (!contains(policy.writableRoot, target) ||
92
+ protectedSandboxPaths(policy.readOnly).some((protectedPath) => contains(protectedPath, target)) ||
93
+ protectedSandboxPaths(policy.immovable).includes(target))
94
+ throw new Error("Files write targets a protected Local path");
95
+ },
96
+ status: () => {
97
+ if (closed)
98
+ throw new Error("Local sandbox owner is closed");
99
+ return unavailable === undefined
100
+ ? { state: "available", requireSandbox: input.required() }
101
+ : {
102
+ state: "unavailable",
103
+ requireSandbox: input.required(),
104
+ reason: unavailable,
105
+ };
106
+ },
107
+ close: () => {
108
+ if (closed)
109
+ return;
110
+ closed = true;
111
+ sandbox?.close();
112
+ },
113
+ };
114
+ }
@@ -0,0 +1,24 @@
1
+ import { type LocalSandboxPolicy } from "./private-local-sandbox-policy.js";
2
+ /**
3
+ * The containment owner supplies temporary-directory lifetime and command-time
4
+ * protected paths; this adapter owns only the maintained Seatbelt translation.
5
+ */
6
+ export type SeatbeltPolicy = LocalSandboxPolicy & Readonly<{
7
+ temporaryDirectory: string;
8
+ }>;
9
+ /**
10
+ * Paths are argv definitions, never SBPL source. Both the logical name and its
11
+ * resolved target are protected, including when a protected entry is a symlink.
12
+ */
13
+ export declare function seatbeltCommand(policy: SeatbeltPolicy, argv: readonly [string, ...string[]]): {
14
+ executable: string;
15
+ args: string[];
16
+ temporaryDirectory: string;
17
+ };
18
+ /** A real startup probe; failure is data, never permission to execute bare. */
19
+ export declare function seatbeltReadiness(policy: SeatbeltPolicy): Readonly<{
20
+ ready: true;
21
+ }> | Readonly<{
22
+ ready: false;
23
+ reason: string;
24
+ }>;
@@ -0,0 +1,68 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { readFileSync, realpathSync } from "node:fs";
3
+ import { protectedSandboxPaths, } from "./private-local-sandbox-policy.js";
4
+ const sandboxExecutable = "/usr/bin/sandbox-exec";
5
+ /**
6
+ * Paths are argv definitions, never SBPL source. Both the logical name and its
7
+ * resolved target are protected, including when a protected entry is a symlink.
8
+ */
9
+ export function seatbeltCommand(policy, argv) {
10
+ const definitions = [];
11
+ const parameter = (name, path) => {
12
+ definitions.push("-D", `${name}=${path}`);
13
+ return `(param "${name}")`;
14
+ };
15
+ const excluded = protectedSandboxPaths(policy.readOnly);
16
+ const carveouts = excluded.flatMap((path, index) => {
17
+ const value = parameter(`PROTECTED_${String(index)}`, path);
18
+ return [
19
+ `(require-not (literal ${value}))`,
20
+ `(require-not (subpath ${value}))`,
21
+ ];
22
+ });
23
+ const roots = [policy.writableRoot, policy.temporaryDirectory];
24
+ const grants = roots.map((path, index) => {
25
+ const value = parameter(`WRITABLE_ROOT_${String(index)}`, realpathSync(path));
26
+ return `(require-all (subpath ${value}) ${carveouts.join(" ")})`;
27
+ });
28
+ const anchors = [
29
+ ...new Set([
30
+ ...roots.map((path) => realpathSync(path)),
31
+ ...protectedSandboxPaths(policy.immovable),
32
+ ]),
33
+ ].map((path, index) => {
34
+ const value = parameter(`ANCHOR_${String(index)}`, path);
35
+ return `(deny file-write-unlink (require-all (vnode-type DIRECTORY) (literal ${value})))`;
36
+ });
37
+ const base = readFileSync(new URL("./seatbelt/safe-base.sbpl", import.meta.url), "utf8");
38
+ const profile = [
39
+ base,
40
+ "(allow file-read*)",
41
+ `(allow file-write* ${grants.join(" ")})`,
42
+ ...anchors,
43
+ "(deny system-fcntl (fcntl-command 80 110))",
44
+ ].join("\n");
45
+ return {
46
+ executable: sandboxExecutable,
47
+ args: ["-p", profile, ...definitions, "--", ...argv],
48
+ temporaryDirectory: realpathSync(policy.temporaryDirectory),
49
+ };
50
+ }
51
+ /** A real startup probe; failure is data, never permission to execute bare. */
52
+ export function seatbeltReadiness(policy) {
53
+ const command = seatbeltCommand(policy, ["/usr/bin/true"]);
54
+ const result = spawnSync(command.executable, command.args, {
55
+ env: { TMPDIR: command.temporaryDirectory },
56
+ encoding: "utf8",
57
+ timeout: 5_000,
58
+ });
59
+ const diagnostic = result.error?.message ?? result.stderr.trim();
60
+ return result.status === 0
61
+ ? { ready: true }
62
+ : {
63
+ ready: false,
64
+ reason: diagnostic.length > 0
65
+ ? diagnostic
66
+ : `sandbox-exec exited ${String(result.status)} (${String(result.signal)})`,
67
+ };
68
+ }
@@ -1,19 +1,29 @@
1
- import type { PrivateFungiMcpSelection } from "./private-fungi-mcp-selection.js";
1
+ import type { PrivateFungiMcpSelection } from "./private-fungi-mcp-selection-schema.js";
2
2
  import type { AuthInteraction } from "@earendil-works/pi-ai";
3
3
  import type { PrivateLocalAuthRequest } from "./private-local-auth-rpc.js";
4
4
  import type { LocalAuthResult } from "./auth-cli.js";
5
5
  import type { SessionSnapshot, ShiitakeAgent, ShiitakeSession } from "@fungi.computer/shiitake";
6
6
  import type { PrivateLocalModelChoice } from "./local-models.js";
7
7
  import type { PrivateLocalEffectiveModel } from "./private-local-effective-model.js";
8
- /** Service identity is checked by handshake, never inferred from a discovery PID. */
8
+ import type { StopAllResult } from "./private-local-stop-all.js";
9
+ import type { ServiceDiagnosticsStatus } from "./private-local-service-log.js";
10
+ import type { LocalSandboxStatus } from "./private-local-sandbox.js";
11
+ import type { LocalUiAttachmentLifetime } from "./private-local-ui-attachment.js";
12
+ /** Live health, not the on-disk identity record or a cached connection snapshot. */
9
13
  export type PrivateLocalServiceStatus = Readonly<{
14
+ /** The client-service wire contract this service speaks. */
15
+ protocol: number;
16
+ /** The build: the packed CLI's content hash, or `source`. */
17
+ version: string;
10
18
  serviceId: string;
19
+ pid: number;
11
20
  baseUrl: string;
12
- /** Captured from the packed CLI at service launch. */
13
- buildId?: string | undefined;
21
+ diagnostics: ServiceDiagnosticsStatus;
14
22
  }>;
15
- export type PrivateLocalServiceStopResult = Readonly<{
16
- kind: "stopped";
23
+ export type PrivateLocalServiceStopResult =
24
+ /** `killed`: it did not exit within five seconds of SIGTERM. */
25
+ Readonly<{
26
+ kind: "stopped" | "killed";
17
27
  }> | Readonly<{
18
28
  kind: "not-running";
19
29
  recordCleaned: boolean;
@@ -23,14 +33,20 @@ export type PrivateLocalServiceStopResult = Readonly<{
23
33
  /** Selection addresses an existing Agent store; Session identity is not a new store. */
24
34
  export type PrivateLocalServiceSelection = Readonly<{
25
35
  invocationRoot: string;
36
+ /** TUI/form watches own their attachment; ACP retains it until connection close. */
37
+ uiAttachmentLifetime?: LocalUiAttachmentLifetime | undefined;
26
38
  clientPath?: string | undefined;
39
+ /** Person-selected sandbox requirement, not an approval setting. */
40
+ requireSandbox?: boolean | undefined;
27
41
  databasePath?: string | undefined;
28
42
  sessionId?: string | undefined;
29
43
  fungiMcp?: PrivateFungiMcpSelection | undefined;
30
44
  }>;
31
45
  export type PrivateLocalSessionCreation = Readonly<{
32
46
  invocationRoot: string;
47
+ uiAttachmentLifetime?: LocalUiAttachmentLifetime | undefined;
33
48
  clientPath?: string | undefined;
49
+ requireSandbox?: boolean | undefined;
34
50
  databasePath: string;
35
51
  requestId: string;
36
52
  fungiMcp?: PrivateFungiMcpSelection | undefined;
@@ -38,22 +54,54 @@ export type PrivateLocalSessionCreation = Readonly<{
38
54
  export type PrivateLocalServiceRequestOptions = Readonly<{
39
55
  signal?: AbortSignal | undefined;
40
56
  }>;
57
+ /** Discovery lists the Agents in use; `archived` lists only the archived ones. */
58
+ export type PrivateLocalDiscoveryOptions = PrivateLocalServiceRequestOptions & Readonly<{
59
+ archived?: boolean | undefined;
60
+ }>;
61
+ /** One human request to archive or restore an Agent of this folder. */
62
+ export type PrivateLocalArchiveRequest = Readonly<{
63
+ invocationRoot: string;
64
+ agentId: string;
65
+ archived: boolean;
66
+ }>;
41
67
  export type PrivateLocalSessionInspection = Readonly<{
68
+ diagnostics: ServiceDiagnosticsStatus;
42
69
  session: ShiitakeSession;
43
70
  snapshot: SessionSnapshot;
44
71
  close: () => Promise<void>;
45
72
  }>;
46
73
  export type PrivateLocalServiceConnection = Readonly<{
74
+ /** The service this connection currently talks to; a rebind replaces it. */
47
75
  service: PrivateLocalServiceStatus;
48
76
  agentId: string;
49
77
  databasePath: string;
50
78
  sessionId: string;
51
79
  client: ShiitakeAgent;
52
80
  session: ShiitakeSession;
81
+ /** Stop this Agent and its made-by descendants without retiring their chats. */
82
+ stopAll: () => Promise<StopAllResult>;
53
83
  effectiveModel: (options?: PrivateLocalServiceRequestOptions) => Promise<PrivateLocalEffectiveModel>;
54
- /** Releases this client's watches only. Work and service ownership remain alive. */
84
+ sandboxStatus: (options?: PrivateLocalServiceRequestOptions) => Promise<LocalSandboxStatus>;
85
+ /**
86
+ * Point this connection at the service now named by service.json, which may
87
+ * have a new port and id after a restart, and select the same Session
88
+ * there. The Session handle stays valid; observations on the old service
89
+ * must be reopened. Never starts a service.
90
+ */
91
+ rebind: (options?: PrivateLocalServiceRequestOptions) => Promise<PrivateLocalServiceRebind>;
92
+ /**
93
+ * Close observations and release this client's UI attachment. Other work and
94
+ * service ownership stay alive; unanswered forms lose this UI's authority.
95
+ */
55
96
  close: () => Promise<void>;
56
97
  }>;
98
+ /** `unavailable`: service.json names no service that answers. */
99
+ export type PrivateLocalServiceRebind = Readonly<{
100
+ kind: "rebound";
101
+ service: PrivateLocalServiceStatus;
102
+ }> | Readonly<{
103
+ kind: "unavailable";
104
+ }>;
57
105
  export type PrivateLocalSessionChoice = Readonly<{
58
106
  databasePath: string;
59
107
  sessionId: string;
@@ -63,28 +111,60 @@ export type PrivateLocalSessionChoice = Readonly<{
63
111
  /** Reopen this saved store through the persistent service connection. */
64
112
  resume: (options?: PrivateLocalServiceRequestOptions & {
65
113
  fungiMcp?: PrivateFungiMcpSelection | undefined;
114
+ requireSandbox?: boolean | undefined;
66
115
  }) => Promise<PrivateLocalServiceConnection>;
67
116
  }>;
68
117
  export type PrivateLocalSessionProblem = Readonly<{
69
118
  kind: "corrupt" | "in_use" | "unreadable";
70
119
  }>;
120
+ /** One saved Agent store in the folder, whether or not it has conversations yet. */
121
+ export type PrivateLocalAgentChoice = Readonly<{
122
+ agentId: string;
123
+ /** Why this Agent's store cannot be read (it predates the current schema); set, the row shows it. */
124
+ unreadable?: string | undefined;
125
+ /** The Agent's chosen display name; absent while its store is unreadable. */
126
+ name?: string | undefined;
127
+ /** Creating Agent id; null for a human-created Agent, absent if unreadable. */
128
+ createdBy?: string | null | undefined;
129
+ databasePath: string;
130
+ sessionCount: number;
131
+ /** Latest Session activity; absent while the Agent has no readable Session. */
132
+ updatedAt?: number | undefined;
133
+ }>;
71
134
  export type PrivateLocalSessionDiscovery = Readonly<{
72
- agentCount: number;
135
+ agents: readonly PrivateLocalAgentChoice[];
73
136
  sessions: readonly PrivateLocalSessionChoice[];
74
137
  problems: readonly PrivateLocalSessionProblem[];
75
138
  }>;
76
139
  /** Local process management stays outside the shared Session HTTP contract. */
77
140
  export type PrivateLocalServiceManager = Readonly<{
141
+ reload: (invocationRoot: string) => Promise<string>;
142
+ /** Throws `LocalServiceUpdated` when a live service speaks another protocol. */
78
143
  status: () => Promise<PrivateLocalServiceStatus | undefined>;
144
+ /**
145
+ * Ensure a service of this build: reuse a match, otherwise stop the
146
+ * registered one and start this build's. Once this client has reached a
147
+ * service it keeps any service of its protocol and throws
148
+ * `LocalServiceUpdated` for another protocol. Every request starts here.
149
+ */
79
150
  start: (options?: PrivateLocalServiceRequestOptions) => Promise<PrivateLocalServiceStatus>;
80
151
  createSession: (creation: PrivateLocalSessionCreation, options?: PrivateLocalServiceRequestOptions) => Promise<PrivateLocalServiceConnection>;
81
152
  connect: (selection: PrivateLocalServiceSelection, options?: PrivateLocalServiceRequestOptions) => Promise<PrivateLocalServiceConnection>;
82
- discover: (invocationRoot: string, options?: PrivateLocalServiceRequestOptions) => Promise<PrivateLocalSessionDiscovery>;
83
- inspect: (selection: Omit<PrivateLocalServiceSelection, "clientPath">, options?: PrivateLocalServiceRequestOptions & Readonly<{
153
+ discover: (invocationRoot: string, options?: PrivateLocalDiscoveryOptions) => Promise<PrivateLocalSessionDiscovery>;
154
+ /**
155
+ * Archive or restore any Agent of the folder; refused with
156
+ * `AgentArchiveRefused` while it has a running command. Nothing is deleted.
157
+ */
158
+ archive: (request: PrivateLocalArchiveRequest, options?: PrivateLocalServiceRequestOptions) => Promise<void>;
159
+ inspect: (selection: Omit<PrivateLocalServiceSelection, "clientPath" | "uiAttachmentLifetime">, options?: PrivateLocalServiceRequestOptions & Readonly<{
84
160
  limit?: number | undefined;
85
161
  }>) => Promise<PrivateLocalSessionInspection>;
86
162
  auth: (request: PrivateLocalAuthRequest, interaction: AuthInteraction) => Promise<LocalAuthResult>;
87
163
  models: (options?: PrivateLocalServiceRequestOptions) => Promise<readonly PrivateLocalModelChoice[]>;
88
- /** An expected identity avoids stopping a replacement started by another client. */
164
+ /**
165
+ * SIGTERM, then SIGKILL after five seconds; the record and owner lock are
166
+ * cleared either way. An expected identity avoids stopping a replacement
167
+ * started by another client.
168
+ */
89
169
  stop: (expectedServiceId?: string) => Promise<PrivateLocalServiceStopResult>;
90
170
  }>;