@wrongstack/core 0.313.0 → 0.316.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 (343) hide show
  1. package/dist/chronicle/index.js +84 -6391
  2. package/dist/chronicle/legacy-journal-import.d.ts +0 -6
  3. package/dist/chronicle/metrics-ingest.d.ts +14 -0
  4. package/dist/chronicle/metrics-schema.d.ts +10 -1
  5. package/dist/chronicle/metrics-store.d.ts +20 -2
  6. package/dist/chronicle/project-server.js +98 -4874
  7. package/dist/chronicle/sink.d.ts +13 -0
  8. package/dist/chronicle/sqlite-journal.d.ts +20 -2
  9. package/dist/chunk-25LSLC3G.js +334 -0
  10. package/dist/chunk-27QIURLN.js +1073 -0
  11. package/dist/chunk-2FK4WTFF.js +207 -0
  12. package/dist/chunk-2YU4WVBW.js +134 -0
  13. package/dist/chunk-3AGWVSS7.js +4608 -0
  14. package/dist/chunk-3BDV7RL3.js +211 -0
  15. package/dist/chunk-3ECENVHL.js +325 -0
  16. package/dist/chunk-3XPVB7BA.js +135 -0
  17. package/dist/chunk-45WI72ZB.js +16884 -0
  18. package/dist/chunk-4BMVPOHO.js +3748 -0
  19. package/dist/chunk-4I7HGVVA.js +363 -0
  20. package/dist/chunk-4LCZVJVF.js +317 -0
  21. package/dist/chunk-4SJNS7OB.js +217 -0
  22. package/dist/chunk-4VWO3ZXL.js +342 -0
  23. package/dist/chunk-57GEWK5X.js +26 -0
  24. package/dist/chunk-5BMIE7QG.js +46 -0
  25. package/dist/chunk-5S437PRC.js +50 -0
  26. package/dist/chunk-5XDUGTKN.js +511 -0
  27. package/dist/chunk-5YPSXQXM.js +1443 -0
  28. package/dist/chunk-6FHWXLPT.js +10 -0
  29. package/dist/chunk-6H5GEOET.js +2406 -0
  30. package/dist/chunk-6NWEVFEL.js +151 -0
  31. package/dist/chunk-AYW5YINX.js +58 -0
  32. package/dist/chunk-B4AMLZPB.js +9 -0
  33. package/dist/chunk-B7F4WVP4.js +65 -0
  34. package/dist/chunk-BJHVULHO.js +55 -0
  35. package/dist/chunk-BPBKKBUK.js +2085 -0
  36. package/dist/chunk-BYD345OX.js +589 -0
  37. package/dist/chunk-C27KDLOX.js +279 -0
  38. package/dist/chunk-CU546ESW.js +597 -0
  39. package/dist/chunk-DLHHXUAM.js +172 -0
  40. package/dist/chunk-DOTHQSJ2.js +429 -0
  41. package/dist/chunk-DYLPMV4Q.js +711 -0
  42. package/dist/chunk-EODDVJBM.js +82 -0
  43. package/dist/chunk-F3JWNST7.js +361 -0
  44. package/dist/chunk-FC2ZGRF3.js +140 -0
  45. package/dist/chunk-FCBTO76Z.js +5786 -0
  46. package/dist/chunk-FHZVFYSL.js +396 -0
  47. package/dist/chunk-FNXVT7HF.js +52 -0
  48. package/dist/chunk-FWNLEZYG.js +259 -0
  49. package/dist/chunk-FXQLIX3K.js +211 -0
  50. package/dist/chunk-G5S3S5ZN.js +38 -0
  51. package/dist/chunk-GDQ7CZRQ.js +116 -0
  52. package/dist/chunk-GJJGJB2R.js +1374 -0
  53. package/dist/chunk-GTCU6DZ3.js +1 -0
  54. package/dist/chunk-HA4ZYA6T.js +178 -0
  55. package/dist/chunk-HI3JGZR2.js +188 -0
  56. package/dist/chunk-HMLIGAU7.js +395 -0
  57. package/dist/chunk-HXKZ7OBQ.js +303 -0
  58. package/dist/chunk-I7DALG7J.js +292 -0
  59. package/dist/chunk-IRUTEHCE.js +960 -0
  60. package/dist/chunk-IYFBFX34.js +86 -0
  61. package/dist/chunk-J2WRWR37.js +1104 -0
  62. package/dist/chunk-JFPJ6LLQ.js +129 -0
  63. package/dist/chunk-JMULFFLN.js +9 -0
  64. package/dist/chunk-JO4MGB6Z.js +1264 -0
  65. package/dist/chunk-JVKB2TBL.js +48 -0
  66. package/dist/chunk-JX7VBZDK.js +31 -0
  67. package/dist/chunk-K6BXGITA.js +931 -0
  68. package/dist/chunk-KDQJCT2J.js +45 -0
  69. package/dist/chunk-KJ2ZTQSL.js +47 -0
  70. package/dist/chunk-KZMSZV32.js +187 -0
  71. package/dist/chunk-LA7CLDRW.js +129 -0
  72. package/dist/chunk-LHOA5DWA.js +28 -0
  73. package/dist/chunk-LSMMOS2Z.js +119 -0
  74. package/dist/chunk-LX5QWLBL.js +27 -0
  75. package/dist/chunk-LXWCMQNJ.js +789 -0
  76. package/dist/chunk-M27LIEKV.js +2829 -0
  77. package/dist/chunk-MJPHJRAL.js +663 -0
  78. package/dist/chunk-MTL4O5YR.js +228 -0
  79. package/dist/chunk-NDBIPFNY.js +102 -0
  80. package/dist/chunk-NEMIWEXC.js +1199 -0
  81. package/dist/chunk-NHJ75HXY.js +1 -0
  82. package/dist/chunk-NIOAGFPB.js +1510 -0
  83. package/dist/chunk-NS52ISWS.js +355 -0
  84. package/dist/chunk-NSOFPKGT.js +5352 -0
  85. package/dist/chunk-NVIZG63R.js +757 -0
  86. package/dist/chunk-O37BQ65S.js +21 -0
  87. package/dist/chunk-OOYZPOXM.js +308 -0
  88. package/dist/chunk-OSGLYW5J.js +28 -0
  89. package/dist/chunk-PFCMSSUF.js +1640 -0
  90. package/dist/chunk-PFZO3C6H.js +14 -0
  91. package/dist/chunk-PGRVXIMY.js +158 -0
  92. package/dist/chunk-PHH7LD45.js +113 -0
  93. package/dist/chunk-PISBFBJN.js +109 -0
  94. package/dist/chunk-POOOIWE5.js +9 -0
  95. package/dist/chunk-PQNS3U7Z.js +1240 -0
  96. package/dist/chunk-QSTSJOCK.js +402 -0
  97. package/dist/chunk-RFQMOKWK.js +282 -0
  98. package/dist/chunk-RJIZEJ42.js +30 -0
  99. package/dist/chunk-RTOTNK3P.js +484 -0
  100. package/dist/chunk-RXKSDUAU.js +1172 -0
  101. package/dist/chunk-S45JPKQN.js +3317 -0
  102. package/dist/chunk-SAFHACFA.js +152 -0
  103. package/dist/chunk-SQJVFEBV.js +41 -0
  104. package/dist/chunk-SVT4RNTR.js +49 -0
  105. package/dist/chunk-SWUFN7XM.js +43 -0
  106. package/dist/chunk-SXMVYC5H.js +174 -0
  107. package/dist/chunk-SZ6AYUIZ.js +23 -0
  108. package/dist/chunk-TBRL2RCC.js +9 -0
  109. package/dist/chunk-TCCHINEF.js +655 -0
  110. package/dist/chunk-THEGTRAG.js +316 -0
  111. package/dist/chunk-TR4LQE7L.js +2256 -0
  112. package/dist/chunk-TRT2HRWZ.js +18 -0
  113. package/dist/chunk-TWJMGXCI.js +900 -0
  114. package/dist/chunk-U45AXT5D.js +1023 -0
  115. package/dist/chunk-U4FWBSO3.js +509 -0
  116. package/dist/chunk-UBGDX6FI.js +147 -0
  117. package/dist/chunk-UJILKYES.js +75 -0
  118. package/dist/chunk-ULMAQV3T.js +234 -0
  119. package/dist/chunk-VVBP6KGT.js +9 -0
  120. package/dist/chunk-W5VITDM2.js +168 -0
  121. package/dist/chunk-WAHQVNWT.js +194 -0
  122. package/dist/chunk-WBPVKEEE.js +227 -0
  123. package/dist/chunk-WGGYRBXH.js +5434 -0
  124. package/dist/chunk-WLYKU45P.js +5770 -0
  125. package/dist/chunk-WP7BVANJ.js +53 -0
  126. package/dist/chunk-WQQON3SV.js +473 -0
  127. package/dist/chunk-WQTOVMGV.js +119 -0
  128. package/dist/chunk-XBDP4QPW.js +7 -0
  129. package/dist/chunk-XEBHSJLT.js +103 -0
  130. package/dist/chunk-XF4VUMKW.js +231 -0
  131. package/dist/chunk-XQ2ECNK3.js +1 -0
  132. package/dist/chunk-Y5YXDM7D.js +74 -0
  133. package/dist/chunk-YOPAUVU4.js +2588 -0
  134. package/dist/chunk-YPT4A46F.js +362 -0
  135. package/dist/chunk-YWXT3JNM.js +425 -0
  136. package/dist/chunk-YY6APYUM.js +49 -0
  137. package/dist/chunk-Z2IJUO45.js +691 -0
  138. package/dist/chunk-ZHZ2Y2DH.js +1643 -0
  139. package/dist/chunk-ZT7JNIGX.js +152 -0
  140. package/dist/coordination/agent-monitor.d.ts +9 -1
  141. package/dist/coordination/agent-subagent-runner.d.ts +1 -1
  142. package/dist/coordination/agents/agent-prompts.d.ts +8 -0
  143. package/dist/coordination/agents/index.d.ts +33 -1
  144. package/dist/coordination/agents/index.js +184 -6269
  145. package/dist/coordination/agents/phase3-wave1-platform.d.ts +1 -3
  146. package/dist/coordination/agents/phase3-wave2-meta.d.ts +1 -3
  147. package/dist/coordination/agents/phase8-wave3-products.d.ts +1 -3
  148. package/dist/coordination/agents/phase9-wave4-platform-meta.d.ts +1 -3
  149. package/dist/coordination/agents/project-agent-identity.d.ts +11 -11
  150. package/dist/coordination/agents/types.d.ts +13 -2
  151. package/dist/coordination/brain-monitor.d.ts +30 -7
  152. package/dist/coordination/collab-debug.d.ts +1 -1
  153. package/dist/coordination/collab-pause.d.ts +1 -1
  154. package/dist/coordination/delegate-tool.d.ts +7 -5
  155. package/dist/coordination/director-kanban-queue-helpers.d.ts +1 -0
  156. package/dist/coordination/director-options.d.ts +7 -0
  157. package/dist/coordination/director-spawn-model.d.ts +17 -0
  158. package/dist/coordination/director-tools.d.ts +2 -2
  159. package/dist/coordination/director.d.ts +12 -0
  160. package/dist/coordination/dispatcher.d.ts +8 -0
  161. package/dist/coordination/fleet-bus.d.ts +35 -4
  162. package/dist/coordination/fleet-supervisor.d.ts +10 -2
  163. package/dist/coordination/fleet.d.ts +16 -7
  164. package/dist/coordination/index.d.ts +4 -0
  165. package/dist/coordination/index.js +402 -34324
  166. package/dist/coordination/kanban-dispatch-port.d.ts +2 -2
  167. package/dist/coordination/mailbox-http-router.d.ts +4 -3
  168. package/dist/coordination/mailbox-http-validation.d.ts +1 -1
  169. package/dist/coordination/mailbox-project-server.js +48 -3276
  170. package/dist/coordination/mailbox-types.d.ts +24 -6
  171. package/dist/coordination/model-tier-leader.d.ts +122 -0
  172. package/dist/coordination/model-tier.d.ts +122 -0
  173. package/dist/coordination/multi-agent-coordinator.d.ts +24 -0
  174. package/dist/coordination/origin-session.d.ts +16 -0
  175. package/dist/coordination/provider-status-tracker.d.ts +41 -5
  176. package/dist/coordination/session-note-hub.d.ts +44 -0
  177. package/dist/coordination/session-note-tool.d.ts +10 -0
  178. package/dist/core/agent.d.ts +15 -2
  179. package/dist/core/context.d.ts +22 -3
  180. package/dist/core/index.d.ts +2 -0
  181. package/dist/core/index.js +239 -12360
  182. package/dist/core/model-ref.js +5 -23
  183. package/dist/core/provider-runner.d.ts +36 -1
  184. package/dist/core/request-conversation-binding.d.ts +37 -0
  185. package/dist/core/session-notes.d.ts +25 -0
  186. package/dist/core/system-prompt-builder.d.ts +11 -2
  187. package/dist/core/system-prompt-variants.d.ts +78 -0
  188. package/dist/design/index.js +50 -1087
  189. package/dist/execution/auto-compaction-middleware.d.ts +11 -0
  190. package/dist/execution/autonomy-brain.d.ts +2 -1
  191. package/dist/execution/autonomy-prompt-contributor.d.ts +7 -2
  192. package/dist/execution/compaction-core.d.ts +4 -4
  193. package/dist/execution/eternal-autonomy.d.ts +1 -1
  194. package/dist/execution/index.d.ts +1 -1
  195. package/dist/execution/index.js +1615 -21895
  196. package/dist/execution/model-runtime.d.ts +0 -7
  197. package/dist/execution/parallel-eternal-engine.d.ts +7 -7
  198. package/dist/execution/prompt-enhancer.js +16 -535
  199. package/dist/execution/retry-policy.d.ts +31 -1
  200. package/dist/extension/index.js +7 -311
  201. package/dist/goal/index.js +33 -3435
  202. package/dist/hooks/index.js +14 -1138
  203. package/dist/hq/index.js +114 -5139
  204. package/dist/hq/persistence/event-log.d.ts +0 -1
  205. package/dist/hq/persistence.d.ts +4 -4
  206. package/dist/hq/protocol.js +21 -668
  207. package/dist/hq/publisher.d.ts +2 -0
  208. package/dist/index.d.ts +14 -11
  209. package/dist/index.js +1502 -92655
  210. package/dist/infrastructure/index.d.ts +4 -4
  211. package/dist/infrastructure/index.js +504 -5230
  212. package/dist/infrastructure/token-counter.d.ts +9 -0
  213. package/dist/kernel/events/agent-events.d.ts +77 -3
  214. package/dist/kernel/events/brain-events.d.ts +12 -0
  215. package/dist/kernel/events/provider-events.d.ts +22 -1
  216. package/dist/kernel/events/session-events.d.ts +14 -0
  217. package/dist/kernel/events/tool-events.d.ts +13 -0
  218. package/dist/kernel/index.js +14 -992
  219. package/dist/mailbox-attach.d.ts +1 -1
  220. package/dist/models/index.js +41 -1117
  221. package/dist/notifications/index.js +3 -108
  222. package/dist/observability/event-bridge.d.ts +25 -5
  223. package/dist/observability/index.d.ts +9 -8
  224. package/dist/observability/index.js +477 -631
  225. package/dist/observability/tool-usage-source.d.ts +65 -0
  226. package/dist/plugin/index.d.ts +5 -0
  227. package/dist/plugin/index.js +600 -14835
  228. package/dist/plugins/auto-review-plugin.d.ts +3 -2
  229. package/dist/plugins/review-finding-commands.d.ts +0 -8
  230. package/dist/plugins/review-finding-store.js +22 -0
  231. package/dist/plugins/review-finding-types.js +19 -0
  232. package/dist/plugins/review-report-integration.js +21 -0
  233. package/dist/plugins/review-report-store.js +20 -0
  234. package/dist/plugins/review-report-types.js +13 -0
  235. package/dist/prompts/index.js +14 -948
  236. package/dist/registry/index.js +9 -822
  237. package/dist/registry/tool-registry.d.ts +59 -9
  238. package/dist/registry-7N4O3E7W.js +14 -0
  239. package/dist/replay/index.js +6 -84
  240. package/dist/security/capabilities.d.ts +6 -0
  241. package/dist/security/file-permissions.d.ts +1 -1
  242. package/dist/security/index.js +161 -2235
  243. package/dist/security/permission-policy.d.ts +12 -1
  244. package/dist/security/secret-scrubber.d.ts +15 -0
  245. package/dist/session-catalog/index.d.ts +1 -0
  246. package/dist/session-catalog/index.js +41 -2103
  247. package/dist/session-catalog/project-server.js +59 -1435
  248. package/dist/session-catalog/protocol.d.ts +32 -1
  249. package/dist/session-catalog/registry.d.ts +17 -1
  250. package/dist/session-catalog/session-agents.d.ts +56 -0
  251. package/dist/session-catalog/store-rebuild.d.ts +14 -0
  252. package/dist/session-catalog/store-schema.d.ts +26 -0
  253. package/dist/session-catalog/store.d.ts +27 -1
  254. package/dist/session-note-attach.d.ts +9 -0
  255. package/dist/skills/index.js +35 -1578
  256. package/dist/statusline/index.d.ts +43 -0
  257. package/dist/statusline/index.js +102 -0
  258. package/dist/storage/board-store-port.d.ts +2 -2
  259. package/dist/storage/config-loader/in-project-policy.d.ts +29 -0
  260. package/dist/storage/config-loader.d.ts +3 -2
  261. package/dist/storage/file-session-writer.d.ts +4 -2
  262. package/dist/storage/goal-coordination.d.ts +6 -1
  263. package/dist/storage/goal-kanban.d.ts +4 -1
  264. package/dist/storage/index.d.ts +12 -4
  265. package/dist/storage/index.js +198 -15601
  266. package/dist/storage/input-history-store.d.ts +2 -4
  267. package/dist/storage/memory-curator.d.ts +133 -0
  268. package/dist/storage/session-agent-attribution.d.ts +39 -0
  269. package/dist/storage/session-doctor.d.ts +109 -0
  270. package/dist/storage/session-event-bridge.d.ts +5 -7
  271. package/dist/storage/session-outcome.d.ts +42 -0
  272. package/dist/storage/session-recovery.d.ts +55 -0
  273. package/dist/storage/session-store/load-session-data.d.ts +11 -0
  274. package/dist/storage/session-store/prune-helpers.d.ts +1 -1
  275. package/dist/storage/session-store/resume-session.d.ts +0 -2
  276. package/dist/storage/session-store/types.d.ts +8 -0
  277. package/dist/storage/session-store.d.ts +3 -3
  278. package/dist/storage/session-summary-tracker.d.ts +8 -1
  279. package/dist/storage/session-write-buffer.d.ts +18 -0
  280. package/dist/storage/session-writer-truncate.d.ts +2 -0
  281. package/dist/tasking/index.js +10 -676
  282. package/dist/tools/fallback-manage-tools.d.ts +6 -4
  283. package/dist/tools/index.js +52 -9590
  284. package/dist/tools/model-tier-set-tool.d.ts +66 -0
  285. package/dist/types/agent-bridge.d.ts +2 -1
  286. package/dist/types/config/autonomy.d.ts +5 -0
  287. package/dist/types/config/mcp-features.d.ts +7 -0
  288. package/dist/types/config/model-tiers.d.ts +148 -0
  289. package/dist/types/config/root.d.ts +9 -0
  290. package/dist/types/config/tools.d.ts +73 -0
  291. package/dist/types/config/ui.d.ts +1 -1
  292. package/dist/types/config.d.ts +1 -0
  293. package/dist/types/context.d.ts +2 -0
  294. package/dist/types/default-config.d.ts +6 -0
  295. package/dist/types/errors.d.ts +20 -1
  296. package/dist/types/index.d.ts +11 -9
  297. package/dist/types/index.js +131 -1342
  298. package/dist/types/multi-agent.d.ts +43 -0
  299. package/dist/types/plugin.d.ts +1 -1
  300. package/dist/types/provider-runner.d.ts +8 -0
  301. package/dist/types/provider.d.ts +15 -0
  302. package/dist/types/runtime-capability-manifest.d.ts +1 -1
  303. package/dist/types/secret-scrubber.d.ts +15 -0
  304. package/dist/types/session-markers.d.ts +37 -10
  305. package/dist/types/session-markers.js +8 -144
  306. package/dist/types/session-timeline.d.ts +197 -0
  307. package/dist/types/session-timeline.js +12 -0
  308. package/dist/types/session.d.ts +172 -11
  309. package/dist/types/system-prompt.d.ts +27 -0
  310. package/dist/utils/config-backup.d.ts +9 -1
  311. package/dist/utils/error.js +3 -4
  312. package/dist/utils/expect-defined.js +3 -9
  313. package/dist/utils/heap-watchdog.js +6 -706
  314. package/dist/utils/index.js +271 -5685
  315. package/dist/utils/message-invariants.d.ts +4 -1
  316. package/dist/utils/regex-guard.d.ts +1 -1
  317. package/dist/utils/sage-output-block.js +5 -42
  318. package/dist/utils/socket-path.d.ts +1 -1
  319. package/dist/utils/token-estimate.d.ts +6 -2
  320. package/dist/utils/wstack-paths.d.ts +2 -0
  321. package/dist/wiring/proxy-rewrite.d.ts +46 -0
  322. package/dist/wiring/proxy-rewrite.js +24 -121
  323. package/dist/worktree/index.js +6 -887
  324. package/instructions/agents/explore-companion.md +45 -18
  325. package/instructions/agents/memory-curator.md +34 -22
  326. package/instructions/coordination/director-preamble.md +12 -7
  327. package/instructions/coordination/subagent-baseline.md +26 -5
  328. package/instructions/leader-after-task.md +4 -1
  329. package/instructions/llm/chimera-review.md +30 -9
  330. package/instructions/llm/memory-curator.md +31 -0
  331. package/instructions/sections/tool/delegation-compact.md +2 -0
  332. package/instructions/sections/tool/delegation-full.md +5 -1
  333. package/instructions/sections/tool/mailbox-compact.md +5 -1
  334. package/instructions/sections/tool/mailbox-full.md +3 -0
  335. package/instructions/sections/tool/session-note-compact.md +3 -0
  336. package/instructions/sections/tool/session-note-full.md +15 -0
  337. package/instructions/system-lite.md +9 -4
  338. package/instructions/system-pro.md +66 -143
  339. package/instructions/system.md +73 -175
  340. package/package.json +32 -4
  341. package/dist/coordination/agents.d.ts +0 -8
  342. package/dist/coordination/transport.d.ts +0 -2
  343. package/dist/storage/session-writer/session-writer-summary-tracker.d.ts +0 -28
