@odla-ai/harness 0.10.4 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
@@ -1148,8 +1148,8 @@ var CodeRuntimeCheckpointManager = class {
1148
1148
  trustedBaseDigest: active.trustedBaseDigest,
1149
1149
  planningInputDigest: active.planningInputDigest,
1150
1150
  conversationRefs: active.conversationRefs,
1151
- fallbackPolicyDigest: this.options.fallbackPolicyDigest,
1152
- recipes: this.options.recipes,
1151
+ fallbackPolicyDigest: active.buildPolicyDigest ?? this.options.fallbackPolicyDigest,
1152
+ recipes: active.recipes ?? this.options.recipes,
1153
1153
  recipeExecutor: this.options.recipeExecutor,
1154
1154
  review: (patch2, verification) => this.options.control.review(command.sessionId, { patch: patch2, verification })
1155
1155
  });
@@ -2089,13 +2089,19 @@ function createCodeRuntimeSessionSkillLoader(control) {
2089
2089
  ...tool.acceptsTaint === void 0 ? {} : { acceptsTaint: tool.acceptsTaint },
2090
2090
  handler: async (input, context) => {
2091
2091
  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);
2092
+ try {
2093
+ return await execute2(command.sessionId, {
2094
+ commandId: command.commandId,
2095
+ toolCallId: context.toolCallId,
2096
+ skill: manifest.name,
2097
+ tool: tool.name,
2098
+ input
2099
+ }, context.signal);
2100
+ } catch (cause) {
2101
+ if (context.signal?.aborted) throw cause;
2102
+ const detail = (cause instanceof Error ? cause.message : String(cause)).slice(0, 500);
2103
+ return { content: `Tool "${tool.name}" failed: ${detail}`, isError: true };
2104
+ }
2099
2105
  }
2100
2106
  }))
2101
2107
  }));
@@ -3150,6 +3156,110 @@ function assertBudget(budget) {
3150
3156
  }
3151
3157
  }
3152
3158
 
