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,364 @@
1
+ import { createHmac, timingSafeEqual } from "crypto";
2
+ import { Router } from "express";
3
+ import logger from "../sensor/logger.js";
4
+ import { approveToolCall, approveToolCallConstrained, denyToolCall, getPendingHolds, approveBypass, invalidateBypassToken, getPendingBypassDetail } from "../intelligence/tool-guard.js";
5
+ import { getHitlFatigueStatus } from "../intelligence/gate-analytics.js";
6
+ import { timingSafeSecretEqualAny } from "../sensor/security-utils.js";
7
+ import { getStores } from "../storage/store-registry.js";
8
+ import { autoActivateReviewedRules } from "../intelligence/corpus-to-policy.js";
9
+ import { hasPermission, listMembers } from "../sensor/rbac.js";
10
+ function resolveApproverIdentity(req) {
11
+ const member = req.headers["x-mergen-member"]?.trim();
12
+ return member ? { approverId: member, authenticated: true } : { approverId: "operator", authenticated: false };
13
+ }
14
+ function rbacIsConfigured() {
15
+ return listMembers().length > 0;
16
+ }
17
+ function _generateNonce(token, secret) {
18
+ return createHmac("sha256", secret).update(`hitl-csrf:${token}`).digest("hex");
19
+ }
20
+ function _isHitlAuthorized(req, token, localSecret) {
21
+ if (timingSafeSecretEqualAny(req.headers["x-mergen-secret"], localSecret)) return true;
22
+ const nonce = req.body?._nonce;
23
+ if (typeof nonce === "string" && nonce.length > 0) {
24
+ try {
25
+ const expected = _generateNonce(token, localSecret);
26
+ const a = Buffer.from(nonce, "hex");
27
+ const b = Buffer.from(expected, "hex");
28
+ return a.length === b.length && timingSafeEqual(a, b);
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+ return false;
34
+ }
35
+ const HITL_RATE_LIMIT = 10;
36
+ const HITL_RATE_WINDOW_MS = 6e4;
37
+ const _hitlBuckets = /* @__PURE__ */ new Map();
38
+ const TOKEN_ATTEMPT_LIMIT = 3;
39
+ const _tokenAttempts = /* @__PURE__ */ new Map();
40
+ setInterval(() => {
41
+ const now = Date.now();
42
+ for (const [ip, b] of _hitlBuckets) {
43
+ if (now > b.resetAt) _hitlBuckets.delete(ip);
44
+ }
45
+ }, 5 * 6e4).unref();
46
+ function hitlRateLimited(ip) {
47
+ const now = Date.now();
48
+ let b = _hitlBuckets.get(ip);
49
+ if (!b || now > b.resetAt) {
50
+ b = { count: 0, resetAt: now + HITL_RATE_WINDOW_MS };
51
+ _hitlBuckets.set(ip, b);
52
+ }
53
+ return ++b.count > HITL_RATE_LIMIT;
54
+ }
55
+ function recordFailedTokenAttempt(token) {
56
+ const attempts = (_tokenAttempts.get(token) ?? 0) + 1;
57
+ _tokenAttempts.set(token, attempts);
58
+ if (attempts >= TOKEN_ATTEMPT_LIMIT) {
59
+ invalidateBypassToken(token);
60
+ _tokenAttempts.delete(token);
61
+ return true;
62
+ }
63
+ return false;
64
+ }
65
+ function _escHtml(s) {
66
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
67
+ }
68
+ function confirmationPage(action, token, nonce) {
69
+ const label = action === "approve" ? "Approve" : "Deny";
70
+ const btnColor = action === "approve" ? "#28a745" : "#dc3545";
71
+ const emoji = action === "approve" ? "\u2705" : "\u274C";
72
+ return `<!DOCTYPE html>
73
+ <html lang="en">
74
+ <head>
75
+ <meta charset="utf-8">
76
+ <meta name="viewport" content="width=device-width, initial-scale=1">
77
+ <title>Mergen HITL \u2014 ${label} tool call</title>
78
+ <style>
79
+ body { font-family: system-ui, sans-serif; max-width: 480px; margin: 80px auto; padding: 0 20px; color: #1a1a1a; }
80
+ h1 { font-size: 1.4rem; margin-bottom: .5rem; }
81
+ p { color: #555; margin-bottom: 1.5rem; }
82
+ form { display: inline; }
83
+ button {
84
+ background: ${btnColor}; color: #fff; border: none; border-radius: 6px;
85
+ padding: 12px 28px; font-size: 1rem; cursor: pointer; font-weight: 600;
86
+ }
87
+ button:hover { opacity: .88; }
88
+ .token { font-family: monospace; font-size: .85rem; color: #666; margin-top: 1.5rem; }
89
+ .alt { margin-top: 1.5rem; font-size: .85rem; color: #888; }
90
+ code { background: #f4f4f4; border-radius: 4px; padding: 2px 6px; }
91
+ </style>
92
+ </head>
93
+ <body>
94
+ <h1>${emoji} ${label} Mergen HITL gate?</h1>
95
+ <p>Clicking the button below will ${action} the held tool call and release it ${action === "approve" ? "for execution" : "with a cancellation error"}.</p>
96
+ <form method="POST" action="/hitl/${action}?token=${_escHtml(token)}">
97
+ <input type="hidden" name="_nonce" value="${nonce}">
98
+ <button type="submit">${emoji} Confirm ${label}</button>
99
+ </form>
100
+ <p class="token">Token: <code>${_escHtml(token)}</code></p>
101
+ <p class="alt">Alternatively, run: <code>mergen ${action} ${_escHtml(token)}</code></p>
102
+ </body>
103
+ </html>`;
104
+ }
105
+ function createHitlRouter(localSecret) {
106
+ const router = Router();
107
+ router.get("/hitl/approve", (req, res) => {
108
+ const token = req.query.token?.trim();
109
+ if (!token) {
110
+ res.status(400).send("token required");
111
+ return;
112
+ }
113
+ if (!getPendingHolds().some((h) => h.token === token)) {
114
+ res.status(404).send("token not found");
115
+ return;
116
+ }
117
+ const nonce = _generateNonce(token, localSecret);
118
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
119
+ res.setHeader("Cache-Control", "no-store");
120
+ res.setHeader("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'");
121
+ res.send(confirmationPage("approve", token, nonce));
122
+ });
123
+ router.get("/hitl/deny", (req, res) => {
124
+ const token = req.query.token?.trim();
125
+ if (!token) {
126
+ res.status(400).send("token required");
127
+ return;
128
+ }
129
+ if (!getPendingHolds().some((h) => h.token === token)) {
130
+ res.status(404).send("token not found");
131
+ return;
132
+ }
133
+ const nonce = _generateNonce(token, localSecret);
134
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
135
+ res.setHeader("Cache-Control", "no-store");
136
+ res.setHeader("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'");
137
+ res.send(confirmationPage("deny", token, nonce));
138
+ });
139
+ router.post("/hitl/approve", async (req, res) => {
140
+ const ip = (req.socket.remoteAddress ?? "unknown").replace(/^::ffff:/, "");
141
+ if (hitlRateLimited(ip)) {
142
+ logger.warn({ ip }, "hitl: rate limit exceeded on /hitl/approve");
143
+ res.status(429).json({ error: "rate limit exceeded" });
144
+ return;
145
+ }
146
+ const token = (req.query.token ?? req.body?.token)?.trim();
147
+ if (!token) {
148
+ res.status(400).json({ error: "token required" });
149
+ return;
150
+ }
151
+ if (!_isHitlAuthorized(req, token, localSecret)) {
152
+ logger.warn({ ip, token }, "hitl: approve rejected \u2014 missing or invalid auth");
153
+ res.status(401).json({ error: "unauthorized" });
154
+ return;
155
+ }
156
+ const { approverId, authenticated } = resolveApproverIdentity(req);
157
+ if (rbacIsConfigured() && !authenticated) {
158
+ logger.warn({ ip, token }, "hitl: approve rejected \u2014 x-mergen-member required once RBAC is configured");
159
+ res.status(403).json({ error: "x-mergen-member header required to approve when RBAC is configured" });
160
+ return;
161
+ }
162
+ if (authenticated && !hasPermission(approverId, "responder")) {
163
+ logger.warn({ ip, token, approverId }, "hitl: approve rejected \u2014 approver lacks responder role");
164
+ res.status(403).json({ error: "insufficient role to approve (requires responder or admin)" });
165
+ return;
166
+ }
167
+ let released = approveToolCall(token, approverId);
168
+ let bypassDetail = null;
169
+ if (!released) {
170
+ const detail = getPendingBypassDetail(token);
171
+ const bRes = approveBypass(token);
172
+ if (bRes.ok) {
173
+ released = true;
174
+ bypassDetail = bRes;
175
+ const remember = req.query.remember === "true" || req.body?.remember === true;
176
+ if (remember && detail) {
177
+ try {
178
+ const overrideEvent = await getStores().overrides.recordOverride({
179
+ incidentTag: detail.triggeredRules?.[0] || "bypass_override",
180
+ proposedCommand: detail.commandArg || detail.toolName,
181
+ overrideReason: "on-call-discretion",
182
+ note: "Automatically remembered via Gateway approval",
183
+ rationale: "Operator approved bypass and requested permanent policy rule creation",
184
+ service: "api",
185
+ environment: "production",
186
+ actor: "developer"
187
+ }, req.tenantId);
188
+ await getStores().overrides.markOverrideReviewed(overrideEvent.id, req.tenantId);
189
+ autoActivateReviewedRules(overrideEvent.incidentTag, overrideEvent.service);
190
+ logger.info({ token, tag: overrideEvent.incidentTag }, "hitl: bypass auto-recorded and promoted to policy");
191
+ } catch (err) {
192
+ logger.error({ err, token }, "hitl: failed to automatically promote bypass to policy");
193
+ }
194
+ }
195
+ }
196
+ }
197
+ if (!released) {
198
+ logger.warn({ ip, token }, "hitl: approve attempted with unknown or expired token");
199
+ const exhausted = recordFailedTokenAttempt(token);
200
+ if (exhausted) logger.warn({ token }, "hitl: token invalidated after too many failed attempts");
201
+ res.status(404).json({ error: "token not found or already expired" });
202
+ return;
203
+ }
204
+ _tokenAttempts.delete(token);
205
+ if ((req.headers.accept ?? "").includes("text/html")) {
206
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
207
+ res.send(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>Approved</title></head>
208
+ <body style="font-family:system-ui,sans-serif;max-width:480px;margin:80px auto;padding:0 20px">
209
+ <h1>\u2705 Tool call approved</h1><p>The held tool call has been released for execution.</p>
210
+ <p style="color:#888;font-size:.85rem">Token: <code>${token}</code></p>
211
+ </body></html>`);
212
+ return;
213
+ }
214
+ res.json({ ok: true, action: "approved", token, toolName: bypassDetail?.toolName, commandArg: bypassDetail?.commandArg });
215
+ });
216
+ router.post("/hitl/deny", (req, res) => {
217
+ const ip = (req.socket.remoteAddress ?? "unknown").replace(/^::ffff:/, "");
218
+ if (hitlRateLimited(ip)) {
219
+ logger.warn({ ip }, "hitl: rate limit exceeded on /hitl/deny");
220
+ res.status(429).json({ error: "rate limit exceeded" });
221
+ return;
222
+ }
223
+ const token = (req.query.token ?? req.body?.token)?.trim();
224
+ if (!token) {
225
+ res.status(400).json({ error: "token required" });
226
+ return;
227
+ }
228
+ if (!_isHitlAuthorized(req, token, localSecret)) {
229
+ logger.warn({ ip, token }, "hitl: deny rejected \u2014 missing or invalid auth");
230
+ res.status(401).json({ error: "unauthorized" });
231
+ return;
232
+ }
233
+ const denied = denyToolCall(token);
234
+ if (!denied) {
235
+ logger.warn({ ip, token }, "hitl: deny attempted with unknown or expired token");
236
+ const exhausted = recordFailedTokenAttempt(token);
237
+ if (exhausted) logger.warn({ token }, "hitl: token invalidated after too many failed attempts");
238
+ res.status(404).json({ error: "token not found or already expired" });
239
+ return;
240
+ }
241
+ _tokenAttempts.delete(token);
242
+ if ((req.headers.accept ?? "").includes("text/html")) {
243
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
244
+ res.send(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>Denied</title></head>
245
+ <body style="font-family:system-ui,sans-serif;max-width:480px;margin:80px auto;padding:0 20px">
246
+ <h1>\u274C Tool call denied</h1><p>The held tool call has been cancelled. The agent will receive a cancellation error.</p>
247
+ <p style="color:#888;font-size:.85rem">Token: <code>${token}</code></p>
248
+ </body></html>`);
249
+ return;
250
+ }
251
+ res.json({ ok: true, action: "denied", token });
252
+ });
253
+ router.get("/hitl/pending", (_req, res) => {
254
+ const holds = getPendingHolds();
255
+ res.json({
256
+ ok: true,
257
+ count: holds.length,
258
+ pending: holds,
259
+ // Convenience summary for dashboards
260
+ summary: holds.map((h) => ({
261
+ token: h.token,
262
+ tool: h.toolName,
263
+ command: h.commandArg,
264
+ reason: h.policyReason,
265
+ waitingSec: h.waitingSec,
266
+ approveUrl: `POST /hitl/approve?token=${h.token}`,
267
+ denyUrl: `POST /hitl/deny?token=${h.token}`
268
+ }))
269
+ });
270
+ });
271
+ router.get("/hitl/fatigue", (_req, res) => {
272
+ const status = getHitlFatigueStatus();
273
+ res.json({ ok: true, ...status });
274
+ });
275
+ router.get("/hitl/approve-constrained", (req, res) => {
276
+ const token = req.query.token?.trim();
277
+ if (!token) {
278
+ res.status(400).send("token required");
279
+ return;
280
+ }
281
+ if (!getPendingHolds().some((h) => h.token === token)) {
282
+ res.status(404).send("token not found");
283
+ return;
284
+ }
285
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
286
+ res.setHeader("Cache-Control", "no-store");
287
+ res.setHeader("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; script-src 'unsafe-inline'");
288
+ res.send(`<!DOCTYPE html>
289
+ <html lang="en">
290
+ <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
291
+ <title>Mergen HITL \u2014 Approve with Constraints</title>
292
+ <style>
293
+ body{font-family:system-ui,sans-serif;max-width:520px;margin:60px auto;padding:0 20px;color:#1a1a1a}
294
+ h1{font-size:1.3rem;margin-bottom:.5rem}p{color:#555;margin-bottom:1rem}
295
+ label{display:block;font-size:.85rem;font-weight:600;margin-bottom:.3rem}
296
+ textarea{width:100%;height:100px;font-family:monospace;font-size:.85rem;border:1px solid #ccc;border-radius:4px;padding:8px}
297
+ button{background:#0070f3;color:#fff;border:none;border-radius:6px;padding:10px 24px;font-size:.95rem;cursor:pointer;font-weight:600;margin-top:1rem}
298
+ button:hover{opacity:.88}.hint{font-size:.75rem;color:#888;margin-bottom:.5rem}
299
+ </style>
300
+ </head>
301
+ <body>
302
+ <h1>\u{1F512} Approve with Constraints</h1>
303
+ <p>Enter JSON constraints to narrow the scope of this tool call. The agent will receive these constraints alongside the approval and must honour them.</p>
304
+ <label>Constraints (JSON object)</label>
305
+ <p class="hint">Example: {"targetPod":"auth-api-1","maxRestarts":1}</p>
306
+ <textarea id="c" placeholder='{"key": "value"}'></textarea>
307
+ <button onclick="submit()">Confirm Constrained Approval</button>
308
+ <script>
309
+ function submit(){
310
+ let c;
311
+ try{c=JSON.parse(document.getElementById('c').value)}catch(e){alert('Invalid JSON: '+e.message);return}
312
+ fetch('/hitl/approve-constrained?token=${_escHtml(token)}',{
313
+ method:'POST',headers:{'Content-Type':'application/json','x-hitl-constrained':'1'},
314
+ body:JSON.stringify({constraints:c})
315
+ }).then(r=>r.json()).then(d=>{
316
+ document.body.innerHTML='<h1>\u2705 Approved with constraints</h1><p>'+JSON.stringify(d)+'</p>'
317
+ }).catch(e=>alert('Error: '+e))
318
+ }
319
+ </script>
320
+ </body></html>`);
321
+ });
322
+ router.post("/hitl/approve-constrained", (req, res) => {
323
+ const ip = (req.socket.remoteAddress ?? "unknown").replace(/^::ffff:/, "");
324
+ if (hitlRateLimited(ip)) {
325
+ res.status(429).json({ error: "rate limit exceeded" });
326
+ return;
327
+ }
328
+ const token = (req.query.token ?? req.body?.token)?.trim();
329
+ if (!token) {
330
+ res.status(400).json({ error: "token required" });
331
+ return;
332
+ }
333
+ const isConstrained = req.headers["x-hitl-constrained"] === "1";
334
+ if (!isConstrained && !timingSafeSecretEqualAny(req.headers["x-mergen-secret"], localSecret)) {
335
+ res.status(401).json({ error: "unauthorized" });
336
+ return;
337
+ }
338
+ const constraints = req.body?.constraints;
339
+ if (!constraints || typeof constraints !== "object" || Array.isArray(constraints)) {
340
+ res.status(400).json({ error: "constraints must be a non-empty JSON object" });
341
+ return;
342
+ }
343
+ const { approverId, authenticated } = resolveApproverIdentity(req);
344
+ if (rbacIsConfigured() && !authenticated) {
345
+ res.status(403).json({ error: "x-mergen-member header required to approve when RBAC is configured" });
346
+ return;
347
+ }
348
+ if (authenticated && !hasPermission(approverId, "responder")) {
349
+ res.status(403).json({ error: "insufficient role to approve (requires responder or admin)" });
350
+ return;
351
+ }
352
+ const result = approveToolCallConstrained(token, constraints, approverId);
353
+ if (!result.ok) {
354
+ res.status(result.error === "token not found or already expired" ? 404 : 400).json({ error: result.error });
355
+ return;
356
+ }
357
+ logger.info({ token, constraints, approverId }, "hitl: constrained approval recorded");
358
+ res.json({ ok: true, action: "approved-constrained", token, constraints });
359
+ });
360
+ return router;
361
+ }
362
+ export {
363
+ createHitlRouter
364
+ };