@mrciphersmith/keryx 0.3.17 → 0.3.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +733 -320
  2. package/dist/core.js +491 -90
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -3913,6 +3913,19 @@ var init_provider_catalog_cache = __esm(() => {
3913
3913
 
3914
3914
  // src/lib/oauth/open-url.ts
3915
3915
  import { spawn } from "child_process";
3916
+ function isLoopbackHost(hostname) {
3917
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
3918
+ }
3919
+ function isSafeVerificationUrl(url) {
3920
+ let parsed;
3921
+ try {
3922
+ parsed = new URL(url);
3923
+ } catch {
3924
+ return false;
3925
+ }
3926
+ const schemeOk = parsed.protocol === "https:" || parsed.protocol === "http:" && isLoopbackHost(parsed.hostname);
3927
+ return schemeOk && ALLOWED_URL_CHARS.test(url);
3928
+ }
3916
3929
  function browserOpenPlan(url, platform = process.platform, env = process.env) {
3917
3930
  if (url.length === 0) {
3918
3931
  return;
@@ -3921,7 +3934,10 @@ function browserOpenPlan(url, platform = process.platform, env = process.env) {
3921
3934
  return { cmd: "open", args: [url] };
3922
3935
  }
3923
3936
  if (platform === "win32") {
3924
- return { cmd: "cmd", args: ["/c", "start", "", url] };
3937
+ if (!isSafeVerificationUrl(url)) {
3938
+ return;
3939
+ }
3940
+ return { cmd: "rundll32", args: ["url.dll,FileProtocolHandler", url] };
3925
3941
  }
3926
3942
  if (typeof env.DISPLAY === "string" && env.DISPLAY.length > 0) {
3927
3943
  return { cmd: "xdg-open", args: [url] };
@@ -3943,7 +3959,10 @@ function openVerificationUrl(url, deps = {}) {
3943
3959
  child.unref();
3944
3960
  } catch {}
3945
3961
  }
3946
- var init_open_url = () => {};
3962
+ var ALLOWED_URL_CHARS;
3963
+ var init_open_url = __esm(() => {
3964
+ ALLOWED_URL_CHARS = /^[A-Za-z0-9\-._~:/?#[\]@!$'()*+,;=%]+$/;
3965
+ });
3947
3966
 
3948
3967
  // src/harness/policy/engine.ts
3949
3968
  import path8 from "path";
@@ -4282,7 +4301,7 @@ function isPrivateEgressToken(token) {
4282
4301
  function isPrivateEgressHost(host) {
4283
4302
  return isPrivateEgressToken(host);
4284
4303
  }
4285
- function isLoopbackHost(host) {
4304
+ function isLoopbackHost2(host) {
4286
4305
  if (typeof host !== "string" || host.length === 0)
4287
4306
  return false;
4288
4307
  const normalized = host.trim();
@@ -4415,7 +4434,7 @@ async function probeOllamaModels(deps, baseUrl) {
4415
4434
  } catch {
4416
4435
  host = baseUrl;
4417
4436
  }
4418
- if (isPrivateEgressHost(host) && !isLoopbackHost(host)) {
4437
+ if (isPrivateEgressHost(host) && !isLoopbackHost2(host)) {
4419
4438
  return;
4420
4439
  }
4421
4440
  let response;
@@ -9060,6 +9079,269 @@ var init_config = __esm(() => {
9060
9079
  ]);
9061
9080
  });
9062
9081
 
9082
+ // src/security/detect/entropy.ts
9083
+ function isHexBlob(value) {
9084
+ return HEX_BLOB.test(value) && /[a-f]/i.test(value);
9085
+ }
9086
+ function isAllowShapedValue(value) {
9087
+ return FULL_GIT_SHA_RE.test(value) || UUID_RE.test(value) || INTEGRITY_RE.test(value);
9088
+ }
9089
+ function lastLineOf(before) {
9090
+ const newline = before.lastIndexOf(`
9091
+ `);
9092
+ return newline < 0 ? before : before.slice(newline + 1);
9093
+ }
9094
+ function isSingleTransitionTag(segment) {
9095
+ return /^[A-Za-z]+[0-9]+$/.test(segment) || /^[0-9]+[A-Za-z]+$/.test(segment);
9096
+ }
9097
+ function isWordSlug(value) {
9098
+ const segments = value.split(/[-_]/);
9099
+ if (segments.length < 3) {
9100
+ return false;
9101
+ }
9102
+ let words = 0;
9103
+ for (const segment of segments) {
9104
+ const isAlpha = /^[A-Za-z]+$/.test(segment);
9105
+ const isDigits = /^[0-9]+$/.test(segment);
9106
+ if (!isAlpha && !isDigits && !isSingleTransitionTag(segment)) {
9107
+ return false;
9108
+ }
9109
+ if (isAlpha && segment.length >= 3) {
9110
+ words += 1;
9111
+ }
9112
+ }
9113
+ return words >= 1;
9114
+ }
9115
+ function shannonEntropy(value) {
9116
+ const counts = new Map;
9117
+ for (const ch of value) {
9118
+ counts.set(ch, (counts.get(ch) ?? 0) + 1);
9119
+ }
9120
+ let entropy = 0;
9121
+ for (const count of counts.values()) {
9122
+ const p = count / value.length;
9123
+ entropy -= p * Math.log2(p);
9124
+ }
9125
+ return entropy;
9126
+ }
9127
+ function bareShapeQualifies(head) {
9128
+ if (!/[0-9]/.test(head) && !/[+=]/.test(head)) {
9129
+ return { qualifies: false, entropy: 0 };
9130
+ }
9131
+ if (isWordSlug(head)) {
9132
+ return { qualifies: false, entropy: 0 };
9133
+ }
9134
+ const entropy = shannonEntropy(head);
9135
+ const hexBlob = isHexBlob(head);
9136
+ if (entropy < 3.6 && !hexBlob) {
9137
+ return { qualifies: false, entropy };
9138
+ }
9139
+ return { qualifies: true, entropy };
9140
+ }
9141
+ function shapeQualifiesAsSecret(value) {
9142
+ if (isAllowShapedValue(value)) {
9143
+ return { qualifies: false, entropy: 0 };
9144
+ }
9145
+ return bareShapeQualifies(value);
9146
+ }
9147
+ function looksSecretShaped(value) {
9148
+ return shapeQualifiesAsSecret(value).qualifies;
9149
+ }
9150
+ function buildMatch(start, end, value, entropy) {
9151
+ const confidence = Math.max(0.4, Math.min(0.7, 0.4 + (entropy - 3.6) * 0.21));
9152
+ return {
9153
+ category: "secret",
9154
+ policyId: "secrets.high-entropy",
9155
+ severity: "medium",
9156
+ confidence: Number(confidence.toFixed(2)),
9157
+ start,
9158
+ end,
9159
+ value,
9160
+ mask: "entropy",
9161
+ remediation: "Verify this high-entropy value is not a live credential."
9162
+ };
9163
+ }
9164
+ function findUrlSpans(content) {
9165
+ const spans = [];
9166
+ URL_SPAN_RE.lastIndex = 0;
9167
+ let m;
9168
+ while ((m = URL_SPAN_RE.exec(content)) !== null) {
9169
+ let raw = m[0];
9170
+ while (raw.length > 0 && URL_TRAILING_PUNCTUATION_RE.test(raw)) {
9171
+ raw = raw.slice(0, -1);
9172
+ }
9173
+ if (raw.length > 0) {
9174
+ spans.push({ start: m.index, end: m.index + raw.length, raw });
9175
+ }
9176
+ if (m.index === URL_SPAN_RE.lastIndex) {
9177
+ URL_SPAN_RE.lastIndex += 1;
9178
+ }
9179
+ }
9180
+ return spans;
9181
+ }
9182
+ function urlComponentSpans(raw) {
9183
+ const spans = [];
9184
+ const schemeMatch = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.exec(raw);
9185
+ if (schemeMatch === null) {
9186
+ return spans;
9187
+ }
9188
+ let i = schemeMatch[0].length;
9189
+ while (i < raw.length && raw[i] !== "/" && raw[i] !== "?" && raw[i] !== "#") {
9190
+ i += 1;
9191
+ }
9192
+ let pathEnd = i;
9193
+ while (pathEnd < raw.length && raw[pathEnd] !== "?" && raw[pathEnd] !== "#") {
9194
+ pathEnd += 1;
9195
+ }
9196
+ let segStart = i;
9197
+ for (let j = i;j <= pathEnd; j += 1) {
9198
+ if (j === pathEnd || raw[j] === "/") {
9199
+ if (j > segStart) {
9200
+ spans.push({ start: segStart, end: j });
9201
+ }
9202
+ segStart = j + 1;
9203
+ }
9204
+ }
9205
+ let cursor = pathEnd;
9206
+ if (cursor < raw.length && raw[cursor] === "?") {
9207
+ let queryEnd = cursor + 1;
9208
+ while (queryEnd < raw.length && raw[queryEnd] !== "#") {
9209
+ queryEnd += 1;
9210
+ }
9211
+ let pairStart = cursor + 1;
9212
+ for (let j = cursor + 1;j <= queryEnd; j += 1) {
9213
+ if (j === queryEnd || raw[j] === "&") {
9214
+ const pair = raw.slice(pairStart, j);
9215
+ const eq = pair.indexOf("=");
9216
+ if (eq !== -1 && eq + 1 < pair.length) {
9217
+ spans.push({ start: pairStart + eq + 1, end: j });
9218
+ } else if (eq === -1 && pair.length > 0) {
9219
+ spans.push({ start: pairStart, end: j });
9220
+ }
9221
+ pairStart = j + 1;
9222
+ }
9223
+ }
9224
+ cursor = queryEnd;
9225
+ }
9226
+ if (cursor < raw.length && raw[cursor] === "#" && cursor + 1 < raw.length) {
9227
+ spans.push({ start: cursor + 1, end: raw.length });
9228
+ }
9229
+ return spans;
9230
+ }
9231
+ function secretShapedCandidatesIn(text) {
9232
+ const normalized = text.replace(/\+/g, " ");
9233
+ const out = [];
9234
+ TOKEN.lastIndex = 0;
9235
+ let m;
9236
+ while ((m = TOKEN.exec(normalized)) !== null) {
9237
+ const value = m[0];
9238
+ if (value.length < URL_COMPONENT_MIN_LENGTH) {
9239
+ continue;
9240
+ }
9241
+ const head = TOKEN_HEAD.exec(value)?.[0] ?? value;
9242
+ const { qualifies, entropy } = shapeQualifiesAsSecret(head);
9243
+ if (!qualifies) {
9244
+ continue;
9245
+ }
9246
+ const start = m.index;
9247
+ const end = start + value.length;
9248
+ out.push({ start, end, value: text.slice(start, end), entropy });
9249
+ }
9250
+ return out;
9251
+ }
9252
+ function looksSecretShapedIn(text) {
9253
+ return secretShapedCandidatesIn(text).length > 0;
9254
+ }
9255
+ function detectEntropy(content) {
9256
+ const matches = [];
9257
+ const urlSpans = findUrlSpans(content);
9258
+ for (const url of urlSpans) {
9259
+ for (const comp of urlComponentSpans(url.raw)) {
9260
+ const compText = url.raw.slice(comp.start, comp.end);
9261
+ for (const cand of secretShapedCandidatesIn(compText)) {
9262
+ const absStart = url.start + comp.start + cand.start;
9263
+ const absEnd = url.start + comp.start + cand.end;
9264
+ matches.push(buildMatch(absStart, absEnd, cand.value, cand.entropy));
9265
+ }
9266
+ }
9267
+ }
9268
+ LABEL_ASSIGNMENT_RE.lastIndex = 0;
9269
+ let am;
9270
+ while ((am = LABEL_ASSIGNMENT_RE.exec(content)) !== null) {
9271
+ const value = am[1];
9272
+ const end = am.index + am[0].length;
9273
+ const start = end - value.length;
9274
+ if (urlSpans.some((u) => start < u.end && end > u.start)) {
9275
+ continue;
9276
+ }
9277
+ const { qualifies, entropy } = bareShapeQualifies(value);
9278
+ if (qualifies) {
9279
+ matches.push(buildMatch(start, end, value, entropy));
9280
+ }
9281
+ }
9282
+ TOKEN.lastIndex = 0;
9283
+ let m;
9284
+ while ((m = TOKEN.exec(content)) !== null) {
9285
+ const value = m[0];
9286
+ const start = m.index;
9287
+ const end = start + value.length;
9288
+ if (urlSpans.some((u) => start < u.end && end > u.start)) {
9289
+ continue;
9290
+ }
9291
+ if (matches.some((existing) => start < existing.end && end > existing.start)) {
9292
+ continue;
9293
+ }
9294
+ const head = TOKEN_HEAD.exec(value)?.[0] ?? value;
9295
+ const { qualifies, entropy } = bareShapeQualifies(head);
9296
+ if (!qualifies) {
9297
+ continue;
9298
+ }
9299
+ const before = lastLineOf(content.slice(Math.max(0, start - LABEL_WINDOW), start));
9300
+ if (!SENSITIVE_LABEL.test(before)) {
9301
+ continue;
9302
+ }
9303
+ if (isAllowShapedValue(head) && !ADJACENT_LABEL.test(before)) {
9304
+ continue;
9305
+ }
9306
+ matches.push(buildMatch(start, end, value, entropy));
9307
+ }
9308
+ return matches.sort((a, b) => a.start - b.start);
9309
+ }
9310
+ var SENSITIVE_LABEL_WORDS = "key|secret|token|password|passwd|api|credential|auth|bearer", caseless = (words) => words.replace(/[a-z]/g, (ch) => `[${ch}${ch.toUpperCase()}]`), SENSITIVE_LABEL, ADJACENT_LABEL, LABEL_ASSIGNMENT_RE, TOKEN, TOKEN_HEAD, LABEL_WINDOW = 40, HEX_BLOB, FULL_GIT_SHA_RE, UUID_RE, INTEGRITY_RE, URL_SPAN_RE, URL_TRAILING_PUNCTUATION_RE, URL_COMPONENT_MIN_LENGTH = 20;
9311
+ var init_entropy = __esm(() => {
9312
+ SENSITIVE_LABEL = new RegExp(`(?:^|[^A-Za-z0-9])(${caseless(SENSITIVE_LABEL_WORDS)})(?:$|[^A-Za-z0-9]|(?=[A-Z]))`);
9313
+ ADJACENT_LABEL = new RegExp(`(?:^|[^A-Za-z0-9])(?:${caseless(SENSITIVE_LABEL_WORDS)})[A-Za-z0-9_-]*["']?\\s*[:=]?\\s*["']?$`);
9314
+ LABEL_ASSIGNMENT_RE = new RegExp(`(?:^|[^A-Za-z0-9])(?:${SENSITIVE_LABEL_WORDS})[A-Za-z0-9_-]*"?\\s*=\\s*"?([A-Za-z0-9+/_=-]{6,})`, "gi");
9315
+ TOKEN = /[A-Za-z0-9+/=_-]{20,}(?:\.[A-Za-z0-9+/=_-]{6,})*/g;
9316
+ TOKEN_HEAD = /^[A-Za-z0-9+/=_-]+/;
9317
+ HEX_BLOB = /^[0-9a-f]{24,}$/i;
9318
+ FULL_GIT_SHA_RE = /^[0-9a-f]{40}$/i;
9319
+ UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
9320
+ INTEGRITY_RE = /^(?:sha256|sha512)-[A-Za-z0-9+/]+=*$/;
9321
+ URL_SPAN_RE = /\bhttps?:\/\/[^\s<>"'\\]+/g;
9322
+ URL_TRAILING_PUNCTUATION_RE = /[.,;:!?)\]}]$/;
9323
+ });
9324
+
9325
+ // src/security/entropy-gate.ts
9326
+ function ensureLoading() {
9327
+ if (loadStarted) {
9328
+ return;
9329
+ }
9330
+ loadStarted = true;
9331
+ loadSecurityConfig(process.cwd()).then((config) => {
9332
+ cachedEnabled = config.backends.entropy.enabled;
9333
+ }).catch(() => {});
9334
+ }
9335
+ function isEntropyBackendEnabled() {
9336
+ ensureLoading();
9337
+ return cachedEnabled;
9338
+ }
9339
+ var cachedEnabled, loadStarted = false;
9340
+ var init_entropy_gate = __esm(() => {
9341
+ init_config();
9342
+ cachedEnabled = DEFAULT_SECURITY_CONFIG.backends.entropy.enabled;
9343
+ });
9344
+
9063
9345
  // src/security/redact.ts
9064
9346
  import path14 from "path";
9065
9347
  import { createHmac, randomBytes } from "crypto";
@@ -9129,7 +9411,12 @@ function redactSensitiveText(text) {
9129
9411
  if (text.length === 0) {
9130
9412
  return text;
9131
9413
  }
9132
- const matches = [...detectSecrets(text), ...detectPii(text), ...detectExfil(text)];
9414
+ const matches = [
9415
+ ...detectSecrets(text),
9416
+ ...detectPii(text),
9417
+ ...detectExfil(text),
9418
+ ...isEntropyBackendEnabled() ? detectEntropy(text) : []
9419
+ ];
9133
9420
  if (matches.length === 0) {
9134
9421
  return text;
9135
9422
  }
@@ -9166,6 +9453,8 @@ var init_redact = __esm(() => {
9166
9453
  init_secrets();
9167
9454
  init_pii();
9168
9455
  init_exfil();
9456
+ init_entropy();
9457
+ init_entropy_gate();
9169
9458
  });
9170
9459
 
9171
9460
  // src/security/output-validation.ts
@@ -9909,102 +10198,33 @@ var init_seam = __esm(() => {
9909
10198
  init_warn_once();
9910
10199
  });
9911
10200
 
9912
- // src/security/detect/entropy.ts
9913
- function lastLineOf(before) {
9914
- const newline = before.lastIndexOf(`
9915
- `);
9916
- return newline < 0 ? before : before.slice(newline + 1);
9917
- }
9918
- function isWordSlug(value) {
9919
- const segments = value.split(/[-_]/);
9920
- if (segments.length < 3) {
9921
- return false;
9922
- }
9923
- let words = 0;
9924
- for (const segment of segments) {
9925
- const isAlpha = /^[A-Za-z]+$/.test(segment);
9926
- const isDigits = /^[0-9]+$/.test(segment);
9927
- if (!isAlpha && !isDigits) {
9928
- return false;
9929
- }
9930
- if (isAlpha && segment.length >= 3) {
9931
- words += 1;
9932
- }
9933
- }
9934
- return words >= 2;
9935
- }
9936
- function shannonEntropy(value) {
9937
- const counts = new Map;
9938
- for (const ch of value) {
9939
- counts.set(ch, (counts.get(ch) ?? 0) + 1);
9940
- }
9941
- let entropy = 0;
9942
- for (const count of counts.values()) {
9943
- const p = count / value.length;
9944
- entropy -= p * Math.log2(p);
9945
- }
9946
- return entropy;
9947
- }
9948
- function detectEntropy(content) {
9949
- const matches = [];
9950
- TOKEN.lastIndex = 0;
9951
- let m;
9952
- while ((m = TOKEN.exec(content)) !== null) {
9953
- const value = m[0];
9954
- const head = TOKEN_HEAD.exec(value)?.[0] ?? value;
9955
- if (!/[0-9]/.test(head) && !/[+=]/.test(head)) {
9956
- continue;
9957
- }
9958
- if (isWordSlug(head)) {
9959
- continue;
9960
- }
9961
- const entropy = shannonEntropy(head);
9962
- const hexBlob = HEX_BLOB.test(head);
9963
- if (entropy < 3.6 && !hexBlob) {
9964
- continue;
9965
- }
9966
- const before = lastLineOf(content.slice(Math.max(0, m.index - LABEL_WINDOW), m.index));
9967
- if (!SENSITIVE_LABEL.test(before)) {
9968
- continue;
9969
- }
9970
- const confidence = Math.max(0.4, Math.min(0.7, 0.4 + (entropy - 3.6) * 0.21));
9971
- matches.push({
9972
- category: "secret",
9973
- policyId: "secrets.high-entropy",
9974
- severity: "medium",
9975
- confidence: Number(confidence.toFixed(2)),
9976
- start: m.index,
9977
- end: m.index + value.length,
9978
- value,
9979
- mask: "secret",
9980
- remediation: "Verify this high-entropy value is not a live credential."
9981
- });
10201
+ // src/security/detect/injection.ts
10202
+ function foldForMatching(content) {
10203
+ let folded = "";
10204
+ for (let i = 0;i < content.length; i += 1) {
10205
+ const ch = content[i];
10206
+ folded += CONFUSABLES[ch] ?? ch;
9982
10207
  }
9983
- return matches;
10208
+ const nfkc = folded.normalize("NFKC");
10209
+ return nfkc.length === folded.length ? nfkc : folded;
9984
10210
  }
9985
- var SENSITIVE_LABEL, TOKEN, TOKEN_HEAD, LABEL_WINDOW = 40, HEX_BLOB;
9986
- var init_entropy = __esm(() => {
9987
- SENSITIVE_LABEL = /(key|secret|token|password|passwd|api|credential|auth)/i;
9988
- TOKEN = /[A-Za-z0-9+/=_-]{20,}(?:\.[A-Za-z0-9+/=_-]{6,})*/g;
9989
- TOKEN_HEAD = /^[A-Za-z0-9+/=_-]+/;
9990
- HEX_BLOB = /^[0-9a-f]{24,}$/i;
9991
- });
9992
-
9993
- // src/security/detect/injection.ts
9994
10211
  function detectInjection(content) {
9995
10212
  const matches = [];
10213
+ const folded = foldForMatching(content);
9996
10214
  for (const pattern of PATTERNS) {
9997
10215
  pattern.regex.lastIndex = 0;
9998
10216
  let m;
9999
- while ((m = pattern.regex.exec(content)) !== null) {
10217
+ while ((m = pattern.regex.exec(folded)) !== null) {
10218
+ const start = Math.min(m.index, content.length);
10219
+ const end = Math.min(m.index + m[0].length, content.length);
10000
10220
  matches.push({
10001
10221
  category: "prompt-injection",
10002
10222
  policyId: pattern.policyId,
10003
10223
  severity: "low",
10004
10224
  confidence: pattern.confidence,
10005
- start: m.index,
10006
- end: m.index + m[0].length,
10007
- value: m[0],
10225
+ start,
10226
+ end,
10227
+ value: content.slice(start, end),
10008
10228
  remediation: "Treat external content as data, not instruction; require human review."
10009
10229
  });
10010
10230
  if (m.index === pattern.regex.lastIndex) {
@@ -10014,27 +10234,69 @@ function detectInjection(content) {
10014
10234
  }
10015
10235
  return matches;
10016
10236
  }
10017
- var PATTERNS;
10237
+ var CONFUSABLES, PATTERNS;
10018
10238
  var init_injection = __esm(() => {
10239
+ CONFUSABLES = {
10240
+ "\u0430": "a",
10241
+ "\u0410": "A",
10242
+ "\u0435": "e",
10243
+ "\u0415": "E",
10244
+ "\u043E": "o",
10245
+ "\u041E": "O",
10246
+ "\u0440": "p",
10247
+ "\u0420": "P",
10248
+ "\u0441": "c",
10249
+ "\u0421": "C",
10250
+ "\u0445": "x",
10251
+ "\u0425": "X",
10252
+ "\u0443": "y",
10253
+ "\u0423": "Y",
10254
+ "\u0455": "s",
10255
+ "\u0405": "S",
10256
+ "\u0456": "o",
10257
+ "\u0406": "O",
10258
+ "\u0458": "j",
10259
+ "\u0408": "J",
10260
+ "\u03B1": "a",
10261
+ "\u0391": "A",
10262
+ "\u03B2": "b",
10263
+ "\u0392": "B",
10264
+ "\u03B5": "e",
10265
+ "\u0395": "E",
10266
+ "\u03B9": "i",
10267
+ "\u0399": "I",
10268
+ "\u03BA": "k",
10269
+ "\u039A": "K",
10270
+ "\u03BF": "o",
10271
+ "\u039F": "O",
10272
+ "\u03C1": "p",
10273
+ "\u03A1": "P",
10274
+ "\u03C4": "t",
10275
+ "\u03A4": "T",
10276
+ "\u03C5": "u",
10277
+ "\u03A5": "Y",
10278
+ "\u03C7": "x",
10279
+ "\u03A7": "X"
10280
+ };
10019
10281
  PATTERNS = [
10020
10282
  {
10021
10283
  policyId: "prompt-injection.ignore-instructions",
10022
- regex: /\b(ignore|disregard|forget|override)\b[^.\n]{0,40}\b(all\s+)?(previous|prior|above|earlier|system|developer)\b[^.\n]{0,20}\b(instructions?|prompts?|rules?|context)\b/gi,
10284
+ regex: /\b(ignore|disregard|forget|override)\b[^.]{0,40}\b(all\s+)?(previous|prior|above|earlier|system|developer)\b[^.]{0,20}\b(instructions?|prompts?|rules?|context)\b/gi,
10023
10285
  confidence: 0.4
10024
10286
  },
10025
10287
  {
10026
10288
  policyId: "prompt-injection.reveal-context",
10027
- regex: /\b(reveal|show|print|expose|leak|dump|repeat)\b[^.\n]{0,30}\b(your\s+)?(system\s+prompt|instructions?|memory|secrets?|hidden\s+context|api\s*keys?)\b/gi,
10289
+ regex: /\b(reveal|show|print|expose|leak|dump|repeat)\b[^.]{0,30}\b(your\s+)?(system\s+prompt|instructions?|memory|secrets?|hidden\s+context|api\s*keys?)\b/gi,
10028
10290
  confidence: 0.45
10029
10291
  },
10030
10292
  {
10031
10293
  policyId: "prompt-injection.role-override",
10032
- regex: /\b(you\s+are\s+now|from\s+now\s+on|act\s+as|pretend\s+to\s+be)\b[^.\n]{0,40}\b(unrestricted|jailbroken|dan|admin|developer\s+mode|no\s+rules)\b/gi,
10294
+ regex: /\b(you\s+are\s+now|from\s+now\s+on|act\s+as|pretend\s+to\s+be)\b[^.]{0,40}\b(unrestricted|jailbroken|dan|admin|developer\s+mode|no\s+rules)\b/gi,
10033
10295
  confidence: 0.4
10034
10296
  },
10035
10297
  {
10036
10298
  policyId: "prompt-injection.priority-override",
10037
- regex: /\b(treat|follow|obey)\b[^.\n]{0,30}\b(the\s+)?(following|external|below)\b[^.\n]{0,20}\b(as\s+)?(instructions?|higher\s+priority|commands?)\b/gi,
10299
+ regex: /\b(treat|follow|obey)\b[^.]{0,30}\b(the\s+)?(following|external|below)\b[^.]{0,20}\b(as\s+)?(instructions?|higher\s+priority|commands?)\b/gi,
10038
10300
  confidence: 0.35
10039
10301
  }
10040
10302
  ];
@@ -11204,6 +11466,140 @@ var init_read_source = __esm(() => {
11204
11466
  init_contained_path();
11205
11467
  });
11206
11468
 
11469
+ // src/security/credential-shape.ts
11470
+ function isDeniedForMcpChild(name, value) {
11471
+ if (value !== undefined && CREDENTIAL_IN_VALUE_RE.test(value))
11472
+ return true;
11473
+ const upper = name.toUpperCase();
11474
+ if (MCP_ENV_ALLOW_EXACT.includes(upper))
11475
+ return false;
11476
+ const conditional = VALUE_DEPENDENT[upper];
11477
+ if (conditional !== undefined) {
11478
+ return value === undefined ? true : conditional(value);
11479
+ }
11480
+ if (EXTERNAL_ENV_DENY.some((denied) => denied.toUpperCase() === upper))
11481
+ return true;
11482
+ if (MCP_ENV_DENY.includes(upper))
11483
+ return true;
11484
+ if (EXTERNAL_ENV_PREFIX_SWEEPS.some((prefix) => upper.startsWith(prefix.toUpperCase())))
11485
+ return true;
11486
+ if (MCP_ENV_PREFIX_SWEEPS.some((prefix) => upper.startsWith(prefix)))
11487
+ return true;
11488
+ return SECRET_SEGMENT_RE.test(upper) || SECRET_SUBSTRING_RE.test(upper) || GLUED_SECRET_RE.test(upper) || CONNECTION_STRING_RE.test(upper);
11489
+ }
11490
+ var EXTERNAL_ENV_DENY, EXTERNAL_ENV_PREFIX_SWEEPS, MCP_ENV_ALLOW_EXACT, MCP_ENV_DENY, MCP_ENV_PREFIX_SWEEPS, SECRET_SEGMENTS, SECRET_SEGMENT_RE, SECRET_SUBSTRING_RE, GLUED_SECRET_RE, CONNECTION_STRING_RE, CREDENTIAL_IN_VALUE_RE, VALUE_DEPENDENT;
11491
+ var init_credential_shape = __esm(() => {
11492
+ EXTERNAL_ENV_DENY = [
11493
+ "ANTHROPIC_API_KEY",
11494
+ "ANTHROPIC_AUTH_TOKEN",
11495
+ "ANTHROPIC_BASE_URL",
11496
+ "ANTHROPIC_MODEL",
11497
+ "CLAUDE_CONFIG_DIR",
11498
+ "CODEX_HOME",
11499
+ "CLAUDECODE"
11500
+ ];
11501
+ EXTERNAL_ENV_PREFIX_SWEEPS = ["CLAUDE_CODE_", "KERYX_"];
11502
+ MCP_ENV_ALLOW_EXACT = ["PWD", "OLDPWD"];
11503
+ MCP_ENV_DENY = [
11504
+ "SSH_AUTH_SOCK",
11505
+ "SSH_AGENT_PID",
11506
+ "GPG_AGENT_INFO",
11507
+ "GNUPGHOME",
11508
+ "GNOME_KEYRING_CONTROL",
11509
+ "GNOME_KEYRING_PID",
11510
+ "KDE_FULL_SESSION",
11511
+ "GIT_ASKPASS",
11512
+ "SSH_ASKPASS",
11513
+ "SUDO_ASKPASS",
11514
+ "GIT_CREDENTIAL_HELPER",
11515
+ "GIT_SSH_COMMAND",
11516
+ "KUBECONFIG",
11517
+ "NETRC",
11518
+ "PGPASSFILE",
11519
+ "PGSERVICEFILE",
11520
+ "GOOGLE_APPLICATION_CREDENTIALS",
11521
+ "CLOUDSDK_CONFIG",
11522
+ "DOCKER_CONFIG",
11523
+ "AWS_SHARED_CREDENTIALS_FILE",
11524
+ "AWS_CONFIG_FILE",
11525
+ "BOTO_CONFIG",
11526
+ "AZURE_CONFIG_DIR",
11527
+ "CURLRC",
11528
+ "WGETRC",
11529
+ "NPMRC",
11530
+ "NPM_CONFIG_USERCONFIG",
11531
+ "PIP_CONFIG_FILE",
11532
+ "RCLONE_CONFIG",
11533
+ "MAVEN_SETTINGS",
11534
+ "GIT_CONFIG",
11535
+ "GIT_CONFIG_GLOBAL",
11536
+ "HGRCPATH",
11537
+ "ANSIBLE_VAULT_PASSWORD_FILE",
11538
+ "AZURE_CLIENT_CERTIFICATE_PATH",
11539
+ "ARM_CLIENT_CERTIFICATE_PATH",
11540
+ "VAULT_CLIENT_CERT",
11541
+ "VAULT_CLIENT_KEY",
11542
+ "SSL_CLIENT_CERT_PATH",
11543
+ "SSL_CLIENT_KEY_FILE",
11544
+ "IDENTITY_FILE",
11545
+ "SSH_IDENTITY_FILE",
11546
+ "GCP_SERVICE_ACCOUNT",
11547
+ "XAUTHORITY",
11548
+ "OPENAI_API_KEY",
11549
+ "GEMINI_API_KEY",
11550
+ "GOOGLE_API_KEY",
11551
+ "GROQ_API_KEY",
11552
+ "MISTRAL_API_KEY",
11553
+ "XAI_API_KEY",
11554
+ "DEEPSEEK_API_KEY",
11555
+ "OPENROUTER_API_KEY",
11556
+ "TOGETHER_API_KEY",
11557
+ "PERPLEXITY_API_KEY"
11558
+ ];
11559
+ MCP_ENV_PREFIX_SWEEPS = [
11560
+ "OP_SESSION",
11561
+ "BW_SESSION"
11562
+ ];
11563
+ SECRET_SEGMENTS = [
11564
+ "KEY",
11565
+ "KEYS",
11566
+ "TOKEN",
11567
+ "TOKENS",
11568
+ "SECRET",
11569
+ "SECRETS",
11570
+ "PASSWORD",
11571
+ "PASSWD",
11572
+ "PWD",
11573
+ "PASS",
11574
+ "PASSPHRASE",
11575
+ "PASSCODE",
11576
+ "PASSKEY",
11577
+ "CREDENTIAL",
11578
+ "CREDENTIALS",
11579
+ "CREDS",
11580
+ "AUTH",
11581
+ "AUTHORIZATION",
11582
+ "BEARER",
11583
+ "JWT",
11584
+ "COOKIE",
11585
+ "SIGNATURE",
11586
+ "APIKEY",
11587
+ "SECRETKEY",
11588
+ "ACCESSKEY",
11589
+ "AUTHTOKEN",
11590
+ "PAT",
11591
+ "WEBHOOK"
11592
+ ];
11593
+ SECRET_SEGMENT_RE = new RegExp(`(^|_)(${SECRET_SEGMENTS.join("|")})($|_)`);
11594
+ SECRET_SUBSTRING_RE = /(PASSWORD|PASSWD|PASSPHRASE|PASSCODE|SSHPASS|SECRET|CREDENTIAL|APIKEY|KEYFILE|PRIVKEY)/;
11595
+ GLUED_SECRET_RE = /(PRIVATE|SECRET|ACCESS|REFRESH|SESSION|CLIENT|API|APP)(KEY|TOKEN|SECRET)($|_)|DB(PASS|PWD)($|_)/;
11596
+ CONNECTION_STRING_RE = /(^|_)(DSN|CONNECTION_STRING|CONNECTIONSTRING)($|_)/;
11597
+ CREDENTIAL_IN_VALUE_RE = /:\/\/[^/\s@]*(:[^/\s@]*)?@/;
11598
+ VALUE_DEPENDENT = {
11599
+ DOCKER_HOST: (value) => !value.startsWith("unix://") && !value.startsWith("fd://")
11600
+ };
11601
+ });
11602
+
11207
11603
  // src/security/audit-harness/codex-toml.ts
11208
11604
  function stripComment(line) {
11209
11605
  let quoted = false;
@@ -17641,7 +18037,7 @@ var package_default;
17641
18037
  var init_package = __esm(() => {
17642
18038
  package_default = {
17643
18039
  name: "@mrciphersmith/keryx",
17644
- version: "0.3.17",
18040
+ version: "0.3.19",
17645
18041
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
17646
18042
  private: false,
17647
18043
  publishConfig: {
@@ -28787,6 +29183,11 @@ var init_service4 = __esm(() => {
28787
29183
  init_path_scan();
28788
29184
  init_read_source();
28789
29185
  init_redact();
29186
+ init_secrets();
29187
+ init_entropy();
29188
+ init_incidents();
29189
+ init_entropy_gate();
29190
+ init_credential_shape();
28790
29191
  init_audit_harness();
28791
29192
  init_config();
28792
29193
  init_impact_evidence();
@@ -28955,7 +29356,7 @@ function retryableFor2(kind, fallback) {
28955
29356
  const concrete = defaultRetryable(kind);
28956
29357
  return concrete === undefined ? fallback : concrete;
28957
29358
  }
28958
- function mergeUsage2(promptTokens, completionTokens, totalTokens) {
29359
+ function mergeUsage2(promptTokens, completionTokens, totalTokens, cacheReadTokens) {
28959
29360
  const usage = { exact: true };
28960
29361
  if (promptTokens !== undefined) {
28961
29362
  usage.inputTokens = promptTokens;
@@ -28968,6 +29369,9 @@ function mergeUsage2(promptTokens, completionTokens, totalTokens) {
28968
29369
  } else if (promptTokens !== undefined || completionTokens !== undefined) {
28969
29370
  usage.totalTokens = (promptTokens ?? 0) + (completionTokens ?? 0);
28970
29371
  }
29372
+ if (cacheReadTokens !== undefined) {
29373
+ usage.cacheReadTokens = cacheReadTokens;
29374
+ }
28971
29375
  return usage;
28972
29376
  }
28973
29377
  function classifyHttpError2(status, headers, code) {
@@ -29141,7 +29545,7 @@ class OpenAiCompatEngine {
29141
29545
  } catch {
29142
29546
  host = baseUrl;
29143
29547
  }
29144
- const permitted = !isPrivateEgressHost(host) || grant.allowLoopback === true && isLoopbackHost(host) || grant.allowPrivateLan === true && isPrivateLanHost(host);
29548
+ const permitted = !isPrivateEgressHost(host) || grant.allowLoopback === true && isLoopbackHost2(host) || grant.allowPrivateLan === true && isPrivateLanHost(host);
29145
29549
  if (!permitted) {
29146
29550
  yield errorEvent({
29147
29551
  kind: "invalid_request",
@@ -29394,6 +29798,14 @@ ${message.content}` });
29394
29798
  }
29395
29799
  return "idx:0";
29396
29800
  };
29801
+ const pendingToolCallStillAccumulating = () => {
29802
+ for (const acc of pendingTools.values()) {
29803
+ if (!acc.ended) {
29804
+ return acc.id;
29805
+ }
29806
+ }
29807
+ return;
29808
+ };
29397
29809
  const flushPendingToolEnds = () => {
29398
29810
  for (const acc of pendingTools.values()) {
29399
29811
  if (acc.ended) {
@@ -29487,7 +29899,7 @@ ${message.content}` });
29487
29899
  const usage = asRecord4(data.usage);
29488
29900
  bodies.push({
29489
29901
  kind: "usage_update",
29490
- usage: mergeUsage2(asNumber3(usage.prompt_tokens), asNumber3(usage.completion_tokens), asNumber3(usage.total_tokens))
29902
+ usage: mergeUsage2(asNumber3(usage.prompt_tokens), asNumber3(usage.completion_tokens), asNumber3(usage.total_tokens), asNumber3(asRecord4(usage.prompt_tokens_details).cached_tokens))
29491
29903
  });
29492
29904
  }
29493
29905
  const choice0 = asRecord4(asArray(data.choices)[0]);
@@ -29600,6 +30012,7 @@ ${message.content}` });
29600
30012
  parser.push(trailing);
29601
30013
  }
29602
30014
  const torn = parser.flush();
30015
+ const pendingToolCallId = pendingToolCallStillAccumulating();
29603
30016
  if (torn.length > 0) {
29604
30017
  malformed = {
29605
30018
  kind: "malformed",
@@ -29612,8 +30025,14 @@ ${message.content}` });
29612
30025
  retryable: retryableFor2("malformed", false),
29613
30026
  message: "empty response body"
29614
30027
  };
30028
+ } else if (pendingToolCallId !== undefined) {
30029
+ malformed = {
30030
+ kind: "malformed",
30031
+ retryable: retryableFor2("malformed", false),
30032
+ message: `${this.label} SSE stream ended while a tool call was still accumulating`,
30033
+ detail: { pendingToolCallId }
30034
+ };
29615
30035
  } else {
29616
- flushPendingToolEnds();
29617
30036
  if (sawStart && sawFinish) {
29618
30037
  flushThinkTagParser();
29619
30038
  flushFieldTagStripper();
@@ -30272,6 +30691,16 @@ class GeminiProvider {
30272
30691
  break;
30273
30692
  }
30274
30693
  const chunk = asRecord5(parsed);
30694
+ const errorEnvelope = asRecord5(chunk.error);
30695
+ if (Object.keys(errorEnvelope).length > 0) {
30696
+ const classified = classifyGeminiError(asNumber4(errorEnvelope.code) ?? 0, asString4(errorEnvelope.status));
30697
+ const envelopeMessage = asString4(errorEnvelope.message);
30698
+ malformed = {
30699
+ ...classified,
30700
+ message: redact(envelopeMessage !== undefined && envelopeMessage.length > 0 ? envelopeMessage : "Gemini stream carried an in-band error envelope")
30701
+ };
30702
+ break;
30703
+ }
30275
30704
  if (!sawFirstChunk) {
30276
30705
  sawFirstChunk = true;
30277
30706
  bodies.push({ kind: "model_start" });
@@ -30295,7 +30724,7 @@ class GeminiProvider {
30295
30724
  bodies.push({ kind: "text_delta", text });
30296
30725
  }
30297
30726
  } else if (callName !== undefined) {
30298
- const callId = asString4(functionCall.id) ?? callName;
30727
+ const callId = asString4(functionCall.id) ?? `${callName}#${functionCallIndexInRound}`;
30299
30728
  const argsInput = JSON.stringify(asRecord5(functionCall.args));
30300
30729
  bodies.push({ kind: "tool_call_start", toolCallId: callId, toolName: callName });
30301
30730
  bodies.push({ kind: "tool_call_end", toolCallId: callId, input: argsInput });
@@ -30512,7 +30941,7 @@ function retryableFor4(kind, fallback) {
30512
30941
  const concrete = defaultRetryable(kind);
30513
30942
  return concrete === undefined ? fallback : concrete;
30514
30943
  }
30515
- function mergeUsage4(inputTokens, outputTokens, totalTokens) {
30944
+ function mergeUsage4(inputTokens, outputTokens, totalTokens, cacheReadTokens) {
30516
30945
  const usage = { exact: true };
30517
30946
  if (inputTokens !== undefined) {
30518
30947
  usage.inputTokens = inputTokens;
@@ -30525,6 +30954,9 @@ function mergeUsage4(inputTokens, outputTokens, totalTokens) {
30525
30954
  } else if (inputTokens !== undefined || outputTokens !== undefined) {
30526
30955
  usage.totalTokens = (inputTokens ?? 0) + (outputTokens ?? 0);
30527
30956
  }
30957
+ if (cacheReadTokens !== undefined) {
30958
+ usage.cacheReadTokens = cacheReadTokens;
30959
+ }
30528
30960
  return usage;
30529
30961
  }
30530
30962
  function extractErrorFields(data) {
@@ -30942,9 +31374,10 @@ class OpenAiProvider {
30942
31374
  const outputTokens = asNumber5(usage.output_tokens);
30943
31375
  const totalTokens = asNumber5(usage.total_tokens);
30944
31376
  const reasoningTokens = asNumber5(asRecord6(usage.output_tokens_details).reasoning_tokens);
31377
+ const cacheReadTokens = asNumber5(asRecord6(usage.input_tokens_details).cached_tokens);
30945
31378
  const usageBody = {
30946
31379
  kind: "usage_update",
30947
- usage: mergeUsage4(inputTokens, outputTokens, totalTokens)
31380
+ usage: mergeUsage4(inputTokens, outputTokens, totalTokens, cacheReadTokens)
30948
31381
  };
30949
31382
  if (reasoningTokens !== undefined) {
30950
31383
  usageBody.unknownExtensions = { "openai.reasoning_tokens": reasoningTokens };
@@ -58148,8 +58581,10 @@ async function runAgentTurn(io, deps, history, userLine, options = {}) {
58148
58581
 
58149
58582
  ${buildControlMarkerInstruction(controlNonce)}`
58150
58583
  };
58584
+ const caughtToolErrors = [];
58151
58585
  try {
58152
- const result = await runAgentTurnCore(io, turnDeps, history, userLine, options);
58586
+ const coreResult = await runAgentTurnCore(io, turnDeps, history, userLine, options, caughtToolErrors);
58587
+ const result = caughtToolErrors.length > 0 ? { ...coreResult, caughtToolErrors } : coreResult;
58153
58588
  await fireStopHookBestEffort(io, deps, result);
58154
58589
  return result;
58155
58590
  } finally {
@@ -58231,7 +58666,7 @@ function computeReasoningDurationMs(startedAt, endedAt) {
58231
58666
  const endMs = Date.parse(endedAt);
58232
58667
  return !Number.isNaN(startMs) && !Number.isNaN(endMs) && endMs >= startMs ? endMs - startMs : undefined;
58233
58668
  }
58234
- async function runAgentTurnCore(io, deps, history, userLine, options = {}) {
58669
+ async function runAgentTurnCore(io, deps, history, userLine, options = {}, caughtToolErrors = []) {
58235
58670
  const now = deps.now ?? (() => new Date().toISOString());
58236
58671
  const controlNonce = deps.controlNonce ?? generateControlNonce();
58237
58672
  const scrub = (text) => scrubControlNonce(text, controlNonce);
@@ -58821,7 +59256,7 @@ ${shown.join(`
58821
59256
  }
58822
59257
  const spawnConcurrencyCandidates = calls.filter((call) => call.name === "spawn_subagent" && reservationByCallId.get(call.id)?.ok === true);
58823
59258
  const untrustedGateBlocksSpawns = untrustedContentSeen || batchContainsUntrustedWeb;
58824
- const concurrentSpawnResults = spawnConcurrencyCandidates.length >= 2 && !untrustedGateBlocksSpawns && maxToolCalls === undefined ? await runConcurrentSpawnBatch(spawnConcurrencyCandidates, toolByName, io, deps, hasInvocationCapacity, reserveInvocation, signal) : undefined;
59259
+ const concurrentSpawnResults = spawnConcurrencyCandidates.length >= 2 && !untrustedGateBlocksSpawns && maxToolCalls === undefined ? await runConcurrentSpawnBatch(spawnConcurrencyCandidates, toolByName, io, deps, hasInvocationCapacity, reserveInvocation, signal, caughtToolErrors) : undefined;
58825
59260
  const answeredToolIds = new Set;
58826
59261
  let anchorsToAnnounce;
58827
59262
  let repeatedFailureHint;
@@ -58859,53 +59294,18 @@ ${shown.join(`
58859
59294
  }
58860
59295
  const risk = toolByName.get(call.name)?.definition.risk;
58861
59296
  const isPureReadTool = risk === "read" && !DURABLE_READ_TOOL_NAMES.has(call.name);
58862
- if (!isPureReadTool && untrustedContentSeen) {
58863
- const approver = io.requestApproval;
58864
- if (deps.unattended === true || approver === undefined) {
58865
- const result = {
58866
- output: "tool blocked: external web content cannot authorize further tool calls in this turn",
58867
- isError: true
58868
- };
58869
- if (deps.unattended === true) {
58870
- io.onUnattendedDenial?.(call.name, "untrusted external content in this turn cannot authorize the call");
58871
- }
58872
- io.onToolResult?.(call.name, result);
58873
- history.push({ role: "tool", content: withBudgetWarning(result.output), provenance: "tool", toolCallId: call.id, ts: now() });
58874
- io.onHistoryChange?.("tool");
58875
- gateBlockedAny = true;
58876
- continue;
58877
- }
58878
- const taintFingerprint = toolCallHash(call.name, call.input);
58879
- let taintApproved;
58880
- try {
58881
- const response = await approver(call.name, call.input, {
58882
- fingerprint: taintFingerprint,
58883
- destructive: risk === "destructive",
58884
- untrustedOrigin: true
58885
- });
58886
- taintApproved = isApprovalFor(response, taintFingerprint);
58887
- } catch (err) {
58888
- const result = {
58889
- output: `${call.name} not executed: the approval prompt failed (${err instanceof Error ? err.message : String(err)})`,
58890
- isError: true
58891
- };
58892
- io.onToolResult?.(call.name, result);
58893
- history.push({ role: "tool", content: withBudgetWarning(result.output), provenance: "tool", toolCallId: call.id, ts: now() });
58894
- io.onHistoryChange?.("tool");
58895
- gateBlockedAny = true;
58896
- continue;
58897
- }
58898
- if (!taintApproved) {
58899
- const result = {
58900
- output: "tool blocked: external web content cannot authorize this call, and the user did not authorize it either",
58901
- isError: true
58902
- };
58903
- io.onToolResult?.(call.name, result);
58904
- history.push({ role: "tool", content: withBudgetWarning(result.output), provenance: "tool", toolCallId: call.id, ts: now() });
58905
- io.onHistoryChange?.("tool");
58906
- gateBlockedAny = true;
58907
- continue;
58908
- }
59297
+ const untrustedOrigin = !isPureReadTool && untrustedContentSeen;
59298
+ if (untrustedOrigin && deps.unattended === true) {
59299
+ const result = {
59300
+ output: "tool blocked: external web content cannot authorize further tool calls in this turn",
59301
+ isError: true
59302
+ };
59303
+ io.onUnattendedDenial?.(call.name, "untrusted external content in this turn cannot authorize the call");
59304
+ io.onToolResult?.(call.name, result);
59305
+ history.push({ role: "tool", content: withBudgetWarning(result.output), provenance: "tool", toolCallId: call.id, ts: now() });
59306
+ io.onHistoryChange?.("tool");
59307
+ gateBlockedAny = true;
59308
+ continue;
58909
59309
  }
58910
59310
  io.onToolCall?.(call.name, call.input);
58911
59311
  const reservation = reservationByCallId.get(call.id) ?? reserveToolAttempt(budget, call.name, call.input);
@@ -58921,7 +59321,19 @@ ${shown.join(`
58921
59321
  if (toolByName.has(call.name)) {
58922
59322
  turnExecutedToolCall = true;
58923
59323
  }
58924
- const result = concurrentSpawnResults?.get(call.id) ?? await executeCall(call, toolByName, io.requestApproval, io.permissionMode, io.readOnly, io.onAutoApproved, hasInvocationCapacity, reserveInvocation, invocationBudget.maxCalls, signal, deps.busLeases, deps.hardDeny === undefined ? undefined : { check: deps.hardDeny, onDenied: io.onUnattendedDenial }, deps.hooks);
59324
+ const precomputedResult = concurrentSpawnResults?.get(call.id);
59325
+ let result;
59326
+ if (precomputedResult !== undefined) {
59327
+ result = precomputedResult;
59328
+ } else {
59329
+ try {
59330
+ result = await executeCall(call, toolByName, io.requestApproval, io.permissionMode, io.readOnly, io.onAutoApproved, hasInvocationCapacity, reserveInvocation, invocationBudget.maxCalls, signal, deps.busLeases, deps.hardDeny === undefined ? undefined : { check: deps.hardDeny, onDenied: io.onUnattendedDenial }, deps.hooks, untrustedOrigin);
59331
+ } catch (err) {
59332
+ const message = err instanceof Error ? err.message : String(err);
59333
+ result = { output: `${call.name} failed: ${message}`, isError: true };
59334
+ caughtToolErrors.push({ toolName: call.name, message });
59335
+ }
59336
+ }
58925
59337
  io.onToolResult?.(call.name, result);
58926
59338
  const modelOutput = redactSensitiveText(result.output);
58927
59339
  const untrusted = result.untrusted === true;
@@ -59371,7 +59783,7 @@ function isApprovalFor(response, fingerprint) {
59371
59783
  }
59372
59784
  return response.fingerprint === undefined || response.fingerprint === fingerprint;
59373
59785
  }
59374
- async function runConcurrentSpawnBatch(spawnCalls, toolByName, io, deps, hasInvocationCapacity, reserveInvocation, signal) {
59786
+ async function runConcurrentSpawnBatch(spawnCalls, toolByName, io, deps, hasInvocationCapacity, reserveInvocation, signal, caughtToolErrors) {
59375
59787
  const maxConcurrency = deps.maxSubagentConcurrency ?? DEFAULT_MAX_SUBAGENT_CONCURRENCY;
59376
59788
  const perTaskRuntimeMs = NOMINAL_CONCURRENT_SPAWN_RUNTIME_MS;
59377
59789
  const tasks = spawnCalls.map((call) => ({
@@ -59398,6 +59810,7 @@ async function runConcurrentSpawnBatch(spawnCalls, toolByName, io, deps, hasInvo
59398
59810
  output: `subagent call ${call.id} failed: sequential fallback error: ${message}`,
59399
59811
  isError: true
59400
59812
  });
59813
+ caughtToolErrors.push({ toolName: call.name, message });
59401
59814
  }
59402
59815
  }
59403
59816
  return results;
@@ -59425,6 +59838,9 @@ async function runConcurrentSpawnBatch(spawnCalls, toolByName, io, deps, hasInvo
59425
59838
  const results = new Map;
59426
59839
  for (const call of spawnCalls) {
59427
59840
  const settled = partialResults?.get(call.id);
59841
+ if (settled === undefined) {
59842
+ caughtToolErrors.push({ toolName: call.name, message });
59843
+ }
59428
59844
  results.set(call.id, settled ?? {
59429
59845
  output: `subagent call ${call.id} failed: concurrent wave error: ${message}`,
59430
59846
  isError: true
@@ -59481,7 +59897,7 @@ function composeWithHook(toolName, rawDecision, hookResult, interactive) {
59481
59897
  }
59482
59898
  return { decision, hookTightened: true, hookAsked, ...denyMessage !== undefined ? { denyMessage } : {} };
59483
59899
  }
59484
- async function executeCall(call, toolByName, requestApproval, permissionMode, readOnly, onAutoApproved, hasInvocationCapacity, reserveInvocation, maxToolCalls, signal, busLeases, hardDeny, hooks) {
59900
+ async function executeCall(call, toolByName, requestApproval, permissionMode, readOnly, onAutoApproved, hasInvocationCapacity, reserveInvocation, maxToolCalls, signal, busLeases, hardDeny, hooks, untrustedOrigin = false) {
59485
59901
  const tool = toolByName.get(call.name);
59486
59902
  if (tool === undefined) {
59487
59903
  return { output: `unknown tool: ${call.name}`, isError: true };
@@ -59507,6 +59923,7 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, re
59507
59923
  }
59508
59924
  }
59509
59925
  const mode = permissionMode?.() ?? DEFAULT_PERMISSION_MODE;
59926
+ const untrustedDenial = "tool blocked: external web content cannot authorize this call, and the user did not authorize it either";
59510
59927
  const isReadOnly = readOnly?.() ?? false;
59511
59928
  let hookResult;
59512
59929
  if (hooks !== undefined) {
@@ -59547,7 +59964,7 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, re
59547
59964
  isError: true
59548
59965
  };
59549
59966
  }
59550
- if (gated.decision === "auto") {
59967
+ if (gated.decision === "auto" && !untrustedOrigin) {
59551
59968
  onAutoApproved?.(call.name, call.input, { destructive, credentials });
59552
59969
  } else {
59553
59970
  const fingerprint = toolCallHash(call.name, call.input);
@@ -59557,10 +59974,11 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, re
59557
59974
  ...credentials ? { credentials } : {},
59558
59975
  ...publishLease ? { publishLease } : {},
59559
59976
  ...publishLease && publishLeaseHolder !== undefined ? { publishLeaseDetail: `held by @${publishLeaseHolder.name} \u2014 "${publishLeaseHolder.reason}"` } : {},
59560
- ...gated.hookAsked ? { hookAsk: true } : {}
59977
+ ...gated.hookAsked ? { hookAsk: true } : {},
59978
+ ...untrustedOrigin ? { untrustedOrigin: true } : {}
59561
59979
  });
59562
59980
  if (!isApprovalFor(response, fingerprint)) {
59563
- return { output: `command not approved by the user; not executed`, isError: true };
59981
+ return { output: untrustedOrigin ? untrustedDenial : "command not approved by the user; not executed", isError: true };
59564
59982
  }
59565
59983
  }
59566
59984
  } else if (risk === "delegate") {
@@ -59579,17 +59997,18 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, re
59579
59997
  isError: true
59580
59998
  };
59581
59999
  }
59582
- if (gated.decision === "auto") {
60000
+ if (gated.decision === "auto" && !untrustedOrigin) {
59583
60001
  onAutoApproved?.(call.name, call.input, { destructive: false, credentials: false });
59584
60002
  } else {
59585
60003
  const fingerprint = toolCallHash(call.name, call.input);
59586
60004
  const response = requestApproval === undefined ? false : await requestApproval(call.name, call.input, {
59587
60005
  fingerprint,
59588
60006
  destructive: false,
59589
- ...gated.hookAsked ? { hookAsk: true } : {}
60007
+ ...gated.hookAsked ? { hookAsk: true } : {},
60008
+ ...untrustedOrigin ? { untrustedOrigin: true } : {}
59590
60009
  });
59591
60010
  if (!isApprovalFor(response, fingerprint)) {
59592
- return { output: `subagent spawn not approved by the user; not executed`, isError: true };
60011
+ return { output: untrustedOrigin ? untrustedDenial : "subagent spawn not approved by the user; not executed", isError: true };
59593
60012
  }
59594
60013
  }
59595
60014
  } else if (risk === "write") {
@@ -59620,7 +60039,7 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, re
59620
60039
  isError: true
59621
60040
  };
59622
60041
  }
59623
- if (gated.decision === "auto") {
60042
+ if (gated.decision === "auto" && !untrustedOrigin) {
59624
60043
  onAutoApproved?.(call.name, call.input, { destructive, credentials });
59625
60044
  } else {
59626
60045
  const fingerprint = toolCallHash(call.name, call.input);
@@ -59629,13 +60048,14 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, re
59629
60048
  destructive,
59630
60049
  ...credentials ? { credentials } : {},
59631
60050
  ...card !== undefined ? { alwaysAsk: true, card } : {},
59632
- ...gated.hookAsked ? { hookAsk: true } : {}
60051
+ ...gated.hookAsked ? { hookAsk: true } : {},
60052
+ ...untrustedOrigin ? { untrustedOrigin: true } : {}
59633
60053
  });
59634
60054
  if (!isApprovalFor(response, fingerprint)) {
59635
60055
  if (confirmationToken !== undefined)
59636
60056
  tool.confirmationDeclined?.(confirmationToken);
59637
60057
  return {
59638
- output: card !== undefined ? `${call.name} not confirmed by the operator; nothing was written or installed` : `patch not approved by the user; not executed`,
60058
+ output: untrustedOrigin ? untrustedDenial : card !== undefined ? `${call.name} not confirmed by the operator; nothing was written or installed` : `patch not approved by the user; not executed`,
59639
60059
  isError: true
59640
60060
  };
59641
60061
  }
@@ -59648,11 +60068,11 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, re
59648
60068
  if (gated.decision === "deny") {
59649
60069
  return { output: gated.denyMessage ?? `tool "${call.name}" refused by a policy hook`, isError: true };
59650
60070
  }
59651
- if (gated.decision === "ask") {
60071
+ if (gated.decision === "ask" || untrustedOrigin) {
59652
60072
  const fingerprint = toolCallHash(call.name, call.input);
59653
- const response = requestApproval === undefined ? false : await requestApproval(call.name, call.input, { fingerprint, destructive: false, hookAsk: true });
60073
+ const response = requestApproval === undefined ? false : await requestApproval(call.name, call.input, { fingerprint, destructive: false, ...gated.hookAsked ? { hookAsk: true } : {}, ...untrustedOrigin ? { untrustedOrigin: true } : {} });
59654
60074
  if (!isApprovalFor(response, fingerprint)) {
59655
- return { output: `${call.name} not approved by the user; not executed`, isError: true };
60075
+ return { output: untrustedOrigin ? untrustedDenial : `${call.name} not approved by the user; not executed`, isError: true };
59656
60076
  }
59657
60077
  }
59658
60078
  } else {
@@ -112108,6 +112528,7 @@ function resolveFqn(catalog, fqn) {
112108
112528
  }
112109
112529
 
112110
112530
  // src/mcp-servers/http-headers.ts
112531
+ init_service4();
112111
112532
  var VAR_PATTERN2 = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-[^}]*)?\}/;
112112
112533
  var VAR_PATTERN_ALL = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
112113
112534
  function unresolvedVariables(raw, env) {
@@ -112225,6 +112646,14 @@ function elideCredentialText(raw) {
112225
112646
  const trimmed = query === -1 ? raw : `${raw.slice(0, query)}?\u2026`;
112226
112647
  return trimmed.replace(/^([a-zA-Z][a-zA-Z0-9+.-]*:)?\/\/[^/@]*@/, (_m, scheme) => `${scheme ?? ""}//\u2026@`);
112227
112648
  }
112649
+ var MASKED_SEGMENT_MIN_LENGTH = 16;
112650
+ function maskSecretShapedPathSegments(pathname) {
112651
+ return pathname.split("/").map((segment) => {
112652
+ if (segment.length < MASKED_SEGMENT_MIN_LENGTH)
112653
+ return segment;
112654
+ return detectSecrets(segment).length > 0 || looksSecretShaped(segment) ? "\u2026" : segment;
112655
+ }).join("/");
112656
+ }
112228
112657
  function displayUrl(raw) {
112229
112658
  if (raw === undefined)
112230
112659
  return "";
@@ -112234,7 +112663,8 @@ function displayUrl(raw) {
112234
112663
  const parsed = new URL(raw);
112235
112664
  const query = parsed.search === "" ? "" : "?\u2026";
112236
112665
  const auth = parsed.username === "" ? "" : "\u2026@";
112237
- return `${parsed.protocol}//${auth}${parsed.host}${parsed.pathname}${query}`;
112666
+ const pathname = maskSecretShapedPathSegments(parsed.pathname);
112667
+ return `${parsed.protocol}//${auth}${parsed.host}${pathname}${query}`;
112238
112668
  } catch {
112239
112669
  return elideCredentialText(raw);
112240
112670
  }
@@ -113645,139 +114075,10 @@ function fileForScope(scope, targets) {
113645
114075
  return projectConfigFile(targets.projectRoot);
113646
114076
  }
113647
114077
 
113648
- // src/harness/external/env-deny.ts
113649
- var EXTERNAL_ENV_DENY = [
113650
- "ANTHROPIC_API_KEY",
113651
- "ANTHROPIC_AUTH_TOKEN",
113652
- "ANTHROPIC_BASE_URL",
113653
- "ANTHROPIC_MODEL",
113654
- "CLAUDE_CONFIG_DIR",
113655
- "CODEX_HOME",
113656
- "CLAUDECODE"
113657
- ];
113658
- var EXTERNAL_ENV_PREFIX_SWEEPS = ["CLAUDE_CODE_", "KERYX_"];
113659
-
113660
114078
  // src/mcp-servers/spawn-env.ts
114079
+ init_service4();
113661
114080
  init_shell_config();
113662
- var MCP_ENV_ALLOW_EXACT = ["PWD", "OLDPWD"];
113663
- var MCP_ENV_DENY = [
113664
- "SSH_AUTH_SOCK",
113665
- "SSH_AGENT_PID",
113666
- "GPG_AGENT_INFO",
113667
- "GNUPGHOME",
113668
- "GNOME_KEYRING_CONTROL",
113669
- "GNOME_KEYRING_PID",
113670
- "KDE_FULL_SESSION",
113671
- "GIT_ASKPASS",
113672
- "SSH_ASKPASS",
113673
- "SUDO_ASKPASS",
113674
- "GIT_CREDENTIAL_HELPER",
113675
- "GIT_SSH_COMMAND",
113676
- "KUBECONFIG",
113677
- "NETRC",
113678
- "PGPASSFILE",
113679
- "PGSERVICEFILE",
113680
- "GOOGLE_APPLICATION_CREDENTIALS",
113681
- "CLOUDSDK_CONFIG",
113682
- "DOCKER_CONFIG",
113683
- "AWS_SHARED_CREDENTIALS_FILE",
113684
- "AWS_CONFIG_FILE",
113685
- "BOTO_CONFIG",
113686
- "AZURE_CONFIG_DIR",
113687
- "CURLRC",
113688
- "WGETRC",
113689
- "NPMRC",
113690
- "NPM_CONFIG_USERCONFIG",
113691
- "PIP_CONFIG_FILE",
113692
- "RCLONE_CONFIG",
113693
- "MAVEN_SETTINGS",
113694
- "GIT_CONFIG",
113695
- "GIT_CONFIG_GLOBAL",
113696
- "HGRCPATH",
113697
- "ANSIBLE_VAULT_PASSWORD_FILE",
113698
- "AZURE_CLIENT_CERTIFICATE_PATH",
113699
- "ARM_CLIENT_CERTIFICATE_PATH",
113700
- "VAULT_CLIENT_CERT",
113701
- "VAULT_CLIENT_KEY",
113702
- "SSL_CLIENT_CERT_PATH",
113703
- "SSL_CLIENT_KEY_FILE",
113704
- "IDENTITY_FILE",
113705
- "SSH_IDENTITY_FILE",
113706
- "GCP_SERVICE_ACCOUNT",
113707
- "XAUTHORITY",
113708
- "OPENAI_API_KEY",
113709
- "GEMINI_API_KEY",
113710
- "GOOGLE_API_KEY",
113711
- "GROQ_API_KEY",
113712
- "MISTRAL_API_KEY",
113713
- "XAI_API_KEY",
113714
- "DEEPSEEK_API_KEY",
113715
- "OPENROUTER_API_KEY",
113716
- "TOGETHER_API_KEY",
113717
- "PERPLEXITY_API_KEY"
113718
- ];
113719
- var MCP_ENV_PREFIX_SWEEPS = [
113720
- "OP_SESSION",
113721
- "BW_SESSION"
113722
- ];
113723
- var SECRET_SEGMENTS = [
113724
- "KEY",
113725
- "KEYS",
113726
- "TOKEN",
113727
- "TOKENS",
113728
- "SECRET",
113729
- "SECRETS",
113730
- "PASSWORD",
113731
- "PASSWD",
113732
- "PWD",
113733
- "PASS",
113734
- "PASSPHRASE",
113735
- "PASSCODE",
113736
- "PASSKEY",
113737
- "CREDENTIAL",
113738
- "CREDENTIALS",
113739
- "CREDS",
113740
- "AUTH",
113741
- "AUTHORIZATION",
113742
- "BEARER",
113743
- "JWT",
113744
- "COOKIE",
113745
- "SIGNATURE",
113746
- "APIKEY",
113747
- "SECRETKEY",
113748
- "ACCESSKEY",
113749
- "AUTHTOKEN",
113750
- "PAT",
113751
- "WEBHOOK"
113752
- ];
113753
- var SECRET_SEGMENT_RE = new RegExp(`(^|_)(${SECRET_SEGMENTS.join("|")})($|_)`);
113754
- var SECRET_SUBSTRING_RE = /(PASSWORD|PASSWD|PASSPHRASE|PASSCODE|SSHPASS|SECRET|CREDENTIAL|APIKEY|KEYFILE|PRIVKEY)/;
113755
- var GLUED_SECRET_RE = /(PRIVATE|SECRET|ACCESS|REFRESH|SESSION|CLIENT|API|APP)(KEY|TOKEN|SECRET)|DB(PASS|PWD)/;
113756
- var CONNECTION_STRING_RE = /(^|_)(DSN|CONNECTION_STRING|CONNECTIONSTRING)($|_)/;
113757
- var CREDENTIAL_IN_VALUE_RE = /:\/\/[^/\s@]*(:[^/\s@]*)?@/;
113758
- var VALUE_DEPENDENT = {
113759
- DOCKER_HOST: (value) => !value.startsWith("unix://") && !value.startsWith("fd://")
113760
- };
113761
- function isDeniedForMcpChild(name, value) {
113762
- if (value !== undefined && CREDENTIAL_IN_VALUE_RE.test(value))
113763
- return true;
113764
- const upper = name.toUpperCase();
113765
- if (MCP_ENV_ALLOW_EXACT.includes(upper))
113766
- return false;
113767
- const conditional = VALUE_DEPENDENT[upper];
113768
- if (conditional !== undefined) {
113769
- return value === undefined ? true : conditional(value);
113770
- }
113771
- if (EXTERNAL_ENV_DENY.some((denied) => denied.toUpperCase() === upper))
113772
- return true;
113773
- if (MCP_ENV_DENY.includes(upper))
113774
- return true;
113775
- if (EXTERNAL_ENV_PREFIX_SWEEPS.some((prefix) => upper.startsWith(prefix.toUpperCase())))
113776
- return true;
113777
- if (MCP_ENV_PREFIX_SWEEPS.some((prefix) => upper.startsWith(prefix)))
113778
- return true;
113779
- return SECRET_SEGMENT_RE.test(upper) || SECRET_SUBSTRING_RE.test(upper) || GLUED_SECRET_RE.test(upper) || CONNECTION_STRING_RE.test(upper);
113780
- }
114081
+ init_service4();
113781
114082
  function buildMcpChildEnv(input) {
113782
114083
  const saved = input.savedCredentialKeys ?? new Set([...savedCredentialEnvKeys(), ...declaredCredentialEnvKeys(input.configDir)]);
113783
114084
  const env = {};
@@ -127701,7 +128002,7 @@ async function dispatchLocked(projectRoot, entry, flow, dispatch, deps, runId) {
127701
128002
  });
127702
128003
  const provider = guardUsage(built, () => meter.markUsageMissing());
127703
128004
  try {
127704
- const tools = buildUnattendedRoster(worktree, unattendedRunner(worktree, sandbox));
128005
+ const tools = [...buildUnattendedRoster(worktree, unattendedRunner(worktree, sandbox)), ...deps.extraTools ?? []];
127705
128006
  const toolNames = tools.map((t) => t.definition.name);
127706
128007
  const shellHooks = buildShellHookRuntime({
127707
128008
  projectRoot: worktree,
@@ -127756,6 +128057,9 @@ async function dispatchLocked(projectRoot, entry, flow, dispatch, deps, runId) {
127756
128057
  signal: controller.signal
127757
128058
  });
127758
128059
  finishReason = result.finishReason;
128060
+ if (result.caughtToolErrors !== undefined && result.caughtToolErrors.length > 0) {
128061
+ crashed = result.caughtToolErrors.map((e) => `${e.toolName}: ${e.message}`).join("; ");
128062
+ }
127759
128063
  } catch (error) {
127760
128064
  crashed = error instanceof Error ? error.message : String(error);
127761
128065
  } finally {
@@ -128790,6 +129094,70 @@ function createSystemWebWorkerRunner() {
128790
129094
  return new SystemWebWorkerRunner({ workspace: process.cwd(), home: homedir10() });
128791
129095
  }
128792
129096
 
129097
+ // src/harness/web/outbound-secret.ts
129098
+ init_service4();
129099
+ function safeDecodeComponent(value) {
129100
+ try {
129101
+ return decodeURIComponent(value);
129102
+ } catch {
129103
+ return value;
129104
+ }
129105
+ }
129106
+ function outboundUrlComponents(text) {
129107
+ let url;
129108
+ try {
129109
+ url = new URL(text);
129110
+ } catch {
129111
+ return [];
129112
+ }
129113
+ const components = [];
129114
+ for (const segment of url.pathname.split("/")) {
129115
+ if (segment.length > 0) {
129116
+ components.push(safeDecodeComponent(segment));
129117
+ }
129118
+ }
129119
+ for (const [, value] of url.searchParams) {
129120
+ if (value.length > 0) {
129121
+ components.push(value);
129122
+ }
129123
+ }
129124
+ if (url.hash.length > 1) {
129125
+ components.push(safeDecodeComponent(url.hash.slice(1)));
129126
+ }
129127
+ return components;
129128
+ }
129129
+ function anyComponentLooksSecret(text) {
129130
+ const entropyGateOpen = isEntropyBackendEnabled();
129131
+ for (const component of outboundUrlComponents(text)) {
129132
+ if (detectSecrets(component).length > 0) {
129133
+ return true;
129134
+ }
129135
+ if (entropyGateOpen && looksSecretShapedIn(component)) {
129136
+ return true;
129137
+ }
129138
+ }
129139
+ return false;
129140
+ }
129141
+ function containsOutboundSecret(text) {
129142
+ if (detectSecrets(text).length > 0) {
129143
+ return true;
129144
+ }
129145
+ if (isEntropyBackendEnabled() && detectEntropy(text).length > 0) {
129146
+ return true;
129147
+ }
129148
+ return anyComponentLooksSecret(text);
129149
+ }
129150
+ async function recordOutboundSecretFinding(cwd, tool, subject) {
129151
+ try {
129152
+ await appendIncident(cwd, {
129153
+ at: new Date().toISOString(),
129154
+ type: "egress-blocked",
129155
+ message: "outbound secret-shaped content",
129156
+ details: { policyId: "egress.outbound-secret", tool, subject }
129157
+ });
129158
+ } catch {}
129159
+ }
129160
+
128793
129161
  // src/harness/tool/builtin/web-fetch-tool.ts
128794
129162
  function transportFor(deps) {
128795
129163
  if (deps.transport !== undefined)
@@ -128813,6 +129181,12 @@ function webFetchTool(deps = {}) {
128813
129181
  if (typeof input.url !== "string") {
128814
129182
  return { output: "web_fetch: url must be an absolute HTTPS URL without credentials", isError: true };
128815
129183
  }
129184
+ if (containsOutboundSecret(input.url)) {
129185
+ if (deps.cwd !== undefined) {
129186
+ await recordOutboundSecretFinding(deps.cwd, "web_fetch", "url");
129187
+ }
129188
+ return { output: "web_fetch: outbound secret-shaped content", isError: true };
129189
+ }
128816
129190
  const result = await transport.fetchPage({ url: input.url, providerId: "web_fetch" });
128817
129191
  return result.ok ? { output: result.value.text, isError: false, untrusted: true } : { output: `web_fetch: ${result.reason}`, isError: true };
128818
129192
  }
@@ -128843,7 +129217,7 @@ ${result.canonicalUrl}`;
128843
129217
  return lines.join(`
128844
129218
  `).trim();
128845
129219
  }
128846
- function webSearchTool(service) {
129220
+ function webSearchTool(service, deps = {}) {
128847
129221
  return {
128848
129222
  definition: {
128849
129223
  name: "web_search",
@@ -128855,7 +129229,14 @@ function webSearchTool(service) {
128855
129229
  if (typeof input.query !== "string" || input.query.trim().length === 0) {
128856
129230
  return { output: "web_search: query must be a non-empty string", isError: true };
128857
129231
  }
128858
- const response = await service.search(input.query.trim());
129232
+ const query = input.query.trim();
129233
+ if (containsOutboundSecret(query)) {
129234
+ if (deps.cwd !== undefined) {
129235
+ await recordOutboundSecretFinding(deps.cwd, "web_search", "query");
129236
+ }
129237
+ return { output: "web_search: outbound secret-shaped content", isError: true };
129238
+ }
129239
+ const response = await service.search(query);
128859
129240
  if (!response.ok) {
128860
129241
  return {
128861
129242
  output: response.reason === "no-active-provider" ? "web_search: no active connected provider. Use /search-provider to configure one, test it, then use /search-connect to select it." : response.reason === "search-failed" ? `web_search: search failed${response.detail !== undefined && response.detail.length > 0 ? ` (${response.detail})` : ""}. Wait a few minutes before trying again, and do not switch providers yourself.` : "web_search: active provider is unavailable; reconnect it with /search-provider before retrying.",
@@ -129351,8 +129732,8 @@ function buildInteractiveAgentTools(input) {
129351
129732
  const built = [
129352
129733
  ...builtinReadOnlyTools(input.cwd),
129353
129734
  ...metaprojectTools,
129354
- webFetchTool(),
129355
- webSearchTool(input.searchController),
129735
+ webFetchTool({ cwd: input.cwd }),
129736
+ webSearchTool(input.searchController, { cwd: input.cwd }),
129356
129737
  shellExecTool(input.cwd, undefined, jobRegistry),
129357
129738
  ...jobRegistry !== undefined ? [
129358
129739
  shellTaskOutputTool(jobRegistry, { observer: "main" }),
@@ -129825,12 +130206,13 @@ function evaluateShellApproval(input) {
129825
130206
  const publishLease = input.meta?.publishLease === true;
129826
130207
  const publishLeaseDetail = input.meta?.publishLeaseDetail;
129827
130208
  const hookAsk = input.meta?.hookAsk === true;
130209
+ const untrustedOrigin = input.meta?.untrustedOrigin === true;
129828
130210
  const audit = io.loadAudit();
129829
130211
  for (const pattern of audit.permissions.allow) {
129830
130212
  input.sessionAllow.add(pattern);
129831
130213
  }
129832
130214
  const tampered = io.fingerprint() !== input.fingerprintAtStart;
129833
- const autoApprove = !destructive && !credentials && !sacReviewConfirmation && !publishLease && !hookAsk && isShellCommandAllowed(command, [...input.sessionAllow]);
130215
+ const autoApprove = !destructive && !credentials && !sacReviewConfirmation && !publishLease && !hookAsk && !untrustedOrigin && isShellCommandAllowed(command, [...input.sessionAllow]);
129834
130216
  return {
129835
130217
  command,
129836
130218
  destructive,
@@ -129839,6 +130221,7 @@ function evaluateShellApproval(input) {
129839
130221
  publishLease,
129840
130222
  ...publishLeaseDetail !== undefined ? { publishLeaseDetail } : {},
129841
130223
  hookAsk,
130224
+ untrustedOrigin,
129842
130225
  autoApprove,
129843
130226
  rejected: audit.rejected,
129844
130227
  tampered
@@ -129872,6 +130255,9 @@ function formatShellApprovalHints(evaled) {
129872
130255
  if (evaled.hookAsk) {
129873
130256
  lines.push("a policy hook asked about this call \u2014 will not be remembered");
129874
130257
  }
130258
+ if (evaled.untrustedOrigin) {
130259
+ lines.push("follows untrusted external content \u2014 will not be remembered");
130260
+ }
129875
130261
  if (evaled.publishLease) {
129876
130262
  lines.push(evaled.publishLeaseDetail !== undefined ? `a git-publish lease applies \u2014 ${evaled.publishLeaseDetail} \u2014 will not be remembered` : "a peer's git-publish lease applies \u2014 will not be remembered");
129877
130263
  }
@@ -131945,6 +132331,12 @@ function readRuntimeBlock(dispatch) {
131945
132331
  return raw;
131946
132332
  }
131947
132333
 
132334
+ // src/harness/external/env.ts
132335
+ init_service4();
132336
+
132337
+ // src/harness/external/env-deny.ts
132338
+ init_service4();
132339
+
131948
132340
  // src/harness/external/env.ts
131949
132341
  var EXTERNAL_RUNTIME_CREDENTIAL_ALLOW = {
131950
132342
  "codex-cli": ["OPENAI_API_KEY"],
@@ -131952,17 +132344,18 @@ var EXTERNAL_RUNTIME_CREDENTIAL_ALLOW = {
131952
132344
  };
131953
132345
  var ENV_EXTERNAL_DEPTH = "KERYX_EXTERNAL_DEPTH";
131954
132346
  function buildExternalChildEnv(input) {
131955
- const denied = new Set(EXTERNAL_ENV_DENY);
132347
+ const denied = new Set(EXTERNAL_ENV_DENY.map((name) => name.toUpperCase()));
131956
132348
  const allowedForThisRuntime = new Set((EXTERNAL_RUNTIME_CREDENTIAL_ALLOW[input.runtimeId] ?? []).map((name) => name.toUpperCase()));
131957
132349
  const env = {};
131958
132350
  for (const [key, value] of Object.entries(input.parent)) {
131959
132351
  if (value === undefined)
131960
132352
  continue;
131961
- if (denied.has(key))
132353
+ const upper = key.toUpperCase();
132354
+ if (denied.has(upper))
131962
132355
  continue;
131963
- if (EXTERNAL_ENV_PREFIX_SWEEPS.some((prefix) => key.startsWith(prefix)))
132356
+ if (EXTERNAL_ENV_PREFIX_SWEEPS.some((prefix) => upper.startsWith(prefix.toUpperCase())))
131964
132357
  continue;
131965
- if (!allowedForThisRuntime.has(key.toUpperCase()) && isDeniedForMcpChild(key, value))
132358
+ if (!allowedForThisRuntime.has(upper) && isDeniedForMcpChild(key, value))
131966
132359
  continue;
131967
132360
  env[key] = value;
131968
132361
  }
@@ -148035,7 +148428,7 @@ function ollamaShowAllowed(baseUrl) {
148035
148428
  } catch {
148036
148429
  return false;
148037
148430
  }
148038
- if (isPrivateEgressHost(host) && !isLoopbackHost(host)) {
148431
+ if (isPrivateEgressHost(host) && !isLoopbackHost2(host)) {
148039
148432
  return false;
148040
148433
  }
148041
148434
  return true;
@@ -153368,7 +153761,12 @@ async function runLocked(projectRoot, entry, action, deps, runId, now) {
153368
153761
  const env = deps.grantedEnv ?? process.env;
153369
153762
  const extraSecrets = await ghAuthToken(binaries.realpaths["gh"], grantedCwd, env);
153370
153763
  const granted = action.grants.tools.map((id) => grantedToolSpec(id)).filter((spec) => spec !== undefined).map((spec) => grantedTool(spec, action, grantedCwd, env, grantedCalls, binaries.realpaths, extraSecrets, binaries.stats));
153371
- const tools = [...builtinReadOnlyTools(projectRoot), shellExecTool(workdir, sandboxedRunner(workdir, sandbox)), ...granted];
153764
+ const tools = [
153765
+ ...builtinReadOnlyTools(projectRoot),
153766
+ shellExecTool(workdir, sandboxedRunner(workdir, sandbox)),
153767
+ ...granted,
153768
+ ...deps.extraTools ?? []
153769
+ ];
153372
153770
  const offending = tools.filter((t) => UNATTENDED_EXCLUDED_TOOLS.includes(t.definition.name));
153373
153771
  if (offending.length > 0)
153374
153772
  throw new Error(`agent-task roster must not offer: ${offending.map((t) => t.definition.name).join(", ")}`);
@@ -153418,6 +153816,9 @@ async function runLocked(projectRoot, entry, action, deps, runId, now) {
153418
153816
  };
153419
153817
  const result = await runAgentTurn(io, agentDeps, history, buildAgentTaskPrompt(entry, action), { signal: controller.signal });
153420
153818
  finishReason = result.finishReason;
153819
+ if (result.caughtToolErrors !== undefined && result.caughtToolErrors.length > 0) {
153820
+ crashed = result.caughtToolErrors.map((e) => `${e.toolName}: ${e.message}`).join("; ");
153821
+ }
153421
153822
  } catch (error) {
153422
153823
  crashed = error instanceof Error ? error.message : String(error);
153423
153824
  } finally {
@@ -155507,14 +155908,19 @@ function attachUsageIo(io, chrome) {
155507
155908
  }
155508
155909
  });
155509
155910
  }
155510
- function estimateTaskCostUsd(profile, inputTokens, outputTokens) {
155911
+ var OPENAI_CACHED_INPUT_DISCOUNT = 0.5;
155912
+ function estimateTaskCostUsd(profile, providerId, inputTokens, outputTokens, cacheReadTokens) {
155511
155913
  if (profile === undefined)
155512
155914
  return;
155513
155915
  const priceIn = profile.priceInputPerMillion.value;
155514
155916
  const priceOut = profile.priceOutputPerMillion.value;
155515
155917
  if (priceIn === "unknown" || priceOut === "unknown")
155516
155918
  return;
155517
- return inputTokens / 1e6 * priceIn + outputTokens / 1e6 * priceOut;
155919
+ const cachedInputDiscount = providerId === "openai" || providerId === "openai-codex" ? OPENAI_CACHED_INPUT_DISCOUNT : 1;
155920
+ const cacheRead = cacheReadTokens !== undefined && cacheReadTokens > 0 ? Math.min(cacheReadTokens, inputTokens) : 0;
155921
+ const fullPriceInputTokens = inputTokens - cacheRead;
155922
+ const inputCost = fullPriceInputTokens / 1e6 * priceIn + cacheRead / 1e6 * priceIn * cachedInputDiscount;
155923
+ return inputCost + outputTokens / 1e6 * priceOut;
155518
155924
  }
155519
155925
  async function recordTurnTaskCostBestEffort(input) {
155520
155926
  if (input.inputTokens === 0 && input.outputTokens === 0)
@@ -155522,7 +155928,7 @@ async function recordTurnTaskCostBestEffort(input) {
155522
155928
  try {
155523
155929
  const profiles = loadModelProfiles(input.userConfigDir);
155524
155930
  const profile = profiles[profileKey(input.providerId, input.modelId)];
155525
- const costUsd = estimateTaskCostUsd(profile, input.inputTokens, input.outputTokens);
155931
+ const costUsd = estimateTaskCostUsd(profile, input.providerId, input.inputTokens, input.outputTokens, input.cacheReadTokens);
155526
155932
  await appendTaskCostRecord({
155527
155933
  providerId: input.providerId,
155528
155934
  modelId: input.modelId,
@@ -155536,8 +155942,8 @@ async function recordTurnTaskCostBestEffort(input) {
155536
155942
  }, input.userConfigDir);
155537
155943
  } catch {}
155538
155944
  }
155539
- function shouldAutoApproveReadOnlySpawn(mode, hookAsk) {
155540
- return mode === "read_only" && hookAsk !== true;
155945
+ function shouldAutoApproveReadOnlySpawn(mode, hookAsk, untrustedOrigin) {
155946
+ return mode === "read_only" && hookAsk !== true && untrustedOrigin !== true;
155541
155947
  }
155542
155948
  function isUserPromptApprovalCard(tool) {
155543
155949
  return tool === "user_prompt";
@@ -157753,7 +158159,7 @@ async function launchTuiAgentShell(opts) {
157753
158159
  }
157754
158160
  }
157755
158161
  } catch {}
157756
- if (shouldAutoApproveReadOnlySpawn(mode, meta?.hookAsk)) {
158162
+ if (shouldAutoApproveReadOnlySpawn(mode, meta?.hookAsk, meta?.untrustedOrigin)) {
157757
158163
  transcript.add(new otui.TextRenderable(r, {
157758
158164
  id: `ap${uid++}`,
157759
158165
  content: otui.t`${roleChunk(otui, "accent", "\u25C7 subagent auto-approved")} ${dimChunk(otui, `mode=read_only (no shell) \xB7 ${taskPreview}`)}`
@@ -157763,7 +158169,7 @@ async function launchTuiAgentShell(opts) {
157763
158169
  chrome.hideMenu();
157764
158170
  setMainAgent("blocked", "approval");
157765
158171
  const id = await chrome.withOverlay(() => showComposerChoice(otui, r, chrome.dock, {
157766
- title: mode === "read_only" ? "Spawn read-only subagent? (policy hook asked)" : "Spawn general subagent?",
158172
+ title: mode === "read_only" ? meta?.hookAsk === true ? "Spawn read-only subagent? (policy hook asked)" : "Spawn read-only subagent? (follows untrusted content)" : "Spawn general subagent?",
157767
158173
  subtitle: taskPreview,
157768
158174
  cancelId: "deny",
157769
158175
  onOpen: () => chrome.blurComposer(),
@@ -157958,7 +158364,7 @@ async function launchTuiAgentShell(opts) {
157958
158364
  const choice = await chrome.withOverlay(() => pickShellApproval(otui, r, chrome.dock, cmd, approvalContext, destructive, meta?.credentials === true, {
157959
158365
  onOpen: () => chrome.blurComposer(),
157960
158366
  signal: foregroundOperation.signal
157961
- }, ev.publishLease, ev.publishLeaseDetail, ev.hookAsk));
158367
+ }, ev.publishLease, ev.publishLeaseDetail, ev.hookAsk || ev.untrustedOrigin));
157962
158368
  input.focus();
157963
158369
  if (choice === "deny") {
157964
158370
  transcript.add(new otui.TextRenderable(r, {
@@ -160296,10 +160702,12 @@ Usage: /reasoning <${REASONING_EFFORT_LEVELS.join("|")}>
160296
160702
  };
160297
160703
  let turnInputTokens = 0;
160298
160704
  let turnOutputTokens = 0;
160705
+ let turnCacheReadTokens = 0;
160299
160706
  const prevOnUsageForCost = io.onUsage;
160300
160707
  io.onUsage = (usage) => {
160301
160708
  turnInputTokens += usage.inputTokens ?? 0;
160302
160709
  turnOutputTokens += usage.outputTokens ?? 0;
160710
+ turnCacheReadTokens += usage.cacheReadTokens ?? 0;
160303
160711
  prevOnUsageForCost?.(usage);
160304
160712
  };
160305
160713
  const suggestNextStep = async () => {
@@ -160399,6 +160807,7 @@ Usage: /reasoning <${REASONING_EFFORT_LEVELS.join("|")}>
160399
160807
  modelId: deps.modelId,
160400
160808
  inputTokens: turnInputTokens,
160401
160809
  outputTokens: turnOutputTokens,
160810
+ ...turnCacheReadTokens > 0 ? { cacheReadTokens: turnCacheReadTokens } : {},
160402
160811
  success: !turnFailed,
160403
160812
  ...turnCategory !== undefined ? { category: turnCategory } : {}
160404
160813
  });
@@ -162012,7 +162421,9 @@ async function runAgentRepl(lines, rich, deps, metaprojectPort, sessionOpts, ini
162012
162421
  return settled;
162013
162422
  }
162014
162423
  const completion = new Promise((resolve) => {
162424
+ completionWaiters = [];
162015
162425
  completionWaiters.push(() => resolve({ kind: "completion" }));
162426
+ rich.onCompletionWaitersSize?.(completionWaiters.length);
162016
162427
  });
162017
162428
  const winner = await Promise.race([pendingLine, completion]);
162018
162429
  if (winner.kind !== "completion") {
@@ -162229,7 +162640,7 @@ ${indentBlock(style.dim(context), GUTTER)}`);
162229
162640
  out(`${GUTTER}${style.yellow(hint)}
162230
162641
  `);
162231
162642
  }
162232
- const rememberable = !evaled.destructive && !evaled.credentials && !evaled.sacReviewConfirmation && !evaled.publishLease && !evaled.hookAsk;
162643
+ const rememberable = !evaled.destructive && !evaled.credentials && !evaled.sacReviewConfirmation && !evaled.publishLease && !evaled.hookAsk && !evaled.untrustedOrigin;
162233
162644
  const prompt = rememberable ? "[y/N/A=always] " : "[y/N] ";
162234
162645
  out(`
162235
162646
  ${GUTTER}${style.yellow(`Run: ${evaled.command}`)} ${style.dim(prompt)}`);
@@ -162677,6 +163088,8 @@ ${GUTTER}${turnSeparator()}
162677
163088
  continue;
162678
163089
  }
162679
163090
  await closeSlateSession(slateSession, mintTimestampAttemptId);
163091
+ lastToolOutput = undefined;
163092
+ lastToolName = undefined;
162680
163093
  if (nextLive !== undefined) {
162681
163094
  live = nextLive;
162682
163095
  history = [];