@wrongstack/core 0.308.7 → 0.309.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/coordination/director/director-toolset.d.ts +2 -2
  2. package/dist/coordination/director-mutation-test-tool.d.ts +29 -0
  3. package/dist/coordination/director-tools.d.ts +2 -0
  4. package/dist/coordination/director.d.ts +16 -0
  5. package/dist/coordination/explore-companion.d.ts +9 -6
  6. package/dist/coordination/fleet.d.ts +12 -0
  7. package/dist/coordination/index.d.ts +1 -1
  8. package/dist/coordination/index.js +990 -61
  9. package/dist/coordination/mail-tools.d.ts +10 -6
  10. package/dist/coordination/mailbox-codecs.d.ts +31 -0
  11. package/dist/coordination/multi-agent-coordinator.d.ts +14 -0
  12. package/dist/coordination/multi-agent-timeout.d.ts +11 -1
  13. package/dist/coordination/mutation-engine.d.ts +76 -0
  14. package/dist/coordination/subagent-budget.d.ts +54 -0
  15. package/dist/coordination/subagent-finish.d.ts +78 -0
  16. package/dist/core/index.js +58 -13
  17. package/dist/defaults/index.js +1132 -106
  18. package/dist/execution/index.js +260 -20
  19. package/dist/hq/index.js +45 -5
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +1456 -263
  22. package/dist/infrastructure/index.js +22 -3
  23. package/dist/kernel/events/agent-events.d.ts +31 -2
  24. package/dist/models/index.js +11 -1
  25. package/dist/observability/index.js +1 -1
  26. package/dist/plugin/index.js +113 -12
  27. package/dist/prompts/index.js +360 -3
  28. package/dist/security/auto-approve-policy.d.ts +2 -2
  29. package/dist/security/index.js +177 -50
  30. package/dist/security/permission-helpers.d.ts +11 -0
  31. package/dist/security/permission-policy.d.ts +10 -1
  32. package/dist/security/yolo-risk.d.ts +17 -0
  33. package/dist/session-catalog/index.js +32 -2
  34. package/dist/session-catalog/project-server.js +32 -2
  35. package/dist/skills/index.js +39 -6
  36. package/dist/storage/index.js +44 -3
  37. package/dist/types/index.d.ts +1 -1
  38. package/dist/types/index.js +14 -0
  39. package/dist/types/multi-agent.d.ts +15 -0
  40. package/dist/types/provider.d.ts +29 -1
  41. package/dist/types/tool.d.ts +15 -0
  42. package/dist/utils/index.d.ts +1 -0
  43. package/dist/utils/index.js +54 -4
  44. package/dist/utils/terminal-sanitize.d.ts +41 -0
  45. package/dist/utils/tool-subject.d.ts +1 -1
  46. package/instructions/agents/chaos-monkey.md +61 -0
  47. package/package.json +4 -4
@@ -175,7 +175,7 @@ function walk(node, vault, transform) {
175
175
  }
176
176
  return out;
177
177
  }
178
- var SECRET_KEY_PATTERN = /(?:apikey|api_key|authtoken|auth_token|bearer|secret|password|passwd|pwd|refreshtoken|refresh_token|sessionkey|session_key|access[_-]?token|private[_-]?key|token\b)/i;
178
+ var SECRET_KEY_PATTERN = /(?:api[-_]?key|auth[-_]?token|authorization|proxy-authorization|cookie|bearer|secret|password|passwd|pwd|refresh[-_]?token|session[-_]?key|access[_-]?token|private[_-]?key|token\b)/i;
179
179
  var NON_SECRET_OVERRIDES = /* @__PURE__ */ new Set(["publickey", "public_key"]);
180
180
  function isSecretField(name) {
181
181
  const lc = name.toLowerCase();
@@ -921,6 +921,14 @@ var PATTERNS = [
921
921
  anchor: "sk-ant-"
922
922
  },
923
923
  { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g, anchor: "sk-" },
924
+ {
925
+ // `xai` is a first-class provider in this codebase, but its key shape was
926
+ // absent here — so the one credential format WrongStack itself hands users
927
+ // was the one the scrubber could not recognize (audit 2026-08-20).
928
+ type: "xai_key",
929
+ regex: /(?<![A-Za-z0-9])xai-[A-Za-z0-9]{20,}(?![A-Za-z0-9])/g,
930
+ anchor: "xai-"
931
+ },
924
932
  { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g, anchor: "ghp_" },
925
933
  { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g, anchor: "github_pat_" },
926
934
  { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g, anchor: "AKIA" },
@@ -1011,8 +1019,8 @@ var PATTERNS = [
1011
1019
  // replacement so the separator between adjacent secrets is preserved
1012
1020
  // rather than collapsed. Capture groups are therefore: 1=leading
1013
1021
  // delimiter, 2=key name, 3=value.
1014
- regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
1015
- anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
1022
+ regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD|PASSPHRASE))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
1023
+ anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD", "PASSPHRASE"]
1016
1024
  },