@@ -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-CU546ESW.js";
14
+ import {
15
+ ReadOnlyPermissionPolicy,
16
+ toolMutates
17
+ } from "../chunk-JFPJ6LLQ.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-OOYZPOXM.js";
29
+ import {
30
+ matchAny,
31
+ matchAnyCommand,
32
+ matchGlob,
33
+ subjectForToolInput
34
+ } from "../chunk-WAHQVNWT.js";
35
+ import {
36
+ safeParse
37
+ } from "../chunk-W5VITDM2.js";
38
+ import "../chunk-B7F4WVP4.js";
39
+ import {
40
+ ERROR_DETAIL_MAX,
41
+ sanitizeApiError,
42
+ scrubErrorDetail,
43
+ scrubErrorText
44
+ } from "../chunk-Y5YXDM7D.js";
45
+ import {
46
+ DefaultSecretScrubber
47
+ } from "../chunk-QSTSJOCK.js";
48
+ import {
49
+ SECRET_DIR_MODE,
50
+ SECRET_FILE_MODE,
51
+ restrictDirPermissions,
52
+ restrictFilePermissions
53
+ } from "../chunk-6FHWXLPT.js";
54
+ import {
55
+ DANGEROUS_FOR_SUBAGENTS,
56
+ ToolCapabilities,
57
+ WIDE_SUBAGENT_CAPABILITIES,
58
+ clampSubagentCapabilities,
59
+ getDangerousCapabilities,
60
+ hasCapability,
61
+ hasDangerousCapabilityForSubagents
62
+ } from "../chunk-UBGDX6FI.js";
63
+ import {
64
+ atomicWrite
65
+ } from "../chunk-OSGLYW5J.js";
66
+ import {
67
+ wstackGlobalRoot
68
+ } from "../chunk-SAFHACFA.js";
69
+ import "../chunk-PFZO3C6H.js";
70
+ import "../chunk-25LSLC3G.js";
71
+ import "../chunk-TBRL2RCC.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, "/"));
@@ -2655,205 +1468,38 @@ function explainPermissionTrace(state, tool, input, ctx) {
2655
1468
  "safe default auto",
2656
1469
  true,
2657
1470
  "auto",
2658
- "default",
2659
- `tool "${tool.name}" has auto permission and is not mutating \u2014 safe to auto-approve`
2660
- );
2661
- winnerIndex = steps.length - 1;
2662
- return {
2663
- toolName: tool.name,
2664
- subject,
2665
- steps,
2666
- winnerIndex,
2667
- decision: { permission: "auto", source: "default" }
2668
- };
2669
- }
2670
- add(
2671
- "mutating default confirm",
2672
- true,
2673
- "confirm",
2674
- "default",
2675
- isMutating ? `tool "${tool.name}" is mutating (default auto not enough) \u2014 needs confirmation` : `tool "${tool.name}" has "${tool.permission}" permission \u2014 needs confirmation`
2676
- );
2677
- winnerIndex = steps.length - 1;
2678
- const hasDelegate = state.promptDelegatePresent;
2679
- return {
2680
- toolName: tool.name,
2681
- subject,
2682
- steps,
2683
- winnerIndex,
2684
- decision: {
2685
- permission: "confirm",
2686
- source: "default",
2687
- ...hasDelegate ? { reason: "would prompt user via delegate" } : {}
2688
- }
2689
- };
2690
- }
2691
-
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)) {
1471
+ "default",
1472
+ `tool "${tool.name}" has auto permission and is not mutating \u2014 safe to auto-approve`
1473
+ );
1474
+ winnerIndex = steps.length - 1;
2783
1475
  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
