@fungi.computer/truffle 0.1.1 → 0.2.1

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,3 +1,4 @@
1
1
  import type { FilesPatch, FilesPort } from "@fungi.computer/knapsack";
2
+ import { type LocalSandboxLaunch } from "./private-invocation-computer.js";
2
3
  /** Advertise only a packaged native engine; never install tools during a model call. */
3
- export declare function createPrivateFilesPatch(root: string, read: FilesPort["read"]): Promise<FilesPatch | undefined>;
4
+ export declare function createPrivateFilesPatch(root: string, read: FilesPort["read"], sandbox?: LocalSandboxLaunch): Promise<FilesPatch | undefined>;
@@ -5,7 +5,7 @@ import process from "node:process";
5
5
  import * as ChildProcess from "effect/process/ChildProcess";
6
6
  import { parsePatch } from "diff";
7
7
  import * as Stream from "effect/Stream";
8
- import { runPrivateInvocationProcess } from "./private-invocation-computer.js";
8
+ import { runPrivateInvocationProcess, } from "./private-invocation-computer.js";
9
9
  function patchExecutable() {
10
10
  if (process.env.FUNGI_BUNDLED_CLI) {
11
11
  const name = `@fungi.computer/spore-native-${process.platform}-${process.arch}`;
@@ -33,6 +33,8 @@ async function unifiedDiffContext(patch, read, context) {
33
33
  if (path === undefined || hunk === undefined || path === "/dev/null")
34
34
  return "\nInclude *** Update File: path/to/file before @@ to identify the target.\n";
35
35
  const current = await read({ path, offset: 0, limit: maximumPatchBytes }, context);
36
+ if ("image" in current)
37
+ throw new Error(`${path} is an image, not text`);
36
38
  const lines = current.content.split(/\r?\n/u);
37
39
  const from = Math.min(Math.max(0, hunk.oldStart - 3), Math.max(0, lines.length - 7));
38
40
  const nearby = lines
@@ -46,7 +48,7 @@ async function unifiedDiffContext(patch, read, context) {
46
48
  }
47
49
  }
48
50
  /** Advertise only a packaged native engine; never install tools during a model call. */
49
- export async function createPrivateFilesPatch(root, read) {
51
+ export async function createPrivateFilesPatch(root, read, sandbox) {
50
52
  const executable = patchExecutable();
51
53
  try {
52
54
  await access(executable, constants.X_OK);
@@ -61,9 +63,25 @@ export async function createPrivateFilesPatch(root, read) {
61
63
  const bytes = new TextEncoder().encode(patch);
62
64
  if (bytes.byteLength > maximumPatchBytes)
63
65
  throw new Error("Patch exceeds the 128 KiB input bound");
64
- const command = ChildProcess.make(executable, {
66
+ const launch = sandbox?.(root, [executable]);
67
+ const env = { ...process.env, ...launch?.environment };
68
+ if (launch !== undefined) {
69
+ delete env.XDG_RUNTIME_DIR;
70
+ delete env.DBUS_SESSION_BUS_ADDRESS;
71
+ }
72
+ const command = ChildProcess.make(launch?.executable ?? executable, launch?.args ?? [], {
65
73
  cwd: root,
74
+ env,
75
+ extendEnv: false,
66
76
  stdin: Stream.make(bytes),
77
+ additionalFds: launch?.seccompFilter === undefined
78
+ ? undefined
79
+ : {
80
+ fd3: {
81
+ type: "input",
82
+ stream: Stream.make(launch.seccompFilter),
83
+ },
84
+ },
67
85
  });
68
86
  const result = await runPrivateInvocationProcess(command, context.signal, 30_000);
69
87
  if (result.status === "deadline_exceeded")
@@ -1,6 +1,8 @@
1
1
  import { type AuthProvider, type FetchLike, type OAuthClientProvider, type StoredOAuthClientInformation } from "@modelcontextprotocol/client";
2
2
  import { z } from "zod";
3
3
  import type { Credential, CredentialStore, OAuthAuth } from "@earendil-works/pi-ai";
4
+ import { type LocalMachineEnrollmentAccount } from "./private-local-mesh-record.js";
5
+ import { type FungiAccountLogoutResult } from "./private-fungi-service-logout.js";
4
6
  /**
5
7
  * One folder attached to a Team Computer. The absolute folder path and the
6
8
  * device's private key never leave this file; the Hub holds only the public
@@ -35,10 +37,18 @@ export type PrivateFungiAccountOAuthProviderOptions = {
35
37
  redirectUrl?: string;
36
38
  openBrowser?: (url: URL) => void;
37
39
  };
40
+ /**
41
+ * Enrollment uses the account file and lock, without Pi's selected-payer view.
42
+ * Fungi enrollments are partitioned by issuer in the origin-keyed enrollment
43
+ * records: only the signed-in issuer's partition is ever read for dialing, so
44
+ * a switch never lends one issuer's Team or device identifiers to another,
45
+ * and signing in to an issuer again finds its own partition.
46
+ */
47
+ export declare function createLocalMachineEnrollmentAccount(credentialPath: string): LocalMachineEnrollmentAccount;
38
48
  /** Hand a Fungi sign-in page to the platform browser; callers show the URL. */
39
49
  export declare function launchPlatformBrowser(url: URL): void;
40
50
  /** Persisted OAuth state uses Local's existing owner-only Pi credential file. */
41
- export declare function privateFungiAccountOAuthProvider(options?: PrivateFungiAccountOAuthProviderOptions): OAuthClientProvider & {
51
+ export declare function privateFungiAccountOAuthProvider(input?: PrivateFungiAccountOAuthProviderOptions): OAuthClientProvider & {
42
52
  saveClientInformation(information: StoredOAuthClientInformation): Promise<void>;
43
53
  expectedState(): string | undefined;
44
54
  authorizationUrl(): URL | undefined;
@@ -58,14 +68,13 @@ export type PrivateFungiAccountLoginOptions = {
58
68
  /** Opens the Hub pages; the default launches the platform browser. */
59
69
  openBrowser?: (url: URL) => void;
60
70
  };
61
- export type PrivateFungiAccountLogoutResult = Readonly<{
62
- kind: "not-logged-in" | "removed" | "revoked";
63
- message: string;
64
- unconfirmedComputers: readonly Readonly<{
65
- name: string;
66
- url: string;
67
- }>[];
71
+ export type PrivateFungiAccountLogoutResult = FungiAccountLogoutResult;
72
+ /** An in-process account change the service owner reacts to. */
73
+ export type FungiAccountChange = Readonly<{
74
+ type: "signed-out";
68
75
  }>;
76
+ /** Installed only by the service holding this home's owner lease. */
77
+ export declare function registerFungiAccountOwner(credentialPath: string, changed: (event: FungiAccountChange) => Promise<void>): () => void;
69
78
  export type FungiAccountStatus = {
70
79
  issuer: string;
71
80
  accessExpiresAt: Date;
@@ -73,14 +82,22 @@ export type FungiAccountStatus = {
73
82
  teamCount?: number;
74
83
  };
75
84
  /** Non-secret status of the shared Fungi account. */
76
- export declare function readFungiAccountStatus(options?: {
85
+ export declare function readFungiAccountStatus(input?: {
77
86
  credentialPath?: string;
78
87
  }): Promise<FungiAccountStatus | undefined>;
79
88
  /**
80
- * Remove the saved Fungi MCP login for the endpoint's origin and ask that
81
- * issuer to revoke its refresh grant. A login for another origin is kept.
89
+ * Ask the running Local account owner to remove this origin's login and revoke
90
+ * its refresh grant. Without a service, clear offline; never start one for
91
+ * logout. A login for another origin is kept. Explicit standalone stores have
92
+ * no service home; clients with a custom service directory name it explicitly.
82
93
  */
83
- export declare function signOutFungiAccount(rawEndpoint?: string | URL, options?: {
94
+ export declare function signOutFungiAccount(rawEndpoint?: string | URL, input?: {
95
+ credentialPath?: string;
96
+ serviceDirectory?: string;
97
+ fetch?: FetchLike;
98
+ }): Promise<PrivateFungiAccountLogoutResult>;
99
+ /** Store mutation for the service owner or a verified no-service caller. */
100
+ export declare function signOutOwnedFungiAccount(rawEndpoint?: string | URL, input?: {
84
101
  credentialPath?: string;
85
102
  fetch?: FetchLike;
86
103
  }): Promise<PrivateFungiAccountLogoutResult>;
@@ -101,20 +118,20 @@ export declare function fungiMcpTeamEndpoint(rawEndpoint: string | URL): {
101
118
  teamId: string;
102
119
  };
103
120
  /** The machine's attached folders, as Local keeps them. */
104
- export declare function readFungiMcpLocalBackends(options?: Readonly<{
121
+ export declare function readFungiMcpLocalBackends(input?: Readonly<{
105
122
  credentialPath?: string;
106
123
  }>): Promise<Readonly<{
107
124
  issuer: string | undefined;
108
125
  backends: LocalBackendRecord[];
109
126
  }>>;
110
127
  /** Replace the attached folder records in the owner-only credential file. */
111
- export declare function writeFungiMcpLocalBackends(update: (current: readonly LocalBackendRecord[]) => LocalBackendRecord[], options?: Readonly<{
128
+ export declare function writeFungiMcpLocalBackends(update: (current: readonly LocalBackendRecord[]) => LocalBackendRecord[], input?: Readonly<{
112
129
  credentialPath?: string;
113
130
  }>): Promise<void>;
114
131
  /** Complete first-party registration and OAuth authorization in a foreground TUI. */
115
- export declare function signInFungiAccount(rawEndpoint: string | URL, options?: PrivateFungiAccountLoginOptions): Promise<void>;
132
+ export declare function signInFungiAccount(rawEndpoint: string | URL, input?: PrivateFungiAccountLoginOptions): Promise<void>;
116
133
  /** Switch only to a fresh membership; failure preserves the prior selected Team. */
117
- export declare function switchFungiAccountTeam(teamId: string, options?: {
134
+ export declare function switchFungiAccountTeam(teamId: string, input?: {
118
135
  credentialPath?: string;
119
136
  fetch?: FetchLike;
120
137
  }): Promise<void>;
@@ -123,7 +140,7 @@ export type FungiAccountTeams = Readonly<{
123
140
  selectedTeamId?: string;
124
141
  }>;
125
142
  /** Fresh memberships for the saved account, with the Team that pays for models. */
126
- export declare function readFungiAccountTeams(options?: {
143
+ export declare function readFungiAccountTeams(input?: {
127
144
  credentialPath?: string;
128
145
  fetch?: FetchLike;
129
146
  }): Promise<FungiAccountTeams>;
@@ -192,7 +209,7 @@ export type FungiTeamBalance = Readonly<{
192
209
  kind: "not-available";
193
210
  reason: "no-team" | "not-a-member" | "sign-in-again" | "sign-in-not-admitted";
194
211
  }>;
195
- export declare function readFungiTeamBalance(options?: {
212
+ export declare function readFungiTeamBalance(input?: {
196
213
  credentialPath?: string;
197
214
  fetch?: FetchLike;
198
215
  }): Promise<FungiTeamBalance>;
@@ -201,7 +218,7 @@ export declare function readFungiTeamBalance(options?: {
201
218
  * when the saved access token actually needs it: a plain read never takes
202
219
  * the lock or rewrites the file, so a still-valid credential stays untouched.
203
220
  */
204
- export declare function freshFungiAccess(rawHub: string | URL, options?: {
221
+ export declare function freshFungiAccess(rawHub: string | URL, input?: {
205
222
  credentialPath?: string;
206
223
  fetch?: FetchLike;
207
224
  signal?: AbortSignal;
@@ -209,13 +226,13 @@ export declare function freshFungiAccess(rawHub: string | URL, options?: {
209
226
  rejectedAccess?: string;
210
227
  }): Promise<string>;
211
228
  /** Retry an explicitly refused request once; the cloned body is replayable. */
212
- export declare function fungiAccountFetch(rawHub: string | URL, options?: {
229
+ export declare function fungiAccountFetch(rawHub: string | URL, input?: {
213
230
  credentialPath?: string;
214
231
  fetch?: FetchLike;
215
232
  signal?: AbortSignal;
216
233
  }): typeof globalThis.fetch;
217
234
  /** The SDK owns exactly one retry; this hook rotates only the refused credential. */
218
- export declare function fungiMcpAccountAuth(endpoint: URL, options?: {
235
+ export declare function fungiMcpAccountAuth(endpoint: URL, input?: {
219
236
  credentialPath?: string;
220
237
  fetch?: FetchLike;
221
238
  signal?: AbortSignal;
@@ -223,12 +240,12 @@ export declare function fungiMcpAccountAuth(endpoint: URL, options?: {
223
240
  /** Selected payer metadata belongs to the shared account, never a model request body. */
224
241
  export declare function fungiAccountTeam(credential: Credential): FungiAccountTeam;
225
242
  /** Pi uses its own locked modify; this owner supplies the same refresh as MCP. */
226
- export declare function createFungiAccountOAuth(options?: {
243
+ export declare function createFungiAccountOAuth(input?: {
227
244
  credentialPath?: string;
228
245
  fetch?: FetchLike;
229
246
  }): OAuthAuth;
230
247
  /** Pi deletion clears Local custody and reports unconfirmed remote revocation. */
231
- export declare function createFungiAccountCredentialStore(options?: {
248
+ export declare function createFungiAccountCredentialStore(input?: {
232
249
  credentialPath?: string;
233
250
  fetch?: FetchLike;
234
251
  }): CredentialStore;
@@ -4,12 +4,16 @@ import { createServer, } from "node:http";
4
4
  import { randomBytes } from "node:crypto";
5
5
  import { spawn } from "node:child_process";
6
6
  import process from "node:process";
7
+ import { dirname, resolve } from "node:path";
7
8
  import { auth, Client, discoverOAuthServerInfo, refreshAuthorization, OAuthError, StreamableHTTPClientTransport, } from "@modelcontextprotocol/client";
8
9
  import { z } from "zod";
9
10
  import { createPrivateCredentialStore } from "./private-file-credential-store.js";
11
+ import { capturePrivateLocalConfigRoot } from "./private-local-config-root.js";
10
12
  import { parseFungiMcpEndpoint } from "./private-fungi-mcp-endpoint.js";
11
13
  import { logEvent } from "./log-event.js";
14
+ import { machineEnrollmentAuthoritySchema, machineEnrollmentRecordKind, } from "./private-local-mesh-record.js";
12
15
  import { fungiMcpClientInfo, fungiMcpReachableFetch, rejectedFungiMcpAccess, } from "./private-fungi-mcp-connection.js";
16
+ import { signOutFungiThroughLocalService, } from "./private-fungi-service-logout.js";
13
17
  const credentialId = "fungi";
14
18
  const localScopes = "openid profile email offline_access models:invoke git:tracker billing:read";
15
19
  const callbackHost = "127.0.0.1";
@@ -46,7 +50,8 @@ const teamChoiceSchema = z.object({
46
50
  handle: z.string().min(1).max(256),
47
51
  name: z.string().min(1).max(256),
48
52
  });
49
- const savedCredentialSchema = z.looseObject({
53
+ const savedCredentialSchema = z
54
+ .looseObject({
50
55
  type: z.literal("oauth"),
51
56
  access: z.string(),
52
57
  refresh: z.string(),
@@ -59,6 +64,14 @@ const savedCredentialSchema = z.looseObject({
59
64
  localBackends: z.array(localBackendRecordSchema).max(16).optional(),
60
65
  team: teamChoiceSchema.optional(),
61
66
  teamCount: z.number().int().min(0).max(1000).optional(),
67
+ })
68
+ .transform((credential) => {
69
+ // Preview credentials saved device enrollments here without an issuer
70
+ // binding, private keys included. They cannot be attributed to an issuer,
71
+ // so they are never read and the next write of this credential drops them.
72
+ const current = { ...credential };
73
+ delete current.machineEnrollments;
74
+ return current;
62
75
  });
63
76
  function jsonValue(value) {
64
77
  const serialized = JSON.stringify(value);
@@ -87,6 +100,63 @@ async function readCredential(path) {
87
100
  throw new Error("Fungi Local credential data is invalid");
88
101
  return result.data;
89
102
  }
103
+ /**
104
+ * Enrollment uses the account file and lock, without Pi's selected-payer view.
105
+ * Fungi enrollments are partitioned by issuer in the origin-keyed enrollment
106
+ * records: only the signed-in issuer's partition is ever read for dialing, so
107
+ * a switch never lends one issuer's Team or device identifiers to another,
108
+ * and signing in to an issuer again finds its own partition.
109
+ */
110
+ export function createLocalMachineEnrollmentAccount(credentialPath) {
111
+ return {
112
+ read: async () => {
113
+ const saved = await readCredential(credentialPath);
114
+ const records = await credentialStore(credentialPath).readMachineEnrollments();
115
+ const issuer = saved?.issuer !== undefined && saved.access !== ""
116
+ ? saved.issuer
117
+ : undefined;
118
+ const partition = records.find((record) => record.origin === issuer &&
119
+ machineEnrollmentRecordKind(record) === "oauth");
120
+ return [
121
+ ...(issuer !== undefined
122
+ ? [
123
+ {
124
+ kind: "oauth",
125
+ origin: issuer,
126
+ enrollments: partition?.enrollments ?? [],
127
+ },
128
+ ]
129
+ : []),
130
+ ...records.flatMap((record) => machineEnrollmentRecordKind(record) === "oyster"
131
+ ? [
132
+ {
133
+ kind: "oyster",
134
+ origin: record.origin,
135
+ enrollments: record.enrollments,
136
+ },
137
+ ]
138
+ : []),
139
+ ];
140
+ },
141
+ update: async (rawAuthority, edit) => {
142
+ const authority = machineEnrollmentAuthoritySchema.parse(rawAuthority);
143
+ if (authority.kind === "oyster") {
144
+ await credentialStore(credentialPath).modifyMachineEnrollments(authority.origin, edit);
145
+ return;
146
+ }
147
+ await credentialStore(credentialPath).modifyMachineEnrollments(authority.origin, edit, {
148
+ providerId: credentialId,
149
+ admit: (current) => {
150
+ const parsed = savedCredentialSchema.safeParse(current);
151
+ if (!parsed.success ||
152
+ parsed.data.access === "" ||
153
+ parsed.data.issuer !== authority.origin)
154
+ throw new Error("Fungi sign-in changed before machine enrollment was persisted");
155
+ },
156
+ });
157
+ },
158
+ };
159
+ }
90
160
  async function updateCredential(patch, path) {
91
161
  const store = credentialStore(path);
92
162
  await store.modify(credentialId, (current) => {
@@ -128,7 +198,11 @@ function respond(response, status, message) {
128
198
  response.end(`<!doctype html><meta charset="utf-8"><title>Fungi Local</title><p>${message}</p>`);
129
199
  }
130
200
  /** Persisted OAuth state uses Local's existing owner-only Pi credential file. */
131
- export function privateFungiAccountOAuthProvider(options = {}) {
201
+ export function privateFungiAccountOAuthProvider(input = {}) {
202
+ const options = {
203
+ ...input,
204
+ credentialPath: input.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath,
205
+ };
132
206
  const interactive = options.interactive ?? false;
133
207
  const credentialPath = options.credentialPath;
134
208
  const issuer = options.issuer ?? options.endpoint?.origin;
@@ -278,6 +352,17 @@ export function privateFungiAccountOAuthProvider(options = {}) {
278
352
  function validateEndpoint(value) {
279
353
  return parseFungiMcpEndpoint(value);
280
354
  }
355
+ const accountOwners = new Map();
356
+ /** Installed only by the service holding this home's owner lease. */
357
+ export function registerFungiAccountOwner(credentialPath, changed) {
358
+ const path = resolve(credentialPath);
359
+ if (accountOwners.has(path))
360
+ throw new Error("Fungi account already has an in-process owner");
361
+ accountOwners.set(path, changed);
362
+ return () => {
363
+ accountOwners.delete(path);
364
+ };
365
+ }
281
366
  function logoutResult(kind, issuer, unconfirmedComputers = []) {
282
367
  const status = kind === "not-logged-in"
283
368
  ? `Fungi was not logged in${issuer ? ` to ${issuer}` : ""}.`
@@ -413,6 +498,10 @@ async function removeSavedLogin(saved, options) {
413
498
  }
414
499
  return value;
415
500
  });
501
+ // Publish after the committed removal, before remote revocation can stall.
502
+ await accountOwners.get(resolve(options.credentialPath))?.({
503
+ type: "signed-out",
504
+ });
416
505
  if (latest === undefined || latest.refresh === "")
417
506
  return logoutResult("not-logged-in", saved.issuer, unconfirmed);
418
507
  let revoked = await revokeSavedGrant(latest, options.fetch);
@@ -430,7 +519,11 @@ async function removeSavedLogin(saved, options) {
430
519
  return logoutResult(revoked ? "revoked" : "removed", latest.issuer, unconfirmed);
431
520
  }
432
521
  /** Non-secret status of the shared Fungi account. */
433
- export async function readFungiAccountStatus(options = {}) {
522
+ export async function readFungiAccountStatus(input = {}) {
523
+ const options = {
524
+ ...input,
525
+ credentialPath: input.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath,
526
+ };
434
527
  const saved = await readCredential(options.credentialPath);
435
528
  if (saved?.issuer === undefined || saved.access === "")
436
529
  return undefined;
@@ -445,16 +538,60 @@ export async function readFungiAccountStatus(options = {}) {
445
538
  return status;
446
539
  }
447
540
  /**
448
- * Remove the saved Fungi MCP login for the endpoint's origin and ask that
449
- * issuer to revoke its refresh grant. A login for another origin is kept.
541
+ * Ask the running Local account owner to remove this origin's login and revoke
542
+ * its refresh grant. Without a service, clear offline; never start one for
543
+ * logout. A login for another origin is kept. Explicit standalone stores have
544
+ * no service home; clients with a custom service directory name it explicitly.
450
545
  */
451
- export async function signOutFungiAccount(rawEndpoint, options = {}) {
546
+ export async function signOutFungiAccount(rawEndpoint, input = {}) {
547
+ const credentialPath = resolve(input.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath);
548
+ if (accountOwners.has(credentialPath))
549
+ return await signOutOwnedFungiAccount(rawEndpoint, {
550
+ ...input,
551
+ credentialPath,
552
+ });
553
+ // The ordinary CLI supplies this canonical path, including for an explicitly
554
+ // selected home. Do not reselect ambient HOME after that boundary.
555
+ const root = capturePrivateLocalConfigRoot(dirname(dirname(dirname(credentialPath))));
556
+ const directory = input.serviceDirectory ??
557
+ (root.credentialPath === credentialPath
558
+ ? root.serviceDirectory
559
+ : undefined);
560
+ if (directory !== undefined) {
561
+ return await signOutFungiThroughLocalService(rawEndpoint, {
562
+ ...input,
563
+ credentialPath,
564
+ directory,
565
+ clearOffline: signOutOwnedFungiAccount,
566
+ });
567
+ }
568
+ // Explicit standalone credential stores have no service home. A client using
569
+ // a custom service directory supplies it above; the server checks the path.
570
+ return await signOutOwnedFungiAccount(rawEndpoint, {
571
+ ...input,
572
+ credentialPath,
573
+ });
574
+ }
575
+ /** Store mutation for the service owner or a verified no-service caller. */
576
+ export async function signOutOwnedFungiAccount(rawEndpoint, input = {}) {
577
+ const options = {
578
+ ...input,
579
+ credentialPath: input.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath,
580
+ };
452
581
  const saved = await readCredential(options.credentialPath);
453
582
  const origin = rawEndpoint === undefined
454
583
  ? saved?.issuer
455
584
  : validateEndpoint(rawEndpoint).origin;
456
- if (saved === undefined || saved.issuer !== origin)
457
- return logoutResult("not-logged-in", origin);
585
+ const removedMachine = origin !== undefined &&
586
+ (await credentialStore(options.credentialPath).removeMachineEnrollments(origin));
587
+ if (saved === undefined || saved.issuer !== origin) {
588
+ if (removedMachine)
589
+ await accountOwners.get(resolve(options.credentialPath))?.({
590
+ type: "signed-out",
591
+ });
592
+ // Local custody ended; this is not a claim of remote device revocation.
593
+ return logoutResult(removedMachine ? "removed" : "not-logged-in", origin);
594
+ }
458
595
  return removeSavedLogin(saved, options);
459
596
  }
460
597
  /**
@@ -523,7 +660,11 @@ export function fungiMcpTeamEndpoint(rawEndpoint) {
523
660
  return { endpoint, teamId };
524
661
  }
525
662
  /** The machine's attached folders, as Local keeps them. */
526
- export async function readFungiMcpLocalBackends(options = {}) {
663
+ export async function readFungiMcpLocalBackends(input = {}) {
664
+ const options = {
665
+ ...input,
666
+ credentialPath: input.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath,
667
+ };
527
668
  const saved = await readCredential(options.credentialPath);
528
669
  return {
529
670
  issuer: saved?.issuer,
@@ -531,7 +672,11 @@ export async function readFungiMcpLocalBackends(options = {}) {
531
672
  };
532
673
  }
533
674
  /** Replace the attached folder records in the owner-only credential file. */
534
- export async function writeFungiMcpLocalBackends(update, options = {}) {
675
+ export async function writeFungiMcpLocalBackends(update, input = {}) {
676
+ const options = {
677
+ ...input,
678
+ credentialPath: input.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath,
679
+ };
535
680
  await credentialStore(options.credentialPath).modify(credentialId, (current) => {
536
681
  const saved = savedCredentialSchema.parse(current);
537
682
  if (saved.access === "")
@@ -585,7 +730,11 @@ async function retryPendingLogout(endpoint, options) {
585
730
  }
586
731
  }
587
732
  /** Complete first-party registration and OAuth authorization in a foreground TUI. */
588
- export async function signInFungiAccount(rawEndpoint, options = {}) {
733
+ export async function signInFungiAccount(rawEndpoint, input = {}) {
734
+ const options = {
735
+ ...input,
736
+ credentialPath: input.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath,
737
+ };
589
738
  options.signal?.throwIfAborted();
590
739
  const endpoint = validateEndpoint(rawEndpoint);
591
740
  // Check the Hub before offering a browser handoff or replacing a saved login.
@@ -664,12 +813,11 @@ export async function signInFungiAccount(rawEndpoint, options = {}) {
664
813
  const redirectUrl = `${callbackOrigin}${callbackPath}`;
665
814
  const providerOptions = {
666
815
  interactive: true,
816
+ credentialPath: options.credentialPath,
667
817
  endpoint,
668
818
  redirectUrl,
669
819
  openBrowser: launchBrowser,
670
820
  };
671
- if (options.credentialPath !== undefined)
672
- providerOptions.credentialPath = options.credentialPath;
673
821
  const provider = privateFungiAccountOAuthProvider(providerOptions);
674
822
  // The browser is opened only after this handler is attached.
675
823
  server.on("request", (request, response) => {
@@ -835,7 +983,11 @@ async function chooseFungiAccountTeam(rawHub, options) {
835
983
  await saveSelectedTeam(issuer, team, teams.length, options.credentialPath);
836
984
  }
837
985
  /** Switch only to a fresh membership; failure preserves the prior selected Team. */
838
- export async function switchFungiAccountTeam(teamId, options = {}) {
986
+ export async function switchFungiAccountTeam(teamId, input = {}) {
987
+ const options = {
988
+ ...input,
989
+ credentialPath: input.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath,
990
+ };
839
991
  const saved = await readCredential(options.credentialPath);
840
992
  if (saved?.issuer === undefined)
841
993
  throw new Error("Sign in to Fungi to choose a Team");
@@ -846,7 +998,11 @@ export async function switchFungiAccountTeam(teamId, options = {}) {
846
998
  await saveSelectedTeam(saved.issuer, selected, teams.length, options.credentialPath);
847
999
  }
848
1000
  /** Fresh memberships for the saved account, with the Team that pays for models. */
849
- export async function readFungiAccountTeams(options = {}) {
1001
+ export async function readFungiAccountTeams(input = {}) {
1002
+ const options = {
1003
+ ...input,
1004
+ credentialPath: input.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath,
1005
+ };
850
1006
  const saved = await readCredential(options.credentialPath);
851
1007
  if (saved?.issuer === undefined || saved.access === "")
852
1008
  throw new Error("Sign in to Fungi to list your Teams");
@@ -881,7 +1037,11 @@ const teamBalanceSchema = z.object({
881
1037
  reserved: moneySnapshotSchema,
882
1038
  available: moneySnapshotSchema,
883
1039
  });
884
- export async function readFungiTeamBalance(options = {}) {
1040
+ export async function readFungiTeamBalance(input = {}) {
1041
+ const options = {
1042
+ ...input,
1043
+ credentialPath: input.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath,
1044
+ };
885
1045
  const saved = await readCredential(options.credentialPath);
886
1046
  if (saved?.issuer === undefined || saved.access === "")
887
1047
  throw new Error("Sign in to Fungi to read your Team balance");
@@ -973,7 +1133,11 @@ function cachedFungiAccess(cached, issuer, rejectedAccess) {
973
1133
  * when the saved access token actually needs it: a plain read never takes
974
1134
  * the lock or rewrites the file, so a still-valid credential stays untouched.
975
1135
  */
976
- export async function freshFungiAccess(rawHub, options = {}) {
1136
+ export async function freshFungiAccess(rawHub, input = {}) {
1137
+ const options = {
1138
+ ...input,
1139
+ credentialPath: input.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath,
1140
+ };
977
1141
  const issuer = validateEndpoint(rawHub).origin;
978
1142
  options.signal?.throwIfAborted();
979
1143
  const cached = await readCredential(options.credentialPath);
@@ -995,7 +1159,11 @@ export async function freshFungiAccess(rawHub, options = {}) {
995
1159
  return savedCredentialSchema.parse(current).access;
996
1160
  }
997
1161
  /** Retry an explicitly refused request once; the cloned body is replayable. */
998
- export function fungiAccountFetch(rawHub, options = {}) {
1162
+ export function fungiAccountFetch(rawHub, input = {}) {
1163
+ const options = {
1164
+ ...input,
1165
+ credentialPath: input.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath,
1166
+ };
999
1167
  const issuer = validateEndpoint(rawHub).origin;
1000
1168
  const transport = ownFungiFetch(issuer, options.fetch, options.signal);
1001
1169
  const requestWithRetry = async (input, init) => {
@@ -1040,7 +1208,11 @@ export function fungiAccountFetch(rawHub, options = {}) {
1040
1208
  });
1041
1209
  }
1042
1210
  /** The SDK owns exactly one retry; this hook rotates only the refused credential. */
1043
- export function fungiMcpAccountAuth(endpoint, options = {}) {
1211
+ export function fungiMcpAccountAuth(endpoint, input = {}) {
1212
+ const options = {
1213
+ ...input,
1214
+ credentialPath: input.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath,
1215
+ };
1044
1216
  let lastToken;
1045
1217
  return {
1046
1218
  token: async () => {
@@ -1063,7 +1235,11 @@ export function fungiAccountTeam(credential) {
1063
1235
  return saved.team;
1064
1236
  }
1065
1237
  /** Pi uses its own locked modify; this owner supplies the same refresh as MCP. */
1066
- export function createFungiAccountOAuth(options = {}) {
1238
+ export function createFungiAccountOAuth(input = {}) {
1239
+ const options = {
1240
+ ...input,
1241
+ credentialPath: input.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath,
1242
+ };
1067
1243
  return {
1068
1244
  name: "Fungi",
1069
1245
  loginLabel: "Sign in to Fungi",
@@ -1112,7 +1288,11 @@ export function createFungiAccountOAuth(options = {}) {
1112
1288
  };
1113
1289
  }
1114
1290
  /** Pi deletion clears Local custody and reports unconfirmed remote revocation. */
1115
- export function createFungiAccountCredentialStore(options = {}) {
1291
+ export function createFungiAccountCredentialStore(input = {}) {
1292
+ const options = {
1293
+ ...input,
1294
+ credentialPath: input.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath,
1295
+ };
1116
1296
  const store = credentialStore(options.credentialPath);
1117
1297
  return {
1118
1298
  async read(id, operation) {
@@ -1,5 +1,6 @@
1
1
  import { type AuthProvider, type FetchLike, type OAuthClientProvider } from "@modelcontextprotocol/client";
2
2
  import { type CapabilitySource } from "@fungi.computer/mycelium";
3
+ import { type RequestMcpFormApproval } from "./private-mcp-form-elicitation.js";
3
4
  /** The MCP client identity Local reports to the Hub. */
4
5
  export declare const fungiMcpClientInfo: Readonly<{
5
6
  name: "fungi-local";
@@ -10,9 +11,12 @@ export declare function fungiMcpReachableFetch(endpoint: URL, fetchImpl?: FetchL
10
11
  /** Host-held selection and credentials; neither enters the model's arguments. */
11
12
  export type PrivateFungiMcpConnectionOptions = Readonly<{
12
13
  endpoint: URL;
14
+ authentication?: "oyster-key" | undefined;
13
15
  authProvider: AuthProvider | OAuthClientProvider;
14
16
  /** The maintained transport's fetch seam, also used by the local caller proof. */
15
17
  fetch?: FetchLike;
18
+ /** Trusted human interaction; never supplied by model arguments. */
19
+ requestApproval?: RequestMcpFormApproval | undefined;
16
20
  }>;
17
21
  /**
18
22
  * Add the existing Fungi MCP execute door to a Local run's capability scope.