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,169 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
function createPolicyNlRouter() {
|
|
5
|
+
const router = Router();
|
|
6
|
+
router.post("/policies/from-description", (req, res) => {
|
|
7
|
+
const body = z.object({ description: z.string().min(5).max(500) }).safeParse(req.body);
|
|
8
|
+
if (!body.success) {
|
|
9
|
+
res.status(400).json({ error: "description (string, 5-500 chars) required" });
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const result = parseNaturalLanguagePolicy(body.data.description);
|
|
13
|
+
res.json({ ok: true, ...result });
|
|
14
|
+
});
|
|
15
|
+
return router;
|
|
16
|
+
}
|
|
17
|
+
const DESTRUCTIVE_KEYWORDS = [
|
|
18
|
+
"terraform destroy",
|
|
19
|
+
"kubectl delete",
|
|
20
|
+
"rm -rf",
|
|
21
|
+
"drop table",
|
|
22
|
+
"drop database",
|
|
23
|
+
"truncate",
|
|
24
|
+
"delete all",
|
|
25
|
+
"wipe",
|
|
26
|
+
"nuke",
|
|
27
|
+
"format"
|
|
28
|
+
];
|
|
29
|
+
const SCHEMA_KEYWORDS = [
|
|
30
|
+
"migration",
|
|
31
|
+
"schema",
|
|
32
|
+
"alter table",
|
|
33
|
+
"add column",
|
|
34
|
+
"drop column",
|
|
35
|
+
"prisma migrate",
|
|
36
|
+
"db:migrate"
|
|
37
|
+
];
|
|
38
|
+
const KEYWORD_DOMAINS = [
|
|
39
|
+
{ patterns: ["terraform", "infra", "infrastructure"], commands: ["terraform destroy", "terraform apply"], files: [".tf"] },
|
|
40
|
+
{ patterns: ["database", "db", "postgres", "mysql", "sql"], commands: ["drop table", "drop database", "truncate table"], files: ["migration", "schema.sql"] },
|
|
41
|
+
{ patterns: ["schema", "migration", "migrate"], commands: ["alter table", "db:migrate", "prisma migrate", "knex migrate"], files: ["migration"] },
|
|
42
|
+
{ patterns: ["kubernetes", "k8s", "cluster", "kubectl"], commands: ["kubectl delete", "kubectl drain"], files: [] },
|
|
43
|
+
{ patterns: ["auth", "authentication", "login", "jwt", "token"], commands: [], files: ["auth", "login", "jwt", "middleware"] },
|
|
44
|
+
{ patterns: ["payment", "billing", "stripe", "checkout"], commands: [], files: ["payment", "billing", "stripe", "checkout"] },
|
|
45
|
+
{ patterns: ["production", "prod"], commands: [], files: [] }
|
|
46
|
+
];
|
|
47
|
+
const DAY_MAP = {
|
|
48
|
+
sunday: 0,
|
|
49
|
+
monday: 1,
|
|
50
|
+
tuesday: 2,
|
|
51
|
+
wednesday: 3,
|
|
52
|
+
thursday: 4,
|
|
53
|
+
friday: 5,
|
|
54
|
+
saturday: 6
|
|
55
|
+
};
|
|
56
|
+
function parseNaturalLanguagePolicy(description) {
|
|
57
|
+
const lower = description.toLowerCase();
|
|
58
|
+
const notes = [];
|
|
59
|
+
const conditions = {};
|
|
60
|
+
let confidence = 0.5;
|
|
61
|
+
let action = "warn";
|
|
62
|
+
if (/\b(block|prevent|stop|deny|forbid|disallow|reject)\b/.test(lower)) {
|
|
63
|
+
action = "block";
|
|
64
|
+
confidence += 0.1;
|
|
65
|
+
} else if (/\b(warn|flag|alert|notify|hold|pause|review)\b/.test(lower)) {
|
|
66
|
+
action = "warn";
|
|
67
|
+
confidence += 0.1;
|
|
68
|
+
} else if (/\b(allow|permit|enable|pass|approve)\b/.test(lower)) {
|
|
69
|
+
action = "pass";
|
|
70
|
+
confidence += 0.05;
|
|
71
|
+
} else {
|
|
72
|
+
notes.push("Could not determine action (block/warn/pass) \u2014 defaulting to warn");
|
|
73
|
+
}
|
|
74
|
+
if (/\b(claude|cursor|agent|ai|bot|copilot|windsurf|automated|autonomous)\b/.test(lower)) {
|
|
75
|
+
conditions.actorType = "ai";
|
|
76
|
+
confidence += 0.05;
|
|
77
|
+
} else if (/\bhuman\b/.test(lower)) {
|
|
78
|
+
conditions.actorType = "human";
|
|
79
|
+
confidence += 0.05;
|
|
80
|
+
}
|
|
81
|
+
const envMatch = lower.match(/\b(production|prod|staging|stage|development|dev|test|testing)\b/);
|
|
82
|
+
if (envMatch) {
|
|
83
|
+
conditions.environments = [envMatch[1] === "stage" ? "staging" : envMatch[1] === "dev" ? "development" : envMatch[1]];
|
|
84
|
+
confidence += 0.1;
|
|
85
|
+
}
|
|
86
|
+
const daysFound = [];
|
|
87
|
+
for (const [name2, num] of Object.entries(DAY_MAP)) {
|
|
88
|
+
if (lower.includes(name2)) daysFound.push(num);
|
|
89
|
+
}
|
|
90
|
+
if (/\bweekend\b/.test(lower)) daysFound.push(0, 6);
|
|
91
|
+
if (/\bweekday\b/.test(lower)) daysFound.push(1, 2, 3, 4, 5);
|
|
92
|
+
if (daysFound.length > 0) {
|
|
93
|
+
conditions.daysOfWeek = [...new Set(daysFound)].sort();
|
|
94
|
+
confidence += 0.1;
|
|
95
|
+
}
|
|
96
|
+
const afterMatch = lower.match(/after\s+(\d{1,2})(am|pm)?/);
|
|
97
|
+
const beforeMatch = lower.match(/before\s+(\d{1,2})(am|pm)?/);
|
|
98
|
+
const betweenMatch = lower.match(/between\s+(\d{1,2})(am|pm)?\s+and\s+(\d{1,2})(am|pm)?/);
|
|
99
|
+
if (betweenMatch) {
|
|
100
|
+
const start = toHour(parseInt(betweenMatch[1]), betweenMatch[2]);
|
|
101
|
+
const end = toHour(parseInt(betweenMatch[3]), betweenMatch[4]);
|
|
102
|
+
conditions.hourWindow = [start, end];
|
|
103
|
+
confidence += 0.1;
|
|
104
|
+
} else if (afterMatch) {
|
|
105
|
+
const start = toHour(parseInt(afterMatch[1]), afterMatch[2]);
|
|
106
|
+
conditions.hourWindow = [start, 24];
|
|
107
|
+
confidence += 0.1;
|
|
108
|
+
} else if (beforeMatch) {
|
|
109
|
+
const end = toHour(parseInt(beforeMatch[1]), beforeMatch[2]);
|
|
110
|
+
conditions.hourWindow = [0, end];
|
|
111
|
+
confidence += 0.1;
|
|
112
|
+
}
|
|
113
|
+
const extractedCommands = [];
|
|
114
|
+
const extractedFiles = [];
|
|
115
|
+
for (const kw of DESTRUCTIVE_KEYWORDS) {
|
|
116
|
+
if (lower.includes(kw.split(" ")[0])) {
|
|
117
|
+
extractedCommands.push(kw);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const kw of SCHEMA_KEYWORDS) {
|
|
121
|
+
if (lower.includes(kw.split(" ")[0])) {
|
|
122
|
+
extractedCommands.push(kw);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (const domain of KEYWORD_DOMAINS) {
|
|
126
|
+
if (domain.patterns.some((p) => lower.includes(p))) {
|
|
127
|
+
extractedCommands.push(...domain.commands);
|
|
128
|
+
if (domain.files) extractedFiles.push(...domain.files);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (extractedCommands.length > 0) {
|
|
132
|
+
conditions.commands = [...new Set(extractedCommands)];
|
|
133
|
+
confidence += 0.15;
|
|
134
|
+
} else {
|
|
135
|
+
notes.push("Could not extract specific commands \u2014 you may need to add conditions.commands manually");
|
|
136
|
+
confidence -= 0.1;
|
|
137
|
+
}
|
|
138
|
+
if (extractedFiles.length > 0) {
|
|
139
|
+
conditions.files = [...new Set(extractedFiles)];
|
|
140
|
+
}
|
|
141
|
+
const actionWord = action;
|
|
142
|
+
const envWord = conditions.environments?.[0] ?? "";
|
|
143
|
+
const actorWord = conditions.actorType === "ai" ? "ai" : conditions.actorType === "human" ? "human" : "";
|
|
144
|
+
const domainWord = (conditions.commands?.[0] ?? "action").replace(/\s+/g, "_").replace(/[^a-z0-9_]/gi, "").slice(0, 20);
|
|
145
|
+
const id = [actionWord, actorWord, envWord, domainWord].filter(Boolean).join("_").toLowerCase().slice(0, 60) + "_" + randomUUID().slice(0, 4);
|
|
146
|
+
const name = description.slice(0, 60) + (description.length > 60 ? "\u2026" : "");
|
|
147
|
+
const envPhrase = conditions.environments ? ` in ${conditions.environments.join("/")}` : "";
|
|
148
|
+
const actorPhrase = conditions.actorType === "ai" ? " for AI agents" : conditions.actorType === "human" ? " for human operators" : "";
|
|
149
|
+
const reason = `Policy: ${actionWord === "block" ? "Blocked" : "Flagged"}${envPhrase}${actorPhrase} \u2014 ${description.slice(0, 120)}`;
|
|
150
|
+
if (Object.keys(conditions).length === 0) {
|
|
151
|
+
notes.push("No conditions could be extracted \u2014 this rule will match everything");
|
|
152
|
+
confidence = 0.2;
|
|
153
|
+
}
|
|
154
|
+
confidence = Math.min(0.95, Math.max(0.1, confidence));
|
|
155
|
+
return {
|
|
156
|
+
rule: { id, name, description, action, reason, conditions },
|
|
157
|
+
confidence: Math.round(confidence * 100) / 100,
|
|
158
|
+
notes
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function toHour(h, meridiem) {
|
|
162
|
+
if (!meridiem) return h;
|
|
163
|
+
if (meridiem === "pm" && h < 12) return h + 12;
|
|
164
|
+
if (meridiem === "am" && h === 12) return 0;
|
|
165
|
+
return h;
|
|
166
|
+
}
|
|
167
|
+
export {
|
|
168
|
+
createPolicyNlRouter
|
|
169
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { fetchSlackThread } from "../intelligence/slack.js";
|
|
4
|
+
import { draftPostmortemDoc } from "../intelligence/tools-runbook.js";
|
|
5
|
+
import { generatePostmortem } from "../intelligence/postmortem-store.js";
|
|
6
|
+
import { updateRunbookFromPostmortem } from "../intelligence/runbook-updater.js";
|
|
7
|
+
import { getStores } from "../storage/store-registry.js";
|
|
8
|
+
const BodySchema = z.object({
|
|
9
|
+
thread_url: z.string().url(),
|
|
10
|
+
service: z.string().optional(),
|
|
11
|
+
summary: z.string().optional(),
|
|
12
|
+
severity: z.enum(["sev1", "sev2", "sev3"]).optional(),
|
|
13
|
+
duration_minutes: z.number().int().min(1).max(1440).optional(),
|
|
14
|
+
affected_users: z.string().optional()
|
|
15
|
+
});
|
|
16
|
+
function createPostmortemRouter() {
|
|
17
|
+
const router = express.Router();
|
|
18
|
+
router.post("/postmortem/from-slack", async (req, res) => {
|
|
19
|
+
const parsed = BodySchema.safeParse(req.body);
|
|
20
|
+
if (!parsed.success) {
|
|
21
|
+
res.status(400).json({ error: "invalid request", details: parsed.error.flatten() });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const { thread_url, service = "unknown", summary, severity, duration_minutes, affected_users } = parsed.data;
|
|
25
|
+
const slackThread = await fetchSlackThread(thread_url);
|
|
26
|
+
if (slackThread === null) {
|
|
27
|
+
res.status(422).json({
|
|
28
|
+
error: "Could not fetch Slack thread. Ensure MERGEN_SLACK_BOT_TOKEN is set with channels:history scope and the URL is a valid Slack thread link."
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const markdown = await draftPostmortemDoc({
|
|
33
|
+
service,
|
|
34
|
+
summary,
|
|
35
|
+
severity,
|
|
36
|
+
duration_minutes,
|
|
37
|
+
affected_users,
|
|
38
|
+
slack_thread: slackThread
|
|
39
|
+
});
|
|
40
|
+
const overrideEvent = await getStores().overrides.compileOverrideFromSlackThread(slackThread, service, req.tenantId);
|
|
41
|
+
const responsePayload = { markdown };
|
|
42
|
+
if (overrideEvent) {
|
|
43
|
+
responsePayload.overrideCompiled = true;
|
|
44
|
+
responsePayload.overrideId = overrideEvent.id;
|
|
45
|
+
} else {
|
|
46
|
+
responsePayload.overrideCompiled = false;
|
|
47
|
+
}
|
|
48
|
+
res.json(responsePayload);
|
|
49
|
+
});
|
|
50
|
+
const GenerateSchema = z.object({
|
|
51
|
+
pid: z.string().uuid(),
|
|
52
|
+
tag: z.string().min(1),
|
|
53
|
+
service: z.string().min(1),
|
|
54
|
+
rootCause: z.string().min(1),
|
|
55
|
+
fixCommand: z.string().nullable().optional(),
|
|
56
|
+
confidence: z.number().min(0).max(1),
|
|
57
|
+
mttrMs: z.number().int().min(0).nullable(),
|
|
58
|
+
resolvedAutonomously: z.boolean().default(false),
|
|
59
|
+
causallyCorrect: z.boolean().default(false),
|
|
60
|
+
evidence: z.array(z.string()).optional(),
|
|
61
|
+
fixHint: z.string().nullable().optional()
|
|
62
|
+
});
|
|
63
|
+
router.post("/postmortem/generate", async (req, res) => {
|
|
64
|
+
const parsed = GenerateSchema.safeParse(req.body);
|
|
65
|
+
if (!parsed.success) {
|
|
66
|
+
res.status(400).json({ error: "invalid request", details: parsed.error.flatten() });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const input = parsed.data;
|
|
70
|
+
const pm = generatePostmortem({
|
|
71
|
+
pid: input.pid,
|
|
72
|
+
tag: input.tag,
|
|
73
|
+
service: input.service,
|
|
74
|
+
rootCause: input.rootCause,
|
|
75
|
+
fixCommand: input.fixCommand ?? null,
|
|
76
|
+
confidence: input.confidence,
|
|
77
|
+
mttrMs: input.mttrMs,
|
|
78
|
+
resolvedAutonomously: input.resolvedAutonomously,
|
|
79
|
+
causallyCorrect: input.causallyCorrect,
|
|
80
|
+
evidence: input.evidence,
|
|
81
|
+
fixHint: input.fixHint ?? null
|
|
82
|
+
});
|
|
83
|
+
let runbookUpdated = false;
|
|
84
|
+
try {
|
|
85
|
+
updateRunbookFromPostmortem(pm);
|
|
86
|
+
runbookUpdated = true;
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
res.json({
|
|
90
|
+
pid: pm.pid,
|
|
91
|
+
tag: pm.tag,
|
|
92
|
+
service: pm.service,
|
|
93
|
+
mttrMs: pm.mttrMs,
|
|
94
|
+
causallyCorrect: pm.causallyCorrect,
|
|
95
|
+
runbookUpdated
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
return router;
|
|
99
|
+
}
|
|
100
|
+
export {
|
|
101
|
+
createPostmortemRouter
|
|
102
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { getPRShadowStats, getPRShadowResults } from "../sensor/pr-shadow-store.js";
|
|
3
|
+
import { getUsageSnapshot } from "../intelligence/usage.js";
|
|
4
|
+
const USEFUL_RATE_THRESHOLD = 40;
|
|
5
|
+
const HELPFUL_RATE_THRESHOLD = 80;
|
|
6
|
+
function createPRShadowRouter() {
|
|
7
|
+
const router = Router();
|
|
8
|
+
router.get("/pr-shadow/stats", (_req, res) => {
|
|
9
|
+
const stats = getPRShadowStats();
|
|
10
|
+
const usage = getUsageSnapshot();
|
|
11
|
+
const helpfulRate = usage.helpfulRate7d;
|
|
12
|
+
const helpfulRateOk = helpfulRate !== null && helpfulRate >= HELPFUL_RATE_THRESHOLD;
|
|
13
|
+
const usefulRateOk = stats.wouldHaveBeenUsefulRate !== null && stats.wouldHaveBeenUsefulRate >= USEFUL_RATE_THRESHOLD;
|
|
14
|
+
const readyForPRComments = helpfulRateOk && usefulRateOk;
|
|
15
|
+
res.json({
|
|
16
|
+
ok: true,
|
|
17
|
+
shadowMode: true,
|
|
18
|
+
prCommentsEnabled: process.env.MERGEN_PR_COMMENTS === "true",
|
|
19
|
+
readyForPRComments,
|
|
20
|
+
readyConditions: {
|
|
21
|
+
wouldHaveBeenUsefulRate: stats.wouldHaveBeenUsefulRate,
|
|
22
|
+
wouldHaveBeenUsefulRateThreshold: USEFUL_RATE_THRESHOLD,
|
|
23
|
+
wouldHaveBeenUsefulRateOk: usefulRateOk,
|
|
24
|
+
helpfulRate7d: helpfulRate,
|
|
25
|
+
helpfulRateThreshold: HELPFUL_RATE_THRESHOLD,
|
|
26
|
+
helpfulRateOk
|
|
27
|
+
},
|
|
28
|
+
...stats,
|
|
29
|
+
nextSteps: readyForPRComments ? [
|
|
30
|
+
"Set MERGEN_PR_COMMENTS=true to enable Phase 2 PR comments.",
|
|
31
|
+
"Start with high-confidence incident matches only.",
|
|
32
|
+
"Monitor helpfulRate7d \u2014 disable if it drops below 70%."
|
|
33
|
+
] : [
|
|
34
|
+
usefulRateOk ? `\u2713 wouldHaveBeenUsefulRate is ${stats.wouldHaveBeenUsefulRate}% \u2014 above threshold.` : `wouldHaveBeenUsefulRate is ${stats.wouldHaveBeenUsefulRate ?? "N/A"}% \u2014 needs ${USEFUL_RATE_THRESHOLD}%. Run mergen backfill github to populate the intent archive.`,
|
|
35
|
+
helpfulRateOk ? `\u2713 helpfulRate7d is ${helpfulRate}% \u2014 above threshold.` : `helpfulRate7d is ${helpfulRate ?? "N/A"}% \u2014 needs ${HELPFUL_RATE_THRESHOLD}%. Collect more explain_why feedback.`
|
|
36
|
+
]
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
router.get("/pr-shadow/results", (req, res) => {
|
|
40
|
+
const limit = Math.min(200, Math.max(1, Number(req.query.limit ?? 50)));
|
|
41
|
+
res.json({ ok: true, results: getPRShadowResults(limit) });
|
|
42
|
+
});
|
|
43
|
+
return router;
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
createPRShadowRouter
|
|
47
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { listMembers, upsertMember, removeMember, hasPermission, resolveRole } from "../sensor/rbac.js";
|
|
4
|
+
import logger from "../sensor/logger.js";
|
|
5
|
+
const RoleSchema = z.enum(["viewer", "responder", "admin"]);
|
|
6
|
+
function createRbacRouter() {
|
|
7
|
+
const router = Router();
|
|
8
|
+
router.get("/rbac/members", (_req, res) => {
|
|
9
|
+
res.json({ ok: true, members: listMembers() });
|
|
10
|
+
});
|
|
11
|
+
router.post("/rbac/members", (req, res) => {
|
|
12
|
+
const caller = req.headers["x-mergen-member"] ?? "unknown";
|
|
13
|
+
if (!hasPermission(caller, "admin")) {
|
|
14
|
+
res.status(403).json({ error: "admin role required" });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const parsed = z.object({ id: z.string().min(1), role: RoleSchema }).safeParse(req.body);
|
|
18
|
+
if (!parsed.success) {
|
|
19
|
+
res.status(400).json({ error: "id and role (viewer|responder|admin) are required" });
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const member = upsertMember(parsed.data.id, parsed.data.role);
|
|
23
|
+
logger.info({ caller, target: member.id, role: member.role }, "rbac: member added via API");
|
|
24
|
+
res.json({ ok: true, member });
|
|
25
|
+
});
|
|
26
|
+
router.put("/rbac/members/:id", (req, res) => {
|
|
27
|
+
const caller = req.headers["x-mergen-member"] ?? "unknown";
|
|
28
|
+
if (!hasPermission(caller, "admin")) {
|
|
29
|
+
res.status(403).json({ error: "admin role required" });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const parsed = z.object({ role: RoleSchema }).safeParse(req.body);
|
|
33
|
+
if (!parsed.success) {
|
|
34
|
+
res.status(400).json({ error: "role (viewer|responder|admin) is required" });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const member = upsertMember(req.params.id, parsed.data.role);
|
|
38
|
+
res.json({ ok: true, member });
|
|
39
|
+
});
|
|
40
|
+
router.delete("/rbac/members/:id", (req, res) => {
|
|
41
|
+
const caller = req.headers["x-mergen-member"] ?? "unknown";
|
|
42
|
+
if (!hasPermission(caller, "admin")) {
|
|
43
|
+
res.status(403).json({ error: "admin role required" });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const removed = removeMember(req.params.id);
|
|
47
|
+
if (!removed) {
|
|
48
|
+
res.status(404).json({ error: "member not found" });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
res.json({ ok: true });
|
|
52
|
+
});
|
|
53
|
+
router.get("/rbac/me", (req, res) => {
|
|
54
|
+
const actor = req.headers["x-mergen-member"] ?? "unknown";
|
|
55
|
+
res.json({ ok: true, actor, role: resolveRole(actor) });
|
|
56
|
+
});
|
|
57
|
+
return router;
|
|
58
|
+
}
|
|
59
|
+
export {
|
|
60
|
+
createRbacRouter
|
|
61
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { buildReplay } from "../intelligence/replay.js";
|
|
3
|
+
function createReplayRouter() {
|
|
4
|
+
const router = Router();
|
|
5
|
+
router.get("/replay/:traceId", async (req, res) => {
|
|
6
|
+
const result = await buildReplay(req.params.traceId);
|
|
7
|
+
res.json({
|
|
8
|
+
ok: true,
|
|
9
|
+
...result,
|
|
10
|
+
note: result.entries.length === 0 ? `No gate-event or blunder-log entries found for trace id "${req.params.traceId}".` : `${result.entries.length} entrie(s) reconstructed for this trace.`
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
return router;
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
createReplayRouter
|
|
17
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { getStores } from "../storage/store-registry.js";
|
|
3
|
+
import { loadEnterprisePolicy } from "../intelligence/enterprise-policy-engine.js";
|
|
4
|
+
import { isNonBlockingBlunderType } from "../sensor/agent-blunder-store.js";
|
|
5
|
+
function createRiskReportRouter() {
|
|
6
|
+
const router = Router();
|
|
7
|
+
router.get("/risk-report", async (req, res) => {
|
|
8
|
+
const format = req.query.format;
|
|
9
|
+
const data = await computeRiskData(req.tenantId);
|
|
10
|
+
if (format === "md") {
|
|
11
|
+
res.setHeader("Content-Type", "text/markdown; charset=utf-8");
|
|
12
|
+
res.setHeader("Content-Disposition", 'attachment; filename="mergen-risk-report.md"');
|
|
13
|
+
res.send(buildMarkdown(data));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
res.json({ ok: true, report: data });
|
|
17
|
+
});
|
|
18
|
+
return router;
|
|
19
|
+
}
|
|
20
|
+
const TYPE_DESCRIPTIONS = {
|
|
21
|
+
allowlist_block: "Command not on the approved allowlist",
|
|
22
|
+
injection_attempt: "Prompt injection detected in command payload",
|
|
23
|
+
rbac_block: "Actor missing required role for this action",
|
|
24
|
+
override_corpus_block: "Action previously overridden by a human decision",
|
|
25
|
+
pipeline_block: "Governance pipeline blocked the action",
|
|
26
|
+
planning_gate_block: "Planning gate: confidence or blast-radius check failed"
|
|
27
|
+
};
|
|
28
|
+
async function computeRiskData(tenantId) {
|
|
29
|
+
const [stats, blunders] = await Promise.all([
|
|
30
|
+
getStores().blunders.getStats(tenantId),
|
|
31
|
+
getStores().blunders.list(tenantId)
|
|
32
|
+
]);
|
|
33
|
+
const overrides = await getStores().overrides.getOverrideSummary(tenantId);
|
|
34
|
+
const policy = loadEnterprisePolicy();
|
|
35
|
+
const breakdown = Object.entries(stats.byType).filter(([type]) => !isNonBlockingBlunderType(type)).map(([type, count]) => ({
|
|
36
|
+
type,
|
|
37
|
+
count,
|
|
38
|
+
description: TYPE_DESCRIPTIONS[type] ?? type
|
|
39
|
+
})).sort((a, b) => b.count - a.count);
|
|
40
|
+
const cmdCounts = /* @__PURE__ */ new Map();
|
|
41
|
+
for (const b of blunders) {
|
|
42
|
+
if (isNonBlockingBlunderType(b.blunderType)) continue;
|
|
43
|
+
if (b.command) {
|
|
44
|
+
const key = b.command.trim().slice(0, 80);
|
|
45
|
+
cmdCounts.set(key, (cmdCounts.get(key) ?? 0) + 1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const topBlockedCommands = [...cmdCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([command, count]) => ({ command, count }));
|
|
49
|
+
let riskScore;
|
|
50
|
+
let riskRationale;
|
|
51
|
+
if (stats.last7Days <= 5) {
|
|
52
|
+
riskScore = "low";
|
|
53
|
+
riskRationale = `${stats.last7Days} blocked actions in the past 7 days \u2014 agent activity is within safe bounds.`;
|
|
54
|
+
} else if (stats.last7Days <= 20) {
|
|
55
|
+
riskScore = "medium";
|
|
56
|
+
riskRationale = `${stats.last7Days} blocked actions in the past 7 days \u2014 elevated agent activity. Review breakdown for patterns.`;
|
|
57
|
+
} else {
|
|
58
|
+
riskScore = "high";
|
|
59
|
+
riskRationale = `${stats.last7Days} blocked actions in the past 7 days \u2014 significant autonomous activity intercepted. Audit recommended.`;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
63
|
+
windowDays: 30,
|
|
64
|
+
totalBlocked: stats.total,
|
|
65
|
+
last7Days: stats.last7Days,
|
|
66
|
+
last30Days: stats.last30Days,
|
|
67
|
+
breakdown,
|
|
68
|
+
topBlockedCommands,
|
|
69
|
+
activeRules: policy.enabled ? policy.rules.filter((r) => r.action === "block").length : 0,
|
|
70
|
+
overrideCorpusSize: overrides.length,
|
|
71
|
+
riskScore,
|
|
72
|
+
riskRationale
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function buildMarkdown(d) {
|
|
76
|
+
const riskBadge = { low: "\u{1F7E2} LOW", medium: "\u{1F7E1} MEDIUM", high: "\u{1F534} HIGH" }[d.riskScore];
|
|
77
|
+
const breakdownTable = d.breakdown.length > 0 ? [
|
|
78
|
+
"| Type | Count | Description |",
|
|
79
|
+
"|------|------:|-------------|",
|
|
80
|
+
...d.breakdown.map((r) => `| \`${r.type}\` | ${r.count} | ${r.description} |`)
|
|
81
|
+
].join("\n") : "_No blocked actions recorded yet._";
|
|
82
|
+
const topCmdsSection = d.topBlockedCommands.length > 0 ? [
|
|
83
|
+
"| Command | Times Blocked |",
|
|
84
|
+
"|---------|-------------:|",
|
|
85
|
+
...d.topBlockedCommands.map((c) => `| \`${c.command}\` | ${c.count} |`)
|
|
86
|
+
].join("\n") : "_No commands blocked yet._";
|
|
87
|
+
return `# Mergen \u2014 AI Agent Security Risk Report
|
|
88
|
+
|
|
89
|
+
**Generated:** ${d.generatedAt}
|
|
90
|
+
**Risk Level:** ${riskBadge}
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Executive Summary
|
|
95
|
+
|
|
96
|
+
> ${d.riskRationale}
|
|
97
|
+
|
|
98
|
+
Mergen is the inline Execution and Security Gateway running on your engineering
|
|
99
|
+
infrastructure. Every time an AI agent attempted a potentially harmful action,
|
|
100
|
+
Mergen intercepted it **before the handler executed** \u2014 in under 1ms, with no
|
|
101
|
+
LLM in the critical path.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Blocked Actions
|
|
106
|
+
|
|
107
|
+
| Metric | Value |
|
|
108
|
+
|--------|------:|
|
|
109
|
+
| Total intercepted (all time) | **${d.totalBlocked}** |
|
|
110
|
+
| Intercepted \u2014 last 7 days | **${d.last7Days}** |
|
|
111
|
+
| Intercepted \u2014 last 30 days | **${d.last30Days}** |
|
|
112
|
+
| Active block rules | **${d.activeRules}** |
|
|
113
|
+
| Override corpus entries | **${d.overrideCorpusSize}** |
|
|
114
|
+
|
|
115
|
+
### Breakdown by Type
|
|
116
|
+
|
|
117
|
+
${breakdownTable}
|
|
118
|
+
|
|
119
|
+
### Top Blocked Commands
|
|
120
|
+
|
|
121
|
+
${topCmdsSection}
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Enforcement Architecture
|
|
126
|
+
|
|
127
|
+
Mergen applies three enforcement layers before any autonomous tool call executes:
|
|
128
|
+
|
|
129
|
+
1. **Hard Safety Policies** \u2014 Immutable JSON rules evaluated in <1ms. No LLM.
|
|
130
|
+
No amount of agent persuasion bypasses them.
|
|
131
|
+
2. **Override Corpus** \u2014 Every human override is encoded as binding enforcement
|
|
132
|
+
policy. ${d.overrideCorpusSize} entries active.
|
|
133
|
+
3. **Confidence Gate** \u2014 Autonomous execution only proceeds above a
|
|
134
|
+
Platt-calibrated threshold set by your team.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Audit Trail
|
|
139
|
+
|
|
140
|
+
The Agent Blunder Log is **hash-chained and tamper-evident**. Every blocked
|
|
141
|
+
action is recorded with a SHA-256 hash of its content plus the previous
|
|
142
|
+
entry's hash. An external auditor can verify integrity at any time:
|
|
143
|
+
|
|
144
|
+
\`\`\`
|
|
145
|
+
GET /agent-blunders/verify
|
|
146
|
+
\`\`\`
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Next Steps
|
|
151
|
+
|
|
152
|
+
- Review the breakdown table for repeat offenders
|
|
153
|
+
- Run \`mergen-server test-safety\` to verify the gate is holding against known bypass patterns
|
|
154
|
+
- Share this report with your security team \u2014 data is sourced from your local
|
|
155
|
+
infrastructure only; nothing leaves your environment
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
_Mergen \u2014 Agent Execution Governance. All data stays on your infrastructure._
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
export {
|
|
163
|
+
createRiskReportRouter
|
|
164
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
import { Router } from "express";
|
|
5
|
+
import { DATA_DIR } from "../sensor/paths.js";
|
|
6
|
+
import logger from "../sensor/logger.js";
|
|
7
|
+
const LIBRARY_FILE = path.join(DATA_DIR, "runbooks", "library.json");
|
|
8
|
+
const RUNBOOKS_DIR = path.join(DATA_DIR, "runbooks");
|
|
9
|
+
function readLibrary() {
|
|
10
|
+
try {
|
|
11
|
+
if (!fs.existsSync(LIBRARY_FILE)) return [];
|
|
12
|
+
return JSON.parse(fs.readFileSync(LIBRARY_FILE, "utf8"));
|
|
13
|
+
} catch {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function writeLibrary(runbooks) {
|
|
18
|
+
try {
|
|
19
|
+
fs.mkdirSync(RUNBOOKS_DIR, { recursive: true });
|
|
20
|
+
const tmp = `${LIBRARY_FILE}.tmp`;
|
|
21
|
+
fs.writeFileSync(tmp, JSON.stringify(runbooks, null, 2), "utf8");
|
|
22
|
+
fs.renameSync(tmp, LIBRARY_FILE);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
logger.error({ err }, "runbooks: failed to persist library");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function getRunbookForTag(incidentTag) {
|
|
28
|
+
return readLibrary().find((r) => r.incidentTag === incidentTag && r.approved);
|
|
29
|
+
}
|
|
30
|
+
function createRunbooksRouter() {
|
|
31
|
+
const router = Router();
|
|
32
|
+
router.get("/runbooks", (_req, res) => {
|
|
33
|
+
const runbooks = readLibrary();
|
|
34
|
+
res.json({ ok: true, total: runbooks.length, runbooks });
|
|
35
|
+
});
|
|
36
|
+
router.get("/runbooks/:id", (req, res) => {
|
|
37
|
+
const runbook = readLibrary().find((r) => r.id === req.params["id"]);
|
|
38
|
+
if (!runbook) {
|
|
39
|
+
res.status(404).json({ error: "not found" });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
res.json({ ok: true, runbook });
|
|
43
|
+
});
|
|
44
|
+
router.post("/runbooks", (req, res) => {
|
|
45
|
+
const { name, incidentTag, steps, riskTier, requiredApprovers } = req.body;
|
|
46
|
+
if (!name || typeof name !== "string") {
|
|
47
|
+
res.status(400).json({ error: "name is required" });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (!incidentTag || typeof incidentTag !== "string") {
|
|
51
|
+
res.status(400).json({ error: "incidentTag is required" });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!Array.isArray(steps) || steps.length === 0) {
|
|
55
|
+
res.status(400).json({ error: "steps must be a non-empty array" });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const validTiers = ["RESTART", "DEPLOY", "FULL"];
|
|
59
|
+
if (!riskTier || !validTiers.includes(riskTier)) {
|
|
60
|
+
res.status(400).json({ error: "riskTier must be RESTART, DEPLOY, or FULL" });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
const runbook = {
|
|
65
|
+
id: randomUUID(),
|
|
66
|
+
name: name.slice(0, 200),
|
|
67
|
+
incidentTag: incidentTag.slice(0, 100),
|
|
68
|
+
steps: steps.map((s) => String(s).slice(0, 500)),
|
|
69
|
+
riskTier,
|
|
70
|
+
requiredApprovers: typeof requiredApprovers === "number" && requiredApprovers > 0 ? requiredApprovers : 1,
|
|
71
|
+
approvals: [],
|
|
72
|
+
approved: false,
|
|
73
|
+
createdAt: now,
|
|
74
|
+
updatedAt: now
|
|
75
|
+
};
|
|
76
|
+
const library = readLibrary();
|
|
77
|
+
library.push(runbook);
|
|
78
|
+
writeLibrary(library);
|
|
79
|
+
logger.info({ id: runbook.id, name: runbook.name, incidentTag }, "runbooks: created");
|
|
80
|
+
res.status(201).json({ ok: true, runbook });
|
|
81
|
+
});
|
|
82
|
+
router.put("/runbooks/:id/approve", (req, res) => {
|
|
83
|
+
const { actor } = req.body;
|
|
84
|
+
if (!actor || typeof actor !== "string") {
|
|
85
|
+
res.status(400).json({ error: "actor is required" });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const library = readLibrary();
|
|
89
|
+
const runbook = library.find((r) => r.id === req.params["id"]);
|
|
90
|
+
if (!runbook) {
|
|
91
|
+
res.status(404).json({ error: "not found" });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (runbook.approvals.some((a) => a.actor === actor)) {
|
|
95
|
+
res.status(409).json({ error: "actor has already approved this runbook" });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
runbook.approvals.push({ actor: actor.slice(0, 100), approvedAt: Date.now() });
|
|
99
|
+
runbook.approved = runbook.approvals.length >= runbook.requiredApprovers;
|
|
100
|
+
runbook.updatedAt = Date.now();
|
|
101
|
+
writeLibrary(library);
|
|
102
|
+
logger.info(
|
|
103
|
+
{ id: runbook.id, actor, approved: runbook.approved, approvals: runbook.approvals.length },
|
|
104
|
+
"runbooks: approval recorded"
|
|
105
|
+
);
|
|
106
|
+
res.json({ ok: true, runbook });
|
|
107
|
+
});
|
|
108
|
+
router.delete("/runbooks/:id", (req, res) => {
|
|
109
|
+
const library = readLibrary();
|
|
110
|
+
const idx = library.findIndex((r) => r.id === req.params["id"]);
|
|
111
|
+
if (idx === -1) {
|
|
112
|
+
res.status(404).json({ error: "not found" });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const [removed] = library.splice(idx, 1);
|
|
116
|
+
writeLibrary(library);
|
|
117
|
+
logger.info({ id: removed.id, name: removed.name }, "runbooks: deleted");
|
|
118
|
+
res.json({ ok: true });
|
|
119
|
+
});
|
|
120
|
+
return router;
|
|
121
|
+
}
|
|
122
|
+
export {
|
|
123
|
+
createRunbooksRouter,
|
|
124
|
+
getRunbookForTag
|
|
125
|
+
};
|