- ]
1476
+ toolName: tool.name,
1477
+ subject,
1478
+ steps,
1479
+ winnerIndex,
1480
+ decision: { permission: "auto", source: "default" }
2793
1481
  };
2794
1482
  }
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
- }
1483
+ add(
1484
+ "mutating default confirm",
1485
+ true,
1486
+ "confirm",
1487
+ "default",
1488
+ isMutating ? `tool "${tool.name}" is mutating (default auto not enough) \u2014 needs confirmation` : `tool "${tool.name}" has "${tool.permission}" permission \u2014 needs confirmation`
1489
+ );
1490
+ winnerIndex = steps.length - 1;
1491
+ const hasDelegate = state.promptDelegatePresent;
1492
+ return {
1493
+ toolName: tool.name,
1494
+ subject,
1495
+ steps,
1496
+ winnerIndex,
1497
+ decision: {
1498
+ permission: "confirm",
1499
+ source: "default",
1500
+ ...hasDelegate ? { reason: "would prompt user via delegate" } : {}
2853
1501
  }
2854
- policy[toolName] = rule;
2855
- }
2856
- return diagnostics.some((diagnostic) => diagnostic.severity === "error") ? { ok: false, diagnostics } : { ok: true, policy, diagnostics };
1502
+ };
2857
1503
  }
