@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.
@@ -3,7 +3,7 @@
3
3
 
4
4
  // src/code-runtime-cli.ts
5
5
  var import_node_os4 = require("os");
6
- var import_promises13 = require("fs/promises");
6
+ var import_promises14 = require("fs/promises");
7
7
 
8
8
  // src/code-runtime-client-validation.ts
9
9
  var import_code = require("@odla-ai/camel/code");
@@ -1412,8 +1412,8 @@ var CodeRuntimeCheckpointManager = class {
1412
1412
  trustedBaseDigest: active.trustedBaseDigest,
1413
1413
  planningInputDigest: active.planningInputDigest,
1414
1414
  conversationRefs: active.conversationRefs,
1415
- fallbackPolicyDigest: this.options.fallbackPolicyDigest,
1416
- recipes: this.options.recipes,
1415
+ fallbackPolicyDigest: active.buildPolicyDigest ?? this.options.fallbackPolicyDigest,
1416
+ recipes: active.recipes ?? this.options.recipes,
1417
1417
  recipeExecutor: this.options.recipeExecutor,
1418
1418
  review: (patch2, verification) => this.options.control.review(command.sessionId, { patch: patch2, verification })
1419
1419
  });
@@ -2244,6 +2244,10 @@ function codeSkill(opts) {
2244
2244
  }
2245
2245
 
2246
2246
  // src/code-agent.ts
2247
+ function readOnlyNotice(readOnly, recipes) {
2248
+ if (!readOnly) return "";
2249
+ 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.";
2250
+ }
2247
2251
  async function runCodeAgent(options) {
2248
2252
  const toolCalls = [];
2249
2253
  const surface = options.surface ?? "v1";
@@ -2253,6 +2257,7 @@ async function runCodeAgent(options) {
2253
2257
  workspaceDir: options.workspaceDir,
2254
2258
  surface,
2255
2259
  ...options.readOnly === void 0 ? {} : { readOnly: options.readOnly },
2260
+ ...options.recipes === void 0 ? {} : { recipes: options.recipes },
2256
2261
  ...options.recipeIds ? { recipeIds: options.recipeIds } : {},
2257
2262
  onToolCall: (call) => {
2258
2263
  toolCalls.push(call);
@@ -2265,7 +2270,7 @@ async function runCodeAgent(options) {
2265
2270
  {
2266
2271
  name: "odla-code",
2267
2272
  model: options.model,
2268
- 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." : ""}`,
2273
+ system: options.system ?? `${SYSTEM_PROMPT_FOR[surface]}${readOnlyNotice(options.readOnly, options.recipes)}`,
2269
2274
  skills: [skill, ...options.extraSkills ?? []],
2270
2275
  maxSteps: options.maxSteps ?? 24,
2271
2276
  maxTokens: options.maxTokens ?? 16384
@@ -2349,13 +2354,19 @@ function createCodeRuntimeSessionSkillLoader(control) {
2349
2354
  ...tool.acceptsTaint === void 0 ? {} : { acceptsTaint: tool.acceptsTaint },
2350
2355
  handler: async (input, context) => {
2351
2356
  if (!context.toolCallId) throw new TypeError("collaboration tool call identity is required");
2352
- return execute2(command.sessionId, {
2353
- commandId: command.commandId,
2354
- toolCallId: context.toolCallId,
2355
- skill: manifest.name,
2356
- tool: tool.name,
2357
- input
2358
- }, context.signal);
2357
+ try {
2358
+ return await execute2(command.sessionId, {
2359
+ commandId: command.commandId,
2360
+ toolCallId: context.toolCallId,
2361
+ skill: manifest.name,
2362
+ tool: tool.name,
2363
+ input
2364
+ }, context.signal);
2365
+ } catch (cause) {
2366
+ if (context.signal?.aborted) throw cause;
2367
+ const detail = (cause instanceof Error ? cause.message : String(cause)).slice(0, 500);
2368
+ return { content: `Tool "${tool.name}" failed: ${detail}`, isError: true };
2369
+ }
2359
2370
  }
2360
2371
  }))
2361
2372
  }));
@@ -3418,16 +3429,17 @@ function createCodeRuntimeToolBroker(input, lease, role) {
3418
3429
  readerId: `code-session:${lease.task.taskId}`,
3419
3430
  readOnlyPrefixes: [".odla-references"]
3420
3431
  });
3421
- const reviewReads = /* @__PURE__ */ new Set([
3432
+ const reviewTools = /* @__PURE__ */ new Set([
3422
3433
  "sandbox.read",
3423
3434
  "sandbox.list",
3424
3435
  "sandbox.search",
3425
3436
  "sandbox.overview",
3426
3437
  "sandbox.where_is",
3427
3438
  "sandbox.who_imports",
3428
- "sandbox.who_touches"
3439
+ "sandbox.who_touches",
3440
+ "sandbox.run_recipe"
3429
3441
  ]);
3430
- 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" }) };
3442
+ 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" }) };
3431
3443
  }
3432
3444
 
3433
3445
  // src/code-memory.ts
@@ -3800,6 +3812,96 @@ function observeCodeRuntimeSessionSkills(command, skills, emit) {
3800
3812
  }));
