@mrciphersmith/keryx 0.3.18 → 0.3.20

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 +701 -305
  2. package/dist/core.js +547 -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,325 @@ 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 isPlainCasedWord(letters) {
9095
+ return /^[a-z]+$/.test(letters) || /^[A-Z]+$/.test(letters) || /^[A-Z][a-z]*$/.test(letters);
9096
+ }
9097
+ function isSingleTransitionTag(segment) {
9098
+ const letterThenDigit = /^([A-Za-z]+)([0-9]+)$/.exec(segment);
9099
+ if (letterThenDigit !== null) {
9100
+ const letters = letterThenDigit[1];
9101
+ return letters.length <= TAG_MAX_LETTER_RUN && isPlainCasedWord(letters);
9102
+ }
9103
+ const digitThenLetter = /^([0-9]+)([A-Za-z]+)$/.exec(segment);
9104
+ if (digitThenLetter !== null) {
9105
+ const letters = digitThenLetter[2];
9106
+ return letters.length <= TAG_MAX_LETTER_RUN && isPlainCasedWord(letters);
9107
+ }
9108
+ return false;
9109
+ }
9110
+ function isSlugHexTail(segment) {
9111
+ return SLUG_HEX_TAIL_RE.test(segment);
9112
+ }
9113
+ function isWordSlug(value) {
9114
+ const segments = value.split(/[-_]/);
9115
+ if (segments.length < 3) {
9116
+ return false;
9117
+ }
9118
+ let words = 0;
9119
+ for (let i = 0;i < segments.length; i += 1) {
9120
+ const segment = segments[i];
9121
+ const isDigits = /^[0-9]+$/.test(segment);
9122
+ if (isDigits) {
9123
+ continue;
9124
+ }
9125
+ if (isPlainCasedWord(segment)) {
9126
+ if (segment.length >= 3) {
9127
+ words += 1;
9128
+ }
9129
+ continue;
9130
+ }
9131
+ if (isSingleTransitionTag(segment)) {
9132
+ continue;
9133
+ }
9134
+ if (i === segments.length - 1 && isSlugHexTail(segment)) {
9135
+ continue;
9136
+ }
9137
+ return false;
9138
+ }
9139
+ return words >= 1;
9140
+ }
9141
+ function shannonEntropy(value) {
9142
+ const counts = new Map;
9143
+ for (const ch of value) {
9144
+ counts.set(ch, (counts.get(ch) ?? 0) + 1);
9145
+ }
9146
+ let entropy = 0;
9147
+ for (const count of counts.values()) {
9148
+ const p = count / value.length;
9149
+ entropy -= p * Math.log2(p);
9150
+ }
9151
+ return entropy;
9152
+ }
9153
+ function bareShapeQualifies(head) {
9154
+ if (!/[0-9]/.test(head) && !/[+=]/.test(head)) {
9155
+ return { qualifies: false, entropy: 0 };
9156
+ }
9157
+ if (isWordSlug(head)) {
9158
+ return { qualifies: false, entropy: 0 };
9159
+ }
9160
+ const entropy = shannonEntropy(head);
9161
+ const hexBlob = isHexBlob(head);
9162
+ if (entropy < 3.6 && !hexBlob) {
9163
+ return { qualifies: false, entropy };
9164
+ }
9165
+ return { qualifies: true, entropy };
9166
+ }
9167
+ function shapeQualifiesAsSecret(value) {
9168
+ if (isAllowShapedValue(value)) {
9169
+ return { qualifies: false, entropy: 0 };
9170
+ }
9171
+ return bareShapeQualifies(value);
9172
+ }
9173
+ function looksSecretShaped(value) {
9174
+ return shapeQualifiesAsSecret(value).qualifies;
9175
+ }
9176
+ function buildMatch(start, end, value, entropy) {
9177
+ const confidence = Math.max(0.4, Math.min(0.7, 0.4 + (entropy - 3.6) * 0.21));
9178
+ return {
9179
+ category: "secret",
9180
+ policyId: "secrets.high-entropy",
9181
+ severity: "medium",
9182
+ confidence: Number(confidence.toFixed(2)),
9183
+ start,
9184
+ end,
9185
+ value,
9186
+ mask: "entropy",
9187
+ remediation: "Verify this high-entropy value is not a live credential."
9188
+ };
9189
+ }
9190
+ function findUrlSpans(content) {
9191
+ const spans = [];
9192
+ URL_SPAN_RE.lastIndex = 0;
9193
+ let m;
9194
+ while ((m = URL_SPAN_RE.exec(content)) !== null) {
9195
+ let raw = m[0];
9196
+ while (raw.length > 0 && URL_TRAILING_PUNCTUATION_RE.test(raw)) {
9197
+ raw = raw.slice(0, -1);
9198
+ }
9199
+ if (raw.length > 0) {
9200
+ spans.push({ start: m.index, end: m.index + raw.length, raw });
9201
+ }
9202
+ if (m.index === URL_SPAN_RE.lastIndex) {
9203
+ URL_SPAN_RE.lastIndex += 1;
9204
+ }
9205
+ }
9206
+ return spans;
9207
+ }
9208
+ function urlComponentSpans(raw) {
9209
+ const spans = [];
9210
+ const schemeMatch = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.exec(raw);
9211
+ if (schemeMatch === null) {
9212
+ return spans;
9213
+ }
9214
+ let i = schemeMatch[0].length;
9215
+ while (i < raw.length && raw[i] !== "/" && raw[i] !== "?" && raw[i] !== "#") {
9216
+ i += 1;
9217
+ }
9218
+ let pathEnd = i;
9219
+ while (pathEnd < raw.length && raw[pathEnd] !== "?" && raw[pathEnd] !== "#") {
9220
+ pathEnd += 1;
9221
+ }
9222
+ let segStart = i;
9223
+ for (let j = i;j <= pathEnd; j += 1) {
9224
+ if (j === pathEnd || raw[j] === "/") {
9225
+ if (j > segStart) {
9226
+ spans.push({ start: segStart, end: j });
9227
+ }
9228
+ segStart = j + 1;
9229
+ }
9230
+ }
9231
+ let cursor = pathEnd;
9232
+ if (cursor < raw.length && raw[cursor] === "?") {
9233
+ let queryEnd = cursor + 1;
9234
+ while (queryEnd < raw.length && raw[queryEnd] !== "#") {
9235
+ queryEnd += 1;
9236
+ }
9237
+ let pairStart = cursor + 1;
9238
+ for (let j = cursor + 1;j <= queryEnd; j += 1) {
9239
+ if (j === queryEnd || raw[j] === "&") {
9240
+ const pair = raw.slice(pairStart, j);
9241
+ const eq = pair.indexOf("=");
9242
+ if (eq !== -1 && eq + 1 < pair.length) {
9243
+ spans.push({ start: pairStart + eq + 1, end: j });
9244
+ } else if (eq === -1 && pair.length > 0) {
9245
+ spans.push({ start: pairStart, end: j });
9246
+ }
9247
+ pairStart = j + 1;
9248
+ }
9249
+ }
9250
+ cursor = queryEnd;
9251
+ }
9252
+ if (cursor < raw.length && raw[cursor] === "#" && cursor + 1 < raw.length) {
9253
+ spans.push({ start: cursor + 1, end: raw.length });
9254
+ }
9255
+ return spans;
9256
+ }
9257
+ function secretShapedCandidatesIn(text) {
9258
+ const normalized = text.replace(/\+/g, " ");
9259
+ const out = [];
9260
+ TOKEN.lastIndex = 0;
9261
+ let m;
9262
+ while ((m = TOKEN.exec(normalized)) !== null) {
9263
+ const value = m[0];
9264
+ if (value.length < URL_COMPONENT_MIN_LENGTH) {
9265
+ continue;
9266
+ }
9267
+ const head = TOKEN_HEAD.exec(value)?.[0] ?? value;
9268
+ const { qualifies, entropy } = shapeQualifiesAsSecret(head);
9269
+ if (!qualifies) {
9270
+ continue;
9271
+ }
9272
+ const start = m.index;
9273
+ const end = start + value.length;
9274
+ out.push({ start, end, value: text.slice(start, end), entropy });
9275
+ }
9276
+ return out;
9277
+ }
9278
+ function looksSecretShapedIn(text) {
9279
+ return secretShapedCandidatesIn(text).length > 0;
9280
+ }
9281
+ function labelledPieceQualifies(piece) {
9282
+ const base = bareShapeQualifies(piece);
9283
+ if (base.qualifies) {
9284
+ return { entropy: base.entropy };
9285
+ }
9286
+ if (isAllowShapedValue(piece)) {
9287
+ return { entropy: base.entropy };
9288
+ }
9289
+ return null;
9290
+ }
9291
+ function firstQualifyingSlashPiece(head) {
9292
+ let offset = 0;
9293
+ for (const piece of head.split("/")) {
9294
+ if (piece.length > 0) {
9295
+ const found = labelledPieceQualifies(piece);
9296
+ if (found !== null) {
9297
+ return { offset, length: piece.length, entropy: found.entropy };
9298
+ }
9299
+ }
9300
+ offset += piece.length + 1;
9301
+ }
9302
+ return null;
9303
+ }
9304
+ function detectEntropy(content) {
9305
+ const matches = [];
9306
+ const urlSpans = findUrlSpans(content);
9307
+ for (const url of urlSpans) {
9308
+ for (const comp of urlComponentSpans(url.raw)) {
9309
+ const compText = url.raw.slice(comp.start, comp.end);
9310
+ for (const cand of secretShapedCandidatesIn(compText)) {
9311
+ const absStart = url.start + comp.start + cand.start;
9312
+ const absEnd = url.start + comp.start + cand.end;
9313
+ matches.push(buildMatch(absStart, absEnd, cand.value, cand.entropy));
9314
+ }
9315
+ }
9316
+ }
9317
+ LABEL_ASSIGNMENT_RE.lastIndex = 0;
9318
+ let am;
9319
+ while ((am = LABEL_ASSIGNMENT_RE.exec(content)) !== null) {
9320
+ const value = am[1];
9321
+ const end = am.index + am[0].length;
9322
+ const start = end - value.length;
9323
+ if (urlSpans.some((u) => start < u.end && end > u.start)) {
9324
+ continue;
9325
+ }
9326
+ const { qualifies, entropy } = bareShapeQualifies(value);
9327
+ if (qualifies) {
9328
+ matches.push(buildMatch(start, end, value, entropy));
9329
+ }
9330
+ }
9331
+ TOKEN.lastIndex = 0;
9332
+ let m;
9333
+ while ((m = TOKEN.exec(content)) !== null) {
9334
+ const value = m[0];
9335
+ const start = m.index;
9336
+ const end = start + value.length;
9337
+ if (urlSpans.some((u) => start < u.end && end > u.start)) {
9338
+ continue;
9339
+ }
9340
+ if (matches.some((existing) => start < existing.end && end > existing.start)) {
9341
+ continue;
9342
+ }
9343
+ const before = lastLineOf(content.slice(Math.max(0, start - LABEL_WINDOW), start));
9344
+ if (!ADJACENT_LABEL.test(before)) {
9345
+ continue;
9346
+ }
9347
+ const head = TOKEN_HEAD.exec(value)?.[0] ?? value;
9348
+ if (head.includes("/")) {
9349
+ const piece = firstQualifyingSlashPiece(head);
9350
+ if (piece === null) {
9351
+ continue;
9352
+ }
9353
+ const pieceStart = start + piece.offset;
9354
+ const pieceEnd = pieceStart + piece.length;
9355
+ matches.push(buildMatch(pieceStart, pieceEnd, content.slice(pieceStart, pieceEnd), piece.entropy));
9356
+ continue;
9357
+ }
9358
+ const found = labelledPieceQualifies(head);
9359
+ if (found === null) {
9360
+ continue;
9361
+ }
9362
+ matches.push(buildMatch(start, end, value, found.entropy));
9363
+ }
9364
+ return matches.sort((a, b) => a.start - b.start);
9365
+ }
9366
+ var SENSITIVE_LABEL_WORDS = "key|secret|token|password|passwd|api|credential|auth|bearer", caseless = (words) => words.replace(/[a-z]/g, (ch) => `[${ch}${ch.toUpperCase()}]`), ADJACENT_LABEL, LABEL_ASSIGNMENT_RE, TOKEN, TOKEN_HEAD, LABEL_WINDOW = 40, HEX_BLOB, FULL_GIT_SHA_RE, UUID_RE, INTEGRITY_RE, TAG_MAX_LETTER_RUN = 8, SLUG_HEX_TAIL_RE, URL_SPAN_RE, URL_TRAILING_PUNCTUATION_RE, URL_COMPONENT_MIN_LENGTH = 20;
9367
+ var init_entropy = __esm(() => {
9368
+ ADJACENT_LABEL = new RegExp(`(?:^|[^A-Za-z0-9])(?:${caseless(SENSITIVE_LABEL_WORDS)})[A-Za-z0-9_-]*["']?\\s*[:=]?\\s*["']?$`);
9369
+ LABEL_ASSIGNMENT_RE = new RegExp(`(?:^|[^A-Za-z0-9])(?:${SENSITIVE_LABEL_WORDS})[A-Za-z0-9_-]*"?\\s*=\\s*"?([A-Za-z0-9+/_=-]{6,})`, "gi");
9370
+ TOKEN = /[A-Za-z0-9+/=_-]{20,}(?:\.[A-Za-z0-9+/=_-]{6,})*/g;
9371
+ TOKEN_HEAD = /^[A-Za-z0-9+/=_-]+/;
9372
+ HEX_BLOB = /^[0-9a-f]{24,}$/i;
9373
+ FULL_GIT_SHA_RE = /^[0-9a-f]{40}$/i;
9374
+ UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
9375
+ INTEGRITY_RE = /^(?:sha256|sha512)-[A-Za-z0-9+/]+=*$/;
9376
+ SLUG_HEX_TAIL_RE = /^[0-9a-f]{10,16}$/i;
9377
+ URL_SPAN_RE = /\bhttps?:\/\/[^\s<>"'\\]+/g;
9378
+ URL_TRAILING_PUNCTUATION_RE = /[.,;:!?)\]}]$/;
9379
+ });
9380
+
9381
+ // src/security/entropy-gate.ts
9382
+ function ensureLoading() {
9383
+ if (loadStarted) {
9384
+ return;
9385
+ }
9386
+ loadStarted = true;
9387
+ loadSecurityConfig(process.cwd()).then((config) => {
9388
+ cachedEnabled = config.backends.entropy.enabled;
9389
+ }).catch(() => {});
9390
+ }
9391
+ function isEntropyBackendEnabled() {
9392
+ ensureLoading();
9393
+ return cachedEnabled;
9394
+ }
9395
+ var cachedEnabled, loadStarted = false;
9396
+ var init_entropy_gate = __esm(() => {
9397
+ init_config();
9398
+ cachedEnabled = DEFAULT_SECURITY_CONFIG.backends.entropy.enabled;
9399
+ });
9400
+
9063
9401
  // src/security/redact.ts
