@odla-ai/harness 0.1.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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +178 -0
  3. package/dist/chunk-ATKV6VTU.js +168 -0
  4. package/dist/chunk-ATKV6VTU.js.map +1 -0
  5. package/dist/chunk-GE6CCN7W.js +93 -0
  6. package/dist/chunk-GE6CCN7W.js.map +1 -0
  7. package/dist/chunk-GMVZ4LZH.js +1769 -0
  8. package/dist/chunk-GMVZ4LZH.js.map +1 -0
  9. package/dist/chunk-PHXQH4YM.js +550 -0
  10. package/dist/chunk-PHXQH4YM.js.map +1 -0
  11. package/dist/chunk-PTXZVYD4.js +81 -0
  12. package/dist/chunk-PTXZVYD4.js.map +1 -0
  13. package/dist/chunk-QTUEF2HZ.js +9 -0
  14. package/dist/chunk-QTUEF2HZ.js.map +1 -0
  15. package/dist/cli.cjs +795 -0
  16. package/dist/cli.cjs.map +1 -0
  17. package/dist/cli.d.cts +1 -0
  18. package/dist/cli.d.ts +1 -0
  19. package/dist/cli.js +98 -0
  20. package/dist/cli.js.map +1 -0
  21. package/dist/code-runtime-cli.cjs +2341 -0
  22. package/dist/code-runtime-cli.cjs.map +1 -0
  23. package/dist/code-runtime-cli.d.cts +1 -0
  24. package/dist/code-runtime-cli.d.ts +1 -0
  25. package/dist/code-runtime-cli.js +133 -0
  26. package/dist/code-runtime-cli.js.map +1 -0
  27. package/dist/index.cjs +228 -0
  28. package/dist/index.cjs.map +1 -0
  29. package/dist/index.d.cts +44 -0
  30. package/dist/index.d.ts +44 -0
  31. package/dist/index.js +46 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/node.cjs +2580 -0
  34. package/dist/node.cjs.map +1 -0
  35. package/dist/node.d.cts +544 -0
  36. package/dist/node.d.ts +544 -0
  37. package/dist/node.js +71 -0
  38. package/dist/node.js.map +1 -0
  39. package/dist/testing.cjs +106 -0
  40. package/dist/testing.cjs.map +1 -0
  41. package/dist/testing.d.cts +25 -0
  42. package/dist/testing.d.ts +25 -0
  43. package/dist/testing.js +79 -0
  44. package/dist/testing.js.map +1 -0
  45. package/dist/types-D12vK3K9.d.cts +249 -0
  46. package/dist/types-D12vK3K9.d.ts +249 -0
  47. package/package.json +84 -0