1017
1025
  {
1018
1026
  type: "json_credential_key",
@@ -1129,6 +1137,27 @@ var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key
1129
1137
  var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
1130
1138
  var SCRUB_CHUNK_BYTES = 64 * 1024;
1131
1139
  var SCRUB_OVERLAP_BYTES = 1024;
1140
+ var PEM_PRIVATE_KEY_BEGIN_RE = /-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----/;
1141
+ var PEM_END_MARKER = "-----END";
1142
+ var MAX_PEM_BLOCK_BYTES = 64 * 1024;
1143
+ var PEM_END_LINE_TOLERANCE = 64;
1144
+ function extendChunkBoundaryPastPem(text, chunkStart, proposedEnd) {
1145
+ const head = text.slice(chunkStart, proposedEnd);
1146
+ const lastBegin = head.lastIndexOf("-----BEGIN ");
1147
+ if (lastBegin === -1) return proposedEnd;
1148
+ const fromBegin = text.slice(chunkStart + lastBegin);
1149
+ const marker = PEM_PRIVATE_KEY_BEGIN_RE.exec(fromBegin);
1150
+ if (!marker || marker.index !== 0) return proposedEnd;
1151
+ const bodyStart = marker[0].length;
1152
+ const cap = Math.min(text.length, chunkStart + lastBegin + MAX_PEM_BLOCK_BYTES);
1153
+ const closeIdx = fromBegin.indexOf(PEM_END_MARKER, bodyStart);
1154
+ if (closeIdx === -1 || chunkStart + lastBegin + closeIdx >= cap + PEM_END_LINE_TOLERANCE) {
1155
+ return proposedEnd;
1156
+ }
1157
+ const lineEnd = fromBegin.indexOf("\n", closeIdx);
1158
+ const end = lineEnd === -1 ? text.length : chunkStart + lastBegin + lineEnd + 1;
1159
+ return Math.max(proposedEnd, end);
1160
+ }
1132
1161
  var PATTERN_ANCHORS = [
1133
1162
  ...new Set(
1134
1163
  PATTERNS.flatMap(
@@ -1165,6 +1194,7 @@ var DefaultSecretScrubber = class {
1165
1194
  }
1166
1195
  }
1167
1196
  end = safe === -1 ? end : safe + 1;
1197
+ end = extendChunkBoundaryPastPem(text, i, end);
1168
1198
  }
1169
1199
  out.push(this.scrubOne(text.slice(i, end)));
1170
1200
  i = end;
@@ -1737,9 +1767,21 @@ function renderCommandLine(command, args) {
1737
1767
  });
1738
1768
  return [command, ...rendered].join(" ");
1739
1769
  }
