@wrongstack/core 0.306.4 → 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 (103) 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 +6011 -5894
  14. package/dist/coordination/mail-tools.d.ts +1 -1
  15. package/dist/coordination/mailbox-http-actor-query.d.ts +16 -0
  16. package/dist/coordination/mailbox-http-router.d.ts +0 -48
  17. package/dist/coordination/mailbox-http-sse.d.ts +12 -0
  18. package/dist/coordination/mailbox-message-types.d.ts +71 -0
  19. package/dist/coordination/mailbox-predicates.d.ts +13 -0
  20. package/dist/coordination/mailbox-project-server.js +1 -1
  21. package/dist/coordination/mailbox-session-sync.d.ts +8 -0
  22. package/dist/coordination/mailbox-types.d.ts +6 -793
  23. package/dist/core/agent-loop-context.d.ts +28 -0
  24. package/dist/core/agent-loop-detector.d.ts +25 -0
  25. package/dist/core/fallback-doctor.d.ts +72 -0
  26. package/dist/core/index.d.ts +1 -0
  27. package/dist/core/index.js +1159 -888
  28. package/dist/defaults/index.js +3579 -3545
  29. package/dist/execution/autonomy-brain-llm.d.ts +55 -0
  30. package/dist/execution/autonomy-brain.d.ts +3 -156
  31. package/dist/execution/compaction-budget.d.ts +38 -0
  32. package/dist/execution/compaction-core.d.ts +3 -170
  33. package/dist/execution/compaction-elision.d.ts +58 -0
  34. package/dist/execution/council-orchestrator-helpers.d.ts +39 -0
  35. package/dist/execution/council-orchestrator.d.ts +3 -42
  36. package/dist/execution/council-response-parser.d.ts +56 -0
  37. package/dist/execution/index.js +2415 -2443
  38. package/dist/execution/prompt-enhancer.js +11 -3
  39. package/dist/execution/tool-executor-guard.d.ts +18 -0
  40. package/dist/execution/tool-executor-runner.d.ts +12 -0
  41. package/dist/execution/tool-executor.d.ts +0 -75
  42. package/dist/goal/index.js +19 -6
  43. package/dist/hq/index.js +8 -2
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.js +7813 -7292
  46. package/dist/infrastructure/index.js +3 -1
  47. package/dist/kernel/events/agent-events.d.ts +2 -0
  48. package/dist/kernel/events/provider-events.d.ts +13 -0
  49. package/dist/plugin/index.js +2590 -2561
  50. package/dist/plugins/auto-review-config.d.ts +53 -0
  51. package/dist/plugins/auto-review-git.d.ts +19 -0
  52. package/dist/plugins/auto-review-plugin.d.ts +2 -140
  53. package/dist/plugins/review-finding-verification.d.ts +7 -0
  54. package/dist/prompts/index.d.ts +1 -0
  55. package/dist/prompts/prompt-journal.d.ts +94 -0
  56. package/dist/security/index.js +627 -652
  57. package/dist/security/permission-explain.d.ts +19 -0
  58. package/dist/security/permission-policy.d.ts +0 -101
  59. package/dist/security/yolo-risk.d.ts +5 -4
  60. package/dist/session-catalog/index.js +220 -212
  61. package/dist/session-catalog/project-server.js +227 -219
  62. package/dist/session-catalog/store-rebuild.d.ts +9 -0
  63. package/dist/session-catalog/store-schema.d.ts +48 -0
  64. package/dist/session-catalog/store.d.ts +0 -17
  65. package/dist/storage/director-state.d.ts +6 -0
  66. package/dist/storage/file-session-writer.d.ts +2 -58
  67. package/dist/storage/index.d.ts +2 -1
  68. package/dist/storage/index.js +2057 -1925
  69. package/dist/storage/orphan-lock-cleaner.d.ts +15 -0
  70. package/dist/storage/session-recovery.d.ts +9 -0
  71. package/dist/storage/session-store/rename-session.d.ts +16 -0
  72. package/dist/storage/session-store/resume-session.d.ts +26 -0
  73. package/dist/storage/session-store/session-store-clear.d.ts +11 -0
  74. package/dist/storage/session-store/session-store-index.d.ts +13 -0
  75. package/dist/storage/session-store.d.ts +1 -129
  76. package/dist/storage/session-summary-tracker.d.ts +39 -0
  77. package/dist/storage/session-write-buffer.d.ts +43 -0
  78. package/dist/storage/session-writer/session-writer-flush.d.ts +4 -0
  79. package/dist/storage/session-writer/session-writer-summary-tracker.d.ts +28 -0
  80. package/dist/tools/fallback-agent-model-assign-tool.d.ts +18 -0
  81. package/dist/tools/fallback-leader-model-set-tool.d.ts +18 -0
  82. package/dist/tools/fallback-manage-tools.d.ts +8 -45
  83. package/dist/tools/fallback-profile-manage-tool.d.ts +16 -0
  84. package/dist/tools/fallback-provider-key-set-tool.d.ts +17 -0
  85. package/dist/tools/fallback-provider-manage-tool.d.ts +26 -0
  86. package/dist/tools/index.js +5070 -5071
  87. package/dist/types/config/root.d.ts +7 -2
  88. package/dist/types/default-config.d.ts +1 -1
  89. package/dist/types/index.d.ts +54 -55
  90. package/dist/types/index.js +953 -932
  91. package/dist/types/runtime-capability-manifest.d.ts +5 -5
  92. package/dist/types/session-markers.d.ts +12 -0
  93. package/dist/types/session-markers.js +19 -0
  94. package/dist/worktree/worktree-manager.d.ts +1 -1
  95. package/instructions/coordination/subagent-baseline.md +2 -2
  96. package/instructions/llm/prompt-enhancer.md +2 -1
  97. package/instructions/sections/tool/common-patterns.md +21 -0
  98. package/instructions/sections/tool/delegation-compact.md +3 -1
  99. package/instructions/sections/tool/delegation-full.md +5 -1
  100. package/instructions/system-lite.md +28 -3
  101. package/instructions/system-pro.md +37 -8
  102. package/instructions/system.md +4 -2
  103. package/package.json +5 -5
