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.
- package/LICENSE +57 -0
- package/README.md +96 -0
- package/dist/__stubs__/calibration.js +471 -0
- package/dist/__stubs__/causal.js +601 -0
- package/dist/__stubs__/closed-source.js +150 -0
- package/dist/__stubs__/license.js +35 -0
- package/dist/__stubs__/plans.js +197 -0
- package/dist/app.js +467 -0
- package/dist/cli.js +250 -0
- package/dist/commands/agent-identity.js +79 -0
- package/dist/commands/gate.js +107 -0
- package/dist/commands/github.js +632 -0
- package/dist/commands/incident.js +644 -0
- package/dist/commands/policy.js +710 -0
- package/dist/commands/scan.js +243 -0
- package/dist/commands/setup.js +1218 -0
- package/dist/commands/shared.js +109 -0
- package/dist/commands/team.js +809 -0
- package/dist/datadog/blame-attribution.js +228 -0
- package/dist/datadog/client.js +159 -0
- package/dist/datadog/compactor.js +169 -0
- package/dist/datadog/fingerprinter.js +34 -0
- package/dist/datadog/incident-state.js +21 -0
- package/dist/datadog/line-matcher.js +56 -0
- package/dist/datadog/memory-store.js +399 -0
- package/dist/datadog/otel-trace.js +67 -0
- package/dist/index.js +505 -0
- package/dist/intelligence/action-risk.js +132 -0
- package/dist/intelligence/activity-feed.js +40 -0
- package/dist/intelligence/agent-identity.js +115 -0
- package/dist/intelligence/agent-pipeline.js +329 -0
- package/dist/intelligence/agent-profiles.js +81 -0
- package/dist/intelligence/ai-commit.js +16 -0
- package/dist/intelligence/approval-analytics.js +42 -0
- package/dist/intelligence/approval-events.js +5 -0
- package/dist/intelligence/arch-boundaries.js +94 -0
- package/dist/intelligence/arch-graph.js +117 -0
- package/dist/intelligence/autonomy.js +397 -0
- package/dist/intelligence/azure-broker.js +57 -0
- package/dist/intelligence/azure-proxy-sessions.js +49 -0
- package/dist/intelligence/baseline.js +1 -0
- package/dist/intelligence/behavior-baseline.js +134 -0
- package/dist/intelligence/billing.js +1 -0
- package/dist/intelligence/blast-radius.js +224 -0
- package/dist/intelligence/bypass.js +157 -0
- package/dist/intelligence/calibration-classifier.js +77 -0
- package/dist/intelligence/calibration-git-sync.js +74 -0
- package/dist/intelligence/calibration.js +1 -0
- package/dist/intelligence/cascade-detector.js +58 -0
- package/dist/intelligence/case-study-generator.js +126 -0
- package/dist/intelligence/causal-graph.js +126 -0
- package/dist/intelligence/causal.js +1 -0
- package/dist/intelligence/change-risk.js +102 -0
- package/dist/intelligence/compliance-report.js +134 -0
- package/dist/intelligence/confidence-report.js +85 -0
- package/dist/intelligence/corpus-to-policy.js +140 -0
- package/dist/intelligence/credential-broker.js +112 -0
- package/dist/intelligence/credential-misuse.js +39 -0
- package/dist/intelligence/debug-sessions.js +108 -0
- package/dist/intelligence/decision-escalation.js +36 -0
- package/dist/intelligence/decision-memory.js +47 -0
- package/dist/intelligence/decision-similarity.js +85 -0
- package/dist/intelligence/default-safety-tests.js +53 -0
- package/dist/intelligence/degradation-watcher.js +108 -0
- package/dist/intelligence/detector-plugins.js +70 -0
- package/dist/intelligence/detectors.js +1 -0
- package/dist/intelligence/device-auth.js +138 -0
- package/dist/intelligence/diff-size.js +42 -0
- package/dist/intelligence/ebpf-verify.js +45 -0
- package/dist/intelligence/enterprise-policy-engine.js +1122 -0
- package/dist/intelligence/error-fingerprint.js +1 -0
- package/dist/intelligence/execution-gate.js +100 -0
- package/dist/intelligence/execution-mode.js +22 -0
- package/dist/intelligence/gate-analytics.js +256 -0
- package/dist/intelligence/gate-decision.js +64 -0
- package/dist/intelligence/gcp-broker.js +50 -0
- package/dist/intelligence/git-adr-sync.js +207 -0
- package/dist/intelligence/hitl-hold.js +460 -0
- package/dist/intelligence/hypothesis-history.js +93 -0
- package/dist/intelligence/idp-identity.js +83 -0
- package/dist/intelligence/impl-critic.js +216 -0
- package/dist/intelligence/incident-autopilot.js +622 -0
- package/dist/intelligence/incident-replay.js +162 -0
- package/dist/intelligence/incident-result-cache.js +33 -0
- package/dist/intelligence/incident-similarity.js +58 -0
- package/dist/intelligence/infra-detectors.js +312 -0
- package/dist/intelligence/license.js +185 -0
- package/dist/intelligence/llm-spokesperson.js +62 -0
- package/dist/intelligence/mcp-prompts.js +320 -0
- package/dist/intelligence/mcp-resources.js +171 -0
- package/dist/intelligence/normalize.js +108 -0
- package/dist/intelligence/notifications.js +83 -0
- package/dist/intelligence/oidc-issuer.js +87 -0
- package/dist/intelligence/override-corpus.js +597 -0
- package/dist/intelligence/planning-gate.js +84 -0
- package/dist/intelligence/plans.js +223 -0
- package/dist/intelligence/platt-scaling.js +164 -0
- package/dist/intelligence/policy-proposals.js +84 -0
- package/dist/intelligence/policy-suggester.js +161 -0
- package/dist/intelligence/policy-sync.js +103 -0
- package/dist/intelligence/policy-test-runner.js +35 -0
- package/dist/intelligence/postmortem-parser.js +181 -0
- package/dist/intelligence/postmortem-retrieval.js +224 -0
- package/dist/intelligence/postmortem-store.js +596 -0
- package/dist/intelligence/pr-commenter.js +81 -0
- package/dist/intelligence/pr-policy-gate.js +38 -0
- package/dist/intelligence/pr-shadow-analyzer.js +229 -0
- package/dist/intelligence/process-tree.js +108 -0
- package/dist/intelligence/prompts.js +1 -0
- package/dist/intelligence/replay.js +38 -0
- package/dist/intelligence/repro-steps.js +1 -0
- package/dist/intelligence/resource-extractors.js +0 -0
- package/dist/intelligence/resource-file-context.js +138 -0
- package/dist/intelligence/risk-score.js +22 -0
- package/dist/intelligence/rollback.js +137 -0
- package/dist/intelligence/runbook-updater.js +143 -0
- package/dist/intelligence/sandbox.js +194 -0
- package/dist/intelligence/script-trust.js +104 -0
- package/dist/intelligence/session-metrics.js +67 -0
- package/dist/intelligence/session-threat-tracker.js +231 -0
- package/dist/intelligence/shadow-digest-cron.js +81 -0
- package/dist/intelligence/shadow-log.js +284 -0
- package/dist/intelligence/shell-ast.js +145 -0
- package/dist/intelligence/siem-forward.js +104 -0
- package/dist/intelligence/slack-digest.js +151 -0
- package/dist/intelligence/slack-override-loop.js +217 -0
- package/dist/intelligence/slack-routing.js +67 -0
- package/dist/intelligence/slack.js +900 -0
- package/dist/intelligence/sql-ast.js +31 -0
- package/dist/intelligence/team.js +1 -0
- package/dist/intelligence/telemetry.js +76 -0
- package/dist/intelligence/threshold-optimizer.js +75 -0
- package/dist/intelligence/token-budget.js +66 -0
- package/dist/intelligence/tool-guard.js +708 -0
- package/dist/intelligence/tool-manifest 2.js +74 -0
- package/dist/intelligence/tool-manifest.js +125 -0
- package/dist/intelligence/tools-analysis.js +857 -0
- package/dist/intelligence/tools-arch.js +152 -0
- package/dist/intelligence/tools-autonomy.js +495 -0
- package/dist/intelligence/tools-blast-radius.js +140 -0
- package/dist/intelligence/tools-browser.js +431 -0
- package/dist/intelligence/tools-change-timeline.js +154 -0
- package/dist/intelligence/tools-credentials.js +117 -0
- package/dist/intelligence/tools-datadog.js +233 -0
- package/dist/intelligence/tools-debug-sessions.js +209 -0
- package/dist/intelligence/tools-discovery.js +284 -0
- package/dist/intelligence/tools-infra.js +612 -0
- package/dist/intelligence/tools-intent.js +128 -0
- package/dist/intelligence/tools-memory.js +514 -0
- package/dist/intelligence/tools-runbook.js +951 -0
- package/dist/intelligence/tools-sessions.js +107 -0
- package/dist/intelligence/tools-state.js +232 -0
- package/dist/intelligence/tools-utility.js +430 -0
- package/dist/intelligence/tools-validate.js +215 -0
- package/dist/intelligence/tools.js +1 -0
- package/dist/intelligence/trace-context.js +11 -0
- package/dist/intelligence/unclassified-clusters.js +87 -0
- package/dist/intelligence/usage.js +195 -0
- package/dist/routes/active-authors.js +63 -0
- package/dist/routes/activity-feed.js +21 -0
- package/dist/routes/adr.js +117 -0
- package/dist/routes/agent-activity.js +184 -0
- package/dist/routes/agent-blunders.js +163 -0
- package/dist/routes/agents.js +175 -0
- package/dist/routes/api-keys.js +65 -0
- package/dist/routes/arch.js +75 -0
- package/dist/routes/audit-export.js +93 -0
- package/dist/routes/billing-dashboard.js +158 -0
- package/dist/routes/billing-outcome.js +77 -0
- package/dist/routes/calibration.js +305 -0
- package/dist/routes/ci-gate-history.js +20 -0
- package/dist/routes/ci-gate.js +232 -0
- package/dist/routes/ci.js +293 -0
- package/dist/routes/compliance-report.js +24 -0
- package/dist/routes/confidence.js +30 -0
- package/dist/routes/credentials.js +119 -0
- package/dist/routes/dashboard.js +1407 -0
- package/dist/routes/decision-memory.js +42 -0
- package/dist/routes/demo.js +844 -0
- package/dist/routes/device-auth-stub.js +173 -0
- package/dist/routes/explain-why.js +39 -0
- package/dist/routes/gate-analytics.js +213 -0
- package/dist/routes/gate.js +28 -0
- package/dist/routes/github-webhook.js +364 -0
- package/dist/routes/habituation.js +30 -0
- package/dist/routes/health-integrations.js +243 -0
- package/dist/routes/heartbeats.js +45 -0
- package/dist/routes/hitl.js +364 -0
- package/dist/routes/impact-report.js +645 -0
- package/dist/routes/incident-webhook.js +63 -0
- package/dist/routes/incidents.js +366 -0
- package/dist/routes/layers.js +62 -0
- package/dist/routes/license.js +187 -0
- package/dist/routes/oidc-issuer.js +30 -0
- package/dist/routes/onboarding.js +170 -0
- package/dist/routes/otel.js +67 -0
- package/dist/routes/otlp-receiver.js +179 -0
- package/dist/routes/overrides.js +241 -0
- package/dist/routes/pagerduty.js +258 -0
- package/dist/routes/policies.js +809 -0
- package/dist/routes/policy-nl.js +169 -0
- package/dist/routes/postmortem.js +102 -0
- package/dist/routes/pr-shadow.js +47 -0
- package/dist/routes/rbac.js +61 -0
- package/dist/routes/replay.js +17 -0
- package/dist/routes/risk-report.js +164 -0
- package/dist/routes/runbooks.js +125 -0
- package/dist/routes/safety-test.js +174 -0
- package/dist/routes/sdk.js +222 -0
- package/dist/routes/sensor.js +819 -0
- package/dist/routes/sentry.js +140 -0
- package/dist/routes/sessions.js +43 -0
- package/dist/routes/setup-ui.js +594 -0
- package/dist/routes/shadow-report.js +107 -0
- package/dist/routes/slack-routing.js +171 -0
- package/dist/routes/team-usage.js +71 -0
- package/dist/routes/telemetry.js +28 -0
- package/dist/routes/tenants.js +199 -0
- package/dist/routes/tickets.js +167 -0
- package/dist/routes/validate.js +13 -0
- package/dist/routes/war-room.js +113 -0
- package/dist/scripts/check-arch.js +22 -0
- package/dist/seeds/community-corpus.js +176 -0
- package/dist/seeds/corpus.js +1224 -0
- package/dist/sensor/action-ledger.js +327 -0
- package/dist/sensor/adr-store.js +140 -0
- package/dist/sensor/agent-blunder-store.js +370 -0
- package/dist/sensor/agent-context-store.js +225 -0
- package/dist/sensor/agent-memory-store.js +231 -0
- package/dist/sensor/audit-fetch.js +23 -0
- package/dist/sensor/audit-log.js +273 -0
- package/dist/sensor/buffer-schemas.js +225 -0
- package/dist/sensor/buffer.js +599 -0
- package/dist/sensor/bypass-tracker.js +93 -0
- package/dist/sensor/ci-gate-history.js +70 -0
- package/dist/sensor/cloud-auth.js +183 -0
- package/dist/sensor/commit-context-store.js +235 -0
- package/dist/sensor/docker-log-stream.js +164 -0
- package/dist/sensor/docker-monitor.js +122 -0
- package/dist/sensor/extended-buffer.js +46 -0
- package/dist/sensor/feedback-token.js +41 -0
- package/dist/sensor/file-lock.js +6 -0
- package/dist/sensor/fs-watcher.js +106 -0
- package/dist/sensor/gate-heartbeat.js +122 -0
- package/dist/sensor/git-suspect.js +136 -0
- package/dist/sensor/habituation-store.js +77 -0
- package/dist/sensor/heartbeat-monitor.js +133 -0
- package/dist/sensor/incident-store.js +340 -0
- package/dist/sensor/infra-normalizer.js +513 -0
- package/dist/sensor/ingest.js +339 -0
- package/dist/sensor/jest-reporter.js +52 -0
- package/dist/sensor/k8s-events.js +132 -0
- package/dist/sensor/layer2-store.js +111 -0
- package/dist/sensor/layer3-store.js +230 -0
- package/dist/sensor/layer4-store.js +147 -0
- package/dist/sensor/logger.js +13 -0
- package/dist/sensor/otel-exporter.js +161 -0
- package/dist/sensor/paths.js +71 -0
- package/dist/sensor/policy-history.js +147 -0
- package/dist/sensor/pr-shadow-store.js +83 -0
- package/dist/sensor/process-watcher.js +162 -0
- package/dist/sensor/rbac.js +92 -0
- package/dist/sensor/redact.js +114 -0
- package/dist/sensor/redis-store.js +191 -0
- package/dist/sensor/route-reachability.js +64 -0
- package/dist/sensor/security-utils.js +18 -0
- package/dist/sensor/service-graph.js +170 -0
- package/dist/sensor/service-topology.js +227 -0
- package/dist/sensor/session-history.js +74 -0
- package/dist/sensor/session-persist.js +38 -0
- package/dist/sensor/shadow-promote.js +122 -0
- package/dist/sensor/sourcemap.js +281 -0
- package/dist/sensor/sqlite-store.js +205 -0
- package/dist/sensor/sso.js +164 -0
- package/dist/sensor/vitest-reporter.js +65 -0
- package/dist/sensor/watcher.js +48 -0
- package/dist/storage/interfaces.js +0 -0
- package/dist/storage/pg/pg-action-ledger.js +138 -0
- package/dist/storage/pg/pg-approval-store.js +107 -0
- package/dist/storage/pg/pg-blunder-store.js +193 -0
- package/dist/storage/pg/pg-ci-gate-history.js +83 -0
- package/dist/storage/pg/pg-client.js +24 -0
- package/dist/storage/pg/pg-event-store.js +63 -0
- package/dist/storage/pg/pg-incident-store.js +190 -0
- package/dist/storage/pg/pg-migrations.js +26 -0
- package/dist/storage/pg/pg-override-corpus.js +447 -0
- package/dist/storage/pg/pg-shadow-log.js +129 -0
- package/dist/storage/sqlite/sqlite-action-ledger.js +18 -0
- package/dist/storage/sqlite/sqlite-approval-store.js +37 -0
- package/dist/storage/sqlite/sqlite-blunder-store.js +27 -0
- package/dist/storage/sqlite/sqlite-ci-gate-history.js +19 -0
- package/dist/storage/sqlite/sqlite-event-store.js +29 -0
- package/dist/storage/sqlite/sqlite-incident-store.js +34 -0
- package/dist/storage/sqlite/sqlite-override-corpus.js +75 -0
- package/dist/storage/sqlite/sqlite-shadow-log.js +36 -0
- package/dist/storage/store-factory.js +55 -0
- package/dist/storage/store-registry.js +31 -0
- package/dist/update-checker.js +107 -0
- package/dist/workers/autopilot-worker.js +28 -0
- package/dist/workers/notification-worker.js +28 -0
- package/dist/workers/queues.js +58 -0
- package/dist/workers/validation-worker.js +24 -0
- package/dist/workers/worker-registry.js +18 -0
- package/package.json +123 -0
- package/sdk/mergen-inject.js +260 -0
- package/sdk/node.js +313 -0
- package/sdk/vite-plugin.ts +57 -0
- package/sdk/webpack-plugin.js +106 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { randomBytes, randomUUID } from "crypto";
|
|
3
|
+
import { PLANS } from "../intelligence/plans.js";
|
|
4
|
+
import logger from "../sensor/logger.js";
|
|
5
|
+
const DEVICE_TTL_MS = 10 * 60 * 1e3;
|
|
6
|
+
const byUserCode = /* @__PURE__ */ new Map();
|
|
7
|
+
const byDeviceCode = /* @__PURE__ */ new Map();
|
|
8
|
+
const issuedKeys = /* @__PURE__ */ new Map();
|
|
9
|
+
function stubPlanId() {
|
|
10
|
+
const p = process.env.MERGEN_AUTH_STUB_PLAN ?? "team";
|
|
11
|
+
return PLANS[p] ? p : "team";
|
|
12
|
+
}
|
|
13
|
+
function stubEmail() {
|
|
14
|
+
return process.env.MERGEN_AUTH_STUB_EMAIL ?? "dev@local.mergen";
|
|
15
|
+
}
|
|
16
|
+
function makeUserCode() {
|
|
17
|
+
const alphabet = "ABCDEFGHJKLMNPQRSTVWXYZ23456789";
|
|
18
|
+
const pick = () => Array.from(randomBytes(4)).map((b) => alphabet[b % alphabet.length]).join("");
|
|
19
|
+
return `${pick()}-${pick()}`;
|
|
20
|
+
}
|
|
21
|
+
function baseUrl(req) {
|
|
22
|
+
if (process.env.MERGEN_PUBLIC_URL) return process.env.MERGEN_PUBLIC_URL.replace(/\/+$/, "");
|
|
23
|
+
const proto = req.headers["x-forwarded-proto"] ?? req.protocol ?? "http";
|
|
24
|
+
const host = req.headers.host ?? "127.0.0.1:3000";
|
|
25
|
+
return `${proto}://${host}`;
|
|
26
|
+
}
|
|
27
|
+
function reap() {
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
for (const [code, rec] of byUserCode) {
|
|
30
|
+
if (rec.expiresAt < now) {
|
|
31
|
+
byUserCode.delete(code);
|
|
32
|
+
byDeviceCode.delete(rec.deviceCode);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function createDeviceAuthStubRouter() {
|
|
37
|
+
const router = Router();
|
|
38
|
+
logger.warn(
|
|
39
|
+
"device-auth stub ENABLED (MERGEN_AUTH_STUB=true) \u2014 approves logins with no auth. Do not enable in production."
|
|
40
|
+
);
|
|
41
|
+
router.post("/v1/device/code", (req, res) => {
|
|
42
|
+
reap();
|
|
43
|
+
const userCode = makeUserCode();
|
|
44
|
+
const deviceCode = randomUUID();
|
|
45
|
+
const rec = {
|
|
46
|
+
deviceCode,
|
|
47
|
+
userCode,
|
|
48
|
+
state: "pending",
|
|
49
|
+
planId: stubPlanId(),
|
|
50
|
+
email: stubEmail(),
|
|
51
|
+
key: null,
|
|
52
|
+
expiresAt: Date.now() + DEVICE_TTL_MS
|
|
53
|
+
};
|
|
54
|
+
byUserCode.set(userCode, rec);
|
|
55
|
+
byDeviceCode.set(deviceCode, rec);
|
|
56
|
+
const verificationUri = `${baseUrl(req)}/device`;
|
|
57
|
+
res.json({
|
|
58
|
+
device_code: deviceCode,
|
|
59
|
+
user_code: userCode,
|
|
60
|
+
verification_uri: verificationUri,
|
|
61
|
+
verification_uri_complete: `${verificationUri}?code=${encodeURIComponent(userCode)}`,
|
|
62
|
+
interval: 2,
|
|
63
|
+
expires_in: Math.round(DEVICE_TTL_MS / 1e3)
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
router.post("/v1/device/token", (req, res) => {
|
|
67
|
+
const deviceCode = req.body?.device_code ?? req.body?.deviceCode;
|
|
68
|
+
const rec = deviceCode ? byDeviceCode.get(deviceCode) : void 0;
|
|
69
|
+
if (!rec || rec.expiresAt < Date.now()) {
|
|
70
|
+
res.status(400).json({ error: "expired_token" });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (rec.state === "denied") {
|
|
74
|
+
res.status(400).json({ error: "access_denied" });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (rec.state === "pending") {
|
|
78
|
+
res.status(400).json({ error: "authorization_pending" });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
res.json({ status: "approved", key: rec.key, planId: rec.planId, email: rec.email });
|
|
82
|
+
});
|
|
83
|
+
router.get("/device", (req, res) => {
|
|
84
|
+
const prefill = typeof req.query.code === "string" ? req.query.code : "";
|
|
85
|
+
res.type("html").send(approvalPage(prefill));
|
|
86
|
+
});
|
|
87
|
+
router.post("/v1/device/approve", (req, res) => {
|
|
88
|
+
const userCode = String(req.body?.user_code ?? req.body?.userCode ?? "").trim().toUpperCase();
|
|
89
|
+
const approve = req.body?.approve !== false && req.body?.approve !== "false";
|
|
90
|
+
const rec = byUserCode.get(userCode);
|
|
91
|
+
if (!rec || rec.expiresAt < Date.now()) {
|
|
92
|
+
res.status(404).json({ ok: false, error: "unknown_or_expired_code" });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (!approve) {
|
|
96
|
+
rec.state = "denied";
|
|
97
|
+
res.json({ ok: true, state: "denied" });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
rec.state = "approved";
|
|
101
|
+
rec.key = `mrgn_live_stub_${randomBytes(16).toString("hex")}`;
|
|
102
|
+
issuedKeys.set(rec.key, { planId: rec.planId, email: rec.email });
|
|
103
|
+
res.json({ ok: true, state: "approved", plan: rec.planId });
|
|
104
|
+
});
|
|
105
|
+
router.post("/v1/validate", (req, res) => {
|
|
106
|
+
const key = req.body?.key ?? "";
|
|
107
|
+
const info = issuedKeys.get(key);
|
|
108
|
+
if (!info) {
|
|
109
|
+
res.status(401).json({ valid: false, error: "unknown key" });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
res.json({ valid: true, planId: info.planId, email: info.email, name: "Local Dev" });
|
|
113
|
+
});
|
|
114
|
+
router.post("/v1/deactivate", (req, res) => {
|
|
115
|
+
const key = req.body?.key ?? "";
|
|
116
|
+
issuedKeys.delete(key);
|
|
117
|
+
res.json({ ok: true });
|
|
118
|
+
});
|
|
119
|
+
return router;
|
|
120
|
+
}
|
|
121
|
+
function approvalPage(prefill) {
|
|
122
|
+
return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Approve Mergen CLI</title>
|
|
123
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
124
|
+
<style>
|
|
125
|
+
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#07080e;color:#fff;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0}
|
|
126
|
+
.card{background:#11121a;border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:2.5rem;max-width:420px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,.4)}
|
|
127
|
+
h1{font-size:1.3rem;margin:0 0 .5rem}
|
|
128
|
+
p{color:#9aa0ac;font-size:.9rem;line-height:1.5}
|
|
129
|
+
label{display:block;font-size:.7rem;text-transform:uppercase;letter-spacing:.1em;color:#6b7280;margin:1.25rem 0 .4rem}
|
|
130
|
+
input{width:100%;box-sizing:border-box;background:#0a0a0a;border:1px solid #2a2d38;border-radius:6px;color:#fff;padding:12px 14px;font-family:ui-monospace,monospace;font-size:1.1rem;letter-spacing:.15em;text-align:center}
|
|
131
|
+
.row{display:flex;gap:.75rem;margin-top:1.5rem}
|
|
132
|
+
button{flex:1;padding:12px;border-radius:6px;border:none;font-weight:700;font-size:.85rem;cursor:pointer}
|
|
133
|
+
.approve{background:#fff;color:#000}
|
|
134
|
+
.deny{background:transparent;color:#9aa0ac;border:1px solid #2a2d38}
|
|
135
|
+
.msg{margin-top:1.25rem;font-size:.85rem;padding:.75rem 1rem;border-radius:6px;display:none}
|
|
136
|
+
.ok{display:block;background:rgba(34,197,94,.1);color:#22c55e;border:1px solid rgba(34,197,94,.2)}
|
|
137
|
+
.err{display:block;background:rgba(239,68,68,.1);color:#ef4444;border:1px solid rgba(239,68,68,.2)}
|
|
138
|
+
.badge{font-family:ui-monospace,monospace;font-size:.65rem;color:#f59e0b;letter-spacing:.15em;text-transform:uppercase}
|
|
139
|
+
</style></head><body>
|
|
140
|
+
<div class="card">
|
|
141
|
+
<span class="badge">Local Auth Stub</span>
|
|
142
|
+
<h1>Approve Mergen CLI</h1>
|
|
143
|
+
<p>Confirm the code shown in your terminal to sign this device in. This local stub grants the <b>${escapeHtml(process.env.MERGEN_AUTH_STUB_PLAN ?? "team")}</b> plan.</p>
|
|
144
|
+
<label for="code">Device code</label>
|
|
145
|
+
<input id="code" value="${escapeHtml(prefill)}" autocapitalize="characters" autocomplete="off" />
|
|
146
|
+
<div class="row">
|
|
147
|
+
<button class="approve" onclick="act(true)">Approve</button>
|
|
148
|
+
<button class="deny" onclick="act(false)">Deny</button>
|
|
149
|
+
</div>
|
|
150
|
+
<div id="msg" class="msg"></div>
|
|
151
|
+
</div>
|
|
152
|
+
<script>
|
|
153
|
+
async function act(approve){
|
|
154
|
+
var code=document.getElementById('code').value.trim().toUpperCase();
|
|
155
|
+
var msg=document.getElementById('msg');
|
|
156
|
+
if(!code){msg.className='msg err';msg.textContent='Enter the code from your terminal.';return;}
|
|
157
|
+
try{
|
|
158
|
+
var r=await fetch('/v1/device/approve',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({user_code:code,approve:approve})});
|
|
159
|
+
var d=await r.json();
|
|
160
|
+
if(d.ok&&d.state==='approved'){msg.className='msg ok';msg.textContent='Approved \u2014 return to your terminal. You can close this tab.';}
|
|
161
|
+
else if(d.ok&&d.state==='denied'){msg.className='msg err';msg.textContent='Login denied.';}
|
|
162
|
+
else{msg.className='msg err';msg.textContent=d.error||'Unknown or expired code.';}
|
|
163
|
+
}catch(e){msg.className='msg err';msg.textContent='Request failed: '+e.message;}
|
|
164
|
+
}
|
|
165
|
+
</script>
|
|
166
|
+
</body></html>`;
|
|
167
|
+
}
|
|
168
|
+
function escapeHtml(s) {
|
|
169
|
+
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
170
|
+
}
|
|
171
|
+
export {
|
|
172
|
+
createDeviceAuthStubRouter
|
|
173
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { Router } from "express";
|
|
3
|
+
import { commitContextStore } from "../sensor/commit-context-store.js";
|
|
4
|
+
function createExplainWhyRouter() {
|
|
5
|
+
const router = Router();
|
|
6
|
+
router.get("/explain-why/file", (req, res) => {
|
|
7
|
+
const rawPath = typeof req.query.path === "string" ? req.query.path.trim() : "";
|
|
8
|
+
const filePath = path.normalize(rawPath).replace(/^(\.\.[\\/])+/, "");
|
|
9
|
+
const repo = typeof req.query.repo === "string" && req.query.repo.trim() ? req.query.repo.trim() : void 0;
|
|
10
|
+
const limit = Math.min(10, Math.max(1, Number(req.query.limit ?? 5)));
|
|
11
|
+
if (!filePath) {
|
|
12
|
+
res.status(400).json({ error: '"path" query param is required' });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const contexts = commitContextStore.listByFile(filePath, repo, limit);
|
|
16
|
+
res.json({
|
|
17
|
+
ok: true,
|
|
18
|
+
file: filePath,
|
|
19
|
+
count: contexts.length,
|
|
20
|
+
contexts: contexts.map((c) => ({
|
|
21
|
+
sha: c.sha.slice(0, 7),
|
|
22
|
+
prNumber: c.prNumber,
|
|
23
|
+
prTitle: c.prTitle,
|
|
24
|
+
prBody: c.prBody ? c.prBody.slice(0, 500) : null,
|
|
25
|
+
author: c.author,
|
|
26
|
+
approvers: c.approvers,
|
|
27
|
+
linkedIssues: c.linkedIssues,
|
|
28
|
+
aiGenerated: c.aiGenerated,
|
|
29
|
+
aiTool: c.aiTool,
|
|
30
|
+
mergedAt: c.mergedAt,
|
|
31
|
+
capturedAt: c.capturedAt
|
|
32
|
+
}))
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
return router;
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
createExplainWhyRouter
|
|
39
|
+
};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { loadEnterprisePolicy } from "../intelligence/enterprise-policy-engine.js";
|
|
3
|
+
import {
|
|
4
|
+
getRetryStats,
|
|
5
|
+
getToolCallCounts,
|
|
6
|
+
getUnguardedCounts,
|
|
7
|
+
getRuleFirings,
|
|
8
|
+
getHitlStats,
|
|
9
|
+
getReformulationRates,
|
|
10
|
+
getHitlFatigueStatus,
|
|
11
|
+
getGateEvents,
|
|
12
|
+
subscribeGateStream
|
|
13
|
+
} from "../intelligence/gate-analytics.js";
|
|
14
|
+
import { computeBlastRadius } from "../intelligence/blast-radius.js";
|
|
15
|
+
import { computeApprovalAnalytics } from "../intelligence/approval-analytics.js";
|
|
16
|
+
function retryAssessment(passRate) {
|
|
17
|
+
if (passRate >= 75) return "guidance working \u2014 agent reformulates successfully";
|
|
18
|
+
if (passRate >= 50) return "guidance partially working \u2014 consider refining the suggested alternative";
|
|
19
|
+
return "guidance ineffective \u2014 agent cannot reformulate from this hint";
|
|
20
|
+
}
|
|
21
|
+
function hitlRecommendation(approvalRate, avgApprovalLatencyMs) {
|
|
22
|
+
if (approvalRate >= 90) return "90%+ approval rate \u2014 candidate for auto-approve after corpus validation";
|
|
23
|
+
if (approvalRate <= 30) return "70%+ denial rate \u2014 consider converting to hard block";
|
|
24
|
+
if (avgApprovalLatencyMs > 30 * 60 * 1e3) return "avg approval latency >30 min \u2014 consider narrowing the hold condition";
|
|
25
|
+
return "calibrated \u2014 no action needed";
|
|
26
|
+
}
|
|
27
|
+
function createGateAnalyticsRouter() {
|
|
28
|
+
const router = Router();
|
|
29
|
+
router.get("/gate-analytics", (_req, res) => {
|
|
30
|
+
const policy = loadEnterprisePolicy();
|
|
31
|
+
const retryStats = getRetryStats();
|
|
32
|
+
const toolCalls = getToolCallCounts();
|
|
33
|
+
const ungarded = getUnguardedCounts();
|
|
34
|
+
const ruleFirings = getRuleFirings();
|
|
35
|
+
const hitlStats = getHitlStats();
|
|
36
|
+
const retryByRule = {};
|
|
37
|
+
for (const [ruleId, stats] of retryStats) {
|
|
38
|
+
const resolved = stats.retryPassed + stats.retryBlocked;
|
|
39
|
+
const passRate = resolved > 0 ? Math.round(stats.retryPassed / resolved * 100) : null;
|
|
40
|
+
retryByRule[ruleId] = {
|
|
41
|
+
fired: stats.fired,
|
|
42
|
+
retryPassed: stats.retryPassed,
|
|
43
|
+
retryBlocked: stats.retryBlocked,
|
|
44
|
+
retryPassRate: passRate,
|
|
45
|
+
assessment: passRate !== null ? retryAssessment(passRate) : "no retries yet"
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
let totalCalls = 0;
|
|
49
|
+
let coveredCalls = 0;
|
|
50
|
+
for (const [toolName, count] of toolCalls) {
|
|
51
|
+
totalCalls += count;
|
|
52
|
+
const ungardedCount = ungarded.get(toolName) ?? 0;
|
|
53
|
+
coveredCalls += count - ungardedCount;
|
|
54
|
+
}
|
|
55
|
+
const ungardedTools = [...ungarded.entries()].filter(([, n]) => n > 0).sort((a, b) => b[1] - a[1]).map(([toolName, calls]) => ({ toolName, calls }));
|
|
56
|
+
const allRuleIds = policy.rules.map((r) => r.id);
|
|
57
|
+
const deadRules = allRuleIds.filter((id) => !ruleFirings.has(id));
|
|
58
|
+
const liveRuleFirings = {};
|
|
59
|
+
for (const [ruleId, count] of ruleFirings) liveRuleFirings[ruleId] = count;
|
|
60
|
+
const hitlByRule = {};
|
|
61
|
+
for (const [ruleId, stats] of hitlStats) {
|
|
62
|
+
const total = stats.approvals + stats.denials;
|
|
63
|
+
const approvalRate = total > 0 ? Math.round(stats.approvals / total * 100) : null;
|
|
64
|
+
const avgApprovalMs = stats.approvals > 0 ? Math.round(stats.totalApprovalLatencyMs / stats.approvals) : null;
|
|
65
|
+
const avgDenialMs = stats.denials > 0 ? Math.round(stats.totalDenialLatencyMs / stats.denials) : null;
|
|
66
|
+
hitlByRule[ruleId] = {
|
|
67
|
+
approvals: stats.approvals,
|
|
68
|
+
denials: stats.denials,
|
|
69
|
+
approvalRate,
|
|
70
|
+
avgApprovalLatencyMs: avgApprovalMs,
|
|
71
|
+
avgDenialLatencyMs: avgDenialMs,
|
|
72
|
+
recommendation: approvalRate !== null ? hitlRecommendation(approvalRate, avgApprovalMs ?? 0) : "no decisions recorded yet"
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const reformRates = getReformulationRates();
|
|
76
|
+
const reformByRule = {};
|
|
77
|
+
for (const [ruleId, data] of reformRates) {
|
|
78
|
+
reformByRule[ruleId] = {
|
|
79
|
+
fired: data.fired,
|
|
80
|
+
reformulated: data.reformulated,
|
|
81
|
+
rate: Math.round(data.rate * 100),
|
|
82
|
+
assessment: data.rate >= 0.75 ? "guided alternative is working \u2014 agent reformulates successfully" : data.rate >= 0.4 ? "partial success \u2014 consider refining the suggested alternative text" : "guided alternative ineffective \u2014 agent cannot reformulate from this hint"
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const fatigue = getHitlFatigueStatus();
|
|
86
|
+
res.json({
|
|
87
|
+
note: "In-memory since last restart.",
|
|
88
|
+
retrySuccessRate: {
|
|
89
|
+
description: "After a BLOCK, did the agent reformulate and pass on the next call? (60s attribution window)",
|
|
90
|
+
byRule: retryByRule
|
|
91
|
+
},
|
|
92
|
+
guidedAlternativeEffectiveness: {
|
|
93
|
+
description: 'What fraction of blocked calls resulted in a successful reformulation? Measures whether the "what to do instead" hint is actionable.',
|
|
94
|
+
byRule: reformByRule
|
|
95
|
+
},
|
|
96
|
+
policyCoverage: {
|
|
97
|
+
description: "Tool calls with no matching policy rule (unguarded) and rules that have never fired (dead).",
|
|
98
|
+
totalCalls,
|
|
99
|
+
coveredCalls,
|
|
100
|
+
coverageRate: totalCalls > 0 ? Math.round(coveredCalls / totalCalls * 100) : null,
|
|
101
|
+
ungardedTools,
|
|
102
|
+
deadRules,
|
|
103
|
+
liveRuleFirings
|
|
104
|
+
},
|
|
105
|
+
hitlPatterns: {
|
|
106
|
+
description: "Approve/deny ratios and latency per rule. High approval rate \u2192 candidate for auto-approve. High denial rate \u2192 candidate for hard block.",
|
|
107
|
+
byRule: hitlByRule
|
|
108
|
+
},
|
|
109
|
+
hitlFatigue: {
|
|
110
|
+
description: "Approval fatigue detection \u2014 when too many HOLDs fire in a short window, engineers stop responding promptly.",
|
|
111
|
+
...fatigue
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
router.get("/gate/stream", (req, res) => {
|
|
116
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
117
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
118
|
+
res.setHeader("Connection", "keep-alive");
|
|
119
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
120
|
+
res.flushHeaders();
|
|
121
|
+
const recent = getGateEvents().slice(-20);
|
|
122
|
+
for (const ev of recent) {
|
|
123
|
+
res.write(`event: gate_decision
|
|
124
|
+
data: ${JSON.stringify(ev)}
|
|
125
|
+
|
|
126
|
+
`);
|
|
127
|
+
}
|
|
128
|
+
res.write(`event: connected
|
|
129
|
+
data: {"note":"Streaming live gate decisions","recentSent":${recent.length}}
|
|
130
|
+
|
|
131
|
+
`);
|
|
132
|
+
const heartbeat = setInterval(() => {
|
|
133
|
+
res.write(`: heartbeat ${Date.now()}
|
|
134
|
+
|
|
135
|
+
`);
|
|
136
|
+
}, 3e4);
|
|
137
|
+
const unsubscribe = subscribeGateStream((event) => {
|
|
138
|
+
res.write(`event: gate_decision
|
|
139
|
+
data: ${JSON.stringify(event)}
|
|
140
|
+
|
|
141
|
+
`);
|
|
142
|
+
});
|
|
143
|
+
req.on("close", () => {
|
|
144
|
+
clearInterval(heartbeat);
|
|
145
|
+
unsubscribe();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
router.get("/gate/heatmap", (req, res) => {
|
|
149
|
+
const windowMs = Math.max(6e4, Number(req.query.windowMs ?? 60 * 60 * 1e3));
|
|
150
|
+
const cutoff = Date.now() - windowMs;
|
|
151
|
+
const events = getGateEvents().filter((e) => e.ts >= cutoff);
|
|
152
|
+
const serviceMap = /* @__PURE__ */ new Map();
|
|
153
|
+
for (const ev of events) {
|
|
154
|
+
const svc = ev.service ?? "unknown";
|
|
155
|
+
let row = serviceMap.get(svc);
|
|
156
|
+
if (!row) {
|
|
157
|
+
row = { calls: 0, blocked: 0, held: 0, passed: 0, blastScores: [], blastScopes: [], agents: /* @__PURE__ */ new Map(), tools: /* @__PURE__ */ new Map() };
|
|
158
|
+
serviceMap.set(svc, row);
|
|
159
|
+
}
|
|
160
|
+
row.calls++;
|
|
161
|
+
if (ev.verdict === "block") row.blocked++;
|
|
162
|
+
else if (ev.verdict === "hold") row.held++;
|
|
163
|
+
else row.passed++;
|
|
164
|
+
if (ev.command) {
|
|
165
|
+
const blast = computeBlastRadius(ev.command);
|
|
166
|
+
const scopeScore = {
|
|
167
|
+
"data-destructive": 100,
|
|
168
|
+
"cluster": 90,
|
|
169
|
+
"namespace": 70,
|
|
170
|
+
"data-write": 60,
|
|
171
|
+
"deployment": 50,
|
|
172
|
+
"config-change": 40,
|
|
173
|
+
"pod": 20,
|
|
174
|
+
"unknown": 10
|
|
175
|
+
};
|
|
176
|
+
row.blastScores.push(scopeScore[blast.scope] ?? 10);
|
|
177
|
+
row.blastScopes.push(blast.scope);
|
|
178
|
+
}
|
|
179
|
+
const agentKey = ev.agentId ?? ev.actor ?? "unknown";
|
|
180
|
+
row.agents.set(agentKey, (row.agents.get(agentKey) ?? 0) + 1);
|
|
181
|
+
row.tools.set(ev.toolName, (row.tools.get(ev.toolName) ?? 0) + 1);
|
|
182
|
+
}
|
|
183
|
+
const heatmap = [...serviceMap.entries()].map(([service, row]) => ({
|
|
184
|
+
service,
|
|
185
|
+
totalCalls: row.calls,
|
|
186
|
+
blocked: row.blocked,
|
|
187
|
+
held: row.held,
|
|
188
|
+
passed: row.passed,
|
|
189
|
+
avgBlastScore: row.blastScores.length > 0 ? Math.round(row.blastScores.reduce((a, b) => a + b, 0) / row.blastScores.length) : 0,
|
|
190
|
+
maxBlastScope: row.blastScopes.reduce((max, s) => {
|
|
191
|
+
const order = ["data-destructive", "cluster", "namespace", "data-write", "deployment", "config-change", "pod", "unknown"];
|
|
192
|
+
return order.indexOf(s) < order.indexOf(max) ? s : max;
|
|
193
|
+
}, "unknown"),
|
|
194
|
+
topAgents: [...row.agents.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([a]) => a),
|
|
195
|
+
topTools: [...row.tools.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([t]) => t)
|
|
196
|
+
})).sort((a, b) => b.avgBlastScore - a.avgBlastScore);
|
|
197
|
+
res.json({
|
|
198
|
+
ok: true,
|
|
199
|
+
windowMs,
|
|
200
|
+
eventsAnalyzed: events.length,
|
|
201
|
+
services: heatmap,
|
|
202
|
+
note: "Services sorted by average blast-radius score. High score + high call frequency = over-privileged agent."
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
router.get("/analytics/approvals", async (_req, res) => {
|
|
206
|
+
const analytics = await computeApprovalAnalytics();
|
|
207
|
+
res.json({ ok: true, ...analytics });
|
|
208
|
+
});
|
|
209
|
+
return router;
|
|
210
|
+
}
|
|
211
|
+
export {
|
|
212
|
+
createGateAnalyticsRouter
|
|
213
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { applyGate } from "../intelligence/tool-guard.js";
|
|
4
|
+
const EvaluateSchema = z.object({
|
|
5
|
+
toolName: z.string().min(1).optional(),
|
|
6
|
+
command: z.string().min(1)
|
|
7
|
+
});
|
|
8
|
+
function createGateRouter(port) {
|
|
9
|
+
const router = Router();
|
|
10
|
+
router.post("/gate/evaluate", async (req, res) => {
|
|
11
|
+
const parsed = EvaluateSchema.safeParse(req.body);
|
|
12
|
+
if (!parsed.success) {
|
|
13
|
+
res.status(400).json({ error: "command is required", details: parsed.error.flatten() });
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const { toolName, command } = parsed.data;
|
|
17
|
+
const next = async () => ({ content: [{ type: "text", text: "pass" }] });
|
|
18
|
+
const result = await applyGate(toolName ?? "cli_exec", { command }, next, port);
|
|
19
|
+
res.json({
|
|
20
|
+
isError: result.isError === true,
|
|
21
|
+
text: result.content[0]?.text ?? ""
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
return router;
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
createGateRouter
|
|
28
|
+
};
|