mergen-server 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (308) hide show
  1. package/LICENSE +57 -0
  2. package/README.md +96 -0
  3. package/dist/__stubs__/calibration.js +471 -0
  4. package/dist/__stubs__/causal.js +601 -0
  5. package/dist/__stubs__/closed-source.js +150 -0
  6. package/dist/__stubs__/license.js +35 -0
  7. package/dist/__stubs__/plans.js +197 -0
  8. package/dist/app.js +467 -0
  9. package/dist/cli.js +250 -0
  10. package/dist/commands/agent-identity.js +79 -0
  11. package/dist/commands/gate.js +107 -0
  12. package/dist/commands/github.js +632 -0
  13. package/dist/commands/incident.js +644 -0
  14. package/dist/commands/policy.js +710 -0
  15. package/dist/commands/scan.js +243 -0
  16. package/dist/commands/setup.js +1218 -0
  17. package/dist/commands/shared.js +109 -0
  18. package/dist/commands/team.js +809 -0
  19. package/dist/datadog/blame-attribution.js +228 -0
  20. package/dist/datadog/client.js +159 -0
  21. package/dist/datadog/compactor.js +169 -0
  22. package/dist/datadog/fingerprinter.js +34 -0
  23. package/dist/datadog/incident-state.js +21 -0
  24. package/dist/datadog/line-matcher.js +56 -0
  25. package/dist/datadog/memory-store.js +399 -0
  26. package/dist/datadog/otel-trace.js +67 -0
  27. package/dist/index.js +505 -0
  28. package/dist/intelligence/action-risk.js +132 -0
  29. package/dist/intelligence/activity-feed.js +40 -0
  30. package/dist/intelligence/agent-identity.js +115 -0
  31. package/dist/intelligence/agent-pipeline.js +329 -0
  32. package/dist/intelligence/agent-profiles.js +81 -0
  33. package/dist/intelligence/ai-commit.js +16 -0
  34. package/dist/intelligence/approval-analytics.js +42 -0
  35. package/dist/intelligence/approval-events.js +5 -0
  36. package/dist/intelligence/arch-boundaries.js +94 -0
  37. package/dist/intelligence/arch-graph.js +117 -0
  38. package/dist/intelligence/autonomy.js +397 -0
  39. package/dist/intelligence/azure-broker.js +57 -0
  40. package/dist/intelligence/azure-proxy-sessions.js +49 -0
  41. package/dist/intelligence/baseline.js +1 -0
  42. package/dist/intelligence/behavior-baseline.js +134 -0
  43. package/dist/intelligence/billing.js +1 -0
  44. package/dist/intelligence/blast-radius.js +224 -0
  45. package/dist/intelligence/bypass.js +157 -0
  46. package/dist/intelligence/calibration-classifier.js +77 -0
  47. package/dist/intelligence/calibration-git-sync.js +74 -0
  48. package/dist/intelligence/calibration.js +1 -0
  49. package/dist/intelligence/cascade-detector.js +58 -0
  50. package/dist/intelligence/case-study-generator.js +126 -0
  51. package/dist/intelligence/causal-graph.js +126 -0
  52. package/dist/intelligence/causal.js +1 -0
  53. package/dist/intelligence/change-risk.js +102 -0
  54. package/dist/intelligence/compliance-report.js +134 -0
  55. package/dist/intelligence/confidence-report.js +85 -0
  56. package/dist/intelligence/corpus-to-policy.js +140 -0
  57. package/dist/intelligence/credential-broker.js +112 -0
  58. package/dist/intelligence/credential-misuse.js +39 -0
  59. package/dist/intelligence/debug-sessions.js +108 -0
  60. package/dist/intelligence/decision-escalation.js +36 -0
  61. package/dist/intelligence/decision-memory.js +47 -0
  62. package/dist/intelligence/decision-similarity.js +85 -0
  63. package/dist/intelligence/default-safety-tests.js +53 -0
  64. package/dist/intelligence/degradation-watcher.js +108 -0
  65. package/dist/intelligence/detector-plugins.js +70 -0
  66. package/dist/intelligence/detectors.js +1 -0
  67. package/dist/intelligence/device-auth.js +138 -0
  68. package/dist/intelligence/diff-size.js +42 -0
  69. package/dist/intelligence/ebpf-verify.js +45 -0
  70. package/dist/intelligence/enterprise-policy-engine.js +1122 -0
  71. package/dist/intelligence/error-fingerprint.js +1 -0
  72. package/dist/intelligence/execution-gate.js +100 -0
  73. package/dist/intelligence/execution-mode.js +22 -0
  74. package/dist/intelligence/gate-analytics.js +256 -0
  75. package/dist/intelligence/gate-decision.js +64 -0
  76. package/dist/intelligence/gcp-broker.js +50 -0
  77. package/dist/intelligence/git-adr-sync.js +207 -0
  78. package/dist/intelligence/hitl-hold.js +460 -0
  79. package/dist/intelligence/hypothesis-history.js +93 -0
  80. package/dist/intelligence/idp-identity.js +83 -0
  81. package/dist/intelligence/impl-critic.js +216 -0
  82. package/dist/intelligence/incident-autopilot.js +622 -0
  83. package/dist/intelligence/incident-replay.js +162 -0
  84. package/dist/intelligence/incident-result-cache.js +33 -0
  85. package/dist/intelligence/incident-similarity.js +58 -0
  86. package/dist/intelligence/infra-detectors.js +312 -0
  87. package/dist/intelligence/license.js +185 -0
  88. package/dist/intelligence/llm-spokesperson.js +62 -0
  89. package/dist/intelligence/mcp-prompts.js +320 -0
  90. package/dist/intelligence/mcp-resources.js +171 -0
  91. package/dist/intelligence/normalize.js +108 -0
  92. package/dist/intelligence/notifications.js +83 -0
  93. package/dist/intelligence/oidc-issuer.js +87 -0
  94. package/dist/intelligence/override-corpus.js +597 -0
  95. package/dist/intelligence/planning-gate.js +84 -0
  96. package/dist/intelligence/plans.js +223 -0
  97. package/dist/intelligence/platt-scaling.js +164 -0
  98. package/dist/intelligence/policy-proposals.js +84 -0
  99. package/dist/intelligence/policy-suggester.js +161 -0
  100. package/dist/intelligence/policy-sync.js +103 -0
  101. package/dist/intelligence/policy-test-runner.js +35 -0
  102. package/dist/intelligence/postmortem-parser.js +181 -0
  103. package/dist/intelligence/postmortem-retrieval.js +224 -0
  104. package/dist/intelligence/postmortem-store.js +596 -0
  105. package/dist/intelligence/pr-commenter.js +81 -0
  106. package/dist/intelligence/pr-policy-gate.js +38 -0
  107. package/dist/intelligence/pr-shadow-analyzer.js +229 -0
  108. package/dist/intelligence/process-tree.js +108 -0
  109. package/dist/intelligence/prompts.js +1 -0
  110. package/dist/intelligence/replay.js +38 -0
  111. package/dist/intelligence/repro-steps.js +1 -0
  112. package/dist/intelligence/resource-extractors.js +0 -0
  113. package/dist/intelligence/resource-file-context.js +138 -0
  114. package/dist/intelligence/risk-score.js +22 -0
  115. package/dist/intelligence/rollback.js +137 -0
  116. package/dist/intelligence/runbook-updater.js +143 -0
  117. package/dist/intelligence/sandbox.js +194 -0
  118. package/dist/intelligence/script-trust.js +104 -0
  119. package/dist/intelligence/session-metrics.js +67 -0
  120. package/dist/intelligence/session-threat-tracker.js +231 -0
  121. package/dist/intelligence/shadow-digest-cron.js +81 -0
  122. package/dist/intelligence/shadow-log.js +284 -0
  123. package/dist/intelligence/shell-ast.js +145 -0
  124. package/dist/intelligence/siem-forward.js +104 -0
  125. package/dist/intelligence/slack-digest.js +151 -0
  126. package/dist/intelligence/slack-override-loop.js +217 -0
  127. package/dist/intelligence/slack-routing.js +67 -0
  128. package/dist/intelligence/slack.js +900 -0
  129. package/dist/intelligence/sql-ast.js +31 -0
  130. package/dist/intelligence/team.js +1 -0
  131. package/dist/intelligence/telemetry.js +76 -0
  132. package/dist/intelligence/threshold-optimizer.js +75 -0
  133. package/dist/intelligence/token-budget.js +66 -0
  134. package/dist/intelligence/tool-guard.js +708 -0
  135. package/dist/intelligence/tool-manifest 2.js +74 -0
  136. package/dist/intelligence/tool-manifest.js +125 -0
  137. package/dist/intelligence/tools-analysis.js +857 -0
  138. package/dist/intelligence/tools-arch.js +152 -0
  139. package/dist/intelligence/tools-autonomy.js +495 -0
  140. package/dist/intelligence/tools-blast-radius.js +140 -0
  141. package/dist/intelligence/tools-browser.js +431 -0
  142. package/dist/intelligence/tools-change-timeline.js +154 -0
  143. package/dist/intelligence/tools-credentials.js +117 -0
  144. package/dist/intelligence/tools-datadog.js +233 -0
  145. package/dist/intelligence/tools-debug-sessions.js +209 -0
  146. package/dist/intelligence/tools-discovery.js +284 -0
  147. package/dist/intelligence/tools-infra.js +612 -0
  148. package/dist/intelligence/tools-intent.js +128 -0
  149. package/dist/intelligence/tools-memory.js +514 -0
  150. package/dist/intelligence/tools-runbook.js +951 -0
  151. package/dist/intelligence/tools-sessions.js +107 -0
  152. package/dist/intelligence/tools-state.js +232 -0
  153. package/dist/intelligence/tools-utility.js +430 -0
  154. package/dist/intelligence/tools-validate.js +215 -0
  155. package/dist/intelligence/tools.js +1 -0
  156. package/dist/intelligence/trace-context.js +11 -0
  157. package/dist/intelligence/unclassified-clusters.js +87 -0
  158. package/dist/intelligence/usage.js +195 -0
  159. package/dist/routes/active-authors.js +63 -0
  160. package/dist/routes/activity-feed.js +21 -0
  161. package/dist/routes/adr.js +117 -0
  162. package/dist/routes/agent-activity.js +184 -0
  163. package/dist/routes/agent-blunders.js +163 -0
  164. package/dist/routes/agents.js +175 -0
  165. package/dist/routes/api-keys.js +65 -0
  166. package/dist/routes/arch.js +75 -0
  167. package/dist/routes/audit-export.js +93 -0
  168. package/dist/routes/billing-dashboard.js +158 -0
  169. package/dist/routes/billing-outcome.js +77 -0
  170. package/dist/routes/calibration.js +305 -0
  171. package/dist/routes/ci-gate-history.js +20 -0
  172. package/dist/routes/ci-gate.js +232 -0
  173. package/dist/routes/ci.js +293 -0
  174. package/dist/routes/compliance-report.js +24 -0
  175. package/dist/routes/confidence.js +30 -0
  176. package/dist/routes/credentials.js +119 -0
  177. package/dist/routes/dashboard.js +1407 -0
  178. package/dist/routes/decision-memory.js +42 -0
  179. package/dist/routes/demo.js +844 -0
  180. package/dist/routes/device-auth-stub.js +173 -0
  181. package/dist/routes/explain-why.js +39 -0
  182. package/dist/routes/gate-analytics.js +213 -0
  183. package/dist/routes/gate.js +28 -0
  184. package/dist/routes/github-webhook.js +364 -0
  185. package/dist/routes/habituation.js +30 -0
  186. package/dist/routes/health-integrations.js +243 -0
  187. package/dist/routes/heartbeats.js +45 -0
  188. package/dist/routes/hitl.js +364 -0
  189. package/dist/routes/impact-report.js +645 -0
  190. package/dist/routes/incident-webhook.js +63 -0
  191. package/dist/routes/incidents.js +366 -0
  192. package/dist/routes/layers.js +62 -0
  193. package/dist/routes/license.js +187 -0
  194. package/dist/routes/oidc-issuer.js +30 -0
  195. package/dist/routes/onboarding.js +170 -0
  196. package/dist/routes/otel.js +67 -0
  197. package/dist/routes/otlp-receiver.js +179 -0
  198. package/dist/routes/overrides.js +241 -0
  199. package/dist/routes/pagerduty.js +258 -0
  200. package/dist/routes/policies.js +809 -0
  201. package/dist/routes/policy-nl.js +169 -0
  202. package/dist/routes/postmortem.js +102 -0
  203. package/dist/routes/pr-shadow.js +47 -0
  204. package/dist/routes/rbac.js +61 -0
  205. package/dist/routes/replay.js +17 -0
  206. package/dist/routes/risk-report.js +164 -0
  207. package/dist/routes/runbooks.js +125 -0
  208. package/dist/routes/safety-test.js +174 -0
  209. package/dist/routes/sdk.js +222 -0
  210. package/dist/routes/sensor.js +819 -0
  211. package/dist/routes/sentry.js +140 -0
  212. package/dist/routes/sessions.js +43 -0
  213. package/dist/routes/setup-ui.js +594 -0
  214. package/dist/routes/shadow-report.js +107 -0
  215. package/dist/routes/slack-routing.js +171 -0
  216. package/dist/routes/team-usage.js +71 -0
  217. package/dist/routes/telemetry.js +28 -0
  218. package/dist/routes/tenants.js +199 -0
  219. package/dist/routes/tickets.js +167 -0
  220. package/dist/routes/validate.js +13 -0
  221. package/dist/routes/war-room.js +113 -0
  222. package/dist/scripts/check-arch.js +22 -0
  223. package/dist/seeds/community-corpus.js +176 -0
  224. package/dist/seeds/corpus.js +1224 -0
  225. package/dist/sensor/action-ledger.js +327 -0
  226. package/dist/sensor/adr-store.js +140 -0
  227. package/dist/sensor/agent-blunder-store.js +370 -0
  228. package/dist/sensor/agent-context-store.js +225 -0
  229. package/dist/sensor/agent-memory-store.js +231 -0
  230. package/dist/sensor/audit-fetch.js +23 -0
  231. package/dist/sensor/audit-log.js +273 -0
  232. package/dist/sensor/buffer-schemas.js +225 -0
  233. package/dist/sensor/buffer.js +599 -0
  234. package/dist/sensor/bypass-tracker.js +93 -0
  235. package/dist/sensor/ci-gate-history.js +70 -0
  236. package/dist/sensor/cloud-auth.js +183 -0
  237. package/dist/sensor/commit-context-store.js +235 -0
  238. package/dist/sensor/docker-log-stream.js +164 -0
  239. package/dist/sensor/docker-monitor.js +122 -0
  240. package/dist/sensor/extended-buffer.js +46 -0
  241. package/dist/sensor/feedback-token.js +41 -0
  242. package/dist/sensor/file-lock.js +6 -0
  243. package/dist/sensor/fs-watcher.js +106 -0
  244. package/dist/sensor/gate-heartbeat.js +122 -0
  245. package/dist/sensor/git-suspect.js +136 -0
  246. package/dist/sensor/habituation-store.js +77 -0
  247. package/dist/sensor/heartbeat-monitor.js +133 -0
  248. package/dist/sensor/incident-store.js +340 -0
  249. package/dist/sensor/infra-normalizer.js +513 -0
  250. package/dist/sensor/ingest.js +339 -0
  251. package/dist/sensor/jest-reporter.js +52 -0
  252. package/dist/sensor/k8s-events.js +132 -0
  253. package/dist/sensor/layer2-store.js +111 -0
  254. package/dist/sensor/layer3-store.js +230 -0
  255. package/dist/sensor/layer4-store.js +147 -0
  256. package/dist/sensor/logger.js +13 -0
  257. package/dist/sensor/otel-exporter.js +161 -0
  258. package/dist/sensor/paths.js +71 -0
  259. package/dist/sensor/policy-history.js +147 -0
  260. package/dist/sensor/pr-shadow-store.js +83 -0
  261. package/dist/sensor/process-watcher.js +162 -0
  262. package/dist/sensor/rbac.js +92 -0
  263. package/dist/sensor/redact.js +114 -0
  264. package/dist/sensor/redis-store.js +191 -0
  265. package/dist/sensor/route-reachability.js +64 -0
  266. package/dist/sensor/security-utils.js +18 -0
  267. package/dist/sensor/service-graph.js +170 -0
  268. package/dist/sensor/service-topology.js +227 -0
  269. package/dist/sensor/session-history.js +74 -0
  270. package/dist/sensor/session-persist.js +38 -0
  271. package/dist/sensor/shadow-promote.js +122 -0
  272. package/dist/sensor/sourcemap.js +281 -0
  273. package/dist/sensor/sqlite-store.js +205 -0
  274. package/dist/sensor/sso.js +164 -0
  275. package/dist/sensor/vitest-reporter.js +65 -0
  276. package/dist/sensor/watcher.js +48 -0
  277. package/dist/storage/interfaces.js +0 -0
  278. package/dist/storage/pg/pg-action-ledger.js +138 -0
  279. package/dist/storage/pg/pg-approval-store.js +107 -0
  280. package/dist/storage/pg/pg-blunder-store.js +193 -0
  281. package/dist/storage/pg/pg-ci-gate-history.js +83 -0
  282. package/dist/storage/pg/pg-client.js +24 -0
  283. package/dist/storage/pg/pg-event-store.js +63 -0
  284. package/dist/storage/pg/pg-incident-store.js +190 -0
  285. package/dist/storage/pg/pg-migrations.js +26 -0
  286. package/dist/storage/pg/pg-override-corpus.js +447 -0
  287. package/dist/storage/pg/pg-shadow-log.js +129 -0
  288. package/dist/storage/sqlite/sqlite-action-ledger.js +18 -0
  289. package/dist/storage/sqlite/sqlite-approval-store.js +37 -0
  290. package/dist/storage/sqlite/sqlite-blunder-store.js +27 -0
  291. package/dist/storage/sqlite/sqlite-ci-gate-history.js +19 -0
  292. package/dist/storage/sqlite/sqlite-event-store.js +29 -0
  293. package/dist/storage/sqlite/sqlite-incident-store.js +34 -0
  294. package/dist/storage/sqlite/sqlite-override-corpus.js +75 -0
  295. package/dist/storage/sqlite/sqlite-shadow-log.js +36 -0
  296. package/dist/storage/store-factory.js +55 -0
  297. package/dist/storage/store-registry.js +31 -0
  298. package/dist/update-checker.js +107 -0
  299. package/dist/workers/autopilot-worker.js +28 -0
  300. package/dist/workers/notification-worker.js +28 -0
  301. package/dist/workers/queues.js +58 -0
  302. package/dist/workers/validation-worker.js +24 -0
  303. package/dist/workers/worker-registry.js +18 -0
  304. package/package.json +123 -0
  305. package/sdk/mergen-inject.js +260 -0
  306. package/sdk/node.js +313 -0
  307. package/sdk/vite-plugin.ts +57 -0
  308. package/sdk/webpack-plugin.js +106 -0
