@ordewell/core 0.9.0 → 0.9.1
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/{IFileSystem-DpaaFdit.d.ts → IFileSystem-BjiyCQ8O.d.ts} +3 -1
- package/dist/{IFileSystem-_m7HdoxJ.d.mts → IFileSystem-Cy-TAWvu.d.mts} +3 -1
- package/dist/{IRunner-BiQio8gJ.d.ts → IRunner-Corods20.d.ts} +389 -67
- package/dist/{IRunner-CxiymkP_.d.mts → IRunner-D6YfbmHn.d.mts} +389 -67
- package/dist/{chunk-QUARDKVT.mjs → chunk-OOZWSUIE.mjs} +2 -2
- package/dist/chunk-OOZWSUIE.mjs.map +1 -0
- package/dist/index.d.mts +357 -198
- package/dist/index.d.ts +357 -198
- package/dist/index.js +2388 -511
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2365 -496
- package/dist/index.mjs.map +1 -1
- package/dist/{plan-utils-P3PIEwNa.d.mts → plan-utils-BmUp_Lko.d.mts} +3 -3
- package/dist/{plan-utils-6rUr7ZOx.d.ts → plan-utils-tWfH48YX.d.ts} +3 -3
- package/dist/plan-utils.d.mts +2 -2
- package/dist/plan-utils.d.ts +2 -2
- package/dist/plan-utils.js.map +1 -1
- package/dist/plan-utils.mjs +1 -1
- package/dist/testing.d.mts +2 -2
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +1 -0
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +1 -0
- package/dist/testing.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-QUARDKVT.mjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -59,7 +59,9 @@ __export(index_exports, {
|
|
|
59
59
|
DEFAULT_MAX_PARALLEL: () => DEFAULT_MAX_PARALLEL,
|
|
60
60
|
DEFAULT_PLANNER_ALLOWLIST: () => DEFAULT_PLANNER_ALLOWLIST,
|
|
61
61
|
DEFAULT_RUNNERS: () => DEFAULT_RUNNERS,
|
|
62
|
+
DEFAULT_WEB_READ_BUDGET: () => DEFAULT_WEB_READ_BUDGET,
|
|
62
63
|
DENY_ALL: () => DENY_ALL,
|
|
64
|
+
DuckDuckGoSearchProvider: () => DuckDuckGoSearchProvider,
|
|
63
65
|
EMPTY_CONVERSATION: () => EMPTY_CONVERSATION,
|
|
64
66
|
EMPTY_HOLD: () => EMPTY_HOLD,
|
|
65
67
|
EMPTY_TASK_LOG: () => EMPTY_TASK_LOG,
|
|
@@ -71,6 +73,7 @@ __export(index_exports, {
|
|
|
71
73
|
GREP_PCRE_PROBE_ARGS: () => GREP_PCRE_PROBE_ARGS,
|
|
72
74
|
GeminiService: () => GeminiService,
|
|
73
75
|
HARNESS_PLANNER_REASON: () => HARNESS_PLANNER_REASON,
|
|
76
|
+
HttpWebFetcher: () => HttpWebFetcher,
|
|
74
77
|
JSON_REPAIR_INSTRUCTION: () => JSON_REPAIR_INSTRUCTION,
|
|
75
78
|
LineBuffer: () => LineBuffer,
|
|
76
79
|
ModelCatalog: () => ModelCatalog,
|
|
@@ -137,6 +140,7 @@ __export(index_exports, {
|
|
|
137
140
|
TaskOrchestrator: () => TaskOrchestrator,
|
|
138
141
|
VerdictEngine: () => VerdictEngine,
|
|
139
142
|
WINDOWS_MAX_COMMAND_LINE: () => WINDOWS_MAX_COMMAND_LINE,
|
|
143
|
+
WebResponseCache: () => WebResponseCache,
|
|
140
144
|
WorkspaceNotAProjectError: () => WorkspaceNotAProjectError,
|
|
141
145
|
WorkspaceNotFoundError: () => WorkspaceNotFoundError,
|
|
142
146
|
abbreviateHome: () => abbreviateHome,
|
|
@@ -278,6 +282,7 @@ __export(index_exports, {
|
|
|
278
282
|
loadSessionPlanState: () => loadSessionPlanState,
|
|
279
283
|
loadState: () => loadState,
|
|
280
284
|
loadedSkillTokens: () => loadedSkillTokens,
|
|
285
|
+
looksLikeChallenge: () => looksLikeChallenge,
|
|
281
286
|
looksLikePlanAttempt: () => looksLikePlanAttempt,
|
|
282
287
|
mapAgentTool: () => mapAgentTool,
|
|
283
288
|
markAction: () => markAction,
|
|
@@ -310,6 +315,7 @@ __export(index_exports, {
|
|
|
310
315
|
parseMaxParallel: () => parseMaxParallel,
|
|
311
316
|
parsePartialPlan: () => parsePartialPlan,
|
|
312
317
|
parsePlanJson: () => parsePlanJson,
|
|
318
|
+
parseSearchHits: () => parseSearchHits,
|
|
313
319
|
parseTaskOpsJson: () => parseTaskOpsJson,
|
|
314
320
|
parseTaskQueryJson: () => parseTaskQueryJson,
|
|
315
321
|
partedPromptUsage: () => partedPromptUsage,
|
|
@@ -370,6 +376,8 @@ __export(index_exports, {
|
|
|
370
376
|
sessionDataDir: () => sessionDataDir,
|
|
371
377
|
sessionRuntimeSettings: () => sessionRuntimeSettings,
|
|
372
378
|
sharedMcpServer: () => sharedMcpServer,
|
|
379
|
+
sharedSearchProvider: () => sharedSearchProvider,
|
|
380
|
+
sharedWebResponseCache: () => sharedWebResponseCache,
|
|
373
381
|
skillNames: () => skillNames,
|
|
374
382
|
skillTokens: () => skillTokens,
|
|
375
383
|
stateExists: () => stateExists,
|
|
@@ -953,7 +961,7 @@ function toTaskLogEvent(event) {
|
|
|
953
961
|
}
|
|
954
962
|
}
|
|
955
963
|
function taskLogForSurface(events) {
|
|
956
|
-
return events.map((event) => event.type === "task_skills" ? { type: "task_skills", skills: event.skills.map(({ name, source, path:
|
|
964
|
+
return events.map((event) => event.type === "task_skills" ? { type: "task_skills", skills: event.skills.map(({ name, source, path: path32 }) => ({ name, source, path: path32 })) } : event);
|
|
957
965
|
}
|
|
958
966
|
function coalesceTaskLog(events) {
|
|
959
967
|
const out = [];
|
|
@@ -1208,6 +1216,7 @@ function lex(command, nested2, dialect) {
|
|
|
1208
1216
|
let followable = true;
|
|
1209
1217
|
let bareSegment = false;
|
|
1210
1218
|
let tokens = [];
|
|
1219
|
+
let wordEnds = [];
|
|
1211
1220
|
let expandable = false;
|
|
1212
1221
|
let globbed = false;
|
|
1213
1222
|
let current = "";
|
|
@@ -1244,7 +1253,10 @@ function lex(command, nested2, dialect) {
|
|
|
1244
1253
|
started = false;
|
|
1245
1254
|
return;
|
|
1246
1255
|
}
|
|
1247
|
-
if (started)
|
|
1256
|
+
if (started) {
|
|
1257
|
+
tokens.push(current);
|
|
1258
|
+
wordEnds.push(i);
|
|
1259
|
+
}
|
|
1248
1260
|
current = "";
|
|
1249
1261
|
started = false;
|
|
1250
1262
|
};
|
|
@@ -1273,12 +1285,13 @@ function lex(command, nested2, dialect) {
|
|
|
1273
1285
|
followable = false;
|
|
1274
1286
|
bareSegment = true;
|
|
1275
1287
|
}
|
|
1276
|
-
segments.push({ ...seg, expandable, globbed, stdinRedirected, inputs, joinedBy });
|
|
1288
|
+
segments.push({ ...seg, expandable, globbed, stdinRedirected, inputs, joinedBy, wordEnds });
|
|
1277
1289
|
} else if (!last || segments.length > 0 || joinedBy !== void 0) {
|
|
1278
1290
|
followable = false;
|
|
1279
1291
|
bareSegment = true;
|
|
1280
1292
|
}
|
|
1281
1293
|
tokens = [];
|
|
1294
|
+
wordEnds = [];
|
|
1282
1295
|
inputs = [];
|
|
1283
1296
|
expandable = false;
|
|
1284
1297
|
globbed = false;
|
|
@@ -1302,6 +1315,10 @@ function lex(command, nested2, dialect) {
|
|
|
1302
1315
|
}
|
|
1303
1316
|
if (c === dialect.escape && (quote === "" || dialect.escapeInQuotes)) {
|
|
1304
1317
|
const next = command[i + 1];
|
|
1318
|
+
if (dialect.escape === "\\" && next === "\n") {
|
|
1319
|
+
i += 2;
|
|
1320
|
+
continue;
|
|
1321
|
+
}
|
|
1305
1322
|
if (redirectTarget === "heredoc") hereDocQuoted = true;
|
|
1306
1323
|
if (next !== void 0 && !(quote === "" && nameDelimiter(next))) {
|
|
1307
1324
|
current += next;
|
|
@@ -1543,7 +1560,7 @@ function lexAll(command, dialect, cdpathSet = false) {
|
|
|
1543
1560
|
if (top.followable && !top.grouped && queue.length === 0 && !cdpathSet) followCd(all.segments);
|
|
1544
1561
|
for (let depth = 0; depth < 32 && queue.length > 0; depth++) {
|
|
1545
1562
|
const inner = lex(queue.shift(), queue, dialect);
|
|
1546
|
-
all.segments.push(...inner.segments.filter((s) => s.binary));
|
|
1563
|
+
all.segments.push(...inner.segments.filter((s) => s.binary).map((s) => ({ ...s, nested: true })));
|
|
1547
1564
|
all.unsafeRedirect ??= inner.unsafeRedirect;
|
|
1548
1565
|
all.processSubstitution ||= inner.processSubstitution;
|
|
1549
1566
|
all.unbalanced ||= inner.unbalanced;
|
|
@@ -3718,7 +3735,7 @@ function exclusionWord(pattern) {
|
|
|
3718
3735
|
return { matches: (word) => re.test(word), wildcard: pattern.includes("*") };
|
|
3719
3736
|
}
|
|
3720
3737
|
function isScopePattern(entry) {
|
|
3721
|
-
return /^(\/|~|https?:\/\/|mcp__|[A-Za-z]:[\\/])/.test(entry);
|
|
3738
|
+
return /^(\/|~|https?:\/\/|web_search:|mcp__|mcp-server:|[A-Za-z]:[\\/])/.test(entry);
|
|
3722
3739
|
}
|
|
3723
3740
|
function parseAllowlist(entries) {
|
|
3724
3741
|
const list = { commands: [], commandExclusions: [], tools: [], toolExclusions: [], scopes: [], scopeExclusions: [] };
|
|
@@ -3752,6 +3769,7 @@ function ruleCovers(rule, command) {
|
|
|
3752
3769
|
return rule.words.every((word, i) => word.matches(lead[i]) && !(word.wildcard && MUTATING_VERB.test(lead[i])));
|
|
3753
3770
|
}
|
|
3754
3771
|
function exclusionCovers(rule, command) {
|
|
3772
|
+
if (/^!\* \*https?:\/\/\*$/.test(rule.text) && /^(curl|wget)$/.test(command.binary) && command.args.length === 1 && /^https?:\/\/\S+\/\*$/.test(command.args[0])) return false;
|
|
3755
3773
|
if (rule.binary !== "*" && rule.binary !== command.binary) return false;
|
|
3756
3774
|
let next = 0;
|
|
3757
3775
|
for (const arg of command.args) {
|
|
@@ -3767,8 +3785,14 @@ function toolAllowed(list, tool2) {
|
|
|
3767
3785
|
if (list.toolExclusions.some((re) => re.test(tool2))) return false;
|
|
3768
3786
|
return list.tools.some((re) => re.test(tool2));
|
|
3769
3787
|
}
|
|
3788
|
+
function scopeExcluded(list, scope) {
|
|
3789
|
+
const origin = /^(?:curl|wget) (https?:\/\/\S+\/\*)$/.exec(scope)?.[1];
|
|
3790
|
+
return list.scopeExclusions.some((re) => re.test(scope) || origin !== void 0 && re.test(origin));
|
|
3791
|
+
}
|
|
3770
3792
|
function scopeAllowed(list, scope) {
|
|
3771
|
-
if (list
|
|
3793
|
+
if (scopeExcluded(list, scope)) return false;
|
|
3794
|
+
const web = /^(curl|wget) (https?:\/\/\S+\/\*)$/.exec(scope);
|
|
3795
|
+
if (web) return commandAllowedBy(list, { binary: web[1], args: [web[2]], scopeWords: 1 }) !== void 0;
|
|
3772
3796
|
return list.scopes.some((re) => re.test(scope)) || list.commands.some((rule) => rule.text === scope);
|
|
3773
3797
|
}
|
|
3774
3798
|
function withArgument(binary, verbs) {
|
|
@@ -4036,6 +4060,405 @@ var DEFAULT_PLANNER_ALLOWLIST = [
|
|
|
4036
4060
|
...["*secret*", "*token*", "*password*", "*credential*", "*key*", "*url*", "*fetch*", "*http*"].map((w) => `!mcp:${w}`)
|
|
4037
4061
|
];
|
|
4038
4062
|
|
|
4063
|
+
// src/services/webCommands.ts
|
|
4064
|
+
var import_node_net2 = require("node:net");
|
|
4065
|
+
|
|
4066
|
+
// src/services/egressGuard.ts
|
|
4067
|
+
var import_promises = require("node:dns/promises");
|
|
4068
|
+
var import_node_net = require("node:net");
|
|
4069
|
+
var resolveAll = async (host) => (await (0, import_promises.lookup)(host, { all: true, verbatim: true })).map((a) => a.address);
|
|
4070
|
+
var ALLOWED_PORTS = /* @__PURE__ */ new Set(["", "80", "443"]);
|
|
4071
|
+
var LOCAL_NAMES = /* @__PURE__ */ new Set(["localhost", "ip6-localhost", "ip6-loopback"]);
|
|
4072
|
+
function ipv4ToInt(ip) {
|
|
4073
|
+
return ip.split(".").reduce((acc, part) => acc * 256 + Number(part), 0);
|
|
4074
|
+
}
|
|
4075
|
+
var BLOCKED_V4 = [
|
|
4076
|
+
["0.0.0.0", 8, '"this network"'],
|
|
4077
|
+
["10.0.0.0", 8, "private"],
|
|
4078
|
+
["100.64.0.0", 10, "shared (CGNAT)"],
|
|
4079
|
+
["127.0.0.0", 8, "loopback"],
|
|
4080
|
+
["169.254.0.0", 16, "link-local"],
|
|
4081
|
+
["172.16.0.0", 12, "private"],
|
|
4082
|
+
["192.168.0.0", 16, "private"],
|
|
4083
|
+
["198.18.0.0", 15, "benchmarking"],
|
|
4084
|
+
["224.0.0.0", 4, "multicast"],
|
|
4085
|
+
// Includes the 255.255.255.255 limited broadcast.
|
|
4086
|
+
["240.0.0.0", 4, "reserved"]
|
|
4087
|
+
].map(([base, bits, label]) => [ipv4ToInt(base), bits, label]);
|
|
4088
|
+
function blockedV4Reason(ip) {
|
|
4089
|
+
const n = ipv4ToInt(ip);
|
|
4090
|
+
for (const [base, bits, label] of BLOCKED_V4) {
|
|
4091
|
+
const size = 2 ** (32 - bits);
|
|
4092
|
+
if (n >= base && n < base + size) return `${label} address ${ip}`;
|
|
4093
|
+
}
|
|
4094
|
+
return void 0;
|
|
4095
|
+
}
|
|
4096
|
+
function ipv6Bytes(ip) {
|
|
4097
|
+
let text2 = ip;
|
|
4098
|
+
const tail = [];
|
|
4099
|
+
const dotted = /(\d+\.\d+\.\d+\.\d+)$/.exec(text2);
|
|
4100
|
+
if (dotted) {
|
|
4101
|
+
tail.push(...dotted[1].split(".").map(Number));
|
|
4102
|
+
text2 = text2.slice(0, -dotted[1].length) + "0:0";
|
|
4103
|
+
}
|
|
4104
|
+
const [head, rest] = text2.includes("::") ? text2.split("::") : [text2, void 0];
|
|
4105
|
+
const left = head ? head.split(":") : [];
|
|
4106
|
+
const right = rest ? rest.split(":") : [];
|
|
4107
|
+
const groups = rest === void 0 ? left : [...left, ...Array(8 - left.length - right.length).fill("0"), ...right];
|
|
4108
|
+
const bytes = groups.flatMap((g) => {
|
|
4109
|
+
const v = parseInt(g, 16);
|
|
4110
|
+
return [v >> 8, v & 255];
|
|
4111
|
+
});
|
|
4112
|
+
if (tail.length) bytes.splice(12, 4, ...tail);
|
|
4113
|
+
return bytes;
|
|
4114
|
+
}
|
|
4115
|
+
function inPrefix(bytes, prefix, bits) {
|
|
4116
|
+
for (let i = 0; i < bits; i += 8) {
|
|
4117
|
+
const mask = 255 << 8 - Math.min(8, bits - i) & 255;
|
|
4118
|
+
if ((bytes[i / 8] & mask) !== ((prefix[i / 8] ?? 0) & mask)) return false;
|
|
4119
|
+
}
|
|
4120
|
+
return true;
|
|
4121
|
+
}
|
|
4122
|
+
var BLOCKED_V6 = [
|
|
4123
|
+
[[], 127, "unspecified/loopback"],
|
|
4124
|
+
[[252], 7, "unique-local"],
|
|
4125
|
+
[[254, 128], 10, "link-local"],
|
|
4126
|
+
[[254, 192], 10, "site-local"],
|
|
4127
|
+
[[255], 8, "multicast"]
|
|
4128
|
+
];
|
|
4129
|
+
var MAPPED_V4 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255];
|
|
4130
|
+
var NAT64_V4 = [0, 100, 255, 155];
|
|
4131
|
+
function blockedV6Reason(ip) {
|
|
4132
|
+
const bytes = ipv6Bytes(ip);
|
|
4133
|
+
const v4 = bytes.slice(12).join(".");
|
|
4134
|
+
if (inPrefix(bytes, MAPPED_V4, 96) || inPrefix(bytes, NAT64_V4, 96)) return blockedV4Reason(v4);
|
|
4135
|
+
for (const [prefix, bits, label] of BLOCKED_V6) {
|
|
4136
|
+
if (inPrefix(bytes, prefix, bits)) return `${label} address ${ip}`;
|
|
4137
|
+
}
|
|
4138
|
+
return void 0;
|
|
4139
|
+
}
|
|
4140
|
+
function blockedAddressReason(ip) {
|
|
4141
|
+
if (typeof ip !== "string") return "unparseable address";
|
|
4142
|
+
const bare = ip.replace(/%.*$/, "");
|
|
4143
|
+
switch ((0, import_node_net.isIP)(bare)) {
|
|
4144
|
+
case 4:
|
|
4145
|
+
return blockedV4Reason(bare);
|
|
4146
|
+
case 6:
|
|
4147
|
+
return blockedV6Reason(bare.toLowerCase());
|
|
4148
|
+
default:
|
|
4149
|
+
return "unparseable address";
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
function stripBrackets(host) {
|
|
4153
|
+
return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
4154
|
+
}
|
|
4155
|
+
function vetHostUnresolved(host) {
|
|
4156
|
+
const bare = stripBrackets(host);
|
|
4157
|
+
if ((0, import_node_net.isIP)(bare)) {
|
|
4158
|
+
const reason = blockedAddressReason(bare);
|
|
4159
|
+
return reason ? { ok: false, reason } : { ok: true, addresses: [bare] };
|
|
4160
|
+
}
|
|
4161
|
+
if (LOCAL_NAMES.has(bare.toLowerCase())) return { ok: false, reason: `local host name ${bare}` };
|
|
4162
|
+
return void 0;
|
|
4163
|
+
}
|
|
4164
|
+
async function vetHost(host, resolve9) {
|
|
4165
|
+
const settled = vetHostUnresolved(host);
|
|
4166
|
+
if (settled) return settled;
|
|
4167
|
+
const bare = stripBrackets(host);
|
|
4168
|
+
let addresses;
|
|
4169
|
+
try {
|
|
4170
|
+
addresses = await resolve9(bare);
|
|
4171
|
+
} catch {
|
|
4172
|
+
return { ok: false, reason: `${bare} did not resolve` };
|
|
4173
|
+
}
|
|
4174
|
+
if (addresses.length === 0) return { ok: false, reason: `${bare} did not resolve` };
|
|
4175
|
+
for (const address of addresses) {
|
|
4176
|
+
const reason = blockedAddressReason(address);
|
|
4177
|
+
if (reason) return { ok: false, reason: `${bare} resolves to a ${reason}` };
|
|
4178
|
+
}
|
|
4179
|
+
return { ok: true, addresses };
|
|
4180
|
+
}
|
|
4181
|
+
async function vetEgress(host, port, resolve9 = resolveAll) {
|
|
4182
|
+
return vetEgressUnresolved(host, port) ?? vetHost(host, resolve9);
|
|
4183
|
+
}
|
|
4184
|
+
function vetEgressUnresolved(host, port) {
|
|
4185
|
+
if (!ALLOWED_PORTS.has(port)) return { ok: false, reason: `port ${port} is not 80 or 443` };
|
|
4186
|
+
return vetHostUnresolved(host);
|
|
4187
|
+
}
|
|
4188
|
+
function guardedLookup(resolve9 = resolveAll) {
|
|
4189
|
+
return (hostname, options, callback) => {
|
|
4190
|
+
void vetHost(hostname, resolve9).then((check2) => {
|
|
4191
|
+
if (!check2.ok) {
|
|
4192
|
+
callback(Object.assign(new Error(`blocked host: ${check2.reason}`), { code: "EBLOCKEDHOST" }), []);
|
|
4193
|
+
return;
|
|
4194
|
+
}
|
|
4195
|
+
const family = options.family === "IPv4" ? 4 : options.family === "IPv6" ? 6 : options.family;
|
|
4196
|
+
const addresses = check2.addresses.map((address) => ({ address, family: (0, import_node_net.isIP)(address) })).filter((a) => !family || a.family === family);
|
|
4197
|
+
if (addresses.length === 0) {
|
|
4198
|
+
callback(Object.assign(new Error(`getaddrinfo ENOTFOUND ${hostname}`), { code: "ENOTFOUND" }), []);
|
|
4199
|
+
} else if (options.all) {
|
|
4200
|
+
callback(null, addresses);
|
|
4201
|
+
} else {
|
|
4202
|
+
callback(null, addresses[0].address, addresses[0].family);
|
|
4203
|
+
}
|
|
4204
|
+
});
|
|
4205
|
+
};
|
|
4206
|
+
}
|
|
4207
|
+
|
|
4208
|
+
// src/services/webCommands.ts
|
|
4209
|
+
var READ_REFUSAL_HINT = "Fetch the page with your web fetch tool instead.";
|
|
4210
|
+
var EFFECT_REFUSAL_HINT = "This sends data or changes something, so the planner should not do it itself: add it to the plan as an ops task (and, if it writes files, a change task).";
|
|
4211
|
+
var CURL_EFFECT = /* @__PURE__ */ new Set([
|
|
4212
|
+
"d",
|
|
4213
|
+
"F",
|
|
4214
|
+
"T",
|
|
4215
|
+
"O",
|
|
4216
|
+
"D",
|
|
4217
|
+
"c",
|
|
4218
|
+
"J",
|
|
4219
|
+
"--data",
|
|
4220
|
+
"--data-raw",
|
|
4221
|
+
"--data-binary",
|
|
4222
|
+
"--data-ascii",
|
|
4223
|
+
"--data-urlencode",
|
|
4224
|
+
"--form",
|
|
4225
|
+
"--form-string",
|
|
4226
|
+
"--json",
|
|
4227
|
+
"--upload-file",
|
|
4228
|
+
"--remote-name",
|
|
4229
|
+
"--remote-name-all",
|
|
4230
|
+
"--dump-header",
|
|
4231
|
+
"--cookie-jar",
|
|
4232
|
+
"--create-dirs",
|
|
4233
|
+
"--output-dir",
|
|
4234
|
+
"--remote-header-name"
|
|
4235
|
+
]);
|
|
4236
|
+
var WGET_EFFECT = /* @__PURE__ */ new Set([
|
|
4237
|
+
"P",
|
|
4238
|
+
"r",
|
|
4239
|
+
"m",
|
|
4240
|
+
"N",
|
|
4241
|
+
"c",
|
|
4242
|
+
"a",
|
|
4243
|
+
"o",
|
|
4244
|
+
"--post-data",
|
|
4245
|
+
"--post-file",
|
|
4246
|
+
"--body-data",
|
|
4247
|
+
"--body-file",
|
|
4248
|
+
"--method",
|
|
4249
|
+
"--output-file",
|
|
4250
|
+
"--append-output",
|
|
4251
|
+
"--directory-prefix",
|
|
4252
|
+
"--recursive",
|
|
4253
|
+
"--mirror",
|
|
4254
|
+
"--timestamping",
|
|
4255
|
+
"--continue",
|
|
4256
|
+
"--save-headers",
|
|
4257
|
+
"--content-disposition",
|
|
4258
|
+
"--input-file",
|
|
4259
|
+
"--warc-file",
|
|
4260
|
+
"--save-cookies"
|
|
4261
|
+
]);
|
|
4262
|
+
var CURL_SWITCHES = /* @__PURE__ */ new Set([
|
|
4263
|
+
"s",
|
|
4264
|
+
"S",
|
|
4265
|
+
"f",
|
|
4266
|
+
"i",
|
|
4267
|
+
"I",
|
|
4268
|
+
"v",
|
|
4269
|
+
"L",
|
|
4270
|
+
"q",
|
|
4271
|
+
"G",
|
|
4272
|
+
"--silent",
|
|
4273
|
+
"--show-error",
|
|
4274
|
+
"--fail",
|
|
4275
|
+
"--include",
|
|
4276
|
+
"--head",
|
|
4277
|
+
"--verbose",
|
|
4278
|
+
"--location",
|
|
4279
|
+
"--disable",
|
|
4280
|
+
"--get",
|
|
4281
|
+
"--compressed",
|
|
4282
|
+
"--version"
|
|
4283
|
+
]);
|
|
4284
|
+
var CURL_VALUES = /* @__PURE__ */ new Set(["A", "m", "o", "w", "--user-agent", "--max-time", "--connect-timeout", "--output", "--write-out", "--noproxy"]);
|
|
4285
|
+
var CURL_SENSITIVE = /* @__PURE__ */ new Set([
|
|
4286
|
+
"d",
|
|
4287
|
+
"F",
|
|
4288
|
+
"u",
|
|
4289
|
+
"H",
|
|
4290
|
+
"b",
|
|
4291
|
+
"e",
|
|
4292
|
+
"X",
|
|
4293
|
+
"--data",
|
|
4294
|
+
"--data-raw",
|
|
4295
|
+
"--data-binary",
|
|
4296
|
+
"--data-ascii",
|
|
4297
|
+
"--data-urlencode",
|
|
4298
|
+
"--form",
|
|
4299
|
+
"--form-string",
|
|
4300
|
+
"--user",
|
|
4301
|
+
"--header",
|
|
4302
|
+
"--cookie",
|
|
4303
|
+
"--referer",
|
|
4304
|
+
"--request"
|
|
4305
|
+
]);
|
|
4306
|
+
var WGET_SWITCHES = /* @__PURE__ */ new Set(["q", "S", "v", "--quiet", "--server-response", "--verbose", "--no-verbose", "--spider", "--no-config", "--no-hsts", "--no-proxy", "--no-netrc", "--version"]);
|
|
4307
|
+
var WGET_VALUES = /* @__PURE__ */ new Set(["O", "U", "T", "--output-document", "--user-agent", "--timeout", "--connect-timeout", "--read-timeout", "--max-redirect"]);
|
|
4308
|
+
function webCommand(seg, trustedResolves) {
|
|
4309
|
+
const binary = seg.binary.toLowerCase().replace(/\.exe$/, "");
|
|
4310
|
+
if (binary === "wget2") return { urls: [], reason: `wget2 can write persistent databases. ${READ_REFUSAL_HINT}` };
|
|
4311
|
+
if (binary !== "curl" && binary !== "wget") return void 0;
|
|
4312
|
+
if (seg.binaryPath !== void 0) return { urls: [], reason: `Call ${binary} by name so the research shell resolves it from PATH. ${READ_REFUSAL_HINT}` };
|
|
4313
|
+
const urls = [];
|
|
4314
|
+
const outboundTexts = [];
|
|
4315
|
+
const switches = binary === "curl" ? CURL_SWITCHES : WGET_SWITCHES;
|
|
4316
|
+
const values = binary === "curl" ? CURL_VALUES : WGET_VALUES;
|
|
4317
|
+
let stdout = false;
|
|
4318
|
+
let spider = false;
|
|
4319
|
+
let perInvocation = false;
|
|
4320
|
+
let redirects = binary === "wget";
|
|
4321
|
+
let getQuery = false;
|
|
4322
|
+
let queryData = false;
|
|
4323
|
+
let effect = false;
|
|
4324
|
+
const effectFlags = binary === "curl" ? CURL_EFFECT : WGET_EFFECT;
|
|
4325
|
+
const refused3 = (flag, sends = false) => sends ? { urls, effect: true, reason: `"${binary} ${flag}" is not a read-only web command. ${EFFECT_REFUSAL_HINT}` } : { urls, reason: `"${binary} ${flag}" is not known to be a read-only web command. ${READ_REFUSAL_HINT}` };
|
|
4326
|
+
if (seg.expandable || seg.globbed) return refused3("with shell expansions or globbing");
|
|
4327
|
+
for (let i = 0; i < seg.args.length; i++) {
|
|
4328
|
+
const arg = seg.args[i];
|
|
4329
|
+
if (arg === "-") return refused3(arg);
|
|
4330
|
+
if (!arg.startsWith("-")) {
|
|
4331
|
+
let url;
|
|
4332
|
+
try {
|
|
4333
|
+
url = new URL(arg);
|
|
4334
|
+
} catch {
|
|
4335
|
+
return refused3(arg);
|
|
4336
|
+
}
|
|
4337
|
+
if (!/^https?:\/\//i.test(arg) || /[\\\s{}]/.test(arg)) return refused3(arg);
|
|
4338
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
4339
|
+
const authority = /^https?:\/\/([^/?#]*)/i.exec(arg)?.[1] ?? "";
|
|
4340
|
+
const rawHost = authority.slice(authority.lastIndexOf("@") + 1).replace(/:\d*$/, "");
|
|
4341
|
+
if (binary === "curl" && !/^[A-Za-z0-9.-]+$/.test(rawHost) && !(/^\[[0-9A-Fa-f:.]+\]$/.test(rawHost) && (0, import_node_net2.isIP)(host) === 6)) return refused3("with a non-literal ASCII hostname");
|
|
4342
|
+
const withoutHost = arg.replace(url.hostname, "");
|
|
4343
|
+
if (/[[\]]/.test(withoutHost)) return refused3(arg);
|
|
4344
|
+
if ((0, import_node_net2.isIP)(host)) {
|
|
4345
|
+
const reason = blockedAddressReason(host);
|
|
4346
|
+
if (reason) return { urls, reason: `Blocked host: ${reason}. ${READ_REFUSAL_HINT}` };
|
|
4347
|
+
}
|
|
4348
|
+
if (url.username || url.password) perInvocation = true;
|
|
4349
|
+
urls.push(url);
|
|
4350
|
+
outboundTexts.push(arg);
|
|
4351
|
+
continue;
|
|
4352
|
+
}
|
|
4353
|
+
const long = arg.startsWith("--");
|
|
4354
|
+
const flags = long ? [arg.split("=")[0]] : [...arg.slice(1)];
|
|
4355
|
+
for (let j = 0; j < flags.length; j++) {
|
|
4356
|
+
const flag = flags[j];
|
|
4357
|
+
if (binary === "curl" && (flag === "n" || flag === "--netrc" || flag === "--netrc-optional")) {
|
|
4358
|
+
if (arg.includes("=")) return refused3(arg);
|
|
4359
|
+
perInvocation = true;
|
|
4360
|
+
continue;
|
|
4361
|
+
}
|
|
4362
|
+
if (switches.has(flag)) {
|
|
4363
|
+
if (long && arg.includes("=")) return refused3(arg);
|
|
4364
|
+
if (flag === "--spider") spider = true;
|
|
4365
|
+
if (flag === "L" || flag === "--location") redirects = true;
|
|
4366
|
+
if (flag === "G" || flag === "--get") getQuery = true;
|
|
4367
|
+
continue;
|
|
4368
|
+
}
|
|
4369
|
+
const sensitive = binary === "curl" && CURL_SENSITIVE.has(flag);
|
|
4370
|
+
const sends = effectFlags.has(flag) && !sensitive;
|
|
4371
|
+
if (sends && !(binary === "wget" && (flag === "O" || flag === "--output-document"))) return refused3(arg, true);
|
|
4372
|
+
const trustedResolve = binary === "curl" && flag === "--resolve" && trustedResolves !== void 0;
|
|
4373
|
+
if (!values.has(flag) && !sensitive && !trustedResolve) return refused3(arg);
|
|
4374
|
+
const attached2 = long ? arg.includes("=") ? arg.slice(flag.length + 1) : void 0 : flags.slice(j + 1).join("") || void 0;
|
|
4375
|
+
const value = attached2 ?? seg.args[++i];
|
|
4376
|
+
if (value === void 0) return refused3(arg);
|
|
4377
|
+
outboundTexts.push(value);
|
|
4378
|
+
if (trustedResolve && !trustedResolves?.has(value)) return refused3(arg);
|
|
4379
|
+
if (flag === "--noproxy" && value !== "*") return refused3(arg);
|
|
4380
|
+
if (binary === "wget" && flag === "--max-redirect") redirects = value !== "0";
|
|
4381
|
+
if (sensitive) {
|
|
4382
|
+
if (flag === "--data-urlencode" && !value.includes("@")) queryData = true;
|
|
4383
|
+
else if (!((flag === "X" || flag === "--request") && (value === "GET" || value === "HEAD"))) {
|
|
4384
|
+
perInvocation = true;
|
|
4385
|
+
effect = true;
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
if (binary === "wget" && (flag === "O" || flag === "--output-document")) {
|
|
4389
|
+
if (value !== "-") return refused3(arg, true);
|
|
4390
|
+
stdout = true;
|
|
4391
|
+
}
|
|
4392
|
+
if ((flag === "o" || flag === "--output") && value !== "/dev/null") return refused3(arg, binary === "curl");
|
|
4393
|
+
if ((flag === "w" || flag === "--write-out") && (value.startsWith("@") || value.includes("%output{"))) return refused3(arg);
|
|
4394
|
+
break;
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
if (urls.length === 0) return refused3("without a literal HTTP(S) URL");
|
|
4398
|
+
if (binary === "wget" && !stdout && !spider) return refused3("without stdout or --spider");
|
|
4399
|
+
return { urls, outboundTexts, ...effect ? { effect } : {}, ...perInvocation || redirects || queryData && !getQuery ? { perInvocation: true } : {} };
|
|
4400
|
+
}
|
|
4401
|
+
function disableWebCommandStartup(command, dialect) {
|
|
4402
|
+
const syntax = dialectFor(dialect);
|
|
4403
|
+
const edits = [];
|
|
4404
|
+
for (const original of lex(command, [], syntax).segments) {
|
|
4405
|
+
const { seg } = unwrap(original, syntax);
|
|
4406
|
+
const binary = seg.binary.toLowerCase().replace(/\.exe$/, "");
|
|
4407
|
+
if (binary !== "curl" && binary !== "wget") continue;
|
|
4408
|
+
const ends = original.wordEnds ?? [];
|
|
4409
|
+
const end = ends[ends.length - seg.args.length - 1];
|
|
4410
|
+
const noProxy = syntax.escape === "^" ? '"*"' : "'*'";
|
|
4411
|
+
if (end !== void 0) edits.push({ end, flags: binary === "curl" ? ` -q --noproxy ${noProxy}` : " --no-config --no-hsts --no-proxy --no-netrc" });
|
|
4412
|
+
}
|
|
4413
|
+
for (const { end, flags } of edits.sort((a, b) => b.end - a.end)) {
|
|
4414
|
+
command = command.slice(0, end) + flags + command.slice(end);
|
|
4415
|
+
}
|
|
4416
|
+
return command;
|
|
4417
|
+
}
|
|
4418
|
+
function pinWebCommandAddresses(command, addresses, dialect) {
|
|
4419
|
+
const syntax = dialectFor(dialect);
|
|
4420
|
+
const resolves2 = /* @__PURE__ */ new Set();
|
|
4421
|
+
const edits = [];
|
|
4422
|
+
for (const original of lex(command, [], syntax).segments) {
|
|
4423
|
+
const { seg } = unwrap(original, syntax);
|
|
4424
|
+
if (seg.binary.toLowerCase().replace(/\.exe$/, "") !== "curl") continue;
|
|
4425
|
+
const web = webCommand(seg);
|
|
4426
|
+
if (!web || web.reason) continue;
|
|
4427
|
+
const flags = [...new Set(web.urls.map((url) => url.origin))].flatMap((origin) => {
|
|
4428
|
+
const url = new URL(origin);
|
|
4429
|
+
if ((0, import_node_net2.isIP)(url.hostname.replace(/^\[|\]$/g, ""))) return [];
|
|
4430
|
+
const address = addresses.get(origin);
|
|
4431
|
+
if (!address) throw new Error("Missing vetted curl address");
|
|
4432
|
+
const value = `${url.hostname}:${url.port || (url.protocol === "https:" ? "443" : "80")}:${(0, import_node_net2.isIP)(address) === 6 ? `[${address}]` : address}`;
|
|
4433
|
+
resolves2.add(value);
|
|
4434
|
+
return [` --resolve ${syntax.escape === "^" ? `"${value}"` : `'${value}'`}`];
|
|
4435
|
+
}).join("");
|
|
4436
|
+
const ends = original.wordEnds ?? [];
|
|
4437
|
+
const end = ends[ends.length - seg.args.length - (seg.args[0] === "-q" ? 0 : 1)];
|
|
4438
|
+
if (end !== void 0 && flags) edits.push({ end, flags });
|
|
4439
|
+
}
|
|
4440
|
+
for (const { end, flags } of edits.sort((a, b) => b.end - a.end)) {
|
|
4441
|
+
command = command.slice(0, end) + flags + command.slice(end);
|
|
4442
|
+
}
|
|
4443
|
+
return { command, resolves: resolves2 };
|
|
4444
|
+
}
|
|
4445
|
+
function wgetVersionCommands(command, dialect) {
|
|
4446
|
+
const syntax = dialectFor(dialect);
|
|
4447
|
+
const { segments } = lexAll(command, syntax, !!process.env.CDPATH);
|
|
4448
|
+
const quote = (word) => syntax.escape === "^" ? `"${word}"` : `'${word.replace(/'/g, "'\\''")}'`;
|
|
4449
|
+
return [...new Set(segments.flatMap((original) => {
|
|
4450
|
+
const { seg } = unwrap(original, syntax);
|
|
4451
|
+
if (seg.binary.toLowerCase().replace(/\.exe$/, "") !== "wget") return [];
|
|
4452
|
+
if (seg.binaryPath !== void 0) return [void 0];
|
|
4453
|
+
const executable = seg.binary;
|
|
4454
|
+
if (original.cwd === void 0 && segments.some((s) => /^(cd|pushd|popd)$/i.test(s.binary))) return [void 0];
|
|
4455
|
+
if (syntax.escape === "^" && [...original.cwd ?? [], executable].some((word) => /["%&|<>^()!]/.test(word))) return [void 0];
|
|
4456
|
+
const navigation = (original.cwd ?? []).map((target) => `cd ${quote(target)} && `).join("");
|
|
4457
|
+
const env = syntax.escape === "^" ? 'set "XDG_DATA_HOME=/dev/null" && set "XDG_CONFIG_HOME=/dev/null" && set "LOCALAPPDATA=NUL" && ' : "XDG_DATA_HOME=/dev/null XDG_CONFIG_HOME=/dev/null LOCALAPPDATA=NUL ";
|
|
4458
|
+
return [`${navigation}${env}${quote(executable)} --no-config --version`];
|
|
4459
|
+
}))];
|
|
4460
|
+
}
|
|
4461
|
+
|
|
4039
4462
|
// src/services/commandPolicy.ts
|
|
4040
4463
|
var AUTO_COMMANDS = [
|
|
4041
4464
|
"ls",
|
|
@@ -4389,7 +4812,7 @@ function pathRefs(command, opts = {}) {
|
|
|
4389
4812
|
return lexAll(command.trim(), dialect, opts.cdpathSet).segments.flatMap((seg) => {
|
|
4390
4813
|
const patterns = patternArgs(seg);
|
|
4391
4814
|
const cwd = seg.cwd ?? [];
|
|
4392
|
-
return [...seg.args.filter((_, i) => !patterns.has(i)), ...seg.inputs].flatMap(pathIn).map((
|
|
4815
|
+
return [...seg.args.filter((_, i) => !patterns.has(i)), ...seg.inputs].flatMap(pathIn).map((path32) => ({ path: path32, cwd }));
|
|
4393
4816
|
});
|
|
4394
4817
|
}
|
|
4395
4818
|
function pathIn(a) {
|
|
@@ -4488,7 +4911,9 @@ function isAuto(seg, opts) {
|
|
|
4488
4911
|
const sub = subcommandOf(seg);
|
|
4489
4912
|
return sub !== void 0 && GIT_READONLY_SUBCOMMANDS.includes(sub);
|
|
4490
4913
|
}
|
|
4491
|
-
function refusalFor(seg) {
|
|
4914
|
+
function refusalFor(seg, trustedResolves) {
|
|
4915
|
+
const web = webCommand(seg, trustedResolves);
|
|
4916
|
+
if (web?.reason) return web.reason;
|
|
4492
4917
|
if (seg.computedBinary !== void 0) {
|
|
4493
4918
|
return `"${seg.computedBinary}" is a command name the shell computes as it runs, so this classifier cannot tell what would run. Name the program directly.`;
|
|
4494
4919
|
}
|
|
@@ -4648,9 +5073,25 @@ function classifyCommand(command, opts = {}) {
|
|
|
4648
5073
|
}
|
|
4649
5074
|
if (segments.length === 0) return { tier: "refuse", scope: "", reason: "No command found." };
|
|
4650
5075
|
const unwrapped = segments.map((seg) => unwrap(seg, dialect));
|
|
5076
|
+
const nestedWeb = segments.flatMap((seg, i) => seg.nested ? [webCommand(unwrapped[i].seg, opts.trustedWebResolves)] : []).find((web) => web);
|
|
5077
|
+
if (nestedWeb) {
|
|
5078
|
+
return {
|
|
5079
|
+
tier: "refuse",
|
|
5080
|
+
scope: "",
|
|
5081
|
+
reason: `A curl or wget command inside a substitution cannot have its startup files disabled safely. ${nestedWeb.effect ? EFFECT_REFUSAL_HINT : `Run it directly, or ${READ_REFUSAL_HINT.charAt(0).toLowerCase()}${READ_REFUSAL_HINT.slice(1)}`}`
|
|
5082
|
+
};
|
|
5083
|
+
}
|
|
4651
5084
|
for (const { seg, wrappers, runner, reason: wrapperReason } of unwrapped) {
|
|
4652
5085
|
if (wrapperReason) return { tier: "refuse", scope: "", reason: wrapperReason };
|
|
4653
|
-
const
|
|
5086
|
+
const web = webCommand(seg, opts.trustedWebResolves);
|
|
5087
|
+
if (web && (opts.autonomous || opts.approvalMode === "deny")) {
|
|
5088
|
+
return { tier: "refuse", scope: "", reason: `curl and wget are refused in Full and allowlist mode. ${web.effect ? EFFECT_REFUSAL_HINT : READ_REFUSAL_HINT}` };
|
|
5089
|
+
}
|
|
5090
|
+
const allow = opts.allow;
|
|
5091
|
+
if (web && allow && web.urls.some((url) => scopeExcluded(allow, `${seg.binary.toLowerCase().replace(/\.exe$/, "")} ${url.origin}/*`))) {
|
|
5092
|
+
return { tier: "refuse", scope: "", reason: `The web origin is excluded by the planner allowlist. ${READ_REFUSAL_HINT}` };
|
|
5093
|
+
}
|
|
5094
|
+
const reason = refusalFor({ ...seg, binary: seg.binary.toLowerCase() }, opts.trustedWebResolves);
|
|
4654
5095
|
if (reason) return { tier: "refuse", scope: "", reason: withWrapperNote(reason, wrappers) };
|
|
4655
5096
|
if (runner && !XARGS_TARGETS.includes(seg.binary)) {
|
|
4656
5097
|
return {
|
|
@@ -4660,22 +5101,49 @@ function classifyCommand(command, opts = {}) {
|
|
|
4660
5101
|
};
|
|
4661
5102
|
}
|
|
4662
5103
|
}
|
|
5104
|
+
if (unwrapped.some(({ seg }) => seg.binary.toLowerCase().replace(/\.exe$/, "") === "wget") && wgetVersionCommands(command, opts.dialect).includes(void 0)) {
|
|
5105
|
+
return { tier: "refuse", scope: "", reason: `The wget executable cannot be checked safely in this working directory. ${READ_REFUSAL_HINT}` };
|
|
5106
|
+
}
|
|
4663
5107
|
const nonAuto = unwrapped.filter((u) => u.runner !== void 0 || u.wrappers.some((w) => w !== w.toLowerCase()) || !isAuto(u.seg, opts));
|
|
4664
5108
|
if (nonAuto.length === 0) return { tier: "auto", scope: "" };
|
|
4665
|
-
|
|
5109
|
+
const lineScoped = nonAuto.some((u) => {
|
|
4666
5110
|
const name = u.seg.binary.toLowerCase();
|
|
4667
5111
|
return name === "cd" || name !== "command" && SHELL_STATE_COMMANDS.has(name);
|
|
4668
|
-
})
|
|
5112
|
+
});
|
|
5113
|
+
const webUrls = nonAuto.flatMap((u) => webCommand(u.seg, opts.trustedWebResolves)?.urls ?? []);
|
|
5114
|
+
const webOutboundTexts = nonAuto.flatMap((u) => webCommand(u.seg, opts.trustedWebResolves)?.outboundTexts ?? []);
|
|
5115
|
+
if (lineScoped && webUrls.length === 0) {
|
|
4669
5116
|
return { tier: "ask", scope: trimmed };
|
|
4670
5117
|
}
|
|
4671
|
-
const
|
|
4672
|
-
|
|
4673
|
-
|
|
5118
|
+
const scopes = [...new Set(nonAuto.flatMap((u) => {
|
|
5119
|
+
const web = webCommand(u.seg, opts.trustedWebResolves);
|
|
5120
|
+
return web ? web.urls.map((url) => `${u.seg.binary.toLowerCase().replace(/\.exe$/, "")} ${url.origin}/*`) : [lineScoped ? trimmed : u.runner ? `${u.runner} ${scopeFor(u.seg)}` : scopeFor(u.seg)];
|
|
5121
|
+
}))].sort();
|
|
5122
|
+
const scope = scopes.join(" + ");
|
|
5123
|
+
const perInvocation = nonAuto.some((u) => webCommand(u.seg, opts.trustedWebResolves)?.perInvocation);
|
|
5124
|
+
const allowedBy = opts.allow && !perInvocation && !lineScoped ? allowlistRule(nonAuto, opts.allow, opts.trustedWebResolves) : void 0;
|
|
5125
|
+
return {
|
|
5126
|
+
tier: "ask",
|
|
5127
|
+
scope,
|
|
5128
|
+
...webUrls.length ? { webUrls, webOutboundTexts } : {},
|
|
5129
|
+
...scopes.length > 1 && webUrls.length ? { scopes } : {},
|
|
5130
|
+
...perInvocation ? { perInvocation } : {},
|
|
5131
|
+
...allowedBy ? { allowedBy } : {}
|
|
5132
|
+
};
|
|
4674
5133
|
}
|
|
4675
|
-
function allowlistRule(parts, allow) {
|
|
5134
|
+
function allowlistRule(parts, allow, trustedResolves) {
|
|
4676
5135
|
const rules = [];
|
|
4677
5136
|
for (const { seg, runner } of parts) {
|
|
4678
5137
|
if (runner || seg.expandable || seg.globbed || seg.binaryPath !== void 0 || seg.assignments.length > 0) return void 0;
|
|
5138
|
+
const web = webCommand(seg, trustedResolves);
|
|
5139
|
+
if (web) {
|
|
5140
|
+
for (const url of web.urls) {
|
|
5141
|
+
const rule2 = commandAllowedBy(allow, { binary: seg.binary, args: [`${url.origin}/*`], scopeWords: 1 });
|
|
5142
|
+
if (!rule2) return void 0;
|
|
5143
|
+
rules.push(rule2);
|
|
5144
|
+
}
|
|
5145
|
+
continue;
|
|
5146
|
+
}
|
|
4679
5147
|
const rule = commandAllowedBy(allow, { binary: seg.binary, args: seg.args, scopeWords: scopeFor(seg).split(" ").length - 1 });
|
|
4680
5148
|
if (!rule) return void 0;
|
|
4681
5149
|
rules.push(rule);
|
|
@@ -4846,9 +5314,62 @@ function referencePattern(symbol) {
|
|
|
4846
5314
|
return `\\b${escapeRegex(symbol)}\\b`;
|
|
4847
5315
|
}
|
|
4848
5316
|
|
|
5317
|
+
// src/services/outboundSecrets.ts
|
|
5318
|
+
var SENSITIVE_ENV_NAME = /(?:^|_)(?:KEY|API_KEY|TOKEN|SECRET|PASSWORD|CREDENTIALS?)(?:_|$)/i;
|
|
5319
|
+
var MIN_SECRET_LENGTH = 8;
|
|
5320
|
+
var MAX_DECODE_ROUNDS = 3;
|
|
5321
|
+
var TOKEN_SHAPES = [
|
|
5322
|
+
/\bgh[pousr]_[A-Za-z0-9]{36,}/,
|
|
5323
|
+
/\bgithub_pat_[A-Za-z0-9_]{22,}/,
|
|
5324
|
+
/\bAIza[A-Za-z0-9_-]{30,}/,
|
|
5325
|
+
/\bglpat-[A-Za-z0-9_-]{20,}/,
|
|
5326
|
+
/\b[sr]k_live_[A-Za-z0-9]{16,}/,
|
|
5327
|
+
/\b(?:hf|npm)_[A-Za-z0-9]{20,}/,
|
|
5328
|
+
/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/,
|
|
5329
|
+
/\bxox[abpr]-[A-Za-z0-9-]{10,}/,
|
|
5330
|
+
/\beyJ[A-Za-z0-9_-]{5,}\.eyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{10,}/,
|
|
5331
|
+
/-----BEGIN [A-Z0-9 ]+-----/
|
|
5332
|
+
];
|
|
5333
|
+
var SK_KEY = /\bsk-[A-Za-z0-9_-]{20,}/g;
|
|
5334
|
+
function looksRandom(tail) {
|
|
5335
|
+
return /[a-z]/.test(tail) && /[A-Z]/.test(tail) && /\d/.test(tail);
|
|
5336
|
+
}
|
|
5337
|
+
function percentDecode(text2) {
|
|
5338
|
+
return text2.replace(/(?:%[0-9a-f]{2})+/gi, (run) => {
|
|
5339
|
+
try {
|
|
5340
|
+
return decodeURIComponent(run);
|
|
5341
|
+
} catch {
|
|
5342
|
+
return run;
|
|
5343
|
+
}
|
|
5344
|
+
});
|
|
5345
|
+
}
|
|
5346
|
+
function forms(text2) {
|
|
5347
|
+
const seen = [text2, text2.replace(/\+/g, " ")];
|
|
5348
|
+
let current = text2;
|
|
5349
|
+
for (let round = 0; round < MAX_DECODE_ROUNDS; round++) {
|
|
5350
|
+
const next = percentDecode(current);
|
|
5351
|
+
if (next === current) break;
|
|
5352
|
+
seen.push(next, next.replace(/\+/g, " "));
|
|
5353
|
+
current = next;
|
|
5354
|
+
}
|
|
5355
|
+
return seen;
|
|
5356
|
+
}
|
|
5357
|
+
function sensitiveEnvValues(env) {
|
|
5358
|
+
return Object.entries(env).filter(([name, value]) => SENSITIVE_ENV_NAME.test(name) && !/_(?:URL|URI|PATH|FILE|DIR)$/i.test(name) && value !== void 0).map(([, value]) => (value ?? "").trim());
|
|
5359
|
+
}
|
|
5360
|
+
function carriesSecret(texts, held) {
|
|
5361
|
+
const secrets = [...held].map((s) => s.trim()).filter((s) => s.length >= MIN_SECRET_LENGTH);
|
|
5362
|
+
return texts.flatMap(forms).some((text2) => secrets.some((secret) => text2.includes(secret)) || TOKEN_SHAPES.some((shape) => shape.test(text2)) || [...text2.matchAll(SK_KEY)].some((m) => looksRandom(m[0].slice(3))));
|
|
5363
|
+
}
|
|
5364
|
+
|
|
4849
5365
|
// src/services/BaseFileSystem.ts
|
|
4850
5366
|
var BaseFileSystem = class {
|
|
5367
|
+
constructor(resolveWebHost = resolveAll) {
|
|
5368
|
+
this.resolveWebHost = resolveWebHost;
|
|
5369
|
+
}
|
|
5370
|
+
resolveWebHost;
|
|
4851
5371
|
approval = DENY_ALL;
|
|
5372
|
+
outboundSecrets = () => [];
|
|
4852
5373
|
/**
|
|
4853
5374
|
* The interpreter `execBashImpl` will hand the command to. Owned here rather
|
|
4854
5375
|
* than per-adapter because {@link classifyCommand} has to be told the same
|
|
@@ -4860,6 +5381,9 @@ var BaseFileSystem = class {
|
|
|
4860
5381
|
setApproval(approval) {
|
|
4861
5382
|
this.approval = approval;
|
|
4862
5383
|
}
|
|
5384
|
+
setOutboundSecrets(secrets) {
|
|
5385
|
+
this.outboundSecrets = secrets;
|
|
5386
|
+
}
|
|
4863
5387
|
/**
|
|
4864
5388
|
* Resolve `p` and confirm the planner may touch it. In-workspace paths pass
|
|
4865
5389
|
* silently; anything else needs one approval, remembered per containing
|
|
@@ -4996,19 +5520,34 @@ ${refs.output.trim()}`);
|
|
|
4996
5520
|
* directory the next command names.
|
|
4997
5521
|
*/
|
|
4998
5522
|
async bash(command, signal) {
|
|
5523
|
+
command = disableWebCommandStartup(command, this.researchShell.dialect);
|
|
4999
5524
|
const allow = this.approval.allowlist?.();
|
|
5000
|
-
const { tier, scope, reason, allowedBy
|
|
5525
|
+
const { tier, scope, reason, allowedBy, perInvocation, scopes: commandScopes, webUrls, webOutboundTexts } = classifyCommand(command, {
|
|
5526
|
+
...this.policyOptions(),
|
|
5527
|
+
...this.approval.commandPolicy?.(),
|
|
5528
|
+
...allow ? { allow } : {}
|
|
5529
|
+
});
|
|
5001
5530
|
if (tier === "refuse") {
|
|
5002
5531
|
return { success: false, output: `Command refused: ${reason}`, truncated: false };
|
|
5003
5532
|
}
|
|
5533
|
+
if (webUrls?.length && carriesSecret([...webOutboundTexts ?? [], ...webUrls.map((url) => url.href)], [...this.outboundSecrets(), ...sensitiveEnvValues(process.env)])) {
|
|
5534
|
+
return { success: false, output: "Command refused: a web command contains what looks like a secret. Fetch the page with your web fetch tool, without credentials.", truncated: false };
|
|
5535
|
+
}
|
|
5536
|
+
const vettedAddresses = /* @__PURE__ */ new Map();
|
|
5537
|
+
for (const url of webUrls ?? []) {
|
|
5538
|
+
const check2 = await vetEgress(url.hostname, url.port, this.resolveWebHost);
|
|
5539
|
+
if (!check2.ok) return { success: false, output: `Command refused: Blocked host: ${check2.reason}. ${READ_REFUSAL_HINT}`, truncated: false };
|
|
5540
|
+
vettedAddresses.set(url.origin, check2.addresses[0]);
|
|
5541
|
+
}
|
|
5004
5542
|
const outside = this.outsidePaths(command);
|
|
5005
5543
|
const asks = tier === "ask";
|
|
5006
5544
|
if (asks || outside.length > 0) {
|
|
5007
5545
|
const root = this.getWorkspaceRoot();
|
|
5008
|
-
const scopes = [...asks ? [scope] : [], ...outside.map((o) => o.scope)];
|
|
5546
|
+
const scopes = [...asks ? commandScopes ?? [scope] : [], ...outside.map((o) => o.scope)];
|
|
5009
5547
|
const touched = outside.map((o) => o.abs).join(", ");
|
|
5010
5548
|
const granted = await this.approval.request({
|
|
5011
5549
|
kind: asks ? "shell_command" : "external_path",
|
|
5550
|
+
...perInvocation ? { perInvocation } : {},
|
|
5012
5551
|
subject: asks ? command : outside[0].abs,
|
|
5013
5552
|
scope: scopes[0],
|
|
5014
5553
|
...scopes.length > 1 ? { scopes } : {},
|
|
@@ -5024,6 +5563,23 @@ It is outside the auto-allowed read-only set and the planner allowlist (${scope}
|
|
|
5024
5563
|
};
|
|
5025
5564
|
}
|
|
5026
5565
|
}
|
|
5566
|
+
if (webUrls?.length) {
|
|
5567
|
+
const latest = classifyCommand(command, { ...this.policyOptions(), ...this.approval.commandPolicy?.(), allow: this.approval.allowlist?.() });
|
|
5568
|
+
if (latest.tier === "refuse") return { success: false, output: `Command refused: ${latest.reason}`, truncated: false };
|
|
5569
|
+
}
|
|
5570
|
+
for (const probe of wgetVersionCommands(command, this.researchShell.dialect)) {
|
|
5571
|
+
if (!probe) return { success: false, output: `Command refused: the wget executable cannot be checked in an uncertain working directory. ${READ_REFUSAL_HINT}`, truncated: false };
|
|
5572
|
+
const version = await this.execBashImpl(probe, signal);
|
|
5573
|
+
if (!version.success || !/^GNU Wget 1\./.test(version.output)) {
|
|
5574
|
+
return { success: false, output: `Command refused: only GNU Wget 1.x is supported; wget2 can write persistent databases. ${READ_REFUSAL_HINT}`, truncated: false };
|
|
5575
|
+
}
|
|
5576
|
+
}
|
|
5577
|
+
const pinned = pinWebCommandAddresses(command, vettedAddresses, this.researchShell.dialect);
|
|
5578
|
+
command = pinned.command;
|
|
5579
|
+
if (webUrls?.length) {
|
|
5580
|
+
const latest = classifyCommand(command, { ...this.policyOptions(), ...this.approval.commandPolicy?.(), allow: this.approval.allowlist?.(), trustedWebResolves: pinned.resolves });
|
|
5581
|
+
if (latest.tier === "refuse") return { success: false, output: `Command refused: ${latest.reason}`, truncated: false };
|
|
5582
|
+
}
|
|
5027
5583
|
const outcome = await this.execBashImpl(command, signal);
|
|
5028
5584
|
if (outcome.success) return outcome;
|
|
5029
5585
|
const warning = researchShellWarning(this.researchShell);
|
|
@@ -5220,10 +5776,7 @@ async function runTool(name, args, fs17, fetcher, signal) {
|
|
|
5220
5776
|
return fs17.bash(String(args.command ?? ""), signal);
|
|
5221
5777
|
case "fetch": {
|
|
5222
5778
|
if (!fetcher) return { success: false, output: "Web fetching is not available in this environment.", truncated: false };
|
|
5223
|
-
|
|
5224
|
-
const allowed = await fetcher.confirm(url);
|
|
5225
|
-
if (!allowed) return { success: false, output: `URL fetch denied by user: ${url}`, truncated: false };
|
|
5226
|
-
return fetcher.fetch(url);
|
|
5779
|
+
return fetcher.fetch(String(args.url ?? ""));
|
|
5227
5780
|
}
|
|
5228
5781
|
case "web_search": {
|
|
5229
5782
|
if (!fetcher?.search) {
|
|
@@ -5247,9 +5800,11 @@ async function runTool(name, args, fs17, fetcher, signal) {
|
|
|
5247
5800
|
}
|
|
5248
5801
|
|
|
5249
5802
|
// src/services/ApprovalPolicy.ts
|
|
5250
|
-
|
|
5803
|
+
var FULL_ALLOWS = /* @__PURE__ */ new Set(["url_fetch", "web_search"]);
|
|
5804
|
+
function effectiveApprovalMode(setting, autonomous, kind) {
|
|
5251
5805
|
if (setting !== "auto") return setting;
|
|
5252
|
-
|
|
5806
|
+
if (!autonomous) return "ask";
|
|
5807
|
+
return kind !== void 0 && FULL_ALLOWS.has(kind) ? "allow" : "deny";
|
|
5253
5808
|
}
|
|
5254
5809
|
function parseApprovalModeSetting(raw) {
|
|
5255
5810
|
const value = (raw ?? "").trim().toLowerCase();
|
|
@@ -5258,6 +5813,7 @@ function parseApprovalModeSetting(raw) {
|
|
|
5258
5813
|
}
|
|
5259
5814
|
var ApprovalPolicy = class {
|
|
5260
5815
|
modeOf;
|
|
5816
|
+
autonomousOf;
|
|
5261
5817
|
entriesOf;
|
|
5262
5818
|
parsed = null;
|
|
5263
5819
|
asker;
|
|
@@ -5273,6 +5829,7 @@ var ApprovalPolicy = class {
|
|
|
5273
5829
|
constructor(opts = {}) {
|
|
5274
5830
|
const { mode = "ask", preApproved = [] } = opts;
|
|
5275
5831
|
this.modeOf = typeof mode === "function" ? mode : () => mode;
|
|
5832
|
+
this.autonomousOf = opts.autonomous ?? (() => false);
|
|
5276
5833
|
this.entriesOf = typeof preApproved === "function" ? preApproved : () => preApproved;
|
|
5277
5834
|
this.asker = opts.ask;
|
|
5278
5835
|
this.onDecision = opts.onDecision;
|
|
@@ -5282,20 +5839,20 @@ var ApprovalPolicy = class {
|
|
|
5282
5839
|
this.onDecision?.(req, granted, source);
|
|
5283
5840
|
return granted;
|
|
5284
5841
|
};
|
|
5285
|
-
const scopes = approvalScopes(req);
|
|
5286
|
-
const key = scopes.join("\n");
|
|
5287
5842
|
const list = this.allowlist();
|
|
5288
|
-
|
|
5289
|
-
|
|
5843
|
+
if (approvalScopes(req).some((scope) => scopeExcluded(list, scope))) return decide(false, "excluded");
|
|
5844
|
+
if (req.perInvocation) {
|
|
5845
|
+
if (this.modeOf(req.kind) === "deny" || this.autonomousOf()) return decide(false, "mode");
|
|
5846
|
+
if (!this.asker) return decide(false, "no-channel");
|
|
5847
|
+
return decide(await this.asker(req).catch(() => false), "asked");
|
|
5848
|
+
}
|
|
5849
|
+
const { scopes, key, open } = this.standing(req);
|
|
5290
5850
|
if (open.length === 0) {
|
|
5291
5851
|
return decide(true, scopes.every((s) => this.granted.has(s)) ? "remembered" : "pre-approved");
|
|
5292
5852
|
}
|
|
5293
5853
|
if (scopes.some((s) => this.refused.has(s)) || this.refused.has(key)) return decide(false, "remembered");
|
|
5294
|
-
const mode = this.modeOf();
|
|
5295
|
-
if (mode === "allow")
|
|
5296
|
-
open.forEach((s) => this.granted.add(s));
|
|
5297
|
-
return decide(true, "mode");
|
|
5298
|
-
}
|
|
5854
|
+
const mode = this.modeOf(req.kind);
|
|
5855
|
+
if (mode === "allow") return decide(true, "mode");
|
|
5299
5856
|
if (mode === "deny") return decide(false, "mode");
|
|
5300
5857
|
if (!this.asker) return decide(false, "no-channel");
|
|
5301
5858
|
const openKey = open.join("\n");
|
|
@@ -5314,6 +5871,44 @@ var ApprovalPolicy = class {
|
|
|
5314
5871
|
}
|
|
5315
5872
|
return decide(answer2, "asked");
|
|
5316
5873
|
}
|
|
5874
|
+
commandPolicy() {
|
|
5875
|
+
return { autonomous: this.autonomousOf(), approvalMode: this.modeOf("shell_command") };
|
|
5876
|
+
}
|
|
5877
|
+
/**
|
|
5878
|
+
* What `request` would answer without a person, for a caller that may never
|
|
5879
|
+
* prompt (a research subagent): the same standing grants, remembered
|
|
5880
|
+
* refusals and mode the interactive path uses, stopping at `ask`.
|
|
5881
|
+
*/
|
|
5882
|
+
peek(req) {
|
|
5883
|
+
const list = this.allowlist();
|
|
5884
|
+
if (approvalScopes(req).some((scope) => scopeExcluded(list, scope))) return { decision: "deny", source: "excluded" };
|
|
5885
|
+
const { scopes, key, open } = this.standing(req);
|
|
5886
|
+
if (open.length === 0) return { decision: "allow", source: scopes.every((s) => this.granted.has(s)) ? "remembered" : "pre-approved" };
|
|
5887
|
+
if (scopes.some((s) => this.refused.has(s)) || this.refused.has(key)) return { decision: "deny", source: "remembered" };
|
|
5888
|
+
const mode = this.modeOf(req.kind);
|
|
5889
|
+
if (mode === "allow") return { decision: "allow", source: "mode" };
|
|
5890
|
+
if (mode === "deny") return { decision: "deny", source: "mode" };
|
|
5891
|
+
return { decision: "ask", source: "no-channel" };
|
|
5892
|
+
}
|
|
5893
|
+
standing(req) {
|
|
5894
|
+
const scopes = approvalScopes(req);
|
|
5895
|
+
const key = scopes.join("\n");
|
|
5896
|
+
const list = this.allowlist();
|
|
5897
|
+
const preApproved = (scope, i) => scopeAllowed(list, scope) || i === 0 && (req.kind === "shell_command" && req.allowedBy !== void 0 || req.kind === "mcp_tool" && req.tool !== void 0 && toolAllowed(list, req.tool));
|
|
5898
|
+
const open = scopes.filter((s, i) => !this.granted.has(s) && !preApproved(s, i));
|
|
5899
|
+
return { scopes, key, open };
|
|
5900
|
+
}
|
|
5901
|
+
/** Report a decision a guard reached before this policy was asked. */
|
|
5902
|
+
record(req, granted, source) {
|
|
5903
|
+
this.onDecision?.(req, granted, source);
|
|
5904
|
+
}
|
|
5905
|
+
grantsUnasked({ kind, scope }) {
|
|
5906
|
+
const list = this.allowlist();
|
|
5907
|
+
if (scopeExcluded(list, scope)) return false;
|
|
5908
|
+
if (this.granted.has(scope) || scopeAllowed(list, scope)) return true;
|
|
5909
|
+
if (this.refused.has(scope)) return false;
|
|
5910
|
+
return this.modeOf(kind) === "allow";
|
|
5911
|
+
}
|
|
5317
5912
|
/** The standing approvals, parsed from the entries in force now. */
|
|
5318
5913
|
allowlist() {
|
|
5319
5914
|
const entries = this.entriesOf();
|
|
@@ -5334,8 +5929,582 @@ var ApprovalPolicy = class {
|
|
|
5334
5929
|
}
|
|
5335
5930
|
};
|
|
5336
5931
|
|
|
5932
|
+
// src/services/webReadPolicy.ts
|
|
5933
|
+
var DEFAULT_WEB_READ_BUDGET = 40;
|
|
5934
|
+
var ALLOWED = { allowed: true };
|
|
5935
|
+
var WebReadPolicy = class {
|
|
5936
|
+
approval;
|
|
5937
|
+
resolveHost;
|
|
5938
|
+
secrets;
|
|
5939
|
+
env;
|
|
5940
|
+
budget;
|
|
5941
|
+
turn;
|
|
5942
|
+
spent = { turn: void 0, reads: 0 };
|
|
5943
|
+
constructor(opts) {
|
|
5944
|
+
this.approval = opts.approval;
|
|
5945
|
+
this.secrets = opts.secrets ?? (() => []);
|
|
5946
|
+
this.env = opts.env ?? (() => process.env);
|
|
5947
|
+
this.budget = opts.budget ?? (() => DEFAULT_WEB_READ_BUDGET);
|
|
5948
|
+
this.turn = opts.turn ?? (() => void 0);
|
|
5949
|
+
this.resolveHost = opts.resolveHost ?? resolveAll;
|
|
5950
|
+
}
|
|
5951
|
+
/**
|
|
5952
|
+
* Every check that needs no network runs first, then the budget and the
|
|
5953
|
+
* approval, and only then is a fetch's host resolved: a lookup is itself a
|
|
5954
|
+
* request, so `<data>.attacker.example` would leave through DNS even for a
|
|
5955
|
+
* read that is then excluded, over budget or declined.
|
|
5956
|
+
*/
|
|
5957
|
+
async decide(read2, opts = {}) {
|
|
5958
|
+
const target = read2.kind === "fetch" ? this.vetFetch(read2.url) : this.vetSearch(read2);
|
|
5959
|
+
const { request, label } = target;
|
|
5960
|
+
if (target.refused) return this.refuse(request, target.refused.source, target.refused.reason);
|
|
5961
|
+
const list = this.approval.allowlist?.();
|
|
5962
|
+
if (list && scopeExcluded(list, request.scope)) {
|
|
5963
|
+
return this.refuse(request, "excluded", `${label} refused: ${request.scope} is excluded by the planner allowlist. Continue without it.`);
|
|
5964
|
+
}
|
|
5965
|
+
const spent = this.spend();
|
|
5966
|
+
if (!spent) {
|
|
5967
|
+
return this.refuse(request, "budget", `${label} refused: this planner turn has used its web-read budget (${this.budget()}). Continue with what you have, and tell the user that more reading would need a higher budget.`);
|
|
5968
|
+
}
|
|
5969
|
+
if (opts.prompting === false) {
|
|
5970
|
+
const standing = this.approval.peek?.(request);
|
|
5971
|
+
if (standing?.decision === "allow") {
|
|
5972
|
+
this.approval.record?.(request, true, standing.source);
|
|
5973
|
+
return this.resolved(target, spent.turn);
|
|
5974
|
+
}
|
|
5975
|
+
this.refund(spent.turn);
|
|
5976
|
+
return this.subagentRefusal(read2, request, standing?.source ?? "no-channel");
|
|
5977
|
+
}
|
|
5978
|
+
if (await this.approval.request(request)) return this.resolved(target, spent.turn);
|
|
5979
|
+
this.refund(spent.turn);
|
|
5980
|
+
return { allowed: false, reason: read2.kind === "fetch" ? `Fetch not approved: ${request.scope}. Continue without it, or ask the user to approve this origin or add it to the planner allowlist.` : "Web search not approved. Continue without it, or fetch a documentation URL you already know." };
|
|
5981
|
+
}
|
|
5982
|
+
subagentRefusal(read2, request, source) {
|
|
5983
|
+
const label = read2.kind === "fetch" ? "Fetch" : "Web search";
|
|
5984
|
+
if (source === "mode") {
|
|
5985
|
+
return this.refuse(request, source, `${label} refused: ${request.scope} is not covered by the planner allowlist in allowlist mode. Note the gap in your digest; the user must add a standing approval or change the approval mode.`);
|
|
5986
|
+
}
|
|
5987
|
+
if (source === "remembered") {
|
|
5988
|
+
return this.refuse(request, source, `${label} refused: ${request.scope} was previously refused in this session. Note the gap in your digest; the user must add a standing approval or start a new session.`);
|
|
5989
|
+
}
|
|
5990
|
+
this.approval.record?.(request, false, source);
|
|
5991
|
+
return { allowed: false, reason: read2.kind === "fetch" ? `Fetch refused: ${request.scope} is not approved in this session, and research agents run in the background where nothing can be approved. Note the gap in your digest; the planner can fetch it directly.` : "Web search refused: research agents run in the background where nothing can be approved. Note the gap in your digest; the planner can search directly." };
|
|
5992
|
+
}
|
|
5993
|
+
/**
|
|
5994
|
+
* Whether a search through `provider` would run without asking anyone, for
|
|
5995
|
+
* a runner whose own search raises no request and is set on or off when
|
|
5996
|
+
* its session starts (ADR-0027). A zero budget disables it; no query is
|
|
5997
|
+
* known yet, so secrets and individual reads cannot be checked or counted.
|
|
5998
|
+
*/
|
|
5999
|
+
searchesUnasked(provider) {
|
|
6000
|
+
if (this.budget() === 0) return false;
|
|
6001
|
+
const scope = searchScope(provider);
|
|
6002
|
+
const list = this.approval.allowlist?.();
|
|
6003
|
+
if (list && scopeExcluded(list, scope)) return false;
|
|
6004
|
+
return this.approval.grantsUnasked?.({ kind: "web_search", scope }) ?? false;
|
|
6005
|
+
}
|
|
6006
|
+
/** The last check, the one that leaves the machine: every DNS answer for the host must be public. */
|
|
6007
|
+
async resolved(target, turn) {
|
|
6008
|
+
if (!target.resolve) return ALLOWED;
|
|
6009
|
+
const egress = await vetEgress(target.resolve.hostname, target.resolve.port, this.resolveHost);
|
|
6010
|
+
if (egress.ok) return ALLOWED;
|
|
6011
|
+
this.refund(turn);
|
|
6012
|
+
return this.refuse(target.request, "guard", guardRefusal(egress.reason));
|
|
6013
|
+
}
|
|
6014
|
+
refuse(request, source, reason) {
|
|
6015
|
+
this.approval.record?.(request, false, source);
|
|
6016
|
+
return { allowed: false, reason };
|
|
6017
|
+
}
|
|
6018
|
+
vetFetch(raw) {
|
|
6019
|
+
const label = "Fetch";
|
|
6020
|
+
const request = (subject, scope, origin = scope) => ({ kind: "url_fetch", subject, scope, detail: `Planner research wants to fetch ${origin}` });
|
|
6021
|
+
if (this.carriesSecret([raw])) {
|
|
6022
|
+
return { label, request: request(WITHHELD, WITHHELD), refused: { source: "secret", reason: secretRefusal(label, "URL") } };
|
|
6023
|
+
}
|
|
6024
|
+
let url;
|
|
6025
|
+
try {
|
|
6026
|
+
url = new URL(raw);
|
|
6027
|
+
} catch {
|
|
6028
|
+
return { label, request: request(raw, raw), refused: { source: "guard", reason: "Fetch refused: not a valid URL." } };
|
|
6029
|
+
}
|
|
6030
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
6031
|
+
return { label, request: request(raw, raw), refused: { source: "guard", reason: "Fetch refused: only http and https URLs can be fetched." } };
|
|
6032
|
+
}
|
|
6033
|
+
const fetchRequest = request(raw, `${url.origin}/*`, url.origin);
|
|
6034
|
+
const egress = vetEgressUnresolved(url.hostname, url.port);
|
|
6035
|
+
if (egress && !egress.ok) {
|
|
6036
|
+
return { label, request: fetchRequest, refused: { source: "guard", reason: guardRefusal(egress.reason) } };
|
|
6037
|
+
}
|
|
6038
|
+
return { label, request: fetchRequest, ...egress ? {} : { resolve: { hostname: url.hostname, port: url.port } } };
|
|
6039
|
+
}
|
|
6040
|
+
vetSearch({ query, provider, providerLabel, note }) {
|
|
6041
|
+
const label = "Web search";
|
|
6042
|
+
const detail = `Planner research wants to search the web with ${providerLabel ?? provider}${providerLabel ? "" : ", a third party that receives the query"}${note ? `. ${note}` : ""}`;
|
|
6043
|
+
const request = (subject) => ({ kind: "web_search", subject, scope: searchScope(provider), detail });
|
|
6044
|
+
if (this.carriesSecret([query])) {
|
|
6045
|
+
return { label, request: request(WITHHELD), refused: { source: "secret", reason: secretRefusal(label, "query") } };
|
|
6046
|
+
}
|
|
6047
|
+
return { label, request: request(query) };
|
|
6048
|
+
}
|
|
6049
|
+
spend() {
|
|
6050
|
+
const turn = this.turn();
|
|
6051
|
+
if (this.spent.turn !== turn) this.spent = { turn, reads: 0 };
|
|
6052
|
+
if (this.spent.reads >= this.budget()) return void 0;
|
|
6053
|
+
this.spent.reads += 1;
|
|
6054
|
+
return { turn };
|
|
6055
|
+
}
|
|
6056
|
+
refund(turn) {
|
|
6057
|
+
if (this.spent.turn === turn && this.spent.reads > 0) this.spent.reads -= 1;
|
|
6058
|
+
}
|
|
6059
|
+
carriesSecret(texts) {
|
|
6060
|
+
return carriesSecret(texts, [...this.secrets(), ...sensitiveEnvValues(this.env())]);
|
|
6061
|
+
}
|
|
6062
|
+
};
|
|
6063
|
+
var WITHHELD = "(withheld: it contains a secret)";
|
|
6064
|
+
function searchScope(provider) {
|
|
6065
|
+
return `web_search:${provider}`;
|
|
6066
|
+
}
|
|
6067
|
+
function guardRefusal(reason) {
|
|
6068
|
+
return `Fetch refused: ${reason}. Private, local and non-web addresses are never fetched.`;
|
|
6069
|
+
}
|
|
6070
|
+
function secretRefusal(label, what) {
|
|
6071
|
+
return `${label} refused: the ${what} contains what looks like a secret (an API key, token or credential). Never put credentials in a ${what}; continue without it.`;
|
|
6072
|
+
}
|
|
6073
|
+
|
|
6074
|
+
// src/services/DuckDuckGoSearchProvider.ts
|
|
6075
|
+
var SEARCH_ENDPOINT = "https://lite.duckduckgo.com/lite/?q=";
|
|
6076
|
+
var COOLDOWN_BASE_MS = 3e4;
|
|
6077
|
+
var COOLDOWN_MAX_MS = 5 * 6e4;
|
|
6078
|
+
var CHALLENGE_MARKERS = [
|
|
6079
|
+
/<form\b[^>]*\bid\s*=\s*["']challenge-form["']/i,
|
|
6080
|
+
/<form\b[^>]*\baction\s*=\s*["'][^"']*\/anomaly\.js\b/i,
|
|
6081
|
+
/<div\b[^>]*\bclass\s*=\s*["'][^"']*\banomaly-modal/i
|
|
6082
|
+
];
|
|
6083
|
+
function looksLikeChallenge(html) {
|
|
6084
|
+
return CHALLENGE_MARKERS.some((marker2) => marker2.test(html));
|
|
6085
|
+
}
|
|
6086
|
+
function decodeEntities(text2) {
|
|
6087
|
+
return text2.replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16))).replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(Number(dec))).replace(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
6088
|
+
}
|
|
6089
|
+
function plainText(html) {
|
|
6090
|
+
return decodeEntities(html.replace(/<[^>]+>/g, " ")).replace(/\s+/g, " ").trim();
|
|
6091
|
+
}
|
|
6092
|
+
function attributesOf(tag) {
|
|
6093
|
+
const attrs = /* @__PURE__ */ new Map();
|
|
6094
|
+
const pattern = /([a-z_:][-\w:.]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/gi;
|
|
6095
|
+
for (let m = pattern.exec(tag); m; m = pattern.exec(tag)) {
|
|
6096
|
+
attrs.set(m[1].toLowerCase(), m[2] ?? m[3] ?? m[4] ?? "");
|
|
6097
|
+
}
|
|
6098
|
+
return attrs;
|
|
6099
|
+
}
|
|
6100
|
+
var RESULT_LINK_CLASS = /(?:^|\s)result(?:-link|__a)(?:\s|$)/;
|
|
6101
|
+
var SNIPPET_ELEMENT = /<(\w+)\b(?=[^>]*\bclass\s*=\s*["'][^"']*\bresult(?:-snippet|__snippet)\b)[^>]*>([\s\S]*?)<\/\1>/i;
|
|
6102
|
+
function resolveResultUrl(href) {
|
|
6103
|
+
let url;
|
|
6104
|
+
try {
|
|
6105
|
+
url = new URL(href, "https://duckduckgo.com/");
|
|
6106
|
+
} catch {
|
|
6107
|
+
return void 0;
|
|
6108
|
+
}
|
|
6109
|
+
if (url.hostname === "duckduckgo.com" || url.hostname.endsWith(".duckduckgo.com")) {
|
|
6110
|
+
const target = url.pathname === "/l/" ? url.searchParams.get("uddg") : null;
|
|
6111
|
+
if (!target) return void 0;
|
|
6112
|
+
try {
|
|
6113
|
+
url = new URL(target);
|
|
6114
|
+
} catch {
|
|
6115
|
+
return void 0;
|
|
6116
|
+
}
|
|
6117
|
+
}
|
|
6118
|
+
return url.protocol === "https:" || url.protocol === "http:" ? url.href : void 0;
|
|
6119
|
+
}
|
|
6120
|
+
function parseSearchHits(html) {
|
|
6121
|
+
const links = [];
|
|
6122
|
+
const anchor = /<a\b([^>]*)>([\s\S]*?)<\/a>/gi;
|
|
6123
|
+
for (let m = anchor.exec(html); m; m = anchor.exec(html)) {
|
|
6124
|
+
const attrs = attributesOf(m[1]);
|
|
6125
|
+
const href = attrs.get("href");
|
|
6126
|
+
if (!href || !RESULT_LINK_CLASS.test(attrs.get("class") ?? "")) continue;
|
|
6127
|
+
links.push({ start: m.index, end: m.index + m[0].length, href: decodeEntities(href), title: plainText(m[2]) });
|
|
6128
|
+
}
|
|
6129
|
+
const hits = [];
|
|
6130
|
+
links.forEach((link, i) => {
|
|
6131
|
+
const url = resolveResultUrl(link.href);
|
|
6132
|
+
if (!url || !link.title) return;
|
|
6133
|
+
const region = html.slice(link.end, links[i + 1]?.start ?? html.length);
|
|
6134
|
+
hits.push({ title: link.title, url, snippet: snippetIn(region) });
|
|
6135
|
+
});
|
|
6136
|
+
return hits;
|
|
6137
|
+
}
|
|
6138
|
+
function snippetIn(region) {
|
|
6139
|
+
const match = SNIPPET_ELEMENT.exec(region);
|
|
6140
|
+
return match ? plainText(match[2]) || void 0 : void 0;
|
|
6141
|
+
}
|
|
6142
|
+
var DuckDuckGoSearchProvider = class {
|
|
6143
|
+
id = "duckduckgo";
|
|
6144
|
+
name = "DuckDuckGo";
|
|
6145
|
+
failures = 0;
|
|
6146
|
+
cooldownUntil = 0;
|
|
6147
|
+
now;
|
|
6148
|
+
constructor(now = Date.now) {
|
|
6149
|
+
this.now = now;
|
|
6150
|
+
}
|
|
6151
|
+
coolingDown() {
|
|
6152
|
+
return this.now() < this.cooldownUntil;
|
|
6153
|
+
}
|
|
6154
|
+
async search(query, get) {
|
|
6155
|
+
const page = await get(SEARCH_ENDPOINT + encodeURIComponent(query));
|
|
6156
|
+
if (!page.ok) {
|
|
6157
|
+
if (page.status === 429 || page.status === 403) return this.block(`HTTP ${page.status}`);
|
|
6158
|
+
return { kind: "error", error: page.error };
|
|
6159
|
+
}
|
|
6160
|
+
if (page.status === 202) return this.block("HTTP 202");
|
|
6161
|
+
const hits = parseSearchHits(page.body).slice(0, 10);
|
|
6162
|
+
if (hits.length === 0) return looksLikeChallenge(page.body) ? this.block("a challenge page") : { kind: "none" };
|
|
6163
|
+
this.failures = 0;
|
|
6164
|
+
this.cooldownUntil = 0;
|
|
6165
|
+
return { kind: "hits", hits };
|
|
6166
|
+
}
|
|
6167
|
+
block(reason) {
|
|
6168
|
+
if (!this.coolingDown()) {
|
|
6169
|
+
this.failures += 1;
|
|
6170
|
+
const backoff = Math.min(COOLDOWN_BASE_MS * 2 ** (this.failures - 1), COOLDOWN_MAX_MS);
|
|
6171
|
+
this.cooldownUntil = this.now() + backoff;
|
|
6172
|
+
}
|
|
6173
|
+
return { kind: "blocked", reason };
|
|
6174
|
+
}
|
|
6175
|
+
};
|
|
6176
|
+
var sharedSearchProvider = new DuckDuckGoSearchProvider();
|
|
6177
|
+
|
|
6178
|
+
// src/services/WebResponseCache.ts
|
|
6179
|
+
var DEFAULT_TTL_MS = 5 * 6e4;
|
|
6180
|
+
var DEFAULT_MAX_ENTRIES = 100;
|
|
6181
|
+
var DEFAULT_MAX_BYTES = 2e7;
|
|
6182
|
+
var WebResponseCache = class {
|
|
6183
|
+
entries = /* @__PURE__ */ new Map();
|
|
6184
|
+
ttlMs;
|
|
6185
|
+
maxEntries;
|
|
6186
|
+
maxBytes;
|
|
6187
|
+
now;
|
|
6188
|
+
bytes = 0;
|
|
6189
|
+
constructor(opts = {}) {
|
|
6190
|
+
this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
6191
|
+
this.maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
6192
|
+
this.maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
6193
|
+
this.now = opts.now ?? Date.now;
|
|
6194
|
+
}
|
|
6195
|
+
getFetch(url) {
|
|
6196
|
+
return this.get(`get\0${url}`);
|
|
6197
|
+
}
|
|
6198
|
+
/** `maxAgeMs` is the response's own freshness; it shortens the TTL, never extends it. */
|
|
6199
|
+
setFetch(url, page, maxAgeMs = Infinity) {
|
|
6200
|
+
this.set(`get\0${url}`, page, Buffer.byteLength(page.body), maxAgeMs);
|
|
6201
|
+
}
|
|
6202
|
+
getSearch(providerAndQuery) {
|
|
6203
|
+
return this.get(`search\0${providerAndQuery}`);
|
|
6204
|
+
}
|
|
6205
|
+
setSearch(providerAndQuery, hits) {
|
|
6206
|
+
const bytes = hits.reduce((sum, h) => sum + Buffer.byteLength(h.title) + Buffer.byteLength(h.url) + Buffer.byteLength(h.snippet ?? ""), 0);
|
|
6207
|
+
this.set(`search\0${providerAndQuery}`, hits, bytes, Infinity);
|
|
6208
|
+
}
|
|
6209
|
+
get(key) {
|
|
6210
|
+
const entry = this.entries.get(key);
|
|
6211
|
+
if (!entry) return void 0;
|
|
6212
|
+
if (entry.expiresAt <= this.now()) {
|
|
6213
|
+
this.delete(key);
|
|
6214
|
+
return void 0;
|
|
6215
|
+
}
|
|
6216
|
+
this.entries.delete(key);
|
|
6217
|
+
this.entries.set(key, entry);
|
|
6218
|
+
return entry.value;
|
|
6219
|
+
}
|
|
6220
|
+
set(key, value, bytes, maxAgeMs) {
|
|
6221
|
+
this.delete(key);
|
|
6222
|
+
if (bytes > this.maxBytes) return;
|
|
6223
|
+
this.entries.set(key, { value, bytes, expiresAt: this.now() + Math.min(this.ttlMs, maxAgeMs) });
|
|
6224
|
+
this.bytes += bytes;
|
|
6225
|
+
while (this.entries.size > this.maxEntries || this.bytes > this.maxBytes) {
|
|
6226
|
+
const oldest = this.entries.keys().next().value;
|
|
6227
|
+
if (oldest === void 0) break;
|
|
6228
|
+
this.delete(oldest);
|
|
6229
|
+
}
|
|
6230
|
+
}
|
|
6231
|
+
delete(key) {
|
|
6232
|
+
const entry = this.entries.get(key);
|
|
6233
|
+
if (!entry) return;
|
|
6234
|
+
this.entries.delete(key);
|
|
6235
|
+
this.bytes -= entry.bytes;
|
|
6236
|
+
}
|
|
6237
|
+
};
|
|
6238
|
+
var sharedWebResponseCache = new WebResponseCache();
|
|
6239
|
+
|
|
6240
|
+
// src/services/HttpWebFetcher.ts
|
|
6241
|
+
var http = __toESM(require("node:http"));
|
|
6242
|
+
var https = __toESM(require("node:https"));
|
|
6243
|
+
var import_node_stream = require("node:stream");
|
|
6244
|
+
var zlib = __toESM(require("node:zlib"));
|
|
6245
|
+
var DEFAULT_MAX_CHARS = 4e4;
|
|
6246
|
+
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
6247
|
+
var MAX_BODY_BYTES = 2e6;
|
|
6248
|
+
var MAX_REDIRECTS = 5;
|
|
6249
|
+
var USER_AGENT = "Mozilla/5.0 (compatible; Ordewell-Planner/1.0; +https://github.com/ordewell)";
|
|
6250
|
+
var SEARCH_COOLDOWN_OUTPUT = "Web search is temporarily unavailable: the search provider backed off after pushing back on automated queries. Do not retry now; fetch a documentation URL you already know instead.";
|
|
6251
|
+
function decodeEntities2(text2) {
|
|
6252
|
+
return text2.replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/'/g, "'");
|
|
6253
|
+
}
|
|
6254
|
+
function htmlToText(html) {
|
|
6255
|
+
return decodeEntities2(
|
|
6256
|
+
html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ").replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ").replace(/<noscript\b[^>]*>[\s\S]*?<\/noscript>/gi, " ").replace(/<\/(p|div|li|h[1-6]|tr|section|article)>/gi, "\n").replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, " ")
|
|
6257
|
+
).replace(/[ \t\f\v]+/g, " ").replace(/\n\s*\n\s*\n+/g, "\n\n").split("\n").map((line) => line.trim()).join("\n").trim();
|
|
6258
|
+
}
|
|
6259
|
+
function formatHits(hits) {
|
|
6260
|
+
const output = hits.map((hit, i) => `${i + 1}. ${hit.title}
|
|
6261
|
+
${hit.url}${hit.snippet ? `
|
|
6262
|
+
${hit.snippet}` : ""}`).join("\n\n");
|
|
6263
|
+
return { success: true, output, truncated: false };
|
|
6264
|
+
}
|
|
6265
|
+
function sanitizeError(err) {
|
|
6266
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6267
|
+
if (/abort/i.test(msg)) return "timed out";
|
|
6268
|
+
if (/ECONNREFUSED|ECONNRESET|EHOSTUNREACH|ENETUNREACH/i.test(msg)) return "host not reachable";
|
|
6269
|
+
if (/ENOTFOUND|getaddrinfo/i.test(msg)) return "host not found";
|
|
6270
|
+
return msg.replace(/https?:\/\/\S+/g, "<url>");
|
|
6271
|
+
}
|
|
6272
|
+
function sharedLifetime(cacheControl, setCookie) {
|
|
6273
|
+
if (setCookie || /(?:^|,)\s*(?:no-store|private|no-cache)\b/i.test(cacheControl)) return 0;
|
|
6274
|
+
const ages = [...cacheControl.matchAll(/(?:^|,)\s*(?:s-)?max-age\s*=\s*"?(\d+)/gi)].map((m) => Number(m[1]) * 1e3);
|
|
6275
|
+
return ages.length ? Math.min(...ages) : Infinity;
|
|
6276
|
+
}
|
|
6277
|
+
var HttpWebFetcher = class _HttpWebFetcher {
|
|
6278
|
+
policy;
|
|
6279
|
+
fetchImpl;
|
|
6280
|
+
maxChars;
|
|
6281
|
+
timeoutMs;
|
|
6282
|
+
resolveHost;
|
|
6283
|
+
cache;
|
|
6284
|
+
searchProvider;
|
|
6285
|
+
prompting;
|
|
6286
|
+
constructor(opts) {
|
|
6287
|
+
this.policy = opts.policy;
|
|
6288
|
+
this.maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
|
|
6289
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
6290
|
+
this.resolveHost = opts.resolveHost ?? resolveAll;
|
|
6291
|
+
this.fetchImpl = opts.fetchImpl ?? pinnedTransport(guardedLookup(this.resolveHost));
|
|
6292
|
+
this.cache = opts.cache ?? new WebResponseCache();
|
|
6293
|
+
this.searchProvider = opts.searchProvider ?? new DuckDuckGoSearchProvider();
|
|
6294
|
+
this.prompting = opts.prompting ?? true;
|
|
6295
|
+
}
|
|
6296
|
+
decide(read2) {
|
|
6297
|
+
return this.policy.decide(read2, this.decideOptions);
|
|
6298
|
+
}
|
|
6299
|
+
get decideOptions() {
|
|
6300
|
+
return { prompting: this.prompting };
|
|
6301
|
+
}
|
|
6302
|
+
forSubagent() {
|
|
6303
|
+
return new _HttpWebFetcher({
|
|
6304
|
+
policy: this.policy,
|
|
6305
|
+
fetchImpl: this.fetchImpl,
|
|
6306
|
+
maxChars: this.maxChars,
|
|
6307
|
+
timeoutMs: this.timeoutMs,
|
|
6308
|
+
resolveHost: this.resolveHost,
|
|
6309
|
+
cache: this.cache,
|
|
6310
|
+
searchProvider: this.searchProvider,
|
|
6311
|
+
prompting: false
|
|
6312
|
+
});
|
|
6313
|
+
}
|
|
6314
|
+
searchesUnasked(provider) {
|
|
6315
|
+
return this.policy.searchesUnasked?.(provider) ?? false;
|
|
6316
|
+
}
|
|
6317
|
+
async fetch(url) {
|
|
6318
|
+
const decision = await this.policy.decide({ kind: "fetch", url }, this.decideOptions);
|
|
6319
|
+
if (!decision.allowed) return { success: false, output: decision.reason, truncated: false };
|
|
6320
|
+
return this.get(url, "Fetch");
|
|
6321
|
+
}
|
|
6322
|
+
async search(query) {
|
|
6323
|
+
const provider = this.searchProvider;
|
|
6324
|
+
if (provider.coolingDown()) return { success: false, output: SEARCH_COOLDOWN_OUTPUT, truncated: false };
|
|
6325
|
+
const decision = await this.policy.decide({ kind: "search", query, provider: provider.id }, this.decideOptions);
|
|
6326
|
+
if (!decision.allowed) return { success: false, output: decision.reason, truncated: false };
|
|
6327
|
+
const cacheKey = `${provider.id}\0${query}`;
|
|
6328
|
+
const cached2 = this.cache.getSearch(cacheKey);
|
|
6329
|
+
if (cached2) return formatHits(cached2);
|
|
6330
|
+
const outcome = await provider.search(query, (url) => this.getSearchPage(url));
|
|
6331
|
+
switch (outcome.kind) {
|
|
6332
|
+
case "hits":
|
|
6333
|
+
this.cache.setSearch(cacheKey, outcome.hits);
|
|
6334
|
+
return formatHits(outcome.hits);
|
|
6335
|
+
case "none":
|
|
6336
|
+
return {
|
|
6337
|
+
success: false,
|
|
6338
|
+
output: `No results could be parsed for "${query}". Search is best-effort; if you already know a documentation URL, use the fetch tool directly.`,
|
|
6339
|
+
truncated: false
|
|
6340
|
+
};
|
|
6341
|
+
case "blocked":
|
|
6342
|
+
return { success: false, output: SEARCH_COOLDOWN_OUTPUT, truncated: false };
|
|
6343
|
+
case "error":
|
|
6344
|
+
return { success: false, output: `Web search failed: ${outcome.error}`, truncated: false };
|
|
6345
|
+
}
|
|
6346
|
+
}
|
|
6347
|
+
async get(url, label) {
|
|
6348
|
+
const cached2 = this.cache.getFetch(url);
|
|
6349
|
+
if (cached2) return this.render(label, cached2.body, cached2.isHtml, false);
|
|
6350
|
+
const page = await this.request(url);
|
|
6351
|
+
if (!page.ok) return { success: false, output: `${label} failed: ${page.error}`, truncated: false };
|
|
6352
|
+
if (page.cacheFor > 0 && !page.truncated && page.origin === new URL(url).origin) {
|
|
6353
|
+
this.cache.setFetch(url, { body: page.body, isHtml: page.isHtml }, page.cacheFor);
|
|
6354
|
+
}
|
|
6355
|
+
return this.render(label, page.body, page.isHtml, page.truncated);
|
|
6356
|
+
}
|
|
6357
|
+
render(label, body, isHtml, byteTruncated) {
|
|
6358
|
+
const text2 = isHtml ? htmlToText(body) : body;
|
|
6359
|
+
if (byteTruncated || text2.length > this.maxChars) {
|
|
6360
|
+
const sliced = text2.slice(0, this.maxChars);
|
|
6361
|
+
return {
|
|
6362
|
+
success: true,
|
|
6363
|
+
output: `${sliced}
|
|
6364
|
+
|
|
6365
|
+
[... truncated to ${this.maxChars} chars, total ${text2.length}${byteTruncated ? "+, body was byte-capped at the source" : ""}]`,
|
|
6366
|
+
truncated: true
|
|
6367
|
+
};
|
|
6368
|
+
}
|
|
6369
|
+
return { success: true, output: text2, truncated: false };
|
|
6370
|
+
}
|
|
6371
|
+
async getSearchPage(url) {
|
|
6372
|
+
const page = await this.request(url);
|
|
6373
|
+
return page.ok ? { ok: true, status: page.status, body: page.body } : { ok: false, status: page.status, error: page.error };
|
|
6374
|
+
}
|
|
6375
|
+
async request(url) {
|
|
6376
|
+
let current = new URL(url);
|
|
6377
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
6378
|
+
const result = await this.hop(current);
|
|
6379
|
+
if (!("redirect" in result)) return result;
|
|
6380
|
+
const refused3 = await this.refuseRedirect(current, result.redirect);
|
|
6381
|
+
if (refused3) return { ok: false, status: result.status, error: refused3 };
|
|
6382
|
+
current = result.redirect;
|
|
6383
|
+
}
|
|
6384
|
+
return { ok: false, error: `too many redirects (>${MAX_REDIRECTS})` };
|
|
6385
|
+
}
|
|
6386
|
+
/**
|
|
6387
|
+
* Why a redirect must not be followed, or `undefined` if it may. The policy
|
|
6388
|
+
* decided only the first URL, so a hop to another origin is a new read with
|
|
6389
|
+
* the same decision; otherwise a redirect would carry the fetch past an
|
|
6390
|
+
* exclusion, a Guarded grant or the budget.
|
|
6391
|
+
*/
|
|
6392
|
+
async refuseRedirect(from, to) {
|
|
6393
|
+
if (to.protocol !== "https:" && to.protocol !== "http:") return "redirect to a non-HTTP scheme";
|
|
6394
|
+
if (from.protocol === "https:" && to.protocol === "http:") return "redirect from https to plain http";
|
|
6395
|
+
if (to.origin === from.origin) return void 0;
|
|
6396
|
+
const decision = await this.policy.decide({ kind: "fetch", url: to.href }, this.decideOptions);
|
|
6397
|
+
return decision.allowed ? void 0 : `the page redirected to another origin, which was refused. ${decision.reason}`;
|
|
6398
|
+
}
|
|
6399
|
+
async hop(url) {
|
|
6400
|
+
const controller = new AbortController();
|
|
6401
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
6402
|
+
try {
|
|
6403
|
+
const res = await this.fetchImpl(url.href, {
|
|
6404
|
+
signal: controller.signal,
|
|
6405
|
+
headers: { "user-agent": USER_AGENT, accept: "text/html,application/xhtml+xml,application/json;q=0.9,*/*;q=0.8" },
|
|
6406
|
+
redirect: "manual"
|
|
6407
|
+
});
|
|
6408
|
+
if (res.status >= 300 && res.status < 400) {
|
|
6409
|
+
const location = res.headers.get("location");
|
|
6410
|
+
if (!location) return { ok: false, status: res.status, error: `redirect (${res.status}) with no Location` };
|
|
6411
|
+
return { redirect: new URL(location, url), status: res.status };
|
|
6412
|
+
}
|
|
6413
|
+
if (!res.ok) return { ok: false, status: res.status, error: `HTTP ${res.status}` };
|
|
6414
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
6415
|
+
const cacheFor = sharedLifetime(res.headers.get("cache-control") ?? "", res.headers.get("set-cookie"));
|
|
6416
|
+
const { body, truncated } = await readBounded(res, MAX_BODY_BYTES);
|
|
6417
|
+
return { ok: true, status: res.status, origin: url.origin, body, isHtml: /html|xml/i.test(contentType), truncated, cacheFor };
|
|
6418
|
+
} catch (err) {
|
|
6419
|
+
return { ok: false, error: sanitizeError(err) };
|
|
6420
|
+
} finally {
|
|
6421
|
+
clearTimeout(timer);
|
|
6422
|
+
controller.abort();
|
|
6423
|
+
}
|
|
6424
|
+
}
|
|
6425
|
+
};
|
|
6426
|
+
function isAsyncIterable(value) {
|
|
6427
|
+
return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
|
|
6428
|
+
}
|
|
6429
|
+
async function readBounded(res, maxBytes) {
|
|
6430
|
+
return isAsyncIterable(res.body) ? readFromIterable(res.body, maxBytes) : readFromText(res, maxBytes);
|
|
6431
|
+
}
|
|
6432
|
+
async function readFromIterable(source, maxBytes) {
|
|
6433
|
+
const chunks = [];
|
|
6434
|
+
let total = 0;
|
|
6435
|
+
let truncated = false;
|
|
6436
|
+
for await (const chunk of source) {
|
|
6437
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
6438
|
+
if (total + buf.length > maxBytes) {
|
|
6439
|
+
chunks.push(buf.subarray(0, maxBytes - total));
|
|
6440
|
+
total = maxBytes;
|
|
6441
|
+
truncated = true;
|
|
6442
|
+
break;
|
|
6443
|
+
}
|
|
6444
|
+
chunks.push(buf);
|
|
6445
|
+
total += buf.length;
|
|
6446
|
+
}
|
|
6447
|
+
return { body: Buffer.concat(chunks).toString("utf8"), truncated };
|
|
6448
|
+
}
|
|
6449
|
+
async function readFromText(res, maxBytes) {
|
|
6450
|
+
const text2 = await res.text();
|
|
6451
|
+
if (Buffer.byteLength(text2, "utf8") <= maxBytes) return { body: text2, truncated: false };
|
|
6452
|
+
return { body: Buffer.from(text2, "utf8").subarray(0, maxBytes).toString("utf8"), truncated: true };
|
|
6453
|
+
}
|
|
6454
|
+
function headerGetter(res) {
|
|
6455
|
+
return {
|
|
6456
|
+
get: (name) => {
|
|
6457
|
+
const value = res.headers[name.toLowerCase()];
|
|
6458
|
+
if (value === void 0) return null;
|
|
6459
|
+
return Array.isArray(value) ? value.join(", ") : value;
|
|
6460
|
+
}
|
|
6461
|
+
};
|
|
6462
|
+
}
|
|
6463
|
+
function decodedBody(res) {
|
|
6464
|
+
const encoding = String(res.headers["content-encoding"] ?? "").trim().toLowerCase();
|
|
6465
|
+
const decoder = encoding === "gzip" || encoding === "x-gzip" ? zlib.createGunzip() : encoding === "deflate" ? zlib.createInflate() : encoding === "br" ? zlib.createBrotliDecompress() : void 0;
|
|
6466
|
+
if (!decoder) return res;
|
|
6467
|
+
(0, import_node_stream.pipeline)(res, decoder, () => void 0);
|
|
6468
|
+
return decoder;
|
|
6469
|
+
}
|
|
6470
|
+
function pinnedTransport(lookup2) {
|
|
6471
|
+
return (url, init) => new Promise((resolve9, reject) => {
|
|
6472
|
+
const target = new URL(url);
|
|
6473
|
+
const client = target.protocol === "https:" ? https : http;
|
|
6474
|
+
const req = client.request(
|
|
6475
|
+
{
|
|
6476
|
+
protocol: target.protocol,
|
|
6477
|
+
hostname: target.hostname.replace(/^\[|\]$/g, ""),
|
|
6478
|
+
port: target.port || void 0,
|
|
6479
|
+
path: `${target.pathname}${target.search}`,
|
|
6480
|
+
method: "GET",
|
|
6481
|
+
// Sent, and decoded below, because the platform fetch did the same and
|
|
6482
|
+
// some hosts challenge clients that don't.
|
|
6483
|
+
headers: { ...init.headers, "accept-encoding": "gzip, deflate, br" },
|
|
6484
|
+
signal: init.signal,
|
|
6485
|
+
lookup: lookup2,
|
|
6486
|
+
// A pooled keep-alive socket would be reused without a fresh lookup.
|
|
6487
|
+
agent: false
|
|
6488
|
+
},
|
|
6489
|
+
(res) => {
|
|
6490
|
+
res.on("error", () => void 0);
|
|
6491
|
+
const body = decodedBody(res);
|
|
6492
|
+
resolve9({
|
|
6493
|
+
ok: res.statusCode !== void 0 && res.statusCode >= 200 && res.statusCode < 300,
|
|
6494
|
+
status: res.statusCode ?? 0,
|
|
6495
|
+
headers: headerGetter(res),
|
|
6496
|
+
body,
|
|
6497
|
+
text: async () => (await readFromIterable(body, Infinity)).body
|
|
6498
|
+
});
|
|
6499
|
+
}
|
|
6500
|
+
);
|
|
6501
|
+
req.on("error", reject);
|
|
6502
|
+
req.end();
|
|
6503
|
+
});
|
|
6504
|
+
}
|
|
6505
|
+
|
|
5337
6506
|
// src/services/PendingApprovals.ts
|
|
5338
|
-
var
|
|
6507
|
+
var DEFAULT_TIMEOUT_MS2 = 5 * 6e4;
|
|
5339
6508
|
var counter = 0;
|
|
5340
6509
|
var PendingApprovals = class {
|
|
5341
6510
|
constructor(opts = {}) {
|
|
@@ -5356,7 +6525,7 @@ var PendingApprovals = class {
|
|
|
5356
6525
|
return Promise.resolve(refused3);
|
|
5357
6526
|
}
|
|
5358
6527
|
const pending = { id, request, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
5359
|
-
return new Promise((
|
|
6528
|
+
return new Promise((resolve9) => {
|
|
5360
6529
|
const settle = (decision) => {
|
|
5361
6530
|
const entry = this.entries.get(id);
|
|
5362
6531
|
if (!entry) return;
|
|
@@ -5364,11 +6533,11 @@ var PendingApprovals = class {
|
|
|
5364
6533
|
this.entries.delete(id);
|
|
5365
6534
|
options.onDecision?.(decision);
|
|
5366
6535
|
this.opts.onSettled?.(id, isGranted(decision), { request, decision });
|
|
5367
|
-
|
|
6536
|
+
resolve9(decision);
|
|
5368
6537
|
};
|
|
5369
6538
|
let timer = null;
|
|
5370
6539
|
if (!options.noTimeout) {
|
|
5371
|
-
timer = setTimeout(() => settle({ decision: "deny" }), this.opts.timeoutMs ??
|
|
6540
|
+
timer = setTimeout(() => settle({ decision: "deny" }), this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2);
|
|
5372
6541
|
timer.unref?.();
|
|
5373
6542
|
}
|
|
5374
6543
|
this.entries.set(id, { pending, settle, timer });
|
|
@@ -5491,8 +6660,8 @@ function firstLineOf(command) {
|
|
|
5491
6660
|
function text(value) {
|
|
5492
6661
|
return typeof value === "string" ? value : "";
|
|
5493
6662
|
}
|
|
5494
|
-
function searchArg(pattern,
|
|
5495
|
-
return `${pattern}${
|
|
6663
|
+
function searchArg(pattern, path32, include = "") {
|
|
6664
|
+
return `${pattern}${path32 ? ` in ${path32}` : ""}${include ? ` (${include})` : ""}`;
|
|
5496
6665
|
}
|
|
5497
6666
|
function hintArg(args) {
|
|
5498
6667
|
for (const field of HINT_FIELDS) {
|
|
@@ -5593,8 +6762,8 @@ function userMessage(id, text2, skills, turnId) {
|
|
|
5593
6762
|
const block = settledMessage(id, "user", text2, turnId);
|
|
5594
6763
|
return skills.length > 0 ? { ...block, skills: skills.map((s) => s.name) } : block;
|
|
5595
6764
|
}
|
|
5596
|
-
function skillLoadNotice({ invokedBy, name, source, path:
|
|
5597
|
-
return { invokedBy, name, source, path:
|
|
6765
|
+
function skillLoadNotice({ invokedBy, name, source, path: path32, attaches }) {
|
|
6766
|
+
return { invokedBy, name, source, path: path32, ...attaches ? { attaches } : {} };
|
|
5598
6767
|
}
|
|
5599
6768
|
var LOAD_SKILL_TOOL = /(?:^|_)load_skill$/;
|
|
5600
6769
|
function surfaceStep(step) {
|
|
@@ -5611,8 +6780,8 @@ function loadedSkillName(args) {
|
|
|
5611
6780
|
return void 0;
|
|
5612
6781
|
}
|
|
5613
6782
|
}
|
|
5614
|
-
function skillLoadBlock(id, { invokedBy, name, source, path:
|
|
5615
|
-
return { type: "skill_load", id, invokedBy, name, source, path:
|
|
6783
|
+
function skillLoadBlock(id, { invokedBy, name, source, path: path32, attaches }, turnId) {
|
|
6784
|
+
return { type: "skill_load", id, invokedBy, name, source, path: path32, ...attaches ? { attaches } : {}, ...turnId ? { turnId } : {} };
|
|
5616
6785
|
}
|
|
5617
6786
|
function subagentBlock(id, { toolCallId, model, usage, turnId, ...fields }) {
|
|
5618
6787
|
return {
|
|
@@ -7079,6 +8248,9 @@ var BaseConfig = class {
|
|
|
7079
8248
|
get approvalMode() {
|
|
7080
8249
|
return parseApprovalModeSetting(process.env.ORDEWELL_APPROVAL_MODE);
|
|
7081
8250
|
}
|
|
8251
|
+
get webReadBudget() {
|
|
8252
|
+
return nonNegativeInt(process.env.ORDEWELL_WEB_READ_BUDGET, DEFAULT_WEB_READ_BUDGET);
|
|
8253
|
+
}
|
|
7082
8254
|
get approvalDefaults() {
|
|
7083
8255
|
return process.env.ORDEWELL_APPROVAL_DEFAULTS !== "false" && process.env.ORDEWELL_APPROVAL_DEFAULTS !== "0";
|
|
7084
8256
|
}
|
|
@@ -7298,28 +8470,28 @@ function posixBinDirs() {
|
|
|
7298
8470
|
];
|
|
7299
8471
|
}
|
|
7300
8472
|
function windowsBinDirs(env, home) {
|
|
7301
|
-
const
|
|
7302
|
-
const under = (base, ...parts) => base ? [
|
|
8473
|
+
const join31 = (...parts) => path5.win32.join(...parts);
|
|
8474
|
+
const under = (base, ...parts) => base ? [join31(base, ...parts)] : [];
|
|
7303
8475
|
const appData = env.APPDATA;
|
|
7304
8476
|
const localAppData = env.LOCALAPPDATA;
|
|
7305
8477
|
const programData = env.ProgramData;
|
|
7306
8478
|
return [
|
|
7307
8479
|
// Native per-user installers, PowerShell-driven and otherwise.
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
8480
|
+
join31(home, ".local", "bin"),
|
|
8481
|
+
join31(home, ".opencode", "bin"),
|
|
8482
|
+
join31(home, ".claude", "bin"),
|
|
8483
|
+
join31(home, ".codex", "bin"),
|
|
7312
8484
|
...under(localAppData, "Programs", "claude"),
|
|
7313
8485
|
// Node package managers. npm's global prefix is `%APPDATA%\npm`; the
|
|
7314
8486
|
// literal spelling is kept as a fallback for a host that does not export
|
|
7315
8487
|
// APPDATA (a service account, a stripped CI container).
|
|
7316
8488
|
...under(appData, "npm"),
|
|
7317
|
-
|
|
8489
|
+
join31(home, "AppData", "Roaming", "npm"),
|
|
7318
8490
|
...under(localAppData, "pnpm"),
|
|
7319
8491
|
...under(localAppData, "Yarn", "bin"),
|
|
7320
|
-
|
|
8492
|
+
join31(home, ".bun", "bin"),
|
|
7321
8493
|
// Windows package managers.
|
|
7322
|
-
|
|
8494
|
+
join31(home, "scoop", "shims"),
|
|
7323
8495
|
...under(env.SCOOP, "shims"),
|
|
7324
8496
|
...under(env.SCOOP_GLOBAL ?? programData, "scoop", "shims"),
|
|
7325
8497
|
...under(programData, "chocolatey", "bin"),
|
|
@@ -7329,7 +8501,7 @@ function windowsBinDirs(env, home) {
|
|
|
7329
8501
|
...under(localAppData, "Microsoft", "WindowsApps"),
|
|
7330
8502
|
// Version managers. Volta's Windows home is not the POSIX `~/.volta`.
|
|
7331
8503
|
...under(localAppData, "Volta", "bin"),
|
|
7332
|
-
|
|
8504
|
+
join31(home, ".volta", "bin")
|
|
7333
8505
|
];
|
|
7334
8506
|
}
|
|
7335
8507
|
function wellKnownBinDirs(deps = {}) {
|
|
@@ -9431,7 +10603,7 @@ function buildResearchToolsPrompt() {
|
|
|
9431
10603
|
}
|
|
9432
10604
|
function buildSubagentSystemPrompt() {
|
|
9433
10605
|
return [
|
|
9434
|
-
"You are a read-only research subagent working for a project planner. You receive one self-contained research task, explore the workspace with your tools, and reply with a single digest. You never modify files
|
|
10606
|
+
"You are a read-only research subagent working for a project planner. You receive one self-contained research task, explore the workspace with your tools, and reply with a single digest. You never modify files and cannot spawn further agents. You may fetch or search the web, but a read that would need approval is refused \u2014 note the gap in your digest rather than retrying.",
|
|
9435
10607
|
"",
|
|
9436
10608
|
"Work efficiently: batch independent tool calls in one reply; prefer glob/grep to locate, then read only what matters. Your tool budget is small \u2014 stop exploring when you can answer the task.",
|
|
9437
10609
|
"",
|
|
@@ -10016,12 +11188,12 @@ function buildConflictRepairPrompt(task, conflict, integrationBranch) {
|
|
|
10016
11188
|
const files = conflict.conflictFiles ?? [];
|
|
10017
11189
|
const asked = `What the task was asked to do:
|
|
10018
11190
|
${task.prompt ?? task.description}`;
|
|
10019
|
-
const
|
|
11191
|
+
const resolve9 = "resolve every conflict so that both sides' intent survives: keep the work already integrated and add what the task contributed.";
|
|
10020
11192
|
const repos = conflict.repos.filter((repo) => repo !== SELF_REPO);
|
|
10021
11193
|
if (repos.length === 0) {
|
|
10022
11194
|
return [
|
|
10023
11195
|
`Task #${task.order} "${task.title}" passed, but merging its branch \`${branch}\` into \`${integrationBranch}\` conflicted${files.length > 0 ? ` in ${files.join(", ")}` : ""}. None of its work has landed yet.`,
|
|
10024
|
-
`This is the task's own worktree, on \`${branch}\`, with its work committed. Run \`git merge --no-edit ${integrationBranch}\` here and ${
|
|
11196
|
+
`This is the task's own worktree, on \`${branch}\`, with its work committed. Run \`git merge --no-edit ${integrationBranch}\` here and ${resolve9} Never drop either side wholesale, and never abort the merge or reset it away.`,
|
|
10025
11197
|
"Build and test the result the way this project does, commit the merge, and only then call `task_complete`.",
|
|
10026
11198
|
"",
|
|
10027
11199
|
asked
|
|
@@ -10030,7 +11202,7 @@ ${task.prompt ?? task.description}`;
|
|
|
10030
11202
|
const stoppedIn = conflict.conflictRepo ?? repos[0];
|
|
10031
11203
|
return [
|
|
10032
11204
|
`Task #${task.order} "${task.title}" passed, but landing its branch \`${branch}\` on \`${integrationBranch}\` conflicted in ${stoppedIn}${files.length > 0 ? ` (${files.join(", ")})` : ""}. A task lands in every repository it changed or in none, so none of its work has landed yet.`,
|
|
10033
|
-
`This is the task's own workspace, with each repository at its usual path on \`${branch}\` and the task's work committed. In each repository the task changed \u2014 ${repos.join(", ")} \u2014 run \`git merge --no-edit ${integrationBranch}\` inside that repository's directory, and ${
|
|
11205
|
+
`This is the task's own workspace, with each repository at its usual path on \`${branch}\` and the task's work committed. In each repository the task changed \u2014 ${repos.join(", ")} \u2014 run \`git merge --no-edit ${integrationBranch}\` inside that repository's directory, and ${resolve9} Never drop either side wholesale, and never abort a merge or reset it away.`,
|
|
10034
11206
|
"Build and test the result the way this project does, commit the merge in each repository, and only then call `task_complete`.",
|
|
10035
11207
|
"",
|
|
10036
11208
|
asked
|
|
@@ -11387,7 +12559,7 @@ var RESEARCH_TOOLS = [
|
|
|
11387
12559
|
},
|
|
11388
12560
|
{
|
|
11389
12561
|
name: "fetch",
|
|
11390
|
-
description: "Fetch content from a URL (HTTP/HTTPS). Use for reading API documentation, library references, or package docs.
|
|
12562
|
+
description: "Fetch content from a URL (HTTP/HTTPS). Use for reading API documentation, library references, or package docs. Depending on the approval settings, the first fetch from a site may wait for the user to approve it. Private and local addresses are always refused, as is a URL that contains a credential, and each turn has a limited number of web reads; a refusal says why.",
|
|
11391
12563
|
properties: {
|
|
11392
12564
|
url: { type: "string", description: "The URL to fetch" }
|
|
11393
12565
|
},
|
|
@@ -11395,16 +12567,15 @@ var RESEARCH_TOOLS = [
|
|
|
11395
12567
|
},
|
|
11396
12568
|
{
|
|
11397
12569
|
name: "web_search",
|
|
11398
|
-
description: "Search the web for result titles, URLs and snippets. When to use: you need docs for an unfamiliar library or API and have no URL yet \u2014 search first, then fetch the most promising result. When NOT to use: anything answerable from the workspace itself (use grep or find_symbol).",
|
|
12570
|
+
description: "Search the web for result titles, URLs and snippets. When to use: you need docs for an unfamiliar library or API and have no URL yet \u2014 search first, then fetch the most promising result. When NOT to use: anything answerable from the workspace itself (use grep or find_symbol). Depending on the approval settings, the first search may wait for the user to approve it; a query that contains a credential is refused, and searches count toward the turn's web reads.",
|
|
11399
12571
|
properties: {
|
|
11400
12572
|
query: { type: "string", description: "Search query" }
|
|
11401
12573
|
},
|
|
11402
12574
|
required: ["query"]
|
|
11403
12575
|
}
|
|
11404
12576
|
];
|
|
11405
|
-
var SUBAGENT_EXCLUDED = /* @__PURE__ */ new Set(["fetch", "web_search"]);
|
|
11406
12577
|
function subagentToolSpecs() {
|
|
11407
|
-
return RESEARCH_TOOLS
|
|
12578
|
+
return RESEARCH_TOOLS;
|
|
11408
12579
|
}
|
|
11409
12580
|
function withSpawn() {
|
|
11410
12581
|
return [...RESEARCH_TOOLS, SPAWN_RESEARCH_AGENT_SPEC];
|
|
@@ -11650,9 +12821,9 @@ function summarizeToolCall(tool2, argsJson, toolLabel) {
|
|
|
11650
12821
|
const scope = args.include ? ` in ${args.include}` : "";
|
|
11651
12822
|
return `grep ${ellipsize(String(args.pattern), 40)}${scope}${mode}`;
|
|
11652
12823
|
}
|
|
11653
|
-
const
|
|
11654
|
-
if (
|
|
11655
|
-
const basename4 = String(
|
|
12824
|
+
const path32 = args.filePath || args.file_path || args.path;
|
|
12825
|
+
if (path32) {
|
|
12826
|
+
const basename4 = String(path32).split("/").pop() || String(path32);
|
|
11656
12827
|
return `${display} ${basename4}`;
|
|
11657
12828
|
}
|
|
11658
12829
|
if (args.pattern) {
|
|
@@ -11667,7 +12838,7 @@ function summarizeToolCall(tool2, argsJson, toolLabel) {
|
|
|
11667
12838
|
}
|
|
11668
12839
|
function classifyOutcome(success, output) {
|
|
11669
12840
|
if (!success) {
|
|
11670
|
-
if (/^Command refused:/.test(output) || /is not available\. You are a read-only research subagent/.test(output)) return "refused";
|
|
12841
|
+
if (/^(Command|Fetch|Web search) refused:/.test(output) || /is not available\. You are a read-only research subagent/.test(output)) return "refused";
|
|
11671
12842
|
if (/not approved/i.test(output) || /^Access denied:/.test(output) || /needs user approval/i.test(output)) return "denied";
|
|
11672
12843
|
return "failure";
|
|
11673
12844
|
}
|
|
@@ -11732,7 +12903,7 @@ ${turn.text}`;
|
|
|
11732
12903
|
const results = [];
|
|
11733
12904
|
for (const tc of turn.toolCalls) {
|
|
11734
12905
|
if (!ALLOWED_TOOL_NAMES.has(tc.name)) {
|
|
11735
|
-
const refusal = `Tool "${tc.name}" is not available. You are a read-only research subagent: your only tools are ${[...ALLOWED_TOOL_NAMES].join(", ")}. You cannot
|
|
12906
|
+
const refusal = `Tool "${tc.name}" is not available. You are a read-only research subagent: your only tools are ${[...ALLOWED_TOOL_NAMES].join(", ")}. You cannot spawn further agents or modify files. If a specific resource would have answered your question, name it in your digest so the planner can fetch it directly. Otherwise continue with the available tools, then reply with your digest.`;
|
|
11736
12907
|
results.push({ name: tc.name, output: refusal, truncated: false, totalChars: refusal.length, id: tc.id });
|
|
11737
12908
|
continue;
|
|
11738
12909
|
}
|
|
@@ -11740,7 +12911,7 @@ ${turn.text}`;
|
|
|
11740
12911
|
if (tc.name === "read_file" && !("maxBytes" in args) && !("limit" in args)) args.limit = 2e3;
|
|
11741
12912
|
const toolArgs = JSON.stringify(tc.args);
|
|
11742
12913
|
deps.onProgress?.({ type: "tool_call", tool: tc.name, toolArgs, toolCallId: tc.id, subagentId: deps.subagentId });
|
|
11743
|
-
const res = await executeTool(tc.name, args, fs17,
|
|
12914
|
+
const res = await executeTool(tc.name, args, fs17, deps.fetcher, deps.signal);
|
|
11744
12915
|
const output = res.output.length > SUBAGENT_LIMITS.toolOutputMaxChars ? res.output.slice(0, SUBAGENT_LIMITS.toolOutputMaxChars) + `
|
|
11745
12916
|
[... truncated to ${SUBAGENT_LIMITS.toolOutputMaxChars} chars, total ${res.output.length}]` : res.output;
|
|
11746
12917
|
const stepEntry = {
|
|
@@ -11869,7 +13040,7 @@ var BaseAiService = class _BaseAiService {
|
|
|
11869
13040
|
* steering/failed result and the loop continues sequentially: a subagent can
|
|
11870
13041
|
* never fail a plan or a turn.
|
|
11871
13042
|
*/
|
|
11872
|
-
async executeSpawnAgent(args, fs17, onProgress, signal, subagentId) {
|
|
13043
|
+
async executeSpawnAgent(args, fs17, onProgress, signal, subagentId, fetcher) {
|
|
11873
13044
|
const prompt = typeof args.prompt === "string" ? args.prompt.trim() : "";
|
|
11874
13045
|
if (!prompt) {
|
|
11875
13046
|
return { success: false, output: 'spawn_research_agent requires a non-empty "prompt" string: a self-contained task for the agent, including what its digest must report back.', truncated: false };
|
|
@@ -11884,6 +13055,10 @@ var BaseAiService = class _BaseAiService {
|
|
|
11884
13055
|
fs: fs17,
|
|
11885
13056
|
signal,
|
|
11886
13057
|
subagentId,
|
|
13058
|
+
// The subagent gets web tools only behind a non-prompting gate: no
|
|
13059
|
+
// approval channel exists in the background, so a read that would ask is
|
|
13060
|
+
// refused instead (ADR-0027, ADR-0008 T6).
|
|
13061
|
+
fetcher: fetcher?.forSubagent?.(),
|
|
11887
13062
|
model: this.config.researchSubagentModel,
|
|
11888
13063
|
usage: () => usage,
|
|
11889
13064
|
onProgress: (progress) => {
|
|
@@ -11919,7 +13094,7 @@ var BaseAiService = class _BaseAiService {
|
|
|
11919
13094
|
const subagentId = (0, import_uuid3.v4)();
|
|
11920
13095
|
spawnSubagentIds.set(i, subagentId);
|
|
11921
13096
|
onProgress({ type: "tool_call", tool: toolCalls[i].name, toolArgs: JSON.stringify(toolCalls[i].args), subagentId, toolCallId: toolCalls[i].id });
|
|
11922
|
-
precomputed.set(i, await this.executeSpawnAgent({ ...toolCalls[i].args }, fs17, onProgress, signal, subagentId));
|
|
13097
|
+
precomputed.set(i, await this.executeSpawnAgent({ ...toolCalls[i].args }, fs17, onProgress, signal, subagentId, fetcher));
|
|
11923
13098
|
}) : Promise.resolve(),
|
|
11924
13099
|
parallelIndexes.length > 1 ? mapWithConcurrency(parallelIndexes, TOOL_ROUND_CONCURRENCY, async (i) => {
|
|
11925
13100
|
onProgress({ type: "tool_call", tool: toolCalls[i].name, toolArgs: JSON.stringify(toolCalls[i].args), toolCallId: toolCalls[i].id });
|
|
@@ -12942,6 +14117,12 @@ var listModelsInput = import_v4.z.object({
|
|
|
12942
14117
|
var runCommandInput = import_v4.z.object({
|
|
12943
14118
|
command: import_v4.z.string().min(1).describe("One shell command line, run from the workspace root.")
|
|
12944
14119
|
});
|
|
14120
|
+
var fetchInput = import_v4.z.object({
|
|
14121
|
+
url: import_v4.z.string().min(1).describe("The http or https URL to read.")
|
|
14122
|
+
});
|
|
14123
|
+
var webSearchInput = import_v4.z.object({
|
|
14124
|
+
query: import_v4.z.string().trim().min(1).describe("What to search for.")
|
|
14125
|
+
});
|
|
12945
14126
|
var loadSkillInput = import_v4.z.object({
|
|
12946
14127
|
name: import_v4.z.string().min(1).describe("The name of an Ordewell planner skill to load.")
|
|
12947
14128
|
});
|
|
@@ -13088,6 +14269,23 @@ var PLANNER_TOOLS = [
|
|
|
13088
14269
|
(h) => h.runCommand?.bind(h),
|
|
13089
14270
|
PLANNER_READ_ONLY
|
|
13090
14271
|
),
|
|
14272
|
+
// Fallbacks (ADR-0027): a runner's own fetch or search, where it has one, is
|
|
14273
|
+
// decided by the same policy and carries the provider's own summaries, so
|
|
14274
|
+
// the descriptions send the model there first.
|
|
14275
|
+
tool(
|
|
14276
|
+
"fetch",
|
|
14277
|
+
"Read a web page over http or https: documentation, a changelog, an API reference. A fallback: if you have a built-in web fetch tool, use that instead. Whether a site may be read is up to the user's settings, and a refusal says why; private and local addresses and URLs that carry a credential are always refused, and each planning turn has a limited number of web reads.",
|
|
14278
|
+
fetchInput,
|
|
14279
|
+
(h) => h.fetch?.bind(h),
|
|
14280
|
+
PLANNER_READ_ONLY
|
|
14281
|
+
),
|
|
14282
|
+
tool(
|
|
14283
|
+
"web_search",
|
|
14284
|
+
"Search the web for result titles, URLs and snippets, then read the most promising result with a fetch. A fallback: if you have a built-in web search tool, use that instead. Whether a search may run is up to the user's settings, and a refusal says why; a query that carries a credential is refused, and searches count toward the turn's web reads.",
|
|
14285
|
+
webSearchInput,
|
|
14286
|
+
(h) => h.webSearch?.bind(h),
|
|
14287
|
+
PLANNER_READ_ONLY
|
|
14288
|
+
),
|
|
13091
14289
|
tool(
|
|
13092
14290
|
"load_skill",
|
|
13093
14291
|
"Load an Ordewell planner skill by name. Returns its instructions; only model-invocable planner skills can be loaded.",
|
|
@@ -13117,8 +14315,14 @@ var OrdewellMcpServer = class {
|
|
|
13117
14315
|
issueTaskToken(scope, handler = {}) {
|
|
13118
14316
|
return this.issue({ role: "task", scope: { ...scope }, handler, revoked: new AbortController() });
|
|
13119
14317
|
}
|
|
13120
|
-
issuePlannerToken(scope, handler = {}) {
|
|
13121
|
-
|
|
14318
|
+
async issuePlannerToken(scope, handler = {}) {
|
|
14319
|
+
const hidden = [...scope.hidden ?? []];
|
|
14320
|
+
const credential = await this.issue({ role: "planner", scope: { ...scope, hidden }, handler, revoked: new AbortController() });
|
|
14321
|
+
return hidden.length > 0 ? { ...credential, hidden } : credential;
|
|
14322
|
+
}
|
|
14323
|
+
/** Every token still good here: secrets the outbound check keeps out of a URL (ADR-0027). */
|
|
14324
|
+
heldTokens() {
|
|
14325
|
+
return [...this.grants.keys()];
|
|
13122
14326
|
}
|
|
13123
14327
|
/** Refuse every later request carrying `token`, and abort the calls it has in flight. */
|
|
13124
14328
|
revoke(token) {
|
|
@@ -13129,14 +14333,14 @@ var OrdewellMcpServer = class {
|
|
|
13129
14333
|
}
|
|
13130
14334
|
async dispose() {
|
|
13131
14335
|
for (const token of [...this.grants.keys()]) this.revoke(token);
|
|
13132
|
-
const
|
|
14336
|
+
const http2 = this.http;
|
|
13133
14337
|
this.http = void 0;
|
|
13134
14338
|
this.listening = void 0;
|
|
13135
14339
|
this.boundUrl = void 0;
|
|
13136
14340
|
this.boundHost = void 0;
|
|
13137
|
-
if (!
|
|
13138
|
-
|
|
13139
|
-
await new Promise((
|
|
14341
|
+
if (!http2) return;
|
|
14342
|
+
http2.closeAllConnections();
|
|
14343
|
+
await new Promise((resolve9) => http2.close(() => resolve9()));
|
|
13140
14344
|
}
|
|
13141
14345
|
async issue(grant) {
|
|
13142
14346
|
const url = await this.start();
|
|
@@ -13145,29 +14349,29 @@ var OrdewellMcpServer = class {
|
|
|
13145
14349
|
return { url, token };
|
|
13146
14350
|
}
|
|
13147
14351
|
start() {
|
|
13148
|
-
this.listening ??= new Promise((
|
|
13149
|
-
const
|
|
14352
|
+
this.listening ??= new Promise((resolve9, reject) => {
|
|
14353
|
+
const http2 = (0, import_http.createServer)((req, res) => {
|
|
13150
14354
|
this.handle(req, res).catch(() => {
|
|
13151
14355
|
if (!res.headersSent) reply(res, 500, "Internal error");
|
|
13152
14356
|
else res.destroy();
|
|
13153
14357
|
});
|
|
13154
14358
|
});
|
|
13155
|
-
|
|
14359
|
+
http2.once("error", (err) => {
|
|
13156
14360
|
this.listening = void 0;
|
|
13157
14361
|
reject(err);
|
|
13158
14362
|
});
|
|
13159
|
-
|
|
13160
|
-
const address =
|
|
14363
|
+
http2.listen(0, "127.0.0.1", () => {
|
|
14364
|
+
const address = http2.address();
|
|
13161
14365
|
if (address === null || typeof address === "string") {
|
|
13162
14366
|
reject(new Error("Ordewell MCP server bound to no TCP port"));
|
|
13163
14367
|
return;
|
|
13164
14368
|
}
|
|
13165
14369
|
this.boundHost = `127.0.0.1:${address.port}`;
|
|
13166
14370
|
this.boundUrl = `http://${this.boundHost}${ORDEWELL_MCP_PATH}`;
|
|
13167
|
-
|
|
14371
|
+
resolve9(this.boundUrl);
|
|
13168
14372
|
});
|
|
13169
|
-
|
|
13170
|
-
this.http =
|
|
14373
|
+
http2.unref();
|
|
14374
|
+
this.http = http2;
|
|
13171
14375
|
});
|
|
13172
14376
|
return this.listening;
|
|
13173
14377
|
}
|
|
@@ -13188,7 +14392,7 @@ var OrdewellMcpServer = class {
|
|
|
13188
14392
|
}
|
|
13189
14393
|
const mcp = new import_server.Server({ name: "ordewell", version: "1" }, { capabilities: { tools: {} } });
|
|
13190
14394
|
if (grant.role === "task") serveTools(mcp, TASK_TOOLS, grant.handler, grant.revoked.signal, this.heartbeatMs);
|
|
13191
|
-
else serveTools(mcp, PLANNER_TOOLS, grant.handler, grant.revoked.signal, this.heartbeatMs);
|
|
14395
|
+
else serveTools(mcp, PLANNER_TOOLS.filter((tool2) => !grant.scope.hidden?.includes(tool2.name)), grant.handler, grant.revoked.signal, this.heartbeatMs);
|
|
13192
14396
|
const transport = new import_streamableHttp.StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
13193
14397
|
res.on("close", () => {
|
|
13194
14398
|
void transport.close();
|
|
@@ -13239,14 +14443,15 @@ function mcpClientConfig(credential) {
|
|
|
13239
14443
|
return {
|
|
13240
14444
|
name: ORDEWELL_MCP_SERVER_NAME,
|
|
13241
14445
|
url: credential.url,
|
|
13242
|
-
headers: { Authorization: bearerHeaderValue(credential.token) }
|
|
14446
|
+
headers: { Authorization: bearerHeaderValue(credential.token) },
|
|
14447
|
+
...credential.hidden?.length ? { hidden: credential.hidden } : {}
|
|
13243
14448
|
};
|
|
13244
14449
|
}
|
|
13245
14450
|
function ownerOnlyConfigFile(fileName, contents) {
|
|
13246
14451
|
const dir = (0, import_fs3.mkdtempSync)((0, import_path3.join)((0, import_os.tmpdir)(), "ordewell-mcp-"));
|
|
13247
|
-
const
|
|
13248
|
-
(0, import_fs3.writeFileSync)(
|
|
13249
|
-
return { path:
|
|
14452
|
+
const path32 = (0, import_path3.join)(dir, fileName);
|
|
14453
|
+
(0, import_fs3.writeFileSync)(path32, contents, { mode: 384 });
|
|
14454
|
+
return { path: path32, remove: () => (0, import_fs3.rmSync)(dir, { recursive: true, force: true }) };
|
|
13250
14455
|
}
|
|
13251
14456
|
|
|
13252
14457
|
// src/services/harness/ordewellBinding.ts
|
|
@@ -13256,13 +14461,13 @@ var McpAttachError = class extends Error {
|
|
|
13256
14461
|
this.name = "McpAttachError";
|
|
13257
14462
|
}
|
|
13258
14463
|
};
|
|
13259
|
-
function ordewellToolNames(role) {
|
|
13260
|
-
return (role === "task" ? TASK_TOOLS : PLANNER_TOOLS).map((tool2) => tool2.name);
|
|
14464
|
+
function ordewellToolNames(role, hidden = []) {
|
|
14465
|
+
return (role === "task" ? TASK_TOOLS : PLANNER_TOOLS).map((tool2) => tool2.name).filter((tool2) => !hidden.includes(tool2));
|
|
13261
14466
|
}
|
|
13262
14467
|
function prefixedToolNames(prefix) {
|
|
13263
14468
|
return {
|
|
13264
14469
|
toolName: (tool2) => `${prefix}${tool2}`,
|
|
13265
|
-
toolNames: (role) => ordewellToolNames(role).map((tool2) => `${prefix}${tool2}`),
|
|
14470
|
+
toolNames: (role, hidden) => ordewellToolNames(role, hidden).map((tool2) => `${prefix}${tool2}`),
|
|
13266
14471
|
hasPrefix: (name) => name.startsWith(prefix)
|
|
13267
14472
|
};
|
|
13268
14473
|
}
|
|
@@ -13278,8 +14483,8 @@ async function awaitAttach(probe, timeoutMs, pollMs) {
|
|
|
13278
14483
|
const state = await probe(left);
|
|
13279
14484
|
if (state !== "pending") return state === "connected";
|
|
13280
14485
|
if (pollMs > 0) {
|
|
13281
|
-
await new Promise((
|
|
13282
|
-
const timer = setTimeout(
|
|
14486
|
+
await new Promise((resolve9) => {
|
|
14487
|
+
const timer = setTimeout(resolve9, pollMs);
|
|
13283
14488
|
timer.unref?.();
|
|
13284
14489
|
});
|
|
13285
14490
|
}
|
|
@@ -13287,7 +14492,7 @@ async function awaitAttach(probe, timeoutMs, pollMs) {
|
|
|
13287
14492
|
}
|
|
13288
14493
|
|
|
13289
14494
|
// src/services/TaskOrchestrator.ts
|
|
13290
|
-
var
|
|
14495
|
+
var path24 = __toESM(require("path"));
|
|
13291
14496
|
|
|
13292
14497
|
// src/services/taskSkills.ts
|
|
13293
14498
|
var path15 = __toESM(require("path"));
|
|
@@ -13656,11 +14861,11 @@ async function committedInHead(files) {
|
|
|
13656
14861
|
}
|
|
13657
14862
|
const result = /* @__PURE__ */ new Map();
|
|
13658
14863
|
await Promise.all([...byDir].map(async ([dir, inDir]) => {
|
|
13659
|
-
const
|
|
14864
|
+
const relative11 = (file) => path15.relative(dir, file).split(path15.sep).join("/");
|
|
13660
14865
|
try {
|
|
13661
|
-
const { stdout } = await runGit("git", ["ls-tree", "--name-only", "HEAD", "--", ...inDir.map(
|
|
14866
|
+
const { stdout } = await runGit("git", ["ls-tree", "--name-only", "HEAD", "--", ...inDir.map(relative11)], { cwd: dir, env: cleanEnv() });
|
|
13662
14867
|
const listed2 = new Set(stdout.split("\n").map((line) => line.trim()).filter(Boolean));
|
|
13663
|
-
for (const file of inDir) result.set(file, listed2.has(
|
|
14868
|
+
for (const file of inDir) result.set(file, listed2.has(relative11(file)));
|
|
13664
14869
|
} catch {
|
|
13665
14870
|
}
|
|
13666
14871
|
}));
|
|
@@ -13691,13 +14896,13 @@ function checkPlanSkills(tasks, lookup2) {
|
|
|
13691
14896
|
function checkTaskSkillsEdit(task, skills, lookup2) {
|
|
13692
14897
|
return check([{ taskId: task.id, owner: `Task "${task.title}"`, ...attached(skills) }], lookup2);
|
|
13693
14898
|
}
|
|
13694
|
-
function subtaskEntries(owner, spec,
|
|
14899
|
+
function subtaskEntries(owner, spec, path32 = []) {
|
|
13695
14900
|
const subtasks = spec?.subtasks;
|
|
13696
14901
|
if (!Array.isArray(subtasks)) return [];
|
|
13697
14902
|
return subtasks.flatMap((sub, i) => {
|
|
13698
14903
|
if (typeof sub !== "object" || sub === null) return [];
|
|
13699
14904
|
const child = sub;
|
|
13700
|
-
const at = [...
|
|
14905
|
+
const at = [...path32, typeof child.title === "string" && child.title.trim() ? `"${child.title}"` : `#${i + 1}`];
|
|
13701
14906
|
return [{ owner: `${owner}: subtask ${at.join(" > ")}`, ...attached(child.skills) }, ...subtaskEntries(owner, child, at)];
|
|
13702
14907
|
});
|
|
13703
14908
|
}
|
|
@@ -13928,7 +15133,7 @@ ${reminder.join("\n\n")}${COMPLETION_INSTRUCTION}`;
|
|
|
13928
15133
|
}
|
|
13929
15134
|
|
|
13930
15135
|
// src/services/VerdictEngine.ts
|
|
13931
|
-
var IDLE_TIMEOUT_MS = 6e4;
|
|
15136
|
+
var IDLE_TIMEOUT_MS = 5 * 6e4;
|
|
13932
15137
|
var VerdictEngine = class {
|
|
13933
15138
|
pendingVerdicts = /* @__PURE__ */ new Map();
|
|
13934
15139
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -14110,14 +15315,14 @@ var VerdictEngine = class {
|
|
|
14110
15315
|
if (this.toolCheckpoints.has(taskId)) {
|
|
14111
15316
|
return Promise.resolve({ kind: "withdrawn", why: "another checkpoint is still waiting for an answer. Wait for it before asking again." });
|
|
14112
15317
|
}
|
|
14113
|
-
return new Promise((
|
|
15318
|
+
return new Promise((resolve9) => {
|
|
14114
15319
|
const settle = (answer2) => {
|
|
14115
15320
|
signal.removeEventListener("abort", onAbort);
|
|
14116
15321
|
if (this.toolCheckpoints.get(taskId) === settle) {
|
|
14117
15322
|
this.toolCheckpoints.delete(taskId);
|
|
14118
15323
|
this.checkpointQuestions.delete(taskId);
|
|
14119
15324
|
}
|
|
14120
|
-
|
|
15325
|
+
resolve9(answer2);
|
|
14121
15326
|
};
|
|
14122
15327
|
const onAbort = () => {
|
|
14123
15328
|
settle({ kind: "withdrawn", why: "the call was cancelled." });
|
|
@@ -15576,6 +16781,7 @@ var SETTINGS_ENV_REFUSED = [
|
|
|
15576
16781
|
"ORDEWELL_APPROVAL_MODE",
|
|
15577
16782
|
"ORDEWELL_APPROVAL_ALLOW",
|
|
15578
16783
|
"ORDEWELL_APPROVAL_DEFAULTS",
|
|
16784
|
+
"ORDEWELL_WEB_READ_BUDGET",
|
|
15579
16785
|
// Where settings are persisted.
|
|
15580
16786
|
"ORDEWELL_SETTINGS_PATH",
|
|
15581
16787
|
// Runtime options and dynamic loaders.
|
|
@@ -15627,10 +16833,10 @@ var DIRENV_TIMEOUT_MS = 5e3;
|
|
|
15627
16833
|
var KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
15628
16834
|
function defaultDeps() {
|
|
15629
16835
|
return {
|
|
15630
|
-
run: (command, args, opts) => new Promise((
|
|
16836
|
+
run: (command, args, opts) => new Promise((resolve9) => {
|
|
15631
16837
|
(0, import_child_process4.execFile)(command, args, { ...opts, timeout: DIRENV_TIMEOUT_MS, encoding: "utf8" }, (err, stdout, stderr) => {
|
|
15632
16838
|
const code = err ? typeof err.code === "number" ? Number(err.code) : -1 : 0;
|
|
15633
|
-
|
|
16839
|
+
resolve9({ code, stdout: String(stdout ?? ""), stderr: String(stderr ?? "") });
|
|
15634
16840
|
});
|
|
15635
16841
|
}),
|
|
15636
16842
|
readFile: (file) => {
|
|
@@ -15712,10 +16918,10 @@ async function fromDirenv(cwd, deps) {
|
|
|
15712
16918
|
}
|
|
15713
16919
|
return { env, blocked: null };
|
|
15714
16920
|
}
|
|
15715
|
-
function findUp(start,
|
|
16921
|
+
function findUp(start, relative11, deps) {
|
|
15716
16922
|
let dir = path16.resolve(start);
|
|
15717
16923
|
for (; ; ) {
|
|
15718
|
-
const candidate = path16.join(dir,
|
|
16924
|
+
const candidate = path16.join(dir, relative11);
|
|
15719
16925
|
if (deps.readFile(candidate) !== null) return candidate;
|
|
15720
16926
|
const parent = path16.dirname(dir);
|
|
15721
16927
|
if (parent === dir) return null;
|
|
@@ -16174,6 +17380,7 @@ function killTree(proc, deps = {}) {
|
|
|
16174
17380
|
// src/services/harness/openCodeOrdewell.ts
|
|
16175
17381
|
var NAMES = prefixedToolNames(`${ORDEWELL_MCP_SERVER_NAME}_`);
|
|
16176
17382
|
var RULE = NAMES.toolName("*");
|
|
17383
|
+
var OPENCODE_SEARCH_OFF = { websearch: "deny", codesearch: "deny" };
|
|
16177
17384
|
var OPENCODE_PLANNER_PERMISSION = {
|
|
16178
17385
|
"*": "ask",
|
|
16179
17386
|
read: "allow",
|
|
@@ -16183,10 +17390,10 @@ var OPENCODE_PLANNER_PERMISSION = {
|
|
|
16183
17390
|
lsp: "allow",
|
|
16184
17391
|
skill: "allow",
|
|
16185
17392
|
todoread: "allow",
|
|
16186
|
-
websearch: "
|
|
16187
|
-
codesearch: "
|
|
16188
|
-
webfetch: "ask",
|
|
17393
|
+
websearch: "ask",
|
|
17394
|
+
codesearch: "ask",
|
|
16189
17395
|
external_directory: "ask",
|
|
17396
|
+
webfetch: "deny",
|
|
16190
17397
|
edit: "deny",
|
|
16191
17398
|
write: "deny",
|
|
16192
17399
|
patch: "deny",
|
|
@@ -16201,6 +17408,8 @@ var OPENCODE_PLANNER_PERMISSION = {
|
|
|
16201
17408
|
browser: "deny"
|
|
16202
17409
|
};
|
|
16203
17410
|
var OPENCODE_CONFIG_VARIABLE = "OPENCODE_CONFIG_CONTENT";
|
|
17411
|
+
var OPENCODE_PLANNER_ENV = { OPENCODE_DISABLE_PROJECT_CONFIG: "1" };
|
|
17412
|
+
var OPENCODE_SEARCH_PROVIDER = { provider: "opencode", providerLabel: "OpenCode's web search (Exa, unless another search provider is chosen in OpenCode)" };
|
|
16204
17413
|
function isRecord2(value) {
|
|
16205
17414
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16206
17415
|
}
|
|
@@ -16237,7 +17446,7 @@ function withoutParentOrdewell(content) {
|
|
|
16237
17446
|
const kept = withoutOrdewell(parsed);
|
|
16238
17447
|
return JSON.stringify(kept) === JSON.stringify(parsed) ? content : JSON.stringify(kept);
|
|
16239
17448
|
}
|
|
16240
|
-
function mergeOrdewellConfig(existing, mcp, role) {
|
|
17449
|
+
function mergeOrdewellConfig(existing, mcp, role, projectServers = {}) {
|
|
16241
17450
|
let base = {};
|
|
16242
17451
|
if (existing?.trim()) {
|
|
16243
17452
|
let parsed;
|
|
@@ -16254,8 +17463,15 @@ function mergeOrdewellConfig(existing, mcp, role) {
|
|
|
16254
17463
|
delete base.permissions;
|
|
16255
17464
|
}
|
|
16256
17465
|
const policy = typeof base.permission === "string" ? { "*": base.permission } : base.permission;
|
|
17466
|
+
const servers = {
|
|
17467
|
+
...role === "planner" ? projectServers : {},
|
|
17468
|
+
...mcp ? { [mcp.name]: { type: "remote", url: mcp.url, headers: mcp.headers, enabled: true } } : {}
|
|
17469
|
+
};
|
|
16257
17470
|
const ours = {
|
|
16258
|
-
...
|
|
17471
|
+
...Object.keys(servers).length > 0 ? { mcp: servers } : {},
|
|
17472
|
+
// A language server opened on a file the planner reads can run the
|
|
17473
|
+
// repository's toolchain; 1.x's `read` opens every file it reads (ADR-0028).
|
|
17474
|
+
...role === "planner" ? { lsp: false } : {},
|
|
16259
17475
|
permission: { ...role === "planner" ? OPENCODE_PLANNER_PERMISSION : {}, ...mcp ? { [RULE]: "allow" } : {} }
|
|
16260
17476
|
};
|
|
16261
17477
|
return JSON.stringify(deepMerge({ ...base, ...policy === void 0 ? {} : { permission: policy } }, ours));
|
|
@@ -16263,12 +17479,17 @@ function mergeOrdewellConfig(existing, mcp, role) {
|
|
|
16263
17479
|
var OPENCODE_ORDEWELL = {
|
|
16264
17480
|
toolName: NAMES.toolName,
|
|
16265
17481
|
toolNames: NAMES.toolNames,
|
|
17482
|
+
plannerHidden: () => [],
|
|
16266
17483
|
/** A permission request's `permission`, which for a tool is its name. */
|
|
16267
17484
|
isOrdewellAsk: (permission) => NAMES.hasPrefix(permission),
|
|
16268
17485
|
/** `/mcp` lists a server only once it has started connecting, as `pending` until it settles. */
|
|
16269
17486
|
attachState: (status) => status === "connected" ? "connected" : status === void 0 || status === null || status === "pending" ? "pending" : "failed",
|
|
16270
17487
|
configContent: mergeOrdewellConfig,
|
|
16271
|
-
sessionRules: () => [{ action: RULE, resource: "*", effect: "allow" }]
|
|
17488
|
+
sessionRules: () => [{ action: RULE, resource: "*", effect: "allow" }],
|
|
17489
|
+
plannerInstructions: (ownSearch = true) => [
|
|
17490
|
+
`Read web pages with \`${NAMES.toolName("fetch")}\`. Your own \`webfetch\` is off while planning: it follows redirects Ordewell cannot check.`,
|
|
17491
|
+
ownSearch ? `Search the web with your own \`websearch\` first. If \`websearch\` fails, is cancelled or is unavailable for your provider, search with \`${NAMES.toolName("web_search")}\` instead.` : `Your own \`websearch\` is off for this session: no search provider is chosen in OpenCode. Search the web with \`${NAMES.toolName("web_search")}\`.`
|
|
17492
|
+
].join(" ")
|
|
16272
17493
|
};
|
|
16273
17494
|
|
|
16274
17495
|
// src/services/harness/runnerEnv.ts
|
|
@@ -16296,8 +17517,8 @@ var STDERR_TAIL_CHARS = 4e3;
|
|
|
16296
17517
|
var RunnerProcess = class {
|
|
16297
17518
|
constructor(deps) {
|
|
16298
17519
|
this.deps = deps;
|
|
16299
|
-
this.ended = new Promise((
|
|
16300
|
-
this.markEnded =
|
|
17520
|
+
this.ended = new Promise((resolve9) => {
|
|
17521
|
+
this.markEnded = resolve9;
|
|
16301
17522
|
});
|
|
16302
17523
|
}
|
|
16303
17524
|
deps;
|
|
@@ -16326,9 +17547,10 @@ var RunnerProcess = class {
|
|
|
16326
17547
|
throw err;
|
|
16327
17548
|
}
|
|
16328
17549
|
}
|
|
16329
|
-
async launch(cwd,
|
|
17550
|
+
async launch(cwd, describe2, handshake) {
|
|
16330
17551
|
assertWorkspaceExists(cwd, { isDirectory: this.deps.isDirectory });
|
|
16331
|
-
const
|
|
17552
|
+
const workspace = await (this.deps.workspaceEnv ?? workspaceEnvOf)(cwd);
|
|
17553
|
+
const command = describe2(workspace);
|
|
16332
17554
|
const resolvePath = this.deps.resolvePath ?? augmentedPath;
|
|
16333
17555
|
const PATH = await resolvePath();
|
|
16334
17556
|
const launch = await planDirectLaunch(command.command, command.args, {
|
|
@@ -16338,7 +17560,6 @@ var RunnerProcess = class {
|
|
|
16338
17560
|
if (!isExecutableResolved(command.command, launch, PATH, { platform: this.deps.platform, exists: this.deps.exists })) {
|
|
16339
17561
|
throw new ExecutableNotFoundError(command.command, PATH);
|
|
16340
17562
|
}
|
|
16341
|
-
const workspace = await (this.deps.workspaceEnv ?? workspaceEnvOf)(cwd);
|
|
16342
17563
|
this.spawnEnv = runnerEnv(PATH, workspace, command.env?.(workspace));
|
|
16343
17564
|
const child = spawnInOwnGroup((detached) => this.deps.spawn(launch.file, launch.args, {
|
|
16344
17565
|
env: this.spawnEnv,
|
|
@@ -16461,8 +17682,8 @@ var StdioAgentAdapter = class {
|
|
|
16461
17682
|
async start(opts) {
|
|
16462
17683
|
this.role = opts.kind;
|
|
16463
17684
|
try {
|
|
16464
|
-
await this.runner.start(opts.cwd, () => {
|
|
16465
|
-
const spec = this.spawnSpec(opts);
|
|
17685
|
+
await this.runner.start(opts.cwd, (workspace) => {
|
|
17686
|
+
const spec = this.spawnSpec(opts, workspace);
|
|
16466
17687
|
return { command: spec.command, args: spec.args, env: () => spec.env ?? {} };
|
|
16467
17688
|
}, (child) => {
|
|
16468
17689
|
this.process = child;
|
|
@@ -16506,7 +17727,7 @@ var StdioAgentAdapter = class {
|
|
|
16506
17727
|
onEvent({ type: "error", message: this.exitMessage() });
|
|
16507
17728
|
return;
|
|
16508
17729
|
}
|
|
16509
|
-
await new Promise((
|
|
17730
|
+
await new Promise((resolve9) => {
|
|
16510
17731
|
let settled = false;
|
|
16511
17732
|
const finish = () => {
|
|
16512
17733
|
if (settled) return;
|
|
@@ -16517,7 +17738,7 @@ var StdioAgentAdapter = class {
|
|
|
16517
17738
|
signal?.removeEventListener("abort", onAbort);
|
|
16518
17739
|
this.process?.removeListener("exit", onExit);
|
|
16519
17740
|
this.process?.removeListener("error", onExit);
|
|
16520
|
-
|
|
17741
|
+
resolve9();
|
|
16521
17742
|
};
|
|
16522
17743
|
const emit = (event) => {
|
|
16523
17744
|
onEvent(event);
|
|
@@ -16644,6 +17865,8 @@ ${lines2.map((line) => `${line}
|
|
|
16644
17865
|
var CLAUDE_ORDEWELL = {
|
|
16645
17866
|
toolName: MCP_CLIENT_TOOL_NAMES.toolName,
|
|
16646
17867
|
toolNames: MCP_CLIENT_TOOL_NAMES.toolNames,
|
|
17868
|
+
/** ADR-0027: `WebSearch` and `WebFetch` carry Claude's web reads, both asked through the policy. */
|
|
17869
|
+
plannerHidden: () => ["fetch", "web_search"],
|
|
16647
17870
|
/** `can_use_tool`'s `tool_name`. */
|
|
16648
17871
|
isOrdewellAsk: (toolName) => MCP_CLIENT_TOOL_NAMES.hasPrefix(toolName),
|
|
16649
17872
|
/** `mcp_status` reports `pending` until the CLI's connection attempt settles. */
|
|
@@ -16655,24 +17878,24 @@ var CLAUDE_ORDEWELL = {
|
|
|
16655
17878
|
* search, and a model that has to look an Ordewell tool up first tends to
|
|
16656
17879
|
* go on without it.
|
|
16657
17880
|
*/
|
|
16658
|
-
launch(mcp, role) {
|
|
17881
|
+
launch(mcp, role, servers = {}) {
|
|
16659
17882
|
const config = ownerOnlyConfigFile("mcp.json", JSON.stringify({
|
|
16660
|
-
mcpServers: { [mcp.name]: { type: "http", url: mcp.url, headers: mcp.headers, alwaysLoad: true } }
|
|
17883
|
+
mcpServers: { ...servers, [mcp.name]: { type: "http", url: mcp.url, headers: mcp.headers, alwaysLoad: true } }
|
|
16661
17884
|
}));
|
|
16662
|
-
return { args: ["--mcp-config", config.path, "--allowedTools", MCP_CLIENT_TOOL_NAMES.toolNames(role).join(",")], config };
|
|
17885
|
+
return { args: ["--mcp-config", config.path, "--allowedTools", MCP_CLIENT_TOOL_NAMES.toolNames(role, mcp.hidden).join(",")], config };
|
|
16663
17886
|
}
|
|
16664
17887
|
};
|
|
16665
17888
|
|
|
16666
17889
|
// src/services/harness/settleWithin.ts
|
|
16667
17890
|
function settleWithin(promise, wait) {
|
|
16668
|
-
return new Promise((
|
|
17891
|
+
return new Promise((resolve9, reject) => {
|
|
16669
17892
|
let settled = false;
|
|
16670
17893
|
const settle = (outcome) => {
|
|
16671
17894
|
if (settled) return;
|
|
16672
17895
|
settled = true;
|
|
16673
17896
|
clearTimeout(timer);
|
|
16674
17897
|
try {
|
|
16675
|
-
|
|
17898
|
+
resolve9(outcome());
|
|
16676
17899
|
} catch (err) {
|
|
16677
17900
|
reject(err);
|
|
16678
17901
|
}
|
|
@@ -16689,6 +17912,180 @@ function settleWithin(promise, wait) {
|
|
|
16689
17912
|
});
|
|
16690
17913
|
}
|
|
16691
17914
|
|
|
17915
|
+
// src/services/harness/claudeRepoConfig.ts
|
|
17916
|
+
var os6 = __toESM(require("os"));
|
|
17917
|
+
var path20 = __toESM(require("path"));
|
|
17918
|
+
|
|
17919
|
+
// src/services/harness/repoConfig.ts
|
|
17920
|
+
var import_fs4 = require("fs");
|
|
17921
|
+
var path19 = __toESM(require("path"));
|
|
17922
|
+
var NO_SERVERS = { servers: [], unreadable: [] };
|
|
17923
|
+
var INSTRUCTIONS_MAX_CHARS = 4e4;
|
|
17924
|
+
function isRecord3(value) {
|
|
17925
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17926
|
+
}
|
|
17927
|
+
function readText(file) {
|
|
17928
|
+
try {
|
|
17929
|
+
return (0, import_fs4.statSync)(file).isFile() ? (0, import_fs4.readFileSync)(file, "utf8") : void 0;
|
|
17930
|
+
} catch {
|
|
17931
|
+
return void 0;
|
|
17932
|
+
}
|
|
17933
|
+
}
|
|
17934
|
+
function onDisk(target) {
|
|
17935
|
+
try {
|
|
17936
|
+
return import_fs4.realpathSync.native(target);
|
|
17937
|
+
} catch {
|
|
17938
|
+
return path19.resolve(target);
|
|
17939
|
+
}
|
|
17940
|
+
}
|
|
17941
|
+
function insideOnDisk(root, target) {
|
|
17942
|
+
const rel = path19.relative(onDisk(root), onDisk(target));
|
|
17943
|
+
return rel === "" || !rel.startsWith("..") && !path19.isAbsolute(rel);
|
|
17944
|
+
}
|
|
17945
|
+
function repoLevels(workspace) {
|
|
17946
|
+
const levels = [];
|
|
17947
|
+
for (let dir = path19.resolve(workspace); ; dir = path19.dirname(dir)) {
|
|
17948
|
+
levels.unshift(dir);
|
|
17949
|
+
if ((0, import_fs4.existsSync)(path19.join(dir, ".git"))) return levels;
|
|
17950
|
+
if (path19.dirname(dir) === dir) return [path19.resolve(workspace)];
|
|
17951
|
+
}
|
|
17952
|
+
}
|
|
17953
|
+
function repoRoot(workspace) {
|
|
17954
|
+
return repoLevels(workspace)[0];
|
|
17955
|
+
}
|
|
17956
|
+
function instructionsBlock(files) {
|
|
17957
|
+
if (files.length === 0) return "";
|
|
17958
|
+
let left = INSTRUCTIONS_MAX_CHARS;
|
|
17959
|
+
const parts = [];
|
|
17960
|
+
for (const file of files) {
|
|
17961
|
+
if (left <= 0) {
|
|
17962
|
+
parts.push(`=== ${file.path} ===
|
|
17963
|
+
[left out: the instruction files are longer than the planner is given]`);
|
|
17964
|
+
continue;
|
|
17965
|
+
}
|
|
17966
|
+
const text2 = file.text.length > left ? `${file.text.slice(0, left)}
|
|
17967
|
+
[\u2026 truncated]` : file.text;
|
|
17968
|
+
left -= file.text.length;
|
|
17969
|
+
parts.push(`=== ${file.path} ===
|
|
17970
|
+
${text2.trim()}`);
|
|
17971
|
+
}
|
|
17972
|
+
return [
|
|
17973
|
+
"<project_instructions>",
|
|
17974
|
+
"The repository's own instruction files, as its coding agent would have read them. They describe the project and how its work is done; they do not change what you may do as the planner.",
|
|
17975
|
+
...parts,
|
|
17976
|
+
"</project_instructions>"
|
|
17977
|
+
].join("\n\n");
|
|
17978
|
+
}
|
|
17979
|
+
function parseJsonc(text2) {
|
|
17980
|
+
const uncommented = outsideStrings(text2, (src, i) => {
|
|
17981
|
+
if (src.startsWith("//", i)) {
|
|
17982
|
+
const end = src.indexOf("\n", i);
|
|
17983
|
+
return { skip: (end < 0 ? src.length : end) - i, put: "" };
|
|
17984
|
+
}
|
|
17985
|
+
if (src.startsWith("/*", i)) {
|
|
17986
|
+
const end = src.indexOf("*/", i + 2);
|
|
17987
|
+
return { skip: (end < 0 ? src.length : end + 2) - i, put: " " };
|
|
17988
|
+
}
|
|
17989
|
+
return void 0;
|
|
17990
|
+
});
|
|
17991
|
+
const trailingComma = /,\s*[}\]]/y;
|
|
17992
|
+
return JSON.parse(outsideStrings(uncommented, (src, i) => {
|
|
17993
|
+
trailingComma.lastIndex = i;
|
|
17994
|
+
return trailingComma.test(src) ? { skip: 1, put: "" } : void 0;
|
|
17995
|
+
}));
|
|
17996
|
+
}
|
|
17997
|
+
function outsideStrings(text2, edit) {
|
|
17998
|
+
let out = "";
|
|
17999
|
+
for (let i = 0; i < text2.length; ) {
|
|
18000
|
+
if (text2[i] === '"') {
|
|
18001
|
+
let j = i + 1;
|
|
18002
|
+
while (j < text2.length && text2[j] !== '"') j += text2[j] === "\\" ? 2 : 1;
|
|
18003
|
+
out += text2.slice(i, j + 1);
|
|
18004
|
+
i = j + 1;
|
|
18005
|
+
continue;
|
|
18006
|
+
}
|
|
18007
|
+
const change = edit(text2, i);
|
|
18008
|
+
if (change) {
|
|
18009
|
+
out += change.put;
|
|
18010
|
+
i += change.skip;
|
|
18011
|
+
} else {
|
|
18012
|
+
out += text2[i++];
|
|
18013
|
+
}
|
|
18014
|
+
}
|
|
18015
|
+
return out;
|
|
18016
|
+
}
|
|
18017
|
+
|
|
18018
|
+
// src/services/harness/claudeRepoConfig.ts
|
|
18019
|
+
var MEMORY_FILES = ["CLAUDE.md", path20.join(".claude", "CLAUDE.md"), "CLAUDE.local.md"];
|
|
18020
|
+
var MAX_IMPORT_DEPTH = 5;
|
|
18021
|
+
function importsOf(text2) {
|
|
18022
|
+
const prose = text2.replace(/```[\s\S]*?```/g, "").replace(/`[^`\n]*`/g, "");
|
|
18023
|
+
return [...prose.matchAll(/(?:^|\s)@([^\s`]+)/g)].map((m) => m[1].replace(/[.,;:)]+$/, ""));
|
|
18024
|
+
}
|
|
18025
|
+
function withImports(workspace, file, depth, seen, into) {
|
|
18026
|
+
if (seen.has(file)) return;
|
|
18027
|
+
seen.add(file);
|
|
18028
|
+
const text2 = readText(file);
|
|
18029
|
+
if (text2 === void 0) return;
|
|
18030
|
+
into.push({ path: path20.relative(workspace, file), text: text2 });
|
|
18031
|
+
if (depth >= MAX_IMPORT_DEPTH) return;
|
|
18032
|
+
for (const target of importsOf(text2)) {
|
|
18033
|
+
if (target.startsWith("~")) continue;
|
|
18034
|
+
const abs = path20.resolve(path20.dirname(file), target);
|
|
18035
|
+
if (insideOnDisk(workspace, abs)) withImports(workspace, abs, depth + 1, seen, into);
|
|
18036
|
+
}
|
|
18037
|
+
}
|
|
18038
|
+
function server(name, entry, source) {
|
|
18039
|
+
if (!isRecord3(entry)) return "not an object";
|
|
18040
|
+
const remote = entry.type === "http" || entry.type === "sse" || entry.type === void 0 && typeof entry.url === "string";
|
|
18041
|
+
if (remote) {
|
|
18042
|
+
if (typeof entry.url !== "string") return "no url";
|
|
18043
|
+
return { name, source, transport: "remote", target: entry.url, names: isRecord3(entry.headers) ? Object.keys(entry.headers) : [], definition: entry };
|
|
18044
|
+
}
|
|
18045
|
+
if (typeof entry.command !== "string") return "no command";
|
|
18046
|
+
const args = Array.isArray(entry.args) ? entry.args.map(String) : [];
|
|
18047
|
+
return { name, source, transport: "stdio", target: [entry.command, ...args].join(" "), names: isRecord3(entry.env) ? Object.keys(entry.env) : [], definition: entry };
|
|
18048
|
+
}
|
|
18049
|
+
var CLAUDE_REPO_CONFIG = {
|
|
18050
|
+
instructions(workspace) {
|
|
18051
|
+
const files = [];
|
|
18052
|
+
const seen = /* @__PURE__ */ new Set();
|
|
18053
|
+
for (const name of MEMORY_FILES) withImports(workspace, path20.join(workspace, name), 0, seen, files);
|
|
18054
|
+
return files;
|
|
18055
|
+
},
|
|
18056
|
+
projectServers(workspace) {
|
|
18057
|
+
const source = ".mcp.json";
|
|
18058
|
+
const text2 = readText(path20.join(workspace, source));
|
|
18059
|
+
if (text2 === void 0) return NO_SERVERS;
|
|
18060
|
+
let parsed;
|
|
18061
|
+
try {
|
|
18062
|
+
parsed = JSON.parse(text2);
|
|
18063
|
+
} catch (err) {
|
|
18064
|
+
return { servers: [], unreadable: [{ source, reason: err instanceof Error ? err.message : String(err) }] };
|
|
18065
|
+
}
|
|
18066
|
+
const entries = isRecord3(parsed) && isRecord3(parsed.mcpServers) ? parsed.mcpServers : {};
|
|
18067
|
+
const found = { servers: [], unreadable: [] };
|
|
18068
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
18069
|
+
const read2 = server(name, entry, source);
|
|
18070
|
+
if (typeof read2 === "string") found.unreadable.push({ source: `${source} (${name})`, reason: read2 });
|
|
18071
|
+
else found.servers.push(read2);
|
|
18072
|
+
}
|
|
18073
|
+
return found;
|
|
18074
|
+
}
|
|
18075
|
+
};
|
|
18076
|
+
function claudeLocalServers(configDir2, workspace) {
|
|
18077
|
+
const file = configDir2 ? path20.join(configDir2, ".claude.json") : path20.join(os6.homedir(), ".claude.json");
|
|
18078
|
+
const text2 = readText(file);
|
|
18079
|
+
if (text2 === void 0) return {};
|
|
18080
|
+
try {
|
|
18081
|
+
const parsed = JSON.parse(text2);
|
|
18082
|
+
const project = isRecord3(parsed) && isRecord3(parsed.projects) ? parsed.projects[path20.resolve(workspace)] : void 0;
|
|
18083
|
+
return isRecord3(project) && isRecord3(project.mcpServers) ? project.mcpServers : {};
|
|
18084
|
+
} catch {
|
|
18085
|
+
return {};
|
|
18086
|
+
}
|
|
18087
|
+
}
|
|
18088
|
+
|
|
16692
18089
|
// src/services/harness/ClaudeCodeAdapter.ts
|
|
16693
18090
|
var DISALLOWED_TOOLS = [
|
|
16694
18091
|
"Edit",
|
|
@@ -16702,20 +18099,25 @@ var DISALLOWED_TOOLS = [
|
|
|
16702
18099
|
"ExitPlanMode"
|
|
16703
18100
|
];
|
|
16704
18101
|
var PLANNER_PERMISSION_MODE = "default";
|
|
18102
|
+
var PLANNER_SETTINGS = JSON.stringify({
|
|
18103
|
+
disableAllHooks: true,
|
|
18104
|
+
permissions: { ask: ["WebFetch", "WebFetch(domain:*)", "WebSearch", "mcp__*", "Read", "Grep", "Glob", "LS"] }
|
|
18105
|
+
});
|
|
16705
18106
|
var PATH_TOOLS = {
|
|
16706
18107
|
Read: { field: "file_path", directory: false },
|
|
16707
18108
|
Grep: { field: "path", directory: true },
|
|
16708
18109
|
Glob: { field: "path", directory: true },
|
|
16709
18110
|
LS: { field: "path", directory: true }
|
|
16710
18111
|
};
|
|
16711
|
-
function plannerAsk(name, input,
|
|
18112
|
+
function plannerAsk(name, input, server4) {
|
|
16712
18113
|
if (name.startsWith("mcp__")) {
|
|
16713
18114
|
const tool2 = name.split("__").slice(2).join("__");
|
|
16714
|
-
return { kind: "mcp", scope: name, tool: tool2 || name, ...
|
|
18115
|
+
return { kind: "mcp", scope: name, tool: tool2 || name, ...server4 ? { server: server4 } : {} };
|
|
16715
18116
|
}
|
|
16716
18117
|
if (name === "WebFetch" && typeof input.url === "string") return { kind: "fetch", url: input.url };
|
|
18118
|
+
if (name === "WebSearch" && typeof input.query === "string") return { kind: "search", query: input.query, provider: "anthropic" };
|
|
16717
18119
|
const pathTool = PATH_TOOLS[name];
|
|
16718
|
-
const target = pathTool ? input[pathTool.field] : void 0;
|
|
18120
|
+
const target = pathTool ? input[pathTool.field] ?? (pathTool.directory ? "." : void 0) : void 0;
|
|
16719
18121
|
if (pathTool && typeof target === "string") return { kind: "path", path: target, directory: pathTool.directory };
|
|
16720
18122
|
return { kind: "other" };
|
|
16721
18123
|
}
|
|
@@ -16823,7 +18225,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
16823
18225
|
steers = /* @__PURE__ */ new Map();
|
|
16824
18226
|
/** The last turn's `result` ended it, so a steer's echo now opens a turn the CLI started for that message. */
|
|
16825
18227
|
turnClosed = false;
|
|
16826
|
-
spawnSpec(opts) {
|
|
18228
|
+
spawnSpec(opts, workspace) {
|
|
16827
18229
|
if (opts.kind === "task") return this.taskSpawnSpec(opts);
|
|
16828
18230
|
const args = [
|
|
16829
18231
|
...PROTOCOL_ARGS,
|
|
@@ -16834,10 +18236,16 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
16834
18236
|
PLANNER_PERMISSION_MODE,
|
|
16835
18237
|
"--disallowedTools",
|
|
16836
18238
|
DISALLOWED_TOOLS.join(","),
|
|
18239
|
+
// In `-p` a repository's settings apply trusted or not: their hooks,
|
|
18240
|
+
// `env` and helper commands run before the first turn (ADR-0028).
|
|
18241
|
+
"--setting-sources",
|
|
18242
|
+
"user",
|
|
18243
|
+
"--settings",
|
|
18244
|
+
PLANNER_SETTINGS,
|
|
16837
18245
|
"--append-system-prompt",
|
|
16838
18246
|
opts.systemPrompt
|
|
16839
18247
|
];
|
|
16840
|
-
if (opts.mcp) args.push(...this.ordewellServerArgs(opts.mcp, "planner"));
|
|
18248
|
+
if (opts.mcp) args.push(...this.ordewellServerArgs(opts.mcp, "planner", this.plannerServers(opts, workspace)));
|
|
16841
18249
|
if (opts.model) args.push("--model", opts.model);
|
|
16842
18250
|
if (opts.effort && opts.effort !== "adaptive") args.push("--effort", opts.effort);
|
|
16843
18251
|
if (opts.resumeSessionId) {
|
|
@@ -16875,10 +18283,23 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
16875
18283
|
if (opts.mcp) args.push(...this.ordewellServerArgs(opts.mcp, "task"));
|
|
16876
18284
|
return { command: "claude", args };
|
|
16877
18285
|
}
|
|
18286
|
+
/**
|
|
18287
|
+
* What user-only settings stop the CLI loading that is not the
|
|
18288
|
+
* repository's to choose (ADR-0028): the user's own local-scope servers,
|
|
18289
|
+
* under the `CLAUDE_CONFIG_DIR` the planner runs with, and the project
|
|
18290
|
+
* servers approved for it.
|
|
18291
|
+
*/
|
|
18292
|
+
plannerServers(opts, workspace) {
|
|
18293
|
+
const configDir2 = workspace.CLAUDE_CONFIG_DIR ?? process.env.CLAUDE_CONFIG_DIR;
|
|
18294
|
+
return {
|
|
18295
|
+
...claudeLocalServers(configDir2, opts.cwd),
|
|
18296
|
+
...Object.fromEntries((opts.projectServers ?? []).map((server4) => [server4.name, server4.definition]))
|
|
18297
|
+
};
|
|
18298
|
+
}
|
|
16878
18299
|
/** The file holding the token lives exactly as long as the process that reads it. */
|
|
16879
|
-
ordewellServerArgs(mcp, role) {
|
|
18300
|
+
ordewellServerArgs(mcp, role, servers) {
|
|
16880
18301
|
this.removeMcpConfig();
|
|
16881
|
-
const { args, config } = CLAUDE_ORDEWELL.launch(mcp, role);
|
|
18302
|
+
const { args, config } = CLAUDE_ORDEWELL.launch(mcp, role, servers);
|
|
16882
18303
|
this.mcpConfig = config;
|
|
16883
18304
|
void this.processEnded.then(() => config.remove());
|
|
16884
18305
|
this.givenOrdewell = true;
|
|
@@ -16927,8 +18348,8 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
16927
18348
|
if (!this.process) return Promise.resolve(null);
|
|
16928
18349
|
this.controlCount += 1;
|
|
16929
18350
|
const requestId = `ordewell-${request.subtype}-${this.controlCount}`;
|
|
16930
|
-
const answered = new Promise((
|
|
16931
|
-
this.pendingControl.set(requestId,
|
|
18351
|
+
const answered = new Promise((resolve9) => {
|
|
18352
|
+
this.pendingControl.set(requestId, resolve9);
|
|
16932
18353
|
});
|
|
16933
18354
|
this.writeLine({ type: "control_request", request_id: requestId, request });
|
|
16934
18355
|
return settleWithin(answered, { timeoutMs, ended: this.processEnded, onTimeout: () => null, onEnded: () => null }).finally(() => {
|
|
@@ -17259,14 +18680,14 @@ function codexSandboxUnavailableMessage(role = "planner") {
|
|
|
17259
18680
|
].join("\n");
|
|
17260
18681
|
}
|
|
17261
18682
|
function runProbe(deps, cwd, env, flags) {
|
|
17262
|
-
return new Promise((
|
|
18683
|
+
return new Promise((resolve9) => {
|
|
17263
18684
|
let output = "";
|
|
17264
18685
|
let settled = false;
|
|
17265
18686
|
const finish = (code) => {
|
|
17266
18687
|
if (settled) return;
|
|
17267
18688
|
settled = true;
|
|
17268
18689
|
clearTimeout(timer);
|
|
17269
|
-
|
|
18690
|
+
resolve9({ code, output });
|
|
17270
18691
|
};
|
|
17271
18692
|
const timer = setTimeout(() => finish(null), PROBE_TIMEOUT_MS);
|
|
17272
18693
|
timer.unref?.();
|
|
@@ -17281,27 +18702,306 @@ function runProbe(deps, cwd, env, flags) {
|
|
|
17281
18702
|
finish(null);
|
|
17282
18703
|
return;
|
|
17283
18704
|
}
|
|
17284
|
-
proc.stdout?.on("data", (chunk) => {
|
|
17285
|
-
output += chunk.toString();
|
|
17286
|
-
});
|
|
17287
|
-
proc.stderr?.on("data", (chunk) => {
|
|
17288
|
-
output += chunk.toString();
|
|
17289
|
-
});
|
|
17290
|
-
proc.on("error", () => finish(null));
|
|
17291
|
-
proc.on("exit", (code) => finish(code));
|
|
17292
|
-
});
|
|
18705
|
+
proc.stdout?.on("data", (chunk) => {
|
|
18706
|
+
output += chunk.toString();
|
|
18707
|
+
});
|
|
18708
|
+
proc.stderr?.on("data", (chunk) => {
|
|
18709
|
+
output += chunk.toString();
|
|
18710
|
+
});
|
|
18711
|
+
proc.on("error", () => finish(null));
|
|
18712
|
+
proc.on("exit", (code) => finish(code));
|
|
18713
|
+
});
|
|
18714
|
+
}
|
|
18715
|
+
|
|
18716
|
+
// src/services/harness/codexRepoConfig.ts
|
|
18717
|
+
var path21 = __toESM(require("path"));
|
|
18718
|
+
|
|
18719
|
+
// src/services/harness/toml.ts
|
|
18720
|
+
function parseToml(text2) {
|
|
18721
|
+
return new TomlReader(text2).document();
|
|
18722
|
+
}
|
|
18723
|
+
var TomlError = class extends Error {
|
|
18724
|
+
};
|
|
18725
|
+
var BARE_KEY = /[A-Za-z0-9_-]/;
|
|
18726
|
+
var ESCAPES = { b: "\b", t: " ", n: "\n", f: "\f", r: "\r", '"': '"', "\\": "\\", e: "\x1B" };
|
|
18727
|
+
var TomlReader = class {
|
|
18728
|
+
constructor(src) {
|
|
18729
|
+
this.src = src;
|
|
18730
|
+
}
|
|
18731
|
+
src;
|
|
18732
|
+
i = 0;
|
|
18733
|
+
document() {
|
|
18734
|
+
const root = {};
|
|
18735
|
+
let table = root;
|
|
18736
|
+
for (; ; ) {
|
|
18737
|
+
this.skipBlank(true);
|
|
18738
|
+
if (this.i >= this.src.length) return root;
|
|
18739
|
+
if (this.src.startsWith("[[", this.i)) {
|
|
18740
|
+
this.i += 2;
|
|
18741
|
+
const path32 = this.key();
|
|
18742
|
+
this.expect("]]");
|
|
18743
|
+
const parent = this.tableAt(root, path32.slice(0, -1));
|
|
18744
|
+
const last = path32[path32.length - 1];
|
|
18745
|
+
const list = parent[last] ?? (parent[last] = []);
|
|
18746
|
+
if (!Array.isArray(list)) throw this.fail(`${path32.join(".")} is not an array of tables`);
|
|
18747
|
+
table = {};
|
|
18748
|
+
list.push(table);
|
|
18749
|
+
} else if (this.src[this.i] === "[") {
|
|
18750
|
+
this.i += 1;
|
|
18751
|
+
const path32 = this.key();
|
|
18752
|
+
this.expect("]");
|
|
18753
|
+
table = this.tableAt(root, path32);
|
|
18754
|
+
} else {
|
|
18755
|
+
const path32 = this.key();
|
|
18756
|
+
this.skipBlank(false);
|
|
18757
|
+
this.expect("=");
|
|
18758
|
+
this.skipBlank(false);
|
|
18759
|
+
const owner = this.tableAt(table, path32.slice(0, -1));
|
|
18760
|
+
owner[path32[path32.length - 1]] = this.value();
|
|
18761
|
+
}
|
|
18762
|
+
this.endOfLine();
|
|
18763
|
+
}
|
|
18764
|
+
}
|
|
18765
|
+
tableAt(from, path32) {
|
|
18766
|
+
let table = from;
|
|
18767
|
+
for (const part of path32) {
|
|
18768
|
+
const next = table[part];
|
|
18769
|
+
if (next === void 0) table = table[part] = {};
|
|
18770
|
+
else if (isRecord3(next)) table = next;
|
|
18771
|
+
else if (Array.isArray(next) && isRecord3(next[next.length - 1])) table = next[next.length - 1];
|
|
18772
|
+
else throw this.fail(`${part} is not a table`);
|
|
18773
|
+
}
|
|
18774
|
+
return table;
|
|
18775
|
+
}
|
|
18776
|
+
key() {
|
|
18777
|
+
const parts = [];
|
|
18778
|
+
for (; ; ) {
|
|
18779
|
+
this.skipBlank(false);
|
|
18780
|
+
const ch = this.src[this.i];
|
|
18781
|
+
if (ch === '"') parts.push(this.basicString());
|
|
18782
|
+
else if (ch === "'") parts.push(this.literalString());
|
|
18783
|
+
else {
|
|
18784
|
+
const start = this.i;
|
|
18785
|
+
while (this.i < this.src.length && BARE_KEY.test(this.src[this.i])) this.i++;
|
|
18786
|
+
if (this.i === start) throw this.fail("expected a key");
|
|
18787
|
+
parts.push(this.src.slice(start, this.i));
|
|
18788
|
+
}
|
|
18789
|
+
this.skipBlank(false);
|
|
18790
|
+
if (this.src[this.i] !== ".") return parts;
|
|
18791
|
+
this.i++;
|
|
18792
|
+
}
|
|
18793
|
+
}
|
|
18794
|
+
value() {
|
|
18795
|
+
const ch = this.src[this.i];
|
|
18796
|
+
if (this.src.startsWith('"""', this.i)) return this.multilineString('"""');
|
|
18797
|
+
if (this.src.startsWith("'''", this.i)) return this.multilineString("'''");
|
|
18798
|
+
if (ch === '"') return this.basicString();
|
|
18799
|
+
if (ch === "'") return this.literalString();
|
|
18800
|
+
if (ch === "[") return this.array();
|
|
18801
|
+
if (ch === "{") return this.inlineTable();
|
|
18802
|
+
const start = this.i;
|
|
18803
|
+
while (this.i < this.src.length && !/[\s,\]}#]/.test(this.src[this.i])) this.i++;
|
|
18804
|
+
const raw = this.src.slice(start, this.i);
|
|
18805
|
+
if (!raw) throw this.fail("expected a value");
|
|
18806
|
+
if (raw === "true" || raw === "false") return raw === "true";
|
|
18807
|
+
const number = raw.replace(/_/g, "");
|
|
18808
|
+
if (/^[+-]?(\d+(\.\d+)?([eE][+-]?\d+)?|0x[0-9a-fA-F]+|0o[0-7]+|0b[01]+|inf|nan)$/.test(number)) return Number(number);
|
|
18809
|
+
return raw;
|
|
18810
|
+
}
|
|
18811
|
+
array() {
|
|
18812
|
+
this.i++;
|
|
18813
|
+
const items = [];
|
|
18814
|
+
for (; ; ) {
|
|
18815
|
+
this.skipBlank(true);
|
|
18816
|
+
if (this.src[this.i] === "]") {
|
|
18817
|
+
this.i++;
|
|
18818
|
+
return items;
|
|
18819
|
+
}
|
|
18820
|
+
items.push(this.value());
|
|
18821
|
+
this.skipBlank(true);
|
|
18822
|
+
if (this.src[this.i] === ",") this.i++;
|
|
18823
|
+
else if (this.src[this.i] !== "]") throw this.fail("expected , or ] in an array");
|
|
18824
|
+
}
|
|
18825
|
+
}
|
|
18826
|
+
inlineTable() {
|
|
18827
|
+
this.i++;
|
|
18828
|
+
const table = {};
|
|
18829
|
+
for (; ; ) {
|
|
18830
|
+
this.skipBlank(true);
|
|
18831
|
+
if (this.src[this.i] === "}") {
|
|
18832
|
+
this.i++;
|
|
18833
|
+
return table;
|
|
18834
|
+
}
|
|
18835
|
+
const path32 = this.key();
|
|
18836
|
+
this.expect("=");
|
|
18837
|
+
this.skipBlank(false);
|
|
18838
|
+
this.tableAt(table, path32.slice(0, -1))[path32[path32.length - 1]] = this.value();
|
|
18839
|
+
this.skipBlank(true);
|
|
18840
|
+
if (this.src[this.i] === ",") this.i++;
|
|
18841
|
+
else if (this.src[this.i] !== "}") throw this.fail("expected , or } in an inline table");
|
|
18842
|
+
}
|
|
18843
|
+
}
|
|
18844
|
+
basicString() {
|
|
18845
|
+
this.i++;
|
|
18846
|
+
let out = "";
|
|
18847
|
+
while (this.src[this.i] !== '"') {
|
|
18848
|
+
if (this.i >= this.src.length || this.src[this.i] === "\n") throw this.fail("unterminated string");
|
|
18849
|
+
out += this.src[this.i] === "\\" ? this.escape() : this.src[this.i++];
|
|
18850
|
+
}
|
|
18851
|
+
this.i++;
|
|
18852
|
+
return out;
|
|
18853
|
+
}
|
|
18854
|
+
literalString() {
|
|
18855
|
+
const end = this.src.indexOf("'", this.i + 1);
|
|
18856
|
+
const body = end < 0 ? "" : this.src.slice(this.i + 1, end);
|
|
18857
|
+
if (end < 0 || body.includes("\n")) throw this.fail("unterminated string");
|
|
18858
|
+
this.i = end + 1;
|
|
18859
|
+
return body;
|
|
18860
|
+
}
|
|
18861
|
+
multilineString(quote) {
|
|
18862
|
+
this.i += 3;
|
|
18863
|
+
if (this.src[this.i] === "\n") this.i++;
|
|
18864
|
+
else if (this.src.startsWith("\r\n", this.i)) this.i += 2;
|
|
18865
|
+
let out = "";
|
|
18866
|
+
for (; ; ) {
|
|
18867
|
+
if (this.i >= this.src.length) throw this.fail("unterminated string");
|
|
18868
|
+
if (this.src.startsWith(quote, this.i)) {
|
|
18869
|
+
let extra = 0;
|
|
18870
|
+
while (extra < 2 && this.src[this.i + 3 + extra] === quote[0]) extra++;
|
|
18871
|
+
out += quote[0].repeat(extra);
|
|
18872
|
+
this.i += 3 + extra;
|
|
18873
|
+
return out;
|
|
18874
|
+
}
|
|
18875
|
+
if (quote === '"""' && this.src[this.i] === "\\") {
|
|
18876
|
+
const rest = /^\\[ \t]*\r?\n[\s]*/.exec(this.src.slice(this.i, this.i + 256));
|
|
18877
|
+
if (rest) {
|
|
18878
|
+
this.i += rest[0].length;
|
|
18879
|
+
continue;
|
|
18880
|
+
}
|
|
18881
|
+
out += this.escape();
|
|
18882
|
+
continue;
|
|
18883
|
+
}
|
|
18884
|
+
out += this.src[this.i++];
|
|
18885
|
+
}
|
|
18886
|
+
}
|
|
18887
|
+
escape() {
|
|
18888
|
+
const code = this.src[this.i + 1];
|
|
18889
|
+
if (code in ESCAPES) {
|
|
18890
|
+
this.i += 2;
|
|
18891
|
+
return ESCAPES[code];
|
|
18892
|
+
}
|
|
18893
|
+
const width = code === "u" ? 4 : code === "U" ? 8 : 0;
|
|
18894
|
+
const hex = this.src.slice(this.i + 2, this.i + 2 + width);
|
|
18895
|
+
if (!width || !/^[0-9a-fA-F]+$/.test(hex) || hex.length !== width) throw this.fail("bad escape");
|
|
18896
|
+
this.i += 2 + width;
|
|
18897
|
+
return String.fromCodePoint(parseInt(hex, 16));
|
|
18898
|
+
}
|
|
18899
|
+
/** Spaces, tabs and comments; newlines too when `newlines`. */
|
|
18900
|
+
skipBlank(newlines) {
|
|
18901
|
+
while (this.i < this.src.length) {
|
|
18902
|
+
const ch = this.src[this.i];
|
|
18903
|
+
if (ch === " " || ch === " " || newlines && (ch === "\n" || ch === "\r")) this.i++;
|
|
18904
|
+
else if (ch === "#") while (this.i < this.src.length && this.src[this.i] !== "\n") this.i++;
|
|
18905
|
+
else return;
|
|
18906
|
+
}
|
|
18907
|
+
}
|
|
18908
|
+
endOfLine() {
|
|
18909
|
+
this.skipBlank(false);
|
|
18910
|
+
if (this.i < this.src.length && this.src[this.i] !== "\n" && this.src[this.i] !== "\r") throw this.fail("expected the end of the line");
|
|
18911
|
+
}
|
|
18912
|
+
expect(token) {
|
|
18913
|
+
this.skipBlank(false);
|
|
18914
|
+
if (!this.src.startsWith(token, this.i)) throw this.fail(`expected ${token}`);
|
|
18915
|
+
this.i += token.length;
|
|
18916
|
+
}
|
|
18917
|
+
fail(message) {
|
|
18918
|
+
const line = this.src.slice(0, this.i).split("\n").length;
|
|
18919
|
+
return new TomlError(`line ${line}: ${message}`);
|
|
18920
|
+
}
|
|
18921
|
+
};
|
|
18922
|
+
|
|
18923
|
+
// src/services/harness/codexRepoConfig.ts
|
|
18924
|
+
var CONFIG_FILE = path21.join(".codex", "config.toml");
|
|
18925
|
+
function stringsOf(value) {
|
|
18926
|
+
return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
18927
|
+
}
|
|
18928
|
+
function keysOf(value) {
|
|
18929
|
+
return isRecord3(value) ? Object.keys(value) : [];
|
|
18930
|
+
}
|
|
18931
|
+
function server2(name, entry, source) {
|
|
18932
|
+
if (!isRecord3(entry)) return "not a table";
|
|
18933
|
+
if (entry.enabled === false) return null;
|
|
18934
|
+
if (typeof entry.url === "string") {
|
|
18935
|
+
const names2 = [...keysOf(entry.http_headers), ...keysOf(entry.env_http_headers), ...typeof entry.bearer_token_env_var === "string" ? ["Authorization"] : []];
|
|
18936
|
+
return { name, source, transport: "remote", target: entry.url, names: names2, definition: entry };
|
|
18937
|
+
}
|
|
18938
|
+
if (typeof entry.command !== "string") return "no command or url";
|
|
18939
|
+
const names = [...keysOf(entry.env), ...stringsOf(entry.env_vars)];
|
|
18940
|
+
return { name, source, transport: "stdio", target: [entry.command, ...stringsOf(entry.args)].join(" "), names, definition: entry };
|
|
18941
|
+
}
|
|
18942
|
+
var CODEX_REPO_CONFIG = {
|
|
18943
|
+
/** Codex reads one file per directory from the repository root down, an override in place of the plain one. */
|
|
18944
|
+
instructions(workspace) {
|
|
18945
|
+
const files = [];
|
|
18946
|
+
for (const dir of repoLevels(workspace)) {
|
|
18947
|
+
for (const name of ["AGENTS.override.md", "AGENTS.md"]) {
|
|
18948
|
+
const text2 = readText(path21.join(dir, name));
|
|
18949
|
+
if (text2 === void 0) continue;
|
|
18950
|
+
files.push({ path: path21.relative(workspace, path21.join(dir, name)) || name, text: text2 });
|
|
18951
|
+
break;
|
|
18952
|
+
}
|
|
18953
|
+
}
|
|
18954
|
+
return files;
|
|
18955
|
+
},
|
|
18956
|
+
/** Each level's `.codex/config.toml`, the one nearer the workspace winning for a server both name. */
|
|
18957
|
+
projectServers(workspace) {
|
|
18958
|
+
const byName = /* @__PURE__ */ new Map();
|
|
18959
|
+
const unreadable = [];
|
|
18960
|
+
for (const dir of repoLevels(workspace)) {
|
|
18961
|
+
const file = path21.join(dir, CONFIG_FILE);
|
|
18962
|
+
const text2 = readText(file);
|
|
18963
|
+
if (text2 === void 0) continue;
|
|
18964
|
+
const source = path21.relative(workspace, file);
|
|
18965
|
+
let parsed;
|
|
18966
|
+
try {
|
|
18967
|
+
parsed = parseToml(text2);
|
|
18968
|
+
} catch (err) {
|
|
18969
|
+
unreadable.push({ source, reason: err instanceof Error ? err.message : String(err) });
|
|
18970
|
+
continue;
|
|
18971
|
+
}
|
|
18972
|
+
for (const [name, entry] of Object.entries(isRecord3(parsed.mcp_servers) ? parsed.mcp_servers : {})) {
|
|
18973
|
+
const read2 = server2(name, entry, source);
|
|
18974
|
+
if (typeof read2 === "string") unreadable.push({ source: `${source} (${name})`, reason: read2 });
|
|
18975
|
+
else if (read2) byName.set(name, read2);
|
|
18976
|
+
else byName.delete(name);
|
|
18977
|
+
}
|
|
18978
|
+
}
|
|
18979
|
+
return { servers: [...byName.values()], unreadable };
|
|
18980
|
+
}
|
|
18981
|
+
};
|
|
18982
|
+
function untrustedProjects(workspace) {
|
|
18983
|
+
const projects = {};
|
|
18984
|
+
for (let dir = path21.resolve(workspace); ; dir = path21.dirname(dir)) {
|
|
18985
|
+
projects[dir] = { trust_level: "untrusted" };
|
|
18986
|
+
if (path21.dirname(dir) === dir) return projects;
|
|
18987
|
+
}
|
|
17293
18988
|
}
|
|
17294
18989
|
|
|
17295
18990
|
// src/services/harness/codexOrdewell.ts
|
|
18991
|
+
var CODEX_SEARCH_PROVIDER = "openai";
|
|
17296
18992
|
function headerEnv(mcp) {
|
|
17297
18993
|
return Object.fromEntries(Object.keys(mcp.headers).map((header, i) => [header, `${MCP_TOKEN_VARIABLE_PREFIX}${i}`]));
|
|
17298
18994
|
}
|
|
17299
18995
|
var COUNT_WORDS = ["no", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"];
|
|
18996
|
+
var countOf = (names) => COUNT_WORDS[names.length] ?? String(names.length);
|
|
17300
18997
|
var quoted2 = (names) => listed(names.map((name) => `\`${name}\``));
|
|
17301
18998
|
var CODEX_ORDEWELL = {
|
|
17302
18999
|
// Codex names a call by server and tool; the model reaches the tool by Claude Code's name for it.
|
|
17303
19000
|
toolName: MCP_CLIENT_TOOL_NAMES.toolName,
|
|
17304
19001
|
toolNames: MCP_CLIENT_TOOL_NAMES.toolNames,
|
|
19002
|
+
// With its own search on, a model offered Ordewell's too tends to take that instead. Codex has no fetch of its own.
|
|
19003
|
+
plannerHidden: (start) => start.nativeSearch ? ["web_search"] : [],
|
|
19004
|
+
sessionSearchProvider: CODEX_SEARCH_PROVIDER,
|
|
17305
19005
|
/** An elicitation Codex raises for one of the server's tools: an approval the grant should have made unnecessary. */
|
|
17306
19006
|
isOrdewellAsk: ({ method, params }) => method === "mcpServer/elicitation/request" && params.serverName === ORDEWELL_MCP_SERVER_NAME,
|
|
17307
19007
|
/** `mcpServer/startupStatus/updated`'s status, unsent until the thread has tried to connect. */
|
|
@@ -17327,25 +19027,28 @@ var CODEX_ORDEWELL = {
|
|
|
17327
19027
|
taskInstructions() {
|
|
17328
19028
|
const tools = ordewellToolNames("task");
|
|
17329
19029
|
return [
|
|
17330
|
-
`This task has ${
|
|
19030
|
+
`This task has ${countOf(tools)} tools from the \`${ORDEWELL_MCP_SERVER_NAME}\` MCP server: ${quoted2(tools)}.`,
|
|
17331
19031
|
`They are not in your tool list up front. Find them with the tool discovery you have (the \`exec\` tool's \`ALL_TOOLS\` list) and call them by their full names, ${quoted2(this.toolNames("task"))}.`,
|
|
17332
19032
|
`Look for them before you finish; the task is reported complete through \`${TASK_COMPLETE_TOOL}\`.`
|
|
17333
19033
|
].join(" ");
|
|
17334
19034
|
},
|
|
17335
19035
|
/**
|
|
17336
19036
|
* The same discovery note for a planner, which also needs to know that its
|
|
17337
|
-
* commands go through
|
|
17338
|
-
* network, so `gh
|
|
17339
|
-
* shell first concludes the command cannot run.
|
|
19037
|
+
* commands and fetches go through Ordewell's tools: Codex's own shell is
|
|
19038
|
+
* sandboxed without network, so `gh`, a cloud CLI or `curl` fails there, and
|
|
19039
|
+
* a model that tried its shell first concludes the command cannot run.
|
|
17340
19040
|
*/
|
|
17341
|
-
plannerInstructions() {
|
|
17342
|
-
const tools = ordewellToolNames("planner");
|
|
19041
|
+
plannerInstructions(nativeSearch, hidden) {
|
|
19042
|
+
const tools = ordewellToolNames("planner", hidden);
|
|
17343
19043
|
return [
|
|
17344
|
-
`You have ${
|
|
17345
|
-
`They are not in your tool list up front. Find them with the tool discovery you have (the \`exec\` tool's \`ALL_TOOLS\` list) and call them by their full names, ${quoted2(this.toolNames("planner"))}.`,
|
|
17346
|
-
`Run shell commands with \`${this.toolName("run_command")}\`, not your own shell: yours is sandboxed without network, so git remotes, gh and cloud CLIs fail there
|
|
19044
|
+
`You have ${countOf(tools)} tools from the \`${ORDEWELL_MCP_SERVER_NAME}\` MCP server: ${quoted2(tools)}.`,
|
|
19045
|
+
`They are not in your tool list up front. Find them with the tool discovery you have (the \`exec\` tool's \`ALL_TOOLS\` list) and call them by their full names, ${quoted2(this.toolNames("planner", hidden))}.`,
|
|
19046
|
+
`Run shell commands with \`${this.toolName("run_command")}\`, not your own shell: yours is sandboxed without network, so git remotes, gh and cloud CLIs fail there.`,
|
|
19047
|
+
`Fetch web pages with \`${this.toolName("fetch")}\`, not curl or your shell.`,
|
|
19048
|
+
nativeSearch ? "Search the web with your built-in web search." : `Your built-in web search is off for this session; search with \`${this.toolName("web_search")}\`.`
|
|
17347
19049
|
].join(" ");
|
|
17348
|
-
}
|
|
19050
|
+
},
|
|
19051
|
+
plannerWebSearch: (nativeSearch) => nativeSearch ? "cached" : "disabled"
|
|
17349
19052
|
};
|
|
17350
19053
|
|
|
17351
19054
|
// src/services/harness/codexRpc.ts
|
|
@@ -17365,7 +19068,7 @@ var CodexRpc = class {
|
|
|
17365
19068
|
}
|
|
17366
19069
|
/** The request as the awaited form of {@link send}, for steps that wait on one answer and nothing else. */
|
|
17367
19070
|
call(method, params) {
|
|
17368
|
-
return new Promise((
|
|
19071
|
+
return new Promise((resolve9) => this.send(method, params, (response) => resolve9(response)));
|
|
17369
19072
|
}
|
|
17370
19073
|
/**
|
|
17371
19074
|
* A request framed as the line to write, left to the caller to write — for
|
|
@@ -17466,6 +19169,34 @@ function shownToolName(item) {
|
|
|
17466
19169
|
const tool2 = item.tool ?? "mcp_tool";
|
|
17467
19170
|
return item.server === ORDEWELL_MCP_SERVER_NAME ? CODEX_ORDEWELL.toolName(tool2) : tool2;
|
|
17468
19171
|
}
|
|
19172
|
+
function webSearchSummary(item) {
|
|
19173
|
+
const action = item.action;
|
|
19174
|
+
switch (action?.type) {
|
|
19175
|
+
case "search": {
|
|
19176
|
+
const queries = action.queries?.length ? action.queries : [action.query ?? item.query ?? ""];
|
|
19177
|
+
return queries.filter(Boolean).join("\n");
|
|
19178
|
+
}
|
|
19179
|
+
case "openPage":
|
|
19180
|
+
return `open ${action.url ?? "(no URL)"}`;
|
|
19181
|
+
case "findInPage":
|
|
19182
|
+
return `find ${JSON.stringify(action.pattern ?? "")} in ${action.url ?? "(no URL)"}`;
|
|
19183
|
+
default:
|
|
19184
|
+
return item.query ?? "";
|
|
19185
|
+
}
|
|
19186
|
+
}
|
|
19187
|
+
function webSearchArgs(item) {
|
|
19188
|
+
const action = item.action;
|
|
19189
|
+
switch (action?.type) {
|
|
19190
|
+
case "search":
|
|
19191
|
+
return action.queries?.length ? { queries: action.queries } : { query: action.query ?? item.query ?? "" };
|
|
19192
|
+
case "openPage":
|
|
19193
|
+
return { url: action.url ?? "" };
|
|
19194
|
+
case "findInPage":
|
|
19195
|
+
return { url: action.url ?? "", pattern: action.pattern ?? "" };
|
|
19196
|
+
default:
|
|
19197
|
+
return { query: item.query ?? "" };
|
|
19198
|
+
}
|
|
19199
|
+
}
|
|
17469
19200
|
var REVIEW_DECISIONS = { allow: "accept", allowForTask: "acceptForSession", deny: "decline" };
|
|
17470
19201
|
var TASK_APPROVALS = {
|
|
17471
19202
|
"item/commandExecution/requestApproval": {
|
|
@@ -17638,6 +19369,8 @@ ${this.exitMessage()}`);
|
|
|
17638
19369
|
...threadConfig
|
|
17639
19370
|
};
|
|
17640
19371
|
}
|
|
19372
|
+
const nativeSearch = opts?.nativeSearch ?? false;
|
|
19373
|
+
const servers = { ...Object.fromEntries((opts?.projectServers ?? []).map((server4) => [server4.name, server4.definition])), ...config.mcp_servers };
|
|
17641
19374
|
return {
|
|
17642
19375
|
...common,
|
|
17643
19376
|
sandbox: "read-only",
|
|
@@ -17646,13 +19379,22 @@ ${this.exitMessage()}`);
|
|
|
17646
19379
|
// changes it asks about are still declined: the sandbox holds, and commands go
|
|
17647
19380
|
// through Ordewell's `run_command`.
|
|
17648
19381
|
approvalPolicy: "on-request",
|
|
17649
|
-
|
|
19382
|
+
// Codex's search raises no request the policy could decide call by
|
|
19383
|
+
// call, so it is decided once here (ADR-0027).
|
|
19384
|
+
// The repository's own config never loads for a planner, so its servers
|
|
19385
|
+
// are only those approved, handed over as parsed (ADR-0028).
|
|
19386
|
+
config: {
|
|
19387
|
+
...config,
|
|
19388
|
+
...Object.keys(servers).length > 0 ? { mcp_servers: servers } : {},
|
|
19389
|
+
projects: opts ? untrustedProjects(opts.cwd) : {},
|
|
19390
|
+
web_search: CODEX_ORDEWELL.plannerWebSearch(nativeSearch)
|
|
19391
|
+
},
|
|
17650
19392
|
// `developerInstructions` layers on top of Codex's own base prompt, the
|
|
17651
19393
|
// way Claude Code's `--append-system-prompt` does. `baseInstructions`
|
|
17652
19394
|
// replaces it — which takes Codex's description of its own tools with
|
|
17653
19395
|
// it, and a planner that has forgotten it can read the workspace
|
|
17654
19396
|
// answers from a web search instead.
|
|
17655
|
-
developerInstructions: [opts?.systemPrompt, opts?.mcp ? CODEX_ORDEWELL.plannerInstructions() : ""].filter(Boolean).join("\n\n") || void 0
|
|
19397
|
+
developerInstructions: [opts?.systemPrompt, opts?.mcp ? CODEX_ORDEWELL.plannerInstructions(nativeSearch, opts.mcp.hidden) : ""].filter(Boolean).join("\n\n") || void 0
|
|
17656
19398
|
};
|
|
17657
19399
|
}
|
|
17658
19400
|
/**
|
|
@@ -17665,8 +19407,8 @@ ${this.exitMessage()}`);
|
|
|
17665
19407
|
return awaitAttach(async (left) => {
|
|
17666
19408
|
const known = CODEX_ORDEWELL.attachState(this.mcpStartup);
|
|
17667
19409
|
if (known !== "pending") return known;
|
|
17668
|
-
const reported = new Promise((
|
|
17669
|
-
this.mcpStartupSettled = () =>
|
|
19410
|
+
const reported = new Promise((resolve9) => {
|
|
19411
|
+
this.mcpStartupSettled = () => resolve9(CODEX_ORDEWELL.attachState(this.mcpStartup));
|
|
17670
19412
|
});
|
|
17671
19413
|
const state = await settleWithin(reported, {
|
|
17672
19414
|
timeoutMs: left,
|
|
@@ -17691,8 +19433,8 @@ ${this.exitMessage()}`);
|
|
|
17691
19433
|
const turn = this.turn;
|
|
17692
19434
|
if (!this.process || !turn) return Promise.resolve(false);
|
|
17693
19435
|
this.interruptRequested = true;
|
|
17694
|
-
const acknowledged = new Promise((
|
|
17695
|
-
this.whenNamed(turn, () => this.rpc.send("turn/interrupt", { threadId: this.threadId, turnId: turn.id }, (response) =>
|
|
19436
|
+
const acknowledged = new Promise((resolve9) => {
|
|
19437
|
+
this.whenNamed(turn, () => this.rpc.send("turn/interrupt", { threadId: this.threadId, turnId: turn.id }, (response) => resolve9(response.ok)));
|
|
17696
19438
|
});
|
|
17697
19439
|
return settleWithin(acknowledged, { timeoutMs, ended: this.processEnded, onTimeout: () => false, onEnded: () => false });
|
|
17698
19440
|
}
|
|
@@ -17713,7 +19455,7 @@ ${this.exitMessage()}`);
|
|
|
17713
19455
|
const turn = this.turn;
|
|
17714
19456
|
if (!this.process || !turn) return Promise.resolve(false);
|
|
17715
19457
|
const clientId = (0, import_crypto7.randomUUID)();
|
|
17716
|
-
return new Promise((
|
|
19458
|
+
return new Promise((resolve9) => {
|
|
17717
19459
|
let settled = false;
|
|
17718
19460
|
const steer = {
|
|
17719
19461
|
id,
|
|
@@ -17722,7 +19464,7 @@ ${this.exitMessage()}`);
|
|
|
17722
19464
|
if (settled) return;
|
|
17723
19465
|
settled = true;
|
|
17724
19466
|
if (!accepted) turn.steers.delete(clientId);
|
|
17725
|
-
|
|
19467
|
+
resolve9(accepted);
|
|
17726
19468
|
}
|
|
17727
19469
|
};
|
|
17728
19470
|
turn.steers.set(clientId, steer);
|
|
@@ -18071,16 +19813,16 @@ ${this.exitMessage()}`);
|
|
|
18071
19813
|
holdMcpToolApproval(msg, method, params, emit) {
|
|
18072
19814
|
if (method !== "mcpServer/elicitation/request") return false;
|
|
18073
19815
|
const meta = params._meta;
|
|
18074
|
-
const
|
|
18075
|
-
if (meta?.codex_approval_kind !== "mcp_tool_call" || typeof
|
|
19816
|
+
const server4 = params.serverName;
|
|
19817
|
+
if (meta?.codex_approval_kind !== "mcp_tool_call" || typeof server4 !== "string") return false;
|
|
18076
19818
|
const asked = typeof params.message === "string" ? /run tool "([^"]+)"/.exec(params.message)?.[1] : void 0;
|
|
18077
|
-
const tool2 = asked ?? this.mcpCallsInProgress.get(
|
|
19819
|
+
const tool2 = asked ?? this.mcpCallsInProgress.get(server4);
|
|
18078
19820
|
if (!tool2) return false;
|
|
18079
19821
|
const id = String(msg.id);
|
|
18080
19822
|
const input = typeof meta.tool_params === "object" && meta.tool_params !== null ? meta.tool_params : {};
|
|
18081
|
-
const scope = `mcp__${
|
|
19823
|
+
const scope = `mcp__${server4}__${tool2}`;
|
|
18082
19824
|
this.openPermissions.set(id, { requestId: msg.id, method, params });
|
|
18083
|
-
emit({ type: "permission_request", id, name: scope, detail: JSON.stringify(input), input, ask: { kind: "mcp", scope, tool: tool2, server } });
|
|
19825
|
+
emit({ type: "permission_request", id, name: scope, detail: JSON.stringify(input), input, ask: { kind: "mcp", scope, tool: tool2, server: server4 } });
|
|
18084
19826
|
return true;
|
|
18085
19827
|
}
|
|
18086
19828
|
/** A tool item entering `inProgress` — announce the call so the timeline moves. */
|
|
@@ -18100,11 +19842,11 @@ ${this.exitMessage()}`);
|
|
|
18100
19842
|
case "dynamicToolCall":
|
|
18101
19843
|
emit({ type: "tool_call", id: item.id, name: item.tool ?? "tool", args: item.arguments ?? {}, ...subagentId ? { subagentId } : {} });
|
|
18102
19844
|
return;
|
|
19845
|
+
// Announced on completion instead: at start the query is still empty.
|
|
18103
19846
|
case "webSearch":
|
|
18104
|
-
emit({ type: "tool_call", id: item.id, name: "web_search", args: { query: item.query }, ...subagentId ? { subagentId } : {} });
|
|
18105
19847
|
return;
|
|
18106
19848
|
case "fileChange": {
|
|
18107
|
-
const paths = (item.changes ?? []).map((change) => change.path).filter((
|
|
19849
|
+
const paths = (item.changes ?? []).map((change) => change.path).filter((path32) => !!path32);
|
|
18108
19850
|
this.fileChangePaths.set(item.id, paths);
|
|
18109
19851
|
if (this.startOpts?.kind === "task") {
|
|
18110
19852
|
for (const change of item.changes ?? []) this.announceFileChange(item.id, change, emit, subagentId);
|
|
@@ -18175,9 +19917,10 @@ ${item.text}` : item.text });
|
|
|
18175
19917
|
});
|
|
18176
19918
|
return;
|
|
18177
19919
|
// The query is empty when the search starts and filled when it lands, so
|
|
18178
|
-
// the
|
|
19920
|
+
// the call is announced only now, with what was actually searched.
|
|
18179
19921
|
case "webSearch":
|
|
18180
|
-
emit({ type: "
|
|
19922
|
+
emit({ type: "tool_call", id, name: "web_search", args: webSearchArgs(item), ...subagentId ? { subagentId } : {} });
|
|
19923
|
+
emit({ type: "tool_result", id, name: "web_search", output: webSearchSummary(item), success: true, ...subagentId ? { subagentId } : {} });
|
|
18181
19924
|
return;
|
|
18182
19925
|
case "fileChange":
|
|
18183
19926
|
this.fileChangePaths.delete(id);
|
|
@@ -18205,20 +19948,20 @@ ${item.text}` : item.text });
|
|
|
18205
19948
|
announceFileChange(itemId, change, emit, subagentId) {
|
|
18206
19949
|
const rows = this.fileChangeRows.get(itemId) ?? /* @__PURE__ */ new Map();
|
|
18207
19950
|
this.fileChangeRows.set(itemId, rows);
|
|
18208
|
-
const
|
|
18209
|
-
const known = rows.get(
|
|
19951
|
+
const path32 = this.shownChangePath(change);
|
|
19952
|
+
const known = rows.get(path32);
|
|
18210
19953
|
if (known) return known;
|
|
18211
|
-
const row = rows.size === 0 ? itemId : `${itemId}:${
|
|
18212
|
-
rows.set(
|
|
18213
|
-
emit({ type: "tool_call", id: row, name: changeName(change), args: { path:
|
|
19954
|
+
const row = rows.size === 0 ? itemId : `${itemId}:${path32}`;
|
|
19955
|
+
rows.set(path32, row);
|
|
19956
|
+
emit({ type: "tool_call", id: row, name: changeName(change), args: { path: path32 }, ...subagentId ? { subagentId } : {} });
|
|
18214
19957
|
return row;
|
|
18215
19958
|
}
|
|
18216
19959
|
/** Relative to the task's worktree, where every path but a stray one lies; a move names both ends. */
|
|
18217
19960
|
shownChangePath(change) {
|
|
18218
|
-
const shown = (
|
|
19961
|
+
const shown = (path32) => {
|
|
18219
19962
|
const cwd = this.startOpts?.cwd;
|
|
18220
|
-
const rel = cwd ? (0, import_path4.relative)(cwd,
|
|
18221
|
-
return rel && rel !== ".." && !rel.startsWith(`..${import_path4.sep}`) && !(0, import_path4.isAbsolute)(rel) ? rel :
|
|
19963
|
+
const rel = cwd ? (0, import_path4.relative)(cwd, path32) : path32;
|
|
19964
|
+
return rel && rel !== ".." && !rel.startsWith(`..${import_path4.sep}`) && !(0, import_path4.isAbsolute)(rel) ? rel : path32;
|
|
18222
19965
|
};
|
|
18223
19966
|
const from = shown(change.path ?? "");
|
|
18224
19967
|
return change.kind?.move_path ? `${from} \u2192 ${shown(change.kind.move_path)}` : from;
|
|
@@ -18262,10 +20005,91 @@ ${item.text}` : item.text });
|
|
|
18262
20005
|
};
|
|
18263
20006
|
|
|
18264
20007
|
// src/services/harness/OpenCodeAdapter.ts
|
|
18265
|
-
var
|
|
20008
|
+
var import_crypto10 = require("crypto");
|
|
18266
20009
|
|
|
18267
|
-
// src/services/harness/
|
|
20010
|
+
// src/services/harness/projectServers.ts
|
|
18268
20011
|
var import_crypto8 = require("crypto");
|
|
20012
|
+
var path22 = __toESM(require("path"));
|
|
20013
|
+
function canonical(value) {
|
|
20014
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
20015
|
+
if (!isRecord3(value)) return value;
|
|
20016
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
|
|
20017
|
+
}
|
|
20018
|
+
function projectServerScope(server4) {
|
|
20019
|
+
const hash = (0, import_crypto8.createHash)("sha256").update(JSON.stringify(canonical(server4.definition))).digest("hex").slice(0, 12);
|
|
20020
|
+
return `mcp-server:${server4.name}@${hash}`;
|
|
20021
|
+
}
|
|
20022
|
+
var scopeName = (scope) => scope.slice(0, scope.lastIndexOf("@"));
|
|
20023
|
+
function fileProjectServerApprovals(file = path22.join(globalDataDir(), "project-mcp-servers.json")) {
|
|
20024
|
+
const load = () => {
|
|
20025
|
+
const text2 = readText(file);
|
|
20026
|
+
if (text2 === void 0) return {};
|
|
20027
|
+
try {
|
|
20028
|
+
const parsed = JSON.parse(text2);
|
|
20029
|
+
const repositories = isRecord3(parsed) && isRecord3(parsed.repositories) ? parsed.repositories : {};
|
|
20030
|
+
return Object.fromEntries(Object.entries(repositories).map(([repo, scopes]) => [repo, Array.isArray(scopes) ? scopes.filter((s) => typeof s === "string") : []]));
|
|
20031
|
+
} catch {
|
|
20032
|
+
return {};
|
|
20033
|
+
}
|
|
20034
|
+
};
|
|
20035
|
+
return {
|
|
20036
|
+
approved: (repoRoot2, scope) => load()[repoRoot2]?.includes(scope) ?? false,
|
|
20037
|
+
remember(repoRoot2, scope) {
|
|
20038
|
+
const repositories = load();
|
|
20039
|
+
repositories[repoRoot2] = [...(repositories[repoRoot2] ?? []).filter((s) => scopeName(s) !== scopeName(scope)), scope];
|
|
20040
|
+
writePrivateFile(file, `${JSON.stringify({ repositories }, null, 2)}
|
|
20041
|
+
`);
|
|
20042
|
+
}
|
|
20043
|
+
};
|
|
20044
|
+
}
|
|
20045
|
+
function describe(server4, scope) {
|
|
20046
|
+
const what = server4.transport === "stdio" ? `Starting it runs this command as you${server4.names.length ? `, setting ${server4.names.join(", ")}` : ""}.` : `Starting it connects to this URL${server4.names.length ? `, sending ${server4.names.join(", ")}` : ""}.`;
|
|
20047
|
+
return [
|
|
20048
|
+
`The repository declares this MCP server for the planner in ${server4.source}. ${what}`,
|
|
20049
|
+
"Any ${VAR} in it is filled in from your environment.",
|
|
20050
|
+
`Approving is remembered for this repository until the definition changes; the planner allowlist entry ${scope} starts it without asking.`
|
|
20051
|
+
].join(" ");
|
|
20052
|
+
}
|
|
20053
|
+
function startRequest(server4) {
|
|
20054
|
+
const scope = projectServerScope(server4);
|
|
20055
|
+
return { kind: "mcp_server", subject: `${server4.name}: ${server4.target}`, scope, detail: describe(server4, scope) };
|
|
20056
|
+
}
|
|
20057
|
+
async function decideProjectServers(servers, opts) {
|
|
20058
|
+
const decision = { started: [], held: [] };
|
|
20059
|
+
for (const server4 of servers) {
|
|
20060
|
+
const req = startRequest(server4);
|
|
20061
|
+
const { scope } = req;
|
|
20062
|
+
const list = opts.approval?.allowlist?.();
|
|
20063
|
+
let started = false;
|
|
20064
|
+
if (list && scopeExcluded(list, scope)) {
|
|
20065
|
+
opts.approval?.record?.(req, false, "excluded");
|
|
20066
|
+
} else if (opts.approvals.approved(opts.repoRoot, scope)) {
|
|
20067
|
+
opts.approval?.record?.(req, true, "remembered");
|
|
20068
|
+
started = true;
|
|
20069
|
+
} else if (opts.approval) {
|
|
20070
|
+
const unasked = opts.approval.peek?.(req).decision === "allow";
|
|
20071
|
+
started = await opts.approval.request(req).catch(() => false);
|
|
20072
|
+
if (started && !unasked) opts.approvals.remember(opts.repoRoot, scope);
|
|
20073
|
+
}
|
|
20074
|
+
(started ? decision.started : decision.held).push(server4);
|
|
20075
|
+
}
|
|
20076
|
+
return decision;
|
|
20077
|
+
}
|
|
20078
|
+
function recordUnusable(servers, approval) {
|
|
20079
|
+
for (const server4 of servers) approval?.record?.(startRequest(server4), false, "unsupported");
|
|
20080
|
+
}
|
|
20081
|
+
var NOT_APPROVED = "not approved for planning";
|
|
20082
|
+
function heldServersNote(held, unreadable = []) {
|
|
20083
|
+
const lines2 = [
|
|
20084
|
+
...held.map(({ server: server4, reason }) => `- ${server4.name} (${server4.source}): ${reason}`),
|
|
20085
|
+
...unreadable.map((file) => `- ${file.source}: Ordewell could not read it (${file.reason})`)
|
|
20086
|
+
];
|
|
20087
|
+
if (lines2.length === 0) return "";
|
|
20088
|
+
return ["The repository declares MCP servers whose tools are not available to you while planning. Work that needs them belongs in the plan's tasks.", ...lines2].join("\n");
|
|
20089
|
+
}
|
|
20090
|
+
|
|
20091
|
+
// src/services/harness/openCodeTransport.ts
|
|
20092
|
+
var import_crypto9 = require("crypto");
|
|
18269
20093
|
var STREAM_CONNECT_TIMEOUT_MS = 5e3;
|
|
18270
20094
|
var OPENCODE_BUILTINS = /* @__PURE__ */ new Set([
|
|
18271
20095
|
"bash",
|
|
@@ -18302,9 +20126,9 @@ var OPENCODE_BUILTINS = /* @__PURE__ */ new Set([
|
|
|
18302
20126
|
function openCodePlannerAsk(permission, details, servers) {
|
|
18303
20127
|
const meta = details.metadata ?? {};
|
|
18304
20128
|
const pattern = details.patterns?.[0];
|
|
18305
|
-
if (permission === "
|
|
18306
|
-
const
|
|
18307
|
-
return
|
|
20129
|
+
if (permission === "websearch" || permission === "codesearch") {
|
|
20130
|
+
const query = typeof meta.query === "string" ? meta.query : pattern;
|
|
20131
|
+
return query ? { kind: "search", query, ...OPENCODE_SEARCH_PROVIDER } : { kind: "other" };
|
|
18308
20132
|
}
|
|
18309
20133
|
if (permission === "external_directory") {
|
|
18310
20134
|
if (typeof meta.filepath === "string") return { kind: "path", path: meta.filepath, directory: false };
|
|
@@ -18312,9 +20136,13 @@ function openCodePlannerAsk(permission, details, servers) {
|
|
|
18312
20136
|
return dir ? { kind: "path", path: dir, directory: true } : { kind: "other" };
|
|
18313
20137
|
}
|
|
18314
20138
|
if (OPENCODE_BUILTINS.has(permission)) return { kind: "other" };
|
|
18315
|
-
const
|
|
18316
|
-
if (!
|
|
18317
|
-
return { kind: "mcp", scope: permission, tool: permission.slice(
|
|
20139
|
+
const server4 = servers.filter((s) => permission.startsWith(`${s}_`)).sort((a, b) => b.length - a.length)[0];
|
|
20140
|
+
if (!server4) return { kind: "other" };
|
|
20141
|
+
return { kind: "mcp", scope: permission, tool: permission.slice(server4.length + 1), server: server4 };
|
|
20142
|
+
}
|
|
20143
|
+
function plannerSystem(opts, ownSearch = true) {
|
|
20144
|
+
const system = [opts.systemPrompt, opts.mcp ? OPENCODE_ORDEWELL.plannerInstructions(ownSearch) : ""].filter(Boolean).join("\n\n");
|
|
20145
|
+
return system ? { system } : {};
|
|
18318
20146
|
}
|
|
18319
20147
|
function splitModelId(id) {
|
|
18320
20148
|
const slash = id.indexOf("/");
|
|
@@ -18322,14 +20150,14 @@ function splitModelId(id) {
|
|
|
18322
20150
|
return { providerID: id.slice(0, slash), modelID: id.slice(slash + 1) };
|
|
18323
20151
|
}
|
|
18324
20152
|
function delay(ms, signal) {
|
|
18325
|
-
return new Promise((
|
|
20153
|
+
return new Promise((resolve9) => {
|
|
18326
20154
|
const onAbort = () => {
|
|
18327
20155
|
clearTimeout(timer);
|
|
18328
|
-
|
|
20156
|
+
resolve9();
|
|
18329
20157
|
};
|
|
18330
20158
|
const timer = setTimeout(() => {
|
|
18331
20159
|
signal?.removeEventListener("abort", onAbort);
|
|
18332
|
-
|
|
20160
|
+
resolve9();
|
|
18333
20161
|
}, ms);
|
|
18334
20162
|
timer.unref?.();
|
|
18335
20163
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
@@ -18356,7 +20184,7 @@ function autoApproves(task) {
|
|
|
18356
20184
|
return task.flags.modeSettings.approvals === "auto";
|
|
18357
20185
|
}
|
|
18358
20186
|
function newUserMessageId() {
|
|
18359
|
-
return `msg_${(0,
|
|
20187
|
+
return `msg_${(0, import_crypto9.randomBytes)(12).toString("hex")}`;
|
|
18360
20188
|
}
|
|
18361
20189
|
var PendingSteers = class {
|
|
18362
20190
|
/** User message id → the Ordewell message id it was sent for. */
|
|
@@ -18531,8 +20359,8 @@ function streamTurn() {
|
|
|
18531
20359
|
}
|
|
18532
20360
|
function turnLatch() {
|
|
18533
20361
|
let markDone;
|
|
18534
|
-
const ended = new Promise((
|
|
18535
|
-
markDone =
|
|
20362
|
+
const ended = new Promise((resolve9) => {
|
|
20363
|
+
markDone = resolve9;
|
|
18536
20364
|
});
|
|
18537
20365
|
const turn = { live: false, done: false, ended, finish: () => {
|
|
18538
20366
|
turn.done = true;
|
|
@@ -18542,9 +20370,9 @@ function turnLatch() {
|
|
|
18542
20370
|
}
|
|
18543
20371
|
async function settleTurn(turn, how, onEvent) {
|
|
18544
20372
|
const { signal } = how;
|
|
18545
|
-
const aborted = new Promise((
|
|
18546
|
-
if (signal?.aborted)
|
|
18547
|
-
signal?.addEventListener("abort", () =>
|
|
20373
|
+
const aborted = new Promise((resolve9) => {
|
|
20374
|
+
if (signal?.aborted) resolve9();
|
|
20375
|
+
signal?.addEventListener("abort", () => resolve9(), { once: true });
|
|
18548
20376
|
});
|
|
18549
20377
|
await Promise.race([turn.ended, aborted, how.processEnded]);
|
|
18550
20378
|
if (signal?.aborted) return false;
|
|
@@ -18563,8 +20391,8 @@ async function interruptAcknowledged(turn, processEnded, timeoutMs) {
|
|
|
18563
20391
|
async function openEventStream(connect, onFrame, onActivity) {
|
|
18564
20392
|
const streamAbort = new AbortController();
|
|
18565
20393
|
let connected;
|
|
18566
|
-
const ready = new Promise((
|
|
18567
|
-
connected =
|
|
20394
|
+
const ready = new Promise((resolve9) => {
|
|
20395
|
+
connected = resolve9;
|
|
18568
20396
|
});
|
|
18569
20397
|
const live = streamEvents(connect, streamAbort.signal, onFrame, connected, onActivity);
|
|
18570
20398
|
await settleWithin(ready, { timeoutMs: STREAM_CONNECT_TIMEOUT_MS, onTimeout: () => void 0 });
|
|
@@ -18600,7 +20428,35 @@ async function streamEvents(connect, signal, onFrame, onConnected, onActivity) {
|
|
|
18600
20428
|
var ACTIVE_POLL_INTERVAL_MS = 1e3;
|
|
18601
20429
|
var IDLE_POLLS_BEFORE_START = 3;
|
|
18602
20430
|
var PLANNER_RULES = Object.entries(OPENCODE_PLANNER_PERMISSION).map(([action, effect]) => ({ action, resource: "*", effect }));
|
|
20431
|
+
var SEARCH_OFF_RULES = Object.entries(OPENCODE_SEARCH_OFF).map(([action, effect]) => ({ action, resource: "*", effect }));
|
|
20432
|
+
var CODE_MODE_OFF_RULES = [{ action: "execute", resource: "*", effect: "deny" }];
|
|
20433
|
+
function codeModeOnly(version) {
|
|
20434
|
+
return `OpenCode${version ? ` ${version}` : ""} cannot run the planner safely: it can only offer Ordewell's tools inside code mode, which runs code the planner writes without asking. The planner needs an OpenCode version that can list MCP tools outside code mode. Update OpenCode, or choose another planner runner.`;
|
|
20435
|
+
}
|
|
20436
|
+
var UNUSABLE = "not usable on this OpenCode version: its tools are only reachable through code mode's `execute`, which the planner may not use";
|
|
18603
20437
|
var TASK_RULES = [{ action: "question", resource: "*", effect: "deny" }];
|
|
20438
|
+
function isRecord4(value) {
|
|
20439
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20440
|
+
}
|
|
20441
|
+
function searchProviderChosen(config) {
|
|
20442
|
+
const layers = Array.isArray(config) ? config : isRecord4(config) && Array.isArray(config.data) ? config.data : [];
|
|
20443
|
+
for (let i = layers.length - 1; i >= 0; i--) {
|
|
20444
|
+
const layer = layers[i];
|
|
20445
|
+
if (!isRecord4(layer) || layer.type !== "document" || !isRecord4(layer.info) || layer.info.websearch === void 0) continue;
|
|
20446
|
+
const setting = layer.info.websearch;
|
|
20447
|
+
return isRecord4(setting) && typeof setting.provider === "string" && setting.provider !== "";
|
|
20448
|
+
}
|
|
20449
|
+
return false;
|
|
20450
|
+
}
|
|
20451
|
+
function rulesDeny(rules, action) {
|
|
20452
|
+
if (!Array.isArray(rules)) return false;
|
|
20453
|
+
const last = [...rules].reverse().find((rule) => isRecord4(rule) && rule.action === action && (rule.resource === "*" || rule.resource === void 0));
|
|
20454
|
+
return isRecord4(last) && last.effect === "deny";
|
|
20455
|
+
}
|
|
20456
|
+
function transportFields(definition) {
|
|
20457
|
+
const fields = definition.type === "remote" ? ["type", "url", "headers"] : ["type", "command", "environment"];
|
|
20458
|
+
return Object.fromEntries(fields.filter((field) => definition[field] !== void 0).map((field) => [field, definition[field]]));
|
|
20459
|
+
}
|
|
18604
20460
|
function newTurnState() {
|
|
18605
20461
|
return { ...streamTurn(), toolNames: /* @__PURE__ */ new Map(), stepModels: /* @__PURE__ */ new Map(), briefs: /* @__PURE__ */ new Map(), streamedMessages: /* @__PURE__ */ new Set() };
|
|
18606
20462
|
}
|
|
@@ -18638,6 +20494,12 @@ var OpenCodeV2 = class {
|
|
|
18638
20494
|
agent;
|
|
18639
20495
|
turn = null;
|
|
18640
20496
|
interruptRequested = false;
|
|
20497
|
+
/** Whether a planner's own `websearch` is on for its session (ADR-0027). */
|
|
20498
|
+
ownSearch = true;
|
|
20499
|
+
/** Approved project servers whose tools are listed as tools of their own. */
|
|
20500
|
+
exposed = [];
|
|
20501
|
+
/** Approved project servers whose tools this OpenCode would list only inside `execute`, which a planner may not use. */
|
|
20502
|
+
unusable = [];
|
|
18641
20503
|
permissions = new OpenCodePermissions(
|
|
18642
20504
|
(id, sessionId, decision) => this.replyPermission(id, sessionId, decision),
|
|
18643
20505
|
() => this.opts.mcp
|
|
@@ -18645,13 +20507,18 @@ var OpenCodeV2 = class {
|
|
|
18645
20507
|
nativeSessionId() {
|
|
18646
20508
|
return this.sessionId;
|
|
18647
20509
|
}
|
|
20510
|
+
unusableProjectServers() {
|
|
20511
|
+
return [...this.unusable];
|
|
20512
|
+
}
|
|
18648
20513
|
async start() {
|
|
18649
|
-
await this.
|
|
20514
|
+
await this.exposeServers();
|
|
18650
20515
|
const resume = this.opts.resumeSessionId;
|
|
18651
20516
|
if (resume) {
|
|
18652
20517
|
const existing = await this.host.json("GET", `/api/session/${resume}`).catch(() => null);
|
|
18653
|
-
|
|
20518
|
+
const resumable = this.role === "task" || rulesDeny(existing?.data?.permissions, "execute");
|
|
20519
|
+
if (existing?.data?.id && resumable) {
|
|
18654
20520
|
this.sessionId = existing.data.id;
|
|
20521
|
+
if (this.role === "planner") this.ownSearch = !rulesDeny(existing.data.permissions, "websearch");
|
|
18655
20522
|
if (this.model) await this.host.request("POST", `/api/session/${this.sessionId}/model`, { model: this.model }).catch(() => void 0);
|
|
18656
20523
|
await this.host.request("POST", `/api/session/${this.sessionId}/agent`, { agent: this.agent }).catch(() => void 0);
|
|
18657
20524
|
await this.applyInstructions();
|
|
@@ -18659,10 +20526,11 @@ var OpenCodeV2 = class {
|
|
|
18659
20526
|
}
|
|
18660
20527
|
if (this.opts.kind === "task") throw new Error(`OpenCode could not resume session ${resume}: the server does not know it.`);
|
|
18661
20528
|
}
|
|
20529
|
+
if (this.role === "planner") this.ownSearch = searchProviderChosen(await this.host.json("GET", "/api/config").catch(() => null));
|
|
18662
20530
|
const created = await this.host.json("POST", "/api/session", {
|
|
18663
20531
|
agent: this.agent,
|
|
18664
20532
|
...this.model ? { model: this.model } : {},
|
|
18665
|
-
permissions: [...this.role === "planner" ? PLANNER_RULES : TASK_RULES, ...this.ordewellRules()]
|
|
20533
|
+
permissions: [...this.role === "planner" ? [...PLANNER_RULES, ...this.ownSearch ? [] : SEARCH_OFF_RULES, ...CODE_MODE_OFF_RULES] : TASK_RULES, ...this.ordewellRules()]
|
|
18666
20534
|
});
|
|
18667
20535
|
if (!created?.data?.id) throw new Error(`The OpenCode ${this.role} server did not return a session id.`);
|
|
18668
20536
|
this.sessionId = created.data.id;
|
|
@@ -18670,16 +20538,31 @@ var OpenCodeV2 = class {
|
|
|
18670
20538
|
}
|
|
18671
20539
|
/**
|
|
18672
20540
|
* 2.x lists an MCP server's tools inside its code-mode `execute` tool, where
|
|
18673
|
-
* a model told to call `task_complete` does not look for it
|
|
18674
|
-
*
|
|
18675
|
-
*
|
|
18676
|
-
*
|
|
18677
|
-
*
|
|
18678
|
-
|
|
18679
|
-
|
|
20541
|
+
* a model told to call `task_complete` does not look for it, and where a
|
|
20542
|
+
* planner may not reach them: `execute` runs model-written code over every
|
|
20543
|
+
* tool at once, past per-tool approval. Each server is replaced at runtime
|
|
20544
|
+
* with `codemode: false`, which lists its tools as tools of their own: the
|
|
20545
|
+
* config it started with is in the 1.x shape both versions read, and that
|
|
20546
|
+
* shape drops the setting. Replaced before anything lists the servers, a
|
|
20547
|
+
* local one starts once; replaced later, OpenCode stops it and starts the
|
|
20548
|
+
* new one. If the route is gone, Ordewell's tools are reachable only
|
|
20549
|
+
* through `execute`: a task keeps that, but a planner would have to choose
|
|
20550
|
+
* between an unchecked code runner and no way to submit a plan, so it is
|
|
20551
|
+
* not started. A project server's tools are then not reachable at all.
|
|
20552
|
+
*/
|
|
20553
|
+
async exposeServers() {
|
|
18680
20554
|
const mcp = this.opts.mcp;
|
|
18681
|
-
if (!mcp)
|
|
18682
|
-
|
|
20555
|
+
if (mcp && !await this.expose(mcp.name, { type: "remote", url: mcp.url, headers: mcp.headers }) && this.role === "planner") {
|
|
20556
|
+
throw new Error(codeModeOnly(this.host.version));
|
|
20557
|
+
}
|
|
20558
|
+
const project = this.opts.kind === "planner" ? this.opts.projectServers ?? [] : [];
|
|
20559
|
+
for (const server4 of project) {
|
|
20560
|
+
if (await this.expose(server4.name, transportFields(server4.definition))) this.exposed.push(server4.name);
|
|
20561
|
+
else this.unusable.push(server4);
|
|
20562
|
+
}
|
|
20563
|
+
}
|
|
20564
|
+
async expose(name, config) {
|
|
20565
|
+
return this.host.request("PUT", `/api/experimental/mcp/${name}`, { config: { ...config, codemode: false } }).then((response) => response.ok, () => false);
|
|
18683
20566
|
}
|
|
18684
20567
|
/** Allow the Ordewell server's tools, so a call never waits on a person (ADR-0022, S3). */
|
|
18685
20568
|
ordewellRules() {
|
|
@@ -18687,8 +20570,11 @@ var OpenCodeV2 = class {
|
|
|
18687
20570
|
}
|
|
18688
20571
|
/** The planner's system prompt rides on the session as an instruction entry — a prompt has no system field. */
|
|
18689
20572
|
async applyInstructions() {
|
|
18690
|
-
if (this.opts.kind !== "planner"
|
|
18691
|
-
const
|
|
20573
|
+
if (this.opts.kind !== "planner") return;
|
|
20574
|
+
const unusable = heldServersNote(this.unusable.map((server4) => ({ server: server4, reason: UNUSABLE })));
|
|
20575
|
+
const system = [plannerSystem(this.opts, this.ownSearch).system, unusable].filter(Boolean).join("\n\n");
|
|
20576
|
+
if (!system) return;
|
|
20577
|
+
const response = await this.host.request("PUT", `/api/experimental/session/${this.sessionId}/instructions/entries/ordewell-planner`, { value: system });
|
|
18692
20578
|
if (!response.ok) throw new Error(`OpenCode did not take the planner instructions: ${response.status} ${response.statusText}`);
|
|
18693
20579
|
}
|
|
18694
20580
|
async send(message, onEvent, signal, onActivity) {
|
|
@@ -18946,7 +20832,8 @@ var OpenCodeV2 = class {
|
|
|
18946
20832
|
const request = this.permissionRequest(ask);
|
|
18947
20833
|
if (!request) return;
|
|
18948
20834
|
if (this.role !== "task") {
|
|
18949
|
-
const
|
|
20835
|
+
const listed2 = (this.host.mcpServers?.() ?? []).filter((name) => !this.unusable.some((server4) => server4.name === name));
|
|
20836
|
+
const target = openCodePlannerAsk(request.name, { patterns: ask.resources }, [...listed2, ...this.exposed]);
|
|
18950
20837
|
this.permissions.ask({ ...request, ask: target }, session, false, state.seen, onEvent);
|
|
18951
20838
|
} else this.permissions.ask(request, session, this.opts.kind === "task" && autoApproves(this.opts), state.seen, onEvent);
|
|
18952
20839
|
}
|
|
@@ -19068,18 +20955,20 @@ var OpenCodeAdapter = class {
|
|
|
19068
20955
|
async launch(opts) {
|
|
19069
20956
|
if (opts.kind === "task") this.task = opts;
|
|
19070
20957
|
else this.planner = opts;
|
|
19071
|
-
const password = (0,
|
|
20958
|
+
const password = (0, import_crypto10.randomBytes)(24).toString("base64url");
|
|
19072
20959
|
const command = () => ({
|
|
19073
20960
|
command: "opencode",
|
|
19074
20961
|
args: ["serve", "--hostname", "127.0.0.1", "--port", "0"],
|
|
19075
20962
|
env: (workspaceEnv) => {
|
|
19076
20963
|
const existing = workspaceEnv[OPENCODE_CONFIG_VARIABLE] ?? process.env[OPENCODE_CONFIG_VARIABLE];
|
|
19077
|
-
const
|
|
20964
|
+
const projectServers = opts.kind === "planner" ? Object.fromEntries((opts.projectServers ?? []).map((server4) => [server4.name, server4.definition])) : {};
|
|
20965
|
+
const ordewellConfig = opts.kind === "planner" ? OPENCODE_ORDEWELL.configContent(existing, opts.mcp ?? null, "planner", projectServers) ?? OPENCODE_ORDEWELL.configContent(void 0, opts.mcp ?? null, "planner", projectServers) : opts.mcp ? OPENCODE_ORDEWELL.configContent(existing, opts.mcp, "task") : null;
|
|
19078
20966
|
if (opts.mcp && ordewellConfig === null) {
|
|
19079
20967
|
console.error("[opencode] OPENCODE_CONFIG_CONTENT is not a JSON object, so the Ordewell tools were not injected.");
|
|
19080
20968
|
}
|
|
19081
20969
|
this.ordewell = ordewellConfig === null ? null : opts.mcp ?? null;
|
|
19082
20970
|
return {
|
|
20971
|
+
...opts.kind === "planner" ? OPENCODE_PLANNER_ENV : {},
|
|
19083
20972
|
...ordewellConfig === null ? {} : { [OPENCODE_CONFIG_VARIABLE]: ordewellConfig },
|
|
19084
20973
|
OPENCODE_SERVER_USERNAME: SERVER_USERNAME,
|
|
19085
20974
|
OPENCODE_SERVER_PASSWORD: password
|
|
@@ -19099,7 +20988,7 @@ var OpenCodeAdapter = class {
|
|
|
19099
20988
|
* only until the address is known.
|
|
19100
20989
|
*/
|
|
19101
20990
|
serverAddress() {
|
|
19102
|
-
return new Promise((
|
|
20991
|
+
return new Promise((resolve9) => {
|
|
19103
20992
|
let partial = "";
|
|
19104
20993
|
const stopReading = this.runner.readStdout((text2) => {
|
|
19105
20994
|
const lines2 = (partial + text2).split("\n");
|
|
@@ -19114,7 +21003,7 @@ var OpenCodeAdapter = class {
|
|
|
19114
21003
|
const settle = (url) => {
|
|
19115
21004
|
stopReading();
|
|
19116
21005
|
clearTimeout(timer);
|
|
19117
|
-
|
|
21006
|
+
resolve9(url);
|
|
19118
21007
|
};
|
|
19119
21008
|
void this.processEnded.then(() => settle(null));
|
|
19120
21009
|
});
|
|
@@ -19125,10 +21014,12 @@ var OpenCodeAdapter = class {
|
|
|
19125
21014
|
if (!this.baseUrl) {
|
|
19126
21015
|
throw new Error(this.runner.withStderrTail(`The OpenCode ${this.role()} server did not start.`));
|
|
19127
21016
|
}
|
|
19128
|
-
|
|
21017
|
+
const version = await this.v2Version();
|
|
21018
|
+
if (version !== null) {
|
|
19129
21019
|
this.v2 = new OpenCodeV2({
|
|
19130
|
-
|
|
19131
|
-
|
|
21020
|
+
version,
|
|
21021
|
+
request: (method, path32, body, signal) => this.request(method, path32, body, signal),
|
|
21022
|
+
json: (method, path32, body, signal) => this.json(method, path32, body, signal),
|
|
19132
21023
|
openEvents: (signal) => this.deps.fetch(`${this.baseUrl}/api/event`, { signal, headers: this.headers() }),
|
|
19133
21024
|
processEnded: this.processEnded,
|
|
19134
21025
|
isExited: () => this.exited,
|
|
@@ -19178,7 +21069,7 @@ var OpenCodeAdapter = class {
|
|
|
19178
21069
|
tools: DISABLED_TOOLS,
|
|
19179
21070
|
...model ? { model } : {},
|
|
19180
21071
|
...this.planner?.effort ? { variant: this.planner.effort } : {},
|
|
19181
|
-
...this.planner
|
|
21072
|
+
...this.planner ? plannerSystem(this.planner) : {}
|
|
19182
21073
|
};
|
|
19183
21074
|
const reply2 = await this.json("POST", `/session/${this.sessionId}/message`, body, signal);
|
|
19184
21075
|
if (signal?.aborted) {
|
|
@@ -19394,9 +21285,9 @@ var OpenCodeAdapter = class {
|
|
|
19394
21285
|
* only 2.x answers `/api/info` with its version. A 1.x server — or one that
|
|
19395
21286
|
* answers with anything else — keeps the 1.x protocol this class speaks.
|
|
19396
21287
|
*/
|
|
19397
|
-
async
|
|
21288
|
+
async v2Version() {
|
|
19398
21289
|
const info = await this.json("GET", "/api/info").catch(() => null);
|
|
19399
|
-
return typeof info === "object" && info !== null && typeof info.version === "string";
|
|
21290
|
+
return typeof info === "object" && info !== null && typeof info.version === "string" ? info.version : null;
|
|
19400
21291
|
}
|
|
19401
21292
|
role() {
|
|
19402
21293
|
return this.task ? "task" : "planner";
|
|
@@ -19639,13 +21530,13 @@ var OpenCodeAdapter = class {
|
|
|
19639
21530
|
onActivity
|
|
19640
21531
|
);
|
|
19641
21532
|
}
|
|
19642
|
-
async json(method,
|
|
19643
|
-
const response = await this.request(method,
|
|
19644
|
-
if (!response.ok) throw new Error(`${method} ${
|
|
21533
|
+
async json(method, path32, body, signal) {
|
|
21534
|
+
const response = await this.request(method, path32, body, signal);
|
|
21535
|
+
if (!response.ok) throw new Error(`${method} ${path32} failed: ${response.status} ${response.statusText}`);
|
|
19645
21536
|
return await response.json().catch(() => null);
|
|
19646
21537
|
}
|
|
19647
|
-
request(method,
|
|
19648
|
-
return this.deps.fetch(`${this.baseUrl}${
|
|
21538
|
+
request(method, path32, body, signal) {
|
|
21539
|
+
return this.deps.fetch(`${this.baseUrl}${path32}`, {
|
|
19649
21540
|
method,
|
|
19650
21541
|
headers: this.headers({ "content-type": "application/json" }),
|
|
19651
21542
|
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
@@ -19658,6 +21549,9 @@ var OpenCodeAdapter = class {
|
|
|
19658
21549
|
nativeSessionId() {
|
|
19659
21550
|
return this.sessionId;
|
|
19660
21551
|
}
|
|
21552
|
+
unusableProjectServers() {
|
|
21553
|
+
return this.v2?.unusableProjectServers() ?? [];
|
|
21554
|
+
}
|
|
19661
21555
|
/** Asks the server, which lists every MCP server it has with the state of its connection. */
|
|
19662
21556
|
async mcpAttached() {
|
|
19663
21557
|
const ordewell = this.ordewell;
|
|
@@ -19692,11 +21586,68 @@ var OpenCodeAdapter = class {
|
|
|
19692
21586
|
}
|
|
19693
21587
|
};
|
|
19694
21588
|
|
|
21589
|
+
// src/services/harness/openCodeRepoConfig.ts
|
|
21590
|
+
var path23 = __toESM(require("path"));
|
|
21591
|
+
var CONFIG_FILES = ["opencode.json", "opencode.jsonc", path23.join(".opencode", "opencode.json"), path23.join(".opencode", "opencode.jsonc")];
|
|
21592
|
+
var INSTRUCTION_NAMES = ["AGENTS.md", "CLAUDE.md", "CONTEXT.md"];
|
|
21593
|
+
function server3(name, entry, source) {
|
|
21594
|
+
if (!isRecord3(entry)) return "not an object";
|
|
21595
|
+
if (entry.enabled === false) return null;
|
|
21596
|
+
if (entry.type === "remote") {
|
|
21597
|
+
if (typeof entry.url !== "string") return "no url";
|
|
21598
|
+
return { name, source, transport: "remote", target: entry.url, names: isRecord3(entry.headers) ? Object.keys(entry.headers) : [], definition: entry };
|
|
21599
|
+
}
|
|
21600
|
+
const command = Array.isArray(entry.command) ? entry.command.map(String) : [];
|
|
21601
|
+
if (entry.type !== "local" || command.length === 0) return "no command or url";
|
|
21602
|
+
return { name, source, transport: "stdio", target: command.join(" "), names: isRecord3(entry.environment) ? Object.keys(entry.environment) : [], definition: entry };
|
|
21603
|
+
}
|
|
21604
|
+
var OPENCODE_REPO_CONFIG = {
|
|
21605
|
+
instructions(workspace) {
|
|
21606
|
+
const levels = repoLevels(workspace);
|
|
21607
|
+
for (const name of INSTRUCTION_NAMES) {
|
|
21608
|
+
const files = levels.flatMap((dir) => {
|
|
21609
|
+
const text2 = readText(path23.join(dir, name));
|
|
21610
|
+
return text2 === void 0 ? [] : [{ path: path23.relative(workspace, path23.join(dir, name)) || name, text: text2 }];
|
|
21611
|
+
});
|
|
21612
|
+
if (files.length > 0) return files;
|
|
21613
|
+
}
|
|
21614
|
+
return [];
|
|
21615
|
+
},
|
|
21616
|
+
/** Every level's config, the one nearer the workspace winning for a server both name. */
|
|
21617
|
+
projectServers(workspace) {
|
|
21618
|
+
const byName = /* @__PURE__ */ new Map();
|
|
21619
|
+
const unreadable = [];
|
|
21620
|
+
for (const dir of repoLevels(workspace)) {
|
|
21621
|
+
for (const name of CONFIG_FILES) {
|
|
21622
|
+
const file = path23.join(dir, name);
|
|
21623
|
+
const text2 = readText(file);
|
|
21624
|
+
if (text2 === void 0) continue;
|
|
21625
|
+
const source = path23.relative(workspace, file);
|
|
21626
|
+
let parsed;
|
|
21627
|
+
try {
|
|
21628
|
+
parsed = parseJsonc(text2);
|
|
21629
|
+
} catch (err) {
|
|
21630
|
+
unreadable.push({ source, reason: err instanceof Error ? err.message : String(err) });
|
|
21631
|
+
continue;
|
|
21632
|
+
}
|
|
21633
|
+
const entries = isRecord3(parsed) && isRecord3(parsed.mcp) ? parsed.mcp : {};
|
|
21634
|
+
for (const [serverName, entry] of Object.entries(entries)) {
|
|
21635
|
+
const read2 = server3(serverName, entry, source);
|
|
21636
|
+
if (typeof read2 === "string") unreadable.push({ source: `${source} (${serverName})`, reason: read2 });
|
|
21637
|
+
else if (read2) byName.set(serverName, read2);
|
|
21638
|
+
else byName.delete(serverName);
|
|
21639
|
+
}
|
|
21640
|
+
}
|
|
21641
|
+
}
|
|
21642
|
+
return { servers: [...byName.values()], unreadable };
|
|
21643
|
+
}
|
|
21644
|
+
};
|
|
21645
|
+
|
|
19695
21646
|
// src/services/harness/connectors.ts
|
|
19696
21647
|
var CONNECTORS = {
|
|
19697
|
-
"claude-code": { create: (deps) => new ClaudeCodeAdapter(deps), ordewellTools: CLAUDE_ORDEWELL },
|
|
19698
|
-
codex: { create: (deps) => new CodexAdapter(deps), ordewellTools: CODEX_ORDEWELL },
|
|
19699
|
-
opencode: { create: (deps) => new OpenCodeAdapter(deps), ordewellTools: OPENCODE_ORDEWELL }
|
|
21648
|
+
"claude-code": { create: (deps) => new ClaudeCodeAdapter(deps), ordewellTools: CLAUDE_ORDEWELL, repoConfig: CLAUDE_REPO_CONFIG },
|
|
21649
|
+
codex: { create: (deps) => new CodexAdapter(deps), ordewellTools: CODEX_ORDEWELL, repoConfig: CODEX_REPO_CONFIG },
|
|
21650
|
+
opencode: { create: (deps) => new OpenCodeAdapter(deps), ordewellTools: OPENCODE_ORDEWELL, repoConfig: OPENCODE_REPO_CONFIG }
|
|
19700
21651
|
};
|
|
19701
21652
|
function connectorFor(runner) {
|
|
19702
21653
|
return Object.hasOwn(CONNECTORS, runner) ? CONNECTORS[runner] : void 0;
|
|
@@ -19909,7 +21860,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
19909
21860
|
this.notifications.warn(message);
|
|
19910
21861
|
};
|
|
19911
21862
|
if (resolved.blockedEnvrc) {
|
|
19912
|
-
warn(`blocked:${resolved.blockedEnvrc}`, `direnv has blocked ${resolved.blockedEnvrc}, so tasks start without its variables. Run \`direnv allow\` in ${
|
|
21863
|
+
warn(`blocked:${resolved.blockedEnvrc}`, `direnv has blocked ${resolved.blockedEnvrc}, so tasks start without its variables. Run \`direnv allow\` in ${path24.dirname(resolved.blockedEnvrc)} to use them.`);
|
|
19913
21864
|
}
|
|
19914
21865
|
if (resolved.trackedEnvFile) {
|
|
19915
21866
|
warn(`tracked:${resolved.trackedEnvFile}`, `Ignored ${resolved.trackedEnvFile}: git tracks it, and a committed file must not choose the environment agents run in. Untrack it to use it.`);
|
|
@@ -20912,7 +22863,7 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
20912
22863
|
uncommittedSkillFolder(name) {
|
|
20913
22864
|
const root = this.workspaceRootFn();
|
|
20914
22865
|
const skill = this.skillsAt(this.skillRoots(root)).findSkill(name);
|
|
20915
|
-
return skill?.source === "workspace" ?
|
|
22866
|
+
return skill?.source === "workspace" ? path24.relative(root, path24.dirname(skill.path)).split(path24.sep).join("/") : void 0;
|
|
20916
22867
|
}
|
|
20917
22868
|
/**
|
|
20918
22869
|
* The roots whose `.ordewell/skills/` an attempt at `cwd` reads: in a repo
|
|
@@ -21151,7 +23102,7 @@ ${errors.map((e) => `- ${e}`).join("\n")}`
|
|
|
21151
23102
|
};
|
|
21152
23103
|
|
|
21153
23104
|
// src/services/ModelDiscovery.ts
|
|
21154
|
-
var
|
|
23105
|
+
var path25 = __toESM(require("path"));
|
|
21155
23106
|
var import_child_process6 = require("child_process");
|
|
21156
23107
|
var import_util4 = require("util");
|
|
21157
23108
|
var fsSync3 = __toESM(require("fs"));
|
|
@@ -21166,7 +23117,7 @@ var defaultAppServerClient = async (command, args, options) => {
|
|
|
21166
23117
|
const PATH = await augmentedPath();
|
|
21167
23118
|
const launch = await planDirectLaunch(command, args).catch(() => null);
|
|
21168
23119
|
if (!launch) return null;
|
|
21169
|
-
return new Promise((
|
|
23120
|
+
return new Promise((resolve9) => {
|
|
21170
23121
|
let child;
|
|
21171
23122
|
try {
|
|
21172
23123
|
child = (0, import_child_process6.spawn)(launch.file, launch.args, {
|
|
@@ -21175,7 +23126,7 @@ var defaultAppServerClient = async (command, args, options) => {
|
|
|
21175
23126
|
windowsVerbatimArguments: launch.verbatim
|
|
21176
23127
|
});
|
|
21177
23128
|
} catch {
|
|
21178
|
-
|
|
23129
|
+
resolve9(null);
|
|
21179
23130
|
return;
|
|
21180
23131
|
}
|
|
21181
23132
|
let settled = false;
|
|
@@ -21184,7 +23135,7 @@ var defaultAppServerClient = async (command, args, options) => {
|
|
|
21184
23135
|
settled = true;
|
|
21185
23136
|
clearTimeout(timer);
|
|
21186
23137
|
killTree(child);
|
|
21187
|
-
|
|
23138
|
+
resolve9(value);
|
|
21188
23139
|
};
|
|
21189
23140
|
const timer = setTimeout(() => finish(null), options?.timeout ?? 2e4);
|
|
21190
23141
|
child.on("error", () => finish(null));
|
|
@@ -21251,7 +23202,7 @@ function geminiDiscoveryCacheKey(apiKey) {
|
|
|
21251
23202
|
return {
|
|
21252
23203
|
key: `gemini-models:${hash}`,
|
|
21253
23204
|
dir,
|
|
21254
|
-
file:
|
|
23205
|
+
file: path25.join(dir, `gemini_models_${hash}.json`)
|
|
21255
23206
|
};
|
|
21256
23207
|
}
|
|
21257
23208
|
function readGeminiCache(apiKey) {
|
|
@@ -21448,10 +23399,10 @@ async function discoverFromCommand(manifest, execImpl) {
|
|
|
21448
23399
|
return { models: applyVariants(buildFallbackModels(manifest), manifest), fromFallback: true };
|
|
21449
23400
|
}
|
|
21450
23401
|
function mergeCanonicalAliases(discovered, manifest) {
|
|
21451
|
-
const
|
|
21452
|
-
if (!
|
|
23402
|
+
const canonical2 = manifest.modelDiscovery.canonicalAliases;
|
|
23403
|
+
if (!canonical2 || canonical2.length === 0) return discovered;
|
|
21453
23404
|
const seen = new Set(discovered.map((m) => m.modelId));
|
|
21454
|
-
const gaps =
|
|
23405
|
+
const gaps = canonical2.filter((f) => !seen.has(f.modelId)).map((f) => ({
|
|
21455
23406
|
modelId: f.modelId,
|
|
21456
23407
|
modelLabel: f.modelLabel,
|
|
21457
23408
|
runnerProvider: f.modelId.includes("/") ? f.modelId.split("/")[0] : void 0,
|
|
@@ -21775,7 +23726,7 @@ var ModelDiscovery = class {
|
|
|
21775
23726
|
|
|
21776
23727
|
// src/services/ModelCatalog.ts
|
|
21777
23728
|
var fs11 = __toESM(require("fs"));
|
|
21778
|
-
var
|
|
23729
|
+
var path26 = __toESM(require("path"));
|
|
21779
23730
|
var CACHE_KEY = "model-catalog";
|
|
21780
23731
|
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
21781
23732
|
function storageKey(baseUrl) {
|
|
@@ -21827,7 +23778,7 @@ var ModelCatalog = class {
|
|
|
21827
23778
|
const url = (baseUrl || "https://openrouter.ai/api/v1").replace(/\/+$/, "");
|
|
21828
23779
|
const key = storageKey(url);
|
|
21829
23780
|
const cacheDir = globalCacheDir();
|
|
21830
|
-
const cacheFile =
|
|
23781
|
+
const cacheFile = path26.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
|
|
21831
23782
|
if (fs11.existsSync(cacheFile)) fs11.rmSync(cacheFile);
|
|
21832
23783
|
} catch {
|
|
21833
23784
|
}
|
|
@@ -21835,7 +23786,7 @@ var ModelCatalog = class {
|
|
|
21835
23786
|
static readCache(key) {
|
|
21836
23787
|
try {
|
|
21837
23788
|
const cacheDir = globalCacheDir();
|
|
21838
|
-
const cacheFile =
|
|
23789
|
+
const cacheFile = path26.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
|
|
21839
23790
|
if (!fs11.existsSync(cacheFile)) return null;
|
|
21840
23791
|
const raw = fs11.readFileSync(cacheFile, "utf8");
|
|
21841
23792
|
const data = JSON.parse(raw);
|
|
@@ -21849,7 +23800,7 @@ var ModelCatalog = class {
|
|
|
21849
23800
|
try {
|
|
21850
23801
|
const cacheDir = globalCacheDir();
|
|
21851
23802
|
fs11.mkdirSync(cacheDir, { recursive: true });
|
|
21852
|
-
const cacheFile =
|
|
23803
|
+
const cacheFile = path26.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
|
|
21853
23804
|
fs11.writeFileSync(
|
|
21854
23805
|
cacheFile,
|
|
21855
23806
|
JSON.stringify({ models, cachedAt: Date.now() }, null, 2)
|
|
@@ -22269,6 +24220,14 @@ function plannerToolHandler(host) {
|
|
|
22269
24220
|
const outcome = await host.runCommand(command, signal);
|
|
22270
24221
|
return { text: outcome.output || (outcome.success ? "(no output)" : "The command failed with no output."), ...outcome.success ? {} : { isError: true } };
|
|
22271
24222
|
},
|
|
24223
|
+
async fetch({ url }) {
|
|
24224
|
+
return outcomeReply(await host.fetchUrl(url));
|
|
24225
|
+
},
|
|
24226
|
+
async webSearch({ query }) {
|
|
24227
|
+
const outcome = await host.webSearch(query);
|
|
24228
|
+
if (!outcome) return { isError: true, text: "Web search is not available in this session. If you already know a documentation URL, fetch it instead." };
|
|
24229
|
+
return outcomeReply(outcome);
|
|
24230
|
+
},
|
|
22272
24231
|
async listRunners() {
|
|
22273
24232
|
const catalog = await host.liveCatalog();
|
|
22274
24233
|
return answer({
|
|
@@ -22423,6 +24382,9 @@ function catalogDetail(catalog) {
|
|
|
22423
24382
|
})
|
|
22424
24383
|
};
|
|
22425
24384
|
}
|
|
24385
|
+
function outcomeReply(outcome) {
|
|
24386
|
+
return { text: outcome.output || (outcome.success ? "(nothing returned)" : "The read failed with no message."), ...outcome.success ? {} : { isError: true } };
|
|
24387
|
+
}
|
|
22426
24388
|
function answer(body) {
|
|
22427
24389
|
return { text: JSON.stringify(body) };
|
|
22428
24390
|
}
|
|
@@ -22519,15 +24481,28 @@ var TurnStream = class {
|
|
|
22519
24481
|
};
|
|
22520
24482
|
|
|
22521
24483
|
// src/services/harness/plannerPermissions.ts
|
|
22522
|
-
var
|
|
24484
|
+
var import_fs5 = require("fs");
|
|
24485
|
+
var UNKNOWN_TOOL_DENIAL = "The Ordewell planner is read-only and its envelope has no rule for this tool, so it did not run. Read with the read tools, use run_command for commands and fetch or web_search for the web; changes belong to the plan's tasks.";
|
|
22523
24486
|
var OPS_HINT = "Continue without it. If it would change something outside the repository, add it to the plan as an ops task; if it only reads, ask the user to approve it or add it to the planner allowlist.";
|
|
22524
24487
|
var ALLOW2 = { decision: "allow" };
|
|
22525
24488
|
var SUBJECT_MAX_CHARS = 400;
|
|
22526
24489
|
function deny(note) {
|
|
22527
24490
|
return { decision: "deny", note };
|
|
22528
24491
|
}
|
|
24492
|
+
function onDisk2(target) {
|
|
24493
|
+
try {
|
|
24494
|
+
return import_fs5.realpathSync.native(target);
|
|
24495
|
+
} catch {
|
|
24496
|
+
return void 0;
|
|
24497
|
+
}
|
|
24498
|
+
}
|
|
24499
|
+
function readTarget(root, target) {
|
|
24500
|
+
const written = resolveWithin(root, target);
|
|
24501
|
+
const real = onDisk2(written.abs);
|
|
24502
|
+
return real === void 0 ? written : resolveWithin(onDisk2(root) ?? root, real);
|
|
24503
|
+
}
|
|
22529
24504
|
async function decidePlannerPermission(ask, input, gate) {
|
|
22530
|
-
if (!ask || ask.kind === "other") return deny(
|
|
24505
|
+
if (!ask || ask.kind === "other") return deny(ask?.note ?? UNKNOWN_TOOL_DENIAL);
|
|
22531
24506
|
if (!gate) return deny("Nobody can approve this here. Continue without it.");
|
|
22532
24507
|
switch (ask.kind) {
|
|
22533
24508
|
case "mcp": {
|
|
@@ -22542,11 +24517,25 @@ async function decidePlannerPermission(ask, input, gate) {
|
|
|
22542
24517
|
});
|
|
22543
24518
|
return granted ? ALLOW2 : deny(`Not approved: ${ask.scope}. ${OPS_HINT}`);
|
|
22544
24519
|
}
|
|
22545
|
-
case "fetch":
|
|
24520
|
+
case "fetch": {
|
|
22546
24521
|
if (!gate.fetcher) return deny("This fetch cannot be approved here.");
|
|
22547
|
-
|
|
24522
|
+
const decision = await gate.fetcher.decide({ kind: "fetch", url: ask.url });
|
|
24523
|
+
return decision.allowed ? ALLOW2 : deny(decision.reason);
|
|
24524
|
+
}
|
|
24525
|
+
case "search": {
|
|
24526
|
+
if (!gate.fetcher) return deny("This search cannot be approved here.");
|
|
24527
|
+
const decision = await gate.fetcher.decide({
|
|
24528
|
+
kind: "search",
|
|
24529
|
+
query: ask.query,
|
|
24530
|
+
provider: ask.provider,
|
|
24531
|
+
providerLabel: ask.providerLabel,
|
|
24532
|
+
// It runs at the provider, which already holds the conversation the query came from.
|
|
24533
|
+
note: `${ask.provider} runs this search itself and already sees this planner's conversation.`
|
|
24534
|
+
});
|
|
24535
|
+
return decision.allowed ? ALLOW2 : deny(decision.reason);
|
|
24536
|
+
}
|
|
22548
24537
|
case "path": {
|
|
22549
|
-
const { abs, inside } =
|
|
24538
|
+
const { abs, inside } = readTarget(gate.workspaceRoot, ask.path);
|
|
22550
24539
|
if (inside) return ALLOW2;
|
|
22551
24540
|
const granted = await gate.approval.request({
|
|
22552
24541
|
kind: "external_path",
|
|
@@ -22578,23 +24567,31 @@ var CliAgentAiService = class {
|
|
|
22578
24567
|
const runner = runnerForProvider(config.aiProvider);
|
|
22579
24568
|
if (!runner) throw new Error(`${config.aiProvider} is not a coding-agent planner.`);
|
|
22580
24569
|
this.runner = runner;
|
|
24570
|
+
const spawn2 = deps.spawn ?? import_child_process8.spawn;
|
|
22581
24571
|
this.processDeps = {
|
|
22582
|
-
spawn:
|
|
24572
|
+
spawn: (command, args, options) => {
|
|
24573
|
+
const child = spawn2(command, args, options);
|
|
24574
|
+
this.environmentSecrets = sensitiveEnvValues(options.env);
|
|
24575
|
+
return child;
|
|
24576
|
+
},
|
|
22583
24577
|
fetch: deps.fetch ?? globalThis.fetch,
|
|
22584
24578
|
resolvePath: deps.resolvePath,
|
|
22585
24579
|
platform: deps.platform,
|
|
22586
24580
|
isDirectory: deps.isDirectory,
|
|
22587
|
-
exists: deps.exists
|
|
24581
|
+
exists: deps.exists,
|
|
24582
|
+
workspaceEnv: deps.workspaceEnv
|
|
22588
24583
|
};
|
|
22589
24584
|
this.makeAdapter = deps.createAdapter ?? createPlannerAdapter;
|
|
22590
24585
|
this.workspaceRoot = deps.workspaceRoot ?? (() => process.cwd());
|
|
22591
24586
|
this.mcpServer = deps.mcpServer ?? sharedMcpServer();
|
|
24587
|
+
this.projectServerApprovals = deps.projectServerApprovals ?? fileProjectServerApprovals();
|
|
22592
24588
|
}
|
|
22593
24589
|
config;
|
|
22594
24590
|
runner;
|
|
22595
24591
|
processDeps;
|
|
22596
24592
|
makeAdapter;
|
|
22597
24593
|
workspaceRoot;
|
|
24594
|
+
environmentSecrets = [];
|
|
22598
24595
|
adapter = null;
|
|
22599
24596
|
/**
|
|
22600
24597
|
* The agent's own session id, kept across a process death so the next turn
|
|
@@ -22604,9 +24601,18 @@ var CliAgentAiService = class {
|
|
|
22604
24601
|
lastNativeSessionId = null;
|
|
22605
24602
|
/** `gate` is what the agent's own tool requests are decided against; absent, only Ordewell's tools are allowed (ADR-0026). */
|
|
22606
24603
|
conversation = null;
|
|
24604
|
+
/**
|
|
24605
|
+
* What the runner's own session-wide search is decided against
|
|
24606
|
+
* (ADR-0027), asked again before every turn: the session's mode, its
|
|
24607
|
+
* exclusions or its budget can change between turns. Absent, it is off.
|
|
24608
|
+
*/
|
|
24609
|
+
searchesUnasked;
|
|
22607
24610
|
mcpServer;
|
|
24611
|
+
projectServerApprovals;
|
|
22608
24612
|
/** The planner token the running process was spawned with; revoked with the process (ADR-0022, A3). */
|
|
22609
24613
|
plannerToken = null;
|
|
24614
|
+
/** The {@link PlannerStartOptions.nativeSearch} the running process was spawned with. */
|
|
24615
|
+
adapterNativeSearch = false;
|
|
22610
24616
|
activeAbort = null;
|
|
22611
24617
|
/**
|
|
22612
24618
|
* Every subagent this conversation has reported starting, and finishing.
|
|
@@ -22637,12 +24643,17 @@ var CliAgentAiService = class {
|
|
|
22637
24643
|
nativeSessionId() {
|
|
22638
24644
|
return this.adapter?.nativeSessionId() ?? null;
|
|
22639
24645
|
}
|
|
24646
|
+
heldSecrets() {
|
|
24647
|
+
return this.environmentSecrets;
|
|
24648
|
+
}
|
|
22640
24649
|
reset() {
|
|
22641
24650
|
this.activeAbort?.abort();
|
|
22642
24651
|
this.activeAbort = null;
|
|
22643
24652
|
this.disposeAdapter();
|
|
24653
|
+
this.environmentSecrets = [];
|
|
22644
24654
|
this.lastNativeSessionId = null;
|
|
22645
24655
|
this.conversation = null;
|
|
24656
|
+
this.searchesUnasked = void 0;
|
|
22646
24657
|
this.subagents.started.clear();
|
|
22647
24658
|
this.subagents.finished.clear();
|
|
22648
24659
|
}
|
|
@@ -22662,15 +24673,19 @@ var CliAgentAiService = class {
|
|
|
22662
24673
|
req.autonomousDefault ?? true,
|
|
22663
24674
|
{ harness: true, isolatedExecution: req.isolatedExecution, skills: req.skills }
|
|
22664
24675
|
);
|
|
24676
|
+
const fetcher = req.fetcher;
|
|
24677
|
+
const cwd = this.workspaceRoot();
|
|
22665
24678
|
const startOptions = {
|
|
22666
24679
|
kind: "planner",
|
|
22667
|
-
cwd
|
|
22668
|
-
systemPrompt,
|
|
24680
|
+
cwd,
|
|
24681
|
+
...await this.repoConfig(systemPrompt, cwd, req.approval),
|
|
22669
24682
|
model: this.plannerModel(),
|
|
22670
24683
|
effort: this.config.plannerThinkingEffort
|
|
22671
24684
|
};
|
|
24685
|
+
if (req.approval && fetcher?.searchesUnasked) this.searchesUnasked = (provider) => fetcher.searchesUnasked?.(provider) ?? false;
|
|
22672
24686
|
const tools = req.plannerTools;
|
|
22673
|
-
await this.startAdapter(startOptions, tools);
|
|
24687
|
+
const adapter = await this.startAdapter(startOptions, tools);
|
|
24688
|
+
recordUnusable(adapter.unusableProjectServers?.() ?? [], req.approval);
|
|
22674
24689
|
const gate = req.approval ? { approval: req.approval, fetcher: req.fetcher, workspaceRoot: startOptions.cwd } : void 0;
|
|
22675
24690
|
this.conversation = { startOptions, tools, gate };
|
|
22676
24691
|
const opening = this.openingMessage(req);
|
|
@@ -22905,25 +24920,26 @@ var CliAgentAiService = class {
|
|
|
22905
24920
|
}
|
|
22906
24921
|
/** One spawn with the Ordewell server injected: the adapter once the runner reports it connected, otherwise why not, with nothing left running. */
|
|
22907
24922
|
async startWithTools(opts, tools) {
|
|
22908
|
-
const
|
|
22909
|
-
if (!
|
|
24923
|
+
const server4 = this.mcpServer;
|
|
24924
|
+
if (!server4) return "no Ordewell MCP server was given";
|
|
22910
24925
|
const adapter = this.newAdapter();
|
|
22911
24926
|
if (!adapter.mcpAttached) {
|
|
22912
24927
|
adapter.dispose();
|
|
22913
24928
|
return `${this.runner} cannot be handed Ordewell's MCP server`;
|
|
22914
24929
|
}
|
|
24930
|
+
const start = { ...opts, nativeSearch: this.nativeSearch() };
|
|
22915
24931
|
let credential;
|
|
22916
24932
|
try {
|
|
22917
|
-
credential = await
|
|
24933
|
+
credential = await server4.issuePlannerToken({ sessionId: tools.sessionId, hidden: connectorFor(this.runner)?.ordewellTools.plannerHidden(start) }, tools.handler);
|
|
22918
24934
|
} catch (err) {
|
|
22919
24935
|
adapter.dispose();
|
|
22920
24936
|
return `Ordewell's MCP server could not issue a token (${err instanceof Error ? err.message : String(err)})`;
|
|
22921
24937
|
}
|
|
22922
24938
|
try {
|
|
22923
|
-
await adapter.start({ ...
|
|
24939
|
+
await adapter.start({ ...start, mcp: mcpClientConfig(credential) });
|
|
22924
24940
|
} catch (err) {
|
|
22925
24941
|
adapter.dispose();
|
|
22926
|
-
|
|
24942
|
+
server4.revoke(credential.token);
|
|
22927
24943
|
throw err;
|
|
22928
24944
|
}
|
|
22929
24945
|
let failure = `${this.runner} did not report Ordewell's MCP server connected`;
|
|
@@ -22931,13 +24947,14 @@ var CliAgentAiService = class {
|
|
|
22931
24947
|
if (await adapter.mcpAttached()) {
|
|
22932
24948
|
this.adapter = adapter;
|
|
22933
24949
|
this.plannerToken = credential.token;
|
|
24950
|
+
this.adapterNativeSearch = start.nativeSearch ?? false;
|
|
22934
24951
|
return adapter;
|
|
22935
24952
|
}
|
|
22936
24953
|
} catch (err) {
|
|
22937
24954
|
failure = `${this.runner} could not check Ordewell's MCP attach state (${err instanceof Error ? err.message : String(err)})`;
|
|
22938
24955
|
}
|
|
22939
24956
|
adapter.dispose();
|
|
22940
|
-
|
|
24957
|
+
server4.revoke(credential.token);
|
|
22941
24958
|
return failure;
|
|
22942
24959
|
}
|
|
22943
24960
|
/** Kill the planner process, and with it the token it was spawned with. */
|
|
@@ -22947,19 +24964,46 @@ var CliAgentAiService = class {
|
|
|
22947
24964
|
if (this.plannerToken) this.mcpServer?.revoke(this.plannerToken);
|
|
22948
24965
|
this.plannerToken = null;
|
|
22949
24966
|
}
|
|
24967
|
+
/** Whether the web-read policy lets the runner's own session-wide search run unasked now (ADR-0027). */
|
|
24968
|
+
nativeSearch() {
|
|
24969
|
+
const provider = connectorFor(this.runner)?.ordewellTools.sessionSearchProvider;
|
|
24970
|
+
return provider !== void 0 && (this.searchesUnasked?.(provider) ?? false);
|
|
24971
|
+
}
|
|
22950
24972
|
/**
|
|
22951
24973
|
* The live agent process, restarted from its own session id if it died
|
|
22952
24974
|
* between turns. Resume is a hint: when it fails, the caller's next
|
|
22953
24975
|
* `startConversation` reseeds from Ordewell's transcript, which is the same
|
|
22954
24976
|
* degradation `restoreChat` already performs on every surface.
|
|
24977
|
+
*
|
|
24978
|
+
* Also restarted when the policy's decision on the runner's own search has
|
|
24979
|
+
* changed since the process was spawned: the running thread would otherwise
|
|
24980
|
+
* keep searching unasked after Full turned Guarded. Codex 0.162 applies
|
|
24981
|
+
* `thread/resume`'s `config.web_search` either way (checked live).
|
|
22955
24982
|
*/
|
|
22956
24983
|
async ensureAdapter() {
|
|
22957
|
-
if (this.adapter) return this.adapter;
|
|
24984
|
+
if (this.adapter && this.adapterNativeSearch === this.nativeSearch()) return this.adapter;
|
|
24985
|
+
this.disposeAdapter();
|
|
22958
24986
|
const conversation = this.conversation;
|
|
22959
24987
|
if (!conversation) throw new Error("No active planner conversation.");
|
|
22960
24988
|
await this.startAdapter({ ...conversation.startOptions, resumeSessionId: this.lastNativeSessionId ?? void 0 }, conversation.tools);
|
|
22961
24989
|
return this.adapter;
|
|
22962
24990
|
}
|
|
24991
|
+
/**
|
|
24992
|
+
* What the planner gets of the repository's config, its runner being
|
|
24993
|
+
* launched without it (ADR-0028): the instruction files, which are text, in
|
|
24994
|
+
* its system prompt, and the project MCP servers it may start. Decided once
|
|
24995
|
+
* per conversation, so a respawn starts the same servers without asking.
|
|
24996
|
+
*/
|
|
24997
|
+
async repoConfig(systemPrompt, cwd, approval) {
|
|
24998
|
+
const reader = connectorFor(this.runner)?.repoConfig;
|
|
24999
|
+
if (!reader) return { systemPrompt };
|
|
25000
|
+
const found = reader.projectServers(cwd);
|
|
25001
|
+
const { started, held } = await decideProjectServers(found.servers, { repoRoot: repoRoot(cwd), approval, approvals: this.projectServerApprovals });
|
|
25002
|
+
return {
|
|
25003
|
+
systemPrompt: [systemPrompt, instructionsBlock(reader.instructions(cwd)), heldServersNote(held.map((server4) => ({ server: server4, reason: NOT_APPROVED })), found.unreadable)].filter(Boolean).join("\n\n"),
|
|
25004
|
+
...started.length > 0 ? { projectServers: started } : {}
|
|
25005
|
+
};
|
|
25006
|
+
}
|
|
22963
25007
|
plannerModel() {
|
|
22964
25008
|
const id = (this.config.orchestratorModel ?? "").trim();
|
|
22965
25009
|
return id || void 0;
|
|
@@ -23002,8 +25046,11 @@ ${corrective}`, catalog, onProgress, signal),
|
|
|
23002
25046
|
const previousConversation = this.conversation;
|
|
23003
25047
|
const previousSessionId = this.lastNativeSessionId;
|
|
23004
25048
|
const previousToken = this.plannerToken;
|
|
25049
|
+
const previousNativeSearch = this.adapterNativeSearch;
|
|
25050
|
+
const previousSearchesUnasked = this.searchesUnasked;
|
|
23005
25051
|
this.adapter = null;
|
|
23006
25052
|
this.plannerToken = null;
|
|
25053
|
+
this.searchesUnasked = void 0;
|
|
23007
25054
|
let submitted;
|
|
23008
25055
|
let refused3 = [];
|
|
23009
25056
|
let ended = false;
|
|
@@ -23034,11 +25081,13 @@ ${corrective}`, catalog, onProgress, signal),
|
|
|
23034
25081
|
}
|
|
23035
25082
|
}
|
|
23036
25083
|
};
|
|
25084
|
+
const cwd = this.workspaceRoot();
|
|
23037
25085
|
const startOptions = {
|
|
23038
25086
|
kind: "planner",
|
|
23039
|
-
cwd
|
|
23040
|
-
|
|
23041
|
-
|
|
25087
|
+
cwd,
|
|
25088
|
+
// No approval channel here: only servers a person approved before start.
|
|
25089
|
+
...await this.repoConfig(`${prompt}
|
|
25090
|
+
Before submitting, call list_runners and list_models to check the available assignments.`, cwd, void 0),
|
|
23042
25091
|
model: this.plannerModel(),
|
|
23043
25092
|
effort: this.config.plannerThinkingEffort
|
|
23044
25093
|
};
|
|
@@ -23064,6 +25113,8 @@ Before submitting, call list_runners and list_models to check the available assi
|
|
|
23064
25113
|
if (this.plannerToken) this.mcpServer?.revoke(this.plannerToken);
|
|
23065
25114
|
this.adapter = previous;
|
|
23066
25115
|
this.plannerToken = previousToken;
|
|
25116
|
+
this.adapterNativeSearch = previousNativeSearch;
|
|
25117
|
+
this.searchesUnasked = previousSearchesUnasked;
|
|
23067
25118
|
this.conversation = previousConversation;
|
|
23068
25119
|
this.lastNativeSessionId = previousSessionId;
|
|
23069
25120
|
}
|
|
@@ -24135,213 +26186,6 @@ function forkPlanState(plan, tasks, dialogue, now) {
|
|
|
24135
26186
|
};
|
|
24136
26187
|
}
|
|
24137
26188
|
|
|
24138
|
-
// src/services/HttpWebFetcher.ts
|
|
24139
|
-
var import_promises = require("node:dns/promises");
|
|
24140
|
-
var DEFAULT_MAX_CHARS = 4e4;
|
|
24141
|
-
var DEFAULT_TIMEOUT_MS2 = 2e4;
|
|
24142
|
-
var MAX_BODY_BYTES = 2e6;
|
|
24143
|
-
var MAX_REDIRECTS = 5;
|
|
24144
|
-
var USER_AGENT = "Mozilla/5.0 (compatible; Ordewell-Planner/1.0; +https://github.com/ordewell)";
|
|
24145
|
-
var SEARCH_ENDPOINT = "https://lite.duckduckgo.com/lite/?q=";
|
|
24146
|
-
function decodeEntities(text2) {
|
|
24147
|
-
return text2.replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/'/g, "'");
|
|
24148
|
-
}
|
|
24149
|
-
function htmlToText(html) {
|
|
24150
|
-
return decodeEntities(
|
|
24151
|
-
html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ").replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ").replace(/<noscript\b[^>]*>[\s\S]*?<\/noscript>/gi, " ").replace(/<\/(p|div|li|h[1-6]|tr|section|article)>/gi, "\n").replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, " ")
|
|
24152
|
-
).replace(/[ \t\f\v]+/g, " ").replace(/\n\s*\n\s*\n+/g, "\n\n").split("\n").map((line) => line.trim()).join("\n").trim();
|
|
24153
|
-
}
|
|
24154
|
-
function parseSearchHits(html) {
|
|
24155
|
-
const hits = [];
|
|
24156
|
-
const linkPattern = /<a\b[^>]*class="[^"]*result(?:-link|__a)[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
24157
|
-
const snippetPattern = /class="[^"]*result(?:-snippet|__snippet)[^"]*"[^>]*>([\s\S]*?)</gi;
|
|
24158
|
-
const snippets = [];
|
|
24159
|
-
for (let m = snippetPattern.exec(html); m; m = snippetPattern.exec(html)) {
|
|
24160
|
-
snippets.push(decodeEntities(m[1].replace(/<[^>]+>/g, " ")).replace(/\s+/g, " ").trim());
|
|
24161
|
-
}
|
|
24162
|
-
for (let m = linkPattern.exec(html); m; m = linkPattern.exec(html)) {
|
|
24163
|
-
const title = decodeEntities(m[2].replace(/<[^>]+>/g, " ")).replace(/\s+/g, " ").trim();
|
|
24164
|
-
if (!title) continue;
|
|
24165
|
-
hits.push({ title, url: decodeEntities(m[1]), snippet: snippets[hits.length] });
|
|
24166
|
-
}
|
|
24167
|
-
return hits;
|
|
24168
|
-
}
|
|
24169
|
-
function isPrivateIp(ip) {
|
|
24170
|
-
if (!ip) return true;
|
|
24171
|
-
if (ip === "0.0.0.0" || ip === "::" || ip === "::1" || ip === "::ffff:127.0.0.1") return true;
|
|
24172
|
-
if (/^127\./.test(ip)) return true;
|
|
24173
|
-
if (/^169\.254\./.test(ip)) return true;
|
|
24174
|
-
if (/^10\./.test(ip)) return true;
|
|
24175
|
-
if (/^192\.168\./.test(ip)) return true;
|
|
24176
|
-
const m172 = /^172\.(\d+)\./.exec(ip);
|
|
24177
|
-
if (m172 && Number(m172[1]) >= 16 && Number(m172[1]) <= 31) return true;
|
|
24178
|
-
if (/^fe[89ab][0-9a-f]/i.test(ip)) return true;
|
|
24179
|
-
return false;
|
|
24180
|
-
}
|
|
24181
|
-
function isIpLiteral(host) {
|
|
24182
|
-
return /^\d{1,3}(\.\d{1,3}){3}$/.test(host) || /^\[?[0-9a-fA-F:]+\]?$/i.test(host);
|
|
24183
|
-
}
|
|
24184
|
-
function sanitizeError(err) {
|
|
24185
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
24186
|
-
if (/abort/i.test(msg)) return "timed out";
|
|
24187
|
-
if (/ECONNREFUSED|ECONNRESET|EHOSTUNREACH|ENETUNREACH/i.test(msg)) return "host not reachable";
|
|
24188
|
-
if (/ENOTFOUND|getaddrinfo/i.test(msg)) return "host not found";
|
|
24189
|
-
return msg.replace(/https?:\/\/\S+/g, "<url>");
|
|
24190
|
-
}
|
|
24191
|
-
var HttpWebFetcher = class {
|
|
24192
|
-
approval;
|
|
24193
|
-
fetchImpl;
|
|
24194
|
-
maxChars;
|
|
24195
|
-
timeoutMs;
|
|
24196
|
-
isBlockedHost;
|
|
24197
|
-
resolveHost;
|
|
24198
|
-
constructor(opts) {
|
|
24199
|
-
this.approval = opts.approval;
|
|
24200
|
-
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
24201
|
-
this.maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
|
|
24202
|
-
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
24203
|
-
this.isBlockedHost = opts.isBlockedHost ?? this.defaultIsBlocked.bind(this);
|
|
24204
|
-
this.resolveHost = opts.resolveHost ?? (async (h) => {
|
|
24205
|
-
const r = await (0, import_promises.lookup)(h);
|
|
24206
|
-
return [r.address];
|
|
24207
|
-
});
|
|
24208
|
-
}
|
|
24209
|
-
async defaultIsBlocked(host, port) {
|
|
24210
|
-
if (!["", "80", "443"].includes(port)) return true;
|
|
24211
|
-
if (isIpLiteral(host)) return isPrivateIp(host.startsWith("[") ? host.slice(1, -1) : host);
|
|
24212
|
-
const lower = host.toLowerCase();
|
|
24213
|
-
if (lower === "localhost" || lower === "ip6-localhost" || lower === "ip6-loopback") return true;
|
|
24214
|
-
try {
|
|
24215
|
-
const addrs = await this.resolveHost(host);
|
|
24216
|
-
if (addrs.some((a) => isPrivateIp(a))) return true;
|
|
24217
|
-
} catch {
|
|
24218
|
-
return true;
|
|
24219
|
-
}
|
|
24220
|
-
return false;
|
|
24221
|
-
}
|
|
24222
|
-
async confirm(url) {
|
|
24223
|
-
let parsed;
|
|
24224
|
-
try {
|
|
24225
|
-
parsed = new URL(url);
|
|
24226
|
-
} catch {
|
|
24227
|
-
return false;
|
|
24228
|
-
}
|
|
24229
|
-
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return false;
|
|
24230
|
-
if (await this.isBlockedHost(parsed.hostname, parsed.port)) return false;
|
|
24231
|
-
return this.approval.request({
|
|
24232
|
-
kind: "url_fetch",
|
|
24233
|
-
subject: url,
|
|
24234
|
-
scope: `${parsed.origin}/*`,
|
|
24235
|
-
detail: `Planner research wants to fetch ${parsed.origin}`
|
|
24236
|
-
});
|
|
24237
|
-
}
|
|
24238
|
-
async fetch(url) {
|
|
24239
|
-
if (!await this.confirm(url)) {
|
|
24240
|
-
return { success: false, output: `Fetch denied (blocked host or not approved): ${url}. Continue without it, or ask the user to approve this origin or add it to the planner allowlist.`, truncated: false };
|
|
24241
|
-
}
|
|
24242
|
-
return this.get(url, "Fetch");
|
|
24243
|
-
}
|
|
24244
|
-
async search(query) {
|
|
24245
|
-
const url = SEARCH_ENDPOINT + encodeURIComponent(query);
|
|
24246
|
-
if (!await this.confirm(url)) {
|
|
24247
|
-
return { success: false, output: `Web search denied by user for query: ${query}`, truncated: false };
|
|
24248
|
-
}
|
|
24249
|
-
const page = await this.request(url);
|
|
24250
|
-
if (!page.ok) return { success: false, output: `Web search failed: ${page.error}`, truncated: false };
|
|
24251
|
-
const hits = parseSearchHits(page.body).slice(0, 10);
|
|
24252
|
-
if (hits.length === 0) {
|
|
24253
|
-
return {
|
|
24254
|
-
success: false,
|
|
24255
|
-
output: `No results could be parsed for "${query}". Search is best-effort; if you already know a documentation URL, use the fetch tool directly.`,
|
|
24256
|
-
truncated: false
|
|
24257
|
-
};
|
|
24258
|
-
}
|
|
24259
|
-
const output = hits.map((hit, i) => `${i + 1}. ${hit.title}
|
|
24260
|
-
${hit.url}${hit.snippet ? `
|
|
24261
|
-
${hit.snippet}` : ""}`).join("\n\n");
|
|
24262
|
-
return { success: true, output, truncated: false };
|
|
24263
|
-
}
|
|
24264
|
-
async get(url, label) {
|
|
24265
|
-
const page = await this.request(url);
|
|
24266
|
-
if (!page.ok) return { success: false, output: `${label} failed: ${page.error}`, truncated: false };
|
|
24267
|
-
const text2 = page.isHtml ? htmlToText(page.body) : page.body;
|
|
24268
|
-
if (page.truncated || text2.length > this.maxChars) {
|
|
24269
|
-
const sliced = text2.slice(0, this.maxChars);
|
|
24270
|
-
return {
|
|
24271
|
-
success: true,
|
|
24272
|
-
output: `${sliced}
|
|
24273
|
-
|
|
24274
|
-
[... truncated to ${this.maxChars} chars, total ${text2.length}${page.truncated ? "+, body was byte-capped at the source" : ""}]`,
|
|
24275
|
-
truncated: true
|
|
24276
|
-
};
|
|
24277
|
-
}
|
|
24278
|
-
return { success: true, output: text2, truncated: false };
|
|
24279
|
-
}
|
|
24280
|
-
async request(url) {
|
|
24281
|
-
let current = url;
|
|
24282
|
-
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
24283
|
-
const controller = new AbortController();
|
|
24284
|
-
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
24285
|
-
try {
|
|
24286
|
-
const res = await this.fetchImpl(current, {
|
|
24287
|
-
signal: controller.signal,
|
|
24288
|
-
headers: { "user-agent": USER_AGENT, accept: "text/html,application/xhtml+xml,application/json;q=0.9,*/*;q=0.8" },
|
|
24289
|
-
redirect: "manual"
|
|
24290
|
-
});
|
|
24291
|
-
if (res.status >= 300 && res.status < 400) {
|
|
24292
|
-
const location = res.headers.get("location");
|
|
24293
|
-
if (!location) return { ok: false, error: `redirect (${res.status}) with no Location` };
|
|
24294
|
-
const next = new URL(location, current).href;
|
|
24295
|
-
const nextParsed = new URL(next);
|
|
24296
|
-
if (nextParsed.protocol !== "https:" && nextParsed.protocol !== "http:") {
|
|
24297
|
-
return { ok: false, error: "redirect to a non-HTTP scheme" };
|
|
24298
|
-
}
|
|
24299
|
-
if (await this.isBlockedHost(nextParsed.hostname, nextParsed.port)) {
|
|
24300
|
-
return { ok: false, error: "redirect to a blocked host" };
|
|
24301
|
-
}
|
|
24302
|
-
current = next;
|
|
24303
|
-
continue;
|
|
24304
|
-
}
|
|
24305
|
-
if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
|
|
24306
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
24307
|
-
const { body, truncated } = await readBounded(res, MAX_BODY_BYTES);
|
|
24308
|
-
return { ok: true, body, isHtml: /html|xml/i.test(contentType), truncated };
|
|
24309
|
-
} catch (err) {
|
|
24310
|
-
return { ok: false, error: sanitizeError(err) };
|
|
24311
|
-
} finally {
|
|
24312
|
-
clearTimeout(timer);
|
|
24313
|
-
}
|
|
24314
|
-
}
|
|
24315
|
-
return { ok: false, error: `too many redirects (>${MAX_REDIRECTS})` };
|
|
24316
|
-
}
|
|
24317
|
-
};
|
|
24318
|
-
async function readBounded(res, maxBytes) {
|
|
24319
|
-
const reader = res.body?.[Symbol.asyncIterator] ? readFromIterable(res.body, maxBytes) : readFromText(res, maxBytes);
|
|
24320
|
-
return reader;
|
|
24321
|
-
}
|
|
24322
|
-
async function readFromIterable(source, maxBytes) {
|
|
24323
|
-
const chunks = [];
|
|
24324
|
-
let total = 0;
|
|
24325
|
-
let truncated = false;
|
|
24326
|
-
for await (const chunk of source) {
|
|
24327
|
-
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
24328
|
-
if (total + buf.length > maxBytes) {
|
|
24329
|
-
chunks.push(buf.subarray(0, maxBytes - total));
|
|
24330
|
-
total = maxBytes;
|
|
24331
|
-
truncated = true;
|
|
24332
|
-
break;
|
|
24333
|
-
}
|
|
24334
|
-
chunks.push(buf);
|
|
24335
|
-
total += buf.length;
|
|
24336
|
-
}
|
|
24337
|
-
return { body: Buffer.concat(chunks).toString("utf8"), truncated };
|
|
24338
|
-
}
|
|
24339
|
-
async function readFromText(res, maxBytes) {
|
|
24340
|
-
const text2 = await res.text();
|
|
24341
|
-
if (Buffer.byteLength(text2, "utf8") <= maxBytes) return { body: text2, truncated: false };
|
|
24342
|
-
return { body: Buffer.from(text2, "utf8").subarray(0, maxBytes).toString("utf8"), truncated: true };
|
|
24343
|
-
}
|
|
24344
|
-
|
|
24345
26189
|
// src/services/SessionMessage.ts
|
|
24346
26190
|
function serializeTask(t) {
|
|
24347
26191
|
return {
|
|
@@ -24390,7 +26234,7 @@ function surfaceQueued(queued) {
|
|
|
24390
26234
|
function surfaceTask({ attemptSkills, subtasks, runnerSessionId: _runnerSessionId, ...task }) {
|
|
24391
26235
|
return {
|
|
24392
26236
|
...task,
|
|
24393
|
-
...attemptSkills ? { attemptSkills: attemptSkills.map(({ name, source, path:
|
|
26237
|
+
...attemptSkills ? { attemptSkills: attemptSkills.map(({ name, source, path: path32 }) => ({ name, source, path: path32 })) } : {},
|
|
24394
26238
|
subtasks: (subtasks ?? []).map(surfaceTask)
|
|
24395
26239
|
};
|
|
24396
26240
|
}
|
|
@@ -24646,24 +26490,24 @@ var SessionEventRelay = class {
|
|
|
24646
26490
|
|
|
24647
26491
|
// src/utils/sessionStore.ts
|
|
24648
26492
|
var fs12 = __toESM(require("fs"));
|
|
24649
|
-
var
|
|
26493
|
+
var path27 = __toESM(require("path"));
|
|
24650
26494
|
|
|
24651
26495
|
// src/utils/sessionId.ts
|
|
24652
|
-
var
|
|
26496
|
+
var import_crypto11 = require("crypto");
|
|
24653
26497
|
function mintSessionId() {
|
|
24654
|
-
return `session-${(0,
|
|
26498
|
+
return `session-${(0, import_crypto11.randomBytes)(16).toString("hex")}`;
|
|
24655
26499
|
}
|
|
24656
26500
|
|
|
24657
26501
|
// src/utils/sessionStore.ts
|
|
24658
26502
|
var SESSIONS_SUBDIR = "sessions";
|
|
24659
26503
|
function getSessionsDir(baseDir) {
|
|
24660
|
-
return
|
|
26504
|
+
return path27.join(getStateDir(baseDir), SESSIONS_SUBDIR);
|
|
24661
26505
|
}
|
|
24662
26506
|
function idSegment(id) {
|
|
24663
26507
|
return /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,127}$/.test(id) ? id : `~${Buffer.from(id, "utf-8").toString("hex")}`;
|
|
24664
26508
|
}
|
|
24665
26509
|
function sessionDataDir(sessionId, baseDir) {
|
|
24666
|
-
return
|
|
26510
|
+
return path27.join(getSessionsDir(baseDir), idSegment(sessionId));
|
|
24667
26511
|
}
|
|
24668
26512
|
var REJECTION_MESSAGE = "This session was created with an older version of Ordewell. Please re-plan.";
|
|
24669
26513
|
function hasScalarRunner(parsed) {
|
|
@@ -24685,8 +26529,8 @@ function legacySessionFilename(session) {
|
|
|
24685
26529
|
return `${filenameStem(session)}_${session.id.slice(0, 8)}.json`;
|
|
24686
26530
|
}
|
|
24687
26531
|
function dropLegacyFile(sessionsDir, meta) {
|
|
24688
|
-
const legacy =
|
|
24689
|
-
if (legacy ===
|
|
26532
|
+
const legacy = path27.join(sessionsDir, legacySessionFilename(meta));
|
|
26533
|
+
if (legacy === path27.join(sessionsDir, sessionFilename(meta)) || !fs12.existsSync(legacy)) return;
|
|
24690
26534
|
try {
|
|
24691
26535
|
const held = JSON.parse(fs12.readFileSync(legacy, "utf-8")).meta?.id;
|
|
24692
26536
|
if (held === meta.id) fs12.unlinkSync(legacy);
|
|
@@ -24712,7 +26556,7 @@ function saveSession(plan, goal, baseDir, id) {
|
|
|
24712
26556
|
plan: JSON.parse(JSON.stringify(plan))
|
|
24713
26557
|
};
|
|
24714
26558
|
const filename = sessionFilename(meta);
|
|
24715
|
-
const filePath =
|
|
26559
|
+
const filePath = path27.join(sessionsDir, filename);
|
|
24716
26560
|
fs12.writeFileSync(filePath, JSON.stringify(session, null, 2));
|
|
24717
26561
|
dropLegacyFile(sessionsDir, meta);
|
|
24718
26562
|
return meta;
|
|
@@ -24723,7 +26567,7 @@ function listSessions(baseDir, logger = defaultLogger) {
|
|
|
24723
26567
|
const files = fs12.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
24724
26568
|
return files.map((f) => {
|
|
24725
26569
|
try {
|
|
24726
|
-
const raw = fs12.readFileSync(
|
|
26570
|
+
const raw = fs12.readFileSync(path27.join(sessionsDir, f), "utf-8");
|
|
24727
26571
|
const parsed = JSON.parse(raw);
|
|
24728
26572
|
if (hasScalarRunner(parsed)) {
|
|
24729
26573
|
logger.warn("sessionStore", `session file ${f}: ${REJECTION_MESSAGE}`);
|
|
@@ -24741,7 +26585,7 @@ function findSessionFile(sessionsDir, sessionId, logger) {
|
|
|
24741
26585
|
const files = fs12.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
24742
26586
|
for (const f of files) {
|
|
24743
26587
|
try {
|
|
24744
|
-
const raw = fs12.readFileSync(
|
|
26588
|
+
const raw = fs12.readFileSync(path27.join(sessionsDir, f), "utf-8");
|
|
24745
26589
|
const session = JSON.parse(raw);
|
|
24746
26590
|
if (session.meta.id === sessionId) return f;
|
|
24747
26591
|
} catch (err) {
|
|
@@ -24756,7 +26600,7 @@ function loadSession(sessionId, baseDir, logger = defaultLogger) {
|
|
|
24756
26600
|
try {
|
|
24757
26601
|
const file = findSessionFile(sessionsDir, sessionId, logger);
|
|
24758
26602
|
if (file) {
|
|
24759
|
-
const raw = fs12.readFileSync(
|
|
26603
|
+
const raw = fs12.readFileSync(path27.join(sessionsDir, file), "utf-8");
|
|
24760
26604
|
const parsed = JSON.parse(raw);
|
|
24761
26605
|
if (hasScalarRunner(parsed)) {
|
|
24762
26606
|
logger.warn("sessionStore", `session file ${file}: ${REJECTION_MESSAGE}`);
|
|
@@ -24797,7 +26641,7 @@ function deleteSession(sessionId, baseDir, logger = defaultLogger) {
|
|
|
24797
26641
|
try {
|
|
24798
26642
|
const file = findSessionFile(sessionsDir, sessionId, logger);
|
|
24799
26643
|
if (file) {
|
|
24800
|
-
fs12.unlinkSync(
|
|
26644
|
+
fs12.unlinkSync(path27.join(sessionsDir, file));
|
|
24801
26645
|
return true;
|
|
24802
26646
|
}
|
|
24803
26647
|
} catch (err) {
|
|
@@ -24808,10 +26652,10 @@ function deleteSession(sessionId, baseDir, logger = defaultLogger) {
|
|
|
24808
26652
|
|
|
24809
26653
|
// src/utils/taskLogStore.ts
|
|
24810
26654
|
var fs13 = __toESM(require("fs"));
|
|
24811
|
-
var
|
|
26655
|
+
var path28 = __toESM(require("path"));
|
|
24812
26656
|
var EXTENSION = ".jsonl";
|
|
24813
26657
|
function taskDir({ baseDir, sessionId }, taskId) {
|
|
24814
|
-
return
|
|
26658
|
+
return path28.join(sessionDataDir(sessionId, baseDir), "tasks", idSegment(taskId));
|
|
24815
26659
|
}
|
|
24816
26660
|
function listTaskLogAttempts(location, taskId) {
|
|
24817
26661
|
let names;
|
|
@@ -24828,7 +26672,7 @@ function openTaskLog(location, taskId) {
|
|
|
24828
26672
|
fs13.mkdirSync(dir, { recursive: true });
|
|
24829
26673
|
let attempt = (listTaskLogAttempts(location, taskId).pop() ?? 0) + 1;
|
|
24830
26674
|
for (; ; ) {
|
|
24831
|
-
const file =
|
|
26675
|
+
const file = path28.join(dir, `${attempt}${EXTENSION}`);
|
|
24832
26676
|
try {
|
|
24833
26677
|
fs13.writeFileSync(file, "", { flag: "wx" });
|
|
24834
26678
|
} catch (err) {
|
|
@@ -24855,7 +26699,7 @@ function readTaskLog(location, taskId, attempt) {
|
|
|
24855
26699
|
if (!Number.isInteger(attempt) || attempt < 1) return [];
|
|
24856
26700
|
let raw;
|
|
24857
26701
|
try {
|
|
24858
|
-
raw = fs13.readFileSync(
|
|
26702
|
+
raw = fs13.readFileSync(path28.join(taskDir(location, taskId), `${attempt}${EXTENSION}`), "utf-8");
|
|
24859
26703
|
} catch {
|
|
24860
26704
|
return [];
|
|
24861
26705
|
}
|
|
@@ -25283,7 +27127,8 @@ function isPlannerApproval(request) {
|
|
|
25283
27127
|
}
|
|
25284
27128
|
function createSession(deps) {
|
|
25285
27129
|
const pinnedAiService = deps.aiService;
|
|
25286
|
-
const
|
|
27130
|
+
const mcpServer = deps.mcpServer ?? sharedMcpServer();
|
|
27131
|
+
const aiService = pinnedAiService ? () => pinnedAiService : liveAiService(deps.config, deps.workspaceRoot, mcpServer);
|
|
25287
27132
|
const store = new PlanStore();
|
|
25288
27133
|
const taskLogs = new TaskLogRecorder({
|
|
25289
27134
|
broadcast: deps.broadcast,
|
|
@@ -25339,8 +27184,9 @@ function createSession(deps) {
|
|
|
25339
27184
|
checkpointQuestion: (taskId) => orchestrator.getCheckpointQuestion(taskId)
|
|
25340
27185
|
});
|
|
25341
27186
|
const approvalPolicy = new ApprovalPolicy({
|
|
27187
|
+
autonomous: () => deps.config.autonomousMode,
|
|
25342
27188
|
// Read per request, so a change of autonomy level or allowlist applies to the next call.
|
|
25343
|
-
mode: () => effectiveApprovalMode(deps.config.approvalMode, deps.config.autonomousMode),
|
|
27189
|
+
mode: (kind) => effectiveApprovalMode(deps.config.approvalMode, deps.config.autonomousMode, kind),
|
|
25344
27190
|
preApproved: () => [...deps.config.approvalDefaults ? DEFAULT_PLANNER_ALLOWLIST : [], ...deps.config.approvalPreApproved],
|
|
25345
27191
|
ask: (req) => approvals.ask(req),
|
|
25346
27192
|
// The interactive path (`asked`) already broadcasts approval_request +
|
|
@@ -25360,6 +27206,14 @@ function createSession(deps) {
|
|
|
25360
27206
|
}
|
|
25361
27207
|
});
|
|
25362
27208
|
deps.fsAdapter.setApproval?.(approvalPolicy);
|
|
27209
|
+
const outboundSecrets = () => [...configSecrets(deps.config), ...mcpServer.heldTokens(), ...aiService().heldSecrets?.() ?? []];
|
|
27210
|
+
deps.fsAdapter.setOutboundSecrets?.(outboundSecrets);
|
|
27211
|
+
const webReads = new WebReadPolicy({
|
|
27212
|
+
approval: approvalPolicy,
|
|
27213
|
+
secrets: outboundSecrets,
|
|
27214
|
+
budget: () => deps.config.webReadBudget,
|
|
27215
|
+
turn: () => conversation?.currentTurnId
|
|
27216
|
+
});
|
|
25363
27217
|
const catalog = new SessionCatalog({
|
|
25364
27218
|
config: deps.config,
|
|
25365
27219
|
registry: deps.registry,
|
|
@@ -25388,15 +27242,28 @@ function createSession(deps) {
|
|
|
25388
27242
|
usage,
|
|
25389
27243
|
approvals,
|
|
25390
27244
|
approvalPolicy,
|
|
25391
|
-
//
|
|
25392
|
-
//
|
|
25393
|
-
|
|
27245
|
+
// Every planner's web reads, carried here or natively, are decided by the
|
|
27246
|
+
// one policy, which asks through the same channel as paths and commands.
|
|
27247
|
+
// The response cache and search provider are process-wide, so parallel
|
|
27248
|
+
// sessions and their subagents share bytes and the search cooldown.
|
|
27249
|
+
fetcher: new HttpWebFetcher({ policy: webReads, cache: sharedWebResponseCache, searchProvider: sharedSearchProvider }),
|
|
25394
27250
|
skillsService: deps.skillsService ?? new SkillsService(deps.workspaceRoot()),
|
|
25395
27251
|
saveSession: deps.saveSession ?? saveSession,
|
|
25396
27252
|
conversation: (host) => conversation = new PlannerConversation(host)
|
|
25397
27253
|
});
|
|
25398
27254
|
return session;
|
|
25399
27255
|
}
|
|
27256
|
+
function configSecrets(config) {
|
|
27257
|
+
const providers = Object.keys(ALL_PROVIDERS);
|
|
27258
|
+
return [
|
|
27259
|
+
config.apiKey,
|
|
27260
|
+
config.openAiApiKey,
|
|
27261
|
+
config.openrouterKey,
|
|
27262
|
+
config.geminiKey,
|
|
27263
|
+
config.openaiCompatibleApiKey,
|
|
27264
|
+
...providers.map((provider) => config.getProviderApiKey(provider))
|
|
27265
|
+
];
|
|
27266
|
+
}
|
|
25400
27267
|
var Session = class {
|
|
25401
27268
|
aiService;
|
|
25402
27269
|
usage;
|
|
@@ -25445,7 +27312,9 @@ var Session = class {
|
|
|
25445
27312
|
liveOutput: (taskId, opts) => this.orchestrator.getLiveOutput(taskId, opts),
|
|
25446
27313
|
lastAttempt: (taskId) => lastAttemptDigest(this.taskLogLocation, taskId),
|
|
25447
27314
|
taskSkills: () => this.workspaceSkills(),
|
|
25448
|
-
runCommand: (command, signal) => this.fsAdapter.bash(command, signal)
|
|
27315
|
+
runCommand: (command, signal) => this.fsAdapter.bash(command, signal),
|
|
27316
|
+
fetchUrl: (url) => this.fetcher.fetch(url),
|
|
27317
|
+
webSearch: async (query) => this.fetcher.search?.(query)
|
|
25449
27318
|
});
|
|
25450
27319
|
constructor(parts) {
|
|
25451
27320
|
this.config = parts.config;
|
|
@@ -27097,10 +28966,10 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
27097
28966
|
if (second) throw new McpAttachError(`Could not start ${this.launch.runner} with Ordewell's tools, so the task was not sent to it. First spawn: ${first}. Respawn: ${second}.`);
|
|
27098
28967
|
}
|
|
27099
28968
|
async startWithTools(opts) {
|
|
27100
|
-
const { server, scope } = this.launch.tools;
|
|
28969
|
+
const { server: server4, scope } = this.launch.tools;
|
|
27101
28970
|
let credential;
|
|
27102
28971
|
try {
|
|
27103
|
-
credential = await
|
|
28972
|
+
credential = await server4.issueTaskToken(scope, {
|
|
27104
28973
|
taskComplete: async (report) => {
|
|
27105
28974
|
this.structuredEmitter.emit("taskComplete", report);
|
|
27106
28975
|
return { text: "Recorded. End your turn now." };
|
|
@@ -27114,7 +28983,7 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
27114
28983
|
return `Ordewell's MCP server could not issue a token (${err instanceof Error ? err.message : String(err)})`;
|
|
27115
28984
|
}
|
|
27116
28985
|
if (this.exited) {
|
|
27117
|
-
|
|
28986
|
+
server4.revoke(credential.token);
|
|
27118
28987
|
return null;
|
|
27119
28988
|
}
|
|
27120
28989
|
this.toolToken = credential.token;
|
|
@@ -27285,7 +29154,7 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
27285
29154
|
}
|
|
27286
29155
|
async interruptTurn(turn, adapter) {
|
|
27287
29156
|
const grace = this.launch.interruptGraceMs;
|
|
27288
|
-
const ended = new Promise((
|
|
29157
|
+
const ended = new Promise((resolve9) => this.structuredEmitter.once("turnEnd", () => resolve9()));
|
|
27289
29158
|
if (await adapter.interrupt(grace)) {
|
|
27290
29159
|
const settled = await settleWithin(ended.then(() => true), { timeoutMs: grace, onTimeout: () => false });
|
|
27291
29160
|
if (settled) return;
|
|
@@ -27826,7 +29695,7 @@ var RunnerRegistry = class {
|
|
|
27826
29695
|
|
|
27827
29696
|
// src/plugins/removedPluginNotice.ts
|
|
27828
29697
|
var fs14 = __toESM(require("fs"));
|
|
27829
|
-
var
|
|
29698
|
+
var path29 = __toESM(require("path"));
|
|
27830
29699
|
function isManifestFile(file) {
|
|
27831
29700
|
try {
|
|
27832
29701
|
return fs14.statSync(file).isFile();
|
|
@@ -27835,10 +29704,10 @@ function isManifestFile(file) {
|
|
|
27835
29704
|
}
|
|
27836
29705
|
}
|
|
27837
29706
|
function removedPluginNotice(dataDir = globalDataDir()) {
|
|
27838
|
-
const directory =
|
|
29707
|
+
const directory = path29.join(dataDir, "plugins");
|
|
27839
29708
|
try {
|
|
27840
|
-
const hasManifest = isManifestFile(
|
|
27841
|
-
(entry) => (entry.isDirectory() || entry.isSymbolicLink()) && isManifestFile(
|
|
29709
|
+
const hasManifest = isManifestFile(path29.join(directory, "manifest.json")) || fs14.readdirSync(directory, { withFileTypes: true }).some(
|
|
29710
|
+
(entry) => (entry.isDirectory() || entry.isSymbolicLink()) && isManifestFile(path29.join(directory, entry.name, "manifest.json"))
|
|
27842
29711
|
);
|
|
27843
29712
|
if (hasManifest) return "Plugin runners were removed; manifests in ~/.ordewell/plugins/ are ignored. See the CHANGELOG.";
|
|
27844
29713
|
} catch {
|
|
@@ -27849,14 +29718,14 @@ function removedPluginNotice(dataDir = globalDataDir()) {
|
|
|
27849
29718
|
|
|
27850
29719
|
// src/utils/stateStore.ts
|
|
27851
29720
|
var fs15 = __toESM(require("fs"));
|
|
27852
|
-
var
|
|
29721
|
+
var path30 = __toESM(require("path"));
|
|
27853
29722
|
var STATE_FILE = "state.json";
|
|
27854
29723
|
function getStatePath(baseDir) {
|
|
27855
|
-
return
|
|
29724
|
+
return path30.join(getStateDir(baseDir), STATE_FILE);
|
|
27856
29725
|
}
|
|
27857
29726
|
function saveState(plan, baseDir) {
|
|
27858
29727
|
const statePath = getStatePath(baseDir ?? process.cwd());
|
|
27859
|
-
ensureDir(
|
|
29728
|
+
ensureDir(path30.dirname(statePath));
|
|
27860
29729
|
plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
27861
29730
|
fs15.writeFileSync(statePath, JSON.stringify(plan, null, 2));
|
|
27862
29731
|
}
|
|
@@ -27892,11 +29761,11 @@ function stateExists(baseDir) {
|
|
|
27892
29761
|
|
|
27893
29762
|
// src/services/SettingsService.ts
|
|
27894
29763
|
var fs16 = __toESM(require("fs"));
|
|
27895
|
-
var
|
|
29764
|
+
var path31 = __toESM(require("path"));
|
|
27896
29765
|
function getSettingsPath() {
|
|
27897
29766
|
const override = process.env.ORDEWELL_SETTINGS_PATH;
|
|
27898
29767
|
if (override && override.trim()) return override.trim();
|
|
27899
|
-
return
|
|
29768
|
+
return path31.join(globalDataDir(), "settings.json");
|
|
27900
29769
|
}
|
|
27901
29770
|
function readPlannerModels(raw) {
|
|
27902
29771
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
@@ -28005,7 +29874,7 @@ var SettingsService = class {
|
|
|
28005
29874
|
return {};
|
|
28006
29875
|
}
|
|
28007
29876
|
persist() {
|
|
28008
|
-
const dir =
|
|
29877
|
+
const dir = path31.dirname(this.filePath);
|
|
28009
29878
|
if (!fs16.existsSync(dir)) {
|
|
28010
29879
|
fs16.mkdirSync(dir, { recursive: true });
|
|
28011
29880
|
}
|
|
@@ -28079,7 +29948,9 @@ var PlannerModelMemory = class {
|
|
|
28079
29948
|
DEFAULT_MAX_PARALLEL,
|
|
28080
29949
|
DEFAULT_PLANNER_ALLOWLIST,
|
|
28081
29950
|
DEFAULT_RUNNERS,
|
|
29951
|
+
DEFAULT_WEB_READ_BUDGET,
|
|
28082
29952
|
DENY_ALL,
|
|
29953
|
+
DuckDuckGoSearchProvider,
|
|
28083
29954
|
EMPTY_CONVERSATION,
|
|
28084
29955
|
EMPTY_HOLD,
|
|
28085
29956
|
EMPTY_TASK_LOG,
|
|
@@ -28091,6 +29962,7 @@ var PlannerModelMemory = class {
|
|
|
28091
29962
|
GREP_PCRE_PROBE_ARGS,
|
|
28092
29963
|
GeminiService,
|
|
28093
29964
|
HARNESS_PLANNER_REASON,
|
|
29965
|
+
HttpWebFetcher,
|
|
28094
29966
|
JSON_REPAIR_INSTRUCTION,
|
|
28095
29967
|
LineBuffer,
|
|
28096
29968
|
ModelCatalog,
|
|
@@ -28157,6 +30029,7 @@ var PlannerModelMemory = class {
|
|
|
28157
30029
|
TaskOrchestrator,
|
|
28158
30030
|
VerdictEngine,
|
|
28159
30031
|
WINDOWS_MAX_COMMAND_LINE,
|
|
30032
|
+
WebResponseCache,
|
|
28160
30033
|
WorkspaceNotAProjectError,
|
|
28161
30034
|
WorkspaceNotFoundError,
|
|
28162
30035
|
abbreviateHome,
|
|
@@ -28298,6 +30171,7 @@ var PlannerModelMemory = class {
|
|
|
28298
30171
|
loadSessionPlanState,
|
|
28299
30172
|
loadState,
|
|
28300
30173
|
loadedSkillTokens,
|
|
30174
|
+
looksLikeChallenge,
|
|
28301
30175
|
looksLikePlanAttempt,
|
|
28302
30176
|
mapAgentTool,
|
|
28303
30177
|
markAction,
|
|
@@ -28330,6 +30204,7 @@ var PlannerModelMemory = class {
|
|
|
28330
30204
|
parseMaxParallel,
|
|
28331
30205
|
parsePartialPlan,
|
|
28332
30206
|
parsePlanJson,
|
|
30207
|
+
parseSearchHits,
|
|
28333
30208
|
parseTaskOpsJson,
|
|
28334
30209
|
parseTaskQueryJson,
|
|
28335
30210
|
partedPromptUsage,
|
|
@@ -28390,6 +30265,8 @@ var PlannerModelMemory = class {
|
|
|
28390
30265
|
sessionDataDir,
|
|
28391
30266
|
sessionRuntimeSettings,
|
|
28392
30267
|
sharedMcpServer,
|
|
30268
|
+
sharedSearchProvider,
|
|
30269
|
+
sharedWebResponseCache,
|
|
28393
30270
|
skillNames,
|
|
28394
30271
|
skillTokens,
|
|
28395
30272
|
stateExists,
|