@wrongstack/core 0.306.3 → 0.307.0

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 (114) hide show
  1. package/dist/chronicle/index.js +957 -953
  2. package/dist/chronicle/metrics-ingest.d.ts +55 -0
  3. package/dist/chronicle/metrics-schema.d.ts +86 -0
  4. package/dist/chronicle/metrics-store.d.ts +5 -120
  5. package/dist/chronicle/project-server.js +1083 -1084
  6. package/dist/chronicle/sqlite-journal-quota.d.ts +39 -0
  7. package/dist/chronicle/sqlite-journal-schema.d.ts +25 -0
  8. package/dist/chronicle/sqlite-journal.d.ts +12 -177
  9. package/dist/coordination/agents/index.js +1738 -1690
  10. package/dist/coordination/agents/project-agent-capture.d.ts +48 -0
  11. package/dist/coordination/agents/project-agent-identity.d.ts +3 -129
  12. package/dist/coordination/agents/project-agent-roster.d.ts +17 -0
  13. package/dist/coordination/index.js +6103 -5890
  14. package/dist/coordination/mailbox-http-actor-query.d.ts +16 -0
  15. package/dist/coordination/mailbox-http-router.d.ts +0 -48
  16. package/dist/coordination/mailbox-http-sse.d.ts +12 -0
  17. package/dist/coordination/mailbox-message-types.d.ts +71 -0
  18. package/dist/coordination/mailbox-predicates.d.ts +13 -0
  19. package/dist/coordination/mailbox-project-server.js +1 -1
  20. package/dist/coordination/mailbox-session-sync.d.ts +8 -0
  21. package/dist/coordination/mailbox-types.d.ts +6 -793
  22. package/dist/core/agent-loop-context.d.ts +28 -0
  23. package/dist/core/agent-loop-detector.d.ts +25 -0
  24. package/dist/core/conversation-state.d.ts +1 -2
  25. package/dist/core/fallback-doctor.d.ts +72 -0
  26. package/dist/core/fallback-model.d.ts +19 -1
  27. package/dist/core/fallback-profile-manager.d.ts +21 -3
  28. package/dist/core/index.d.ts +2 -1
  29. package/dist/core/index.js +1143 -766
  30. package/dist/defaults/index.js +3833 -3595
  31. package/dist/execution/auto-compaction-middleware.d.ts +64 -0
  32. package/dist/execution/autonomy-brain-llm.d.ts +55 -0
  33. package/dist/execution/autonomy-brain.d.ts +3 -156
  34. package/dist/execution/compaction-budget.d.ts +38 -0
  35. package/dist/execution/compaction-core.d.ts +3 -166
  36. package/dist/execution/compaction-elision.d.ts +58 -0
  37. package/dist/execution/council-orchestrator-helpers.d.ts +39 -0
  38. package/dist/execution/council-orchestrator.d.ts +3 -42
  39. package/dist/execution/council-response-parser.d.ts +56 -0
  40. package/dist/execution/index.d.ts +1 -1
  41. package/dist/execution/index.js +10016 -9903
  42. package/dist/execution/prompt-enhancer.js +11 -3
  43. package/dist/execution/retry-policy.d.ts +27 -0
  44. package/dist/execution/tool-executor-guard.d.ts +18 -0
  45. package/dist/execution/tool-executor-runner.d.ts +12 -0
  46. package/dist/execution/tool-executor.d.ts +0 -75
  47. package/dist/goal/index.js +19 -6
  48. package/dist/hq/index.js +58 -17
  49. package/dist/index.d.ts +2 -1
  50. package/dist/index.js +8731 -7756
  51. package/dist/infrastructure/index.js +24 -1
  52. package/dist/kernel/events/agent-events.d.ts +2 -0
  53. package/dist/kernel/events/provider-events.d.ts +13 -0
  54. package/dist/plugin/index.js +2123 -1918
  55. package/dist/plugins/auto-review-config.d.ts +53 -0
  56. package/dist/plugins/auto-review-git.d.ts +19 -0
  57. package/dist/plugins/auto-review-plugin.d.ts +2 -140
  58. package/dist/plugins/review-finding-verification.d.ts +7 -0
  59. package/dist/prompts/index.d.ts +1 -0
  60. package/dist/prompts/prompt-journal.d.ts +94 -0
  61. package/dist/security/index.js +677 -667
  62. package/dist/security/permission-explain.d.ts +19 -0
  63. package/dist/security/permission-policy.d.ts +0 -101
  64. package/dist/security/yolo-risk.d.ts +5 -4
  65. package/dist/session-catalog/index.js +270 -227
  66. package/dist/session-catalog/project-server.js +277 -234
  67. package/dist/session-catalog/store-rebuild.d.ts +9 -0
  68. package/dist/session-catalog/store-schema.d.ts +48 -0
  69. package/dist/session-catalog/store.d.ts +0 -17
  70. package/dist/storage/cloud-config-sync/sanitize.d.ts +18 -0
  71. package/dist/storage/cloud-config-sync.d.ts +1 -1
  72. package/dist/storage/director-state.d.ts +6 -0
  73. package/dist/storage/file-session-writer.d.ts +2 -58
  74. package/dist/storage/index.d.ts +2 -1
  75. package/dist/storage/index.js +2121 -1736
  76. package/dist/storage/orphan-lock-cleaner.d.ts +15 -0
  77. package/dist/storage/provider-config-watcher.d.ts +9 -0
  78. package/dist/storage/session-recovery.d.ts +9 -0
  79. package/dist/storage/session-store/rename-session.d.ts +16 -0
  80. package/dist/storage/session-store/resume-session.d.ts +26 -0
  81. package/dist/storage/session-store/session-store-clear.d.ts +11 -0
  82. package/dist/storage/session-store/session-store-index.d.ts +13 -0
  83. package/dist/storage/session-store/strict-empty-check.d.ts +7 -0
  84. package/dist/storage/session-store.d.ts +2 -129
  85. package/dist/storage/session-summary-tracker.d.ts +39 -0
  86. package/dist/storage/session-write-buffer.d.ts +43 -0
  87. package/dist/storage/session-writer/session-writer-flush.d.ts +4 -0
  88. package/dist/storage/session-writer/session-writer-summary-tracker.d.ts +28 -0
  89. package/dist/tools/fallback-agent-model-assign-tool.d.ts +18 -0
  90. package/dist/tools/fallback-leader-model-set-tool.d.ts +18 -0
  91. package/dist/tools/fallback-manage-tools.d.ts +8 -45
  92. package/dist/tools/fallback-profile-manage-tool.d.ts +16 -0
  93. package/dist/tools/fallback-provider-key-set-tool.d.ts +17 -0
  94. package/dist/tools/fallback-provider-manage-tool.d.ts +26 -0
  95. package/dist/tools/index.d.ts +1 -1
  96. package/dist/tools/index.js +5150 -5090
  97. package/dist/types/config/root.d.ts +21 -2
  98. package/dist/types/default-config.d.ts +1 -1
  99. package/dist/types/index.d.ts +54 -55
  100. package/dist/types/index.js +953 -932
  101. package/dist/types/runtime-capability-manifest.d.ts +5 -5
  102. package/dist/types/session-markers.d.ts +12 -0
  103. package/dist/types/session-markers.js +19 -0
  104. package/dist/types/session.d.ts +7 -0
  105. package/dist/worktree/worktree-manager.d.ts +1 -1
  106. package/instructions/coordination/subagent-baseline.md +2 -2
  107. package/instructions/llm/prompt-enhancer.md +2 -1
  108. package/instructions/sections/tool/common-patterns.md +21 -0
  109. package/instructions/sections/tool/delegation-compact.md +3 -1
  110. package/instructions/sections/tool/delegation-full.md +5 -1
  111. package/instructions/system-lite.md +28 -3
  112. package/instructions/system-pro.md +37 -8
  113. package/instructions/system.md +4 -2
  114. package/package.json +5 -5
