@odla-ai/harness 0.10.1 → 0.10.2

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.
Files changed (38) hide show
  1. package/dist/{chunk-U324RQ4N.js → chunk-CPV7ZVHH.js} +1 -1
  2. package/dist/{chunk-U324RQ4N.js.map → chunk-CPV7ZVHH.js.map} +1 -1
  3. package/dist/{chunk-OZBJNTML.js → chunk-ESLFS3FY.js} +4 -4
  4. package/dist/{chunk-5LRYJKUI.js → chunk-FUDTSAOZ.js} +3 -3
  5. package/dist/{chunk-VDY5V7ZG.js → chunk-JV45JAAW.js} +3 -3
  6. package/dist/chunk-JV45JAAW.js.map +1 -0
  7. package/dist/{chunk-FAN2R3GW.js → chunk-KXFI3WM2.js} +5 -1
  8. package/dist/chunk-KXFI3WM2.js.map +1 -0
  9. package/dist/{chunk-WM34GGTK.js → chunk-VIZALA6O.js} +101 -12
  10. package/dist/chunk-VIZALA6O.js.map +1 -0
  11. package/dist/cli.cjs +1 -1
  12. package/dist/cli.cjs.map +1 -1
  13. package/dist/cli.js +4 -4
  14. package/dist/code-runtime-cli.cjs +173 -78
  15. package/dist/code-runtime-cli.cjs.map +1 -1
  16. package/dist/code-runtime-cli.js +5 -5
  17. package/dist/index.cjs +5 -1
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +2 -2
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.js +3 -3
  22. package/dist/node.cjs +177 -82
  23. package/dist/node.cjs.map +1 -1
  24. package/dist/node.d.cts +2 -2
  25. package/dist/node.d.ts +2 -2
  26. package/dist/node.js +6 -6
  27. package/dist/testing.cjs.map +1 -1
  28. package/dist/testing.d.cts +1 -1
  29. package/dist/testing.d.ts +1 -1
  30. package/dist/testing.js +1 -1
  31. package/dist/{types-_8y8vBDI.d.ts → types-ocy70g_p.d.cts} +1 -1
  32. package/dist/{types-_8y8vBDI.d.cts → types-ocy70g_p.d.ts} +1 -1
  33. package/package.json +1 -1
  34. package/dist/chunk-FAN2R3GW.js.map +0 -1
  35. package/dist/chunk-VDY5V7ZG.js.map +0 -1
  36. package/dist/chunk-WM34GGTK.js.map +0 -1
  37. /package/dist/{chunk-OZBJNTML.js.map → chunk-ESLFS3FY.js.map} +0 -0
  38. /package/dist/{chunk-5LRYJKUI.js.map → chunk-FUDTSAOZ.js.map} +0 -0
@@ -3,7 +3,7 @@
3
3
 
4
4
  // src/code-runtime-cli.ts
5
5
  var import_node_os4 = require("os");
6
- var import_promises12 = require("fs/promises");
6
+ var import_promises13 = require("fs/promises");
7
7
 
8
8
  // src/code-runtime-client-validation.ts
9
9
  var import_code = require("@odla-ai/camel/code");
@@ -2071,10 +2071,12 @@ Prefer these over listing the tree \u2014 a full listing of a real repository is
2071
2071
  of thousands of tokens and you will carry it for the rest of the session.
2072
2072
 
2073
2073
  Then odla_search for a literal string, odla_read for a bounded range, and
2074
- odla_apply_git_diff to change something. A patch must start with
2075
- "diff --git a/<path> b/<path>", include matching "---" and "+++" headers and
2076
- numbered "@@" hunks with at least one line of surrounding context, and must never
2077
- use "*** Begin Patch" wrappers.
2074
+ odla_edit_file to change an existing file: give it the exact current text
2075
+ (copied verbatim from odla_read) and the text that replaces it; it must match
2076
+ once. Use odla_apply_git_diff only to create or delete whole files. A patch must
2077
+ start with "diff --git a/<path> b/<path>", include matching "---" and "+++"
2078
+ headers and numbered "@@" hunks with at least one line of surrounding context,
2079
+ and must never use "*** Begin Patch" wrappers.
2078
2080
 
