@alisio/core 0.5.2 → 0.7.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 +28 -2
- package/dist/application.js +189 -4
- package/dist/changelog/data.js +42 -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/eval/canonical.d.ts +14 -0
- package/dist/eval/canonical.js +40 -0
- package/dist/eval/cassette.d.ts +180 -0
- package/dist/eval/cassette.js +424 -0
- package/dist/eval/compare.d.ts +147 -0
- package/dist/eval/compare.js +436 -0
- package/dist/eval/engine.d.ts +144 -0
- package/dist/eval/engine.js +728 -0
- package/dist/eval/export.d.ts +40 -0
- package/dist/eval/export.js +90 -0
- package/dist/eval/fingerprint.d.ts +88 -0
- package/dist/eval/fingerprint.js +186 -0
- package/dist/eval/normalize.d.ts +68 -0
- package/dist/eval/normalize.js +138 -0
- package/dist/eval/paths.d.ts +11 -0
- package/dist/eval/paths.js +50 -0
- package/dist/eval/plan.d.ts +26 -0
- package/dist/eval/plan.js +44 -0
- package/dist/eval/report.d.ts +53 -0
- package/dist/eval/report.js +258 -0
- package/dist/eval/run-suite.d.ts +68 -0
- package/dist/eval/run-suite.js +94 -0
- package/dist/eval/secrets.d.ts +12 -0
- package/dist/eval/secrets.js +45 -0
- package/dist/eval/stats.d.ts +17 -0
- package/dist/eval/stats.js +50 -0
- package/dist/eval/suite.d.ts +20 -0
- package/dist/eval/suite.js +165 -0
- package/dist/eval/variants.d.ts +35 -0
- package/dist/eval/variants.js +137 -0
- package/dist/eval/verifiers.d.ts +49 -0
- package/dist/eval/verifiers.js +264 -0
- 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 +27 -0
- package/dist/forge/javascript/hosts/worker-host.js +116 -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 +339 -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 +23 -1
- package/dist/index.js +23 -1
- package/dist/permissions/decide.d.ts +42 -0
- package/dist/permissions/decide.js +24 -0
- package/dist/plan/exit-plan.js +2 -1
- 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/ids.d.ts +21 -0
- package/dist/runtime/ids.js +41 -1
- 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 +106 -2
- 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,50 @@
|
|
|
1
|
+
import { DEFAULT_STATE_TOTAL_BYTES, planCommit, } from "./state-delta.js";
|
|
2
|
+
export class SqliteStateStore {
|
|
3
|
+
#db;
|
|
4
|
+
#now;
|
|
5
|
+
#maxTotalBytes;
|
|
6
|
+
constructor(db, options = {}) {
|
|
7
|
+
this.#db = db;
|
|
8
|
+
this.#now = options.now ?? Date.now;
|
|
9
|
+
const max = options.maxTotalBytes ?? DEFAULT_STATE_TOTAL_BYTES;
|
|
10
|
+
this.#maxTotalBytes = typeof max === "function" ? max : () => max;
|
|
11
|
+
}
|
|
12
|
+
/** Live values and every revision (tombstones included) of a root session. */
|
|
13
|
+
snapshot(rootSessionId) {
|
|
14
|
+
const snapshot = { values: {}, revisions: {} };
|
|
15
|
+
for (const [key, entry] of this.#read(rootSessionId)) {
|
|
16
|
+
snapshot.revisions[key] = entry.revision;
|
|
17
|
+
if (entry.valueJson !== "")
|
|
18
|
+
snapshot.values[key] = JSON.parse(entry.valueJson);
|
|
19
|
+
}
|
|
20
|
+
return snapshot;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Applies a delta against the snapshot it was built from. Throws `STATE_CONFLICT` or
|
|
24
|
+
* `BUDGET_EXCEEDED` without writing anything. Returns the number of rows written.
|
|
25
|
+
*/
|
|
26
|
+
commit(rootSessionId, snapshot, ops) {
|
|
27
|
+
return this.#db.transaction(() => {
|
|
28
|
+
const plan = planCommit({
|
|
29
|
+
current: this.#read(rootSessionId),
|
|
30
|
+
snapshot,
|
|
31
|
+
ops,
|
|
32
|
+
maxTotalBytes: this.#maxTotalBytes(),
|
|
33
|
+
});
|
|
34
|
+
const upsert = this.#db.prepare(`INSERT INTO forge_state(root_session_id,key,value_json,revision,updated_at)
|
|
35
|
+
VALUES(?,?,?,?,?)
|
|
36
|
+
ON CONFLICT(root_session_id,key) DO UPDATE SET
|
|
37
|
+
value_json=excluded.value_json, revision=excluded.revision, updated_at=excluded.updated_at`);
|
|
38
|
+
const now = this.#now();
|
|
39
|
+
for (const write of plan.writes)
|
|
40
|
+
upsert.run(rootSessionId, write.key, write.valueJson, write.revision, now);
|
|
41
|
+
return plan.writes.length;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
#read(rootSessionId) {
|
|
45
|
+
const rows = this.#db
|
|
46
|
+
.prepare("SELECT key, value_json, revision FROM forge_state WHERE root_session_id=?")
|
|
47
|
+
.all(rootSessionId);
|
|
48
|
+
return new Map(rows.map((row) => [row.key, { valueJson: row.value_json, revision: Number(row.revision) }]));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const TASK_LOG_MAX_BYTES: number;
|
|
2
|
+
export declare class TaskLog {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(path: string, max?: number);
|
|
5
|
+
/** Appends one line (a newline is added). Over the bound, one marker line is written and the rest dropped. */
|
|
6
|
+
write(line: string): void;
|
|
7
|
+
/** Resolves when everything written so far reached the file. */
|
|
8
|
+
flush(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live log of an execution, `<sessionDir>/tasks/<executionId>.log` (spec §9.4): bounded,
|
|
3
|
+
* created `0600`, best effort. The model can `tail` it while a long script runs. A failure to
|
|
4
|
+
* write never affects the execution.
|
|
5
|
+
*/
|
|
6
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
7
|
+
import { dirname } from "node:path";
|
|
8
|
+
export const TASK_LOG_MAX_BYTES = 256 * 1024;
|
|
9
|
+
export class TaskLog {
|
|
10
|
+
#path;
|
|
11
|
+
#max;
|
|
12
|
+
#written = 0;
|
|
13
|
+
#closed = false;
|
|
14
|
+
#chain = Promise.resolve();
|
|
15
|
+
constructor(path, max = TASK_LOG_MAX_BYTES) {
|
|
16
|
+
this.#path = path;
|
|
17
|
+
this.#max = max;
|
|
18
|
+
}
|
|
19
|
+
/** Appends one line (a newline is added). Over the bound, one marker line is written and the rest dropped. */
|
|
20
|
+
write(line) {
|
|
21
|
+
if (this.#closed)
|
|
22
|
+
return;
|
|
23
|
+
const text = line.endsWith("\n") ? line : `${line}\n`;
|
|
24
|
+
const bytes = Buffer.byteLength(text);
|
|
25
|
+
let data = text;
|
|
26
|
+
if (this.#written + bytes > this.#max) {
|
|
27
|
+
this.#closed = true;
|
|
28
|
+
data = "[log truncated: size limit reached]\n";
|
|
29
|
+
}
|
|
30
|
+
this.#written += Buffer.byteLength(data);
|
|
31
|
+
this.#chain = this.#chain
|
|
32
|
+
.then(async () => {
|
|
33
|
+
await mkdir(dirname(this.#path), { recursive: true, mode: 0o700 });
|
|
34
|
+
await appendFile(this.#path, data, { mode: 0o600 });
|
|
35
|
+
})
|
|
36
|
+
.catch(() => { });
|
|
37
|
+
}
|
|
38
|
+
/** Resolves when everything written so far reached the file. */
|
|
39
|
+
async flush() {
|
|
40
|
+
await this.#chain;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who decides how an execution ends (spec §5.2, D5). Pure: no I/O.
|
|
3
|
+
*
|
|
4
|
+
* The host's own stop (a user cancellation, or its deadline) decides the terminal state whatever
|
|
5
|
+
* the guest said afterwards; a guest that merely noticed the same thing first reports the same
|
|
6
|
+
* code; anything else keeps the guest's own outcome. A guest that says TIMEOUT or CANCELLED when
|
|
7
|
+
* the host stopped nothing is only reporting a failure: the terminal states are the host's alone.
|
|
8
|
+
*/
|
|
9
|
+
import type { ForgeErrorCode } from "./errors.ts";
|
|
10
|
+
import type { EngineOutcome } from "./ports.ts";
|
|
11
|
+
import type { ForgeStatus } from "./result.ts";
|
|
12
|
+
export interface Verdict {
|
|
13
|
+
status: ForgeStatus;
|
|
14
|
+
error?: {
|
|
15
|
+
code: ForgeErrorCode;
|
|
16
|
+
message: string;
|
|
17
|
+
};
|
|
18
|
+
value?: unknown;
|
|
19
|
+
}
|
|
20
|
+
export declare function verdictOf(outcome: EngineOutcome, stop: "cancelled" | "timeout" | undefined, timeoutMs: number): Verdict;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const timedOut = (timeoutMs) => ({
|
|
2
|
+
status: "timed_out",
|
|
3
|
+
error: { code: "TIMEOUT", message: `Execution exceeded ${timeoutMs} ms` },
|
|
4
|
+
});
|
|
5
|
+
const cancelled = () => ({
|
|
6
|
+
status: "cancelled",
|
|
7
|
+
error: { code: "CANCELLED", message: "Execution was cancelled" },
|
|
8
|
+
});
|
|
9
|
+
export function verdictOf(outcome, stop, timeoutMs) {
|
|
10
|
+
if (stop === "timeout")
|
|
11
|
+
return timedOut(timeoutMs);
|
|
12
|
+
if (stop === "cancelled")
|
|
13
|
+
return cancelled();
|
|
14
|
+
if (outcome.kind === "completed")
|
|
15
|
+
return {
|
|
16
|
+
status: "succeeded",
|
|
17
|
+
...(outcome.value !== undefined ? { value: outcome.value } : {}),
|
|
18
|
+
};
|
|
19
|
+
if (outcome.code === "TIMEOUT" || outcome.code === "CANCELLED")
|
|
20
|
+
return {
|
|
21
|
+
status: "failed",
|
|
22
|
+
error: {
|
|
23
|
+
code: "SCRIPT_RUNTIME",
|
|
24
|
+
message: `the script reported ${outcome.code} on its own: ${outcome.message}`,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
return { status: "failed", error: { code: outcome.code, message: outcome.message } };
|
|
28
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The entry the compiled `alisio` binary runs for its hidden `__forge-isolate` command (spec D-21):
|
|
3
|
+
* the body of a Forja process isolate. It is a separate export on purpose, so the CLI loads it
|
|
4
|
+
* without loading the application, the configuration or SQLite.
|
|
5
|
+
*/
|
|
6
|
+
export { runProcessIsolate } from "./forge/javascript/hosts/isolate-main.ts";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The entry the compiled `alisio` binary runs for its hidden `__forge-isolate` command (spec D-21):
|
|
3
|
+
* the body of a Forja process isolate. It is a separate export on purpose, so the CLI loads it
|
|
4
|
+
* without loading the application, the configuration or SQLite.
|
|
5
|
+
*/
|
|
6
|
+
export { runProcessIsolate } from "./forge/javascript/hosts/isolate-main.js";
|
package/dist/index.d.ts
CHANGED
|
@@ -41,7 +41,29 @@ export { ToolRegistry } from "./core/registry.ts";
|
|
|
41
41
|
export { RunClock } from "./core/run-clock.ts";
|
|
42
42
|
export { AgentRunner, type CompactionResult, defaultTokenBudget, type RunnerOptions, type RunnerSettingsPatch, type RunOptions, } from "./core/runner.ts";
|
|
43
43
|
export { CircuitBreaker, type CircuitBreakerOptions, type CircuitState, DECISION_LIMITS, DEFAULT_DECISIONS_CONFIG, type DecisionAttempt, DecisionError, type DecisionMetricEntry, DecisionMetrics, type DecisionMetricsSnapshot, type DecisionOutcome, DecisionRegistry, DecisionService, type DecisionServiceDeps, type DecisionStatus, type DecisionsConfig, formatDecisionStatsSection, formatDecisionsReport, HEALTH_TIMEOUT_MS, type RegisteredDecisionProvider, splitSupported, validateAnswers, validateRequest, } from "./decisions/index.ts";
|
|
44
|
+
export { canonicalJson, hashCanonical } from "./eval/canonical.ts";
|
|
45
|
+
export { CASSETTE_SUFFIX, type Cassette, type CassetteCall, CassetteFormatError, type CassetteHeader, type CassetteMeta, CassetteRecorder, CassetteWriteError, MAX_CASSETTE_BYTES, MAX_CASSETTE_DIR_BYTES, parseCassette, RecordingProvider, ReplayDivergenceError, ReplayExhausted, ReplayProvider, ReplayUnconsumedError, readCassette, serializeCassette, type WriteOptions, writeCassette, } from "./eval/cassette.ts";
|
|
46
|
+
export { type Conditions, compareReports, compareVariants, EvalCompareError, type EvalComparison, type Interval, type MetricDelta, parseReport, renderComparisonMarkdown, renderVariantComparisonMarkdown, type TaskComparison, type TokenComparison, type VariantComparison, type Verdict, } from "./eval/compare.ts";
|
|
47
|
+
export { CONFIG_HASH_COVERAGE, type ConfigHashExtras, checkTaskPolicy, computeConfigHash, type EvalApplication, EvalBudgetExceeded, type EvalRunMode, EvalRunRefusedError, type InvokerPermissions, type RunTaskOptions, runTask, } from "./eval/engine.ts";
|
|
48
|
+
export { buildExportBundle, type EvalExportBundle, EvalExportRefusedError, type ExportContext, } from "./eval/export.ts";
|
|
49
|
+
export { computeFingerprint, type FingerprintDiff, type FingerprintDiffEntry, } from "./eval/fingerprint.ts";
|
|
50
|
+
export { EvalPathError, resolveInside } from "./eval/paths.ts";
|
|
51
|
+
export { interleaveVariants, mulberry32, type PlannedRun, planRuns, type VariantStep, } from "./eval/plan.ts";
|
|
52
|
+
export { type BuildReportOptions, buildReport, EvalReportError, estimateRunCost, lowPowerTasks, MIN_POWER_K, renderReportMarkdown, } from "./eval/report.ts";
|
|
53
|
+
export { type RunEvaluationOptions, type RunEvaluationResult, type RunVariantsOptions, runEvaluation, runVariantEvaluations, } from "./eval/run-suite.ts";
|
|
54
|
+
export { findSecrets, redactSecrets } from "./eval/secrets.ts";
|
|
55
|
+
export { median, passAtK, passPowK, sampleVariance, wilsonInterval, } from "./eval/stats.ts";
|
|
56
|
+
export { EvalSuiteError, type LoadedSuite, loadSuite, MAX_VERIFIER_TIMEOUT_MS, } from "./eval/suite.ts";
|
|
57
|
+
export { applyConfigOverrides, EvalVariantError, loadVariantsFile, resolveVariants, type VariantsFile, } from "./eval/variants.ts";
|
|
58
|
+
export { runVerifier, runVerifiers, snapshotTree, type ToolEvent, type TreeSnapshot, type VerifierContext, type VerifierResult, type VerifierStatus, } from "./eval/verifiers.ts";
|
|
44
59
|
export { type ExtensionConflict, ExtensionRegistry } from "./extensions/registry.ts";
|
|
60
|
+
export { ForgeError } from "./forge/errors.ts";
|
|
61
|
+
export { downloadSize as pyodideDownloadSize, inspectPyodide, installPyodide, PyodideIntegrityError, pyodideDir, } from "./forge/python/pyodide-install.ts";
|
|
62
|
+
export { PYODIDE_MANIFEST, type PyodideManifest, type PyodideProfile, } from "./forge/python/pyodide-manifest.ts";
|
|
63
|
+
export { describePythonBackends } from "./forge/python/status.ts";
|
|
64
|
+
export { createPythonBackends } from "./forge/python/wiring.ts";
|
|
65
|
+
export { describeForge, type ForgeStatus, forgeEngineFor } from "./forge/runtime.ts";
|
|
66
|
+
export { emptyScratch, listScratch, openScratchFile, SCRATCH_LIST_LIMIT, type ScratchFileInfo, } from "./forge/scratch-files.ts";
|
|
45
67
|
export * from "./goal/index.ts";
|
|
46
68
|
export { HerdrBridge } from "./integrations/herdr.ts";
|
|
47
69
|
export { McpConnector, type McpServerInfo, type McpStatus, type McpToolInfo, } from "./mcp/connector.ts";
|
|
@@ -62,7 +84,7 @@ export { type ExternalDirectoryDecision, type ExternalDirectoryHandler, type Ext
|
|
|
62
84
|
export { BlobStore, type BlobStoreOptions, BlobTooLarge } from "./runtime/blobs.ts";
|
|
63
85
|
export { clipLines, unifiedPatch } from "./runtime/diff.ts";
|
|
64
86
|
export { exists, fileSize, readHead, readJson, readText, which } from "./runtime/fs.ts";
|
|
65
|
-
export { newId, ulid } from "./runtime/ids.ts";
|
|
87
|
+
export { type IdSource, newId, newUuid, seededIdSource, setIdSource, ulid, } from "./runtime/ids.ts";
|
|
66
88
|
export { defaultGlobalRoots, isPathSpec, PLUGIN_KEYWORD, resolvePluginSpec, } from "./runtime/modules.ts";
|
|
67
89
|
export { findWorkspace, inside, outsideRootsMessage, safePath, workspaceKey, } from "./runtime/paths.ts";
|
|
68
90
|
export { isMissingCommand, type ProcessResult, RIPGREP_INSTALL_HINT, runProcess, } from "./runtime/process.ts";
|
package/dist/index.js
CHANGED
|
@@ -40,7 +40,29 @@ export { ToolRegistry } from "./core/registry.js";
|
|
|
40
40
|
export { RunClock } from "./core/run-clock.js";
|
|
41
41
|
export { AgentRunner, defaultTokenBudget, } from "./core/runner.js";
|
|
42
42
|
export { CircuitBreaker, DECISION_LIMITS, DEFAULT_DECISIONS_CONFIG, DecisionError, DecisionMetrics, DecisionRegistry, DecisionService, formatDecisionStatsSection, formatDecisionsReport, HEALTH_TIMEOUT_MS, splitSupported, validateAnswers, validateRequest, } from "./decisions/index.js";
|
|
43
|
+
export { canonicalJson, hashCanonical } from "./eval/canonical.js";
|
|
44
|
+
export { CASSETTE_SUFFIX, CassetteFormatError, CassetteRecorder, CassetteWriteError, MAX_CASSETTE_BYTES, MAX_CASSETTE_DIR_BYTES, parseCassette, RecordingProvider, ReplayDivergenceError, ReplayExhausted, ReplayProvider, ReplayUnconsumedError, readCassette, serializeCassette, writeCassette, } from "./eval/cassette.js";
|
|
45
|
+
export { compareReports, compareVariants, EvalCompareError, parseReport, renderComparisonMarkdown, renderVariantComparisonMarkdown, } from "./eval/compare.js";
|
|
46
|
+
export { CONFIG_HASH_COVERAGE, checkTaskPolicy, computeConfigHash, EvalBudgetExceeded, EvalRunRefusedError, runTask, } from "./eval/engine.js";
|
|
47
|
+
export { buildExportBundle, EvalExportRefusedError, } from "./eval/export.js";
|
|
48
|
+
export { computeFingerprint, } from "./eval/fingerprint.js";
|
|
49
|
+
export { EvalPathError, resolveInside } from "./eval/paths.js";
|
|
50
|
+
export { interleaveVariants, mulberry32, planRuns, } from "./eval/plan.js";
|
|
51
|
+
export { buildReport, EvalReportError, estimateRunCost, lowPowerTasks, MIN_POWER_K, renderReportMarkdown, } from "./eval/report.js";
|
|
52
|
+
export { runEvaluation, runVariantEvaluations, } from "./eval/run-suite.js";
|
|
53
|
+
export { findSecrets, redactSecrets } from "./eval/secrets.js";
|
|
54
|
+
export { median, passAtK, passPowK, sampleVariance, wilsonInterval, } from "./eval/stats.js";
|
|
55
|
+
export { EvalSuiteError, loadSuite, MAX_VERIFIER_TIMEOUT_MS, } from "./eval/suite.js";
|
|
56
|
+
export { applyConfigOverrides, EvalVariantError, loadVariantsFile, resolveVariants, } from "./eval/variants.js";
|
|
57
|
+
export { runVerifier, runVerifiers, snapshotTree, } from "./eval/verifiers.js";
|
|
43
58
|
export { ExtensionRegistry } from "./extensions/registry.js";
|
|
59
|
+
export { ForgeError } from "./forge/errors.js";
|
|
60
|
+
export { downloadSize as pyodideDownloadSize, inspectPyodide, installPyodide, PyodideIntegrityError, pyodideDir, } from "./forge/python/pyodide-install.js";
|
|
61
|
+
export { PYODIDE_MANIFEST, } from "./forge/python/pyodide-manifest.js";
|
|
62
|
+
export { describePythonBackends } from "./forge/python/status.js";
|
|
63
|
+
export { createPythonBackends } from "./forge/python/wiring.js";
|
|
64
|
+
export { describeForge, forgeEngineFor } from "./forge/runtime.js";
|
|
65
|
+
export { emptyScratch, listScratch, openScratchFile, SCRATCH_LIST_LIMIT, } from "./forge/scratch-files.js";
|
|
44
66
|
export * from "./goal/index.js";
|
|
45
67
|
export { HerdrBridge } from "./integrations/herdr.js";
|
|
46
68
|
export { McpConnector, } from "./mcp/connector.js";
|
|
@@ -61,7 +83,7 @@ export { PathAccess, } from "./runtime/access.js";
|
|
|
61
83
|
export { BlobStore, BlobTooLarge } from "./runtime/blobs.js";
|
|
62
84
|
export { clipLines, unifiedPatch } from "./runtime/diff.js";
|
|
63
85
|
export { exists, fileSize, readHead, readJson, readText, which } from "./runtime/fs.js";
|
|
64
|
-
export { newId, ulid } from "./runtime/ids.js";
|
|
86
|
+
export { newId, newUuid, seededIdSource, setIdSource, ulid, } from "./runtime/ids.js";
|
|
65
87
|
export { defaultGlobalRoots, isPathSpec, PLUGIN_KEYWORD, resolvePluginSpec, } from "./runtime/modules.js";
|
|
66
88
|
export { findWorkspace, inside, outsideRootsMessage, safePath, workspaceKey, } from "./runtime/paths.js";
|
|
67
89
|
export { isMissingCommand, RIPGREP_INSTALL_HINT, runProcess, } from "./runtime/process.js";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The effect gate of a tool call as a pure decision (spec D-25). The runner, `availableTools`, the
|
|
3
|
+
* legacy `execute` tool and the Forja nested-call dispatcher all ask this one function, so a call
|
|
4
|
+
* made from inside a script gets exactly the decision a direct call would get; there is no second
|
|
5
|
+
* permission system.
|
|
6
|
+
*
|
|
7
|
+
* Rules, kept from the runner they were extracted from:
|
|
8
|
+
* - `read` and `internal` always run; `write`, `process` and `external` run when the policy says so.
|
|
9
|
+
* - A tool with no declared effect is `external`.
|
|
10
|
+
* - A capability tool (`analysis.run`) also runs on the `--allow-analysis` flag alone, and on a
|
|
11
|
+
* stored grant; the grant never widens the effect.
|
|
12
|
+
* - Otherwise the call asks, but only for `write`, `process` and `external`, and only when a
|
|
13
|
+
* person can be asked. Anything else is denied.
|
|
14
|
+
*
|
|
15
|
+
* The policy is read by reference on purpose: an "allow for the session" answer widens the very
|
|
16
|
+
* same object, and the next decision must see it.
|
|
17
|
+
*/
|
|
18
|
+
import type { ToolDefinition } from "@alisio/sdk";
|
|
19
|
+
import type { Policy } from "../core/contracts.ts";
|
|
20
|
+
export type EffectDecision = {
|
|
21
|
+
kind: "allow";
|
|
22
|
+
via: "effect" | "flag" | "grant";
|
|
23
|
+
} | {
|
|
24
|
+
kind: "ask";
|
|
25
|
+
} | {
|
|
26
|
+
kind: "deny";
|
|
27
|
+
};
|
|
28
|
+
export interface DecideContext {
|
|
29
|
+
/** Whether an interactive approval could be opened for this call. */
|
|
30
|
+
approvals: boolean;
|
|
31
|
+
/** Whether the tool's capability holds a stored grant for the session. */
|
|
32
|
+
capabilityGranted: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** The tool's effect; a tool that declares none is treated as `external`. */
|
|
35
|
+
export declare const effectOf: (tool: Pick<ToolDefinition, "effect">) => string;
|
|
36
|
+
/** Whether the policy lets an effect run without asking. */
|
|
37
|
+
export declare const effectAllowed: (policy: Policy, effect: string) => boolean;
|
|
38
|
+
/** Whether an effect can ever be put to a person (`read` and `internal` never are). */
|
|
39
|
+
export declare const effectAskable: (effect: string) => effect is "write" | "process" | "external";
|
|
40
|
+
/** A tool's capability is pre-allowed by the policy alone (`--allow-analysis`). */
|
|
41
|
+
export declare const capabilityFlagAllowed: (policy: Policy, tool: Pick<ToolDefinition, "capability">) => boolean;
|
|
42
|
+
export declare function decideEffect(policy: Policy, tool: Pick<ToolDefinition, "effect" | "capability">, context: DecideContext): EffectDecision;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** The tool's effect; a tool that declares none is treated as `external`. */
|
|
2
|
+
export const effectOf = (tool) => tool.effect ?? "external";
|
|
3
|
+
/** Whether the policy lets an effect run without asking. */
|
|
4
|
+
export const effectAllowed = (policy, effect) => effect === "read" ||
|
|
5
|
+
effect === "internal" ||
|
|
6
|
+
((effect === "write" || effect === "process" || effect === "external") && !!policy[effect]);
|
|
7
|
+
/** Whether an effect can ever be put to a person (`read` and `internal` never are). */
|
|
8
|
+
export const effectAskable = (effect) => effect === "write" || effect === "process" || effect === "external";
|
|
9
|
+
/** A tool's capability is pre-allowed by the policy alone (`--allow-analysis`). */
|
|
10
|
+
export const capabilityFlagAllowed = (policy, tool) => tool.capability === "analysis.run" && !!policy.analysis;
|
|
11
|
+
export function decideEffect(policy, tool, context) {
|
|
12
|
+
const effect = effectOf(tool);
|
|
13
|
+
const byEffect = effectAllowed(policy, effect);
|
|
14
|
+
if (tool.capability) {
|
|
15
|
+
// A broad process grant or --allow-analysis: allowed, audited as a flag decision.
|
|
16
|
+
if (byEffect || capabilityFlagAllowed(policy, tool))
|
|
17
|
+
return { kind: "allow", via: "flag" };
|
|
18
|
+
if (context.capabilityGranted)
|
|
19
|
+
return { kind: "allow", via: "grant" };
|
|
20
|
+
}
|
|
21
|
+
else if (byEffect)
|
|
22
|
+
return { kind: "allow", via: "effect" };
|
|
23
|
+
return context.approvals && effectAskable(effect) ? { kind: "ask" } : { kind: "deny" };
|
|
24
|
+
}
|
package/dist/plan/exit-plan.js
CHANGED
|
@@ -15,6 +15,7 @@ import { tmpdir } from "node:os";
|
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { textResult } from "@alisio/sdk";
|
|
17
17
|
import { EXIT_PLAN_TOOL } from "../core/opt-in.js";
|
|
18
|
+
import { newUuid } from "../runtime/ids.js";
|
|
18
19
|
import { objectSchema } from "../tools/standard.js";
|
|
19
20
|
import { DIAGRAM_MAX_BYTES, DIAGRAM_MAX_NODES, validateDiagrams, } from "./diagrams.js";
|
|
20
21
|
import { buildPlanManifest } from "./manifest.js";
|
|
@@ -118,7 +119,7 @@ export function registerExitPlan(registry, deps) {
|
|
|
118
119
|
throw new Error("exit_plan needs the whole plan in the `plan` field");
|
|
119
120
|
const sessionId = context.session;
|
|
120
121
|
const title = planTitle(plan, input.title);
|
|
121
|
-
const planId =
|
|
122
|
+
const planId = newUuid();
|
|
122
123
|
const reviewable = !!sessionId && deps.ui.interactive() && !deps.readOnly;
|
|
123
124
|
const revision = sessionId ? nextPlanRevision(deps.store, sessionId) : 1;
|
|
124
125
|
const previous = sessionId
|
|
@@ -16,7 +16,8 @@ export interface ProjectContextOptions {
|
|
|
16
16
|
}
|
|
17
17
|
export declare class ProjectContext implements ContextSource {
|
|
18
18
|
readonly workspace: string;
|
|
19
|
-
|
|
19
|
+
/** Extra system-prompt sections; `sessionId` lets one depend on the session (the scratchpad path). */
|
|
20
|
+
extras: Array<(sessionId?: string) => Promise<string>>;
|
|
20
21
|
readonly cwd: string;
|
|
21
22
|
private options;
|
|
22
23
|
/** Per session: nested instruction file path → fingerprint already attached. */
|
|
@@ -18,6 +18,7 @@ const PREAMBLE = "You are Alisio, a coding assistant. Follow the user's task. To
|
|
|
18
18
|
const HEADER = "# Project instructions (AGENTS.md)\nFiles are listed from the global and repository root down to the current directory; when they conflict, the closest file wins. Explicit user prompts override these instructions. Instructions for nested directories are attached when you touch paths below them.";
|
|
19
19
|
export class ProjectContext {
|
|
20
20
|
workspace;
|
|
21
|
+
/** Extra system-prompt sections; `sessionId` lets one depend on the session (the scratchpad path). */
|
|
21
22
|
extras = [];
|
|
22
23
|
cwd;
|
|
23
24
|
options;
|
|
@@ -143,7 +144,7 @@ export class ProjectContext {
|
|
|
143
144
|
if (files.length)
|
|
144
145
|
chunks.push(HEADER, ...this.capped(files));
|
|
145
146
|
for (const extra of this.extras)
|
|
146
|
-
chunks.push(await extra());
|
|
147
|
+
chunks.push(await extra(sessionId));
|
|
147
148
|
return chunks.join("\n\n");
|
|
148
149
|
}
|
|
149
150
|
/** Keeps the closest files within the byte budget; drops or truncates the farthest ones. */
|
package/dist/runtime/access.d.ts
CHANGED
|
@@ -23,6 +23,15 @@ export interface PathAccessOptions {
|
|
|
23
23
|
approve?: ExternalDirectoryHandler;
|
|
24
24
|
/** Fully locked: declared roots are ignored and no external directory is ever approvable. */
|
|
25
25
|
readOnly?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* The verified scratchpad of a session's ROOT session (Forja, spec §9.4), created on first use.
|
|
28
|
+
* When given, that folder is an authorized root: a path inside it needs no directory approval.
|
|
29
|
+
* Absent when Forja is off, so nothing changes; ignored under `--read-only`. A failure to
|
|
30
|
+
* provide it just means "no scratchpad".
|
|
31
|
+
*/
|
|
32
|
+
scratchpad?: (session: string) => Promise<string | undefined>;
|
|
33
|
+
/** `forge.scratch.maxBytes`, read on every check: the quota of one scratchpad. */
|
|
34
|
+
scratchMaxBytes?: () => number;
|
|
26
35
|
}
|
|
27
36
|
export interface ResolvePathOptions {
|
|
28
37
|
/** Workspace to resolve a relative path against (child sessions may use a worktree). */
|
|
@@ -41,11 +50,36 @@ export declare class PathAccess {
|
|
|
41
50
|
private readonly roots;
|
|
42
51
|
private readonly approve?;
|
|
43
52
|
private readonly readOnly;
|
|
53
|
+
private readonly scratchpad?;
|
|
54
|
+
private readonly scratchMaxBytes?;
|
|
44
55
|
/** Session-approved directory subtrees (allow for the session). */
|
|
45
56
|
private readonly approved;
|
|
46
57
|
/** Canonicalized roots, memoized; a nonexistent declared root never matches. */
|
|
47
58
|
private readonly canonical;
|
|
48
59
|
constructor(options: PathAccessOptions);
|
|
60
|
+
/**
|
|
61
|
+
* `target` made safe inside the session's scratchpad, or undefined when it is not there: outside
|
|
62
|
+
* it, no session, no scratchpad, or a symlink on the way (an escaping link is "outside").
|
|
63
|
+
*/
|
|
64
|
+
private scratchTarget;
|
|
65
|
+
/** Whether an already resolved path lies in the session's verified scratchpad (never read-only). */
|
|
66
|
+
isScratchPath(target: string, session?: string): Promise<boolean>;
|
|
67
|
+
/** Scratchpad writers in flight, one queue per scratchpad directory (see `lockScratch`). */
|
|
68
|
+
private scratchLocks;
|
|
69
|
+
/**
|
|
70
|
+
* Serializes the quota check and the write that follows it (L1): two parallel writes that each
|
|
71
|
+
* fit would otherwise both pass the check and together overflow. Resolves to the release. Keyed
|
|
72
|
+
* by the scratchpad directory, so a child session shares the lock of its root session.
|
|
73
|
+
*/
|
|
74
|
+
lockScratch(session: string | undefined): Promise<() => void>;
|
|
75
|
+
/**
|
|
76
|
+
* Why a write of about `incomingBytes` into the session's scratchpad must be refused (spec §9.4,
|
|
77
|
+
* FJ-AC-34), or undefined when it fits. Nothing is ever deleted to make room: the model is told,
|
|
78
|
+
* plainly, that the scratchpad is full.
|
|
79
|
+
*/
|
|
80
|
+
scratchQuotaProblem(session: string | undefined, incomingBytes: number,
|
|
81
|
+
/** Files the write replaces: their current size is not counted twice. */
|
|
82
|
+
replacing?: readonly string[]): Promise<string | undefined>;
|
|
49
83
|
/** Extra roots actually in force (always empty under `--read-only`). */
|
|
50
84
|
get extraRoots(): readonly string[];
|
|
51
85
|
/** Directory subtrees approved for the session (diagnostics and tests). */
|
package/dist/runtime/access.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { lstat, realpath } from "node:fs/promises";
|
|
15
15
|
import { dirname, resolve } from "node:path";
|
|
16
|
+
import { scratchUsage } from "../forge/session-dir.js";
|
|
16
17
|
import { inside, outsideRootsMessage, safePath } from "./paths.js";
|
|
17
18
|
/** Nearest existing ancestor directory of `target`, canonicalized; used as the approval boundary. */
|
|
18
19
|
async function nearestExistingDirectory(target) {
|
|
@@ -38,6 +39,8 @@ export class PathAccess {
|
|
|
38
39
|
roots;
|
|
39
40
|
approve;
|
|
40
41
|
readOnly;
|
|
42
|
+
scratchpad;
|
|
43
|
+
scratchMaxBytes;
|
|
41
44
|
/** Session-approved directory subtrees (allow for the session). */
|
|
42
45
|
approved = new Set();
|
|
43
46
|
/** Canonicalized roots, memoized; a nonexistent declared root never matches. */
|
|
@@ -48,6 +51,80 @@ export class PathAccess {
|
|
|
48
51
|
this.readOnly = !!options.readOnly;
|
|
49
52
|
if (options.approve && !options.readOnly)
|
|
50
53
|
this.approve = options.approve;
|
|
54
|
+
if (options.scratchpad && !options.readOnly)
|
|
55
|
+
this.scratchpad = options.scratchpad;
|
|
56
|
+
if (options.scratchMaxBytes)
|
|
57
|
+
this.scratchMaxBytes = options.scratchMaxBytes;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `target` made safe inside the session's scratchpad, or undefined when it is not there: outside
|
|
61
|
+
* it, no session, no scratchpad, or a symlink on the way (an escaping link is "outside").
|
|
62
|
+
*/
|
|
63
|
+
async scratchTarget(target, session) {
|
|
64
|
+
if (!this.scratchpad || !session)
|
|
65
|
+
return undefined;
|
|
66
|
+
try {
|
|
67
|
+
const base = await this.scratchpad(session);
|
|
68
|
+
if (!base || !inside(base, target))
|
|
69
|
+
return undefined;
|
|
70
|
+
return await safePath(base, target);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Whether an already resolved path lies in the session's verified scratchpad (never read-only). */
|
|
77
|
+
async isScratchPath(target, session) {
|
|
78
|
+
return (await this.scratchTarget(target, session)) !== undefined;
|
|
79
|
+
}
|
|
80
|
+
/** Scratchpad writers in flight, one queue per scratchpad directory (see `lockScratch`). */
|
|
81
|
+
scratchLocks = new Map();
|
|
82
|
+
/**
|
|
83
|
+
* Serializes the quota check and the write that follows it (L1): two parallel writes that each
|
|
84
|
+
* fit would otherwise both pass the check and together overflow. Resolves to the release. Keyed
|
|
85
|
+
* by the scratchpad directory, so a child session shares the lock of its root session.
|
|
86
|
+
*/
|
|
87
|
+
async lockScratch(session) {
|
|
88
|
+
const base = this.scratchpad && session
|
|
89
|
+
? await this.scratchpad(session).catch(() => undefined)
|
|
90
|
+
: undefined;
|
|
91
|
+
const key = base ?? "";
|
|
92
|
+
const previous = this.scratchLocks.get(key) ?? Promise.resolve();
|
|
93
|
+
let release = () => { };
|
|
94
|
+
const mine = new Promise((resolve) => {
|
|
95
|
+
release = resolve;
|
|
96
|
+
});
|
|
97
|
+
const tail = previous.then(() => mine);
|
|
98
|
+
this.scratchLocks.set(key, tail);
|
|
99
|
+
await previous;
|
|
100
|
+
return () => {
|
|
101
|
+
release();
|
|
102
|
+
if (this.scratchLocks.get(key) === tail)
|
|
103
|
+
this.scratchLocks.delete(key);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Why a write of about `incomingBytes` into the session's scratchpad must be refused (spec §9.4,
|
|
108
|
+
* FJ-AC-34), or undefined when it fits. Nothing is ever deleted to make room: the model is told,
|
|
109
|
+
* plainly, that the scratchpad is full.
|
|
110
|
+
*/
|
|
111
|
+
async scratchQuotaProblem(session, incomingBytes,
|
|
112
|
+
/** Files the write replaces: their current size is not counted twice. */
|
|
113
|
+
replacing = []) {
|
|
114
|
+
if (!this.scratchpad || !this.scratchMaxBytes || !session)
|
|
115
|
+
return undefined;
|
|
116
|
+
const base = await this.scratchpad(session).catch(() => undefined);
|
|
117
|
+
if (!base)
|
|
118
|
+
return undefined;
|
|
119
|
+
const max = this.scratchMaxBytes();
|
|
120
|
+
let used = await scratchUsage(base);
|
|
121
|
+
for (const path of replacing)
|
|
122
|
+
used -= (await lstat(path).catch(() => undefined))?.size ?? 0;
|
|
123
|
+
used = Math.max(0, used);
|
|
124
|
+
if (used + incomingBytes <= max)
|
|
125
|
+
return undefined;
|
|
126
|
+
return (`The session scratchpad is full: ${used} of ${max} bytes used and this write needs about ` +
|
|
127
|
+
`${incomingBytes}. Nothing was deleted; remove files you no longer need, or ask the user to clear it.`);
|
|
51
128
|
}
|
|
52
129
|
/** Extra roots actually in force (always empty under `--read-only`). */
|
|
53
130
|
get extraRoots() {
|
|
@@ -88,6 +165,9 @@ export class PathAccess {
|
|
|
88
165
|
const root = await this.rootFor(target, workspace);
|
|
89
166
|
if (root)
|
|
90
167
|
return safePath(root, target);
|
|
168
|
+
const scratch = await this.scratchTarget(target, options.session);
|
|
169
|
+
if (scratch)
|
|
170
|
+
return scratch;
|
|
91
171
|
if (this.readOnly)
|
|
92
172
|
throw new Error(outsideRootsMessage(target, "--read-only disables access outside the workspace; run without it to allow it"));
|
|
93
173
|
const boundary = await nearestExistingDirectory(target);
|
package/dist/runtime/ids.d.ts
CHANGED
|
@@ -6,3 +6,24 @@
|
|
|
6
6
|
export declare function ulid(now?: number): string;
|
|
7
7
|
/** A prefixed id such as `art_01J…` or `exec_01J…`. */
|
|
8
8
|
export declare const newId: (prefix: string, now?: number) => string;
|
|
9
|
+
/**
|
|
10
|
+
* A replacement for the random identifiers the runtime generates (session ids, plan ids, forge
|
|
11
|
+
* execution ids, `ulid`-based ids). The reliability lab installs a seeded one while it records or
|
|
12
|
+
* replays a cassette so the requests a run sends do not depend on fresh randomness. Nothing
|
|
13
|
+
* installs one in normal use, and then every function here behaves exactly as before.
|
|
14
|
+
*/
|
|
15
|
+
export interface IdSource {
|
|
16
|
+
/** A UUID (the shape of `crypto.randomUUID()`). */
|
|
17
|
+
uuid(): string;
|
|
18
|
+
/** The 26-character body of a `ulid` (the argument is the caller's clock, which a source may ignore). */
|
|
19
|
+
ulid(now?: number): string;
|
|
20
|
+
}
|
|
21
|
+
/** Installs (or, with `undefined`, removes) the process-wide id source. */
|
|
22
|
+
export declare function setIdSource(source?: IdSource): void;
|
|
23
|
+
/** A random v4 UUID, or the next one of the installed id source. */
|
|
24
|
+
export declare const newUuid: () => string;
|
|
25
|
+
/**
|
|
26
|
+
* A deterministic source: the n-th UUID (and the n-th ULID) depends only on `seed`. UUIDs are
|
|
27
|
+
* v4-shaped; ULIDs carry a counter in their time part, so they stay sortable by creation order.
|
|
28
|
+
*/
|
|
29
|
+
export declare function seededIdSource(seed: string): IdSource;
|
package/dist/runtime/ids.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
/** Crockford base32 (no I, L, O, U), the ULID alphabet. */
|
|
3
3
|
const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
4
4
|
/**
|
|
@@ -7,6 +7,8 @@ const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
|
7
7
|
* millisecond sort arbitrarily, which is fine for row ids and folder names.
|
|
8
8
|
*/
|
|
9
9
|
export function ulid(now = Date.now()) {
|
|
10
|
+
if (installed)
|
|
11
|
+
return installed.ulid(now);
|
|
10
12
|
let time = "";
|
|
11
13
|
let t = Math.max(0, Math.floor(now));
|
|
12
14
|
for (let i = 0; i < 10; i++) {
|
|
@@ -30,3 +32,41 @@ export function ulid(now = Date.now()) {
|
|
|
30
32
|
}
|
|
31
33
|
/** A prefixed id such as `art_01J…` or `exec_01J…`. */
|
|
32
34
|
export const newId = (prefix, now) => `${prefix}_${ulid(now)}`;
|
|
35
|
+
let installed;
|
|
36
|
+
/** Installs (or, with `undefined`, removes) the process-wide id source. */
|
|
37
|
+
export function setIdSource(source) {
|
|
38
|
+
installed = source;
|
|
39
|
+
}
|
|
40
|
+
/** A random v4 UUID, or the next one of the installed id source. */
|
|
41
|
+
export const newUuid = () => (installed ? installed.uuid() : randomUUID());
|
|
42
|
+
/**
|
|
43
|
+
* A deterministic source: the n-th UUID (and the n-th ULID) depends only on `seed`. UUIDs are
|
|
44
|
+
* v4-shaped; ULIDs carry a counter in their time part, so they stay sortable by creation order.
|
|
45
|
+
*/
|
|
46
|
+
export function seededIdSource(seed) {
|
|
47
|
+
let uuids = 0;
|
|
48
|
+
let ulids = 0;
|
|
49
|
+
const digest = (kind, n) => createHash("sha256").update(`${seed}\0${kind}\0${n}`).digest();
|
|
50
|
+
return {
|
|
51
|
+
uuid() {
|
|
52
|
+
const bytes = digest("uuid", uuids++).subarray(0, 16);
|
|
53
|
+
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40;
|
|
54
|
+
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
|
|
55
|
+
const hex = bytes.toString("hex");
|
|
56
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
57
|
+
},
|
|
58
|
+
ulid() {
|
|
59
|
+
let time = "";
|
|
60
|
+
let t = ulids;
|
|
61
|
+
for (let i = 0; i < 10; i++) {
|
|
62
|
+
time = (ALPHABET[t % 32] ?? "0") + time;
|
|
63
|
+
t = Math.floor(t / 32);
|
|
64
|
+
}
|
|
65
|
+
const bytes = digest("ulid", ulids++);
|
|
66
|
+
let random = "";
|
|
67
|
+
for (let i = 0; i < 16; i++)
|
|
68
|
+
random += ALPHABET[(bytes[i] ?? 0) & 31];
|
|
69
|
+
return time + random;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -27,7 +27,8 @@ export declare function runProcess(command: string, args: string[], options: {
|
|
|
27
27
|
timeoutMs?: number;
|
|
28
28
|
maxBytes?: number;
|
|
29
29
|
env?: Record<string, string>;
|
|
30
|
-
|
|
30
|
+
/** Live text; the second argument says which stream it came from. */
|
|
31
|
+
onData?: (chunk: string, stream: "stdout" | "stderr") => void;
|
|
31
32
|
/** On cancellation: SIGTERM the process group, then SIGKILL after this grace (default 5s). */
|
|
32
33
|
killGraceMs?: number;
|
|
33
34
|
/**
|