@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,105 @@
1
+ import { Worker } from "node:worker_threads";
2
+ import { imageSize } from "image-size";
3
+ import { z } from "zod";
4
+ /**
5
+ * Local shrinks every image it reads before it becomes an image part, as the
6
+ * retained `read_image` did on the Images binding: at most 512 px on the long
7
+ * edge as a quality-80 JPEG, stepping down to a 256 px floor until the bytes
8
+ * meet the 100 KB budget. Photon (Rust `image` compiled to WebAssembly, inlined
9
+ * in the package) decodes and encodes with no network, no Cloudflare and no
10
+ * per-platform native build.
11
+ *
12
+ * Photon's WebAssembly memory grows to the largest decode and never shrinks,
13
+ * and the Local service is long-running, so each image is decoded in its own
14
+ * worker thread that is terminated afterwards, returning that memory.
15
+ */
16
+ /** Largest image file Local reads to shrink; larger files are refused unread. */
17
+ export const maximumLocalImageInputBytes = 16 * 1_024 * 1_024;
18
+ /**
19
+ * Largest decoded image Local shrinks, checked from the header before any
20
+ * worker starts. 25 megapixels admits 24 MP phone photos (5712 x 4284) and 6K
21
+ * screenshots.
22
+ */
23
+ export const maximumLocalImagePixels = 25_000_000;
24
+ /** At most this many bytes of one image reach the model. */
25
+ export const maximumLocalImageViewBytes = 100 * 1_024;
26
+ /** The retained steps: 512 px long edge, down to a 256 px floor. */
27
+ const viewDimensions = [512, 384, 256];
28
+ const viewQuality = 80;
29
+ /** Built beside this module; the CLI bundle ships it at the same relative path. */
30
+ const workerUrl = new URL("./internal/local-image-fit-worker.mjs", import.meta.url);
31
+ const replySchema = z.discriminatedUnion("kind", [
32
+ z.strictObject({
33
+ kind: z.literal("fitted"),
34
+ bytes: z.instanceof(Uint8Array),
35
+ }),
36
+ z.strictObject({ kind: z.literal("undecodable") }),
37
+ z.strictObject({ kind: z.literal("over-budget") }),
38
+ ]);
39
+ const undecodable = (path) => new Error(`Image ${path} could not be decoded. Refusing to read it.`);
40
+ /** Read the header with `image-size`; a header it cannot parse is refused. */
41
+ function header(path, bytes) {
42
+ try {
43
+ return imageSize(bytes);
44
+ }
45
+ catch {
46
+ throw undecodable(path);
47
+ }
48
+ }
49
+ /** Run one throwaway worker over `bytes`; it is terminated however it ends. */
50
+ async function shrinkInWorker(bytes, orientation, signal) {
51
+ signal.throwIfAborted();
52
+ const worker = new Worker(workerUrl, {
53
+ workerData: {
54
+ bytes,
55
+ orientation,
56
+ dimensions: viewDimensions,
57
+ quality: viewQuality,
58
+ maxBytes: maximumLocalImageViewBytes,
59
+ },
60
+ });
61
+ // Ends the abort listener with the worker.
62
+ const settled = new AbortController();
63
+ try {
64
+ return await new Promise((resolve, reject) => {
65
+ signal.addEventListener("abort", () => {
66
+ reject(new Error("Local image read was cancelled", {
67
+ cause: signal.reason,
68
+ }));
69
+ }, { once: true, signal: settled.signal });
70
+ worker.once("message", (message) => {
71
+ const reply = replySchema.safeParse(message);
72
+ if (reply.success)
73
+ resolve(reply.data);
74
+ else
75
+ reject(new Error("Local image worker sent an invalid reply"));
76
+ });
77
+ worker.once("error", reject);
78
+ worker.once("exit", (code) => {
79
+ reject(new Error(`Local image worker exited (${String(code)}) before replying`));
80
+ });
81
+ });
82
+ }
83
+ finally {
84
+ settled.abort();
85
+ await worker.terminate();
86
+ }
87
+ }
88
+ /**
89
+ * Shrink one whole png, jpeg, gif (first frame) or webp file for the model.
90
+ * Refuses, with the Files read error, an image whose header or pixels cannot
91
+ * be decoded, one above {@link maximumLocalImagePixels}, and one still above
92
+ * {@link maximumLocalImageViewBytes} at the 256 px floor. Never passes the
93
+ * original bytes through.
94
+ */
95
+ export async function fitLocalImage(path, bytes, signal) {
96
+ const { width, height, orientation } = header(path, bytes);
97
+ if (width * height > maximumLocalImagePixels)
98
+ throw new Error(`Image ${path} is ${String(width)} x ${String(height)} pixels; the read limit is ${String(maximumLocalImagePixels)} pixels. Refusing to read it.`);
99
+ const reply = await shrinkInWorker(bytes, orientation, signal);
100
+ if (reply.kind === "fitted")
101
+ return reply.bytes;
102
+ if (reply.kind === "undecodable")
103
+ throw undecodable(path);
104
+ throw new Error(`Image ${path} does not fit ${String(maximumLocalImageViewBytes)} bytes at ${String(viewDimensions.at(-1))} px. Refusing to read it.`);
105
+ }
@@ -0,0 +1,55 @@
1
+ import type { AuthInteraction, Provider } from "@earendil-works/pi-ai";
2
+ import type { WhistleInteraction } from "@fungi.computer/whistle";
3
+ import type { PrivateLocalServiceManager } from "./private-local-service.js";
4
+ /** Display order for popular model providers; labels remain owned by Pi. */
5
+ export declare const localPopularProviderIds: readonly string[];
6
+ /** One sign-in method, in the provider registry's vocabulary. */
7
+ export type LocalLoginMethod = Readonly<{
8
+ kind: "api_key" | "subscription";
9
+ label: string;
10
+ }>;
11
+ /** One provider Local can sign in to, derived from Pi's provider data. */
12
+ export type LocalLoginProvider = Readonly<{
13
+ id: string;
14
+ name: string;
15
+ methods: readonly LocalLoginMethod[];
16
+ }>;
17
+ /**
18
+ * Describe Pi's providers as registry data. Pi's OAuth sign-in is the
19
+ * provider's consumer subscription; ambient-only API keys (no interactive
20
+ * setup) are not offered. Callers pass the policy-filtered collection from
21
+ * `createPrivateModels`.
22
+ */
23
+ export declare function localLoginProviders(providers: readonly Provider[]): readonly LocalLoginProvider[];
24
+ /**
25
+ * Present Pi's provider sign-in through a Whistle interaction. `option`
26
+ * answers Pi's own method question (e.g. `device_code`) without asking. If
27
+ * the flow's first event -- a prompt or a notification -- is not a `select`
28
+ * offering that option, the flow never asks the question at all (e.g.
29
+ * Anthropic, which has only one sign-in method); cancel before relaying
30
+ * anything to the host, so no browser URL or other handoff is ever shown
31
+ * for a method the provider silently ignored.
32
+ */
33
+ export declare function authInteractionFor(interaction: WhistleInteraction, option?: string): AuthInteraction;
34
+ /** The Local credential owner behind provider-agnostic account commands. */
35
+ export declare function localAccountOwner(manager: Pick<PrivateLocalServiceManager, "auth">, providers: () => readonly Provider[]): {
36
+ popularProviderIds: readonly string[];
37
+ access: (signal: AbortSignal) => Promise<Readonly<{
38
+ providerId: string;
39
+ providerName: string;
40
+ state: "configured";
41
+ type: import("@earendil-works/pi-ai").AuthType;
42
+ }>[]>;
43
+ providers: () => Promise<readonly Readonly<{
44
+ id: string;
45
+ name: string;
46
+ methods: readonly LocalLoginMethod[];
47
+ }>[]>;
48
+ login: (request: Readonly<{
49
+ providerId: string;
50
+ method: LocalLoginMethod["kind"];
51
+ option?: string;
52
+ }>, interaction: WhistleInteraction) => Promise<void>;
53
+ logout: (providerId: string, interaction: WhistleInteraction) => Promise<void>;
54
+ status: (signal: AbortSignal) => Promise<string>;
55
+ };
@@ -0,0 +1,167 @@
1
+ /** Display order for popular model providers; labels remain owned by Pi. */
2
+ export const localPopularProviderIds = Object.freeze([
3
+ "fungi",
4
+ "openai",
5
+ "anthropic",
6
+ "deepseek",
7
+ "kimi-coding",
8
+ "openrouter",
9
+ ]);
10
+ /**
11
+ * Describe Pi's providers as registry data. Pi's OAuth sign-in is the
12
+ * provider's consumer subscription; ambient-only API keys (no interactive
13
+ * setup) are not offered. Callers pass the policy-filtered collection from
14
+ * `createPrivateModels`.
15
+ */
16
+ export function localLoginProviders(providers) {
17
+ return providers.flatMap((provider) => {
18
+ const methods = [];
19
+ if (provider.auth.apiKey?.login !== undefined)
20
+ methods.push({ kind: "api_key", label: provider.auth.apiKey.name });
21
+ if (provider.auth.oauth !== undefined)
22
+ methods.push({
23
+ kind: "subscription",
24
+ label: provider.auth.oauth.loginLabel ?? provider.auth.oauth.name,
25
+ });
26
+ return methods.length === 0
27
+ ? []
28
+ : [{ id: provider.id, name: provider.name, methods }];
29
+ });
30
+ }
31
+ function notice(event) {
32
+ switch (event.type) {
33
+ case "auth_url":
34
+ return event.instructions === undefined
35
+ ? { type: "handoff", url: event.url }
36
+ : { type: "handoff", url: event.url, message: event.instructions };
37
+ case "device_code":
38
+ return {
39
+ type: "handoff",
40
+ url: event.verificationUri,
41
+ code: event.userCode,
42
+ };
43
+ case "info":
44
+ return event.links === undefined
45
+ ? { type: "info", message: event.message }
46
+ : {
47
+ type: "info",
48
+ message: event.message,
49
+ links: event.links.map((link) => link.label === undefined
50
+ ? { url: link.url }
51
+ : { url: link.url, label: link.label }),
52
+ };
53
+ case "progress":
54
+ return { type: "progress", message: event.message };
55
+ }
56
+ }
57
+ async function ask(interaction, prompt) {
58
+ const options = prompt.signal === undefined ? {} : { signal: prompt.signal };
59
+ switch (prompt.type) {
60
+ case "select":
61
+ return interaction.prompt({
62
+ type: "choice",
63
+ message: prompt.message,
64
+ options: prompt.options.map((option) => option.description === undefined
65
+ ? { value: option.id, label: option.label }
66
+ : {
67
+ value: option.id,
68
+ label: option.label,
69
+ description: option.description,
70
+ }),
71
+ }, options);
72
+ case "manual_code": {
73
+ // Generic across every provider that falls back to a pasted code or
74
+ // redirect URL (OpenAI, Anthropic): the provider's own
75
+ // message explains what to paste; this adds the SSH hint once, here,
76
+ // instead of baking it into one provider's registry label.
77
+ const message = `${prompt.message} If you're connecting over SSH, paste the URL here instead of opening a browser.`;
78
+ return interaction.prompt(prompt.placeholder === undefined
79
+ ? { type: "text", message }
80
+ : { type: "text", message, placeholder: prompt.placeholder }, options);
81
+ }
82
+ case "secret":
83
+ case "text":
84
+ return interaction.prompt(prompt.placeholder === undefined
85
+ ? {
86
+ type: "text",
87
+ message: prompt.message,
88
+ secret: prompt.type === "secret",
89
+ }
90
+ : {
91
+ type: "text",
92
+ message: prompt.message,
93
+ placeholder: prompt.placeholder,
94
+ secret: prompt.type === "secret",
95
+ }, options);
96
+ }
97
+ }
98
+ const methodNotOffered = "The provider did not offer that sign-in method";
99
+ /**
100
+ * Present Pi's provider sign-in through a Whistle interaction. `option`
101
+ * answers Pi's own method question (e.g. `device_code`) without asking. If
102
+ * the flow's first event -- a prompt or a notification -- is not a `select`
103
+ * offering that option, the flow never asks the question at all (e.g.
104
+ * Anthropic, which has only one sign-in method); cancel before relaying
105
+ * anything to the host, so no browser URL or other handoff is ever shown
106
+ * for a method the provider silently ignored.
107
+ */
108
+ export function authInteractionFor(interaction, option) {
109
+ let preset = option;
110
+ return {
111
+ signal: interaction.signal,
112
+ prompt: (prompt) => {
113
+ if (preset === undefined)
114
+ return ask(interaction, prompt);
115
+ if (prompt.type !== "select") {
116
+ preset = undefined;
117
+ return Promise.reject(new Error(methodNotOffered));
118
+ }
119
+ const chosen = prompt.options.find((one) => one.id === preset);
120
+ preset = undefined;
121
+ if (chosen === undefined)
122
+ return Promise.reject(new Error(methodNotOffered));
123
+ return Promise.resolve(chosen.id);
124
+ },
125
+ notify: (event) => {
126
+ if (preset !== undefined) {
127
+ preset = undefined;
128
+ throw new Error(methodNotOffered);
129
+ }
130
+ interaction.notify(notice(event));
131
+ },
132
+ };
133
+ }
134
+ /** The Local credential owner behind provider-agnostic account commands. */
135
+ export function localAccountOwner(manager, providers) {
136
+ const access = async (signal) => {
137
+ const result = await manager.auth({ command: "status" }, {
138
+ signal,
139
+ prompt: () => Promise.reject(new Error("Account status cannot prompt")),
140
+ notify: () => undefined,
141
+ });
142
+ if (result.kind !== "status")
143
+ throw new Error("Account status returned a non-status result");
144
+ return result.providers.filter((provider) => provider.state === "configured");
145
+ };
146
+ return {
147
+ popularProviderIds: localPopularProviderIds,
148
+ access,
149
+ providers: () => Promise.resolve(localLoginProviders(providers())),
150
+ login: async (request, interaction) => {
151
+ await manager.auth({
152
+ command: "login",
153
+ provider: request.providerId,
154
+ authType: request.method === "subscription" ? "oauth" : "api_key",
155
+ }, authInteractionFor(interaction, request.option));
156
+ },
157
+ logout: async (providerId, interaction) => {
158
+ await manager.auth({ command: "logout", provider: providerId }, authInteractionFor(interaction));
159
+ },
160
+ status: async (signal) => {
161
+ const configured = (await access(signal)).map((provider) => `${provider.providerName}: configured (${provider.type})`);
162
+ return configured.length === 0
163
+ ? "No model provider is signed in. Use /login to add one."
164
+ : configured.join("\n");
165
+ },
166
+ };
167
+ }
@@ -0,0 +1,16 @@
1
+ import { type MutableModels, type Provider, type ThinkingLevel } from "@earendil-works/pi-ai";
2
+ /** Register Pi's standalone OAuth modules on the same instance Local uses. */
3
+ export declare function registerPrivateBundledModelOAuthFlows(): void;
4
+ /** Create the private local Pi collection with Pi's policy-permitted providers. */
5
+ export declare function createPrivateModels(credentialPath?: string, providerOptions?: {
6
+ providers?: readonly Provider[];
7
+ }): MutableModels;
8
+ export type PrivateLocalModelChoice = Readonly<{
9
+ providerId: string;
10
+ providerName: string;
11
+ id: string;
12
+ name: string;
13
+ reasoning: readonly ThinkingLevel[];
14
+ }>;
15
+ /** Refresh configured dynamic catalogs before exposing the bounded owner picker. */
16
+ export declare function listPrivateModels(credentialPath?: string): Promise<readonly PrivateLocalModelChoice[]>;
@@ -0,0 +1,82 @@
1
+ import { createModels, getSupportedThinkingLevels, } from "@earendil-works/pi-ai";
2
+ import { builtinProviders } from "@earendil-works/pi-ai/providers/all";
3
+ import { registerBunOAuthFlows } from "@earendil-works/pi-ai/bun-oauth";
4
+ import { loginAllowed, subscriptionKillSwitchFrom, } from "@fungi.computer/veil/model-credentials";
5
+ import { createFungiAccountCredentialStore } from "./private-fungi-account.js";
6
+ import { createFungiModelProvider } from "./private-fungi-model-provider.js";
7
+ import { retainSubscriptionIdentity } from "./subscription-identity.js";
8
+ import { registerSubscriptionTurnOwner } from "./subscription-turn.js";
9
+ import { capturePrivateLocalConfigRoot } from "./private-local-config-root.js";
10
+ /** Register Pi's standalone OAuth modules on the same instance Local uses. */
11
+ export function registerPrivateBundledModelOAuthFlows() {
12
+ registerBunOAuthFlows();
13
+ }
14
+ /** Local credentials belong to the one person who owns this machine. */
15
+ const allowedHere = (providerId, method) => loginAllowed({ providerId, method, host: "local", scope: "user" },
16
+ // FUNGI_SUBSCRIPTION_KILL=anthropic,openai turns logins off without a release.
17
+ subscriptionKillSwitchFrom(process.env.FUNGI_SUBSCRIPTION_KILL));
18
+ /** Keep only the login methods Veil's provider policy allows on this machine. */
19
+ function locallyPermitted(provider) {
20
+ const apiKey = allowedHere(provider.id, "api_key")
21
+ ? provider.auth.apiKey
22
+ : undefined;
23
+ const oauth = allowedHere(provider.id, "subscription")
24
+ ? provider.auth.oauth
25
+ : undefined;
26
+ if (apiKey === undefined && oauth === undefined)
27
+ return undefined;
28
+ const auth = {};
29
+ if (apiKey !== undefined)
30
+ auth.apiKey = apiKey;
31
+ if (oauth !== undefined)
32
+ auth.oauth =
33
+ provider.id === "anthropic" || provider.id === "openai"
34
+ ? retainSubscriptionIdentity(oauth, provider.id)
35
+ : oauth;
36
+ return { ...provider, auth };
37
+ }
38
+ /** Create the private local Pi collection with Pi's policy-permitted providers. */
39
+ export function createPrivateModels(credentialPath = capturePrivateLocalConfigRoot().credentialPath, providerOptions = {}) {
40
+ const options = { credentialPath };
41
+ const credentials = createFungiAccountCredentialStore(options);
42
+ const models = createModels({ credentials });
43
+ registerSubscriptionTurnOwner(models, credentials);
44
+ for (const provider of providerOptions.providers ?? builtinProviders()) {
45
+ const permitted = locallyPermitted(provider);
46
+ if (permitted !== undefined)
47
+ models.setProvider(permitted);
48
+ }
49
+ const fungi = locallyPermitted(createFungiModelProvider(options));
50
+ if (fungi !== undefined)
51
+ models.setProvider(fungi);
52
+ return models;
53
+ }
54
+ /** Refresh configured dynamic catalogs before exposing the bounded owner picker. */
55
+ export async function listPrivateModels(credentialPath) {
56
+ const models = createPrivateModels(credentialPath);
57
+ const result = await models.refresh();
58
+ if (result.aborted)
59
+ throw new Error("Model catalog refresh was cancelled");
60
+ const fungiFailure = result.errors.get("fungi");
61
+ if (fungiFailure !== undefined)
62
+ throw fungiFailure;
63
+ if (result.errors.size > 0)
64
+ throw new AggregateError([...result.errors.values()], "Could not refresh model catalogs");
65
+ return Object.freeze(models
66
+ .getProviders()
67
+ .flatMap((provider) => models.getModels(provider.id).map((model) => ({
68
+ providerId: provider.id,
69
+ providerName: provider.name,
70
+ id: model.id,
71
+ name: model.name,
72
+ reasoning: getSupportedThinkingLevels(model).filter((level) => level !== "off"),
73
+ })))
74
+ .sort((left, right) => left.providerName.localeCompare(right.providerName) ||
75
+ left.name.localeCompare(right.name) ||
76
+ left.providerId.localeCompare(right.providerId) ||
77
+ left.id.localeCompare(right.id))
78
+ .map((choice) => Object.freeze({
79
+ ...choice,
80
+ reasoning: Object.freeze(choice.reasoning),
81
+ })));
82
+ }
@@ -0,0 +1,8 @@
1
+ import type { ArchiveWriter } from "@fungi.computer/zookeeper";
2
+ /**
3
+ * Plain archive files beside the Agent store, never synced through Home:
4
+ * `<storeDirectory>/archives/<sessionId>/<contextRevision>-<materialKey>`.
5
+ * Each archive is streamed to a temp file, fsynced, then hard-linked into
6
+ * place so an existing archive is never overwritten.
7
+ */
8
+ export declare function localArchiveWriter(storeDirectory: string, syncDirectoryPort?: (directory: string) => Promise<void>): ArchiveWriter;
@@ -0,0 +1,152 @@
1
+ import { createHash } from "node:crypto";
2
+ import { link, mkdir, open, rm } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { archiveSessionSegment } from "@fungi.computer/zookeeper";
5
+ import { nanoid } from "@fungi.computer/cairn";
6
+ import { Effect } from "effect";
7
+ import { z } from "zod";
8
+ const MAX_CHUNK_BYTES = 64 * 1024;
9
+ const SAFE_SEGMENT = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/;
10
+ const SHA256_HEX = /^[0-9a-f]{64}$/;
11
+ /** Internal control flow: carries the reason a write was refused. */
12
+ class ArchiveRefusal extends Error {
13
+ reason;
14
+ constructor(reason) {
15
+ super(`archive write refused: ${reason}`);
16
+ this.reason = reason;
17
+ }
18
+ }
19
+ const keyTakenSchema = z.object({ code: z.literal("EEXIST") });
20
+ const refusalSchema = z.instanceof(ArchiveRefusal);
21
+ function validate(input) {
22
+ const segments = [
23
+ archiveSessionSegment(input.sessionId),
24
+ input.contextRevision,
25
+ input.materialKey,
26
+ ];
27
+ if (!segments.every((segment) => SAFE_SEGMENT.test(segment)) ||
28
+ !SHA256_HEX.test(input.sha256) ||
29
+ input.materialKey !== input.sha256.slice(0, 32) ||
30
+ !Number.isSafeInteger(input.byteLength) ||
31
+ input.byteLength < 0) {
32
+ throw new ArchiveRefusal("invalid");
33
+ }
34
+ }
35
+ /** Stream the chunks into an open file, then check them against the declaration. */
36
+ async function writeVerified(path, input, signal) {
37
+ const handle = await open(path, "wx", 0o600);
38
+ try {
39
+ const hash = createHash("sha256");
40
+ let total = 0;
41
+ for await (const chunk of input.chunks()) {
42
+ signal.throwIfAborted();
43
+ if (chunk.byteLength > MAX_CHUNK_BYTES) {
44
+ throw new ArchiveRefusal("invalid");
45
+ }
46
+ hash.update(chunk);
47
+ total += chunk.byteLength;
48
+ let written = 0;
49
+ while (written < chunk.byteLength) {
50
+ const result = await handle.write(chunk, written);
51
+ written += result.bytesWritten;
52
+ }
53
+ }
54
+ if (total !== input.byteLength || hash.digest("hex") !== input.sha256) {
55
+ throw new ArchiveRefusal("invalid");
56
+ }
57
+ await handle.sync();
58
+ }
59
+ finally {
60
+ await handle.close();
61
+ }
62
+ }
63
+ async function syncDirectory(directory) {
64
+ const handle = await open(directory, "r");
65
+ try {
66
+ await handle.sync();
67
+ }
68
+ finally {
69
+ await handle.close();
70
+ }
71
+ }
72
+ async function existingMatches(path, input) {
73
+ const handle = await open(path, "r");
74
+ try {
75
+ const hash = createHash("sha256");
76
+ const buffer = Buffer.alloc(MAX_CHUNK_BYTES);
77
+ let total = 0;
78
+ for (;;) {
79
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
80
+ if (bytesRead === 0) {
81
+ break;
82
+ }
83
+ hash.update(buffer.subarray(0, bytesRead));
84
+ total += bytesRead;
85
+ }
86
+ return total === input.byteLength && hash.digest("hex") === input.sha256;
87
+ }
88
+ finally {
89
+ await handle.close();
90
+ }
91
+ }
92
+ /** Hard-link the finished temp file into place; false when the key is taken. */
93
+ async function linkIntoPlace(tempPath, finalPath) {
94
+ try {
95
+ await link(tempPath, finalPath);
96
+ return true;
97
+ }
98
+ catch (error) {
99
+ if (keyTakenSchema.safeParse(error).success) {
100
+ return false;
101
+ }
102
+ throw error;
103
+ }
104
+ }
105
+ async function writeArchiveFile(storeDirectory, input, signal, syncDirectoryPort) {
106
+ validate(input);
107
+ const name = `${input.contextRevision}-${input.materialKey}`;
108
+ const session = archiveSessionSegment(input.sessionId);
109
+ const directory = join(storeDirectory, "archives", session);
110
+ const finalPath = join(directory, name);
111
+ const tempPath = join(directory, `.${input.materialKey}.${nanoid()}.tmp`);
112
+ const receipt = {
113
+ key: `archives/${session}/${name}`,
114
+ sha256: input.sha256,
115
+ byteLength: input.byteLength,
116
+ };
117
+ await mkdir(directory, { recursive: true, mode: 0o700 });
118
+ try {
119
+ await writeVerified(tempPath, input, signal);
120
+ const created = await linkIntoPlace(tempPath, finalPath);
121
+ if (!created && !(await existingMatches(finalPath, input))) {
122
+ throw new ArchiveRefusal("conflict");
123
+ }
124
+ // A verified archive stays even when the directory fsync fails: a retry
125
+ // finds it by link and returns this same receipt.
126
+ await syncDirectoryPort(directory);
127
+ return receipt;
128
+ }
129
+ finally {
130
+ await rm(tempPath, { force: true });
131
+ }
132
+ }
133
+ /**
134
+ * Plain archive files beside the Agent store, never synced through Home:
135
+ * `<storeDirectory>/archives/<sessionId>/<contextRevision>-<materialKey>`.
136
+ * Each archive is streamed to a temp file, fsynced, then hard-linked into
137
+ * place so an existing archive is never overwritten.
138
+ */
139
+ export function localArchiveWriter(storeDirectory, syncDirectoryPort = syncDirectory) {
140
+ return {
141
+ write: (input) => Effect.tryPromise({
142
+ try: (signal) => writeArchiveFile(storeDirectory, input, signal, syncDirectoryPort),
143
+ catch: (error) => {
144
+ const refusal = refusalSchema.safeParse(error);
145
+ return {
146
+ _tag: "ArchiveWriteError",
147
+ reason: refusal.success ? refusal.data.reason : "unavailable",
148
+ };
149
+ },
150
+ }),
151
+ };
152
+ }
@@ -0,0 +1,21 @@
1
+ import type { SkillCatalog, SkillSource } from "@fungi.computer/shiitake-extensions/skills/source";
2
+ /** An enabled extension's skills folder, as the extension owner reports it. */
3
+ export type ExtensionSkillFolder = Readonly<{
4
+ extensionId: string;
5
+ directory: string;
6
+ }>;
7
+ /** What an extension's skills folder offers, for the install prompt. */
8
+ export declare function describeExtensionSkills(folder: ExtensionSkillFolder, signal: AbortSignal): Promise<SkillCatalog>;
9
+ /**
10
+ * Discover skills without widening the invocation Files jail. Folders are
11
+ * searched project first, then personal, then installed extensions' (lowest);
12
+ * the first skill with a name wins, so your own skill beats an extension's.
13
+ * `extensions` is already filtered by the extension owner's activation: a
14
+ * folder that is not passed here contributes nothing. Local installed
15
+ * extensions only; Hub-hosted extensions are not covered.
16
+ */
17
+ export declare function createLocalSkillSource(options: Readonly<{
18
+ cwd: string;
19
+ home: string;
20
+ extensions?: readonly ExtensionSkillFolder[];
21
+ }>): SkillSource;