@wrongstack/core 0.310.1 → 0.313.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/dist/chronicle/index.js +84 -6391
  2. package/dist/chronicle/project-server.js +84 -4868
  3. package/dist/chunk-2BUUIMQB.js +225 -0
  4. package/dist/chunk-2FK4WTFF.js +207 -0
  5. package/dist/chunk-2FQL4PDC.js +292 -0
  6. package/dist/chunk-2KKODIQU.js +2256 -0
  7. package/dist/chunk-2YU4WVBW.js +134 -0
  8. package/dist/chunk-3E244UDA.js +356 -0
  9. package/dist/chunk-3XPVB7BA.js +135 -0
  10. package/dist/chunk-4LJJYBAU.js +388 -0
  11. package/dist/chunk-4VWO3ZXL.js +342 -0
  12. package/dist/chunk-4XV6IR37.js +187 -0
  13. package/dist/chunk-4ZHTKJHM.js +183 -0
  14. package/dist/chunk-52RTVH4M.js +361 -0
  15. package/dist/chunk-57GEWK5X.js +26 -0
  16. package/dist/chunk-5DSMEVA4.js +931 -0
  17. package/dist/chunk-5RP3QTRL.js +1223 -0
  18. package/dist/chunk-5S437PRC.js +50 -0
  19. package/dist/chunk-5XDUGTKN.js +511 -0
  20. package/dist/chunk-62V2AFVG.js +422 -0
  21. package/dist/chunk-65RDIOR2.js +2470 -0
  22. package/dist/chunk-6AKOTGLM.js +597 -0
  23. package/dist/chunk-7ZHZE5EO.js +28 -0
  24. package/dist/chunk-A455ATZB.js +140 -0
  25. package/dist/chunk-AD7ZFZCS.js +46 -0
  26. package/dist/chunk-AI4LLQ2U.js +856 -0
  27. package/dist/chunk-ASX6AW36.js +789 -0
  28. package/dist/chunk-AT4V4XGI.js +3522 -0
  29. package/dist/chunk-AWNQ6ED6.js +311 -0
  30. package/dist/chunk-B4AMLZPB.js +9 -0
  31. package/dist/chunk-BAKVJLEV.js +417 -0
  32. package/dist/chunk-BJHVULHO.js +55 -0
  33. package/dist/chunk-CB2HFKTP.js +9 -0
  34. package/dist/chunk-CE4ZOCOI.js +1640 -0
  35. package/dist/chunk-CQQL4ZOQ.js +151 -0
  36. package/dist/chunk-DEIQMBD2.js +65 -0
  37. package/dist/chunk-E6I5RXVK.js +58 -0
  38. package/dist/chunk-EFEHDQGY.js +1589 -0
  39. package/dist/chunk-EODDVJBM.js +82 -0
  40. package/dist/chunk-EPC3GZD4.js +308 -0
  41. package/dist/chunk-FAYRX2AD.js +13 -0
  42. package/dist/chunk-FFSX2QDQ.js +362 -0
  43. package/dist/chunk-FHAMQS7Q.js +5856 -0
  44. package/dist/chunk-FIV4IJL7.js +2263 -0
  45. package/dist/chunk-FNXVT7HF.js +52 -0
  46. package/dist/chunk-FSWCWSTS.js +395 -0
  47. package/dist/chunk-FW5LDDMJ.js +16464 -0
  48. package/dist/chunk-FXQLIX3K.js +211 -0
  49. package/dist/chunk-G5AAKEEZ.js +228 -0
  50. package/dist/chunk-G5S3S5ZN.js +38 -0
  51. package/dist/chunk-GAZEWQEZ.js +231 -0
  52. package/dist/chunk-GFJOMSHG.js +125 -0
  53. package/dist/chunk-GFXZTHNI.js +158 -0
  54. package/dist/chunk-GM54GJBU.js +960 -0
  55. package/dist/chunk-GPQ3NJPR.js +900 -0
  56. package/dist/chunk-GTCU6DZ3.js +1 -0
  57. package/dist/chunk-H5E77KUJ.js +15 -0
  58. package/dist/chunk-H72H4XDU.js +1023 -0
  59. package/dist/chunk-HA4ZYA6T.js +178 -0
  60. package/dist/chunk-I45KAW2N.js +5020 -0
  61. package/dist/chunk-IQ35HMQL.js +1396 -0
  62. package/dist/chunk-IYFBFX34.js +86 -0
  63. package/dist/chunk-JJPMWPB6.js +109 -0
  64. package/dist/chunk-JTHKOYWU.js +663 -0
  65. package/dist/chunk-JVGPR3TS.js +365 -0
  66. package/dist/chunk-JVKB2TBL.js +48 -0
  67. package/dist/chunk-JX7VBZDK.js +31 -0
  68. package/dist/chunk-K3G6JBUC.js +5232 -0
  69. package/dist/chunk-K4YUNKU7.js +589 -0
  70. package/dist/chunk-KBZGE6LN.js +710 -0
  71. package/dist/chunk-KDQJCT2J.js +45 -0
  72. package/dist/chunk-KI4L2DUL.js +560 -0
  73. package/dist/chunk-KJ2ZTQSL.js +47 -0
  74. package/dist/chunk-KSJ5UTP7.js +154 -0
  75. package/dist/chunk-LHOA5DWA.js +28 -0
  76. package/dist/chunk-LX5QWLBL.js +27 -0
  77. package/dist/chunk-LZDDLNCZ.js +279 -0
  78. package/dist/chunk-M2WF6AXG.js +115 -0
  79. package/dist/chunk-M7BS2K6U.js +1 -0
  80. package/dist/chunk-NDBIPFNY.js +102 -0
  81. package/dist/chunk-NEMIWEXC.js +1199 -0
  82. package/dist/chunk-NFAYHPRH.js +323 -0
  83. package/dist/chunk-O37BQ65S.js +21 -0
  84. package/dist/chunk-O74MJHXU.js +187 -0
  85. package/dist/chunk-OJZEX73F.js +112 -0
  86. package/dist/chunk-PAAWLLZX.js +74 -0
  87. package/dist/chunk-PFXFQQRT.js +339 -0
  88. package/dist/chunk-PFZO3C6H.js +14 -0
  89. package/dist/chunk-PHH7LD45.js +113 -0
  90. package/dist/chunk-QRTG4XTI.js +188 -0
  91. package/dist/chunk-QVOVMXJR.js +1351 -0
  92. package/dist/chunk-RCQYT2I4.js +757 -0
  93. package/dist/chunk-RD6DEE36.js +1178 -0
  94. package/dist/chunk-RJIZEJ42.js +30 -0
  95. package/dist/chunk-RPOA7IAE.js +150 -0
  96. package/dist/chunk-SN2DQRL6.js +1172 -0
  97. package/dist/chunk-SQJVFEBV.js +41 -0
  98. package/dist/chunk-SQP64RCS.js +5391 -0
  99. package/dist/chunk-SVT4RNTR.js +49 -0
  100. package/dist/chunk-SWUFN7XM.js +43 -0
  101. package/dist/chunk-SXMVYC5H.js +174 -0
  102. package/dist/chunk-SZ6AYUIZ.js +23 -0
  103. package/dist/chunk-T3KGZBU5.js +3171 -0
  104. package/dist/chunk-TBRL2RCC.js +9 -0
  105. package/dist/chunk-TRT2HRWZ.js +18 -0
  106. package/dist/chunk-TYW2GI5I.js +1077 -0
  107. package/dist/chunk-UIWONWVG.js +4959 -0
  108. package/dist/chunk-VB3H5WWB.js +151 -0
  109. package/dist/chunk-W4A6QA3D.js +258 -0
  110. package/dist/chunk-WBPVKEEE.js +227 -0
  111. package/dist/chunk-WEBMSQP2.js +123 -0
  112. package/dist/chunk-WOEZV3Q4.js +333 -0
  113. package/dist/chunk-WQQON3SV.js +473 -0
  114. package/dist/chunk-WQTOVMGV.js +119 -0
  115. package/dist/chunk-WZO23AWO.js +2779 -0
  116. package/dist/chunk-XBDP4QPW.js +7 -0
  117. package/dist/chunk-XEBHSJLT.js +103 -0
  118. package/dist/chunk-XQ2ECNK3.js +1 -0
  119. package/dist/chunk-XQ2XGIBF.js +2086 -0
  120. package/dist/chunk-Y2GE5TSI.js +355 -0
  121. package/dist/chunk-YBUJCZ57.js +351 -0
  122. package/dist/chunk-YNCVSFGF.js +149 -0
  123. package/dist/chunk-YUPMXOGP.js +282 -0
  124. package/dist/chunk-YY6APYUM.js +49 -0
  125. package/dist/chunk-Z2IJUO45.js +691 -0
  126. package/dist/chunk-ZEHUCJHY.js +129 -0
  127. package/dist/chunk-ZYU2YOJ6.js +1496 -0
  128. package/dist/chunk-ZZ4V3WIZ.js +163 -0
  129. package/dist/coordination/agents/index.js +145 -6260
  130. package/dist/coordination/index.js +365 -34323
  131. package/dist/coordination/mailbox-project-server.js +48 -3220
  132. package/dist/coordination/mailbox-types.d.ts +13 -1
  133. package/dist/coordination/provider-status-tracker.d.ts +4 -0
  134. package/dist/coordination/sqlite-mailbox.d.ts +2 -1
  135. package/dist/core/agent.d.ts +14 -1
  136. package/dist/core/index.js +267 -12479
  137. package/dist/core/model-ref.js +5 -23
  138. package/dist/core/provider-runner.d.ts +36 -1
  139. package/dist/design/index.js +50 -1087
  140. package/dist/execution/index.d.ts +1 -1
  141. package/dist/execution/index.js +1599 -21887
  142. package/dist/execution/prompt-enhancer.js +16 -535
  143. package/dist/execution/retry-policy.d.ts +31 -1
  144. package/dist/extension/index.js +7 -311
  145. package/dist/goal/index.js +33 -3435
  146. package/dist/hooks/index.js +14 -1138
  147. package/dist/hq/index.js +112 -5139
  148. package/dist/hq/protocol.js +21 -668
  149. package/dist/index.js +1410 -92655
  150. package/dist/infrastructure/index.js +64 -4811
  151. package/dist/kernel/events/provider-events.d.ts +15 -0
  152. package/dist/kernel/events/wrongtrace-events.d.ts +26 -0
  153. package/dist/kernel/events.d.ts +16 -7
  154. package/dist/kernel/index.js +14 -992
  155. package/dist/models/index.js +40 -1117
  156. package/dist/notifications/index.js +3 -108
  157. package/dist/observability/index.js +17 -279
  158. package/dist/plugin/index.js +584 -14835
  159. package/dist/prompts/index.js +14 -948
  160. package/dist/registry/index.js +9 -822
  161. package/dist/registry-R23UBE7H.js +14 -0
  162. package/dist/replay/index.js +6 -84
  163. package/dist/review-finding-store-DKAV3AS6.js +21 -0
  164. package/dist/review-report-store-OWY3EC3N.js +19 -0
  165. package/dist/security/index.js +114 -2202
  166. package/dist/session-catalog/index.js +36 -2103
  167. package/dist/session-catalog/project-server.js +42 -1435
  168. package/dist/skills/index.js +35 -1578
  169. package/dist/storage/index.js +182 -15600
  170. package/dist/tasking/index.js +10 -676
  171. package/dist/tools/index.js +52 -9589
  172. package/dist/types/config/autonomy.d.ts +5 -0
  173. package/dist/types/config/tools.d.ts +2 -2
  174. package/dist/types/config/ui.d.ts +1 -1
  175. package/dist/types/errors.d.ts +18 -0
  176. package/dist/types/index.js +121 -1341
  177. package/dist/types/provider-runner.d.ts +8 -0
  178. package/dist/types/session-markers.js +8 -144
  179. package/dist/utils/error.js +3 -4
  180. package/dist/utils/expect-defined.js +3 -9
  181. package/dist/utils/heap-watchdog.js +6 -706
  182. package/dist/utils/index.js +269 -5685
  183. package/dist/utils/sage-output-block.js +5 -42
  184. package/dist/wiring/proxy-rewrite.d.ts +92 -4
  185. package/dist/wiring/proxy-rewrite.js +20 -61
  186. package/dist/worktree/index.js +6 -887
  187. package/package.json +4 -4
