@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.
- package/README.md +282 -34
- package/dist/auth-cli.d.ts +10 -0
- package/dist/auth-cli.js +36 -14
- package/dist/fungi-account-command.d.ts +1 -1
- package/dist/fungi-account-command.js +6 -3
- package/dist/index.d.ts +10 -2
- package/dist/index.js +8 -2
- package/dist/internal/image-fit-metafile.json +106 -0
- package/dist/internal/local-image-fit-worker.mjs +960 -0
- package/dist/internal/private-workerd-sandbox-worker.mjs +11 -12
- package/dist/internal/sandbox-metafile.json +7 -7
- package/dist/local-archive-handoff.d.ts +12 -0
- package/dist/local-archive-handoff.js +21 -0
- package/dist/local-extensions.d.ts +58 -12
- package/dist/local-extensions.js +195 -55
- package/dist/local-image-fit.d.ts +30 -0
- package/dist/local-image-fit.js +105 -0
- package/dist/local-models.d.ts +4 -2
- package/dist/local-models.js +11 -6
- package/dist/local-session-archives.d.ts +8 -0
- package/dist/local-session-archives.js +152 -0
- package/dist/local-skill-source.d.ts +18 -3
- package/dist/local-skill-source.js +216 -140
- package/dist/log-event.d.ts +7 -0
- package/dist/log-event.js +8 -4
- package/dist/private-bounded-response.d.ts +2 -0
- package/dist/private-bounded-response.js +41 -0
- package/dist/private-file-credential-store.d.ts +28 -3
- package/dist/private-file-credential-store.js +57 -9
- package/dist/private-files-patch.d.ts +2 -1
- package/dist/private-files-patch.js +21 -3
- package/dist/private-fungi-account.d.ts +40 -23
- package/dist/private-fungi-account.js +201 -21
- package/dist/private-fungi-mcp-connection.d.ts +4 -0
- package/dist/private-fungi-mcp-connection.js +25 -6
- package/dist/private-fungi-mcp-selection-schema.d.ts +7 -0
- package/dist/private-fungi-mcp-selection-schema.js +15 -0
- package/dist/private-fungi-mcp-selection.d.ts +6 -6
- package/dist/private-fungi-mcp-selection.js +29 -16
- package/dist/private-fungi-model-api.js +4 -4
- package/dist/private-fungi-model-catalog.d.ts +10 -0
- package/dist/private-fungi-model-catalog.js +10 -2
- package/dist/private-fungi-model-provider.d.ts +1 -1
- package/dist/private-fungi-model-provider.js +6 -1
- package/dist/private-fungi-pending-logouts.d.ts +1 -1
- package/dist/private-fungi-pending-logouts.js +1 -2
- package/dist/private-fungi-service-logout.d.ts +44 -0
- package/dist/private-fungi-service-logout.js +59 -0
- package/dist/private-invocation-computer.d.ts +24 -3
- package/dist/private-invocation-computer.js +101 -62
- package/dist/private-invocation-files.d.ts +16 -1
- package/dist/private-invocation-files.js +154 -83
- package/dist/private-local-acp-stdio.js +3 -0
- package/dist/private-local-admission-gate.d.ts +14 -0
- package/dist/private-local-admission-gate.js +52 -0
- package/dist/private-local-agent-control.d.ts +101 -0
- package/dist/private-local-agent-control.js +30 -0
- package/dist/private-local-agent-default-model.d.ts +12 -0
- package/dist/private-local-agent-default-model.js +31 -0
- package/dist/private-local-auth-rpc.d.ts +67 -7
- package/dist/private-local-auth-rpc.js +140 -17
- package/dist/private-local-bwrap.d.ts +13 -0
- package/dist/private-local-bwrap.js +70 -0
- package/dist/private-local-client-version.d.ts +5 -0
- package/dist/private-local-client-version.js +43 -0
- package/dist/private-local-computer-attach.d.ts +3 -3
- package/dist/private-local-computer-attach.js +17 -3
- package/dist/private-local-computer-processes.d.ts +6 -3
- package/dist/private-local-computer-processes.js +55 -31
- package/dist/private-local-config-root.d.ts +21 -0
- package/dist/private-local-config-root.js +19 -0
- package/dist/private-local-containment.d.ts +51 -0
- package/dist/private-local-containment.js +161 -0
- package/dist/private-local-delegation.d.ts +10 -3
- package/dist/private-local-delegation.js +70 -15
- package/dist/private-local-effective-model.d.ts +9 -1
- package/dist/private-local-effective-model.js +3 -0
- package/dist/private-local-exec-environment.d.ts +16 -0
- package/dist/private-local-exec-environment.js +11 -0
- package/dist/private-local-linux-sandbox.d.ts +8 -0
- package/dist/private-local-linux-sandbox.js +67 -0
- package/dist/private-local-mail.d.ts +4 -2
- package/dist/private-local-mail.js +41 -33
- package/dist/private-local-mesh-enrollment.d.ts +34 -0
- package/dist/private-local-mesh-enrollment.js +188 -0
- package/dist/private-local-mesh-links.d.ts +21 -0
- package/dist/private-local-mesh-links.js +229 -0
- package/dist/private-local-mesh-record.d.ts +120 -0
- package/dist/private-local-mesh-record.js +55 -0
- package/dist/private-local-process-identity.d.ts +20 -0
- package/dist/private-local-process-identity.js +40 -0
- package/dist/private-local-runtime-host.d.ts +64 -13
- package/dist/private-local-runtime-host.js +361 -172
- package/dist/private-local-sandbox-policy.d.ts +21 -0
- package/dist/private-local-sandbox-policy.js +121 -0
- package/dist/private-local-sandbox.d.ts +37 -0
- package/dist/private-local-sandbox.js +114 -0
- package/dist/private-local-seatbelt.d.ts +24 -0
- package/dist/private-local-seatbelt.js +68 -0
- package/dist/private-local-service-contract.d.ts +91 -11
- package/dist/private-local-service-discovery.d.ts +129 -13
- package/dist/private-local-service-discovery.js +148 -47
- package/dist/private-local-service-lease.d.ts +16 -0
- package/dist/private-local-service-lease.js +105 -0
- package/dist/private-local-service-listener.d.ts +9 -2
- package/dist/private-local-service-listener.js +260 -64
- package/dist/private-local-service-log.d.ts +128 -0
- package/dist/private-local-service-log.js +321 -0
- package/dist/private-local-service-owner.d.ts +87 -1519
- package/dist/private-local-service-owner.js +1069 -71
- package/dist/private-local-service-process.js +3 -1
- package/dist/private-local-service-termination.d.ts +7 -0
- package/dist/private-local-service-termination.js +67 -0
- package/dist/private-local-service.d.ts +31 -2
- package/dist/private-local-service.js +402 -126
- package/dist/private-local-session-discovery.d.ts +52 -5
- package/dist/private-local-session-discovery.js +132 -11
- package/dist/private-local-shiitake-host.js +2 -2
- package/dist/private-local-startup-model.d.ts +51 -0
- package/dist/private-local-startup-model.js +86 -0
- package/dist/private-local-stop-all.d.ts +27 -0
- package/dist/private-local-stop-all.js +26 -0
- package/dist/private-local-ui-attachment.d.ts +19 -0
- package/dist/private-local-ui-attachment.js +71 -0
- package/dist/private-local-worker-server.d.ts +1 -0
- package/dist/private-local-worker-server.js +13 -5
- package/dist/private-mcp-form-contract.d.ts +186 -0
- package/dist/private-mcp-form-contract.js +53 -0
- package/dist/private-mcp-form-elicitation.d.ts +18 -0
- package/dist/private-mcp-form-elicitation.js +97 -0
- package/dist/private-mcp-form-session.d.ts +172 -0
- package/dist/private-mcp-form-session.js +148 -0
- package/dist/private-openai-claims.d.ts +30 -0
- package/dist/private-openai-claims.js +69 -0
- package/dist/private-oyster-mcp-auth.d.ts +13 -0
- package/dist/private-oyster-mcp-auth.js +57 -0
- package/dist/private-provider-rejection.d.ts +15 -0
- package/dist/private-provider-rejection.js +24 -0
- package/dist/private-sqlite-owner.d.ts +34 -2
- package/dist/private-sqlite-owner.js +103 -37
- package/dist/private-team-computers.d.ts +4 -4
- package/dist/private-team-computers.js +11 -2
- package/dist/seatbelt/safe-base.sbpl +79 -0
- package/dist/subscription-activation.d.ts +9 -0
- package/dist/subscription-activation.js +62 -0
- package/dist/subscription-identity.d.ts +23 -0
- package/dist/subscription-identity.js +124 -0
- package/dist/subscription-turn.d.ts +17 -0
- package/dist/subscription-turn.js +143 -0
- package/dist/team-apps.d.ts +2 -2
- package/dist/team-apps.js +7 -2
- package/native/bin/LICENSE.bubblewrap +502 -0
- package/native/bin/LICENSE.libcap +112 -0
- package/native/bin/LICENSE.sandbox-runtime +201 -0
- package/native/bin/SOURCE.bubblewrap.tar.xz +0 -0
- package/native/bin/bwrap-build-source/Dockerfile +48 -0
- package/native/bin/bwrap-build-source/README.md +96 -0
- package/native/bin/bwrap-build-source/build.mjs +52 -0
- package/native/bin/bwrap-build-source/linux-arm64.ini +14 -0
- package/native/bin/bwrap-build-source/pin.mjs +21 -0
- package/native/bin/bwrap-build-source/record.mjs +74 -0
- package/native/bin/bwrap-build-source/vendor/LICENSE.sandbox-runtime +201 -0
- package/native/bin/bwrap-build-source/vendor/README.md +23 -0
- package/native/bin/bwrap-build-source/vendor/seccomp-unix-block.c +280 -0
- package/native/bin/bwrap-build.json +44 -0
- package/native/bin/fungi-bwrap +0 -0
- package/native/bin/fungi-bwrap.sha256 +1 -0
- package/native/bin/fungi-seccomp.bpf +0 -0
- package/native/bin/fungi-seccomp.bpf.sha256 +1 -0
- package/package.json +44 -31
- package/dist/private-local-model-settings.d.ts +0 -5
- package/dist/private-local-model-settings.js +0 -31
- package/dist/private-local-subagents.d.ts +0 -27
- package/dist/private-local-subagents.js +0 -77
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local shrinks every image it reads before it becomes an image part, as the
|
|
3
|
+
* retained `read_image` did on the Images binding: at most 512 px on the long
|
|
4
|
+
* edge as a quality-80 JPEG, stepping down to a 256 px floor until the bytes
|
|
5
|
+
* meet the 100 KB budget. Photon (Rust `image` compiled to WebAssembly, inlined
|
|
6
|
+
* in the package) decodes and encodes with no network, no Cloudflare and no
|
|
7
|
+
* per-platform native build.
|
|
8
|
+
*
|
|
9
|
+
* Photon's WebAssembly memory grows to the largest decode and never shrinks,
|
|
10
|
+
* and the Local service is long-running, so each image is decoded in its own
|
|
11
|
+
* worker thread that is terminated afterwards, returning that memory.
|
|
12
|
+
*/
|
|
13
|
+
/** Largest image file Local reads to shrink; larger files are refused unread. */
|
|
14
|
+
export declare const maximumLocalImageInputBytes: number;
|
|
15
|
+
/**
|
|
16
|
+
* Largest decoded image Local shrinks, checked from the header before any
|
|
17
|
+
* worker starts. 25 megapixels admits 24 MP phone photos (5712 x 4284) and 6K
|
|
18
|
+
* screenshots.
|
|
19
|
+
*/
|
|
20
|
+
export declare const maximumLocalImagePixels = 25000000;
|
|
21
|
+
/** At most this many bytes of one image reach the model. */
|
|
22
|
+
export declare const maximumLocalImageViewBytes: number;
|
|
23
|
+
/**
|
|
24
|
+
* Shrink one whole png, jpeg, gif (first frame) or webp file for the model.
|
|
25
|
+
* Refuses, with the Files read error, an image whose header or pixels cannot
|
|
26
|
+
* be decoded, one above {@link maximumLocalImagePixels}, and one still above
|
|
27
|
+
* {@link maximumLocalImageViewBytes} at the 256 px floor. Never passes the
|
|
28
|
+
* original bytes through.
|
|
29
|
+
*/
|
|
30
|
+
export declare function fitLocalImage(path: string, bytes: Uint8Array, signal: AbortSignal): Promise<Uint8Array>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Worker } from "node:worker_threads";
|
|
2
|
+
import { imageSize } from "image-size";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
/**
|
|
5
|
+
* Local shrinks every image it reads before it becomes an image part, as the
|
|
6
|
+
* retained `read_image` did on the Images binding: at most 512 px on the long
|
|
7
|
+
* edge as a quality-80 JPEG, stepping down to a 256 px floor until the bytes
|
|
8
|
+
* meet the 100 KB budget. Photon (Rust `image` compiled to WebAssembly, inlined
|
|
9
|
+
* in the package) decodes and encodes with no network, no Cloudflare and no
|
|
10
|
+
* per-platform native build.
|
|
11
|
+
*
|
|
12
|
+
* Photon's WebAssembly memory grows to the largest decode and never shrinks,
|
|
13
|
+
* and the Local service is long-running, so each image is decoded in its own
|
|
14
|
+
* worker thread that is terminated afterwards, returning that memory.
|
|
15
|
+
*/
|
|
16
|
+
/** Largest image file Local reads to shrink; larger files are refused unread. */
|
|
17
|
+
export const maximumLocalImageInputBytes = 16 * 1_024 * 1_024;
|
|
18
|
+
/**
|
|
19
|
+
* Largest decoded image Local shrinks, checked from the header before any
|
|
20
|
+
* worker starts. 25 megapixels admits 24 MP phone photos (5712 x 4284) and 6K
|
|
21
|
+
* screenshots.
|
|
22
|
+
*/
|
|
23
|
+
export const maximumLocalImagePixels = 25_000_000;
|
|
24
|
+
/** At most this many bytes of one image reach the model. */
|
|
25
|
+
export const maximumLocalImageViewBytes = 100 * 1_024;
|
|
26
|
+
/** The retained steps: 512 px long edge, down to a 256 px floor. */
|
|
27
|
+
const viewDimensions = [512, 384, 256];
|
|
28
|
+
const viewQuality = 80;
|
|
29
|
+
/** Built beside this module; the CLI bundle ships it at the same relative path. */
|
|
30
|
+
const workerUrl = new URL("./internal/local-image-fit-worker.mjs", import.meta.url);
|
|
31
|
+
const replySchema = z.discriminatedUnion("kind", [
|
|
32
|
+
z.strictObject({
|
|
33
|
+
kind: z.literal("fitted"),
|
|
34
|
+
bytes: z.instanceof(Uint8Array),
|
|
35
|
+
}),
|
|
36
|
+
z.strictObject({ kind: z.literal("undecodable") }),
|
|
37
|
+
z.strictObject({ kind: z.literal("over-budget") }),
|
|
38
|
+
]);
|
|
39
|
+
const undecodable = (path) => new Error(`Image ${path} could not be decoded. Refusing to read it.`);
|
|
40
|
+
/** Read the header with `image-size`; a header it cannot parse is refused. */
|
|
41
|
+
function header(path, bytes) {
|
|
42
|
+
try {
|
|
43
|
+
return imageSize(bytes);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw undecodable(path);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Run one throwaway worker over `bytes`; it is terminated however it ends. */
|
|
50
|
+
async function shrinkInWorker(bytes, orientation, signal) {
|
|
51
|
+
signal.throwIfAborted();
|
|
52
|
+
const worker = new Worker(workerUrl, {
|
|
53
|
+
workerData: {
|
|
54
|
+
bytes,
|
|
55
|
+
orientation,
|
|
56
|
+
dimensions: viewDimensions,
|
|
57
|
+
quality: viewQuality,
|
|
58
|
+
maxBytes: maximumLocalImageViewBytes,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
// Ends the abort listener with the worker.
|
|
62
|
+
const settled = new AbortController();
|
|
63
|
+
try {
|
|
64
|
+
return await new Promise((resolve, reject) => {
|
|
65
|
+
signal.addEventListener("abort", () => {
|
|
66
|
+
reject(new Error("Local image read was cancelled", {
|
|
67
|
+
cause: signal.reason,
|
|
68
|
+
}));
|
|
69
|
+
}, { once: true, signal: settled.signal });
|
|
70
|
+
worker.once("message", (message) => {
|
|
71
|
+
const reply = replySchema.safeParse(message);
|
|
72
|
+
if (reply.success)
|
|
73
|
+
resolve(reply.data);
|
|
74
|
+
else
|
|
75
|
+
reject(new Error("Local image worker sent an invalid reply"));
|
|
76
|
+
});
|
|
77
|
+
worker.once("error", reject);
|
|
78
|
+
worker.once("exit", (code) => {
|
|
79
|
+
reject(new Error(`Local image worker exited (${String(code)}) before replying`));
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
settled.abort();
|
|
85
|
+
await worker.terminate();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Shrink one whole png, jpeg, gif (first frame) or webp file for the model.
|
|
90
|
+
* Refuses, with the Files read error, an image whose header or pixels cannot
|
|
91
|
+
* be decoded, one above {@link maximumLocalImagePixels}, and one still above
|
|
92
|
+
* {@link maximumLocalImageViewBytes} at the 256 px floor. Never passes the
|
|
93
|
+
* original bytes through.
|
|
94
|
+
*/
|
|
95
|
+
export async function fitLocalImage(path, bytes, signal) {
|
|
96
|
+
const { width, height, orientation } = header(path, bytes);
|
|
97
|
+
if (width * height > maximumLocalImagePixels)
|
|
98
|
+
throw new Error(`Image ${path} is ${String(width)} x ${String(height)} pixels; the read limit is ${String(maximumLocalImagePixels)} pixels. Refusing to read it.`);
|
|
99
|
+
const reply = await shrinkInWorker(bytes, orientation, signal);
|
|
100
|
+
if (reply.kind === "fitted")
|
|
101
|
+
return reply.bytes;
|
|
102
|
+
if (reply.kind === "undecodable")
|
|
103
|
+
throw undecodable(path);
|
|
104
|
+
throw new Error(`Image ${path} does not fit ${String(maximumLocalImageViewBytes)} bytes at ${String(viewDimensions.at(-1))} px. Refusing to read it.`);
|
|
105
|
+
}
|
package/dist/local-models.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { type MutableModels, type ThinkingLevel } from "@earendil-works/pi-ai";
|
|
1
|
+
import { type MutableModels, type Provider, type ThinkingLevel } from "@earendil-works/pi-ai";
|
|
2
2
|
/** Register Pi's standalone OAuth modules on the same instance Local uses. */
|
|
3
3
|
export declare function registerPrivateBundledModelOAuthFlows(): void;
|
|
4
4
|
/** Create the private local Pi collection with Pi's policy-permitted providers. */
|
|
5
|
-
export declare function createPrivateModels(credentialPath?: string
|
|
5
|
+
export declare function createPrivateModels(credentialPath?: string, providerOptions?: {
|
|
6
|
+
providers?: readonly Provider[];
|
|
7
|
+
}): MutableModels;
|
|
6
8
|
export type PrivateLocalModelChoice = Readonly<{
|
|
7
9
|
providerId: string;
|
|
8
10
|
providerName: string;
|
package/dist/local-models.js
CHANGED
|
@@ -4,6 +4,9 @@ import { registerBunOAuthFlows } from "@earendil-works/pi-ai/bun-oauth";
|
|
|
4
4
|
import { loginAllowed, subscriptionKillSwitchFrom, } from "@fungi.computer/veil/model-credentials";
|
|
5
5
|
import { createFungiAccountCredentialStore } from "./private-fungi-account.js";
|
|
6
6
|
import { createFungiModelProvider } from "./private-fungi-model-provider.js";
|
|
7
|
+
import { retainSubscriptionIdentity } from "./subscription-identity.js";
|
|
8
|
+
import { registerSubscriptionTurnOwner } from "./subscription-turn.js";
|
|
9
|
+
import { capturePrivateLocalConfigRoot } from "./private-local-config-root.js";
|
|
7
10
|
/** Register Pi's standalone OAuth modules on the same instance Local uses. */
|
|
8
11
|
export function registerPrivateBundledModelOAuthFlows() {
|
|
9
12
|
registerBunOAuthFlows();
|
|
@@ -26,17 +29,19 @@ function locallyPermitted(provider) {
|
|
|
26
29
|
if (apiKey !== undefined)
|
|
27
30
|
auth.apiKey = apiKey;
|
|
28
31
|
if (oauth !== undefined)
|
|
29
|
-
auth.oauth =
|
|
32
|
+
auth.oauth =
|
|
33
|
+
provider.id === "anthropic" || provider.id === "openai"
|
|
34
|
+
? retainSubscriptionIdentity(oauth, provider.id)
|
|
35
|
+
: oauth;
|
|
30
36
|
return { ...provider, auth };
|
|
31
37
|
}
|
|
32
38
|
/** Create the private local Pi collection with Pi's policy-permitted providers. */
|
|
33
|
-
export function createPrivateModels(credentialPath) {
|
|
34
|
-
const options = {};
|
|
35
|
-
if (credentialPath !== undefined)
|
|
36
|
-
options.credentialPath = credentialPath;
|
|
39
|
+
export function createPrivateModels(credentialPath = capturePrivateLocalConfigRoot().credentialPath, providerOptions = {}) {
|
|
40
|
+
const options = { credentialPath };
|
|
37
41
|
const credentials = createFungiAccountCredentialStore(options);
|
|
38
42
|
const models = createModels({ credentials });
|
|
39
|
-
|
|
43
|
+
registerSubscriptionTurnOwner(models, credentials);
|
|
44
|
+
for (const provider of providerOptions.providers ?? builtinProviders()) {
|
|
40
45
|
const permitted = locallyPermitted(provider);
|
|
41
46
|
if (permitted !== undefined)
|
|
42
47
|
models.setProvider(permitted);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ArchiveWriter } from "@fungi.computer/zookeeper";
|
|
2
|
+
/**
|
|
3
|
+
* Plain archive files beside the Agent store, never synced through Home:
|
|
4
|
+
* `<storeDirectory>/archives/<sessionId>/<contextRevision>-<materialKey>`.
|
|
5
|
+
* Each archive is streamed to a temp file, fsynced, then hard-linked into
|
|
6
|
+
* place so an existing archive is never overwritten.
|
|
7
|
+
*/
|
|
8
|
+
export declare function localArchiveWriter(storeDirectory: string, syncDirectoryPort?: (directory: string) => Promise<void>): ArchiveWriter;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { link, mkdir, open, rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { archiveSessionSegment } from "@fungi.computer/zookeeper";
|
|
5
|
+
import { nanoid } from "@fungi.computer/cairn";
|
|
6
|
+
import { Effect } from "effect";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
const MAX_CHUNK_BYTES = 64 * 1024;
|
|
9
|
+
const SAFE_SEGMENT = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/;
|
|
10
|
+
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
11
|
+
/** Internal control flow: carries the reason a write was refused. */
|
|
12
|
+
class ArchiveRefusal extends Error {
|
|
13
|
+
reason;
|
|
14
|
+
constructor(reason) {
|
|
15
|
+
super(`archive write refused: ${reason}`);
|
|
16
|
+
this.reason = reason;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const keyTakenSchema = z.object({ code: z.literal("EEXIST") });
|
|
20
|
+
const refusalSchema = z.instanceof(ArchiveRefusal);
|
|
21
|
+
function validate(input) {
|
|
22
|
+
const segments = [
|
|
23
|
+
archiveSessionSegment(input.sessionId),
|
|
24
|
+
input.contextRevision,
|
|
25
|
+
input.materialKey,
|
|
26
|
+
];
|
|
27
|
+
if (!segments.every((segment) => SAFE_SEGMENT.test(segment)) ||
|
|
28
|
+
!SHA256_HEX.test(input.sha256) ||
|
|
29
|
+
input.materialKey !== input.sha256.slice(0, 32) ||
|
|
30
|
+
!Number.isSafeInteger(input.byteLength) ||
|
|
31
|
+
input.byteLength < 0) {
|
|
32
|
+
throw new ArchiveRefusal("invalid");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Stream the chunks into an open file, then check them against the declaration. */
|
|
36
|
+
async function writeVerified(path, input, signal) {
|
|
37
|
+
const handle = await open(path, "wx", 0o600);
|
|
38
|
+
try {
|
|
39
|
+
const hash = createHash("sha256");
|
|
40
|
+
let total = 0;
|
|
41
|
+
for await (const chunk of input.chunks()) {
|
|
42
|
+
signal.throwIfAborted();
|
|
43
|
+
if (chunk.byteLength > MAX_CHUNK_BYTES) {
|
|
44
|
+
throw new ArchiveRefusal("invalid");
|
|
45
|
+
}
|
|
46
|
+
hash.update(chunk);
|
|
47
|
+
total += chunk.byteLength;
|
|
48
|
+
let written = 0;
|
|
49
|
+
while (written < chunk.byteLength) {
|
|
50
|
+
const result = await handle.write(chunk, written);
|
|
51
|
+
written += result.bytesWritten;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (total !== input.byteLength || hash.digest("hex") !== input.sha256) {
|
|
55
|
+
throw new ArchiveRefusal("invalid");
|
|
56
|
+
}
|
|
57
|
+
await handle.sync();
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
await handle.close();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function syncDirectory(directory) {
|
|
64
|
+
const handle = await open(directory, "r");
|
|
65
|
+
try {
|
|
66
|
+
await handle.sync();
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await handle.close();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function existingMatches(path, input) {
|
|
73
|
+
const handle = await open(path, "r");
|
|
74
|
+
try {
|
|
75
|
+
const hash = createHash("sha256");
|
|
76
|
+
const buffer = Buffer.alloc(MAX_CHUNK_BYTES);
|
|
77
|
+
let total = 0;
|
|
78
|
+
for (;;) {
|
|
79
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
|
|
80
|
+
if (bytesRead === 0) {
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
84
|
+
total += bytesRead;
|
|
85
|
+
}
|
|
86
|
+
return total === input.byteLength && hash.digest("hex") === input.sha256;
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
await handle.close();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Hard-link the finished temp file into place; false when the key is taken. */
|
|
93
|
+
async function linkIntoPlace(tempPath, finalPath) {
|
|
94
|
+
try {
|
|
95
|
+
await link(tempPath, finalPath);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (keyTakenSchema.safeParse(error).success) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function writeArchiveFile(storeDirectory, input, signal, syncDirectoryPort) {
|
|
106
|
+
validate(input);
|
|
107
|
+
const name = `${input.contextRevision}-${input.materialKey}`;
|
|
108
|
+
const session = archiveSessionSegment(input.sessionId);
|
|
109
|
+
const directory = join(storeDirectory, "archives", session);
|
|
110
|
+
const finalPath = join(directory, name);
|
|
111
|
+
const tempPath = join(directory, `.${input.materialKey}.${nanoid()}.tmp`);
|
|
112
|
+
const receipt = {
|
|
113
|
+
key: `archives/${session}/${name}`,
|
|
114
|
+
sha256: input.sha256,
|
|
115
|
+
byteLength: input.byteLength,
|
|
116
|
+
};
|
|
117
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
118
|
+
try {
|
|
119
|
+
await writeVerified(tempPath, input, signal);
|
|
120
|
+
const created = await linkIntoPlace(tempPath, finalPath);
|
|
121
|
+
if (!created && !(await existingMatches(finalPath, input))) {
|
|
122
|
+
throw new ArchiveRefusal("conflict");
|
|
123
|
+
}
|
|
124
|
+
// A verified archive stays even when the directory fsync fails: a retry
|
|
125
|
+
// finds it by link and returns this same receipt.
|
|
126
|
+
await syncDirectoryPort(directory);
|
|
127
|
+
return receipt;
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
await rm(tempPath, { force: true });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Plain archive files beside the Agent store, never synced through Home:
|
|
135
|
+
* `<storeDirectory>/archives/<sessionId>/<contextRevision>-<materialKey>`.
|
|
136
|
+
* Each archive is streamed to a temp file, fsynced, then hard-linked into
|
|
137
|
+
* place so an existing archive is never overwritten.
|
|
138
|
+
*/
|
|
139
|
+
export function localArchiveWriter(storeDirectory, syncDirectoryPort = syncDirectory) {
|
|
140
|
+
return {
|
|
141
|
+
write: (input) => Effect.tryPromise({
|
|
142
|
+
try: (signal) => writeArchiveFile(storeDirectory, input, signal, syncDirectoryPort),
|
|
143
|
+
catch: (error) => {
|
|
144
|
+
const refusal = refusalSchema.safeParse(error);
|
|
145
|
+
return {
|
|
146
|
+
_tag: "ArchiveWriteError",
|
|
147
|
+
reason: refusal.success ? refusal.data.reason : "unavailable",
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -1,6 +1,21 @@
|
|
|
1
|
-
import type { SkillSource } from "@fungi.computer/shiitake-extensions/skills/source";
|
|
2
|
-
/**
|
|
1
|
+
import type { SkillCatalog, SkillSource } from "@fungi.computer/shiitake-extensions/skills/source";
|
|
2
|
+
/** An enabled extension's skills folder, as the extension owner reports it. */
|
|
3
|
+
export type ExtensionSkillFolder = Readonly<{
|
|
4
|
+
extensionId: string;
|
|
5
|
+
directory: string;
|
|
6
|
+
}>;
|
|
7
|
+
/** What an extension's skills folder offers, for the install prompt. */
|
|
8
|
+
export declare function describeExtensionSkills(folder: ExtensionSkillFolder, signal: AbortSignal): Promise<SkillCatalog>;
|
|
9
|
+
/**
|
|
10
|
+
* Discover skills without widening the invocation Files jail. Folders are
|
|
11
|
+
* searched project first, then personal, then installed extensions' (lowest);
|
|
12
|
+
* the first skill with a name wins, so your own skill beats an extension's.
|
|
13
|
+
* `extensions` is already filtered by the extension owner's activation: a
|
|
14
|
+
* folder that is not passed here contributes nothing. Local installed
|
|
15
|
+
* extensions only; Hub-hosted extensions are not covered.
|
|
16
|
+
*/
|
|
3
17
|
export declare function createLocalSkillSource(options: Readonly<{
|
|
4
18
|
cwd: string;
|
|
5
|
-
home
|
|
19
|
+
home: string;
|
|
20
|
+
extensions?: readonly ExtensionSkillFolder[];
|
|
6
21
|
}>): SkillSource;
|