@@ -894,6 +894,24 @@ function validateDirectoryPolicy(value) {
894
894
  import { homedir } from "node:os";
895
895
 
896
896
  // src/security/secret-scrubber.ts
897
+ var JSON_CREDENTIAL_KEY_ANCHORS = [
898
+ 'Key"',
899
+ 'key"',
900
+ 'KEY"',
901
+ 'token"',
902
+ 'Token"',
903
+ 'TOKEN"',
904
+ 'secret"',
905
+ 'Secret"',
906
+ 'SECRET"',
907
+ 'password"',
908
+ 'Password"',
909
+ 'PASSWORD"',
910
+ 'authorization"',
911
+ 'Authorization"',
912
+ 'bearer"',
913
+ 'Bearer"'
914
+ ];
897
915
  var PATTERNS = [
898
916
  // Anchored at the start where possible so partial matches inside larger
899
917
  // strings don't trigger false positives.
@@ -996,6 +1014,30 @@ var PATTERNS = [
996
1014
  regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
997
1015
  anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
998
1016
  },
1017
+ {
1018
+ type: "json_credential_key",
1019
+ // The JSON counterpart to `high_entropy_env`, and the pattern that the
1020
+ // now-deleted `JSON_KEY_ANCHORS` list was written for. Without it those
1021
+ // anchors only widened the cheap pre-scan — `hasCredentialAnchors` said
1022
+ // "this text may hold a secret", every pattern then declined to match, and
1023
+ // the value went out verbatim. `high_entropy_env` cannot cover these: it
1024
+ // requires an UPPERCASE unquoted key (`API_KEY=…`), so `{"apiKey":"…"}`
1025
+ // never matched.
1026
+ //
1027
+ // Tool results are routinely serialised as JSON, and a credential with no
1028
+ // recognisable prefix (Azure, self-hosted gateways, Anthropic/Codex OAuth)
1029
+ // has no other pattern that can catch it — this is the only thing standing
1030
+ // between such a value and the session JSONL, chronicle, HQ broadcast and
1031
+ // the model's own context.
1032
+ //
1033
+ // The key may carry a prefix (`"anthropicApiKey"`), but the credential word
1034
+ // must END the key: `"tokenCount"` and `"maxTokens"` do not match, because
1035
+ // the closing quote has to follow the word immediately.
1036
+ // Value floor of 8 chars keeps enum-ish values (`"authorization":"none"`)
1037
+ // out. Capture groups: 1=key + punctuation, 2=value, 3=closing quote.
1038
+ regex: /("[A-Za-z0-9_]*(?:apiKey|api_key|token|secret|password|authorization|bearer|private_key|access_token|refresh_token|client_secret)"\s*:\s*")([^"\\]{8,512})(")/gi,
1039
+ anchor: JSON_CREDENTIAL_KEY_ANCHORS
1040
+ },
999
1041
  // ── Ported from packages/plugins credential-patterns.ts (WS-034) ─────────
1000
1042
  // The plugin runtime carried 37 patterns while this scrubber — the one that
1001
1043
  // guards session JSONL, chronicle, HQ broadcast, WebUI events and the auth
@@ -1078,9 +1120,12 @@ var PATTERNS = [
1078
1120
  anchor: "GOCSPX-"
1079
1121
  }
1080
1122
  ];
1081
- var SIMPLE_PATTERNS = PATTERNS.filter((p) => p.type !== "high_entropy_env");
1123
+ var SIMPLE_PATTERNS = PATTERNS.filter(
1124
+ (p) => p.type !== "high_entropy_env" && p.type !== "json_credential_key"
1125
+ );
1082
1126
  var COMBINED_REGEX = new RegExp(SIMPLE_PATTERNS.map((p) => `(${p.regex.source})`).join("|"), "g");
1083
1127
  var HIGH_ENTROPY_REGEX = PATTERNS.find((p) => p.type === "high_entropy_env").regex;
1128
+ var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key").regex;
1084
1129
  var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
1085
1130
  var SCRUB_CHUNK_BYTES = 64 * 1024;
1086
1131
  var SCRUB_OVERLAP_BYTES = 1024;
@@ -1091,20 +1136,7 @@ var PATTERN_ANCHORS = [
1091
1136
  )
1092
1137
  )
1093
1138
  ];
