@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.
Files changed (204) hide show
  1. package/dist/analysis/janitor.d.ts +48 -0
  2. package/dist/analysis/janitor.js +170 -18
  3. package/dist/analysis/jobs.d.ts +11 -2
  4. package/dist/analysis/jobs.js +13 -0
  5. package/dist/analysis/oci.d.ts +11 -2
  6. package/dist/analysis/oci.js +20 -4
  7. package/dist/analysis/python/sources.js +2 -1
  8. package/dist/analysis/rerun.d.ts +1 -1
  9. package/dist/analysis/status.d.ts +3 -1
  10. package/dist/analysis/status.js +3 -0
  11. package/dist/application.d.ts +28 -2
  12. package/dist/application.js +189 -4
  13. package/dist/changelog/data.js +42 -0
  14. package/dist/config.d.ts +110 -2
  15. package/dist/config.js +188 -6
  16. package/dist/core/call-internals.d.ts +22 -0
  17. package/dist/core/call-internals.js +9 -0
  18. package/dist/core/contracts.d.ts +2 -2
  19. package/dist/core/runner.js +111 -38
  20. package/dist/eval/canonical.d.ts +14 -0
  21. package/dist/eval/canonical.js +40 -0
  22. package/dist/eval/cassette.d.ts +180 -0
  23. package/dist/eval/cassette.js +424 -0
  24. package/dist/eval/compare.d.ts +147 -0
  25. package/dist/eval/compare.js +436 -0
  26. package/dist/eval/engine.d.ts +144 -0
  27. package/dist/eval/engine.js +728 -0
  28. package/dist/eval/export.d.ts +40 -0
  29. package/dist/eval/export.js +90 -0
  30. package/dist/eval/fingerprint.d.ts +88 -0
  31. package/dist/eval/fingerprint.js +186 -0
  32. package/dist/eval/normalize.d.ts +68 -0
  33. package/dist/eval/normalize.js +138 -0
  34. package/dist/eval/paths.d.ts +11 -0
  35. package/dist/eval/paths.js +50 -0
  36. package/dist/eval/plan.d.ts +26 -0
  37. package/dist/eval/plan.js +44 -0
  38. package/dist/eval/report.d.ts +53 -0
  39. package/dist/eval/report.js +258 -0
  40. package/dist/eval/run-suite.d.ts +68 -0
  41. package/dist/eval/run-suite.js +94 -0
  42. package/dist/eval/secrets.d.ts +12 -0
  43. package/dist/eval/secrets.js +45 -0
  44. package/dist/eval/stats.d.ts +17 -0
  45. package/dist/eval/stats.js +50 -0
  46. package/dist/eval/suite.d.ts +20 -0
  47. package/dist/eval/suite.js +165 -0
  48. package/dist/eval/variants.d.ts +35 -0
  49. package/dist/eval/variants.js +137 -0
  50. package/dist/eval/verifiers.d.ts +49 -0
  51. package/dist/eval/verifiers.js +264 -0
  52. package/dist/forge/broker.d.ts +51 -0
  53. package/dist/forge/broker.js +244 -0
  54. package/dist/forge/budget.d.ts +27 -0
  55. package/dist/forge/budget.js +60 -0
  56. package/dist/forge/catalog-view.d.ts +24 -0
  57. package/dist/forge/catalog-view.js +43 -0
  58. package/dist/forge/errors.d.ts +26 -0
  59. package/dist/forge/errors.js +49 -0
  60. package/dist/forge/guides/javascript.d.ts +25 -0
  61. package/dist/forge/guides/javascript.js +90 -0
  62. package/dist/forge/guides/provider.d.ts +42 -0
  63. package/dist/forge/guides/provider.js +96 -0
  64. package/dist/forge/guides/python-environment.d.ts +15 -0
  65. package/dist/forge/guides/python-environment.js +81 -0
  66. package/dist/forge/guides/python.d.ts +38 -0
  67. package/dist/forge/guides/python.js +170 -0
  68. package/dist/forge/javascript/engine.d.ts +44 -0
  69. package/dist/forge/javascript/engine.js +229 -0
  70. package/dist/forge/javascript/hosts/factory.d.ts +25 -0
  71. package/dist/forge/javascript/hosts/factory.js +21 -0
  72. package/dist/forge/javascript/hosts/framing.d.ts +17 -0
  73. package/dist/forge/javascript/hosts/framing.js +49 -0
  74. package/dist/forge/javascript/hosts/isolate-main.d.ts +9 -0
  75. package/dist/forge/javascript/hosts/isolate-main.js +63 -0
  76. package/dist/forge/javascript/hosts/process-entry.d.ts +1 -0
  77. package/dist/forge/javascript/hosts/process-entry.js +3 -0
  78. package/dist/forge/javascript/hosts/process-host.d.ts +33 -0
  79. package/dist/forge/javascript/hosts/process-host.js +160 -0
  80. package/dist/forge/javascript/hosts/worker-entry.d.ts +1 -0
  81. package/dist/forge/javascript/hosts/worker-entry.js +3 -0
  82. package/dist/forge/javascript/hosts/worker-host.d.ts +27 -0
  83. package/dist/forge/javascript/hosts/worker-host.js +116 -0
  84. package/dist/forge/javascript/prelude.d.ts +2 -0
  85. package/dist/forge/javascript/prelude.js +333 -0
  86. package/dist/forge/javascript/probe.d.ts +2 -0
  87. package/dist/forge/javascript/probe.js +53 -0
  88. package/dist/forge/javascript/runtime-manifest.d.ts +14 -0
  89. package/dist/forge/javascript/runtime-manifest.js +14 -0
  90. package/dist/forge/javascript/vm-runner.d.ts +21 -0
  91. package/dist/forge/javascript/vm-runner.js +301 -0
  92. package/dist/forge/javascript/wasm-asset.d.ts +8 -0
  93. package/dist/forge/javascript/wasm-asset.js +8 -0
  94. package/dist/forge/javascript/wasm.d.ts +15 -0
  95. package/dist/forge/javascript/wasm.js +54 -0
  96. package/dist/forge/journal.d.ts +105 -0
  97. package/dist/forge/journal.js +174 -0
  98. package/dist/forge/kernel.d.ts +76 -0
  99. package/dist/forge/kernel.js +339 -0
  100. package/dist/forge/limits.d.ts +38 -0
  101. package/dist/forge/limits.js +73 -0
  102. package/dist/forge/nested.d.ts +96 -0
  103. package/dist/forge/nested.js +214 -0
  104. package/dist/forge/ports.d.ts +120 -0
  105. package/dist/forge/ports.js +1 -0
  106. package/dist/forge/progress.d.ts +24 -0
  107. package/dist/forge/progress.js +76 -0
  108. package/dist/forge/protocol.d.ts +110 -0
  109. package/dist/forge/protocol.js +171 -0
  110. package/dist/forge/python/backend-selector.d.ts +38 -0
  111. package/dist/forge/python/backend-selector.js +111 -0
  112. package/dist/forge/python/backend.d.ts +89 -0
  113. package/dist/forge/python/backend.js +1 -0
  114. package/dist/forge/python/engine.d.ts +84 -0
  115. package/dist/forge/python/engine.js +279 -0
  116. package/dist/forge/python/frame-parser.d.ts +24 -0
  117. package/dist/forge/python/frame-parser.js +107 -0
  118. package/dist/forge/python/frame-transport.d.ts +40 -0
  119. package/dist/forge/python/frame-transport.js +101 -0
  120. package/dist/forge/python/import-scan.d.ts +22 -0
  121. package/dist/forge/python/import-scan.js +133 -0
  122. package/dist/forge/python/managed-backend.d.ts +30 -0
  123. package/dist/forge/python/managed-backend.js +111 -0
  124. package/dist/forge/python/oci-backend.d.ts +27 -0
  125. package/dist/forge/python/oci-backend.js +84 -0
  126. package/dist/forge/python/pyodide-backend.d.ts +40 -0
  127. package/dist/forge/python/pyodide-backend.js +289 -0
  128. package/dist/forge/python/pyodide-install.d.ts +41 -0
  129. package/dist/forge/python/pyodide-install.js +167 -0
  130. package/dist/forge/python/pyodide-manifest.d.ts +42 -0
  131. package/dist/forge/python/pyodide-manifest.js +376 -0
  132. package/dist/forge/python/pyodide-runner-source.d.ts +2 -0
  133. package/dist/forge/python/pyodide-runner-source.js +3 -0
  134. package/dist/forge/python/rss.d.ts +2 -0
  135. package/dist/forge/python/rss.js +33 -0
  136. package/dist/forge/python/scratch-copy.d.ts +6 -0
  137. package/dist/forge/python/scratch-copy.js +61 -0
  138. package/dist/forge/python/settle.d.ts +6 -0
  139. package/dist/forge/python/settle.js +21 -0
  140. package/dist/forge/python/status.d.ts +8 -0
  141. package/dist/forge/python/status.js +15 -0
  142. package/dist/forge/python/wasm-node.d.ts +59 -0
  143. package/dist/forge/python/wasm-node.js +220 -0
  144. package/dist/forge/python/wiring.d.ts +39 -0
  145. package/dist/forge/python/wiring.js +47 -0
  146. package/dist/forge/registry.d.ts +26 -0
  147. package/dist/forge/registry.js +83 -0
  148. package/dist/forge/result.d.ts +58 -0
  149. package/dist/forge/result.js +86 -0
  150. package/dist/forge/routing-rule.d.ts +19 -0
  151. package/dist/forge/routing-rule.js +31 -0
  152. package/dist/forge/runtime.d.ts +64 -0
  153. package/dist/forge/runtime.js +88 -0
  154. package/dist/forge/scratch-files.d.ts +36 -0
  155. package/dist/forge/scratch-files.js +94 -0
  156. package/dist/forge/scratch-provider.d.ts +41 -0
  157. package/dist/forge/scratch-provider.js +79 -0
  158. package/dist/forge/scratch-quota.d.ts +8 -0
  159. package/dist/forge/scratch-quota.js +25 -0
  160. package/dist/forge/selector.d.ts +35 -0
  161. package/dist/forge/selector.js +51 -0
  162. package/dist/forge/session-dir.d.ts +59 -0
  163. package/dist/forge/session-dir.js +239 -0
  164. package/dist/forge/session-path.d.ts +31 -0
  165. package/dist/forge/session-path.js +61 -0
  166. package/dist/forge/slots.d.ts +16 -0
  167. package/dist/forge/slots.js +47 -0
  168. package/dist/forge/state-delta.d.ts +54 -0
  169. package/dist/forge/state-delta.js +99 -0
  170. package/dist/forge/state-machine.d.ts +20 -0
  171. package/dist/forge/state-machine.js +40 -0
  172. package/dist/forge/state-store.d.ts +25 -0
  173. package/dist/forge/state-store.js +50 -0
  174. package/dist/forge/task-log.d.ts +9 -0
  175. package/dist/forge/task-log.js +42 -0
  176. package/dist/forge/verdict.d.ts +20 -0
  177. package/dist/forge/verdict.js +28 -0
  178. package/dist/forge-isolate.d.ts +6 -0
  179. package/dist/forge-isolate.js +6 -0
  180. package/dist/index.d.ts +23 -1
  181. package/dist/index.js +23 -1
  182. package/dist/permissions/decide.d.ts +42 -0
  183. package/dist/permissions/decide.js +24 -0
  184. package/dist/plan/exit-plan.js +2 -1
  185. package/dist/resources/context.d.ts +2 -1
  186. package/dist/resources/context.js +2 -1
  187. package/dist/runtime/access.d.ts +34 -0
  188. package/dist/runtime/access.js +80 -0
  189. package/dist/runtime/ids.d.ts +21 -0
  190. package/dist/runtime/ids.js +41 -1
  191. package/dist/runtime/process.d.ts +2 -1
  192. package/dist/runtime/process.js +5 -5
  193. package/dist/runtime/store.d.ts +11 -0
  194. package/dist/runtime/store.js +106 -2
  195. package/dist/tools/analysis.d.ts +25 -2
  196. package/dist/tools/analysis.js +315 -267
  197. package/dist/tools/execute.js +6 -7
  198. package/dist/tools/forge-guide.d.ts +6 -0
  199. package/dist/tools/forge-guide.js +60 -0
  200. package/dist/tools/js-run.d.ts +19 -0
  201. package/dist/tools/js-run.js +126 -0
  202. package/dist/tools/standard.d.ts +7 -1
  203. package/dist/tools/standard.js +44 -43
  204. package/package.json +7 -2
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The common Forja protocol (spec §5.4, D10): one logical message schema over several transports.
3
+ * This module is the host-side validator of what the guest sends. Shape is checked with Ajv; the
4
+ * stateful rules (sequence, call correlation, terminal closure, capability gate) are plain code.
5
+ * Every rejection is a `PROTOCOL_VIOLATION` and the caller terminates the execution.
6
+ */
7
+ import type { EngineId } from "@alisio/sdk";
8
+ import { ForgeError, type ForgeErrorCode } from "./errors.ts";
9
+ import type { GuestLimits } from "./limits.ts";
10
+ export declare const MAX_MESSAGE_BYTES: number;
11
+ /** The registry's naming rule (`ToolRegistry.register`); the protocol refuses any other name. */
12
+ export declare const TOOL_NAME_PATTERN = "^[a-zA-Z0-9_-]{1,64}$";
13
+ export declare const MAX_JSON_DEPTH = 32;
14
+ export type ForgeOutputItem = {
15
+ type: "text";
16
+ text: string;
17
+ } | {
18
+ type: "json";
19
+ value: unknown;
20
+ };
21
+ export interface Envelope<T extends string, P> {
22
+ protocol: 1;
23
+ type: T;
24
+ /** Strictly increasing per direction. */
25
+ seq: number;
26
+ payload: P;
27
+ }
28
+ /** Guest to host. */
29
+ export type GuestMessage = Envelope<"ready", {
30
+ engine: EngineId;
31
+ preludeHash: string;
32
+ }> | Envelope<"progress", {
33
+ message: string;
34
+ completed?: number;
35
+ total?: number;
36
+ }> | Envelope<"output", ForgeOutputItem> | Envelope<"state.set", {
37
+ key: string;
38
+ value: unknown;
39
+ }> | Envelope<"state.delete", {
40
+ key: string;
41
+ }>
42
+ /** Only when the engine's `capabilities.toolCalls` is true. */
43
+ | Envelope<"call", {
44
+ callId: number;
45
+ tool: string;
46
+ input: unknown;
47
+ }> | Envelope<"catalog", {
48
+ requestId: number;
49
+ op: "search" | "describe";
50
+ query: string;
51
+ limit?: number;
52
+ }> | Envelope<"complete", {
53
+ value?: unknown;
54
+ }> | Envelope<"fatal", {
55
+ code: ForgeErrorCode;
56
+ message: string;
57
+ }>;
58
+ /** Host to guest. */
59
+ export type HostMessage = Envelope<"start", {
60
+ source: string;
61
+ state: Record<string, unknown>;
62
+ limits: GuestLimits;
63
+ catalogSize: number;
64
+ }> | Envelope<"result", {
65
+ callId: number;
66
+ ok: true;
67
+ value: unknown;
68
+ } | {
69
+ callId: number;
70
+ ok: false;
71
+ error: {
72
+ code: ForgeErrorCode;
73
+ message: string;
74
+ };
75
+ }> | Envelope<"catalog.result", {
76
+ requestId: number;
77
+ entries: unknown[];
78
+ }>
79
+ /** Backpressure credits. */
80
+ | Envelope<"grant", {
81
+ calls: number;
82
+ bytes: number;
83
+ }>;
84
+ export type GuestMessageType = GuestMessage["type"];
85
+ export type GuestValidation = {
86
+ ok: true;
87
+ message: GuestMessage;
88
+ } | {
89
+ ok: false;
90
+ error: ForgeError;
91
+ };
92
+ export type Correlation = {
93
+ ok: true;
94
+ } | {
95
+ ok: false;
96
+ error: ForgeError;
97
+ };
98
+ export declare class GuestMessageValidator {
99
+ #private;
100
+ constructor(options: {
101
+ toolCalls: boolean;
102
+ });
103
+ /** Validates one guest message. Any violation closes the validator for good. */
104
+ accept(raw: unknown): GuestValidation;
105
+ /** Marks a call as answered by the host; unknown or already answered ids are violations. */
106
+ resolveCall(callId: number): Correlation;
107
+ resolveCatalog(requestId: number): Correlation;
108
+ /** The execution reached a terminal state: nothing more is accepted. */
109
+ close(): void;
110
+ }
@@ -0,0 +1,171 @@
1
+ import Ajv from "ajv";
2
+ import { FORGE_ERROR_CODES, ForgeError } from "./errors.js";
3
+ export const MAX_MESSAGE_BYTES = 256 * 1024;
4
+ /** The registry's naming rule (`ToolRegistry.register`); the protocol refuses any other name. */
5
+ export const TOOL_NAME_PATTERN = "^[a-zA-Z0-9_-]{1,64}$";
6
+ export const MAX_JSON_DEPTH = 32;
7
+ const FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
8
+ const ENGINE_IDS = [
9
+ "javascript-quickjs",
10
+ "python-managed",
11
+ "python-oci",
12
+ "python-wasm",
13
+ ];
14
+ const obj = (properties, required) => ({
15
+ type: "object",
16
+ properties,
17
+ required,
18
+ additionalProperties: false,
19
+ });
20
+ const count = { type: "number", minimum: 0 };
21
+ const id = { type: "integer", minimum: 0 };
22
+ const PAYLOAD_SCHEMAS = {
23
+ ready: obj({ engine: { enum: ENGINE_IDS }, preludeHash: { type: "string" } }, [
24
+ "engine",
25
+ "preludeHash",
26
+ ]),
27
+ progress: obj({ message: { type: "string" }, completed: count, total: count }, ["message"]),
28
+ output: {
29
+ oneOf: [
30
+ obj({ type: { const: "text" }, text: { type: "string" } }, ["type", "text"]),
31
+ obj({ type: { const: "json" }, value: {} }, ["type", "value"]),
32
+ ],
33
+ },
34
+ "state.set": obj({ key: { type: "string", minLength: 1 }, value: {} }, ["key", "value"]),
35
+ "state.delete": obj({ key: { type: "string", minLength: 1 } }, ["key"]),
36
+ call: obj({
37
+ callId: id,
38
+ tool: { type: "string", minLength: 1, maxLength: 128, pattern: TOOL_NAME_PATTERN },
39
+ input: {},
40
+ }, ["callId", "tool", "input"]),
41
+ catalog: obj({
42
+ requestId: id,
43
+ op: { enum: ["search", "describe"] },
44
+ query: { type: "string" },
45
+ limit: { type: "integer", minimum: 1 },
46
+ }, ["requestId", "op", "query"]),
47
+ complete: obj({ value: {} }, []),
48
+ fatal: obj({ code: { enum: [...FORGE_ERROR_CODES] }, message: { type: "string" } }, [
49
+ "code",
50
+ "message",
51
+ ]),
52
+ };
53
+ const ENVELOPE_SCHEMA = obj({
54
+ protocol: { const: 1 },
55
+ type: { enum: Object.keys(PAYLOAD_SCHEMAS) },
56
+ seq: { type: "integer", minimum: 0 },
57
+ payload: { type: "object" },
58
+ }, ["protocol", "type", "seq", "payload"]);
59
+ const ajv = new Ajv({ allErrors: true, strict: true });
60
+ const validateEnvelope = ajv.compile(ENVELOPE_SCHEMA);
61
+ const validatePayload = new Map(Object.entries(PAYLOAD_SCHEMAS).map(([type, schema]) => [type, ajv.compile(schema)]));
62
+ /** Returns why `value` is not plain JSON within the depth bound, or undefined when it is. */
63
+ function jsonViolation(value, depth) {
64
+ if (value === null || typeof value === "string" || typeof value === "boolean")
65
+ return undefined;
66
+ if (typeof value === "number")
67
+ return Number.isFinite(value) ? undefined : "non-finite number";
68
+ if (typeof value !== "object")
69
+ return `non-JSON value (${typeof value})`;
70
+ if (depth > MAX_JSON_DEPTH)
71
+ return `JSON deeper than ${MAX_JSON_DEPTH} levels`;
72
+ if (Array.isArray(value)) {
73
+ for (let i = 0; i < value.length; i++) {
74
+ const reason = jsonViolation(value[i], depth + 1);
75
+ if (reason)
76
+ return reason;
77
+ }
78
+ return undefined;
79
+ }
80
+ const proto = Object.getPrototypeOf(value);
81
+ if (proto !== Object.prototype && proto !== null)
82
+ return "non-plain object";
83
+ for (const key of Object.keys(value)) {
84
+ if (FORBIDDEN_KEYS.has(key))
85
+ return `forbidden key ${key}`;
86
+ const reason = jsonViolation(value[key], depth + 1);
87
+ if (reason)
88
+ return reason;
89
+ }
90
+ return undefined;
91
+ }
92
+ const violation = (reason) => ({
93
+ ok: false,
94
+ error: new ForgeError("PROTOCOL_VIOLATION", reason),
95
+ });
96
+ export class GuestMessageValidator {
97
+ #toolCalls;
98
+ #lastSeq = -1;
99
+ #closed = false;
100
+ #pendingCalls = new Set();
101
+ #usedCalls = new Set();
102
+ #pendingCatalog = new Set();
103
+ #usedCatalog = new Set();
104
+ constructor(options) {
105
+ this.#toolCalls = options.toolCalls;
106
+ }
107
+ /** Validates one guest message. Any violation closes the validator for good. */
108
+ accept(raw) {
109
+ const result = this.#check(raw);
110
+ if (!result.ok)
111
+ this.#closed = true;
112
+ return result;
113
+ }
114
+ /** Marks a call as answered by the host; unknown or already answered ids are violations. */
115
+ resolveCall(callId) {
116
+ if (!this.#pendingCalls.delete(callId)) {
117
+ return violation(`callId ${callId} is unknown or already resolved`);
118
+ }
119
+ return { ok: true };
120
+ }
121
+ resolveCatalog(requestId) {
122
+ if (!this.#pendingCatalog.delete(requestId)) {
123
+ return violation(`catalog requestId ${requestId} is unknown or already resolved`);
124
+ }
125
+ return { ok: true };
126
+ }
127
+ /** The execution reached a terminal state: nothing more is accepted. */
128
+ close() {
129
+ this.#closed = true;
130
+ }
131
+ #check(raw) {
132
+ if (this.#closed)
133
+ return violation("message after the channel was closed");
134
+ const unsafe = jsonViolation(raw, 1);
135
+ if (unsafe)
136
+ return violation(unsafe);
137
+ if (Buffer.byteLength(JSON.stringify(raw), "utf8") > MAX_MESSAGE_BYTES) {
138
+ return violation(`message larger than ${MAX_MESSAGE_BYTES} bytes`);
139
+ }
140
+ if (!validateEnvelope(raw))
141
+ return violation("malformed envelope or unknown message type");
142
+ const message = raw;
143
+ if (!validatePayload.get(message.type)?.(message.payload)) {
144
+ return violation(`invalid payload for ${message.type}`);
145
+ }
146
+ if (message.seq <= this.#lastSeq)
147
+ return violation(`seq ${message.seq} is not increasing`);
148
+ if ((message.type === "call" || message.type === "catalog") && !this.#toolCalls) {
149
+ return violation(`${message.type} is not allowed: this engine cannot call tools`);
150
+ }
151
+ if (message.type === "call") {
152
+ const callId = message.payload.callId;
153
+ if (this.#usedCalls.has(callId))
154
+ return violation(`callId ${callId} was already used`);
155
+ this.#usedCalls.add(callId);
156
+ this.#pendingCalls.add(callId);
157
+ }
158
+ else if (message.type === "catalog") {
159
+ const requestId = message.payload.requestId;
160
+ if (this.#usedCatalog.has(requestId)) {
161
+ return violation(`catalog requestId ${requestId} was already used`);
162
+ }
163
+ this.#usedCatalog.add(requestId);
164
+ this.#pendingCatalog.add(requestId);
165
+ }
166
+ this.#lastSeq = message.seq;
167
+ if (message.type === "complete" || message.type === "fatal")
168
+ this.#closed = true;
169
+ return { ok: true, message };
170
+ }
171
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `PythonBackendSelector` (Python spec §6.1): a chain of pure rules, each `(context) -> decision |
3
+ * next`. It decides once, before anything is prepared (PD5: no fallback after a run started), from
4
+ * facts the caller already gathered; it never probes.
5
+ *
6
+ * 1. an explicit `analysis.runtime` (`managed`, `oci`, `wasm`) runs there or fails with the reason
7
+ * and the way out; a policy (`analysis.isolation`) that forbids it is an error too;
8
+ * 2. `auto` + `rerunOf`: back to the original backend when it is ready, fits and the policy allows;
9
+ * 3. `auto`: oci -> wasm -> managed, the first eligible one;
10
+ * 4. nothing eligible: `ENGINE_UNAVAILABLE` with every reason and the shortest command.
11
+ */
12
+ import type { EngineId } from "@alisio/sdk";
13
+ import { EngineSelector } from "../selector.ts";
14
+ import type { BackendReadiness, PythonBackendId, SupportVerdict } from "./backend.ts";
15
+ export type PythonRuntimeSetting = "auto" | PythonBackendId;
16
+ export type IsolationPolicy = "preferred" | "required" | "strict";
17
+ export interface PythonSelectionContext {
18
+ /** `analysis.runtime`. */
19
+ runtime: PythonRuntimeSetting;
20
+ /** `analysis.isolation`; `preferred` when absent. */
21
+ isolation?: IsolationPolicy;
22
+ /** Backend of the execution a `rerunOf` repeats. */
23
+ rerunOfBackend?: PythonBackendId;
24
+ /** What the caller found out; a backend missing here was not checked and is not ready. */
25
+ readiness: Partial<Record<PythonBackendId, BackendReadiness>>;
26
+ /** `backend.supports(requirements)` per backend; a backend missing here fits. */
27
+ support?: Partial<Record<PythonBackendId, SupportVerdict>>;
28
+ /** `networkDeniedByOS` of each backend's descriptor; missing means open. Needed by `strict`. */
29
+ networkDenied?: Partial<Record<PythonBackendId, boolean>>;
30
+ }
31
+ export declare const engineIdOf: (backend: PythonBackendId) => EngineId;
32
+ /** The backends worth probing for this call. */
33
+ export declare function neededBackends(context: {
34
+ runtime: PythonRuntimeSetting;
35
+ isolation?: IsolationPolicy;
36
+ rerunOfBackend?: PythonBackendId;
37
+ }): PythonBackendId[];
38
+ export declare function createPythonBackendSelector(): EngineSelector<PythonSelectionContext, PythonBackendId>;
@@ -0,0 +1,111 @@
1
+ import { EngineSelector, reject, select } from "../selector.js";
2
+ /** Candidates of `auto`, most isolated first. */
3
+ const AUTO_ORDER = ["oci", "wasm", "managed"];
4
+ /** Higher is more isolated. */
5
+ const ISOLATION_RANK = { managed: 0, wasm: 1, oci: 2 };
6
+ /** The command that would make a not-ready backend ready; `fix: ""` says none would. */
7
+ const fixOf = (state, backend) => (state.fix !== undefined ? state.fix || undefined : SETUP[backend]);
8
+ /** The setup command of a backend that can be made ready. */
9
+ const SETUP = {
10
+ oci: "alisio analysis setup --oci",
11
+ wasm: "alisio analysis setup --wasm",
12
+ };
13
+ export const engineIdOf = (backend) => `python-${backend}`;
14
+ /** The backends worth probing for this call. */
15
+ export function neededBackends(context) {
16
+ const hostAllowed = (context.isolation ?? "preferred") === "preferred";
17
+ const wanted = context.runtime === "auto"
18
+ ? AUTO_ORDER.filter((backend) => backend !== "managed" || hostAllowed)
19
+ : [context.runtime];
20
+ if (context.rerunOfBackend && (context.rerunOfBackend !== "managed" || hostAllowed))
21
+ wanted.push(context.rerunOfBackend);
22
+ return [...new Set(wanted)];
23
+ }
24
+ const readyOf = (context, backend) => context.readiness[backend] ?? { ready: false, reason: `${backend} was not checked` };
25
+ const isolationOf = (context) => context.isolation ?? "preferred";
26
+ /** Why the policy alone rules a backend out, or undefined. */
27
+ function policyExclusion(context, backend) {
28
+ const policy = isolationOf(context);
29
+ if (backend === "managed" && policy !== "preferred")
30
+ return `excluded by analysis.isolation=${policy} (it runs on the host)`;
31
+ if (policy === "strict" && context.networkDenied?.[backend] !== true)
32
+ return `excluded by analysis.isolation=strict (its network is not denied by the OS)`;
33
+ return undefined;
34
+ }
35
+ /** Policy, then readiness, then fit. */
36
+ function eligibility(context, backend) {
37
+ const excluded = policyExclusion(context, backend);
38
+ if (excluded)
39
+ return { ok: false, reason: excluded };
40
+ const state = readyOf(context, backend);
41
+ if (!state.ready) {
42
+ const fix = fixOf(state, backend);
43
+ return { ok: false, reason: `not ready (${state.reason})`, ...(fix ? { fix } : {}) };
44
+ }
45
+ const verdict = context.support?.[backend];
46
+ if (verdict && !verdict.ok)
47
+ return { ok: false, reason: verdict.reason, ...(verdict.fix ? { fix: verdict.fix } : {}) };
48
+ return { ok: true };
49
+ }
50
+ /** Why a rerun did not use its original backend, for the reason line. */
51
+ function rerunNote(context, chosen) {
52
+ const original = context.rerunOfBackend;
53
+ if (!original || original === chosen)
54
+ return "";
55
+ const state = eligibility(context, original);
56
+ return state.ok ? "" : `; rerun: original backend ${original} not used (${state.reason})`;
57
+ }
58
+ /** Rule 1: the explicit runtime, as is. Not ready or not fitting is an error with the way out. */
59
+ const explicitRuntime = (context) => {
60
+ const { runtime } = context;
61
+ if (runtime === "auto")
62
+ return null;
63
+ const policy = policyExclusion(context, runtime);
64
+ if (policy)
65
+ return reject(`${runtime} ${policy.replace(/^excluded/, "is excluded")}; set analysis.runtime=auto (or another backend) or relax analysis.isolation`);
66
+ const state = readyOf(context, runtime);
67
+ const setup = state.ready ? undefined : fixOf(state, runtime);
68
+ const way = (fix) => `; set analysis.runtime=auto${fix ? ` or run "${fix}"` : ""}`;
69
+ if (!state.ready)
70
+ return reject(`${runtime} is not ready: ${state.reason}${runtime === "managed" ? "" : way(setup)}`);
71
+ const verdict = context.support?.[runtime];
72
+ if (verdict && !verdict.ok)
73
+ return reject(`${runtime} cannot run this script: ${verdict.reason}${way(verdict.fix)}`);
74
+ // A rerun of an execution that ran right here is a repeat, and the reason says so.
75
+ return select(runtime, context.rerunOfBackend === runtime
76
+ ? `rerun: original backend ${runtime}`
77
+ : `analysis.runtime=${runtime}`);
78
+ };
79
+ /** Rule 2: under `auto`, a rerun goes back to the backend that ran the original when it can. */
80
+ const rerunOriginalBackend = (context) => {
81
+ const original = context.rerunOfBackend;
82
+ if (!original || context.runtime !== "auto")
83
+ return null;
84
+ return eligibility(context, original).ok
85
+ ? select(original, `rerun: original backend ${original}`)
86
+ : null;
87
+ };
88
+ /** Rules 3 and 4: the first eligible candidate of `auto`, or the reasons nothing was. */
89
+ const autoChain = (context) => {
90
+ if (context.runtime !== "auto")
91
+ return null;
92
+ const skipped = [];
93
+ const fixes = [];
94
+ for (const backend of AUTO_ORDER) {
95
+ const state = eligibility(context, backend);
96
+ if (state.ok) {
97
+ const why = skipped.length ? `; ${skipped.join("; ")}` : "";
98
+ return select(backend, `analysis.runtime=auto, analysis.isolation=${isolationOf(context)}: ${backend} is ready${why}${rerunNote(context, backend)}`);
99
+ }
100
+ skipped.push(`${backend}: ${state.reason}`);
101
+ if (state.fix)
102
+ fixes.push({ backend, fix: state.fix });
103
+ }
104
+ // The least effort first: wasm needs no container engine, so its command comes before oci's.
105
+ const fix = [...fixes].sort((a, b) => ISOLATION_RANK[a.backend] - ISOLATION_RANK[b.backend])[0];
106
+ return reject(`no Python backend can run this script (analysis.isolation=${isolationOf(context)}): ${skipped.join("; ")}${fix ? `. Shortest fix: run "${fix.fix}"` : ""}`);
107
+ };
108
+ const RULES = [explicitRuntime, rerunOriginalBackend, autoChain];
109
+ export function createPythonBackendSelector() {
110
+ return new EngineSelector(RULES);
111
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The strategy behind `PythonEngine` (Python spec §4.2, PD1). A backend knows how to start a
3
+ * prepared job and how to stop it; everything else of an execution (job folders, the frame
4
+ * transport, publication, journal, state) is shared. The three implementations are `managed`, `oci`
5
+ * and `wasm` (Pyodide in a restricted Node child process).
6
+ */
7
+ import type { EngineDescriptor } from "@alisio/sdk";
8
+ import type { JobPaths } from "../../analysis/jobs.ts";
9
+ import type { ProcessResult } from "../../runtime/process.ts";
10
+ import type { TerminationReason, TerminationReport } from "../ports.ts";
11
+ export type PythonBackendId = "managed" | "oci" | "wasm";
12
+ /** What the selector may ask of a backend before choosing it (pure, no I/O). */
13
+ export interface PythonRequirements {
14
+ extras: Array<"analysis" | "science">;
15
+ /** Top-level modules the script imports (`scanImports`). */
16
+ importedModules: string[];
17
+ /** Sum of the input sizes. */
18
+ inputBytes: number;
19
+ rerunOfBackend?: PythonBackendId;
20
+ }
21
+ export type SupportVerdict = {
22
+ ok: true;
23
+ } | {
24
+ ok: false;
25
+ reason: string;
26
+ code: "PROFILE_UNSUPPORTED" | "PACKAGE_UNAVAILABLE" | "INPUT_TOO_LARGE" | "NOT_READY";
27
+ /** The command that would make it fit, when there is one (`alisio analysis setup --wasm …`). */
28
+ fix?: string;
29
+ };
30
+ /** The live answer of "can this backend run now?", gathered before selection. */
31
+ export type BackendReadiness = {
32
+ ready: true;
33
+ } | {
34
+ ready: false;
35
+ /** Actionable, as the user reads it. */
36
+ reason: string;
37
+ /** The backend's own diagnosis (managed: the failed interpreter resolution), for rendering. */
38
+ detail?: unknown;
39
+ /** The command that makes it ready when it differs from the usual setup one; "" = none helps. */
40
+ fix?: string;
41
+ };
42
+ /** A job whose folders exist, handed to a backend to start. */
43
+ export interface PreparedJob {
44
+ executionId: string;
45
+ paths: JobPaths;
46
+ /** Variables for the script on top of the job paths: the frame nonce, the scratchpad. */
47
+ env: Record<string, string>;
48
+ /** The Forja scratchpad, exposed read-only (managed: env var; oci: `:ro` mount). */
49
+ scratch?: string;
50
+ /** Aborted when the kernel stops the execution (deadline or cancellation). */
51
+ signal: AbortSignal;
52
+ /** A backstop for the process, longer than the kernel's own deadline. */
53
+ backstopMs: number;
54
+ maxBytes: number;
55
+ maxLogBytes: number;
56
+ logFiles: {
57
+ stdout: string;
58
+ stderr: string;
59
+ };
60
+ }
61
+ export interface ProcessSink {
62
+ /** Live text of the script; `stdout` is where frames travel. */
63
+ onData(chunk: string, stream: "stdout" | "stderr"): void;
64
+ }
65
+ export interface RunningProcess {
66
+ /** Settles when the process ended; rejects with an abort error when it was stopped. */
67
+ readonly result: Promise<ProcessResult>;
68
+ /** The public runtime facts for the artifacts' provenance (`runtime`). */
69
+ readonly runtime: Record<string, unknown>;
70
+ /** Backend-private stop handle. */
71
+ readonly stop: AbortController;
72
+ /**
73
+ * A limit the BACKEND enforced by killing the process (the wasm memory watchdog). The engine
74
+ * reports it as the failure instead of a bare non-zero exit.
75
+ */
76
+ readonly limit?: () => {
77
+ code: "MEMORY_LIMIT";
78
+ message: string;
79
+ } | undefined;
80
+ }
81
+ export interface PythonBackend {
82
+ readonly id: PythonBackendId;
83
+ describe(): Promise<EngineDescriptor>;
84
+ /** Live readiness. May run a process (interpreter discovery) or call the container engine. */
85
+ check(signal?: AbortSignal): Promise<BackendReadiness>;
86
+ supports(requirements: PythonRequirements): SupportVerdict;
87
+ launch(job: PreparedJob, sink: ProcessSink): Promise<RunningProcess>;
88
+ terminate(process: RunningProcess, reason: TerminationReason): Promise<TerminationReport>;
89
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,84 @@
1
+ import type { EngineDescriptor, EngineId } from "@alisio/sdk";
2
+ import type { DatasetService } from "../../analysis/data/datasets.ts";
3
+ import type { AnalysisJobs, JobInput, JobPaths } from "../../analysis/jobs.ts";
4
+ import type { RerunPlan } from "../../analysis/rerun.ts";
5
+ import type { CoreArtifactPublisher, PublishedArtifactInfo } from "../../core/contracts.ts";
6
+ import type { ProcessResult } from "../../runtime/process.ts";
7
+ import type { EngineOutcome, ExecutionEngine, ExecutionRequest, KernelChannel, PreparedExecution, TerminationReason, TerminationReport, TrustedExecutionContext } from "../ports.ts";
8
+ import type { PythonBackend } from "./backend.ts";
9
+ /** Key of the plan in `ExecutionRequest.payload`. */
10
+ export declare const PYTHON_PLAN_KEY = "python";
11
+ /** The terminal text the model and the UI read is bounded to this many bytes. */
12
+ export declare const TERMINAL_BYTES: number;
13
+ /** What the tool learns about a run besides the kernel's result. Filled by the engine. */
14
+ export interface PythonRunReport {
15
+ job?: JobPaths & {
16
+ scriptSha256: string;
17
+ };
18
+ inputs: JobInput[];
19
+ /** The frame nonce of this run, to clean captured output. */
20
+ nonce?: string;
21
+ /** Raw result of the process (stdout already without frames). */
22
+ process?: ProcessResult;
23
+ /** The process could not run at all (spawn failure), or crashed outside the script's own exit. */
24
+ crash?: unknown;
25
+ /** Why `prepare` failed, as the original error (the tool rethrows it). */
26
+ prepareError?: unknown;
27
+ published: PublishedArtifactInfo[];
28
+ publishError?: string;
29
+ warnings: string[];
30
+ /** Wall time of the process, for the head line. */
31
+ durationMs?: number;
32
+ /** The public runtime facts of the backend that ran it (`runtime` in the provenance). */
33
+ runtime?: Record<string, unknown>;
34
+ }
35
+ export declare const newPythonRunReport: () => PythonRunReport;
36
+ /** Everything one `python_run` call hands to the engine. Built by the tool; never by the model. */
37
+ export interface PythonRunPlan {
38
+ code: string;
39
+ rerun?: RerunPlan;
40
+ inputs: Array<{
41
+ path?: string;
42
+ datasetId?: string;
43
+ }>;
44
+ /** Extras the run asked for (also recorded for reruns). */
45
+ extrasAsked: string[];
46
+ resolvePath: (path: string) => Promise<string>;
47
+ datasets?: DatasetService;
48
+ rootSessionId: string;
49
+ sessionId: string;
50
+ workspace: string;
51
+ runId?: string;
52
+ callId?: string;
53
+ publisher: CoreArtifactPublisher;
54
+ title?: string;
55
+ publishOnError: boolean;
56
+ /** Live text of the script (frames removed), for the terminal view. */
57
+ onText: (text: string) => void;
58
+ /** The Forja scratchpad to expose read-only, when there is one. */
59
+ scratch?: string;
60
+ maxLogBytes: number;
61
+ /** Engine, isolation and reason go to the artifacts' provenance (PD12). */
62
+ engine: {
63
+ id: EngineId;
64
+ isolation: string;
65
+ selectionReason: string;
66
+ };
67
+ report: PythonRunReport;
68
+ }
69
+ export interface PythonEngineDeps {
70
+ backend: PythonBackend;
71
+ jobs: AnalysisJobs;
72
+ }
73
+ export declare class PythonEngine implements ExecutionEngine {
74
+ #private;
75
+ readonly id: EngineId;
76
+ constructor(deps: PythonEngineDeps);
77
+ describe(): Promise<EngineDescriptor>;
78
+ prepare(request: ExecutionRequest, ctx: TrustedExecutionContext): Promise<PreparedExecution>;
79
+ run(prepared: PreparedExecution, channel: KernelChannel): Promise<EngineOutcome>;
80
+ /** Publishes the files the script left, when the run earned it (exit 0, or `publishOnError`). */
81
+ collect(prepared: PreparedExecution, outcome: EngineOutcome): Promise<EngineOutcome>;
82
+ terminate(prepared: PreparedExecution, reason: TerminationReason): Promise<TerminationReport>;
83
+ dispose(prepared: PreparedExecution): Promise<void>;
84
+ }