@odla-ai/harness 0.11.8 → 0.11.10

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.
@@ -7,7 +7,7 @@ import {
7
7
  createCodeRuntimeControlClient,
8
8
  createCodeRuntimeSessionSkillLoader,
9
9
  runCodeRuntimeHeartbeatLoop
10
- } from "./chunk-NUDKRYL4.js";
10
+ } from "./chunk-4IGSN53G.js";
11
11
  import "./chunk-INL642J5.js";
12
12
  import {
13
13
  assertPinnedImage,
package/dist/node.cjs CHANGED
@@ -25,6 +25,7 @@ __export(node_exports, {
25
25
  CodeRuntimeCheckpointManager: () => CodeRuntimeCheckpointManager,
26
26
  CodeRuntimeControlError: () => CodeRuntimeControlError,
27
27
  CodeRuntimeReconciler: () => CodeRuntimeReconciler,
28
+ DEPENDENCY_INSTALL_TIMEOUT_MS: () => DEPENDENCY_INSTALL_TIMEOUT_MS,
28
29
  DecompositionError: () => DecompositionError,
29
30
  MAX_MEMORY_BODY: () => MAX_MEMORY_BODY,
30
31
  MEASURED_PREMIUM: () => MEASURED_PREMIUM,
@@ -51,6 +52,8 @@ __export(node_exports, {
51
52
  createCodeToolBroker: () => createCodeToolBroker,
52
53
  createCodeWorkspaceCheckpoint: () => createCodeWorkspaceCheckpoint,
53
54
  createContainerRecipeExecutor: () => createContainerRecipeExecutor,
55
+ dependencyCacheKey: () => dependencyCacheKey,
56
+ dependencyInstallArgs: () => dependencyInstallArgs,
54
57
  describeCodeRecipes: () => describeCodeRecipes,
55
58
  describeGateFailure: () => describeGateFailure,
56
59
  describePatchFailure: () => describePatchFailure,
@@ -70,6 +73,7 @@ __export(node_exports, {
70
73
  parseRepositoryRecipes: () => parseRepositoryRecipes,
71
74
  patchPaths: () => patchPaths,
72
75
  planReachCollisions: () => planReachCollisions,
76
+ prepareContainerDependencies: () => prepareContainerDependencies,
73
77
  prepareRuntimeCheckpoint: () => prepareRuntimeCheckpoint,
74
78
  racedAttempt: () => racedAttempt,
75
79
  readOnlyNotice: () => readOnlyNotice,
@@ -85,6 +89,7 @@ __export(node_exports, {
85
89
  runCodeAgentAttempt: () => runCodeAgentAttempt,
86
90
  runCodeRuntimeHeartbeatLoop: () => runCodeRuntimeHeartbeatLoop,
87
91
  runContainerAttempt: () => runContainerAttempt,
92
+ runContainerCommand: () => runContainerCommand,
88
93
  runGoal: () => runGoal,
89
94
  runHarnessRunner: () => runHarnessRunner,
90
95
  runLeasedAttempt: () => runLeasedAttempt,
@@ -94,6 +99,7 @@ __export(node_exports, {
94
99
  selectWinner: () => selectWinner,
95
100
  sessionRecipesFor: () => sessionRecipesFor,
96
101
  sessionSkillsFor: () => sessionSkillsFor,
102
+ stageManifests: () => stageManifests,
97
103
  stageWorkspace: () => stageWorkspace,
98
104
  stageWorkspacePair: () => stageWorkspacePair,
99
105
  straySubGoalFiles: () => straySubGoalFiles,
@@ -1704,8 +1710,9 @@ var import_node_process2 = require("process");
1704
1710
  var import_node_crypto2 = require("crypto");
1705
1711
  var ARTIFACT_PATH = /^[A-Za-z0-9_@+.,-]+(?:\/[A-Za-z0-9_@+.,-]+)*$/;
1706
1712
  var PRIVATE_ARTIFACT_PART = /^(?:\.git|\.odla|\.wrangler|\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json)$/i;
1707
- function buildRecipeContainerArgs(engine, workspaceDir, recipe2, name = `odla-recipe-${(0, import_node_crypto2.randomUUID)().slice(0, 12)}`) {
1713
+ function buildRecipeContainerArgs(engine, workspaceDir, recipe2, name = `odla-recipe-${(0, import_node_crypto2.randomUUID)().slice(0, 12)}`, dependencies = null) {
1708
1714
  assertCodeBuildRecipe(recipe2);
1715
+ const mounts = dependencies ? [dependencyMount(engine, dependencies)] : [];
1709
1716
  if (/[,\r\n]/.test(workspaceDir)) throw new TypeError("workspace path contains unsupported mount characters");
1710
1717
  const uid = typeof import_node_process2.getuid === "function" ? (0, import_node_process2.getuid)() : 1e3;
1711
1718
  const gid = typeof import_node_process2.getgid === "function" ? (0, import_node_process2.getgid)() : 1e3;
@@ -1728,6 +1735,7 @@ function buildRecipeContainerArgs(engine, workspaceDir, recipe2, name = `odla-re
1728
1735
  `--user=${uid}:${gid}`,
1729
1736
  "--tmpfs=/tmp",
1730
1737
  `--mount=type=bind,source=${workspaceDir},target=/workspace`,
1738
+ ...mounts,
1731
1739
  "--workdir=/workspace",
1732
1740
  "--env=CI=1",
1733
1741
  recipe2.image,
@@ -1749,18 +1757,28 @@ function buildRecipeContainerArgs(engine, workspaceDir, recipe2, name = `odla-re
1749
1757
  `--user=${uid}:${gid}`,
1750
1758
  `--tmpfs=/tmp:rw,noexec,nosuid,nodev,size=${limits.tmpfs}`,
1751
1759
  `--mount=type=bind,src=${workspaceDir},dst=/workspace`,
1760
+ ...mounts,
1752
1761
  "--workdir=/workspace",
1753
1762
  "--env=CI=1",
1754
1763
  recipe2.image,
1755
1764
  ...recipe2.command
1756
1765
  ];
1757
1766
  }
1758
- function createContainerRecipeExecutor(engine) {
1767
+ var LENDABLE = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage"]);
1768
+ function dependencyMount(engine, dependencies) {
1769
+ const mountAs = dependencies.mountAs ?? "node_modules";
1770
+ if (!dependencies.source.startsWith("/") || /[,\r\n]/.test(dependencies.source)) {
1771
+ throw new TypeError("recipe dependency source must be an absolute path without mount separators");
1772
+ }
1773
+ if (!LENDABLE.has(mountAs)) throw new TypeError(`recipe dependencies must mount at a reserved name, not "${mountAs}"`);
1774
+ return engine === "container" ? `--mount=type=bind,source=${dependencies.source},target=/workspace/${mountAs},readonly` : `--mount=type=bind,src=${dependencies.source},dst=/workspace/${mountAs},readonly`;
1775
+ }
1776
+ function createContainerRecipeExecutor(engine, options = {}) {
1759
1777
  return {
1760
1778
  async run(input) {
1761
1779
  await verifyContainerEngineBoundary(engine);
1762
1780
  const name = `odla-recipe-${(0, import_node_crypto2.randomUUID)().slice(0, 12)}`;
1763
- const args = buildRecipeContainerArgs(engine, input.workspaceDir, input.recipe, name);
1781
+ const args = buildRecipeContainerArgs(engine, input.workspaceDir, input.recipe, name, options.dependencies ?? null);
1764
1782
  return execute(engine, args, name, input.recipe, input.signal);
1765
1783
  }
1766
1784
  };
@@ -1781,6 +1799,9 @@ function parseMemory(value) {
1781
1799
  return Number(match[1]) * scale;
1782
1800
  }
1783
1801
  function execute(engine, args, name, recipe2, signal) {
1802
+ return runContainerCommand(engine, args, name, { timeoutMs: recipe2.timeoutMs, maxOutputBytes: recipe2.maxOutputBytes }, signal);
1803
+ }
1804
+ function runContainerCommand(engine, args, name, recipe2, signal) {
1784
1805
  return new Promise((accept, reject) => {
1785
1806
  const started = Date.now();
1786
1807
  const child = (0, import_node_child_process5.spawn)(engine, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
@@ -2881,7 +2902,7 @@ function codeSkill(opts) {
2881
2902
  },
2882
2903
  handler: (input, ctx) => call("sandbox.edit", input, ctx.signal)
2883
2904
  };
2884
- const writeFile6 = {
2905
+ const writeFile7 = {
2885
2906
  name: "odla_write_file",
2886
2907
  description: "Create a file or replace its entire contents with exactly the text given. Use this for a NEW file (a test, a module, a fixture) and for a rewrite where most lines change; use odla_edit_file for a small change inside an existing file. Paths are repository-relative; parent directories are created. The content must be the complete file.",
2887
2908
  inputSchema: {
@@ -2983,7 +3004,7 @@ function codeSkill(opts) {
2983
3004
  ];
2984
3005
  const runsRecipes = opts.recipes ?? !opts.readOnly;
2985
3006
  const effects = [...opts.readOnly ? [] : [applyPatch], ...runsRecipes ? [runRecipe] : []];
2986
- const tools = opts.surface === "v3" ? [...orientation, searchFiles, read2, ...opts.readOnly ? [] : [editFile, writeFile6], ...effects] : opts.surface === "v2" ? [listFiles, searchFiles, read2, ...effects] : [read2, ...effects];
3007
+ const tools = opts.surface === "v3" ? [...orientation, searchFiles, read2, ...opts.readOnly ? [] : [editFile, writeFile7], ...effects] : opts.surface === "v2" ? [listFiles, searchFiles, read2, ...effects] : [read2, ...effects];
2987
3008
  return { name: "code", tools };
2988
3009
  }
2989
3010
 
@@ -3814,8 +3835,8 @@ async function write(context, request, options, policy, registry) {
3814
3835
  if (options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`))) {
3815
3836
  throw new TypeError("write targets a read-only reference source");
3816
3837
  }
3817
- const exists = (await registry.files(context.workspaceDir)).includes(path);
3818
- const current = exists ? await (0, import_promises13.readFile)(resolveCodePath(context.workspaceDir, path), "utf8") : null;
3838
+ const exists2 = (await registry.files(context.workspaceDir)).includes(path);
3839
+ const current = exists2 ? await (0, import_promises13.readFile)(resolveCodePath(context.workspaceDir, path), "utf8") : null;
3819
3840
  const patch2 = writeAsDiff(path, current, content);
3820
3841
  const allowed = await policy.write(policyContext(context, request, options, { patch: patch2 }));
3821
3842
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
@@ -4769,7 +4790,7 @@ var TheseusRuntimeEngine = class {
4769
4790
  this.#checkpoints = new CodeRuntimeCheckpointManager({
4770
4791
  control: options.control,
4771
4792
  recipes: options.recipes,
4772
- recipeExecutor: options.recipeExecutor ?? createContainerRecipeExecutor(options.engine),
4793
+ recipeExecutor: options.recipeExecutor ?? createContainerRecipeExecutor(options.engine, { dependencies: options.recipeDependencies ?? null }),
4773
4794
  fallbackPolicyDigest: this.#buildPolicyDigest,
4774
4795
  event: (command, event, refs) => this.#event(command, event, refs)
4775
4796
  });
@@ -4877,7 +4898,7 @@ var TheseusRuntimeEngine = class {
4877
4898
  active.done = startGoalPursuit({
4878
4899
  spec,
4879
4900
  recipes: active.recipes,
4880
- recipeExecutor: this.options.recipeExecutor ?? createContainerRecipeExecutor(this.options.engine),
4901
+ recipeExecutor: this.options.recipeExecutor ?? createContainerRecipeExecutor(this.options.engine, { dependencies: this.options.recipeDependencies ?? null }),
4881
4902
  workspace: active.workspace,
4882
4903
  baseCommitSha: active.baseCommitSha,
4883
4904
  trustedBaseDigest: active.trustedBaseDigest,
@@ -5347,6 +5368,95 @@ async function installedDependencies(repoRoot) {
5347
5368
  const info = await (0, import_promises17.lstat)(source).catch(() => null);
5348
5369
  return info?.isDirectory() ? { source } : null;
5349
5370
  }
5371
+
5372
+ // src/code-recipe-dependency-cache.ts
5373
+ var import_node_crypto5 = require("crypto");
5374
+ var import_promises18 = require("fs/promises");
5375
+ var import_node_process3 = require("process");
5376
+ var import_node_path16 = require("path");
5377
+ var DEPENDENCY_INSTALL_TIMEOUT_MS = 20 * 6e4;
5378
+ var OUTPUT_CAP = 4 * 1024 * 1024;
5379
+ var exists = async (path) => Boolean(await (0, import_promises18.stat)(path).catch(() => null));
5380
+ async function dependencyCacheKey(repoRoot, image, arch = process.arch) {
5381
+ const lock = await (0, import_promises18.readFile)((0, import_node_path16.join)(repoRoot, "package-lock.json")).catch(() => null);
5382
+ if (!lock) return null;
5383
+ return (0, import_node_crypto5.createHash)("sha256").update(lock).update("\n").update(image).update("\n").update(arch).digest("hex").slice(0, 32);
5384
+ }
5385
+ async function stageManifests(repoRoot, stageDir) {
5386
+ await (0, import_promises18.mkdir)(stageDir, { recursive: true });
5387
+ const staged = [];
5388
+ for (const name of ["package.json", "package-lock.json", ".npmrc"]) {
5389
+ if (!await exists((0, import_node_path16.join)(repoRoot, name))) continue;
5390
+ await (0, import_promises18.copyFile)((0, import_node_path16.join)(repoRoot, name), (0, import_node_path16.join)(stageDir, name));
5391
+ staged.push(name);
5392
+ }
5393
+ const root = JSON.parse(await (0, import_promises18.readFile)((0, import_node_path16.join)(repoRoot, "package.json"), "utf8"));
5394
+ const globs = Array.isArray(root.workspaces) ? root.workspaces : root.workspaces?.packages ?? [];
5395
+ for (const glob of globs) {
5396
+ const dirs = glob.endsWith("/*") ? (await (0, import_promises18.readdir)((0, import_node_path16.join)(repoRoot, glob.slice(0, -2)), { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isDirectory()).map((entry) => `${glob.slice(0, -2)}/${entry.name}`) : [glob];
5397
+ for (const dir of dirs) {
5398
+ const manifest = (0, import_node_path16.join)(repoRoot, dir, "package.json");
5399
+ if (!await exists(manifest)) continue;
5400
+ await (0, import_promises18.mkdir)((0, import_node_path16.join)(stageDir, dir), { recursive: true });
5401
+ await (0, import_promises18.copyFile)(manifest, (0, import_node_path16.join)(stageDir, dir, "package.json"));
5402
+ staged.push(`${dir}/package.json`);
5403
+ }
5404
+ }
5405
+ return staged;
5406
+ }
5407
+ function dependencyInstallArgs(engine, image, stageDir, name) {
5408
+ if (/[,\r\n]/.test(stageDir)) throw new TypeError("stage path contains unsupported mount characters");
5409
+ const uid = typeof import_node_process3.getuid === "function" ? (0, import_node_process3.getuid)() : 1e3;
5410
+ const gid = typeof import_node_process3.getgid === "function" ? (0, import_node_process3.getgid)() : 1e3;
5411
+ const mount = engine === "container" ? `--mount=type=bind,source=${stageDir},target=/workspace` : `--mount=type=bind,src=${stageDir},dst=/workspace`;
5412
+ const npm = ["npm", "ci", "--no-audit", "--no-fund", "--ignore-scripts"];
5413
+ return [
5414
+ "run",
5415
+ "--rm",
5416
+ `--name=${name}`,
5417
+ "--cap-drop=ALL",
5418
+ "--memory=2g",
5419
+ "--cpus=2",
5420
+ `--user=${uid}:${gid}`,
5421
+ "--tmpfs=/tmp",
5422
+ mount,
5423
+ "--workdir=/workspace",
5424
+ "--env=CI=1",
5425
+ "--env=npm_config_cache=/tmp/npm-cache",
5426
+ "--env=HOME=/tmp",
5427
+ image,
5428
+ ...npm
5429
+ ];
5430
+ }
5431
+ async function prepareContainerDependencies(input) {
5432
+ const key = await dependencyCacheKey(input.repoRoot, input.image);
5433
+ if (!key) return null;
5434
+ const dir = (0, import_node_path16.join)(input.cacheRoot, key);
5435
+ const tree = (0, import_node_path16.join)(dir, "node_modules");
5436
+ const stage = (0, import_node_path16.join)(dir, "stage");
5437
+ if (await exists(tree)) return { source: tree };
5438
+ const run = input.run ?? runContainerCommand;
5439
+ await (0, import_promises18.rm)(stage, { recursive: true, force: true });
5440
+ const staged = await stageManifests(input.repoRoot, stage);
5441
+ input.log?.(`installing ${key.slice(0, 12)}: ${staged.length} manifests, npm ci in ${input.image}`);
5442
+ const name = `odla-deps-${key.slice(0, 12)}`;
5443
+ const result = await run(
5444
+ input.engine,
5445
+ dependencyInstallArgs(input.engine, input.image, stage, name),
5446
+ name,
5447
+ { timeoutMs: DEPENDENCY_INSTALL_TIMEOUT_MS, maxOutputBytes: OUTPUT_CAP }
5448
+ );
5449
+ if (result.exitCode !== 0 || result.timedOut || !await exists((0, import_node_path16.join)(stage, "node_modules"))) {
5450
+ input.log?.(`install failed (exit ${result.exitCode}${result.timedOut ? ", timed out" : ""}): ${result.stderr.trim().split("\n").slice(-3).join(" | ").slice(0, 400)}`);
5451
+ await (0, import_promises18.rm)(stage, { recursive: true, force: true });
5452
+ return null;
5453
+ }
5454
+ await (0, import_promises18.rename)((0, import_node_path16.join)(stage, "node_modules"), tree);
5455
+ await (0, import_promises18.rm)(stage, { recursive: true, force: true });
5456
+ await (0, import_promises18.writeFile)((0, import_node_path16.join)(dir, "manifest.json"), JSON.stringify({ image: input.image, key, createdAt: Date.now() }, null, 2));
5457
+ input.log?.(`installed ${key.slice(0, 12)} in ${Math.round(result.durationMs / 1e3)}s`);
5458
+ return { source: tree };
5459
+ }
5350
5460
  // Annotate the CommonJS export names for ESM import in node:
5351
5461
  0 && (module.exports = {
5352
5462
  CODE_PATCH_PATH,
@@ -5354,6 +5464,7 @@ async function installedDependencies(repoRoot) {
5354
5464
  CodeRuntimeCheckpointManager,
5355
5465
  CodeRuntimeControlError,
5356
5466
  CodeRuntimeReconciler,
5467
+ DEPENDENCY_INSTALL_TIMEOUT_MS,
5357
5468
  DecompositionError,
5358
5469
  MAX_MEMORY_BODY,
5359
5470
  MEASURED_PREMIUM,
@@ -5380,6 +5491,8 @@ async function installedDependencies(repoRoot) {
5380
5491
  createCodeToolBroker,
5381
5492
  createCodeWorkspaceCheckpoint,
5382
5493
  createContainerRecipeExecutor,
5494
+ dependencyCacheKey,
5495
+ dependencyInstallArgs,
5383
5496
  describeCodeRecipes,
5384
5497
  describeGateFailure,
5385
5498
  describePatchFailure,
@@ -5399,6 +5512,7 @@ async function installedDependencies(repoRoot) {
5399
5512
  parseRepositoryRecipes,
5400
5513
  patchPaths,
5401
5514
  planReachCollisions,
5515
+ prepareContainerDependencies,
5402
5516
  prepareRuntimeCheckpoint,
5403
5517
  racedAttempt,
5404
5518
  readOnlyNotice,
@@ -5414,6 +5528,7 @@ async function installedDependencies(repoRoot) {
5414
5528
  runCodeAgentAttempt,
5415
5529
  runCodeRuntimeHeartbeatLoop,
5416
5530
  runContainerAttempt,
5531
+ runContainerCommand,
5417
5532
  runGoal,
5418
5533
  runHarnessRunner,
5419
5534
  runLeasedAttempt,
@@ -5423,6 +5538,7 @@ async function installedDependencies(repoRoot) {
5423
5538
  selectWinner,
5424
5539
  sessionRecipesFor,
5425
5540
  sessionSkillsFor,
5541
+ stageManifests,
5426
5542
  stageWorkspace,
5427
5543
  stageWorkspacePair,
5428
5544
  straySubGoalFiles,