@@ -1786,175 +1786,23 @@ function subjectForToolInput(toolName, input, subjectKey) {
1786
1786
  return void 0;
1787
1787
  }
1788
1788
 
1789
- // src/security/permission-policy-schema.ts
1790
- var TRUST_POLICY_SCHEMA_VERSION = 1;
1791
- var TRUST_POLICY_LIMITS = Object.freeze({
1792
- maxTools: 1e3,
1793
- maxPatternsPerRule: 1e3,
1794
- maxToolNameChars: 256,
1795
- maxPatternChars: 4096
1796
- });
1797
- var TRUST_POLICY_JSON_SCHEMA = Object.freeze({
1798
- $schema: "https://json-schema.org/draft/2020-12/schema",
1799
- $id: "https://wrongstack.dev/schemas/trust-policy-v1.json",
1800
- title: "WrongStack Trust Policy",
1801
- type: "object",
1802
- maxProperties: TRUST_POLICY_LIMITS.maxTools,
1803
- propertyNames: {
1804
- minLength: 1,
1805
- maxLength: TRUST_POLICY_LIMITS.maxToolNameChars,
1806
- not: { enum: ["__proto__", "prototype", "constructor"] }
1807
- },
1808
- additionalProperties: {
1809
- type: "object",
1810
- additionalProperties: false,
1811
- properties: {
1812
- allow: {
1813
- type: "array",
1814
- maxItems: TRUST_POLICY_LIMITS.maxPatternsPerRule,
1815
- items: { type: "string", minLength: 1, maxLength: TRUST_POLICY_LIMITS.maxPatternChars }
1816
- },
1817
- deny: {
1818
- type: "array",
1819
- maxItems: TRUST_POLICY_LIMITS.maxPatternsPerRule,
1820
- items: { type: "string", minLength: 1, maxLength: TRUST_POLICY_LIMITS.maxPatternChars }
1821
- },
1822
- auto: { type: "boolean" },
1823
- trustWorkdir: { type: "boolean" },
1824
- denyPrivate: { type: "boolean" }
1825
- }
1826
- }
1827
- });
1828
- var RULE_FIELDS2 = /* @__PURE__ */ new Set(["allow", "deny", "auto", "trustWorkdir", "denyPrivate"]);
1829
- var UNSAFE_PROPERTY_NAMES2 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1830
- function isRecord2(value) {
1831
- return typeof value === "object" && value !== null && !Array.isArray(value);
1832
- }
1833
- function error2(diagnostics, code, path9, message) {
1834
- diagnostics.push({ severity: "error", code, path: path9, message });
1835
- }
1836
- function validatePatterns(value, path9, diagnostics) {
1837
- if (!Array.isArray(value)) {
1838
- error2(diagnostics, "invalid_pattern_list", path9, "must be an array of strings");
1839
- return void 0;
1840
- }
1841
- if (value.length > TRUST_POLICY_LIMITS.maxPatternsPerRule) {
1842
- error2(
1843
- diagnostics,
1844
- "too_many_patterns",
1845
- path9,
1846
- `must contain at most ${TRUST_POLICY_LIMITS.maxPatternsPerRule} patterns`
1847
- );
1848
- return void 0;
1849
- }
1850
- const patterns = [];
1851
- const seen = /* @__PURE__ */ new Set();
1852
- for (const [index, pattern] of value.entries()) {
1853
- const itemPath = `${path9}[${index}]`;
1854
- if (typeof pattern !== "string" || pattern.length === 0 || pattern.length > TRUST_POLICY_LIMITS.maxPatternChars) {
1855
- error2(
1856
- diagnostics,
1857
- "invalid_pattern",
1858
- itemPath,
1859
- `must be a non-empty string up to ${TRUST_POLICY_LIMITS.maxPatternChars} characters`
1860
- );
1861
- continue;
1862
- }
1863
- if (seen.has(pattern)) {
1864
- diagnostics.push({
1865
- severity: "warning",
1866
- code: "duplicate_pattern",
1867
- path: itemPath,
1868
- message: "duplicates an earlier pattern and was normalized away"
1869
- });
1870
- continue;
1871
- }
1872
- seen.add(pattern);
1873
- patterns.push(pattern);
1874
- }
1875
- return patterns;
1876
- }
1877
- function validateTrustPolicy(value) {
1878
- const diagnostics = [];
1879
- if (!isRecord2(value)) {
1880
- return {
1881
- ok: false,
1882
- diagnostics: [
1883
- {
1884
- severity: "error",
1885
- code: "invalid_root",
1886
- path: "$",
1887
- message: "trust policy must be an object"
1888
- }
1889
- ]
1890
- };
1891
- }
1892
- const entries = Object.entries(value);
1893
- if (entries.length > TRUST_POLICY_LIMITS.maxTools) {
1894
- error2(
1895
- diagnostics,
1896
- "too_many_tools",
1897
- "$",
1898
- `must contain at most ${TRUST_POLICY_LIMITS.maxTools} tool rules`
1899
- );
1900
- }
1901
- const policy = {};
1902
- for (const [toolName, rawRule] of entries.slice(0, TRUST_POLICY_LIMITS.maxTools)) {
1903
- const toolPath = `$[${JSON.stringify(toolName)}]`;
1904
- if (toolName.length === 0 || toolName.length > TRUST_POLICY_LIMITS.maxToolNameChars) {
1905
- error2(
1906
- diagnostics,
1907
- "invalid_tool_name",
1908
- toolPath,
1909
- `tool name must be 1-${TRUST_POLICY_LIMITS.maxToolNameChars} characters`
1910
- );
1911
- continue;
1912
- }
1913
- if (UNSAFE_PROPERTY_NAMES2.has(toolName)) {
1914
- error2(
1915
- diagnostics,
1916
- "unsafe_tool_name",
1917
- toolPath,
1918
- "reserved object property names are not valid tool rules"
1919
- );
1920
- continue;
1921
- }
1922
- if (!isRecord2(rawRule)) {
1923
- error2(diagnostics, "invalid_rule", toolPath, "tool rule must be an object");
1924
- continue;
1925
- }
1926
- for (const field of Object.keys(rawRule)) {
1927
- if (!RULE_FIELDS2.has(field)) {
1928
- error2(
1929
- diagnostics,
1930
- "unknown_field",
1931
- `${toolPath}.${field}`,
1932
- `unknown policy field "${field}"`
1933
- );
1934
- }
1935
- }
1936
- const rule = {};
1937
- for (const field of ["allow", "deny"]) {
1938
- if (rawRule[field] === void 0) continue;
1939
- const patterns = validatePatterns(rawRule[field], `${toolPath}.${field}`, diagnostics);
1940
- if (patterns) rule[field] = patterns;
1941
- }
1942
- for (const field of ["auto", "trustWorkdir", "denyPrivate"]) {
1943
- const setting = rawRule[field];
1944
- if (setting === void 0) continue;
1945
- if (typeof setting !== "boolean") {
1946
- error2(diagnostics, "invalid_boolean", `${toolPath}.${field}`, "must be a boolean");
1947
- } else {
1948
- rule[field] = setting;
1949
- }
1950
- }
1951
- policy[toolName] = rule;
1952
- }
1953
- 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");
1954
1800
  }
