@fungi.computer/truffle 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +542 -2
  3. package/computerd-loopback.cjs +16 -0
  4. package/dist/auth-cli.d.ts +46 -0
  5. package/dist/auth-cli.js +373 -0
  6. package/dist/fungi-account-command.d.ts +18 -0
  7. package/dist/fungi-account-command.js +95 -0
  8. package/dist/index.d.ts +27 -0
  9. package/dist/index.js +23 -0
  10. package/dist/internal/image-fit-metafile.json +106 -0
  11. package/dist/internal/local-image-fit-worker.mjs +960 -0
  12. package/dist/internal/private-workerd-sandbox-worker.mjs +27487 -0
  13. package/dist/internal/sandbox-metafile.json +6726 -0
  14. package/dist/linear-auth.d.ts +4 -0
  15. package/dist/linear-auth.js +72 -0
  16. package/dist/local-archive-handoff.d.ts +12 -0
  17. package/dist/local-archive-handoff.js +21 -0
  18. package/dist/local-extensions.d.ts +143 -0
  19. package/dist/local-extensions.js +624 -0
  20. package/dist/local-image-fit.d.ts +30 -0
  21. package/dist/local-image-fit.js +105 -0
  22. package/dist/local-login.d.ts +55 -0
  23. package/dist/local-login.js +167 -0
  24. package/dist/local-models.d.ts +16 -0
  25. package/dist/local-models.js +82 -0
  26. package/dist/local-session-archives.d.ts +8 -0
  27. package/dist/local-session-archives.js +152 -0
  28. package/dist/local-skill-source.d.ts +21 -0
  29. package/dist/local-skill-source.js +433 -0
  30. package/dist/log-event.d.ts +9 -0
  31. package/dist/log-event.js +13 -0
  32. package/dist/private-bounded-response.d.ts +2 -0
  33. package/dist/private-bounded-response.js +41 -0
  34. package/dist/private-bun-shiitake-host.d.ts +11 -0
  35. package/dist/private-bun-shiitake-host.js +66 -0
  36. package/dist/private-file-credential-store.d.ts +58 -0
  37. package/dist/private-file-credential-store.js +331 -0
  38. package/dist/private-files-patch.d.ts +4 -0
  39. package/dist/private-files-patch.js +104 -0
  40. package/dist/private-fungi-account.d.ts +252 -0
  41. package/dist/private-fungi-account.js +1329 -0
  42. package/dist/private-fungi-auth-error.d.ts +5 -0
  43. package/dist/private-fungi-auth-error.js +7 -0
  44. package/dist/private-fungi-mcp-connection.d.ts +26 -0
  45. package/dist/private-fungi-mcp-connection.js +162 -0
  46. package/dist/private-fungi-mcp-endpoint.d.ts +2 -0
  47. package/dist/private-fungi-mcp-endpoint.js +15 -0
  48. package/dist/private-fungi-mcp-selection-schema.d.ts +7 -0
  49. package/dist/private-fungi-mcp-selection-schema.js +15 -0
  50. package/dist/private-fungi-mcp-selection.d.ts +14 -0
  51. package/dist/private-fungi-mcp-selection.js +49 -0
  52. package/dist/private-fungi-model-api.d.ts +6 -0
  53. package/dist/private-fungi-model-api.js +125 -0
  54. package/dist/private-fungi-model-catalog.d.ts +16 -0
  55. package/dist/private-fungi-model-catalog.js +86 -0
  56. package/dist/private-fungi-model-provider.d.ts +6 -0
  57. package/dist/private-fungi-model-provider.js +51 -0
  58. package/dist/private-fungi-pending-logouts.d.ts +34 -0
  59. package/dist/private-fungi-pending-logouts.js +135 -0
  60. package/dist/private-fungi-service-logout.d.ts +44 -0
  61. package/dist/private-fungi-service-logout.js +59 -0
  62. package/dist/private-invocation-computer.d.ts +58 -0
  63. package/dist/private-invocation-computer.js +498 -0
  64. package/dist/private-invocation-files.d.ts +30 -0
  65. package/dist/private-invocation-files.js +520 -0
  66. package/dist/private-local-acp-stdio.d.ts +17 -0
  67. package/dist/private-local-acp-stdio.js +145 -0
  68. package/dist/private-local-admission-gate.d.ts +14 -0
  69. package/dist/private-local-admission-gate.js +52 -0
  70. package/dist/private-local-agent-control.d.ts +101 -0
  71. package/dist/private-local-agent-control.js +30 -0
  72. package/dist/private-local-agent-default-model.d.ts +12 -0
  73. package/dist/private-local-agent-default-model.js +31 -0
  74. package/dist/private-local-auth-rpc.d.ts +129 -0
  75. package/dist/private-local-auth-rpc.js +396 -0
  76. package/dist/private-local-bwrap.d.ts +13 -0
  77. package/dist/private-local-bwrap.js +70 -0
  78. package/dist/private-local-client-version.d.ts +5 -0
  79. package/dist/private-local-client-version.js +43 -0
  80. package/dist/private-local-computer-attach.d.ts +49 -0
  81. package/dist/private-local-computer-attach.js +230 -0
  82. package/dist/private-local-computer-backend.d.ts +282 -0
  83. package/dist/private-local-computer-backend.js +402 -0
  84. package/dist/private-local-computer-processes.d.ts +202 -0
  85. package/dist/private-local-computer-processes.js +613 -0
  86. package/dist/private-local-config-root.d.ts +21 -0
  87. package/dist/private-local-config-root.js +19 -0
  88. package/dist/private-local-containment.d.ts +51 -0
  89. package/dist/private-local-containment.js +161 -0
  90. package/dist/private-local-default-model.d.ts +11 -0
  91. package/dist/private-local-default-model.js +45 -0
  92. package/dist/private-local-delegation.d.ts +36 -0
  93. package/dist/private-local-delegation.js +133 -0
  94. package/dist/private-local-effective-model.d.ts +27 -0
  95. package/dist/private-local-effective-model.js +17 -0
  96. package/dist/private-local-exec-environment.d.ts +16 -0
  97. package/dist/private-local-exec-environment.js +11 -0
  98. package/dist/private-local-exec-launch.d.ts +41 -0
  99. package/dist/private-local-exec-launch.js +51 -0
  100. package/dist/private-local-exec-path.d.ts +6 -0
  101. package/dist/private-local-exec-path.js +40 -0
  102. package/dist/private-local-git-exclude.d.ts +2 -0
  103. package/dist/private-local-git-exclude.js +33 -0
  104. package/dist/private-local-http-dispatch.d.ts +5 -0
  105. package/dist/private-local-http-dispatch.js +14 -0
  106. package/dist/private-local-linux-sandbox.d.ts +8 -0
  107. package/dist/private-local-linux-sandbox.js +67 -0
  108. package/dist/private-local-mail.d.ts +13 -0
  109. package/dist/private-local-mail.js +77 -0
  110. package/dist/private-local-mesh-enrollment.d.ts +34 -0
  111. package/dist/private-local-mesh-enrollment.js +188 -0
  112. package/dist/private-local-mesh-links.d.ts +21 -0
  113. package/dist/private-local-mesh-links.js +229 -0
  114. package/dist/private-local-mesh-record.d.ts +120 -0
  115. package/dist/private-local-mesh-record.js +55 -0
  116. package/dist/private-local-process-identity.d.ts +20 -0
  117. package/dist/private-local-process-identity.js +40 -0
  118. package/dist/private-local-runtime-host.d.ts +192 -0
  119. package/dist/private-local-runtime-host.js +1177 -0
  120. package/dist/private-local-sandbox-policy.d.ts +21 -0
  121. package/dist/private-local-sandbox-policy.js +121 -0
  122. package/dist/private-local-sandbox.d.ts +37 -0
  123. package/dist/private-local-sandbox.js +114 -0
  124. package/dist/private-local-seatbelt.d.ts +24 -0
  125. package/dist/private-local-seatbelt.js +68 -0
  126. package/dist/private-local-service-contract.d.ts +170 -0
  127. package/dist/private-local-service-contract.js +1 -0
  128. package/dist/private-local-service-discovery.d.ts +217 -0
  129. package/dist/private-local-service-discovery.js +322 -0
  130. package/dist/private-local-service-ids.d.ts +3 -0
  131. package/dist/private-local-service-ids.js +8 -0
  132. package/dist/private-local-service-lease.d.ts +16 -0
  133. package/dist/private-local-service-lease.js +105 -0
  134. package/dist/private-local-service-listener.d.ts +22 -0
  135. package/dist/private-local-service-listener.js +407 -0
  136. package/dist/private-local-service-log.d.ts +128 -0
  137. package/dist/private-local-service-log.js +321 -0
  138. package/dist/private-local-service-owner.d.ts +135 -0
  139. package/dist/private-local-service-owner.js +1369 -0
  140. package/dist/private-local-service-process.d.ts +1 -0
  141. package/dist/private-local-service-process.js +8 -0
  142. package/dist/private-local-service-termination.d.ts +7 -0
  143. package/dist/private-local-service-termination.js +67 -0
  144. package/dist/private-local-service.d.ts +46 -0
  145. package/dist/private-local-service.js +606 -0
  146. package/dist/private-local-session-discovery.d.ts +66 -0
  147. package/dist/private-local-session-discovery.js +278 -0
  148. package/dist/private-local-shiitake-host.d.ts +44 -0
  149. package/dist/private-local-shiitake-host.js +143 -0
  150. package/dist/private-local-startup-model.d.ts +51 -0
  151. package/dist/private-local-startup-model.js +86 -0
  152. package/dist/private-local-stop-all.d.ts +27 -0
  153. package/dist/private-local-stop-all.js +26 -0
  154. package/dist/private-local-ui-attachment.d.ts +19 -0
  155. package/dist/private-local-ui-attachment.js +71 -0
  156. package/dist/private-local-worker-server.d.ts +16 -0
  157. package/dist/private-local-worker-server.js +144 -0
  158. package/dist/private-mcp-form-contract.d.ts +186 -0
  159. package/dist/private-mcp-form-contract.js +53 -0
  160. package/dist/private-mcp-form-elicitation.d.ts +18 -0
  161. package/dist/private-mcp-form-elicitation.js +97 -0
  162. package/dist/private-mcp-form-session.d.ts +172 -0
  163. package/dist/private-mcp-form-session.js +148 -0
  164. package/dist/private-openai-claims.d.ts +30 -0
  165. package/dist/private-openai-claims.js +69 -0
  166. package/dist/private-oyster-mcp-auth.d.ts +13 -0
  167. package/dist/private-oyster-mcp-auth.js +57 -0
  168. package/dist/private-provider-rejection.d.ts +15 -0
  169. package/dist/private-provider-rejection.js +24 -0
  170. package/dist/private-sqlite-owner.d.ts +39 -0
  171. package/dist/private-sqlite-owner.js +169 -0
  172. package/dist/private-team-computers.d.ts +38 -0
  173. package/dist/private-team-computers.js +110 -0
  174. package/dist/private-workerd-sandbox.d.ts +3 -0
  175. package/dist/private-workerd-sandbox.js +347 -0
  176. package/dist/seatbelt/safe-base.sbpl +79 -0
  177. package/dist/subscription-activation.d.ts +9 -0
  178. package/dist/subscription-activation.js +62 -0
  179. package/dist/subscription-identity.d.ts +23 -0
  180. package/dist/subscription-identity.js +124 -0
  181. package/dist/subscription-turn.d.ts +17 -0
  182. package/dist/subscription-turn.js +143 -0
  183. package/dist/team-apps.d.ts +24 -0
  184. package/dist/team-apps.js +128 -0
  185. package/dist/tracker-git-credential.d.ts +15 -0
  186. package/dist/tracker-git-credential.js +87 -0
  187. package/native/bin/LICENSE.bubblewrap +502 -0
  188. package/native/bin/LICENSE.libcap +112 -0
  189. package/native/bin/LICENSE.sandbox-runtime +201 -0
  190. package/native/bin/SOURCE.bubblewrap.tar.xz +0 -0
  191. package/native/bin/build.json +4037 -0
  192. package/native/bin/bwrap-build-source/Dockerfile +48 -0
  193. package/native/bin/bwrap-build-source/README.md +96 -0
  194. package/native/bin/bwrap-build-source/build.mjs +52 -0
  195. package/native/bin/bwrap-build-source/linux-arm64.ini +14 -0
  196. package/native/bin/bwrap-build-source/pin.mjs +21 -0
  197. package/native/bin/bwrap-build-source/record.mjs +74 -0
  198. package/native/bin/bwrap-build-source/vendor/LICENSE.sandbox-runtime +201 -0
  199. package/native/bin/bwrap-build-source/vendor/README.md +23 -0
  200. package/native/bin/bwrap-build-source/vendor/seccomp-unix-block.c +280 -0
  201. package/native/bin/bwrap-build.json +44 -0
  202. package/native/bin/fungi-apply-patch +0 -0
  203. package/native/bin/fungi-bwrap +0 -0
  204. package/native/bin/fungi-bwrap.sha256 +1 -0
  205. package/native/bin/fungi-seccomp.bpf +0 -0
  206. package/native/bin/fungi-seccomp.bpf.sha256 +1 -0
  207. package/package.json +161 -3
