@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
@@ -1,6 +1,5 @@
1
1
  import { open, opendir, realpath, stat } from "node:fs/promises";
2
- import { homedir } from "node:os";
3
- import { basename, dirname, isAbsolute, join, relative, resolve, } from "node:path";
2
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep, } from "node:path";
4
3
  import { parse as parseYaml } from "yaml";
5
4
  import { z } from "zod";
6
5
  const maximumSkillCount = 64;
@@ -44,6 +43,23 @@ function contained(root, path) {
44
43
  throw new Error("Skill path escapes its directory");
45
44
  return path;
46
45
  }
46
+ function inside(root, path) {
47
+ const within = relative(root, path);
48
+ return within === "" ||
49
+ isAbsolute(within) ||
50
+ within === ".." ||
51
+ within.startsWith(`..${sep}`)
52
+ ? undefined
53
+ : within;
54
+ }
55
+ /** The model's view of a skill file: invocation-relative, `~/…`, else absolute. */
56
+ function displayLocation(location, roots) {
57
+ const project = inside(roots.cwd, location);
58
+ if (project !== undefined)
59
+ return project;
60
+ const personal = inside(roots.home, location);
61
+ return personal === undefined ? location : join("~", personal);
62
+ }
47
63
  async function readBounded(path, signal) {
48
64
  signal.throwIfAborted();
49
65
  const handle = await open(path, "r");
@@ -144,166 +160,226 @@ async function configuredRoots(path, base, home, signal, diagnostics) {
144
160
  return [];
145
161
  }
146
162
  }
