@odla-ai/harness 0.10.4 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -109,6 +109,30 @@ before approval:
109
109
  }
110
110
  ~~~
111
111
 
112
+ A repository can replace that release-owned list with its own. Code reads
113
+ `odla.recipes.json` from the root of the trusted base (the default-branch
114
+ snapshot it staged, never the candidate's working tree) when a session starts:
115
+
116
+ ~~~json
117
+ {
118
+ "version": 1,
119
+ "recipes": [
120
+ { "id": "engine-tests", "command": ["node", "--test", "test/"] },
121
+ { "id": "gates", "command": ["node", "scripts/gates.mjs"], "timeoutMs": 60000 }
122
+ ]
123
+ }
124
+ ~~~
125
+
126
+ The repository chooses each recipe's `id`, argv `command`, and optional
127
+ `timeoutMs` (default 120000, at most 900000, up to 16 recipes); the host
128
+ supplies the digest-pinned image and the resource limits, and a declaration that
129
+ names an `image` or any other field is refused. A malformed file fails the
130
+ session start with the fault named rather than falling back, and the session
131
+ thread's first system message says which list gates it and where that list came
132
+ from. Every declared recipe runs in the same fresh, networkless container as the
133
+ release list, so a repository whose tests need `node_modules` still cannot run
134
+ them here.
135
+
112
136
  The runtime validates HTTPS, never places its credential in a request body,
113
137
  never opens a listener, and rejects malformed or cross-host binding responses.
114
138
  Theseus reads, patches, and runs only registered build recipes through the typed
@@ -260,40 +260,40 @@ async function readBoundedResponse(body, maximum) {
260
260
  return result;
261
261
  }
262
262
 
263
- // src/code-runtime.ts
264
- var CODE_RUNTIME_PROTOCOL_VERSION = 3;
265
- async function runCodeRuntimeHeartbeatLoop(options) {
266
- const heartbeatMs = options.heartbeatMs ?? 15e3;
267
- if (!Number.isSafeInteger(heartbeatMs) || heartbeatMs < 1e3 || heartbeatMs > 3e5) {
268
- throw new TypeError("heartbeatMs must be an integer from 1000 to 300000");
269
- }
270
- let retryMs = 1e3;
271
- do {
272
- if (options.signal?.aborted) return;
263
+ // src/code-runtime-overload.ts
264
+ var OVERLOAD_RETRY_DELAYS_MS = [2e3, 4e3, 8e3, 16e3];
265
+ var RETRYABLE_CODES = /* @__PURE__ */ new Set(["control_plane_overloaded", "registry_overloaded", "transport_unavailable"]);
266
+ function overloadedControlFailure(cause) {
267
+ if (!cause || typeof cause !== "object") return false;
268
+ const failure = cause;
269
+ return failure.status === 503 && typeof failure.code === "string" && RETRYABLE_CODES.has(failure.code);
270
+ }
271
+ async function withOverloadRetry(call, wait2, onRetry = () => void 0) {
272
+ for (let attempt = 0; ; attempt += 1) {
273
273
  try {
274
- const snapshot = await options.control.heartbeat(options.runtimeVersion, options.capabilities);
275
- await options.onSnapshot?.(snapshot);
276
- retryMs = 1e3;
277
- if (options.once) return;
278
- await wait(heartbeatMs, options.signal);
279
- } catch (error) {
280
- if (options.signal?.aborted) return;
281
- if (options.once || !retryableControlFailure(error)) throw error;
282
- await options.onRetry?.(error, retryMs);
283
- await wait(retryMs, options.signal);
284
- retryMs = Math.min(retryMs * 2, 3e4);
274
+ return await call();
275
+ } catch (cause) {
276
+ const delayMs = OVERLOAD_RETRY_DELAYS_MS[attempt];
277
+ if (delayMs === void 0 || !overloadedControlFailure(cause)) throw cause;
278
+ await onRetry(cause, delayMs);
279
+ await wait2(delayMs);
285
280
  }
286
- } while (!options.signal?.aborted);
281
+ }
287
282
  }
283
+
284
+ // src/code-runtime-reconciler.ts
285
+ var sleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
288
286
  var CodeRuntimeReconciler = class {
289
- constructor(control, engine, onDiagnostic) {
287
+ constructor(control, engine, onDiagnostic, options = {}) {
290
288
  this.control = control;
291
289
  this.engine = engine;
292
290
  this.onDiagnostic = onDiagnostic;
291
+ this.options = options;
293
292
  }
294
293
  control;
295
294
  engine;
296
295
  onDiagnostic;
296
+ options;
297
297
  results = /* @__PURE__ */ new Map();
298
298
  async reconcile(snapshot) {
299
299
  for (const command of snapshot.commands) {
@@ -312,7 +312,15 @@ var CodeRuntimeReconciler = class {
312
312
  await this.control.acknowledge(command.commandId, completed.result);
313
313
  if (!completed.notified) {
314
314
  try {
315
- await this.engine.acknowledged?.(command, completed.result);
315
+ await withOverloadRetry(
316
+ async () => {
317
+ await this.engine.acknowledged?.(command, completed.result);
318
+ },
319
+ (ms) => (this.options.wait ?? sleep)(ms),
320
+ (cause, delayMs) => this.onDiagnostic?.(
321
+ `command ${command.commandId} acknowledged handling waits ${delayMs}ms for an overloaded control plane \xB7 ${cause instanceof Error ? cause.message : String(cause)}`
322
+ )
323
+ );
316
324
  } catch (error) {
317
325
  this.onDiagnostic?.(
318
326
  `command ${command.commandId} acknowledged handling failed \xB7 ${error instanceof Error ? error.message : String(error)}`
@@ -323,6 +331,32 @@ var CodeRuntimeReconciler = class {
323
331
  }
324
332
  }
325
333
  };
334
+
335
+ // src/code-runtime.ts
336
+ var CODE_RUNTIME_PROTOCOL_VERSION = 3;
337
+ async function runCodeRuntimeHeartbeatLoop(options) {
338
+ const heartbeatMs = options.heartbeatMs ?? 15e3;
339
+ if (!Number.isSafeInteger(heartbeatMs) || heartbeatMs < 1e3 || heartbeatMs > 3e5) {
340
+ throw new TypeError("heartbeatMs must be an integer from 1000 to 300000");
341
+ }
342
+ let retryMs = 1e3;
343
+ do {
344
+ if (options.signal?.aborted) return;
345
+ try {
346
+ const snapshot = await options.control.heartbeat(options.runtimeVersion, options.capabilities);
347
+ await options.onSnapshot?.(snapshot);
348
+ retryMs = 1e3;
349
+ if (options.once) return;
350
+ await wait(heartbeatMs, options.signal);
351
+ } catch (error) {
352
+ if (options.signal?.aborted) return;
353
+ if (options.once || !retryableControlFailure(error)) throw error;
354
+ await options.onRetry?.(error, retryMs);
355
+ await wait(retryMs, options.signal);
356
+ retryMs = Math.min(retryMs * 2, 3e4);
357
+ }
358
+ } while (!options.signal?.aborted);
359
+ }
326
360
  function retryableControlFailure(value) {
327
361
  if (!value || typeof value !== "object") return false;
328
362
  const failure = value;
@@ -1148,8 +1182,8 @@ var CodeRuntimeCheckpointManager = class {
1148
1182
  trustedBaseDigest: active.trustedBaseDigest,
1149
1183
  planningInputDigest: active.planningInputDigest,
1150
1184
  conversationRefs: active.conversationRefs,
1151
- fallbackPolicyDigest: this.options.fallbackPolicyDigest,
1152
- recipes: this.options.recipes,
1185
+ fallbackPolicyDigest: active.buildPolicyDigest ?? this.options.fallbackPolicyDigest,
1186
+ recipes: active.recipes ?? this.options.recipes,
1153
1187
  recipeExecutor: this.options.recipeExecutor,
1154
1188
  review: (patch2, verification) => this.options.control.review(command.sessionId, { patch: patch2, verification })
1155
1189
  });
@@ -2089,13 +2123,19 @@ function createCodeRuntimeSessionSkillLoader(control) {
2089
2123
  ...tool.acceptsTaint === void 0 ? {} : { acceptsTaint: tool.acceptsTaint },
2090
2124
  handler: async (input, context) => {
2091
2125
  if (!context.toolCallId) throw new TypeError("collaboration tool call identity is required");
2092
- return execute2(command.sessionId, {
2093
- commandId: command.commandId,
2094
- toolCallId: context.toolCallId,
2095
- skill: manifest.name,
2096
- tool: tool.name,
2097
- input
2098
- }, context.signal);
2126
+ try {
2127
+ return await execute2(command.sessionId, {
2128
+ commandId: command.commandId,
2129
+ toolCallId: context.toolCallId,
2130
+ skill: manifest.name,
2131
+ tool: tool.name,
2132
+ input
2133
+ }, context.signal);
2134
+ } catch (cause) {
2135
+ if (context.signal?.aborted) throw cause;
2136
+ const detail = (cause instanceof Error ? cause.message : String(cause)).slice(0, 500);
2137
+ return { content: `Tool "${tool.name}" failed: ${detail}`, isError: true };
2138
+ }
2099
2139
  }
2100
2140
  }))
2101
2141
  }));
@@ -2113,23 +2153,7 @@ async function sessionSkillsFor(options, command) {
2113
2153
  }
2114
2154
 
2115
2155
  // src/code-runtime-inference.ts
2116
- var OVERLOAD_RETRY_DELAYS_MS = [2e3, 4e3, 8e3, 16e3];
2117
- var RETRYABLE_CODES = /* @__PURE__ */ new Set(["control_plane_overloaded", "registry_overloaded", "transport_unavailable"]);
2118
- function overloadedControlFailure(cause) {
2119
- return cause instanceof CodeRuntimeControlError && cause.status === 503 && RETRYABLE_CODES.has(cause.code);
2120
- }
2121
- async function inferWithBackoff(infer, wait2, onRetry) {
2122
- for (let attempt = 0; ; attempt += 1) {
2123
- try {
2124
- return await infer();
2125
- } catch (cause) {
2126
- const delayMs = OVERLOAD_RETRY_DELAYS_MS[attempt];
2127
- if (delayMs === void 0 || !overloadedControlFailure(cause)) throw cause;
2128
- await onRetry(cause, delayMs);
2129
- await wait2(delayMs);
2130
- }
2131
- }
2132
- }
2156
+ var inferWithBackoff = (infer, wait2, onRetry) => withOverloadRetry(infer, wait2, (cause, delayMs) => onRetry(cause, delayMs));
2133
2157
  async function handleCodeRuntimeInference(input) {
2134
2158
  const { command, request, state } = input;
2135
2159
  const startedAt = Date.now();
@@ -3150,6 +3174,110 @@ function assertBudget(budget) {
3150
3174
  }
3151
3175
  }
3152
3176
 
3177
+ // src/code-repository-recipes.ts
3178
+ import { readFile as readFile6 } from "fs/promises";
3179
+ import { join as join6 } from "path";
3180
+
3181
+ // src/code-runtime-events.ts
3182
+ import { createHash as createHash3 } from "crypto";
3183
+ async function appendCodeRuntimeEvent(control, command, event, refs) {
3184
+ const eventId = `${command.commandId.slice(0, 45)}:${refs.length + 1}`;
3185
+ refs.push(eventId);
3186
+ const attributed = { ...event, interactionId: command.commandId };
3187
+ const bounded = attributed.type === "message" ? { ...attributed, body: attributed.body.trim().slice(0, 2e4) || `${attributed.actor} event` } : attributed;
3188
+ await control.appendSessionEvent(command.sessionId, eventId, bounded);
3189
+ }
3190
+ var digestRuntimeValue = (value) => `sha256:${createHash3("sha256").update(value).digest("hex")}`;
3191
+ var runtimeErrorMessage = (value) => value instanceof Error ? value.message : String(value);
3192
+
3193
+ // src/code-repository-recipes.ts
3194
+ var REPOSITORY_RECIPES_FILE = "odla.recipes.json";
3195
+ var MAX_RECIPES = 16;
3196
+ var DEFAULT_TIMEOUT_MS = 12e4;
3197
+ var MAX_TIMEOUT_MS = 15 * 6e4;
3198
+ var ID2 = /^[A-Za-z0-9._:-]{1,120}$/;
3199
+ var RECIPE_FIELDS = /* @__PURE__ */ new Set(["id", "command", "timeoutMs"]);
3200
+ var fault = (detail) => new TypeError(`${REPOSITORY_RECIPES_FILE} is malformed: ${detail}`);
3201
+ function parseRepositoryRecipes(text, envelope) {
3202
+ let parsed;
3203
+ try {
3204
+ parsed = JSON.parse(text);
3205
+ } catch {
3206
+ throw fault("not valid JSON");
3207
+ }
3208
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw fault("the document must be an object");
3209
+ const document = parsed;
3210
+ if (document.version !== 1) throw fault("version must be 1");
3211
+ const entries = document.recipes;
3212
+ if (!Array.isArray(entries) || entries.length < 1) throw fault("recipes must be a non-empty array");
3213
+ if (entries.length > MAX_RECIPES) throw fault(`at most ${MAX_RECIPES} recipes may be declared`);
3214
+ const recipes = [];
3215
+ const seen = /* @__PURE__ */ new Set();
3216
+ entries.forEach((entry, index) => {
3217
+ const label = `recipe ${index + 1}`;
3218
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw fault(`${label} must be an object`);
3219
+ const row = entry;
3220
+ const unknown = Object.keys(row).filter((key) => !RECIPE_FIELDS.has(key));
3221
+ if (unknown.length) throw fault(`${label} has unsupported field ${unknown[0]}; the host owns image and resource limits`);
3222
+ if (typeof row.id !== "string" || !ID2.test(row.id)) throw fault(`${label} needs an id of 1 to 120 letters, digits, . _ : or -`);
3223
+ if (seen.has(row.id)) throw fault(`${label} repeats id ${row.id}`);
3224
+ seen.add(row.id);
3225
+ if (!Array.isArray(row.command) || row.command.length < 1 || row.command.some((part) => typeof part !== "string" || !part)) {
3226
+ throw fault(`${label} (${row.id}) needs command as a non-empty array of non-empty strings`);
3227
+ }
3228
+ const timeoutMs = row.timeoutMs ?? DEFAULT_TIMEOUT_MS;
3229
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_TIMEOUT_MS) {
3230
+ throw fault(`${label} (${row.id}) timeoutMs must be an integer from 1 to ${MAX_TIMEOUT_MS}`);
3231
+ }
3232
+ const recipe2 = {
3233
+ id: row.id,
3234
+ command: [...row.command],
3235
+ timeoutMs,
3236
+ image: envelope.image,
3237
+ maxOutputBytes: envelope.maxOutputBytes,
3238
+ cpus: envelope.cpus,
3239
+ memory: envelope.memory,
3240
+ pids: envelope.pids
3241
+ };
3242
+ try {
3243
+ assertCodeBuildRecipe(recipe2);
3244
+ } catch (cause) {
3245
+ throw fault(`${label} (${row.id}) is not a runnable recipe: ${cause instanceof Error ? cause.message : String(cause)}`);
3246
+ }
3247
+ recipes.push(recipe2);
3248
+ });
3249
+ return recipes;
3250
+ }
3251
+ async function readRepositoryRecipes(baselineDir, envelope) {
3252
+ let text;
3253
+ try {
3254
+ text = await readFile6(join6(baselineDir, REPOSITORY_RECIPES_FILE), "utf8");
3255
+ } catch (cause) {
3256
+ if (cause.code === "ENOENT") return null;
3257
+ throw cause;
3258
+ }
3259
+ return parseRepositoryRecipes(text, envelope);
3260
+ }
3261
+ async function resolveCodeRecipes(baselineDir, release, envelope) {
3262
+ const declared = envelope ? await readRepositoryRecipes(baselineDir, envelope) : null;
3263
+ return declared ? { recipes: declared, source: "repository" } : { recipes: release, source: "release" };
3264
+ }
3265
+ function describeCodeRecipes(resolved) {
3266
+ const ids = resolved.recipes.map((recipe2) => recipe2.id).join(", ");
3267
+ return resolved.source === "repository" ? `Verification recipes: declared by the repository in ${REPOSITORY_RECIPES_FILE} \xB7 ${ids}` : `Verification recipes: release-owned (the repository declares none in ${REPOSITORY_RECIPES_FILE}) \xB7 ${ids}`;
3268
+ }
3269
+ async function sessionRecipesFor(workspace, options, resume) {
3270
+ const resolved = await resolveCodeRecipes(workspace.baselineDir, options.recipes, options.repositoryRecipes ?? null).catch(async (cause) => {
3271
+ await workspace.cleanup();
3272
+ throw cause;
3273
+ });
3274
+ return {
3275
+ ...resolved,
3276
+ buildPolicyDigest: digestRuntimeValue(JSON.stringify(resolved.recipes)),
3277
+ note: !resume && resolved.source === "repository" ? describeCodeRecipes(resolved) : null
3278
+ };
3279
+ }
3280
+
3153
3281
  // src/code-runtime-broker.ts
3154
3282
  function createCodeRuntimeToolBroker(input, lease, role) {
3155
3283
  const broker = createCodeToolBroker({
@@ -3331,18 +3459,6 @@ async function startGoalPursuit(input) {
3331
3459
  return { status: run.met ? "completed" : "failed", finalText: "" };
3332
3460
  }
3333
3461
 
3334
- // src/code-runtime-events.ts
3335
- import { createHash as createHash3 } from "crypto";
3336
- async function appendCodeRuntimeEvent(control, command, event, refs) {
3337
- const eventId = `${command.commandId.slice(0, 45)}:${refs.length + 1}`;
3338
- refs.push(eventId);
3339
- const attributed = { ...event, interactionId: command.commandId };
3340
- const bounded = attributed.type === "message" ? { ...attributed, body: attributed.body.trim().slice(0, 2e4) || `${attributed.actor} event` } : attributed;
3341
- await control.appendSessionEvent(command.sessionId, eventId, bounded);
3342
- }
3343
- var digestRuntimeValue = (value) => `sha256:${createHash3("sha256").update(value).digest("hex")}`;
3344
- var runtimeErrorMessage = (value) => value instanceof Error ? value.message : String(value);
3345
-
3346
3462
  // src/code-runtime-acknowledgement-gate.ts
3347
3463
  function codeRuntimeAcknowledgementGate(signal) {
3348
3464
  let settle;
@@ -3464,6 +3580,7 @@ var TheseusRuntimeEngine = class {
3464
3580
  control: this.options.control,
3465
3581
  ...this.options.localSource ? { localSource: this.options.localSource } : {}
3466
3582
  });
3583
+ const resolvedRecipes = await sessionRecipesFor(workspace, this.options, resume);
3467
3584
  const abort = new AbortController(), startGate = codeRuntimeAcknowledgementGate(abort.signal);
3468
3585
  const conversationRefs = [];
3469
3586
  const active = {
@@ -3472,6 +3589,8 @@ var TheseusRuntimeEngine = class {
3472
3589
  conversationRefs,
3473
3590
  acknowledged: false,
3474
3591
  startGate,
3592
+ recipes: resolvedRecipes.recipes,
3593
+ buildPolicyDigest: resolvedRecipes.buildPolicyDigest,
3475
3594
  role: metadata.role,
3476
3595
  readOnly: metadata.readOnly,
3477
3596
  title: metadata.title,
@@ -3489,6 +3608,7 @@ var TheseusRuntimeEngine = class {
3489
3608
  done: Promise.resolve(null)
3490
3609
  };
3491
3610
  this.#active.set(command.sessionId, active);
3611
+ if (resolvedRecipes.note) await this.#event(command, { type: "message", actor: "system", body: resolvedRecipes.note }, conversationRefs);
3492
3612
  if (requestedLocal) {
3493
3613
  await this.#event(command, {
3494
3614
  type: "message",
@@ -3524,7 +3644,7 @@ var TheseusRuntimeEngine = class {
3524
3644
  const active = await this.#takeOver(command, "pursue requires an active Code session");
3525
3645
  active.done = startGoalPursuit({
3526
3646
  spec,
3527
- recipes: this.options.recipes,
3647
+ recipes: active.recipes,
3528
3648
  recipeExecutor: this.options.recipeExecutor ?? createContainerRecipeExecutor(this.options.engine),
3529
3649
  workspace: active.workspace,
3530
3650
  baseCommitSha: active.baseCommitSha,
@@ -3603,7 +3723,7 @@ var TheseusRuntimeEngine = class {
3603
3723
  async #runAttempt(command, metadata, active) {
3604
3724
  const lease = fakeCodeLease(command, metadata);
3605
3725
  const broker = this.#observed(command, active, createCodeRuntimeToolBroker({
3606
- recipes: this.options.recipes,
3726
+ recipes: active.recipes,
3607
3727
  engine: this.options.engine,
3608
3728
  recipeAuthorization: this.options.recipeAuthorization
3609
3729
  }, lease, metadata.role));
@@ -3629,7 +3749,7 @@ var TheseusRuntimeEngine = class {
3629
3749
  // A review-role session is read-only and still runs the proof; a planner
3630
3750
  // marked read-only by its payload runs nothing.
3631
3751
  recipes: metadata.role === "review" || !metadata.readOnly,
3632
- recipeIds: this.options.recipes.map((recipe2) => recipe2.id),
3752
+ recipeIds: active.recipes.map((recipe2) => recipe2.id),
3633
3753
  ...extraSkills.length ? { extraSkills } : {}
3634
3754
  });
3635
3755
  const closing = result.finalText.trim();
@@ -3697,9 +3817,9 @@ export {
3697
3817
  digestStagedWorkspace,
3698
3818
  CodeRuntimeControlError,
3699
3819
  createCodeRuntimeControlClient,
3820
+ CodeRuntimeReconciler,
3700
3821
  CODE_RUNTIME_PROTOCOL_VERSION,
3701
3822
  runCodeRuntimeHeartbeatLoop,
3702
- CodeRuntimeReconciler,
3703
3823
  stripPatchEnvelope,
3704
3824
  applyPatchDialectToDiff,
3705
3825
  validateCodePatch,
@@ -3740,6 +3860,12 @@ export {
3740
3860
  renderMemories,
3741
3861
  hazardFromAttempt,
3742
3862
  runGoal,
3863
+ REPOSITORY_RECIPES_FILE,
3864
+ parseRepositoryRecipes,
3865
+ readRepositoryRecipes,
3866
+ resolveCodeRecipes,
3867
+ describeCodeRecipes,
3868
+ sessionRecipesFor,
3743
3869
  TheseusRuntimeEngine
3744
3870
  };
3745
- //# sourceMappingURL=chunk-IV3VMKDV.js.map
3871
+ //# sourceMappingURL=chunk-DP6LOGBF.js.map