@@ -1,308 +1,77 @@
1
- // src/types/secret-vault.ts
2
- var ENCRYPTED_PREFIX_PATTERN = /^enc:v(\d+):/;
3
- function encryptedPrefixForVersion(version) {
4
- return `enc:v${version}:`;
5
- }
6
- var noOpVault = {
7
- encrypt: (v) => v,
8
- decrypt: (v) => v,
9
- isEncrypted: () => false,
10
- keyVersion: 1
11
- };
12
-
13
- // src/security/capabilities.ts
14
- var ToolCapabilities = {
15
- /** Can execute arbitrary commands in the user's shell (the `bash` tool). */
16
- SHELL_ARBITRARY: "shell.arbitrary",
17
- /**
18
- * Can execute the `exec` tool's allowlisted commands.
19
- *
20
- * "Restricted" describes the command NAME check, not the resulting power.
21
- * Treat this as equivalent to {@link SHELL_ARBITRARY} when deciding what to
22
- * grant: the allowlist includes general-purpose interpreters (`node`,
23
- * `python`, `perl`, `ruby`) and, on Windows, the platform shells (`cmd`,
24
- * `powershell`, `pwsh`) — deliberately, since without them `exec` cannot run
25
- * `dir`/`type`/any cmdlet. `node -e …` or `cmd /c …` therefore reaches
26
- * arbitrary execution, and per-argument denylisting cannot close that while
27
- * the shells remain (blocking `node -e` still leaves `cmd /c node -e`).
28
- *
29
- * What actually bounds this is downstream, not the name check: the permission
30
- * policy reconstructs the full command line and runs it through the
31
- * destructive classifier, so destructive calls still require confirmation.
32
- *
33
- * Granting `shell.restricted` while withholding `shell.arbitrary` therefore
34
- * buys no meaningful reduction in blast radius. Documented rather than
35
- * silently implied, because the two read as a narrow/wide pair (WS-083).
36
- */
37
- SHELL_RESTRICTED: "shell.restricted",
38
- /** Can run a restricted project formatter/linter-style command. */
39
- SHELL_EXEC: "shell.exec",
40
- /** Can read files inside the project (and possibly outside via symlinks if not guarded). */
41
- FS_READ: "fs.read",
42
- /** Can write / modify / delete files inside the project. */
43
- FS_WRITE: "fs.write",
44
- /** Can write files outside the current project root (very high risk). */
45
- FS_WRITE_OUTSIDE_PROJECT: "fs.write.outside-project",
46
- /** Can perform outbound network requests. */
47
- NET_OUTBOUND: "net.outbound",
48
- /** Can mutate in-memory session todos only. */
49
- SESSION_TODO: "session.todo",
50
- /** Can park after-task `<nextsteps>` suggestions for the current turn. */
51
- SESSION_NEXTSTEPS: "session.nextsteps",
52
- /** Can mutate in-memory session mode only. */
53
- SESSION_MODE: "session.mode",
54
- /** Can inspect registered tool metadata. */
55
- TOOL_META: "tool.meta",
56
- /** Can invoke arbitrary registered tools through a meta-tool. */
57
- TOOL_MUTATE_ANY: "tool.mutate.any",
58
- /** Can read persistent memory. */
59
- MEMORY_READ: "memory.read",
60
- /** Can write persistent memory. */
61
- MEMORY_WRITE: "memory.write",
62
- /** Can delete persistent memory. */
63
- MEMORY_DELETE: "memory.delete",
64
- /** Proxies tools from external MCP servers (unknown capability). */
65
- MCP_PROXY: "mcp.proxy",
66
- /** Can spawn or manage subagents / multi-agent tasks. */
67
- SUBAGENT_SPAWN: "subagent.spawn",
68
- /** Can inspect fleet/subagent coordination state without mutating it. */
69
- COORDINATION_FLEET_READ: "coordination.fleet.read",
70
- /** Can publish attributed, schema-checked events onto the fleet bus. */
71
- COORDINATION_FLEET_EMIT: "coordination.fleet.emit",
72
- /** Can submit a task-local structured result to the parent Director. */
73
- COORDINATION_RESULT_SUBMIT: "coordination.result.submit",
74
- /** Can read or write inter-agent mailbox messages. */
75
- COORDINATION_MAIL: "coordination.mail",
76
- /** Can schedule, inspect, or cancel in-session cron jobs. */
77
- COORDINATION_CRON: "coordination.cron",
78
- /** Can mutate global or session configuration / trust state. */
79
- CONFIG_MUTATE: "config.mutate",
80
- /** Can install packages or run package managers with side effects. */
81
- PACKAGE_INSTALL: "package.install"
82
- };
83
- var DANGEROUS_FOR_SUBAGENTS = [
84
- ToolCapabilities.SHELL_ARBITRARY,
85
- ToolCapabilities.SHELL_RESTRICTED,
86
- ToolCapabilities.SHELL_EXEC,
87
- ToolCapabilities.FS_WRITE,
88
- ToolCapabilities.FS_WRITE_OUTSIDE_PROJECT,
89
- ToolCapabilities.TOOL_MUTATE_ANY,
90
- ToolCapabilities.MEMORY_WRITE,
91
- ToolCapabilities.MEMORY_DELETE,
92
- ToolCapabilities.MCP_PROXY,
93
- ToolCapabilities.SUBAGENT_SPAWN,
94
- ToolCapabilities.CONFIG_MUTATE,
95
- ToolCapabilities.PACKAGE_INSTALL
96
- ];
97
- var WIDE_SUBAGENT_CAPABILITIES = [
98
- ToolCapabilities.FS_READ,
99
- ToolCapabilities.FS_WRITE,
100
- ToolCapabilities.NET_OUTBOUND,
101
- ToolCapabilities.SESSION_TODO,
102
- ToolCapabilities.TOOL_META,
103
- ToolCapabilities.MEMORY_READ,
104
- ToolCapabilities.SHELL_ARBITRARY,
105
- ToolCapabilities.SHELL_RESTRICTED,
106
- ToolCapabilities.SHELL_EXEC,
107
- ToolCapabilities.PACKAGE_INSTALL,
108
- // Read-only fleet visibility (fleet_status) — peer awareness has no blast
109
- // radius and every fleet worker should coordinate around its peers.
110
- ToolCapabilities.COORDINATION_FLEET_READ,
111
- ToolCapabilities.COORDINATION_RESULT_SUBMIT
112
- ];
113
- function clampSubagentCapabilities(requested, ceiling = WIDE_SUBAGENT_CAPABILITIES) {
114
- const ceilingSet = new Set(ceiling);
115
- const requestedSet = new Set(requested);
116
- return {
117
- granted: ceiling.filter((capability) => requestedSet.has(capability)),
118
- dropped: [...requestedSet].filter((capability) => !ceilingSet.has(capability))
119
- };
120
- }
121
- function hasDangerousCapabilityForSubagents(toolOrCaps) {
122
- if (!toolOrCaps) return false;
123
- const input = toolOrCaps;
124
- const caps = Array.isArray(toolOrCaps) ? toolOrCaps : input.capabilities ?? [];
125
- return caps.some((c) => DANGEROUS_FOR_SUBAGENTS.includes(c));
126
- }
127
- function hasCapability(toolOrCaps, capability) {
128
- if (!toolOrCaps) return false;
129
- const input = toolOrCaps;
130
- const caps = Array.isArray(toolOrCaps) ? toolOrCaps : input.capabilities ?? [];
131
- const toCheck = Array.isArray(capability) ? capability : [capability];
132
- return toCheck.some((c) => caps.includes(c));
133
- }
134
- function getDangerousCapabilities(toolOrCaps) {
135
- if (!toolOrCaps) return [];
136
- const input = toolOrCaps;
137
- const caps = Array.isArray(toolOrCaps) ? toolOrCaps : input.capabilities ?? [];
138
- return caps.filter(
139
- (c) => DANGEROUS_FOR_SUBAGENTS.includes(c)
140
- );
141
- }
142
-
143
- // src/security/config-secrets.ts
144
- function decryptConfigSecrets(cfg, vault, opts) {
145
- const warn = opts?.warn ?? ((msg) => console.warn(msg));
146
- return walk(cfg, vault, (v, key) => {
147
- try {
148
- return vault.decrypt(v);
149
- } catch (err) {
150
- warn(
151
- `[secret-vault] Failed to decrypt "${key}": ${err instanceof Error ? err.message : err}`
152
- );
153
- return "";
154
- }
155
- });
156
- }
157
- function encryptConfigSecrets(cfg, vault, _opts) {
158
- return walk(cfg, vault, (v) => vault.encrypt(v));
159
- }
160
- function walk(node, vault, transform) {
161
- if (node === null || node === void 0) return node;
162
- if (typeof node !== "object") return node;
163
- if (Array.isArray(node)) {
164
- return node.map((item) => walk(item, vault, transform));
165
- }
166
- const out = /* @__PURE__ */ Object.create(null);
167
- for (const [k, v] of Object.entries(node)) {
168
- if (typeof v === "string" && isSecretField(k)) {
169
- out[k] = transform(v, k);
170
- } else if (typeof v === "object" && v !== null) {
171
- out[k] = walk(v, vault, transform);
172
- } else {
173
- out[k] = v;
174
- }
175
- }
176
- return out;
177
- }
178
- var SECRET_KEY_PATTERN = /(?:api[-_]?key|auth[-_]?token|authorization|proxy-authorization|cookie|bearer|secret|password|passwd|pwd|refresh[-_]?token|session[-_]?key|access[_-]?token|private[_-]?key|token\b)/i;
179
- var NON_SECRET_OVERRIDES = /* @__PURE__ */ new Set(["publickey", "public_key"]);
180
- function isSecretField(name) {
181
- const lc = name.toLowerCase();
182
- if (NON_SECRET_OVERRIDES.has(lc)) return false;
183
- return SECRET_KEY_PATTERN.test(lc);
184
- }
185
-
186
- // src/security/kanban-governance-port.ts
187
- var notWired = () => {
188
- throw new Error(
189
- "KanbanGovernancePort is not wired \u2014 register the implementation at the CLI composition root (see setKanbanGovernance)."
190
- );
191
- };
192
- var port = void 0;
193
- function setKanbanGovernance(impl) {
194
- port = impl;
195
- }
196
- function kanbanGovernance() {
197
- return port ?? notWired();
198
- }
1
+ import {
2
+ TRUST_POLICY_JSON_SCHEMA,
3
+ TRUST_POLICY_LIMITS,
4
+ TRUST_POLICY_SCHEMA_VERSION,
5
+ validateTrustPolicy
6
+ } from "../chunk-SXMVYC5H.js";
7
+ import {
8
+ DefaultSecretVault,
9
+ migratePlaintextSecrets,
10
+ noOpVault,
11
+ rewriteConfigEncrypted,
12
+ rotateConfigKeys
13
+ } from "../chunk-6AKOTGLM.js";
14
+ import {
15
+ ReadOnlyPermissionPolicy,
16
+ toolMutates
17
+ } from "../chunk-ZEHUCJHY.js";
18
+ import {
19
+ decryptConfigSecrets,
20
+ encryptConfigSecrets,
21
+ isSecretField
22
+ } from "../chunk-YY6APYUM.js";
23
+ import {
24
+ LruCache,
25
+ evaluateToolKanbanBoundary,
26
+ kanbanGovernance,
27
+ setKanbanGovernance
28
+ } from "../chunk-EPC3GZD4.js";
29
+ import {
30
+ ERROR_DETAIL_MAX,
31
+ sanitizeApiError,
32
+ scrubErrorDetail,
33
+ scrubErrorText
34
+ } from "../chunk-PAAWLLZX.js";
35
+ import {
36
+ DANGEROUS_FOR_SUBAGENTS,
37
+ ToolCapabilities,
38
+ WIDE_SUBAGENT_CAPABILITIES,
39
+ clampSubagentCapabilities,
40
+ getDangerousCapabilities,
41
+ hasCapability,
42
+ hasDangerousCapabilityForSubagents
43
+ } from "../chunk-A455ATZB.js";
44
+ import {
45
+ SECRET_DIR_MODE,
46
+ SECRET_FILE_MODE,
47
+ restrictDirPermissions,
48
+ restrictFilePermissions
49
+ } from "../chunk-H5E77KUJ.js";
50
+ import {
51
+ DefaultSecretScrubber
52
+ } from "../chunk-3E244UDA.js";
53
+ import {
54
+ matchAny,
55
+ matchAnyCommand,
56
+ matchGlob,
57
+ subjectForToolInput
58
+ } from "../chunk-QRTG4XTI.js";
59
+ import {
60
+ safeParse
61
+ } from "../chunk-RPOA7IAE.js";
62
+ import "../chunk-DEIQMBD2.js";
63
+ import {
64
+ atomicWrite
65
+ } from "../chunk-7ZHZE5EO.js";
66
+ import {
67
+ wstackGlobalRoot
68
+ } from "../chunk-YNCVSFGF.js";
69
+ import "../chunk-WOEZV3Q4.js";
70
+ import "../chunk-TBRL2RCC.js";
71
+ import "../chunk-PFZO3C6H.js";
199
72
 
200
73
  // src/security/directory-permission-policy.ts
201
74
  import * as path from "node:path";
