@fungi.computer/truffle 0.0.0-stage → 0.1.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 (116) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +294 -2
  3. package/computerd-loopback.cjs +16 -0
  4. package/dist/auth-cli.d.ts +36 -0
  5. package/dist/auth-cli.js +351 -0
  6. package/dist/fungi-account-command.d.ts +18 -0
  7. package/dist/fungi-account-command.js +92 -0
  8. package/dist/index.d.ts +19 -0
  9. package/dist/index.js +17 -0
  10. package/dist/internal/private-workerd-sandbox-worker.mjs +27488 -0
  11. package/dist/internal/sandbox-metafile.json +6726 -0
  12. package/dist/linear-auth.d.ts +4 -0
  13. package/dist/linear-auth.js +72 -0
  14. package/dist/local-extensions.d.ts +97 -0
  15. package/dist/local-extensions.js +484 -0
  16. package/dist/local-login.d.ts +55 -0
  17. package/dist/local-login.js +167 -0
  18. package/dist/local-models.d.ts +14 -0
  19. package/dist/local-models.js +77 -0
  20. package/dist/local-skill-source.d.ts +6 -0
  21. package/dist/local-skill-source.js +357 -0
  22. package/dist/log-event.d.ts +2 -0
  23. package/dist/log-event.js +9 -0
  24. package/dist/private-bun-shiitake-host.d.ts +11 -0
  25. package/dist/private-bun-shiitake-host.js +66 -0
  26. package/dist/private-file-credential-store.d.ts +33 -0
  27. package/dist/private-file-credential-store.js +283 -0
  28. package/dist/private-files-patch.d.ts +3 -0
  29. package/dist/private-files-patch.js +86 -0
  30. package/dist/private-fungi-account.d.ts +235 -0
  31. package/dist/private-fungi-account.js +1149 -0
  32. package/dist/private-fungi-auth-error.d.ts +5 -0
  33. package/dist/private-fungi-auth-error.js +7 -0
  34. package/dist/private-fungi-mcp-connection.d.ts +22 -0
  35. package/dist/private-fungi-mcp-connection.js +143 -0
  36. package/dist/private-fungi-mcp-endpoint.d.ts +2 -0
  37. package/dist/private-fungi-mcp-endpoint.js +15 -0
  38. package/dist/private-fungi-mcp-selection.d.ts +14 -0
  39. package/dist/private-fungi-mcp-selection.js +36 -0
  40. package/dist/private-fungi-model-api.d.ts +6 -0
  41. package/dist/private-fungi-model-api.js +125 -0
  42. package/dist/private-fungi-model-catalog.d.ts +6 -0
  43. package/dist/private-fungi-model-catalog.js +78 -0
  44. package/dist/private-fungi-model-provider.d.ts +6 -0
  45. package/dist/private-fungi-model-provider.js +46 -0
  46. package/dist/private-fungi-pending-logouts.d.ts +34 -0
  47. package/dist/private-fungi-pending-logouts.js +136 -0
  48. package/dist/private-invocation-computer.d.ts +37 -0
  49. package/dist/private-invocation-computer.js +459 -0
  50. package/dist/private-invocation-files.d.ts +15 -0
  51. package/dist/private-invocation-files.js +449 -0
  52. package/dist/private-local-acp-stdio.d.ts +17 -0
  53. package/dist/private-local-acp-stdio.js +142 -0
  54. package/dist/private-local-auth-rpc.d.ts +69 -0
  55. package/dist/private-local-auth-rpc.js +273 -0
  56. package/dist/private-local-computer-attach.d.ts +49 -0
  57. package/dist/private-local-computer-attach.js +216 -0
  58. package/dist/private-local-computer-backend.d.ts +282 -0
  59. package/dist/private-local-computer-backend.js +402 -0
  60. package/dist/private-local-computer-processes.d.ts +199 -0
  61. package/dist/private-local-computer-processes.js +589 -0
  62. package/dist/private-local-default-model.d.ts +11 -0
  63. package/dist/private-local-default-model.js +45 -0
  64. package/dist/private-local-delegation.d.ts +29 -0
  65. package/dist/private-local-delegation.js +78 -0
  66. package/dist/private-local-effective-model.d.ts +19 -0
  67. package/dist/private-local-effective-model.js +14 -0
  68. package/dist/private-local-exec-launch.d.ts +41 -0
  69. package/dist/private-local-exec-launch.js +51 -0
  70. package/dist/private-local-exec-path.d.ts +6 -0
  71. package/dist/private-local-exec-path.js +40 -0
  72. package/dist/private-local-git-exclude.d.ts +2 -0
  73. package/dist/private-local-git-exclude.js +33 -0
  74. package/dist/private-local-http-dispatch.d.ts +5 -0
  75. package/dist/private-local-http-dispatch.js +14 -0
  76. package/dist/private-local-mail.d.ts +11 -0
  77. package/dist/private-local-mail.js +69 -0
  78. package/dist/private-local-model-settings.d.ts +5 -0
  79. package/dist/private-local-model-settings.js +31 -0
  80. package/dist/private-local-runtime-host.d.ts +141 -0
  81. package/dist/private-local-runtime-host.js +988 -0
  82. package/dist/private-local-service-contract.d.ts +90 -0
  83. package/dist/private-local-service-contract.js +1 -0
  84. package/dist/private-local-service-discovery.d.ts +101 -0
  85. package/dist/private-local-service-discovery.js +221 -0
  86. package/dist/private-local-service-ids.d.ts +3 -0
  87. package/dist/private-local-service-ids.js +8 -0
  88. package/dist/private-local-service-listener.d.ts +15 -0
  89. package/dist/private-local-service-listener.js +211 -0
  90. package/dist/private-local-service-owner.d.ts +1567 -0
  91. package/dist/private-local-service-owner.js +371 -0
  92. package/dist/private-local-service-process.d.ts +1 -0
  93. package/dist/private-local-service-process.js +6 -0
  94. package/dist/private-local-service.d.ts +17 -0
  95. package/dist/private-local-service.js +330 -0
  96. package/dist/private-local-session-discovery.d.ts +19 -0
  97. package/dist/private-local-session-discovery.js +157 -0
  98. package/dist/private-local-shiitake-host.d.ts +44 -0
  99. package/dist/private-local-shiitake-host.js +143 -0
  100. package/dist/private-local-subagents.d.ts +27 -0
  101. package/dist/private-local-subagents.js +77 -0
  102. package/dist/private-local-worker-server.d.ts +15 -0
  103. package/dist/private-local-worker-server.js +136 -0
  104. package/dist/private-sqlite-owner.d.ts +7 -0
  105. package/dist/private-sqlite-owner.js +103 -0
  106. package/dist/private-team-computers.d.ts +38 -0
  107. package/dist/private-team-computers.js +101 -0
  108. package/dist/private-workerd-sandbox.d.ts +3 -0
  109. package/dist/private-workerd-sandbox.js +347 -0
  110. package/dist/team-apps.d.ts +24 -0
  111. package/dist/team-apps.js +123 -0
  112. package/dist/tracker-git-credential.d.ts +15 -0
  113. package/dist/tracker-git-credential.js +87 -0
  114. package/native/bin/build.json +4037 -0
  115. package/native/bin/fungi-apply-patch +0 -0
  116. package/package.json +148 -3