9064
9402
  import path14 from "path";
9065
9403
  import { createHmac, randomBytes } from "crypto";
@@ -9129,7 +9467,12 @@ function redactSensitiveText(text) {
9129
9467
  if (text.length === 0) {
9130
9468
  return text;
9131
9469
  }
9132
- const matches = [...detectSecrets(text), ...detectPii(text), ...detectExfil(text)];
9470
+ const matches = [
9471
+ ...detectSecrets(text),
9472
+ ...detectPii(text),
9473
+ ...detectExfil(text),
9474
+ ...isEntropyBackendEnabled() ? detectEntropy(text) : []
9475
+ ];
9133
9476
  if (matches.length === 0) {
9134
9477
  return text;
9135
9478
  }
@@ -9166,6 +9509,8 @@ var init_redact = __esm(() => {
9166
9509
  init_secrets();
9167
9510
  init_pii();
9168
9511
  init_exfil();
9512
+ init_entropy();
9513
+ init_entropy_gate();
9169
9514
  });
9170
9515
 
9171
9516
  // src/security/output-validation.ts
@@ -9909,102 +10254,33 @@ var init_seam = __esm(() => {
9909
10254
  init_warn_once();
9910
10255
  });
9911
10256
 
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
- });
10257
+ // src/security/detect/injection.ts
10258
+ function foldForMatching(content) {
10259
+ let folded = "";
10260
+ for (let i = 0;i < content.length; i += 1) {
10261
+ const ch = content[i];
10262
+ folded += CONFUSABLES[ch] ?? ch;
9982
10263
  }
9983
- return matches;
10264
+ const nfkc = folded.normalize("NFKC");
10265
+ return nfkc.length === folded.length ? nfkc : folded;
9984
10266
  }
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
10267
  function detectInjection(content) {
9995
10268
  const matches = [];
10269
+ const folded = foldForMatching(content);
9996
10270
  for (const pattern of PATTERNS) {
9997
10271
  pattern.regex.lastIndex = 0;
9998
10272
  let m;
9999
- while ((m = pattern.regex.exec(content)) !== null) {
10273
+ while ((m = pattern.regex.exec(folded)) !== null) {
10274
+ const start = Math.min(m.index, content.length);
10275
+ const end = Math.min(m.index + m[0].length, content.length);
10000
10276
  matches.push({
10001
10277
  category: "prompt-injection",
10002
10278
  policyId: pattern.policyId,
10003
10279
  severity: "low",
10004
10280
  confidence: pattern.confidence,
10005
- start: m.index,
10006
- end: m.index + m[0].length,
10007
- value: m[0],
10281
+ start,
10282
+ end,
10283
+ value: content.slice(start, end),
10008
10284
  remediation: "Treat external content as data, not instruction; require human review."
10009
10285
  });
10010
10286
  if (m.index === pattern.regex.lastIndex) {
@@ -10014,27 +10290,69 @@ function detectInjection(content) {
10014
10290
  }
10015
10291
  return matches;
10016
10292
  }
10017
- var PATTERNS;
10293
+ var CONFUSABLES, PATTERNS;
10018
10294
  var init_injection = __esm(() => {
10295
+ CONFUSABLES = {
10296
+ "\u0430": "a",
10297
+ "\u0410": "A",
10298
+ "\u0435": "e",
10299
+ "\u0415": "E",
10300
+ "\u043E": "o",
10301
+ "\u041E": "O",
10302
+ "\u0440": "p",
10303
+ "\u0420": "P",
10304
+ "\u0441": "c",
10305
+ "\u0421": "C",
10306
+ "\u0445": "x",
10307
+ "\u0425": "X",
10308
+ "\u0443": "y",
10309
+ "\u0423": "Y",
10310
+ "\u0455": "s",
10311
+ "\u0405": "S",
10312
+ "\u0456": "o",
10313
+ "\u0406": "O",
10314
+ "\u0458": "j",
10315
+ "\u0408": "J",
10316
+ "\u03B1": "a",
10317
+ "\u0391": "A",
10318
+ "\u03B2": "b",
10319
+ "\u0392": "B",
10320
+ "\u03B5": "e",
10321
+ "\u0395": "E",
10322
+ "\u03B9": "i",
10323
+ "\u0399": "I",
10324
+ "\u03BA": "k",
10325
+ "\u039A": "K",
10326
+ "\u03BF": "o",
10327
+ "\u039F": "O",
10328
+ "\u03C1": "p",
10329
+ "\u03A1": "P",
10330
+ "\u03C4": "t",
10331
+ "\u03A4": "T",
10332
+ "\u03C5": "u",
10333
+ "\u03A5": "Y",
10334
+ "\u03C7": "x",
10335
+ "\u03A7": "X"
10336
+ };
10019
10337
  PATTERNS = [
10020
10338
  {
10021
10339
  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,
10340
+ 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
10341
  confidence: 0.4
10024
10342
  },
10025
10343
  {
10026
10344
  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,
10345
+ 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
10346
  confidence: 0.45
10029
10347
  },
10030
10348
  {
10031
10349
  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,
10350
+ 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
10351
  confidence: 0.4
10034
10352
  },
10035
10353
  {
10036
10354
  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,
10355
+ 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
10356
  confidence: 0.35
10039
10357
  }
10040
10358
  ];
@@ -11204,6 +11522,140 @@ var init_read_source = __esm(() => {
11204
11522
  init_contained_path();
11205
11523
  });
11206
11524
 
11525
+ // src/security/credential-shape.ts
11526
+ function isDeniedForMcpChild(name, value) {
11527
+ if (value !== undefined && CREDENTIAL_IN_VALUE_RE.test(value))
11528
+ return true;
11529
+ const upper = name.toUpperCase();
11530
+ if (MCP_ENV_ALLOW_EXACT.includes(upper))
11531
+ return false;
11532
+ const conditional = VALUE_DEPENDENT[upper];
11533
+ if (conditional !== undefined) {
11534
+ return value === undefined ? true : conditional(value);
11535
+ }
11536
+ if (EXTERNAL_ENV_DENY.some((denied) => denied.toUpperCase() === upper))
11537
+ return true;
11538
+ if (MCP_ENV_DENY.includes(upper))
11539
+ return true;
11540
+ if (EXTERNAL_ENV_PREFIX_SWEEPS.some((prefix) => upper.startsWith(prefix.toUpperCase())))
11541
+ return true;
11542
+ if (MCP_ENV_PREFIX_SWEEPS.some((prefix) => upper.startsWith(prefix)))
11543
+ return true;
11544
+ return SECRET_SEGMENT_RE.test(upper) || SECRET_SUBSTRING_RE.test(upper) || GLUED_SECRET_RE.test(upper) || CONNECTION_STRING_RE.test(upper);
11545
+ }
11546
+ 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;
11547
+ var init_credential_shape = __esm(() => {
11548
+ EXTERNAL_ENV_DENY = [
11549
+ "ANTHROPIC_API_KEY",
11550
+ "ANTHROPIC_AUTH_TOKEN",
11551
+ "ANTHROPIC_BASE_URL",
11552
+ "ANTHROPIC_MODEL",
11553
+ "CLAUDE_CONFIG_DIR",
11554
+ "CODEX_HOME",
11555
+ "CLAUDECODE"
11556
+ ];
11557
+ EXTERNAL_ENV_PREFIX_SWEEPS = ["CLAUDE_CODE_", "KERYX_"];
11558
+ MCP_ENV_ALLOW_EXACT = ["PWD", "OLDPWD"];
11559
+ MCP_ENV_DENY = [
11560
+ "SSH_AUTH_SOCK",
11561
+ "SSH_AGENT_PID",
11562
+ "GPG_AGENT_INFO",
11563
+ "GNUPGHOME",
11564
+ "GNOME_KEYRING_CONTROL",
11565
+ "GNOME_KEYRING_PID",
11566
+ "KDE_FULL_SESSION",
11567
+ "GIT_ASKPASS",
11568
+ "SSH_ASKPASS",
11569
+ "SUDO_ASKPASS",
11570
+ "GIT_CREDENTIAL_HELPER",
11571
+ "GIT_SSH_COMMAND",
11572
+ "KUBECONFIG",
11573
+ "NETRC",
11574
+ "PGPASSFILE",
11575
+ "PGSERVICEFILE",
11576
+ "GOOGLE_APPLICATION_CREDENTIALS",
11577
+ "CLOUDSDK_CONFIG",
11578
+ "DOCKER_CONFIG",
11579
+ "AWS_SHARED_CREDENTIALS_FILE",
11580
+ "AWS_CONFIG_FILE",
11581
+ "BOTO_CONFIG",
11582
+ "AZURE_CONFIG_DIR",
11583
+ "CURLRC",
11584
+ "WGETRC",
11585
+ "NPMRC",
11586
+ "NPM_CONFIG_USERCONFIG",
11587
+ "PIP_CONFIG_FILE",
11588
+ "RCLONE_CONFIG",
11589
+ "MAVEN_SETTINGS",
11590
+ "GIT_CONFIG",
11591
+ "GIT_CONFIG_GLOBAL",
11592
+ "HGRCPATH",
11593
+ "ANSIBLE_VAULT_PASSWORD_FILE",
11594
+ "AZURE_CLIENT_CERTIFICATE_PATH",
11595
+ "ARM_CLIENT_CERTIFICATE_PATH",
11596
+ "VAULT_CLIENT_CERT",
11597
+ "VAULT_CLIENT_KEY",
11598
+ "SSL_CLIENT_CERT_PATH",
11599
+ "SSL_CLIENT_KEY_FILE",
11600
+ "IDENTITY_FILE",
11601
+ "SSH_IDENTITY_FILE",
11602
+ "GCP_SERVICE_ACCOUNT",
11603
+ "XAUTHORITY",
11604
+ "OPENAI_API_KEY",
11605
+ "GEMINI_API_KEY",
11606
+ "GOOGLE_API_KEY",
11607
+ "GROQ_API_KEY",
11608
+ "MISTRAL_API_KEY",
11609
+ "XAI_API_KEY",
11610
+ "DEEPSEEK_API_KEY",
11611
+ "OPENROUTER_API_KEY",
11612
+ "TOGETHER_API_KEY",
11613
+ "PERPLEXITY_API_KEY"
11614
+ ];
11615
+ MCP_ENV_PREFIX_SWEEPS = [
11616
+ "OP_SESSION",
11617
+ "BW_SESSION"
11618
+ ];
11619
+ SECRET_SEGMENTS = [
11620
+ "KEY",
11621
+ "KEYS",
11622
+ "TOKEN",
11623
+ "TOKENS",
11624
+ "SECRET",
11625
+ "SECRETS",
11626
+ "PASSWORD",
11627
+ "PASSWD",
11628
+ "PWD",
11629
+ "PASS",
11630
+ "PASSPHRASE",
11631
+ "PASSCODE",
11632
+ "PASSKEY",
11633
+ "CREDENTIAL",
11634
+ "CREDENTIALS",
11635
+ "CREDS",
11636
+ "AUTH",
11637
+ "AUTHORIZATION",
11638
+ "BEARER",
11639
+ "JWT",
11640
+ "COOKIE",
11641
+ "SIGNATURE",
11642
+ "APIKEY",
11643
+ "SECRETKEY",
11644
+ "ACCESSKEY",
11645
+ "AUTHTOKEN",
11646
+ "PAT",
11647
+ "WEBHOOK"
11648
+ ];
11649
+ SECRET_SEGMENT_RE = new RegExp(`(^|_)(${SECRET_SEGMENTS.join("|")})($|_)`);
11650
+ SECRET_SUBSTRING_RE = /(PASSWORD|PASSWD|PASSPHRASE|PASSCODE|SSHPASS|SECRET|CREDENTIAL|APIKEY|KEYFILE|PRIVKEY)/;
11651
+ GLUED_SECRET_RE = /(PRIVATE|SECRET|ACCESS|REFRESH|SESSION|CLIENT|API|APP)(KEY|TOKEN|SECRET)($|_)|DB(PASS|PWD)($|_)/;
11652
+ CONNECTION_STRING_RE = /(^|_)(DSN|CONNECTION_STRING|CONNECTIONSTRING)($|_)/;
11653
+ CREDENTIAL_IN_VALUE_RE = /:\/\/[^/\s@]*(:[^/\s@]*)?@/;
11654
+ VALUE_DEPENDENT = {
11655
+ DOCKER_HOST: (value) => !value.startsWith("unix://") && !value.startsWith("fd://")
11656
+ };
11657
+ });
11658
+
11207
11659
  // src/security/audit-harness/codex-toml.ts
11208
11660
  function stripComment(line) {
11209
11661
  let quoted = false;
@@ -17641,7 +18093,7 @@ var package_default;
17641
18093
  var init_package = __esm(() => {
17642
18094
  package_default = {
17643
18095
  name: "@mrciphersmith/keryx",
17644
- version: "0.3.18",
18096
+ version: "0.3.20",
17645
18097
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
17646
18098
  private: false,
17647
18099
  publishConfig: {
@@ -28787,6 +29239,11 @@ var init_service4 = __esm(() => {
28787
29239
  init_path_scan();
28788
29240
  init_read_source();
28789
29241
  init_redact();
29242
+ init_secrets();
29243
+ init_entropy();
29244
+ init_incidents();
29245
+ init_entropy_gate();
29246
+ init_credential_shape();
28790
29247
  init_audit_harness();
28791
29248
  init_config();
28792
29249
  init_impact_evidence();
@@ -29144,7 +29601,7 @@ class OpenAiCompatEngine {
29144
29601
  } catch {
29145
29602
  host = baseUrl;
29146
29603
  }
29147
- const permitted = !isPrivateEgressHost(host) || grant.allowLoopback === true && isLoopbackHost(host) || grant.allowPrivateLan === true && isPrivateLanHost(host);
29604
+ const permitted = !isPrivateEgressHost(host) || grant.allowLoopback === true && isLoopbackHost2(host) || grant.allowPrivateLan === true && isPrivateLanHost(host);
29148
29605
  if (!permitted) {
29149
29606
  yield errorEvent({
29150
29607
  kind: "invalid_request",
@@ -58893,53 +59350,18 @@ ${shown.join(`
58893
59350
  }
58894
59351
  const risk = toolByName.get(call.name)?.definition.risk;
58895
59352
  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
- }
59353
+ const untrustedOrigin = !isPureReadTool && untrustedContentSeen;
59354
+ if (untrustedOrigin && deps.unattended === true) {
59355
+ const result = {
59356
+ output: "tool blocked: external web content cannot authorize further tool calls in this turn",
59357
+ isError: true
59358
+ };
59359
+ io.onUnattendedDenial?.(call.name, "untrusted external content in this turn cannot authorize the call");
59360
+ io.onToolResult?.(call.name, result);
59361
+ history.push({ role: "tool", content: withBudgetWarning(result.output), provenance: "tool", toolCallId: call.id, ts: now() });
59362
+ io.onHistoryChange?.("tool");
59363
+ gateBlockedAny = true;
59364
+ continue;
58943
59365
  }
58944
59366
  io.onToolCall?.(call.name, call.input);
58945
59367
  const reservation = reservationByCallId.get(call.id) ?? reserveToolAttempt(budget, call.name, call.input);
@@ -58961,7 +59383,7 @@ ${shown.join(`
58961
59383
  result = precomputedResult;
58962
59384
  } else {
58963
59385
  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);
59386
+ 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
59387
  } catch (err) {
58966
59388
  const message = err instanceof Error ? err.message : String(err);
58967
59389
  result = { output: `${call.name} failed: ${message}`, isError: true };
@@ -59531,7 +59953,7 @@ function composeWithHook(toolName, rawDecision, hookResult, interactive) {
59531
59953
  }
59532
59954
  return { decision, hookTightened: true, hookAsked, ...denyMessage !== undefined ? { denyMessage } : {} };
59533
59955
  }
59534
- async function executeCall(call, toolByName, requestApproval, permissionMode, readOnly, onAutoApproved, hasInvocationCapacity, reserveInvocation, maxToolCalls, signal, busLeases, hardDeny, hooks) {
59956
+ async function executeCall(call, toolByName, requestApproval, permissionMode, readOnly, onAutoApproved, hasInvocationCapacity, reserveInvocation, maxToolCalls, signal, busLeases, hardDeny, hooks, untrustedOrigin = false) {
59535
59957
  const tool = toolByName.get(call.name);
59536
59958
  if (tool === undefined) {
59537
59959
  return { output: `unknown tool: ${call.name}`, isError: true };
@@ -59557,6 +59979,7 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, re
59557
59979
  }
59558
59980
  }
59559
59981
  const mode = permissionMode?.() ?? DEFAULT_PERMISSION_MODE;
59982
+ const untrustedDenial = "tool blocked: external web content cannot authorize this call, and the user did not authorize it either";
59560
59983
  const isReadOnly = readOnly?.() ?? false;
59561
59984
  let hookResult;
59562
59985
  if (hooks !== undefined) {
@@ -59597,7 +60020,7 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, re
59597
60020
  isError: true
59598
60021
  };
59599
60022
  }
59600
- if (gated.decision === "auto") {
60023
+ if (gated.decision === "auto" && !untrustedOrigin) {
59601
60024
  onAutoApproved?.(call.name, call.input, { destructive, credentials });
59602
60025
  } else {
59603
60026
  const fingerprint = toolCallHash(call.name, call.input);
@@ -59607,10 +60030,11 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, re
59607
60030
  ...credentials ? { credentials } : {},
59608
60031
  ...publishLease ? { publishLease } : {},
59609
60032
  ...publishLease && publishLeaseHolder !== undefined ? { publishLeaseDetail: `held by @${publishLeaseHolder.name} \u2014 "${publishLeaseHolder.reason}"` } : {},
59610
- ...gated.hookAsked ? { hookAsk: true } : {}
60033
+ ...gated.hookAsked ? { hookAsk: true } : {},
60034
+ ...untrustedOrigin ? { untrustedOrigin: true } : {}
59611
60035
  });
59612
60036
  if (!isApprovalFor(response, fingerprint)) {
59613
- return { output: `command not approved by the user; not executed`, isError: true };
60037
+ return { output: untrustedOrigin ? untrustedDenial : "command not approved by the user; not executed", isError: true };
59614
60038
  }
59615
60039
  }
59616
60040
  } else if (risk === "delegate") {
@@ -59629,17 +60053,18 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, re
59629
60053
  isError: true
59630
60054
  };
59631
60055
  }
59632
- if (gated.decision === "auto") {
60056
+ if (gated.decision === "auto" && !untrustedOrigin) {
59633
60057
  onAutoApproved?.(call.name, call.input, { destructive: false, credentials: false });
59634
60058
  } else {
59635
60059
  const fingerprint = toolCallHash(call.name, call.input);
59636
60060
  const response = requestApproval === undefined ? false : await requestApproval(call.name, call.input, {
59637
60061
  fingerprint,
59638
60062
  destructive: false,
59639
- ...gated.hookAsked ? { hookAsk: true } : {}
60063
+ ...gated.hookAsked ? { hookAsk: true } : {},
60064
+ ...untrustedOrigin ? { untrustedOrigin: true } : {}
59640
60065
  });
59641
60066
  if (!isApprovalFor(response, fingerprint)) {
59642
- return { output: `subagent spawn not approved by the user; not executed`, isError: true };
60067
+ return { output: untrustedOrigin ? untrustedDenial : "subagent spawn not approved by the user; not executed", isError: true };
59643
60068
  }
59644
60069
  }
59645
60070
  } else if (risk === "write") {
@@ -59670,7 +60095,7 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, re
59670
60095
  isError: true
59671
60096
  };
59672
60097
  }
59673
- if (gated.decision === "auto") {
60098
+ if (gated.decision === "auto" && !untrustedOrigin) {
59674
60099
  onAutoApproved?.(call.name, call.input, { destructive, credentials });
59675
60100
  } else {
59676
60101
  const fingerprint = toolCallHash(call.name, call.input);
@@ -59679,13 +60104,14 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, re
59679
60104
  destructive,
59680
60105
  ...credentials ? { credentials } : {},
59681
60106
  ...card !== undefined ? { alwaysAsk: true, card } : {},
59682
- ...gated.hookAsked ? { hookAsk: true } : {}
60107
+ ...gated.hookAsked ? { hookAsk: true } : {},
60108
+ ...untrustedOrigin ? { untrustedOrigin: true } : {}
59683
60109
  });
59684
60110
  if (!isApprovalFor(response, fingerprint)) {
59685
60111
  if (confirmationToken !== undefined)
59686
60112
  tool.confirmationDeclined?.(confirmationToken);
59687
60113
  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`,
60114
+ 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
60115
  isError: true
59690
60116
  };
59691
60117
  }
@@ -59698,11 +60124,11 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, re
59698
60124
  if (gated.decision === "deny") {
59699
60125
  return { output: gated.denyMessage ?? `tool "${call.name}" refused by a policy hook`, isError: true };
59700
60126
  }
59701
- if (gated.decision === "ask") {
60127
+ if (gated.decision === "ask" || untrustedOrigin) {
59702
60128
  const fingerprint = toolCallHash(call.name, call.input);
59703
- const response = requestApproval === undefined ? false : await requestApproval(call.name, call.input, { fingerprint, destructive: false, hookAsk: true });
60129
+ const response = requestApproval === undefined ? false : await requestApproval(call.name, call.input, { fingerprint, destructive: false, ...gated.hookAsked ? { hookAsk: true } : {}, ...untrustedOrigin ? { untrustedOrigin: true } : {} });
59704
60130
  if (!isApprovalFor(response, fingerprint)) {
59705
- return { output: `${call.name} not approved by the user; not executed`, isError: true };
60131
+ return { output: untrustedOrigin ? untrustedDenial : `${call.name} not approved by the user; not executed`, isError: true };
59706
60132
  }
59707
60133
  }
59708
60134
  } else {
@@ -112158,6 +112584,7 @@ function resolveFqn(catalog, fqn) {
112158
112584
  }
112159
112585
 
112160
112586
  // src/mcp-servers/http-headers.ts
112587
+ init_service4();
112161
112588
  var VAR_PATTERN2 = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-[^}]*)?\}/;
112162
112589
  var VAR_PATTERN_ALL = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
112163
112590
  function unresolvedVariables(raw, env) {
@@ -112275,6 +112702,14 @@ function elideCredentialText(raw) {
112275
112702
  const trimmed = query === -1 ? raw : `${raw.slice(0, query)}?\u2026`;
112276
112703
  return trimmed.replace(/^([a-zA-Z][a-zA-Z0-9+.-]*:)?\/\/[^/@]*@/, (_m, scheme) => `${scheme ?? ""}//\u2026@`);
112277
112704
  }
112705
+ var MASKED_SEGMENT_MIN_LENGTH = 16;
112706
+ function maskSecretShapedPathSegments(pathname) {
112707
+ return pathname.split("/").map((segment) => {
112708
+ if (segment.length < MASKED_SEGMENT_MIN_LENGTH)
112709
+ return segment;
112710
+ return detectSecrets(segment).length > 0 || looksSecretShaped(segment) ? "\u2026" : segment;
112711
+ }).join("/");
112712
+ }
112278
112713
  function displayUrl(raw) {
112279
112714
  if (raw === undefined)
112280
112715
  return "";
@@ -112284,7 +112719,8 @@ function displayUrl(raw) {
112284
112719
  const parsed = new URL(raw);
112285
112720
  const query = parsed.search === "" ? "" : "?\u2026";
112286
112721
  const auth = parsed.username === "" ? "" : "\u2026@";
112287
- return `${parsed.protocol}//${auth}${parsed.host}${parsed.pathname}${query}`;
112722
+ const pathname = maskSecretShapedPathSegments(parsed.pathname);
112723
+ return `${parsed.protocol}//${auth}${parsed.host}${pathname}${query}`;
112288
112724
  } catch {
112289
112725
  return elideCredentialText(raw);
112290
112726
  }
@@ -113695,139 +114131,10 @@ function fileForScope(scope, targets) {
113695
114131
  return projectConfigFile(targets.projectRoot);
113696
114132
  }
113697
114133
 
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
114134
  // src/mcp-servers/spawn-env.ts
114135
+ init_service4();
113711
114136
  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
- }
114137
+ init_service4();
113831
114138
  function buildMcpChildEnv(input) {
113832
114139
  const saved = input.savedCredentialKeys ?? new Set([...savedCredentialEnvKeys(), ...declaredCredentialEnvKeys(input.configDir)]);
113833
114140
  const env = {};
@@ -128843,6 +129150,70 @@ function createSystemWebWorkerRunner() {
128843
129150
  return new SystemWebWorkerRunner({ workspace: process.cwd(), home: homedir10() });
128844
129151
  }
128845
129152
 
129153
+ // src/harness/web/outbound-secret.ts
129154
+ init_service4();
129155
+ function safeDecodeComponent(value) {
129156
+ try {
129157
+ return decodeURIComponent(value);
129158
+ } catch {
129159
+ return value;
129160
+ }
129161
+ }
129162
+ function outboundUrlComponents(text) {
129163
+ let url;
129164
+ try {
129165
+ url = new URL(text);
129166
+ } catch {
129167
+ return [];
129168
+ }
129169
+ const components = [];
129170
+ for (const segment of url.pathname.split("/")) {
129171
+ if (segment.length > 0) {
129172
+ components.push(safeDecodeComponent(segment));
129173
+ }
129174
+ }
129175
+ for (const [, value] of url.searchParams) {
129176
+ if (value.length > 0) {
129177
+ components.push(value);
129178
+ }
129179
+ }
129180
+ if (url.hash.length > 1) {
129181
+ components.push(safeDecodeComponent(url.hash.slice(1)));
129182
+ }
129183
+ return components;
129184
+ }
129185
+ function anyComponentLooksSecret(text) {
129186
+ const entropyGateOpen = isEntropyBackendEnabled();
129187
+ for (const component of outboundUrlComponents(text)) {
129188
+ if (detectSecrets(component).length > 0) {
129189
+ return true;
129190
+ }
129191
+ if (entropyGateOpen && looksSecretShapedIn(component)) {
129192
+ return true;
129193
+ }
129194
+ }
129195
+ return false;
129196
+ }
129197
+ function containsOutboundSecret(text) {
129198
+ if (detectSecrets(text).length > 0) {
129199
+ return true;
129200
+ }
129201
+ if (isEntropyBackendEnabled() && detectEntropy(text).length > 0) {
129202
+ return true;
129203
+ }
129204
+ return anyComponentLooksSecret(text);
129205
+ }
129206
+ async function recordOutboundSecretFinding(cwd, tool, subject) {
129207
+ try {
129208
+ await appendIncident(cwd, {
129209
+ at: new Date().toISOString(),
129210
+ type: "egress-blocked",
129211
+ message: "outbound secret-shaped content",
129212
+ details: { policyId: "egress.outbound-secret", tool, subject }
129213
+ });
129214
+ } catch {}
129215
+ }
129216
+
128846
129217
  // src/harness/tool/builtin/web-fetch-tool.ts
128847
129218
  function transportFor(deps) {
128848
129219
  if (deps.transport !== undefined)
@@ -128866,6 +129237,12 @@ function webFetchTool(deps = {}) {
128866
129237
  if (typeof input.url !== "string") {
128867
129238
  return { output: "web_fetch: url must be an absolute HTTPS URL without credentials", isError: true };
128868
129239
  }
129240
+ if (containsOutboundSecret(input.url)) {
129241
+ if (deps.cwd !== undefined) {
129242
+ await recordOutboundSecretFinding(deps.cwd, "web_fetch", "url");
129243
+ }
129244
+ return { output: "web_fetch: outbound secret-shaped content", isError: true };
129245
+ }
128869
129246
  const result = await transport.fetchPage({ url: input.url, providerId: "web_fetch" });
128870
129247
  return result.ok ? { output: result.value.text, isError: false, untrusted: true } : { output: `web_fetch: ${result.reason}`, isError: true };
128871
129248
  }
@@ -128896,7 +129273,7 @@ ${result.canonicalUrl}`;
128896
129273
  return lines.join(`
128897
129274
  `).trim();
128898
129275
  }
128899
- function webSearchTool(service) {
129276
+ function webSearchTool(service, deps = {}) {
128900
129277
  return {
128901
129278
  definition: {
128902
129279
  name: "web_search",
@@ -128908,7 +129285,14 @@ function webSearchTool(service) {
128908
129285
  if (typeof input.query !== "string" || input.query.trim().length === 0) {
128909
129286
  return { output: "web_search: query must be a non-empty string", isError: true };
128910
129287
  }
128911
- const response = await service.search(input.query.trim());
129288
+ const query = input.query.trim();
129289
+ if (containsOutboundSecret(query)) {
129290
+ if (deps.cwd !== undefined) {
129291
+ await recordOutboundSecretFinding(deps.cwd, "web_search", "query");
129292
+ }
129293
+ return { output: "web_search: outbound secret-shaped content", isError: true };
129294
+ }
129295
+ const response = await service.search(query);
128912
129296
  if (!response.ok) {
128913
129297
  return {
128914
129298
  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 +129788,8 @@ function buildInteractiveAgentTools(input) {
129404
129788
  const built = [
129405
129789
  ...builtinReadOnlyTools(input.cwd),
129406
129790
  ...metaprojectTools,
129407
- webFetchTool(),
129408
- webSearchTool(input.searchController),
129791
+ webFetchTool({ cwd: input.cwd }),
129792
+ webSearchTool(input.searchController, { cwd: input.cwd }),
129409
129793
  shellExecTool(input.cwd, undefined, jobRegistry),
129410
129794
  ...jobRegistry !== undefined ? [
129411
129795
  shellTaskOutputTool(jobRegistry, { observer: "main" }),
@@ -129878,12 +130262,13 @@ function evaluateShellApproval(input) {
129878
130262
  const publishLease = input.meta?.publishLease === true;
129879
130263
  const publishLeaseDetail = input.meta?.publishLeaseDetail;
129880
130264
  const hookAsk = input.meta?.hookAsk === true;
130265
+ const untrustedOrigin = input.meta?.untrustedOrigin === true;
129881
130266
  const audit = io.loadAudit();
129882
130267
  for (const pattern of audit.permissions.allow) {
129883
130268
  input.sessionAllow.add(pattern);
129884
130269
  }
129885
130270
  const tampered = io.fingerprint() !== input.fingerprintAtStart;
129886
- const autoApprove = !destructive && !credentials && !sacReviewConfirmation && !publishLease && !hookAsk && isShellCommandAllowed(command, [...input.sessionAllow]);
130271
+ const autoApprove = !destructive && !credentials && !sacReviewConfirmation && !publishLease && !hookAsk && !untrustedOrigin && isShellCommandAllowed(command, [...input.sessionAllow]);
129887
130272
  return {
129888
130273
  command,
129889
130274
  destructive,
@@ -129892,6 +130277,7 @@ function evaluateShellApproval(input) {
129892
130277
  publishLease,
129893
130278
  ...publishLeaseDetail !== undefined ? { publishLeaseDetail } : {},
129894
130279
  hookAsk,
130280
+ untrustedOrigin,
129895
130281
  autoApprove,
129896
130282
  rejected: audit.rejected,
129897
130283
  tampered
@@ -129925,6 +130311,9 @@ function formatShellApprovalHints(evaled) {
129925
130311
  if (evaled.hookAsk) {
129926
130312
  lines.push("a policy hook asked about this call \u2014 will not be remembered");
129927
130313
  }
130314
+ if (evaled.untrustedOrigin) {
130315
+ lines.push("follows untrusted external content \u2014 will not be remembered");
130316
+ }
129928
130317
  if (evaled.publishLease) {
129929
130318
  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
130319
  }
@@ -131998,6 +132387,12 @@ function readRuntimeBlock(dispatch) {
131998
132387
  return raw;
131999
132388
  }
132000
132389
 
132390
+ // src/harness/external/env.ts
132391
+ init_service4();
132392
+
132393
+ // src/harness/external/env-deny.ts
132394
+ init_service4();
132395
+
132001
132396
  // src/harness/external/env.ts
132002
132397
  var EXTERNAL_RUNTIME_CREDENTIAL_ALLOW = {
132003
132398
  "codex-cli": ["OPENAI_API_KEY"],
@@ -132005,17 +132400,18 @@ var EXTERNAL_RUNTIME_CREDENTIAL_ALLOW = {
132005
132400
  };
132006
132401
  var ENV_EXTERNAL_DEPTH = "KERYX_EXTERNAL_DEPTH";
132007
132402
  function buildExternalChildEnv(input) {
132008
- const denied = new Set(EXTERNAL_ENV_DENY);
132403
+ const denied = new Set(EXTERNAL_ENV_DENY.map((name) => name.toUpperCase()));
132009
132404
  const allowedForThisRuntime = new Set((EXTERNAL_RUNTIME_CREDENTIAL_ALLOW[input.runtimeId] ?? []).map((name) => name.toUpperCase()));
132010
132405
  const env = {};
132011
132406
  for (const [key, value] of Object.entries(input.parent)) {
132012
132407
  if (value === undefined)
132013
132408
  continue;
132014
- if (denied.has(key))
132409
+ const upper = key.toUpperCase();
132410
+ if (denied.has(upper))
132015
132411
  continue;
132016
- if (EXTERNAL_ENV_PREFIX_SWEEPS.some((prefix) => key.startsWith(prefix)))
132412
+ if (EXTERNAL_ENV_PREFIX_SWEEPS.some((prefix) => upper.startsWith(prefix.toUpperCase())))
132017
132413
  continue;
132018
- if (!allowedForThisRuntime.has(key.toUpperCase()) && isDeniedForMcpChild(key, value))
132414
+ if (!allowedForThisRuntime.has(upper) && isDeniedForMcpChild(key, value))
132019
132415
  continue;
132020
132416
  env[key] = value;
132021
132417
  }
@@ -148088,7 +148484,7 @@ function ollamaShowAllowed(baseUrl) {
148088
148484
  } catch {
148089
148485
  return false;
148090
148486
  }
148091
- if (isPrivateEgressHost(host) && !isLoopbackHost(host)) {
148487
+ if (isPrivateEgressHost(host) && !isLoopbackHost2(host)) {
148092
148488
  return false;
148093
148489
  }
148094
148490
  return true;
@@ -155602,8 +155998,8 @@ async function recordTurnTaskCostBestEffort(input) {
155602
155998
  }, input.userConfigDir);
155603
155999
  } catch {}
155604
156000
  }
155605
- function shouldAutoApproveReadOnlySpawn(mode, hookAsk) {
155606
- return mode === "read_only" && hookAsk !== true;
156001
+ function shouldAutoApproveReadOnlySpawn(mode, hookAsk, untrustedOrigin) {
156002
+ return mode === "read_only" && hookAsk !== true && untrustedOrigin !== true;
155607
156003
  }
155608
156004
  function isUserPromptApprovalCard(tool) {
155609
156005
  return tool === "user_prompt";
@@ -157819,7 +158215,7 @@ async function launchTuiAgentShell(opts) {
157819
158215
  }
157820
158216
  }
157821
158217
  } catch {}
157822
- if (shouldAutoApproveReadOnlySpawn(mode, meta?.hookAsk)) {
158218
+ if (shouldAutoApproveReadOnlySpawn(mode, meta?.hookAsk, meta?.untrustedOrigin)) {
157823
158219
  transcript.add(new otui.TextRenderable(r, {
157824
158220
  id: `ap${uid++}`,
157825
158221
  content: otui.t`${roleChunk(otui, "accent", "\u25C7 subagent auto-approved")} ${dimChunk(otui, `mode=read_only (no shell) \xB7 ${taskPreview}`)}`
@@ -157829,7 +158225,7 @@ async function launchTuiAgentShell(opts) {
157829
158225
  chrome.hideMenu();
157830
158226
  setMainAgent("blocked", "approval");
157831
158227
  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?",
158228
+ 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
158229
  subtitle: taskPreview,
157834
158230
  cancelId: "deny",
157835
158231
  onOpen: () => chrome.blurComposer(),
@@ -158024,7 +158420,7 @@ async function launchTuiAgentShell(opts) {
158024
158420
  const choice = await chrome.withOverlay(() => pickShellApproval(otui, r, chrome.dock, cmd, approvalContext, destructive, meta?.credentials === true, {
158025
158421
  onOpen: () => chrome.blurComposer(),
158026
158422
  signal: foregroundOperation.signal
158027
- }, ev.publishLease, ev.publishLeaseDetail, ev.hookAsk));
158423
+ }, ev.publishLease, ev.publishLeaseDetail, ev.hookAsk || ev.untrustedOrigin));
158028
158424
  input.focus();
158029
158425
  if (choice === "deny") {
158030
158426
  transcript.add(new otui.TextRenderable(r, {
@@ -162300,7 +162696,7 @@ ${indentBlock(style.dim(context), GUTTER)}`);
162300
162696
  out(`${GUTTER}${style.yellow(hint)}
162301
162697
  `);
162302
162698
  }
162303
- const rememberable = !evaled.destructive && !evaled.credentials && !evaled.sacReviewConfirmation && !evaled.publishLease && !evaled.hookAsk;
162699
+ const rememberable = !evaled.destructive && !evaled.credentials && !evaled.sacReviewConfirmation && !evaled.publishLease && !evaled.hookAsk && !evaled.untrustedOrigin;
162304
162700
  const prompt = rememberable ? "[y/N/A=always] " : "[y/N] ";
162305
162701
  out(`
162306
162702
  ${GUTTER}${style.yellow(`Run: ${evaled.command}`)} ${style.dim(prompt)}`);