1955
1801
 
1956
1802
  // src/security/yolo-risk.ts
1957
- 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;
1958
1806
  var CATASTROPHIC_PATTERNS = [
1959
1807
  /\b(?:mkfs(?:\.[a-z0-9]+)?|mke2fs|newfs)\b/i,
1960
1808
  // make a filesystem — wipes a partition
@@ -2014,9 +1862,9 @@ function getInputString(input, key) {
2014
1862
  function pathLooksInsideProject(rawPath, projectRoot) {
2015
1863
  if (!projectRoot) return false;
2016
1864
  if (rawPath === "~" || rawPath.startsWith("~/") || rawPath.startsWith("~\\")) return false;
2017
- const resolved = path3.resolve(projectRoot, rawPath);
2018
- const relative4 = path3.relative(projectRoot, resolved);
2019
- 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);
2020
1868
  }
2021
1869
  function tokenizeShell(command) {
2022
1870
  return command.match(/"[^"]*"|'[^']*'|\S+/g)?.map((token) => token.replace(/^['"]|['"]$/g, "")) ?? [];
@@ -2190,6 +2038,48 @@ function hasCatastrophicDelete(command) {
2190
2038
  }
2191
2039
  return false;
2192
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
+ }
2193
2083
  function isClearlyDestructiveBashCommand(command, projectRoot) {
2194
2084
  const trimmed = command.trim();
2195
2085
  if (!trimmed) return false;
@@ -2198,85 +2088,12 @@ function isClearlyDestructiveBashCommand(command, projectRoot) {
2198
2088
  if (hasExternalPublish(trimmed)) return true;
2199
2089
  if (hasFindExec(trimmed)) return true;
2200
2090
  if (hasCatastrophicDelete(trimmed)) return true;
2091
+ if (hasWriteToAgentStateRoot(trimmed)) return true;
2201
2092
  if (CATASTROPHIC_PATTERNS.some((pattern) => pattern.test(trimmed))) return true;
2202
2093
  if (HIGH_IMPACT_PATTERNS.some((pattern) => pattern.test(trimmed))) return true;
2203
2094
  return false;
2204
2095
  }
2205
2096
 
2206
- // src/security/auto-approve-policy.ts
2207
- var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
2208
- allowedCapabilities;
2209
- constructor(allowedCapabilities) {
2210
- this.allowedCapabilities = allowedCapabilities ?? [
2211
- ToolCapabilities.FS_READ,
2212
- ToolCapabilities.NET_OUTBOUND
2213
- ];
2214
- }
2215
- static isMcpTool(name) {
2216
- return name.startsWith("mcp__");
2217
- }
2218
- async evaluate(tool) {
2219
- const caps = tool.capabilities ?? [];
2220
- const hasAllowedCap = caps.some((c) => this.allowedCapabilities.includes(c));
2221
- const isMcp = _AutoApprovePermissionPolicy.isMcpTool(tool.name);
2222
- const mcpProxyAllowed = this.allowedCapabilities.includes(ToolCapabilities.MCP_PROXY);
2223
- const dangerousNotAllowed = getDangerousCapabilities(tool).filter(
2224
- (c) => !this.allowedCapabilities.includes(c)
2225
- );
2226
- const blocked = tool.permission === "deny" || isMcp && !mcpProxyAllowed || !hasAllowedCap || dangerousNotAllowed.length > 0;
2227
- if (blocked) {
2228
- 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(", ")})`;
2229
- return {
2230
- permission: "deny",
2231
- source: "subagent_guard",
2232
- reason
2233
- };
2234
- }
2235
- return { permission: "auto", source: "yolo" };
2236
- }
2237
- async trust() {
2238
- }
2239
- async deny() {
2240
- }
2241
- denyOnce() {
2242
- }
2243
- allowOnce() {
2244
- }
2245
- async explain(tool) {
2246
- const decision = await this.evaluate(tool);
2247
- return {
2248
- toolName: tool.name,
2249
- subject: null,
2250
- steps: [
2251
- {
2252
- rule: "subagent auto",
2253
- matched: decision.permission === "auto",
2254
- decision: decision.permission,
2255
- source: decision.source,
2256
- detail: decision.reason ?? `subagent policy: ${decision.permission}`
2257
- }
2258
- ],
2259
- winnerIndex: 0,
2260
- decision
2261
- };
2262
- }
2263
- async reload() {
2264
- }
2265
- };
2266
-
2267
- // src/security/permission-helpers.ts
2268
- import { realpathSync } from "node:fs";
2269
- import * as path5 from "node:path";
2270
-
2271
- // src/utils/wstack-paths.ts
2272
- import * as os from "node:os";
2273
- import * as path4 from "node:path";
2274
- function wstackGlobalRoot() {
2275
- const fromEnv = process.env["WRONGSTACK_HOME"];
2276
- if (fromEnv && fromEnv.trim().length > 0) return path4.resolve(fromEnv);
2277
- return path4.join(os.homedir(), ".wrongstack");
2278
- }
2279
-
2280
2097
  // src/security/permission-helpers.ts
2281
2098
  function matchesTrust(patterns, subject) {
2282
2099
  return patterns.includes(subject) || matchAny(patterns, subject);
@@ -2412,8 +2229,558 @@ function shellCommandReadsSensitivePath(command) {
2412
2229
  const rest = tokens.slice(i + 1);
2413
2230
  if (rest.some((arg) => pathLooksSensitive(arg))) return true;
2414
2231
  }
2415
- return false;
2416
- }
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
+ };
2417
2784
 
2418
2785
  // src/security/permission-policy.ts
2419
2786
  function fsWriteTargetPaths(input) {
@@ -2449,70 +2816,13 @@ var DefaultPermissionPolicy = class {
2449
2816
  loaded = false;
2450
2817
  trustFile;
2451
2818
  yolo;
2452
- /**
2453
- * Session-scoped "soft deny" map. When the user presses 'n' (block once),
2454
- * the tool+pattern is added here. If the LLM retries in the same session,
2455
- * we return deny directly without asking again.
2456
- *
2457
- * Cleared on reload() since reload = fresh trust file snapshot.
2458
- */
2459
2819
  sessionDenied = /* @__PURE__ */ new Map();
2460
- /**
2461
- * Session-scoped one-shot "soft trust" map. When the user presses 'y', the
2462
- * tool+pattern is added here so the immediate confirm re-run can proceed.
2463
- * The entry is consumed on first use; future calls must ask again unless the
2464
- * user chose persistent trust.
2465
- *
2466
- * Cleared on reload().
2467
- */
2468
2820
  sessionAllowed = /* @__PURE__ */ new Map();
2469
- /**
2470
- * Interactive prompt delegate. When set, `evaluate()` calls it to get a
2471
- * user decision synchronously (CLI REPL path). When cleared (TUI / WebUI),
2472
- * `evaluate()` returns `confirm` so the caller can emit
2473
- * `tool.confirm_needed` for the UI layer to handle.
2474
- *
2475
- * Mutable so the host can switch from CLI-prompt to event-driven
2476
- * confirmation at runtime (e.g. when `--goal` forces TUI mode after
2477
- * the agent was already constructed).
2478
- */
2479
2821
  promptDelegate;
2480
- /** Pre-compiled wildcard patterns — rebuilt on reload for O(1) lookup. */
2481
2822
  wildcardEntries = [];
2482
- /**
2483
- * Evaluate-result cache. Keyed by `tool.name::subject::permissionFingerprint`
2484
- * so repeated calls with the same tool+input skip namespace matching,
2485
- * subject computation, and pattern matching (matchAny). The fingerprint
2486
- * suffix covers the `Tool` fields `evaluate()` reads (`permission`,
2487
- * `riskTier`, `mutating`, `capabilities`), so a tool redefined via
2488
- * `ToolRegistry.wrap()` misses the cache instead of inheriting a stale
2489
- * verdict.
2490
- *
2491
- * Cleared on any state change (reload, trust, deny, yolo toggle) because
2492
- * the result depends on the full policy state. The write-tool smart-bypass
2493
- * (step 7 in `evaluate()`) is not cached since `ctx.hasRead()` changes
2494
- * dynamically within a session.
2495
- *
2496
- * P3 #17 (before-release.md): capped at 500 entries via LRU eviction. An
2497
- * iteration that evaluates thousands of unique tool+subject combinations
2498
- * (e.g. repeated `tool-help` or `bash` with different commands) can grow
2499
- * the cache without bound. 500 is far more than any single iteration needs;
2500
- * the LRU evicts the least-recently-used entries first.
2501
- */
2502
2823
  _evalCache = new LruCache(500);
2503
2824
  policyDiagnostics = [];
2504
2825
  policyInvalid = false;
2505
- /**
2506
- * When true, YOLO also auto-approves clearly destructive shell commands.
2507
- *
2508
- * WS-008: `isClearlyDestructiveBashCommand` was exported and heavily tested
2509
- * but called from zero production modules, so YOLO returned `auto` for
2510
- * everything — including `rm -rf`, git history rewrites, and external
2511
- * publishes. The `'yolo_destructive'` decision source, the getter/setter on
2512
- * PermissionPolicy, and the TUI branch that reads it all existed; only the
2513
- * gate itself was missing. Defaults to false: destructive commands still ask,
2514
- * even in YOLO.
2515
- */
2516
2826
  yoloDestructive;
2517
2827
  constructor(opts) {
2518
2828
  this.trustFile = opts.trustFile;
@@ -2520,30 +2830,13 @@ var DefaultPermissionPolicy = class {
2520
2830
  this.yoloDestructive = opts.yoloDestructive ?? false;
2521
2831
  this.promptDelegate = opts.promptDelegate;
2522
2832
  }
2523
- /** Allow YOLO to cover destructive shell commands too (off by default). */
2524
2833
  setYoloDestructive(enabled) {
2525
2834
  if (this.yoloDestructive !== enabled) this._evalCache.clear();
2526
2835
  this.yoloDestructive = enabled;
2527
2836
  }
2528
- /** Whether YOLO currently extends to destructive shell commands. */
2529
2837
  getYoloDestructive() {
2530
2838
  return this.yoloDestructive;
2531
2839
  }
2532
- /**
2533
- * True when this call is an FS_WRITE whose target lands inside WrongStack's
2534
- * own state root.
2535
- *
2536
- * FS_WRITE tools disagree on where the target path lives: write/edit use
2537
- * `path`, replace/format/git `files` (string OR array), patch `directory`,
2538
- * design `out`, scaffold writes under `cwd`/`template`. Checking only
2539
- * `path`/`file_path` let a state-root write merely switch tools, so every
2540
- * path-bearing key is inspected.
2541
- *
2542
- * Shared by the YOLO carve-out and the eval-cache guard so the two cannot
2543
- * drift: the carve-out scans all path keys while the cache key fingerprints
2544
- * only the subject key, so the cache must consult the same predicate or a
2545
- * secondary state-root key can be replayed a cached `auto`.
2546
- */
2547
2840
  hasAgentStateWriteTarget(tool, input, ctx) {
2548
2841
  if (!hasCapability(tool, ToolCapabilities.FS_WRITE)) return false;
2549
2842
  for (const targetPath of fsWriteTargetPaths(input)) {
@@ -2553,11 +2846,6 @@ var DefaultPermissionPolicy = class {
2553
2846
  }
2554
2847
  return false;
2555
2848
  }
2556
- /**
2557
- * True when YOLO is on but this specific call is a destructive shell command
2558
- * the user has not opted to auto-approve. Shared by `evaluate()` and the
2559
- * explain/trace path so the two cannot drift.
2560
- */
2561
2849
  yoloBlockedAsDestructive(tool, input, ctx) {
2562
2850
  if (!this.yolo || this.yoloDestructive) return false;
2563
2851
  if (this.hasAgentStateWriteTarget(tool, input, ctx)) return true;
@@ -2567,25 +2855,16 @@ var DefaultPermissionPolicy = class {
2567
2855
  if (!command) return false;
2568
2856
  return isClearlyDestructiveBashCommand(command, ctx.projectRoot);
2569
2857
  }
2570
- /**
2571
- * Replace (or clear) the interactive prompt delegate at runtime.
2572
- * Used by the CLI to switch from inline prompts (REPL) to event-driven
2573
- * confirmation (TUI) when the run mode is determined after the policy
2574
- * was constructed (e.g. `--goal` auto-flipping to TUI).
2575
- */
2576
2858
  setPromptDelegate(delegate) {
2577
2859
  this.promptDelegate = delegate;
2578
2860
  }
2579
- /** Toggle YOLO (auto-approve) mode at runtime. */
2580
2861
  setYolo(enabled) {
2581
2862
  if (this.yolo !== enabled) this._evalCache.clear();
2582
2863
  this.yolo = enabled;
2583
2864
  }
2584
- /** Check whether YOLO mode is currently active. */
2585
2865
  getYolo() {
2586
2866
  return this.yolo;
2587
2867
  }
2588
- /** Read-only diagnostics for policy inspector/editor surfaces. */
2589
2868
  getPolicyDiagnostics() {
2590
2869
  return this.policyDiagnostics.map((diagnostic) => ({ ...diagnostic }));
2591
2870
  }
@@ -2854,7 +3133,6 @@ var DefaultPermissionPolicy = class {
2854
3133
  throw err;
2855
3134
  }
2856
3135
  }
2857
- /** Persist a deny rule — this tool+pattern pair is permanently blocked. */
2858
3136
  async deny(rule) {
2859
3137
  if (!this.loaded) await this.reload();
2860
3138
  if (this.policyInvalid) {
@@ -2875,335 +3153,32 @@ var DefaultPermissionPolicy = class {
2875
3153
  throw err;
2876
3154
  }
2877
3155
  }
2878
- /** Block this tool+pattern for the rest of this session (no trust file). */
2879
3156
  denyOnce(rule) {
2880
3157
  this.sessionDenied.set(`${rule.tool}::${rule.pattern}`, true);
2881
3158
  this._evalCache.clear();
2882
3159
  }
2883
- /** Auto-approve this tool+pattern once (no trust file). */
2884
3160
  allowOnce(rule) {
2885
3161
  this.sessionAllowed.set(`${rule.tool}::${rule.pattern}`, true);
2886
3162
  this._evalCache.clear();
2887
3163
  }
2888
- /**
2889
- * Side-effect-free permission evaluation trace. Mirrors `evaluate()`
2890
- * step-by-step without prompting the user, writing to trust files, or
2891
- * mutating session state. Returns the full ordered trace and winner.
2892
- */
2893
3164
  async explain(tool, input, ctx) {
2894
3165
  if (!this.loaded) await this.reload();
2895
- const subject = subjectForToolInput(tool.name, input, tool.subjectKey);
2896
- const steps = [];
2897
- let winnerIndex = -1;
2898
- const add = (rule, matched, decision, source, detail) => {
2899
- steps.push({ rule, matched, decision, source, detail });
2900
- };
2901
- if (this.policyInvalid) {
2902
- add("policy invalid", true, "deny", "deny", "trust policy is invalid; all tools are denied");
2903
- winnerIndex = 0;
2904
- return {
2905
- toolName: tool.name,
2906
- subject,
2907
- steps,
2908
- winnerIndex,
2909
- decision: { permission: "deny", source: "deny", reason: "trust policy is invalid" }
2910
- };
2911
- }
2912
- add("policy valid", false, "auto", "default", "trust policy loaded successfully");
2913
- const namespaceEntry = this.findNamespaceEntry(tool.name);
2914
- const entry = this.policy[tool.name] ?? namespaceEntry;
2915
- const namespaceSource = namespaceEntry ? `wildcard entry matched (${Object.keys(this.policy).find((k) => k.includes("*") && matchGlob(k, tool.name))})` : "no namespace match";
2916
- const cacheKey = `${tool.name}::${subject ?? tool.name}`;
2917
- if (this.sessionDenied.has(cacheKey)) {
2918
- add(
2919
- "session soft deny",
2920
- true,
2921
- "deny",
2922
- "deny",
2923
- 'user pressed "no" earlier in this session \u2014 blocked until reload'
2924
- );
2925
- winnerIndex = steps.length - 1;
2926
- return {
2927
- toolName: tool.name,
2928
- subject,
2929
- steps,
2930
- winnerIndex,
2931
- decision: {
2932
- permission: "deny",
2933
- source: "deny",
2934
- reason: "session soft deny (user pressed no)"
2935
- }
2936
- };
2937
- }
2938
- add(
2939
- "session soft deny",
2940
- false,
2941
- "deny",
2942
- "deny",
2943
- "no session-level soft deny for this tool+subject"
2944
- );
2945
- if (this.sessionAllowed.has(cacheKey)) {
2946
- add(
2947
- "session soft allow",
2948
- true,
2949
- "auto",
2950
- "trust",
2951
- 'user pressed "yes" in current session \u2014 one-shot auto-approve (consumed)'
2952
- );
2953
- winnerIndex = steps.length - 1;
2954
- return {
2955
- toolName: tool.name,
2956
- subject,
2957
- steps,
2958
- winnerIndex,
2959
- decision: {
2960
- permission: "auto",
2961
- source: "trust",
2962
- reason: "session one-shot allow (user pressed yes)"
2963
- }
2964
- };
2965
- }
2966
- add(
2967
- "session soft allow",
2968
- false,
2969
- "auto",
2970
- "trust",
2971
- "no session-level one-shot allow for this tool+subject"
2972
- );
2973
- if (entry?.deny && subject && matchesTrust(entry.deny, subject)) {
2974
- add(
2975
- "trust deny",
2976
- true,
2977
- "deny",
2978
- "deny",
2979
- `subject "${subject}" matched a deny pattern in trust file`
2980
- );
2981
- winnerIndex = steps.length - 1;
2982
- return {
2983
- toolName: tool.name,
2984
- subject,
2985
- steps,
2986
- winnerIndex,
2987
- decision: { permission: "deny", source: "deny", reason: "matched deny pattern" }
2988
- };
2989
- }
2990
- add(
2991
- "trust deny",
2992
- false,
2993
- "deny",
2994
- "deny",
2995
- `no deny pattern matched (namespace: ${namespaceSource})`
2996
- );
2997
- if (tool.permission === "deny") {
2998
- add(
2999
- "tool default deny",
3000
- true,
3001
- "deny",
3002
- "default",
3003
- `tool "${tool.name}" has permission: deny by default`
3004
- );
3005
- winnerIndex = steps.length - 1;
3006
- return {
3007
- toolName: tool.name,
3008
- subject,
3009
- steps,
3010
- winnerIndex,
3011
- decision: { permission: "deny", source: "default", reason: "tool default deny" }
3012
- };
3013
- }
3014
- add(
3015
- "tool default deny",
3016
- false,
3017
- "deny",
3018
- "default",
3019
- `tool "${tool.name}" has permission: "${tool.permission}"`
3020
- );
3021
- if (entry?.allow && subject && matchesTrust(entry.allow, subject)) {
3022
- add(
3023
- "trust allow",
3024
- true,
3025
- "auto",
3026
- "trust",
3027
- `subject "${subject}" matched an allow pattern in trust file`
3028
- );
3029
- winnerIndex = steps.length - 1;
3030
- return {
3031
- toolName: tool.name,
3032
- subject,
3033
- steps,
3034
- winnerIndex,
3035
- decision: { permission: "auto", source: "trust", reason: "matched allow pattern" }
3036
- };
3037
- }
3038
- add(
3039
- "trust allow",
3040
- false,
3041
- "auto",
3042
- "trust",
3043
- `no allow pattern matched (namespace: ${namespaceSource})`
3044
- );
3045
- if (entry?.auto) {
3046
- add("trust auto", true, "auto", "trust", `trust file has auto: true for "${tool.name}"`);
3047
- winnerIndex = steps.length - 1;
3048
- return {
3049
- toolName: tool.name,
3050
- subject,
3051
- steps,
3052
- winnerIndex,
3053
- decision: { permission: "auto", source: "trust", reason: "trust auto" }
3054
- };
3055
- }
3056
- add("trust auto", false, "auto", "trust", `no auto flag for "${tool.name}" in trust file`);
3057
- if (!this.yolo && this.isSensitiveReadCall(tool, input)) {
3058
- const hasDelegate2 = this.promptDelegate !== void 0;
3059
- add(
3060
- "sensitive read",
3061
- true,
3062
- hasDelegate2 ? "confirm" : "confirm",
3063
- "default",
3064
- hasDelegate2 ? "sensitive file read detected \u2014 would prompt user for approval" : "sensitive file read detected \u2014 returns confirm (no prompt delegate)"
3065
- );
3066
- winnerIndex = steps.length - 1;
3067
- return {
3068
- toolName: tool.name,
3069
- subject,
3070
- steps,
3071
- winnerIndex,
3072
- decision: {
3073
- permission: "confirm",
3074
- source: "default",
3075
- riskTier: "standard",
3076
- reason: "sensitive file read needs explicit approval"
3077
- }
3078
- };
3079
- }
3080
- add(
3081
- "sensitive read",
3082
- false,
3083
- "confirm",
3084
- "default",
3085
- "not a sensitive read call (or YOLO bypasses this check)"
3086
- );
3087
- if (this.yolo) {
3088
- if (this.yoloBlockedAsDestructive(tool, input, ctx)) {
3089
- add(
3090
- "yolo destructive gate",
3091
- true,
3092
- "confirm",
3093
- "yolo_destructive",
3094
- "YOLO is active but this is a clearly destructive command \u2014 still asking"
3095
- );
3096
- winnerIndex = steps.length - 1;
3097
- return {
3098
- toolName: tool.name,
3099
- subject,
3100
- steps,
3101
- winnerIndex,
3102
- decision: {
3103
- permission: "confirm",
3104
- source: "yolo_destructive",
3105
- riskTier: "destructive",
3106
- reason: "destructive command needs explicit approval even in YOLO mode"
3107
- }
3108
- };
3109
- }
3110
- add(
3111
- "yolo",
3112
- true,
3113
- "auto",
3114
- "yolo",
3115
- "YOLO mode is active \u2014 auto-approving every non-denied call"
3116
- );
3117
- winnerIndex = steps.length - 1;
3118
- return {
3119
- toolName: tool.name,
3120
- subject,
3121
- steps,
3122
- winnerIndex,
3123
- decision: { permission: "auto", source: "yolo" }
3124
- };
3125
- }
3126
- add("yolo", false, "auto", "yolo", "YOLO mode is not active");
3127
- if (tool.name === "write" && subject) {
3128
- const isAgentState = isInsideAgentStateRoot(subject);
3129
- const hasRead = ctx.hasRead(subject) && !isAgentState;
3130
- add(
3131
- "write smart bypass",
3132
- hasRead,
3133
- "auto",
3134
- "context",
3135
- 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`
3136
- );
3137
- if (hasRead) {
3138
- winnerIndex = steps.length - 1;
3139
- return {
3140
- toolName: tool.name,
3141
- subject,
3142
- steps,
3143
- winnerIndex,
3144
- decision: {
3145
- permission: "auto",
3146
- source: "context",
3147
- reason: "file already read in this session"
3148
- }
3149
- };
3150
- }
3151
- } else {
3152
- add(
3153
- "write smart bypass",
3154
- false,
3155
- "auto",
3156
- "context",
3157
- 'tool is not "write" or has no subject \u2014 bypass does not apply'
3158
- );
3159
- }
3160
- const hasWriteCap = hasCapability(tool, ToolCapabilities.FS_WRITE);
3161
- const hasShellCap = hasCapability(tool, [
3162
- ToolCapabilities.SHELL_ARBITRARY,
3163
- ToolCapabilities.SHELL_RESTRICTED,
3164
- ToolCapabilities.SHELL_EXEC
3165
- ]);
3166
- const hasInstallCap = hasCapability(tool, ToolCapabilities.PACKAGE_INSTALL);
3167
- const hasConfigCap = hasCapability(tool, ToolCapabilities.CONFIG_MUTATE);
3168
- const hasSubagentCap = hasCapability(tool, ToolCapabilities.SUBAGENT_SPAWN);
3169
- const isMutating = tool.mutating || hasWriteCap || hasShellCap || hasInstallCap || hasConfigCap || hasSubagentCap;
3170
- if (tool.permission === "auto" && !isMutating) {
3171
- add(
3172
- "safe default auto",
3173
- true,
3174
- "auto",
3175
- "default",
3176
- `tool "${tool.name}" has auto permission and is not mutating \u2014 safe to auto-approve`
3177
- );
3178
- winnerIndex = steps.length - 1;
3179
- return {
3180
- toolName: tool.name,
3181
- subject,
3182
- steps,
3183
- winnerIndex,
3184
- decision: { permission: "auto", source: "default" }
3185
- };
3186
- }
3187
- add(
3188
- "mutating default confirm",
3189
- true,
3190
- "confirm",
3191
- "default",
3192
- 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
3193
3181
  );
3194
- winnerIndex = steps.length - 1;
3195
- const hasDelegate = this.promptDelegate !== void 0;
3196
- return {
3197
- toolName: tool.name,
3198
- subject,
3199
- steps,
3200
- winnerIndex,
3201
- decision: {
3202
- permission: "confirm",
3203
- source: "default",
3204
- ...hasDelegate ? { reason: "would prompt user via delegate" } : {}
3205
- }
3206
- };
3207
3182
  }
3208
3183
  findNamespaceEntry(toolName) {
3209
3184
  for (const { pattern, value } of this.wildcardEntries) {