@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.
@@ -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
  });
@@ -2354,13 +2354,19 @@ function createCodeRuntimeSessionSkillLoader(control) {
2354
2354
  ...tool.acceptsTaint === void 0 ? {} : { acceptsTaint: tool.acceptsTaint },
2355
2355
  handler: async (input, context) => {
2356
2356
  if (!context.toolCallId) throw new TypeError("collaboration tool call identity is required");
2357
- return execute2(command.sessionId, {
2358
- commandId: command.commandId,
2359
- toolCallId: context.toolCallId,
2360
- skill: manifest.name,
2361
- tool: tool.name,
2362
- input
2363
- }, 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
+ }
2364
2370
  }
2365
2371
  }))
2366
2372
  }));
@@ -3806,6 +3812,96 @@ function observeCodeRuntimeSessionSkills(command, skills, emit) {
3806
3812
  }));
3807
3813
  }
3808
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
+
3809
3905
  // src/code-runtime-engine.ts
3810
3906
  var TheseusRuntimeEngine = class {
3811
3907
  constructor(options) {
@@ -3857,6 +3953,7 @@ var TheseusRuntimeEngine = class {
3857
3953
  control: this.options.control,
3858
3954
  ...this.options.localSource ? { localSource: this.options.localSource } : {}
3859
3955
  });
3956
+ const resolvedRecipes = await sessionRecipesFor(workspace, this.options, resume);
3860
3957
  const abort = new AbortController(), startGate = codeRuntimeAcknowledgementGate(abort.signal);
3861
3958
  const conversationRefs = [];
3862
3959
  const active = {
@@ -3865,6 +3962,8 @@ var TheseusRuntimeEngine = class {
3865
3962
  conversationRefs,
3866
3963
  acknowledged: false,
3867
3964
  startGate,
3965
+ recipes: resolvedRecipes.recipes,
3966
+ buildPolicyDigest: resolvedRecipes.buildPolicyDigest,
3868
3967
  role: metadata.role,
3869
3968
  readOnly: metadata.readOnly,
3870
3969
  title: metadata.title,
@@ -3882,6 +3981,7 @@ var TheseusRuntimeEngine = class {
3882
3981
  done: Promise.resolve(null)
3883
3982
  };
3884
3983
  this.#active.set(command.sessionId, active);
3984
+ if (resolvedRecipes.note) await this.#event(command, { type: "message", actor: "system", body: resolvedRecipes.note }, conversationRefs);
3885
3985
  if (requestedLocal) {
3886
3986
  await this.#event(command, {
3887
3987
  type: "message",
@@ -3917,7 +4017,7 @@ var TheseusRuntimeEngine = class {
3917
4017
  const active = await this.#takeOver(command, "pursue requires an active Code session");
3918
4018
  active.done = startGoalPursuit({
3919
4019
  spec,
3920
- recipes: this.options.recipes,
4020
+ recipes: active.recipes,
3921
4021
  recipeExecutor: this.options.recipeExecutor ?? createContainerRecipeExecutor(this.options.engine),
3922
4022
  workspace: active.workspace,
3923
4023
  baseCommitSha: active.baseCommitSha,
@@ -3996,7 +4096,7 @@ var TheseusRuntimeEngine = class {
3996
4096
  async #runAttempt(command, metadata, active) {
3997
4097
  const lease = fakeCodeLease(command, metadata);
3998
4098
  const broker = this.#observed(command, active, createCodeRuntimeToolBroker({
3999
- recipes: this.options.recipes,
4099
+ recipes: active.recipes,
4000
4100
  engine: this.options.engine,
4001
4101
  recipeAuthorization: this.options.recipeAuthorization
4002
4102
  }, lease, metadata.role));
@@ -4022,7 +4122,7 @@ var TheseusRuntimeEngine = class {
4022
4122
  // A review-role session is read-only and still runs the proof; a planner
4023
4123
  // marked read-only by its payload runs nothing.
4024
4124
  recipes: metadata.role === "review" || !metadata.readOnly,
4025
- recipeIds: this.options.recipes.map((recipe2) => recipe2.id),
4125
+ recipeIds: active.recipes.map((recipe2) => recipe2.id),
4026
4126
  ...extraSkills.length ? { extraSkills } : {}
4027
4127
  });
4028
4128
  const closing = result.finalText.trim();
@@ -4131,7 +4231,7 @@ function parse(argv) {
4131
4231
  };
4132
4232
  }
4133
4233
  async function readPolicy(path) {
4134
- const value = JSON.parse(await (0, import_promises13.readFile)(path, "utf8"));
4234
+ const value = JSON.parse(await (0, import_promises14.readFile)(path, "utf8"));
4135
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");
4136
4236
  const recipes = value.recipes;
4137
4237
  for (const recipe2 of recipes) assertCodeBuildRecipe(recipe2);