@@ -0,0 +1,9 @@
1
+ import * as Redacted from "effect/Redacted";
2
+ /** Retain the original Local failure without printing exception or input text. */
3
+ export function logEvent(operation, cause) {
4
+ globalThis.console.error(JSON.stringify({
5
+ source: "local-runtime",
6
+ operation,
7
+ failureKind: cause instanceof Error ? "Error" : "NonErrorThrown",
8
+ }), Redacted.make(cause));
9
+ }
@@ -0,0 +1,11 @@
1
+ import { type RpcTarget } from "@fungi.computer/capnweb";
2
+ import type { PrivateLocalShiitakeServer } from "./private-local-shiitake-host.js";
3
+ import { type PrivateLocalShiitakeWorkerBatch, type PrivateLocalShiitakeWorkerInput } from "./private-local-shiitake-host.js";
4
+ /** Retained native Bun listener, now owned beside the persistent Local service. */
5
+ export declare function openPrivateLocalBunServer(fetch: (request: Request) => Response | Promise<Response>, listenAddress?: string, auth?: Readonly<{
6
+ path: string;
7
+ admit: (request: Request) => boolean;
8
+ createTarget: () => RpcTarget;
9
+ }>): PrivateLocalShiitakeServer;
10
+ /** Retained static worker server; persistent service callers use the manager. */
11
+ export declare function openPrivateLocalBunShiitakeWorkerBatch(inputs: readonly PrivateLocalShiitakeWorkerInput[], listenAddress: string): Promise<PrivateLocalShiitakeWorkerBatch>;
@@ -0,0 +1,66 @@
1
+ /// <reference types="bun-types" />
2
+ import { serve } from "bun";
3
+ import { dispatchPrivateLocalRequest } from "./private-local-http-dispatch.js";
4
+ import { newBunWebSocketRpcHandler, } from "@fungi.computer/capnweb";
5
+ import { openPrivateLocalShiitakeWorkerBatch, } from "./private-local-shiitake-host.js";
6
+ function parsePrivateLoopbackListener(value) {
7
+ const url = new URL(value);
8
+ const loopback = url.hostname === "127.0.0.1" || url.hostname === "[::1]";
9
+ const port = Number(url.port);
10
+ if (url.protocol !== "http:" ||
11
+ !loopback ||
12
+ url.username !== "" ||
13
+ url.password !== "" ||
14
+ url.pathname !== "/" ||
15
+ url.search !== "" ||
16
+ url.hash !== "" ||
17
+ !Number.isInteger(port) ||
18
+ port < 1 ||
19
+ port > 65_535)
20
+ throw new Error("Private local Shiitake listener requires loopback HTTP with an explicit port");
21
+ return {
22
+ hostname: url.hostname === "127.0.0.1" ? "127.0.0.1" : "::1",
23
+ port,
24
+ };
25
+ }
26
+ /** Retained native Bun listener, now owned beside the persistent Local service. */
27
+ export function openPrivateLocalBunServer(fetch, listenAddress, auth) {
28
+ const listener = listenAddress === undefined
29
+ ? { hostname: "127.0.0.1", port: 0 }
30
+ : parsePrivateLoopbackListener(listenAddress);
31
+ // A model retry can leave SSE quiet, and command admission/cancellation can
32
+ // await owner cleanup. Bun's socket-idle clock must not sever either while
33
+ // the handler is pending. Restore its ordinary idle limit for finite replies.
34
+ const dispatch = async (request, server) => {
35
+ return dispatchPrivateLocalRequest(request, server, fetch);
36
+ };
37
+ const server = auth === undefined
38
+ ? serve({ ...listener, fetch: dispatch })
39
+ : serve({
40
+ ...listener,
41
+ websocket: newBunWebSocketRpcHandler(auth.createTarget),
42
+ fetch(request, server) {
43
+ if (new URL(request.url).pathname === auth.path) {
44
+ if (!auth.admit(request))
45
+ return new Response(null, { status: 403 });
46
+ if (request.headers.get("upgrade")?.toLowerCase() !== "websocket")
47
+ return new Response(null, { status: 426 });
48
+ if (server.upgrade(request, { data: undefined }))
49
+ return;
50
+ return new Response(null, { status: 400 });
51
+ }
52
+ return dispatch(request, server);
53
+ },
54
+ });
55
+ return Object.freeze({
56
+ baseUrl: server.url.origin,
57
+ close: () => server.stop(true),
58
+ });
59
+ }
60
+ /** Retained static worker server; persistent service callers use the manager. */
61
+ export async function openPrivateLocalBunShiitakeWorkerBatch(inputs, listenAddress) {
62
+ // Validate the native listener before any Local owner opens its store.
63
+ parsePrivateLoopbackListener(listenAddress);
64
+ return openPrivateLocalShiitakeWorkerBatch(inputs, (fetch) => openPrivateLocalBunServer(fetch, listenAddress));
65
+ }
66
+ /// <reference types="bun-types" />
@@ -0,0 +1,33 @@
1
+ import type { AuthOperationOptions, Credential, CredentialInfo, CredentialStore } from "@earendil-works/pi-ai";
2
+ export declare const maximumCredentialStringLength: number;
3
+ export type AtomicWrite = (path: string, content: string, options: Readonly<{
4
+ encoding: BufferEncoding;
5
+ fsync: boolean;
6
+ mode: number;
7
+ }>) => Promise<void>;
8
+ /** A host-private Pi credential store selected by one absolute filesystem path. */
9
+ export type PrivateCredentialStore = CredentialStore & {
10
+ /** Stable host identity supplied to Pi's ChatGPT sign-in, never a credential. */
11
+ getDeviceId(): Promise<string>;
12
+ /** Revoke or validate current custody under the same lock, then delete it atomically. */
13
+ remove<T>(providerId: string, beforeRemove: (credential: Credential) => Promise<T>, options?: AuthOperationOptions): Promise<T | undefined>;
14
+ };
15
+ /** Resolve the host-owned default path without consulting project or Agent Home state. */
16
+ export declare function resolveDefaultCredentialPath(home?: string): string;
17
+ /**
18
+ * Internal constructor permits one focused write-failure test while the public
19
+ * package surface remains the Pi CredentialStore factory below.
20
+ */
21
+ export declare class FileCredentialStore implements CredentialStore {
22
+ #private;
23
+ constructor(path: string, writeAtomic?: AtomicWrite);
24
+ /** Keep the installation UUID beside credentials, across logout and restart. */
25
+ getDeviceId(): Promise<string>;
26
+ read(providerId: string, options?: AuthOperationOptions): Promise<Credential | undefined>;
27
+ list(options?: AuthOperationOptions): Promise<readonly CredentialInfo[]>;
28
+ modify(providerId: string, callback: (current: Credential | undefined) => Promise<Credential | undefined>, options?: AuthOperationOptions): Promise<Credential | undefined>;
29
+ delete(providerId: string, options?: AuthOperationOptions): Promise<void>;
30
+ remove<T>(providerId: string, beforeRemove: (credential: Credential) => Promise<T>, options?: AuthOperationOptions): Promise<T | undefined>;
31
+ }
32
+ /** Create the one private Pi CredentialStore selected by the local host. */
33
+ export declare function createPrivateCredentialStore(path?: string): PrivateCredentialStore;
@@ -0,0 +1,283 @@
1
+ import { correlationKey } from "@fungi.computer/cairn";
2
+ import { chmod, mkdir, open } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { dirname, isAbsolute, resolve } from "node:path";
5
+ import lockfile from "proper-lockfile";
6
+ import writeFileAtomic from "write-file-atomic";
7
+ import { z } from "zod";
8
+ const maximumCredentialFileBytes = 256 * 1_024;
9
+ const maximumCredentialEntries = 128;
10
+ const maximumProviderIdLength = 256;
11
+ export const maximumCredentialStringLength = 64 * 1_024;
12
+ const maximumEnvironmentEntries = 64;
13
+ const maximumExtensionEntries = 64;
14
+ const privateDirectoryMode = 0o700;
15
+ const privateFileMode = 0o600;
16
+ const lockOptions = {
17
+ realpath: false,
18
+ stale: 30_000,
19
+ update: 10_000,
20
+ retries: { retries: 20, factor: 1.2, minTimeout: 25, maxTimeout: 100 },
21
+ };
22
+ const providerIdSchema = z.string().min(1).max(maximumProviderIdLength);
23
+ const credentialStringSchema = z.string().max(maximumCredentialStringLength);
24
+ const environmentSchema = z
25
+ .record(z.string().min(1).max(maximumProviderIdLength), credentialStringSchema)
26
+ .superRefine((environment, context) => {
27
+ if (Object.keys(environment).length > maximumEnvironmentEntries) {
28
+ context.addIssue({
29
+ code: "custom",
30
+ message: "Credential environment contains too many entries",
31
+ });
32
+ }
33
+ });
34
+ const apiKeyCredentialSchema = z
35
+ .strictObject({
36
+ type: z.literal("api_key"),
37
+ key: credentialStringSchema.optional(),
38
+ env: environmentSchema.optional(),
39
+ })
40
+ .transform((input) => {
41
+ const credential = { type: "api_key" };
42
+ if (input.key !== undefined)
43
+ credential.key = input.key;
44
+ if (input.env !== undefined)
45
+ credential.env = input.env;
46
+ return credential;
47
+ });
48
+ const oauthCredentialSchema = z
49
+ .object({
50
+ type: z.literal("oauth"),
51
+ refresh: credentialStringSchema,
52
+ access: credentialStringSchema,
53
+ expires: z.number(),
54
+ })
55
+ .catchall(z.json())
56
+ .superRefine((credential, context) => {
57
+ if (Object.keys(credential).length > maximumExtensionEntries + 4) {
58
+ context.addIssue({
59
+ code: "custom",
60
+ message: "OAuth credential contains too many entries",
61
+ });
62
+ }
63
+ for (const key of Object.keys(credential)) {
64
+ if (key.length > maximumProviderIdLength) {
65
+ context.addIssue({
66
+ code: "custom",
67
+ message: "OAuth credential contains an oversized key",
68
+ });
69
+ }
70
+ }
71
+ });
72
+ const credentialSchema = z.discriminatedUnion("type", [
73
+ apiKeyCredentialSchema,
74
+ oauthCredentialSchema,
75
+ ]);
76
+ const credentialMapSchema = z
77
+ .record(providerIdSchema, credentialSchema)
78
+ .superRefine((credentials, context) => {
79
+ if (Object.keys(credentials).length > maximumCredentialEntries) {
80
+ context.addIssue({
81
+ code: "custom",
82
+ message: "Credential store contains too many providers",
83
+ });
84
+ }
85
+ });
86
+ const missingFileErrorSchema = z.object({ code: z.literal("ENOENT") });
87
+ /** Resolve the host-owned default path without consulting project or Agent Home state. */
88
+ export function resolveDefaultCredentialPath(home = homedir()) {
89
+ if (!isAbsolute(home)) {
90
+ throw new Error("Default credential home must be absolute");
91
+ }
92
+ return resolve(home, ".config", "fungi", "credentials.json");
93
+ }
94
+ function credentialStoreError(message) {
95
+ return new Error(message);
96
+ }
97
+ function ownCredential(credentials, providerId) {
98
+ if (credentials === undefined || !Object.hasOwn(credentials, providerId)) {
99
+ return undefined;
100
+ }
101
+ return credentials[providerId];
102
+ }
103
+ function parseCredentials(serialized) {
104
+ let parsed;
105
+ try {
106
+ parsed = JSON.parse(serialized);
107
+ }
108
+ catch {
109
+ throw credentialStoreError("Credential store contains malformed JSON");
110
+ }
111
+ const result = credentialMapSchema.safeParse(parsed);
112
+ if (!result.success) {
113
+ throw credentialStoreError("Credential store contains invalid credential data");
114
+ }
115
+ return result.data;
116
+ }
117
+ /**
118
+ * Internal constructor permits one focused write-failure test while the public
119
+ * package surface remains the Pi CredentialStore factory below.
120
+ */
121
+ export class FileCredentialStore {
122
+ #path;
123
+ #writeAtomic;
124
+ constructor(path, writeAtomic = writeFileAtomic) {
125
+ if (!isAbsolute(path)) {
126
+ throw new Error("Private credential path must be absolute");
127
+ }
128
+ this.#path = path;
129
+ this.#writeAtomic = writeAtomic;
130
+ }
131
+ /** Keep the installation UUID beside credentials, across logout and restart. */
132
+ async getDeviceId() {
133
+ return this.#withLock(async () => {
134
+ const path = `${this.#path}.device-id`;
135
+ let handle;
136
+ try {
137
+ handle = await open(path, "r");
138
+ }
139
+ catch (error) {
140
+ if (!missingFileErrorSchema.safeParse(error).success)
141
+ throw error;
142
+ const id = correlationKey();
143
+ await this.#writeAtomic(path, id, {
144
+ encoding: "utf8",
145
+ fsync: true,
146
+ mode: privateFileMode,
147
+ });
148
+ return id;
149
+ }
150
+ try {
151
+ // A UUID has exactly 36 bytes; the extra byte detects oversized input.
152
+ const bytes = Buffer.alloc(37);
153
+ const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
154
+ const parsed = z
155
+ .uuid()
156
+ .safeParse(bytes.subarray(0, bytesRead).toString("utf8"));
157
+ if (!parsed.success)
158
+ throw credentialStoreError("Local model device ID is invalid");
159
+ return parsed.data;
160
+ }
161
+ finally {
162
+ await handle.close();
163
+ }
164
+ });
165
+ }
166
+ async read(providerId, options) {
167
+ options?.signal?.throwIfAborted();
168
+ return ownCredential(await this.#readCredentials(), providerId);
169
+ }
170
+ async list(options) {
171
+ options?.signal?.throwIfAborted();
172
+ const credentials = await this.#readCredentials();
173
+ if (credentials === undefined)
174
+ return [];
175
+ return Object.entries(credentials).map(([providerId, credential]) => ({
176
+ providerId,
177
+ type: credential.type,
178
+ }));
179
+ }
180
+ async modify(providerId, callback, options) {
181
+ options?.signal?.throwIfAborted();
182
+ return await this.#withLock(async () => {
183
+ options?.signal?.throwIfAborted();
184
+ const credentials = (await this.#readCredentials()) ?? {};
185
+ const current = ownCredential(credentials, providerId);
186
+ const next = await callback(current);
187
+ options?.signal?.throwIfAborted();
188
+ if (next === undefined)
189
+ return current;
190
+ const validated = this.#validateCredentials({
191
+ ...credentials,
192
+ [providerId]: next,
193
+ });
194
+ await this.#replace(validated);
195
+ const persisted = ownCredential(validated, providerId);
196
+ if (persisted === undefined) {
197
+ throw credentialStoreError("Credential store failed to retain its update");
198
+ }
199
+ return persisted;
200
+ });
201
+ }
202
+ async delete(providerId, options) {
203
+ await this.remove(providerId, () => Promise.resolve(), options);
204
+ }
205
+ async remove(providerId, beforeRemove, options) {
206
+ options?.signal?.throwIfAborted();
207
+ return this.#withLock(async () => {
208
+ options?.signal?.throwIfAborted();
209
+ const credentials = await this.#readCredentials();
210
+ const current = ownCredential(credentials, providerId);
211
+ if (credentials === undefined || current === undefined)
212
+ return undefined;
213
+ const result = await beforeRemove(current);
214
+ const next = Object.fromEntries(Object.entries(credentials).filter(([id]) => id !== providerId));
215
+ options?.signal?.throwIfAborted();
216
+ await this.#replace(this.#validateCredentials(next));
217
+ return result;
218
+ });
219
+ }
220
+ #validateCredentials(credentials) {
221
+ const result = credentialMapSchema.safeParse(credentials);
222
+ if (!result.success) {
223
+ throw credentialStoreError("Credential update contains invalid credential data");
224
+ }
225
+ return result.data;
226
+ }
227
+ async #readCredentials() {
228
+ let handle;
229
+ try {
230
+ handle = await open(this.#path, "r");
231
+ }
232
+ catch (error) {
233
+ if (missingFileErrorSchema.safeParse(error).success)
234
+ return undefined;
235
+ throw error;
236
+ }
237
+ try {
238
+ const bytes = Buffer.alloc(maximumCredentialFileBytes + 1);
239
+ let totalBytesRead = 0;
240
+ while (totalBytesRead < bytes.length) {
241
+ const { bytesRead } = await handle.read(bytes, totalBytesRead, bytes.length - totalBytesRead, totalBytesRead);
242
+ if (bytesRead === 0)
243
+ break;
244
+ totalBytesRead += bytesRead;
245
+ }
246
+ if (totalBytesRead > maximumCredentialFileBytes) {
247
+ throw credentialStoreError("Credential store exceeds its private file bound");
248
+ }
249
+ return parseCredentials(bytes.subarray(0, totalBytesRead).toString("utf8"));
250
+ }
251
+ finally {
252
+ await handle.close();
253
+ }
254
+ }
255
+ async #replace(credentials) {
256
+ await this.#writeAtomic(this.#path, JSON.stringify(credentials), {
257
+ encoding: "utf8",
258
+ fsync: true,
259
+ mode: privateFileMode,
260
+ });
261
+ }
262
+ /**
263
+ * proper-lockfile bounds acquisition through retries but cannot cancel it
264
+ * while queued. Callers check their signal before acquisition and
265
+ * immediately after it succeeds.
266
+ */
267
+ async #withLock(callback) {
268
+ const directory = dirname(this.#path);
269
+ await mkdir(directory, { recursive: true, mode: privateDirectoryMode });
270
+ await chmod(directory, privateDirectoryMode);
271
+ const release = await lockfile.lock(this.#path, lockOptions);
272
+ try {
273
+ return await callback();
274
+ }
275
+ finally {
276
+ await release();
277
+ }
278
+ }
279
+ }
280
+ /** Create the one private Pi CredentialStore selected by the local host. */
281
+ export function createPrivateCredentialStore(path = resolveDefaultCredentialPath()) {
282
+ return new FileCredentialStore(path);
283
+ }
@@ -0,0 +1,3 @@
1
+ import type { FilesPatch, FilesPort } from "@fungi.computer/knapsack";
2
+ /** 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>;
@@ -0,0 +1,86 @@
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
+ const lines = current.content.split(/\r?\n/u);
37
+ const from = Math.min(Math.max(0, hunk.oldStart - 3), Math.max(0, lines.length - 7));
38
+ const nearby = lines
39
+ .slice(from, from + 7)
40
+ .map((line, index) => ` ${String(from + index + 1)}: ${line.length > 160 ? line.slice(0, 160) + "…" : line}`);
41
+ return `\nCurrent lines in ${path.slice(0, 160)}:\n${nearby.join("\n")}\n${current.truncated ? "File preview is truncated at 128 KiB.\n" : ""}`;
42
+ }
43
+ catch (cause) {
44
+ context.signal.throwIfAborted();
45
+ return `\nCurrent file preview unavailable: ${cause instanceof Error ? cause.message.slice(0, 512) : "unified diff could not be decoded"}\n`;
46
+ }
47
+ }
48
+ /** Advertise only a packaged native engine; never install tools during a model call. */
49
+ export async function createPrivateFilesPatch(root, read) {
50
+ const executable = patchExecutable();
51
+ try {
52
+ await access(executable, constants.X_OK);
53
+ }
54
+ catch (cause) {
55
+ if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
56
+ return undefined;
57
+ throw cause;
58
+ }
59
+ return async ({ patch }, context) => {
60
+ context.signal.throwIfAborted();
61
+ const bytes = new TextEncoder().encode(patch);
62
+ if (bytes.byteLength > maximumPatchBytes)
63
+ throw new Error("Patch exceeds the 128 KiB input bound");
64
+ const command = ChildProcess.make(executable, {
65
+ cwd: root,
66
+ stdin: Stream.make(bytes),
67
+ });
68
+ const result = await runPrivateInvocationProcess(command, context.signal, 30_000);
69
+ if (result.status === "deadline_exceeded")
70
+ return {
71
+ status: "failed",
72
+ exitCode: result.exitCode,
73
+ stdout: result.stdout,
74
+ stderr: `${result.stderr}\n${result.message} Earlier patch writes may remain.`,
75
+ truncated: result.truncated,
76
+ };
77
+ if (result.status === "failed" && result.exitCode === formatExitCode)
78
+ return {
79
+ ...result,
80
+ stderr: result.stderr + (await unifiedDiffContext(patch, read, context)),
81
+ };
82
+ return result.status === "failed" && result.exitCode === mismatchExitCode
83
+ ? { ...result, error: { kind: "mismatch" } }
84
+ : result;
85
+ };
86
+ }