147
- /** Discover project-first skills without widening the invocation Files jail. */
148
- export function createLocalSkillSource(options) {
149
- const cwd = resolve(options.cwd);
150
- const home = resolve(options.home ?? homedir());
151
- const catalog = async (signal) => {
152
- const skills = [];
153
- const diagnostics = [];
154
- const names = new Set();
155
- const paths = new Set();
156
- const projectExtras = await configuredRoots(join(cwd, ".fungi/settings.json"), cwd, home, signal, diagnostics);
157
- const personalExtras = await configuredRoots(join(home, ".fungi/settings.json"), join(home, ".fungi"), home, signal, diagnostics);
158
- const extras = new Set([...projectExtras, ...personalExtras]);
159
- const roots = [
160
- ...(await discoveryRoots(cwd)),
161
- ...projectExtras,
162
- join(home, ".fungi/skills"),
163
- join(home, ".agents/skills"),
164
- ...personalExtras,
165
- ];
166
- const visitedRoots = new Set();
167
- for (const path of roots) {
163
+ function describeOrigin(origin) {
164
+ return origin.kind === "extension"
165
+ ? `extension ${origin.extensionId}`
166
+ : origin.kind;
167
+ }
168
+ /** Walk labeled roots in order; the first skill with a name wins. */
169
+ async function scanRoots(roots, displayRoots, signal, diagnostics) {
170
+ const skills = [];
171
+ const owners = new Map();
172
+ const paths = new Set();
173
+ const visitedRoots = new Set();
174
+ for (const { path, origin, named } of roots) {
175
+ signal.throwIfAborted();
176
+ let root;
177
+ try {
178
+ root = await realpath(path);
179
+ }
180
+ catch (cause) {
181
+ signal.throwIfAborted();
182
+ if (!absent(cause) || named)
183
+ diagnostics.push({
184
+ path,
185
+ reason: "unavailable",
186
+ message: "Skill root is unavailable",
187
+ cause,
188
+ });
189
+ continue;
190
+ }
191
+ if (visitedRoots.has(root))
192
+ continue;
193
+ visitedRoots.add(root);
194
+ const visited = new Set();
195
+ let entries = 0;
196
+ let found = 0;
197
+ const before = diagnostics.length;
198
+ const walk = async (directory, depth) => {
168
199
  signal.throwIfAborted();
169
- let root;
200
+ if (depth > maximumDiscoveryDepth) {
201
+ diagnostics.push(diagnostic(directory, "bound", "Skill discovery exceeds its 16-level depth bound"));
202
+ return;
203
+ }
204
+ if (visited.has(directory))
205
+ return;
206
+ visited.add(directory);
207
+ const skillPath = join(directory, "SKILL.md");
208
+ let location;
170
209
  try {
171
- root = await realpath(path);
210
+ location = contained(directory, await realpath(skillPath));
172
211
  }
173
212
  catch (cause) {
174
213
  signal.throwIfAborted();
175
- if (!absent(cause) || extras.has(path))
214
+ if (!absent(cause)) {
176
215
  diagnostics.push({
177
- path,
178
- reason: "unavailable",
179
- message: "Skill root is unavailable",
216
+ path: skillPath,
217
+ reason: "invalid",
218
+ message: "Skill file escapes its directory or cannot be read",
180
219
  cause,
181
220
  });
182
- continue;
183
- }
184
- if (visitedRoots.has(root))
185
- continue;
186
- visitedRoots.add(root);
187
- const visited = new Set();
188
- let entries = 0;
189
- let found = 0;
190
- const before = diagnostics.length;
191
- const walk = async (directory, depth) => {
192
- signal.throwIfAborted();
193
- if (depth > maximumDiscoveryDepth) {
194
- diagnostics.push(diagnostic(directory, "bound", "Skill discovery exceeds its 16-level depth bound"));
195
221
  return;
196
222
  }
197
- if (visited.has(directory))
198
- return;
199
- visited.add(directory);
200
- const skillPath = join(directory, "SKILL.md");
201
- let location;
223
+ }
224
+ if (location !== undefined) {
225
+ found++;
202
226
  try {
203
- location = contained(directory, await realpath(skillPath));
204
- }
205
- catch (cause) {
206
- signal.throwIfAborted();
207
- if (!absent(cause)) {
208
- diagnostics.push({
209
- path: skillPath,
210
- reason: "invalid",
211
- message: "Skill file escapes its directory or cannot be read",
212
- cause,
213
- });
227
+ if (paths.has(location))
228
+ return;
229
+ paths.add(location);
230
+ const { metadata } = parseSkill(await readBounded(location, signal), directory);
231
+ if (!nameSchema.safeParse(metadata.name).success ||
232
+ metadata.name !== basename(directory))
233
+ diagnostics.push(diagnostic(skillPath, "warning", "Skill name should match its directory, use lowercase letters, digits and single hyphens, and be at most 64 characters"));
234
+ if (metadata.description.length > 1_024)
235
+ diagnostics.push(diagnostic(skillPath, "warning", "Skill description exceeds the recommended 1024 characters"));
236
+ const winner = owners.get(metadata.name);
237
+ if (winner !== undefined) {
238
+ diagnostics.push(diagnostic(skillPath, "collision", `${describeOrigin(winner)} skill ${metadata.name} wins over ${describeOrigin(origin)}`));
214
239
  return;
215
240
  }
216
- }
217
- if (location !== undefined) {
218
- found++;
219
- try {
220
- if (paths.has(location))
221
- return;
222
- paths.add(location);
223
- const { metadata } = parseSkill(await readBounded(location, signal), directory);
224
- if (!nameSchema.safeParse(metadata.name).success ||
225
- metadata.name !== basename(directory))
226
- diagnostics.push(diagnostic(skillPath, "warning", "Skill name should match its directory, use lowercase letters, digits and single hyphens, and be at most 64 characters"));
227
- if (metadata.description.length > 1_024)
228
- diagnostics.push(diagnostic(skillPath, "warning", "Skill description exceeds the recommended 1024 characters"));
229
- if (names.has(metadata.name)) {
230
- diagnostics.push(diagnostic(skillPath, "collision", "Earlier skill with this name wins"));
231
- return;
232
- }
233
- names.add(metadata.name);
234
- if (metadata["disable-model-invocation"]) {
235
- diagnostics.push(diagnostic(skillPath, "skipped", "Skill disables model invocation"));
236
- return;
237
- }
238
- const skill = {
239
- name: metadata.name,
240
- description: metadata.description,
241
- location,
242
- };
243
- if (skills.length >= maximumSkillCount ||
244
- utf8.encode(JSON.stringify([...skills, skill])).byteLength >
245
- maximumCatalogBytes) {
246
- diagnostics.push(diagnostic(skillPath, "bound", "Skill catalog exceeds its 64-skill or 16 KB bound"));
247
- return;
248
- }
249
- skills.push(skill);
241
+ owners.set(metadata.name, origin);
242
+ if (metadata["disable-model-invocation"]) {
243
+ diagnostics.push(diagnostic(skillPath, "skipped", "Skill disables model invocation"));
244
+ return;
250
245
  }
251
- catch (cause) {
252
- signal.throwIfAborted();
253
- diagnostics.push({
254
- path: skillPath,
255
- reason: "invalid",
256
- message: explanation(cause, "Skill metadata or file is invalid"),
257
- cause,
258
- });
246
+ const skill = {
247
+ name: metadata.name,
248
+ description: metadata.description,
249
+ location,
250
+ displayLocation: displayLocation(location, displayRoots),
251
+ origin,
252
+ };
253
+ if (skills.length >= maximumSkillCount ||
254
+ utf8.encode(JSON.stringify([...skills, skill])).byteLength >
255
+ maximumCatalogBytes) {
256
+ diagnostics.push(diagnostic(skillPath, "bound", "Skill catalog exceeds its 64-skill or 16 KB bound"));
257
+ return;
259
258
  }
260
- return;
259
+ skills.push(skill);
261
260
  }
262
- const children = [];
263
- for await (const entry of await opendir(directory)) {
261
+ catch (cause) {
264
262
  signal.throwIfAborted();
265
- if (++entries > maximumDirectoryEntries)
266
- throw new SkillSourceError("Skill discovery exceeds its 512-entry bound");
267
- if (entry.name.startsWith(".") || entry.name === "node_modules") {
268
- if (entry.isDirectory() || entry.isSymbolicLink())
269
- diagnostics.push(diagnostic(join(directory, entry.name), "skipped", "Dot directories and node_modules are not searched"));
270
- continue;
271
- }
272
- children.push(entry.name);
273
- }
274
- for (const child of children.sort()) {
275
- const candidate = join(directory, child);
276
- try {
277
- const canonical = contained(root, await realpath(candidate));
278
- if ((await stat(canonical)).isDirectory())
279
- await walk(canonical, depth + 1);
280
- }
281
- catch (cause) {
282
- signal.throwIfAborted();
283
- if (cause instanceof SkillSourceError)
284
- throw cause;
285
- diagnostics.push(diagnostic(join(candidate, "SKILL.md"), "invalid", "Skill entry escapes its root or is unavailable", cause));
286
- }
263
+ diagnostics.push({
264
+ path: skillPath,
265
+ reason: "invalid",
266
+ message: explanation(cause, "Skill metadata or file is invalid"),
267
+ cause,
268
+ });
287
269
  }
288
- };
289
- try {
290
- await walk(root, 0);
291
- if (found === 0 && diagnostics.length === before)
292
- diagnostics.push(diagnostic(path, "empty", entries === 0
293
- ? "Skill root is empty"
294
- : "Skill root contains no SKILL.md files"));
270
+ return;
295
271
  }
296
- catch (cause) {
272
+ const children = [];
273
+ for await (const entry of await opendir(directory)) {
297
274
  signal.throwIfAborted();
298
- diagnostics.push({
299
- path,
300
- reason: cause instanceof SkillSourceError ? "bound" : "unavailable",
301
- message: explanation(cause, "Skill root cannot be searched"),
302
- cause,
303
- });
275
+ if (++entries > maximumDirectoryEntries)
276
+ throw new SkillSourceError("Skill discovery exceeds its 512-entry bound");
277
+ if (entry.name.startsWith(".") || entry.name === "node_modules") {
278
+ if (entry.isDirectory() || entry.isSymbolicLink())
279
+ diagnostics.push(diagnostic(join(directory, entry.name), "skipped", "Dot directories and node_modules are not searched"));
280
+ continue;
281
+ }
282
+ children.push(entry.name);
283
+ }
284
+ for (const child of children.sort()) {
285
+ const candidate = join(directory, child);
286
+ try {
287
+ const canonical = contained(root, await realpath(candidate));
288
+ if ((await stat(canonical)).isDirectory())
289
+ await walk(canonical, depth + 1);
290
+ }
291
+ catch (cause) {
292
+ signal.throwIfAborted();
293
+ if (cause instanceof SkillSourceError)
294
+ throw cause;
295
+ diagnostics.push(diagnostic(join(candidate, "SKILL.md"), "invalid", "Skill entry escapes its root or is unavailable", cause));
296
+ }
304
297
  }
298
+ };
299
+ try {
300
+ await walk(root, 0);
301
+ if (found === 0 && diagnostics.length === before)
302
+ diagnostics.push(diagnostic(path, "empty", entries === 0
303
+ ? "Skill root is empty"
304
+ : "Skill root contains no SKILL.md files"));
305
305
  }
306
- return { skills, diagnostics };
306
+ catch (cause) {
307
+ signal.throwIfAborted();
308
+ diagnostics.push({
309
+ path,
310
+ reason: cause instanceof SkillSourceError ? "bound" : "unavailable",
311
+ message: explanation(cause, "Skill root cannot be searched"),
312
+ cause,
313
+ });
314
+ }
315
+ }
316
+ return { skills, diagnostics };
317
+ }
318
+ /** What an extension's skills folder offers, for the install prompt. */
319
+ export function describeExtensionSkills(folder, signal) {
320
+ // The install prompt lists names only, so display paths are never shown.
321
+ return scanRoots([
322
+ {
323
+ path: folder.directory,
324
+ origin: { kind: "extension", extensionId: folder.extensionId },
325
+ named: true,
326
+ },
327
+ ], { cwd: folder.directory, home: folder.directory }, signal, []);
328
+ }
329
+ /**
330
+ * Discover skills without widening the invocation Files jail. Folders are
331
+ * searched project first, then personal, then installed extensions' (lowest);
332
+ * the first skill with a name wins, so your own skill beats an extension's.
333
+ * `extensions` is already filtered by the extension owner's activation: a
334
+ * folder that is not passed here contributes nothing. Local installed
335
+ * extensions only; Hub-hosted extensions are not covered.
336
+ */
337
+ export function createLocalSkillSource(options) {
338
+ const cwd = resolve(options.cwd);
339
+ const home = resolve(options.home);
340
+ // Skill files are canonical paths, so compare them with canonical roots.
341
+ // A root that does not exist has no symlinks to resolve; any other failure
342
+ // (permissions, loops) propagates to the caller.
343
+ const canonical = async (path) => {
344
+ try {
345
+ return await realpath(path);
346
+ }
347
+ catch (cause) {
348
+ if (absent(cause))
349
+ return path;
350
+ throw cause;
351
+ }
352
+ };
353
+ const catalog = async (signal) => {
354
+ const displayRoots = {
355
+ cwd: await canonical(cwd),
356
+ home: await canonical(home),
357
+ };
358
+ const diagnostics = [];
359
+ const projectExtras = await configuredRoots(join(cwd, ".fungi/settings.json"), cwd, home, signal, diagnostics);
360
+ const personalExtras = await configuredRoots(join(home, ".fungi/settings.json"), join(home, ".fungi"), home, signal, diagnostics);
361
+ const project = { kind: "project" };
362
+ const personal = { kind: "personal" };
363
+ const roots = [
364
+ ...(await discoveryRoots(cwd)).map((path) => ({
365
+ path,
366
+ origin: project,
367
+ named: false,
368
+ })),
369
+ ...projectExtras.map((path) => ({ path, origin: project, named: true })),
370
+ ...[join(home, ".fungi/skills"), join(home, ".agents/skills")].map((path) => ({ path, origin: personal, named: false })),
371
+ ...personalExtras.map((path) => ({
372
+ path,
373
+ origin: personal,
374
+ named: true,
375
+ })),
376
+ ...(options.extensions ?? []).map(({ extensionId, directory }) => ({
377
+ path: directory,
378
+ origin: { kind: "extension", extensionId },
379
+ named: true,
380
+ })),
381
+ ];
382
+ return scanRoots(roots, displayRoots, signal, diagnostics);
307
383
  };
308
384
  return {
309
385
  catalog,
@@ -1,2 +1,9 @@
1
+ export type LocalFailureMetadata = Readonly<{
2
+ source: "local-runtime";
3
+ operation: string;
4
+ failureKind: "Error" | "NonErrorThrown";
5
+ }>;
6
+ /** The only persistable projection of a caught Local failure. */
7
+ export declare function localFailureMetadata(operation: string, cause: unknown): LocalFailureMetadata;
1
8
  /** Retain the original Local failure without printing exception or input text. */
2
9
  export declare function logEvent(operation: string, cause: unknown): void;
package/dist/log-event.js CHANGED
@@ -1,9 +1,13 @@
1
1
  import * as Redacted from "effect/Redacted";
2
- /** Retain the original Local failure without printing exception or input text. */
3
- export function logEvent(operation, cause) {
4
- globalThis.console.error(JSON.stringify({
2
+ /** The only persistable projection of a caught Local failure. */
3
+ export function localFailureMetadata(operation, cause) {
4
+ return {
5
5
  source: "local-runtime",
6
6
  operation,
7
7
  failureKind: cause instanceof Error ? "Error" : "NonErrorThrown",
8
- }), Redacted.make(cause));
8
+ };
9
+ }
10
+ /** Retain the original Local failure without printing exception or input text. */
11
+ export function logEvent(operation, cause) {
12
+ globalThis.console.error(JSON.stringify(localFailureMetadata(operation, cause)), Redacted.make(cause));
9
13
  }
@@ -0,0 +1,2 @@
1
+ /** Local HTTP response budget, checked before text/JSON/schema decoding. */
2
+ export declare function readBoundedResponse(response: Response, maximumBytes: number, description: string): Promise<string>;
@@ -0,0 +1,41 @@
1
+ /** Local HTTP response budget, checked before text/JSON/schema decoding. */
2
+ export async function readBoundedResponse(response, maximumBytes, description) {
3
+ const declared = response.headers.get("content-length");
4
+ if (declared !== null) {
5
+ const declaredBytes = Number(declared);
6
+ if (!Number.isSafeInteger(declaredBytes) ||
7
+ declaredBytes < 0 ||
8
+ declaredBytes > maximumBytes)
9
+ throw new Error(`${description} response is too large`);
10
+ }
11
+ if (response.body === null)
12
+ throw new Error(`${description} returned no body`);
13
+ const stream = response.body.pipeThrough(new TransformStream());
14
+ const reader = stream.getReader();
15
+ const chunks = [];
16
+ let size = 0;
17
+ try {
18
+ for (;;) {
19
+ const next = await reader.read();
20
+ if (next.done)
21
+ break;
22
+ const bytes = next.value;
23
+ size += bytes.byteLength;
24
+ if (size > maximumBytes) {
25
+ await reader.cancel();
26
+ throw new Error(`${description} response is too large`);
27
+ }
28
+ chunks.push(bytes);
29
+ }
30
+ }
31
+ finally {
32
+ reader.releaseLock();
33
+ }
34
+ const joined = new Uint8Array(size);
35
+ let offset = 0;
36
+ for (const chunk of chunks) {
37
+ joined.set(chunk, offset);
38
+ offset += chunk.byteLength;
39
+ }
40
+ return new TextDecoder().decode(joined);
41
+ }
@@ -1,24 +1,46 @@
1
+ import { type MachineEnrollmentRecord, type LocalMachineEnrollment } from "./private-local-mesh-record.js";
1
2
  import type { AuthOperationOptions, Credential, CredentialInfo, CredentialStore } from "@earendil-works/pi-ai";
3
+ import { z } from "zod";
2
4
  export declare const maximumCredentialStringLength: number;
5
+ export declare const oauthCredentialSchema: z.ZodObject<{
6
+ type: z.ZodLiteral<"oauth">;
7
+ refresh: z.ZodString;
8
+ access: z.ZodString;
9
+ expires: z.ZodNumber;
10
+ }, z.core.$catchall<z.ZodJSONSchema>>;
3
11
  export type AtomicWrite = (path: string, content: string, options: Readonly<{
4
12
  encoding: BufferEncoding;
5
13
  fsync: boolean;
6
14
  mode: number;
7
15
  }>) => Promise<void>;
16
+ /** The signed-in credential that must still own an enrollment origin. */
17
+ export type MachineEnrollmentOwner = Readonly<{
18
+ providerId: string;
19
+ /** Throws unless this credential owns the origin being edited. */
20
+ admit: (current: Credential | undefined) => void;
21
+ }>;
8
22
  /** A host-private Pi credential store selected by one absolute filesystem path. */
9
23
  export type PrivateCredentialStore = CredentialStore & {
24
+ readMachineEnrollments(): Promise<MachineEnrollmentRecord[]>;
25
+ /**
26
+ * Edit one origin's enrollments under the file lock. An empty result removes
27
+ * the record. When the origin belongs to a signed-in credential, `owner`
28
+ * admits that credential under the same lock before anything is written.
29
+ */
30
+ modifyMachineEnrollments(origin: string, edit: (current: readonly LocalMachineEnrollment[]) => LocalMachineEnrollment[], owner?: MachineEnrollmentOwner): Promise<void>;
31
+ removeMachineEnrollments(origin: string): Promise<boolean>;
10
32
  /** Stable host identity supplied to Pi's ChatGPT sign-in, never a credential. */
11
33
  getDeviceId(): Promise<string>;
12
34
  /** Revoke or validate current custody under the same lock, then delete it atomically. */
13
35
  remove<T>(providerId: string, beforeRemove: (credential: Credential) => Promise<T>, options?: AuthOperationOptions): Promise<T | undefined>;
14
36
  };
15
37
  /** Resolve the host-owned default path without consulting project or Agent Home state. */
16
- export declare function resolveDefaultCredentialPath(home?: string): string;
38
+ export declare function resolveDefaultCredentialPath(home: string): string;
17
39
  /**
18
40
  * Internal constructor permits one focused write-failure test while the public
19
41
  * package surface remains the Pi CredentialStore factory below.
20
42
  */
21
- export declare class FileCredentialStore implements CredentialStore {
43
+ export declare class FileCredentialStore implements PrivateCredentialStore {
22
44
  #private;
23
45
  constructor(path: string, writeAtomic?: AtomicWrite);
24
46
  /** Keep the installation UUID beside credentials, across logout and restart. */
@@ -27,7 +49,10 @@ export declare class FileCredentialStore implements CredentialStore {
27
49
  list(options?: AuthOperationOptions): Promise<readonly CredentialInfo[]>;
28
50
  modify(providerId: string, callback: (current: Credential | undefined) => Promise<Credential | undefined>, options?: AuthOperationOptions): Promise<Credential | undefined>;
29
51
  delete(providerId: string, options?: AuthOperationOptions): Promise<void>;
52
+ readMachineEnrollments(): Promise<MachineEnrollmentRecord[]>;
53
+ modifyMachineEnrollments(origin: string, edit: (current: readonly LocalMachineEnrollment[]) => LocalMachineEnrollment[], owner?: MachineEnrollmentOwner): Promise<void>;
54
+ removeMachineEnrollments(origin: string): Promise<boolean>;
30
55
  remove<T>(providerId: string, beforeRemove: (credential: Credential) => Promise<T>, options?: AuthOperationOptions): Promise<T | undefined>;
31
56
  }
32
57
  /** Create the one private Pi CredentialStore selected by the local host. */
33
- export declare function createPrivateCredentialStore(path?: string): PrivateCredentialStore;
58
+ export declare function createPrivateCredentialStore(path: string): PrivateCredentialStore;
@@ -1,6 +1,7 @@
1
1
  import { correlationKey } from "@fungi.computer/cairn";
2
+ import { createHash } from "node:crypto";
3
+ import { machineEnrollmentRecordSchema, } from "./private-local-mesh-record.js";
2
4
  import { chmod, mkdir, open } from "node:fs/promises";
3
- import { homedir } from "node:os";
4
5
  import { dirname, isAbsolute, resolve } from "node:path";
5
6
  import lockfile from "proper-lockfile";
6
7
  import writeFileAtomic from "write-file-atomic";
@@ -45,7 +46,7 @@ const apiKeyCredentialSchema = z
45
46
  credential.env = input.env;
46
47
  return credential;
47
48
  });
48
- const oauthCredentialSchema = z
49
+ export const oauthCredentialSchema = z
49
50
  .object({
50
51
  type: z.literal("oauth"),
51
52
  refresh: credentialStringSchema,
@@ -72,10 +73,19 @@ const oauthCredentialSchema = z
72
73
  const credentialSchema = z.discriminatedUnion("type", [
73
74
  apiKeyCredentialSchema,
74
75
  oauthCredentialSchema,
76
+ machineEnrollmentRecordSchema,
75
77
  ]);
78
+ const machineRecordId = (origin) => `machine-enrollment:${createHash("sha256").update(origin).digest("hex")}`;
76
79
  const credentialMapSchema = z
77
80
  .record(providerIdSchema, credentialSchema)
78
81
  .superRefine((credentials, context) => {
82
+ for (const [id, record] of Object.entries(credentials))
83
+ if (record.type === "machine_enrollment" &&
84
+ id !== machineRecordId(record.origin))
85
+ context.addIssue({
86
+ code: "custom",
87
+ message: "Machine enrollment origin does not match its record",
88
+ });
79
89
  if (Object.keys(credentials).length > maximumCredentialEntries) {
80
90
  context.addIssue({
81
91
  code: "custom",
@@ -85,7 +95,7 @@ const credentialMapSchema = z
85
95
  });
86
96
  const missingFileErrorSchema = z.object({ code: z.literal("ENOENT") });
87
97
  /** Resolve the host-owned default path without consulting project or Agent Home state. */
88
- export function resolveDefaultCredentialPath(home = homedir()) {
98
+ export function resolveDefaultCredentialPath(home) {
89
99
  if (!isAbsolute(home)) {
90
100
  throw new Error("Default credential home must be absolute");
91
101
  }
@@ -98,7 +108,8 @@ function ownCredential(credentials, providerId) {
98
108
  if (credentials === undefined || !Object.hasOwn(credentials, providerId)) {
99
109
  return undefined;
100
110
  }
101
- return credentials[providerId];
111
+ const record = credentials[providerId];
112
+ return record?.type === "machine_enrollment" ? undefined : record;
102
113
  }
103
114
  function parseCredentials(serialized) {
104
115
  let parsed;
@@ -172,16 +183,17 @@ export class FileCredentialStore {
172
183
  const credentials = await this.#readCredentials();
173
184
  if (credentials === undefined)
174
185
  return [];
175
- return Object.entries(credentials).map(([providerId, credential]) => ({
176
- providerId,
177
- type: credential.type,
178
- }));
186
+ return Object.entries(credentials).flatMap(([providerId, credential]) => credential.type === "machine_enrollment"
187
+ ? []
188
+ : [{ providerId, type: credential.type }]);
179
189
  }
180
190
  async modify(providerId, callback, options) {
181
191
  options?.signal?.throwIfAborted();
182
192
  return await this.#withLock(async () => {
183
193
  options?.signal?.throwIfAborted();
184
194
  const credentials = (await this.#readCredentials()) ?? {};
195
+ if (credentials[providerId]?.type === "machine_enrollment")
196
+ throw credentialStoreError("A model credential cannot replace a machine enrollment");
185
197
  const current = ownCredential(credentials, providerId);
186
198
  const next = await callback(current);
187
199
  options?.signal?.throwIfAborted();
@@ -202,6 +214,42 @@ export class FileCredentialStore {
202
214
  async delete(providerId, options) {
203
215
  await this.remove(providerId, () => Promise.resolve(), options);
204
216
  }
217
+ async readMachineEnrollments() {
218
+ return Object.values((await this.#readCredentials()) ?? {}).filter((record) => record.type === "machine_enrollment");
219
+ }
220
+ async modifyMachineEnrollments(origin, edit, owner) {
221
+ await this.#withLock(async () => {
222
+ const credentials = (await this.#readCredentials()) ?? {};
223
+ owner?.admit(ownCredential(credentials, owner.providerId));
224
+ const id = machineRecordId(origin);
225
+ const previous = credentials[id];
226
+ if (previous !== undefined && previous.type !== "machine_enrollment")
227
+ throw credentialStoreError("Machine enrollment record is occupied");
228
+ const enrollments = edit(previous?.enrollments ?? []);
229
+ const others = Object.fromEntries(Object.entries(credentials).filter(([key]) => key !== id));
230
+ await this.#replace(this.#validateCredentials(enrollments.length === 0
231
+ ? others
232
+ : {
233
+ ...others,
234
+ [id]: machineEnrollmentRecordSchema.parse({
235
+ type: "machine_enrollment",
236
+ origin,
237
+ enrollments,
238
+ }),
239
+ }));
240
+ });
241
+ }
242
+ async removeMachineEnrollments(origin) {
243
+ return this.#withLock(async () => {
244
+ const credentials = await this.#readCredentials();
245
+ const id = machineRecordId(origin);
246
+ if (credentials?.[id]?.type !== "machine_enrollment")
247
+ return false;
248
+ const next = Object.fromEntries(Object.entries(credentials).filter(([key]) => key !== id));
249
+ await this.#replace(this.#validateCredentials(next));
250
+ return true;
251
+ });
252
+ }
205
253
  async remove(providerId, beforeRemove, options) {
206
254
  options?.signal?.throwIfAborted();
207
255
  return this.#withLock(async () => {
@@ -278,6 +326,6 @@ export class FileCredentialStore {
278
326
  }
279
327
  }
280
328
  /** Create the one private Pi CredentialStore selected by the local host. */
281
- export function createPrivateCredentialStore(path = resolveDefaultCredentialPath()) {
329
+ export function createPrivateCredentialStore(path) {
282
330
  return new FileCredentialStore(path);
283
331
  }