202
-
203
- // src/utils/expect-defined.ts
204
- function expectDefined(value, label) {
205
- if (value === null || value === void 0) {
206
- const err = new Error(label ? `Expected ${label} to be defined` : "Expected value to be defined");
207
- err.name = "ExpectDefinedError";
208
- throw err;
209
- }
210
- return value;
211
- }
212
-
213
- // src/utils/glob-match.ts
214
- function escapeRegex(s) {
215
- return s.replace(/[.+^${}()|\\]/g, "\\$&");
216
- }
217
- var COMPILED_GLOB_CACHE = /* @__PURE__ */ new Map();
218
- var CACHE_MAX_SIZE = 2e3;
219
- var NEVER_MATCH = /[^\s\S]/;
220
- var COMMAND_WILDCARD_STOP = ";&|\\n\\r`$<>";
221
- function getCachedGlob(pattern, commandSubject = false) {
222
- const cacheKey = commandSubject ? `cmd\0${pattern}` : pattern;
223
- const cached = COMPILED_GLOB_CACHE.get(cacheKey);
224
- if (cached) return cached;
225
- if (COMPILED_GLOB_CACHE.size >= CACHE_MAX_SIZE) {
226
- const keys = [...COMPILED_GLOB_CACHE.keys()];
227
- for (let i = 0; i < Math.floor(CACHE_MAX_SIZE / 4); i++) {
228
- COMPILED_GLOB_CACHE.delete(expectDefined(keys[i]));
229
- }
230
- }
231
- let re;
232
- try {
233
- re = compileGlob(pattern, commandSubject);
234
- } catch {
235
- re = NEVER_MATCH;
236
- }
237
- COMPILED_GLOB_CACHE.set(cacheKey, re);
238
- return re;
239
- }
240
- var MAX_GLOB_PATTERN_LEN = 1024;
241
- function compileGlob(pattern, commandSubject = false) {
242
- if (pattern.length > MAX_GLOB_PATTERN_LEN) {
243
- throw new Error(`Glob pattern exceeds ${MAX_GLOB_PATTERN_LEN} characters`);
244
- }
245
- const star = commandSubject ? `[^/${COMMAND_WILDCARD_STOP}]*` : "[^/]*";
246
- const question = commandSubject ? `[^/${COMMAND_WILDCARD_STOP}]` : "[^/]";
247
- let i = 0;
248
- let re = "^";
249
- while (i < pattern.length) {
250
- const c = pattern[i];
251
- if (c === "*") {
252
- if (pattern[i + 1] === "*") {
253
- re += commandSubject ? `[^${COMMAND_WILDCARD_STOP}]*` : ".*";
254
- i += 2;
255
- if (pattern[i] === "/") i++;
256
- } else {
257
- re += star;
258
- i++;
259
- }
260
- } else if (c === "?") {
261
- re += question;
262
- i++;
263
- } else if (c === "[") {
264
- let cls = "[";
265
- i++;
266
- if (pattern[i] === "!" || pattern[i] === "^") {
267
- cls += "^";
268
- i++;
269
- }
270
- while (i < pattern.length && pattern[i] !== "]") {
271
- const ch = pattern[i] ?? "";
272
- if (ch === "\\") {
273
- cls += "\\\\";
274
- } else if (ch === "]" || ch === "^") {
275
- cls += `\\${ch}`;
276
- } else {
277
- cls += ch;
278
- }
279
- i++;
280
- }
281
- cls += "]";
282
- re += cls;
283
- i++;
284
- } else {
285
- re += escapeRegex(c ?? "");
286
- i++;
287
- }
288
- }
289
- re += "$";
290
- return new RegExp(re);
291
- }
292
- function matchGlob(pattern, input) {
293
- return getCachedGlob(pattern).test(input);
294
- }
295
- function matchAny(patterns, input) {
296
- return patterns.some((p) => matchGlob(p, input));
297
- }
298
- function matchCommandGlob(pattern, input) {
299
- return getCachedGlob(pattern, true).test(input);
300
- }
301
- function matchAnyCommand(patterns, input) {
302
- return patterns.some((p) => matchCommandGlob(p, input));
303
- }
304
-
305
- // src/security/directory-permission-policy.ts
306
75
  var PATH_KEYS = /* @__PURE__ */ new Set([
307
76
  "path",
308
77
  "paths",
@@ -728,22 +497,22 @@ var UNSAFE_PROPERTY_NAMES = /* @__PURE__ */ new Set(["__proto__", "prototype", "
728
497
  function isRecord(value) {
729
498
  return typeof value === "object" && value !== null && !Array.isArray(value);
730
499
  }
731
- function error(diagnostics, code, path9, message) {
732
- diagnostics.push({ severity: "error", code, path: path9, message });
500
+ function error(diagnostics, code, path5, message) {
501
+ diagnostics.push({ severity: "error", code, path: path5, message });
733
502
  }
734
- function validateStringList(value, path9, diagnostics, limits) {
503
+ function validateStringList(value, path5, diagnostics, limits) {
735
504
  if (!Array.isArray(value)) {
736
- error(diagnostics, limits.listErrorCode, path9, `must be an array of strings`);
505
+ error(diagnostics, limits.listErrorCode, path5, `must be an array of strings`);
737
506
  return void 0;
738
507
  }
739
508
  if (value.length > limits.maxItems) {
740
- error(diagnostics, limits.tooManyCode, path9, `must contain at most ${limits.maxItems} ${limits.itemLabel}s`);
509
+ error(diagnostics, limits.tooManyCode, path5, `must contain at most ${limits.maxItems} ${limits.itemLabel}s`);
741
510
  return void 0;
742
511
  }
743
512
  const seen = /* @__PURE__ */ new Set();
744
513
  const out = [];
745
514
  for (const [index, entry] of value.entries()) {
746
- const itemPath = `${path9}[${index}]`;
515
+ const itemPath = `${path5}[${index}]`;
747
516
  if (typeof entry !== "string" || entry.length === 0 || entry.length > limits.maxChars) {
748
517
  error(
749
518
  diagnostics,
@@ -904,973 +673,17 @@ function validateDirectoryPolicy(value) {
904
673
  return { ok: true, policy: { schemaVersion: DIRECTORY_POLICY_SCHEMA_VERSION, rules }, diagnostics };
905
674
  }
906
675
 
907
- // src/security/error-sanitize.ts
908
- import { homedir } from "node:os";
909
-
910
- // src/security/secret-scrubber.ts
911
- var JSON_CREDENTIAL_KEY_ANCHORS = [
912
- 'Key"',
913
- 'key"',
914
- 'KEY"',
915
- 'token"',
916
- 'Token"',
917
- 'TOKEN"',
918
- 'secret"',
919
- 'Secret"',
920
- 'SECRET"',
921
- 'password"',
922
- 'Password"',
923
- 'PASSWORD"',
924
- 'authorization"',
925
- 'Authorization"',
926
- 'bearer"',
927
- 'Bearer"'
928
- ];
929
- var PATTERNS = [
930
- // Anchored at the start where possible so partial matches inside larger
931
- // strings don't trigger false positives.
932
- {
933
- type: "anthropic_key",
934
- regex: /(?<![A-Za-z0-9])sk-ant-api\d+-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g,
935
- anchor: "sk-ant-"
936
- },
937
- { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g, anchor: "sk-" },
938
- {
939
- // `xai` is a first-class provider in this codebase, but its key shape was
940
- // absent here — so the one credential format WrongStack itself hands users
941
- // was the one the scrubber could not recognize (audit 2026-08-20).
942
- type: "xai_key",
943
- regex: /(?<![A-Za-z0-9])xai-[A-Za-z0-9]{20,}(?![A-Za-z0-9])/g,
944
- anchor: "xai-"
945
- },
946
- { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g, anchor: "ghp_" },
947
- { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g, anchor: "github_pat_" },
948
- { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g, anchor: "AKIA" },
949
- { type: "gcp_key", regex: /(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9])/g, anchor: "AIza" },
950
- { type: "slack_token", regex: /(?<![A-Za-z0-9-])xox[abpos]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g, anchor: "xox" },
951
- {
952
- type: "stripe_key",
953
- regex: /(?<![A-Za-z0-9])sk_(?:live|test)_[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g,
954
- anchor: "sk_"
955
- },
956
- {
957
- type: "twilio_sid",
958
- regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g,
959
- anchor: "AC"
960
- },
961
- {
962
- type: "telegram_bot_token",
963
- // Telegram tokens are of the form bot<digits>:<alphanum> in URL paths
964
- regex: /\/bot\d+:[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g,
965
- anchor: "/bot"
966
- },
967
- {
968
- type: "jwt",
969
- // Anchored: look for literal "eyJ" which is unambiguous for JWT header
970
- regex: /(?<![A-Za-z0-9/+=])eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}(?![A-Za-z0-9/+=])/g,
971
- anchor: "eyJ"
972
- },
973
- {
974
- type: "private_key",
975
- // Anchored: start must be BEGIN, end must be END with no extra dashes after END
976
- regex: /(?:^|\n)-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----[\s\S]*?-----END[^-]*-----(?:\n|$)/g,
977
- anchor: "-----BEGIN"
978
- },
979
- { type: "mongodb_uri", regex: /mongodb(?:\+srv)?:\/\/[^\s"'`]+/g, anchor: "mongodb" },
980
- { type: "postgres_uri", regex: /postgres(?:ql)?:\/\/[^\s"'`]+/g, anchor: "postgres" },
981
- { type: "mysql_uri", regex: /mysql:\/\/[^\s"'`]+/g, anchor: "mysql://" },
982
- { type: "redis_uri", regex: /redis:\/\/[^\s"'`]+/g, anchor: "redis://" },
983
- // AI/ML provider keys — modern LLM services with well-known prefixes
984
- {
985
- type: "huggingface_token",
986
- // HuggingFace tokens: hf_ followed by 34 alphanumeric chars
987
- regex: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{34}(?![A-Za-z0-9])/g,
988
- anchor: "hf_"
989
- },
990
- {
991
- type: "replicate_token",
992
- // Replicate tokens: r8_ followed by 40+ alphanumeric chars
993
- regex: /(?<![A-Za-z0-9])r8_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g,
994
- anchor: "r8_"
995
- },
996
- {
997
- type: "perplexity_key",
998
- // Perplexity API keys: pplx- followed by 40+ alphanumeric chars
999
- regex: /(?<![A-Za-z0-9])pplx-[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g,
1000
- anchor: "pplx-"
1001
- },
1002
- {
1003
- type: "groq_key",
1004
- // Groq API keys: gsk_ followed by 40+ alphanumeric chars
1005
- regex: /(?<![A-Za-z0-9])gsk_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g,
1006
- anchor: "gsk_"
1007
- },
1008
- {
1009
- type: "bearer_token",
1010
- // Anchored with alternation instead of negative lookahead — avoids V8
1011
- // backtracking risk on adversarial input. Bounded at 512 chars.
1012
- // Min 12 chars: some OAuth providers issue shorter-lived tokens (< 20
1013
- // chars). A 12-char base64 string has ~71 bits of entropy — above the
1014
- // threshold where random strings are unlikely to produce false matches.
1015
- // The trailing boundary is a NON-consuming lookahead: two adjacent bearer
1016
- // tokens sharing a single delimiter (`Bearer a… Bearer b…`) must both be
1017
- // redacted. A consuming trailing delimiter would eat the separator the
1018
- // next match needs for its leading anchor, leaking the second token.
1019
- regex: /(?:^|[^A-Za-z0-9_.~+/-])Bearer\s+[A-Za-z0-9._~+/-]{12,512}=*(?=$|[^A-Za-z0-9_.~+/-])/g,
1020
- anchor: "Bearer"
1021
- },
1022
- {
1023
- type: "high_entropy_env",
1024
- // Anchored with alternation instead of lookbehind to avoid backtracking.
1025
- // Value bounded at 512 chars.
1026
- // The trailing boundary is a NON-consuming lookahead so two secrets
1027
- // separated by a single delimiter (one space OR one newline, e.g.
1028
- // `printenv` / `.env` dumps: `API_KEY=… \n SESSION_TOKEN=…`) are BOTH
1029
- // redacted. A consuming trailing `\s` would swallow the separator the
1030
- // next match needs for its leading anchor, so every other secret would
1031
- // leak in plaintext.
1032
- // The leading delimiter is CAPTURED (group 1) and re-emitted by the
1033
- // replacement so the separator between adjacent secrets is preserved
1034
- // rather than collapsed. Capture groups are therefore: 1=leading
1035
- // delimiter, 2=key name, 3=value.
1036
- regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD|PASSPHRASE))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
1037
- anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD", "PASSPHRASE"]
1038
- },
1039
- {
1040
- type: "json_credential_key",
1041
- // The JSON counterpart to `high_entropy_env`, and the pattern that the
1042
- // now-deleted `JSON_KEY_ANCHORS` list was written for. Without it those
1043
- // anchors only widened the cheap pre-scan — `hasCredentialAnchors` said
1044
- // "this text may hold a secret", every pattern then declined to match, and
1045
- // the value went out verbatim. `high_entropy_env` cannot cover these: it
1046
- // requires an UPPERCASE unquoted key (`API_KEY=…`), so `{"apiKey":"…"}`
1047
- // never matched.
1048
- //
1049
- // Tool results are routinely serialised as JSON, and a credential with no
1050
- // recognisable prefix (Azure, self-hosted gateways, Anthropic/Codex OAuth)
1051
- // has no other pattern that can catch it — this is the only thing standing
1052
- // between such a value and the session JSONL, chronicle, HQ broadcast and
1053
- // the model's own context.
1054
- //
1055
- // The key may carry a prefix (`"anthropicApiKey"`), but the credential word
1056
- // must END the key: `"tokenCount"` and `"maxTokens"` do not match, because
1057
- // the closing quote has to follow the word immediately.
1058
- // Value floor of 8 chars keeps enum-ish values (`"authorization":"none"`)
1059
- // out. Capture groups: 1=key + punctuation, 2=value, 3=closing quote.
1060
- 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,
1061
- anchor: JSON_CREDENTIAL_KEY_ANCHORS
1062
- },
1063
- // ── Ported from packages/plugins credential-patterns.ts (WS-034) ─────────
1064
- // The plugin runtime carried 37 patterns while this scrubber — the one that
1065
- // guards session JSONL, chronicle, HQ broadcast, WebUI events and the auth
1066
- // audit — carried 22. The plugin side already had a parity test; it just did
1067
- // not cover core. Most consequential: WrongStack mints `gho_` tokens itself
1068
- // in the Copilot OAuth flow, and `gh[ousr]_` was absent here.
1069
- {
1070
- type: "github_oauth_token",
1071
- regex: /(?<![A-Za-z0-9])gh[ousr]_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g,
1072
- anchor: ["gho_", "ghu_", "ghs_", "ghr_"]
1073
- },
1074
- {
1075
- type: "gitlab_pat",
1076
- regex: /(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g,
1077
- anchor: "glpat-"
1078
- },
1079
- {
1080
- type: "gitlab_runner_token",
1081
- regex: /(?<![A-Za-z0-9])glrt-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g,
1082
- anchor: "glrt-"
1083
- },
1084
- {
1085
- type: "npm_token",
1086
- regex: /(?<![A-Za-z0-9])npm_[A-Za-z0-9]{36}(?![A-Za-z0-9])/g,
1087
- anchor: "npm_"
1088
- },
1089
- {
1090
- type: "slack_app_token",
1091
- regex: /(?<![A-Za-z0-9-])xapp-\d-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g,
1092
- anchor: "xapp-"
1093
- },
1094
- {
1095
- type: "slack_webhook",
1096
- regex: /https:\/\/hooks\.slack\.com\/services\/T[A-Za-z0-9_-]+\/B[A-Za-z0-9_-]+\/[A-Za-z0-9]{16,}/g,
1097
- anchor: "hooks.slack.com"
1098
- },
1099
- {
1100
- type: "sendgrid_key",
1101
- regex: /(?<![A-Za-z0-9])SG\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g,
1102
- anchor: "SG."
1103
- },
1104
- {
1105
- type: "digitalocean_token",
1106
- regex: /(?<![A-Za-z0-9])dop_v1_[a-f0-9]{64}(?![A-Za-z0-9])/g,
1107
- anchor: "dop_v1_"
1108
- },
1109
- {
1110
- type: "doppler_token",
1111
- regex: /(?<![A-Za-z0-9])dp\.(?:pt|st|sa|scim|audit)\.[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g,
1112
- anchor: "dp."
1113
- },
1114
- {
1115
- type: "shopify_token",
1116
- regex: /(?<![A-Za-z0-9])shp(?:at|ca|pa|ss)_[a-fA-F0-9]{32}(?![A-Za-z0-9])/g,
1117
- anchor: "shp"
1118
- },
1119
- {
1120
- type: "docker_pat",
1121
- regex: /(?<![A-Za-z0-9])dckr_pat_[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g,
1122
- anchor: "dckr_pat_"
1123
- },
1124
- {
1125
- type: "linear_key",
1126
- regex: /(?<![A-Za-z0-9])lin_api_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g,
1127
- anchor: "lin_api_"
1128
- },
1129
- {
1130
- type: "atlassian_token",
1131
- regex: /(?<![A-Za-z0-9])ATATT3[A-Za-z0-9_\-=]{40,}(?![A-Za-z0-9_\-=])/g,
1132
- anchor: "ATATT3"
1133
- },
1134
- {
1135
- type: "square_token",
1136
- regex: /(?<![A-Za-z0-9])(?:sq0(?:atp|csp)-|EAAA)[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g,
1137
- anchor: ["sq0atp-", "sq0csp-", "EAAA"]
1138
- },
1139
- {
1140
- type: "google_oauth_client_secret",
1141
- regex: /(?<![A-Za-z0-9_-])GOCSPX-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g,
1142
- anchor: "GOCSPX-"
1143
- }
1144
- ];
1145
- var SIMPLE_PATTERNS = PATTERNS.filter(
1146
- (p) => p.type !== "high_entropy_env" && p.type !== "json_credential_key"
1147
- );
1148
- var COMBINED_REGEX = new RegExp(SIMPLE_PATTERNS.map((p) => `(${p.regex.source})`).join("|"), "g");
1149
- var HIGH_ENTROPY_REGEX = PATTERNS.find((p) => p.type === "high_entropy_env").regex;
1150
- var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key").regex;
1151
- var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
1152
- var SCRUB_CHUNK_BYTES = 64 * 1024;
1153
- var SCRUB_OVERLAP_BYTES = 1024;
1154
- var PEM_PRIVATE_KEY_BEGIN_RE = /-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----/;
1155
- var PEM_END_MARKER = "-----END";
1156
- var MAX_PEM_BLOCK_BYTES = 64 * 1024;
1157
- var PEM_END_LINE_TOLERANCE = 64;
1158
- function extendChunkBoundaryPastPem(text, chunkStart, proposedEnd) {
1159
- const head = text.slice(chunkStart, proposedEnd);
1160
- const lastBegin = head.lastIndexOf("-----BEGIN ");
1161
- if (lastBegin === -1) return proposedEnd;
1162
- const fromBegin = text.slice(chunkStart + lastBegin);
1163
- const marker = PEM_PRIVATE_KEY_BEGIN_RE.exec(fromBegin);
1164
- if (!marker || marker.index !== 0) return proposedEnd;
1165
- const bodyStart = marker[0].length;
1166
- const cap = Math.min(text.length, chunkStart + lastBegin + MAX_PEM_BLOCK_BYTES);
1167
- const closeIdx = fromBegin.indexOf(PEM_END_MARKER, bodyStart);
1168
- if (closeIdx === -1 || chunkStart + lastBegin + closeIdx >= cap + PEM_END_LINE_TOLERANCE) {
1169
- return proposedEnd;
1170
- }
1171
- const lineEnd = fromBegin.indexOf("\n", closeIdx);
1172
- const end = lineEnd === -1 ? text.length : chunkStart + lastBegin + lineEnd + 1;
1173
- return Math.max(proposedEnd, end);
1174
- }
1175
- var PATTERN_ANCHORS = [
1176
- ...new Set(
1177
- PATTERNS.flatMap(
1178
- (pattern) => typeof pattern.anchor === "string" ? [pattern.anchor] : [...pattern.anchor]
1179
- )
1180
- )
1181
- ];
1182
- var ALL_ANCHORS = PATTERN_ANCHORS;
1183
- function hasCredentialAnchors(text) {
1184
- for (const anchor of ALL_ANCHORS) {
1185
- if (text.includes(anchor)) return true;
1186
- }
1187
- return false;
1188
- }
1189
- var DefaultSecretScrubber = class {
1190
- scrub(text) {
1191
- if (!text) return text;
1192
- if (!hasCredentialAnchors(text)) return text;
1193
- if (text.length <= SCRUB_CHUNK_BYTES) {
1194
- return this.scrubOne(text);
1195
- }
1196
- const out = [];
1197
- let i = 0;
1198
- while (i < text.length) {
1199
- let end = Math.min(i + SCRUB_CHUNK_BYTES, text.length);
1200
- if (end < text.length) {
1201
- const limit = Math.min(end + SCRUB_OVERLAP_BYTES, text.length);
1202
- let safe = -1;
1203
- for (let j = end; j < limit; j++) {
1204
- const ch = text.charCodeAt(j);
1205
- if (ch === 32 || ch === 9 || ch === 10 || ch === 13) {
1206
- safe = j;
1207
- break;
1208
- }
1209
- }
1210
- end = safe === -1 ? end : safe + 1;
1211
- end = extendChunkBoundaryPastPem(text, i, end);
1212
- }
1213
- out.push(this.scrubOne(text.slice(i, end)));
1214
- i = end;
1215
- }
1216
- return out.join("");
1217
- }
1218
- scrubOne(text) {
1219
- if (!hasCredentialAnchors(text)) return text;
1220
- let out = text.replace(
1221
- COMBINED_REGEX,
1222
- (match, ...groups) => {
1223
- const idx = groups.findIndex((g) => g !== void 0);
1224
- if (idx < 0) return match;
1225
- const replacement = COMBINED_REPLACEMENTS[idx];
1226
- return replacement !== void 0 ? replacement : match;
1227
- }
1228
- );
1229
- out = out.replace(HIGH_ENTROPY_REGEX, (_match, lead, key, _value) => {
1230
- return `${lead}${key}=[REDACTED:high_entropy_env]`;
1231
- });
1232
- out = out.replace(JSON_CREDENTIAL_REGEX, (_match, keyPrefix, _value, closingQuote) => {
1233
- return `${keyPrefix}[REDACTED:json_credential_key]${closingQuote}`;
1234
- });
1235
- return out;
1236
- }
1237
- /**
1238
- * Recursively scrub every string value in an object/array graph. Secrets can
1239
- * appear under any key — a URL query param, an `authorization` header, an
1240
- * arbitrarily-named nested field — so we don't gate recursion on key names.
1241
- * The per-string `scrub()` fast-path (anchor pre-scan) keeps this cheap: any
1242
- * value without a credential anchor returns immediately without regex work.
1243
- */
1244
- scrubObject(obj) {
1245
- const seen = /* @__PURE__ */ new WeakSet();
1246
- const visit = (v) => {
1247
- if (typeof v === "string") return this.scrub(v);
1248
- if (v === null || typeof v !== "object") return v;
1249
- if (seen.has(v)) return v;
1250
- seen.add(v);
1251
- if (Array.isArray(v)) return v.map(visit);
1252
- const out = {};
1253
- for (const [k, val] of Object.entries(v)) {
1254
- out[k] = visit(val);
1255
- }
1256
- return out;
1257
- };
1258
- return visit(obj);
1259
- }
1260
- };
1261
-
1262
- // src/security/error-sanitize.ts
1263
- var scrubber = new DefaultSecretScrubber();
1264
- var ERROR_DETAIL_MAX = 500;
1265
- function messageOf(err) {
1266
- if (err instanceof Error) return err.message;
1267
- if (typeof err === "string") return err;
1268
- try {
1269
- return String(err);
1270
- } catch {
1271
- return "unknown error";
1272
- }
1273
- }
1274
- function sanitizeApiError(err) {
1275
- const lower = messageOf(err).toLowerCase();
1276
- if (lower.includes("enoent") || lower.includes("no such file")) {
1277
- return "resource not found";
1278
- }
1279
- if (lower.includes("eacces") || lower.includes("permission")) {
1280
- return "permission denied";
1281
- }
1282
- if (lower.includes("json") && (lower.includes("parse") || lower.includes("unexpected"))) {
1283
- return "malformed data";
1284
- }
1285
- return "internal error";
1286
- }
1287
- function scrubErrorDetail(err) {
1288
- const out = scrubErrorText(messageOf(err));
1289
- return out.length > ERROR_DETAIL_MAX ? `${out.slice(0, ERROR_DETAIL_MAX - 1)}\u2026` : out;
1290
- }
1291
- function scrubErrorText(text) {
1292
- if (!text) return text;
1293
- let out = scrubber.scrub(text);
1294
- const home = safeHomedir();
1295
- if (home && home.length > 2) {
1296
- out = replaceAllCaseInsensitive(out, home, "~");
1297
- const alt = home.includes("\\") ? home.replace(/\\/g, "/") : home.replace(/\//g, "\\");
1298
- if (alt !== home) out = replaceAllCaseInsensitive(out, alt, "~");
1299
- }
1300
- return out;
1301
- }
1302
- function safeHomedir() {
1303
- try {
1304
- return homedir();
1305
- } catch {
1306
- return "";
1307
- }
1308
- }
1309
- function replaceAllCaseInsensitive(haystack, needle, replacement) {
1310
- const lowerHay = haystack.toLowerCase();
1311
- const lowerNeedle = needle.toLowerCase();
1312
- let idx = lowerHay.indexOf(lowerNeedle);
1313
- if (idx === -1) return haystack;
1314
- let out = "";
1315
- let from = 0;
1316
- while (idx !== -1) {
1317
- out += haystack.slice(from, idx) + replacement;
1318
- from = idx + needle.length;
1319
- idx = lowerHay.indexOf(lowerNeedle, from);
1320
- }
1321
- return out + haystack.slice(from);
1322
- }
1323
-
1324
- // src/security/file-permissions.ts
1325
- import {
1326
- restrictDirPermissions,
1327
- restrictFilePermissions,
1328
- SECRET_DIR_MODE,
1329
- SECRET_FILE_MODE
1330
- } from "@wrongstack/persistence";
1331
-
1332
- // src/security/kanban-boundary.ts
1333
- import { realpath } from "node:fs/promises";
1334
- import * as path2 from "node:path";
1335
- var GOVERNANCE_CONTROL_TOOLS = /* @__PURE__ */ new Set(["kanban", "plan", "task", "todo", "nextsteps"]);
1336
- var PATH_KEYS2 = /* @__PURE__ */ new Set([
1337
- "path",
1338
- "paths",
1339
- "file",
1340
- "files",
1341
- "directory",
1342
- "dir",
1343
- "cwd",
1344
- "root",
1345
- "baseDir",
1346
- "out",
1347
- "outputPath",
1348
- "sourcePath",
1349
- "targetPath",
1350
- "destinationPath",
1351
- "fromPath",
1352
- "toPath",
1353
- "worktreePath"
1354
- ]);
1355
- var TOOL_PATH_KEYS = {
1356
- // `target` is an identifier in tools such as plan/task/document, but a
1357
- // filesystem target in language_info. Keep ambiguous names tool-specific.
1358
- language_info: ["target"]
1359
- };
1360
- async function evaluateToolKanbanBoundary(tool, input, ctx, options = {}) {
1361
- if (tool.name === "kanban") return { decision: "allow" };
1362
- const identity = resolveKanbanIdentity(ctx);
1363
- const governanceRequired = options.requireGovernance && isGovernedMutation(tool, input);
1364
- if (!identity.boardId) {
1365
- return governanceRequired ? {
1366
- decision: "block",
1367
- reason: "Kanban governance is mandatory before product mutation. Create a managed card, add its required details and executable acceptance criteria, then call kanban start_task to bind it to this run."
1368
- } : { decision: "allow" };
1369
- }
1370
- const board = await kanbanGovernance().readBoard(
1371
- identity.policyRoot ?? ctx.projectRoot,
1372
- identity.boardId
1373
- );
1374
- if (!board) {
1375
- return governanceRequired ? {
1376
- decision: "block",
1377
- reason: `Active Kanban board not found: ${identity.boardId}. Recreate or select a valid managed card before mutation.`,
1378
- boardId: identity.boardId,
1379
- ...identity.taskId ? { taskId: identity.taskId } : {}
1380
- } : { decision: "allow" };
1381
- }
1382
- const task = identity.taskId ? board.tasks.find((candidate) => candidate.id === identity.taskId) : void 0;
1383
- if (governanceRequired && board.lifecycle?.mode === "managed") {
1384
- if (!identity.taskId) {
1385
- return {
1386
- decision: "block",
1387
- reason: "Kanban governance requires an active task, not only a board. Complete the card details and call kanban start_task before mutation.",
1388
- boardId: board.id
1389
- };
1390
- }
1391
- if (!task) {
1392
- return {
1393
- decision: "block",
1394
- reason: `Active Kanban task not found: ${identity.taskId}. Call kanban start_task with a valid card.`,
1395
- boardId: board.id,
1396
- taskId: identity.taskId
1397
- };
1398
- }
1399
- const readiness = kanbanGovernance().evaluateContractGraphReadiness(board, task.id);
1400
- if (!readiness.ready) {
1401
- const issueLines = readiness.issues.map((issue, index) => ` ${index + 1}. ${issue.message}`).join("\n");
1402
- return {
1403
- decision: "block",
1404
- reason: `Active card failed contract-readiness check (${readiness.issues.length} issue(s)):
1405
- ` + issueLines + `
1406
- Resolve the readiness issues, then call kanban start_task to retry.`,
1407
- boardId: board.id,
1408
- taskId: task.id,
1409
- readinessIssues: readiness.issues
1410
- };
1411
- }
1412
- const lifecycleStage = task.lifecycle?.currentStage;
1413
- const assignmentStatus = task.assignment?.status;
1414
- const lifecycleOk = lifecycleStage === "running";
1415
- const assignmentOk = assignmentStatus === "running";
1416
- if (!lifecycleOk || !assignmentOk) {
1417
- const failed = [];
1418
- let reassessmentNote = "";
1419
- if (!lifecycleOk) {
1420
- failed.push(`lifecycle.currentStage (got: ${lifecycleStage ?? "missing"}; want: 'running')`);
1421
- }
1422
- if (!assignmentOk) {
1423
- failed.push(`assignment.status (got: ${assignmentStatus ?? "missing"}; want: 'running')`);
1424
- if (lifecycleOk && (assignmentStatus === "queued" || assignmentStatus === void 0 || assignmentStatus === null)) {
1425
- reassessmentNote = '\nNote: lifecycle is still "running" but the assignment was cleared \u2014 the automated kanban reassessment agent likely reset the binding. Re-issue kanban start_task to re-bind the leader to this card.';
1426
- }
1427
- }
1428
- return {
1429
- decision: "block",
1430
- reason: `Active card failed ${failed.length} governance check(s) before product mutation:
1431
- ` + failed.map((line, index) => ` ${index + 1}. ${line}`).join("\n") + reassessmentNote,
1432
- boardId: board.id,
1433
- taskId: task.id
1434
- };
1435
- }
1436
- }
1437
- if (identity.leaseId && task?.assignment) {
1438
- const caps2 = tool.capabilities ?? [];
1439
- const isWrite = caps2.includes("fs.write") || caps2.includes("fs.write.outside-project") || caps2.some((c) => c.startsWith("shell."));
1440
- if (isWrite && tool.name !== "kanban" && task.assignment.leaseId !== identity.leaseId) {
1441
- return {
1442
- decision: "block",
1443
- reason: `Task ${task.id} lease mismatch: the current worker's lease (${identity.leaseId}) does not match the active lease on the board (${task.assignment.leaseId}). This task was likely recovered and reassigned to another worker. File modifications from a stale worker are blocked. Use kanban mark_assignment with expectedLeaseId to resolve, or use kanban get_board to reassess the current state.`,
1444
- boardId: board.id,
1445
- taskId: task.id
1446
- };
1447
- }
1448
- }
1449
- const layers = kanbanGovernance().resolveKanbanBoundaryLayers(board, task);
1450
- if (layers.length === 0) return { decision: "allow" };
1451
- const caps = tool.capabilities ?? [];
1452
- const access = caps.includes("fs.write") || caps.includes("fs.write.outside-project") ? "write" : caps.includes("fs.read") ? "read" : void 0;
1453
- const shellLike = caps.some((capability) => capability.startsWith("shell."));
1454
- const candidates = access ? await extractCandidatePaths(tool.name, input, ctx) : [];
1455
- let evaluation;
1456
- if (shellLike || access && candidates.length === 0) {
1457
- evaluation = kanbanGovernance().evaluateKanbanBoundaryOpaque(layers, tool.name);
1458
- } else if (access) {
1459
- const results = candidates.map(
1460
- (candidate) => kanbanGovernance().evaluateKanbanBoundaryPath(layers, candidate, access)
1461
- );
1462
- evaluation = results.find((result) => result.decision === "block") ?? results.find((result) => result.decision === "confirm") ?? { decision: "allow" };
1463
- } else {
1464
- evaluation = { decision: "allow" };
1465
- }
1466
- return {
1467
- ...evaluation,
1468
- boardId: board.id,
1469
- ...task ? { taskId: task.id } : {}
1470
- };
1471
- }
1472
- function isGovernedMutation(tool, input) {
1473
- if (!tool.mutating || GOVERNANCE_CONTROL_TOOLS.has(tool.name)) return false;
1474
- if (tool.capabilities?.includes("tool.meta")) return false;
1475
- if (tool.name === "git") {
1476
- const command = input["command"];
1477
- if (command === "status" || command === "log" || command === "diff") return false;
1478
- if (command === "worktree" && input["worktreeAction"] === "list") return false;
1479
- }
1480
- const capabilities = tool.capabilities ?? [];
1481
- return capabilities.some(
1482
- (capability) => capability === "fs.write" || capability === "fs.write.outside-project" || capability === "package.install" || capability === "tool.mutate.any" || capability.startsWith("shell.") || capability === "net.outbound"
1483
- );
1484
- }
1485
- function resolveKanbanIdentity(ctx) {
1486
- const metaKanban = ctx.meta["kanban"];
1487
- const record = metaKanban && typeof metaKanban === "object" ? metaKanban : void 0;
1488
- const boardId = ctx.currentKanbanBoardId ?? (typeof record?.["boardId"] === "string" ? record["boardId"] : void 0);
1489
- const taskId = ctx.currentKanbanTaskId ?? (typeof record?.["taskId"] === "string" ? record["taskId"] : void 0);
1490
- const policyRoot = typeof record?.["projectRoot"] === "string" ? record["projectRoot"] : void 0;
1491
- const leaseId = typeof record?.["leaseId"] === "string" ? record["leaseId"] : void 0;
1492
- return {
1493
- ...boardId ? { boardId } : {},
1494
- ...taskId ? { taskId } : {},
1495
- ...policyRoot ? { policyRoot } : {},
1496
- ...leaseId ? { leaseId } : {}
1497
- };
1498
- }
1499
- async function extractCandidatePaths(toolName, input, ctx) {
1500
- if (toolName === "patch" && typeof input["patch"] === "string") {
1501
- const directoryInput = stringValue(input["directory"]) ?? ctx.workingDir;
1502
- const directory = path2.isAbsolute(directoryInput) ? directoryInput : path2.resolve(ctx.workingDir, directoryInput);
1503
- const strip = Math.max(1, numericValue(input["strip"]) ?? 1);
1504
- const targets = extractPatchTargets(input["patch"], strip).map(
1505
- (target) => relativeToProject(path2.resolve(directory, target), ctx.projectRoot)
1506
- );
1507
- return Promise.all(targets.map((target) => canonicalizeCandidatePath(target, ctx)));
1508
- }
1509
- const values = [];
1510
- const pathKeys = new Set(PATH_KEYS2);
1511
- for (const key of TOOL_PATH_KEYS[toolName] ?? []) pathKeys.add(key);
1512
- collectPathValues(input, values, pathKeys);
1513
- if (toolName === "scaffold" && typeof input["name"] === "string") {
1514
- const cwd = stringValue(input["cwd"]) ?? ctx.workingDir;
1515
- values.push(path2.join(cwd, input["name"]));
1516
- }
1517
- const candidates = [
1518
- ...new Set(values.flatMap(splitPathList).map((value) => resolveInputPath(value, ctx)))
1519
- ];
1520
- return Promise.all(candidates.map((candidate) => canonicalizeCandidatePath(candidate, ctx)));
1521
- }
1522
- async function canonicalizeCandidatePath(candidate, ctx) {
1523
- if (path2.isAbsolute(candidate)) return candidate;
1524
- const absolute = path2.resolve(ctx.projectRoot, candidate);
1525
- const canonicalRoot = await realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
1526
- let probe = absolute;
1527
- const missingSegments = [];
1528
- while (true) {
1529
- try {
1530
- const canonical = path2.join(await realpath(probe), ...missingSegments);
1531
- return relativeToProject(canonical, canonicalRoot);
1532
- } catch (cause) {
1533
- const code = cause.code;
1534
- if (code !== "ENOENT" && code !== "ENOTDIR") return absolute;
1535
- const parent = path2.dirname(probe);
1536
- if (parent === probe) return absolute;
1537
- missingSegments.unshift(path2.basename(probe));
1538
- probe = parent;
1539
- }
1540
- }
1541
- }
1542
- function collectPathValues(value, output, pathKeys, key) {
1543
- if (typeof value === "string") {
1544
- if (key && pathKeys.has(key)) output.push(value);
1545
- return;
1546
- }
1547
- if (Array.isArray(value)) {
1548
- for (const item of value) collectPathValues(item, output, pathKeys, key);
1549
- return;
1550
- }
1551
- if (!value || typeof value !== "object") return;
1552
- for (const [childKey, child] of Object.entries(value)) {
1553
- collectPathValues(child, output, pathKeys, childKey);
1554
- }
1555
- }
1556
- function splitPathList(value) {
1557
- return value.split(",").map((item) => item.trim()).filter(Boolean);
1558
- }
1559
- function resolveInputPath(value, ctx) {
1560
- const absolute = path2.isAbsolute(value) ? value : path2.resolve(ctx.workingDir, value);
1561
- return relativeToProject(absolute, ctx.projectRoot);
1562
- }
1563
- function relativeToProject(absolute, projectRoot) {
1564
- const relative4 = path2.relative(projectRoot, absolute).replace(/\\/g, "/");
1565
- return relative4.startsWith("../") || path2.isAbsolute(relative4) ? absolute : relative4 || ".";
1566
- }
1567
- function extractPatchTargets(patchText, strip) {
1568
- const targets = [];
1569
- for (const match of patchText.matchAll(/^\+\+\+\s+([^\t\r\n]+)/gm)) {
1570
- const raw = match[1]?.trim();
1571
- if (!raw || raw === "/dev/null") continue;
1572
- const parts = raw.replace(/\\/g, "/").split("/").filter((part) => part && part !== ".");
1573
- if (parts.length > strip) targets.push(parts.slice(strip).join("/"));
1574
- }
1575
- return targets;
1576
- }
1577
- function stringValue(value) {
1578
- return typeof value === "string" && value.trim() ? value : void 0;
1579
- }
1580
- function numericValue(value) {
1581
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1582
- }
1583
-
1584
676
  // src/security/permission-policy.ts
1585
677
  import * as fs from "node:fs/promises";
1586
- import * as path6 from "node:path";
1587
-
1588
- // src/utils/atomic-write.ts
1589
- import {
1590
- createPersistencePrimitives
1591
- } from "@wrongstack/persistence";
1592
-
1593
- // src/utils/error.ts
1594
- function toErrorMessage(err) {
1595
- return err instanceof Error ? err.message : String(err);
1596
- }
1597
-
1598
- // src/types/errors.ts
1599
- var ERROR_CODES = {
1600
- // Provider
1601
- PROVIDER_RATE_LIMITED: "PROVIDER_RATE_LIMITED",
1602
- PROVIDER_AUTH_FAILED: "PROVIDER_AUTH_FAILED",
1603
- PROVIDER_OVERLOADED: "PROVIDER_OVERLOADED",
1604
- PROVIDER_INVALID_REQUEST: "PROVIDER_INVALID_REQUEST",
1605
- PROVIDER_SERVER_ERROR: "PROVIDER_SERVER_ERROR",
1606
- PROVIDER_NETWORK_ERROR: "PROVIDER_NETWORK_ERROR",
1607
- PROVIDER_CONTEXT_OVERFLOW: "PROVIDER_CONTEXT_OVERFLOW",
1608
- // Tool
1609
- TOOL_NOT_FOUND: "TOOL_NOT_FOUND",
1610
- TOOL_PERMISSION_DENIED: "TOOL_PERMISSION_DENIED",
1611
- TOOL_EXECUTION_FAILED: "TOOL_EXECUTION_FAILED",
1612
- TOOL_TIMEOUT: "TOOL_TIMEOUT",
1613
- TOOL_INPUT_INVALID: "TOOL_INPUT_INVALID",
1614
- // Config
1615
- CONFIG_INVALID: "CONFIG_INVALID",
1616
- CONFIG_NOT_FOUND: "CONFIG_NOT_FOUND",
1617
- CONFIG_PARSE_FAILED: "CONFIG_PARSE_FAILED",
1618
- CONFIG_MIGRATION_NEEDED: "CONFIG_MIGRATION_NEEDED",
1619
- // Plugin
1620
- PLUGIN_LOAD_FAILED: "PLUGIN_LOAD_FAILED",
1621
- PLUGIN_API_MISMATCH: "PLUGIN_API_MISMATCH",
1622
- PLUGIN_MISSING_DEPENDENCY: "PLUGIN_MISSING_DEPENDENCY",
1623
- // Agent
1624
- AGENT_ITERATION_LIMIT: "AGENT_ITERATION_LIMIT",
1625
- AGENT_CONTEXT_OVERFLOW: "AGENT_CONTEXT_OVERFLOW",
1626
- AGENT_ABORTED: "AGENT_ABORTED",
1627
- AGENT_RUN_FAILED: "AGENT_RUN_FAILED",
1628
- // Session
1629
- SESSION_NOT_FOUND: "SESSION_NOT_FOUND",
1630
- SESSION_CORRUPTED: "SESSION_CORRUPTED",
1631
- SESSION_WRITE_FAILED: "SESSION_WRITE_FAILED",
1632
- // Container / Registry
1633
- CONTAINER_TOKEN_ALREADY_BOUND: "CONTAINER_TOKEN_ALREADY_BOUND",
1634
- CONTAINER_TOKEN_NOT_BOUND: "CONTAINER_TOKEN_NOT_BOUND",
1635
- CONTAINER_CIRCULAR_DEPENDENCY: "CONTAINER_CIRCULAR_DEPENDENCY",
1636
- REGISTRY_DUPLICATE: "REGISTRY_DUPLICATE",
1637
- REGISTRY_NOT_FOUND: "REGISTRY_NOT_FOUND",
1638
- REGISTRY_INVALID: "REGISTRY_INVALID",
1639
- // File system
1640
- FS_READ_FAILED: "FS_READ_FAILED",
1641
- FS_WRITE_FAILED: "FS_WRITE_FAILED",
1642
- FS_MKDIR_FAILED: "FS_MKDIR_FAILED",
1643
- FS_DELETE_FAILED: "FS_DELETE_FAILED",
1644
- FS_ATOMIC_WRITE_FAILED: "FS_ATOMIC_WRITE_FAILED",
1645
- // SDD (Spec-Driven Development)
1646
- SDD_VALIDATION_FAILED: "SDD_VALIDATION_FAILED",
1647
- SDD_PARSE_FAILED: "SDD_PARSE_FAILED",
1648
- SDD_INVALID_STATE: "SDD_INVALID_STATE",
1649
- SDD_NOT_READY: "SDD_NOT_READY",
1650
- // General
1651
- VALIDATION_ERROR: "VALIDATION_ERROR",
1652
- PARSE_FAILED: "PARSE_FAILED",
1653
- UNKNOWN: "UNKNOWN"
1654
- };
1655
- var WrongStackError = class extends Error {
1656
- code;
1657
- subsystem;
1658
- severity;
1659
- recoverable;
1660
- context;
1661
- constructor(opts) {
1662
- super(opts.message, { cause: opts.cause });
1663
- this.name = "WrongStackError";
1664
- this.code = opts.code;
1665
- this.subsystem = opts.subsystem;
1666
- this.severity = opts.severity ?? "error";
1667
- this.recoverable = opts.recoverable ?? false;
1668
- this.context = opts.context;
1669
- }
1670
- /**
1671
- * Render a one-line user-facing description.
1672
- * Subclasses should override for domain-specific formatting.
1673
- */
1674
- describe() {
1675
- const ctx = this.context ? ` ${formatContext(this.context)}` : "";
1676
- return `${this.code}: ${this.message}${ctx}`;
1677
- }
1678
- };
1679
- function formatContext(ctx) {
1680
- const parts = Object.entries(ctx).filter(([, v]) => v !== void 0).slice(0, 3).map(([k, v]) => `${k}=${String(v)}`);
1681
- return parts.length > 0 ? `[${parts.join(" ")}]` : "";
1682
- }
1683
- var ConfigError = class extends WrongStackError {
1684
- constructor(opts) {
1685
- super({
1686
- message: opts.message,
1687
- code: opts.code,
1688
- subsystem: "config",
1689
- severity: "fatal",
1690
- recoverable: false,
1691
- context: opts.context,
1692
- cause: opts.cause
1693
- });
1694
- this.name = "ConfigError";
1695
- }
1696
- };
1697
- var FsError = class extends WrongStackError {
1698
- path;
1699
- constructor(opts) {
1700
- super({
1701
- message: opts.message,
1702
- code: opts.code,
1703
- subsystem: "fs",
1704
- severity: "error",
1705
- recoverable: opts.code !== ERROR_CODES.FS_READ_FAILED,
1706
- context: { path: opts.path, ...opts.context },
1707
- cause: opts.cause
1708
- });
1709
- this.name = "FsError";
1710
- this.path = opts.path;
1711
- }
1712
- };
1713
-
1714
- // src/utils/atomic-write.ts
1715
- var primitives = createPersistencePrimitives({
1716
- createLockTimeoutError: ({ targetPath, timeoutMs }) => new FsError({
1717
- message: `Timed out waiting for file lock: ${targetPath}`,
1718
- code: "FS_ATOMIC_WRITE_FAILED",
1719
- path: targetPath,
1720
- context: { timeoutMs }
1721
- })
1722
- });
1723
- var atomicWrite = primitives.atomicWrite;
1724
- var atomicReplaceWithWriter = primitives.atomicReplaceWithWriter;
1725
- var ensureDir = primitives.ensureDir;
1726
- var withFileLock = primitives.withFileLock;
1727
-
1728
- // src/utils/lru-cache.ts
1729
- var LruCache = class {
1730
- store = /* @__PURE__ */ new Map();
1731
- capacity;
1732
- constructor(capacity) {
1733
- if (capacity < 1) throw new Error(`LruCache capacity must be >= 1, got ${capacity}`);
1734
- this.capacity = capacity;
1735
- }
1736
- get(key) {
1737
- if (!this.store.has(key)) return void 0;
1738
- const value = this.store.get(key);
1739
- this.store.delete(key);
1740
- this.store.set(key, value);
1741
- return value;
1742
- }
1743
- set(key, value) {
1744
- if (this.store.has(key)) this.store.delete(key);
1745
- this.store.set(key, value);
1746
- while (this.store.size > this.capacity) {
1747
- const oldest = this.store.keys().next();
1748
- if (oldest.done) break;
1749
- this.store.delete(oldest.value);
1750
- }
1751
- }
1752
- has(key) {
1753
- return this.store.has(key);
1754
- }
1755
- clear() {
1756
- this.store.clear();
1757
- }
1758
- get size() {
1759
- return this.store.size;
1760
- }
1761
- };
1762
-
1763
- // src/utils/safe-json.ts
1764
- function safeParse(input, maxBytes = 5e6) {
1765
- if (Buffer.byteLength(input, "utf8") > maxBytes) {
1766
- return { ok: false, error: `Input exceeds limit (${maxBytes} bytes)` };
1767
- }
1768
- try {
1769
- return { ok: true, value: JSON.parse(input) };
1770
- } catch (err) {
1771
- return {
1772
- ok: false,
1773
- error: toErrorMessage(err)
1774
- };
1775
- }
1776
- }
1777
-
1778
- // src/utils/tool-subject.ts
1779
- var GLOB_METACHARACTERS = /[*?[\]]/g;
1780
- function escapeGlobSubject(value) {
1781
- return value.replace(GLOB_METACHARACTERS, (char) => `\\${char}`);
1782
- }
1783
- function normalizePathSubject(value) {
1784
- return escapeGlobSubject(value.replace(/\\/g, "/"));
1785
- }
1786
- function isPathSubjectKey(subjectKey) {
1787
- return subjectKey === "path" || subjectKey === "file" || subjectKey === "files";
1788
- }
1789
- function renderCommandLine(command, args) {
1790
- if (!Array.isArray(args) || args.length === 0) return command;
1791
- const rendered = args.map((arg) => {
1792
- const str = String(arg);
1793
- return /\s/.test(str) ? `"${str.replace(/"/g, '\\"')}"` : str;
1794
- });
1795
- return [command, ...rendered].join(" ");
1796
- }
1797
- function renderSubjectFields(obj, fields) {
1798
- const parts = [];
1799
- for (const field of fields) {
1800
- const value = obj[field];
1801
- if (value === void 0 || value === null || value === "" || value === false) continue;
1802
- const str = String(value);
1803
- parts.push(`${field}=${/\s/.test(str) ? `"${str.replace(/"/g, '\\"')}"` : str}`);
1804
- }
1805
- return parts.join(" ");
1806
- }
1807
- function subjectForToolInput(toolName, input, subjectKey, subjectFields) {
1808
- if (!input || typeof input !== "object") return void 0;
1809
- const obj = input;
1810
- const extra = subjectFields && subjectFields.length > 0 ? renderSubjectFields(obj, subjectFields) : "";
1811
- const withExtra = (base) => extra ? `${base} ${extra}` : base;
1812
- if (subjectKey) {
1813
- const value = obj[subjectKey];
1814
- if (Array.isArray(value)) {
1815
- return escapeGlobSubject(JSON.stringify(value));
1816
- }
1817
- if (typeof value === "string") {
1818
- if (isPathSubjectKey(subjectKey)) {
1819
- const normalized = normalizePathSubject(value);
1820
- if (subjectKey === "files" && obj["check"] === true) return `${normalized}:check`;
1821
- if (subjectKey === "files" && obj["dry_run"] === true) return `${normalized}:dry-run`;
1822
- return normalized;
1823
- }
1824
- if (subjectKey === "command") {
1825
- const rendered = renderCommandLine(value, obj["args"]);
1826
- if (value === "commit" && obj["dry_run"] === true) {
1827
- return `${escapeGlobSubject(withExtra(rendered))}:dry-run`;
1828
- }
1829
- return escapeGlobSubject(withExtra(rendered));
1830
- }
1831
- if (subjectKey === "directory" && obj["dry_run"] === true) {
1832
- return `${escapeGlobSubject(value)}:dry-run`;
1833
- }
1834
- if (subjectKey === "packages" && obj["dry_run"] === true) {
1835
- return `${escapeGlobSubject(value)}:dry-run`;
1836
- }
1837
- if (subjectKey === "name" && obj["dry_run"] === true) {
1838
- return `${escapeGlobSubject(value)}:dry-run`;
1839
- }
1840
- return escapeGlobSubject(value);
1841
- }
1842
- }
1843
- if (toolName === "bash" && typeof obj.command === "string") {
1844
- return escapeGlobSubject(obj.command);
1845
- }
1846
- if (typeof obj.path === "string") {
1847
- return normalizePathSubject(obj.path);
1848
- }
1849
- if (typeof obj.url === "string") {
1850
- return escapeGlobSubject(obj.url);
1851
- }
1852
- if (typeof obj.name === "string") {
1853
- return escapeGlobSubject(obj.name);
1854
- }
1855
- return void 0;
1856
- }
678
+ import * as path4 from "node:path";
1857
679
 
1858
680
  // src/security/permission-helpers.ts
1859
681
  import { realpathSync } from "node:fs";
1860
- import * as path5 from "node:path";
1861
-
1862
- // src/utils/wstack-paths.ts
1863
- import * as os from "node:os";
1864
682
  import * as path3 from "node:path";
1865
- function wstackGlobalRoot() {
1866
- const fromEnv = process.env["WRONGSTACK_HOME"];
1867
- if (fromEnv && fromEnv.trim().length > 0) return path3.resolve(fromEnv);
1868
- return path3.join(os.homedir(), ".wrongstack");
1869
- }
1870
683
 
1871
684
  // src/security/yolo-risk.ts
1872
- import * as os2 from "node:os";
1873
- import * as path4 from "node:path";
685
+ import * as os from "node:os";
686
+ import * as path2 from "node:path";
1874
687
  var PROTECTED_STATE_BASENAMES = /^(?:config\.json|config\.local\.json|trust\.json|auth\.json|\.key)$/i;
1875
688
  var CATASTROPHIC_PATTERNS = [
1876
689
  /\b(?:mkfs(?:\.[a-z0-9]+)?|mke2fs|newfs)\b/i,
@@ -1931,9 +744,9 @@ function getInputString(input, key) {
1931
744
  function pathLooksInsideProject(rawPath, projectRoot) {
1932
745
  if (!projectRoot) return false;
1933
746
  if (rawPath === "~" || rawPath.startsWith("~/") || rawPath.startsWith("~\\")) return false;
1934
- const resolved = path4.resolve(projectRoot, rawPath);
1935
- const relative4 = path4.relative(projectRoot, resolved);
1936
- return !!relative4 && !relative4.startsWith("..") && !path4.isAbsolute(relative4);
747
+ const resolved = path2.resolve(projectRoot, rawPath);
748
+ const relative3 = path2.relative(projectRoot, resolved);
749
+ return !!relative3 && !relative3.startsWith("..") && !path2.isAbsolute(relative3);
1937
750
  }
1938
751
  function tokenizeShell(command) {
1939
752
  return command.match(/"[^"]*"|'[^']*'|\S+/g)?.map((token) => token.replace(/^['"]|['"]$/g, "")) ?? [];
@@ -2146,15 +959,15 @@ function hasWriteToAgentStateRoot(command) {
2146
959
  return false;
2147
960
  }
2148
961
  function looksLikeAgentStateTarget(rawPath) {
2149
- const expanded = rawPath.replace(/^~([\\/])/, (_, sep2) => `${os2.homedir()}${sep2}`);
2150
- const resolved = path4.resolve(expanded);
962
+ const expanded = rawPath.replace(/^~([\\/])/, (_, sep) => `${os.homedir()}${sep}`);
963
+ const resolved = path2.resolve(expanded);
2151
964
  const rootStr = wstackGlobalRoot();
2152
965
  const resolvedNorm = resolved.replace(/\\/g, "/").toLowerCase();
2153
- const rootNorm = path4.resolve(rootStr).replace(/\\/g, "/").toLowerCase();
966
+ const rootNorm = path2.resolve(rootStr).replace(/\\/g, "/").toLowerCase();
2154
967
  if (!resolvedNorm.startsWith(rootNorm) && !resolvedNorm.includes(".wrongstack")) {
2155
968
  return false;
2156
969
  }
2157
- return PROTECTED_STATE_BASENAMES.test(path4.basename(resolved));
970
+ return PROTECTED_STATE_BASENAMES.test(path2.basename(resolved));
2158
971
  }
2159
972
  function isClearlyDestructiveBashCommand(command, projectRoot) {
2160
973
  const trimmed = command.trim();
@@ -2305,24 +1118,24 @@ function realpathOfNearestExisting(p) {
2305
1118
  const tail = [];
2306
1119
  for (; ; ) {
2307
1120
  try {
2308
- return tail.length === 0 ? realpathSync(probe) : path5.join(realpathSync(probe), ...tail);
1121
+ return tail.length === 0 ? realpathSync(probe) : path3.join(realpathSync(probe), ...tail);
2309
1122
  } catch {
2310
- const parent = path5.dirname(probe);
1123
+ const parent = path3.dirname(probe);
2311
1124
  if (parent === probe) return p;
2312
- tail.unshift(path5.basename(probe));
1125
+ tail.unshift(path3.basename(probe));
2313
1126
  probe = parent;
2314
1127
  }
2315
1128
  }
2316
1129
  }
2317
1130
  var agentStateRootRealCache = /* @__PURE__ */ new Map();
2318
1131
  function isInsideAgentStateRoot(absPath) {
2319
- const lexicalRoot = normalizeForCompare(path5.resolve(wstackGlobalRoot()));
1132
+ const lexicalRoot = normalizeForCompare(path3.resolve(wstackGlobalRoot()));
2320
1133
  if (!lexicalRoot) return false;
2321
1134
  const target = normalizeForCompare(absPath);
2322
1135
  if (target === lexicalRoot || target.startsWith(`${lexicalRoot}/`)) return true;
2323
1136
  let realRoot = agentStateRootRealCache.get(lexicalRoot);
2324
1137
  if (realRoot === void 0) {
2325
- realRoot = normalizeForCompare(realpathOfNearestExisting(path5.resolve(wstackGlobalRoot())));
1138
+ realRoot = normalizeForCompare(realpathOfNearestExisting(path3.resolve(wstackGlobalRoot())));
2326
1139
  agentStateRootRealCache.set(lexicalRoot, realRoot);
2327
1140
  }
2328
1141
  if (!realRoot || realRoot === lexicalRoot) return false;
@@ -2331,7 +1144,7 @@ function isInsideAgentStateRoot(absPath) {
2331
1144
  }
2332
1145
  function isProtectedAgentStatePath(absPath) {
2333
1146
  if (!isInsideAgentStateRoot(absPath)) return false;
2334
- return AGENT_STATE_SENSITIVE_BASENAMES.test(path5.basename(normalizeForCompare(absPath)));
1147
+ return AGENT_STATE_SENSITIVE_BASENAMES.test(path3.basename(normalizeForCompare(absPath)));
2335
1148
  }
2336
1149
  function pathLooksSensitive(rawPath) {
2337
1150
  const normalized = stripAdsSuffix(stripShellQuotes(rawPath).replace(/\\/g, "/"));
@@ -2689,173 +1502,6 @@ function explainPermissionTrace(state, tool, input, ctx) {
2689
1502
  };
2690
1503
  }
2691
1504
 
2692
- // src/security/permission-policy-schema.ts
2693
- var TRUST_POLICY_SCHEMA_VERSION = 1;
2694
- var TRUST_POLICY_LIMITS = Object.freeze({
2695
- maxTools: 1e3,
2696
- maxPatternsPerRule: 1e3,
2697
- maxToolNameChars: 256,
2698
- maxPatternChars: 4096
2699
- });
2700
- var TRUST_POLICY_JSON_SCHEMA = Object.freeze({
2701
- $schema: "https://json-schema.org/draft/2020-12/schema",
2702
- $id: "https://wrongstack.dev/schemas/trust-policy-v1.json",
2703
- title: "WrongStack Trust Policy",
2704
- type: "object",
2705
- maxProperties: TRUST_POLICY_LIMITS.maxTools,
2706
- propertyNames: {
2707
- minLength: 1,
2708
- maxLength: TRUST_POLICY_LIMITS.maxToolNameChars,
2709
- not: { enum: ["__proto__", "prototype", "constructor"] }
2710
- },
2711
- additionalProperties: {
2712
- type: "object",
2713
- additionalProperties: false,
2714
- properties: {
2715
- allow: {
2716
- type: "array",
2717
- maxItems: TRUST_POLICY_LIMITS.maxPatternsPerRule,
2718
- items: { type: "string", minLength: 1, maxLength: TRUST_POLICY_LIMITS.maxPatternChars }
2719
- },
2720
- deny: {
2721
- type: "array",
2722
- maxItems: TRUST_POLICY_LIMITS.maxPatternsPerRule,
2723
- items: { type: "string", minLength: 1, maxLength: TRUST_POLICY_LIMITS.maxPatternChars }
2724
- },
2725
- auto: { type: "boolean" },
2726
- trustWorkdir: { type: "boolean" },
2727
- denyPrivate: { type: "boolean" }
2728
- }
2729
- }
2730
- });
2731
- var RULE_FIELDS2 = /* @__PURE__ */ new Set(["allow", "deny", "auto", "trustWorkdir", "denyPrivate"]);
2732
- var UNSAFE_PROPERTY_NAMES2 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
2733
- function isRecord2(value) {
2734
- return typeof value === "object" && value !== null && !Array.isArray(value);
2735
- }
2736
- function error2(diagnostics, code, path9, message) {
2737
- diagnostics.push({ severity: "error", code, path: path9, message });
2738
- }
2739
- function validatePatterns(value, path9, diagnostics) {
2740
- if (!Array.isArray(value)) {
2741
- error2(diagnostics, "invalid_pattern_list", path9, "must be an array of strings");
2742
- return void 0;
2743
- }
2744
- if (value.length > TRUST_POLICY_LIMITS.maxPatternsPerRule) {
2745
- error2(
2746
- diagnostics,
2747
- "too_many_patterns",
2748
- path9,
2749
- `must contain at most ${TRUST_POLICY_LIMITS.maxPatternsPerRule} patterns`
2750
- );
2751
- return void 0;
2752
- }
2753
- const patterns = [];
2754
- const seen = /* @__PURE__ */ new Set();
2755
- for (const [index, pattern] of value.entries()) {
2756
- const itemPath = `${path9}[${index}]`;
2757
- if (typeof pattern !== "string" || pattern.length === 0 || pattern.length > TRUST_POLICY_LIMITS.maxPatternChars) {
2758
- error2(
2759
- diagnostics,
2760
- "invalid_pattern",
2761
- itemPath,
2762
- `must be a non-empty string up to ${TRUST_POLICY_LIMITS.maxPatternChars} characters`
2763
- );
2764
- continue;
2765
- }
2766
- if (seen.has(pattern)) {
2767
- diagnostics.push({
2768
- severity: "warning",
2769
- code: "duplicate_pattern",
2770
- path: itemPath,
2771
- message: "duplicates an earlier pattern and was normalized away"
2772
- });
2773
- continue;
2774
- }
2775
- seen.add(pattern);
2776
- patterns.push(pattern);
2777
- }
2778
- return patterns;
2779
- }
2780
- function validateTrustPolicy(value) {
2781
- const diagnostics = [];
2782
- if (!isRecord2(value)) {
2783
- return {
2784
- ok: false,
2785
- diagnostics: [
2786
- {
2787
- severity: "error",
2788
- code: "invalid_root",
2789
- path: "$",
2790
- message: "trust policy must be an object"
2791
- }
2792
- ]
2793
- };
2794
- }
2795
- const entries = Object.entries(value);
2796
- if (entries.length > TRUST_POLICY_LIMITS.maxTools) {
2797
- error2(
2798
- diagnostics,
2799
- "too_many_tools",
2800
- "$",
2801
- `must contain at most ${TRUST_POLICY_LIMITS.maxTools} tool rules`
2802
- );
2803
- }
2804
- const policy = {};
2805
- for (const [toolName, rawRule] of entries.slice(0, TRUST_POLICY_LIMITS.maxTools)) {
2806
- const toolPath = `$[${JSON.stringify(toolName)}]`;
2807
- if (toolName.length === 0 || toolName.length > TRUST_POLICY_LIMITS.maxToolNameChars) {
2808
- error2(
2809
- diagnostics,
2810
- "invalid_tool_name",
2811
- toolPath,
2812
- `tool name must be 1-${TRUST_POLICY_LIMITS.maxToolNameChars} characters`
2813
- );
2814
- continue;
2815
- }
2816
- if (UNSAFE_PROPERTY_NAMES2.has(toolName)) {
2817
- error2(
2818
- diagnostics,
2819
- "unsafe_tool_name",
2820
- toolPath,
2821
- "reserved object property names are not valid tool rules"
2822
- );
2823
- continue;
2824
- }
2825
- if (!isRecord2(rawRule)) {
2826
- error2(diagnostics, "invalid_rule", toolPath, "tool rule must be an object");
2827
- continue;
2828
- }
2829
- for (const field of Object.keys(rawRule)) {
2830
- if (!RULE_FIELDS2.has(field)) {
2831
- error2(
2832
- diagnostics,
2833
- "unknown_field",
2834
- `${toolPath}.${field}`,
2835
- `unknown policy field "${field}"`
2836
- );
2837
- }
2838
- }
2839
- const rule = {};
2840
- for (const field of ["allow", "deny"]) {
2841
- if (rawRule[field] === void 0) continue;
2842
- const patterns = validatePatterns(rawRule[field], `${toolPath}.${field}`, diagnostics);
2843
- if (patterns) rule[field] = patterns;
2844
- }
2845
- for (const field of ["auto", "trustWorkdir", "denyPrivate"]) {
2846
- const setting = rawRule[field];
2847
- if (setting === void 0) continue;
2848
- if (typeof setting !== "boolean") {
2849
- error2(diagnostics, "invalid_boolean", `${toolPath}.${field}`, "must be a boolean");
2850
- } else {
2851
- rule[field] = setting;
2852
- }
2853
- }
2854
- policy[toolName] = rule;
2855
- }
2856
- return diagnostics.some((diagnostic) => diagnostic.severity === "error") ? { ok: false, diagnostics } : { ok: true, policy, diagnostics };
2857
- }
2858
-
2859
1505
  // src/security/auto-approve-policy.ts
2860
1506
  var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
2861
1507
  allowedCapabilities;
@@ -2994,7 +1640,7 @@ var DefaultPermissionPolicy = class {
2994
1640
  if (!hasCapability(tool, ToolCapabilities.FS_WRITE)) return false;
2995
1641
  for (const targetPath of fsWriteTargetPaths(input)) {
2996
1642
  const base = ctx.workingDir ?? ctx.cwd;
2997
- const resolved = base ? path6.resolve(base, targetPath) : path6.resolve(targetPath);
1643
+ const resolved = base ? path4.resolve(base, targetPath) : path4.resolve(targetPath);
2998
1644
  if (isInsideAgentStateRoot(resolved)) return true;
2999
1645
  }
3000
1646
  return false;
@@ -3334,126 +1980,6 @@ var DefaultPermissionPolicy = class {
3334
1980
  }
3335
1981
  };
3336
1982
 
3337
- // src/security/readonly-permission-policy.ts
3338
- import * as path7 from "node:path";
3339
- var MUTATION_CAPABILITIES = /* @__PURE__ */ new Set([
3340
- ToolCapabilities.FS_WRITE,
3341
- ToolCapabilities.FS_WRITE_OUTSIDE_PROJECT,
3342
- ToolCapabilities.MEMORY_WRITE,
3343
- ToolCapabilities.MEMORY_DELETE,
3344
- ToolCapabilities.SHELL_ARBITRARY,
3345
- ToolCapabilities.SHELL_RESTRICTED,
3346
- ToolCapabilities.SHELL_EXEC,
3347
- ToolCapabilities.TOOL_MUTATE_ANY,
3348
- ToolCapabilities.CONFIG_MUTATE,
3349
- ToolCapabilities.PACKAGE_INSTALL,
3350
- ToolCapabilities.SUBAGENT_SPAWN
3351
- ]);
3352
- function toolMutates(tool) {
3353
- if (tool.mutating === true) return true;
3354
- return (tool.capabilities ?? []).some((c) => MUTATION_CAPABILITIES.has(c));
3355
- }
3356
- var hasMutationCapability = toolMutates;
3357
- function isAllowedReportFile(input, projectRoot) {
3358
- if (!input || typeof input !== "object") return false;
3359
- const record = input;
3360
- const inputPath = record["path"];
3361
- if (typeof inputPath !== "string") return false;
3362
- if (!inputPath.endsWith(".md")) return false;
3363
- try {
3364
- const resolved = path7.resolve(projectRoot, inputPath);
3365
- const tempDir = path7.resolve(projectRoot, ".temp_files");
3366
- if (!resolved.startsWith(tempDir + path7.sep) && resolved !== tempDir) return false;
3367
- return true;
3368
- } catch {
3369
- return false;
3370
- }
3371
- }
3372
- var ReadOnlyPermissionPolicy = class {
3373
- constructor(inner, projectRoot) {
3374
- this.inner = inner;
3375
- this.projectRoot = projectRoot;
3376
- }
3377
- inner;
3378
- projectRoot;
3379
- async evaluate(tool, input, ctx) {
3380
- if (ctx.meta["readOnly"] !== true) {
3381
- return this.inner.evaluate(tool, input, ctx);
3382
- }
3383
- if (!hasMutationCapability(tool)) {
3384
- return this.inner.evaluate(tool, input, ctx);
3385
- }
3386
- if (isAllowedReportFile(input, this.projectRoot)) {
3387
- return this.inner.evaluate(tool, input, ctx);
3388
- }
3389
- const matched = (tool.capabilities ?? []).filter((c) => MUTATION_CAPABILITIES.has(c));
3390
- const why = matched.length > 0 ? `requires mutation capabilities (${matched.join(", ")})` : "is declared mutating";
3391
- return {
3392
- permission: "deny",
3393
- source: "readonly_mode",
3394
- reason: `Session is in read-only mode: tool "${tool.name}" ${why}, which is not allowed. Only .md report files under .temp_files/ may be written.`
3395
- };
3396
- }
3397
- async trust(rule) {
3398
- return this.inner.trust(rule);
3399
- }
3400
- async deny(rule) {
3401
- return this.inner.deny(rule);
3402
- }
3403
- denyOnce(rule) {
3404
- this.inner.denyOnce(rule);
3405
- }
3406
- allowOnce(rule) {
3407
- this.inner.allowOnce(rule);
3408
- }
3409
- async reload() {
3410
- if ("reload" in this.inner && typeof this.inner.reload === "function") {
3411
- await this.inner.reload();
3412
- }
3413
- }
3414
- async explain(tool, input, ctx) {
3415
- if (ctx.meta["readOnly"] !== true) {
3416
- if (this.inner.explain) return this.inner.explain(tool, input, ctx);
3417
- const decision = await this.inner.evaluate(tool, input, ctx);
3418
- return {
3419
- toolName: tool.name,
3420
- subject: null,
3421
- steps: [{ rule: "inner", matched: true, decision: decision.permission === "deny" ? "deny" : "auto", source: decision.source, detail: decision.reason ?? "Inner policy decision" }],
3422
- winnerIndex: 0,
3423
- decision
3424
- };
3425
- }
3426
- if (!hasMutationCapability(tool) || isAllowedReportFile(input, this.projectRoot)) {
3427
- if (this.inner.explain) return this.inner.explain(tool, input, ctx);
3428
- const decision = await this.evaluate(tool, input, ctx);
3429
- return {
3430
- toolName: tool.name,
3431
- subject: null,
3432
- steps: [{ rule: "readonly_mode", matched: decision.permission === "deny", decision: decision.permission === "deny" ? "deny" : "auto", source: "readonly_mode", detail: decision.reason ?? "Read-only mode" }],
3433
- winnerIndex: 0,
3434
- decision
3435
- };
3436
- }
3437
- return {
3438
- toolName: tool.name,
3439
- subject: null,
3440
- steps: [{
3441
- rule: "readonly_mode",
3442
- matched: true,
3443
- decision: "deny",
3444
- source: "readonly_mode",
3445
- detail: `Read-only mode blocks ${tool.name} because it carries mutation capabilities (${(tool.capabilities ?? []).filter((c) => MUTATION_CAPABILITIES.has(c)).join(", ")}). Only .md files under .temp_files/ may be written.`
3446
- }],
3447
- winnerIndex: 0,
3448
- decision: {
3449
- permission: "deny",
3450
- source: "readonly_mode",
3451
- reason: `Read-only mode: ${tool.name} requires mutation capabilities.`
3452
- }
3453
- };
3454
- }
3455
- };
3456
-
3457
1983
  // src/security/totp.ts
3458
1984
  import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
3459
1985
  var BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
@@ -3572,620 +2098,6 @@ function verifyRecoveryCode(code, storedHashes) {
3572
2098
  return false;
3573
2099
  }
3574
2100
 
3575
- // src/security/secret-vault.ts
3576
- import { createCipheriv, createDecipheriv, randomBytes as randomBytes2, scryptSync } from "node:crypto";
3577
- import * as fs2 from "node:fs";
3578
- import * as fsp from "node:fs/promises";
3579
- import * as path8 from "node:path";
3580
-
3581
- // src/utils/deep-merge.ts
3582
- var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set([
3583
- "__proto__",
3584
- "constructor",
3585
- "prototype",
3586
- "__defineGetter__",
3587
- "__defineSetter__",
3588
- "__lookupGetter__",
3589
- "__lookupSetter__"
3590
- ]);
3591
- function isPrimitiveArray(a) {
3592
- return a.every((v) => v === null || typeof v !== "object" && typeof v !== "function");
3593
- }
3594
- function deepMerge(base, patch, options = {}) {
3595
- const {
3596
- conflictResolution = "prefer-patch",
3597
- arrayMode = "replace",
3598
- protectProto = true,
3599
- onNonPrimitiveArrayReplace
3600
- } = options;
3601
- if (typeof base !== "object" || base === null) {
3602
- return conflictResolution === "prefer-patch" ? patch : base;
3603
- }
3604
- if (typeof patch !== "object" || patch === null) {
3605
- return conflictResolution === "prefer-patch" ? patch : base;
3606
- }
3607
- if (Array.isArray(base) && Array.isArray(patch)) {
3608
- if (arrayMode === "concat-primitives" && isPrimitiveArray(base) && isPrimitiveArray(patch)) {
3609
- return [.../* @__PURE__ */ new Set([...base, ...patch])];
3610
- }
3611
- return conflictResolution === "prefer-patch" ? patch : base;
3612
- }
3613
- if (Array.isArray(base) || Array.isArray(patch)) {
3614
- return conflictResolution === "prefer-patch" ? patch : base;
3615
- }
3616
- const baseObj = base;
3617
- const patchObj = patch;
3618
- const out = { ...baseObj };
3619
- for (const [k, v] of Object.entries(patchObj)) {
3620
- if (protectProto && FORBIDDEN_PROTO_KEYS.has(k)) continue;
3621
- const existing = out[k];
3622
- if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
3623
- out[k] = deepMerge(existing, v, options);
3624
- } else if (Array.isArray(v) && Array.isArray(existing)) {
3625
- if (onNonPrimitiveArrayReplace && !isPrimitiveArray(v)) {
3626
- onNonPrimitiveArrayReplace(k, existing.length, v.length);
3627
- }
3628
- out[k] = deepMerge(existing, v, options);
3629
- } else if (v !== void 0) {
3630
- if (onNonPrimitiveArrayReplace && Array.isArray(v) && !isPrimitiveArray(v)) {
3631
- const existingLen = Array.isArray(existing) ? existing.length : 0;
3632
- onNonPrimitiveArrayReplace(k, existingLen, v.length);
3633
- }
3634
- out[k] = v;
3635
- }
3636
- }
3637
- return out;
3638
- }
3639
-
3640
- // src/security/secret-vault.ts
3641
- var KEY_BYTES = 32;
3642
- var IV_BYTES = 12;
3643
- var TAG_BYTES = 16;
3644
- var ALGO = "aes-256-gcm";
3645
- var KEY_FILE_MODE = 384;
3646
- var KEY_FILE_MAGIC = Buffer.from("WSKV", "ascii");
3647
- var VERSIONED_KEY_FILE_SIZE = KEY_FILE_MAGIC.length + 1 + KEY_BYTES;
3648
- var KEK_MAGIC = Buffer.from("WSKW", "ascii");
3649
- var KEK_SALT_BYTES = 16;
3650
- var WRAPPED_KEY_FILE_SIZE = KEK_MAGIC.length + 1 + KEK_SALT_BYTES + IV_BYTES + TAG_BYTES + KEY_BYTES;
3651
- var SCRYPT_N = 1 << 15;
3652
- var SCRYPT_R = 8;
3653
- var SCRYPT_P = 1;
3654
- var SCRYPT_MAXMEM = 64 * 1024 * 1024;
3655
- function getVaultPassphrase() {
3656
- const v = process.env["WRONGSTACK_VAULT_PASSPHRASE"];
3657
- return v && v.length > 0 ? v : void 0;
3658
- }
3659
- function isWrappedKeyFile(buf) {
3660
- return buf.length === WRAPPED_KEY_FILE_SIZE && buf.subarray(0, KEK_MAGIC.length).equals(KEK_MAGIC);
3661
- }
3662
- function deriveKEK(passphrase, salt) {
3663
- return scryptSync(passphrase, salt, KEY_BYTES, {
3664
- N: SCRYPT_N,
3665
- r: SCRYPT_R,
3666
- p: SCRYPT_P,
3667
- maxmem: SCRYPT_MAXMEM
3668
- });
3669
- }
3670
- function wrapDataKey(dataKey, keyVersion, passphrase) {
3671
- const salt = randomBytes2(KEK_SALT_BYTES);
3672
- const iv = randomBytes2(IV_BYTES);
3673
- const kek = deriveKEK(passphrase, salt);
3674
- const cipher = createCipheriv(ALGO, kek, iv);
3675
- const ct = Buffer.concat([cipher.update(dataKey), cipher.final()]);
3676
- const tag = cipher.getAuthTag();
3677
- const out = Buffer.alloc(WRAPPED_KEY_FILE_SIZE);
3678
- let off = 0;
3679
- KEK_MAGIC.copy(out, off);
3680
- off += KEK_MAGIC.length;
3681
- out[off] = keyVersion & 255;
3682
- off += 1;
3683
- salt.copy(out, off);
3684
- off += KEK_SALT_BYTES;
3685
- iv.copy(out, off);
3686
- off += IV_BYTES;
3687
- tag.copy(out, off);
3688
- off += TAG_BYTES;
3689
- ct.copy(out, off);
3690
- return out;
3691
- }
3692
- function unwrapDataKey(buf, keyFile) {
3693
- const passphrase = getVaultPassphrase();
3694
- if (!passphrase) {
3695
- throw new ConfigError({
3696
- message: `SecretVault: key file ${keyFile} is passphrase-protected \u2014 set the WRONGSTACK_VAULT_PASSPHRASE environment variable to unlock it.`,
3697
- code: ERROR_CODES.CONFIG_INVALID,
3698
- context: { keyFile }
3699
- });
3700
- }
3701
- let off = KEK_MAGIC.length;
3702
- const version = buf[off];
3703
- off += 1;
3704
- const salt = buf.subarray(off, off + KEK_SALT_BYTES);
3705
- off += KEK_SALT_BYTES;
3706
- const iv = buf.subarray(off, off + IV_BYTES);
3707
- off += IV_BYTES;
3708
- const tag = buf.subarray(off, off + TAG_BYTES);
3709
- off += TAG_BYTES;
3710
- const ct = buf.subarray(off, off + KEY_BYTES);
3711
- const kek = deriveKEK(passphrase, salt);
3712
- const decipher = createDecipheriv(ALGO, kek, iv);
3713
- decipher.setAuthTag(tag);
3714
- try {
3715
- const key = Buffer.concat([decipher.update(ct), decipher.final()]);
3716
- return { key: Buffer.from(key), version };
3717
- } catch {
3718
- throw new ConfigError({
3719
- message: `SecretVault: failed to unlock key file ${keyFile} \u2014 wrong WRONGSTACK_VAULT_PASSPHRASE (key unwrap authentication failed).`,
3720
- code: ERROR_CODES.CONFIG_INVALID,
3721
- context: { keyFile }
3722
- });
3723
- }
3724
- }
3725
- function keyFileNeedsHardening(keyFile, opts) {
3726
- if (process.platform === "win32") return false;
3727
- const warn = opts?.warn ?? ((msg) => console.warn(msg));
3728
- try {
3729
- const stat = fs2.statSync(keyFile);
3730
- const actualMode = stat.mode & 511;
3731
- if (actualMode !== KEY_FILE_MODE) {
3732
- warn(
3733
- `Key file ${keyFile} has mode ${actualMode.toString(8)} \u2014 expected ${KEY_FILE_MODE.toString(8)}. Hardening\u2026`
3734
- );
3735
- return true;
3736
- }
3737
- } catch {
3738
- }
3739
- return false;
3740
- }
3741
- function mkdirSecretDirSync(dir) {
3742
- fs2.mkdirSync(dir, { recursive: true, mode: 448 });
3743
- if (process.platform === "win32") return;
3744
- try {
3745
- fs2.chmodSync(dir, 448);
3746
- } catch {
3747
- }
3748
- }
3749
- function writeKeyFileAtomicSync(keyFile, content) {
3750
- const tmp = `${keyFile}.${randomBytes2(4).toString("hex")}.tmp`;
3751
- const fd = fs2.openSync(tmp, "w", 384);
3752
- try {
3753
- fs2.writeFileSync(fd, content);
3754
- try {
3755
- fs2.fsyncSync(fd);
3756
- } catch {
3757
- }
3758
- } finally {
3759
- fs2.closeSync(fd);
3760
- }
3761
- try {
3762
- fs2.renameSync(tmp, keyFile);
3763
- } catch (err) {
3764
- try {
3765
- fs2.unlinkSync(tmp);
3766
- } catch {
3767
- }
3768
- throw err;
3769
- }
3770
- }
3771
- var DefaultSecretVault = class {
3772
- keyFile;
3773
- logger;
3774
- key;
3775
- _keyVersion = 1;
3776
- /**
3777
- * Pending key-file hardening promises (Windows ACL restrictions via icacls).
3778
- * Tracked so async callers can `flushHardening()` before returning, ensuring
3779
- * a short-lived CLI process does not exit before the hardening resolves.
3780
- * Sync-only callers (encrypt/decrypt lazy-load) leave these pending —
3781
- * hardening remains best-effort in that case.
3782
- */
3783
- pendingHardening = [];
3784
- constructor(opts) {
3785
- this.keyFile = opts.keyFile;
3786
- this.logger = opts.logger;
3787
- }
3788
- /**
3789
- * Emit a structured warning. Uses the configured Logger when available;
3790
- * falls back to console.warn(JSON) so warnings are never silently dropped
3791
- * during early boot.
3792
- */
3793
- logWarn(msg, ctx) {
3794
- if (this.logger) {
3795
- this.logger.warn(msg, ctx);
3796
- } else {
3797
- console.warn(JSON.stringify({ ...ctx, message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
3798
- }
3799
- }
3800
- /**
3801
- * Schedule best-effort key-file hardening and track the promise so async
3802
- * callers can await it via `flushHardening()` before returning. Rejections
3803
- * are swallowed here (the helper already warns on failure) so a rejection
3804
- * can never become an unhandled one.
3805
- */
3806
- scheduleKeyHardening() {
3807
- const p = restrictFilePermissions(this.keyFile, {
3808
- label: "secret-vault-key",
3809
- warn: (msg) => this.logWarn(msg)
3810
- }).catch(() => void 0);
3811
- this.pendingHardening.push(p);
3812
- }
3813
- /** Detect and schedule repair of a pre-existing POSIX key with loose mode bits. */
3814
- checkKeyFilePermissions() {
3815
- if (keyFileNeedsHardening(this.keyFile, {
3816
- warn: (msg) => this.logWarn(msg)
3817
- })) {
3818
- this.scheduleKeyHardening();
3819
- }
3820
- }
3821
- /** Flush all pending key-file hardening promises. */
3822
- flushHardening() {
3823
- if (this.pendingHardening.length === 0) return Promise.resolve();
3824
- const all = Promise.all(this.pendingHardening).then(() => void 0);
3825
- this.pendingHardening = [];
3826
- return all;
3827
- }
3828
- /** Current key version. Starts at 1; incremented by rotateKey(). */
3829
- get keyVersion() {
3830
- if (!this.key) this.loadOrCreateKey();
3831
- return this._keyVersion;
3832
- }
3833
- isEncrypted(value) {
3834
- return typeof value === "string" && ENCRYPTED_PREFIX_PATTERN.test(value);
3835
- }
3836
- encrypt(plaintext) {
3837
- if (this.isEncrypted(plaintext)) return plaintext;
3838
- const key = this.loadOrCreateKey();
3839
- const iv = randomBytes2(IV_BYTES);
3840
- const cipher = createCipheriv(ALGO, key, iv);
3841
- const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
3842
- const tag = cipher.getAuthTag();
3843
- const prefix = encryptedPrefixForVersion(this._keyVersion);
3844
- return `${prefix}${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
3845
- }
3846
- decrypt(value) {
3847
- if (!this.isEncrypted(value)) return value;
3848
- const prefixMatch = value.match(ENCRYPTED_PREFIX_PATTERN);
3849
- if (!prefixMatch) {
3850
- throw new ConfigError({
3851
- message: "SecretVault: malformed encrypted value",
3852
- code: ERROR_CODES.CONFIG_PARSE_FAILED,
3853
- context: { field: "encrypted_value" }
3854
- });
3855
- }
3856
- const rest = value.slice(prefixMatch[0].length);
3857
- const parts = rest.split(":");
3858
- if (parts.length !== 3) {
3859
- throw new ConfigError({
3860
- message: "SecretVault: malformed encrypted value",
3861
- code: ERROR_CODES.CONFIG_PARSE_FAILED,
3862
- context: { field: "encrypted_value" }
3863
- });
3864
- }
3865
- const [ivB64, tagB64, ctB64] = parts;
3866
- const iv = Buffer.from(ivB64, "base64");
3867
- const tag = Buffer.from(tagB64, "base64");
3868
- const ct = Buffer.from(ctB64, "base64");
3869
- if (iv.length !== IV_BYTES)
3870
- throw new ConfigError({
3871
- message: "SecretVault: bad IV length",
3872
- code: ERROR_CODES.CONFIG_PARSE_FAILED,
3873
- context: { expected: IV_BYTES, actual: iv.length }
3874
- });
3875
- if (tag.length !== TAG_BYTES)
3876
- throw new ConfigError({
3877
- message: "SecretVault: bad tag length",
3878
- code: ERROR_CODES.CONFIG_PARSE_FAILED,
3879
- context: { expected: TAG_BYTES, actual: tag.length }
3880
- });
3881
- const key = this.loadOrCreateKey();
3882
- const decipher = createDecipheriv(ALGO, key, iv);
3883
- decipher.setAuthTag(tag);
3884
- const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
3885
- return pt.toString("utf8");
3886
- }
3887
- /**
3888
- * Generate a new encryption key, write it to disk, and increment the key version.
3889
- * After rotation, encrypt() emits the new version prefix (e.g. enc:v2:).
3890
- * The caller must re-encrypt existing config values (see rotateConfigKeys()).
3891
- */
3892
- rotateKey() {
3893
- const oldVersion = this._keyVersion;
3894
- const newKey = randomBytes2(KEY_BYTES);
3895
- const newVersion = oldVersion + 1;
3896
- mkdirSecretDirSync(path8.dirname(this.keyFile));
3897
- const passphrase = getVaultPassphrase();
3898
- if (passphrase) {
3899
- writeKeyFileAtomicSync(this.keyFile, wrapDataKey(newKey, newVersion, passphrase));
3900
- } else {
3901
- const keyFileBuf = Buffer.alloc(VERSIONED_KEY_FILE_SIZE);
3902
- KEY_FILE_MAGIC.copy(keyFileBuf, 0);
3903
- keyFileBuf[KEY_FILE_MAGIC.length] = newVersion;
3904
- newKey.copy(keyFileBuf, KEY_FILE_MAGIC.length + 1);
3905
- writeKeyFileAtomicSync(this.keyFile, keyFileBuf);
3906
- }
3907
- this.scheduleKeyHardening();
3908
- this.checkKeyFilePermissions();
3909
- this.key = newKey;
3910
- this._keyVersion = newVersion;
3911
- return { oldVersion, newVersion };
3912
- }
3913
- /**
3914
- * If WRONGSTACK_VAULT_PASSPHRASE is set but the key on disk is still stored
3915
- * unwrapped (legacy v1 / versioned v2), re-write it in passphrase-wrapped (v3)
3916
- * form. The data key is preserved, so all existing ciphertext keeps
3917
- * decrypting. Best-effort: a write failure leaves the working unwrapped file
3918
- * in place and is not fatal to load.
3919
- */
3920
- migrateToWrappedIfPassphrase() {
3921
- const passphrase = getVaultPassphrase();
3922
- if (!passphrase || !this.key) return;
3923
- try {
3924
- writeKeyFileAtomicSync(this.keyFile, wrapDataKey(this.key, this._keyVersion, passphrase));
3925
- this.scheduleKeyHardening();
3926
- this.checkKeyFilePermissions();
3927
- } catch {
3928
- }
3929
- }
3930
- loadOrCreateKey() {
3931
- if (this.key) return this.key;
3932
- try {
3933
- const buf = fs2.readFileSync(this.keyFile);
3934
- if (isWrappedKeyFile(buf)) {
3935
- const { key: key2, version } = unwrapDataKey(buf, this.keyFile);
3936
- this.key = key2;
3937
- this._keyVersion = version;
3938
- this.checkKeyFilePermissions();
3939
- return this.key;
3940
- }
3941
- if (buf.length === KEY_BYTES) {
3942
- this.key = buf;
3943
- this._keyVersion = 1;
3944
- this.checkKeyFilePermissions();
3945
- this.migrateToWrappedIfPassphrase();
3946
- return this.key;
3947
- }
3948
- if (buf.length === VERSIONED_KEY_FILE_SIZE) {
3949
- const magic = buf.subarray(0, KEY_FILE_MAGIC.length);
3950
- if (!magic.equals(KEY_FILE_MAGIC)) {
3951
- throw new ConfigError({
3952
- message: `SecretVault: key file ${this.keyFile} has invalid magic header`,
3953
- code: ERROR_CODES.CONFIG_INVALID,
3954
- context: { keyFile: this.keyFile }
3955
- });
3956
- }
3957
- const version = buf[KEY_FILE_MAGIC.length];
3958
- const key2 = buf.subarray(KEY_FILE_MAGIC.length + 1);
3959
- if (key2.length !== KEY_BYTES) {
3960
- throw new ConfigError({
3961
- message: `SecretVault: key file ${this.keyFile} has wrong key size (${key2.length} bytes, expected ${KEY_BYTES})`,
3962
- code: ERROR_CODES.CONFIG_INVALID,
3963
- context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: key2.length }
3964
- });
3965
- }
3966
- this.key = Buffer.from(key2);
3967
- this._keyVersion = version;
3968
- this.checkKeyFilePermissions();
3969
- this.migrateToWrappedIfPassphrase();
3970
- return this.key;
3971
- }
3972
- throw new ConfigError({
3973
- message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES} for v1 or ${VERSIONED_KEY_FILE_SIZE} for v2+). Remove it manually to generate a new key.`,
3974
- code: ERROR_CODES.CONFIG_INVALID,
3975
- context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
3976
- });
3977
- } catch (err) {
3978
- if (err.code !== "ENOENT") throw err;
3979
- }
3980
- mkdirSecretDirSync(path8.dirname(this.keyFile));
3981
- const key = randomBytes2(KEY_BYTES);
3982
- const passphrase = getVaultPassphrase();
3983
- const initialBytes = passphrase ? wrapDataKey(key, 1, passphrase) : key;
3984
- try {
3985
- fs2.writeFileSync(this.keyFile, initialBytes, { mode: 384, flag: "wx" });
3986
- this.scheduleKeyHardening();
3987
- } catch (err) {
3988
- if (err.code !== "EEXIST") throw err;
3989
- this.scheduleKeyHardening();
3990
- const buf = fs2.readFileSync(this.keyFile);
3991
- if (isWrappedKeyFile(buf)) {
3992
- const { key: winnerKey, version } = unwrapDataKey(buf, this.keyFile);
3993
- this.key = winnerKey;
3994
- this._keyVersion = version;
3995
- this.checkKeyFilePermissions();
3996
- return this.key;
3997
- }
3998
- if (buf.length === KEY_BYTES) {
3999
- this.key = buf;
4000
- this._keyVersion = 1;
4001
- this.checkKeyFilePermissions();
4002
- return this.key;
4003
- }
4004
- if (buf.length === VERSIONED_KEY_FILE_SIZE) {
4005
- const magic = buf.subarray(0, KEY_FILE_MAGIC.length);
4006
- if (!magic.equals(KEY_FILE_MAGIC)) {
4007
- throw new ConfigError({
4008
- message: `SecretVault: key file ${this.keyFile} has invalid magic header`,
4009
- code: ERROR_CODES.CONFIG_INVALID,
4010
- context: { keyFile: this.keyFile }
4011
- });
4012
- }
4013
- const version = buf[KEY_FILE_MAGIC.length];
4014
- const winnerKey = buf.subarray(KEY_FILE_MAGIC.length + 1);
4015
- this.key = Buffer.from(winnerKey);
4016
- this._keyVersion = version;
4017
- this.checkKeyFilePermissions();
4018
- return this.key;
4019
- }
4020
- throw new ConfigError({
4021
- message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES} for v1 or ${VERSIONED_KEY_FILE_SIZE} for v2+). Remove it manually to generate a new key.`,
4022
- code: ERROR_CODES.CONFIG_INVALID,
4023
- context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
4024
- });
4025
- }
4026
- this.key = key;
4027
- this._keyVersion = 1;
4028
- return key;
4029
- }
4030
- };
4031
- async function rewriteConfigEncrypted(configPath, vault, patch) {
4032
- let current = {};
4033
- try {
4034
- const raw = await fsp.readFile(configPath, "utf8");
4035
- current = JSON.parse(raw);
4036
- } catch {
4037
- }
4038
- const merged = deepMerge(current, patch ?? {});
4039
- const encrypted = encryptConfigSecrets(merged, vault);
4040
- await fsp.mkdir(path8.dirname(configPath), { recursive: true });
4041
- await atomicWrite(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
4042
- await restrictFilePermissions2(configPath);
4043
- await vault.flushHardening?.();
4044
- }
4045
- async function migratePlaintextSecrets(configPath, vault, logger) {
4046
- let raw;
4047
- try {
4048
- raw = await fsp.readFile(configPath, "utf8");
4049
- } catch {
4050
- return { migrated: 0, file: configPath };
4051
- }
4052
- let parsed;
4053
- try {
4054
- parsed = JSON.parse(raw);
4055
- } catch {
4056
- return { migrated: 0, file: configPath };
4057
- }
4058
- const counter = { n: 0 };
4059
- const migrated = walkCount(parsed, vault, counter);
4060
- if (counter.n === 0) return { migrated: 0, file: configPath };
4061
- await atomicWrite(configPath, JSON.stringify(migrated, null, 2), { mode: 384 });
4062
- await restrictFilePermissions2(
4063
- configPath,
4064
- logger ? { warn: (msg) => logger.warn(msg) } : void 0
4065
- );
4066
- await vault.flushHardening?.();
4067
- return { migrated: counter.n, file: configPath };
4068
- }
4069
- async function rotateConfigKeys(configPath, vault, logger) {
4070
- const log = logger?.info ?? (() => {
4071
- });
4072
- const warn = logger?.warn ?? ((msg) => console.warn(msg));
4073
- let raw;
4074
- try {
4075
- raw = await fsp.readFile(configPath, "utf8");
4076
- } catch {
4077
- const { oldVersion: oldVersion2, newVersion: newVersion2 } = vault.rotateKey();
4078
- log(
4079
- `[secret-vault] Key rotated (v${oldVersion2} \u2192 v${newVersion2}) \u2014 no config file to re-encrypt`
4080
- );
4081
- await vault.flushHardening?.();
4082
- return { rotated: 0, oldVersion: oldVersion2, newVersion: newVersion2, file: configPath };
4083
- }
4084
- let parsed;
4085
- try {
4086
- parsed = JSON.parse(raw);
4087
- } catch {
4088
- warn(`[secret-vault] Config file ${configPath} is not valid JSON \u2014 skipping rotation`);
4089
- return {
4090
- rotated: 0,
4091
- oldVersion: vault.keyVersion,
4092
- newVersion: vault.keyVersion,
4093
- file: configPath
4094
- };
4095
- }
4096
- const counter = { n: 0, failed: [] };
4097
- const decrypted = walkDecryptCount(parsed, vault, counter);
4098
- if (counter.failed.length > 0) {
4099
- throw new Error(
4100
- `[secret-vault] Aborting key rotation: ${counter.failed.length} field(s) could not be decrypted with the current key and would be permanently lost on rotation: ${counter.failed.join(", ")}. Restore or remove these fields before rotating.`
4101
- );
4102
- }
4103
- if (counter.n === 0) {
4104
- const { oldVersion: oldVersion2, newVersion: newVersion2 } = vault.rotateKey();
4105
- log(
4106
- `[secret-vault] Key rotated (v${oldVersion2} \u2192 v${newVersion2}) \u2014 no encrypted fields to re-encrypt`
4107
- );
4108
- await vault.flushHardening?.();
4109
- return { rotated: 0, oldVersion: oldVersion2, newVersion: newVersion2, file: configPath };
4110
- }
4111
- const { oldVersion, newVersion } = vault.rotateKey();
4112
- const reencrypted = walkReencrypt(decrypted, vault);
4113
- await atomicWrite(configPath, JSON.stringify(reencrypted, null, 2), { mode: 384 });
4114
- await restrictFilePermissions2(configPath, { warn });
4115
- log(
4116
- `[secret-vault] Key rotated (v${oldVersion} \u2192 v${newVersion}) \u2014 re-encrypted ${counter.n} field(s)`
4117
- );
4118
- await vault.flushHardening?.();
4119
- return { rotated: counter.n, oldVersion, newVersion, file: configPath };
4120
- }
4121
- function walkDecryptCount(node, vault, counter, pathPrefix = "") {
4122
- if (node === null || node === void 0) return node;
4123
- if (typeof node !== "object") return node;
4124
- if (Array.isArray(node)) {
4125
- return node.map(
4126
- (item, i) => walkDecryptCount(item, vault, counter, `${pathPrefix}[${i}]`)
4127
- );
4128
- }
4129
- const out = /* @__PURE__ */ Object.create(null);
4130
- for (const [k, v] of Object.entries(node)) {
4131
- const keyPath = pathPrefix ? `${pathPrefix}.${k}` : k;
4132
- if (typeof v === "string" && vault.isEncrypted(v)) {
4133
- try {
4134
- out[k] = vault.decrypt(v);
4135
- counter.n++;
4136
- } catch {
4137
- counter.failed.push(keyPath);
4138
- out[k] = v;
4139
- }
4140
- } else if (typeof v === "object" && v !== null) {
4141
- out[k] = walkDecryptCount(v, vault, counter, keyPath);
4142
- } else {
4143
- out[k] = v;
4144
- }
4145
- }
4146
- return out;
4147
- }
4148
- function walkReencrypt(node, vault) {
4149
- if (node === null || node === void 0) return node;
4150
- if (typeof node !== "object") return node;
4151
- if (Array.isArray(node)) {
4152
- return node.map((item) => walkReencrypt(item, vault));
4153
- }
4154
- const out = /* @__PURE__ */ Object.create(null);
4155
- for (const [k, v] of Object.entries(node)) {
4156
- if (typeof v === "string" && isSecretField(k) && v.length > 0 && !vault.isEncrypted(v)) {
4157
- out[k] = vault.encrypt(v);
4158
- } else if (typeof v === "object" && v !== null) {
4159
- out[k] = walkReencrypt(v, vault);
4160
- } else {
4161
- out[k] = v;
4162
- }
4163
- }
4164
- return out;
4165
- }
4166
- async function restrictFilePermissions2(filePath, opts) {
4167
- await restrictFilePermissions(filePath, { label: "secret-vault", warn: opts?.warn });
4168
- }
4169
- function walkCount(node, vault, counter) {
4170
- if (node === null || node === void 0) return node;
4171
- if (typeof node !== "object") return node;
4172
- if (Array.isArray(node)) {
4173
- return node.map((item) => walkCount(item, vault, counter));
4174
- }
4175
- const out = /* @__PURE__ */ Object.create(null);
4176
- for (const [k, v] of Object.entries(node)) {
4177
- if (typeof v === "string" && isSecretField(k) && !vault.isEncrypted(v) && v.length > 0) {
4178
- out[k] = vault.encrypt(v);
4179
- counter.n++;
4180
- } else if (typeof v === "object" && v !== null) {
4181
- out[k] = walkCount(v, vault, counter);
4182
- } else {
4183
- out[k] = v;
4184
- }
4185
- }
4186
- return out;
4187
- }
4188
-
4189
2101
  // src/security/trust-boundary.ts
4190
2102
  var TRUST_BOUNDARY_VERSION = 1;
4191
2103
  function createCompatibilityTrustBoundary(options = {}) {
@@ -4256,36 +2168,36 @@ var RESERVED_BYPASS_FIELDS = /* @__PURE__ */ new Set([
4256
2168
  "skipAuthorization",
4257
2169
  "skip_authorization"
4258
2170
  ]);
4259
- function isRecord3(value) {
2171
+ function isRecord2(value) {
4260
2172
  return typeof value === "object" && value !== null && !Array.isArray(value);
4261
2173
  }
4262
- function addString(issues, value, path9, required = true) {
2174
+ function addString(issues, value, path5, required = true) {
4263
2175
  if (typeof value === "string" && value.trim().length > 0) return true;
4264
2176
  if (!required && value === void 0) return false;
4265
- issues.push({ path: path9, message: "must be a non-empty string" });
2177
+ issues.push({ path: path5, message: "must be a non-empty string" });
4266
2178
  return false;
4267
2179
  }
4268
- function addEnum(issues, value, path9, values) {
2180
+ function addEnum(issues, value, path5, values) {
4269
2181
  if (typeof value === "string" && values.has(value)) return true;
4270
- issues.push({ path: path9, message: `must be one of: ${[...values].join(", ")}` });
2182
+ issues.push({ path: path5, message: `must be one of: ${[...values].join(", ")}` });
4271
2183
  return false;
4272
2184
  }
4273
- function validateAttributes(issues, value, path9) {
4274
- if (!isRecord3(value)) {
4275
- issues.push({ path: path9, message: "must be an object" });
2185
+ function validateAttributes(issues, value, path5) {
2186
+ if (!isRecord2(value)) {
2187
+ issues.push({ path: path5, message: "must be an object" });
4276
2188
  return false;
4277
2189
  }
4278
2190
  let valid = true;
4279
2191
  for (const [key, item] of Object.entries(value)) {
4280
2192
  if (RESERVED_BYPASS_FIELDS.has(key)) {
4281
2193
  issues.push({
4282
- path: `${path9}.${key}`,
2194
+ path: `${path5}.${key}`,
4283
2195
  message: "generic authorization bypass fields are forbidden"
4284
2196
  });
4285
2197
  valid = false;
4286
2198
  } else if (item !== null && !["string", "number", "boolean"].includes(typeof item)) {
4287
2199
  issues.push({
4288
- path: `${path9}.${key}`,
2200
+ path: `${path5}.${key}`,
4289
2201
  message: "must be a string, number, boolean, or null"
4290
2202
  });
4291
2203
  valid = false;
@@ -4293,21 +2205,21 @@ function validateAttributes(issues, value, path9) {
4293
2205
  }
4294
2206
  return valid;
4295
2207
  }
4296
- function validateScope(issues, value, path9) {
4297
- if (!isRecord3(value)) {
4298
- issues.push({ path: path9, message: "must be an object" });
2208
+ function validateScope(issues, value, path5) {
2209
+ if (!isRecord2(value)) {
2210
+ issues.push({ path: path5, message: "must be an object" });
4299
2211
  return false;
4300
2212
  }
4301
2213
  for (const field of ["projectId", "sessionId", "cwd"]) {
4302
- if (value[field] !== void 0) addString(issues, value[field], `${path9}.${field}`);
2214
+ if (value[field] !== void 0) addString(issues, value[field], `${path5}.${field}`);
4303
2215
  }
4304
2216
  if (value.constraints !== void 0)
4305
- validateAttributes(issues, value.constraints, `${path9}.constraints`);
2217
+ validateAttributes(issues, value.constraints, `${path5}.constraints`);
4306
2218
  return true;
4307
2219
  }
4308
2220
  function decodeTrustBoundaryRequest(input) {
4309
2221
  const issues = [];
4310
- if (!isRecord3(input)) return { ok: false, issues: [{ path: "$", message: "must be an object" }] };
2222
+ if (!isRecord2(input)) return { ok: false, issues: [{ path: "$", message: "must be an object" }] };
4311
2223
  for (const field of RESERVED_BYPASS_FIELDS) {
4312
2224
  if (field in input)
4313
2225
  issues.push({
@@ -4321,14 +2233,14 @@ function decodeTrustBoundaryRequest(input) {
4321
2233
  addEnum(issues, input.surface, "$.surface", SURFACES);
4322
2234
  addString(issues, input.capability, "$.capability");
4323
2235
  addEnum(issues, input.risk, "$.risk", RISKS);
4324
- if (!isRecord3(input.actor)) issues.push({ path: "$.actor", message: "must be an object" });
2236
+ if (!isRecord2(input.actor)) issues.push({ path: "$.actor", message: "must be an object" });
4325
2237
  else {
4326
2238
  addEnum(issues, input.actor.kind, "$.actor.kind", ACTORS);
4327
2239
  if (input.actor.id !== void 0) addString(issues, input.actor.id, "$.actor.id");
4328
2240
  if (input.actor.sessionId !== void 0)
4329
2241
  addString(issues, input.actor.sessionId, "$.actor.sessionId");
4330
2242
  }
4331
- if (!isRecord3(input.subject)) issues.push({ path: "$.subject", message: "must be an object" });
2243
+ if (!isRecord2(input.subject)) issues.push({ path: "$.subject", message: "must be an object" });
4332
2244
  else {
4333
2245
  addEnum(issues, input.subject.kind, "$.subject.kind", SUBJECT_KINDS);
4334
2246
  addString(issues, input.subject.id, "$.subject.id");
@@ -4338,7 +2250,7 @@ function decodeTrustBoundaryRequest(input) {
4338
2250
  validateScope(issues, input.scope, "$.scope");
4339
2251
  if (input.metadata !== void 0) validateAttributes(issues, input.metadata, "$.metadata");
4340
2252
  if (input.authContext !== void 0) {
4341
- if (!isRecord3(input.authContext))
2253
+ if (!isRecord2(input.authContext))
4342
2254
  issues.push({ path: "$.authContext", message: "must be an object" });
4343
2255
  else {
4344
2256
  addEnum(issues, input.authContext.method, "$.authContext.method", AUTH_METHODS);
@@ -4354,7 +2266,7 @@ function decodeTrustBoundaryRequest(input) {
4354
2266
  }
4355
2267
  function decodeTrustBoundaryDecision(input) {
4356
2268
  const issues = [];
4357
- if (!isRecord3(input)) return { ok: false, issues: [{ path: "$", message: "must be an object" }] };
2269
+ if (!isRecord2(input)) return { ok: false, issues: [{ path: "$", message: "must be an object" }] };
4358
2270
  if (!["allow", "deny", "confirm", "scoped-token"].includes(String(input.kind))) {
4359
2271
  issues.push({ path: "$.kind", message: "must be one of: allow, deny, confirm, scoped-token" });
4360
2272
  }