2079
2081
  The workspace, model, and tool effects are controlled by the host broker.
2080
2082
  Never claim a build or test passed unless odla_run_recipe returned that result.`;
@@ -2134,6 +2136,21 @@ function codeSkill(opts) {
2134
2136
  },
2135
2137
  handler: (input, ctx) => call("sandbox.apply_patch", input, ctx.signal)
2136
2138
  };
2139
+ const editFile = {
2140
+ name: "odla_edit_file",
2141
+ description: "Change an existing file by replacing one exact stretch of its current text with new text. oldText must match the file exactly once (copy it verbatim from odla_read, including indentation and blank lines; widen it if it would match more than once). Prefer this over odla_apply_git_diff for every change to an existing file; use odla_apply_git_diff only to create or delete whole files.",
2142
+ inputSchema: {
2143
+ type: "object",
2144
+ required: ["path", "oldText", "newText"],
2145
+ properties: {
2146
+ path: { type: "string", minLength: 1, maxLength: 1024 },
2147
+ oldText: { type: "string", minLength: 1, maxLength: 262144 },
2148
+ newText: { type: "string", maxLength: 262144 }
2149
+ },
2150
+ additionalProperties: false
2151
+ },
2152
+ handler: (input, ctx) => call("sandbox.edit", input, ctx.signal)
2153
+ };
2137
2154
  const runRecipe = {
2138
2155
  name: "odla_run_recipe",
2139
2156
  description: `Run one app-registered build or test recipe through CaMeL policy.${opts.recipeIds?.length ? ` Available recipes: ${opts.recipeIds.join(", ")}.` : ""}`,
@@ -2221,7 +2238,7 @@ function codeSkill(opts) {
2221
2238
  )
2222
2239
  ];
2223
2240
  const effects = opts.readOnly ? [] : [applyPatch, runRecipe];
2224
- const tools = opts.surface === "v3" ? [...orientation, searchFiles, read2, ...effects] : opts.surface === "v2" ? [listFiles, searchFiles, read2, ...effects] : [read2, ...effects];
2241
+ const tools = opts.surface === "v3" ? [...orientation, searchFiles, read2, ...opts.readOnly ? [] : [editFile], ...effects] : opts.surface === "v2" ? [listFiles, searchFiles, read2, ...effects] : [read2, ...effects];
2225
2242
  return { name: "code", tools };
2226
2243
  }
2227
2244
 
@@ -2511,6 +2528,10 @@ function codeToolRequestPresentation(request) {
2511
2528
  if (request.tool === "sandbox.apply_patch") {
2512
2529
  return { kind: "patch", ...patchStats(input.patch) };
2513
2530
  }
2531
+ if (request.tool === "sandbox.edit") {
2532
+ const lines = (value) => typeof value === "string" ? value.slice(0, 262144).split("\n").length : 0;
2533
+ return { kind: "patch", additions: lines(input.newText), deletions: lines(input.oldText) };
2534
+ }
2514
2535
  const recipeId = text(input.recipeId, 120);
2515
2536
  return recipeId ? { kind: "recipe", recipeId } : void 0;
2516
2537
  }
@@ -2638,6 +2659,11 @@ var PATCH = descriptor("sandbox.apply_patch", "reversible_mutation", {
2638
2659
  authority: "authority",
2639
2660
  patch: "payload"
2640
2661
  });
2662
+ var EDIT = descriptor("sandbox.edit", "reversible_mutation", {
2663
+ workspace: "destination",
2664
+ authority: "authority",
2665
+ patch: "payload"
2666
+ });
2641
2667
  var RECIPE = descriptor("sandbox.run_recipe", "code_execution", {
2642
2668
  workspace: "destination",
2643
2669
  authority: "authority",
@@ -2705,6 +2731,14 @@ function createCodePolicyGate(options) {
2705
2731
  patch: { role: "payload", value: patch2 }
2706
2732
  }, []);
2707
2733
  },
2734
+ edit: async (input) => {
2735
+ const base = await environment(input, options, "sandbox.edit");
2736
+ const patch2 = unsafe(base, input.patch, "patch");
2737
+ return authorize(input, options, base, EDIT, {
2738
+ ...base.fixedArgs,
2739
+ patch: { role: "payload", value: patch2 }
2740
+ }, []);
2741
+ },
2708
2742
  recipe: async (input) => {
2709
2743
  const base = await environment(input, options, "sandbox.run_recipe");
2710
2744
  const conversions = await conversionRegistry([
@@ -2849,13 +2883,134 @@ function response(request, ok, content, details) {
2849
2883
  return { requestId: request.requestId, ok, content, details: { ...details, failureReason } };
2850
2884
  }
2851
2885
 
2886
+ // src/code-tool-edit.ts
2887
+ var import_promises10 = require("fs/promises");
2888
+
2889
+ // src/code-tool-graph.ts
2890
+ var import_promises9 = require("fs/promises");
2891
+ var import_node_path10 = require("path");
2892
+ var import_graph = require("@odla-ai/graph");
2893
+ var import_code4 = require("@odla-ai/graph/code");
2894
+ var cache = /* @__PURE__ */ new Map();
2895
+ function workspaceGraphs(workspaceDir, paths2) {
2896
+ const existing = cache.get(workspaceDir);
2897
+ if (existing) return existing;
2898
+ const read2 = (path) => (0, import_promises9.readFile)((0, import_node_path10.join)(workspaceDir, path), "utf8");
2899
+ const built = (async () => ({
2900
+ // No knownTables: a staged workspace may not carry migrations, and a filter
2901
+ // that silently drops every table is worse than an unfiltered one. Callers
2902
+ // with ground truth should build the graph themselves.
2903
+ graph: await (0, import_code4.buildCodeGraph)({ paths: paths2, read: read2, data: { ignore: (path) => path.includes(".generated.") } })
2904
+ }))();
2905
+ cache.set(workspaceDir, built);
2906
+ return built;
2907
+ }
2908
+ function forgetWorkspaceGraphs(workspaceDir) {
2909
+ cache.delete(workspaceDir);
2910
+ }
2911
+ var shortId = (id) => id.slice(id.indexOf(":") + 1);
2912
+ function renderOverview(graphs, prefix) {
2913
+ const rows = (0, import_graph.rollup)(graphs.graph, import_code4.FILE, prefix === void 0 ? {} : { prefix });
2914
+ if (rows.length === 0) return prefix ? `No source under "${prefix}".` : "No source files.";
2915
+ const lines = rows.slice(0, 60).map((row) => `${row.prefix} (${row.count}) e.g. ${row.examples[0] ?? ""}`);
2916
+ const total = (0, import_graph.nodesOfKind)(graphs.graph, import_code4.FILE).length;
2917
+ return [`${total} source files. Directories, largest first \u2014 read one with sandbox.list --prefix.`, ...lines].join("\n");
2918
+ }
2919
+ function renderWhereIs(graphs, symbol) {
2920
+ const sites = (0, import_graph.neighbors)(graphs.graph, (0, import_graph.nodeId)(import_code4.SYMBOL, symbol), { direction: "in", kinds: ["exports"] }).map((id) => ({
2921
+ path: shortId(id),
2922
+ pkg: (0, import_graph.neighbors)(graphs.graph, id, { direction: "in", kinds: ["contains"] })[0],
2923
+ dependents: (0, import_graph.incident)(graphs.graph, id, { direction: "in", kinds: [import_code4.IMPORTS] }).length
2924
+ })).sort((left, right) => right.dependents - left.dependents || left.path.localeCompare(right.path));
2925
+ if (sites.length === 0) return `No exported symbol named "${symbol}". Try sandbox.search for a textual match.`;
2926
+ return sites.slice(0, 20).map((site) => `${site.path}${site.pkg ? ` [${shortId(site.pkg)}]` : ""} ${site.dependents} dependents`).join("\n");
2927
+ }
2928
+ function renderWhoImports(graphs, path) {
2929
+ const id = (0, import_graph.nodeId)(import_code4.FILE, path);
2930
+ const importers = (0, import_graph.neighbors)(graphs.graph, id, { direction: "in", kinds: [import_code4.IMPORTS] });
2931
+ if (importers.length === 0) {
2932
+ return graphs.graph.nodes.has(id) ? `Nothing imports ${path}. It is a leaf.` : `${path} is not a source file in this workspace.`;
2933
+ }
2934
+ return importers.slice(0, 40).map(shortId).sort().join("\n");
2935
+ }
2936
+ function renderWhoTouches(graphs, query) {
2937
+ const needle = query.toLowerCase();
2938
+ const hits = [...graphs.graph.nodes.values()].filter((node) => (node.kind === "table" || node.kind === "namespace") && node.name.toLowerCase().includes(needle)).slice(0, 10);
2939
+ if (hits.length === 0) return `No table or namespace matching "${query}".`;
2940
+ return hits.map((hit) => {
2941
+ const side = (kind) => (0, import_graph.neighbors)(graphs.graph, hit.id, { direction: "in", kinds: [kind] }).map(shortId).sort().slice(0, 8);
2942
+ return [
2943
+ `${hit.name} (${hit.kind})`,
2944
+ ` writes: ${side(import_code4.WRITES).join(", ") || "(none)"}`,
2945
+ ` reads: ${side(import_code4.READS).join(", ") || "(none)"}`
2946
+ ].join("\n");
2947
+ }).join("\n\n");
2948
+ }
2949
+
2950
+ // src/code-tool-edit.ts
2951
+ var MAX_TEXT_BYTES = 256 * 1024;
2952
+ function editAsDiff(path, oldText, newText) {
2953
+ const removed = oldText.split("\n").map((line) => `-${line}`);
2954
+ const added = newText.split("\n").map((line) => `+${line}`);
2955
+ return `diff --git a/${path} b/${path}
2956
+ --- a/${path}
2957
+ +++ b/${path}
2958
+ @@ -1,${removed.length} +1,${added.length} @@
2959
+ ${[...removed, ...added].join("\n")}
2960
+ `;
2961
+ }
2962
+ function occurrences(haystack, needle) {
2963
+ let count = 0;
2964
+ for (let at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, at + needle.length)) count += 1;
2965
+ return count;
2966
+ }
2967
+ async function editCodeFile(workspaceDir, path, oldText, newText) {
2968
+ if (!oldText) throw new TypeError("oldText must not be empty; to create a file use sandbox.apply_patch with a new-file diff");
2969
+ if (Buffer.byteLength(oldText) > MAX_TEXT_BYTES || Buffer.byteLength(newText) > MAX_TEXT_BYTES) {
2970
+ throw new TypeError(`oldText and newText must each stay under ${MAX_TEXT_BYTES} bytes; split the change`);
2971
+ }
2972
+ if (oldText.includes("\0") || newText.includes("\0")) throw new TypeError("edit text contains NUL bytes; use plain text");
2973
+ const target = resolveCodePath(workspaceDir, path);
2974
+ const current = await (0, import_promises10.readFile)(target, "utf8");
2975
+ const found = occurrences(current, oldText);
2976
+ if (found === 0) {
2977
+ throw new TypeError(`oldText was not found in "${path}"; sandbox.read the current lines and copy them exactly, including indentation and blank lines`);
2978
+ }
2979
+ if (found > 1) {
2980
+ throw new TypeError(`oldText occurs ${found} times in "${path}"; include more of the surrounding lines so it matches exactly once`);
2981
+ }
2982
+ const at = current.indexOf(oldText);
2983
+ await (0, import_promises10.writeFile)(target, `${current.slice(0, at)}${newText}${current.slice(at + oldText.length)}`, "utf8");
2984
+ return { deletions: oldText.split("\n").length, additions: newText.split("\n").length };
2985
+ }
2986
+ async function edit(context, request, options, policy, registry) {
2987
+ exactKeys(request.input, ["path", "oldText", "newText"]);
2988
+ const path = stringField(request.input, "path");
2989
+ const oldText = stringField(request.input, "oldText");
2990
+ const newText = typeof request.input.newText === "string" ? request.input.newText : stringField(request.input, "newText");
2991
+ const paths2 = await registry.files(context.workspaceDir);
2992
+ if (!paths2.includes(path)) {
2993
+ throw new TypeError(`no such file in the staged workspace: "${path}". Use sandbox.overview, sandbox.where_is or sandbox.search to find the correct path.`);
2994
+ }
2995
+ if (options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`))) {
2996
+ throw new TypeError("edit targets a read-only reference source");
2997
+ }
2998
+ const patch2 = editAsDiff(path, oldText, newText);
2999
+ const allowed = await policy.edit(policyContext(context, request, options, { patch: patch2 }));
3000
+ if (!allowed) return response(request, false, "tool denied by CaMeL policy");
3001
+ const changed = await editCodeFile(context.workspaceDir, path, oldText, newText);
3002
+ registry.invalidate(context.workspaceDir);
3003
+ forgetWorkspaceGraphs(context.workspaceDir);
3004
+ return response(request, true, `Edited ${path}: -${changed.deletions} +${changed.additions} line(s).`, { paths: [path], ...changed });
3005
+ }
3006
+
2852
3007
  // src/code-tool-reads.ts
