@mrciphersmith/keryx 0.3.18 → 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 +645 -305
  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.18",
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();
@@ -29144,7 +29545,7 @@ class OpenAiCompatEngine {
29144
29545
  } catch {
29145
29546
  host = baseUrl;
29146
29547
  }
29147
- 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);
29148
29549
  if (!permitted) {
29149
29550
  yield errorEvent({
29150
29551
  kind: "invalid_request",
@@ -58893,53 +59294,18 @@ ${shown.join(`
58893
59294
  }
58894
59295
  const risk = toolByName.get(call.name)?.definition.risk;
58895
59296
  const isPureReadTool = risk === "read" && !DURABLE_READ_TOOL_NAMES.has(call.name);
58896
- if (!isPureReadTool && untrustedContentSeen) {
58897
- const approver = io.requestApproval;
58898
- if (deps.unattended === true || approver === undefined) {
58899
- const result = {
58900
- output: "tool blocked: external web content cannot authorize further tool calls in this turn",
58901
- isError: true
58902
- };
58903
- if (deps.unattended === true) {
58904
- io.onUnattendedDenial?.(call.name, "untrusted external content in this turn cannot authorize the call");
58905
- }
58906
- io.onToolResult?.(call.name, result);
58907
- history.push({ role: "tool", content: withBudgetWarning(result.output), provenance: "tool", toolCallId: call.id, ts: now() });
58908
- io.onHistoryChange?.("tool");
58909
- gateBlockedAny = true;
58910
- continue;
58911
- }
58912
- const taintFingerprint = toolCallHash(call.name, call.input);
58913
- let taintApproved;
58914
- try {
58915
- const response = await approver(call.name, call.input, {
58916
- fingerprint: taintFingerprint,
58917
- destructive: risk === "destructive",
58918
- untrustedOrigin: true
58919
- });
58920
- taintApproved = isApprovalFor(response, taintFingerprint);
58921
- } catch (err) {
58922
- const result = {
58923
- output: `${call.name} not executed: the approval prompt failed (${err instanceof Error ? err.message : String(err)})`,
58924
- isError: true
58925
- };
58926
- io.onToolResult?.(call.name, result);
58927
- history.push({ role: "tool", content: withBudgetWarning(result.output), provenance: "tool", toolCallId: call.id, ts: now() });
58928
- io.onHistoryChange?.("tool");
58929
- gateBlockedAny = true;
58930
- continue;
58931
- }
58932
- if (!taintApproved) {
58933
- const result = {
58934
- output: "tool blocked: external web content cannot authorize this call, and the user did not authorize it either",
58935
- isError: true
58936
- };
58937
- io.onToolResult?.(call.name, result);
58938
- history.push({ role: "tool", content: withBudgetWarning(result.output), provenance: "tool", toolCallId: call.id, ts: now() });
58939
- io.onHistoryChange?.("tool");
58940
- gateBlockedAny = true;
58941
- continue;
58942
- }
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;
58943
59309
  }
58944
59310
  io.onToolCall?.(call.name, call.input);
58945
59311
  const reservation = reservationByCallId.get(call.id) ?? reserveToolAttempt(budget, call.name, call.input);
@@ -58961,7 +59327,7 @@ ${shown.join(`
58961
59327
  result = precomputedResult;
58962
59328
  } else {
58963
59329
  try {
58964
- 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);
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);
58965
59331
  } catch (err) {
58966
59332
  const message = err instanceof Error ? err.message : String(err);
58967
59333
  result = { output: `${call.name} failed: ${message}`, isError: true };
@@ -59531,7 +59897,7 @@ function composeWithHook(toolName, rawDecision, hookResult, interactive) {
59531
59897
  }
59532
59898
  return { decision, hookTightened: true, hookAsked, ...denyMessage !== undefined ? { denyMessage } : {} };
59533
59899
  }
59534
- 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) {
59535
59901
  const tool = toolByName.get(call.name);
59536
59902
  if (tool === undefined) {
59537
59903
  return { output: `unknown tool: ${call.name}`, isError: true };
@@ -59557,6 +59923,7 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, re
59557
59923
  }
59558
59924
  }
59559
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";
59560
59927
  const isReadOnly = readOnly?.() ?? false;
59561
59928
  let hookResult;
59562
59929
  if (hooks !== undefined) {
@@ -59597,7 +59964,7 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, re
59597
59964
  isError: true
59598
59965
  };
59599
59966
  }
59600
- if (gated.decision === "auto") {
59967
+ if (gated.decision === "auto" && !untrustedOrigin) {
59601
59968
  onAutoApproved?.(call.name, call.input, { destructive, credentials });
59602
59969
  } else {
59603
59970
  const fingerprint = toolCallHash(call.name, call.input);
@@ -59607,10 +59974,11 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, re
59607
59974
  ...credentials ? { credentials } : {},
59608
59975
  ...publishLease ? { publishLease } : {},
59609
59976
  ...publishLease && publishLeaseHolder !== undefined ? { publishLeaseDetail: `held by @${publishLeaseHolder.name} \u2014 "${publishLeaseHolder.reason}"` } : {},
59610
- ...gated.hookAsked ? { hookAsk: true } : {}
59977
+ ...gated.hookAsked ? { hookAsk: true } : {},
59978
+ ...untrustedOrigin ? { untrustedOrigin: true } : {}
59611
59979
  });
59612
59980
  if (!isApprovalFor(response, fingerprint)) {
59613
- 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 };
59614
59982
  }
