@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,331 @@
1
+ import { correlationKey } from "@fungi.computer/cairn";
2
+ import { createHash } from "node:crypto";
3
+ import { machineEnrollmentRecordSchema, } from "./private-local-mesh-record.js";
4
+ import { chmod, mkdir, open } from "node:fs/promises";
5
+ import { dirname, isAbsolute, resolve } from "node:path";
6
+ import lockfile from "proper-lockfile";
7
+ import writeFileAtomic from "write-file-atomic";
8
+ import { z } from "zod";
9
+ const maximumCredentialFileBytes = 256 * 1_024;
10
+ const maximumCredentialEntries = 128;
11
+ const maximumProviderIdLength = 256;
12
+ export const maximumCredentialStringLength = 64 * 1_024;
13
+ const maximumEnvironmentEntries = 64;
14
+ const maximumExtensionEntries = 64;
15
+ const privateDirectoryMode = 0o700;
16
+ const privateFileMode = 0o600;
17
+ const lockOptions = {
18
+ realpath: false,
19
+ stale: 30_000,
20
+ update: 10_000,
21
+ retries: { retries: 20, factor: 1.2, minTimeout: 25, maxTimeout: 100 },
22
+ };
23
+ const providerIdSchema = z.string().min(1).max(maximumProviderIdLength);
24
+ const credentialStringSchema = z.string().max(maximumCredentialStringLength);
25
+ const environmentSchema = z
26
+ .record(z.string().min(1).max(maximumProviderIdLength), credentialStringSchema)
27
+ .superRefine((environment, context) => {
28
+ if (Object.keys(environment).length > maximumEnvironmentEntries) {
29
+ context.addIssue({
30
+ code: "custom",
31
+ message: "Credential environment contains too many entries",
32
+ });
33
+ }
34
+ });
35
+ const apiKeyCredentialSchema = z
36
+ .strictObject({
37
+ type: z.literal("api_key"),
38
+ key: credentialStringSchema.optional(),
39
+ env: environmentSchema.optional(),
40
+ })
41
+ .transform((input) => {
42
+ const credential = { type: "api_key" };
43
+ if (input.key !== undefined)
44
+ credential.key = input.key;
45
+ if (input.env !== undefined)
46
+ credential.env = input.env;
47
+ return credential;
48
+ });
49
+ export const oauthCredentialSchema = z
50
+ .object({
51
+ type: z.literal("oauth"),
52
+ refresh: credentialStringSchema,
53
+ access: credentialStringSchema,
54
+ expires: z.number(),
55
+ })
56
+ .catchall(z.json())
57
+ .superRefine((credential, context) => {
58
+ if (Object.keys(credential).length > maximumExtensionEntries + 4) {
59
+ context.addIssue({
60
+ code: "custom",
61
+ message: "OAuth credential contains too many entries",
62
+ });
63
+ }
64
+ for (const key of Object.keys(credential)) {
65
+ if (key.length > maximumProviderIdLength) {
66
+ context.addIssue({
67
+ code: "custom",
68
+ message: "OAuth credential contains an oversized key",
69
+ });
70
+ }
71
+ }
72
+ });
73
+ const credentialSchema = z.discriminatedUnion("type", [
74
+ apiKeyCredentialSchema,
75
+ oauthCredentialSchema,
76
+ machineEnrollmentRecordSchema,
77
+ ]);
78
+ const machineRecordId = (origin) => `machine-enrollment:${createHash("sha256").update(origin).digest("hex")}`;
79
+ const credentialMapSchema = z
80
+ .record(providerIdSchema, credentialSchema)
81
+ .superRefine((credentials, context) => {
82
+ for (const [id, record] of Object.entries(credentials))
83
+ if (record.type === "machine_enrollment" &&
84
+ id !== machineRecordId(record.origin))
85
+ context.addIssue({
86
+ code: "custom",
87
+ message: "Machine enrollment origin does not match its record",
88
+ });
89
+ if (Object.keys(credentials).length > maximumCredentialEntries) {
90
+ context.addIssue({
91
+ code: "custom",
92
+ message: "Credential store contains too many providers",
93
+ });
94
+ }
95
+ });
96
+ const missingFileErrorSchema = z.object({ code: z.literal("ENOENT") });
97
+ /** Resolve the host-owned default path without consulting project or Agent Home state. */
98
+ export function resolveDefaultCredentialPath(home) {
99
+ if (!isAbsolute(home)) {
100
+ throw new Error("Default credential home must be absolute");
101
+ }
102
+ return resolve(home, ".config", "fungi", "credentials.json");
103
+ }
104
+ function credentialStoreError(message) {
105
+ return new Error(message);
106
+ }
107
+ function ownCredential(credentials, providerId) {
108
+ if (credentials === undefined || !Object.hasOwn(credentials, providerId)) {
109
+ return undefined;
110
+ }
111
+ const record = credentials[providerId];
112
+ return record?.type === "machine_enrollment" ? undefined : record;
113
+ }
114
+ function parseCredentials(serialized) {
115
+ let parsed;
116
+ try {
117
+ parsed = JSON.parse(serialized);
118
+ }
119
+ catch {
120
+ throw credentialStoreError("Credential store contains malformed JSON");
121
+ }
122
+ const result = credentialMapSchema.safeParse(parsed);
123
+ if (!result.success) {
124
+ throw credentialStoreError("Credential store contains invalid credential data");
125
+ }
126
+ return result.data;
127
+ }
128
+ /**
129
+ * Internal constructor permits one focused write-failure test while the public
130
+ * package surface remains the Pi CredentialStore factory below.
131
+ */
132
+ export class FileCredentialStore {
133
+ #path;
134
+ #writeAtomic;
135
+ constructor(path, writeAtomic = writeFileAtomic) {
136
+ if (!isAbsolute(path)) {
137
+ throw new Error("Private credential path must be absolute");
138
+ }
139
+ this.#path = path;
140
+ this.#writeAtomic = writeAtomic;
141
+ }
142
+ /** Keep the installation UUID beside credentials, across logout and restart. */
143
+ async getDeviceId() {
144
+ return this.#withLock(async () => {
145
+ const path = `${this.#path}.device-id`;
146
+ let handle;
147
+ try {
148
+ handle = await open(path, "r");
149
+ }
150
+ catch (error) {
151
+ if (!missingFileErrorSchema.safeParse(error).success)
152
+ throw error;
153
+ const id = correlationKey();
154
+ await this.#writeAtomic(path, id, {
155
+ encoding: "utf8",
156
+ fsync: true,
157
+ mode: privateFileMode,
158
+ });
159
+ return id;
160
+ }
161
+ try {
162
+ // A UUID has exactly 36 bytes; the extra byte detects oversized input.
163
+ const bytes = Buffer.alloc(37);
164
+ const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
165
+ const parsed = z
166
+ .uuid()
167
+ .safeParse(bytes.subarray(0, bytesRead).toString("utf8"));
168
+ if (!parsed.success)
169
+ throw credentialStoreError("Local model device ID is invalid");
170
+ return parsed.data;
171
+ }
172
+ finally {
173
+ await handle.close();
174
+ }
175
+ });
176
+ }
177
+ async read(providerId, options) {
178
+ options?.signal?.throwIfAborted();
179
+ return ownCredential(await this.#readCredentials(), providerId);
180
+ }
181
+ async list(options) {
182
+ options?.signal?.throwIfAborted();
183
+ const credentials = await this.#readCredentials();
184
+ if (credentials === undefined)
185
+ return [];
186
+ return Object.entries(credentials).flatMap(([providerId, credential]) => credential.type === "machine_enrollment"
187
+ ? []
188
+ : [{ providerId, type: credential.type }]);
189
+ }
190
+ async modify(providerId, callback, options) {
191
+ options?.signal?.throwIfAborted();
192
+ return await this.#withLock(async () => {
193
+ options?.signal?.throwIfAborted();
194
+ const credentials = (await this.#readCredentials()) ?? {};
195
+ if (credentials[providerId]?.type === "machine_enrollment")
196
+ throw credentialStoreError("A model credential cannot replace a machine enrollment");
197
+ const current = ownCredential(credentials, providerId);
198
+ const next = await callback(current);
199
+ options?.signal?.throwIfAborted();
200
+ if (next === undefined)
201
+ return current;
202
+ const validated = this.#validateCredentials({
203
+ ...credentials,
204
+ [providerId]: next,
205
+ });
206
+ await this.#replace(validated);
207
+ const persisted = ownCredential(validated, providerId);
208
+ if (persisted === undefined) {
209
+ throw credentialStoreError("Credential store failed to retain its update");
210
+ }
211
+ return persisted;
212
+ });
213
+ }
214
+ async delete(providerId, options) {
215
+ await this.remove(providerId, () => Promise.resolve(), options);
216
+ }
217
+ async readMachineEnrollments() {
218
+ return Object.values((await this.#readCredentials()) ?? {}).filter((record) => record.type === "machine_enrollment");
219
+ }
220
+ async modifyMachineEnrollments(origin, edit, owner) {
221
+ await this.#withLock(async () => {
222
+ const credentials = (await this.#readCredentials()) ?? {};
223
+ owner?.admit(ownCredential(credentials, owner.providerId));
224
+ const id = machineRecordId(origin);
225
+ const previous = credentials[id];
226
+ if (previous !== undefined && previous.type !== "machine_enrollment")
227
+ throw credentialStoreError("Machine enrollment record is occupied");
228
+ const enrollments = edit(previous?.enrollments ?? []);
229
+ const others = Object.fromEntries(Object.entries(credentials).filter(([key]) => key !== id));
230
+ await this.#replace(this.#validateCredentials(enrollments.length === 0
231
+ ? others
232
+ : {
233
+ ...others,
234
+ [id]: machineEnrollmentRecordSchema.parse({
235
+ type: "machine_enrollment",
236
+ origin,
237
+ enrollments,
238
+ }),
239
+ }));
240
+ });
241
+ }
242
+ async removeMachineEnrollments(origin) {
243
+ return this.#withLock(async () => {
244
+ const credentials = await this.#readCredentials();
245
+ const id = machineRecordId(origin);
246
+ if (credentials?.[id]?.type !== "machine_enrollment")
247
+ return false;
248
+ const next = Object.fromEntries(Object.entries(credentials).filter(([key]) => key !== id));
249
+ await this.#replace(this.#validateCredentials(next));
250
+ return true;
251
+ });
252
+ }
253
+ async remove(providerId, beforeRemove, options) {
254
+ options?.signal?.throwIfAborted();
255
+ return this.#withLock(async () => {
256
+ options?.signal?.throwIfAborted();
257
+ const credentials = await this.#readCredentials();
258
+ const current = ownCredential(credentials, providerId);
259
+ if (credentials === undefined || current === undefined)
260
+ return undefined;
261
+ const result = await beforeRemove(current);
262
+ const next = Object.fromEntries(Object.entries(credentials).filter(([id]) => id !== providerId));
263
+ options?.signal?.throwIfAborted();
264
+ await this.#replace(this.#validateCredentials(next));
265
+ return result;
266
+ });
267
+ }
268
+ #validateCredentials(credentials) {
269
+ const result = credentialMapSchema.safeParse(credentials);
270
+ if (!result.success) {
271
+ throw credentialStoreError("Credential update contains invalid credential data");
272
+ }
273
+ return result.data;
274
+ }
275
+ async #readCredentials() {
276
+ let handle;
277
+ try {
278
+ handle = await open(this.#path, "r");
279
+ }
280
+ catch (error) {
281
+ if (missingFileErrorSchema.safeParse(error).success)
282
+ return undefined;
283
+ throw error;
284
+ }
285
+ try {
286
+ const bytes = Buffer.alloc(maximumCredentialFileBytes + 1);
287
+ let totalBytesRead = 0;
288
+ while (totalBytesRead < bytes.length) {
289
+ const { bytesRead } = await handle.read(bytes, totalBytesRead, bytes.length - totalBytesRead, totalBytesRead);
290
+ if (bytesRead === 0)
291
+ break;
292
+ totalBytesRead += bytesRead;
293
+ }
294
+ if (totalBytesRead > maximumCredentialFileBytes) {
295
+ throw credentialStoreError("Credential store exceeds its private file bound");
296
+ }
297
+ return parseCredentials(bytes.subarray(0, totalBytesRead).toString("utf8"));
298
+ }
299
+ finally {
300
+ await handle.close();
301
+ }
302
+ }
303
+ async #replace(credentials) {
304
+ await this.#writeAtomic(this.#path, JSON.stringify(credentials), {
305
+ encoding: "utf8",
306
+ fsync: true,
307
+ mode: privateFileMode,
308
+ });
309
+ }
310
+ /**
311
+ * proper-lockfile bounds acquisition through retries but cannot cancel it
312
+ * while queued. Callers check their signal before acquisition and
313
+ * immediately after it succeeds.
314
+ */
315
+ async #withLock(callback) {
316
+ const directory = dirname(this.#path);
317
+ await mkdir(directory, { recursive: true, mode: privateDirectoryMode });
318
+ await chmod(directory, privateDirectoryMode);
319
+ const release = await lockfile.lock(this.#path, lockOptions);
320
+ try {
321
+ return await callback();
322
+ }
323
+ finally {
324
+ await release();
325
+ }
326
+ }
327
+ }
328
+ /** Create the one private Pi CredentialStore selected by the local host. */
329
+ export function createPrivateCredentialStore(path) {
330
+ return new FileCredentialStore(path);
331
+ }
@@ -0,0 +1,4 @@
1
+ import type { FilesPatch, FilesPort } from "@fungi.computer/knapsack";
2
+ import { type LocalSandboxLaunch } from "./private-invocation-computer.js";
3
+ /** Advertise only a packaged native engine; never install tools during a model call. */
4
+ export declare function createPrivateFilesPatch(root: string, read: FilesPort["read"], sandbox?: LocalSandboxLaunch): Promise<FilesPatch | undefined>;
@@ -0,0 +1,104 @@
1
+ import { access, constants } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+ import { createRequire } from "node:module";
4
+ import process from "node:process";
5
+ import * as ChildProcess from "effect/process/ChildProcess";
6
+ import { parsePatch } from "diff";
7
+ import * as Stream from "effect/Stream";
8
+ import { runPrivateInvocationProcess, } from "./private-invocation-computer.js";
9
+ function patchExecutable() {
10
+ if (process.env.FUNGI_BUNDLED_CLI) {
11
+ const name = `@fungi.computer/spore-native-${process.platform}-${process.arch}`;
12
+ try {
13
+ return createRequire(process.env.FUNGI_BUNDLED_CLI).resolve(`${name}/bin/fungi-apply-patch`);
14
+ }
15
+ catch (cause) {
16
+ throw new Error(`Missing ${name}; reinstall Fungi with npm optional dependencies enabled`, { cause });
17
+ }
18
+ }
19
+ return fileURLToPath(new URL("../native/bin/fungi-apply-patch", import.meta.url));
20
+ }
21
+ const maximumPatchBytes = 128 * 1024;
22
+ const mismatchExitCode = 23;
23
+ const formatExitCode = 24;
24
+ /** Read-only recovery guidance for rejected standard unified-diff input. */
25
+ async function unifiedDiffContext(patch, read, context) {
26
+ try {
27
+ const unified = patch
28
+ .replace(/^\*\*\* Begin Patch\r?\n/u, "")
29
+ .replace(/\r?\n\*\*\* End Patch\s*$/u, "");
30
+ const file = parsePatch(unified)[0];
31
+ const hunk = file?.hunks[0];
32
+ const path = file?.oldFileName.replace(/^a\//u, "");
33
+ if (path === undefined || hunk === undefined || path === "/dev/null")
34
+ return "\nInclude *** Update File: path/to/file before @@ to identify the target.\n";
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`);
38
+ const lines = current.content.split(/\r?\n/u);
39
+ const from = Math.min(Math.max(0, hunk.oldStart - 3), Math.max(0, lines.length - 7));
40
+ const nearby = lines
41
+ .slice(from, from + 7)
42
+ .map((line, index) => ` ${String(from + index + 1)}: ${line.length > 160 ? line.slice(0, 160) + "…" : line}`);
43
+ return `\nCurrent lines in ${path.slice(0, 160)}:\n${nearby.join("\n")}\n${current.truncated ? "File preview is truncated at 128 KiB.\n" : ""}`;
44
+ }
45
+ catch (cause) {
46
+ context.signal.throwIfAborted();
47
+ return `\nCurrent file preview unavailable: ${cause instanceof Error ? cause.message.slice(0, 512) : "unified diff could not be decoded"}\n`;
48
+ }
49
+ }
50
+ /** Advertise only a packaged native engine; never install tools during a model call. */
51
+ export async function createPrivateFilesPatch(root, read, sandbox) {
52
+ const executable = patchExecutable();
53
+ try {
54
+ await access(executable, constants.X_OK);
55
+ }
56
+ catch (cause) {
57
+ if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
58
+ return undefined;
59
+ throw cause;
60
+ }
61
+ return async ({ patch }, context) => {
62
+ context.signal.throwIfAborted();
63
+ const bytes = new TextEncoder().encode(patch);
64
+ if (bytes.byteLength > maximumPatchBytes)
65
+ throw new Error("Patch exceeds the 128 KiB input bound");
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 ?? [], {
73
+ cwd: root,
74
+ env,
75
+ extendEnv: false,
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
+ },
85
+ });
86
+ const result = await runPrivateInvocationProcess(command, context.signal, 30_000);
87
+ if (result.status === "deadline_exceeded")
88
+ return {
89
+ status: "failed",
90
+ exitCode: result.exitCode,
91
+ stdout: result.stdout,
92
+ stderr: `${result.stderr}\n${result.message} Earlier patch writes may remain.`,
93
+ truncated: result.truncated,
94
+ };
95
+ if (result.status === "failed" && result.exitCode === formatExitCode)
96
+ return {
97
+ ...result,
98
+ stderr: result.stderr + (await unifiedDiffContext(patch, read, context)),
99
+ };
100
+ return result.status === "failed" && result.exitCode === mismatchExitCode
101
+ ? { ...result, error: { kind: "mismatch" } }
102
+ : result;
103
+ };
104
+ }
@@ -0,0 +1,252 @@
1
+ import { type AuthProvider, type FetchLike, type OAuthClientProvider, type StoredOAuthClientInformation } from "@modelcontextprotocol/client";
2
+ import { z } from "zod";
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";
6
+ /**
7
+ * One folder attached to a Team Computer. The absolute folder path and the
8
+ * device's private key never leave this file; the Hub holds only the public
9
+ * key and the two labels.
10
+ */
11
+ declare const localBackendRecordSchema: z.ZodObject<{
12
+ requestId: z.ZodString;
13
+ teamId: z.ZodString;
14
+ computerId: z.ZodString;
15
+ computerName: z.ZodString;
16
+ rootPath: z.ZodString;
17
+ hostLabel: z.ZodString;
18
+ folderLabel: z.ZodString;
19
+ publicKey: z.ZodBase64;
20
+ privateKey: z.ZodBase64;
21
+ backendId: z.ZodOptional<z.ZodString>;
22
+ }, z.core.$strict>;
23
+ export type LocalBackendRecord = z.output<typeof localBackendRecordSchema>;
24
+ declare const teamChoiceSchema: z.ZodObject<{
25
+ teamId: z.ZodString;
26
+ handle: z.ZodString;
27
+ name: z.ZodString;
28
+ }, z.core.$strip>;
29
+ export type FungiAccountTeam = z.output<typeof teamChoiceSchema>;
30
+ export type PrivateFungiAccountOAuthProviderOptions = {
31
+ interactive?: boolean;
32
+ credentialPath?: string;
33
+ issuer?: string;
34
+ /** The Team MCP endpoint named in re-login guidance. */
35
+ endpoint?: URL;
36
+ /** The listening loopback callback for an interactive login. */
37
+ redirectUrl?: string;
38
+ openBrowser?: (url: URL) => void;
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;
48
+ /** Hand a Fungi sign-in page to the platform browser; callers show the URL. */
49
+ export declare function launchPlatformBrowser(url: URL): void;
50
+ /** Persisted OAuth state uses Local's existing owner-only Pi credential file. */
51
+ export declare function privateFungiAccountOAuthProvider(input?: PrivateFungiAccountOAuthProviderOptions): OAuthClientProvider & {
52
+ saveClientInformation(information: StoredOAuthClientInformation): Promise<void>;
53
+ expectedState(): string | undefined;
54
+ authorizationUrl(): URL | undefined;
55
+ };
56
+ export type PrivateFungiAccountLoginOptions = {
57
+ credentialPath?: string;
58
+ /**
59
+ * Asked before a login for another origin replaces the saved login. Without
60
+ * it, a login for another origin fails with the logout command to run.
61
+ */
62
+ confirmReplace?: (savedIssuer: string, nextIssuer: string) => Promise<boolean>;
63
+ /** Choose a current membership when the account has several Teams. */
64
+ chooseTeam?: (teams: readonly FungiAccountTeam[]) => Promise<string>;
65
+ signal?: AbortSignal;
66
+ /** The maintained OAuth fetch seam. */
67
+ fetch?: FetchLike;
68
+ /** Opens the Hub pages; the default launches the platform browser. */
69
+ openBrowser?: (url: URL) => void;
70
+ };
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";
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;
78
+ export type FungiAccountStatus = {
79
+ issuer: string;
80
+ accessExpiresAt: Date;
81
+ team?: FungiAccountTeam;
82
+ teamCount?: number;
83
+ };
84
+ /** Non-secret status of the shared Fungi account. */
85
+ export declare function readFungiAccountStatus(input?: {
86
+ credentialPath?: string;
87
+ }): Promise<FungiAccountStatus | undefined>;
88
+ /**
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.
93
+ */
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?: {
101
+ credentialPath?: string;
102
+ fetch?: FetchLike;
103
+ }): Promise<PrivateFungiAccountLogoutResult>;
104
+ /**
105
+ * Run one Mycelium control call against the Team MCP endpoint with the saved
106
+ * login, and return the tool's text result. An error result or a transport
107
+ * failure is `undefined`: the caller retries with its persisted request id.
108
+ */
109
+ export declare function callFungiMcpTeamControl(teamEndpoint: URL, code: string, options?: Readonly<{
110
+ credentialPath?: string;
111
+ fetch?: FetchLike;
112
+ signal?: AbortSignal;
113
+ timeoutMs?: number;
114
+ }>): Promise<string | undefined>;
115
+ /** The Team MCP endpoint and Team a control call addresses, validated. */
116
+ export declare function fungiMcpTeamEndpoint(rawEndpoint: string | URL): {
117
+ endpoint: URL;
118
+ teamId: string;
119
+ };
120
+ /** The machine's attached folders, as Local keeps them. */
121
+ export declare function readFungiMcpLocalBackends(input?: Readonly<{
122
+ credentialPath?: string;
123
+ }>): Promise<Readonly<{
124
+ issuer: string | undefined;
125
+ backends: LocalBackendRecord[];
126
+ }>>;
127
+ /** Replace the attached folder records in the owner-only credential file. */
128
+ export declare function writeFungiMcpLocalBackends(update: (current: readonly LocalBackendRecord[]) => LocalBackendRecord[], input?: Readonly<{
129
+ credentialPath?: string;
130
+ }>): Promise<void>;
131
+ /** Complete first-party registration and OAuth authorization in a foreground TUI. */
132
+ export declare function signInFungiAccount(rawEndpoint: string | URL, input?: PrivateFungiAccountLoginOptions): Promise<void>;
133
+ /** Switch only to a fresh membership; failure preserves the prior selected Team. */
134
+ export declare function switchFungiAccountTeam(teamId: string, input?: {
135
+ credentialPath?: string;
136
+ fetch?: FetchLike;
137
+ }): Promise<void>;
138
+ export type FungiAccountTeams = Readonly<{
139
+ teams: readonly FungiAccountTeam[];
140
+ selectedTeamId?: string;
141
+ }>;
142
+ /** Fresh memberships for the saved account, with the Team that pays for models. */
143
+ export declare function readFungiAccountTeams(input?: {
144
+ credentialPath?: string;
145
+ fetch?: FetchLike;
146
+ }): Promise<FungiAccountTeams>;
147
+ declare const teamBalanceSchema: z.ZodObject<{
148
+ teamId: z.ZodString;
149
+ balance: z.ZodObject<{
150
+ amount: z.ZodString;
151
+ currency: z.ZodObject<{
152
+ code: z.ZodLiteral<"USD">;
153
+ base: z.ZodLiteral<10>;
154
+ exponent: z.ZodLiteral<2>;
155
+ }, z.core.$strip>;
156
+ scale: z.ZodLiteral<5>;
157
+ }, z.core.$strip>;
158
+ included: z.ZodNullable<z.ZodObject<{
159
+ granted: z.ZodObject<{
160
+ amount: z.ZodString;
161
+ currency: z.ZodObject<{
162
+ code: z.ZodLiteral<"USD">;
163
+ base: z.ZodLiteral<10>;
164
+ exponent: z.ZodLiteral<2>;
165
+ }, z.core.$strip>;
166
+ scale: z.ZodLiteral<5>;
167
+ }, z.core.$strip>;
168
+ remaining: z.ZodObject<{
169
+ amount: z.ZodString;
170
+ currency: z.ZodObject<{
171
+ code: z.ZodLiteral<"USD">;
172
+ base: z.ZodLiteral<10>;
173
+ exponent: z.ZodLiteral<2>;
174
+ }, z.core.$strip>;
175
+ scale: z.ZodLiteral<5>;
176
+ }, z.core.$strip>;
177
+ expiresAt: z.ZodNumber;
178
+ }, z.core.$strict>>;
179
+ reserved: z.ZodObject<{
180
+ amount: z.ZodString;
181
+ currency: z.ZodObject<{
182
+ code: z.ZodLiteral<"USD">;
183
+ base: z.ZodLiteral<10>;
184
+ exponent: z.ZodLiteral<2>;
185
+ }, z.core.$strip>;
186
+ scale: z.ZodLiteral<5>;
187
+ }, z.core.$strip>;
188
+ available: z.ZodObject<{
189
+ amount: z.ZodString;
190
+ currency: z.ZodObject<{
191
+ code: z.ZodLiteral<"USD">;
192
+ base: z.ZodLiteral<10>;
193
+ exponent: z.ZodLiteral<2>;
194
+ }, z.core.$strip>;
195
+ scale: z.ZodLiteral<5>;
196
+ }, z.core.$strip>;
197
+ }, z.core.$strip>;
198
+ export type FungiTeamCredit = z.output<typeof teamBalanceSchema>;
199
+ /**
200
+ * The selected Team's credit as the Hub billing read reports it. Refusals are
201
+ * typed absences: 404 the person is no longer a member, 403 this sign-in
202
+ * lacks the billing:read grant (signed in before it existed; sign in again),
203
+ * 401 the Hub does not admit this sign-in for billing.
204
+ */
205
+ export type FungiTeamBalance = Readonly<{
206
+ kind: "balance";
207
+ credit: FungiTeamCredit;
208
+ }> | Readonly<{
209
+ kind: "not-available";
210
+ reason: "no-team" | "not-a-member" | "sign-in-again" | "sign-in-not-admitted";
211
+ }>;
212
+ export declare function readFungiTeamBalance(input?: {
213
+ credentialPath?: string;
214
+ fetch?: FetchLike;
215
+ }): Promise<FungiTeamBalance>;
216
+ /**
217
+ * One cross-process credential lock serves MCP and model access, but only
218
+ * when the saved access token actually needs it: a plain read never takes
219
+ * the lock or rewrites the file, so a still-valid credential stays untouched.
220
+ */
221
+ export declare function freshFungiAccess(rawHub: string | URL, input?: {
222
+ credentialPath?: string;
223
+ fetch?: FetchLike;
224
+ signal?: AbortSignal;
225
+ /** Compare inside the lock: another request may already have rotated it. */
226
+ rejectedAccess?: string;
227
+ }): Promise<string>;
228
+ /** Retry an explicitly refused request once; the cloned body is replayable. */
229
+ export declare function fungiAccountFetch(rawHub: string | URL, input?: {
230
+ credentialPath?: string;
231
+ fetch?: FetchLike;
232
+ signal?: AbortSignal;
233
+ }): typeof globalThis.fetch;
234
+ /** The SDK owns exactly one retry; this hook rotates only the refused credential. */
235
+ export declare function fungiMcpAccountAuth(endpoint: URL, input?: {
236
+ credentialPath?: string;
237
+ fetch?: FetchLike;
238
+ signal?: AbortSignal;
239
+ }): AuthProvider;
240
+ /** Selected payer metadata belongs to the shared account, never a model request body. */
241
+ export declare function fungiAccountTeam(credential: Credential): FungiAccountTeam;
242
+ /** Pi uses its own locked modify; this owner supplies the same refresh as MCP. */
243
+ export declare function createFungiAccountOAuth(input?: {
244
+ credentialPath?: string;
245
+ fetch?: FetchLike;
246
+ }): OAuthAuth;
247
+ /** Pi deletion clears Local custody and reports unconfirmed remote revocation. */
248
+ export declare function createFungiAccountCredentialStore(input?: {
249
+ credentialPath?: string;
250
+ fetch?: FetchLike;
251
+ }): CredentialStore;
252
+ export {};