2853
- var import_promises11 = require("fs/promises");
3008
+ var import_promises12 = require("fs/promises");
2854
3009
 
2855
3010
  // src/code-tool-discovery.ts
2856
3011
  var import_node_child_process5 = require("child_process");
2857
- var import_promises9 = require("fs/promises");
2858
- var import_node_path10 = require("path");
3012
+ var import_promises11 = require("fs/promises");
3013
+ var import_node_path11 = require("path");
2859
3014
  var DEFAULT_MAX_FILES = 2e4;
2860
3015
  var DEFAULT_MAX_RESULTS = 100;
2861
3016
  var DEFAULT_MAX_FILE_BYTES = 512 * 1024;
@@ -2880,13 +3035,13 @@ function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = regi
2880
3035
  async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
2881
3036
  const paths2 = [];
2882
3037
  const walk = async (directory) => {
2883
- for (const entry of await (0, import_promises9.readdir)(directory, { withFileTypes: true })) {
3038
+ for (const entry of await (0, import_promises11.readdir)(directory, { withFileTypes: true })) {
2884
3039
  if (SKIP_WORKSPACE_DIRS.has(entry.name)) continue;
2885
3040
  if (entry.isSymbolicLink()) throw new TypeError("workspace contains a symbolic link");
2886
- const target = (0, import_node_path10.resolve)(directory, entry.name);
3041
+ const target = (0, import_node_path11.resolve)(directory, entry.name);
2887
3042
  if (entry.isDirectory()) await walk(target);
2888
3043
  else if (entry.isFile()) {
2889
- const path = (0, import_node_path10.relative)(root, target).split("\\").join("/");
3044
+ const path = (0, import_node_path11.relative)(root, target).split("\\").join("/");
2890
3045
  try {
2891
3046
  validateRelativePath(path);
2892
3047
  } catch {
@@ -2897,7 +3052,7 @@ async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
2897
3052
  }
2898
3053
  }
2899
3054
  };
2900
- await walk((0, import_node_path10.resolve)(root));
3055
+ await walk((0, import_node_path11.resolve)(root));
2901
3056
  return paths2.sort();
2902
3057
  }
2903
3058
  function listWorkspace(paths2, options = {}) {
@@ -3011,7 +3166,7 @@ async function fallbackSearch(root, scoped, options) {
3011
3166
  if (matches.length >= options.maxResults) break;
3012
3167
  let source;
3013
3168
  try {
3014
- source = await (0, import_promises9.readFile)((0, import_node_path10.resolve)(root, path));
3169
+ source = await (0, import_promises11.readFile)((0, import_node_path11.resolve)(root, path));
3015
3170
  } catch {
3016
3171
  continue;
3017
3172
  }
@@ -3028,67 +3183,6 @@ async function fallbackSearch(root, scoped, options) {
3028
3183
  return matches;
3029
3184
  }
3030
3185
 
3031
- // src/code-tool-graph.ts
3032
- var import_promises10 = require("fs/promises");
3033
- var import_node_path11 = require("path");
3034
- var import_graph = require("@odla-ai/graph");
3035
- var import_code4 = require("@odla-ai/graph/code");
3036
- var cache = /* @__PURE__ */ new Map();
3037
- function workspaceGraphs(workspaceDir, paths2) {
3038
- const existing = cache.get(workspaceDir);
3039
- if (existing) return existing;
3040
- const read2 = (path) => (0, import_promises10.readFile)((0, import_node_path11.join)(workspaceDir, path), "utf8");
3041
- const built = (async () => ({
3042
- // No knownTables: a staged workspace may not carry migrations, and a filter
3043
- // that silently drops every table is worse than an unfiltered one. Callers
3044
- // with ground truth should build the graph themselves.
3045
- graph: await (0, import_code4.buildCodeGraph)({ paths: paths2, read: read2, data: { ignore: (path) => path.includes(".generated.") } })
3046
- }))();
3047
- cache.set(workspaceDir, built);
3048
- return built;
3049
- }
3050
- function forgetWorkspaceGraphs(workspaceDir) {
3051
- cache.delete(workspaceDir);
3052
- }
3053
- var shortId = (id) => id.slice(id.indexOf(":") + 1);
3054
- function renderOverview(graphs, prefix) {
3055
- const rows = (0, import_graph.rollup)(graphs.graph, import_code4.FILE, prefix === void 0 ? {} : { prefix });
3056
- if (rows.length === 0) return prefix ? `No source under "${prefix}".` : "No source files.";
3057
- const lines = rows.slice(0, 60).map((row) => `${row.prefix} (${row.count}) e.g. ${row.examples[0] ?? ""}`);
3058
- const total = (0, import_graph.nodesOfKind)(graphs.graph, import_code4.FILE).length;
3059
- return [`${total} source files. Directories, largest first \u2014 read one with sandbox.list --prefix.`, ...lines].join("\n");
3060
- }
3061
- function renderWhereIs(graphs, symbol) {
3062
- const sites = (0, import_graph.neighbors)(graphs.graph, (0, import_graph.nodeId)(import_code4.SYMBOL, symbol), { direction: "in", kinds: ["exports"] }).map((id) => ({
3063
- path: shortId(id),
3064
- pkg: (0, import_graph.neighbors)(graphs.graph, id, { direction: "in", kinds: ["contains"] })[0],
3065
- dependents: (0, import_graph.incident)(graphs.graph, id, { direction: "in", kinds: [import_code4.IMPORTS] }).length
3066
- })).sort((left, right) => right.dependents - left.dependents || left.path.localeCompare(right.path));
3067
- if (sites.length === 0) return `No exported symbol named "${symbol}". Try sandbox.search for a textual match.`;
3068
- return sites.slice(0, 20).map((site) => `${site.path}${site.pkg ? ` [${shortId(site.pkg)}]` : ""} ${site.dependents} dependents`).join("\n");
3069
- }
3070
- function renderWhoImports(graphs, path) {
3071
- const id = (0, import_graph.nodeId)(import_code4.FILE, path);
3072
- const importers = (0, import_graph.neighbors)(graphs.graph, id, { direction: "in", kinds: [import_code4.IMPORTS] });
3073
- if (importers.length === 0) {
3074
- return graphs.graph.nodes.has(id) ? `Nothing imports ${path}. It is a leaf.` : `${path} is not a source file in this workspace.`;
3075
- }
3076
- return importers.slice(0, 40).map(shortId).sort().join("\n");
3077
- }
3078
- function renderWhoTouches(graphs, query) {
3079
- const needle = query.toLowerCase();
3080
- const hits = [...graphs.graph.nodes.values()].filter((node) => (node.kind === "table" || node.kind === "namespace") && node.name.toLowerCase().includes(needle)).slice(0, 10);
3081
- if (hits.length === 0) return `No table or namespace matching "${query}".`;
3082
- return hits.map((hit) => {
3083
- const side = (kind) => (0, import_graph.neighbors)(graphs.graph, hit.id, { direction: "in", kinds: [kind] }).map(shortId).sort().slice(0, 8);
3084
- return [
3085
- `${hit.name} (${hit.kind})`,
3086
- ` writes: ${side(import_code4.WRITES).join(", ") || "(none)"}`,
3087
- ` reads: ${side(import_code4.READS).join(", ") || "(none)"}`
3088
- ].join("\n");
3089
- }).join("\n\n");
3090
- }
3091
-
3092
3186
  // src/code-tool-reads.ts
3093
3187
  var GRAPH_TOOLS = /* @__PURE__ */ new Set([
3094
3188
  "sandbox.overview",
@@ -3111,11 +3205,11 @@ async function read(context, request, options, policy, registry) {
3111
3205
  const allowed = await policy.read(policyContext(context, request, options, { paths: paths2, path, startLine, endLine }));
3112
3206
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
3113
3207
  const target = resolveCodePath(context.workspaceDir, path);
3114
- const info = await (0, import_promises11.stat)(target);
3208
+ const info = await (0, import_promises12.stat)(target);
3115
3209
  if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
3116
3210
  throw new TypeError("file is not a bounded regular source file");
3117
3211
  }
3118
- const source = await (0, import_promises11.readFile)(target);
3212
+ const source = await (0, import_promises12.readFile)(target);
3119
3213
  if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
3120
3214
  const lines = source.toString("utf8").split("\n");
3121
3215
  const content = lines.slice(startLine - 1, endLine).join("\n");
@@ -3230,6 +3324,7 @@ async function route(context, request, options, recipes, policy, registry) {
3230
3324
  if (request.tool === "sandbox.search") return await search(context, request, options, policy, registry);
3231
3325
  if (GRAPH_TOOLS.has(request.tool)) return await graphQuery(context, request, options, policy, registry);
3232
3326
  if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy, registry);
3327
+ if (request.tool === "sandbox.edit") return await edit(context, request, options, policy, registry);
3233
3328
  return await recipe(context, request, options, recipes, policy);
3234
3329
  } catch (reason) {
3235
3330
  return response(request, false, toolFailureMessage(reason));
@@ -4025,7 +4120,7 @@ function parse(argv) {
4025
4120
  };
4026
4121
  }
4027
4122
  async function readPolicy(path) {
4028
- const value = JSON.parse(await (0, import_promises12.readFile)(path, "utf8"));
4123
+ const value = JSON.parse(await (0, import_promises13.readFile)(path, "utf8"));
4029
4124
  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");
4030
4125
  const recipes = value.recipes;
4031
4126
  for (const recipe2 of recipes) assertCodeBuildRecipe(recipe2);