3159
+ // src/code-repository-recipes.ts
3160
+ import { readFile as readFile6 } from "fs/promises";
3161
+ import { join as join6 } from "path";
3162
+
3163
+ // src/code-runtime-events.ts
3164
+ import { createHash as createHash3 } from "crypto";
3165
+ async function appendCodeRuntimeEvent(control, command, event, refs) {
3166
+ const eventId = `${command.commandId.slice(0, 45)}:${refs.length + 1}`;
3167
+ refs.push(eventId);
3168
+ const attributed = { ...event, interactionId: command.commandId };
3169
+ const bounded = attributed.type === "message" ? { ...attributed, body: attributed.body.trim().slice(0, 2e4) || `${attributed.actor} event` } : attributed;
3170
+ await control.appendSessionEvent(command.sessionId, eventId, bounded);
3171
+ }
3172
+ var digestRuntimeValue = (value) => `sha256:${createHash3("sha256").update(value).digest("hex")}`;
3173
+ var runtimeErrorMessage = (value) => value instanceof Error ? value.message : String(value);
3174
+
3175
+ // src/code-repository-recipes.ts
3176
+ var REPOSITORY_RECIPES_FILE = "odla.recipes.json";
3177
+ var MAX_RECIPES = 16;
3178
+ var DEFAULT_TIMEOUT_MS = 12e4;
3179
+ var MAX_TIMEOUT_MS = 15 * 6e4;
3180
+ var ID2 = /^[A-Za-z0-9._:-]{1,120}$/;
3181
+ var RECIPE_FIELDS = /* @__PURE__ */ new Set(["id", "command", "timeoutMs"]);
3182
+ var fault = (detail) => new TypeError(`${REPOSITORY_RECIPES_FILE} is malformed: ${detail}`);
3183
+ function parseRepositoryRecipes(text, envelope) {
3184
+ let parsed;
3185
+ try {
3186
+ parsed = JSON.parse(text);
3187
+ } catch {
3188
+ throw fault("not valid JSON");
3189
+ }
3190
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw fault("the document must be an object");
3191
+ const document = parsed;
3192
+ if (document.version !== 1) throw fault("version must be 1");
3193
+ const entries = document.recipes;
3194
+ if (!Array.isArray(entries) || entries.length < 1) throw fault("recipes must be a non-empty array");
3195
+ if (entries.length > MAX_RECIPES) throw fault(`at most ${MAX_RECIPES} recipes may be declared`);
3196
+ const recipes = [];
3197
+ const seen = /* @__PURE__ */ new Set();
3198
+ entries.forEach((entry, index) => {
3199
+ const label = `recipe ${index + 1}`;
3200
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw fault(`${label} must be an object`);
3201
+ const row = entry;
3202
+ const unknown = Object.keys(row).filter((key) => !RECIPE_FIELDS.has(key));
3203
+ if (unknown.length) throw fault(`${label} has unsupported field ${unknown[0]}; the host owns image and resource limits`);
3204
+ if (typeof row.id !== "string" || !ID2.test(row.id)) throw fault(`${label} needs an id of 1 to 120 letters, digits, . _ : or -`);
3205
+ if (seen.has(row.id)) throw fault(`${label} repeats id ${row.id}`);
3206
+ seen.add(row.id);
3207
+ if (!Array.isArray(row.command) || row.command.length < 1 || row.command.some((part) => typeof part !== "string" || !part)) {
3208
+ throw fault(`${label} (${row.id}) needs command as a non-empty array of non-empty strings`);
3209
+ }
3210
+ const timeoutMs = row.timeoutMs ?? DEFAULT_TIMEOUT_MS;
3211
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_TIMEOUT_MS) {
3212
+ throw fault(`${label} (${row.id}) timeoutMs must be an integer from 1 to ${MAX_TIMEOUT_MS}`);
3213
+ }
3214
+ const recipe2 = {
3215
+ id: row.id,
3216
+ command: [...row.command],
3217
+ timeoutMs,
3218
+ image: envelope.image,
3219
+ maxOutputBytes: envelope.maxOutputBytes,
3220
+ cpus: envelope.cpus,
3221
+ memory: envelope.memory,
3222
+ pids: envelope.pids
3223
+ };
3224
+ try {
3225
+ assertCodeBuildRecipe(recipe2);
3226
+ } catch (cause) {
3227
+ throw fault(`${label} (${row.id}) is not a runnable recipe: ${cause instanceof Error ? cause.message : String(cause)}`);
3228
+ }
3229
+ recipes.push(recipe2);
3230
+ });
3231
+ return recipes;
3232
+ }
3233
+ async function readRepositoryRecipes(baselineDir, envelope) {
3234
+ let text;
3235
+ try {
3236
+ text = await readFile6(join6(baselineDir, REPOSITORY_RECIPES_FILE), "utf8");
3237
+ } catch (cause) {
3238
+ if (cause.code === "ENOENT") return null;
3239
+ throw cause;
3240
+ }
3241
+ return parseRepositoryRecipes(text, envelope);
3242
+ }
3243
+ async function resolveCodeRecipes(baselineDir, release, envelope) {
3244
+ const declared = envelope ? await readRepositoryRecipes(baselineDir, envelope) : null;
3245
+ return declared ? { recipes: declared, source: "repository" } : { recipes: release, source: "release" };
3246
+ }
3247
+ function describeCodeRecipes(resolved) {
3248
+ const ids = resolved.recipes.map((recipe2) => recipe2.id).join(", ");
3249
+ 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}`;
3250
+ }
3251
+ async function sessionRecipesFor(workspace, options, resume) {
3252
+ const resolved = await resolveCodeRecipes(workspace.baselineDir, options.recipes, options.repositoryRecipes ?? null).catch(async (cause) => {
3253
+ await workspace.cleanup();
3254
+ throw cause;
3255
+ });
3256
+ return {
3257
+ ...resolved,
3258
+ buildPolicyDigest: digestRuntimeValue(JSON.stringify(resolved.recipes)),
3259
+ note: !resume && resolved.source === "repository" ? describeCodeRecipes(resolved) : null
3260
+ };
3261
+ }
3262
+
3153
3263
  // src/code-runtime-broker.ts
3154
3264
  function createCodeRuntimeToolBroker(input, lease, role) {
3155
3265
  const broker = createCodeToolBroker({
@@ -3331,18 +3441,6 @@ async function startGoalPursuit(input) {
3331
3441
  return { status: run.met ? "completed" : "failed", finalText: "" };
3332
3442
  }
3333
3443
 
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
3444
  // src/code-runtime-acknowledgement-gate.ts
3347
3445
  function codeRuntimeAcknowledgementGate(signal) {
3348
3446
  let settle;
@@ -3464,6 +3562,7 @@ var TheseusRuntimeEngine = class {
3464
3562
  control: this.options.control,
3465
3563
  ...this.options.localSource ? { localSource: this.options.localSource } : {}
3466
3564
  });
3565
+ const resolvedRecipes = await sessionRecipesFor(workspace, this.options, resume);
3467
3566
  const abort = new AbortController(), startGate = codeRuntimeAcknowledgementGate(abort.signal);
3468
3567
  const conversationRefs = [];
3469
3568
  const active = {
@@ -3472,6 +3571,8 @@ var TheseusRuntimeEngine = class {
3472
3571
  conversationRefs,
3473
3572
  acknowledged: false,
3474
3573
  startGate,
3574
+ recipes: resolvedRecipes.recipes,
3575
+ buildPolicyDigest: resolvedRecipes.buildPolicyDigest,
3475
3576
  role: metadata.role,
3476
3577
  readOnly: metadata.readOnly,
3477
3578
  title: metadata.title,
@@ -3489,6 +3590,7 @@ var TheseusRuntimeEngine = class {
3489
3590
  done: Promise.resolve(null)
3490
3591
  };
3491
3592
  this.#active.set(command.sessionId, active);
3593
+ if (resolvedRecipes.note) await this.#event(command, { type: "message", actor: "system", body: resolvedRecipes.note }, conversationRefs);
3492
3594
  if (requestedLocal) {
3493
3595
  await this.#event(command, {
3494
3596
  type: "message",
@@ -3524,7 +3626,7 @@ var TheseusRuntimeEngine = class {
3524
3626
  const active = await this.#takeOver(command, "pursue requires an active Code session");
3525
3627
  active.done = startGoalPursuit({
3526
3628
  spec,
3527
- recipes: this.options.recipes,
3629
+ recipes: active.recipes,
3528
3630
  recipeExecutor: this.options.recipeExecutor ?? createContainerRecipeExecutor(this.options.engine),
3529
3631
  workspace: active.workspace,
3530
3632
  baseCommitSha: active.baseCommitSha,
@@ -3603,7 +3705,7 @@ var TheseusRuntimeEngine = class {
3603
3705
  async #runAttempt(command, metadata, active) {
3604
3706
  const lease = fakeCodeLease(command, metadata);
3605
3707
  const broker = this.#observed(command, active, createCodeRuntimeToolBroker({
3606
- recipes: this.options.recipes,
3708
+ recipes: active.recipes,
3607
3709
  engine: this.options.engine,
3608
3710
  recipeAuthorization: this.options.recipeAuthorization
3609
3711
  }, lease, metadata.role));
@@ -3629,7 +3731,7 @@ var TheseusRuntimeEngine = class {
3629
3731
  // A review-role session is read-only and still runs the proof; a planner
3630
3732
  // marked read-only by its payload runs nothing.
3631
3733
  recipes: metadata.role === "review" || !metadata.readOnly,
3632
- recipeIds: this.options.recipes.map((recipe2) => recipe2.id),
3734
+ recipeIds: active.recipes.map((recipe2) => recipe2.id),
3633
3735
  ...extraSkills.length ? { extraSkills } : {}
3634
3736
  });
3635
3737
  const closing = result.finalText.trim();
@@ -3740,6 +3842,12 @@ export {
3740
3842
  renderMemories,
3741
3843
  hazardFromAttempt,
3742
3844
  runGoal,
3845
+ REPOSITORY_RECIPES_FILE,
3846
+ parseRepositoryRecipes,
3847
+ readRepositoryRecipes,
3848
+ resolveCodeRecipes,
3849
+ describeCodeRecipes,
3850
+ sessionRecipesFor,
3743
3851
  TheseusRuntimeEngine
3744
3852
  };
3745
- //# sourceMappingURL=chunk-IV3VMKDV.js.map
3853
+ //# sourceMappingURL=chunk-IXYKIPRA.js.map