@@ -0,0 +1,2341 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/code-runtime-cli.ts
5
+ var import_node_os3 = require("os");
6
+ var import_promises8 = require("fs/promises");
7
+
8
+ // src/code-runtime-client.ts
9
+ var import_code = require("@odla-ai/camel/code");
10
+ var CodeRuntimeControlError = class extends Error {
11
+ constructor(message2, status, code = "control_error") {
12
+ super(message2);
13
+ this.status = status;
14
+ this.code = code;
15
+ }
16
+ status;
17
+ code;
18
+ name = "CodeRuntimeControlError";
19
+ };
20
+ function createCodeRuntimeControlClient(options) {
21
+ const endpoint = validatedEndpoint(options.endpoint);
22
+ if (!/^odla_code_host_[0-9a-f]{64}$/.test(options.token)) throw new TypeError("invalid Code host credential");
23
+ const requestTimeoutMs = options.requestTimeoutMs ?? 3e4;
24
+ if (!Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 1e3 || requestTimeoutMs > 12e4) {
25
+ throw new TypeError("requestTimeoutMs must be an integer from 1000 to 120000");
26
+ }
27
+ const modelRequestTimeoutMs = options.modelRequestTimeoutMs ?? 15 * 6e4;
28
+ if (!Number.isSafeInteger(modelRequestTimeoutMs) || modelRequestTimeoutMs < 3e4 || modelRequestTimeoutMs > 30 * 6e4) {
29
+ throw new TypeError("modelRequestTimeoutMs must be an integer from 30000 to 1800000");
30
+ }
31
+ const request = options.fetch ?? fetch;
32
+ const call = async (path, body, timeoutMs = requestTimeoutMs) => {
33
+ const timeout = AbortSignal.timeout(timeoutMs);
34
+ const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
35
+ let response2;
36
+ try {
37
+ response2 = await request(`${endpoint}${path}`, {
38
+ method: "POST",
39
+ headers: { authorization: `Bearer ${options.token}`, "content-type": "application/json" },
40
+ body: JSON.stringify(body),
41
+ redirect: "error",
42
+ signal
43
+ });
44
+ } catch (cause) {
45
+ if (options.signal?.aborted) throw cause;
46
+ throw new CodeRuntimeControlError("Code runtime control plane is unavailable", 503, "transport_unavailable");
47
+ }
48
+ const value = await response2.json().catch(() => null);
49
+ if (!response2.ok) {
50
+ const problem = record(record(value)?.error);
51
+ throw new CodeRuntimeControlError(
52
+ typeof problem?.message === "string" ? problem.message : `Code runtime request failed (${response2.status})`,
53
+ response2.status,
54
+ typeof problem?.code === "string" ? problem.code : void 0
55
+ );
56
+ }
57
+ return value;
58
+ };
59
+ return {
60
+ heartbeat: async (version, capabilities) => {
61
+ validateHeartbeat(version, capabilities);
62
+ return parseSnapshot(await call("/registry/code/runtime/heartbeat", { runtimeVersion: version, capabilities }));
63
+ },
64
+ acknowledge: async (commandId, result) => {
65
+ if (!/^ccmd_[0-9a-f]{32}$/.test(commandId)) throw new TypeError("invalid Code runtime command id");
66
+ await call(`/registry/code/runtime/commands/${commandId}/ack`, result);
67
+ },
68
+ source: async (sessionId) => parseSource(
69
+ await call(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/source`, {})
70
+ ),
71
+ infer: async (sessionId, inference) => {
72
+ const value = record(await call(
73
+ `/registry/code/runtime/sessions/${validSessionId(sessionId)}/inference`,
74
+ inference,
75
+ modelRequestTimeoutMs
76
+ ));
77
+ if (!value || value.requestId !== inference.requestId || !record(value.response) || !record(value.receipt)) {
78
+ throw new CodeRuntimeControlError("invalid Code inference response", 502, "invalid_response");
79
+ }
80
+ return value;
81
+ },
82
+ review: async (sessionId, review) => parseReview(
83
+ await call(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/review`, review, modelRequestTimeoutMs)
84
+ ),
85
+ submitCandidate: async (sessionId, checkpointId, verification) => {
86
+ if (!/^cpoint_[0-9a-f]{32}$/.test(checkpointId)) throw new TypeError("invalid Code checkpoint id");
87
+ return parseCandidate(await call(
88
+ `/registry/code/runtime/sessions/${validSessionId(sessionId)}/candidates`,
89
+ { checkpointId, verification }
90
+ ));
91
+ },
92
+ appendSessionEvent: async (sessionId, eventId, event) => {
93
+ const serialized = JSON.stringify(event);
94
+ if (!/^[A-Za-z0-9._:-]{1,120}$/.test(eventId) || !event || typeof event !== "object" || new TextEncoder().encode(serialized).byteLength > 24e3) {
95
+ throw new TypeError("invalid Code session event");
96
+ }
97
+ await call(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/chat/events`, { eventId, event });
98
+ },
99
+ reportSessionFailure: async (sessionId, message2) => {
100
+ if (!message2.trim() || message2.length > 2e3) throw new TypeError("invalid Code session failure");
101
+ await call(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/failure`, { message: message2 });
102
+ }
103
+ };
104
+ }
105
+ function validatedEndpoint(value) {
106
+ const endpoint = value.replace(/\/+$/, "");
107
+ let url;
108
+ try {
109
+ url = new URL(endpoint);
110
+ } catch {
111
+ throw new TypeError("endpoint must be an HTTPS URL");
112
+ }
113
+ const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
114
+ if (url.username || url.password || url.protocol !== "https:" && !(loopback && url.protocol === "http:")) {
115
+ throw new TypeError("endpoint must use HTTPS (HTTP is allowed only for loopback testing)");
116
+ }
117
+ return endpoint;
118
+ }
119
+ function validSessionId(value) {
120
+ if (!/^csess_[0-9a-f]{32}$/.test(value)) throw new TypeError("invalid Code session id");
121
+ return value;
122
+ }
123
+ function validateHeartbeat(version, capabilities) {
124
+ if (!version.trim() || version.length > 80) throw new TypeError("runtimeVersion is required and at most 80 characters");
125
+ if (capabilities.protocolVersion !== CODE_RUNTIME_PROTOCOL_VERSION) throw new TypeError("unsupported Code runtime protocol version");
126
+ if (capabilities.platform !== "macos" && capabilities.platform !== "linux") throw new TypeError("invalid runtime platform");
127
+ if (!capabilities.arch || !capabilities.engines.length || !capabilities.engines.every((engine) => ["container", "podman", "docker"].includes(engine))) {
128
+ throw new TypeError("runtime arch and supported engine are required");
129
+ }
130
+ if (!Number.isSafeInteger(capabilities.cpuCount) || capabilities.cpuCount < 1 || !Number.isSafeInteger(capabilities.memoryBytes) || capabilities.memoryBytes < 1) {
131
+ throw new TypeError("runtime resources must be positive integers");
132
+ }
133
+ }
134
+ function parseSnapshot(value) {
135
+ const root = record(value);
136
+ const host = record(root?.host);
137
+ if (!host || typeof host.hostId !== "string" || typeof host.runtimeVersion !== "string" || !Number.isSafeInteger(host.lastSeenAt) || host.revokedAt !== null || !Array.isArray(root?.bindings) || root.bindings.length > 1024 || !Array.isArray(root?.commands) || root.commands.length > 64) throw invalid("heartbeat");
138
+ const bindingIds = /* @__PURE__ */ new Set();
139
+ const bindings = root.bindings.map((item) => {
140
+ const binding = record(item);
141
+ if (!binding || typeof binding.bindingId !== "string" || typeof binding.appId !== "string" || binding.env !== "dev" && binding.env !== "prod" || typeof binding.offerId !== "string" || binding.hostId !== host.hostId || !Number.isSafeInteger(binding.generation) || Number(binding.generation) < 1 || binding.revokedAt !== null || bindingIds.has(binding.bindingId)) {
142
+ throw invalid("binding");
143
+ }
144
+ bindingIds.add(binding.bindingId);
145
+ return binding;
146
+ });
147
+ const commandIds = /* @__PURE__ */ new Set();
148
+ const commandSequences = /* @__PURE__ */ new Set();
149
+ const commands = root.commands.map((item) => {
150
+ const command = record(item);
151
+ const binding = bindings.find((candidate) => candidate.bindingId === command?.bindingId);
152
+ const sequenceKey = `${String(command?.instanceId)}:${String(command?.sequence)}`;
153
+ if (!command || typeof command.commandId !== "string" || !/^ccmd_[0-9a-f]{32}$/.test(command.commandId) || typeof command.instanceId !== "string" || typeof command.sessionId !== "string" || !/^csess_[0-9a-f]{32}$/.test(command.sessionId) || typeof command.appId !== "string" || command.env !== "dev" && command.env !== "prod" || command.hostId !== host.hostId || !binding || binding.appId !== command.appId || binding.generation !== command.bindingGeneration || !Number.isSafeInteger(command.sequence) || Number(command.sequence) < 1 || commandIds.has(command.commandId) || commandSequences.has(sequenceKey) || !["start", "prompt", "checkpoint_stop", "resume"].includes(String(command.kind)) || !record(command.payload) || !Number.isSafeInteger(command.createdAt)) throw invalid("command");
154
+ commandIds.add(command.commandId);
155
+ commandSequences.add(sequenceKey);
156
+ return command;
157
+ });
158
+ return { host, bindings, commands };
159
+ }
160
+ async function parseSource(value) {
161
+ const snapshot = record(record(value)?.snapshot);
162
+ if (!snapshot || typeof snapshot.repository !== "string" || typeof snapshot.commitSha !== "string" || typeof snapshot.treeDigest !== "string" || !Array.isArray(snapshot.files)) throw invalid("source");
163
+ const files = snapshot.files.map((value2) => {
164
+ const file = record(value2);
165
+ if (!file || typeof file.path !== "string" || typeof file.content !== "string") throw invalid("source file");
166
+ return { path: file.path, content: file.content };
167
+ });
168
+ const referencesValue = snapshot.references === void 0 ? [] : snapshot.references;
169
+ if (!Array.isArray(referencesValue) || referencesValue.length > 5) throw invalid("reference sources");
170
+ const aliases = /* @__PURE__ */ new Set();
171
+ const references = [];
172
+ for (const item of referencesValue) {
173
+ const reference = record(item);
174
+ if (!reference || typeof reference.alias !== "string" || !/^[a-z][a-z0-9-]{0,39}$/.test(reference.alias) || aliases.has(reference.alias) || reference.alias === "primary" || typeof reference.repository !== "string" || typeof reference.commitSha !== "string" || typeof reference.treeDigest !== "string" || !Array.isArray(reference.files)) throw invalid("reference source");
175
+ aliases.add(reference.alias);
176
+ const referenceFiles = reference.files.map((entry) => {
177
+ const file = record(entry);
178
+ if (!file || typeof file.path !== "string" || typeof file.content !== "string") throw invalid("reference source file");
179
+ return { path: file.path, content: file.content };
180
+ });
181
+ const source2 = { repository: reference.repository, commitSha: reference.commitSha, files: referenceFiles };
182
+ const referenceDigest = await (0, import_code.digestCodeRepositorySnapshot)(source2, { maximumFiles: 1e4, maximumBytes: 16 * 1024 * 1024 });
183
+ if (referenceDigest !== reference.treeDigest) throw invalid("reference source digest");
184
+ references.push({ alias: reference.alias, ...source2, treeDigest: referenceDigest });
185
+ }
186
+ const source = { repository: snapshot.repository, commitSha: snapshot.commitSha, files };
187
+ const digest = await (0, import_code.digestCodeRepositorySnapshot)(source, { maximumFiles: 1e4, maximumBytes: 16 * 1024 * 1024 });
188
+ if (digest !== snapshot.treeDigest) throw invalid("source digest");
189
+ return { ...source, treeDigest: digest, ...references.length ? { references } : {} };
190
+ }
191
+ function parseReview(value) {
192
+ const review = record(record(value)?.review);
193
+ if (!review || !["approved", "rejected"].includes(String(review.verdict)) || typeof review.reviewDigest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(review.reviewDigest) || typeof review.provider !== "string" || !review.provider || typeof review.model !== "string" || !review.model || !Number.isSafeInteger(review.policyVersion) || Number(review.policyVersion) < 1) throw invalid("review");
194
+ return review;
195
+ }
196
+ function parseCandidate(value) {
197
+ const candidate = record(record(value)?.candidate);
198
+ if (!candidate || typeof candidate.candidateId !== "string" || !/^ccand_[0-9a-f]{32}$/.test(candidate.candidateId) || !["submitted", "approved", "published", "failed"].includes(String(candidate.status))) {
199
+ throw invalid("candidate");
200
+ }
201
+ return { candidateId: candidate.candidateId, status: candidate.status };
202
+ }
203
+ var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
204
+ var invalid = (part) => new CodeRuntimeControlError(`invalid Code runtime ${part} response`, 502, "invalid_response");
205
+
206
+ // src/code-runtime.ts
207
+ var CODE_RUNTIME_PROTOCOL_VERSION = 1;
208
+ async function runCodeRuntimeHeartbeatLoop(options) {
209
+ const heartbeatMs = options.heartbeatMs ?? 15e3;
210
+ if (!Number.isSafeInteger(heartbeatMs) || heartbeatMs < 1e3 || heartbeatMs > 3e5) {
211
+ throw new TypeError("heartbeatMs must be an integer from 1000 to 300000");
212
+ }
213
+ let retryMs = 1e3;
214
+ do {
215
+ if (options.signal?.aborted) return;
216
+ try {
217
+ const snapshot = await options.control.heartbeat(options.runtimeVersion, options.capabilities);
218
+ await options.onSnapshot?.(snapshot);
219
+ retryMs = 1e3;
220
+ if (options.once) return;
221
+ await wait(heartbeatMs, options.signal);
222
+ } catch (error) {
223
+ if (options.signal?.aborted) return;
224
+ if (options.once || !retryableControlFailure(error)) throw error;
225
+ await options.onRetry?.(error, retryMs);
226
+ await wait(retryMs, options.signal);
227
+ retryMs = Math.min(retryMs * 2, 3e4);
228
+ }
229
+ } while (!options.signal?.aborted);
230
+ }
231
+ var CodeRuntimeReconciler = class {
232
+ constructor(control, engine) {
233
+ this.control = control;
234
+ this.engine = engine;
235
+ }
236
+ control;
237
+ engine;
238
+ results = /* @__PURE__ */ new Map();
239
+ async reconcile(snapshot) {
240
+ for (const command of snapshot.commands) {
241
+ let completed = this.results.get(command.commandId);
242
+ if (!completed) {
243
+ let result;
244
+ try {
245
+ result = await this.engine.execute(command);
246
+ } catch (error) {
247
+ result = { status: "failed", message: (error instanceof Error ? error.message : String(error)).slice(0, 2e3) };
248
+ }
249
+ completed = { result, notified: false };
250
+ this.results.set(command.commandId, completed);
251
+ if (this.results.size > 1024) this.results.delete(this.results.keys().next().value);
252
+ }
253
+ await this.control.acknowledge(command.commandId, completed.result);
254
+ if (!completed.notified) {
255
+ await this.engine.acknowledged?.(command, completed.result);
256
+ completed.notified = true;
257
+ }
258
+ }
259
+ }
260
+ };
261
+ function retryableControlFailure(value) {
262
+ if (!value || typeof value !== "object") return false;
263
+ const failure = value;
264
+ if (failure.code === "invalid_response" || typeof failure.status !== "number") return false;
265
+ return failure.status === 408 || failure.status === 425 || failure.status === 429 || failure.status >= 500;
266
+ }
267
+ function wait(ms, signal) {
268
+ return new Promise((resolve6) => {
269
+ if (signal?.aborted) return resolve6();
270
+ const timer = setTimeout(resolve6, ms);
271
+ signal?.addEventListener("abort", () => {
272
+ clearTimeout(timer);
273
+ resolve6();
274
+ }, { once: true });
275
+ });
276
+ }
277
+
278
+ // src/code-checkpoint.ts
279
+ var import_code2 = require("@odla-ai/camel/code");
280
+
281
+ // src/code-patch.ts
282
+ var import_node_child_process = require("child_process");
283
+ var import_promises = require("fs/promises");
284
+ var import_node_path = require("path");
285
+ var RESERVED = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_modules", "dist", "coverage"]);
286
+ var SECRET = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
287
+ var PATH = /^[A-Za-z0-9_@+.,-]+(?:\/[A-Za-z0-9_@+.,-]+)*$/;
288
+ var FORBIDDEN = /^(?:GIT binary patch|Binary files |rename (?:from|to) |copy (?:from|to) |similarity index |old mode |new mode |deleted file mode 160000|new file mode 160000)/m;
289
+ function validateCodePatch(patch2, maxBytes) {
290
+ if (!patch2 || Buffer.byteLength(patch2) > maxBytes || patch2.includes("\0") || patch2.includes("\r")) {
291
+ throw new TypeError("patch is empty, malformed, or exceeds its byte limit");
292
+ }
293
+ if (FORBIDDEN.test(patch2) || /(?:old|new)(?: file)? mode 120000/.test(patch2)) {
294
+ throw new TypeError("patch uses a forbidden binary, link, mode, rename, or copy operation");
295
+ }
296
+ const paths = [];
297
+ const lines = patch2.split("\n");
298
+ for (let index = 0; index < lines.length; index += 1) {
299
+ const line = lines[index];
300
+ if (!line.startsWith("diff --git ")) continue;
301
+ const match = /^diff --git a\/(\S+) b\/(\S+)$/.exec(line);
302
+ const path = match?.[1];
303
+ if (!path || !match?.[2] || path !== match[2]) throw new TypeError("patch must use one unquoted relative path per diff");
304
+ validateRelativePath(path);
305
+ const header = lines.slice(index + 1).findIndex((candidate) => candidate.startsWith("diff --git "));
306
+ const section = lines.slice(index + 1, header < 0 ? lines.length : index + 1 + header);
307
+ const oldPath = section.find((candidate) => candidate.startsWith("--- "))?.slice(4);
308
+ const newPath = section.find((candidate) => candidate.startsWith("+++ "))?.slice(4);
309
+ if (!validHeaderPath(oldPath, path, "a") || !validHeaderPath(newPath, path, "b")) {
310
+ throw new TypeError("patch file headers do not match the declared path");
311
+ }
312
+ paths.push(path);
313
+ }
314
+ if (!paths.length || new Set(paths).size !== paths.length) throw new TypeError("patch has no diffs or repeats a path");
315
+ return paths;
316
+ }
317
+ function validHeaderPath(value, path, prefix) {
318
+ return value === "/dev/null" || value === `${prefix}/${path}`;
319
+ }
320
+ function validateRelativePath(path) {
321
+ const parts = path.split("/");
322
+ if (!PATH.test(path) || parts.some((part) => part === "." || part === ".." || RESERVED.has(part)) || parts.some((part) => SECRET.test(part))) {
323
+ throw new TypeError("path is outside the allowed staged source tree");
324
+ }
325
+ }
326
+ function resolveCodePath(workspaceDir, path) {
327
+ validateRelativePath(path);
328
+ const root = (0, import_node_path.resolve)(workspaceDir);
329
+ const target = (0, import_node_path.resolve)(root, path);
330
+ if (target !== root && !target.startsWith(`${root}${import_node_path.sep}`)) throw new TypeError("path escapes the staged workspace");
331
+ return target;
332
+ }
333
+ async function applyCodePatch(workspaceDir, patch2, paths) {
334
+ await gitApply(workspaceDir, patch2, true);
335
+ await gitApply(workspaceDir, patch2, false);
336
+ for (const path of paths) {
337
+ try {
338
+ const info = await (0, import_promises.lstat)(resolveCodePath(workspaceDir, path));
339
+ if (info.isSymbolicLink() || !info.isFile() && !info.isDirectory()) {
340
+ throw new TypeError("patch created a non-regular workspace entry");
341
+ }
342
+ } catch (reason) {
343
+ if (reason.code !== "ENOENT") throw reason;
344
+ }
345
+ }
346
+ }
347
+ function gitApply(cwd, patch2, check) {
348
+ return new Promise((accept, reject) => {
349
+ const args = ["apply", "--recount", "--whitespace=nowarn", ...check ? ["--check"] : [], "-"];
350
+ const child = (0, import_node_child_process.spawn)("git", args, {
351
+ cwd,
352
+ shell: false,
353
+ stdio: ["pipe", "ignore", "pipe"],
354
+ env: { PATH: process.env.PATH ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null" }
355
+ });
356
+ let stderr = "";
357
+ child.stderr.setEncoding("utf8");
358
+ child.stderr.on("data", (text) => {
359
+ if (stderr.length < 4e3) stderr += text.slice(0, 4e3);
360
+ });
361
+ child.once("error", reject);
362
+ child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(`patch did not apply: ${stderr.trim().slice(0, 500)}`)));
363
+ child.stdin.end(patch2);
364
+ });
365
+ }
366
+
367
+ // src/workspace.ts
368
+ var import_promises2 = require("fs/promises");
369
+ var import_node_os = require("os");
370
+ var import_node_path3 = require("path");
371
+ var import_node_child_process2 = require("child_process");
372
+
373
+ // src/workspace-policy.ts
374
+ var import_node_path2 = require("path");
375
+ var SKIP_WORKSPACE_DIRS = /* @__PURE__ */ new Set([
376
+ ".git",
377
+ ".odla",
378
+ ".wrangler",
379
+ "node_modules",
380
+ "dist",
381
+ "coverage"
382
+ ]);
383
+ var SECRET_WORKSPACE_FILE = /^(?:\.env(?:\..+)?|\.dev\.vars|\.dev-token(?:\..+)?|credentials(?:\..+)?\.json|dev-token(?:\..+)?(?:\.json)?)$/i;
384
+ function allowedWorkspacePath(relativePath) {
385
+ const parts = relativePath.split("/");
386
+ return !(0, import_node_path2.isAbsolute)(relativePath) && !relativePath.includes("\\") && !relativePath.includes("\0") && !parts.some((part) => !part || part === "." || part === ".." || SKIP_WORKSPACE_DIRS.has(part)) && !SECRET_WORKSPACE_FILE.test(parts.at(-1) ?? "");
387
+ }
388
+
389
+ // src/workspace.ts
390
+ async function sourceFiles(sourceDir, maxFiles, maxBytes) {
391
+ const files = [];
392
+ let bytes = 0;
393
+ const walk = async (dir) => {
394
+ for (const entry of await (0, import_promises2.readdir)(dir, { withFileTypes: true })) {
395
+ if (entry.isDirectory() && SKIP_WORKSPACE_DIRS.has(entry.name)) continue;
396
+ if (!entry.isDirectory() && SECRET_WORKSPACE_FILE.test(entry.name)) continue;
397
+ const path = (0, import_node_path3.join)(dir, entry.name);
398
+ if (entry.isSymbolicLink()) continue;
399
+ if (entry.isDirectory()) {
400
+ await walk(path);
401
+ continue;
402
+ }
403
+ if (!entry.isFile()) continue;
404
+ const metadata = await (0, import_promises2.stat)(path);
405
+ bytes += metadata.size;
406
+ if (files.length + 1 > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
407
+ if (bytes > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
408
+ files.push({
409
+ source: path,
410
+ relativePath: (0, import_node_path3.relative)(sourceDir, path),
411
+ mode: metadata.mode & 511,
412
+ bytes: metadata.size
413
+ });
414
+ }
415
+ };
416
+ await walk(sourceDir);
417
+ return files.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
418
+ }
419
+ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
420
+ const child = (0, import_node_child_process2.spawn)("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
421
+ cwd: sourceDir,
422
+ stdio: ["ignore", "pipe", "pipe"],
423
+ shell: false
424
+ });
425
+ const stdout = [];
426
+ const stderr = [];
427
+ let outputBytes = 0;
428
+ child.stdout.on("data", (chunk) => {
429
+ outputBytes += chunk.byteLength;
430
+ if (outputBytes > 8 * 1024 * 1024) child.kill("SIGKILL");
431
+ else stdout.push(chunk);
432
+ });
433
+ child.stderr.on("data", (chunk) => {
434
+ if (stderr.reduce((sum, value) => sum + value.byteLength, 0) < 16384) stderr.push(chunk);
435
+ });
436
+ const code = await new Promise((accept, reject) => {
437
+ child.once("error", reject);
438
+ child.once("exit", accept);
439
+ });
440
+ if (outputBytes > 8 * 1024 * 1024) throw new Error("git file inventory exceeds 8 MiB");
441
+ if (code !== 0) throw new Error(`git file inventory failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
442
+ const paths = Buffer.concat(stdout).toString("utf8").split("\0").filter(Boolean).sort();
443
+ if (paths.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
444
+ const root = (0, import_node_path3.resolve)(sourceDir);
445
+ const files = [];
446
+ let bytes = 0;
447
+ for (const relativePath of paths) {
448
+ if (!allowedWorkspacePath(relativePath)) continue;
449
+ const source = (0, import_node_path3.resolve)(root, relativePath);
450
+ if (!source.startsWith(`${root}${import_node_path3.sep}`)) throw new TypeError("git file path escapes workspace");
451
+ let metadata;
452
+ try {
453
+ metadata = await (0, import_promises2.lstat)(source);
454
+ } catch (error) {
455
+ if (error.code === "ENOENT") continue;
456
+ throw error;
457
+ }
458
+ if (metadata.isSymbolicLink() || !metadata.isFile()) continue;
459
+ bytes += metadata.size;
460
+ if (bytes > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
461
+ files.push({ source, relativePath, mode: metadata.mode & 511, bytes: metadata.size });
462
+ }
463
+ return files;
464
+ }
465
+ async function copyTree(files, destination) {
466
+ for (const file of files) {
467
+ const target = (0, import_node_path3.join)(destination, file.relativePath);
468
+ await (0, import_promises2.mkdir)((0, import_node_path3.resolve)(target, ".."), { recursive: true });
469
+ await (0, import_promises2.copyFile)(file.source, target);
470
+ await (0, import_promises2.chmod)(target, file.mode);
471
+ }
472
+ }
473
+ async function captureGitDiff(root, maxBytes) {
474
+ const child = (0, import_node_child_process2.spawn)("git", [
475
+ "diff",
476
+ "--no-index",
477
+ "--binary",
478
+ "--no-ext-diff",
479
+ "--src-prefix=a/",
480
+ "--dst-prefix=b/",
481
+ "--",
482
+ "baseline",
483
+ "workspace"
484
+ ], { cwd: root, stdio: ["ignore", "pipe", "pipe"], shell: false });
485
+ const stdout = [];
486
+ const stderr = [];
487
+ let bytes = 0;
488
+ child.stdout.on("data", (chunk) => {
489
+ bytes += chunk.byteLength;
490
+ if (bytes > maxBytes) child.kill("SIGKILL");
491
+ else stdout.push(chunk);
492
+ });
493
+ child.stderr.on("data", (chunk) => {
494
+ if (stderr.reduce((sum, value) => sum + value.byteLength, 0) < 16384) stderr.push(chunk);
495
+ });
496
+ const code = await new Promise((accept, reject) => {
497
+ child.once("error", reject);
498
+ child.once("exit", accept);
499
+ });
500
+ if (bytes > maxBytes) throw new Error(`patch exceeds ${maxBytes} bytes`);
501
+ if (code !== 0 && code !== 1) {
502
+ throw new Error(`git diff failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
503
+ }
504
+ return Buffer.concat(stdout).toString("utf8").replaceAll("a/baseline/", "a/").replaceAll("a/workspace/", "a/").replaceAll("b/baseline/", "b/").replaceAll("b/workspace/", "b/").replaceAll("--- a/baseline", "--- a").replaceAll("+++ b/workspace", "+++ b");
505
+ }
506
+ async function stageWorkspace(source, options = {}) {
507
+ const sourceDir = await (0, import_promises2.realpath)((0, import_node_path3.resolve)(source));
508
+ const sourceStat = await (0, import_promises2.stat)(sourceDir);
509
+ if (!sourceStat.isDirectory()) throw new TypeError("workspace source must be a directory");
510
+ const root = await (0, import_promises2.mkdtemp)((0, import_node_path3.join)(options.tempRoot ?? (0, import_node_os.tmpdir)(), "odla-harness-"));
511
+ const baselineDir = (0, import_node_path3.join)(root, "baseline");
512
+ const workspaceDir = (0, import_node_path3.join)(root, "workspace");
513
+ await Promise.all([(0, import_promises2.mkdir)(baselineDir), (0, import_promises2.mkdir)(workspaceDir)]);
514
+ try {
515
+ const maxFiles = options.maxFiles ?? 2e4;
516
+ const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
517
+ const files = options.gitTrackedAndUnignored ? await gitSourceFiles(sourceDir, maxFiles, maxBytes) : await sourceFiles(sourceDir, maxFiles, maxBytes);
518
+ await Promise.all([copyTree(files, baselineDir), copyTree(files, workspaceDir)]);
519
+ return {
520
+ root,
521
+ baselineDir,
522
+ workspaceDir,
523
+ fileCount: files.length,
524
+ byteCount: files.reduce((sum, file) => sum + file.bytes, 0),
525
+ patch: (maxBytes2) => captureGitDiff(root, maxBytes2),
526
+ cleanup: () => (0, import_promises2.rm)(root, { recursive: true, force: true })
527
+ };
528
+ } catch (error) {
529
+ await (0, import_promises2.rm)(root, { recursive: true, force: true });
530
+ throw error;
531
+ }
532
+ }
533
+ async function stageWorkspacePair(baselineSource, workspaceSource, options = {}) {
534
+ const baselineDirSource = await (0, import_promises2.realpath)((0, import_node_path3.resolve)(baselineSource));
535
+ const workspaceDirSource = await (0, import_promises2.realpath)((0, import_node_path3.resolve)(workspaceSource));
536
+ const maxFiles = options.maxFiles ?? 2e4;
537
+ const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
538
+ const [baselineFiles, workspaceFiles] = await Promise.all([
539
+ sourceFiles(baselineDirSource, maxFiles, maxBytes),
540
+ sourceFiles(workspaceDirSource, maxFiles, maxBytes)
541
+ ]);
542
+ const root = await (0, import_promises2.mkdtemp)((0, import_node_path3.join)(options.tempRoot ?? (0, import_node_os.tmpdir)(), "odla-harness-"));
543
+ const baselineDir = (0, import_node_path3.join)(root, "baseline");
544
+ const workspaceDir = (0, import_node_path3.join)(root, "workspace");
545
+ await Promise.all([(0, import_promises2.mkdir)(baselineDir), (0, import_promises2.mkdir)(workspaceDir)]);
546
+ try {
547
+ await Promise.all([copyTree(baselineFiles, baselineDir), copyTree(workspaceFiles, workspaceDir)]);
548
+ return {
549
+ root,
550
+ baselineDir,
551
+ workspaceDir,
552
+ fileCount: workspaceFiles.length,
553
+ byteCount: workspaceFiles.reduce((sum, file) => sum + file.bytes, 0),
554
+ patch: (maxPatchBytes) => captureGitDiff(root, maxPatchBytes),
555
+ cleanup: () => (0, import_promises2.rm)(root, { recursive: true, force: true })
556
+ };
557
+ } catch (error) {
558
+ await (0, import_promises2.rm)(root, { recursive: true, force: true });
559
+ throw error;
560
+ }
561
+ }
562
+
563
+ // src/code-checkpoint.ts
564
+ async function createCodeWorkspaceCheckpoint(input) {
565
+ const maximum = input.maximumPatchBytes ?? 256 * 1024;
566
+ if (!Number.isSafeInteger(maximum) || maximum < 1 || maximum > 256 * 1024) {
567
+ throw new TypeError("checkpoint patch bound must be from 1 to 262144 bytes");
568
+ }
569
+ const patch2 = await input.workspace.patch(maximum);
570
+ if (patch2) validateCodePatch(patch2, maximum);
571
+ return (0, import_code2.createCodePortableCheckpoint)({ baseCommitSha: input.baseCommitSha, patch: patch2, state: input.state });
572
+ }
573
+ async function restoreCodeWorkspaceCheckpoint(input) {
574
+ const checkpoint = await (0, import_code2.verifyCodePortableCheckpoint)(input.checkpoint);
575
+ if (checkpoint.baseCommitSha !== input.trustedBaseCommitSha) {
576
+ throw new TypeError("checkpoint trusted base does not match the fetched commit");
577
+ }
578
+ const workspace = await stageWorkspace(input.trustedBaseDir, input.stage);
579
+ try {
580
+ if (checkpoint.patch) {
581
+ const paths = validateCodePatch(checkpoint.patch, 256 * 1024);
582
+ await applyCodePatch(workspace.workspaceDir, checkpoint.patch, paths);
583
+ }
584
+ return { workspace, checkpoint };
585
+ } catch (error) {
586
+ await workspace.cleanup();
587
+ throw error;
588
+ }
589
+ }
590
+
591
+ // src/code-verifier.ts
592
+ var import_node_crypto3 = require("crypto");
593
+ var import_node_fs2 = require("fs");
594
+ var import_promises5 = require("fs/promises");
595
+ var import_node_path6 = require("path");
596
+ var import_code3 = require("@odla-ai/camel/code");
597
+
598
+ // src/recipe-container.ts
599
+ var import_node_child_process4 = require("child_process");
600
+ var import_node_process2 = require("process");
601
+ var import_node_crypto = require("crypto");
602
+
603
+ // src/container.ts
604
+ var import_node_child_process3 = require("child_process");
605
+ var import_node_fs = require("fs");
606
+ var import_promises3 = require("fs/promises");
607
+ var import_node_path4 = require("path");
608
+ var import_node_process = require("process");
609
+
610
+ // src/types.ts
611
+ var HARNESS_PROTOCOL_VERSION = 1;
612
+
613
+ // src/protocol.ts
614
+ var CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/;
615
+ var HarnessProtocolError = class extends Error {
616
+ name = "HarnessProtocolError";
617
+ };
618
+ function record2(value) {
619
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
620
+ }
621
+ function boundedText(value, label, max) {
622
+ if (typeof value !== "string" || !value || value.length > max || CONTROL.test(value)) {
623
+ throw new HarnessProtocolError(`${label} must be a non-empty string of at most ${max} characters`);
624
+ }
625
+ return value;
626
+ }
627
+ function parseAgentOutput(line) {
628
+ if (Buffer.byteLength(line, "utf8") > 1e6) throw new HarnessProtocolError("agent message exceeds 1 MB");
629
+ let value;
630
+ try {
631
+ value = JSON.parse(line);
632
+ } catch {
633
+ throw new HarnessProtocolError("agent emitted invalid JSON");
634
+ }
635
+ const message2 = record2(value);
636
+ if (!message2 || message2.protocolVersion !== HARNESS_PROTOCOL_VERSION) {
637
+ throw new HarnessProtocolError(`agent protocolVersion must be ${HARNESS_PROTOCOL_VERSION}`);
638
+ }
639
+ if (message2.type === "event") {
640
+ return {
641
+ protocolVersion: HARNESS_PROTOCOL_VERSION,
642
+ type: "event",
643
+ kind: boundedText(message2.kind, "event.kind", 120),
644
+ ...message2.payload === void 0 ? {} : { payload: message2.payload }
645
+ };
646
+ }
647
+ if (message2.type === "inference.request") {
648
+ const call = record2(message2.call);
649
+ if (!call || !Array.isArray(call.messages) || !Number.isSafeInteger(call.maxTokens)) {
650
+ throw new HarnessProtocolError("inference.request.call requires messages and maxTokens");
651
+ }
652
+ return {
653
+ protocolVersion: HARNESS_PROTOCOL_VERSION,
654
+ type: "inference.request",
655
+ requestId: boundedText(message2.requestId, "requestId", 180),
656
+ call
657
+ };
658
+ }
659
+ if (message2.type === "tool.request") {
660
+ const input = record2(message2.input);
661
+ const tool = String(message2.tool);
662
+ if (!input || !["sandbox.read", "sandbox.apply_patch", "sandbox.run_recipe"].includes(tool)) {
663
+ throw new HarnessProtocolError("tool.request requires a registered tool and object input");
664
+ }
665
+ return {
666
+ protocolVersion: HARNESS_PROTOCOL_VERSION,
667
+ type: "tool.request",
668
+ requestId: boundedText(message2.requestId, "requestId", 180),
669
+ tool,
670
+ input
671
+ };
672
+ }
673
+ if (message2.type === "attempt.complete") {
674
+ if (!(/* @__PURE__ */ new Set(["completed", "failed", "cancelled"])).has(String(message2.status))) {
675
+ throw new HarnessProtocolError("attempt.complete.status is invalid");
676
+ }
677
+ return {
678
+ protocolVersion: HARNESS_PROTOCOL_VERSION,
679
+ type: "attempt.complete",
680
+ status: message2.status,
681
+ ...message2.result === void 0 ? {} : { result: message2.result }
682
+ };
683
+ }
684
+ throw new HarnessProtocolError("agent message type is unsupported");
685
+ }
686
+ function encodeAgentInput(message2) {
687
+ return `${JSON.stringify(message2)}
688
+ `;
689
+ }
690
+
691
+ // src/container.ts
692
+ var DIGEST_IMAGE = /^[a-z0-9][a-z0-9._/-]*(?::[a-zA-Z0-9._-]+)?@sha256:[0-9a-f]{64}$/;
693
+ function assertPinnedImage(image) {
694
+ if (!DIGEST_IMAGE.test(image)) throw new TypeError("container image must be pinned by sha256 digest");
695
+ }
696
+ async function commandAvailable(engine) {
697
+ for (const directory of (process.env.PATH ?? "").split(import_node_path4.delimiter).filter(Boolean)) {
698
+ try {
699
+ await (0, import_promises3.access)((0, import_node_path4.join)(directory, engine), import_node_fs.constants.X_OK);
700
+ return true;
701
+ } catch {
702
+ }
703
+ }
704
+ return false;
705
+ }
706
+ async function selectContainerEngine(requested = "auto", options = {}) {
707
+ const platform = options.platform ?? process.platform;
708
+ const arch = options.arch ?? process.arch;
709
+ const uid = options.uid ?? (typeof import_node_process.getuid === "function" ? (0, import_node_process.getuid)() : 1e3);
710
+ const available = options.available ?? commandAvailable;
711
+ const validate2 = async (engine) => {
712
+ if (engine === "container" && (platform !== "darwin" || arch !== "arm64")) {
713
+ throw new TypeError("Apple container requires Apple Silicon macOS");
714
+ }
715
+ if (engine === "podman" && platform === "linux" && uid === 0) {
716
+ throw new TypeError("the Linux harness requires rootless Podman; do not run the runner as root");
717
+ }
718
+ if (!await available(engine)) throw new TypeError(`${engine} is not installed or executable`);
719
+ return engine;
720
+ };
721
+ if (requested !== "auto") return validate2(requested);
722
+ const candidates = platform === "darwin" ? arch === "arm64" ? ["container", "podman"] : ["podman"] : platform === "linux" ? ["podman"] : [];
723
+ for (const engine of candidates) {
724
+ if (await available(engine)) return validate2(engine);
725
+ }
726
+ if (platform === "linux") {
727
+ throw new TypeError("no rootless Podman found; install Podman or explicitly choose --engine docker after reviewing its daemon boundary");
728
+ }
729
+ if (platform === "darwin") {
730
+ throw new TypeError("Apple container is not installed; on Apple Silicon macOS 26 run `brew install container`, then retry (Podman Machine is the fallback)");
731
+ }
732
+ throw new TypeError("no supported container engine found");
733
+ }
734
+ function inspectRootlessPodman() {
735
+ return new Promise((resolve6, reject) => {
736
+ (0, import_node_child_process3.execFile)(
737
+ "podman",
738
+ ["info", "--format", "{{.Host.Security.Rootless}}"],
739
+ { encoding: "utf8", maxBuffer: 16 * 1024, timeout: 1e4 },
740
+ (error, stdout) => {
741
+ if (error) {
742
+ reject(new TypeError("could not verify that the active Podman service is rootless"));
743
+ return;
744
+ }
745
+ resolve6(stdout.trim() === "true");
746
+ }
747
+ );
748
+ });
749
+ }
750
+ async function verifyContainerEngineBoundary(engine, options = {}) {
751
+ const platform = options.platform ?? process.platform;
752
+ const arch = options.arch ?? process.arch;
753
+ const uid = options.uid ?? (typeof import_node_process.getuid === "function" ? (0, import_node_process.getuid)() : 1e3);
754
+ if (engine === "container" && (platform !== "darwin" || arch !== "arm64")) {
755
+ throw new TypeError("Apple container requires Apple Silicon macOS");
756
+ }
757
+ if (engine !== "podman" || platform !== "linux") return;
758
+ if (uid === 0) throw new TypeError("the Linux harness requires rootless Podman; do not run the runner as root");
759
+ const rootless = await (options.podmanRootless ?? inspectRootlessPodman)();
760
+ if (!rootless) throw new TypeError("the active Podman service is not rootless; refusing to run the harness");
761
+ }
762
+ function buildContainerRunArgs(options) {
763
+ if (!options.allowUnpinnedImage) assertPinnedImage(options.image);
764
+ if (/[,\r\n]/.test(options.workspaceDir)) throw new TypeError("workspace path contains unsupported mount characters");
765
+ const uid = typeof import_node_process.getuid === "function" ? (0, import_node_process.getuid)() : 1e3;
766
+ const gid = typeof import_node_process.getgid === "function" ? (0, import_node_process.getgid)() : 1e3;
767
+ const safeAttempt = options.task.attemptId.toLowerCase().replace(/[^a-z0-9_.-]/g, "-").slice(0, 40);
768
+ const name = `odla-harness-${safeAttempt}-${crypto.randomUUID().slice(0, 8)}`;
769
+ const limits = options.limits ?? {};
770
+ const access2 = options.workspaceAccess ?? "read-write";
771
+ const appleMount = access2 === "none" ? [] : [`--mount=type=bind,source=${options.workspaceDir},target=/workspace${access2 === "read-only" ? ",readonly" : ""}`];
772
+ const ociMount = access2 === "none" ? [] : [`--mount=type=bind,src=${options.workspaceDir},dst=/workspace${access2 === "read-only" ? ",readonly" : ""}`];
773
+ if (options.engine === "container") {
774
+ return [
775
+ "run",
776
+ "--rm",
777
+ "--interactive",
778
+ `--name=${name}`,
779
+ "--network=none",
780
+ "--read-only",
781
+ "--cap-drop=ALL",
782
+ `--memory=${limits.memory ?? "1g"}`,
783
+ `--cpus=${limits.cpus ?? 1}`,
784
+ `--user=${uid}:${gid}`,
785
+ "--tmpfs=/tmp",
786
+ ...appleMount,
787
+ "--workdir=/workspace",
788
+ `--env=ODLA_HARNESS_PROTOCOL=${HARNESS_PROTOCOL_VERSION}`,
789
+ `--label=ai.odla.harness.attempt=${options.task.attemptId}`,
790
+ options.image
791
+ ];
792
+ }
793
+ return [
794
+ "run",
795
+ "--rm",
796
+ "--interactive",
797
+ `--name=${name}`,
798
+ "--pull=never",
799
+ "--network=none",
800
+ "--read-only",
801
+ "--cap-drop=ALL",
802
+ "--security-opt=no-new-privileges",
803
+ `--pids-limit=${limits.pids ?? 256}`,
804
+ `--memory=${limits.memory ?? "1g"}`,
805
+ `--cpus=${limits.cpus ?? 1}`,
806
+ `--user=${uid}:${gid}`,
807
+ `--tmpfs=/tmp:rw,noexec,nosuid,nodev,size=${limits.tmpfsBytes ?? 64 * 1024 * 1024}`,
808
+ ...ociMount,
809
+ "--workdir=/workspace",
810
+ `--env=ODLA_HARNESS_PROTOCOL=${HARNESS_PROTOCOL_VERSION}`,
811
+ `--label=ai.odla.harness.attempt=${options.task.attemptId}`,
812
+ options.image
813
+ ];
814
+ }
815
+ function containerName(args) {
816
+ return args.find((arg) => arg.startsWith("--name=")).slice("--name=".length);
817
+ }
818
+ async function runContainerAttempt(options) {
819
+ if (options.signal?.aborted) return { exitCode: 1, status: "cancelled", stderr: "" };
820
+ await verifyContainerEngineBoundary(options.engine);
821
+ const args = buildContainerRunArgs(options);
822
+ const name = containerName(args);
823
+ const child = (0, import_node_child_process3.spawn)(options.engine, args, { stdio: ["pipe", "pipe", "pipe"], shell: false });
824
+ let stderr = "";
825
+ let outputBytes = 0;
826
+ let complete = null;
827
+ let stopped = false;
828
+ let exited = false;
829
+ child.stderr.setEncoding("utf8");
830
+ child.stderr.on("data", (text) => {
831
+ if (stderr.length < 64 * 1024) stderr += text.slice(0, 64 * 1024 - stderr.length);
832
+ });
833
+ const stop = (reason) => {
834
+ if (stopped || exited) return;
835
+ stopped = true;
836
+ if (!child.stdin.destroyed) {
837
+ const cancel = { protocolVersion: HARNESS_PROTOCOL_VERSION, type: "attempt.cancel", reason };
838
+ child.stdin.write(encodeAgentInput(cancel));
839
+ }
840
+ const removeArgs = options.engine === "container" ? ["delete", "--force", name] : ["rm", "-f", name];
841
+ const killer = (0, import_node_child_process3.spawn)(options.engine, removeArgs, { stdio: "ignore", shell: false });
842
+ killer.unref();
843
+ };
844
+ const abort = () => stop("runner_cancelled");
845
+ options.signal?.addEventListener("abort", abort, { once: true });
846
+ const timeout = setTimeout(() => stop("timeout"), options.task.policy.timeoutMs);
847
+ const start = { protocolVersion: HARNESS_PROTOCOL_VERSION, type: "task.start", task: options.task };
848
+ if (!stopped && !options.signal?.aborted) child.stdin.write(encodeAgentInput(start));
849
+ const consume = (async () => {
850
+ let pending = Buffer.alloc(0);
851
+ const handleLine = async (raw) => {
852
+ const bytes = raw.at(-1) === 13 ? raw.subarray(0, -1) : raw;
853
+ if (bytes.byteLength > 1e6) throw new Error("agent message exceeds 1 MB");
854
+ const line = bytes.toString("utf8");
855
+ if (!line.trim()) return;
856
+ const message2 = parseAgentOutput(line);
857
+ if (message2.type === "attempt.complete") complete = message2;
858
+ const response2 = await options.onMessage(message2);
859
+ if (response2 && !child.stdin.destroyed) child.stdin.write(encodeAgentInput(response2));
860
+ };
861
+ try {
862
+ for await (const raw of child.stdout) {
863
+ const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
864
+ outputBytes += chunk.byteLength;
865
+ if (outputBytes > options.task.policy.maxOutputBytes) {
866
+ throw new Error(`agent output exceeds ${options.task.policy.maxOutputBytes} bytes`);
867
+ }
868
+ pending = Buffer.concat([pending, chunk]);
869
+ let newline = pending.indexOf(10);
870
+ while (newline >= 0) {
871
+ await handleLine(pending.subarray(0, newline));
872
+ pending = pending.subarray(newline + 1);
873
+ newline = pending.indexOf(10);
874
+ }
875
+ if (pending.byteLength > 1e6) throw new Error("agent message exceeds 1 MB");
876
+ }
877
+ if (pending.byteLength) await handleLine(pending);
878
+ } catch (error) {
879
+ stop("protocol_error");
880
+ throw error;
881
+ }
882
+ })();
883
+ const exit = new Promise((accept, reject) => {
884
+ child.once("error", reject);
885
+ child.once("exit", (code) => {
886
+ exited = true;
887
+ accept(code ?? 1);
888
+ });
889
+ });
890
+ try {
891
+ const [exitCode] = await Promise.all([exit, consume]);
892
+ if (stderr && options.onStderr) await options.onStderr(stderr);
893
+ if (options.signal?.aborted) return { exitCode, status: "cancelled", stderr };
894
+ const terminal = complete;
895
+ if (!terminal) return { exitCode, status: "failed", result: { error: "agent exited without completion" }, stderr };
896
+ return { exitCode, status: exitCode === 0 ? terminal.status : "failed", result: terminal.result, stderr };
897
+ } catch (error) {
898
+ stop("runner_error");
899
+ await exit.catch(() => 1);
900
+ throw error;
901
+ } finally {
902
+ clearTimeout(timeout);
903
+ options.signal?.removeEventListener("abort", abort);
904
+ }
905
+ }
906
+
907
+ // src/recipe-container.ts
908
+ var ARTIFACT_PATH = /^[A-Za-z0-9_@+.,-]+(?:\/[A-Za-z0-9_@+.,-]+)*$/;
909
+ var PRIVATE_ARTIFACT_PART = /^(?:\.git|\.odla|\.wrangler|\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json)$/i;
910
+ function buildRecipeContainerArgs(engine, workspaceDir, recipe2, name = `odla-recipe-${(0, import_node_crypto.randomUUID)().slice(0, 12)}`) {
911
+ assertCodeBuildRecipe(recipe2);
912
+ if (/[,\r\n]/.test(workspaceDir)) throw new TypeError("workspace path contains unsupported mount characters");
913
+ const uid = typeof import_node_process2.getuid === "function" ? (0, import_node_process2.getuid)() : 1e3;
914
+ const gid = typeof import_node_process2.getgid === "function" ? (0, import_node_process2.getgid)() : 1e3;
915
+ const limits = {
916
+ cpus: recipe2.cpus ?? 1,
917
+ memory: recipe2.memory ?? "1g",
918
+ pids: recipe2.pids ?? 256,
919
+ tmpfs: recipe2.tmpfsBytes ?? 64 * 1024 * 1024
920
+ };
921
+ if (engine === "container") {
922
+ return [
923
+ "run",
924
+ "--rm",
925
+ `--name=${name}`,
926
+ "--network=none",
927
+ "--read-only",
928
+ "--cap-drop=ALL",
929
+ `--memory=${limits.memory}`,
930
+ `--cpus=${limits.cpus}`,
931
+ `--user=${uid}:${gid}`,
932
+ "--tmpfs=/tmp",
933
+ `--mount=type=bind,source=${workspaceDir},target=/workspace`,
934
+ "--workdir=/workspace",
935
+ "--env=CI=1",
936
+ recipe2.image,
937
+ ...recipe2.command
938
+ ];
939
+ }
940
+ return [
941
+ "run",
942
+ "--rm",
943
+ `--name=${name}`,
944
+ "--pull=never",
945
+ "--network=none",
946
+ "--read-only",
947
+ "--cap-drop=ALL",
948
+ "--security-opt=no-new-privileges",
949
+ `--pids-limit=${limits.pids}`,
950
+ `--memory=${limits.memory}`,
951
+ `--cpus=${limits.cpus}`,
952
+ `--user=${uid}:${gid}`,
953
+ `--tmpfs=/tmp:rw,noexec,nosuid,nodev,size=${limits.tmpfs}`,
954
+ `--mount=type=bind,src=${workspaceDir},dst=/workspace`,
955
+ "--workdir=/workspace",
956
+ "--env=CI=1",
957
+ recipe2.image,
958
+ ...recipe2.command
959
+ ];
960
+ }
961
+ function createContainerRecipeExecutor(engine) {
962
+ return {
963
+ async run(input) {
964
+ await verifyContainerEngineBoundary(engine);
965
+ const name = `odla-recipe-${(0, import_node_crypto.randomUUID)().slice(0, 12)}`;
966
+ const args = buildRecipeContainerArgs(engine, input.workspaceDir, input.recipe, name);
967
+ return execute(engine, args, name, input.recipe, input.signal);
968
+ }
969
+ };
970
+ }
971
+ function assertCodeBuildRecipe(recipe2) {
972
+ const memoryBytes = parseMemory(recipe2.memory ?? "1g");
973
+ const tmpfsBytes = recipe2.tmpfsBytes ?? 64 * 1024 * 1024;
974
+ const artifacts = recipe2.expectedArtifacts ?? [];
975
+ assertPinnedImage(recipe2.image);
976
+ if (!/^[A-Za-z0-9._:-]{1,120}$/.test(recipe2.id) || recipe2.command.length < 1 || recipe2.command.length > 64 || recipe2.command.some((part) => !part || part.length > 4096 || /[\0\r\n]/.test(part)) || !Number.isSafeInteger(recipe2.timeoutMs) || recipe2.timeoutMs < 1 || recipe2.timeoutMs > 30 * 6e4 || !Number.isSafeInteger(recipe2.maxOutputBytes) || recipe2.maxOutputBytes < 1 || recipe2.maxOutputBytes > 16 * 1024 * 1024 || !Number.isFinite(recipe2.cpus ?? 1) || (recipe2.cpus ?? 1) < 0.1 || (recipe2.cpus ?? 1) > 32 || memoryBytes < 64 * 1024 * 1024 || memoryBytes > 32 * 1024 * 1024 * 1024 || !Number.isSafeInteger(recipe2.pids ?? 256) || (recipe2.pids ?? 256) < 16 || (recipe2.pids ?? 256) > 4096 || !Number.isSafeInteger(tmpfsBytes) || tmpfsBytes < 1024 * 1024 || tmpfsBytes > 1024 * 1024 * 1024 || artifacts.length > 64 || new Set(artifacts.map((item) => item.id)).size !== artifacts.length || artifacts.some((item) => !/^[A-Za-z0-9._:-]{1,120}$/.test(item.id) || !ARTIFACT_PATH.test(item.path) || item.path.split("/").some((part) => PRIVATE_ARTIFACT_PART.test(part)) || !Number.isSafeInteger(item.maximumBytes) || item.maximumBytes < 1 || item.maximumBytes > 512 * 1024 * 1024)) {
977
+ throw new TypeError("build recipe is malformed or exceeds its control bounds");
978
+ }
979
+ }
980
+ function parseMemory(value) {
981
+ const match = /^([1-9][0-9]{0,4})([kmg])$/.exec(value.toLowerCase());
982
+ if (!match?.[1] || !match[2]) return 0;
983
+ const scale = match[2] === "k" ? 1024 : match[2] === "m" ? 1024 ** 2 : 1024 ** 3;
984
+ return Number(match[1]) * scale;
985
+ }
986
+ function execute(engine, args, name, recipe2, signal) {
987
+ return new Promise((accept, reject) => {
988
+ const started = Date.now();
989
+ const child = (0, import_node_child_process4.spawn)(engine, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
990
+ const stdout = [];
991
+ const stderr = [];
992
+ let bytes = 0;
993
+ let outputLimitExceeded = false;
994
+ let timedOut = false;
995
+ let stopping = false;
996
+ const stop = (reason) => {
997
+ if (stopping) return;
998
+ stopping = true;
999
+ timedOut = reason === "timeout";
1000
+ outputLimitExceeded = reason === "output";
1001
+ const remove = engine === "container" ? ["delete", "--force", name] : ["rm", "-f", name];
1002
+ const killer = (0, import_node_child_process4.spawn)(engine, remove, { shell: false, stdio: "ignore" });
1003
+ killer.unref();
1004
+ child.kill("SIGTERM");
1005
+ };
1006
+ const collect = (target) => (chunk) => {
1007
+ bytes += chunk.byteLength;
1008
+ if (bytes > recipe2.maxOutputBytes) stop("output");
1009
+ else target.push(chunk);
1010
+ };
1011
+ child.stdout.on("data", collect(stdout));
1012
+ child.stderr.on("data", collect(stderr));
1013
+ const abort = () => stop("abort");
1014
+ signal?.addEventListener("abort", abort, { once: true });
1015
+ if (signal?.aborted) abort();
1016
+ const timer = setTimeout(() => stop("timeout"), recipe2.timeoutMs);
1017
+ child.once("error", (error) => {
1018
+ clearTimeout(timer);
1019
+ signal?.removeEventListener("abort", abort);
1020
+ reject(error);
1021
+ });
1022
+ child.once("exit", (code) => {
1023
+ clearTimeout(timer);
1024
+ signal?.removeEventListener("abort", abort);
1025
+ accept({
1026
+ exitCode: code ?? 1,
1027
+ stdout: Buffer.concat(stdout).toString("utf8"),
1028
+ stderr: Buffer.concat(stderr).toString("utf8"),
1029
+ durationMs: Date.now() - started,
1030
+ outputLimitExceeded,
1031
+ timedOut
1032
+ });
1033
+ });
1034
+ });
1035
+ }
1036
+
1037
+ // src/workspace-digest.ts
1038
+ var import_node_crypto2 = require("crypto");
1039
+ var import_promises4 = require("fs/promises");
1040
+ var import_node_path5 = require("path");
1041
+ async function digestStagedWorkspace(root, limits) {
1042
+ const files = [];
1043
+ const walk = async (directory) => {
1044
+ const entries = await (0, import_promises4.readdir)(directory, { withFileTypes: true });
1045
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
1046
+ if (entry.isSymbolicLink()) throw new TypeError("workspace digest refuses symbolic links");
1047
+ const target = (0, import_node_path5.resolve)(directory, entry.name);
1048
+ if (entry.isDirectory()) await walk(target);
1049
+ else if (entry.isFile()) {
1050
+ files.push({ path: (0, import_node_path5.relative)(root, target).split("\\").join("/"), target });
1051
+ if (files.length > limits.maxFiles) throw new TypeError("workspace digest exceeds its file bound");
1052
+ }
1053
+ }
1054
+ };
1055
+ await walk((0, import_node_path5.resolve)(root));
1056
+ const hash = (0, import_node_crypto2.createHash)("sha256");
1057
+ let bytes = 0;
1058
+ for (const file of files.sort((left, right) => left.path.localeCompare(right.path))) {
1059
+ const content = await (0, import_promises4.readFile)(file.target);
1060
+ bytes += Buffer.byteLength(file.path) + content.byteLength;
1061
+ if (bytes > limits.maxBytes) throw new TypeError("workspace digest exceeds its byte bound");
1062
+ hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${content.byteLength}:`);
1063
+ hash.update(content);
1064
+ }
1065
+ return `sha256:${hash.digest("hex")}`;
1066
+ }
1067
+
1068
+ // src/code-verifier.ts
1069
+ var SHA = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
1070
+ var DIGEST = /^sha256:[0-9a-f]{64}$/;
1071
+ var ID = /^[A-Za-z0-9._:-]{1,160}$/;
1072
+ var RULE = /^[A-Za-z0-9_@+.,/-]{1,160}$/;
1073
+ var DEFAULT_PREFIXES = ["test/", "tests/", "__tests__/"];
1074
+ var DEFAULT_SUFFIXES = [".test.js", ".test.ts", ".test.tsx", ".spec.js", ".spec.ts", ".spec.tsx"];
1075
+ async function verifyCodeCandidate(input) {
1076
+ const policy = validate(input);
1077
+ const limits = { maxFiles: policy.maximumFiles, maxBytes: policy.maximumBytes };
1078
+ const staged = await stageWorkspace(input.trustedBaseDir, limits);
1079
+ try {
1080
+ const baseDigest = await digestStagedWorkspace(staged.workspaceDir, limits);
1081
+ if (baseDigest !== input.trustedBaseDigest) throw new TypeError("trusted base does not match its registered digest");
1082
+ const paths = validateCodePatch(input.candidatePatch, policy.maximumPatchBytes);
1083
+ await applyCodePatch(staged.workspaceDir, input.candidatePatch, paths);
1084
+ const sourceDigest = await digestStagedWorkspace(staged.workspaceDir, limits);
1085
+ const policyDigest = digestPolicy(policy);
1086
+ const patchDigest = digestBytes(input.candidatePatch);
1087
+ const candidateDigest = digestJson({
1088
+ trustedBaseCommitSha: input.trustedBaseCommitSha,
1089
+ trustedBaseDigest: input.trustedBaseDigest,
1090
+ patchDigest
1091
+ });
1092
+ const changedTests = changedTestPaths(paths, policy);
1093
+ if (changedTests.length > policy.maximumChangedTests) throw new TypeError("candidate changes too many test files");
1094
+ const recipes = [];
1095
+ const logs = [];
1096
+ for (const recipe2 of policy.recipes) {
1097
+ const clean = await stageWorkspace(staged.workspaceDir, limits);
1098
+ try {
1099
+ if (await digestStagedWorkspace(clean.workspaceDir, limits) !== sourceDigest) {
1100
+ throw new TypeError("clean verifier source changed before execution");
1101
+ }
1102
+ const result = checkedResult(await input.recipeExecutor.run({
1103
+ workspaceDir: clean.workspaceDir,
1104
+ recipe: recipe2,
1105
+ signal: input.signal
1106
+ }), recipe2.maxOutputBytes);
1107
+ const artifacts = await inspectArtifacts(clean.workspaceDir, recipe2);
1108
+ recipes.push(recipeReceipt(recipe2, result, artifacts));
1109
+ logs.push({ recipeId: recipe2.id, ...boundedLogs(result, recipe2.maxOutputBytes) });
1110
+ } finally {
1111
+ await clean.cleanup();
1112
+ }
1113
+ }
1114
+ const fields = {
1115
+ schemaVersion: 1,
1116
+ verificationId: input.verificationId ?? `verify-${(0, import_node_crypto3.randomUUID)()}`,
1117
+ trustedBaseCommitSha: input.trustedBaseCommitSha,
1118
+ trustedBaseDigest: input.trustedBaseDigest,
1119
+ patchDigest,
1120
+ candidateDigest,
1121
+ sourceDigest,
1122
+ policyDigest,
1123
+ recipes,
1124
+ changedTestCount: changedTests.length,
1125
+ changedTestSetDigest: digestJson(changedTests),
1126
+ changedTestsRequireReview: changedTests.length > 0,
1127
+ outcome: recipes.every((recipe2) => recipe2.status === "passed") ? "passed" : "failed"
1128
+ };
1129
+ return {
1130
+ receipt: { ...fields, receiptDigest: await (0, import_code3.digestCodeVerificationReceipt)(fields) },
1131
+ changedTests: Object.freeze(changedTests),
1132
+ logs: Object.freeze(logs)
1133
+ };
1134
+ } finally {
1135
+ await staged.cleanup();
1136
+ }
1137
+ }
1138
+ function validate(input) {
1139
+ const policy = input.policy;
1140
+ if (!SHA.test(input.trustedBaseCommitSha) || !DIGEST.test(input.trustedBaseDigest) || !ID.test(input.verificationId ?? "verify-generated") || !ID.test(policy.policyId) || policy.recipes.length < 1 || policy.recipes.length > 64 || new Set(policy.recipes.map((recipe2) => recipe2.id)).size !== policy.recipes.length) {
1141
+ throw new TypeError("clean verification input is malformed");
1142
+ }
1143
+ for (const recipe2 of policy.recipes) assertCodeBuildRecipe(recipe2);
1144
+ const result = {
1145
+ policyId: policy.policyId,
1146
+ recipes: policy.recipes,
1147
+ testPathPrefixes: policy.testPathPrefixes ?? DEFAULT_PREFIXES,
1148
+ testPathSuffixes: policy.testPathSuffixes ?? DEFAULT_SUFFIXES,
1149
+ maximumChangedTests: policy.maximumChangedTests ?? 1e3,
1150
+ maximumPatchBytes: policy.maximumPatchBytes ?? 256 * 1024,
1151
+ maximumFiles: policy.maximumFiles ?? 2e4,
1152
+ maximumBytes: policy.maximumBytes ?? 512 * 1024 * 1024
1153
+ };
1154
+ if ([...result.testPathPrefixes, ...result.testPathSuffixes].some((rule) => !RULE.test(rule)) || !integer(result.maximumChangedTests, 0, 1e4) || !integer(result.maximumPatchBytes, 1, 4 * 1024 * 1024) || !integer(result.maximumFiles, 1, 1e5) || !integer(result.maximumBytes, 1, 2 * 1024 * 1024 * 1024)) {
1155
+ throw new TypeError("clean verification policy exceeds its bounds");
1156
+ }
1157
+ return result;
1158
+ }
1159
+ function changedTestPaths(paths, policy) {
1160
+ return paths.filter((path) => policy.testPathSuffixes.some((suffix) => path.endsWith(suffix)) || policy.testPathPrefixes.some((prefix) => path.startsWith(prefix) || path.includes(`/${prefix}`))).sort();
1161
+ }
1162
+ function recipeReceipt(recipe2, result, artifacts) {
1163
+ const status = result.timedOut ? "timed_out" : result.outputLimitExceeded ? "output_limited" : result.exitCode === 0 && artifacts.every((item) => item.status === "verified") ? "passed" : "failed";
1164
+ return {
1165
+ recipeId: recipe2.id,
1166
+ recipeDigest: digestRecipe(recipe2),
1167
+ status,
1168
+ exitCode: result.exitCode,
1169
+ durationMs: result.durationMs,
1170
+ artifacts
1171
+ };
1172
+ }
1173
+ async function inspectArtifacts(workspaceDir, recipe2) {
1174
+ const receipts = [];
1175
+ for (const artifact of recipe2.expectedArtifacts ?? []) {
1176
+ try {
1177
+ const path = (0, import_node_path6.join)(workspaceDir, artifact.path);
1178
+ const info = await (0, import_promises5.lstat)(path);
1179
+ if (!info.isFile() || info.isSymbolicLink()) {
1180
+ receipts.push({ artifactId: artifact.id, status: "invalid", bytes: null, digest: null });
1181
+ } else if (info.size > artifact.maximumBytes) {
1182
+ receipts.push({ artifactId: artifact.id, status: "too_large", bytes: info.size, digest: null });
1183
+ } else {
1184
+ receipts.push({ artifactId: artifact.id, status: "verified", bytes: info.size, digest: await hashFile(path) });
1185
+ }
1186
+ } catch (reason) {
1187
+ if (reason.code !== "ENOENT") throw reason;
1188
+ receipts.push({ artifactId: artifact.id, status: "missing", bytes: null, digest: null });
1189
+ }
1190
+ }
1191
+ return receipts;
1192
+ }
1193
+ function hashFile(path) {
1194
+ return new Promise((accept, reject) => {
1195
+ const hash = (0, import_node_crypto3.createHash)("sha256");
1196
+ const stream = (0, import_node_fs2.createReadStream)(path);
1197
+ stream.on("data", (chunk) => {
1198
+ hash.update(chunk);
1199
+ });
1200
+ stream.once("error", reject);
1201
+ stream.once("end", () => accept(`sha256:${hash.digest("hex")}`));
1202
+ });
1203
+ }
1204
+ function checkedResult(result, maximumOutputBytes) {
1205
+ if (!integer(result.exitCode, 0, 255) || !integer(result.durationMs, 0, 30 * 6e4) || typeof result.stdout !== "string" || typeof result.stderr !== "string" || typeof result.outputLimitExceeded !== "boolean" || typeof result.timedOut !== "boolean") {
1206
+ throw new TypeError("recipe executor returned an invalid result");
1207
+ }
1208
+ const bytes = Buffer.byteLength(result.stdout) + Buffer.byteLength(result.stderr);
1209
+ return bytes > maximumOutputBytes ? { ...result, outputLimitExceeded: true } : result;
1210
+ }
1211
+ function boundedLogs(result, maximum) {
1212
+ const stdout = Buffer.from(result.stdout);
1213
+ const stderr = Buffer.from(result.stderr);
1214
+ const first = stdout.subarray(0, maximum);
1215
+ return {
1216
+ stdout: first.toString("utf8"),
1217
+ stderr: stderr.subarray(0, Math.max(0, maximum - first.byteLength)).toString("utf8")
1218
+ };
1219
+ }
1220
+ function digestPolicy(policy) {
1221
+ return digestJson({
1222
+ policyId: policy.policyId,
1223
+ recipes: policy.recipes.map((recipe2) => normalizedRecipe(recipe2)),
1224
+ testPathPrefixes: [...policy.testPathPrefixes].sort(),
1225
+ testPathSuffixes: [...policy.testPathSuffixes].sort(),
1226
+ maximumChangedTests: policy.maximumChangedTests,
1227
+ maximumPatchBytes: policy.maximumPatchBytes,
1228
+ maximumFiles: policy.maximumFiles,
1229
+ maximumBytes: policy.maximumBytes
1230
+ });
1231
+ }
1232
+ function digestRecipe(recipe2) {
1233
+ return digestJson(normalizedRecipe(recipe2));
1234
+ }
1235
+ function normalizedRecipe(recipe2) {
1236
+ return {
1237
+ id: recipe2.id,
1238
+ image: recipe2.image,
1239
+ command: [...recipe2.command],
1240
+ timeoutMs: recipe2.timeoutMs,
1241
+ maxOutputBytes: recipe2.maxOutputBytes,
1242
+ cpus: recipe2.cpus ?? 1,
1243
+ memory: recipe2.memory ?? "1g",
1244
+ pids: recipe2.pids ?? 256,
1245
+ tmpfsBytes: recipe2.tmpfsBytes ?? 64 * 1024 * 1024,
1246
+ expectedArtifacts: [...recipe2.expectedArtifacts ?? []].sort((left, right) => left.id.localeCompare(right.id)).map((artifact) => ({ id: artifact.id, path: artifact.path, maximumBytes: artifact.maximumBytes }))
1247
+ };
1248
+ }
1249
+ function digestJson(value) {
1250
+ return digestBytes(JSON.stringify(value));
1251
+ }
1252
+ function digestBytes(value) {
1253
+ return `sha256:${(0, import_node_crypto3.createHash)("sha256").update(value).digest("hex")}`;
1254
+ }
1255
+ function integer(value, minimum, maximum) {
1256
+ return Number.isSafeInteger(value) && value >= minimum && value <= maximum;
1257
+ }
1258
+
1259
+ // src/code-runtime-checkpoint.ts
1260
+ async function prepareRuntimeCheckpoint(input) {
1261
+ const patch2 = await input.workspace.patch(256 * 1024);
1262
+ let verification = null;
1263
+ let review = null;
1264
+ let note = patch2 ? "Candidate remains untrusted" : "Checkpoint has no source changes";
1265
+ if (patch2 && input.role === "coding") {
1266
+ try {
1267
+ const evidence = await verifyCodeCandidate({
1268
+ verificationId: `verify-${input.sessionId.slice("csess_".length)}`,
1269
+ trustedBaseDir: input.workspace.baselineDir,
1270
+ trustedBaseCommitSha: input.baseCommitSha,
1271
+ trustedBaseDigest: input.trustedBaseDigest,
1272
+ candidatePatch: patch2,
1273
+ policy: {
1274
+ policyId: "code.runtime",
1275
+ recipes: input.recipes,
1276
+ maximumFiles: 2e4,
1277
+ maximumBytes: 512 * 1024 * 1024
1278
+ },
1279
+ recipeExecutor: input.recipeExecutor
1280
+ });
1281
+ if (evidence.receipt.outcome === "passed") {
1282
+ verification = evidence.receipt;
1283
+ review = await input.review(patch2, verification);
1284
+ note = review.verdict === "approved" ? "Clean verification and independent review passed" : "Clean verification passed; independent review rejected the candidate";
1285
+ } else {
1286
+ note = `Clean verification failed: ${evidence.receipt.recipes.filter((recipe2) => recipe2.status !== "passed").map((recipe2) => `${recipe2.recipeId}=${recipe2.status}`).join(", ")}`;
1287
+ }
1288
+ } catch (cause) {
1289
+ note = `Candidate verification or review failed closed: ${message(cause)}`;
1290
+ }
1291
+ }
1292
+ const reviewed = verification && review?.verdict === "approved";
1293
+ const checkpoint = await createCodeWorkspaceCheckpoint({
1294
+ workspace: input.workspace,
1295
+ baseCommitSha: input.baseCommitSha,
1296
+ state: {
1297
+ planCursor: null,
1298
+ conversationRefs: input.conversationRefs,
1299
+ planningInputDigest: input.planningInputDigest,
1300
+ buildPolicyDigest: verification?.policyDigest ?? input.fallbackPolicyDigest,
1301
+ dependencyLayerDigest: null,
1302
+ verificationDigest: verification?.receiptDigest ?? null,
1303
+ reviewDigest: reviewed && review ? review.reviewDigest : null,
1304
+ completedEffects: [],
1305
+ unresolvedApprovals: [],
1306
+ trustStatus: reviewed ? "reviewed" : verification ? "verified" : "candidate_untrusted"
1307
+ }
1308
+ });
1309
+ return { checkpoint, verification, review, note };
1310
+ }
1311
+ var message = (value) => (value instanceof Error ? value.message : String(value)).slice(0, 500);
1312
+
1313
+ // src/code-runtime-checkpoint-manager.ts
1314
+ var CodeRuntimeCheckpointManager = class {
1315
+ constructor(options) {
1316
+ this.options = options;
1317
+ }
1318
+ options;
1319
+ #pending = /* @__PURE__ */ new Map();
1320
+ async prepare(command, active) {
1321
+ active.abort.abort("checkpoint_stop");
1322
+ await active.done;
1323
+ const prepared = await prepareRuntimeCheckpoint({
1324
+ sessionId: command.sessionId,
1325
+ role: active.role,
1326
+ workspace: active.workspace,
1327
+ baseCommitSha: active.baseCommitSha,
1328
+ trustedBaseDigest: active.trustedBaseDigest,
1329
+ planningInputDigest: active.planningInputDigest,
1330
+ conversationRefs: active.conversationRefs,
1331
+ fallbackPolicyDigest: this.options.fallbackPolicyDigest,
1332
+ recipes: this.options.recipes,
1333
+ recipeExecutor: this.options.recipeExecutor,
1334
+ review: (patch2, verification) => this.options.control.review(command.sessionId, { patch: patch2, verification })
1335
+ });
1336
+ if (prepared.review?.verdict === "approved" && prepared.verification) {
1337
+ this.#pending.set(command.commandId, { verification: prepared.verification, refs: active.conversationRefs });
1338
+ }
1339
+ await this.options.event(command, { type: "message", actor: "system", body: prepared.note }, active.conversationRefs).catch(() => void 0);
1340
+ await active.workspace.cleanup();
1341
+ await this.options.event(command, { type: "status", status: "checkpointed" }, active.conversationRefs).catch(() => void 0);
1342
+ return { status: "checkpointed", checkpoint: prepared.checkpoint, message: "Pi stopped at a portable checkpoint" };
1343
+ }
1344
+ async acknowledged(command, result) {
1345
+ if (command.kind !== "checkpoint_stop" || result.status !== "checkpointed") return false;
1346
+ const pending = this.#pending.get(command.commandId);
1347
+ if (!pending) return true;
1348
+ const checkpointId = `cpoint_${command.commandId.slice("ccmd_".length)}`;
1349
+ const candidate = await this.options.control.submitCandidate(command.sessionId, checkpointId, pending.verification);
1350
+ await this.options.event(command, {
1351
+ type: "message",
1352
+ actor: "system",
1353
+ body: command.payload.sourceSet ? `Candidate ${candidate.candidateId} was verified and delivered to the session PR branch` : `Candidate ${candidate.candidateId} is ready for legacy owner publication approval`
1354
+ }, pending.refs);
1355
+ this.#pending.delete(command.commandId);
1356
+ return true;
1357
+ }
1358
+ };
1359
+
1360
+ // src/code-runtime-source.ts
1361
+ var import_promises6 = require("fs/promises");
1362
+ var import_node_os2 = require("os");
1363
+ var import_node_path7 = require("path");
1364
+ var RESERVED2 = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_modules", "dist", "coverage"]);
1365
+ var SECRET2 = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
1366
+ async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_node_os2.tmpdir)()) {
1367
+ if (!snapshot.files.length || snapshot.files.length > 1e4) throw new TypeError("Code source file count is invalid");
1368
+ const root = await (0, import_promises6.mkdtemp)((0, import_node_path7.join)(tempRoot, "odla-code-source-"));
1369
+ const sourceDir = (0, import_node_path7.join)(root, "source");
1370
+ await (0, import_promises6.mkdir)(sourceDir);
1371
+ const seen = /* @__PURE__ */ new Set();
1372
+ let bytes = 0;
1373
+ try {
1374
+ for (const file of snapshot.files) {
1375
+ validatePath(file.path);
1376
+ if (seen.has(file.path)) throw new TypeError("Code source repeats a path");
1377
+ seen.add(file.path);
1378
+ bytes += Buffer.byteLength(file.path) + Buffer.byteLength(file.content);
1379
+ if (bytes > 16 * 1024 * 1024) throw new TypeError("Code source exceeds its byte bound");
1380
+ const target = (0, import_node_path7.resolve)(sourceDir, file.path);
1381
+ if (!target.startsWith(`${(0, import_node_path7.resolve)(sourceDir)}${import_node_path7.sep}`)) throw new TypeError("Code source path escapes its root");
1382
+ await (0, import_promises6.mkdir)((0, import_node_path7.dirname)(target), { recursive: true });
1383
+ await (0, import_promises6.writeFile)(target, file.content, { flag: "wx", mode: 420 });
1384
+ }
1385
+ for (const reference of snapshot.references ?? []) {
1386
+ validateAlias(reference.alias);
1387
+ if (!reference.files.length || reference.files.length > 1e4) throw new TypeError("Code reference file count is invalid");
1388
+ for (const file of reference.files) {
1389
+ validatePath(file.path);
1390
+ const path = `.odla-references/${reference.alias}/${file.path}`;
1391
+ if (seen.has(path)) throw new TypeError("Code reference repeats a path");
1392
+ seen.add(path);
1393
+ bytes += Buffer.byteLength(path) + Buffer.byteLength(file.content);
1394
+ if (bytes > 80 * 1024 * 1024) throw new TypeError("Code source set exceeds its byte bound");
1395
+ const target = (0, import_node_path7.resolve)(sourceDir, path);
1396
+ if (!target.startsWith(`${(0, import_node_path7.resolve)(sourceDir)}${import_node_path7.sep}`)) throw new TypeError("Code reference path escapes its root");
1397
+ await (0, import_promises6.mkdir)((0, import_node_path7.dirname)(target), { recursive: true });
1398
+ await (0, import_promises6.writeFile)(target, file.content, { flag: "wx", mode: 292 });
1399
+ }
1400
+ }
1401
+ return { sourceDir, cleanup: () => (0, import_promises6.rm)(root, { recursive: true, force: true }) };
1402
+ } catch (cause) {
1403
+ await (0, import_promises6.rm)(root, { recursive: true, force: true });
1404
+ throw cause;
1405
+ }
1406
+ }
1407
+ function validateAlias(alias) {
1408
+ if (!/^[a-z][a-z0-9-]{0,39}$/.test(alias) || alias === "primary") {
1409
+ throw new TypeError("Code reference alias is invalid");
1410
+ }
1411
+ }
1412
+ async function attachCodeRuntimeReferences(workspace, references) {
1413
+ let bytes = 0;
1414
+ for (const reference of references) {
1415
+ validateAlias(reference.alias);
1416
+ for (const file of reference.files) {
1417
+ validatePath(file.path);
1418
+ const path = `.odla-references/${reference.alias}/${file.path}`;
1419
+ bytes += Buffer.byteLength(path) + Buffer.byteLength(file.content);
1420
+ if (bytes > 64 * 1024 * 1024) throw new TypeError("Code reference set exceeds its byte bound");
1421
+ for (const root of [workspace.baselineDir, workspace.workspaceDir]) {
1422
+ const target = (0, import_node_path7.resolve)(root, path);
1423
+ if (!target.startsWith(`${(0, import_node_path7.resolve)(root)}${import_node_path7.sep}`)) throw new TypeError("Code reference path escapes its root");
1424
+ await (0, import_promises6.mkdir)((0, import_node_path7.dirname)(target), { recursive: true });
1425
+ await (0, import_promises6.writeFile)(target, file.content, { flag: "wx", mode: 292 });
1426
+ }
1427
+ }
1428
+ }
1429
+ }
1430
+ function validatePath(path) {
1431
+ const parts = path.split("/");
1432
+ if (!path || path.startsWith("/") || path.includes("\\") || path.includes("\0") || parts.some((part) => !part || part === "." || part === ".." || RESERVED2.has(part) || SECRET2.test(part))) {
1433
+ throw new TypeError("Code source contains an unsafe path");
1434
+ }
1435
+ }
1436
+
1437
+ // src/code-runtime-task.ts
1438
+ function codeCommandMetadata(payload, resume) {
1439
+ const trusted = record3(payload.trustedBase);
1440
+ const role = payload.role;
1441
+ const title = payload.title;
1442
+ const prompt = payload.prompt;
1443
+ const maxTokensPerInteraction = payload.maxTokensPerInteraction ?? 32e3;
1444
+ if (role !== "coding" && role !== "review" || typeof title !== "string" || typeof prompt !== "string") {
1445
+ throw new TypeError(`invalid Code ${resume ? "resume" : "start"} metadata`);
1446
+ }
1447
+ const planning = trusted?.planningInputDigest;
1448
+ const attestation = trusted?.attestationDigest;
1449
+ const repository = trusted?.repository;
1450
+ const baseCommitSha = trusted?.commitSha;
1451
+ const sourceTreeDigest = trusted?.treeDigest;
1452
+ if (typeof repository !== "string" || !repository.includes("/") || typeof baseCommitSha !== "string" || !/^[0-9a-f]{40}$/.test(baseCommitSha) || typeof sourceTreeDigest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(sourceTreeDigest)) {
1453
+ throw new TypeError(`invalid Code ${resume ? "resume" : "start"} trusted base`);
1454
+ }
1455
+ if (!Number.isSafeInteger(maxTokensPerInteraction) || Number(maxTokensPerInteraction) < 4e3 || Number(maxTokensPerInteraction) > 2e5) {
1456
+ throw new TypeError(`invalid Code ${resume ? "resume" : "start"} interaction token limit`);
1457
+ }
1458
+ return {
1459
+ role,
1460
+ title,
1461
+ prompt,
1462
+ maxTokensPerInteraction: Number(maxTokensPerInteraction),
1463
+ planningInputDigest: typeof planning === "string" && /^sha256:[0-9a-f]{64}$/.test(planning) ? planning : null,
1464
+ attestationDigest: typeof attestation === "string" ? attestation : "resume",
1465
+ repository,
1466
+ baseCommitSha,
1467
+ sourceTreeDigest
1468
+ };
1469
+ }
1470
+ function codeLocalSource(payload) {
1471
+ const source = record3(payload.source);
1472
+ if (!source) return null;
1473
+ if (source.kind !== "local_checkout" || typeof source.repository !== "string" || typeof source.headCommitSha !== "string" || !/^[0-9a-f]{40}$/.test(source.headCommitSha) || typeof source.trustedBaseDigest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(source.trustedBaseDigest) || typeof source.developerPatchDigest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(source.developerPatchDigest) || typeof source.snapshotDigest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(source.snapshotDigest) || typeof source.modified !== "boolean" || !Number.isSafeInteger(source.fileCount) || Number(source.fileCount) < 1 || Number(source.fileCount) > 2e4 || !Number.isSafeInteger(source.byteCount) || Number(source.byteCount) < 1 || Number(source.byteCount) > 512 * 1024 * 1024 || !Number.isSafeInteger(source.capturedAt) || Number(source.capturedAt) < 1) {
1474
+ throw new TypeError("invalid local checkout source descriptor");
1475
+ }
1476
+ return source;
1477
+ }
1478
+ function codeCheckpointPayload(payload) {
1479
+ const value = payload.checkpoint;
1480
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("resume checkpoint is missing");
1481
+ return value;
1482
+ }
1483
+ function fakeCodeLease(command, metadata) {
1484
+ return {
1485
+ protocolVersion: HARNESS_PROTOCOL_VERSION,
1486
+ leaseId: `code:${command.commandId}`,
1487
+ generation: command.bindingGeneration,
1488
+ expiresAt: Date.now() + 24 * 60 * 6e4,
1489
+ task: {
1490
+ taskId: command.sessionId,
1491
+ attemptId: command.instanceId,
1492
+ title: metadata.title,
1493
+ prompt: metadata.prompt,
1494
+ workspace: command.appId,
1495
+ aiRoute: metadata.role,
1496
+ policy: {
1497
+ network: "none",
1498
+ timeoutMs: 30 * 6e4,
1499
+ maxOutputBytes: 4 * 1024 * 1024,
1500
+ maxPatchBytes: 256 * 1024
1501
+ }
1502
+ }
1503
+ };
1504
+ }
1505
+ var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
1506
+
1507
+ // src/code-runtime-local-source.ts
1508
+ var SOURCE_LIMITS = { maxFiles: 2e4, maxBytes: 512 * 1024 * 1024 };
1509
+ async function prepareRuntimeLocalSource(input) {
1510
+ const { command, descriptor: descriptor2, available, repository, baseCommitSha, resume } = input;
1511
+ if (!available || JSON.stringify(available.descriptor) !== JSON.stringify(descriptor2) || descriptor2.repository.toLowerCase() !== repository.toLowerCase() || descriptor2.headCommitSha !== baseCommitSha) {
1512
+ throw new TypeError("the session's local checkout snapshot is not available on this terminal");
1513
+ }
1514
+ const workspace = resume ? (await restoreCodeWorkspaceCheckpoint({
1515
+ trustedBaseDir: available.trustedBaseDir,
1516
+ trustedBaseCommitSha: baseCommitSha,
1517
+ checkpoint: codeCheckpointPayload(command.payload)
1518
+ })).workspace : await stageWorkspacePair(available.trustedBaseDir, available.sourceDir, SOURCE_LIMITS);
1519
+ const trustedBaseDigest = await digestStagedWorkspace(workspace.baselineDir, SOURCE_LIMITS);
1520
+ if (trustedBaseDigest !== descriptor2.trustedBaseDigest) {
1521
+ await workspace.cleanup();
1522
+ throw new TypeError("trusted Git base digest changed after connection");
1523
+ }
1524
+ if (!resume && await digestStagedWorkspace(workspace.workspaceDir, SOURCE_LIMITS) !== descriptor2.snapshotDigest) {
1525
+ await workspace.cleanup();
1526
+ throw new TypeError("local checkout snapshot digest changed after connection");
1527
+ }
1528
+ return { workspace, sourceDigest: descriptor2.snapshotDigest, trustedBaseDigest };
1529
+ }
1530
+
1531
+ // src/code-tool-broker.ts
1532
+ var import_promises7 = require("fs/promises");
1533
+ var import_node_path8 = require("path");
1534
+
1535
+ // src/code-tool-policy.ts
1536
+ var import_camel = require("@odla-ai/camel");
1537
+ var import_policy = require("@odla-ai/camel/policy");
1538
+ var DESTINATIONS = "code-workspaces.v1";
1539
+ var READ = descriptor("sandbox.read", "scoped_data_read", {
1540
+ workspace: "destination",
1541
+ authority: "authority",
1542
+ path: "selector",
1543
+ startLine: "selector",
1544
+ endLine: "selector"
1545
+ });
1546
+ var PATCH = descriptor("sandbox.apply_patch", "reversible_mutation", {
1547
+ workspace: "destination",
1548
+ authority: "authority",
1549
+ patch: "payload"
1550
+ });
1551
+ var RECIPE = descriptor("sandbox.run_recipe", "code_execution", {
1552
+ workspace: "destination",
1553
+ authority: "authority",
1554
+ recipeId: "selector",
1555
+ sourceDigest: "payload"
1556
+ });
1557
+ function createCodePolicyGate(options) {
1558
+ return {
1559
+ read: async (input) => {
1560
+ const base = await environment(input, options, "sandbox.read");
1561
+ const conversions = await conversionRegistry([
1562
+ await registeredPolicy("code.path.v1", "code.paths.v1", input.paths),
1563
+ await conversionPolicy("code.line.v1", { kind: "integer", minimum: 1, maximum: 1e6 })
1564
+ ], { "code.paths.v1": input.paths });
1565
+ const path = await conversions.operations.registeredId(unsafe(base, input.path, "path"), "code.path.v1");
1566
+ const start = await conversions.operations.integer(unsafe(base, input.startLine, "start"), "code.line.v1");
1567
+ const end = await conversions.operations.integer(unsafe(base, input.endLine, "end"), "code.line.v1");
1568
+ if (end.value < start.value) return false;
1569
+ return authorize(input, options, base, READ, {
1570
+ ...base.fixedArgs,
1571
+ path: { role: "selector", value: path },
1572
+ startLine: { role: "selector", value: start },
1573
+ endLine: { role: "selector", value: end }
1574
+ }, [path, start, end]);
1575
+ },
1576
+ patch: async (input) => {
1577
+ const base = await environment(input, options, "sandbox.apply_patch");
1578
+ const patch2 = unsafe(base, input.patch, "patch");
1579
+ return authorize(input, options, base, PATCH, {
1580
+ ...base.fixedArgs,
1581
+ patch: { role: "payload", value: patch2 }
1582
+ }, []);
1583
+ },
1584
+ recipe: async (input) => {
1585
+ const base = await environment(input, options, "sandbox.run_recipe");
1586
+ const conversions = await conversionRegistry([
1587
+ await registeredPolicy("code.recipe.v1", "code.recipes.v1", input.recipeIds)
1588
+ ], { "code.recipes.v1": input.recipeIds });
1589
+ const recipe2 = await conversions.operations.registeredId(unsafe(base, input.recipeId, "recipe"), "code.recipe.v1");
1590
+ const source = unsafe(base, input.sourceDigest, "source");
1591
+ return authorize(input, options, base, RECIPE, {
1592
+ ...base.fixedArgs,
1593
+ recipeId: { role: "selector", value: recipe2 },
1594
+ sourceDigest: { role: "payload", value: source }
1595
+ }, [recipe2]);
1596
+ }
1597
+ };
1598
+ }
1599
+ function descriptor(name, effect, argumentRoles) {
1600
+ return { name, version: 1, effect, inputSchema: { type: "object" }, argumentRoles, policyId: `odla.code.${name}.v1` };
1601
+ }
1602
+ async function conversionPolicy(id, output) {
1603
+ const definition = {
1604
+ conversionId: id,
1605
+ version: 1,
1606
+ output,
1607
+ maximumSourceBytes: 1e6,
1608
+ maximumOutputsPerArtifact: 4,
1609
+ presentation: "json_scalar"
1610
+ };
1611
+ return { ...definition, digest: await (0, import_camel.conversionPolicyDigest)(definition) };
1612
+ }
1613
+ async function registeredPolicy(id, registryId, values) {
1614
+ const mapping = Object.fromEntries(values.map((value) => [value, value]));
1615
+ return conversionPolicy(id, {
1616
+ kind: "registered_id",
1617
+ registryId,
1618
+ registryDigest: await (0, import_camel.registeredIdRegistryDigest)(mapping)
1619
+ });
1620
+ }
1621
+ async function conversionRegistry(policies, values) {
1622
+ const registeredIds = Object.fromEntries(await Promise.all(Object.entries(values).map(async ([id, entries]) => {
1623
+ const mapping = Object.fromEntries(entries.map((value) => [value, value]));
1624
+ return [id, { values: mapping, digest: await (0, import_camel.registeredIdRegistryDigest)(mapping) }];
1625
+ })));
1626
+ return (0, import_camel.createConversionRegistry)({ policies, registeredIds });
1627
+ }
1628
+ async function environment(input, options, tool) {
1629
+ const ingress = (0, import_camel.createCamelIngress)([
1630
+ { id: "workspace", value: input.workspaceId, readers: input.readers },
1631
+ { id: "authority", value: `lease:${input.lease.leaseId}`, readers: input.readers },
1632
+ { id: "reader", value: options.readerId, readers: input.readers }
1633
+ ]);
1634
+ const digest = await (0, import_policy.destinationRegistryDigest)([input.workspaceId]);
1635
+ const approvals = ["irreversible_mutation", "external_send", "financial", "secret_read"];
1636
+ if (options.recipeAuthorization === "exact_approval") approvals.push("code_execution");
1637
+ const policy = (0, import_policy.createEffectPolicy)({
1638
+ destinationRegistries: { [DESTINATIONS]: { digest, values: [input.workspaceId] } },
1639
+ approvalEffects: approvals
1640
+ });
1641
+ const fixedArgs = {
1642
+ workspace: { role: "destination", value: ingress.control("workspace"), registryId: DESTINATIONS, registryDigest: digest },
1643
+ authority: { role: "authority", value: ingress.control("authority") }
1644
+ };
1645
+ return { ingress, policy, fixedArgs, reader: ingress.control("reader"), runId: `${input.request.requestId}:${tool}` };
1646
+ }
1647
+ function unsafe(base, value, field) {
1648
+ return base.ingress.quarantinedOutput(value, {
1649
+ readers: base.reader.label.readers,
1650
+ runId: `${base.runId}:${field}`
1651
+ });
1652
+ }
1653
+ async function authorize(input, options, base, tool, args, controlDependencies) {
1654
+ const policy = await base.policy.evaluate({
1655
+ planId: input.lease.task.taskId,
1656
+ tool,
1657
+ args,
1658
+ controlDependencies,
1659
+ intendedReaderIds: [base.reader]
1660
+ });
1661
+ let approvalConsumed = false;
1662
+ if (policy.outcome === "require_approval" && options.consumeApproval) {
1663
+ approvalConsumed = await options.consumeApproval(decision(input, policy, false, tool.name, policy.actionDigest));
1664
+ }
1665
+ await options.onDecision?.(decision(input, policy, approvalConsumed, tool.name));
1666
+ return policy.outcome === "allow" || approvalConsumed;
1667
+ }
1668
+ function decision(input, policy, approvalConsumed, tool, actionDigest) {
1669
+ return {
1670
+ lease: input.lease,
1671
+ request: input.request,
1672
+ tool,
1673
+ policy,
1674
+ approvalConsumed,
1675
+ actionDigest: actionDigest ?? (policy.outcome === "require_approval" ? policy.actionDigest : "")
1676
+ };
1677
+ }
1678
+
1679
+ // src/code-tool-broker.ts
1680
+ function createCodeToolBroker(options) {
1681
+ validateOptions(options);
1682
+ const recipes = new Map(options.recipes.map((recipe2) => [recipe2.id, recipe2]));
1683
+ const policy = createCodePolicyGate(options);
1684
+ let tail = Promise.resolve();
1685
+ return {
1686
+ execute(context, request) {
1687
+ const result = tail.then(() => route(context, request, options, recipes, policy));
1688
+ tail = result.then(() => void 0, () => void 0);
1689
+ return result;
1690
+ }
1691
+ };
1692
+ }
1693
+ async function route(context, request, options, recipes, policy) {
1694
+ try {
1695
+ if (context.signal?.aborted) throw new TypeError("tool request was cancelled");
1696
+ if (request.tool === "sandbox.read") return await read(context, request, options, policy);
1697
+ if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy);
1698
+ return await recipe(context, request, options, recipes, policy);
1699
+ } catch (reason) {
1700
+ return response(request, false, reason instanceof TypeError ? reason.message : "tool failed closed");
1701
+ }
1702
+ }
1703
+ async function read(context, request, options, policy) {
1704
+ exactKeys(request.input, ["path", "startLine", "endLine"]);
1705
+ const path = stringField(request.input, "path");
1706
+ const startLine = optionalInteger(request.input.startLine) ?? 1;
1707
+ const endLine = optionalInteger(request.input.endLine) ?? startLine + (options.maxReadLines ?? 2e3) - 1;
1708
+ if (endLine < startLine || endLine - startLine + 1 > (options.maxReadLines ?? 2e3)) {
1709
+ throw new TypeError("requested line range exceeds its bound");
1710
+ }
1711
+ const paths = await registeredFiles(context.workspaceDir, 2e4);
1712
+ const allowed = await policy.read(policyContext(context, request, options, { paths, path, startLine, endLine }));
1713
+ if (!allowed) return response(request, false, "tool denied by CaMeL policy");
1714
+ const target = resolveCodePath(context.workspaceDir, path);
1715
+ const info = await (0, import_promises7.stat)(target);
1716
+ if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
1717
+ throw new TypeError("file is not a bounded regular source file");
1718
+ }
1719
+ const source = await (0, import_promises7.readFile)(target);
1720
+ if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
1721
+ const lines = source.toString("utf8").split("\n");
1722
+ const content = lines.slice(startLine - 1, endLine).join("\n");
1723
+ if (Buffer.byteLength(content) > (options.maxReadBytes ?? 128 * 1024)) {
1724
+ throw new TypeError("read result exceeds its byte bound");
1725
+ }
1726
+ return response(request, true, content, { path, startLine, endLine: Math.min(endLine, lines.length) });
1727
+ }
1728
+ async function patch(context, request, options, policy) {
1729
+ exactKeys(request.input, ["patch"]);
1730
+ const value = stringField(request.input, "patch");
1731
+ const paths = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
1732
+ if (paths.some((path) => options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)))) {
1733
+ throw new TypeError("patch targets a read-only reference source");
1734
+ }
1735
+ const allowed = await policy.patch(policyContext(context, request, options, { patch: value }));
1736
+ if (!allowed) return response(request, false, "tool denied by CaMeL policy");
1737
+ await applyCodePatch(context.workspaceDir, value, paths);
1738
+ return response(request, true, `Applied patch to ${paths.length} file(s).`, { paths });
1739
+ }
1740
+ async function recipe(context, request, options, recipes, policy) {
1741
+ exactKeys(request.input, ["recipeId"]);
1742
+ const recipeId = stringField(request.input, "recipeId");
1743
+ const digestLimits = {
1744
+ maxFiles: options.maxRecipeWorkspaceFiles ?? 2e4,
1745
+ maxBytes: options.maxRecipeWorkspaceBytes ?? 512 * 1024 * 1024
1746
+ };
1747
+ const sourceDigest = await digestStagedWorkspace(context.workspaceDir, digestLimits);
1748
+ const allowed = await policy.recipe(policyContext(context, request, options, {
1749
+ recipeIds: [...recipes.keys()].sort(),
1750
+ recipeId,
1751
+ sourceDigest
1752
+ }));
1753
+ if (!allowed) return response(request, false, "tool denied by CaMeL policy");
1754
+ const selected = recipes.get(recipeId);
1755
+ if (!selected) return response(request, false, "build recipe is not registered");
1756
+ const staged = await stageWorkspace(context.workspaceDir, {
1757
+ maxFiles: digestLimits.maxFiles,
1758
+ maxBytes: digestLimits.maxBytes
1759
+ });
1760
+ try {
1761
+ if (await digestStagedWorkspace(staged.workspaceDir, digestLimits) !== sourceDigest) {
1762
+ throw new TypeError("workspace changed after recipe authorization");
1763
+ }
1764
+ const result = await options.recipeExecutor.run({
1765
+ workspaceDir: staged.workspaceDir,
1766
+ recipe: selected,
1767
+ signal: context.signal
1768
+ });
1769
+ const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
1770
+ const ok = result.exitCode === 0 && !result.outputLimitExceeded && !result.timedOut;
1771
+ const status = result.timedOut ? "timed out" : result.outputLimitExceeded ? "exceeded output limit" : ok ? "passed" : `failed with exit ${result.exitCode}`;
1772
+ return response(request, ok, `Recipe ${recipeId} ${status}.${output ? `
1773
+ ${output}` : ""}`, {
1774
+ recipeId,
1775
+ exitCode: result.exitCode,
1776
+ durationMs: result.durationMs,
1777
+ outputLimitExceeded: result.outputLimitExceeded,
1778
+ timedOut: result.timedOut
1779
+ });
1780
+ } finally {
1781
+ await staged.cleanup();
1782
+ }
1783
+ }
1784
+ function policyContext(context, request, options, extra) {
1785
+ return {
1786
+ lease: context.lease,
1787
+ request,
1788
+ workspaceId: `workspace:${context.lease.task.attemptId}`,
1789
+ readers: { kind: "principals", principalIds: [options.readerId] },
1790
+ ...extra
1791
+ };
1792
+ }
1793
+ async function registeredFiles(root, limit) {
1794
+ const paths = [];
1795
+ const walk = async (directory) => {
1796
+ for (const entry of await (0, import_promises7.readdir)(directory, { withFileTypes: true })) {
1797
+ if (entry.isSymbolicLink()) throw new TypeError("workspace contains a symbolic link");
1798
+ const target = (0, import_node_path8.resolve)(directory, entry.name);
1799
+ if (entry.isDirectory()) await walk(target);
1800
+ else if (entry.isFile()) {
1801
+ const path = (0, import_node_path8.relative)(root, target).split("\\").join("/");
1802
+ try {
1803
+ validateRelativePath(path);
1804
+ } catch {
1805
+ continue;
1806
+ }
1807
+ paths.push(path);
1808
+ if (paths.length > limit) throw new TypeError("workspace file registry exceeds its bound");
1809
+ }
1810
+ }
1811
+ };
1812
+ await walk((0, import_node_path8.resolve)(root));
1813
+ return paths.sort();
1814
+ }
1815
+ function validateOptions(options) {
1816
+ if (!options.readerId || !options.recipes.length || new Set(options.recipes.map((item) => item.id)).size !== options.recipes.length) {
1817
+ throw new TypeError("Code tool broker requires a reader and unique registered recipes");
1818
+ }
1819
+ for (const recipe2 of options.recipes) assertCodeBuildRecipe(recipe2);
1820
+ if (options.readOnlyPrefixes?.some((prefix) => !/^[A-Za-z0-9_.-]+$/.test(prefix) || prefix === "." || prefix === "..")) {
1821
+ throw new TypeError("Code tool broker read-only prefix is invalid");
1822
+ }
1823
+ }
1824
+ function exactKeys(input, allowed) {
1825
+ if (Object.keys(input).some((key) => !allowed.includes(key))) throw new TypeError("tool input contains an unsupported field");
1826
+ }
1827
+ function stringField(input, name) {
1828
+ const value = input[name];
1829
+ if (typeof value !== "string" || !value) throw new TypeError(`${name} must be a non-empty string`);
1830
+ return value;
1831
+ }
1832
+ function optionalInteger(value) {
1833
+ if (value === void 0) return void 0;
1834
+ if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("line bounds must be positive integers");
1835
+ return value;
1836
+ }
1837
+ function response(request, ok, content, details) {
1838
+ return { requestId: request.requestId, ok, content, ...details ? { details } : {} };
1839
+ }
1840
+
1841
+ // src/code-runtime-broker.ts
1842
+ function createCodeRuntimeToolBroker(input, lease, role) {
1843
+ const broker = createCodeToolBroker({
1844
+ recipes: input.recipes,
1845
+ recipeExecutor: createContainerRecipeExecutor(input.engine),
1846
+ recipeAuthorization: input.recipeAuthorization ?? "registered_recipe",
1847
+ readerId: `code-session:${lease.task.taskId}`,
1848
+ readOnlyPrefixes: [".odla-references"]
1849
+ });
1850
+ return role === "coding" ? broker : { execute: (context, request) => request.tool === "sandbox.read" ? broker.execute(context, request) : Promise.resolve({ requestId: request.requestId, ok: false, content: "review sessions are read-only" }) };
1851
+ }
1852
+
1853
+ // src/code-runtime-inference.ts
1854
+ async function handleCodeRuntimeInference(input) {
1855
+ const { command, metadata, request, state } = input;
1856
+ if (state.tokens >= metadata.maxTokensPerInteraction) {
1857
+ if (!state.noticeEmitted) {
1858
+ state.noticeEmitted = true;
1859
+ await input.event({
1860
+ type: "message",
1861
+ actor: "system",
1862
+ body: `Pi paused at the ${metadata.maxTokensPerInteraction.toLocaleString("en-US")}-token per-interaction limit. Send a new instruction to continue.`
1863
+ }).catch(() => void 0);
1864
+ }
1865
+ return {
1866
+ protocolVersion: HARNESS_PROTOCOL_VERSION,
1867
+ type: "inference.response",
1868
+ requestId: request.requestId,
1869
+ response: {
1870
+ id: `budget:${command.commandId}`,
1871
+ provider: "openai",
1872
+ model: "interaction-budget",
1873
+ role: "assistant",
1874
+ content: [{ type: "text", text: "Pause now. The owner-set token limit for this interaction has been reached." }],
1875
+ stopReason: "end_turn",
1876
+ usage: { inputTokens: 0, outputTokens: 0 }
1877
+ }
1878
+ };
1879
+ }
1880
+ const startedAt = Date.now();
1881
+ const response2 = await input.control.infer(command.sessionId, {
1882
+ requestId: request.requestId,
1883
+ interactionId: command.commandId,
1884
+ call: request.call
1885
+ });
1886
+ state.tokens += response2.receipt.inputTokens + response2.receipt.outputTokens;
1887
+ await input.event({
1888
+ type: "usage",
1889
+ provider: response2.receipt.provider,
1890
+ model: response2.receipt.model,
1891
+ inputTokens: response2.receipt.inputTokens,
1892
+ outputTokens: response2.receipt.outputTokens,
1893
+ durationMs: Date.now() - startedAt,
1894
+ interactionId: command.commandId,
1895
+ interactionTokens: state.tokens,
1896
+ interactionMaxTokens: metadata.maxTokensPerInteraction
1897
+ }).catch(() => void 0);
1898
+ return {
1899
+ protocolVersion: HARNESS_PROTOCOL_VERSION,
1900
+ type: "inference.response",
1901
+ requestId: request.requestId,
1902
+ response: response2.response
1903
+ };
1904
+ }
1905
+
1906
+ // src/code-runtime-events.ts
1907
+ var import_node_crypto4 = require("crypto");
1908
+ async function appendCodeRuntimeEvent(control, command, event, refs) {
1909
+ const eventId = `${command.commandId.slice(0, 45)}:${refs.length + 1}`;
1910
+ refs.push(eventId);
1911
+ const bounded = event.type === "message" ? { ...event, body: event.body.trim().slice(0, 2e4) || `${event.actor} event` } : event;
1912
+ await control.appendSessionEvent(command.sessionId, eventId, bounded);
1913
+ }
1914
+ var digestRuntimeValue = (value) => `sha256:${(0, import_node_crypto4.createHash)("sha256").update(value).digest("hex")}`;
1915
+ var runtimeErrorMessage = (value) => value instanceof Error ? value.message : String(value);
1916
+ var runtimeRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
1917
+ var safeRuntimeJson = (value) => {
1918
+ try {
1919
+ return JSON.stringify(value).slice(0, 1e4);
1920
+ } catch {
1921
+ return "[event]";
1922
+ }
1923
+ };
1924
+ function runtimeResultText(value) {
1925
+ const record4 = runtimeRecord(value);
1926
+ if (record4 && typeof record4.text === "string") return record4.text.slice(0, 2e4);
1927
+ if (record4 && typeof record4.error === "string") return `Pi failed: ${record4.error.slice(0, 19989)}`;
1928
+ return null;
1929
+ }
1930
+ function runtimeResultError(value) {
1931
+ const record4 = runtimeRecord(value);
1932
+ return record4 && typeof record4.error === "string" && record4.error.trim() ? record4.error.trim().slice(0, 2e3) : null;
1933
+ }
1934
+
1935
+ // src/code-runtime-engine.ts
1936
+ var CodePiRuntimeEngine = class {
1937
+ constructor(options) {
1938
+ this.options = options;
1939
+ if (options.imageAuthorization === "cli_embedded" && !/^odla-ai\/pi-agent:embedded-sha256-[0-9a-f]{64}$/.test(options.image)) throw new TypeError("CLI-embedded Pi image must use its content-addressed local tag");
1940
+ this.#run = options.runAttempt ?? runContainerAttempt;
1941
+ this.#buildPolicyDigest = digestRuntimeValue(JSON.stringify(options.recipes));
1942
+ this.#checkpoints = new CodeRuntimeCheckpointManager({
1943
+ control: options.control,
1944
+ recipes: options.recipes,
1945
+ recipeExecutor: options.recipeExecutor ?? createContainerRecipeExecutor(options.engine),
1946
+ fallbackPolicyDigest: this.#buildPolicyDigest,
1947
+ event: (command, event, refs) => this.#event(command, event, refs)
1948
+ });
1949
+ }
1950
+ options;
1951
+ #active = /* @__PURE__ */ new Map();
1952
+ #run;
1953
+ #buildPolicyDigest;
1954
+ #checkpoints;
1955
+ execute(command) {
1956
+ if (command.kind === "checkpoint_stop") return this.#checkpoint(command);
1957
+ if (command.kind === "prompt") return this.#prompt(command);
1958
+ return this.#start(command, command.kind === "resume");
1959
+ }
1960
+ async acknowledged(command, result) {
1961
+ if (await this.#checkpoints.acknowledged(command, result)) return;
1962
+ const active = this.#active.get(command.sessionId);
1963
+ if (!active || result.status !== "running") return;
1964
+ active.acknowledged = true;
1965
+ if (active.failure) await this.options.control.reportSessionFailure(command.sessionId, active.failure).catch(() => void 0);
1966
+ }
1967
+ async close() {
1968
+ const sessions = [...this.#active.values()];
1969
+ for (const session of sessions) session.abort.abort("runtime_shutdown");
1970
+ await Promise.allSettled(sessions.map((session) => session.done));
1971
+ await Promise.allSettled(sessions.map((session) => session.workspace.cleanup()));
1972
+ this.#active.clear();
1973
+ }
1974
+ async #start(command, resume) {
1975
+ if (this.#active.has(command.sessionId)) throw new TypeError("Code session is already active on this runtime");
1976
+ const metadata = codeCommandMetadata(command.payload, resume);
1977
+ const requestedLocal = codeLocalSource(command.payload);
1978
+ let workspace;
1979
+ let sourceDigest;
1980
+ let localTrustedBaseDigest;
1981
+ if (requestedLocal) {
1982
+ const prepared = await prepareRuntimeLocalSource({
1983
+ command,
1984
+ descriptor: requestedLocal,
1985
+ available: this.options.localSource,
1986
+ repository: metadata.repository,
1987
+ baseCommitSha: metadata.baseCommitSha,
1988
+ resume
1989
+ });
1990
+ ({ workspace, sourceDigest, trustedBaseDigest: localTrustedBaseDigest } = prepared);
1991
+ if (command.payload.sourceSet) {
1992
+ const selected = await this.options.control.source(command.sessionId);
1993
+ if (selected.repository !== metadata.repository || selected.commitSha !== metadata.baseCommitSha || selected.treeDigest !== metadata.sourceTreeDigest) {
1994
+ await workspace.cleanup();
1995
+ throw new TypeError("Code local source does not match the selected GitHub primary source");
1996
+ }
1997
+ await attachCodeRuntimeReferences(workspace, selected.references ?? []);
1998
+ }
1999
+ } else {
2000
+ const source = await this.options.control.source(command.sessionId);
2001
+ const materialized = await materializeCodeRuntimeSource(source);
2002
+ try {
2003
+ workspace = resume ? (await restoreCodeWorkspaceCheckpoint({
2004
+ trustedBaseDir: materialized.sourceDir,
2005
+ trustedBaseCommitSha: source.commitSha,
2006
+ checkpoint: codeCheckpointPayload(command.payload)
2007
+ })).workspace : await stageWorkspace(materialized.sourceDir);
2008
+ } finally {
2009
+ await materialized.cleanup();
2010
+ }
2011
+ sourceDigest = source.treeDigest;
2012
+ }
2013
+ const abort = new AbortController();
2014
+ const conversationRefs = [];
2015
+ const active = {
2016
+ workspace,
2017
+ abort,
2018
+ conversationRefs,
2019
+ acknowledged: false,
2020
+ role: metadata.role,
2021
+ title: metadata.title,
2022
+ maxTokensPerInteraction: metadata.maxTokensPerInteraction,
2023
+ baseCommitSha: metadata.baseCommitSha,
2024
+ repository: metadata.repository,
2025
+ sourceTreeDigest: metadata.sourceTreeDigest,
2026
+ trustedBaseDigest: requestedLocal ? localTrustedBaseDigest : await digestStagedWorkspace(workspace.baselineDir, {
2027
+ maxFiles: 2e4,
2028
+ maxBytes: 512 * 1024 * 1024
2029
+ }),
2030
+ planningInputDigest: metadata.planningInputDigest ?? digestRuntimeValue(
2031
+ JSON.stringify({ attestation: metadata.attestationDigest, prompt: metadata.prompt, tree: sourceDigest })
2032
+ ),
2033
+ done: Promise.resolve(null)
2034
+ };
2035
+ this.#active.set(command.sessionId, active);
2036
+ if (requestedLocal) {
2037
+ await this.#event(command, {
2038
+ type: "message",
2039
+ actor: "system",
2040
+ body: `Source snapshot: local checkout ${requestedLocal.snapshotDigest} \xB7 ${requestedLocal.modified ? "modified" : "clean"} \xB7 Git ${requestedLocal.headCommitSha}`
2041
+ }, conversationRefs);
2042
+ }
2043
+ active.done = this.#runAttempt(command, metadata, active).catch(async (cause) => {
2044
+ const detail = runtimeErrorMessage(cause);
2045
+ await this.#event(command, { type: "message", actor: "system", body: `Pi failed: ${detail}` }, conversationRefs).catch(() => void 0);
2046
+ await this.#diagnostic(command, active, detail);
2047
+ await this.#event(command, { type: "status", status: "failed" }, conversationRefs).catch(() => void 0);
2048
+ await this.#failure(command, active, detail);
2049
+ return null;
2050
+ });
2051
+ return { status: "running", message: resume ? "Pi resumed from a portable checkpoint" : "Pi started" };
2052
+ }
2053
+ async #prompt(command) {
2054
+ const active = this.#active.get(command.sessionId);
2055
+ const prompt = command.payload.prompt;
2056
+ if (!active || typeof prompt !== "string" || !prompt.trim() || prompt.length > 2e4) {
2057
+ throw new TypeError("prompt requires an active Code session and bounded text");
2058
+ }
2059
+ const requestedLimit = command.payload.maxTokensPerInteraction ?? active.maxTokensPerInteraction;
2060
+ if (!Number.isSafeInteger(requestedLimit) || Number(requestedLimit) < 4e3 || Number(requestedLimit) > 2e5) {
2061
+ throw new TypeError("prompt requires a valid interaction token limit");
2062
+ }
2063
+ active.maxTokensPerInteraction = Number(requestedLimit);
2064
+ await active.done;
2065
+ active.abort = new AbortController();
2066
+ active.acknowledged = false;
2067
+ active.failure = void 0;
2068
+ active.done = this.#runAttempt(command, {
2069
+ role: active.role,
2070
+ title: active.title,
2071
+ prompt,
2072
+ maxTokensPerInteraction: active.maxTokensPerInteraction,
2073
+ planningInputDigest: active.planningInputDigest,
2074
+ attestationDigest: "follow-up",
2075
+ repository: active.repository,
2076
+ baseCommitSha: active.baseCommitSha,
2077
+ sourceTreeDigest: active.sourceTreeDigest
2078
+ }, active).catch(async (cause) => {
2079
+ const detail = runtimeErrorMessage(cause);
2080
+ await this.#event(
2081
+ command,
2082
+ { type: "message", actor: "system", body: `Pi failed: ${detail}` },
2083
+ active.conversationRefs
2084
+ ).catch(() => void 0);
2085
+ await this.#diagnostic(command, active, detail);
2086
+ await this.#event(command, { type: "status", status: "failed" }, active.conversationRefs).catch(() => void 0);
2087
+ await this.#failure(command, active, detail);
2088
+ return null;
2089
+ });
2090
+ return { status: "running", message: "Pi accepted the owner prompt" };
2091
+ }
2092
+ async #runAttempt(command, metadata, active) {
2093
+ const lease = fakeCodeLease(command, metadata);
2094
+ const broker = createCodeRuntimeToolBroker({
2095
+ recipes: this.options.recipes,
2096
+ engine: this.options.engine,
2097
+ recipeAuthorization: this.options.recipeAuthorization
2098
+ }, lease, metadata.role);
2099
+ const startedAt = Date.now();
2100
+ let completionSeen = false;
2101
+ const interaction = { tokens: 0, noticeEmitted: false };
2102
+ const result = await this.#run({
2103
+ engine: this.options.engine,
2104
+ image: this.options.image,
2105
+ allowUnpinnedImage: this.options.imageAuthorization === "cli_embedded",
2106
+ workspaceDir: active.workspace.workspaceDir,
2107
+ workspaceAccess: "none",
2108
+ task: lease.task,
2109
+ limits: this.options.limits,
2110
+ signal: active.abort.signal,
2111
+ onStderr: (text) => this.#event(command, {
2112
+ type: "message",
2113
+ actor: "system",
2114
+ body: text.slice(0, 4e3)
2115
+ }, active.conversationRefs),
2116
+ onMessage: async (output) => {
2117
+ if (output.type === "inference.request") {
2118
+ return handleCodeRuntimeInference({
2119
+ command,
2120
+ metadata,
2121
+ request: output,
2122
+ state: interaction,
2123
+ control: this.options.control,
2124
+ event: (event) => this.#event(
2125
+ command,
2126
+ event,
2127
+ active.conversationRefs
2128
+ )
2129
+ });
2130
+ }
2131
+ if (output.type === "tool.request") {
2132
+ const toolStarted = Date.now();
2133
+ await this.#event(
2134
+ command,
2135
+ { type: "tool", phase: "started", tool: output.tool },
2136
+ active.conversationRefs
2137
+ ).catch(() => void 0);
2138
+ const response2 = await broker.execute({
2139
+ lease,
2140
+ workspaceDir: active.workspace.workspaceDir,
2141
+ signal: active.abort.signal
2142
+ }, output);
2143
+ await this.#event(command, {
2144
+ type: "tool",
2145
+ phase: "completed",
2146
+ tool: output.tool,
2147
+ ok: response2.ok,
2148
+ durationMs: Date.now() - toolStarted
2149
+ }, active.conversationRefs).catch(() => void 0);
2150
+ return { protocolVersion: HARNESS_PROTOCOL_VERSION, type: "tool.response", ...response2 };
2151
+ }
2152
+ if (output.type === "event") {
2153
+ const payload = runtimeRecord(output.payload);
2154
+ if (output.kind === "pi.started") {
2155
+ await this.#event(command, { type: "status", status: "running" }, active.conversationRefs);
2156
+ } else if (output.kind === "pi.thinking" && payload?.available === true && Number.isSafeInteger(payload.durationMs) && Number(payload.durationMs) >= 0) {
2157
+ await this.#event(command, {
2158
+ type: "thinking",
2159
+ available: true,
2160
+ durationMs: Math.min(Number(payload.durationMs), 864e5)
2161
+ }, active.conversationRefs);
2162
+ } else {
2163
+ await this.#event(command, {
2164
+ type: "message",
2165
+ actor: "system",
2166
+ body: `${output.kind}${output.payload === void 0 ? "" : ` ${safeRuntimeJson(output.payload)}`}`
2167
+ }, active.conversationRefs);
2168
+ }
2169
+ } else if (output.type === "attempt.complete") {
2170
+ completionSeen = true;
2171
+ const body = runtimeResultText(output.result) ?? `Pi ${output.status}.`;
2172
+ await this.#event(command, {
2173
+ type: "message",
2174
+ actor: output.status === "completed" ? "agent" : "system",
2175
+ body
2176
+ }, active.conversationRefs);
2177
+ await this.#event(command, {
2178
+ type: "status",
2179
+ status: output.status === "completed" ? "idle" : "failed",
2180
+ durationMs: Date.now() - startedAt
2181
+ }, active.conversationRefs);
2182
+ }
2183
+ }
2184
+ });
2185
+ if (result.status === "failed" && result.stderr) {
2186
+ await this.#event(command, { type: "message", actor: "system", body: result.stderr.slice(0, 4e3) }, active.conversationRefs);
2187
+ }
2188
+ if (!completionSeen) await this.#event(command, {
2189
+ type: "status",
2190
+ status: result.status === "completed" ? "idle" : "failed",
2191
+ durationMs: Date.now() - startedAt
2192
+ }, active.conversationRefs).catch(() => void 0);
2193
+ if (result.status === "failed") {
2194
+ const detail = (runtimeResultError(result.result) ?? result.stderr.trim()) || "Pi container failed";
2195
+ await this.#diagnostic(command, active, detail);
2196
+ await this.#failure(command, active, detail);
2197
+ }
2198
+ return result;
2199
+ }
2200
+ async #checkpoint(command) {
2201
+ const active = this.#active.get(command.sessionId);
2202
+ if (!active) throw new TypeError("Code session workspace is not active on this runtime");
2203
+ const result = await this.#checkpoints.prepare(command, active);
2204
+ this.#active.delete(command.sessionId);
2205
+ return result;
2206
+ }
2207
+ async #failure(command, active, value) {
2208
+ active.failure = value.slice(0, 2e3);
2209
+ if (active.acknowledged) {
2210
+ await this.options.control.reportSessionFailure(command.sessionId, active.failure).catch(() => void 0);
2211
+ }
2212
+ }
2213
+ async #diagnostic(command, active, value) {
2214
+ const detail = value.trim().slice(0, 2e3) || "Pi runtime failed";
2215
+ this.options.onDiagnostic?.(detail);
2216
+ await this.#event(
2217
+ command,
2218
+ { type: "diagnostic", level: "error", message: detail },
2219
+ active.conversationRefs
2220
+ ).catch(() => void 0);
2221
+ }
2222
+ async #event(command, event, refs) {
2223
+ await appendCodeRuntimeEvent(this.options.control, command, event, refs);
2224
+ }
2225
+ };
2226
+
2227
+ // src/code-runtime-cli.ts
2228
+ var VERSION = "0.1.0";
2229
+ function usage() {
2230
+ return `Usage:
2231
+ ODLA_CODE_HOST_TOKEN=odla_code_host_... odla-code-runtime \\
2232
+ --endpoint https://odla.ai --image registry/odla-pi@sha256:... \\
2233
+ --build-policy ./odla-code-build.json [--engine auto|container|podman|docker] [--once]
2234
+
2235
+ The host token is read only from ODLA_CODE_HOST_TOKEN. The runtime makes
2236
+ outbound HTTPS heartbeats and never opens a listener.`;
2237
+ }
2238
+ function parse(argv) {
2239
+ const values = /* @__PURE__ */ new Map();
2240
+ const flags = /* @__PURE__ */ new Set();
2241
+ for (let index = 0; index < argv.length; index++) {
2242
+ const arg = argv[index];
2243
+ if (arg === "--once") {
2244
+ flags.add(arg);
2245
+ continue;
2246
+ }
2247
+ if (!arg.startsWith("--") || !argv[index + 1]) throw new TypeError(usage());
2248
+ values.set(arg, argv[++index]);
2249
+ }
2250
+ const endpoint = values.get("--endpoint");
2251
+ const image = values.get("--image");
2252
+ const buildPolicy = values.get("--build-policy");
2253
+ const engine = values.get("--engine") ?? "auto";
2254
+ const heartbeatMs = Number(values.get("--heartbeat-ms") ?? 15e3);
2255
+ if (!endpoint || !image || !buildPolicy || !["auto", "container", "podman", "docker"].includes(engine)) {
2256
+ throw new TypeError(usage());
2257
+ }
2258
+ assertPinnedImage(image);
2259
+ if (!Number.isSafeInteger(heartbeatMs) || heartbeatMs < 1e3 || heartbeatMs > 3e5) {
2260
+ throw new TypeError("--heartbeat-ms must be an integer from 1000 to 300000");
2261
+ }
2262
+ return {
2263
+ endpoint,
2264
+ image,
2265
+ buildPolicy,
2266
+ engine,
2267
+ heartbeatMs,
2268
+ once: flags.has("--once")
2269
+ };
2270
+ }
2271
+ async function readPolicy(path) {
2272
+ const value = JSON.parse(await (0, import_promises8.readFile)(path, "utf8"));
2273
+ if (!value || Object.keys(value).some((key) => !["recipes", "recipeAuthorization"].includes(key)) || !Array.isArray(value.recipes) || !value.recipes.length || value.recipeAuthorization !== void 0 && value.recipeAuthorization !== "registered_recipe" && value.recipeAuthorization !== "exact_approval") throw new TypeError("invalid Code build policy file");
2274
+ const recipes = value.recipes;
2275
+ for (const recipe2 of recipes) assertCodeBuildRecipe(recipe2);
2276
+ const recipeAuthorization = value.recipeAuthorization === "exact_approval" ? "exact_approval" : "registered_recipe";
2277
+ return { recipes, recipeAuthorization };
2278
+ }
2279
+ async function main() {
2280
+ const token = process.env.ODLA_CODE_HOST_TOKEN;
2281
+ if (!token) throw new TypeError("ODLA_CODE_HOST_TOKEN is required");
2282
+ const options = parse(process.argv.slice(2));
2283
+ if (process.platform !== "darwin" && process.platform !== "linux") throw new TypeError("Code runtime requires macOS or Linux");
2284
+ const engine = await selectContainerEngine(options.engine);
2285
+ const policy = await readPolicy(options.buildPolicy);
2286
+ const capabilities = {
2287
+ protocolVersion: CODE_RUNTIME_PROTOCOL_VERSION,
2288
+ platform: process.platform === "darwin" ? "macos" : "linux",
2289
+ arch: process.arch,
2290
+ engines: [engine],
2291
+ cpuCount: (0, import_node_os3.cpus)().length,
2292
+ memoryBytes: (0, import_node_os3.totalmem)()
2293
+ };
2294
+ const controller = new AbortController();
2295
+ for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => controller.abort(signal));
2296
+ const control = createCodeRuntimeControlClient({ endpoint: options.endpoint, token, signal: controller.signal });
2297
+ const commandEngine = new CodePiRuntimeEngine({
2298
+ control,
2299
+ engine,
2300
+ image: options.image,
2301
+ recipes: policy.recipes,
2302
+ recipeAuthorization: policy.recipeAuthorization,
2303
+ onDiagnostic: (message2) => process.stderr.write(`[odla-code-runtime] Pi failed \xB7 ${message2}
2304
+ `)
2305
+ });
2306
+ const reconciler = new CodeRuntimeReconciler(control, commandEngine);
2307
+ try {
2308
+ await runCodeRuntimeHeartbeatLoop({
2309
+ control,
2310
+ runtimeVersion: VERSION,
2311
+ capabilities,
2312
+ heartbeatMs: options.heartbeatMs,
2313
+ once: options.once,
2314
+ signal: controller.signal,
2315
+ onSnapshot: async (snapshot) => {
2316
+ process.stderr.write(
2317
+ `[odla-code-runtime] host ${snapshot.host.hostId} online \xB7 ${snapshot.bindings.length} active binding(s) \xB7 ${snapshot.commands.length} command(s)
2318
+ `
2319
+ );
2320
+ if (options.once && snapshot.commands.length) {
2321
+ throw new TypeError("--once is diagnostic-only and refuses pending Code commands");
2322
+ }
2323
+ await reconciler.reconcile(snapshot);
2324
+ },
2325
+ onRetry: (error, delayMs) => {
2326
+ process.stderr.write(
2327
+ `[odla-code-runtime] control plane unavailable; retrying in ${delayMs}ms \xB7 ${error instanceof Error ? error.message : String(error)}
2328
+ `
2329
+ );
2330
+ }
2331
+ });
2332
+ } finally {
2333
+ await commandEngine.close();
2334
+ }
2335
+ }
2336
+ main().catch((error) => {
2337
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
2338
+ `);
2339
+ process.exitCode = 1;
2340
+ });
2341
+ //# sourceMappingURL=code-runtime-cli.cjs.map