59615
59983
  }
59616
59984
  } else if (risk === "delegate") {
@@ -59629,17 +59997,18 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, re
59629
59997
  isError: true
59630
59998
  };
59631
59999
  }
59632
- if (gated.decision === "auto") {
60000
+ if (gated.decision === "auto" && !untrustedOrigin) {
59633
60001
  onAutoApproved?.(call.name, call.input, { destructive: false, credentials: false });
59634
60002
  } else {
59635
60003
  const fingerprint = toolCallHash(call.name, call.input);
59636
60004
  const response = requestApproval === undefined ? false : await requestApproval(call.name, call.input, {
59637
60005
  fingerprint,
59638
60006
  destructive: false,
59639
- ...gated.hookAsked ? { hookAsk: true } : {}
60007
+ ...gated.hookAsked ? { hookAsk: true } : {},
60008
+ ...untrustedOrigin ? { untrustedOrigin: true } : {}
59640
60009
  });
59641
60010
  if (!isApprovalFor(response, fingerprint)) {
59642
- 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 };
59643
60012
  }
59644
60013
  }
59645
60014
  } else if (risk === "write") {
@@ -59670,7 +60039,7 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, re
59670
60039
  isError: true
59671
60040
  };
59672
60041
  }
59673
- if (gated.decision === "auto") {
60042
+ if (gated.decision === "auto" && !untrustedOrigin) {
59674
60043
  onAutoApproved?.(call.name, call.input, { destructive, credentials });
59675
60044
  } else {
59676
60045
  const fingerprint = toolCallHash(call.name, call.input);
@@ -59679,13 +60048,14 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, re
59679
60048
  destructive,
59680
60049
  ...credentials ? { credentials } : {},
59681
60050
  ...card !== undefined ? { alwaysAsk: true, card } : {},
59682
- ...gated.hookAsked ? { hookAsk: true } : {}
60051
+ ...gated.hookAsked ? { hookAsk: true } : {},
60052
+ ...untrustedOrigin ? { untrustedOrigin: true } : {}
59683
60053
  });