2858
1504
 
2859
1505
  // src/security/auto-approve-policy.ts
@@ -2994,13 +1640,13 @@ 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;
3001
1647
  }
3002
1648
  yoloBlockedAsDestructive(tool, input, ctx) {
3003
- if (!this.yolo || this.yoloDestructive) return false;
1649
+ if (!this.effectiveYolo(ctx) || this.yoloDestructive) return false;
3004
1650
  if (this.hasAgentStateWriteTarget(tool, input, ctx)) return true;
3005
1651
  if (attachesWellKnownCredential(input)) return true;
3006
1652
  const isShellSurface = tool.name === "bash" || tool.name === "exec" || (tool.capabilities ?? []).includes("shell.arbitrary");
@@ -3012,6 +1658,20 @@ var DefaultPermissionPolicy = class {
3012
1658
  setPromptDelegate(delegate) {
3013
1659
  this.promptDelegate = delegate;
3014
1660
  }
1661
+ /**
1662
+ * YOLO as it applies to ONE conversation.
1663
+ *
1664
+ * `this.yolo` is a process-wide switch, which is right for a CLI or a TUI —
1665
+ * one process, one conversation. A WebUI holds four at once, and YOLO is a
1666
+ * per-tab preference stored on each session's own context meta, so reading
1667
+ * the process switch meant turning YOLO on in one tab auto-approved the
1668
+ * tools of the other three. The instance flag stays as the fallback for
1669
+ * hosts (and tests) that never write the meta key.
1670
+ */
1671
+ effectiveYolo(ctx) {
1672
+ const scoped = ctx?.meta?.["yolo"];
1673
+ return typeof scoped === "boolean" ? scoped : this.yolo;
1674
+ }
3015
1675
  setYolo(enabled) {
3016
1676
  if (this.yolo !== enabled) this._evalCache.clear();
3017
1677
  this.yolo = enabled;
@@ -3099,7 +1759,7 @@ var DefaultPermissionPolicy = class {
3099
1759
  const entry = mergeTrustEntries(this.policy[tool.name], namespaceEntry);
3100
1760
  const subject = subjectForToolInput(tool.name, input, tool.subjectKey, tool.subjectFields);
3101
1761
  const cacheKey = `${tool.name}::${subject ?? tool.name}`;
3102
- const evalKey = `${cacheKey}::${permissionFingerprint(tool)}`;
1762
+ const evalKey = `${ctx.session?.id ?? "__default__"}::${cacheKey}::${permissionFingerprint(tool)}::y${this.effectiveYolo(ctx) ? 1 : 0}`;
3103
1763
  if (tool.name !== "write" && !this.hasAgentStateWriteTarget(tool, input, ctx)) {
3104
1764
  const cached = this._evalCache.get(evalKey);
3105
1765
  if (cached !== void 0) return cached;
@@ -3159,7 +1819,7 @@ var DefaultPermissionPolicy = class {
3159
1819
  this._evalCache.set(evalKey, decision);
3160
1820
  return decision;
3161
1821
  }
3162
- if (!this.yolo && this.isSensitiveReadCall(tool, input)) {
1822
+ if (!this.effectiveYolo(ctx) && this.isSensitiveReadCall(tool, input)) {
3163
1823
  if (this.promptDelegate) {
3164
1824
  const userDecision = await this.promptDelegate(tool, input, subject ?? tool.name);
3165
1825
  if (userDecision === "always") {
@@ -3195,7 +1855,7 @@ var DefaultPermissionPolicy = class {
3195
1855
  reason: "sensitive file read needs explicit approval"
3196
1856
  };
3197
1857
  }
3198
- if (this.yolo) {
1858
+ if (this.effectiveYolo(ctx)) {
3199
1859
  if (this.yoloBlockedAsDestructive(tool, input, ctx)) {
3200
1860
  return {
3201
1861
  permission: "confirm",
@@ -3316,7 +1976,7 @@ var DefaultPermissionPolicy = class {
3316
1976
  wildcardEntries: this.wildcardEntries,
3317
1977
  sessionDenied: this.sessionDenied,
3318
1978
  sessionAllowed: this.sessionAllowed,
3319
- yolo: this.yolo,
1979
+ yolo: this.effectiveYolo(ctx),
3320
1980
  promptDelegatePresent: this.promptDelegate !== void 0,
3321
1981
  isSensitiveReadCall: (t, inp) => this.isSensitiveReadCall(t, inp),
3322
1982
  yoloBlockedAsDestructive: (t, inp, c) => this.yoloBlockedAsDestructive(t, inp, c)
@@ -3334,126 +1994,6 @@ var DefaultPermissionPolicy = class {
3334
1994
  }
3335
1995
  };
3336
1996
 
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
1997
  // src/security/totp.ts
3458
1998
  import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
3459
1999
  var BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
@@ -3572,620 +2112,6 @@ function verifyRecoveryCode(code, storedHashes) {
3572
2112
  return false;
3573
2113
  }
3574
2114
 
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
2115
  // src/security/trust-boundary.ts
4190
2116
  var TRUST_BOUNDARY_VERSION = 1;
4191
2117
  function createCompatibilityTrustBoundary(options = {}) {
@@ -4256,36 +2182,36 @@ var RESERVED_BYPASS_FIELDS = /* @__PURE__ */ new Set([
4256
2182
  "skipAuthorization",
4257
2183
  "skip_authorization"
4258
2184
  ]);
4259
- function isRecord3(value) {
2185
+ function isRecord2(value) {
4260
2186
  return typeof value === "object" && value !== null && !Array.isArray(value);
4261
2187
  }
4262
- function addString(issues, value, path9, required = true) {
2188
+ function addString(issues, value, path5, required = true) {
4263
2189
  if (typeof value === "string" && value.trim().length > 0) return true;
4264
2190
  if (!required && value === void 0) return false;
4265
- issues.push({ path: path9, message: "must be a non-empty string" });
2191
+ issues.push({ path: path5, message: "must be a non-empty string" });
4266
2192
  return false;
4267
2193
  }
4268
- function addEnum(issues, value, path9, values) {
2194
+ function addEnum(issues, value, path5, values) {
4269
2195
  if (typeof value === "string" && values.has(value)) return true;
4270
- issues.push({ path: path9, message: `must be one of: ${[...values].join(", ")}` });
2196
+ issues.push({ path: path5, message: `must be one of: ${[...values].join(", ")}` });
4271
2197
  return false;
4272
2198
  }
4273
- function validateAttributes(issues, value, path9) {
4274
- if (!isRecord3(value)) {
4275
- issues.push({ path: path9, message: "must be an object" });
2199
+ function validateAttributes(issues, value, path5) {
2200
+ if (!isRecord2(value)) {
2201
+ issues.push({ path: path5, message: "must be an object" });
4276
2202
  return false;
4277
2203
  }
4278
2204
  let valid = true;
4279
2205
  for (const [key, item] of Object.entries(value)) {
4280
2206
  if (RESERVED_BYPASS_FIELDS.has(key)) {
4281
2207
  issues.push({
4282
- path: `${path9}.${key}`,
2208
+ path: `${path5}.${key}`,
4283
2209
  message: "generic authorization bypass fields are forbidden"
4284
2210
  });
4285
2211
  valid = false;
4286
2212
  } else if (item !== null && !["string", "number", "boolean"].includes(typeof item)) {
4287
2213
  issues.push({
4288
- path: `${path9}.${key}`,
2214
+ path: `${path5}.${key}`,
4289
2215
  message: "must be a string, number, boolean, or null"
4290
2216
  });
4291
2217
  valid = false;
@@ -4293,21 +2219,21 @@ function validateAttributes(issues, value, path9) {
4293
2219
  }
4294
2220
  return valid;
4295
2221
  }
4296
- function validateScope(issues, value, path9) {
4297
- if (!isRecord3(value)) {
4298
- issues.push({ path: path9, message: "must be an object" });
2222
+ function validateScope(issues, value, path5) {
2223
+ if (!isRecord2(value)) {
2224
+ issues.push({ path: path5, message: "must be an object" });
4299
2225
  return false;
4300
2226
  }
4301
2227
  for (const field of ["projectId", "sessionId", "cwd"]) {
4302
- if (value[field] !== void 0) addString(issues, value[field], `${path9}.${field}`);
2228
+ if (value[field] !== void 0) addString(issues, value[field], `${path5}.${field}`);
4303
2229
  }
4304
2230
  if (value.constraints !== void 0)
4305
- validateAttributes(issues, value.constraints, `${path9}.constraints`);
2231
+ validateAttributes(issues, value.constraints, `${path5}.constraints`);
4306
2232
  return true;
4307
2233
  }
4308
2234
  function decodeTrustBoundaryRequest(input) {
4309
2235
  const issues = [];
4310
- if (!isRecord3(input)) return { ok: false, issues: [{ path: "$", message: "must be an object" }] };
2236
+ if (!isRecord2(input)) return { ok: false, issues: [{ path: "$", message: "must be an object" }] };
4311
2237
  for (const field of RESERVED_BYPASS_FIELDS) {
4312
2238
  if (field in input)
4313
2239
  issues.push({
@@ -4321,14 +2247,14 @@ function decodeTrustBoundaryRequest(input) {
4321
2247
  addEnum(issues, input.surface, "$.surface", SURFACES);
4322
2248
  addString(issues, input.capability, "$.capability");
4323
2249
  addEnum(issues, input.risk, "$.risk", RISKS);
4324
- if (!isRecord3(input.actor)) issues.push({ path: "$.actor", message: "must be an object" });
2250
+ if (!isRecord2(input.actor)) issues.push({ path: "$.actor", message: "must be an object" });
4325
2251
  else {
4326
2252
  addEnum(issues, input.actor.kind, "$.actor.kind", ACTORS);
4327
2253
  if (input.actor.id !== void 0) addString(issues, input.actor.id, "$.actor.id");
4328
2254
  if (input.actor.sessionId !== void 0)
4329
2255
  addString(issues, input.actor.sessionId, "$.actor.sessionId");
4330
2256
  }
4331
- if (!isRecord3(input.subject)) issues.push({ path: "$.subject", message: "must be an object" });
2257
+ if (!isRecord2(input.subject)) issues.push({ path: "$.subject", message: "must be an object" });
4332
2258
  else {
4333
2259
  addEnum(issues, input.subject.kind, "$.subject.kind", SUBJECT_KINDS);
4334
2260
  addString(issues, input.subject.id, "$.subject.id");
@@ -4338,7 +2264,7 @@ function decodeTrustBoundaryRequest(input) {
4338
2264
  validateScope(issues, input.scope, "$.scope");
4339
2265
  if (input.metadata !== void 0) validateAttributes(issues, input.metadata, "$.metadata");
4340
2266
  if (input.authContext !== void 0) {
4341
- if (!isRecord3(input.authContext))
2267
+ if (!isRecord2(input.authContext))
4342
2268
  issues.push({ path: "$.authContext", message: "must be an object" });
4343
2269
  else {
4344
2270
  addEnum(issues, input.authContext.method, "$.authContext.method", AUTH_METHODS);
@@ -4354,7 +2280,7 @@ function decodeTrustBoundaryRequest(input) {
4354
2280
  }
4355
2281
  function decodeTrustBoundaryDecision(input) {
4356
2282
  const issues = [];
4357
- if (!isRecord3(input)) return { ok: false, issues: [{ path: "$", message: "must be an object" }] };
2283
+ if (!isRecord2(input)) return { ok: false, issues: [{ path: "$", message: "must be an object" }] };
4358
2284
  if (!["allow", "deny", "confirm", "scoped-token"].includes(String(input.kind))) {
4359
2285
  issues.push({ path: "$.kind", message: "must be one of: allow, deny, confirm, scoped-token" });
4360
2286
  }