@alisio/core 0.5.1 → 0.6.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.
- package/dist/analysis/janitor.d.ts +48 -0
- package/dist/analysis/janitor.js +170 -18
- package/dist/analysis/jobs.d.ts +11 -2
- package/dist/analysis/jobs.js +13 -0
- package/dist/analysis/oci.d.ts +11 -2
- package/dist/analysis/oci.js +20 -4
- package/dist/analysis/python/sources.js +2 -1
- package/dist/analysis/rerun.d.ts +1 -1
- package/dist/analysis/status.d.ts +3 -1
- package/dist/analysis/status.js +3 -0
- package/dist/application.d.ts +38 -2
- package/dist/application.js +213 -4
- package/dist/changelog/data.js +64 -0
- package/dist/config.d.ts +110 -2
- package/dist/config.js +188 -6
- package/dist/core/call-internals.d.ts +22 -0
- package/dist/core/call-internals.js +9 -0
- package/dist/core/contracts.d.ts +2 -2
- package/dist/core/runner.js +111 -38
- package/dist/forge/broker.d.ts +51 -0
- package/dist/forge/broker.js +244 -0
- package/dist/forge/budget.d.ts +27 -0
- package/dist/forge/budget.js +60 -0
- package/dist/forge/catalog-view.d.ts +24 -0
- package/dist/forge/catalog-view.js +43 -0
- package/dist/forge/errors.d.ts +26 -0
- package/dist/forge/errors.js +49 -0
- package/dist/forge/guides/javascript.d.ts +25 -0
- package/dist/forge/guides/javascript.js +90 -0
- package/dist/forge/guides/provider.d.ts +42 -0
- package/dist/forge/guides/provider.js +96 -0
- package/dist/forge/guides/python-environment.d.ts +15 -0
- package/dist/forge/guides/python-environment.js +81 -0
- package/dist/forge/guides/python.d.ts +38 -0
- package/dist/forge/guides/python.js +170 -0
- package/dist/forge/javascript/engine.d.ts +44 -0
- package/dist/forge/javascript/engine.js +229 -0
- package/dist/forge/javascript/hosts/factory.d.ts +25 -0
- package/dist/forge/javascript/hosts/factory.js +21 -0
- package/dist/forge/javascript/hosts/framing.d.ts +17 -0
- package/dist/forge/javascript/hosts/framing.js +49 -0
- package/dist/forge/javascript/hosts/isolate-main.d.ts +9 -0
- package/dist/forge/javascript/hosts/isolate-main.js +63 -0
- package/dist/forge/javascript/hosts/process-entry.d.ts +1 -0
- package/dist/forge/javascript/hosts/process-entry.js +3 -0
- package/dist/forge/javascript/hosts/process-host.d.ts +33 -0
- package/dist/forge/javascript/hosts/process-host.js +160 -0
- package/dist/forge/javascript/hosts/worker-entry.d.ts +1 -0
- package/dist/forge/javascript/hosts/worker-entry.js +3 -0
- package/dist/forge/javascript/hosts/worker-host.d.ts +18 -0
- package/dist/forge/javascript/hosts/worker-host.js +101 -0
- package/dist/forge/javascript/prelude.d.ts +2 -0
- package/dist/forge/javascript/prelude.js +333 -0
- package/dist/forge/javascript/probe.d.ts +2 -0
- package/dist/forge/javascript/probe.js +53 -0
- package/dist/forge/javascript/runtime-manifest.d.ts +14 -0
- package/dist/forge/javascript/runtime-manifest.js +14 -0
- package/dist/forge/javascript/vm-runner.d.ts +21 -0
- package/dist/forge/javascript/vm-runner.js +301 -0
- package/dist/forge/javascript/wasm-asset.d.ts +8 -0
- package/dist/forge/javascript/wasm-asset.js +8 -0
- package/dist/forge/javascript/wasm.d.ts +15 -0
- package/dist/forge/javascript/wasm.js +54 -0
- package/dist/forge/journal.d.ts +105 -0
- package/dist/forge/journal.js +174 -0
- package/dist/forge/kernel.d.ts +76 -0
- package/dist/forge/kernel.js +338 -0
- package/dist/forge/limits.d.ts +38 -0
- package/dist/forge/limits.js +73 -0
- package/dist/forge/nested.d.ts +96 -0
- package/dist/forge/nested.js +214 -0
- package/dist/forge/ports.d.ts +120 -0
- package/dist/forge/ports.js +1 -0
- package/dist/forge/progress.d.ts +24 -0
- package/dist/forge/progress.js +76 -0
- package/dist/forge/protocol.d.ts +110 -0
- package/dist/forge/protocol.js +171 -0
- package/dist/forge/python/backend-selector.d.ts +38 -0
- package/dist/forge/python/backend-selector.js +111 -0
- package/dist/forge/python/backend.d.ts +89 -0
- package/dist/forge/python/backend.js +1 -0
- package/dist/forge/python/engine.d.ts +84 -0
- package/dist/forge/python/engine.js +279 -0
- package/dist/forge/python/frame-parser.d.ts +24 -0
- package/dist/forge/python/frame-parser.js +107 -0
- package/dist/forge/python/frame-transport.d.ts +40 -0
- package/dist/forge/python/frame-transport.js +101 -0
- package/dist/forge/python/import-scan.d.ts +22 -0
- package/dist/forge/python/import-scan.js +133 -0
- package/dist/forge/python/managed-backend.d.ts +30 -0
- package/dist/forge/python/managed-backend.js +111 -0
- package/dist/forge/python/oci-backend.d.ts +27 -0
- package/dist/forge/python/oci-backend.js +84 -0
- package/dist/forge/python/pyodide-backend.d.ts +40 -0
- package/dist/forge/python/pyodide-backend.js +289 -0
- package/dist/forge/python/pyodide-install.d.ts +41 -0
- package/dist/forge/python/pyodide-install.js +167 -0
- package/dist/forge/python/pyodide-manifest.d.ts +42 -0
- package/dist/forge/python/pyodide-manifest.js +376 -0
- package/dist/forge/python/pyodide-runner-source.d.ts +2 -0
- package/dist/forge/python/pyodide-runner-source.js +3 -0
- package/dist/forge/python/rss.d.ts +2 -0
- package/dist/forge/python/rss.js +33 -0
- package/dist/forge/python/scratch-copy.d.ts +6 -0
- package/dist/forge/python/scratch-copy.js +61 -0
- package/dist/forge/python/settle.d.ts +6 -0
- package/dist/forge/python/settle.js +21 -0
- package/dist/forge/python/status.d.ts +8 -0
- package/dist/forge/python/status.js +15 -0
- package/dist/forge/python/wasm-node.d.ts +59 -0
- package/dist/forge/python/wasm-node.js +220 -0
- package/dist/forge/python/wiring.d.ts +39 -0
- package/dist/forge/python/wiring.js +47 -0
- package/dist/forge/registry.d.ts +26 -0
- package/dist/forge/registry.js +83 -0
- package/dist/forge/result.d.ts +58 -0
- package/dist/forge/result.js +86 -0
- package/dist/forge/routing-rule.d.ts +19 -0
- package/dist/forge/routing-rule.js +31 -0
- package/dist/forge/runtime.d.ts +64 -0
- package/dist/forge/runtime.js +88 -0
- package/dist/forge/scratch-files.d.ts +36 -0
- package/dist/forge/scratch-files.js +94 -0
- package/dist/forge/scratch-provider.d.ts +41 -0
- package/dist/forge/scratch-provider.js +79 -0
- package/dist/forge/scratch-quota.d.ts +8 -0
- package/dist/forge/scratch-quota.js +25 -0
- package/dist/forge/selector.d.ts +35 -0
- package/dist/forge/selector.js +51 -0
- package/dist/forge/session-dir.d.ts +59 -0
- package/dist/forge/session-dir.js +239 -0
- package/dist/forge/session-path.d.ts +31 -0
- package/dist/forge/session-path.js +61 -0
- package/dist/forge/slots.d.ts +16 -0
- package/dist/forge/slots.js +47 -0
- package/dist/forge/state-delta.d.ts +54 -0
- package/dist/forge/state-delta.js +99 -0
- package/dist/forge/state-machine.d.ts +20 -0
- package/dist/forge/state-machine.js +40 -0
- package/dist/forge/state-store.d.ts +25 -0
- package/dist/forge/state-store.js +50 -0
- package/dist/forge/task-log.d.ts +9 -0
- package/dist/forge/task-log.js +42 -0
- package/dist/forge/verdict.d.ts +20 -0
- package/dist/forge/verdict.js +28 -0
- package/dist/forge-isolate.d.ts +6 -0
- package/dist/forge-isolate.js +6 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/permissions/decide.d.ts +42 -0
- package/dist/permissions/decide.js +24 -0
- package/dist/resources/context.d.ts +2 -1
- package/dist/resources/context.js +2 -1
- package/dist/runtime/access.d.ts +34 -0
- package/dist/runtime/access.js +80 -0
- package/dist/runtime/process.d.ts +2 -1
- package/dist/runtime/process.js +5 -5
- package/dist/runtime/store.d.ts +11 -0
- package/dist/runtime/store.js +103 -0
- package/dist/tools/analysis.d.ts +25 -2
- package/dist/tools/analysis.js +315 -267
- package/dist/tools/execute.js +6 -7
- package/dist/tools/forge-guide.d.ts +6 -0
- package/dist/tools/forge-guide.js +60 -0
- package/dist/tools/js-run.d.ts +19 -0
- package/dist/tools/js-run.js +126 -0
- package/dist/tools/standard.d.ts +7 -1
- package/dist/tools/standard.js +44 -43
- package/package.json +7 -2
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assembly of the Forja engines for one application (spec §4.1): the engine registry with the
|
|
3
|
+
* JavaScript engine's self-check, the kernel and the status the CLI and the API report. Nothing
|
|
4
|
+
* runs here: it builds the objects, and `register()` is the only step that touches the file system
|
|
5
|
+
* (to verify the WASM). Fail closed (D17): an engine that does not verify is registered as
|
|
6
|
+
* unavailable, `js_run` is not offered and `status()` says why.
|
|
7
|
+
*/
|
|
8
|
+
import type { EngineDescriptor } from "@alisio/sdk";
|
|
9
|
+
import type { Config } from "../config.ts";
|
|
10
|
+
import { JavaScriptEngine } from "./javascript/engine.ts";
|
|
11
|
+
import { IsolateHostFactory } from "./javascript/hosts/factory.ts";
|
|
12
|
+
import type { SqliteExecutionJournal } from "./journal.ts";
|
|
13
|
+
import { ExecutionKernel } from "./kernel.ts";
|
|
14
|
+
import { type ForgeLimits } from "./limits.ts";
|
|
15
|
+
import { EngineRegistry } from "./registry.ts";
|
|
16
|
+
import type { SqliteStateStore } from "./state-store.ts";
|
|
17
|
+
export interface ForgeStatus {
|
|
18
|
+
engines: EngineDescriptor[];
|
|
19
|
+
javascript: {
|
|
20
|
+
/** The WASM verified and the host can start, whatever `enabled` says. */
|
|
21
|
+
verified: boolean;
|
|
22
|
+
/** Why it did not verify. */
|
|
23
|
+
verification?: string;
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
host: "worker" | "process";
|
|
26
|
+
hostSetting: "auto" | "worker" | "process";
|
|
27
|
+
wasmSha256: string;
|
|
28
|
+
version: string;
|
|
29
|
+
limits: ForgeLimits;
|
|
30
|
+
};
|
|
31
|
+
scratch: {
|
|
32
|
+
enabled: boolean;
|
|
33
|
+
retentionDays: number;
|
|
34
|
+
maxBytes: number;
|
|
35
|
+
};
|
|
36
|
+
state: {
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
};
|
|
39
|
+
guides: {
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export interface ForgeRuntime {
|
|
44
|
+
engines: EngineRegistry;
|
|
45
|
+
engine: JavaScriptEngine;
|
|
46
|
+
kernel: ExecutionKernel;
|
|
47
|
+
/** The JavaScript engine verified and may be offered (`js_run`). */
|
|
48
|
+
jsReady: boolean;
|
|
49
|
+
status(): Promise<ForgeStatus>;
|
|
50
|
+
/** Starts a real isolate and runs a trivial script (`alisio forge status --probe`). */
|
|
51
|
+
probe(): ReturnType<JavaScriptEngine["probe"]>;
|
|
52
|
+
}
|
|
53
|
+
export interface ForgeRuntimeDeps {
|
|
54
|
+
forge: () => Config["forge"];
|
|
55
|
+
journal: SqliteExecutionJournal;
|
|
56
|
+
state: SqliteStateStore;
|
|
57
|
+
/** Overrides the host factory (tests). */
|
|
58
|
+
hosts?: IsolateHostFactory;
|
|
59
|
+
}
|
|
60
|
+
export declare function createForgeRuntime(deps: ForgeRuntimeDeps): Promise<ForgeRuntime>;
|
|
61
|
+
/** The engine's state as `alisio forge status` and the API report it. */
|
|
62
|
+
export declare function describeForge(forge: Config["forge"], engine: JavaScriptEngine): Promise<ForgeStatus>;
|
|
63
|
+
/** A throwaway engine for the CLI: no application, no database. */
|
|
64
|
+
export declare function forgeEngineFor(forge: Config["forge"]): JavaScriptEngine;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { JavaScriptEngine } from "./javascript/engine.js";
|
|
2
|
+
import { IsolateHostFactory } from "./javascript/hosts/factory.js";
|
|
3
|
+
import { JAVASCRIPT_ENGINE_VERSION, QUICKJS_WASM_SHA256 } from "./javascript/runtime-manifest.js";
|
|
4
|
+
import { loadQuickJsModule } from "./javascript/wasm.js";
|
|
5
|
+
import { ExecutionKernel } from "./kernel.js";
|
|
6
|
+
import { mergeLimits } from "./limits.js";
|
|
7
|
+
import { EngineRegistry } from "./registry.js";
|
|
8
|
+
import { createJavaScriptSelector } from "./selector.js";
|
|
9
|
+
import { SlotTable } from "./slots.js";
|
|
10
|
+
/** One table per process: the per-process cap is shared by every application in it (D-29). */
|
|
11
|
+
const processSlots = new SlotTable();
|
|
12
|
+
const kernelConfig = (forge) => {
|
|
13
|
+
const js = forge.javascript;
|
|
14
|
+
return {
|
|
15
|
+
enabled: js.enabled,
|
|
16
|
+
stateEnabled: forge.state.enabled,
|
|
17
|
+
limits: {
|
|
18
|
+
timeoutMs: js.timeoutMs,
|
|
19
|
+
memoryBytes: js.memoryBytes,
|
|
20
|
+
stackBytes: js.stackBytes,
|
|
21
|
+
maxToolCalls: js.maxToolCalls,
|
|
22
|
+
maxConcurrency: js.maxConcurrency,
|
|
23
|
+
maxOutputChars: js.maxOutputChars,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
export async function createForgeRuntime(deps) {
|
|
28
|
+
const hosts = deps.hosts ??
|
|
29
|
+
new IsolateHostFactory({ choice: deps.forge().javascript.host, module: loadQuickJsModule });
|
|
30
|
+
const engine = new JavaScriptEngine({ hosts });
|
|
31
|
+
const engines = new EngineRegistry();
|
|
32
|
+
let jsReady = false;
|
|
33
|
+
// A disabled engine is never loaded or verified: nothing happens at startup with Forja off.
|
|
34
|
+
if (deps.forge().javascript.enabled)
|
|
35
|
+
jsReady = (await engines.register(engine)).ready;
|
|
36
|
+
const kernel = new ExecutionKernel({
|
|
37
|
+
engines,
|
|
38
|
+
selector: createJavaScriptSelector(),
|
|
39
|
+
journal: deps.journal,
|
|
40
|
+
state: deps.state,
|
|
41
|
+
slots: processSlots,
|
|
42
|
+
config: () => kernelConfig(deps.forge()),
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
engines,
|
|
46
|
+
engine,
|
|
47
|
+
kernel,
|
|
48
|
+
jsReady,
|
|
49
|
+
probe: () => engine.probe(),
|
|
50
|
+
status: () => describeForge(deps.forge(), engine),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** The engine's state as `alisio forge status` and the API report it. */
|
|
54
|
+
export async function describeForge(forge, engine) {
|
|
55
|
+
// Always a fresh self-check: `verified` says whether the engine WOULD run, `enabled` whether it may.
|
|
56
|
+
const verification = await engine.describe();
|
|
57
|
+
const enabled = forge.javascript.enabled;
|
|
58
|
+
const descriptor = !enabled && verification.ready
|
|
59
|
+
? { ...verification, ready: false, reason: "disabled by config (forge.javascript.enabled)" }
|
|
60
|
+
: verification;
|
|
61
|
+
return {
|
|
62
|
+
engines: [descriptor],
|
|
63
|
+
javascript: {
|
|
64
|
+
verified: verification.ready,
|
|
65
|
+
...(verification.ready ? {} : { verification: verification.reason ?? "not verified" }),
|
|
66
|
+
enabled,
|
|
67
|
+
host: engine.hostKind,
|
|
68
|
+
hostSetting: forge.javascript.host,
|
|
69
|
+
wasmSha256: QUICKJS_WASM_SHA256,
|
|
70
|
+
version: JAVASCRIPT_ENGINE_VERSION,
|
|
71
|
+
limits: mergeLimits({ global: kernelConfig(forge).limits }),
|
|
72
|
+
},
|
|
73
|
+
// The folder (`scratch.root`) is a host path: it stays out of what the surfaces show.
|
|
74
|
+
scratch: {
|
|
75
|
+
enabled: forge.scratch.enabled,
|
|
76
|
+
retentionDays: forge.scratch.retentionDays,
|
|
77
|
+
maxBytes: forge.scratch.maxBytes,
|
|
78
|
+
},
|
|
79
|
+
state: { enabled: forge.state.enabled },
|
|
80
|
+
guides: { enabled: forge.guides.enabled },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** A throwaway engine for the CLI: no application, no database. */
|
|
84
|
+
export function forgeEngineFor(forge) {
|
|
85
|
+
return new JavaScriptEngine({
|
|
86
|
+
hosts: new IsolateHostFactory({ choice: forge.javascript.host, module: loadQuickJsModule }),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface ScratchFileInfo {
|
|
2
|
+
name: string;
|
|
3
|
+
size: number;
|
|
4
|
+
mtime: number;
|
|
5
|
+
}
|
|
6
|
+
/** Most files a listing returns. */
|
|
7
|
+
export declare const SCRATCH_LIST_LIMIT = 2000;
|
|
8
|
+
/** Deepest directory level a listing enters. */
|
|
9
|
+
export declare const SCRATCH_LIST_MAX_DEPTH = 32;
|
|
10
|
+
/**
|
|
11
|
+
* Regular files of the scratchpad in name order; links and special files are left out. The walk
|
|
12
|
+
* visits entries in the order of the final sort (a directory sorts as `name/`) and stops as soon
|
|
13
|
+
* as one file more than `limit` is known, so a huge tree costs no more than the cap. `scanned` is
|
|
14
|
+
* how many files were examined.
|
|
15
|
+
*/
|
|
16
|
+
export declare function listScratch(scratchpad: string, limit?: number): Promise<{
|
|
17
|
+
files: ScratchFileInfo[];
|
|
18
|
+
usedBytes: number;
|
|
19
|
+
truncated: boolean;
|
|
20
|
+
scanned: number;
|
|
21
|
+
}>;
|
|
22
|
+
/**
|
|
23
|
+
* A read handle for one file of the scratchpad. The path is confined (lexically and on the real
|
|
24
|
+
* file system), no segment may be a link and the file is opened with `O_NOFOLLOW` where the
|
|
25
|
+
* platform has it. Throws `INVALID_INPUT` for anything that is not a regular file inside.
|
|
26
|
+
*/
|
|
27
|
+
export declare function openScratchFile(scratchpad: string, relative: string): Promise<{
|
|
28
|
+
handle: import("node:fs/promises").FileHandle;
|
|
29
|
+
size: number;
|
|
30
|
+
name: string;
|
|
31
|
+
}>;
|
|
32
|
+
/** Removes everything inside the scratchpad and keeps the folder. Never follows a link. */
|
|
33
|
+
export declare function emptyScratch(scratchpad: string): Promise<{
|
|
34
|
+
removed: number;
|
|
35
|
+
freedBytes: number;
|
|
36
|
+
}>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the HTTP surface does with a session's scratchpad (spec §9.4, §11.1): list it, open one file
|
|
3
|
+
* for download and empty it. Everything is relative to the verified scratchpad folder, symbolic links
|
|
4
|
+
* are never followed (a link is neither listed, opened nor entered; emptying removes the link itself),
|
|
5
|
+
* and nothing returned names an absolute host path.
|
|
6
|
+
*/
|
|
7
|
+
import { constants } from "node:fs";
|
|
8
|
+
import { lstat, open, readdir, rm } from "node:fs/promises";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { ForgeError } from "./errors.js";
|
|
11
|
+
import { resolveScratchPath, scratchUsage } from "./session-dir.js";
|
|
12
|
+
/** Most files a listing returns. */
|
|
13
|
+
export const SCRATCH_LIST_LIMIT = 2_000;
|
|
14
|
+
/** Deepest directory level a listing enters. */
|
|
15
|
+
export const SCRATCH_LIST_MAX_DEPTH = 32;
|
|
16
|
+
/**
|
|
17
|
+
* Regular files of the scratchpad in name order; links and special files are left out. The walk
|
|
18
|
+
* visits entries in the order of the final sort (a directory sorts as `name/`) and stops as soon
|
|
19
|
+
* as one file more than `limit` is known, so a huge tree costs no more than the cap. `scanned` is
|
|
20
|
+
* how many files were examined.
|
|
21
|
+
*/
|
|
22
|
+
export async function listScratch(scratchpad, limit = SCRATCH_LIST_LIMIT) {
|
|
23
|
+
const files = [];
|
|
24
|
+
let scanned = 0;
|
|
25
|
+
const key = (entry) => entry.isDirectory() ? `${entry.name}/` : entry.name;
|
|
26
|
+
const walk = async (dir, prefix, depth) => {
|
|
27
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
28
|
+
entries.sort((a, b) => (key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0));
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
if (files.length > limit)
|
|
31
|
+
return;
|
|
32
|
+
const path = join(dir, entry.name);
|
|
33
|
+
const name = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
34
|
+
if (entry.isDirectory()) {
|
|
35
|
+
if (depth < SCRATCH_LIST_MAX_DEPTH)
|
|
36
|
+
await walk(path, name, depth + 1);
|
|
37
|
+
}
|
|
38
|
+
else if (entry.isFile()) {
|
|
39
|
+
scanned++;
|
|
40
|
+
const info = await lstat(path).catch(() => undefined);
|
|
41
|
+
if (info?.isFile())
|
|
42
|
+
files.push({ name, size: info.size, mtime: Math.floor(info.mtimeMs) });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
await walk(scratchpad, "", 0);
|
|
47
|
+
return {
|
|
48
|
+
files: files.slice(0, limit),
|
|
49
|
+
usedBytes: await scratchUsage(scratchpad),
|
|
50
|
+
truncated: files.length > limit,
|
|
51
|
+
scanned,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A read handle for one file of the scratchpad. The path is confined (lexically and on the real
|
|
56
|
+
* file system), no segment may be a link and the file is opened with `O_NOFOLLOW` where the
|
|
57
|
+
* platform has it. Throws `INVALID_INPUT` for anything that is not a regular file inside.
|
|
58
|
+
*/
|
|
59
|
+
export async function openScratchFile(scratchpad, relative) {
|
|
60
|
+
if (!relative || relative.includes("\0") || relative.includes("\\"))
|
|
61
|
+
throw new ForgeError("INVALID_INPUT", "not a scratchpad file");
|
|
62
|
+
const target = await resolveScratchPath(scratchpad, relative);
|
|
63
|
+
let current = scratchpad;
|
|
64
|
+
const segments = relative.split("/").filter(Boolean);
|
|
65
|
+
for (const segment of segments) {
|
|
66
|
+
current = join(current, segment);
|
|
67
|
+
const info = await lstat(current).catch(() => undefined);
|
|
68
|
+
if (!info || info.isSymbolicLink())
|
|
69
|
+
throw new ForgeError("INVALID_INPUT", "not a scratchpad file");
|
|
70
|
+
}
|
|
71
|
+
const info = await lstat(target);
|
|
72
|
+
if (!info.isFile())
|
|
73
|
+
throw new ForgeError("INVALID_INPUT", "not a scratchpad file");
|
|
74
|
+
const handle = await open(target, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
75
|
+
return { handle, size: info.size, name: segments.at(-1) ?? relative };
|
|
76
|
+
}
|
|
77
|
+
/** Removes everything inside the scratchpad and keeps the folder. Never follows a link. */
|
|
78
|
+
export async function emptyScratch(scratchpad) {
|
|
79
|
+
const freedBytes = await scratchUsage(scratchpad);
|
|
80
|
+
let removed = 0;
|
|
81
|
+
const count = async (dir) => {
|
|
82
|
+
for (const entry of await readdir(dir, { withFileTypes: true }).catch(() => [])) {
|
|
83
|
+
if (entry.isDirectory())
|
|
84
|
+
await count(join(dir, entry.name));
|
|
85
|
+
else
|
|
86
|
+
removed++;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
await count(scratchpad);
|
|
90
|
+
for (const entry of await readdir(scratchpad)) {
|
|
91
|
+
await rm(join(scratchpad, entry), { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
return { removed, freedBytes };
|
|
94
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface ScratchpadProviderOptions {
|
|
2
|
+
/** The root session of `session` (a child session shares its root's scratchpad). */
|
|
3
|
+
rootOf: (session: string) => string;
|
|
4
|
+
/** Workspace of a root session; names its folder. */
|
|
5
|
+
workspaceOf: (rootSessionId: string) => string;
|
|
6
|
+
workspaceKey: (workspace: string) => string;
|
|
7
|
+
/** Home of the `<stateHome>/scratch` fallback. */
|
|
8
|
+
stateHome: string;
|
|
9
|
+
/** `forge.scratch.root`, read on every first use so a changed setting applies. */
|
|
10
|
+
configRoot: () => string | null | undefined;
|
|
11
|
+
/** Reports the diagnostic of a session that fell back to the state folder. */
|
|
12
|
+
onDiagnostic?: (message: string) => void;
|
|
13
|
+
env?: Record<string, string | undefined>;
|
|
14
|
+
}
|
|
15
|
+
export type ScratchpadProvider = (session: string) => Promise<string | undefined>;
|
|
16
|
+
/**
|
|
17
|
+
* The verified scratchpad of the session's ROOT session, created on first use and remembered.
|
|
18
|
+
* Any failure (no usable folder) means "no scratchpad" for that call and is retried next time.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createScratchpadProvider(options: ScratchpadProviderOptions): ScratchpadProvider;
|
|
21
|
+
/**
|
|
22
|
+
* The scratchpad of the session's ROOT session only if its folder already exists: nothing is
|
|
23
|
+
* created. For read-only surfaces (the HTTP listing).
|
|
24
|
+
*/
|
|
25
|
+
export declare function createScratchpadLookup(options: ScratchpadProviderOptions): ScratchpadProvider;
|
|
26
|
+
export interface ScratchRootsOptions {
|
|
27
|
+
/**
|
|
28
|
+
* The store is not the user's default one (an embedder or a test passed its own database): it
|
|
29
|
+
* cannot tell which sessions of the shared temporary root exist, so that root is left alone.
|
|
30
|
+
*/
|
|
31
|
+
customStore: boolean;
|
|
32
|
+
/** `<stateHome>/scratch` of this application's state. */
|
|
33
|
+
stateHome: string;
|
|
34
|
+
configRoot: string | null | undefined;
|
|
35
|
+
env?: Record<string, string | undefined>;
|
|
36
|
+
platform?: NodeJS.Platform;
|
|
37
|
+
uid?: number;
|
|
38
|
+
osTmpdir?: string;
|
|
39
|
+
}
|
|
40
|
+
/** The scratch roots the janitor sweeps: `<tmp>/alisio-<uid>` (default store only) and `<state>/scratch`. */
|
|
41
|
+
export declare function scratchSweepRoots(options: ScratchRootsOptions): string[];
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the application finds the scratchpad of a session and which folders the retention sweep
|
|
3
|
+
* may look at (spec §9.4, D19). Thin glue over `session-dir.ts`: the privacy rules live there, the
|
|
4
|
+
* choice of root lives in `session-path.ts`.
|
|
5
|
+
*/
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { ensureSessionDir, findSessionScratchpad } from "./session-dir.js";
|
|
9
|
+
import { scratchBaseName, tmpRootChoice } from "./session-path.js";
|
|
10
|
+
/**
|
|
11
|
+
* The verified scratchpad of the session's ROOT session, created on first use and remembered.
|
|
12
|
+
* Any failure (no usable folder) means "no scratchpad" for that call and is retried next time.
|
|
13
|
+
*/
|
|
14
|
+
export function createScratchpadProvider(options) {
|
|
15
|
+
const dirs = new Map();
|
|
16
|
+
return async (session) => {
|
|
17
|
+
try {
|
|
18
|
+
const rootSessionId = options.rootOf(session);
|
|
19
|
+
let pending = dirs.get(rootSessionId);
|
|
20
|
+
if (!pending) {
|
|
21
|
+
const workspace = options.workspaceOf(rootSessionId);
|
|
22
|
+
pending = ensureSessionDir({
|
|
23
|
+
rootSessionId,
|
|
24
|
+
workspace,
|
|
25
|
+
workspaceKey: options.workspaceKey(workspace),
|
|
26
|
+
stateHome: options.stateHome,
|
|
27
|
+
configRoot: options.configRoot() ?? null,
|
|
28
|
+
...(options.env ? { env: options.env } : {}),
|
|
29
|
+
});
|
|
30
|
+
dirs.set(rootSessionId, pending);
|
|
31
|
+
pending.then((dir) => {
|
|
32
|
+
if (dir.diagnostic)
|
|
33
|
+
options.onDiagnostic?.(dir.diagnostic);
|
|
34
|
+
}, () => dirs.delete(rootSessionId));
|
|
35
|
+
}
|
|
36
|
+
return (await pending).scratchpad;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The scratchpad of the session's ROOT session only if its folder already exists: nothing is
|
|
45
|
+
* created. For read-only surfaces (the HTTP listing).
|
|
46
|
+
*/
|
|
47
|
+
export function createScratchpadLookup(options) {
|
|
48
|
+
return async (session) => {
|
|
49
|
+
try {
|
|
50
|
+
const rootSessionId = options.rootOf(session);
|
|
51
|
+
const workspace = options.workspaceOf(rootSessionId);
|
|
52
|
+
return await findSessionScratchpad({
|
|
53
|
+
rootSessionId,
|
|
54
|
+
workspace,
|
|
55
|
+
workspaceKey: options.workspaceKey(workspace),
|
|
56
|
+
stateHome: options.stateHome,
|
|
57
|
+
configRoot: options.configRoot() ?? null,
|
|
58
|
+
...(options.env ? { env: options.env } : {}),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** The scratch roots the janitor sweeps: `<tmp>/alisio-<uid>` (default store only) and `<state>/scratch`. */
|
|
67
|
+
export function scratchSweepRoots(options) {
|
|
68
|
+
const roots = [join(options.stateHome, "scratch")];
|
|
69
|
+
if (options.customStore)
|
|
70
|
+
return roots;
|
|
71
|
+
const platform = options.platform ?? process.platform;
|
|
72
|
+
const uid = platform === "win32" ? undefined : (options.uid ?? process.getuid?.());
|
|
73
|
+
const tmp = tmpRootChoice({
|
|
74
|
+
env: (options.env ?? process.env).ALISIO_TMPDIR,
|
|
75
|
+
config: options.configRoot,
|
|
76
|
+
os: options.osTmpdir ?? tmpdir(),
|
|
77
|
+
});
|
|
78
|
+
return [join(tmp, scratchBaseName(platform, uid)), ...roots];
|
|
79
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Whether the quota can be enforced beforehand for this tool. */
|
|
2
|
+
export declare const hasScratchEstimator: (tool: string) => boolean;
|
|
3
|
+
/**
|
|
4
|
+
* The bytes the file(s) will hold after the call: what `PathAccess.scratchQuotaProblem` takes as
|
|
5
|
+
* `incomingBytes` (the current size of the replaced `targets` is subtracted there). A tool with no
|
|
6
|
+
* estimator that a policy allowed anyway counts its `content` string, as before.
|
|
7
|
+
*/
|
|
8
|
+
export declare function scratchIncomingBytes(tool: string, input: Record<string, unknown>, targets: readonly string[]): Promise<number>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many bytes a write into the scratchpad leaves in it (spec §9.4, FJ-AC-34), per tool. The
|
|
3
|
+
* quota check needs the size the file WILL have, and only the tool knows its own semantics:
|
|
4
|
+
* `write_file` stores `content`; `edit_file` replaces one occurrence, so the new size is the
|
|
5
|
+
* current size minus `oldText` plus `newText`. A write-effect tool without an estimate here is not
|
|
6
|
+
* let into the scratchpad without an approval (its size cannot be bounded beforehand).
|
|
7
|
+
*/
|
|
8
|
+
import { lstat } from "node:fs/promises";
|
|
9
|
+
const ESTIMATED = new Set(["write_file", "edit_file"]);
|
|
10
|
+
/** Whether the quota can be enforced beforehand for this tool. */
|
|
11
|
+
export const hasScratchEstimator = (tool) => ESTIMATED.has(tool);
|
|
12
|
+
const bytesOf = (value) => typeof value === "string" ? Buffer.byteLength(value) : 0;
|
|
13
|
+
/**
|
|
14
|
+
* The bytes the file(s) will hold after the call: what `PathAccess.scratchQuotaProblem` takes as
|
|
15
|
+
* `incomingBytes` (the current size of the replaced `targets` is subtracted there). A tool with no
|
|
16
|
+
* estimator that a policy allowed anyway counts its `content` string, as before.
|
|
17
|
+
*/
|
|
18
|
+
export async function scratchIncomingBytes(tool, input, targets) {
|
|
19
|
+
if (tool === "edit_file") {
|
|
20
|
+
const target = targets[0];
|
|
21
|
+
const current = target ? ((await lstat(target).catch(() => undefined))?.size ?? 0) : 0;
|
|
22
|
+
return Math.max(0, current - bytesOf(input.oldText) + bytesOf(input.newText));
|
|
23
|
+
}
|
|
24
|
+
return bytesOf(input.content);
|
|
25
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine selection as a chain of responsibility (spec §4.2, §7.2): ordered, deterministic rules,
|
|
3
|
+
* each returning a decision with its reason or passing to the next. The model never picks the
|
|
4
|
+
* engine. The chain is generic so the Python backend selector reuses it with its own context.
|
|
5
|
+
*/
|
|
6
|
+
import type { EngineDescriptor, EngineId, EngineLanguage } from "@alisio/sdk";
|
|
7
|
+
import { type ForgeErrorCode } from "./errors.ts";
|
|
8
|
+
export interface Selection<I extends string> {
|
|
9
|
+
engine: I;
|
|
10
|
+
reason: string;
|
|
11
|
+
}
|
|
12
|
+
export interface Rejection {
|
|
13
|
+
rejected: true;
|
|
14
|
+
reason: string;
|
|
15
|
+
code: ForgeErrorCode;
|
|
16
|
+
}
|
|
17
|
+
/** `null` passes to the next rule. */
|
|
18
|
+
export type RuleOutcome<I extends string> = Selection<I> | Rejection | null;
|
|
19
|
+
export type SelectionRule<C, I extends string> = (context: C) => RuleOutcome<I>;
|
|
20
|
+
export declare const select: <I extends string>(engine: I, reason: string) => Selection<I>;
|
|
21
|
+
export declare const reject: (reason: string, code?: ForgeErrorCode) => Rejection;
|
|
22
|
+
export declare class EngineSelector<C, I extends string> {
|
|
23
|
+
#private;
|
|
24
|
+
constructor(rules: readonly SelectionRule<C, I>[]);
|
|
25
|
+
/** The first rule that decides wins; a rejection throws and no rule means unavailable. */
|
|
26
|
+
select(context: C): Selection<I>;
|
|
27
|
+
}
|
|
28
|
+
export interface JavaScriptSelectionContext {
|
|
29
|
+
language: EngineLanguage;
|
|
30
|
+
/** `forge.javascript.enabled`. */
|
|
31
|
+
javascriptEnabled: boolean;
|
|
32
|
+
descriptors: Partial<Record<EngineId, EngineDescriptor>>;
|
|
33
|
+
}
|
|
34
|
+
/** The JavaScript chain of §7.2. Python's chain lives with the Python backend. */
|
|
35
|
+
export declare function createJavaScriptSelector(): EngineSelector<JavaScriptSelectionContext, EngineId>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ForgeError } from "./errors.js";
|
|
2
|
+
export const select = (engine, reason) => ({
|
|
3
|
+
engine,
|
|
4
|
+
reason,
|
|
5
|
+
});
|
|
6
|
+
export const reject = (reason, code = "ENGINE_UNAVAILABLE") => ({
|
|
7
|
+
rejected: true,
|
|
8
|
+
reason,
|
|
9
|
+
code,
|
|
10
|
+
});
|
|
11
|
+
export class EngineSelector {
|
|
12
|
+
#rules;
|
|
13
|
+
constructor(rules) {
|
|
14
|
+
this.#rules = rules;
|
|
15
|
+
}
|
|
16
|
+
/** The first rule that decides wins; a rejection throws and no rule means unavailable. */
|
|
17
|
+
select(context) {
|
|
18
|
+
for (const rule of this.#rules) {
|
|
19
|
+
const outcome = rule(context);
|
|
20
|
+
if (outcome === null)
|
|
21
|
+
continue;
|
|
22
|
+
if ("rejected" in outcome)
|
|
23
|
+
throw new ForgeError(outcome.code, outcome.reason);
|
|
24
|
+
return outcome;
|
|
25
|
+
}
|
|
26
|
+
throw new ForgeError("ENGINE_UNAVAILABLE", "No selection rule matched the request");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const JAVASCRIPT_ENGINE = "javascript-quickjs";
|
|
30
|
+
const javascriptDisabled = (c) => c.language === "javascript" && !c.javascriptEnabled ? reject("disabled by config") : null;
|
|
31
|
+
const javascriptNotReady = (c) => {
|
|
32
|
+
if (c.language !== "javascript")
|
|
33
|
+
return null;
|
|
34
|
+
const descriptor = c.descriptors[JAVASCRIPT_ENGINE];
|
|
35
|
+
if (!descriptor)
|
|
36
|
+
return reject(`engine ${JAVASCRIPT_ENGINE} is not registered`);
|
|
37
|
+
if (!descriptor.ready)
|
|
38
|
+
return reject(descriptor.reason ?? `engine ${JAVASCRIPT_ENGINE} is not ready`);
|
|
39
|
+
return null;
|
|
40
|
+
};
|
|
41
|
+
const javascriptDefault = (c) => c.language === "javascript" ? select(JAVASCRIPT_ENGINE, "language=javascript") : null;
|
|
42
|
+
const NO_ENGINE_FOR_LANGUAGE = (c) => c.language === "javascript" ? null : reject(`no engine rule for language ${c.language}`);
|
|
43
|
+
/** The JavaScript chain of §7.2. Python's chain lives with the Python backend. */
|
|
44
|
+
export function createJavaScriptSelector() {
|
|
45
|
+
return new EngineSelector([
|
|
46
|
+
javascriptDisabled,
|
|
47
|
+
javascriptNotReady,
|
|
48
|
+
javascriptDefault,
|
|
49
|
+
NO_ENGINE_FOR_LANGUAGE,
|
|
50
|
+
]);
|
|
51
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export declare const SESSION_MARKER = ".alisio-session";
|
|
2
|
+
export interface SessionDirOptions {
|
|
3
|
+
rootSessionId: string;
|
|
4
|
+
/** Absolute path of the workspace. */
|
|
5
|
+
workspace: string;
|
|
6
|
+
/** `workspaceKey(workspace)` from runtime/paths.ts. */
|
|
7
|
+
workspaceKey: string;
|
|
8
|
+
/** `stateHome()`; home of the fallback. */
|
|
9
|
+
stateHome: string;
|
|
10
|
+
/** `forge.scratch.root`. */
|
|
11
|
+
configRoot?: string | null;
|
|
12
|
+
/** Defaults to `process.env`. */
|
|
13
|
+
env?: Record<string, string | undefined>;
|
|
14
|
+
/** Defaults to `process.platform`. */
|
|
15
|
+
platform?: NodeJS.Platform;
|
|
16
|
+
/** Defaults to `process.getuid()`. */
|
|
17
|
+
uid?: number;
|
|
18
|
+
/** Defaults to `os.tmpdir()`. */
|
|
19
|
+
osTmpdir?: string;
|
|
20
|
+
now?: () => number;
|
|
21
|
+
}
|
|
22
|
+
export interface SessionDir {
|
|
23
|
+
/** `<...>/<rootSessionId>` */
|
|
24
|
+
root: string;
|
|
25
|
+
scratchpad: string;
|
|
26
|
+
tasks: string;
|
|
27
|
+
/** The shared temporary root could not be trusted and the state folder is used instead. */
|
|
28
|
+
fallback: boolean;
|
|
29
|
+
diagnostic?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface SessionMarker {
|
|
32
|
+
sessionId: string;
|
|
33
|
+
workspaceKey: string;
|
|
34
|
+
createdAt: number;
|
|
35
|
+
}
|
|
36
|
+
export declare function ensureSessionDir(options: SessionDirOptions): Promise<SessionDir>;
|
|
37
|
+
/**
|
|
38
|
+
* The scratchpad folder of a session if it already exists and is a private directory, else
|
|
39
|
+
* undefined. Unlike `ensureSessionDir` it never creates anything, so a read-only look (the HTTP
|
|
40
|
+
* listing) leaves no trace. Tries the shared temporary root first, then the state fallback.
|
|
41
|
+
*/
|
|
42
|
+
export declare function findSessionScratchpad(options: SessionDirOptions): Promise<string | undefined>;
|
|
43
|
+
/** The marker of a session folder, or undefined when it is missing or not valid. */
|
|
44
|
+
export declare function readSessionMarker(root: string): Promise<SessionMarker | undefined>;
|
|
45
|
+
/**
|
|
46
|
+
* A scratchpad path the model gave (relative, or absolute inside), checked lexically and then
|
|
47
|
+
* against the real file system: the deepest existing ancestor must resolve inside the real
|
|
48
|
+
* scratchpad, so a symlink that points out is refused. Throws `INVALID_INPUT`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveScratchPath(scratchpad: string, input: string): Promise<string>;
|
|
51
|
+
/** Bytes held by the files of a folder (symlinks are counted as links, never followed). */
|
|
52
|
+
export declare function scratchUsage(dir: string): Promise<number>;
|
|
53
|
+
/**
|
|
54
|
+
* Writes a file into the scratchpad with mode `0600`, refusing paths that escape it and writes
|
|
55
|
+
* that would take the scratchpad past `maxBytes`. Nothing is ever deleted to make room.
|
|
56
|
+
*/
|
|
57
|
+
export declare function writeScratchFile(scratchpad: string, path: string, data: string | Uint8Array, options: {
|
|
58
|
+
maxBytes: number;
|
|
59
|
+
}): Promise<string>;
|