@odla-ai/harness 0.10.1 → 0.10.3
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-TMBYA5JX.js} +107 -13
- package/dist/chunk-TMBYA5JX.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 +179 -79
- 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 +183 -83
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +11 -2
- package/dist/node.d.ts +11 -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(", ")}.` : ""}`,
|
|
@@ -2220,8 +2237,9 @@ function codeSkill(opts) {
|
|
|
2220
2237
|
true
|
|
2221
2238
|
)
|
|
2222
2239
|
];
|
|
2223
|
-
const
|
|
2224
|
-
const
|
|
2240
|
+
const runsRecipes = opts.recipes ?? !opts.readOnly;
|
|
2241
|
+
const effects = [...opts.readOnly ? [] : [applyPatch], ...runsRecipes ? [runRecipe] : []];
|
|
2242
|
+
const tools = opts.surface === "v3" ? [...orientation, searchFiles, read2, ...opts.readOnly ? [] : [editFile], ...effects] : opts.surface === "v2" ? [listFiles, searchFiles, read2, ...effects] : [read2, ...effects];
|
|
2225
2243
|
return { name: "code", tools };
|
|
2226
2244
|
}
|
|
2227
2245
|
|
|
@@ -2278,6 +2296,7 @@ async function runCodeAgentAttempt(options) {
|
|
|
2278
2296
|
model: "brokered",
|
|
2279
2297
|
surface,
|
|
2280
2298
|
...options.readOnly === void 0 ? {} : { readOnly: options.readOnly },
|
|
2299
|
+
...options.recipes === void 0 ? {} : { recipes: options.recipes },
|
|
2281
2300
|
...options.recipeIds ? { recipeIds: options.recipeIds } : {},
|
|
2282
2301
|
...options.extraSkills ? { extraSkills: options.extraSkills } : {},
|
|
2283
2302
|
...options.maxSteps === void 0 ? {} : { maxSteps: options.maxSteps },
|
|
@@ -2511,6 +2530,10 @@ function codeToolRequestPresentation(request) {
|
|
|
2511
2530
|
if (request.tool === "sandbox.apply_patch") {
|
|
2512
2531
|
return { kind: "patch", ...patchStats(input.patch) };
|
|
2513
2532
|
}
|
|
2533
|
+
if (request.tool === "sandbox.edit") {
|
|
2534
|
+
const lines = (value) => typeof value === "string" ? value.slice(0, 262144).split("\n").length : 0;
|
|
2535
|
+
return { kind: "patch", additions: lines(input.newText), deletions: lines(input.oldText) };
|
|
2536
|
+
}
|
|
2514
2537
|
const recipeId = text(input.recipeId, 120);
|
|
2515
2538
|
return recipeId ? { kind: "recipe", recipeId } : void 0;
|
|
2516
2539
|
}
|
|
@@ -2638,6 +2661,11 @@ var PATCH = descriptor("sandbox.apply_patch", "reversible_mutation", {
|
|
|
2638
2661
|
authority: "authority",
|
|
2639
2662
|
patch: "payload"
|
|
2640
2663
|
});
|
|
2664
|
+
var EDIT = descriptor("sandbox.edit", "reversible_mutation", {
|
|
2665
|
+
workspace: "destination",
|
|
2666
|
+
authority: "authority",
|
|
2667
|
+
patch: "payload"
|
|
2668
|
+
});
|
|
2641
2669
|
var RECIPE = descriptor("sandbox.run_recipe", "code_execution", {
|
|
2642
2670
|
workspace: "destination",
|
|
2643
2671
|
authority: "authority",
|
|
@@ -2705,6 +2733,14 @@ function createCodePolicyGate(options) {
|
|
|
2705
2733
|
patch: { role: "payload", value: patch2 }
|
|
2706
2734
|
}, []);
|
|
2707
2735
|
},
|
|
2736
|
+
edit: async (input) => {
|
|
2737
|
+
const base = await environment(input, options, "sandbox.edit");
|
|
2738
|
+
const patch2 = unsafe(base, input.patch, "patch");
|
|
2739
|
+
return authorize(input, options, base, EDIT, {
|
|
2740
|
+
...base.fixedArgs,
|
|
2741
|
+
patch: { role: "payload", value: patch2 }
|
|
2742
|
+
}, []);
|
|
2743
|
+
},
|
|
2708
2744
|
recipe: async (input) => {
|
|
2709
2745
|
const base = await environment(input, options, "sandbox.run_recipe");
|
|
2710
2746
|
const conversions = await conversionRegistry([
|
|
@@ -2849,13 +2885,134 @@ function response(request, ok, content, details) {
|
|
|
2849
2885
|
return { requestId: request.requestId, ok, content, details: { ...details, failureReason } };
|
|
2850
2886
|
}
|
|
2851
2887
|
|
|
2888
|
+
// src/code-tool-edit.ts
|
|
2889
|
+
var import_promises10 = require("fs/promises");
|
|
2890
|
+
|
|
2891
|
+
// src/code-tool-graph.ts
|
|
2892
|
+
var import_promises9 = require("fs/promises");
|
|
2893
|
+
var import_node_path10 = require("path");
|
|
2894
|
+
var import_graph = require("@odla-ai/graph");
|
|
2895
|
+
var import_code4 = require("@odla-ai/graph/code");
|
|
2896
|
+
var cache = /* @__PURE__ */ new Map();
|
|
2897
|
+
function workspaceGraphs(workspaceDir, paths2) {
|
|
2898
|
+
const existing = cache.get(workspaceDir);
|
|
2899
|
+
if (existing) return existing;
|
|
2900
|
+
const read2 = (path) => (0, import_promises9.readFile)((0, import_node_path10.join)(workspaceDir, path), "utf8");
|
|
2901
|
+
const built = (async () => ({
|
|
2902
|
+
// No knownTables: a staged workspace may not carry migrations, and a filter
|
|
2903
|
+
// that silently drops every table is worse than an unfiltered one. Callers
|
|
2904
|
+
// with ground truth should build the graph themselves.
|
|
2905
|
+
graph: await (0, import_code4.buildCodeGraph)({ paths: paths2, read: read2, data: { ignore: (path) => path.includes(".generated.") } })
|
|
2906
|
+
}))();
|
|
2907
|
+
cache.set(workspaceDir, built);
|
|
2908
|
+
return built;
|
|
2909
|
+
}
|
|
2910
|
+
function forgetWorkspaceGraphs(workspaceDir) {
|
|
2911
|
+
cache.delete(workspaceDir);
|
|
2912
|
+
}
|
|
2913
|
+
var shortId = (id) => id.slice(id.indexOf(":") + 1);
|
|
2914
|
+
function renderOverview(graphs, prefix) {
|
|
2915
|
+
const rows = (0, import_graph.rollup)(graphs.graph, import_code4.FILE, prefix === void 0 ? {} : { prefix });
|
|
2916
|
+
if (rows.length === 0) return prefix ? `No source under "${prefix}".` : "No source files.";
|
|
2917
|
+
const lines = rows.slice(0, 60).map((row) => `${row.prefix} (${row.count}) e.g. ${row.examples[0] ?? ""}`);
|
|
2918
|
+
const total = (0, import_graph.nodesOfKind)(graphs.graph, import_code4.FILE).length;
|
|
2919
|
+
return [`${total} source files. Directories, largest first \u2014 read one with sandbox.list --prefix.`, ...lines].join("\n");
|
|
2920
|
+
}
|
|
2921
|
+
function renderWhereIs(graphs, symbol) {
|
|
2922
|
+
const sites = (0, import_graph.neighbors)(graphs.graph, (0, import_graph.nodeId)(import_code4.SYMBOL, symbol), { direction: "in", kinds: ["exports"] }).map((id) => ({
|
|
2923
|
+
path: shortId(id),
|
|
2924
|
+
pkg: (0, import_graph.neighbors)(graphs.graph, id, { direction: "in", kinds: ["contains"] })[0],
|
|
2925
|
+
dependents: (0, import_graph.incident)(graphs.graph, id, { direction: "in", kinds: [import_code4.IMPORTS] }).length
|
|
2926
|
+
})).sort((left, right) => right.dependents - left.dependents || left.path.localeCompare(right.path));
|
|
2927
|
+
if (sites.length === 0) return `No exported symbol named "${symbol}". Try sandbox.search for a textual match.`;
|
|
2928
|
+
return sites.slice(0, 20).map((site) => `${site.path}${site.pkg ? ` [${shortId(site.pkg)}]` : ""} ${site.dependents} dependents`).join("\n");
|
|
2929
|
+
}
|
|
2930
|
+
function renderWhoImports(graphs, path) {
|
|
2931
|
+
const id = (0, import_graph.nodeId)(import_code4.FILE, path);
|
|
2932
|
+
const importers = (0, import_graph.neighbors)(graphs.graph, id, { direction: "in", kinds: [import_code4.IMPORTS] });
|
|
2933
|
+
if (importers.length === 0) {
|
|
2934
|
+
return graphs.graph.nodes.has(id) ? `Nothing imports ${path}. It is a leaf.` : `${path} is not a source file in this workspace.`;
|
|
2935
|
+
}
|
|
2936
|
+
return importers.slice(0, 40).map(shortId).sort().join("\n");
|
|
2937
|
+
}
|
|
2938
|
+
function renderWhoTouches(graphs, query) {
|
|
2939
|
+
const needle = query.toLowerCase();
|
|
2940
|
+
const hits = [...graphs.graph.nodes.values()].filter((node) => (node.kind === "table" || node.kind === "namespace") && node.name.toLowerCase().includes(needle)).slice(0, 10);
|
|
2941
|
+
if (hits.length === 0) return `No table or namespace matching "${query}".`;
|
|
2942
|
+
return hits.map((hit) => {
|
|
2943
|
+
const side = (kind) => (0, import_graph.neighbors)(graphs.graph, hit.id, { direction: "in", kinds: [kind] }).map(shortId).sort().slice(0, 8);
|
|
2944
|
+
return [
|
|
2945
|
+
`${hit.name} (${hit.kind})`,
|
|
2946
|
+
` writes: ${side(import_code4.WRITES).join(", ") || "(none)"}`,
|
|
2947
|
+
` reads: ${side(import_code4.READS).join(", ") || "(none)"}`
|
|
2948
|
+
].join("\n");
|
|
2949
|
+
}).join("\n\n");
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
// src/code-tool-edit.ts
|
|
2953
|
+
var MAX_TEXT_BYTES = 256 * 1024;
|
|
2954
|
+
function editAsDiff(path, oldText, newText) {
|
|
2955
|
+
const removed = oldText.split("\n").map((line) => `-${line}`);
|
|
2956
|
+
const added = newText.split("\n").map((line) => `+${line}`);
|
|
2957
|
+
return `diff --git a/${path} b/${path}
|
|
2958
|
+
--- a/${path}
|
|
2959
|
+
+++ b/${path}
|
|
2960
|
+
@@ -1,${removed.length} +1,${added.length} @@
|
|
2961
|
+
${[...removed, ...added].join("\n")}
|
|
2962
|
+
`;
|
|
2963
|
+
}
|
|
2964
|
+
function occurrences(haystack, needle) {
|
|
2965
|
+
let count = 0;
|
|
2966
|
+
for (let at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, at + needle.length)) count += 1;
|
|
2967
|
+
return count;
|
|
2968
|
+
}
|
|
2969
|
+
async function editCodeFile(workspaceDir, path, oldText, newText) {
|
|
2970
|
+
if (!oldText) throw new TypeError("oldText must not be empty; to create a file use sandbox.apply_patch with a new-file diff");
|
|
2971
|
+
if (Buffer.byteLength(oldText) > MAX_TEXT_BYTES || Buffer.byteLength(newText) > MAX_TEXT_BYTES) {
|
|
2972
|
+
throw new TypeError(`oldText and newText must each stay under ${MAX_TEXT_BYTES} bytes; split the change`);
|
|
2973
|
+
}
|
|
2974
|
+
if (oldText.includes("\0") || newText.includes("\0")) throw new TypeError("edit text contains NUL bytes; use plain text");
|
|
2975
|
+
const target = resolveCodePath(workspaceDir, path);
|
|
2976
|
+
const current = await (0, import_promises10.readFile)(target, "utf8");
|
|
2977
|
+
const found = occurrences(current, oldText);
|
|
2978
|
+
if (found === 0) {
|
|
2979
|
+
throw new TypeError(`oldText was not found in "${path}"; sandbox.read the current lines and copy them exactly, including indentation and blank lines`);
|
|
2980
|
+
}
|
|
2981
|
+
if (found > 1) {
|
|
2982
|
+
throw new TypeError(`oldText occurs ${found} times in "${path}"; include more of the surrounding lines so it matches exactly once`);
|
|
2983
|
+
}
|
|
2984
|
+
const at = current.indexOf(oldText);
|
|
2985
|
+
await (0, import_promises10.writeFile)(target, `${current.slice(0, at)}${newText}${current.slice(at + oldText.length)}`, "utf8");
|
|
2986
|
+
return { deletions: oldText.split("\n").length, additions: newText.split("\n").length };
|
|
2987
|
+
}
|
|
2988
|
+
async function edit(context, request, options, policy, registry) {
|
|
2989
|
+
exactKeys(request.input, ["path", "oldText", "newText"]);
|
|
2990
|
+
const path = stringField(request.input, "path");
|
|
2991
|
+
const oldText = stringField(request.input, "oldText");
|
|
2992
|
+
const newText = typeof request.input.newText === "string" ? request.input.newText : stringField(request.input, "newText");
|
|
2993
|
+
const paths2 = await registry.files(context.workspaceDir);
|
|
2994
|
+
if (!paths2.includes(path)) {
|
|
2995
|
+
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.`);
|
|
2996
|
+
}
|
|
2997
|
+
if (options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`))) {
|
|
2998
|
+
throw new TypeError("edit targets a read-only reference source");
|
|
2999
|
+
}
|
|
3000
|
+
const patch2 = editAsDiff(path, oldText, newText);
|
|
3001
|
+
const allowed = await policy.edit(policyContext(context, request, options, { patch: patch2 }));
|
|
3002
|
+
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
3003
|
+
const changed = await editCodeFile(context.workspaceDir, path, oldText, newText);
|
|
3004
|
+
registry.invalidate(context.workspaceDir);
|
|
3005
|
+
forgetWorkspaceGraphs(context.workspaceDir);
|
|
3006
|
+
return response(request, true, `Edited ${path}: -${changed.deletions} +${changed.additions} line(s).`, { paths: [path], ...changed });
|
|
3007
|
+
}
|
|
3008
|
+
|
|
2852
3009
|
// src/code-tool-reads.ts
|
|
2853
|
-
var
|
|
3010
|
+
var import_promises12 = require("fs/promises");
|
|
2854
3011
|
|
|
2855
3012
|
// src/code-tool-discovery.ts
|
|
2856
3013
|
var import_node_child_process5 = require("child_process");
|
|
2857
|
-
var
|
|
2858
|
-
var
|
|
3014
|
+
var import_promises11 = require("fs/promises");
|
|
3015
|
+
var import_node_path11 = require("path");
|
|
2859
3016
|
var DEFAULT_MAX_FILES = 2e4;
|
|
2860
3017
|
var DEFAULT_MAX_RESULTS = 100;
|
|
2861
3018
|
var DEFAULT_MAX_FILE_BYTES = 512 * 1024;
|
|
@@ -2880,13 +3037,13 @@ function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = regi
|
|
|
2880
3037
|
async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
|
|
2881
3038
|
const paths2 = [];
|
|
2882
3039
|
const walk = async (directory) => {
|
|
2883
|
-
for (const entry of await (0,
|
|
3040
|
+
for (const entry of await (0, import_promises11.readdir)(directory, { withFileTypes: true })) {
|
|
2884
3041
|
if (SKIP_WORKSPACE_DIRS.has(entry.name)) continue;
|
|
2885
3042
|
if (entry.isSymbolicLink()) throw new TypeError("workspace contains a symbolic link");
|
|
2886
|
-
const target = (0,
|
|
3043
|
+
const target = (0, import_node_path11.resolve)(directory, entry.name);
|
|
2887
3044
|
if (entry.isDirectory()) await walk(target);
|
|
2888
3045
|
else if (entry.isFile()) {
|
|
2889
|
-
const path = (0,
|
|
3046
|
+
const path = (0, import_node_path11.relative)(root, target).split("\\").join("/");
|
|
2890
3047
|
try {
|
|
2891
3048
|
validateRelativePath(path);
|
|
2892
3049
|
} catch {
|
|
@@ -2897,7 +3054,7 @@ async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
|
|
|
2897
3054
|
}
|
|
2898
3055
|
}
|
|
2899
3056
|
};
|
|
2900
|
-
await walk((0,
|
|
3057
|
+
await walk((0, import_node_path11.resolve)(root));
|
|
2901
3058
|
return paths2.sort();
|
|
2902
3059
|
}
|
|
2903
3060
|
function listWorkspace(paths2, options = {}) {
|
|
@@ -3011,7 +3168,7 @@ async function fallbackSearch(root, scoped, options) {
|
|
|
3011
3168
|
if (matches.length >= options.maxResults) break;
|
|
3012
3169
|
let source;
|
|
3013
3170
|
try {
|
|
3014
|
-
source = await (0,
|
|
3171
|
+
source = await (0, import_promises11.readFile)((0, import_node_path11.resolve)(root, path));
|
|
3015
3172
|
} catch {
|
|
3016
3173
|
continue;
|
|
3017
3174
|
}
|
|
@@ -3028,67 +3185,6 @@ async function fallbackSearch(root, scoped, options) {
|
|
|
3028
3185
|
return matches;
|
|
3029
3186
|
}
|
|
3030
3187
|
|
|
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
3188
|
// src/code-tool-reads.ts
|
|
3093
3189
|
var GRAPH_TOOLS = /* @__PURE__ */ new Set([
|
|
3094
3190
|
"sandbox.overview",
|
|
@@ -3111,11 +3207,11 @@ async function read(context, request, options, policy, registry) {
|
|
|
3111
3207
|
const allowed = await policy.read(policyContext(context, request, options, { paths: paths2, path, startLine, endLine }));
|
|
3112
3208
|
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
3113
3209
|
const target = resolveCodePath(context.workspaceDir, path);
|
|
3114
|
-
const info = await (0,
|
|
3210
|
+
const info = await (0, import_promises12.stat)(target);
|
|
3115
3211
|
if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
|
|
3116
3212
|
throw new TypeError("file is not a bounded regular source file");
|
|
3117
3213
|
}
|
|
3118
|
-
const source = await (0,
|
|
3214
|
+
const source = await (0, import_promises12.readFile)(target);
|
|
3119
3215
|
if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
|
|
3120
3216
|
const lines = source.toString("utf8").split("\n");
|
|
3121
3217
|
const content = lines.slice(startLine - 1, endLine).join("\n");
|
|
@@ -3230,6 +3326,7 @@ async function route(context, request, options, recipes, policy, registry) {
|
|
|
3230
3326
|
if (request.tool === "sandbox.search") return await search(context, request, options, policy, registry);
|
|
3231
3327
|
if (GRAPH_TOOLS.has(request.tool)) return await graphQuery(context, request, options, policy, registry);
|
|
3232
3328
|
if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy, registry);
|
|
3329
|
+
if (request.tool === "sandbox.edit") return await edit(context, request, options, policy, registry);
|
|
3233
3330
|
return await recipe(context, request, options, recipes, policy);
|
|
3234
3331
|
} catch (reason) {
|
|
3235
3332
|
return response(request, false, toolFailureMessage(reason));
|
|
@@ -3916,6 +4013,9 @@ var TheseusRuntimeEngine = class {
|
|
|
3916
4013
|
prompt: metadata.prompt,
|
|
3917
4014
|
signal: active.abort.signal,
|
|
3918
4015
|
readOnly: metadata.readOnly,
|
|
4016
|
+
// A review-role session is read-only and still runs the proof; a planner
|
|
4017
|
+
// marked read-only by its payload runs nothing.
|
|
4018
|
+
recipes: metadata.role === "review" || !metadata.readOnly,
|
|
3919
4019
|
recipeIds: this.options.recipes.map((recipe2) => recipe2.id),
|
|
3920
4020
|
...extraSkills.length ? { extraSkills } : {}
|
|
3921
4021
|
});
|
|
@@ -4025,7 +4125,7 @@ function parse(argv) {
|
|
|
4025
4125
|
};
|
|
4026
4126
|
}
|
|
4027
4127
|
async function readPolicy(path) {
|
|
4028
|
-
const value = JSON.parse(await (0,
|
|
4128
|
+
const value = JSON.parse(await (0, import_promises13.readFile)(path, "utf8"));
|
|
4029
4129
|
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
4130
|
const recipes = value.recipes;
|
|
4031
4131
|
for (const recipe2 of recipes) assertCodeBuildRecipe(recipe2);
|