1094
- var JSON_KEY_ANCHORS = [
1095
- '"apiKey"',
1096
- '"api_key"',
1097
- '"token"',
1098
- '"secret"',
1099
- '"password"',
1100
- '"authorization"',
1101
- '"bearer"',
1102
- '"private_key"',
1103
- '"access_token"',
1104
- '"refresh_token"',
1105
- '"client_secret"'
1106
- ];
1107
- var ALL_ANCHORS = [...PATTERN_ANCHORS, ...JSON_KEY_ANCHORS];
1139
+ var ALL_ANCHORS = PATTERN_ANCHORS;
1108
1140
  function hasCredentialAnchors(text) {
1109
1141
  for (const anchor of ALL_ANCHORS) {
1110
1142
  if (text.includes(anchor)) return true;
@@ -1153,6 +1185,9 @@ var DefaultSecretScrubber = class {
1153
1185
  out = out.replace(HIGH_ENTROPY_REGEX, (_match, lead, key, _value) => {
1154
1186
  return `${lead}${key}=[REDACTED:high_entropy_env]`;
1155
1187
  });
1188
+ out = out.replace(JSON_CREDENTIAL_REGEX, (_match, keyPrefix, _value, closingQuote) => {
1189
+ return `${keyPrefix}[REDACTED:json_credential_key]${closingQuote}`;
1190
+ });
1156
1191
  return out;
1157
1192
  }
1158
1193
  /**
@@ -1751,175 +1786,23 @@ function subjectForToolInput(toolName, input, subjectKey) {
1751
1786
  return void 0;
1752
1787
  }
1753
1788
 
1754
- // src/security/permission-policy-schema.ts
1755
- var TRUST_POLICY_SCHEMA_VERSION = 1;
1756
- var TRUST_POLICY_LIMITS = Object.freeze({
1757
- maxTools: 1e3,
1758
- maxPatternsPerRule: 1e3,
1759
- maxToolNameChars: 256,
1760
- maxPatternChars: 4096
1761
- });
1762
- var TRUST_POLICY_JSON_SCHEMA = Object.freeze({
1763
- $schema: "https://json-schema.org/draft/2020-12/schema",
1764
- $id: "https://wrongstack.dev/schemas/trust-policy-v1.json",
1765
- title: "WrongStack Trust Policy",
1766
- type: "object",
1767
- maxProperties: TRUST_POLICY_LIMITS.maxTools,
1768
- propertyNames: {
1769
- minLength: 1,
1770
- maxLength: TRUST_POLICY_LIMITS.maxToolNameChars,
1771
- not: { enum: ["__proto__", "prototype", "constructor"] }
1772
- },
1773
- additionalProperties: {
1774
- type: "object",
1775
- additionalProperties: false,
1776
- properties: {
1777
- allow: {
1778
- type: "array",
1779
- maxItems: TRUST_POLICY_LIMITS.maxPatternsPerRule,
1780
- items: { type: "string", minLength: 1, maxLength: TRUST_POLICY_LIMITS.maxPatternChars }
1781
- },
1782
- deny: {
1783
- type: "array",
1784
- maxItems: TRUST_POLICY_LIMITS.maxPatternsPerRule,
1785
- items: { type: "string", minLength: 1, maxLength: TRUST_POLICY_LIMITS.maxPatternChars }
1786
- },
1787
- auto: { type: "boolean" },
1788
- trustWorkdir: { type: "boolean" },
1789
- denyPrivate: { type: "boolean" }
1790
- }
1791
- }
1792
- });
1793
- var RULE_FIELDS2 = /* @__PURE__ */ new Set(["allow", "deny", "auto", "trustWorkdir", "denyPrivate"]);
1794
- var UNSAFE_PROPERTY_NAMES2 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1795
- function isRecord2(value) {
1796
- return typeof value === "object" && value !== null && !Array.isArray(value);
1797
- }
1798
- function error2(diagnostics, code, path9, message) {
1799
- diagnostics.push({ severity: "error", code, path: path9, message });
1800
- }
1801
- function validatePatterns(value, path9, diagnostics) {
1802
- if (!Array.isArray(value)) {
1803
- error2(diagnostics, "invalid_pattern_list", path9, "must be an array of strings");
1804
- return void 0;
1805
- }
1806
- if (value.length > TRUST_POLICY_LIMITS.maxPatternsPerRule) {
1807
- error2(
1808
- diagnostics,
1809
- "too_many_patterns",
1810
- path9,
1811
- `must contain at most ${TRUST_POLICY_LIMITS.maxPatternsPerRule} patterns`
1812
- );
1813
- return void 0;
1814
- }
1815
- const patterns = [];
1816
- const seen = /* @__PURE__ */ new Set();
1817
- for (const [index, pattern] of value.entries()) {
1818
- const itemPath = `${path9}[${index}]`;
1819
- if (typeof pattern !== "string" || pattern.length === 0 || pattern.length > TRUST_POLICY_LIMITS.maxPatternChars) {
1820
- error2(
1821
- diagnostics,
1822
- "invalid_pattern",
1823
- itemPath,
1824
- `must be a non-empty string up to ${TRUST_POLICY_LIMITS.maxPatternChars} characters`
1825
- );
1826
- continue;
1827
- }
1828
- if (seen.has(pattern)) {
1829
- diagnostics.push({
1830
- severity: "warning",
1831
- code: "duplicate_pattern",
1832
- path: itemPath,
1833
- message: "duplicates an earlier pattern and was normalized away"
1834
- });
1835
- continue;
1836
- }
1837
- seen.add(pattern);
1838
- patterns.push(pattern);
1839
- }
1840
- return patterns;
1841
- }
1842
- function validateTrustPolicy(value) {
1843
- const diagnostics = [];
1844
- if (!isRecord2(value)) {
1845
- return {
1846
- ok: false,
1847
- diagnostics: [
1848
- {
1849
- severity: "error",
1850
- code: "invalid_root",
1851
- path: "$",
1852
- message: "trust policy must be an object"
1853
- }
1854
- ]
1855
- };
1856
- }
1857
- const entries = Object.entries(value);
1858
- if (entries.length > TRUST_POLICY_LIMITS.maxTools) {
1859
- error2(
1860
- diagnostics,
1861
- "too_many_tools",
1862
- "$",
1863
- `must contain at most ${TRUST_POLICY_LIMITS.maxTools} tool rules`
1864
- );
1865
- }
1866
- const policy = {};
1867
- for (const [toolName, rawRule] of entries.slice(0, TRUST_POLICY_LIMITS.maxTools)) {
1868
- const toolPath = `$[${JSON.stringify(toolName)}]`;
1869
- if (toolName.length === 0 || toolName.length > TRUST_POLICY_LIMITS.maxToolNameChars) {
1870
- error2(
1871
- diagnostics,
1872
- "invalid_tool_name",
1873
- toolPath,
1874
- `tool name must be 1-${TRUST_POLICY_LIMITS.maxToolNameChars} characters`
1875
- );
1876
- continue;
1877
- }
1878
- if (UNSAFE_PROPERTY_NAMES2.has(toolName)) {
1879
- error2(
1880
- diagnostics,
1881
- "unsafe_tool_name",
1882
- toolPath,
1883
- "reserved object property names are not valid tool rules"
1884
- );
1885
- continue;
1886
- }
1887
- if (!isRecord2(rawRule)) {
1888
- error2(diagnostics, "invalid_rule", toolPath, "tool rule must be an object");
1889
- continue;
1890
- }
1891
- for (const field of Object.keys(rawRule)) {
1892
- if (!RULE_FIELDS2.has(field)) {
1893
- error2(
1894
- diagnostics,
1895
- "unknown_field",
1896
- `${toolPath}.${field}`,
1897
- `unknown policy field "${field}"`
1898
- );
1899
- }
1900
- }
1901
- const rule = {};
1902
- for (const field of ["allow", "deny"]) {
1903
- if (rawRule[field] === void 0) continue;
1904
- const patterns = validatePatterns(rawRule[field], `${toolPath}.${field}`, diagnostics);
1905
- if (patterns) rule[field] = patterns;
1906
- }
1907
- for (const field of ["auto", "trustWorkdir", "denyPrivate"]) {
1908
- const setting = rawRule[field];
1909
- if (setting === void 0) continue;
1910
- if (typeof setting !== "boolean") {
1911
- error2(diagnostics, "invalid_boolean", `${toolPath}.${field}`, "must be a boolean");
1912
- } else {
1913
- rule[field] = setting;
1914
- }
1915
- }
1916
- policy[toolName] = rule;
1917
- }
1918
- return diagnostics.some((diagnostic) => diagnostic.severity === "error") ? { ok: false, diagnostics } : { ok: true, policy, diagnostics };
1789
+ // src/security/permission-helpers.ts
1790
+ import { realpathSync } from "node:fs";
1791
+ import * as path5 from "node:path";
1792
+
1793
+ // src/utils/wstack-paths.ts
1794
+ import * as os from "node:os";
1795
+ import * as path3 from "node:path";
1796
+ function wstackGlobalRoot() {
1797
+ const fromEnv = process.env["WRONGSTACK_HOME"];
1798
+ if (fromEnv && fromEnv.trim().length > 0) return path3.resolve(fromEnv);
1799
+ return path3.join(os.homedir(), ".wrongstack");
1919
1800
  }
1920
1801
 
1921
1802
  // src/security/yolo-risk.ts
1922
- import * as path3 from "node:path";
1803
+ import * as os2 from "node:os";
1804
+ import * as path4 from "node:path";
1805
+ var PROTECTED_STATE_BASENAMES = /^(?:config\.json|config\.local\.json|trust\.json|auth\.json|\.key)$/i;
1923
1806
  var CATASTROPHIC_PATTERNS = [
1924
1807
  /\b(?:mkfs(?:\.[a-z0-9]+)?|mke2fs|newfs)\b/i,
1925
1808
  // make a filesystem — wipes a partition
@@ -1979,9 +1862,9 @@ function getInputString(input, key) {
1979
1862
  function pathLooksInsideProject(rawPath, projectRoot) {
1980
1863
  if (!projectRoot) return false;
1981
1864
  if (rawPath === "~" || rawPath.startsWith("~/") || rawPath.startsWith("~\\")) return false;
1982
- const resolved = path3.resolve(projectRoot, rawPath);
1983
- const relative4 = path3.relative(projectRoot, resolved);
1984
- return !!relative4 && !relative4.startsWith("..") && !path3.isAbsolute(relative4);
1865
+ const resolved = path4.resolve(projectRoot, rawPath);
1866
+ const relative4 = path4.relative(projectRoot, resolved);
1867
+ return !!relative4 && !relative4.startsWith("..") && !path4.isAbsolute(relative4);
1985
1868
  }
1986
1869
  function tokenizeShell(command) {
1987
1870
  return command.match(/"[^"]*"|'[^']*'|\S+/g)?.map((token) => token.replace(/^['"]|['"]$/g, "")) ?? [];
@@ -2155,6 +2038,48 @@ function hasCatastrophicDelete(command) {
2155
2038
  }
2156
2039
  return false;
2157
2040
  }
2041
+ function hasWriteToAgentStateRoot(command) {
2042
+ const tokens = tokenizeShell(command);
2043
+ for (let i = 0; i < tokens.length - 1; i++) {
2044
+ const t = tokens[i];
2045
+ if (t === ">" || t === ">>") {
2046
+ const target = tokens[i + 1];
2047
+ if (target && looksLikeAgentStateTarget(target)) return true;
2048
+ }
2049
+ }
2050
+ for (let i = 0; i < tokens.length; i++) {
2051
+ const t = tokens[i]?.toLowerCase();
2052
+ if (t === "tee") {
2053
+ for (let j = i + 1; j < tokens.length; j++) {
2054
+ const arg = tokens[j];
2055
+ if (!arg || arg.startsWith("-")) continue;
2056
+ if (SHELL_OPERATORS.has(arg)) break;
2057
+ if (looksLikeAgentStateTarget(arg)) return true;
2058
+ break;
2059
+ }
2060
+ }
2061
+ }
2062
+ for (let i = 0; i < tokens.length; i++) {
2063
+ const t = tokens[i]?.toLowerCase();
2064
+ if (t === "cp" || t === "copy" || t === "mv" || t === "move") {
2065
+ const args = commandSegment(tokens, i + 1);
2066
+ const dst = args.filter((a) => !a.startsWith("-") && !SHELL_OPERATORS.has(a)).pop();
2067
+ if (dst && looksLikeAgentStateTarget(dst)) return true;
2068
+ }
2069
+ }
2070
+ return false;
2071
+ }
2072
+ function looksLikeAgentStateTarget(rawPath) {
2073
+ const expanded = rawPath.replace(/^~([\\/])/, (_, sep2) => `${os2.homedir()}${sep2}`);
2074
+ const resolved = path4.resolve(expanded);
2075
+ const rootStr = wstackGlobalRoot();
2076
+ const resolvedNorm = resolved.replace(/\\/g, "/").toLowerCase();
2077
+ const rootNorm = path4.resolve(rootStr).replace(/\\/g, "/").toLowerCase();
2078
+ if (!resolvedNorm.startsWith(rootNorm) && !resolvedNorm.includes(".wrongstack")) {
2079
+ return false;
2080
+ }
2081
+ return PROTECTED_STATE_BASENAMES.test(path4.basename(resolved));
2082
+ }
2158
2083
  function isClearlyDestructiveBashCommand(command, projectRoot) {
2159
2084
  const trimmed = command.trim();
2160
2085
  if (!trimmed) return false;
@@ -2163,85 +2088,12 @@ function isClearlyDestructiveBashCommand(command, projectRoot) {
2163
2088
  if (hasExternalPublish(trimmed)) return true;
2164
2089
  if (hasFindExec(trimmed)) return true;
2165
2090
  if (hasCatastrophicDelete(trimmed)) return true;
2091
+ if (hasWriteToAgentStateRoot(trimmed)) return true;
2166
2092
  if (CATASTROPHIC_PATTERNS.some((pattern) => pattern.test(trimmed))) return true;
2167
2093
  if (HIGH_IMPACT_PATTERNS.some((pattern) => pattern.test(trimmed))) return true;
2168
2094
  return false;
2169
2095
  }
2170
2096
 
2171
- // src/security/auto-approve-policy.ts
2172
- var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
2173
- allowedCapabilities;
2174
- constructor(allowedCapabilities) {
2175
- this.allowedCapabilities = allowedCapabilities ?? [
2176
- ToolCapabilities.FS_READ,
2177
- ToolCapabilities.NET_OUTBOUND
2178
- ];
2179
- }
2180
- static isMcpTool(name) {
2181
- return name.startsWith("mcp__");
2182
- }
2183
- async evaluate(tool) {
2184
- const caps = tool.capabilities ?? [];
2185
- const hasAllowedCap = caps.some((c) => this.allowedCapabilities.includes(c));
2186
- const isMcp = _AutoApprovePermissionPolicy.isMcpTool(tool.name);
2187
- const mcpProxyAllowed = this.allowedCapabilities.includes(ToolCapabilities.MCP_PROXY);
2188
- const dangerousNotAllowed = getDangerousCapabilities(tool).filter(
2189
- (c) => !this.allowedCapabilities.includes(c)
2190
- );
2191
- const blocked = tool.permission === "deny" || isMcp && !mcpProxyAllowed || !hasAllowedCap || dangerousNotAllowed.length > 0;
2192
- if (blocked) {
2193
- const reason = isMcp && !mcpProxyAllowed ? `MCP tool ${tool.name} is not auto-approved for subagents \u2014 ask the leader to allow mcp.proxy explicitly` : tool.permission === "deny" ? "tool default deny" : dangerousNotAllowed.length > 0 ? `tool requires un-granted dangerous capability (needs: ${dangerousNotAllowed.join(", ")}, allowed: ${this.allowedCapabilities.join(", ")})` : `tool lacks allowed capability (has: ${caps.join(", ") || "none"}, allowed: ${this.allowedCapabilities.join(", ")})`;
2194
- return {
2195
- permission: "deny",
2196
- source: "subagent_guard",
2197
- reason
2198
- };
2199
- }
2200
- return { permission: "auto", source: "yolo" };
2201
- }
2202
- async trust() {
2203
- }
2204
- async deny() {
2205
- }
2206
- denyOnce() {
2207
- }
2208
- allowOnce() {
2209
- }
2210
- async explain(tool) {
2211
- const decision = await this.evaluate(tool);
2212
- return {
2213
- toolName: tool.name,
2214
- subject: null,
2215
- steps: [
2216
- {
2217
- rule: "subagent auto",
2218
- matched: decision.permission === "auto",
2219
- decision: decision.permission,
2220
- source: decision.source,
2221
- detail: decision.reason ?? `subagent policy: ${decision.permission}`
2222
- }
2223
- ],
2224
- winnerIndex: 0,
2225
- decision
2226
- };
2227
- }
2228
- async reload() {
2229
- }
2230
- };
2231
-
2232
- // src/security/permission-helpers.ts
2233
- import { realpathSync } from "node:fs";
2234
- import * as path5 from "node:path";
2235
-
2236
- // src/utils/wstack-paths.ts
2237
- import * as os from "node:os";
2238
- import * as path4 from "node:path";
2239
- function wstackGlobalRoot() {
2240
- const fromEnv = process.env["WRONGSTACK_HOME"];
2241
- if (fromEnv && fromEnv.trim().length > 0) return path4.resolve(fromEnv);
2242
- return path4.join(os.homedir(), ".wrongstack");
2243
- }
2244
-
2245
2097
  // src/security/permission-helpers.ts
2246
2098
  function matchesTrust(patterns, subject) {
2247
2099
  return patterns.includes(subject) || matchAny(patterns, subject);
@@ -2377,8 +2229,558 @@ function shellCommandReadsSensitivePath(command) {
2377
2229
  const rest = tokens.slice(i + 1);
2378
2230
  if (rest.some((arg) => pathLooksSensitive(arg))) return true;
2379
2231
  }
2380
- return false;
2381
- }
2232
+ return false;
2233
+ }
2234
+
2235
+ // src/security/permission-explain.ts
2236
+ function explainPermissionTrace(state, tool, input, ctx) {
2237
+ const subject = subjectForToolInput(tool.name, input, tool.subjectKey);
2238
+ const steps = [];
2239
+ let winnerIndex = -1;
2240
+ const add = (rule, matched, decision, source, detail) => {
2241
+ steps.push({ rule, matched, decision, source, detail });
2242
+ };
2243
+ if (state.policyInvalid) {
2244
+ add("policy invalid", true, "deny", "deny", "trust policy is invalid; all tools are denied");
2245
+ winnerIndex = 0;
2246
+ return {
2247
+ toolName: tool.name,
2248
+ subject,
2249
+ steps,
2250
+ winnerIndex,
2251
+ decision: { permission: "deny", source: "deny", reason: "trust policy is invalid" }
2252
+ };
2253
+ }
2254
+ add("policy valid", false, "auto", "default", "trust policy loaded successfully");
2255
+ let namespaceEntry;
2256
+ for (const { pattern, value } of state.wildcardEntries) {
2257
+ if (matchGlob(pattern, tool.name)) {
2258
+ namespaceEntry = value;
2259
+ break;
2260
+ }
2261
+ }
2262
+ const entry = state.policy[tool.name] ?? namespaceEntry;
2263
+ const namespaceSource = namespaceEntry ? `wildcard entry matched (${Object.keys(state.policy).find((k) => k.includes("*") && matchGlob(k, tool.name))})` : "no namespace match";
2264
+ const cacheKey = `${tool.name}::${subject ?? tool.name}`;
2265
+ if (state.sessionDenied.has(cacheKey)) {
2266
+ add(
2267
+ "session soft deny",
2268
+ true,
2269
+ "deny",
2270
+ "deny",
2271
+ 'user pressed "no" earlier in this session \u2014 blocked until reload'
2272
+ );
2273
+ winnerIndex = steps.length - 1;
2274
+ return {
2275
+ toolName: tool.name,
2276
+ subject,
2277
+ steps,
2278
+ winnerIndex,
2279
+ decision: {
2280
+ permission: "deny",
2281
+ source: "deny",
2282
+ reason: "session soft deny (user pressed no)"
2283
+ }
2284
+ };
2285
+ }
2286
+ add(
2287
+ "session soft deny",
2288
+ false,
2289
+ "deny",
2290
+ "deny",
2291
+ "no session-level soft deny for this tool+subject"
2292
+ );
2293
+ if (state.sessionAllowed.has(cacheKey)) {
2294
+ add(
2295
+ "session soft allow",
2296
+ true,
2297
+ "auto",
2298
+ "trust",
2299
+ 'user pressed "yes" in current session \u2014 one-shot auto-approve (consumed)'
2300
+ );
2301
+ winnerIndex = steps.length - 1;
2302
+ return {
2303
+ toolName: tool.name,
2304
+ subject,
2305
+ steps,
2306
+ winnerIndex,
2307
+ decision: {
2308
+ permission: "auto",
2309
+ source: "trust",
2310
+ reason: "session one-shot allow (user pressed yes)"
2311
+ }
2312
+ };
2313
+ }
2314
+ add(
2315
+ "session soft allow",
2316
+ false,
2317
+ "auto",
2318
+ "trust",
2319
+ "no session-level one-shot allow for this tool+subject"
2320
+ );
2321
+ if (entry?.deny && subject && matchesTrust(entry.deny, subject)) {
2322
+ add(
2323
+ "trust deny",
2324
+ true,
2325
+ "deny",
2326
+ "deny",
2327
+ `subject "${subject}" matched a deny pattern in trust file`
2328
+ );
2329
+ winnerIndex = steps.length - 1;
2330
+ return {
2331
+ toolName: tool.name,
2332
+ subject,
2333
+ steps,
2334
+ winnerIndex,
2335
+ decision: { permission: "deny", source: "deny", reason: "matched deny pattern" }
2336
+ };
2337
+ }
2338
+ add(
2339
+ "trust deny",
2340
+ false,
2341
+ "deny",
2342
+ "deny",
2343
+ `no deny pattern matched (namespace: ${namespaceSource})`
2344
+ );
2345
+ if (tool.permission === "deny") {
2346
+ add(
2347
+ "tool default deny",
2348
+ true,
2349
+ "deny",
2350
+ "default",
2351
+ `tool "${tool.name}" has permission: deny by default`
2352
+ );
2353
+ winnerIndex = steps.length - 1;
2354
+ return {
2355
+ toolName: tool.name,
2356
+ subject,
2357
+ steps,
2358
+ winnerIndex,
2359
+ decision: { permission: "deny", source: "default", reason: "tool default deny" }
2360
+ };
2361
+ }
2362
+ add(
2363
+ "tool default deny",
2364
+ false,
2365
+ "deny",
2366
+ "default",
2367
+ `tool "${tool.name}" has permission: "${tool.permission}"`
2368
+ );
2369
+ if (entry?.allow && subject && matchesTrust(entry.allow, subject)) {
2370
+ add(
2371
+ "trust allow",
2372
+ true,
2373
+ "auto",
2374
+ "trust",
2375
+ `subject "${subject}" matched an allow pattern in trust file`
2376
+ );
2377
+ winnerIndex = steps.length - 1;
2378
+ return {
2379
+ toolName: tool.name,
2380
+ subject,
2381
+ steps,
2382
+ winnerIndex,
2383
+ decision: { permission: "auto", source: "trust", reason: "matched allow pattern" }
2384
+ };
2385
+ }
2386
+ add(
2387
+ "trust allow",
2388
+ false,
2389
+ "auto",
2390
+ "trust",
2391
+ `no allow pattern matched (namespace: ${namespaceSource})`
2392
+ );
2393
+ if (entry?.auto) {
2394
+ add("trust auto", true, "auto", "trust", `trust file has auto: true for "${tool.name}"`);
2395
+ winnerIndex = steps.length - 1;
2396
+ return {
2397
+ toolName: tool.name,
2398
+ subject,
2399
+ steps,
2400
+ winnerIndex,
2401
+ decision: { permission: "auto", source: "trust", reason: "trust auto" }
2402
+ };
2403
+ }
2404
+ add("trust auto", false, "auto", "trust", `no auto flag for "${tool.name}" in trust file`);
2405
+ if (!state.yolo && state.isSensitiveReadCall(tool, input)) {
2406
+ const hasDelegate2 = state.promptDelegatePresent;
2407
+ add(
2408
+ "sensitive read",
2409
+ true,
2410
+ "confirm",
2411
+ "default",
2412
+ hasDelegate2 ? "sensitive file read detected \u2014 would prompt user for approval" : "sensitive file read detected \u2014 returns confirm (no prompt delegate)"
2413
+ );
2414
+ winnerIndex = steps.length - 1;
2415
+ return {
2416
+ toolName: tool.name,
2417
+ subject,
2418
+ steps,
2419
+ winnerIndex,
2420
+ decision: {
2421
+ permission: "confirm",
2422
+ source: "default",
2423
+ riskTier: "standard",
2424
+ reason: "sensitive file read needs explicit approval"
2425
+ }
2426
+ };
2427
+ }
2428
+ add(
2429
+ "sensitive read",
2430
+ false,
2431
+ "confirm",
2432
+ "default",
2433
+ "not a sensitive read call (or YOLO bypasses this check)"
2434
+ );
2435
+ if (state.yolo) {
2436
+ if (state.yoloBlockedAsDestructive(tool, input, ctx)) {
2437
+ add(
2438
+ "yolo destructive gate",
2439
+ true,
2440
+ "confirm",
2441
+ "yolo_destructive",
2442
+ "YOLO is active but this is a clearly destructive command \u2014 still asking"
2443
+ );
2444
+ winnerIndex = steps.length - 1;
2445
+ return {
2446
+ toolName: tool.name,
2447
+ subject,
2448
+ steps,
2449
+ winnerIndex,
2450
+ decision: {
2451
+ permission: "confirm",
2452
+ source: "yolo_destructive",
2453
+ riskTier: "destructive",
2454
+ reason: "destructive command needs explicit approval even in YOLO mode"
2455
+ }
2456
+ };
2457
+ }
2458
+ add(
2459
+ "yolo",
2460
+ true,
2461
+ "auto",
2462
+ "yolo",
2463
+ "YOLO mode is active \u2014 auto-approving every non-denied call"
2464
+ );
2465
+ winnerIndex = steps.length - 1;
2466
+ return {
2467
+ toolName: tool.name,
2468
+ subject,
2469
+ steps,
2470
+ winnerIndex,
2471
+ decision: { permission: "auto", source: "yolo" }
2472
+ };
2473
+ }
2474
+ add("yolo", false, "auto", "yolo", "YOLO mode is not active");
2475
+ if (tool.name === "write" && subject) {
2476
+ const isAgentState = isInsideAgentStateRoot(subject);
2477
+ const hasRead = ctx.hasRead(subject) && !isAgentState;
2478
+ add(
2479
+ "write smart bypass",
2480
+ hasRead,
2481
+ "auto",
2482
+ "context",
2483
+ isAgentState ? `file "${subject}" is WrongStack's own state \u2014 bypass never applies, write always confirms` : hasRead ? `file "${subject}" was already read in this session \u2014 auto-approving write` : `file "${subject}" was not read in this session \u2014 bypass does not apply`
2484
+ );
2485
+ if (hasRead) {
2486
+ winnerIndex = steps.length - 1;
2487
+ return {
2488
+ toolName: tool.name,
2489
+ subject,
2490
+ steps,
2491
+ winnerIndex,
2492
+ decision: {
2493
+ permission: "auto",
2494
+ source: "context",
2495
+ reason: "file already read in this session"
2496
+ }
2497
+ };
2498
+ }
2499
+ } else {
2500
+ add(
2501
+ "write smart bypass",
2502
+ false,
2503
+ "auto",
2504
+ "context",
2505
+ 'tool is not "write" or has no subject \u2014 bypass does not apply'
2506
+ );
2507
+ }
2508
+ const hasWriteCap = hasCapability(tool, ToolCapabilities.FS_WRITE);
2509
+ const hasShellCap = hasCapability(tool, [
2510
+ ToolCapabilities.SHELL_ARBITRARY,
2511
+ ToolCapabilities.SHELL_RESTRICTED,
2512
+ ToolCapabilities.SHELL_EXEC
2513
+ ]);
2514
+ const hasInstallCap = hasCapability(tool, ToolCapabilities.PACKAGE_INSTALL);
2515
+ const hasConfigCap = hasCapability(tool, ToolCapabilities.CONFIG_MUTATE);
2516
+ const hasSubagentCap = hasCapability(tool, ToolCapabilities.SUBAGENT_SPAWN);
2517
+ const isMutating = tool.mutating || hasWriteCap || hasShellCap || hasInstallCap || hasConfigCap || hasSubagentCap;
2518
+ if (tool.permission === "auto" && !isMutating) {
2519
+ add(
2520
+ "safe default auto",
2521
+ true,
2522
+ "auto",
2523
+ "default",
2524
+ `tool "${tool.name}" has auto permission and is not mutating \u2014 safe to auto-approve`
2525
+ );
2526
+ winnerIndex = steps.length - 1;
2527
+ return {
2528
+ toolName: tool.name,
2529
+ subject,
2530
+ steps,
2531
+ winnerIndex,
2532
+ decision: { permission: "auto", source: "default" }
2533
+ };
2534
+ }
2535
+ add(
2536
+ "mutating default confirm",
2537
+ true,
2538
+ "confirm",
2539
+ "default",
2540
+ isMutating ? `tool "${tool.name}" is mutating (default auto not enough) \u2014 needs confirmation` : `tool "${tool.name}" has "${tool.permission}" permission \u2014 needs confirmation`
2541
+ );
2542
+ winnerIndex = steps.length - 1;
2543
+ const hasDelegate = state.promptDelegatePresent;
2544
+ return {
2545
+ toolName: tool.name,
2546
+ subject,
2547
+ steps,
2548
+ winnerIndex,
2549
+ decision: {
2550
+ permission: "confirm",
2551
+ source: "default",
2552
+ ...hasDelegate ? { reason: "would prompt user via delegate" } : {}
2553
+ }
2554
+ };
2555
+ }
2556
+
2557
+ // src/security/permission-policy-schema.ts
2558
+ var TRUST_POLICY_SCHEMA_VERSION = 1;
2559
+ var TRUST_POLICY_LIMITS = Object.freeze({
2560
+ maxTools: 1e3,
2561
+ maxPatternsPerRule: 1e3,
2562
+ maxToolNameChars: 256,
2563
+ maxPatternChars: 4096
2564
+ });
2565
+ var TRUST_POLICY_JSON_SCHEMA = Object.freeze({
2566
+ $schema: "https://json-schema.org/draft/2020-12/schema",
2567
+ $id: "https://wrongstack.dev/schemas/trust-policy-v1.json",
2568
+ title: "WrongStack Trust Policy",
2569
+ type: "object",
2570
+ maxProperties: TRUST_POLICY_LIMITS.maxTools,
2571
+ propertyNames: {
2572
+ minLength: 1,
2573
+ maxLength: TRUST_POLICY_LIMITS.maxToolNameChars,
2574
+ not: { enum: ["__proto__", "prototype", "constructor"] }
2575
+ },
2576
+ additionalProperties: {
2577
+ type: "object",
2578
+ additionalProperties: false,
2579
+ properties: {
2580
+ allow: {
2581
+ type: "array",
2582
+ maxItems: TRUST_POLICY_LIMITS.maxPatternsPerRule,
2583
+ items: { type: "string", minLength: 1, maxLength: TRUST_POLICY_LIMITS.maxPatternChars }
2584
+ },
2585
+ deny: {
2586
+ type: "array",
2587
+ maxItems: TRUST_POLICY_LIMITS.maxPatternsPerRule,
2588
+ items: { type: "string", minLength: 1, maxLength: TRUST_POLICY_LIMITS.maxPatternChars }
2589
+ },
2590
+ auto: { type: "boolean" },
2591
+ trustWorkdir: { type: "boolean" },
2592
+ denyPrivate: { type: "boolean" }
2593
+ }
2594
+ }
2595
+ });
2596
+ var RULE_FIELDS2 = /* @__PURE__ */ new Set(["allow", "deny", "auto", "trustWorkdir", "denyPrivate"]);
2597
+ var UNSAFE_PROPERTY_NAMES2 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
2598
+ function isRecord2(value) {
2599
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2600
+ }
2601
+ function error2(diagnostics, code, path9, message) {
2602
+ diagnostics.push({ severity: "error", code, path: path9, message });
2603
+ }
2604
+ function validatePatterns(value, path9, diagnostics) {
2605
+ if (!Array.isArray(value)) {
2606
+ error2(diagnostics, "invalid_pattern_list", path9, "must be an array of strings");
2607
+ return void 0;
2608
+ }
2609
+ if (value.length > TRUST_POLICY_LIMITS.maxPatternsPerRule) {
2610
+ error2(
2611
+ diagnostics,
2612
+ "too_many_patterns",
2613
+ path9,
2614
+ `must contain at most ${TRUST_POLICY_LIMITS.maxPatternsPerRule} patterns`
2615
+ );
2616
+ return void 0;
2617
+ }
2618
+ const patterns = [];
2619
+ const seen = /* @__PURE__ */ new Set();
2620
+ for (const [index, pattern] of value.entries()) {
2621
+ const itemPath = `${path9}[${index}]`;
2622
+ if (typeof pattern !== "string" || pattern.length === 0 || pattern.length > TRUST_POLICY_LIMITS.maxPatternChars) {
2623
+ error2(
2624
+ diagnostics,
2625
+ "invalid_pattern",
2626
+ itemPath,
2627
+ `must be a non-empty string up to ${TRUST_POLICY_LIMITS.maxPatternChars} characters`
2628
+ );
2629
+ continue;
2630
+ }
2631
+ if (seen.has(pattern)) {
2632
+ diagnostics.push({
2633
+ severity: "warning",
2634
+ code: "duplicate_pattern",
2635
+ path: itemPath,
2636
+ message: "duplicates an earlier pattern and was normalized away"
2637
+ });
2638
+ continue;
2639
+ }
2640
+ seen.add(pattern);
2641
+ patterns.push(pattern);
2642
+ }
2643
+ return patterns;
2644
+ }
2645
+ function validateTrustPolicy(value) {
2646
+ const diagnostics = [];
2647
+ if (!isRecord2(value)) {
2648
+ return {
2649
+ ok: false,
2650
+ diagnostics: [
2651
+ {
2652
+ severity: "error",
2653
+ code: "invalid_root",
2654
+ path: "$",
2655
+ message: "trust policy must be an object"
2656
+ }
2657
+ ]
2658
+ };
2659
+ }
2660
+ const entries = Object.entries(value);
2661
+ if (entries.length > TRUST_POLICY_LIMITS.maxTools) {
2662
+ error2(
2663
+ diagnostics,
2664
+ "too_many_tools",
2665
+ "$",
2666
+ `must contain at most ${TRUST_POLICY_LIMITS.maxTools} tool rules`
2667
+ );
2668
+ }
2669
+ const policy = {};
2670
+ for (const [toolName, rawRule] of entries.slice(0, TRUST_POLICY_LIMITS.maxTools)) {
2671
+ const toolPath = `$[${JSON.stringify(toolName)}]`;
2672
+ if (toolName.length === 0 || toolName.length > TRUST_POLICY_LIMITS.maxToolNameChars) {
2673
+ error2(
2674
+ diagnostics,
2675
+ "invalid_tool_name",
2676
+ toolPath,
2677
+ `tool name must be 1-${TRUST_POLICY_LIMITS.maxToolNameChars} characters`
2678
+ );
2679
+ continue;
2680
+ }
2681
+ if (UNSAFE_PROPERTY_NAMES2.has(toolName)) {
2682
+ error2(
2683
+ diagnostics,
2684
+ "unsafe_tool_name",
2685
+ toolPath,
2686
+ "reserved object property names are not valid tool rules"
2687
+ );
2688
+ continue;
2689
+ }
2690
+ if (!isRecord2(rawRule)) {
2691
+ error2(diagnostics, "invalid_rule", toolPath, "tool rule must be an object");
2692
+ continue;
2693
+ }
2694
+ for (const field of Object.keys(rawRule)) {
2695
+ if (!RULE_FIELDS2.has(field)) {
2696
+ error2(
2697
+ diagnostics,
2698
+ "unknown_field",
2699
+ `${toolPath}.${field}`,
2700
+ `unknown policy field "${field}"`
2701
+ );
2702
+ }
2703
+ }
2704
+ const rule = {};
2705
+ for (const field of ["allow", "deny"]) {
2706
+ if (rawRule[field] === void 0) continue;
2707
+ const patterns = validatePatterns(rawRule[field], `${toolPath}.${field}`, diagnostics);
2708
+ if (patterns) rule[field] = patterns;
2709
+ }
2710
+ for (const field of ["auto", "trustWorkdir", "denyPrivate"]) {
2711
+ const setting = rawRule[field];
2712
+ if (setting === void 0) continue;
2713
+ if (typeof setting !== "boolean") {
2714
+ error2(diagnostics, "invalid_boolean", `${toolPath}.${field}`, "must be a boolean");
2715
+ } else {
2716
+ rule[field] = setting;
2717
+ }
2718
+ }
2719
+ policy[toolName] = rule;
2720
+ }
2721
+ return diagnostics.some((diagnostic) => diagnostic.severity === "error") ? { ok: false, diagnostics } : { ok: true, policy, diagnostics };
2722
+ }
2723
+
2724
+ // src/security/auto-approve-policy.ts
2725
+ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
2726
+ allowedCapabilities;
2727
+ constructor(allowedCapabilities) {
2728
+ this.allowedCapabilities = allowedCapabilities ?? [
2729
+ ToolCapabilities.FS_READ,
2730
+ ToolCapabilities.NET_OUTBOUND
2731
+ ];
2732
+ }
2733
+ static isMcpTool(name) {
2734
+ return name.startsWith("mcp__");
2735
+ }
2736
+ async evaluate(tool) {
2737
+ const caps = tool.capabilities ?? [];
2738
+ const hasAllowedCap = caps.some((c) => this.allowedCapabilities.includes(c));
2739
+ const isMcp = _AutoApprovePermissionPolicy.isMcpTool(tool.name);
2740
+ const mcpProxyAllowed = this.allowedCapabilities.includes(ToolCapabilities.MCP_PROXY);
2741
+ const dangerousNotAllowed = getDangerousCapabilities(tool).filter(
2742
+ (c) => !this.allowedCapabilities.includes(c)
2743
+ );
2744
+ const blocked = tool.permission === "deny" || isMcp && !mcpProxyAllowed || !hasAllowedCap || dangerousNotAllowed.length > 0;
2745
+ if (blocked) {
2746
+ const reason = isMcp && !mcpProxyAllowed ? `MCP tool ${tool.name} is not auto-approved for subagents \u2014 ask the leader to allow mcp.proxy explicitly` : tool.permission === "deny" ? "tool default deny" : dangerousNotAllowed.length > 0 ? `tool requires un-granted dangerous capability (needs: ${dangerousNotAllowed.join(", ")}, allowed: ${this.allowedCapabilities.join(", ")})` : `tool lacks allowed capability (has: ${caps.join(", ") || "none"}, allowed: ${this.allowedCapabilities.join(", ")})`;
2747
+ return {
2748
+ permission: "deny",
2749
+ source: "subagent_guard",
2750
+ reason
2751
+ };
2752
+ }
2753
+ return { permission: "auto", source: "yolo" };
2754
+ }
2755
+ async trust() {
2756
+ }
2757
+ async deny() {
2758
+ }
2759
+ denyOnce() {
2760
+ }
2761
+ allowOnce() {
2762
+ }
2763
+ async explain(tool) {
2764
+ const decision = await this.evaluate(tool);
2765
+ return {
2766
+ toolName: tool.name,
2767
+ subject: null,
2768
+ steps: [
2769
+ {
2770
+ rule: "subagent auto",
2771
+ matched: decision.permission === "auto",
2772
+ decision: decision.permission,
2773
+ source: decision.source,
2774
+ detail: decision.reason ?? `subagent policy: ${decision.permission}`
2775
+ }
2776
+ ],
2777
+ winnerIndex: 0,
2778
+ decision
2779
+ };
2780
+ }
2781
+ async reload() {
2782
+ }
2783
+ };
2382
2784
 
2383
2785
  // src/security/permission-policy.ts
2384
2786
  function fsWriteTargetPaths(input) {
@@ -2414,70 +2816,13 @@ var DefaultPermissionPolicy = class {
2414
2816
  loaded = false;
2415
2817
  trustFile;
2416
2818
  yolo;
2417
- /**
2418
- * Session-scoped "soft deny" map. When the user presses 'n' (block once),
2419
- * the tool+pattern is added here. If the LLM retries in the same session,
2420
- * we return deny directly without asking again.
2421
- *
2422
- * Cleared on reload() since reload = fresh trust file snapshot.
2423
- */
2424
2819
  sessionDenied = /* @__PURE__ */ new Map();
2425
- /**
2426
- * Session-scoped one-shot "soft trust" map. When the user presses 'y', the
2427
- * tool+pattern is added here so the immediate confirm re-run can proceed.
2428
- * The entry is consumed on first use; future calls must ask again unless the
2429
- * user chose persistent trust.
2430
- *
2431
- * Cleared on reload().
2432
- */
2433
2820
  sessionAllowed = /* @__PURE__ */ new Map();
2434
- /**
2435
- * Interactive prompt delegate. When set, `evaluate()` calls it to get a
2436
- * user decision synchronously (CLI REPL path). When cleared (TUI / WebUI),
2437
- * `evaluate()` returns `confirm` so the caller can emit
2438
- * `tool.confirm_needed` for the UI layer to handle.
2439
- *
2440
- * Mutable so the host can switch from CLI-prompt to event-driven
2441
- * confirmation at runtime (e.g. when `--goal` forces TUI mode after
2442
- * the agent was already constructed).
2443
- */
2444
2821
  promptDelegate;
2445
- /** Pre-compiled wildcard patterns — rebuilt on reload for O(1) lookup. */
2446
2822
  wildcardEntries = [];
2447
- /**
2448
- * Evaluate-result cache. Keyed by `tool.name::subject::permissionFingerprint`
2449
- * so repeated calls with the same tool+input skip namespace matching,
2450
- * subject computation, and pattern matching (matchAny). The fingerprint
2451
- * suffix covers the `Tool` fields `evaluate()` reads (`permission`,
2452
- * `riskTier`, `mutating`, `capabilities`), so a tool redefined via
2453
- * `ToolRegistry.wrap()` misses the cache instead of inheriting a stale
2454
- * verdict.
2455
- *
2456
- * Cleared on any state change (reload, trust, deny, yolo toggle) because
2457
- * the result depends on the full policy state. The write-tool smart-bypass
2458
- * (step 7 in `evaluate()`) is not cached since `ctx.hasRead()` changes
2459
- * dynamically within a session.
2460
- *
2461
- * P3 #17 (before-release.md): capped at 500 entries via LRU eviction. An
2462
- * iteration that evaluates thousands of unique tool+subject combinations
2463
- * (e.g. repeated `tool-help` or `bash` with different commands) can grow
2464
- * the cache without bound. 500 is far more than any single iteration needs;
2465
- * the LRU evicts the least-recently-used entries first.
2466
- */
2467
2823
  _evalCache = new LruCache(500);
2468
2824
  policyDiagnostics = [];
2469
2825
  policyInvalid = false;
2470
- /**
2471
- * When true, YOLO also auto-approves clearly destructive shell commands.
2472
- *
2473
- * WS-008: `isClearlyDestructiveBashCommand` was exported and heavily tested
2474
- * but called from zero production modules, so YOLO returned `auto` for
2475
- * everything — including `rm -rf`, git history rewrites, and external
2476
- * publishes. The `'yolo_destructive'` decision source, the getter/setter on
2477
- * PermissionPolicy, and the TUI branch that reads it all existed; only the
2478
- * gate itself was missing. Defaults to false: destructive commands still ask,
2479
- * even in YOLO.
2480
- */
2481
2826
  yoloDestructive;
2482
2827
  constructor(opts) {
2483
2828
  this.trustFile = opts.trustFile;
@@ -2485,30 +2830,13 @@ var DefaultPermissionPolicy = class {
2485
2830
  this.yoloDestructive = opts.yoloDestructive ?? false;
2486
2831
  this.promptDelegate = opts.promptDelegate;
2487
2832
  }
2488
- /** Allow YOLO to cover destructive shell commands too (off by default). */
2489
2833
  setYoloDestructive(enabled) {
2490
2834
  if (this.yoloDestructive !== enabled) this._evalCache.clear();
2491
2835
  this.yoloDestructive = enabled;
2492
2836
  }
2493
- /** Whether YOLO currently extends to destructive shell commands. */
2494
2837
  getYoloDestructive() {
2495
2838
  return this.yoloDestructive;
2496
2839
  }
2497
- /**
2498
- * True when this call is an FS_WRITE whose target lands inside WrongStack's
2499
- * own state root.
2500
- *
2501
- * FS_WRITE tools disagree on where the target path lives: write/edit use
2502
- * `path`, replace/format/git `files` (string OR array), patch `directory`,
2503
- * design `out`, scaffold writes under `cwd`/`template`. Checking only
2504
- * `path`/`file_path` let a state-root write merely switch tools, so every
2505
- * path-bearing key is inspected.
2506
- *
2507
- * Shared by the YOLO carve-out and the eval-cache guard so the two cannot
2508
- * drift: the carve-out scans all path keys while the cache key fingerprints
2509
- * only the subject key, so the cache must consult the same predicate or a
2510
- * secondary state-root key can be replayed a cached `auto`.
2511
- */
2512
2840
  hasAgentStateWriteTarget(tool, input, ctx) {
2513
2841
  if (!hasCapability(tool, ToolCapabilities.FS_WRITE)) return false;
2514
2842
  for (const targetPath of fsWriteTargetPaths(input)) {
@@ -2518,11 +2846,6 @@ var DefaultPermissionPolicy = class {
2518
2846
  }
2519
2847
  return false;
2520
2848
  }
2521
- /**
2522
- * True when YOLO is on but this specific call is a destructive shell command
2523
- * the user has not opted to auto-approve. Shared by `evaluate()` and the
2524
- * explain/trace path so the two cannot drift.
2525
- */
2526
2849
  yoloBlockedAsDestructive(tool, input, ctx) {
2527
2850
  if (!this.yolo || this.yoloDestructive) return false;
2528
2851
  if (this.hasAgentStateWriteTarget(tool, input, ctx)) return true;
@@ -2532,25 +2855,16 @@ var DefaultPermissionPolicy = class {
2532
2855
  if (!command) return false;
2533
2856
  return isClearlyDestructiveBashCommand(command, ctx.projectRoot);
2534
2857
  }
2535
- /**
2536
- * Replace (or clear) the interactive prompt delegate at runtime.
2537
- * Used by the CLI to switch from inline prompts (REPL) to event-driven
2538
- * confirmation (TUI) when the run mode is determined after the policy
2539
- * was constructed (e.g. `--goal` auto-flipping to TUI).
2540
- */
2541
2858
  setPromptDelegate(delegate) {
2542
2859
  this.promptDelegate = delegate;
2543
2860
  }
2544
- /** Toggle YOLO (auto-approve) mode at runtime. */
2545
2861
  setYolo(enabled) {
2546
2862
  if (this.yolo !== enabled) this._evalCache.clear();
2547
2863
  this.yolo = enabled;
2548
2864
  }
2549
- /** Check whether YOLO mode is currently active. */
2550
2865
  getYolo() {
2551
2866
  return this.yolo;
2552
2867
  }
2553
- /** Read-only diagnostics for policy inspector/editor surfaces. */
2554
2868
  getPolicyDiagnostics() {
2555
2869
  return this.policyDiagnostics.map((diagnostic) => ({ ...diagnostic }));
2556
2870
  }
@@ -2819,7 +3133,6 @@ var DefaultPermissionPolicy = class {
2819
3133
  throw err;
2820
3134
  }
2821
3135
  }
2822
- /** Persist a deny rule — this tool+pattern pair is permanently blocked. */
2823
3136
  async deny(rule) {
2824
3137
  if (!this.loaded) await this.reload();
2825
3138
  if (this.policyInvalid) {
@@ -2840,335 +3153,32 @@ var DefaultPermissionPolicy = class {
2840
3153
  throw err;
2841
3154
  }
2842
3155
  }
2843
- /** Block this tool+pattern for the rest of this session (no trust file). */
2844
3156
  denyOnce(rule) {
2845
3157
  this.sessionDenied.set(`${rule.tool}::${rule.pattern}`, true);
2846
3158
  this._evalCache.clear();
2847
3159
  }
2848
- /** Auto-approve this tool+pattern once (no trust file). */
2849
3160
  allowOnce(rule) {
2850
3161
  this.sessionAllowed.set(`${rule.tool}::${rule.pattern}`, true);
2851
3162
  this._evalCache.clear();
2852
3163
  }
2853
- /**
2854
- * Side-effect-free permission evaluation trace. Mirrors `evaluate()`
2855
- * step-by-step without prompting the user, writing to trust files, or
2856
- * mutating session state. Returns the full ordered trace and winner.
2857
- */
2858
3164
  async explain(tool, input, ctx) {
2859
3165
  if (!this.loaded) await this.reload();
2860
- const subject = subjectForToolInput(tool.name, input, tool.subjectKey);
2861
- const steps = [];
2862
- let winnerIndex = -1;
2863
- const add = (rule, matched, decision, source, detail) => {
2864
- steps.push({ rule, matched, decision, source, detail });
2865
- };
2866
- if (this.policyInvalid) {
2867
- add("policy invalid", true, "deny", "deny", "trust policy is invalid; all tools are denied");
2868
- winnerIndex = 0;
2869
- return {
2870
- toolName: tool.name,
2871
- subject,
2872
- steps,
2873
- winnerIndex,
2874
- decision: { permission: "deny", source: "deny", reason: "trust policy is invalid" }
2875
- };
2876
- }
2877
- add("policy valid", false, "auto", "default", "trust policy loaded successfully");
2878
- const namespaceEntry = this.findNamespaceEntry(tool.name);
2879
- const entry = this.policy[tool.name] ?? namespaceEntry;
2880
- const namespaceSource = namespaceEntry ? `wildcard entry matched (${Object.keys(this.policy).find((k) => k.includes("*") && matchGlob(k, tool.name))})` : "no namespace match";
2881
- const cacheKey = `${tool.name}::${subject ?? tool.name}`;
2882
- if (this.sessionDenied.has(cacheKey)) {
2883
- add(
2884
- "session soft deny",
2885
- true,
2886
- "deny",
2887
- "deny",
2888
- 'user pressed "no" earlier in this session \u2014 blocked until reload'
2889
- );
2890
- winnerIndex = steps.length - 1;
2891
- return {
2892
- toolName: tool.name,
2893
- subject,
2894
- steps,
2895
- winnerIndex,
2896
- decision: {
2897
- permission: "deny",
2898
- source: "deny",
2899
- reason: "session soft deny (user pressed no)"
2900
- }
2901
- };
2902
- }
2903
- add(
2904
- "session soft deny",
2905
- false,
2906
- "deny",
2907
- "deny",
2908
- "no session-level soft deny for this tool+subject"
2909
- );
2910
- if (this.sessionAllowed.has(cacheKey)) {
2911
- add(
2912
- "session soft allow",
2913
- true,
2914
- "auto",
2915
- "trust",
2916
- 'user pressed "yes" in current session \u2014 one-shot auto-approve (consumed)'
2917
- );
2918
- winnerIndex = steps.length - 1;
2919
- return {
2920
- toolName: tool.name,
2921
- subject,
2922
- steps,
2923
- winnerIndex,
2924
- decision: {
2925
- permission: "auto",
2926
- source: "trust",
2927
- reason: "session one-shot allow (user pressed yes)"
2928
- }
2929
- };
2930
- }
2931
- add(
2932
- "session soft allow",
2933
- false,
2934
- "auto",
2935
- "trust",
2936
- "no session-level one-shot allow for this tool+subject"
2937
- );
2938
- if (entry?.deny && subject && matchesTrust(entry.deny, subject)) {
2939
- add(
2940
- "trust deny",
2941
- true,
2942
- "deny",
2943
- "deny",
2944
- `subject "${subject}" matched a deny pattern in trust file`
2945
- );
2946
- winnerIndex = steps.length - 1;
2947
- return {
2948
- toolName: tool.name,
2949
- subject,
2950
- steps,
2951
- winnerIndex,
2952
- decision: { permission: "deny", source: "deny", reason: "matched deny pattern" }
2953
- };
2954
- }
2955
- add(
2956
- "trust deny",
2957
- false,
2958
- "deny",
2959
- "deny",
2960
- `no deny pattern matched (namespace: ${namespaceSource})`
2961
- );
2962
- if (tool.permission === "deny") {
2963
- add(
2964
- "tool default deny",
2965
- true,
2966
- "deny",
2967
- "default",
2968
- `tool "${tool.name}" has permission: deny by default`
2969
- );
2970
- winnerIndex = steps.length - 1;
2971
- return {
2972
- toolName: tool.name,
2973
- subject,
2974
- steps,
2975
- winnerIndex,
2976
- decision: { permission: "deny", source: "default", reason: "tool default deny" }
2977
- };
2978
- }
2979
- add(
2980
- "tool default deny",
2981
- false,
2982
- "deny",
2983
- "default",
2984
- `tool "${tool.name}" has permission: "${tool.permission}"`
2985
- );
2986
- if (entry?.allow && subject && matchesTrust(entry.allow, subject)) {
2987
- add(
2988
- "trust allow",
2989
- true,
2990
- "auto",
2991
- "trust",
2992
- `subject "${subject}" matched an allow pattern in trust file`
2993
- );
2994
- winnerIndex = steps.length - 1;
2995
- return {
2996
- toolName: tool.name,
2997
- subject,
2998
- steps,
2999
- winnerIndex,
3000
- decision: { permission: "auto", source: "trust", reason: "matched allow pattern" }
3001
- };
3002
- }
3003
- add(
3004
- "trust allow",
3005
- false,
3006
- "auto",
3007
- "trust",
3008
- `no allow pattern matched (namespace: ${namespaceSource})`
3009
- );
3010
- if (entry?.auto) {
3011
- add("trust auto", true, "auto", "trust", `trust file has auto: true for "${tool.name}"`);
3012
- winnerIndex = steps.length - 1;
3013
- return {
3014
- toolName: tool.name,
3015
- subject,
3016
- steps,
3017
- winnerIndex,
3018
- decision: { permission: "auto", source: "trust", reason: "trust auto" }
3019
- };
3020
- }
3021
- add("trust auto", false, "auto", "trust", `no auto flag for "${tool.name}" in trust file`);
3022
- if (!this.yolo && this.isSensitiveReadCall(tool, input)) {
3023
- const hasDelegate2 = this.promptDelegate !== void 0;
3024
- add(
3025
- "sensitive read",
3026
- true,
3027
- hasDelegate2 ? "confirm" : "confirm",
3028
- "default",
3029
- hasDelegate2 ? "sensitive file read detected \u2014 would prompt user for approval" : "sensitive file read detected \u2014 returns confirm (no prompt delegate)"
3030
- );
3031
- winnerIndex = steps.length - 1;
3032
- return {
3033
- toolName: tool.name,
3034
- subject,
3035
- steps,
3036
- winnerIndex,
3037
- decision: {
3038
- permission: "confirm",
3039
- source: "default",
3040
- riskTier: "standard",
3041
- reason: "sensitive file read needs explicit approval"
3042
- }
3043
- };
3044
- }
3045
- add(
3046
- "sensitive read",
3047
- false,
3048
- "confirm",
3049
- "default",
3050
- "not a sensitive read call (or YOLO bypasses this check)"
3051
- );
3052
- if (this.yolo) {
3053
- if (this.yoloBlockedAsDestructive(tool, input, ctx)) {
3054
- add(
3055
- "yolo destructive gate",
3056
- true,
3057
- "confirm",
3058
- "yolo_destructive",
3059
- "YOLO is active but this is a clearly destructive command \u2014 still asking"
3060
- );
3061
- winnerIndex = steps.length - 1;
3062
- return {
3063
- toolName: tool.name,
3064
- subject,
3065
- steps,
3066
- winnerIndex,
3067
- decision: {
3068
- permission: "confirm",
3069
- source: "yolo_destructive",
3070
- riskTier: "destructive",
3071
- reason: "destructive command needs explicit approval even in YOLO mode"
3072
- }
3073
- };
3074
- }
3075
- add(
3076
- "yolo",
3077
- true,
3078
- "auto",
3079
- "yolo",
3080
- "YOLO mode is active \u2014 auto-approving every non-denied call"
3081
- );
3082
- winnerIndex = steps.length - 1;
3083
- return {
3084
- toolName: tool.name,
3085
- subject,
3086
- steps,
3087
- winnerIndex,
3088
- decision: { permission: "auto", source: "yolo" }
3089
- };
3090
- }
3091
- add("yolo", false, "auto", "yolo", "YOLO mode is not active");
3092
- if (tool.name === "write" && subject) {
3093
- const isAgentState = isInsideAgentStateRoot(subject);
3094
- const hasRead = ctx.hasRead(subject) && !isAgentState;
3095
- add(
3096
- "write smart bypass",
3097
- hasRead,
3098
- "auto",
3099
- "context",
3100
- isAgentState ? `file "${subject}" is WrongStack's own state \u2014 bypass never applies, write always confirms` : hasRead ? `file "${subject}" was already read in this session \u2014 auto-approving write` : `file "${subject}" was not read in this session \u2014 bypass does not apply`
3101
- );
3102
- if (hasRead) {
3103
- winnerIndex = steps.length - 1;
3104
- return {
3105
- toolName: tool.name,
3106
- subject,
3107
- steps,
3108
- winnerIndex,
3109
- decision: {
3110
- permission: "auto",
3111
- source: "context",
3112
- reason: "file already read in this session"
3113
- }
3114
- };
3115
- }
3116
- } else {
3117
- add(
3118
- "write smart bypass",
3119
- false,
3120
- "auto",
3121
- "context",
3122
- 'tool is not "write" or has no subject \u2014 bypass does not apply'
3123
- );
3124
- }
3125
- const hasWriteCap = hasCapability(tool, ToolCapabilities.FS_WRITE);
3126
- const hasShellCap = hasCapability(tool, [
3127
- ToolCapabilities.SHELL_ARBITRARY,
3128
- ToolCapabilities.SHELL_RESTRICTED,
3129
- ToolCapabilities.SHELL_EXEC
3130
- ]);
3131
- const hasInstallCap = hasCapability(tool, ToolCapabilities.PACKAGE_INSTALL);
3132
- const hasConfigCap = hasCapability(tool, ToolCapabilities.CONFIG_MUTATE);
3133
- const hasSubagentCap = hasCapability(tool, ToolCapabilities.SUBAGENT_SPAWN);
3134
- const isMutating = tool.mutating || hasWriteCap || hasShellCap || hasInstallCap || hasConfigCap || hasSubagentCap;
3135
- if (tool.permission === "auto" && !isMutating) {
3136
- add(
3137
- "safe default auto",
3138
- true,
3139
- "auto",
3140
- "default",
3141
- `tool "${tool.name}" has auto permission and is not mutating \u2014 safe to auto-approve`
3142
- );
3143
- winnerIndex = steps.length - 1;
3144
- return {
3145
- toolName: tool.name,
3146
- subject,
3147
- steps,
3148
- winnerIndex,
3149
- decision: { permission: "auto", source: "default" }
3150
- };
3151
- }
3152
- add(
3153
- "mutating default confirm",
3154
- true,
3155
- "confirm",
3156
- "default",
3157
- isMutating ? `tool "${tool.name}" is mutating (default auto not enough) \u2014 needs confirmation` : `tool "${tool.name}" has "${tool.permission}" permission \u2014 needs confirmation`
3166
+ return explainPermissionTrace(
3167
+ {
3168
+ policy: this.policy,
3169
+ policyInvalid: this.policyInvalid,
3170
+ wildcardEntries: this.wildcardEntries,
3171
+ sessionDenied: this.sessionDenied,
3172
+ sessionAllowed: this.sessionAllowed,
3173
+ yolo: this.yolo,
3174
+ promptDelegatePresent: this.promptDelegate !== void 0,
3175
+ isSensitiveReadCall: (t, inp) => this.isSensitiveReadCall(t, inp),
3176
+ yoloBlockedAsDestructive: (t, inp, c) => this.yoloBlockedAsDestructive(t, inp, c)
3177
+ },
3178
+ tool,
3179
+ input,
3180
+ ctx
3158
3181
  );
3159
- winnerIndex = steps.length - 1;
3160
- const hasDelegate = this.promptDelegate !== void 0;
3161
- return {
3162
- toolName: tool.name,
3163
- subject,
3164
- steps,
3165
- winnerIndex,
3166
- decision: {
3167
- permission: "confirm",
3168
- source: "default",
3169
- ...hasDelegate ? { reason: "would prompt user via delegate" } : {}
3170
- }
3171
- };
3172
3182
  }
3173
3183
  findNamespaceEntry(toolName) {
3174
3184
  for (const { pattern, value } of this.wildcardEntries) {