59684
60054
  if (!isApprovalFor(response, fingerprint)) {
59685
60055
  if (confirmationToken !== undefined)
59686
60056
  tool.confirmationDeclined?.(confirmationToken);
59687
60057
  return {
59688
- 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`,
59689
60059
  isError: true
59690
60060
  };
59691
60061
  }
@@ -59698,11 +60068,11 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, re
59698
60068
  if (gated.decision === "deny") {
59699
60069
  return { output: gated.denyMessage ?? `tool "${call.name}" refused by a policy hook`, isError: true };
59700
60070
  }
59701
- if (gated.decision === "ask") {
60071
+ if (gated.decision === "ask" || untrustedOrigin) {
59702
60072
  const fingerprint = toolCallHash(call.name, call.input);
59703
- 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 } : {} });
59704
60074
  if (!isApprovalFor(response, fingerprint)) {
59705
- 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 };
59706
60076
  }
59707
60077
  }
59708
60078
  } else {
@@ -112158,6 +112528,7 @@ function resolveFqn(catalog, fqn) {
112158
112528
  }
112159
112529
 
112160
112530
  // src/mcp-servers/http-headers.ts
112531
+ init_service4();
112161
112532
  var VAR_PATTERN2 = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-[^}]*)?\}/;
112162
112533
  var VAR_PATTERN_ALL = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
112163
112534
  function unresolvedVariables(raw, env) {
@@ -112275,6 +112646,14 @@ function elideCredentialText(raw) {
112275
112646
  const trimmed = query === -1 ? raw : `${raw.slice(0, query)}?\u2026`;
112276
112647
  return trimmed.replace(/^([a-zA-Z][a-zA-Z0-9+.-]*:)?\/\/[^/@]*@/, (_m, scheme) => `${scheme ?? ""}//\u2026@`);
112277
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
+ }
112278
112657
  function displayUrl(raw) {
112279
112658
  if (raw === undefined)
112280
112659
  return "";
@@ -112284,7 +112663,8 @@ function displayUrl(raw) {
112284
112663
  const parsed = new URL(raw);
112285
112664
  const query = parsed.search === "" ? "" : "?\u2026";
112286
112665
  const auth = parsed.username === "" ? "" : "\u2026@";
112287
- return `${parsed.protocol}//${auth}${parsed.host}${parsed.pathname}${query}`;
112666
+ const pathname = maskSecretShapedPathSegments(parsed.pathname);
112667
+ return `${parsed.protocol}//${auth}${parsed.host}${pathname}${query}`;
112288
112668
  } catch {
112289
112669
  return elideCredentialText(raw);
112290
112670
  }
@@ -113695,139 +114075,10 @@ function fileForScope(scope, targets) {
113695
114075
  return projectConfigFile(targets.projectRoot);
113696
114076
  }
113697
114077
 
113698
- // src/harness/external/env-deny.ts
113699
- var EXTERNAL_ENV_DENY = [
113700
- "ANTHROPIC_API_KEY",
113701
- "ANTHROPIC_AUTH_TOKEN",
113702
- "ANTHROPIC_BASE_URL",
113703
- "ANTHROPIC_MODEL",
113704
- "CLAUDE_CONFIG_DIR",
113705
- "CODEX_HOME",
113706
- "CLAUDECODE"
113707
- ];
113708
- var EXTERNAL_ENV_PREFIX_SWEEPS = ["CLAUDE_CODE_", "KERYX_"];
113709
-
113710
114078
  // src/mcp-servers/spawn-env.ts
114079
+ init_service4();
113711
114080
  init_shell_config();
113712
- var MCP_ENV_ALLOW_EXACT = ["PWD", "OLDPWD"];
113713
- var MCP_ENV_DENY = [
113714
- "SSH_AUTH_SOCK",
113715
- "SSH_AGENT_PID",
113716
- "GPG_AGENT_INFO",
113717
- "GNUPGHOME",
113718
- "GNOME_KEYRING_CONTROL",
113719
- "GNOME_KEYRING_PID",
113720
- "KDE_FULL_SESSION",
113721
- "GIT_ASKPASS",
113722
- "SSH_ASKPASS",
113723
- "SUDO_ASKPASS",
113724
- "GIT_CREDENTIAL_HELPER",
113725
- "GIT_SSH_COMMAND",
113726
- "KUBECONFIG",
113727
- "NETRC",
113728
- "PGPASSFILE",
113729
- "PGSERVICEFILE",
113730
- "GOOGLE_APPLICATION_CREDENTIALS",
113731
- "CLOUDSDK_CONFIG",
113732
- "DOCKER_CONFIG",
113733
- "AWS_SHARED_CREDENTIALS_FILE",
113734
- "AWS_CONFIG_FILE",
113735
- "BOTO_CONFIG",
113736
- "AZURE_CONFIG_DIR",
113737
- "CURLRC",
113738
- "WGETRC",
113739
- "NPMRC",
113740
- "NPM_CONFIG_USERCONFIG",
113741
- "PIP_CONFIG_FILE",
113742
- "RCLONE_CONFIG",
113743
- "MAVEN_SETTINGS",
113744
- "GIT_CONFIG",
113745
- "GIT_CONFIG_GLOBAL",
113746
- "HGRCPATH",
113747
- "ANSIBLE_VAULT_PASSWORD_FILE",
113748
- "AZURE_CLIENT_CERTIFICATE_PATH",
113749
- "ARM_CLIENT_CERTIFICATE_PATH",
113750
- "VAULT_CLIENT_CERT",
113751
- "VAULT_CLIENT_KEY",
113752
- "SSL_CLIENT_CERT_PATH",
113753
- "SSL_CLIENT_KEY_FILE",
113754
- "IDENTITY_FILE",
113755
- "SSH_IDENTITY_FILE",
113756
- "GCP_SERVICE_ACCOUNT",
113757
- "XAUTHORITY",
113758
- "OPENAI_API_KEY",
113759
- "GEMINI_API_KEY",
113760
- "GOOGLE_API_KEY",
113761
- "GROQ_API_KEY",
113762
- "MISTRAL_API_KEY",
113763
- "XAI_API_KEY",
113764
- "DEEPSEEK_API_KEY",
113765
- "OPENROUTER_API_KEY",
113766
- "TOGETHER_API_KEY",
113767
- "PERPLEXITY_API_KEY"
113768
- ];
113769
- var MCP_ENV_PREFIX_SWEEPS = [
113770
- "OP_SESSION",
113771
- "BW_SESSION"
113772
- ];
113773
- var SECRET_SEGMENTS = [
113774
- "KEY",
113775
- "KEYS",
113776
- "TOKEN",
113777
- "TOKENS",
113778
- "SECRET",
113779
- "SECRETS",
113780
- "PASSWORD",
113781
- "PASSWD",
113782
- "PWD",
113783
- "PASS",
113784
- "PASSPHRASE",
113785
- "PASSCODE",
113786
- "PASSKEY",
113787
- "CREDENTIAL",
113788
- "CREDENTIALS",
113789
- "CREDS",
113790
- "AUTH",
113791
- "AUTHORIZATION",
113792
- "BEARER",
113793
- "JWT",
113794
- "COOKIE",
113795
- "SIGNATURE",
113796
- "APIKEY",
113797
- "SECRETKEY",
113798
- "ACCESSKEY",
113799
- "AUTHTOKEN",
113800
- "PAT",
113801
- "WEBHOOK"
113802
- ];
113803
- var SECRET_SEGMENT_RE = new RegExp(`(^|_)(${SECRET_SEGMENTS.join("|")})($|_)`);
113804
- var SECRET_SUBSTRING_RE = /(PASSWORD|PASSWD|PASSPHRASE|PASSCODE|SSHPASS|SECRET|CREDENTIAL|APIKEY|KEYFILE|PRIVKEY)/;
113805
- var GLUED_SECRET_RE = /(PRIVATE|SECRET|ACCESS|REFRESH|SESSION|CLIENT|API|APP)(KEY|TOKEN|SECRET)|DB(PASS|PWD)/;
113806
- var CONNECTION_STRING_RE = /(^|_)(DSN|CONNECTION_STRING|CONNECTIONSTRING)($|_)/;
113807
- var CREDENTIAL_IN_VALUE_RE = /:\/\/[^/\s@]*(:[^/\s@]*)?@/;
113808
- var VALUE_DEPENDENT = {
113809
- DOCKER_HOST: (value) => !value.startsWith("unix://") && !value.startsWith("fd://")
113810
- };
113811
- function isDeniedForMcpChild(name, value) {
113812
- if (value !== undefined && CREDENTIAL_IN_VALUE_RE.test(value))
113813
- return true;
113814
- const upper = name.toUpperCase();
113815
- if (MCP_ENV_ALLOW_EXACT.includes(upper))
113816
- return false;
113817
- const conditional = VALUE_DEPENDENT[upper];
113818
- if (conditional !== undefined) {
113819
- return value === undefined ? true : conditional(value);
113820
- }
113821
- if (EXTERNAL_ENV_DENY.some((denied) => denied.toUpperCase() === upper))
113822
- return true;
113823
- if (MCP_ENV_DENY.includes(upper))
113824
- return true;
113825
- if (EXTERNAL_ENV_PREFIX_SWEEPS.some((prefix) => upper.startsWith(prefix.toUpperCase())))
113826
- return true;
113827
- if (MCP_ENV_PREFIX_SWEEPS.some((prefix) => upper.startsWith(prefix)))
113828
- return true;
113829
- return SECRET_SEGMENT_RE.test(upper) || SECRET_SUBSTRING_RE.test(upper) || GLUED_SECRET_RE.test(upper) || CONNECTION_STRING_RE.test(upper);
113830
- }
114081
+ init_service4();
113831
114082
  function buildMcpChildEnv(input) {
113832
114083
  const saved = input.savedCredentialKeys ?? new Set([...savedCredentialEnvKeys(), ...declaredCredentialEnvKeys(input.configDir)]);
113833
114084
  const env = {};
@@ -128843,6 +129094,70 @@ function createSystemWebWorkerRunner() {
128843
129094
  return new SystemWebWorkerRunner({ workspace: process.cwd(), home: homedir10() });
128844
129095
  }
128845
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
+
128846
129161
  // src/harness/tool/builtin/web-fetch-tool.ts
128847
129162
  function transportFor(deps) {
128848
129163
  if (deps.transport !== undefined)
@@ -128866,6 +129181,12 @@ function webFetchTool(deps = {}) {
128866
129181
  if (typeof input.url !== "string") {
128867
129182
  return { output: "web_fetch: url must be an absolute HTTPS URL without credentials", isError: true };
128868
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
+ }
128869
129190
  const result = await transport.fetchPage({ url: input.url, providerId: "web_fetch" });
128870
129191
  return result.ok ? { output: result.value.text, isError: false, untrusted: true } : { output: `web_fetch: ${result.reason}`, isError: true };
128871
129192
  }
@@ -128896,7 +129217,7 @@ ${result.canonicalUrl}`;
128896
129217
  return lines.join(`
128897
129218
  `).trim();
128898
129219
  }
128899
- function webSearchTool(service) {
129220
+ function webSearchTool(service, deps = {}) {
128900
129221
  return {
128901
129222
  definition: {
128902
129223
  name: "web_search",
@@ -128908,7 +129229,14 @@ function webSearchTool(service) {
128908
129229
  if (typeof input.query !== "string" || input.query.trim().length === 0) {
128909
129230
  return { output: "web_search: query must be a non-empty string", isError: true };
128910
129231
  }
128911
- 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);
128912
129240
  if (!response.ok) {
128913
129241
  return {
128914
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.",
@@ -129404,8 +129732,8 @@ function buildInteractiveAgentTools(input) {
129404
129732
  const built = [
129405
129733
  ...builtinReadOnlyTools(input.cwd),
129406
129734
  ...metaprojectTools,
129407
- webFetchTool(),
129408
- webSearchTool(input.searchController),
129735
+ webFetchTool({ cwd: input.cwd }),
129736
+ webSearchTool(input.searchController, { cwd: input.cwd }),
129409
129737
  shellExecTool(input.cwd, undefined, jobRegistry),
129410
129738
  ...jobRegistry !== undefined ? [
129411
129739
  shellTaskOutputTool(jobRegistry, { observer: "main" }),
@@ -129878,12 +130206,13 @@ function evaluateShellApproval(input) {
129878
130206
  const publishLease = input.meta?.publishLease === true;
129879
130207
  const publishLeaseDetail = input.meta?.publishLeaseDetail;
129880
130208
  const hookAsk = input.meta?.hookAsk === true;
130209
+ const untrustedOrigin = input.meta?.untrustedOrigin === true;
129881
130210
  const audit = io.loadAudit();
129882
130211
  for (const pattern of audit.permissions.allow) {
129883
130212
  input.sessionAllow.add(pattern);
129884
130213
  }
129885
130214
  const tampered = io.fingerprint() !== input.fingerprintAtStart;
129886
- const autoApprove = !destructive && !credentials && !sacReviewConfirmation && !publishLease && !hookAsk && isShellCommandAllowed(command, [...input.sessionAllow]);
130215
+ const autoApprove = !destructive && !credentials && !sacReviewConfirmation && !publishLease && !hookAsk && !untrustedOrigin && isShellCommandAllowed(command, [...input.sessionAllow]);
129887
130216
  return {
129888
130217
  command,
129889
130218
  destructive,
@@ -129892,6 +130221,7 @@ function evaluateShellApproval(input) {
129892
130221
  publishLease,
129893
130222
  ...publishLeaseDetail !== undefined ? { publishLeaseDetail } : {},
129894
130223
  hookAsk,
130224
+ untrustedOrigin,
129895
130225
  autoApprove,
129896
130226
  rejected: audit.rejected,
129897
130227
  tampered
@@ -129925,6 +130255,9 @@ function formatShellApprovalHints(evaled) {
129925
130255
  if (evaled.hookAsk) {
129926
130256
  lines.push("a policy hook asked about this call \u2014 will not be remembered");
129927
130257
  }
130258
+ if (evaled.untrustedOrigin) {
130259
+ lines.push("follows untrusted external content \u2014 will not be remembered");
130260
+ }
129928
130261
  if (evaled.publishLease) {
129929
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");
129930
130263
  }
@@ -131998,6 +132331,12 @@ function readRuntimeBlock(dispatch) {
131998
132331
  return raw;
131999
132332
  }
132000
132333
 
132334
+ // src/harness/external/env.ts
132335
+ init_service4();
132336
+
132337
+ // src/harness/external/env-deny.ts
132338
+ init_service4();
132339
+
132001
132340
  // src/harness/external/env.ts
132002
132341
  var EXTERNAL_RUNTIME_CREDENTIAL_ALLOW = {
132003
132342
  "codex-cli": ["OPENAI_API_KEY"],
@@ -132005,17 +132344,18 @@ var EXTERNAL_RUNTIME_CREDENTIAL_ALLOW = {
132005
132344
  };
132006
132345
  var ENV_EXTERNAL_DEPTH = "KERYX_EXTERNAL_DEPTH";
132007
132346
  function buildExternalChildEnv(input) {
132008
- const denied = new Set(EXTERNAL_ENV_DENY);
132347
+ const denied = new Set(EXTERNAL_ENV_DENY.map((name) => name.toUpperCase()));
132009
132348
  const allowedForThisRuntime = new Set((EXTERNAL_RUNTIME_CREDENTIAL_ALLOW[input.runtimeId] ?? []).map((name) => name.toUpperCase()));
132010
132349
  const env = {};
132011
132350
  for (const [key, value] of Object.entries(input.parent)) {
132012
132351
  if (value === undefined)
132013
132352
  continue;
132014
- if (denied.has(key))
132353
+ const upper = key.toUpperCase();
132354
+ if (denied.has(upper))
132015
132355
  continue;
132016
- if (EXTERNAL_ENV_PREFIX_SWEEPS.some((prefix) => key.startsWith(prefix)))
132356
+ if (EXTERNAL_ENV_PREFIX_SWEEPS.some((prefix) => upper.startsWith(prefix.toUpperCase())))
132017
132357
  continue;
132018
- if (!allowedForThisRuntime.has(key.toUpperCase()) && isDeniedForMcpChild(key, value))
132358
+ if (!allowedForThisRuntime.has(upper) && isDeniedForMcpChild(key, value))
132019
132359
  continue;
132020
132360
  env[key] = value;
132021
132361
  }
@@ -148088,7 +148428,7 @@ function ollamaShowAllowed(baseUrl) {
148088
148428
  } catch {
148089
148429
  return false;
148090
148430
  }
148091
- if (isPrivateEgressHost(host) && !isLoopbackHost(host)) {
148431
+ if (isPrivateEgressHost(host) && !isLoopbackHost2(host)) {
148092
148432
  return false;
148093
148433
  }
148094
148434
  return true;
@@ -155602,8 +155942,8 @@ async function recordTurnTaskCostBestEffort(input) {
155602
155942
  }, input.userConfigDir);
155603
155943
  } catch {}
155604
155944
  }
155605
- function shouldAutoApproveReadOnlySpawn(mode, hookAsk) {
155606
- return mode === "read_only" && hookAsk !== true;
155945
+ function shouldAutoApproveReadOnlySpawn(mode, hookAsk, untrustedOrigin) {
155946
+ return mode === "read_only" && hookAsk !== true && untrustedOrigin !== true;
155607
155947
  }
155608
155948
  function isUserPromptApprovalCard(tool) {
155609
155949
  return tool === "user_prompt";
@@ -157819,7 +158159,7 @@ async function launchTuiAgentShell(opts) {
157819
158159
  }
157820
158160
  }
157821
158161
  } catch {}
157822
- if (shouldAutoApproveReadOnlySpawn(mode, meta?.hookAsk)) {
158162
+ if (shouldAutoApproveReadOnlySpawn(mode, meta?.hookAsk, meta?.untrustedOrigin)) {
157823
158163
  transcript.add(new otui.TextRenderable(r, {
157824
158164
  id: `ap${uid++}`,
157825
158165
  content: otui.t`${roleChunk(otui, "accent", "\u25C7 subagent auto-approved")} ${dimChunk(otui, `mode=read_only (no shell) \xB7 ${taskPreview}`)}`
@@ -157829,7 +158169,7 @@ async function launchTuiAgentShell(opts) {
157829
158169
  chrome.hideMenu();
157830
158170
  setMainAgent("blocked", "approval");
157831
158171
  const id = await chrome.withOverlay(() => showComposerChoice(otui, r, chrome.dock, {
157832
- 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?",
157833
158173
  subtitle: taskPreview,
157834
158174
  cancelId: "deny",
157835
158175
  onOpen: () => chrome.blurComposer(),
@@ -158024,7 +158364,7 @@ async function launchTuiAgentShell(opts) {
158024
158364
  const choice = await chrome.withOverlay(() => pickShellApproval(otui, r, chrome.dock, cmd, approvalContext, destructive, meta?.credentials === true, {
158025
158365
  onOpen: () => chrome.blurComposer(),
158026
158366
  signal: foregroundOperation.signal
158027
- }, ev.publishLease, ev.publishLeaseDetail, ev.hookAsk));
158367
+ }, ev.publishLease, ev.publishLeaseDetail, ev.hookAsk || ev.untrustedOrigin));
158028
158368
  input.focus();
158029
158369
  if (choice === "deny") {
158030
158370
  transcript.add(new otui.TextRenderable(r, {
@@ -162300,7 +162640,7 @@ ${indentBlock(style.dim(context), GUTTER)}`);
162300
162640
  out(`${GUTTER}${style.yellow(hint)}
162301
162641
  `);
162302
162642
  }
162303
- 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;
162304
162644
  const prompt = rememberable ? "[y/N/A=always] " : "[y/N] ";
162305
162645
  out(`
162306
162646
  ${GUTTER}${style.yellow(`Run: ${evaled.command}`)} ${style.dim(prompt)}`);