@@ -0,0 +1,624 @@
1
+ import { createHash } from "node:crypto";
2
+ import { access, constants, mkdir, mkdtemp, readFile, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
3
+ import { join, resolve, sep } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { Mycelium } from "@fungi.computer/mycelium";
6
+ import { defineExtension, ExtensionId, } from "@fungi.computer/shiitake/extensions";
7
+ import * as Predicate from "effect/Predicate";
8
+ import * as Schema from "effect/Schema";
9
+ import lockfile from "proper-lockfile";
10
+ import writeFileAtomic from "write-file-atomic";
11
+ import { z } from "zod";
12
+ import { describeExtensionSkills, } from "./local-skill-source.js";
13
+ /**
14
+ * Installed Local extensions. One owner holds the package bytes under
15
+ * `~/.fungi/extensions`, and the activations its typed operations write. A
16
+ * project's client part and program launchers, and an Agent's tools, are
17
+ * separate activations keyed by canonical path. Nothing under a project or
18
+ * Agent store authorizes anything: a copied folder starts disabled.
19
+ *
20
+ * A package may also ship a `skills` folder. Its skills are read in place
21
+ * from the installed package (nothing is copied or symlinked) and only while
22
+ * the Agent's activation opts in, so disabling or removing the extension ends
23
+ * them. This covers Local installed extensions only; Hub-hosted extensions
24
+ * are not covered.
25
+ */
26
+ const capabilities = [
27
+ "state",
28
+ "attention",
29
+ "settled",
30
+ "send",
31
+ "timers",
32
+ "commands",
33
+ "projection",
34
+ "run_tools",
35
+ "files",
36
+ "prompt",
37
+ "compaction_handoff",
38
+ ];
39
+ const decodeExtensionId = Schema.decodeUnknownSync(ExtensionId);
40
+ const identifier = z.string().regex(/^[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*$/u);
41
+ const revisionPattern = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/u;
42
+ const manifestSchema = z.strictObject({
43
+ id: z.string().transform((value) => decodeExtensionId(value)),
44
+ capabilities: z.array(z.enum(capabilities)).max(10).default([]),
45
+ /** Package-relative folder of skills, offered to an Agent that opts in. */
46
+ skills: z.string().startsWith("./").max(1_024).optional(),
47
+ terminals: z
48
+ .array(z.strictObject({
49
+ id: identifier.max(64),
50
+ name: z.string().trim().min(1).max(64),
51
+ description: z.string().max(200).default(""),
52
+ /** Package-relative executable and its arguments. */
53
+ command: z
54
+ .array(z.string().min(1).max(1_024))
55
+ .min(1)
56
+ .max(32)
57
+ .refine((command) => command[0]?.startsWith("./") === true, {
58
+ message: "Terminal commands run a package-relative executable",
59
+ }),
60
+ }))
61
+ .max(8)
62
+ .default([]),
63
+ });
64
+ const packageSchema = z.looseObject({
65
+ name: z.string().min(1).max(214),
66
+ version: z.string().min(1).max(64),
67
+ /** Fungi entries are plain subpath exports. */
68
+ exports: z.record(z.string(), z.string()).default({}),
69
+ fungi: manifestSchema,
70
+ });
71
+ const terminalRecord = z.strictObject({
72
+ id: z.string(),
73
+ name: z.string(),
74
+ description: z.string(),
75
+ command: z.array(z.string()),
76
+ });
77
+ const revisionRecord = z.strictObject({
78
+ name: z.string(),
79
+ version: z.string(),
80
+ source: z.string(),
81
+ resolution: z.string(),
82
+ commit: z.string().nullable(),
83
+ directory: z.string(),
84
+ capabilities: z.array(z.enum(capabilities)),
85
+ terminals: z.array(terminalRecord),
86
+ host: z.string().nullable(),
87
+ client: z.string().nullable(),
88
+ skills: z.string().nullable(),
89
+ });
90
+ const installedSchema = z.strictObject({
91
+ extensions: z.record(z.string(), z.strictObject({
92
+ current: z.string(),
93
+ revisions: z.record(z.string(), revisionRecord),
94
+ })),
95
+ });
96
+ const activationsSchema = z.strictObject({
97
+ projects: z.record(z.string(), z.record(z.string(), z.string())),
98
+ agents: z.record(z.string(), z.record(z.string(), z.strictObject({
99
+ revision: z.string(),
100
+ capabilities: z.array(z.enum(capabilities)),
101
+ /** The opt-in to the revision's skills folder; separate from tools. */
102
+ skills: z.boolean(),
103
+ }))),
104
+ });
105
+ const lockSchema = z.looseObject({
106
+ packages: z.record(z.string(), z.array(z.unknown()).min(1)),
107
+ });
108
+ const hostModuleSchema = z.looseObject({
109
+ default: z.custom((value) => Predicate.isFunction(value), { message: "An extension host entry exports a default factory" }),
110
+ });
111
+ const rosteredSchema = z.looseObject({
112
+ manifest: z.looseObject({
113
+ id: z.string(),
114
+ requests: z.array(z.enum(capabilities)),
115
+ }),
116
+ });
117
+ /** A typed refusal from the extension owner; never reported as usable. */
118
+ export class LocalExtensionError extends Error {
119
+ name = "LocalExtensionError";
120
+ reason;
121
+ constructor(reason, message, options) {
122
+ super(message, options);
123
+ this.reason = reason;
124
+ }
125
+ }
126
+ /**
127
+ * An Agent holds an extension when its store has granted tools, covering every
128
+ * capability of the current revision. Opting into only the extension's skills
129
+ * is not holding it: skills carry no authority to hand its tools on.
130
+ */
131
+ function assertAgentHolds(held, installed, ids) {
132
+ const missing = ids.filter((id) => {
133
+ const activation = held?.[id];
134
+ if (activation === undefined || activation.capabilities.length === 0)
135
+ return true;
136
+ const entry = installed.extensions[id];
137
+ const needed = (entry === undefined ? undefined : entry.revisions[entry.current])
138
+ ?.capabilities ?? [];
139
+ return !needed.every((capability) => activation.capabilities.includes(capability));
140
+ });
141
+ if (missing.length > 0)
142
+ throw new LocalExtensionError("not-held", `An Agent cannot give ${missing.join(", ")} to another Agent: it does not hold ${missing.length === 1 ? "it" : "them"} itself`);
143
+ }
144
+ function failure(cause) {
145
+ return cause instanceof Error ? cause : new Error(String(cause));
146
+ }
147
+ function absent(cause) {
148
+ return (cause instanceof Error &&
149
+ "code" in cause &&
150
+ (cause.code === "ENOENT" || cause.code === "ENOTDIR"));
151
+ }
152
+ /** Open the one extension owner for a user home. */
153
+ export function localExtensions(options) {
154
+ const root = join(resolve(options.home), ".fungi", "extensions");
155
+ const installedPath = join(root, "installed.json");
156
+ const activationsPath = join(root, "activations.json");
157
+ async function readJson(path, schema, empty, remedy) {
158
+ let text;
159
+ try {
160
+ text = await readFile(path, "utf8");
161
+ }
162
+ catch (cause) {
163
+ if (absent(cause))
164
+ return empty;
165
+ throw cause;
166
+ }
167
+ const parsed = schema.safeParse(JSON.parse(text));
168
+ if (!parsed.success)
169
+ throw new LocalExtensionError("install", `${path} is from an earlier extension format; ${remedy}`, { cause: parsed.error });
170
+ return parsed.data;
171
+ }
172
+ const readInstalled = () => readJson(installedPath, installedSchema, { extensions: {} }, "delete it and run `fungi extension add` again");
173
+ const readActivations = () => readJson(activationsPath, activationsSchema, { projects: {}, agents: {} }, "delete it, then re-enable your extensions");
174
+ /** Serialize owner writes across CLI and service processes. */
175
+ async function update(change) {
176
+ await mkdir(root, { recursive: true, mode: 0o700 });
177
+ const release = await lockfile.lock(root, {
178
+ realpath: false,
179
+ lockfilePath: join(root, ".owner.lock"),
180
+ stale: 30_000,
181
+ update: 10_000,
182
+ retries: { retries: 20, factor: 1.2, minTimeout: 25, maxTimeout: 100 },
183
+ });
184
+ try {
185
+ const installed = await readInstalled();
186
+ const activations = await readActivations();
187
+ const result = change(installed, activations);
188
+ const options = { encoding: "utf8", fsync: true, mode: 0o600 };
189
+ await writeFileAtomic(installedPath, `${JSON.stringify(installed, null, 2)}\n`, options);
190
+ await writeFileAtomic(activationsPath, `${JSON.stringify(activations, null, 2)}\n`, options);
191
+ return result;
192
+ }
193
+ finally {
194
+ await release();
195
+ }
196
+ }
197
+ function currentRevision(installed, id) {
198
+ const entry = installed.extensions[id];
199
+ const revision = entry === undefined ? undefined : entry.revisions[entry.current];
200
+ if (entry === undefined || revision === undefined)
201
+ throw new LocalExtensionError("not-installed", `Extension ${id} is not installed`);
202
+ return { revision: entry.current, record: revision };
203
+ }
204
+ /** A 0700 directory shared by one project's programs and Agent tools. */
205
+ async function runtimeDirectory(projectRoot, id) {
206
+ const project = createHash("sha256")
207
+ .update(await realpath(projectRoot))
208
+ .digest("hex")
209
+ .slice(0, 16);
210
+ const directory = join(root, "run", project, id);
211
+ await mkdir(directory, { recursive: true, mode: 0o700 });
212
+ return directory;
213
+ }
214
+ /** A declared `./host` or `./client` export must exist in the installed package. */
215
+ async function declaredEntry(directory, exports, subpath) {
216
+ const target = exports[subpath];
217
+ if (target === undefined)
218
+ return null;
219
+ const path = resolve(directory, target);
220
+ if (!path.startsWith(`${directory}${sep}`))
221
+ throw new LocalExtensionError("manifest", `${subpath} must point inside its package`);
222
+ try {
223
+ await access(path, constants.R_OK);
224
+ }
225
+ catch (cause) {
226
+ throw new LocalExtensionError("artifact", `The package declares ${subpath} but ${target} is not built`, { cause });
227
+ }
228
+ return path;
229
+ }
230
+ /** A declared skills folder must be a directory inside the package. */
231
+ async function declaredSkills(directory, declared) {
232
+ if (declared === undefined)
233
+ return null;
234
+ const path = resolve(directory, declared);
235
+ if (!path.startsWith(`${directory}${sep}`))
236
+ throw new LocalExtensionError("manifest", "skills must point inside its package");
237
+ try {
238
+ const real = await realpath(path);
239
+ if (!real.startsWith(`${await realpath(directory)}${sep}`))
240
+ throw new LocalExtensionError("manifest", "skills must point inside its package");
241
+ if (!(await stat(real)).isDirectory())
242
+ throw new LocalExtensionError("artifact", `The package declares skills but ${declared} is not a directory`);
243
+ }
244
+ catch (cause) {
245
+ if (cause instanceof LocalExtensionError)
246
+ throw cause;
247
+ throw new LocalExtensionError("artifact", `The package declares skills but ${declared} is not in the installed package`, { cause });
248
+ }
249
+ return path;
250
+ }
251
+ return {
252
+ root,
253
+ /**
254
+ * Install a package with the shipped Bun package manager (lifecycle
255
+ * scripts disabled) and record the exact resolved revision. A Git source
256
+ * is pinned to the commit Bun's lockfile resolved; updating is another
257
+ * explicit `add`. Missing declared artifacts are a refusal.
258
+ */
259
+ async add(source) {
260
+ await mkdir(join(root, "staging"), { recursive: true, mode: 0o700 });
261
+ const staging = await mkdtemp(join(root, "staging", "install-"));
262
+ try {
263
+ await writeFile(join(staging, "package.json"), `${JSON.stringify({ private: true })}\n`);
264
+ const child = Bun.spawn([process.execPath, "add", "--ignore-scripts", source], {
265
+ cwd: staging,
266
+ // A private cache resolves the named ref afresh instead of a
267
+ // cached checkout of an earlier commit.
268
+ env: {
269
+ ...process.env,
270
+ BUN_BE_BUN: "1",
271
+ BUN_INSTALL_CACHE_DIR: join(staging, ".install-cache"),
272
+ },
273
+ stdout: "pipe",
274
+ stderr: "pipe",
275
+ });
276
+ const [status, stderr] = await Promise.all([
277
+ child.exited,
278
+ new Response(child.stderr).text(),
279
+ ]);
280
+ if (status !== 0)
281
+ throw new LocalExtensionError("install", `Could not install ${source}: ${stderr.trim().slice(0, 2_000)}`);
282
+ const lock = lockSchema.parse(Bun.JSONC.parse(await readFile(join(staging, "bun.lock"), "utf8")));
283
+ const dependencies = Object.keys(z
284
+ .looseObject({ dependencies: z.record(z.string(), z.string()) })
285
+ .parse(JSON.parse(await readFile(join(staging, "package.json"), "utf8"))).dependencies);
286
+ const name = dependencies[0];
287
+ if (name === undefined || dependencies.length !== 1)
288
+ throw new LocalExtensionError("install", `${source} did not install exactly one package`);
289
+ const directory = join(staging, "node_modules", ...name.split("/"));
290
+ const pkg = packageSchema.safeParse(JSON.parse(await readFile(join(directory, "package.json"), "utf8")));
291
+ if (!pkg.success)
292
+ throw new LocalExtensionError("manifest", `${source} does not declare a Fungi extension in package.json "fungi"`, { cause: pkg.error });
293
+ const selected = { name, directory, pkg: pkg.data };
294
+ const entry = lock.packages[selected.name] ?? [];
295
+ const resolution = z.string().parse(entry[0]);
296
+ const pinned = z.string().safeParse(entry[2]);
297
+ const git = /#([0-9a-f]{40})$/u.exec(resolution);
298
+ const commit = git?.[1] ?? null;
299
+ const manifest = selected.pkg.fungi;
300
+ const host = await declaredEntry(selected.directory, selected.pkg.exports, "./host");
301
+ const client = await declaredEntry(selected.directory, selected.pkg.exports, "./client");
302
+ const skills = await declaredSkills(selected.directory, manifest.skills);
303
+ const skillNames = skills === null
304
+ ? []
305
+ : (await describeExtensionSkills({ extensionId: manifest.id, directory: skills }, AbortSignal.timeout(10_000))).skills.map((skill) => skill.name);
306
+ if (skills !== null && skillNames.length === 0)
307
+ throw new LocalExtensionError("artifact", `${manifest.id} declares skills but ${manifest.skills ?? ""} contains none`);
308
+ if (manifest.capabilities.length > 0 && host === null)
309
+ throw new LocalExtensionError("artifact", `${manifest.id} declares Agent capabilities but its package exports no ./host entry`);
310
+ for (const terminal of manifest.terminals) {
311
+ const executable = resolve(selected.directory, terminal.command[0] ?? "");
312
+ if (!executable.startsWith(`${selected.directory}${sep}`))
313
+ throw new LocalExtensionError("manifest", `Terminal ${terminal.id} must run a file inside its package`);
314
+ try {
315
+ await access(executable, constants.X_OK);
316
+ }
317
+ catch (cause) {
318
+ throw new LocalExtensionError("artifact", `Terminal ${terminal.id} program ${terminal.command[0] ?? ""} is not an executable file in the installed package`, { cause });
319
+ }
320
+ }
321
+ const revision = commit ??
322
+ (pinned.success
323
+ ? `${selected.pkg.version}-${createHash("sha256").update(pinned.data).digest("hex").slice(0, 12)}`
324
+ : selected.pkg.version);
325
+ if (!revisionPattern.test(revision))
326
+ throw new LocalExtensionError("manifest", "Installed revision is not representable");
327
+ const target = join(root, "packages", manifest.id, revision);
328
+ const packageDirectory = join(target, "node_modules", ...selected.name.split("/"));
329
+ const record = {
330
+ name: selected.name,
331
+ version: selected.pkg.version,
332
+ source,
333
+ resolution,
334
+ commit,
335
+ directory: packageDirectory,
336
+ capabilities: manifest.capabilities,
337
+ terminals: manifest.terminals,
338
+ host: host === null
339
+ ? null
340
+ : join(target, host.slice(staging.length + sep.length)),
341
+ client: client === null
342
+ ? null
343
+ : join(target, client.slice(staging.length + sep.length)),
344
+ skills: skills === null
345
+ ? null
346
+ : join(target, skills.slice(staging.length + sep.length)),
347
+ };
348
+ await update((installed) => {
349
+ const existing = installed.extensions[manifest.id];
350
+ installed.extensions[manifest.id] = {
351
+ current: revision,
352
+ revisions: { ...existing?.revisions, [revision]: record },
353
+ };
354
+ });
355
+ await mkdir(join(root, "packages", manifest.id), { recursive: true });
356
+ await rm(target, { recursive: true, force: true });
357
+ await rename(staging, target);
358
+ return {
359
+ id: manifest.id,
360
+ revision,
361
+ version: record.version,
362
+ source,
363
+ commit,
364
+ capabilities: record.capabilities,
365
+ terminals: record.terminals.map((terminal) => terminal.id),
366
+ skills: skillNames,
367
+ };
368
+ }
369
+ finally {
370
+ await rm(staging, { recursive: true, force: true });
371
+ }
372
+ },
373
+ /** Remove the package and every activation that references it. */
374
+ async remove(id) {
375
+ const removed = await update((installed, activations) => {
376
+ if (installed.extensions[id] === undefined)
377
+ return false;
378
+ Reflect.deleteProperty(installed.extensions, id);
379
+ for (const project of Object.values(activations.projects))
380
+ Reflect.deleteProperty(project, id);
381
+ for (const agent of Object.values(activations.agents))
382
+ Reflect.deleteProperty(agent, id);
383
+ return true;
384
+ });
385
+ // Loaded code and running programs keep their open files until their
386
+ // own owners end; new owners can no longer select this package.
387
+ await rm(join(root, "packages", id), { recursive: true, force: true });
388
+ return removed;
389
+ },
390
+ async list() {
391
+ const installed = await readInstalled();
392
+ return Object.entries(installed.extensions).flatMap(([id, entry]) => {
393
+ const record = entry.revisions[entry.current];
394
+ return record === undefined
395
+ ? []
396
+ : [
397
+ {
398
+ id,
399
+ revision: entry.current,
400
+ version: record.version,
401
+ source: record.source,
402
+ commit: record.commit,
403
+ capabilities: record.capabilities,
404
+ terminals: record.terminals.map((terminal) => terminal.id),
405
+ },
406
+ ];
407
+ });
408
+ },
409
+ /** Enable the current revision's client part and programs for a project. */
410
+ async enableProject(projectRoot, id) {
411
+ const project = await realpath(projectRoot);
412
+ return update((installed, activations) => {
413
+ const { revision } = currentRevision(installed, id);
414
+ activations.projects[project] = {
415
+ ...activations.projects[project],
416
+ [id]: revision,
417
+ };
418
+ return revision;
419
+ });
420
+ },
421
+ async disableProject(projectRoot, id) {
422
+ const project = await realpath(projectRoot);
423
+ return update((_installed, activations) => {
424
+ const enabled = activations.projects[project];
425
+ if (enabled?.[id] === undefined)
426
+ return false;
427
+ Reflect.deleteProperty(enabled, id);
428
+ return true;
429
+ });
430
+ },
431
+ /**
432
+ * The one check that an Agent holds an extension: refuses with
433
+ * `not-held`, naming every id it lacks. Callers needing a whole request
434
+ * judged before any effect call this first; `enableAgent` applies it too.
435
+ */
436
+ async assertHeldByAgent(agentStoreDirectory, ids) {
437
+ const store = await realpath(agentStoreDirectory);
438
+ const [installed, activations] = await Promise.all([
439
+ readInstalled(),
440
+ readActivations(),
441
+ ]);
442
+ assertAgentHolds(activations.agents[store], installed, ids);
443
+ },
444
+ /**
445
+ * Grant one Agent store the current revision's declared capabilities.
446
+ * Running work keeps its captured roster; the Agent applies this when it
447
+ * next opens. A human (the CLI) names no requester. When an Agent asks, it
448
+ * must hold the extension itself.
449
+ */
450
+ async enableAgent(storeDirectory, id, requester) {
451
+ const store = await realpath(storeDirectory);
452
+ const requesterStore = requester === undefined
453
+ ? undefined
454
+ : await realpath(requester.agentStoreDirectory);
455
+ return update((installed, activations) => {
456
+ if (requesterStore !== undefined)
457
+ assertAgentHolds(activations.agents[requesterStore], installed, [id]);
458
+ const { revision, record } = currentRevision(installed, id);
459
+ if (record.host === null || record.capabilities.length === 0)
460
+ throw new LocalExtensionError("no-tools", `Extension ${id} declares no Agent capabilities`);
461
+ const skills = activations.agents[store]?.[id]?.skills === true &&
462
+ record.skills !== null;
463
+ activations.agents[store] = {
464
+ ...activations.agents[store],
465
+ [id]: { revision, capabilities: record.capabilities, skills },
466
+ };
467
+ return { revision, capabilities: record.capabilities, skills };
468
+ });
469
+ },
470
+ /**
471
+ * Opt one Agent store into the skills the extension ships, without
472
+ * granting any tools. An existing activation keeps its revision and its
473
+ * capabilities; a new one carries none.
474
+ */
475
+ async enableAgentSkills(storeDirectory, id) {
476
+ const store = await realpath(storeDirectory);
477
+ return update((installed, activations) => {
478
+ const current = currentRevision(installed, id);
479
+ const prior = activations.agents[store]?.[id];
480
+ const revision = prior?.revision ?? current.revision;
481
+ const record = installed.extensions[id]?.revisions[revision];
482
+ if (!record?.skills)
483
+ throw new LocalExtensionError("no-skills", `Extension ${id}@${revision} ships no skills`);
484
+ activations.agents[store] = {
485
+ ...activations.agents[store],
486
+ [id]: {
487
+ revision,
488
+ capabilities: prior?.capabilities ?? [],
489
+ skills: true,
490
+ },
491
+ };
492
+ return revision;
493
+ });
494
+ },
495
+ /**
496
+ * Turn only the skills off. Tools stay granted; an activation left with
497
+ * neither is removed.
498
+ */
499
+ async disableAgentSkills(storeDirectory, id) {
500
+ const store = await realpath(storeDirectory);
501
+ return update((_installed, activations) => {
502
+ const enabled = activations.agents[store];
503
+ const activation = enabled?.[id];
504
+ if (enabled === undefined || activation?.skills !== true)
505
+ return false;
506
+ if (activation.capabilities.length === 0)
507
+ Reflect.deleteProperty(enabled, id);
508
+ else
509
+ enabled[id] = { ...activation, skills: false };
510
+ return true;
511
+ });
512
+ },
513
+ async disableAgent(storeDirectory, id) {
514
+ const store = await realpath(storeDirectory);
515
+ return update((_installed, activations) => {
516
+ const enabled = activations.agents[store];
517
+ if (enabled?.[id] === undefined)
518
+ return false;
519
+ Reflect.deleteProperty(enabled, id);
520
+ return true;
521
+ });
522
+ },
523
+ /** The project's enabled client entries and program launchers. */
524
+ async project(projectRoot) {
525
+ const project = await realpath(projectRoot);
526
+ const [installed, activations] = await Promise.all([
527
+ readInstalled(),
528
+ readActivations(),
529
+ ]);
530
+ const clients = [];
531
+ const terminals = [];
532
+ const diagnostics = [];
533
+ for (const [id, revision] of Object.entries(activations.projects[project] ?? {})) {
534
+ const record = installed.extensions[id]?.revisions[revision];
535
+ if (record === undefined) {
536
+ diagnostics.push(new LocalExtensionError("not-installed", `Extension ${id}@${revision} is enabled but not installed`));
537
+ continue;
538
+ }
539
+ if (record.client !== null)
540
+ clients.push(record.client);
541
+ const runtime = await runtimeDirectory(project, id);
542
+ for (const terminal of record.terminals)
543
+ terminals.push({
544
+ extensionId: id,
545
+ terminalId: terminal.id,
546
+ name: terminal.name,
547
+ description: terminal.description,
548
+ command: [
549
+ resolve(record.directory, terminal.command[0] ?? ""),
550
+ ...terminal.command.slice(1),
551
+ ],
552
+ cwd: project,
553
+ env: {
554
+ FUNGI_EXTENSION_ID: id,
555
+ FUNGI_PROJECT_ROOT: project,
556
+ FUNGI_EXTENSION_RUNTIME_DIR: runtime,
557
+ },
558
+ });
559
+ }
560
+ return { clients, terminals, diagnostics };
561
+ },
562
+ /** Capture validated records so one reload cannot mix file generations. */
563
+ async captureAgent() {
564
+ const [installed, activations] = await Promise.all([
565
+ readInstalled(),
566
+ readActivations(),
567
+ ]);
568
+ return (storeDirectory, projectRoot) => loadAgent(installed, activations, storeDirectory, projectRoot);
569
+ },
570
+ async agent(storeDirectory, projectRoot) {
571
+ return loadAgent(await readInstalled(), await readActivations(), storeDirectory, projectRoot);
572
+ },
573
+ };
574
+ async function loadAgent(installed, activations, storeDirectory, projectRoot) {
575
+ const store = await realpath(storeDirectory);
576
+ const roster = [];
577
+ const grants = [];
578
+ const skills = [];
579
+ const diagnostics = [];
580
+ for (const [id, activation] of Object.entries(activations.agents[store] ?? {})) {
581
+ const record = installed.extensions[id]?.revisions[activation.revision];
582
+ try {
583
+ if (record === undefined)
584
+ throw new LocalExtensionError("not-installed", `Extension ${id}@${activation.revision} is enabled but not installed`);
585
+ if (activation.skills && record.skills !== null)
586
+ skills.push({ extensionId: id, directory: record.skills });
587
+ // A skills-only activation grants no tools and loads no host code.
588
+ if (activation.capabilities.length === 0)
589
+ continue;
590
+ if (record.host === null)
591
+ throw new LocalExtensionError("not-installed", `Extension ${id}@${activation.revision} is enabled but its host entry is not installed`);
592
+ const module = hostModuleSchema.parse(await import(pathToFileURL(record.host).href));
593
+ const extension = await module.default({
594
+ runtimeDirectory: await runtimeDirectory(projectRoot, id),
595
+ defineExtension,
596
+ Mycelium,
597
+ z,
598
+ });
599
+ const parsed = rosteredSchema.parse(extension);
600
+ if (parsed.manifest.id !== id)
601
+ throw new LocalExtensionError("load", `Extension ${id} host returned ${parsed.manifest.id}`);
602
+ const requested = new Set(parsed.manifest.requests);
603
+ roster.push(z
604
+ .custom((value) => rosteredSchema.safeParse(value).success)
605
+ .parse(extension));
606
+ grants.push({
607
+ id: decodeExtensionId(id),
608
+ capabilities: activation.capabilities.filter((capability) => requested.has(capability)),
609
+ });
610
+ }
611
+ catch (cause) {
612
+ diagnostics.push({
613
+ extensionId: id,
614
+ error: cause instanceof LocalExtensionError
615
+ ? cause
616
+ : new LocalExtensionError("load", `Extension ${id} could not load`, { cause: failure(cause) }),
617
+ });
618
+ }
619
+ }
620
+ // Same skill name in two extensions: the lower extension id wins, always.
621
+ skills.sort((a, b) => a.extensionId.localeCompare(b.extensionId));
622
+ return { roster, grants, skills, diagnostics };
623
+ }
624
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Local shrinks every image it reads before it becomes an image part, as the
3
+ * retained `read_image` did on the Images binding: at most 512 px on the long
4
+ * edge as a quality-80 JPEG, stepping down to a 256 px floor until the bytes
5
+ * meet the 100 KB budget. Photon (Rust `image` compiled to WebAssembly, inlined
6
+ * in the package) decodes and encodes with no network, no Cloudflare and no
7
+ * per-platform native build.
8
+ *
9
+ * Photon's WebAssembly memory grows to the largest decode and never shrinks,
10
+ * and the Local service is long-running, so each image is decoded in its own
11
+ * worker thread that is terminated afterwards, returning that memory.
12
+ */
13
+ /** Largest image file Local reads to shrink; larger files are refused unread. */
14
+ export declare const maximumLocalImageInputBytes: number;
15
+ /**
16
+ * Largest decoded image Local shrinks, checked from the header before any
17
+ * worker starts. 25 megapixels admits 24 MP phone photos (5712 x 4284) and 6K
18
+ * screenshots.
19
+ */
20
+ export declare const maximumLocalImagePixels = 25000000;
21
+ /** At most this many bytes of one image reach the model. */
22
+ export declare const maximumLocalImageViewBytes: number;
23
+ /**
24
+ * Shrink one whole png, jpeg, gif (first frame) or webp file for the model.
25
+ * Refuses, with the Files read error, an image whose header or pixels cannot
26
+ * be decoded, one above {@link maximumLocalImagePixels}, and one still above
27
+ * {@link maximumLocalImageViewBytes} at the 256 px floor. Never passes the
28
+ * original bytes through.
29
+ */
30
+ export declare function fitLocalImage(path: string, bytes: Uint8Array, signal: AbortSignal): Promise<Uint8Array>;