@alisio/core 0.5.2 → 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 +28 -2
- package/dist/application.js +189 -4
- package/dist/changelog/data.js +26 -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,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
|
@@ -42,6 +42,13 @@ 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
44
|
export { type ExtensionConflict, ExtensionRegistry } from "./extensions/registry.ts";
|
|
45
|
+
export { ForgeError } from "./forge/errors.ts";
|
|
46
|
+
export { downloadSize as pyodideDownloadSize, inspectPyodide, installPyodide, PyodideIntegrityError, pyodideDir, } from "./forge/python/pyodide-install.ts";
|
|
47
|
+
export { PYODIDE_MANIFEST, type PyodideManifest, type PyodideProfile, } from "./forge/python/pyodide-manifest.ts";
|
|
48
|
+
export { describePythonBackends } from "./forge/python/status.ts";
|
|
49
|
+
export { createPythonBackends } from "./forge/python/wiring.ts";
|
|
50
|
+
export { describeForge, type ForgeStatus, forgeEngineFor } from "./forge/runtime.ts";
|
|
51
|
+
export { emptyScratch, listScratch, openScratchFile, SCRATCH_LIST_LIMIT, type ScratchFileInfo, } from "./forge/scratch-files.ts";
|
|
45
52
|
export * from "./goal/index.ts";
|
|
46
53
|
export { HerdrBridge } from "./integrations/herdr.ts";
|
|
47
54
|
export { McpConnector, type McpServerInfo, type McpStatus, type McpToolInfo, } from "./mcp/connector.ts";
|
package/dist/index.js
CHANGED
|
@@ -41,6 +41,13 @@ 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
43
|
export { ExtensionRegistry } from "./extensions/registry.js";
|
|
44
|
+
export { ForgeError } from "./forge/errors.js";
|
|
45
|
+
export { downloadSize as pyodideDownloadSize, inspectPyodide, installPyodide, PyodideIntegrityError, pyodideDir, } from "./forge/python/pyodide-install.js";
|
|
46
|
+
export { PYODIDE_MANIFEST, } from "./forge/python/pyodide-manifest.js";
|
|
47
|
+
export { describePythonBackends } from "./forge/python/status.js";
|
|
48
|
+
export { createPythonBackends } from "./forge/python/wiring.js";
|
|
49
|
+
export { describeForge, forgeEngineFor } from "./forge/runtime.js";
|
|
50
|
+
export { emptyScratch, listScratch, openScratchFile, SCRATCH_LIST_LIMIT, } from "./forge/scratch-files.js";
|
|
44
51
|
export * from "./goal/index.js";
|
|
45
52
|
export { HerdrBridge } from "./integrations/herdr.js";
|
|
46
53
|
export { McpConnector, } from "./mcp/connector.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
|
+
}
|
|
@@ -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);
|
|
@@ -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
|
/**
|
package/dist/runtime/process.js
CHANGED
|
@@ -129,7 +129,7 @@ export async function runProcess(command, args, options) {
|
|
|
129
129
|
written += chunk.byteLength;
|
|
130
130
|
};
|
|
131
131
|
};
|
|
132
|
-
const consume = (stream, logPath) => new Promise((resolveText, reject) => {
|
|
132
|
+
const consume = (stream, name, logPath) => new Promise((resolveText, reject) => {
|
|
133
133
|
const decoder = new TextDecoder();
|
|
134
134
|
const log = logger(logPath);
|
|
135
135
|
let text = "", done = false;
|
|
@@ -155,7 +155,7 @@ export async function runProcess(command, args, options) {
|
|
|
155
155
|
if (tailMode) {
|
|
156
156
|
const live = decoder.decode(value, { stream: true });
|
|
157
157
|
if (live)
|
|
158
|
-
options.onData?.(live);
|
|
158
|
+
options.onData?.(live, name);
|
|
159
159
|
tail.push(value);
|
|
160
160
|
tailBytes += value.byteLength;
|
|
161
161
|
while (tailBytes > max && tail.length) {
|
|
@@ -178,7 +178,7 @@ export async function runProcess(command, args, options) {
|
|
|
178
178
|
const chunk = decoder.decode(slice, { stream: true });
|
|
179
179
|
text += chunk;
|
|
180
180
|
if (chunk)
|
|
181
|
-
options.onData?.(chunk);
|
|
181
|
+
options.onData?.(chunk, name);
|
|
182
182
|
if (value.byteLength > remaining) {
|
|
183
183
|
truncated = true;
|
|
184
184
|
kill();
|
|
@@ -191,8 +191,8 @@ export async function runProcess(command, args, options) {
|
|
|
191
191
|
});
|
|
192
192
|
try {
|
|
193
193
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
194
|
-
consume(child.stdout, options.logFiles?.stdout),
|
|
195
|
-
consume(child.stderr, options.logFiles?.stderr),
|
|
194
|
+
consume(child.stdout, "stdout", options.logFiles?.stdout),
|
|
195
|
+
consume(child.stderr, "stderr", options.logFiles?.stderr),
|
|
196
196
|
exited,
|
|
197
197
|
]);
|
|
198
198
|
signal.throwIfAborted();
|
package/dist/runtime/store.d.ts
CHANGED
|
@@ -46,6 +46,17 @@ export declare class SQLiteStore implements SessionStore {
|
|
|
46
46
|
runId?: string;
|
|
47
47
|
startedAt?: number;
|
|
48
48
|
}>;
|
|
49
|
+
/**
|
|
50
|
+
* Files the nested `write` calls of Forja executions of a session touched (D-31): one entry per
|
|
51
|
+
* journaled call that did not fail, with the tool call (`js_run`/`python_run`) that ran it so the
|
|
52
|
+
* panel can attach the run. `paths` are the absolute paths the gate resolved. Oldest first.
|
|
53
|
+
*/
|
|
54
|
+
forgeWriteCalls(session: string): Array<{
|
|
55
|
+
toolCallId: string;
|
|
56
|
+
paths: string[];
|
|
57
|
+
runId?: string;
|
|
58
|
+
startedAt?: number;
|
|
59
|
+
}>;
|
|
49
60
|
/** Rich (ui/image) parts persist as part of the serialized result and replay verbatim. */
|
|
50
61
|
callResult(id: string, callId: string): ToolResult | undefined;
|
|
51
62
|
reconcile(id: string, acknowledge?: boolean): void;
|
package/dist/runtime/store.js
CHANGED
|
@@ -231,6 +231,86 @@ export class SQLiteStore {
|
|
|
231
231
|
add("owner_pid", "INTEGER");
|
|
232
232
|
this.db.exec("INSERT OR IGNORE INTO schema_migrations VALUES(8)");
|
|
233
233
|
});
|
|
234
|
+
if (!has(9))
|
|
235
|
+
this.db.transaction(() => {
|
|
236
|
+
// v9 (additive): Forja journal (`forge_executions`, `forge_calls`) and the small JSON state
|
|
237
|
+
// shared across executions (`forge_state`). Only hashes of inputs are stored, never the
|
|
238
|
+
// inputs or results themselves. `owner_pid` lets a restart tell its own dead executions
|
|
239
|
+
// from another live process's.
|
|
240
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS forge_executions(
|
|
241
|
+
id TEXT PRIMARY KEY,
|
|
242
|
+
root_session_id TEXT NOT NULL,
|
|
243
|
+
session_id TEXT NOT NULL,
|
|
244
|
+
tool_call_id TEXT NOT NULL,
|
|
245
|
+
engine TEXT NOT NULL,
|
|
246
|
+
isolation TEXT NOT NULL,
|
|
247
|
+
selection_reason TEXT NOT NULL,
|
|
248
|
+
status TEXT NOT NULL,
|
|
249
|
+
code_sha256 TEXT NOT NULL,
|
|
250
|
+
started_at INTEGER NOT NULL, ended_at INTEGER,
|
|
251
|
+
metrics_json TEXT,
|
|
252
|
+
error_code TEXT,
|
|
253
|
+
termination_unconfirmed INTEGER NOT NULL DEFAULT 0,
|
|
254
|
+
analysis_execution_id TEXT REFERENCES analysis_executions(id),
|
|
255
|
+
owner_pid INTEGER);
|
|
256
|
+
CREATE INDEX IF NOT EXISTS forge_executions_root
|
|
257
|
+
ON forge_executions(root_session_id, started_at);
|
|
258
|
+
CREATE TABLE IF NOT EXISTS forge_calls(
|
|
259
|
+
execution_id TEXT NOT NULL REFERENCES forge_executions(id),
|
|
260
|
+
call_id TEXT NOT NULL,
|
|
261
|
+
tool_name TEXT NOT NULL,
|
|
262
|
+
effect TEXT NOT NULL,
|
|
263
|
+
status TEXT NOT NULL,
|
|
264
|
+
duration_ms INTEGER,
|
|
265
|
+
input_sha256 TEXT NOT NULL,
|
|
266
|
+
error_code TEXT,
|
|
267
|
+
paths_json TEXT,
|
|
268
|
+
PRIMARY KEY(execution_id, call_id));
|
|
269
|
+
CREATE INDEX IF NOT EXISTS forge_calls_execution
|
|
270
|
+
ON forge_calls(execution_id, call_id);
|
|
271
|
+
CREATE TABLE IF NOT EXISTS forge_state(
|
|
272
|
+
root_session_id TEXT NOT NULL,
|
|
273
|
+
key TEXT NOT NULL,
|
|
274
|
+
value_json TEXT NOT NULL,
|
|
275
|
+
revision INTEGER NOT NULL,
|
|
276
|
+
updated_at INTEGER NOT NULL,
|
|
277
|
+
PRIMARY KEY(root_session_id, key));
|
|
278
|
+
INSERT OR IGNORE INTO schema_migrations VALUES(9);`);
|
|
279
|
+
});
|
|
280
|
+
if (!has(10)) {
|
|
281
|
+
// v10: `analysis_executions.runtime` admits `wasm` (the Pyodide backend). SQLite cannot alter
|
|
282
|
+
// a CHECK, so the table is rebuilt with the official recipe: foreign keys off, copy, drop,
|
|
283
|
+
// rename. Rows, ids and the artifacts that point at them are untouched.
|
|
284
|
+
this.db.exec("PRAGMA foreign_keys=OFF");
|
|
285
|
+
try {
|
|
286
|
+
this.db.transaction(() => {
|
|
287
|
+
this.db.exec(`CREATE TABLE analysis_executions_v10(
|
|
288
|
+
id TEXT PRIMARY KEY,
|
|
289
|
+
session TEXT NOT NULL,
|
|
290
|
+
root_session TEXT NOT NULL,
|
|
291
|
+
workspace TEXT NOT NULL,
|
|
292
|
+
run_id TEXT, call_id TEXT,
|
|
293
|
+
runtime TEXT NOT NULL CHECK(runtime IN ('managed','oci','wasm')),
|
|
294
|
+
status TEXT NOT NULL CHECK(status IN ('running','completed','failed','cancelled','timed_out')),
|
|
295
|
+
script_sha256 TEXT NOT NULL,
|
|
296
|
+
exit_code INTEGER, error TEXT,
|
|
297
|
+
rel_dir TEXT NOT NULL,
|
|
298
|
+
rerun_of TEXT,
|
|
299
|
+
created_at INTEGER NOT NULL, ended_at INTEGER);
|
|
300
|
+
INSERT INTO analysis_executions_v10 SELECT id,session,root_session,workspace,run_id,call_id,
|
|
301
|
+
runtime,status,script_sha256,exit_code,error,rel_dir,rerun_of,created_at,ended_at
|
|
302
|
+
FROM analysis_executions;
|
|
303
|
+
DROP TABLE analysis_executions;
|
|
304
|
+
ALTER TABLE analysis_executions_v10 RENAME TO analysis_executions;
|
|
305
|
+
CREATE INDEX IF NOT EXISTS analysis_executions_session
|
|
306
|
+
ON analysis_executions(root_session, created_at);
|
|
307
|
+
INSERT OR IGNORE INTO schema_migrations VALUES(10);`);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
finally {
|
|
311
|
+
this.db.exec("PRAGMA foreign_keys=ON");
|
|
312
|
+
}
|
|
313
|
+
}
|
|
234
314
|
}
|
|
235
315
|
/**
|
|
236
316
|
* The root of a session (itself for roots), following `parent_id`. Unknown ids are their own
|
|
@@ -433,6 +513,29 @@ export class SQLiteStore {
|
|
|
433
513
|
...(row.started_at != null ? { startedAt: Number(row.started_at) } : {}),
|
|
434
514
|
}));
|
|
435
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* Files the nested `write` calls of Forja executions of a session touched (D-31): one entry per
|
|
518
|
+
* journaled call that did not fail, with the tool call (`js_run`/`python_run`) that ran it so the
|
|
519
|
+
* panel can attach the run. `paths` are the absolute paths the gate resolved. Oldest first.
|
|
520
|
+
*/
|
|
521
|
+
forgeWriteCalls(session) {
|
|
522
|
+
const rows = this.db
|
|
523
|
+
.prepare(`SELECT e.tool_call_id AS tool_call_id, c.paths_json AS paths_json,
|
|
524
|
+
t.run_id AS run_id, t.started_at AS started_at
|
|
525
|
+
FROM forge_calls c
|
|
526
|
+
JOIN forge_executions e ON e.id = c.execution_id
|
|
527
|
+
LEFT JOIN tool_calls t ON t.session = e.session_id AND t.call_id = e.tool_call_id
|
|
528
|
+
WHERE e.session_id = ? AND c.effect = 'write' AND c.status != 'failed'
|
|
529
|
+
AND c.paths_json IS NOT NULL
|
|
530
|
+
ORDER BY t.started_at, c.rowid`)
|
|
531
|
+
.all(session);
|
|
532
|
+
return rows.map((row) => ({
|
|
533
|
+
toolCallId: row.tool_call_id,
|
|
534
|
+
paths: JSON.parse(row.paths_json),
|
|
535
|
+
...(row.run_id ? { runId: row.run_id } : {}),
|
|
536
|
+
...(row.started_at != null ? { startedAt: Number(row.started_at) } : {}),
|
|
537
|
+
}));
|
|
538
|
+
}
|
|
436
539
|
/** Rich (ui/image) parts persist as part of the serialized result and replay verbatim. */
|
|
437
540
|
callResult(id, callId) {
|
|
438
541
|
const row = this.db
|