@@ -0,0 +1,207 @@
1
+ import { execSync } from "child_process";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { DATA_DIR } from "../sensor/paths.js";
5
+ import { adrStore } from "../sensor/adr-store.js";
6
+ import { getStores } from "../storage/store-registry.js";
7
+ import logger from "../sensor/logger.js";
8
+ const SYSTEM_TENANT = process.env.MERGEN_SYSTEM_TENANT_ID;
9
+ const STATE_FILE = path.join(DATA_DIR, "git-adr-sync.json");
10
+ const POLL_INTERVAL_MS = 24 * 60 * 60 * 1e3;
11
+ const CONSTRAINT_KEYWORDS = [
12
+ "never ",
13
+ "don't ",
14
+ "do not ",
15
+ "avoid ",
16
+ "block:",
17
+ "constraint:",
18
+ "decided:",
19
+ "adr:",
20
+ "policy:",
21
+ "rule:",
22
+ "freeze",
23
+ "hold off",
24
+ "should not",
25
+ "must not",
26
+ "compliance",
27
+ "settlement window",
28
+ "batch window",
29
+ "maintenance window",
30
+ "we won't",
31
+ "not during",
32
+ "unsafe during"
33
+ ];
34
+ const INCIDENT_TAG_HINTS = [
35
+ [/pool|connection.?limit|max.?conn/i, "infra_db_connection_pool"],
36
+ [/oom|out.of.memory|heap|memory.?leak/i, "oom_kill"],
37
+ [/rate.?limit|throttl/i, "rate_limit_cascade"],
38
+ [/auth|token|session|oauth|jwt/i, "auth_token_not_persisted"],
39
+ [/cache|redis|memcach/i, "cache_invalidation"],
40
+ [/deploy|rollout|release|image/i, "deploy_config_drift"],
41
+ [/cert|tls|ssl|https/i, "cert_expiry"],
42
+ [/disk|storage|volume|space/i, "disk_pressure"],
43
+ [/scale|replica|pod.?count|instance/i, "infra_scaling"],
44
+ [/migration|schema|alter.?table/i, "db_migration_lock"],
45
+ [/friday|settlement|batch|weekend|end.of.day/i, "batch_window_conflict"]
46
+ ];
47
+ const OVERRIDE_REASON_HINTS = [
48
+ [/friday|settlement|batch|weekend|end.of.day|window/i, "batch-window"],
49
+ [/cost|budget|billing|expensive/i, "cost-constraint"],
50
+ [/cab|freeze|compliance|approval|change.control/i, "compliance-hold"],
51
+ [/replica|read.only|primary/i, "prefer-read-replica"],
52
+ [/maintenance|scheduled|downtime/i, "maintenance-window"],
53
+ [/wrong.diag|misidentif|incorrect.root/i, "wrong-diagnosis"],
54
+ [/wrong.fix|bad.command|revert/i, "wrong-fix"]
55
+ ];
56
+ function loadState() {
57
+ try {
58
+ if (fs.existsSync(STATE_FILE)) {
59
+ return JSON.parse(fs.readFileSync(STATE_FILE, "utf8"));
60
+ }
61
+ } catch {
62
+ }
63
+ return { processedShas: [], processedAdrIds: [], lastSyncAt: 0 };
64
+ }
65
+ function saveState(state) {
66
+ try {
67
+ fs.mkdirSync(DATA_DIR, { recursive: true });
68
+ state.processedShas = state.processedShas.slice(-2e3);
69
+ state.processedAdrIds = state.processedAdrIds.slice(-500);
70
+ const tmp = `${STATE_FILE}.tmp`;
71
+ fs.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf8");
72
+ fs.renameSync(tmp, STATE_FILE);
73
+ } catch (err) {
74
+ logger.warn({ err }, "git-adr-sync: failed to persist state");
75
+ }
76
+ }
77
+ function inferTag(text) {
78
+ for (const [pattern, tag] of INCIDENT_TAG_HINTS) {
79
+ if (pattern.test(text)) return tag;
80
+ }
81
+ return "operational_constraint";
82
+ }
83
+ function inferReason(text) {
84
+ for (const [pattern, reason] of OVERRIDE_REASON_HINTS) {
85
+ if (pattern.test(text)) return reason;
86
+ }
87
+ return "on-call-discretion";
88
+ }
89
+ function extractCommand(text) {
90
+ const backtick = text.match(/`([^`]{4,200})`/);
91
+ if (backtick) return backtick[1].trim();
92
+ const shellLine = text.split("\n").find(
93
+ (l) => /^(kubectl|docker|systemctl|service|npm|yarn|make|helm|psql)\s/i.test(l.trim())
94
+ );
95
+ if (shellLine) return shellLine.trim().slice(0, 500);
96
+ return text.slice(0, 100).trim();
97
+ }
98
+ function isConstraintText(text) {
99
+ const lower = text.toLowerCase();
100
+ return CONSTRAINT_KEYWORDS.some((kw) => lower.includes(kw));
101
+ }
102
+ async function scanGitLog(state) {
103
+ let added = 0;
104
+ const processed = new Set(state.processedShas);
105
+ try {
106
+ const raw = execSync(
107
+ "git log --no-merges --format=%H%x1F%s%x1F%b%x1E -n 500 2>/dev/null",
108
+ { encoding: "utf8", timeout: 1e4, stdio: ["ignore", "pipe", "ignore"] }
109
+ );
110
+ const records = raw.split("").filter(Boolean);
111
+ for (const record of records) {
112
+ const [sha, subject, body = ""] = record.split("");
113
+ if (!sha) continue;
114
+ const cleanSha = sha.trim();
115
+ if (processed.has(cleanSha)) continue;
116
+ processed.add(cleanSha);
117
+ const fullText = `${subject ?? ""}
118
+ ${body}`;
119
+ if (!isConstraintText(fullText)) continue;
120
+ const sentences = fullText.split(/[.!?\n]/).map((s) => s.trim()).filter(Boolean);
121
+ for (const sentence of sentences) {
122
+ if (!isConstraintText(sentence)) continue;
123
+ const tag = inferTag(sentence);
124
+ const reason = inferReason(sentence);
125
+ const command = extractCommand(fullText);
126
+ try {
127
+ await getStores().overrides.recordOverride(
128
+ {
129
+ incidentTag: tag,
130
+ proposedCommand: command,
131
+ overrideReason: reason,
132
+ service: "unknown",
133
+ environment: "production",
134
+ note: sentence.slice(0, 200),
135
+ manualAction: `git commit ${cleanSha.slice(0, 8)}: ${(subject ?? "").slice(0, 100)}`,
136
+ actor: "git-adr-sync"
137
+ },
138
+ SYSTEM_TENANT
139
+ );
140
+ added++;
141
+ } catch {
142
+ }
143
+ break;
144
+ }
145
+ }
146
+ state.processedShas = [...processed];
147
+ } catch (err) {
148
+ logger.debug({ err: err.message }, "git-adr-sync: git log unavailable");
149
+ }
150
+ return added;
151
+ }
152
+ async function syncAdrStore(state) {
153
+ let added = 0;
154
+ const processed = new Set(state.processedAdrIds);
155
+ const adrs = adrStore.list().filter((adr) => adr.status === "accepted");
156
+ for (const adr of adrs) {
157
+ if (processed.has(adr.id)) continue;
158
+ processed.add(adr.id);
159
+ const fullText = [adr.decision, adr.consequences, adr.rationale].join("\n");
160
+ if (!isConstraintText(fullText)) continue;
161
+ const tag = inferTag(fullText);
162
+ const reason = inferReason(fullText);
163
+ const command = extractCommand(fullText);
164
+ try {
165
+ await getStores().overrides.recordOverride(
166
+ {
167
+ incidentTag: tag,
168
+ proposedCommand: command,
169
+ overrideReason: reason,
170
+ service: "unknown",
171
+ environment: "production",
172
+ note: `${adr.id}: ${adr.title}`.slice(0, 200),
173
+ manualAction: adr.decision.slice(0, 500),
174
+ actor: "adr-store-sync"
175
+ },
176
+ SYSTEM_TENANT
177
+ );
178
+ added++;
179
+ } catch {
180
+ }
181
+ }
182
+ state.processedAdrIds = [...processed];
183
+ return added;
184
+ }
185
+ async function sync() {
186
+ const state = loadState();
187
+ const gitAdded = await scanGitLog(state);
188
+ const adrAdded = await syncAdrStore(state);
189
+ state.lastSyncAt = Date.now();
190
+ saveState(state);
191
+ const total = gitAdded + adrAdded;
192
+ if (total > 0) {
193
+ logger.info({ gitAdded, adrAdded }, "git-adr-sync: new operational constraints added to corpus");
194
+ } else {
195
+ logger.debug("git-adr-sync: sync complete \u2014 no new constraints found");
196
+ }
197
+ }
198
+ function startGitAdrSync() {
199
+ logger.info("git-adr-sync: starting \u2014 scanning git history and ADR store for operational constraints");
200
+ void sync();
201
+ setInterval(() => {
202
+ void sync();
203
+ }, POLL_INTERVAL_MS).unref();
204
+ }
205
+ export {
206
+ startGitAdrSync
207
+ };
@@ -0,0 +1,460 @@
1
+ import fs from "fs";
2
+ import { recordHitlDecision } from "./gate-analytics.js";
3
+ import { loadEnterprisePolicy } from "./enterprise-policy-engine.js";
4
+ import { computeBlastRadius } from "./blast-radius.js";
5
+ import { findSimilarDecisions } from "./decision-similarity.js";
6
+ import { findSimilarIncidents } from "./incident-similarity.js";
7
+ import { getStores } from "../storage/store-registry.js";
8
+ import { HITL_HOLDS_FILE, DATA_DIR, zeroRetentionMode } from "../sensor/paths.js";
9
+ import logger from "../sensor/logger.js";
10
+ const HOLD_TIMEOUT_MS = 15 * 60 * 1e3;
11
+ function _loadQuorumRules() {
12
+ const raw = process.env.MERGEN_HITL_QUORUM_RULES ?? "";
13
+ return raw.split(",").map((s) => s.trim()).filter(Boolean).map((s) => {
14
+ const lastColon = s.lastIndexOf(":");
15
+ if (lastColon === -1) return null;
16
+ const pattern = s.slice(0, lastColon).trim();
17
+ const required = parseInt(s.slice(lastColon + 1).trim(), 10);
18
+ if (!pattern || !Number.isFinite(required) || required < 2) return null;
19
+ return { pattern, required };
20
+ }).filter((r) => r !== null);
21
+ }
22
+ const _quorumRules = _loadQuorumRules();
23
+ function _quorumRequired(commandArg, toolName) {
24
+ const haystack = `${toolName} ${commandArg}`.toLowerCase();
25
+ for (const rule of _quorumRules) {
26
+ if (haystack.includes(rule.pattern.toLowerCase())) return rule.required;
27
+ }
28
+ return 1;
29
+ }
30
+ const _pendingHolds = /* @__PURE__ */ new Map();
31
+ function _persistHolds() {
32
+ if (zeroRetentionMode()) return;
33
+ try {
34
+ fs.mkdirSync(DATA_DIR, { recursive: true });
35
+ const now = Date.now();
36
+ const active = [..._pendingHolds.entries()].filter(([, h]) => now < h.heldAt + HOLD_TIMEOUT_MS).map(([token, h]) => ({
37
+ token,
38
+ toolName: h.toolName,
39
+ policyReason: h.policyReason,
40
+ triggeredRules: h.triggeredRules,
41
+ heldAt: h.heldAt,
42
+ expiresAt: h.heldAt + HOLD_TIMEOUT_MS,
43
+ quorumRequired: h.quorumRequired,
44
+ approvedBy: [...h.approvedBy]
45
+ }));
46
+ const tmp = `${HITL_HOLDS_FILE}.tmp.${process.pid}`;
47
+ fs.writeFileSync(tmp, JSON.stringify({ version: 1, holds: active }), "utf8");
48
+ fs.renameSync(tmp, HITL_HOLDS_FILE);
49
+ } catch (err) {
50
+ logger.warn({ err }, "tool-guard: hold persist failed");
51
+ }
52
+ }
53
+ function denyStaleHoldsOnStartup() {
54
+ if (zeroRetentionMode() || !fs.existsSync(HITL_HOLDS_FILE)) return;
55
+ try {
56
+ const raw = JSON.parse(fs.readFileSync(HITL_HOLDS_FILE, "utf8"));
57
+ if (raw?.version !== 1 || !Array.isArray(raw.holds)) return;
58
+ const now = Date.now();
59
+ const stale = raw.holds.filter((h) => h.expiresAt > now);
60
+ if (stale.length === 0) return;
61
+ logger.warn(
62
+ { count: stale.length, tokens: stale.map((h) => h.token) },
63
+ "tool-guard: denying stale HITL holds from previous server run \u2014 the MCP client must resubmit these tool calls."
64
+ );
65
+ try {
66
+ fs.unlinkSync(HITL_HOLDS_FILE);
67
+ } catch {
68
+ }
69
+ } catch (err) {
70
+ logger.warn({ err }, "tool-guard: failed to load stale holds file");
71
+ }
72
+ }
73
+ function approveToolCall(token, approverId = "operator") {
74
+ const hold = _pendingHolds.get(token);
75
+ if (!hold) return false;
76
+ hold.approvedBy.add(approverId);
77
+ const approvedCount = hold.approvedBy.size;
78
+ const required = hold.quorumRequired;
79
+ if (approvedCount < required) {
80
+ void _fireQuorumProgress(token, hold, approvedCount, required);
81
+ _persistHolds();
82
+ logger.info({ token, approverId, approvedCount, required }, "tool-guard: quorum partial approval");
83
+ return true;
84
+ }
85
+ clearTimeout(hold.timeoutHandle);
86
+ if (hold.reminderHandle) clearTimeout(hold.reminderHandle);
87
+ if (hold.escalationHandle) clearTimeout(hold.escalationHandle);
88
+ _pendingHolds.delete(token);
89
+ _persistHolds();
90
+ recordHitlDecision(hold.triggeredRules, "approve", hold.heldAt);
91
+ logger.info({ token, toolName: hold.toolName, approvedBy: [...hold.approvedBy] }, "tool-guard: HITL approved (quorum met)");
92
+ const constraintNote = hold.approvedConstraints ? `
93
+ Approved with constraints: ${JSON.stringify(hold.approvedConstraints)}` : "";
94
+ hold.resolve({
95
+ content: [{ type: "text", text: `\u2705 Tool call \`${hold.toolName}\` approved by operator (${approvedCount}/${required}).${constraintNote}` }]
96
+ });
97
+ return true;
98
+ }
99
+ function approveToolCallConstrained(token, constraints, approverId = "operator") {
100
+ const hold = _pendingHolds.get(token);
101
+ if (!hold) return { ok: false, error: "token not found or already expired" };
102
+ if (typeof constraints !== "object" || Array.isArray(constraints) || Object.keys(constraints).length === 0) {
103
+ return { ok: false, error: "constraints must be a non-empty object" };
104
+ }
105
+ hold.approvedConstraints = constraints;
106
+ hold.approvedBy.add(approverId);
107
+ const approvedCount = hold.approvedBy.size;
108
+ const required = hold.quorumRequired;
109
+ if (approvedCount < required) {
110
+ void _fireQuorumProgress(token, hold, approvedCount, required);
111
+ _persistHolds();
112
+ logger.info({ token, approverId, approvedCount, required }, "tool-guard: quorum partial approval (with constraints)");
113
+ return { ok: true };
114
+ }
115
+ clearTimeout(hold.timeoutHandle);
116
+ if (hold.reminderHandle) clearTimeout(hold.reminderHandle);
117
+ if (hold.escalationHandle) clearTimeout(hold.escalationHandle);
118
+ _pendingHolds.delete(token);
119
+ _persistHolds();
120
+ recordHitlDecision(hold.triggeredRules, "approve", hold.heldAt);
121
+ logger.info({ token, toolName: hold.toolName, constraints, approvedBy: [...hold.approvedBy] }, "tool-guard: HITL approved with constraints (quorum met)");
122
+ hold.resolve({
123
+ content: [{
124
+ type: "text",
125
+ text: `\u2705 Tool call \`${hold.toolName}\` approved with constraints by ${approverId} (${approvedCount}/${required}).
126
+ Constraints: ${JSON.stringify(constraints)}
127
+ The action must be scoped to these constraints before execution.`
128
+ }]
129
+ });
130
+ return { ok: true };
131
+ }
132
+ function denyToolCall(token) {
133
+ const hold = _pendingHolds.get(token);
134
+ if (!hold) return false;
135
+ clearTimeout(hold.timeoutHandle);
136
+ if (hold.reminderHandle) clearTimeout(hold.reminderHandle);
137
+ if (hold.escalationHandle) clearTimeout(hold.escalationHandle);
138
+ _pendingHolds.delete(token);
139
+ _persistHolds();
140
+ recordHitlDecision(hold.triggeredRules, "deny", hold.heldAt);
141
+ logger.info({ token, toolName: hold.toolName }, "tool-guard: HITL denied");
142
+ hold.resolve({
143
+ content: [{
144
+ type: "text",
145
+ text: [
146
+ `\u274C Tool call \`${hold.toolName}\` denied by operator.`,
147
+ ``,
148
+ `**Why:** ${hold.policyReason}`,
149
+ ``,
150
+ `**What to do instead:** ${hold.suggestedAlt}`
151
+ ].join("\n")
152
+ }],
153
+ isError: true
154
+ });
155
+ return true;
156
+ }
157
+ function getPendingHolds() {
158
+ const now = Date.now();
159
+ return [..._pendingHolds.entries()].map(([token, h]) => ({
160
+ token,
161
+ toolName: h.toolName,
162
+ commandArg: h.commandArg,
163
+ policyReason: h.policyReason,
164
+ suggestedAlt: h.suggestedAlt,
165
+ triggeredRules: h.triggeredRules,
166
+ heldAt: h.heldAt,
167
+ waitingSec: Math.round((now - h.heldAt) / 1e3),
168
+ quorumRequired: h.quorumRequired,
169
+ approvedCount: h.approvedBy.size
170
+ }));
171
+ }
172
+ function escapeMrkdwn(s) {
173
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\*/g, "\uFF0A").replace(/_/g, "\uFF3F").replace(/~/g, "\uFF5E").replace(/`/g, "\uFF40").replace(/@/g, "\uFF20");
174
+ }
175
+ async function fireHitlWebhook(token, toolName, argsSnapshot, reason, port, opts) {
176
+ const webhookUrl = process.env.MERGEN_HITL_WEBHOOK_URL;
177
+ if (!webhookUrl) {
178
+ const baseUrl2 = (process.env.MERGEN_PUBLIC_URL ?? "").replace(/\/$/, "") || `http://127.0.0.1:${port}`;
179
+ logger.warn(
180
+ { token, toolName },
181
+ `tool-guard: HITL triggered but MERGEN_HITL_WEBHOOK_URL is not set \u2014 resolve manually: POST ${baseUrl2}/hitl/approve?token=${token}`
182
+ );
183
+ return;
184
+ }
185
+ const baseUrl = (process.env.MERGEN_PUBLIC_URL ?? "").replace(/\/$/, "") || `http://127.0.0.1:${port}`;
186
+ const approveUrl = `${baseUrl}/hitl/approve?token=${token}`;
187
+ const denyUrl = `${baseUrl}/hitl/deny?token=${token}`;
188
+ const constrainedUrl = `${baseUrl}/hitl/approve-constrained?token=${token}`;
189
+ const policy = loadEnterprisePolicy();
190
+ const matchedRules = policy.rules.filter((r) => opts.triggeredRules.includes(r.id));
191
+ const blast = computeBlastRadius(opts.commandArg || toolName);
192
+ const rulesText = matchedRules.length > 0 ? matchedRules.map((r) => `\u2022 *${r.name}*: ${r.description}`).join("\n") : `\u2022 ${reason}`;
193
+ const blastText = [
194
+ `Scope: \`${blast.scope}\``,
195
+ `Reversible: ${blast.reversible ? "Yes" : "*No*"}`,
196
+ blast.dataAtRisk ? "*Data at risk*" : null,
197
+ blast.summary
198
+ ].filter(Boolean).join(" \xB7 ");
199
+ const safeToolName = escapeMrkdwn(toolName);
200
+ const safeArgsSnippet = escapeMrkdwn(argsSnapshot.slice(0, 200));
201
+ const quorumText = opts.quorumRequired > 1 ? `*Quorum required: ${opts.quorumRequired} approvers*` : null;
202
+ let similarityText = null;
203
+ try {
204
+ const [overrides, shadowEntries] = await Promise.all([
205
+ getStores().overrides.getAllOverrides(),
206
+ getStores().shadowLog.getShadowEntries()
207
+ ]);
208
+ const similarity = findSimilarDecisions(opts.commandArg || toolName, null, overrides, shadowEntries);
209
+ if (similarity.totalConsidered > 0) {
210
+ const icon = similarity.recommendation === "hold" ? "\u26A0\uFE0F" : "\u2139\uFE0F";
211
+ similarityText = `${icon} ${escapeMrkdwn(similarity.rationale)}`;
212
+ }
213
+ } catch (err) {
214
+ logger.warn({ err, token }, "tool-guard: decision-memory similarity lookup failed");
215
+ }
216
+ let relatedIncidentsText = null;
217
+ try {
218
+ const related = findSimilarIncidents({ commandText: opts.commandArg || toolName });
219
+ if (related.length > 0) {
220
+ relatedIncidentsText = related.map((r) => `\u2022 ${escapeMrkdwn(r.hypothesis)} _(${r.daysAgo}d ago \xB7 ${Math.round(r.confidence * 100)}% match)_`).join("\n");
221
+ }
222
+ } catch (err) {
223
+ logger.warn({ err, token }, "tool-guard: incident-similarity lookup failed");
224
+ }
225
+ const payload = {
226
+ text: `\u26A0\uFE0F *Mergen HITL Gate* \u2014 \`${safeToolName}\` is awaiting approval`,
227
+ blocks: [
228
+ {
229
+ type: "header",
230
+ text: { type: "plain_text", text: "\u26A0\uFE0F Mergen HITL Gate \u2014 Approval Required", emoji: true }
231
+ },
232
+ {
233
+ type: "section",
234
+ fields: [
235
+ { type: "mrkdwn", text: `*Tool*
236
+ \`${safeToolName}\`` },
237
+ { type: "mrkdwn", text: `*Args*
238
+ \`${safeArgsSnippet}\`` }
239
+ ]
240
+ },
241
+ {
242
+ type: "section",
243
+ text: { type: "mrkdwn", text: `*Why flagged*
244
+ ${rulesText}` }
245
+ },
246
+ {
247
+ type: "section",
248
+ text: { type: "mrkdwn", text: `*Blast radius*
249
+ ${blastText}` }
250
+ },
251
+ {
252
+ type: "section",
253
+ text: { type: "mrkdwn", text: `*Suggested alternative*
254
+ ${opts.suggestedAlt}` }
255
+ },
256
+ ...similarityText ? [{
257
+ type: "section",
258
+ text: { type: "mrkdwn", text: `*Similar past actions*
259
+ ${similarityText}` }
260
+ }] : [],
261
+ ...relatedIncidentsText ? [{
262
+ type: "section",
263
+ text: { type: "mrkdwn", text: `*Related incidents*
264
+ ${relatedIncidentsText}` }
265
+ }] : [],
266
+ ...quorumText ? [{
267
+ type: "section",
268
+ text: { type: "mrkdwn", text: quorumText }
269
+ }] : [],
270
+ {
271
+ type: "actions",
272
+ elements: [
273
+ { type: "button", style: "primary", text: { type: "plain_text", text: "\u25B6\uFE0F Approve", emoji: true }, url: approveUrl },
274
+ { type: "button", style: "danger", text: { type: "plain_text", text: "\u{1F6AB} Deny", emoji: true }, url: denyUrl },
275
+ { type: "button", text: { type: "plain_text", text: "\u{1F512} Approve with Constraints", emoji: true }, url: constrainedUrl }
276
+ ]
277
+ },
278
+ {
279
+ type: "context",
280
+ elements: [{ type: "mrkdwn", text: `Token \`${token}\` \xB7 Slack reminder at 5 min \xB7 PagerDuty/Slack escalation at 10 min \xB7 auto-cancel at 15 min` }]
281
+ }
282
+ ],
283
+ mergen: { type: "hitl_gate", token, toolName, reason, approveUrl, denyUrl, constrainedUrl, quorumRequired: opts.quorumRequired }
284
+ };
285
+ try {
286
+ const resp = await fetch(webhookUrl, {
287
+ method: "POST",
288
+ headers: { "Content-Type": "application/json" },
289
+ body: JSON.stringify(payload),
290
+ signal: AbortSignal.timeout(5e3)
291
+ });
292
+ if (!resp.ok) {
293
+ logger.warn({ status: resp.status, webhookUrl }, "tool-guard: HITL webhook returned non-2xx");
294
+ }
295
+ } catch (err) {
296
+ logger.warn({ err, token }, "tool-guard: HITL webhook fire failed");
297
+ }
298
+ }
299
+ async function _fireQuorumProgress(token, hold, approvedCount, required) {
300
+ const webhookUrl = process.env.MERGEN_HITL_WEBHOOK_URL;
301
+ if (!webhookUrl) return;
302
+ try {
303
+ await fetch(webhookUrl, {
304
+ method: "POST",
305
+ headers: { "Content-Type": "application/json" },
306
+ body: JSON.stringify({
307
+ text: `\u2705 *Quorum progress*: ${approvedCount}/${required} approvers for \`${escapeMrkdwn(hold.toolName)}\`. Token: \`${token}\``,
308
+ mergen: { type: "hitl_quorum_progress", token, approvedCount, required }
309
+ }),
310
+ signal: AbortSignal.timeout(5e3)
311
+ });
312
+ } catch (err) {
313
+ logger.warn({ err, token }, "tool-guard: quorum progress webhook failed");
314
+ }
315
+ }
316
+ async function fireHitlReminder(token, toolName, minutesRemaining) {
317
+ const webhookUrl = process.env.MERGEN_HITL_WEBHOOK_URL;
318
+ if (!webhookUrl) return;
319
+ try {
320
+ await fetch(webhookUrl, {
321
+ method: "POST",
322
+ headers: { "Content-Type": "application/json" },
323
+ body: JSON.stringify({
324
+ text: `\u26A0\uFE0F *Reminder:* HITL approval still pending for \`${escapeMrkdwn(toolName)}\` \u2014 ${minutesRemaining} minutes remaining. Token: \`${token}\``,
325
+ mergen: { type: "hitl_reminder", token, toolName }
326
+ }),
327
+ signal: AbortSignal.timeout(5e3)
328
+ });
329
+ } catch (err) {
330
+ logger.warn({ err, token }, "tool-guard: HITL reminder webhook failed");
331
+ }
332
+ }
333
+ async function fireDelegationChain(token, toolName) {
334
+ const secondaryUrl = process.env.MERGEN_HITL_SECONDARY_WEBHOOK;
335
+ if (!secondaryUrl) return;
336
+ try {
337
+ await fetch(secondaryUrl, {
338
+ method: "POST",
339
+ headers: { "Content-Type": "application/json" },
340
+ body: JSON.stringify({
341
+ text: `\u{1F534} *Delegation escalation* \u2014 HITL approval for \`${escapeMrkdwn(toolName)}\` has not been responded to after 5 minutes. Escalating to secondary on-call. Token: \`${token}\``,
342
+ mergen: { type: "hitl_delegation", token, toolName }
343
+ }),
344
+ signal: AbortSignal.timeout(5e3)
345
+ });
346
+ logger.info({ token, toolName }, "tool-guard: HITL delegation chain fired to secondary webhook");
347
+ } catch (err) {
348
+ logger.warn({ err, token }, "tool-guard: HITL delegation chain webhook failed");
349
+ }
350
+ }
351
+ async function fireHitlEscalation(token, toolName) {
352
+ const pdSecret = process.env.MERGEN_PAGERDUTY_SECRET;
353
+ const webhookUrl = process.env.MERGEN_HITL_WEBHOOK_URL;
354
+ if (pdSecret) {
355
+ try {
356
+ await fetch("https://events.pagerduty.com/v2/enqueue", {
357
+ method: "POST",
358
+ headers: { "Content-Type": "application/json" },
359
+ body: JSON.stringify({
360
+ routing_key: pdSecret,
361
+ event_action: "trigger",
362
+ payload: {
363
+ summary: `Mergen HITL gate: unanswered approval for \`${toolName}\` (token: ${token})`,
364
+ severity: "warning",
365
+ source: "mergen-hitl",
366
+ custom_details: { token, toolName }
367
+ }
368
+ }),
369
+ signal: AbortSignal.timeout(8e3)
370
+ });
371
+ logger.info({ token, toolName }, "tool-guard: HITL escalation sent to PagerDuty");
372
+ } catch (err) {
373
+ logger.warn({ err, token }, "tool-guard: PagerDuty escalation failed");
374
+ }
375
+ return;
376
+ }
377
+ if (webhookUrl) {
378
+ try {
379
+ await fetch(webhookUrl, {
380
+ method: "POST",
381
+ headers: { "Content-Type": "application/json" },
382
+ body: JSON.stringify({
383
+ text: `\u{1F6A8} *URGENT:* HITL approval for \`${escapeMrkdwn(toolName)}\` pending 10 min. 5 min until auto-cancel. Token: \`${token}\``,
384
+ mergen: { type: "hitl_escalation", token, toolName }
385
+ }),
386
+ signal: AbortSignal.timeout(5e3)
387
+ });
388
+ } catch (err) {
389
+ logger.warn({ err, token }, "tool-guard: HITL escalation webhook failed");
390
+ }
391
+ }
392
+ }
393
+ function holdToolCall(opts) {
394
+ const { token, toolName, argsSnapshot, reason, triggeredRules, heldAt, commandArg, suggestedAlt, port } = opts;
395
+ const quorumRequired = _quorumRequired(commandArg, toolName);
396
+ void fireHitlWebhook(token, toolName, argsSnapshot, reason, port, {
397
+ triggeredRules,
398
+ commandArg,
399
+ suggestedAlt,
400
+ quorumRequired
401
+ });
402
+ return new Promise((resolve) => {
403
+ const timeoutHandle = setTimeout(() => {
404
+ const hold = _pendingHolds.get(token);
405
+ if (hold?.reminderHandle) clearTimeout(hold.reminderHandle);
406
+ if (hold?.escalationHandle) clearTimeout(hold.escalationHandle);
407
+ _pendingHolds.delete(token);
408
+ _persistHolds();
409
+ logger.info({ token, toolName }, "tool-guard: HITL approval window expired");
410
+ resolve({
411
+ content: [{
412
+ type: "text",
413
+ text: [
414
+ `\u23F0 HITL approval window expired (15 min) for \`${toolName}\`. Tool call cancelled.`,
415
+ ``,
416
+ `**Why:** ${reason}`,
417
+ ``,
418
+ `**What to do instead:** ${suggestedAlt}`
419
+ ].join("\n")
420
+ }],
421
+ isError: true
422
+ });
423
+ }, HOLD_TIMEOUT_MS);
424
+ timeoutHandle.unref();
425
+ const reminderHandle = setTimeout(() => {
426
+ void fireHitlReminder(token, toolName, 10);
427
+ void fireDelegationChain(token, toolName);
428
+ }, 5 * 60 * 1e3);
429
+ reminderHandle.unref();
430
+ const escalationHandle = setTimeout(() => {
431
+ void fireHitlEscalation(token, toolName);
432
+ }, 10 * 60 * 1e3);
433
+ escalationHandle.unref();
434
+ _pendingHolds.set(token, {
435
+ toolName,
436
+ argsSnapshot,
437
+ policyReason: reason,
438
+ suggestedAlt,
439
+ triggeredRules,
440
+ heldAt,
441
+ commandArg,
442
+ resolve,
443
+ timeoutHandle,
444
+ reminderHandle,
445
+ escalationHandle,
446
+ quorumRequired,
447
+ approvedBy: /* @__PURE__ */ new Set()
448
+ });
449
+ _persistHolds();
450
+ });
451
+ }
452
+ export {
453
+ HOLD_TIMEOUT_MS,
454
+ approveToolCall,
455
+ approveToolCallConstrained,
456
+ denyStaleHoldsOnStartup,
457
+ denyToolCall,
458
+ getPendingHolds,
459
+ holdToolCall
460
+ };