@odla-ai/harness 0.10.3 → 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
  });
@@ -1978,6 +1978,10 @@ import {
1978
1978
  keepRecentExchanges,
1979
1979
  runAgent
1980
1980
  } from "@odla-ai/ai";
1981
+ function readOnlyNotice(readOnly, recipes) {
1982
+ if (!readOnly) return "";
1983
+ return recipes ? "\n\nThis session is read-only: inspect and report; repository mutation is intentionally unavailable. The registered proof recipes are available through odla_run_recipe and must be run before any verdict." : "\n\nThis session is read-only. Inspect and report; repository mutation and recipe execution are intentionally unavailable.";
1984
+ }
1981
1985
  async function runCodeAgent(options) {
1982
1986
  const toolCalls = [];
1983
1987
  const surface = options.surface ?? "v1";
@@ -1987,6 +1991,7 @@ async function runCodeAgent(options) {
1987
1991
  workspaceDir: options.workspaceDir,
1988
1992
  surface,
1989
1993
  ...options.readOnly === void 0 ? {} : { readOnly: options.readOnly },
1994
+ ...options.recipes === void 0 ? {} : { recipes: options.recipes },
1990
1995
  ...options.recipeIds ? { recipeIds: options.recipeIds } : {},
1991
1996
  onToolCall: (call) => {
1992
1997
  toolCalls.push(call);
@@ -1999,7 +2004,7 @@ async function runCodeAgent(options) {
1999
2004
  {
2000
2005
  name: "odla-code",
2001
2006
  model: options.model,
2002
- system: options.system ?? `${SYSTEM_PROMPT_FOR[surface]}${options.readOnly ? "\n\nThis session is read-only. Inspect and report; repository mutation and recipe execution are intentionally unavailable." : ""}`,
2007
+ system: options.system ?? `${SYSTEM_PROMPT_FOR[surface]}${readOnlyNotice(options.readOnly, options.recipes)}`,
2003
2008
  skills: [skill, ...options.extraSkills ?? []],
2004
2009
  maxSteps: options.maxSteps ?? 24,
2005
2010
  maxTokens: options.maxTokens ?? 16384
@@ -2084,13 +2089,19 @@ function createCodeRuntimeSessionSkillLoader(control) {
2084
2089
  ...tool.acceptsTaint === void 0 ? {} : { acceptsTaint: tool.acceptsTaint },
2085
2090
  handler: async (input, context) => {
2086
2091
  if (!context.toolCallId) throw new TypeError("collaboration tool call identity is required");
2087
- return execute2(command.sessionId, {
2088
- commandId: command.commandId,
2089
- toolCallId: context.toolCallId,
2090
- skill: manifest.name,
2091
- tool: tool.name,
2092
- input
2093
- }, 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
+ }
2094
2105
  }
2095
2106
  }))
2096
2107
  }));
@@ -3145,6 +3156,110 @@ function assertBudget(budget) {
3145
3156
  }
3146
3157
  }
3147
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
+
3148
3263
  // src/code-runtime-broker.ts
