@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,5 @@
1
+ /** Shared refusal for Fungi login refresh, model calls and MCP connections. */
2
+ export declare class FungiSignInRequiredError extends Error {
3
+ readonly name = "FungiSignInRequiredError";
4
+ constructor(options?: ErrorOptions);
5
+ }
@@ -0,0 +1,7 @@
1
+ /** Shared refusal for Fungi login refresh, model calls and MCP connections. */
2
+ export class FungiSignInRequiredError extends Error {
3
+ name = "FungiSignInRequiredError";
4
+ constructor(options) {
5
+ super("Sign in to Fungi again to use models and Computers", options);
6
+ }
7
+ }
@@ -0,0 +1,26 @@
1
+ import { type AuthProvider, type FetchLike, type OAuthClientProvider } from "@modelcontextprotocol/client";
2
+ import { type CapabilitySource } from "@fungi.computer/mycelium";
3
+ import { type RequestMcpFormApproval } from "./private-mcp-form-elicitation.js";
4
+ /** The MCP client identity Local reports to the Hub. */
5
+ export declare const fungiMcpClientInfo: Readonly<{
6
+ name: "fungi-local";
7
+ version: string;
8
+ }>;
9
+ export declare function rejectedFungiMcpAccess(response: Response): string | undefined;
10
+ export declare function fungiMcpReachableFetch(endpoint: URL, fetchImpl?: FetchLike): FetchLike;
11
+ /** Host-held selection and credentials; neither enters the model's arguments. */
12
+ export type PrivateFungiMcpConnectionOptions = Readonly<{
13
+ endpoint: URL;
14
+ authentication?: "oyster-key" | undefined;
15
+ authProvider: AuthProvider | OAuthClientProvider;
16
+ /** The maintained transport's fetch seam, also used by the local caller proof. */
17
+ fetch?: FetchLike;
18
+ /** Trusted human interaction; never supplied by model arguments. */
19
+ requestApproval?: RequestMcpFormApproval | undefined;
20
+ }>;
21
+ /**
22
+ * Add the existing Fungi MCP execute door to a Local run's capability scope.
23
+ * The remote server selects and authorizes permitted Team Computers per request.
24
+ * Computer discovery and operations remain inside the maintained execute door.
25
+ */
26
+ export declare function createPrivateFungiMcpConnection(options: PrivateFungiMcpConnectionOptions): CapabilitySource;
@@ -0,0 +1,162 @@
1
+ import { FungiSignInRequiredError } from "./private-fungi-auth-error.js";
2
+ import { createRequire } from "node:module";
3
+ import { Client, StreamableHTTPClientTransport, UnauthorizedError, } from "@modelcontextprotocol/client";
4
+ import { Mycelium } from "@fungi.computer/mycelium";
5
+ import { z } from "zod";
6
+ import { parseFungiMcpEndpoint } from "./private-fungi-mcp-endpoint.js";
7
+ import { registerMcpFormElicitation, McpFormApprovalError, } from "./private-mcp-form-elicitation.js";
8
+ const localPackage = z
9
+ .looseObject({ version: z.string().min(1) })
10
+ .parse(createRequire(import.meta.url)("../package.json"));
11
+ /** The MCP client identity Local reports to the Hub. */
12
+ export const fungiMcpClientInfo = Object.freeze({
13
+ name: "fungi-local",
14
+ version: localPackage.version,
15
+ });
16
+ /**
17
+ * Fetch rejects (rather than resolving with an HTTP status) only when the Hub
18
+ * was not reached. Keep TypeError, which the maintained SDK treats as a
19
+ * network failure, and name the origin the person has to reach.
20
+ */
21
+ class FungiMcpUnreachableError extends TypeError {
22
+ constructor(origin, cause) {
23
+ super(`Can't reach ${origin}${cause instanceof Error ? ` (${cause.message})` : ""}. Check your connection and try again.`, { cause });
24
+ this.name = "FungiMcpUnreachableError";
25
+ }
26
+ }
27
+ // SDK UnauthorizedContext carries the Response, not the rejected request token.
28
+ // Pair them at the transport seam so late concurrent 401s cannot select a newer token.
29
+ const rejectedAccess = new WeakMap();
30
+ export function rejectedFungiMcpAccess(response) {
31
+ return rejectedAccess.get(response);
32
+ }
33
+ export function fungiMcpReachableFetch(endpoint, fetchImpl = fetch) {
34
+ return async (url, init) => {
35
+ const target = new URL(url instanceof Request ? url.url : String(url));
36
+ if (target.origin !== endpoint.origin)
37
+ throw new Error("Refusing an MCP request outside the selected origin");
38
+ try {
39
+ const response = await fetchImpl(url, { ...init, redirect: "error" });
40
+ const authorization = new Headers(init?.headers).get("Authorization");
41
+ if (response.status === 401 && authorization?.startsWith("Bearer "))
42
+ rejectedAccess.set(response, authorization.slice(7));
43
+ return response;
44
+ }
45
+ catch (cause) {
46
+ if (init?.signal?.aborted === true)
47
+ throw cause;
48
+ throw new FungiMcpUnreachableError(endpoint.origin, cause);
49
+ }
50
+ };
51
+ }
52
+ /**
53
+ * Add the existing Fungi MCP execute door to a Local run's capability scope.
54
+ * The remote server selects and authorizes permitted Team Computers per request.
55
+ * Computer discovery and operations remain inside the maintained execute door.
56
+ */
57
+ export function createPrivateFungiMcpConnection(options) {
58
+ const endpoint = parseFungiMcpEndpoint(options.endpoint);
59
+ const fetch = fungiMcpReachableFetch(endpoint, options.fetch);
60
+ return {
61
+ id: "fungi-mcp",
62
+ acquire: async ({ signal, session }) => {
63
+ signal.throwIfAborted();
64
+ const client = new Client(fungiMcpClientInfo);
65
+ const forms = registerMcpFormElicitation(client, options.requestApproval, session?.sessionId);
66
+ const transportOptions = {
67
+ authProvider: options.authProvider,
68
+ fetch,
69
+ requestInit: { redirect: "error" },
70
+ };
71
+ const transport = new StreamableHTTPClientTransport(endpoint, transportOptions);
72
+ try {
73
+ await client.connect(transport, { signal });
74
+ signal.throwIfAborted();
75
+ return {
76
+ snapshot: {
77
+ version: "fungi-mcp-execute-v1",
78
+ modules: [
79
+ Mycelium.module({
80
+ id: "fungi-mcp",
81
+ name: "fungiMcp",
82
+ description: "The explicitly connected Fungi MCP server.",
83
+ operations: {
84
+ execute: Mycelium.operation({
85
+ replay: "unsafe",
86
+ description: "Execute JavaScript on the connected Fungi MCP server. Discover permitted Team Computer operations with await forage.search({query: 'read'}) and await forage.describe({path: 'files.read'}). Explicitly return the result. Returns the original MCP content and isError; it does not run on the local machine.",
87
+ input: z.strictObject({ code: z.string() }),
88
+ execute: async (input, context) => {
89
+ try {
90
+ return z
91
+ .json()
92
+ .parse(await forms.run(context.signal, (signal) => client.callTool({ name: "execute", arguments: input }, { signal })));
93
+ }
94
+ catch (cause) {
95
+ if (cause instanceof McpFormApprovalError)
96
+ return {
97
+ isError: true,
98
+ content: [{ type: "text", text: cause.message }],
99
+ structuredContent: {
100
+ kind: cause.kind,
101
+ reason: cause.reason,
102
+ },
103
+ };
104
+ // Report uncertainty as a failed MCP result so the caller
105
+ // can distinguish it from a successful remote result.
106
+ // Replaying arbitrary JavaScript here could duplicate work.
107
+ const httpFailure = z
108
+ .looseObject({ status: z.number() })
109
+ .safeParse(cause);
110
+ if (httpFailure.success &&
111
+ httpFailure.data.status !== 401)
112
+ throw cause;
113
+ // A 401 is the Hub refusing before execution: the
114
+ // outcome is known, and reconnecting will not help.
115
+ if (cause instanceof UnauthorizedError ||
116
+ cause instanceof FungiSignInRequiredError ||
117
+ (httpFailure.success &&
118
+ httpFailure.data.status === 401))
119
+ return {
120
+ content: [
121
+ {
122
+ type: "text",
123
+ text: `Fungi MCP refused this call because its sign-in or Team key is missing, expired or revoked; the code did not run. Run fungi login --fungi-mcp ${endpoint.href} or use a current key.`,
124
+ },
125
+ ],
126
+ isError: true,
127
+ };
128
+ return {
129
+ content: [
130
+ {
131
+ type: "text",
132
+ text: "Fungi MCP connection was interrupted during an operation; outcome unknown. Reconnect and inspect Computer state before retrying.",
133
+ },
134
+ ],
135
+ isError: true,
136
+ };
137
+ }
138
+ },
139
+ }),
140
+ },
141
+ }),
142
+ ],
143
+ },
144
+ release: () => {
145
+ forms.close();
146
+ return client.close();
147
+ },
148
+ };
149
+ }
150
+ catch (error) {
151
+ forms.close();
152
+ await client.close();
153
+ if (error instanceof UnauthorizedError)
154
+ throw error;
155
+ if (error instanceof FungiSignInRequiredError ||
156
+ z.object({ status: z.literal(401) }).safeParse(error).success)
157
+ throw new Error(`Fungi MCP sign-in is missing, expired or revoked; run fungi login --fungi-mcp ${endpoint.href}`, { cause: error });
158
+ throw error;
159
+ }
160
+ },
161
+ };
162
+ }
@@ -0,0 +1,2 @@
1
+ /** The Team MCP address is HTTPS, except for an exact loopback host. */
2
+ export declare function parseFungiMcpEndpoint(value: string | URL): URL;
@@ -0,0 +1,15 @@
1
+ /** The Team MCP address is HTTPS, except for an exact loopback host. */
2
+ export function parseFungiMcpEndpoint(value) {
3
+ const endpoint = new URL(value);
4
+ const loopback = endpoint.hostname === "localhost" ||
5
+ endpoint.hostname === "127.0.0.1" ||
6
+ endpoint.hostname === "[::1]";
7
+ if (!(endpoint.protocol === "https:" ||
8
+ (endpoint.protocol === "http:" && loopback)) ||
9
+ endpoint.username ||
10
+ endpoint.password ||
11
+ endpoint.search ||
12
+ endpoint.hash)
13
+ throw new Error("Fungi MCP requires HTTPS (or HTTP on an exact loopback host) without URL credentials, query or fragment");
14
+ return endpoint;
15
+ }
@@ -0,0 +1,7 @@
1
+ import { z } from "zod";
2
+ /** A project selects a target and auth kind; credential material stays with Local. */
3
+ export declare const fungiMcpSelectionSchema: z.ZodObject<{
4
+ endpoint: z.ZodPipe<z.ZodURL, z.ZodString>;
5
+ authentication: z.ZodOptional<z.ZodLiteral<"oyster-key">>;
6
+ }, z.core.$strict>;
7
+ export type PrivateFungiMcpSelection = z.infer<typeof fungiMcpSelectionSchema>;
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ import { parseFungiMcpEndpoint } from "./private-fungi-mcp-endpoint.js";
3
+ /** A project selects a target and auth kind; credential material stays with Local. */
4
+ export const fungiMcpSelectionSchema = z.strictObject({
5
+ endpoint: z.url().pipe(z.string().refine((value) => {
6
+ try {
7
+ parseFungiMcpEndpoint(value);
8
+ return true;
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }, "Fungi MCP requires HTTPS (or HTTP on an exact loopback host) without credentials, query or fragment")),
14
+ authentication: z.literal("oyster-key").optional(),
15
+ });
@@ -0,0 +1,14 @@
1
+ import type { PrivateFungiMcpConnectionOptions } from "./private-fungi-mcp-connection.js";
2
+ import { type PrivateFungiMcpSelection } from "./private-fungi-mcp-selection-schema.js";
3
+ /** The service validates saved project selections with the RPC selection schema. */
4
+ export declare function readProjectFungiMcpSelection(root: string): Promise<{
5
+ endpoint: string;
6
+ authentication?: "oyster-key" | undefined;
7
+ } | undefined>;
8
+ export declare function readFungiMcpSelection(selection: PrivateFungiMcpSelection, options?: {
9
+ credentialPath?: string;
10
+ }): PrivateFungiMcpConnectionOptions;
11
+ export declare function sameFungiMcpConnection(left: PrivateFungiMcpConnectionOptions | undefined, right: PrivateFungiMcpConnectionOptions): boolean;
12
+ export declare class FungiMcpSelectionConflict extends Error {
13
+ constructor();
14
+ }
@@ -0,0 +1,49 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { fungiMcpAccountAuth } from "./private-fungi-account.js";
4
+ import { oysterMcpKeyAuth } from "./private-oyster-mcp-auth.js";
5
+ import { fungiMcpSelectionSchema, } from "./private-fungi-mcp-selection-schema.js";
6
+ /** The service validates saved project selections with the RPC selection schema. */
7
+ export async function readProjectFungiMcpSelection(root) {
8
+ const path = join(root, ".fungi", "team-mcp.json");
9
+ let text;
10
+ try {
11
+ text = await readFile(path, "utf8");
12
+ }
13
+ catch (cause) {
14
+ if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
15
+ return undefined;
16
+ throw cause;
17
+ }
18
+ try {
19
+ return fungiMcpSelectionSchema.parse(JSON.parse(text));
20
+ }
21
+ catch (cause) {
22
+ throw new Error(`${path}: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
23
+ }
24
+ }
25
+ export function readFungiMcpSelection(selection, options = {}) {
26
+ const parsed = fungiMcpSelectionSchema.safeParse(selection);
27
+ if (!parsed.success)
28
+ throw new Error("Invalid Fungi MCP connection selection");
29
+ if (process.platform !== "linux" && process.platform !== "darwin")
30
+ throw new Error("Fungi MCP credential connections are supported only on Linux and macOS");
31
+ const endpoint = new URL(parsed.data.endpoint);
32
+ return {
33
+ endpoint,
34
+ authentication: parsed.data.authentication,
35
+ authProvider: parsed.data.authentication === "oyster-key"
36
+ ? oysterMcpKeyAuth(endpoint, options)
37
+ : fungiMcpAccountAuth(endpoint, options),
38
+ };
39
+ }
40
+ export function sameFungiMcpConnection(left, right) {
41
+ return (left?.endpoint.href === right.endpoint.href &&
42
+ left.authentication === right.authentication);
43
+ }
44
+ export class FungiMcpSelectionConflict extends Error {
45
+ constructor() {
46
+ super("This Local Agent already has a different Fungi MCP connection; stop the Local service before changing it");
47
+ this.name = "FungiMcpSelectionConflict";
48
+ }
49
+ }
@@ -0,0 +1,6 @@
1
+ import { type ProviderStreams, type FetchFunction } from "@earendil-works/pi-ai";
2
+ export declare function assertFungiModelsEnabled(): void;
3
+ /** Access tokens can only travel to the selected Team's two inference endpoints. */
4
+ export declare function fungiModelFetch(baseUrl: string, teamName: string, fetchImpl?: FetchFunction, credentialPath?: string): FetchFunction;
5
+ /** Pi owns streaming; this adapter owns Fungi's destination, incident policy and refusals. */
6
+ export declare function createFungiModelApi(resolveTeamName: (baseUrl: string) => Promise<string>, credentialPath?: string): ProviderStreams;
@@ -0,0 +1,125 @@
1
+ import process from "node:process";
2
+ import { fungiAccountFetch } from "./private-fungi-account.js";
3
+ import { capturePrivateLocalConfigRoot } from "./private-local-config-root.js";
4
+ import { FungiSignInRequiredError } from "./private-fungi-auth-error.js";
5
+ import { hasApi, } from "@earendil-works/pi-ai";
6
+ import { stream, streamSimple, } from "@earendil-works/pi-ai/api/openai-completions";
7
+ import { loginAllowed, subscriptionKillSwitchFrom, } from "@fungi.computer/veil/model-credentials";
8
+ import { z } from "zod";
9
+ import { logEvent } from "./log-event.js";
10
+ import { parseFungiMcpEndpoint } from "./private-fungi-mcp-endpoint.js";
11
+ const refusalSchema = z.object({
12
+ error: z.object({ code: z.string().max(256), message: z.string().max(4096) }),
13
+ });
14
+ /** Re-check incident policy at dispatch, including in an already-open Local host. */
15
+ function fungiModelsEnabled() {
16
+ return loginAllowed({
17
+ providerId: "fungi",
18
+ method: "subscription",
19
+ host: "local",
20
+ scope: "user",
21
+ }, subscriptionKillSwitchFrom(process.env.FUNGI_SUBSCRIPTION_KILL));
22
+ }
23
+ export function assertFungiModelsEnabled() {
24
+ if (!fungiModelsEnabled())
25
+ throw new Error("Fungi models are paused. Try again later.");
26
+ }
27
+ function httpRefusal(status, code, message) {
28
+ return Response.json({ error: { code, message, type: "fungi_error" } }, { status });
29
+ }
30
+ /** Map Hub refusal codes to actions without returning dependency bodies. */
31
+ function modelRefusal(status, body, teamName) {
32
+ const refusal = refusalSchema.safeParse(body);
33
+ const code = refusal.success ? refusal.data.error.code : "unavailable";
34
+ let message = refusal.success
35
+ ? refusal.data.error.message
36
+ : "Fungi could not complete this request. Retry shortly.";
37
+ if (["invalid_token", "insufficient_scope"].includes(code) || status === 401)
38
+ message = "Sign in to Fungi again to use models";
39
+ else if (["insufficient_quota", "subscription_required"].includes(code))
40
+ message = `Add credits to ${teamName}`;
41
+ else if (status === 503)
42
+ message =
43
+ code === "paused"
44
+ ? "Fungi models are paused. Try again later."
45
+ : "Fungi is unreachable. Retry shortly.";
46
+ return Response.json({ error: { code, message, type: "fungi_error" } }, { status: status });
47
+ }
48
+ /** The parsed destination must stay in the selected Team, with no redirect authority. */
49
+ function assertModelEndpoint(base, url) {
50
+ if (url.origin !== base.origin ||
51
+ url.search ||
52
+ url.hash ||
53
+ url.username ||
54
+ url.password ||
55
+ ![`${base.pathname}/models`, `${base.pathname}/chat/completions`].includes(url.pathname))
56
+ throw new Error("Fungi refused a model endpoint outside the selected Team");
57
+ }
58
+ /** Access tokens can only travel to the selected Team's two inference endpoints. */
59
+ export function fungiModelFetch(baseUrl, teamName, fetchImpl = globalThis.fetch, credentialPath = capturePrivateLocalConfigRoot().credentialPath) {
60
+ const base = parseFungiMcpEndpoint(baseUrl);
61
+ if (!/^\/api\/v1\/teams\/[^/]+$/u.test(base.pathname))
62
+ throw new Error("Fungi model requests require a selected Team");
63
+ const accountOptions = {
64
+ fetch: fetchImpl,
65
+ credentialPath,
66
+ };
67
+ const accountFetch = fungiAccountFetch(base.origin, accountOptions);
68
+ const transport = async (input, init) => {
69
+ if (!fungiModelsEnabled())
70
+ return httpRefusal(503, "paused", "Fungi models are paused. Try again later.");
71
+ const url = new URL(input instanceof Request ? input.url : String(input));
72
+ assertModelEndpoint(base, url);
73
+ let response;
74
+ try {
75
+ response = await accountFetch(input, { ...init, redirect: "error" });
76
+ }
77
+ catch (cause) {
78
+ if (init?.signal?.aborted)
79
+ throw cause;
80
+ if (cause instanceof FungiSignInRequiredError)
81
+ return httpRefusal(401, "invalid_token", "Sign in to Fungi again to use models");
82
+ logEvent("fungi-model.request", cause);
83
+ return httpRefusal(503, "unavailable", "Fungi is unreachable. Retry when your Hub connection returns.");
84
+ }
85
+ if (response.ok)
86
+ return response;
87
+ let body;
88
+ try {
89
+ body = await response.json();
90
+ }
91
+ catch (cause) {
92
+ logEvent("fungi-model.refusal", cause);
93
+ return httpRefusal(503, "unavailable", "Fungi is unreachable or returned an unreadable refusal. Retry shortly.");
94
+ }
95
+ return modelRefusal(response.status, body, teamName);
96
+ };
97
+ return Object.assign(transport, { preconnect: fetchImpl.preconnect });
98
+ }
99
+ /** Pi owns streaming; this adapter owns Fungi's destination, incident policy and refusals. */
100
+ export function createFungiModelApi(resolveTeamName, credentialPath = capturePrivateLocalConfigRoot().credentialPath) {
101
+ const fetchFor = (baseUrl, fetchImpl = globalThis.fetch) => Object.assign(async (input, init) => {
102
+ const teamName = await resolveTeamName(baseUrl);
103
+ return fungiModelFetch(baseUrl, teamName, fetchImpl, credentialPath)(input, init);
104
+ }, { preconnect: fetchImpl.preconnect });
105
+ return {
106
+ stream(model, context, options) {
107
+ if (!hasApi(model, "openai-completions"))
108
+ throw new Error("Fungi requires its compatible chat API");
109
+ return stream(model, context, {
110
+ ...options,
111
+ maxRetries: 0,
112
+ fetch: fetchFor(model.baseUrl, options?.fetch),
113
+ });
114
+ },
115
+ streamSimple(model, context, options) {
116
+ if (!hasApi(model, "openai-completions"))
117
+ throw new Error("Fungi requires its compatible chat API");
118
+ return streamSimple(model, context, {
119
+ ...options,
120
+ maxRetries: 0,
121
+ fetch: fetchFor(model.baseUrl, options?.fetch),
122
+ });
123
+ },
124
+ };
125
+ }
@@ -0,0 +1,16 @@
1
+ import type { Model } from "@earendil-works/pi-ai";
2
+ import { z } from "zod";
3
+ /** The thinking levels a Local model may offer. */
4
+ export declare const thinkingLevelSchema: z.ZodEnum<{
5
+ minimal: "minimal";
6
+ low: "low";
7
+ medium: "medium";
8
+ high: "high";
9
+ xhigh: "xhigh";
10
+ max: "max";
11
+ }>;
12
+ export type FungiHostedModel = Model<"openai-completions"> & {
13
+ default: boolean;
14
+ };
15
+ /** Only the Hub's hosted rows become Local models; its Billing price owns the estimate. */
16
+ export declare function fungiHostedModels(value: unknown, baseUrl: string): FungiHostedModel[];
@@ -0,0 +1,86 @@
1
+ import { z } from "zod";
2
+ /** The thinking levels a Local model may offer. */
3
+ export const thinkingLevelSchema = z.enum([
4
+ "minimal",
5
+ "low",
6
+ "medium",
7
+ "high",
8
+ "xhigh",
9
+ "max",
10
+ ]);
11
+ const catalogSchema = z.object({
12
+ object: z.literal("list"),
13
+ data: z
14
+ .array(z.object({
15
+ id: z.string().min(1).max(256),
16
+ owned_by: z.string().min(1).max(256),
17
+ name: z.string().min(1).max(256),
18
+ context_window: z.number().int().positive(),
19
+ max_output_tokens: z.number().int().positive(),
20
+ reasoning_levels: z.array(thinkingLevelSchema).max(6),
21
+ input: z.array(z.literal("text")).length(1),
22
+ default: z.boolean(),
23
+ cost: z.object({
24
+ input: z.number().nonnegative(),
25
+ output: z.number().nonnegative(),
26
+ cacheRead: z.number().nonnegative(),
27
+ cacheWrite: z.number().nonnegative(),
28
+ }),
29
+ }))
30
+ .max(100),
31
+ });
32
+ /** Only the Hub's hosted rows become Local models; its Billing price owns the estimate. */
33
+ export function fungiHostedModels(value, baseUrl) {
34
+ const rows = catalogSchema
35
+ .parse(value)
36
+ .data.filter((row) => row.owned_by === "fungi" && row.id.startsWith("@fungi/"));
37
+ if (rows.filter((row) => row.default).length !== 1 ||
38
+ new Set(rows.map((row) => row.id)).size !== rows.length)
39
+ throw new Error("Fungi returned an invalid hosted model catalog");
40
+ return rows.map((row) => {
41
+ const thinkingLevelMap = {
42
+ off: null,
43
+ minimal: null,
44
+ low: null,
45
+ medium: null,
46
+ high: null,
47
+ xhigh: null,
48
+ max: null,
49
+ };
50
+ for (const level of row.reasoning_levels)
51
+ thinkingLevelMap[level] = level;
52
+ return {
53
+ id: row.id,
54
+ name: row.name,
55
+ provider: "fungi",
56
+ api: "openai-completions",
57
+ baseUrl,
58
+ default: row.default,
59
+ input: row.input,
60
+ contextWindow: row.context_window,
61
+ maxTokens: row.max_output_tokens,
62
+ reasoning: row.reasoning_levels.length > 0,
63
+ thinkingLevelMap,
64
+ cost: row.cost,
65
+ compat: {
66
+ supportsStore: false,
67
+ supportsDeveloperRole: true,
68
+ supportsReasoningEffort: true,
69
+ supportsUsageInStreaming: true,
70
+ maxTokensField: "max_completion_tokens",
71
+ requiresToolResultName: false,
72
+ requiresAssistantAfterToolResult: false,
73
+ requiresThinkingAsText: true,
74
+ requiresReasoningContentOnAssistantMessages: false,
75
+ supportsStrictMode: false,
76
+ supportsOpenAIGrammarTools: false,
77
+ supportsMidConvoSystemMessages: false,
78
+ supportsMidConvoToolAdditions: false,
79
+ supportsLongCacheRetention: false,
80
+ sendSessionAffinityHeaders: false,
81
+ zaiToolStream: false,
82
+ thinkingFormat: "openai",
83
+ },
84
+ };
85
+ });
86
+ }
@@ -0,0 +1,6 @@
1
+ import { type FetchFunction } from "@earendil-works/pi-ai";
2
+ /** The selected account Team owns the endpoint; the Hub owns the live model catalog. */
3
+ export declare function createFungiModelProvider(input?: {
4
+ credentialPath?: string;
5
+ fetch?: FetchFunction;
6
+ }): import("@earendil-works/pi-ai").Provider<"openai-completions">;
@@ -0,0 +1,51 @@
1
+ import { z } from "zod";
2
+ import { createProvider } from "@earendil-works/pi-ai";
3
+ import { createFungiAccountOAuth, fungiAccountTeam, readFungiAccountStatus, } from "./private-fungi-account.js";
4
+ import { fungiHostedModels } from "./private-fungi-model-catalog.js";
5
+ import { capturePrivateLocalConfigRoot } from "./private-local-config-root.js";
6
+ import { assertFungiModelsEnabled, createFungiModelApi, fungiModelFetch, } from "./private-fungi-model-api.js";
7
+ /** The selected account Team owns the endpoint; the Hub owns the live model catalog. */
8
+ export function createFungiModelProvider(input = {}) {
9
+ const options = {
10
+ ...input,
11
+ credentialPath: input.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath,
12
+ };
13
+ const oauth = createFungiAccountOAuth(options);
14
+ return createProvider({
15
+ id: "fungi",
16
+ name: "Fungi",
17
+ auth: { oauth },
18
+ models: [],
19
+ api: createFungiModelApi(async (baseUrl) => {
20
+ const status = await readFungiAccountStatus(options);
21
+ if (status?.team === undefined)
22
+ throw new Error("Sign in to Fungi to use models");
23
+ const currentBase = `${status.issuer}/api/v1/teams/${encodeURIComponent(status.team.teamId)}`;
24
+ if (currentBase !== baseUrl)
25
+ throw new Error("Fungi account changed before dispatch. Retry for your selected Team.");
26
+ return status.team.name;
27
+ }, options.credentialPath),
28
+ async fetchModels(context) {
29
+ assertFungiModelsEnabled();
30
+ if (context.credential?.type !== "oauth")
31
+ throw new Error("Sign in to Fungi to use models");
32
+ const auth = await oauth.toAuth(context.credential);
33
+ if (auth.baseUrl === undefined || auth.apiKey === undefined)
34
+ throw new Error("Choose a Team and sign in to Fungi to use models");
35
+ const team = fungiAccountTeam(context.credential);
36
+ const response = await fungiModelFetch(auth.baseUrl, team.name, options.fetch, options.credentialPath)(`${auth.baseUrl}/models`, {
37
+ headers: { Authorization: `Bearer ${auth.apiKey}` },
38
+ signal: AbortSignal.any([context.signal, AbortSignal.timeout(30_000)]),
39
+ });
40
+ if (!response.ok) {
41
+ const body = await response.json();
42
+ // The transport owns the curated refusal; Pi returns this failure to its caller.
43
+ const refusal = z
44
+ .object({ error: z.object({ message: z.string() }) })
45
+ .parse(body).error.message;
46
+ throw new Error(refusal);
47
+ }
48
+ return fungiHostedModels(await response.json(), auth.baseUrl);
49
+ },
50
+ });
51
+ }
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+ declare const identitySchema: z.ZodObject<{
3
+ issuer: z.ZodURL;
4
+ userId: z.ZodString;
5
+ loginId: z.ZodString;
6
+ }, z.core.$strict>;
7
+ declare const detachSchema: z.ZodObject<{
8
+ teamId: z.ZodString;
9
+ computerId: z.ZodString;
10
+ requestId: z.ZodString;
11
+ }, z.core.$strict>;
12
+ export type PendingDetach = z.output<typeof detachSchema>;
13
+ declare const recordSchema: z.ZodObject<{
14
+ issuer: z.ZodURL;
15
+ userId: z.ZodString;
16
+ loginId: z.ZodString;
17
+ revoke: z.ZodBoolean;
18
+ detaches: z.ZodArray<z.ZodObject<{
19
+ teamId: z.ZodString;
20
+ computerId: z.ZodString;
21
+ requestId: z.ZodString;
22
+ }, z.core.$strict>>;
23
+ }, z.core.$strict>;
24
+ type PendingLogout = z.output<typeof recordSchema>;
25
+ export type LoginIdentity = z.output<typeof identitySchema>;
26
+ /** Decode only routing metadata. These IDs never authorize a revocation. */
27
+ export declare function loginIdentity(issuer: string, access: string): LoginIdentity | undefined;
28
+ /** Durable logout intent, separate from the credential that logout destroys. */
29
+ export declare function pendingLogouts(credentialPath: string): {
30
+ remember(identity: LoginIdentity, detaches: PendingDetach[]): Promise<void>;
31
+ confirmed(identity: LoginIdentity, detach?: PendingDetach): Promise<void>;
32
+ forAccount(identity: LoginIdentity): Promise<PendingLogout[]>;
33
+ };
34
+ export {};