1740
- function subjectForToolInput(toolName, input, subjectKey) {
1770
+ function renderSubjectFields(obj, fields) {
1771
+ const parts = [];
1772
+ for (const field of fields) {
1773
+ const value = obj[field];
1774
+ if (value === void 0 || value === null || value === "" || value === false) continue;
1775
+ const str = String(value);
1776
+ parts.push(`${field}=${/\s/.test(str) ? `"${str.replace(/"/g, '\\"')}"` : str}`);
1777
+ }
1778
+ return parts.join(" ");
1779
+ }
1780
+ function subjectForToolInput(toolName, input, subjectKey, subjectFields) {
1741
1781
  if (!input || typeof input !== "object") return void 0;
1742
1782
  const obj = input;
1783
+ const extra = subjectFields && subjectFields.length > 0 ? renderSubjectFields(obj, subjectFields) : "";
1784
+ const withExtra = (base) => extra ? `${base} ${extra}` : base;
1743
1785
  if (subjectKey) {
1744
1786
  const value = obj[subjectKey];
1745
1787
  if (Array.isArray(value)) {
@@ -1755,9 +1797,9 @@ function subjectForToolInput(toolName, input, subjectKey) {
1755
1797
  if (subjectKey === "command") {
1756
1798
  const rendered = renderCommandLine(value, obj["args"]);
1757
1799
  if (value === "commit" && obj["dry_run"] === true) {
1758
- return `${escapeGlobSubject(rendered)}:dry-run`;
1800
+ return `${escapeGlobSubject(withExtra(rendered))}:dry-run`;
1759
1801
  }
1760
- return escapeGlobSubject(rendered);
1802
+ return escapeGlobSubject(withExtra(rendered));
1761
1803
  }
1762
1804
  if (subjectKey === "directory" && obj["dry_run"] === true) {
1763
1805
  return `${escapeGlobSubject(value)}:dry-run`;
@@ -1918,7 +1960,9 @@ function hasRecursiveForceDelete(command, projectRoot) {
1918
1960
  if (token === "rd" || token === "rmdir") {
1919
1961
  const args = commandSegment(tokens, i + 1).map((arg) => arg.toLowerCase());
1920
1962
  if (args.includes("/s")) {
1921
- const targets = args.filter((arg) => !arg.startsWith("-") && !arg.startsWith("/") && !SHELL_OPERATORS.has(arg));
1963
+ const targets = args.filter(
1964
+ (arg) => !arg.startsWith("-") && !arg.startsWith("/") && !SHELL_OPERATORS.has(arg)
1965
+ );
1922
1966
  if (targets.length === 0) return true;
1923
1967
  if (targets.some(isCatastrophicDeleteTarget)) return true;
1924
1968
  if (targets.some((target) => !pathLooksInsideProject(target, projectRoot))) return true;
@@ -1987,7 +2031,8 @@ function hasFindExec(command) {
1987
2031
  function isCatastrophicDeleteTarget(rawTarget) {
1988
2032
  const t = rawTarget.replace(/^['"]|['"]$/g, "").trim();
1989
2033
  if (!t) return false;
1990
- if (t === "*" || t === "." || t === "./" || t === ".\\" || t === "./*" || t === ".\\*") return true;
2034
+ if (t === "*" || t === "." || t === "./" || t === ".\\" || t === "./*" || t === ".\\*")
2035
+ return true;
1991
2036
  const s = t.replace(/[\\/]\*+$/, "").replace(/[\\/]+$/, "");
1992
2037
  if (s === "") return true;
1993
2038
  if (s === "~" || /^\$HOME$/i.test(s) || /^%USERPROFILE%$/i.test(s)) return true;
@@ -2027,12 +2072,16 @@ function hasCatastrophicDelete(command) {
2027
2072
  const args = tokens.slice(i + 1);
2028
2073
  const recursive = args.some((arg) => arg.toLowerCase() === "/s");
2029
2074
  if (!recursive) continue;
2030
- const targets = args.filter((arg) => !arg.startsWith("-") && !arg.startsWith("/") && !SHELL_OPERATORS.has(arg));
2075
+ const targets = args.filter(
2076
+ (arg) => !arg.startsWith("-") && !arg.startsWith("/") && !SHELL_OPERATORS.has(arg)
2077
+ );
2031
2078
  if (targets.some(isCatastrophicDeleteTarget)) return true;
2032
2079
  }
2033
2080
  if (token === "del" || token === "erase") {
2034
2081
  const args = tokens.slice(i + 1);
2035
- const targets = args.filter((arg) => !arg.startsWith("-") && !arg.startsWith("/") && !SHELL_OPERATORS.has(arg));
2082
+ const targets = args.filter(
2083
+ (arg) => !arg.startsWith("-") && !arg.startsWith("/") && !SHELL_OPERATORS.has(arg)
2084
+ );
2036
2085
  if (targets.some(isCatastrophicDeleteTarget)) return true;
2037
2086
  }
2038
2087
  }
@@ -2093,6 +2142,47 @@ function isClearlyDestructiveBashCommand(command, projectRoot) {
2093
2142
  if (HIGH_IMPACT_PATTERNS.some((pattern) => pattern.test(trimmed))) return true;
2094
2143
  return false;
2095
2144
  }
2145
+ var WELL_KNOWN_CREDENTIAL_ENV_VARS = /* @__PURE__ */ new Set([
2146
+ "ANTHROPIC_API_KEY",
2147
+ "ANTHROPIC_AUTH_TOKEN",
2148
+ "OPENAI_API_KEY",
2149
+ "AZURE_OPENAI_API_KEY",
2150
+ "GEMINI_API_KEY",
2151
+ "GOOGLE_API_KEY",
2152
+ "GOOGLE_APPLICATION_CREDENTIALS",
2153
+ "GOOGLE_GENERATIVE_AI_API_KEY",
2154
+ "GROQ_API_KEY",
2155
+ "MISTRAL_API_KEY",
2156
+ "COHERE_API_KEY",
2157
+ "DEEPSEEK_API_KEY",
2158
+ "XAI_API_KEY",
2159
+ "OPENROUTER_API_KEY",
2160
+ "PERPLEXITY_API_KEY",
2161
+ "TOGETHER_API_KEY",
2162
+ "FIREWORKS_API_KEY",
2163
+ "HUGGINGFACE_API_KEY",
2164
+ "HF_TOKEN",
2165
+ "GITHUB_TOKEN",
2166
+ "GH_TOKEN",
2167
+ "NPM_TOKEN",
2168
+ "AWS_ACCESS_KEY_ID",
2169
+ "AWS_SECRET_ACCESS_KEY",
2170
+ "AWS_SESSION_TOKEN",
2171
+ "AZURE_CLIENT_SECRET",
2172
+ "GITLAB_TOKEN",
2173
+ "SLACK_TOKEN",
2174
+ "STRIPE_SECRET_KEY",
2175
+ "TELEGRAM_BOT_TOKEN",
2176
+ "WRONGSTACK_VAULT_PASSPHRASE"
2177
+ ]);
2178
+ function attachesWellKnownCredential(input) {
2179
+ if (!input || typeof input !== "object") return false;
2180
+ const envVars = input["envVars"];
2181
+ if (!Array.isArray(envVars)) return false;
2182
+ return envVars.some(
2183
+ (name) => typeof name === "string" && WELL_KNOWN_CREDENTIAL_ENV_VARS.has(name.toUpperCase())
2184
+ );
2185
+ }
2096
2186
 
2097
2187
  // src/security/permission-helpers.ts
2098
2188
  function matchesTrust(patterns, subject) {
@@ -2109,7 +2199,7 @@ function hasShellSubject(tool) {
2109
2199
  ]);
2110
2200
  }
2111
2201
  function alwaysAllowUnavailableReason(tool, input) {
2112
- const subject = subjectForToolInput(tool.name, input, tool.subjectKey);
2202
+ const subject = subjectForToolInput(tool.name, input, tool.subjectKey, tool.subjectFields);
2113
2203
  if (subject !== void 0) return void 0;
2114
2204
  return `"always allow" needs a subject to remember, and ${tool.name} calls do not carry one (no subjectKey, and no path/url/name input). Recording it would store a rule that can never match. Approve this call, or set a trust rule for ${tool.name} explicitly.`;
2115
2205
  }
@@ -2169,8 +2259,18 @@ var AGENT_STATE_SENSITIVE_BASENAMES = /^(?:config\.json|config\.local\.json|trus
2169
2259
  function unescapeGlobSubject(value) {
2170
2260
  return value.replace(/\\([*?[\]])/g, "$1");
2171
2261
  }
2262
+ function stripAdsSuffix(forwardSlashPath) {
2263
+ const cut = forwardSlashPath.lastIndexOf("/");
2264
+ const dir = cut === -1 ? "" : forwardSlashPath.slice(0, cut + 1);
2265
+ const base = cut === -1 ? forwardSlashPath : forwardSlashPath.slice(cut + 1);
2266
+ const colon = base.indexOf(":");
2267
+ if (colon === -1 || cut === -1 && colon === 1 && base.length <= 2) return forwardSlashPath;
2268
+ return dir + base.slice(0, colon);
2269
+ }
2172
2270
  function normalizeForCompare(value) {
2173
- const forward = unescapeGlobSubject(value).replace(/\\/g, "/").replace(/\/+$/, "");
2271
+ const forward = stripAdsSuffix(
2272
+ unescapeGlobSubject(value).replace(/\\/g, "/").replace(/\/+$/, "")
2273
+ );
2174
2274
  return process.platform === "win32" ? forward.toLowerCase() : forward;
2175
2275
  }
2176
2276
  function realpathOfNearestExisting(p) {
@@ -2207,7 +2307,7 @@ function isProtectedAgentStatePath(absPath) {
2207
2307
  return AGENT_STATE_SENSITIVE_BASENAMES.test(path5.basename(normalizeForCompare(absPath)));
2208
2308
  }
2209
2309
  function pathLooksSensitive(rawPath) {
2210
- const normalized = stripShellQuotes(rawPath).replace(/\\/g, "/");
2310
+ const normalized = stripAdsSuffix(stripShellQuotes(rawPath).replace(/\\/g, "/"));
2211
2311
  if (SENSITIVE_READ_PATHS.some((pattern) => pattern.test(normalized))) return true;
2212
2312
  return isProtectedAgentStatePath(normalized);
2213
2313
  }
@@ -2231,10 +2331,24 @@ function shellCommandReadsSensitivePath(command) {
2231
2331
  }
2232
2332
  return false;
2233
2333
  }
2334
+ function isSensitiveReadCall(tool, input) {
2335
+ const isReadTool = hasCapability(tool, ToolCapabilities.FS_READ) || tool.name === "read" || tool.name === "grep" || tool.name === "glob" || tool.name === "tree";
2336
+ if (isReadTool && inputPathLooksSensitive(input)) return true;
2337
+ const hasShellCap = hasCapability(tool, [
2338
+ ToolCapabilities.SHELL_ARBITRARY,
2339
+ ToolCapabilities.SHELL_RESTRICTED,
2340
+ ToolCapabilities.SHELL_EXEC
2341
+ ]);
2342
+ if (!hasShellCap && tool.name !== "bash" && tool.name !== "shell" && tool.name !== "exec") {
2343
+ return false;
2344
+ }
2345
+ const command = shellCommandLineFromInput(input);
2346
+ return command ? shellCommandReadsSensitivePath(command) : false;
2347
+ }
2234
2348
 
2235
2349
  // src/security/permission-explain.ts
2236
2350
  function explainPermissionTrace(state, tool, input, ctx) {
2237
- const subject = subjectForToolInput(tool.name, input, tool.subjectKey);
2351
+ const subject = subjectForToolInput(tool.name, input, tool.subjectKey, tool.subjectFields);
2238
2352
  const steps = [];
2239
2353
  let winnerIndex = -1;
2240
2354
  const add = (rule, matched, decision, source, detail) => {
@@ -2455,13 +2569,7 @@ function explainPermissionTrace(state, tool, input, ctx) {
2455
2569
  }
2456
2570
  };
2457
2571
  }
2458
- add(
2459
- "yolo",
2460
- true,
2461
- "auto",
2462
- "yolo",
2463
- "YOLO mode is active \u2014 auto-approving every non-denied call"
2464
- );
2572
+ add("yolo", true, "auto", "yolo", "YOLO mode is active \u2014 auto-approving every non-denied call");
2465
2573
  winnerIndex = steps.length - 1;
2466
2574
  return {
2467
2575
  toolName: tool.name,
@@ -2733,7 +2841,14 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
2733
2841
  static isMcpTool(name) {
2734
2842
  return name.startsWith("mcp__");
2735
2843
  }
2736
- async evaluate(tool) {
2844
+ async evaluate(tool, input) {
2845
+ if (input !== void 0 && isSensitiveReadCall(tool, input)) {
2846
+ return {
2847
+ permission: "deny",
2848
+ source: "subagent_guard",
2849
+ reason: "subagents may not read credential-bearing paths \u2014 the leader must perform this read so the user can approve it"
2850
+ };
2851
+ }
2737
2852
  const caps = tool.capabilities ?? [];
2738
2853
  const hasAllowedCap = caps.some((c) => this.allowedCapabilities.includes(c));
2739
2854
  const isMcp = _AutoApprovePermissionPolicy.isMcpTool(tool.name);
@@ -2760,8 +2875,8 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
2760
2875
  }
2761
2876
  allowOnce() {
2762
2877
  }
2763
- async explain(tool) {
2764
- const decision = await this.evaluate(tool);
2878
+ async explain(tool, input) {
2879
+ const decision = await this.evaluate(tool, input);
2765
2880
  return {
2766
2881
  toolName: tool.name,
2767
2882
  subject: null,
@@ -2811,6 +2926,17 @@ function fsWriteTargetPaths(input) {
2811
2926
  }
2812
2927
  return out;
2813
2928
  }
2929
+ function mergeTrustEntries(exact, wildcard) {
2930
+ if (!exact) return wildcard;
2931
+ if (!wildcard) return exact;
2932
+ const deny2 = [...wildcard.deny ?? [], ...exact.deny ?? []];
2933
+ const merged = {
2934
+ ...wildcard,
2935
+ ...exact
2936
+ };
2937
+ if (deny2.length > 0) merged.deny = [...new Set(deny2)];
2938
+ return merged;
2939
+ }
2814
2940
  var DefaultPermissionPolicy = class {
2815
2941
  policy = {};
2816
2942
  loaded = false;
@@ -2849,6 +2975,7 @@ var DefaultPermissionPolicy = class {
2849
2975
  yoloBlockedAsDestructive(tool, input, ctx) {
2850
2976
  if (!this.yolo || this.yoloDestructive) return false;
2851
2977
  if (this.hasAgentStateWriteTarget(tool, input, ctx)) return true;
2978
+ if (attachesWellKnownCredential(input)) return true;
2852
2979
  const isShellSurface = tool.name === "bash" || tool.name === "exec" || (tool.capabilities ?? []).includes("shell.arbitrary");
2853
2980
  if (!isShellSurface) return false;
2854
2981
  const command = getInputString(input, "command") ?? shellCommandLineFromInput(input);
@@ -2942,8 +3069,8 @@ var DefaultPermissionPolicy = class {
2942
3069
  };
2943
3070
  }
2944
3071
  const namespaceEntry = this.findNamespaceEntry(tool.name);
2945
- const entry = this.policy[tool.name] ?? namespaceEntry;
2946
- const subject = subjectForToolInput(tool.name, input, tool.subjectKey);
3072
+ const entry = mergeTrustEntries(this.policy[tool.name], namespaceEntry);
3073
+ const subject = subjectForToolInput(tool.name, input, tool.subjectKey, tool.subjectFields);
2947
3074
  const cacheKey = `${tool.name}::${subject ?? tool.name}`;
2948
3075
  const evalKey = `${cacheKey}::${permissionFingerprint(tool)}`;
2949
3076
  if (tool.name !== "write" && !this.hasAgentStateWriteTarget(tool, input, ctx)) {
@@ -2960,15 +3087,6 @@ var DefaultPermissionPolicy = class {
2960
3087
  this._evalCache.set(evalKey, decision);
2961
3088
  return decision;
2962
3089
  }
2963
- if (this.sessionAllowed.has(cacheKey)) {
2964
- this.sessionAllowed.delete(cacheKey);
2965
- const decision = {
2966
- permission: "auto",
2967
- source: "trust",
2968
- reason: "session one-shot allow (user pressed yes)"
2969
- };
2970
- return decision;
2971
- }
2972
3090
  if (entry?.deny && subject && matchesTrust(entry.deny, subject)) {
2973
3091
  this._logDeny(tool.name, subject, "matched deny pattern");
2974
3092
  const decision = {
@@ -2979,6 +3097,15 @@ var DefaultPermissionPolicy = class {
2979
3097
  this._evalCache.set(evalKey, decision);
2980
3098
  return decision;
2981
3099
  }
3100
+ if (this.sessionAllowed.has(cacheKey)) {
3101
+ this.sessionAllowed.delete(cacheKey);
3102
+ const decision = {
3103
+ permission: "auto",
3104
+ source: "trust",
3105
+ reason: "session one-shot allow (user pressed yes)"
3106
+ };
3107
+ return decision;
3108
+ }
2982
3109
  if (tool.permission === "deny") {
2983
3110
  this._logDeny(tool.name, subject, "tool default deny");
2984
3111
  const decision = {
@@ -2989,6 +3116,7 @@ var DefaultPermissionPolicy = class {
2989
3116
  this._evalCache.set(evalKey, decision);
2990
3117
  return decision;
2991
3118
  }
3119
+ const denyUnevaluated = Boolean(entry?.deny?.length) && subject === void 0;
2992
3120
  const allowMatches = hasShellSubject(tool) ? matchesCommandTrust : matchesTrust;
2993
3121
  if (entry?.allow && subject && allowMatches(entry.allow, subject)) {
2994
3122
  const decision = {
@@ -2999,7 +3127,7 @@ var DefaultPermissionPolicy = class {
2999
3127
  this._evalCache.set(evalKey, decision);
3000
3128
  return decision;
3001
3129
  }
3002
- if (entry?.auto) {
3130
+ if (entry?.auto && !denyUnevaluated) {
3003
3131
  const decision = { permission: "auto", source: "trust" };
3004
3132
  this._evalCache.set(evalKey, decision);
3005
3133
  return decision;
@@ -3099,19 +3227,10 @@ var DefaultPermissionPolicy = class {
3099
3227
  }
3100
3228
  return { permission: "confirm", source: "default" };
3101
3229
  }
3230
+ // Delegates to the shared helper so the subagent policy applies the exact
3231
+ // same rule — see `isSensitiveReadCall` in ./permission-helpers.ts.
3102
3232
  isSensitiveReadCall(tool, input) {
3103
- const isReadTool = hasCapability(tool, ToolCapabilities.FS_READ) || tool.name === "read" || tool.name === "grep" || tool.name === "glob" || tool.name === "tree";
3104
- if (isReadTool && inputPathLooksSensitive(input)) return true;
3105
- const hasShellCap = hasCapability(tool, [
3106
- ToolCapabilities.SHELL_ARBITRARY,
3107
- ToolCapabilities.SHELL_RESTRICTED,
3108
- ToolCapabilities.SHELL_EXEC
3109
- ]);
3110
- if (!hasShellCap && tool.name !== "bash" && tool.name !== "shell" && tool.name !== "exec") {
3111
- return false;
3112
- }
3113
- const command = shellCommandLineFromInput(input);
3114
- return command ? shellCommandReadsSensitivePath(command) : false;
3233
+ return isSensitiveReadCall(tool, input);
3115
3234
  }
3116
3235
  async trust(rule) {
3117
3236
  if (!this.loaded) await this.reload();
@@ -3592,6 +3711,14 @@ function keyFileNeedsHardening(keyFile, opts) {
3592
3711
  }
3593
3712
  return false;
3594
3713
  }
3714
+ function mkdirSecretDirSync(dir) {
3715
+ fs2.mkdirSync(dir, { recursive: true, mode: 448 });
3716
+ if (process.platform === "win32") return;
3717
+ try {
3718
+ fs2.chmodSync(dir, 448);
3719
+ } catch {
3720
+ }
3721
+ }
3595
3722
  function writeKeyFileAtomicSync(keyFile, content) {
3596
3723
  const tmp = `${keyFile}.${randomBytes2(4).toString("hex")}.tmp`;
3597
3724
  const fd = fs2.openSync(tmp, "w", 384);
@@ -3739,7 +3866,7 @@ var DefaultSecretVault = class {
3739
3866
  const oldVersion = this._keyVersion;
3740
3867
  const newKey = randomBytes2(KEY_BYTES);
3741
3868
  const newVersion = oldVersion + 1;
3742
- fs2.mkdirSync(path8.dirname(this.keyFile), { recursive: true });
3869
+ mkdirSecretDirSync(path8.dirname(this.keyFile));
3743
3870
  const passphrase = getVaultPassphrase();
3744
3871
  if (passphrase) {
3745
3872
  writeKeyFileAtomicSync(this.keyFile, wrapDataKey(newKey, newVersion, passphrase));
@@ -3823,7 +3950,7 @@ var DefaultSecretVault = class {
3823
3950
  } catch (err) {
3824
3951
  if (err.code !== "ENOENT") throw err;
3825
3952
  }
3826
- fs2.mkdirSync(path8.dirname(this.keyFile), { recursive: true });
3953
+ mkdirSecretDirSync(path8.dirname(this.keyFile));
3827
3954
  const key = randomBytes2(KEY_BYTES);
3828
3955
  const passphrase = getVaultPassphrase();
3829
3956
  const initialBytes = passphrase ? wrapDataKey(key, 1, passphrase) : key;
@@ -107,4 +107,15 @@ export declare function isInsideAgentStateRoot(absPath: string): boolean;
107
107
  export declare function isProtectedAgentStatePath(absPath: string): boolean;
108
108
  export declare function inputPathLooksSensitive(input: unknown): boolean;
109
109
  export declare function shellCommandReadsSensitivePath(command: string): boolean;
110
+ /**
111
+ * True when `tool` + `input` amount to reading a credential-bearing path.
112
+ *
113
+ * Shared by both permission policies. It lived as a private method on
114
+ * `DefaultPermissionPolicy`, so `AutoApprovePermissionPolicy` — the policy
115
+ * subagents run under — had no way to apply it, and a delegated read of
116
+ * `~/.aws/credentials` was auto-approved where the leader would have prompted.
117
+ * Keeping one implementation here means widening the sensitive-path list
118
+ * protects the leader and its subagents in the same edit.
119
+ */
120
+ export declare function isSensitiveReadCall(tool: Tool, input: unknown): boolean;
110
121
  //# sourceMappingURL=permission-helpers.d.ts.map
@@ -1,10 +1,19 @@
1
1
  import type { Context } from '../core/context.js';
2
2
  import type { InputReader } from '../types/input-reader.js';
3
- import type { PermissionDecision, PermissionPolicy, PermissionTrace } from '../types/permission.js';
3
+ import type { PermissionDecision, PermissionPolicy, PermissionTrace, TrustPolicy } from '../types/permission.js';
4
4
  import type { Tool } from '../types/tool.js';
5
5
  import { type TrustPolicyDiagnostic } from './permission-policy-schema.js';
6
6
  export { AutoApprovePermissionPolicy } from './auto-approve-policy.js';
7
7
  export { alwaysAllowUnavailableReason, matchesTrust, matchesCommandTrust, hasShellSubject, permissionFingerprint, shellCommandLineFromInput, inputPathLooksSensitive, shellCommandReadsSensitivePath, } from './permission-helpers.js';
8
+ /**
9
+ * Combine an exact-name trust entry with the wildcard entry that also matched.
10
+ *
11
+ * Deny is the union of both levels — a narrow "always allow" must never be able
12
+ * to drop a broad guardrail. Everything permissive (allow / auto / trustWorkdir
13
+ * / denyPrivate) comes from the more specific entry when it says anything, so
14
+ * exact-name rules still win where they are meant to.
15
+ */
16
+ export declare function mergeTrustEntries(exact: TrustPolicy[string] | undefined, wildcard: TrustPolicy[string] | undefined): TrustPolicy[string] | undefined;
8
17
  export interface PermissionPolicyOptions {
9
18
  trustFile: string;
10
19
  yolo?: boolean | undefined;
@@ -9,4 +9,21 @@ export declare function pathLooksInsideProject(rawPath: string, projectRoot: str
9
9
  * state-root detection either (those paths are resolved absolutely).
10
10
  */
11
11
  export declare function isClearlyDestructiveBashCommand(command: string, projectRoot: string | undefined): boolean;
12
+ /**
13
+ * True when a tool call would bind a well-known third-party credential to a
14
+ * provider endpoint.
15
+ *
16
+ * `provider_manage` lets the model create a provider, choose its `baseUrl`
17
+ * (validated for scheme only — no host allowlist) and name the environment
18
+ * variables its key is read from. Nothing claims `ANTHROPIC_API_KEY` on a stock
19
+ * install, so `rejectBorrowedEnvVars` — which only rejects names another
20
+ * provider already lists — let it through. Combined with the sibling
21
+ * `fallback_chain_manage` / `leader_model_set` tools in the same bundle, that is
22
+ * a complete "send my real key to a host I chose" primitive, reachable by
23
+ * prompt injection.
24
+ *
25
+ * The tool stays usable: this only forces the decision back to the human rather
26
+ * than letting YOLO auto-approve it.
27
+ */
28
+ export declare function attachesWellKnownCredential(input: unknown): boolean;
12
29
  //# sourceMappingURL=yolo-risk.d.ts.map
@@ -804,6 +804,14 @@ var PATTERNS = [
804
804
  anchor: "sk-ant-"
805
805
  },
806
806
  { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g, anchor: "sk-" },
807
+ {
808
+ // `xai` is a first-class provider in this codebase, but its key shape was
809
+ // absent here — so the one credential format WrongStack itself hands users
810
+ // was the one the scrubber could not recognize (audit 2026-08-20).
811
+ type: "xai_key",
812
+ regex: /(?<![A-Za-z0-9])xai-[A-Za-z0-9]{20,}(?![A-Za-z0-9])/g,
813
+ anchor: "xai-"
814
+ },
807
815
  { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g, anchor: "ghp_" },
808
816
  { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g, anchor: "github_pat_" },
809
817
  { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g, anchor: "AKIA" },
@@ -894,8 +902,8 @@ var PATTERNS = [
894
902
  // replacement so the separator between adjacent secrets is preserved
895
903
  // rather than collapsed. Capture groups are therefore: 1=leading
896
904
  // delimiter, 2=key name, 3=value.
897
- regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
898
- anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
905
+ regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD|PASSPHRASE))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
906
+ anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD", "PASSPHRASE"]
899
907
  },
900
908
  {
901
909
  type: "json_credential_key",
@@ -1012,6 +1020,27 @@ var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key
1012
1020
  var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
1013
1021
  var SCRUB_CHUNK_BYTES = 64 * 1024;
1014
1022
  var SCRUB_OVERLAP_BYTES = 1024;
1023
+ var PEM_PRIVATE_KEY_BEGIN_RE = /-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----/;
1024
+ var PEM_END_MARKER = "-----END";
1025
+ var MAX_PEM_BLOCK_BYTES = 64 * 1024;
1026
+ var PEM_END_LINE_TOLERANCE = 64;
1027
+ function extendChunkBoundaryPastPem(text, chunkStart, proposedEnd) {
1028
+ const head = text.slice(chunkStart, proposedEnd);
1029
+ const lastBegin = head.lastIndexOf("-----BEGIN ");
1030
+ if (lastBegin === -1) return proposedEnd;
1031
+ const fromBegin = text.slice(chunkStart + lastBegin);
1032
+ const marker = PEM_PRIVATE_KEY_BEGIN_RE.exec(fromBegin);
1033
+ if (!marker || marker.index !== 0) return proposedEnd;
1034
+ const bodyStart = marker[0].length;
1035
+ const cap = Math.min(text.length, chunkStart + lastBegin + MAX_PEM_BLOCK_BYTES);
1036
+ const closeIdx = fromBegin.indexOf(PEM_END_MARKER, bodyStart);
1037
+ if (closeIdx === -1 || chunkStart + lastBegin + closeIdx >= cap + PEM_END_LINE_TOLERANCE) {
1038
+ return proposedEnd;
1039
+ }
1040
+ const lineEnd = fromBegin.indexOf("\n", closeIdx);
1041
+ const end = lineEnd === -1 ? text.length : chunkStart + lastBegin + lineEnd + 1;
1042
+ return Math.max(proposedEnd, end);
1043
+ }
1015
1044
  var PATTERN_ANCHORS = [
1016
1045
  ...new Set(
1017
1046
  PATTERNS.flatMap(
@@ -1048,6 +1077,7 @@ var DefaultSecretScrubber = class {
1048
1077
  }
1049
1078
  }
1050
1079
  end = safe === -1 ? end : safe + 1;
1080
+ end = extendChunkBoundaryPastPem(text, i, end);
1051
1081
  }
1052
1082
  out.push(this.scrubOne(text.slice(i, end)));
1053
1083
  i = end;
@@ -248,6 +248,14 @@ var PATTERNS = [
248
248
  anchor: "sk-ant-"
249
249
  },
250
250
  { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g, anchor: "sk-" },
251
+ {
252
+ // `xai` is a first-class provider in this codebase, but its key shape was
253
+ // absent here — so the one credential format WrongStack itself hands users
254
+ // was the one the scrubber could not recognize (audit 2026-08-20).
255
+ type: "xai_key",
256
+ regex: /(?<![A-Za-z0-9])xai-[A-Za-z0-9]{20,}(?![A-Za-z0-9])/g,
257
+ anchor: "xai-"
258
+ },
251
259
  { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g, anchor: "ghp_" },
252
260
  { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g, anchor: "github_pat_" },
253
261
  { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g, anchor: "AKIA" },
@@ -338,8 +346,8 @@ var PATTERNS = [
338
346
  // replacement so the separator between adjacent secrets is preserved
339
347
  // rather than collapsed. Capture groups are therefore: 1=leading
340
348
  // delimiter, 2=key name, 3=value.
341
- regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
342
- anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
349
+ regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD|PASSPHRASE))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
350
+ anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD", "PASSPHRASE"]
343
351
  },
344
352
  {
345
353
  type: "json_credential_key",
@@ -456,6 +464,27 @@ var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key
456
464
  var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
457
465
  var SCRUB_CHUNK_BYTES = 64 * 1024;
458
466
  var SCRUB_OVERLAP_BYTES = 1024;
467
+ var PEM_PRIVATE_KEY_BEGIN_RE = /-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----/;
468
+ var PEM_END_MARKER = "-----END";
469
+ var MAX_PEM_BLOCK_BYTES = 64 * 1024;
470
+ var PEM_END_LINE_TOLERANCE = 64;
471
+ function extendChunkBoundaryPastPem(text, chunkStart, proposedEnd) {
472
+ const head = text.slice(chunkStart, proposedEnd);
473
+ const lastBegin = head.lastIndexOf("-----BEGIN ");
474
+ if (lastBegin === -1) return proposedEnd;
475
+ const fromBegin = text.slice(chunkStart + lastBegin);
476
+ const marker = PEM_PRIVATE_KEY_BEGIN_RE.exec(fromBegin);
477
+ if (!marker || marker.index !== 0) return proposedEnd;
478
+ const bodyStart = marker[0].length;
479
+ const cap = Math.min(text.length, chunkStart + lastBegin + MAX_PEM_BLOCK_BYTES);
480
+ const closeIdx = fromBegin.indexOf(PEM_END_MARKER, bodyStart);
481
+ if (closeIdx === -1 || chunkStart + lastBegin + closeIdx >= cap + PEM_END_LINE_TOLERANCE) {
482
+ return proposedEnd;
483
+ }
484
+ const lineEnd = fromBegin.indexOf("\n", closeIdx);
485
+ const end = lineEnd === -1 ? text.length : chunkStart + lastBegin + lineEnd + 1;
486
+ return Math.max(proposedEnd, end);
487
+ }
459
488
  var PATTERN_ANCHORS = [
460
489
  ...new Set(
461
490
  PATTERNS.flatMap(
@@ -492,6 +521,7 @@ var DefaultSecretScrubber = class {
492
521
  }
493
522
  }
494
523
  end = safe === -1 ? end : safe + 1;
524
+ end = extendChunkBoundaryPastPem(text, i, end);
495
525
  }
496
526
  out.push(this.scrubOne(text.slice(i, end)));
497
527
  i = end;