3149
3264
  function createCodeRuntimeToolBroker(input, lease, role) {
3150
3265
  const broker = createCodeToolBroker({
@@ -3154,16 +3269,17 @@ function createCodeRuntimeToolBroker(input, lease, role) {
3154
3269
  readerId: `code-session:${lease.task.taskId}`,
3155
3270
  readOnlyPrefixes: [".odla-references"]
3156
3271
  });
3157
- const reviewReads = /* @__PURE__ */ new Set([
3272
+ const reviewTools = /* @__PURE__ */ new Set([
3158
3273
  "sandbox.read",
3159
3274
  "sandbox.list",
3160
3275
  "sandbox.search",
3161
3276
  "sandbox.overview",
3162
3277
  "sandbox.where_is",
3163
3278
  "sandbox.who_imports",
3164
- "sandbox.who_touches"
3279
+ "sandbox.who_touches",
3280
+ "sandbox.run_recipe"
3165
3281
  ]);
3166
- return role === "coding" ? broker : { execute: (context, request) => reviewReads.has(request.tool) ? broker.execute(context, request) : Promise.resolve({ requestId: request.requestId, ok: false, content: "review sessions are read-only" }) };
3282
+ return role === "coding" ? broker : { execute: (context, request) => reviewTools.has(request.tool) ? broker.execute(context, request) : Promise.resolve({ requestId: request.requestId, ok: false, content: "review sessions are read-only" }) };
3167
3283
  }
3168
3284
 
3169
3285
  // src/code-runtime-goal.ts
@@ -3325,18 +3441,6 @@ async function startGoalPursuit(input) {
3325
3441
  return { status: run.met ? "completed" : "failed", finalText: "" };
3326
3442
  }
3327
3443
 
3328
- // src/code-runtime-events.ts
3329
- import { createHash as createHash3 } from "crypto";
3330
- async function appendCodeRuntimeEvent(control, command, event, refs) {
3331
- const eventId = `${command.commandId.slice(0, 45)}:${refs.length + 1}`;
3332
- refs.push(eventId);
3333
- const attributed = { ...event, interactionId: command.commandId };
3334
- const bounded = attributed.type === "message" ? { ...attributed, body: attributed.body.trim().slice(0, 2e4) || `${attributed.actor} event` } : attributed;
3335
- await control.appendSessionEvent(command.sessionId, eventId, bounded);
3336
- }
3337
- var digestRuntimeValue = (value) => `sha256:${createHash3("sha256").update(value).digest("hex")}`;
3338
- var runtimeErrorMessage = (value) => value instanceof Error ? value.message : String(value);
3339
-
3340
3444
  // src/code-runtime-acknowledgement-gate.ts
3341
3445
  function codeRuntimeAcknowledgementGate(signal) {
3342
3446
  let settle;
@@ -3458,6 +3562,7 @@ var TheseusRuntimeEngine = class {
3458
3562
  control: this.options.control,
3459
3563
  ...this.options.localSource ? { localSource: this.options.localSource } : {}
3460
3564
  });
3565
+ const resolvedRecipes = await sessionRecipesFor(workspace, this.options, resume);
3461
3566
  const abort = new AbortController(), startGate = codeRuntimeAcknowledgementGate(abort.signal);
3462
3567
  const conversationRefs = [];
3463
3568
  const active = {
@@ -3466,6 +3571,8 @@ var TheseusRuntimeEngine = class {
3466
3571
  conversationRefs,
3467
3572
  acknowledged: false,
3468
3573
  startGate,
3574
+ recipes: resolvedRecipes.recipes,
3575
+ buildPolicyDigest: resolvedRecipes.buildPolicyDigest,
3469
3576
  role: metadata.role,
3470
3577
  readOnly: metadata.readOnly,
3471
3578
  title: metadata.title,
@@ -3483,6 +3590,7 @@ var TheseusRuntimeEngine = class {
3483
3590
  done: Promise.resolve(null)
3484
3591
  };
3485
3592
  this.#active.set(command.sessionId, active);
3593
+ if (resolvedRecipes.note) await this.#event(command, { type: "message", actor: "system", body: resolvedRecipes.note }, conversationRefs);
3486
3594
  if (requestedLocal) {
3487
3595
  await this.#event(command, {
3488
3596
  type: "message",
@@ -3518,7 +3626,7 @@ var TheseusRuntimeEngine = class {
3518
3626
  const active = await this.#takeOver(command, "pursue requires an active Code session");
3519
3627
  active.done = startGoalPursuit({
3520
3628
  spec,
3521
- recipes: this.options.recipes,
3629
+ recipes: active.recipes,
3522
3630
  recipeExecutor: this.options.recipeExecutor ?? createContainerRecipeExecutor(this.options.engine),
3523
3631
  workspace: active.workspace,
3524
3632
  baseCommitSha: active.baseCommitSha,
@@ -3597,7 +3705,7 @@ var TheseusRuntimeEngine = class {
3597
3705
  async #runAttempt(command, metadata, active) {
3598
3706
  const lease = fakeCodeLease(command, metadata);
3599
3707
  const broker = this.#observed(command, active, createCodeRuntimeToolBroker({
3600
- recipes: this.options.recipes,
3708
+ recipes: active.recipes,
3601
3709
  engine: this.options.engine,
3602
3710
  recipeAuthorization: this.options.recipeAuthorization
3603
3711
  }, lease, metadata.role));
@@ -3623,7 +3731,7 @@ var TheseusRuntimeEngine = class {
3623
3731
  // A review-role session is read-only and still runs the proof; a planner
3624
3732
  // marked read-only by its payload runs nothing.
3625
3733
  recipes: metadata.role === "review" || !metadata.readOnly,
3626
- recipeIds: this.options.recipes.map((recipe2) => recipe2.id),
3734
+ recipeIds: active.recipes.map((recipe2) => recipe2.id),
3627
3735
  ...extraSkills.length ? { extraSkills } : {}
3628
3736
  });
3629
3737
  const closing = result.finalText.trim();
@@ -3720,6 +3828,7 @@ export {
3720
3828
  V3_SYSTEM_PROMPT,
3721
3829
  SYSTEM_PROMPT_FOR,
3722
3830
  codeSkill,
3831
+ readOnlyNotice,
3723
3832
  runCodeAgent,
3724
3833
  runCodeAgentAttempt,
3725
3834
  createCodeRuntimeSessionSkillLoader,
@@ -3733,6 +3842,12 @@ export {
3733
3842
  renderMemories,
3734
3843
  hazardFromAttempt,
3735
3844
  runGoal,
3845
+ REPOSITORY_RECIPES_FILE,
3846
+ parseRepositoryRecipes,
3847
+ readRepositoryRecipes,
3848
+ resolveCodeRecipes,
3849
+ describeCodeRecipes,
3850
+ sessionRecipesFor,
3736
3851
  TheseusRuntimeEngine
3737
3852
  };
3738
- //# sourceMappingURL=chunk-TMBYA5JX.js.map
3853
+ //# sourceMappingURL=chunk-IXYKIPRA.js.map