@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.
- package/dist/{chunk-U324RQ4N.js → chunk-CPV7ZVHH.js} +1 -1
- package/dist/{chunk-U324RQ4N.js.map → chunk-CPV7ZVHH.js.map} +1 -1
- package/dist/{chunk-OZBJNTML.js → chunk-ESLFS3FY.js} +4 -4
- package/dist/{chunk-5LRYJKUI.js → chunk-FUDTSAOZ.js} +3 -3
- package/dist/{chunk-VDY5V7ZG.js → chunk-JV45JAAW.js} +3 -3
- package/dist/chunk-JV45JAAW.js.map +1 -0
- package/dist/{chunk-FAN2R3GW.js → chunk-KXFI3WM2.js} +5 -1
- package/dist/chunk-KXFI3WM2.js.map +1 -0
- package/dist/{chunk-WM34GGTK.js → chunk-VIZALA6O.js} +101 -12
- package/dist/chunk-VIZALA6O.js.map +1 -0
- package/dist/cli.cjs +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +4 -4
- package/dist/code-runtime-cli.cjs +173 -78
- package/dist/code-runtime-cli.cjs.map +1 -1
- package/dist/code-runtime-cli.js +5 -5
- package/dist/index.cjs +5 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/node.cjs +177 -82
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +2 -2
- package/dist/node.d.ts +2 -2
- package/dist/node.js +6 -6
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/dist/{types-_8y8vBDI.d.ts → types-ocy70g_p.d.cts} +1 -1
- package/dist/{types-_8y8vBDI.d.cts → types-ocy70g_p.d.ts} +1 -1
- package/package.json +1 -1
- package/dist/chunk-FAN2R3GW.js.map +0 -1
- package/dist/chunk-VDY5V7ZG.js.map +0 -1
- package/dist/chunk-WM34GGTK.js.map +0 -1
- /package/dist/{chunk-OZBJNTML.js.map → chunk-ESLFS3FY.js.map} +0 -0
- /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
|
|
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
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
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
|
|
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
|
|
2858
|
-
var
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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);
|