@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.
- package/dist/chronicle/index.js +84 -6391
- package/dist/chronicle/legacy-journal-import.d.ts +0 -6
- package/dist/chronicle/metrics-ingest.d.ts +14 -0
- package/dist/chronicle/metrics-schema.d.ts +10 -1
- package/dist/chronicle/metrics-store.d.ts +20 -2
- package/dist/chronicle/project-server.js +98 -4874
- package/dist/chronicle/sink.d.ts +13 -0
- package/dist/chronicle/sqlite-journal.d.ts +20 -2
- package/dist/chunk-25LSLC3G.js +334 -0
- package/dist/chunk-27QIURLN.js +1073 -0
- package/dist/chunk-2FK4WTFF.js +207 -0
- package/dist/chunk-2YU4WVBW.js +134 -0
- package/dist/chunk-3AGWVSS7.js +4608 -0
- package/dist/chunk-3BDV7RL3.js +211 -0
- package/dist/chunk-3ECENVHL.js +325 -0
- package/dist/chunk-3XPVB7BA.js +135 -0
- package/dist/chunk-45WI72ZB.js +16884 -0
- package/dist/chunk-4BMVPOHO.js +3748 -0
- package/dist/chunk-4I7HGVVA.js +363 -0
- package/dist/chunk-4LCZVJVF.js +317 -0
- package/dist/chunk-4SJNS7OB.js +217 -0
- package/dist/chunk-4VWO3ZXL.js +342 -0
- package/dist/chunk-57GEWK5X.js +26 -0
- package/dist/chunk-5BMIE7QG.js +46 -0
- package/dist/chunk-5S437PRC.js +50 -0
- package/dist/chunk-5XDUGTKN.js +511 -0
- package/dist/chunk-5YPSXQXM.js +1443 -0
- package/dist/chunk-6FHWXLPT.js +10 -0
- package/dist/chunk-6H5GEOET.js +2406 -0
- package/dist/chunk-6NWEVFEL.js +151 -0
- package/dist/chunk-AYW5YINX.js +58 -0
- package/dist/chunk-B4AMLZPB.js +9 -0
- package/dist/chunk-B7F4WVP4.js +65 -0
- package/dist/chunk-BJHVULHO.js +55 -0
- package/dist/chunk-BPBKKBUK.js +2085 -0
- package/dist/chunk-BYD345OX.js +589 -0
- package/dist/chunk-C27KDLOX.js +279 -0
- package/dist/chunk-CU546ESW.js +597 -0
- package/dist/chunk-DLHHXUAM.js +172 -0
- package/dist/chunk-DOTHQSJ2.js +429 -0
- package/dist/chunk-DYLPMV4Q.js +711 -0
- package/dist/chunk-EODDVJBM.js +82 -0
- package/dist/chunk-F3JWNST7.js +361 -0
- package/dist/chunk-FC2ZGRF3.js +140 -0
- package/dist/chunk-FCBTO76Z.js +5786 -0
- package/dist/chunk-FHZVFYSL.js +396 -0
- package/dist/chunk-FNXVT7HF.js +52 -0
- package/dist/chunk-FWNLEZYG.js +259 -0
- package/dist/chunk-FXQLIX3K.js +211 -0
- package/dist/chunk-G5S3S5ZN.js +38 -0
- package/dist/chunk-GDQ7CZRQ.js +116 -0
- package/dist/chunk-GJJGJB2R.js +1374 -0
- package/dist/chunk-GTCU6DZ3.js +1 -0
- package/dist/chunk-HA4ZYA6T.js +178 -0
- package/dist/chunk-HI3JGZR2.js +188 -0
- package/dist/chunk-HMLIGAU7.js +395 -0
- package/dist/chunk-HXKZ7OBQ.js +303 -0
- package/dist/chunk-I7DALG7J.js +292 -0
- package/dist/chunk-IRUTEHCE.js +960 -0
- package/dist/chunk-IYFBFX34.js +86 -0
- package/dist/chunk-J2WRWR37.js +1104 -0
- package/dist/chunk-JFPJ6LLQ.js +129 -0
- package/dist/chunk-JMULFFLN.js +9 -0
- package/dist/chunk-JO4MGB6Z.js +1264 -0
- package/dist/chunk-JVKB2TBL.js +48 -0
- package/dist/chunk-JX7VBZDK.js +31 -0
- package/dist/chunk-K6BXGITA.js +931 -0
- package/dist/chunk-KDQJCT2J.js +45 -0
- package/dist/chunk-KJ2ZTQSL.js +47 -0
- package/dist/chunk-KZMSZV32.js +187 -0
- package/dist/chunk-LA7CLDRW.js +129 -0
- package/dist/chunk-LHOA5DWA.js +28 -0
- package/dist/chunk-LSMMOS2Z.js +119 -0
- package/dist/chunk-LX5QWLBL.js +27 -0
- package/dist/chunk-LXWCMQNJ.js +789 -0
- package/dist/chunk-M27LIEKV.js +2829 -0
- package/dist/chunk-MJPHJRAL.js +663 -0
- package/dist/chunk-MTL4O5YR.js +228 -0
- package/dist/chunk-NDBIPFNY.js +102 -0
- package/dist/chunk-NEMIWEXC.js +1199 -0
- package/dist/chunk-NHJ75HXY.js +1 -0
- package/dist/chunk-NIOAGFPB.js +1510 -0
- package/dist/chunk-NS52ISWS.js +355 -0
- package/dist/chunk-NSOFPKGT.js +5352 -0
- package/dist/chunk-NVIZG63R.js +757 -0
- package/dist/chunk-O37BQ65S.js +21 -0
- package/dist/chunk-OOYZPOXM.js +308 -0
- package/dist/chunk-OSGLYW5J.js +28 -0
- package/dist/chunk-PFCMSSUF.js +1640 -0
- package/dist/chunk-PFZO3C6H.js +14 -0
- package/dist/chunk-PGRVXIMY.js +158 -0
- package/dist/chunk-PHH7LD45.js +113 -0
- package/dist/chunk-PISBFBJN.js +109 -0
- package/dist/chunk-POOOIWE5.js +9 -0
- package/dist/chunk-PQNS3U7Z.js +1240 -0
- package/dist/chunk-QSTSJOCK.js +402 -0
- package/dist/chunk-RFQMOKWK.js +282 -0
- package/dist/chunk-RJIZEJ42.js +30 -0
- package/dist/chunk-RTOTNK3P.js +484 -0
- package/dist/chunk-RXKSDUAU.js +1172 -0
- package/dist/chunk-S45JPKQN.js +3317 -0
- package/dist/chunk-SAFHACFA.js +152 -0
- package/dist/chunk-SQJVFEBV.js +41 -0
- package/dist/chunk-SVT4RNTR.js +49 -0
- package/dist/chunk-SWUFN7XM.js +43 -0
- package/dist/chunk-SXMVYC5H.js +174 -0
- package/dist/chunk-SZ6AYUIZ.js +23 -0
- package/dist/chunk-TBRL2RCC.js +9 -0
- package/dist/chunk-TCCHINEF.js +655 -0
- package/dist/chunk-THEGTRAG.js +316 -0
- package/dist/chunk-TR4LQE7L.js +2256 -0
- package/dist/chunk-TRT2HRWZ.js +18 -0
- package/dist/chunk-TWJMGXCI.js +900 -0
- package/dist/chunk-U45AXT5D.js +1023 -0
- package/dist/chunk-U4FWBSO3.js +509 -0
- package/dist/chunk-UBGDX6FI.js +147 -0
- package/dist/chunk-UJILKYES.js +75 -0
- package/dist/chunk-ULMAQV3T.js +234 -0
- package/dist/chunk-VVBP6KGT.js +9 -0
- package/dist/chunk-W5VITDM2.js +168 -0
- package/dist/chunk-WAHQVNWT.js +194 -0
- package/dist/chunk-WBPVKEEE.js +227 -0
- package/dist/chunk-WGGYRBXH.js +5434 -0
- package/dist/chunk-WLYKU45P.js +5770 -0
- package/dist/chunk-WP7BVANJ.js +53 -0
- package/dist/chunk-WQQON3SV.js +473 -0
- package/dist/chunk-WQTOVMGV.js +119 -0
- package/dist/chunk-XBDP4QPW.js +7 -0
- package/dist/chunk-XEBHSJLT.js +103 -0
- package/dist/chunk-XF4VUMKW.js +231 -0
- package/dist/chunk-XQ2ECNK3.js +1 -0
- package/dist/chunk-Y5YXDM7D.js +74 -0
- package/dist/chunk-YOPAUVU4.js +2588 -0
- package/dist/chunk-YPT4A46F.js +362 -0
- package/dist/chunk-YWXT3JNM.js +425 -0
- package/dist/chunk-YY6APYUM.js +49 -0
- package/dist/chunk-Z2IJUO45.js +691 -0
- package/dist/chunk-ZHZ2Y2DH.js +1643 -0
- package/dist/chunk-ZT7JNIGX.js +152 -0
- package/dist/coordination/agent-monitor.d.ts +9 -1
- package/dist/coordination/agent-subagent-runner.d.ts +1 -1
- package/dist/coordination/agents/agent-prompts.d.ts +8 -0
- package/dist/coordination/agents/index.d.ts +33 -1
- package/dist/coordination/agents/index.js +184 -6269
- package/dist/coordination/agents/phase3-wave1-platform.d.ts +1 -3
- package/dist/coordination/agents/phase3-wave2-meta.d.ts +1 -3
- package/dist/coordination/agents/phase8-wave3-products.d.ts +1 -3
- package/dist/coordination/agents/phase9-wave4-platform-meta.d.ts +1 -3
- package/dist/coordination/agents/project-agent-identity.d.ts +11 -11
- package/dist/coordination/agents/types.d.ts +13 -2
- package/dist/coordination/brain-monitor.d.ts +30 -7
- package/dist/coordination/collab-debug.d.ts +1 -1
- package/dist/coordination/collab-pause.d.ts +1 -1
- package/dist/coordination/delegate-tool.d.ts +7 -5
- package/dist/coordination/director-kanban-queue-helpers.d.ts +1 -0
- package/dist/coordination/director-options.d.ts +7 -0
- package/dist/coordination/director-spawn-model.d.ts +17 -0
- package/dist/coordination/director-tools.d.ts +2 -2
- package/dist/coordination/director.d.ts +12 -0
- package/dist/coordination/dispatcher.d.ts +8 -0
- package/dist/coordination/fleet-bus.d.ts +35 -4
- package/dist/coordination/fleet-supervisor.d.ts +10 -2
- package/dist/coordination/fleet.d.ts +16 -7
- package/dist/coordination/index.d.ts +4 -0
- package/dist/coordination/index.js +402 -34324
- package/dist/coordination/kanban-dispatch-port.d.ts +2 -2
- package/dist/coordination/mailbox-http-router.d.ts +4 -3
- package/dist/coordination/mailbox-http-validation.d.ts +1 -1
- package/dist/coordination/mailbox-project-server.js +48 -3276
- package/dist/coordination/mailbox-types.d.ts +24 -6
- package/dist/coordination/model-tier-leader.d.ts +122 -0
- package/dist/coordination/model-tier.d.ts +122 -0
- package/dist/coordination/multi-agent-coordinator.d.ts +24 -0
- package/dist/coordination/origin-session.d.ts +16 -0
- package/dist/coordination/provider-status-tracker.d.ts +41 -5
- package/dist/coordination/session-note-hub.d.ts +44 -0
- package/dist/coordination/session-note-tool.d.ts +10 -0
- package/dist/core/agent.d.ts +15 -2
- package/dist/core/context.d.ts +22 -3
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +239 -12360
- package/dist/core/model-ref.js +5 -23
- package/dist/core/provider-runner.d.ts +36 -1
- package/dist/core/request-conversation-binding.d.ts +37 -0
- package/dist/core/session-notes.d.ts +25 -0
- package/dist/core/system-prompt-builder.d.ts +11 -2
- package/dist/core/system-prompt-variants.d.ts +78 -0
- package/dist/design/index.js +50 -1087
- package/dist/execution/auto-compaction-middleware.d.ts +11 -0
- package/dist/execution/autonomy-brain.d.ts +2 -1
- package/dist/execution/autonomy-prompt-contributor.d.ts +7 -2
- package/dist/execution/compaction-core.d.ts +4 -4
- package/dist/execution/eternal-autonomy.d.ts +1 -1
- package/dist/execution/index.d.ts +1 -1
- package/dist/execution/index.js +1615 -21895
- package/dist/execution/model-runtime.d.ts +0 -7
- package/dist/execution/parallel-eternal-engine.d.ts +7 -7
- package/dist/execution/prompt-enhancer.js +16 -535
- package/dist/execution/retry-policy.d.ts +31 -1
- package/dist/extension/index.js +7 -311
- package/dist/goal/index.js +33 -3435
- package/dist/hooks/index.js +14 -1138
- package/dist/hq/index.js +114 -5139
- package/dist/hq/persistence/event-log.d.ts +0 -1
- package/dist/hq/persistence.d.ts +4 -4
- package/dist/hq/protocol.js +21 -668
- package/dist/hq/publisher.d.ts +2 -0
- package/dist/index.d.ts +14 -11
- package/dist/index.js +1502 -92655
- package/dist/infrastructure/index.d.ts +4 -4
- package/dist/infrastructure/index.js +504 -5230
- package/dist/infrastructure/token-counter.d.ts +9 -0
- package/dist/kernel/events/agent-events.d.ts +77 -3
- package/dist/kernel/events/brain-events.d.ts +12 -0
- package/dist/kernel/events/provider-events.d.ts +22 -1
- package/dist/kernel/events/session-events.d.ts +14 -0
- package/dist/kernel/events/tool-events.d.ts +13 -0
- package/dist/kernel/index.js +14 -992
- package/dist/mailbox-attach.d.ts +1 -1
- package/dist/models/index.js +41 -1117
- package/dist/notifications/index.js +3 -108
- package/dist/observability/event-bridge.d.ts +25 -5
- package/dist/observability/index.d.ts +9 -8
- package/dist/observability/index.js +477 -631
- package/dist/observability/tool-usage-source.d.ts +65 -0
- package/dist/plugin/index.d.ts +5 -0
- package/dist/plugin/index.js +600 -14835
- package/dist/plugins/auto-review-plugin.d.ts +3 -2
- package/dist/plugins/review-finding-commands.d.ts +0 -8
- package/dist/plugins/review-finding-store.js +22 -0
- package/dist/plugins/review-finding-types.js +19 -0
- package/dist/plugins/review-report-integration.js +21 -0
- package/dist/plugins/review-report-store.js +20 -0
- package/dist/plugins/review-report-types.js +13 -0
- package/dist/prompts/index.js +14 -948
- package/dist/registry/index.js +9 -822
- package/dist/registry/tool-registry.d.ts +59 -9
- package/dist/registry-7N4O3E7W.js +14 -0
- package/dist/replay/index.js +6 -84
- package/dist/security/capabilities.d.ts +6 -0
- package/dist/security/file-permissions.d.ts +1 -1
- package/dist/security/index.js +161 -2235
- package/dist/security/permission-policy.d.ts +12 -1
- package/dist/security/secret-scrubber.d.ts +15 -0
- package/dist/session-catalog/index.d.ts +1 -0
- package/dist/session-catalog/index.js +41 -2103
- package/dist/session-catalog/project-server.js +59 -1435
- package/dist/session-catalog/protocol.d.ts +32 -1
- package/dist/session-catalog/registry.d.ts +17 -1
- package/dist/session-catalog/session-agents.d.ts +56 -0
- package/dist/session-catalog/store-rebuild.d.ts +14 -0
- package/dist/session-catalog/store-schema.d.ts +26 -0
- package/dist/session-catalog/store.d.ts +27 -1
- package/dist/session-note-attach.d.ts +9 -0
- package/dist/skills/index.js +35 -1578
- package/dist/statusline/index.d.ts +43 -0
- package/dist/statusline/index.js +102 -0
- package/dist/storage/board-store-port.d.ts +2 -2
- package/dist/storage/config-loader/in-project-policy.d.ts +29 -0
- package/dist/storage/config-loader.d.ts +3 -2
- package/dist/storage/file-session-writer.d.ts +4 -2
- package/dist/storage/goal-coordination.d.ts +6 -1
- package/dist/storage/goal-kanban.d.ts +4 -1
- package/dist/storage/index.d.ts +12 -4
- package/dist/storage/index.js +198 -15601
- package/dist/storage/input-history-store.d.ts +2 -4
- package/dist/storage/memory-curator.d.ts +133 -0
- package/dist/storage/session-agent-attribution.d.ts +39 -0
- package/dist/storage/session-doctor.d.ts +109 -0
- package/dist/storage/session-event-bridge.d.ts +5 -7
- package/dist/storage/session-outcome.d.ts +42 -0
- package/dist/storage/session-recovery.d.ts +55 -0
- package/dist/storage/session-store/load-session-data.d.ts +11 -0
- package/dist/storage/session-store/prune-helpers.d.ts +1 -1
- package/dist/storage/session-store/resume-session.d.ts +0 -2
- package/dist/storage/session-store/types.d.ts +8 -0
- package/dist/storage/session-store.d.ts +3 -3
- package/dist/storage/session-summary-tracker.d.ts +8 -1
- package/dist/storage/session-write-buffer.d.ts +18 -0
- package/dist/storage/session-writer-truncate.d.ts +2 -0
- package/dist/tasking/index.js +10 -676
- package/dist/tools/fallback-manage-tools.d.ts +6 -4
- package/dist/tools/index.js +52 -9590
- package/dist/tools/model-tier-set-tool.d.ts +66 -0
- package/dist/types/agent-bridge.d.ts +2 -1
- package/dist/types/config/autonomy.d.ts +5 -0
- package/dist/types/config/mcp-features.d.ts +7 -0
- package/dist/types/config/model-tiers.d.ts +148 -0
- package/dist/types/config/root.d.ts +9 -0
- package/dist/types/config/tools.d.ts +73 -0
- package/dist/types/config/ui.d.ts +1 -1
- package/dist/types/config.d.ts +1 -0
- package/dist/types/context.d.ts +2 -0
- package/dist/types/default-config.d.ts +6 -0
- package/dist/types/errors.d.ts +20 -1
- package/dist/types/index.d.ts +11 -9
- package/dist/types/index.js +131 -1342
- package/dist/types/multi-agent.d.ts +43 -0
- package/dist/types/plugin.d.ts +1 -1
- package/dist/types/provider-runner.d.ts +8 -0
- package/dist/types/provider.d.ts +15 -0
- package/dist/types/runtime-capability-manifest.d.ts +1 -1
- package/dist/types/secret-scrubber.d.ts +15 -0
- package/dist/types/session-markers.d.ts +37 -10
- package/dist/types/session-markers.js +8 -144
- package/dist/types/session-timeline.d.ts +197 -0
- package/dist/types/session-timeline.js +12 -0
- package/dist/types/session.d.ts +172 -11
- package/dist/types/system-prompt.d.ts +27 -0
- package/dist/utils/config-backup.d.ts +9 -1
- package/dist/utils/error.js +3 -4
- package/dist/utils/expect-defined.js +3 -9
- package/dist/utils/heap-watchdog.js +6 -706
- package/dist/utils/index.js +271 -5685
- package/dist/utils/message-invariants.d.ts +4 -1
- package/dist/utils/regex-guard.d.ts +1 -1
- package/dist/utils/sage-output-block.js +5 -42
- package/dist/utils/socket-path.d.ts +1 -1
- package/dist/utils/token-estimate.d.ts +6 -2
- package/dist/utils/wstack-paths.d.ts +2 -0
- package/dist/wiring/proxy-rewrite.d.ts +46 -0
- package/dist/wiring/proxy-rewrite.js +24 -121
- package/dist/worktree/index.js +6 -887
- package/instructions/agents/explore-companion.md +45 -18
- package/instructions/agents/memory-curator.md +34 -22
- package/instructions/coordination/director-preamble.md +12 -7
- package/instructions/coordination/subagent-baseline.md +26 -5
- package/instructions/leader-after-task.md +4 -1
- package/instructions/llm/chimera-review.md +30 -9
- package/instructions/llm/memory-curator.md +31 -0
- package/instructions/sections/tool/delegation-compact.md +2 -0
- package/instructions/sections/tool/delegation-full.md +5 -1
- package/instructions/sections/tool/mailbox-compact.md +5 -1
- package/instructions/sections/tool/mailbox-full.md +3 -0
- package/instructions/sections/tool/session-note-compact.md +3 -0
- package/instructions/sections/tool/session-note-full.md +15 -0
- package/instructions/system-lite.md +9 -4
- package/instructions/system-pro.md +66 -143
- package/instructions/system.md +73 -175
- package/package.json +32 -4
- package/dist/coordination/agents.d.ts +0 -8
- package/dist/coordination/transport.d.ts +0 -2
- package/dist/storage/session-writer/session-writer-summary-tracker.d.ts +0 -28
package/dist/security/index.js
CHANGED
|
@@ -1,308 +1,77 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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,
|
|
732
|
-
diagnostics.push({ severity: "error", code, path:
|
|
500
|
+
function error(diagnostics, code, path5, message) {
|
|
501
|
+
diagnostics.push({ severity: "error", code, path: path5, message });
|
|
733
502
|
}
|
|
734
|
-
function validateStringList(value,
|
|
503
|
+
function validateStringList(value, path5, diagnostics, limits) {
|
|
735
504
|
if (!Array.isArray(value)) {
|
|
736
|
-
error(diagnostics, limits.listErrorCode,
|
|
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,
|
|
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 = `${
|
|
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
|
|
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
|
|
1873
|
-
import * as
|
|
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 =
|
|
1935
|
-
const
|
|
1936
|
-
return !!
|
|
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(/^~([\\/])/, (_,
|
|
2150
|
-
const resolved =
|
|
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 =
|
|
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(
|
|
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) :
|
|
1121
|
+
return tail.length === 0 ? realpathSync(probe) : path3.join(realpathSync(probe), ...tail);
|
|
2309
1122
|
} catch {
|
|
2310
|
-
const parent =
|
|
1123
|
+
const parent = path3.dirname(probe);
|
|
2311
1124
|
if (parent === probe) return p;
|
|
2312
|
-
tail.unshift(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
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
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
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
|
-
|
|
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 ?
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
2185
|
+
function isRecord2(value) {
|
|
4260
2186
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4261
2187
|
}
|
|
4262
|
-
function addString(issues, value,
|
|
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:
|
|
2191
|
+
issues.push({ path: path5, message: "must be a non-empty string" });
|
|
4266
2192
|
return false;
|
|
4267
2193
|
}
|
|
4268
|
-
function addEnum(issues, value,
|
|
2194
|
+
function addEnum(issues, value, path5, values) {
|
|
4269
2195
|
if (typeof value === "string" && values.has(value)) return true;
|
|
4270
|
-
issues.push({ path:
|
|
2196
|
+
issues.push({ path: path5, message: `must be one of: ${[...values].join(", ")}` });
|
|
4271
2197
|
return false;
|
|
4272
2198
|
}
|
|
4273
|
-
function validateAttributes(issues, value,
|
|
4274
|
-
if (!
|
|
4275
|
-
issues.push({ path:
|
|
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: `${
|
|
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: `${
|
|
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,
|
|
4297
|
-
if (!
|
|
4298
|
-
issues.push({ path:
|
|
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], `${
|
|
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, `${
|
|
2231
|
+
validateAttributes(issues, value.constraints, `${path5}.constraints`);
|
|
4306
2232
|
return true;
|
|
4307
2233
|
}
|
|
4308
2234
|
function decodeTrustBoundaryRequest(input) {
|
|
4309
2235
|
const issues = [];
|
|
4310
|
-
if (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
}
|