3801
3813
  }
3802
3814
 
3815
+ // src/code-repository-recipes.ts
3816
+ var import_promises13 = require("fs/promises");
3817
+ var import_node_path12 = require("path");
3818
+ var REPOSITORY_RECIPES_FILE = "odla.recipes.json";
3819
+ var MAX_RECIPES = 16;
3820
+ var DEFAULT_TIMEOUT_MS = 12e4;
3821
+ var MAX_TIMEOUT_MS = 15 * 6e4;
3822
+ var ID2 = /^[A-Za-z0-9._:-]{1,120}$/;
3823
+ var RECIPE_FIELDS = /* @__PURE__ */ new Set(["id", "command", "timeoutMs"]);
3824
+ var fault = (detail) => new TypeError(`${REPOSITORY_RECIPES_FILE} is malformed: ${detail}`);
3825
+ function parseRepositoryRecipes(text2, envelope) {
3826
+ let parsed;
3827
+ try {
3828
+ parsed = JSON.parse(text2);
3829
+ } catch {
3830
+ throw fault("not valid JSON");
3831
+ }
3832
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw fault("the document must be an object");
3833
+ const document = parsed;
3834
+ if (document.version !== 1) throw fault("version must be 1");
3835
+ const entries = document.recipes;
3836
+ if (!Array.isArray(entries) || entries.length < 1) throw fault("recipes must be a non-empty array");
3837
+ if (entries.length > MAX_RECIPES) throw fault(`at most ${MAX_RECIPES} recipes may be declared`);
3838
+ const recipes = [];
3839
+ const seen = /* @__PURE__ */ new Set();
3840
+ entries.forEach((entry, index) => {
3841
+ const label = `recipe ${index + 1}`;
3842
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw fault(`${label} must be an object`);
3843
+ const row = entry;
3844
+ const unknown = Object.keys(row).filter((key) => !RECIPE_FIELDS.has(key));
3845
+ if (unknown.length) throw fault(`${label} has unsupported field ${unknown[0]}; the host owns image and resource limits`);
3846
+ if (typeof row.id !== "string" || !ID2.test(row.id)) throw fault(`${label} needs an id of 1 to 120 letters, digits, . _ : or -`);
3847
+ if (seen.has(row.id)) throw fault(`${label} repeats id ${row.id}`);
3848
+ seen.add(row.id);
3849
+ if (!Array.isArray(row.command) || row.command.length < 1 || row.command.some((part) => typeof part !== "string" || !part)) {
3850
+ throw fault(`${label} (${row.id}) needs command as a non-empty array of non-empty strings`);
3851
+ }
3852
+ const timeoutMs = row.timeoutMs ?? DEFAULT_TIMEOUT_MS;
3853
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_TIMEOUT_MS) {
3854
+ throw fault(`${label} (${row.id}) timeoutMs must be an integer from 1 to ${MAX_TIMEOUT_MS}`);
3855
+ }
3856
+ const recipe2 = {
3857
+ id: row.id,
3858
+ command: [...row.command],
3859
+ timeoutMs,
3860
+ image: envelope.image,
3861
+ maxOutputBytes: envelope.maxOutputBytes,
3862
+ cpus: envelope.cpus,
3863
+ memory: envelope.memory,
3864
+ pids: envelope.pids
3865
+ };
3866
+ try {
3867
+ assertCodeBuildRecipe(recipe2);
3868
+ } catch (cause) {
3869
+ throw fault(`${label} (${row.id}) is not a runnable recipe: ${cause instanceof Error ? cause.message : String(cause)}`);
3870
+ }
3871
+ recipes.push(recipe2);
3872
+ });
3873
+ return recipes;
3874
+ }
3875
+ async function readRepositoryRecipes(baselineDir, envelope) {
3876
+ let text2;
3877
+ try {
3878
+ text2 = await (0, import_promises13.readFile)((0, import_node_path12.join)(baselineDir, REPOSITORY_RECIPES_FILE), "utf8");
3879
+ } catch (cause) {
3880
+ if (cause.code === "ENOENT") return null;
3881
+ throw cause;
3882
+ }
3883
+ return parseRepositoryRecipes(text2, envelope);
3884
+ }
3885
+ async function resolveCodeRecipes(baselineDir, release, envelope) {
3886
+ const declared = envelope ? await readRepositoryRecipes(baselineDir, envelope) : null;
3887
+ return declared ? { recipes: declared, source: "repository" } : { recipes: release, source: "release" };
3888
+ }
3889
+ function describeCodeRecipes(resolved) {
3890
+ const ids = resolved.recipes.map((recipe2) => recipe2.id).join(", ");
3891
+ 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}`;
3892
+ }
3893
+ async function sessionRecipesFor(workspace, options, resume) {
3894
+ const resolved = await resolveCodeRecipes(workspace.baselineDir, options.recipes, options.repositoryRecipes ?? null).catch(async (cause) => {
3895
+ await workspace.cleanup();
3896
+ throw cause;
3897
+ });
3898
+ return {
3899
+ ...resolved,
3900
+ buildPolicyDigest: digestRuntimeValue(JSON.stringify(resolved.recipes)),
3901
+ note: !resume && resolved.source === "repository" ? describeCodeRecipes(resolved) : null
3902
+ };
3903
+ }
3904
+
3803
3905
  // src/code-runtime-engine.ts
3804
3906
  var TheseusRuntimeEngine = class {
3805
3907
  constructor(options) {
@@ -3851,6 +3953,7 @@ var TheseusRuntimeEngine = class {
3851
3953
  control: this.options.control,
3852
3954
  ...this.options.localSource ? { localSource: this.options.localSource } : {}
3853
3955
  });
3956
+ const resolvedRecipes = await sessionRecipesFor(workspace, this.options, resume);
3854
3957
  const abort = new AbortController(), startGate = codeRuntimeAcknowledgementGate(abort.signal);
3855
3958
  const conversationRefs = [];
3856
3959
  const active = {
@@ -3859,6 +3962,8 @@ var TheseusRuntimeEngine = class {
3859
3962
  conversationRefs,
3860
3963
  acknowledged: false,
3861
3964
  startGate,
3965
+ recipes: resolvedRecipes.recipes,
3966
+ buildPolicyDigest: resolvedRecipes.buildPolicyDigest,
3862
3967
  role: metadata.role,
3863
3968
  readOnly: metadata.readOnly,
3864
3969
  title: metadata.title,
@@ -3876,6 +3981,7 @@ var TheseusRuntimeEngine = class {
3876
3981
  done: Promise.resolve(null)
3877
3982
  };
3878
3983
  this.#active.set(command.sessionId, active);
3984
+ if (resolvedRecipes.note) await this.#event(command, { type: "message", actor: "system", body: resolvedRecipes.note }, conversationRefs);
3879
3985
  if (requestedLocal) {
3880
3986
  await this.#event(command, {
3881
3987
  type: "message",
@@ -3911,7 +4017,7 @@ var TheseusRuntimeEngine = class {
3911
4017
  const active = await this.#takeOver(command, "pursue requires an active Code session");
3912
4018
  active.done = startGoalPursuit({
3913
4019
  spec,
3914
- recipes: this.options.recipes,
4020
+ recipes: active.recipes,
3915
4021
  recipeExecutor: this.options.recipeExecutor ?? createContainerRecipeExecutor(this.options.engine),
3916
4022
  workspace: active.workspace,
3917
4023
  baseCommitSha: active.baseCommitSha,
@@ -3990,7 +4096,7 @@ var TheseusRuntimeEngine = class {
3990
4096
  async #runAttempt(command, metadata, active) {
3991
4097
  const lease = fakeCodeLease(command, metadata);
3992
4098
  const broker = this.#observed(command, active, createCodeRuntimeToolBroker({
3993
- recipes: this.options.recipes,
4099
+ recipes: active.recipes,
3994
4100
  engine: this.options.engine,
3995
4101
  recipeAuthorization: this.options.recipeAuthorization
3996
4102
  }, lease, metadata.role));
@@ -4016,7 +4122,7 @@ var TheseusRuntimeEngine = class {
4016
4122
  // A review-role session is read-only and still runs the proof; a planner
4017
4123
  // marked read-only by its payload runs nothing.
4018
4124
  recipes: metadata.role === "review" || !metadata.readOnly,
4019
- recipeIds: this.options.recipes.map((recipe2) => recipe2.id),
4125
+ recipeIds: active.recipes.map((recipe2) => recipe2.id),
4020
4126
  ...extraSkills.length ? { extraSkills } : {}
4021
4127
  });
4022
4128
  const closing = result.finalText.trim();
@@ -4125,7 +4231,7 @@ function parse(argv) {
4125
4231
  };
4126
4232
  }
4127
4233
  async function readPolicy(path) {
4128
- const value = JSON.parse(await (0, import_promises13.readFile)(path, "utf8"));
4234
+ const value = JSON.parse(await (0, import_promises14.readFile)(path, "utf8"));
4129
4235
  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");
4130
4236
  const recipes = value.recipes;
4131
4237
  for (const recipe2 of recipes) assertCodeBuildRecipe(recipe2);