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
package/dist/index.js
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import net from "net";
|
|
4
|
+
import http from "http";
|
|
5
|
+
import https from "https";
|
|
6
|
+
import { randomUUID } from "crypto";
|
|
7
|
+
import { createRequire } from "module";
|
|
8
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
|
+
import { lemonSqueezySetup } from "@lemonsqueezy/lemonsqueezy.js";
|
|
11
|
+
import logger from "./sensor/logger.js";
|
|
12
|
+
import { DATA_DIR, SECRET_FILE } from "./sensor/paths.js";
|
|
13
|
+
import { setBufferSizeGetter, store, setStore } from "./sensor/buffer.js";
|
|
14
|
+
import { wrapWithRedisPersistence, stopRedisStore } from "./sensor/redis-store.js";
|
|
15
|
+
import { actionLedger } from "./sensor/action-ledger.js";
|
|
16
|
+
import { startWatcher } from "./sensor/watcher.js";
|
|
17
|
+
import { startDockerMonitor, startHeapMonitor, stopDockerMonitor } from "./sensor/docker-monitor.js";
|
|
18
|
+
import { startDockerLogStream, stopDockerLogStream } from "./sensor/docker-log-stream.js";
|
|
19
|
+
import { incidentStore } from "./sensor/incident-store.js";
|
|
20
|
+
import { postmortemStore } from "./intelligence/postmortem-store.js";
|
|
21
|
+
import { memoryStore } from "./datadog/memory-store.js";
|
|
22
|
+
import { commitContextStore } from "./sensor/commit-context-store.js";
|
|
23
|
+
import { agentContextStore as agentMemoryStore } from "./sensor/agent-context-store.js";
|
|
24
|
+
import { complianceLedgerStore } from "./sensor/audit-log.js";
|
|
25
|
+
import { stopAllProcessWatchers } from "./sensor/process-watcher.js";
|
|
26
|
+
import { stopFileWatch } from "./sensor/fs-watcher.js";
|
|
27
|
+
import { saveSession, loadSession } from "./sensor/session-persist.js";
|
|
28
|
+
import { saveSessionToHistory } from "./sensor/session-history.js";
|
|
29
|
+
import { syncMarkdownFilesFromDisk } from "./intelligence/postmortem-parser.js";
|
|
30
|
+
import { initLicense, getActivePlanId, revalidateLicense } from "./intelligence/license.js";
|
|
31
|
+
import { initUsage, flushOverageOnShutdown } from "./intelligence/usage.js";
|
|
32
|
+
import { flushPendingRebuild } from "./intelligence/hypothesis-history.js";
|
|
33
|
+
import { initTeam, broadcastToTeam } from "./intelligence/team.js";
|
|
34
|
+
import { initTelemetry, maybeSendTelemetry } from "./intelligence/telemetry.js";
|
|
35
|
+
import { getPlan } from "./intelligence/plans.js";
|
|
36
|
+
import { registerTools, toolCallCounts } from "./intelligence/tools.js";
|
|
37
|
+
import { createGuardedServer, loadBypasses, persistBypasses, setBypassSecret, denyStaleHoldsOnStartup, assertGateCoversRegisteredTools } from "./intelligence/tool-guard.js";
|
|
38
|
+
import { setPolicySigningSecret, assertUnsignedPolicyFlagIsSafe } from "./intelligence/enterprise-policy-engine.js";
|
|
39
|
+
import { setAgentTokenSecret } from "./intelligence/agent-identity.js";
|
|
40
|
+
import { setBlunderHmacSecret } from "./sensor/agent-blunder-store.js";
|
|
41
|
+
import { flushApprovals } from "./intelligence/execution-gate.js";
|
|
42
|
+
import { registerResources } from "./intelligence/mcp-resources.js";
|
|
43
|
+
import { registerPrompts } from "./intelligence/mcp-prompts.js";
|
|
44
|
+
import { SYSTEM_PROMPT } from "./intelligence/prompts.js";
|
|
45
|
+
import { registerTeamBroadcaster } from "./sensor/ingest.js";
|
|
46
|
+
import { isCorpusSeeded, getRealVerdictCount } from "./__stubs__/calibration.js";
|
|
47
|
+
import { startSlackDailyDigest } from "./intelligence/slack-digest.js";
|
|
48
|
+
import { startSlackOverrideLoop } from "./intelligence/slack-override-loop.js";
|
|
49
|
+
import { startGitAdrSync } from "./intelligence/git-adr-sync.js";
|
|
50
|
+
import { startPolicySync } from "./intelligence/policy-sync.js";
|
|
51
|
+
import { startShadowDigestCron } from "./intelligence/shadow-digest-cron.js";
|
|
52
|
+
import { startDegradationWatcher } from "./intelligence/degradation-watcher.js";
|
|
53
|
+
import { startHeartbeatMonitor, setHeartbeatAlertFn } from "./sensor/heartbeat-monitor.js";
|
|
54
|
+
import { startGateHeartbeat } from "./sensor/gate-heartbeat.js";
|
|
55
|
+
import { startK8sEventsPoller } from "./sensor/k8s-events.js";
|
|
56
|
+
import { loadPlugins } from "./intelligence/detector-plugins.js";
|
|
57
|
+
import { notify } from "./intelligence/notifications.js";
|
|
58
|
+
import { serviceGraph } from "./sensor/service-graph.js";
|
|
59
|
+
import { createStores } from "./storage/store-factory.js";
|
|
60
|
+
import { setStores } from "./storage/store-registry.js";
|
|
61
|
+
import { initCloudApiKeys } from "./sensor/cloud-auth.js";
|
|
62
|
+
import { createApp } from "./app.js";
|
|
63
|
+
import { checkForUpdates, formatUpdateMessage } from "./update-checker.js";
|
|
64
|
+
import { loadCommunityCorpus } from "./seeds/community-corpus.js";
|
|
65
|
+
const _require = createRequire(import.meta.url);
|
|
66
|
+
const { version: SERVER_VERSION } = _require("../package.json");
|
|
67
|
+
const PORT_RANGE_START = 3e3;
|
|
68
|
+
const PORT_RANGE_END = 3010;
|
|
69
|
+
const BIND_HOST = process.env.MERGEN_BIND ?? "127.0.0.1";
|
|
70
|
+
async function isPortAvailable(port) {
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
const s = net.createServer();
|
|
73
|
+
s.once("error", () => resolve(false));
|
|
74
|
+
s.once("listening", () => s.close(() => resolve(true)));
|
|
75
|
+
s.listen(port, BIND_HOST);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async function findPort(start, end) {
|
|
79
|
+
for (let port = start; port <= end; port++) {
|
|
80
|
+
if (await isPortAvailable(port)) return port;
|
|
81
|
+
logger.warn(`port ${port} in use, trying next...`);
|
|
82
|
+
}
|
|
83
|
+
logger.error(`all ports ${start}\u2013${end} are in use; kill the conflicting process and restart`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
function validateConfig() {
|
|
87
|
+
const CLOUD_MODE = process.env.MERGEN_CLOUD_MODE === "true";
|
|
88
|
+
const AUTOPILOT = process.env.MERGEN_AUTOPILOT === "true";
|
|
89
|
+
const TEAM_MODE = BIND_HOST !== "127.0.0.1";
|
|
90
|
+
if (CLOUD_MODE) {
|
|
91
|
+
if (!process.env.MERGEN_PG_URL) {
|
|
92
|
+
logger.error(
|
|
93
|
+
"FATAL: MERGEN_CLOUD_MODE=true but MERGEN_PG_URL is not set. Cloud mode requires a Postgres connection for durable storage. Set MERGEN_PG_URL=postgres://user:pass@host:5432/mergen and restart."
|
|
94
|
+
);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
if (!process.env.MERGEN_REDIS_URL) {
|
|
98
|
+
logger.error(
|
|
99
|
+
"FATAL: MERGEN_CLOUD_MODE=true but MERGEN_REDIS_URL is not set. Cloud mode requires Redis for BullMQ job queues and distributed state. Set MERGEN_REDIS_URL=redis://host:6379 and restart."
|
|
100
|
+
);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (AUTOPILOT && !process.env.MERGEN_PAGERDUTY_SECRET) {
|
|
105
|
+
logger.error(
|
|
106
|
+
"FATAL: MERGEN_AUTOPILOT=true but MERGEN_PAGERDUTY_SECRET is not set. Without webhook signature verification any caller can trigger autonomous execution. Set MERGEN_PAGERDUTY_SECRET to the signing secret from your PagerDuty webhook config, then restart."
|
|
107
|
+
);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
if (TEAM_MODE && !process.env.MERGEN_SECRET) {
|
|
111
|
+
logger.error(
|
|
112
|
+
"FATAL: MERGEN_BIND is not 127.0.0.1 (team mode) but MERGEN_SECRET is not set. The /ingest endpoint would accept events from any network caller. Generate a secret with: openssl rand -hex 32 then set MERGEN_SECRET=<value> and restart."
|
|
113
|
+
);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
if (!process.env.MERGEN_SLACK_BOT_TOKEN) {
|
|
117
|
+
logger.warn(
|
|
118
|
+
"startup: MERGEN_SLACK_BOT_TOKEN not set \u2014 incident Slack alerts and thread replies are disabled. Set it to your Slack bot token (xoxb-...) to enable autonomous incident notifications."
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (process.env.MERGEN_SLACK_BOT_TOKEN && !process.env.MERGEN_SLACK_SIGNING_SECRET) {
|
|
122
|
+
logger.warn(
|
|
123
|
+
"startup: MERGEN_SLACK_BOT_TOKEN is set but MERGEN_SLACK_SIGNING_SECRET is missing \u2014 the /slack/actions endpoint will reject all requests until the signing secret is configured. Set MERGEN_SLACK_SIGNING_SECRET to the HMAC secret from your Slack app configuration."
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (CLOUD_MODE && !process.env.MERGEN_PAGERDUTY_SECRET) {
|
|
127
|
+
logger.error(
|
|
128
|
+
"FATAL: MERGEN_CLOUD_MODE=true but MERGEN_PAGERDUTY_SECRET is not set. In cloud mode the server is network-reachable and an unsigned webhook can trigger autonomous execution. Set MERGEN_PAGERDUTY_SECRET to the signing secret from your PagerDuty webhook config, then restart."
|
|
129
|
+
);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
} else if (!AUTOPILOT && !process.env.MERGEN_PAGERDUTY_SECRET) {
|
|
132
|
+
logger.warn(
|
|
133
|
+
"startup: MERGEN_PAGERDUTY_SECRET not set \u2014 PagerDuty webhook signature verification is disabled. Diagnosis-only mode is active so no commands will execute, but set the secret before enabling autopilot."
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (CLOUD_MODE && !process.env.MERGEN_TLS_CERT) {
|
|
137
|
+
logger.warn(
|
|
138
|
+
"startup: MERGEN_CLOUD_MODE=true but MERGEN_TLS_CERT / MERGEN_TLS_KEY are not set \u2014 server will start in plain HTTP. Set TLS certificates for secure cloud deployments."
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
if (!process.env.MERGEN_SECRET) {
|
|
142
|
+
logger.warn(
|
|
143
|
+
"startup: MERGEN_SECRET not set \u2014 /ingest endpoint accepts events from any local process. Set MERGEN_SECRET=<random-string> to restrict ingest to authenticated sources."
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
if (process.env.MERGEN_POLICY_URL && !process.env.MERGEN_POLICY_URL.startsWith("https://")) {
|
|
147
|
+
logger.error(
|
|
148
|
+
"FATAL: MERGEN_POLICY_URL must use HTTPS (got a plain HTTP URL). A plain HTTP policy URL allows MITM injection of permissive policy rules that disable safety gates. Update MERGEN_POLICY_URL to an https:// address and restart."
|
|
149
|
+
);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
if (TEAM_MODE && !process.env.MERGEN_PUBLIC_URL) {
|
|
153
|
+
logger.warn(
|
|
154
|
+
"startup: MERGEN_BIND is not 127.0.0.1 (team mode) but MERGEN_PUBLIC_URL is not set. HITL webhook approve/deny links will point to http://127.0.0.1 \u2014 unreachable from Slack or external callers. Set MERGEN_PUBLIC_URL=https://your-server.example.com so Slack can POST the approve/deny callback."
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (AUTOPILOT && process.env.MERGEN_SHADOW_MODE === void 0) {
|
|
158
|
+
logger.warn(
|
|
159
|
+
"startup: MERGEN_AUTOPILOT=true but MERGEN_SHADOW_MODE is not set \u2014 defaulting to SHADOW MODE. Mergen will diagnose incidents and post Slack alerts but will NOT execute commands. Review GET /shadow-report to build trust, then set MERGEN_SHADOW_MODE=false to enable live execution."
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
const enabled = [];
|
|
163
|
+
if (AUTOPILOT) enabled.push("autopilot");
|
|
164
|
+
if (process.env.MERGEN_SHADOW_MODE === "true") enabled.push("shadow-mode");
|
|
165
|
+
if (process.env.MERGEN_DOCKER_MONITOR === "true") enabled.push("docker-monitor");
|
|
166
|
+
if (process.env.MERGEN_DOCKER_LOGS === "true") enabled.push("docker-logs");
|
|
167
|
+
if (process.env.MERGEN_K8S_NAMESPACE) enabled.push(`k8s(${process.env.MERGEN_K8S_NAMESPACE})`);
|
|
168
|
+
if (process.env.MERGEN_REDIS_URL) enabled.push("redis");
|
|
169
|
+
if (process.env.DD_API_KEY) enabled.push("datadog");
|
|
170
|
+
if (enabled.length > 0) logger.info({ enabled }, "startup: optional features enabled");
|
|
171
|
+
}
|
|
172
|
+
async function main() {
|
|
173
|
+
validateConfig();
|
|
174
|
+
const lsApiKey = process.env.LS_API_KEY;
|
|
175
|
+
if (lsApiKey) lemonSqueezySetup({ apiKey: lsApiKey });
|
|
176
|
+
let localSecret;
|
|
177
|
+
try {
|
|
178
|
+
fs.mkdirSync(DATA_DIR, { recursive: true, mode: 448 });
|
|
179
|
+
if (fs.existsSync(SECRET_FILE)) {
|
|
180
|
+
localSecret = fs.readFileSync(SECRET_FILE, "utf8").trim();
|
|
181
|
+
} else {
|
|
182
|
+
localSecret = randomUUID();
|
|
183
|
+
fs.writeFileSync(SECRET_FILE, localSecret, { encoding: "utf8", mode: 384 });
|
|
184
|
+
}
|
|
185
|
+
} catch (err) {
|
|
186
|
+
logger.warn({ err }, "secret: could not read/write secret file, generating ephemeral secret");
|
|
187
|
+
localSecret = randomUUID();
|
|
188
|
+
}
|
|
189
|
+
await initLicense();
|
|
190
|
+
await initUsage();
|
|
191
|
+
setInterval(() => {
|
|
192
|
+
void revalidateLicense();
|
|
193
|
+
}, 24 * 60 * 60 * 1e3).unref();
|
|
194
|
+
await initTeam();
|
|
195
|
+
await initTelemetry();
|
|
196
|
+
if (process.env.MERGEN_CLOUD_MODE === "true") {
|
|
197
|
+
const { runMigrations } = await import("./storage/pg/pg-migrations.js");
|
|
198
|
+
await runMigrations();
|
|
199
|
+
logger.info("startup: Postgres migrations applied");
|
|
200
|
+
}
|
|
201
|
+
const stores = await createStores();
|
|
202
|
+
setStores(stores);
|
|
203
|
+
await stores.events.init();
|
|
204
|
+
await stores.incidents.init();
|
|
205
|
+
await initCloudApiKeys();
|
|
206
|
+
setInterval(() => stores.events.pruneOld().catch(() => {
|
|
207
|
+
}), 6e4).unref();
|
|
208
|
+
await postmortemStore.init();
|
|
209
|
+
void syncMarkdownFilesFromDisk().catch((err) => logger.warn({ err }, "startup: markdown sync failed"));
|
|
210
|
+
await complianceLedgerStore.init();
|
|
211
|
+
try {
|
|
212
|
+
const staleCount = postmortemStore.countUnverifiedAutonomous();
|
|
213
|
+
if (staleCount > 0) {
|
|
214
|
+
logger.warn(
|
|
215
|
+
`startup: ${staleCount} postmortem${staleCount !== 1 ? "s" : ""} with resolved_autonomously=1 and causally_correct=0 found in corpus \u2014 likely written before the causal-verification fix. check_fix_history and runbook verified_fixes will show conservative (low) success rates until new causally-verified resolutions are recorded. These rows expire from the 90-day retention window automatically.`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
}
|
|
220
|
+
await memoryStore.init();
|
|
221
|
+
await commitContextStore.init();
|
|
222
|
+
await agentMemoryStore.init();
|
|
223
|
+
await actionLedger.init();
|
|
224
|
+
serviceGraph.loadPersisted();
|
|
225
|
+
const { hydrateGateAnalytics } = await import("./intelligence/gate-analytics.js");
|
|
226
|
+
await hydrateGateAnalytics();
|
|
227
|
+
assertUnsignedPolicyFlagIsSafe();
|
|
228
|
+
setBypassSecret(localSecret);
|
|
229
|
+
const policySigningSecret = process.env.MERGEN_POLICY_SIGNING_SECRET || localSecret;
|
|
230
|
+
if (!process.env.MERGEN_POLICY_SIGNING_SECRET) {
|
|
231
|
+
logger.warn(
|
|
232
|
+
"startup: MERGEN_POLICY_SIGNING_SECRET not set \u2014 enterprise-policy.json is signed with the same on-disk secret (~/.mergen/secret) used for admin API auth. Anything with local filesystem access can read that file and forge a valid policy signature. Set MERGEN_POLICY_SIGNING_SECRET to a value supplied only via the environment (never written to disk) for production/team deployments."
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
setPolicySigningSecret(policySigningSecret);
|
|
236
|
+
const agentTokenSecret = process.env.MERGEN_AGENT_TOKEN_SECRET || localSecret;
|
|
237
|
+
if (!process.env.MERGEN_AGENT_TOKEN_SECRET) {
|
|
238
|
+
logger.warn(
|
|
239
|
+
"startup: MERGEN_AGENT_TOKEN_SECRET not set \u2014 agent identity tokens are signed with the same on-disk secret (~/.mergen/secret) used for admin API auth. Set MERGEN_AGENT_TOKEN_SECRET to a value supplied only via the environment for production/team deployments."
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
setAgentTokenSecret(agentTokenSecret);
|
|
243
|
+
setBlunderHmacSecret(process.env.MERGEN_AUDIT_SECRET || localSecret);
|
|
244
|
+
if (!process.env.MERGEN_OIDC_SIGNING_KEY) {
|
|
245
|
+
logger.warn(
|
|
246
|
+
"startup: MERGEN_OIDC_SIGNING_KEY not set \u2014 the OIDC federation signing key (used for GCP/Azure ephemeral credential brokering) is generated once and persisted in plaintext at ~/.mergen/oidc-signing-key.json. Anything with local filesystem access can read it and mint its own valid federation tokens. Set MERGEN_OIDC_SIGNING_KEY to a PEM-encoded PKCS8 private key supplied only via the environment for production/team deployments using GCP or Azure credential brokering."
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
loadBypasses();
|
|
250
|
+
denyStaleHoldsOnStartup();
|
|
251
|
+
loadCommunityCorpus();
|
|
252
|
+
setBufferSizeGetter(() => getPlan(getActivePlanId()).bufferSize);
|
|
253
|
+
await loadPlugins();
|
|
254
|
+
setHeartbeatAlertFn((name, description) => {
|
|
255
|
+
const pid = randomUUID();
|
|
256
|
+
incidentStore.upsert(pid, {
|
|
257
|
+
status: "open",
|
|
258
|
+
hypothesis: `Heartbeat missed: ${name}`,
|
|
259
|
+
tag: "heartbeat_missed",
|
|
260
|
+
confidence: 1
|
|
261
|
+
});
|
|
262
|
+
void notify(pid, `\u23F0 *Heartbeat Missed* \u2014 \`${name}\`
|
|
263
|
+
${description}`, {
|
|
264
|
+
priority: "high",
|
|
265
|
+
tags: ["warning"]
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
startHeartbeatMonitor();
|
|
269
|
+
startGateHeartbeat();
|
|
270
|
+
startK8sEventsPoller();
|
|
271
|
+
const savedEvents = loadSession();
|
|
272
|
+
if (savedEvents && savedEvents.length > 0) {
|
|
273
|
+
store.rehydrate(savedEvents);
|
|
274
|
+
logger.info({ count: savedEvents.length }, "session rehydrated from disk");
|
|
275
|
+
}
|
|
276
|
+
if (process.env.MERGEN_REDIS_URL) {
|
|
277
|
+
const redisStore = await wrapWithRedisPersistence(store);
|
|
278
|
+
if (redisStore !== store) setStore(redisStore);
|
|
279
|
+
}
|
|
280
|
+
setInterval(() => saveSession(store.serialize()), 1e4).unref();
|
|
281
|
+
registerTeamBroadcaster(broadcastToTeam);
|
|
282
|
+
const port = await findPort(PORT_RANGE_START, PORT_RANGE_END);
|
|
283
|
+
const app = createApp({ serverVersion: SERVER_VERSION, localSecret, port, bindHost: BIND_HOST });
|
|
284
|
+
const tlsCert = process.env.MERGEN_TLS_CERT;
|
|
285
|
+
const tlsKey = process.env.MERGEN_TLS_KEY;
|
|
286
|
+
let httpServer;
|
|
287
|
+
if (tlsCert && tlsKey) {
|
|
288
|
+
try {
|
|
289
|
+
const cert = fs.readFileSync(tlsCert);
|
|
290
|
+
const key = fs.readFileSync(tlsKey);
|
|
291
|
+
httpServer = https.createServer({ cert, key }, app).listen(port, BIND_HOST, () => {
|
|
292
|
+
logger.info({ port, host: BIND_HOST }, `HTTPS ingest listening on https://${BIND_HOST}:${port}`);
|
|
293
|
+
});
|
|
294
|
+
} catch (err) {
|
|
295
|
+
if (BIND_HOST !== "127.0.0.1") {
|
|
296
|
+
logger.error(
|
|
297
|
+
{ err },
|
|
298
|
+
"TLS: failed to read cert/key files \u2014 refusing to start without TLS on a network-accessible interface. Fix the cert/key paths, or set MERGEN_BIND=127.0.0.1 for local-only mode."
|
|
299
|
+
);
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
logger.error({ err }, "TLS: failed to read cert/key files \u2014 falling back to plain HTTP (local-only mode, 127.0.0.1 only)");
|
|
303
|
+
httpServer = http.createServer(app).listen(port, BIND_HOST, () => {
|
|
304
|
+
logger.info({ port, host: BIND_HOST }, `HTTP ingest listening on http://${BIND_HOST}:${port}`);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
httpServer = app.listen(port, BIND_HOST, () => {
|
|
309
|
+
logger.info({ port, host: BIND_HOST }, `HTTP ingest listening on http://${BIND_HOST}:${port}`);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
if (await isPortAvailable(4318)) {
|
|
313
|
+
const otlpApp = createApp({ serverVersion: SERVER_VERSION, localSecret, port: 4318, bindHost: BIND_HOST });
|
|
314
|
+
otlpApp.listen(4318, BIND_HOST, () => {
|
|
315
|
+
logger.info("OTLP HTTP receiver listening on http://127.0.0.1:4318");
|
|
316
|
+
});
|
|
317
|
+
} else {
|
|
318
|
+
logger.warn("port 4318 in use \u2014 OTLP receiver not started on dedicated port (still available on main port)");
|
|
319
|
+
}
|
|
320
|
+
const mcp = new McpServer(
|
|
321
|
+
{ name: "mergen", version: SERVER_VERSION },
|
|
322
|
+
{ instructions: SYSTEM_PROMPT }
|
|
323
|
+
);
|
|
324
|
+
const guardedMcp = createGuardedServer(mcp, port);
|
|
325
|
+
registerTools(guardedMcp);
|
|
326
|
+
registerResources(mcp);
|
|
327
|
+
registerPrompts(mcp);
|
|
328
|
+
await assertGateCoversRegisteredTools(guardedMcp);
|
|
329
|
+
const transport = new StdioServerTransport();
|
|
330
|
+
await mcp.connect(transport);
|
|
331
|
+
logger.info("MCP server ready (stdio transport)");
|
|
332
|
+
checkForUpdates(SERVER_VERSION).then((latestVersion) => {
|
|
333
|
+
if (latestVersion) {
|
|
334
|
+
console.error(formatUpdateMessage(SERVER_VERSION, latestVersion));
|
|
335
|
+
}
|
|
336
|
+
}).catch(() => {
|
|
337
|
+
});
|
|
338
|
+
const telemetryTick = () => {
|
|
339
|
+
void maybeSendTelemetry({
|
|
340
|
+
serverVersion: SERVER_VERSION,
|
|
341
|
+
nodeVersion: process.versions.node.split(".")[0],
|
|
342
|
+
planId: getActivePlanId(),
|
|
343
|
+
toolCallCounts,
|
|
344
|
+
bufferedEvents: store.size()
|
|
345
|
+
});
|
|
346
|
+
};
|
|
347
|
+
setTimeout(telemetryTick, 6e4).unref();
|
|
348
|
+
setInterval(telemetryTick, 60 * 60 * 1e3).unref();
|
|
349
|
+
if (isCorpusSeeded()) {
|
|
350
|
+
const realCount = getRealVerdictCount();
|
|
351
|
+
console.error(
|
|
352
|
+
`
|
|
353
|
+
\u26A0 CALIBRATION WARM-UP \u2014 Running on synthetic priors (${realCount} real verdicts recorded).
|
|
354
|
+
Confidence scores will reflect your production environment after 10 real verdicts.
|
|
355
|
+
Record verdicts via POST /feedback or the Slack verdict buttons.
|
|
356
|
+
`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
startShadowDigestCron();
|
|
360
|
+
try {
|
|
361
|
+
const { startPolicySuggesterCron } = await import("./intelligence/policy-suggester.js");
|
|
362
|
+
startPolicySuggesterCron();
|
|
363
|
+
} catch (err) {
|
|
364
|
+
logger.error({ err }, "startup: policy suggester cron failed to start");
|
|
365
|
+
}
|
|
366
|
+
if (process.env.MERGEN_SLACK_DIGEST === "true") {
|
|
367
|
+
try {
|
|
368
|
+
startSlackDailyDigest();
|
|
369
|
+
} catch (err) {
|
|
370
|
+
logger.error({ err }, "startup: startSlackDailyDigest failed \u2014 Slack digest disabled");
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (process.env.MERGEN_GIT_ADR_SYNC === "true") {
|
|
374
|
+
try {
|
|
375
|
+
startGitAdrSync();
|
|
376
|
+
} catch (err) {
|
|
377
|
+
logger.error({ err }, "startup: startGitAdrSync failed \u2014 ADR sync disabled");
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (process.env.MERGEN_POLICY_URL) {
|
|
381
|
+
try {
|
|
382
|
+
startPolicySync({
|
|
383
|
+
url: process.env.MERGEN_POLICY_URL,
|
|
384
|
+
mergeMode: process.env.MERGEN_POLICY_MERGE ?? "replace"
|
|
385
|
+
});
|
|
386
|
+
} catch (err) {
|
|
387
|
+
logger.error({ err }, "startup: startPolicySync failed \u2014 remote policy sync disabled");
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (process.env.MERGEN_SLACK_OVERRIDE_LOOP === "true") {
|
|
391
|
+
try {
|
|
392
|
+
startSlackOverrideLoop();
|
|
393
|
+
} catch (err) {
|
|
394
|
+
logger.error({ err }, "startup: startSlackOverrideLoop failed \u2014 override loop disabled");
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (process.env.MERGEN_AUTO_CORPUS_PROPOSE !== "false") {
|
|
398
|
+
const { proposeRulesFromCorpus } = await import("./intelligence/corpus-to-policy.js");
|
|
399
|
+
const runProposalScan = () => {
|
|
400
|
+
try {
|
|
401
|
+
proposeRulesFromCorpus();
|
|
402
|
+
} catch (err) {
|
|
403
|
+
logger.error({ err }, "corpus-propose: scan failed");
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
runProposalScan();
|
|
407
|
+
setInterval(runProposalScan, 6 * 60 * 60 * 1e3).unref();
|
|
408
|
+
logger.info("startup: corpus\u2192proposal bridge enabled (default on; MERGEN_AUTO_CORPUS_PROPOSE=false to disable)");
|
|
409
|
+
}
|
|
410
|
+
try {
|
|
411
|
+
startDegradationWatcher();
|
|
412
|
+
} catch (err) {
|
|
413
|
+
logger.error({ err }, "startup: startDegradationWatcher failed \u2014 degradation watcher disabled");
|
|
414
|
+
}
|
|
415
|
+
if ((process.env.MERGEN_CLOUD_MODE === "true" || process.env.MERGEN_BULLMQ === "true") && process.env.MERGEN_REDIS_URL) {
|
|
416
|
+
const { startWorkers } = await import("./workers/worker-registry.js");
|
|
417
|
+
await startWorkers();
|
|
418
|
+
}
|
|
419
|
+
startWatcher();
|
|
420
|
+
if (process.env.MERGEN_DOCKER_MONITOR === "true") startDockerMonitor();
|
|
421
|
+
if (process.env.MERGEN_DOCKER_LOGS === "true") void startDockerLogStream();
|
|
422
|
+
startHeapMonitor();
|
|
423
|
+
if (process.env.MERGEN_AUTO_WATCH !== "false") {
|
|
424
|
+
const net2 = await import("net");
|
|
425
|
+
const DEV_PORTS = [8080, 8e3, 3001, 4e3, 5e3, 8888, 9e3];
|
|
426
|
+
const portsWithServices = await Promise.all(
|
|
427
|
+
DEV_PORTS.map(
|
|
428
|
+
(p) => new Promise((resolve) => {
|
|
429
|
+
const s = new net2.Socket();
|
|
430
|
+
s.setTimeout(200);
|
|
431
|
+
s.once("connect", () => {
|
|
432
|
+
s.destroy();
|
|
433
|
+
resolve(p);
|
|
434
|
+
});
|
|
435
|
+
s.once("timeout", () => {
|
|
436
|
+
s.destroy();
|
|
437
|
+
resolve(null);
|
|
438
|
+
});
|
|
439
|
+
s.once("error", () => {
|
|
440
|
+
s.destroy();
|
|
441
|
+
resolve(null);
|
|
442
|
+
});
|
|
443
|
+
s.connect(p, "127.0.0.1");
|
|
444
|
+
})
|
|
445
|
+
)
|
|
446
|
+
);
|
|
447
|
+
const runningPorts = portsWithServices.filter((p) => p !== null);
|
|
448
|
+
if (runningPorts.length > 0) {
|
|
449
|
+
logger.info({ ports: runningPorts }, "auto-watch: detected dev servers");
|
|
450
|
+
const autoAttach = process.env.MERGEN_AUTO_ATTACH_PORTS;
|
|
451
|
+
if (autoAttach) {
|
|
452
|
+
const { startProcessWatcher } = await import("./sensor/process-watcher.js");
|
|
453
|
+
for (const portStr of autoAttach.split(",")) {
|
|
454
|
+
const p = parseInt(portStr.trim(), 10);
|
|
455
|
+
if (runningPorts.includes(p)) {
|
|
456
|
+
logger.info({ port: p }, `auto-watch: attaching to port ${p}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
} else {
|
|
460
|
+
logger.info(
|
|
461
|
+
{ ports: runningPorts, hint: `Run: mergen-server watch <your-start-command>` },
|
|
462
|
+
'auto-watch: dev servers detected \u2014 run "mergen-server watch <cmd>" to stream their logs'
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function shutdown(signal) {
|
|
468
|
+
logger.info({ signal }, "shutting down");
|
|
469
|
+
const events = store.serialize();
|
|
470
|
+
saveSessionToHistory(events, `shutdown-${signal.toLowerCase()}`);
|
|
471
|
+
saveSession(events);
|
|
472
|
+
serviceGraph.flushSync();
|
|
473
|
+
flushApprovals();
|
|
474
|
+
persistBypasses();
|
|
475
|
+
actionLedger.flush();
|
|
476
|
+
Promise.all([flushOverageOnShutdown(), flushPendingRebuild()]).finally(() => {
|
|
477
|
+
const workerShutdown = (process.env.MERGEN_CLOUD_MODE === "true" || process.env.MERGEN_BULLMQ === "true") && process.env.MERGEN_REDIS_URL ? import("./workers/worker-registry.js").then(({ stopWorkers }) => stopWorkers()).catch(() => {
|
|
478
|
+
}) : Promise.resolve();
|
|
479
|
+
const pgShutdown = process.env.MERGEN_CLOUD_MODE === "true" ? import("./storage/pg/pg-client.js").then(({ closeSql }) => closeSql()).catch(() => {
|
|
480
|
+
}) : Promise.resolve();
|
|
481
|
+
void workerShutdown.finally(() => void pgShutdown.finally(() => {
|
|
482
|
+
stopDockerMonitor();
|
|
483
|
+
stopDockerLogStream();
|
|
484
|
+
stopAllProcessWatchers();
|
|
485
|
+
stopFileWatch();
|
|
486
|
+
stopRedisStore();
|
|
487
|
+
httpServer.close(() => {
|
|
488
|
+
logger.info("HTTP server closed");
|
|
489
|
+
process.exit(0);
|
|
490
|
+
});
|
|
491
|
+
}));
|
|
492
|
+
const _hardExit = setTimeout(() => {
|
|
493
|
+
logger.error("shutdown: graceful shutdown timed out after 30s \u2014 forcing exit");
|
|
494
|
+
process.exit(1);
|
|
495
|
+
}, 3e4);
|
|
496
|
+
_hardExit.unref();
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
500
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
501
|
+
}
|
|
502
|
+
main().catch((err) => {
|
|
503
|
+
logger.error({ err }, "fatal startup error");
|
|
504
|
+
process.exit(1);
|
|
505
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const RESTART_PATTERNS = [
|
|
2
|
+
/\bpm2\s+(restart|reload|gracefulReload)\b/i,
|
|
3
|
+
/\bkubectl\s+rollout\s+restart\b/i,
|
|
4
|
+
/\bsystemctl\s+(restart|reload|try-restart)\b/i,
|
|
5
|
+
/\bservice\s+\S+\s+(restart|reload)\b/i,
|
|
6
|
+
/\bdocker\s+(restart|stop\s+.*&&\s*docker\s+start)\b/i,
|
|
7
|
+
/\bsupervisorctl\s+(restart|reload|reread|update)\b/i,
|
|
8
|
+
/\bnginx\s+-s\s+(reload|reopen)\b/i,
|
|
9
|
+
/\bapache2ctl\s+graceful\b/i,
|
|
10
|
+
/\bkill\s+-(?:HUP|1)\b/i,
|
|
11
|
+
/\bpkill\s+-(?:HUP|1)\b/i,
|
|
12
|
+
/\bheroku\s+ps:restart\b/i,
|
|
13
|
+
/\bflyctl\s+machine\s+restart\b/i,
|
|
14
|
+
/\brailway\s+redeploy\b/i,
|
|
15
|
+
/\bgunicorn\s+.*--reload\b/i,
|
|
16
|
+
/\bunicorn_rails\s+.*-s\s+HUP\b/i
|
|
17
|
+
];
|
|
18
|
+
const DEPLOY_PATTERNS = [
|
|
19
|
+
...RESTART_PATTERNS,
|
|
20
|
+
/\bkubectl\s+rollout\s+undo\b/i,
|
|
21
|
+
/\bkubectl\s+set\s+image\b/i,
|
|
22
|
+
/\bkubectl\s+set\s+env\b/i,
|
|
23
|
+
/\bhelm\s+(upgrade|rollback)\b/i,
|
|
24
|
+
/\bnpm\s+install\s+\S+@\S+/i,
|
|
25
|
+
/\bpip\s+install\s+\S+==\S+/i,
|
|
26
|
+
/\bpip\s+install\s+--upgrade\s+\S+/i,
|
|
27
|
+
/\byarn\s+add\s+\S+@\S+/i,
|
|
28
|
+
/\bgem\s+install\s+\S+\s+-v\s+\S+/i,
|
|
29
|
+
/\bgit\s+checkout\s+\S+\s+--\s+\S+/i,
|
|
30
|
+
/\bgit\s+revert\s+[a-f0-9]{7}/i,
|
|
31
|
+
/\baws\s+ecs\s+update-service\b/i,
|
|
32
|
+
/\bgcloud\s+(app|run)\s+deploy\b/i,
|
|
33
|
+
/\bgc\s+(app|run)\s+deploy\b/i,
|
|
34
|
+
/\baz\s+(webapp|containerapp)\s+(restart|update)\b/i,
|
|
35
|
+
/\bdocker-compose\s+(restart|up\s+--force-recreate)\b/i
|
|
36
|
+
];
|
|
37
|
+
const RISK_TIER_ORDER = {
|
|
38
|
+
restart: 0,
|
|
39
|
+
deploy: 1,
|
|
40
|
+
full: 2
|
|
41
|
+
};
|
|
42
|
+
const LEVEL_ORDER = {
|
|
43
|
+
restarts: 0,
|
|
44
|
+
deploys: 1,
|
|
45
|
+
full: 2
|
|
46
|
+
};
|
|
47
|
+
function classifyCommandRisk(command) {
|
|
48
|
+
if (RESTART_PATTERNS.some((p) => p.test(command))) return "restart";
|
|
49
|
+
if (DEPLOY_PATTERNS.some((p) => p.test(command))) return "deploy";
|
|
50
|
+
return "full";
|
|
51
|
+
}
|
|
52
|
+
function autopilotLevelPermits(command, level) {
|
|
53
|
+
const risk = classifyCommandRisk(command);
|
|
54
|
+
return RISK_TIER_ORDER[risk] <= LEVEL_ORDER[level];
|
|
55
|
+
}
|
|
56
|
+
function getAutopilotLevel() {
|
|
57
|
+
const raw = process.env.MERGEN_AUTOPILOT_LEVEL;
|
|
58
|
+
if (raw === "restarts" || raw === "deploys" || raw === "full") return raw;
|
|
59
|
+
return "full";
|
|
60
|
+
}
|
|
61
|
+
function autopilotLevelDescription(level) {
|
|
62
|
+
switch (level) {
|
|
63
|
+
case "restarts":
|
|
64
|
+
return "service restarts and reloads only";
|
|
65
|
+
case "deploys":
|
|
66
|
+
return "restarts, rollbacks, and dependency pins";
|
|
67
|
+
case "full":
|
|
68
|
+
return "all commands";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function analyzeSemanticRisk(command, context) {
|
|
72
|
+
const lower = command.toLowerCase();
|
|
73
|
+
const isDBPoolResize = (lower.includes("pool") || lower.includes("max_connections") || lower.includes("db_pool")) && (lower.includes("set") || lower.includes("env") || lower.includes("export") || lower.includes("scale"));
|
|
74
|
+
if (isDBPoolResize) {
|
|
75
|
+
const day = context?.service_time?.dayOfWeek ?? (/* @__PURE__ */ new Date()).getUTCDay();
|
|
76
|
+
const hour = context?.service_time?.hourOfDay ?? (/* @__PURE__ */ new Date()).getUTCHours();
|
|
77
|
+
if (day === 5 && hour >= 20 && hour <= 24) {
|
|
78
|
+
return {
|
|
79
|
+
risk: "high",
|
|
80
|
+
blocked: true,
|
|
81
|
+
reason: "Semantic Safety: Database pool resize is unsafe during Friday 20-24 UTC settlement window."
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const isDestructiveSql = (lower.includes("drop") || lower.includes("truncate")) && (lower.includes("table") || lower.includes("database") || lower.includes("schema"));
|
|
86
|
+
const isDestructiveFs = lower.includes("rm -rf") && lower.includes("/");
|
|
87
|
+
const isDestructiveInfra = /terraform\s+destroy|kubectl\s+delete\s+(namespace|cluster)|aws.*delete-cluster/i.test(lower);
|
|
88
|
+
if (isDestructiveSql || isDestructiveFs || isDestructiveInfra) {
|
|
89
|
+
let reason;
|
|
90
|
+
if (isDestructiveSql) {
|
|
91
|
+
const op = lower.includes("truncate") ? "TRUNCATE" : "DROP";
|
|
92
|
+
reason = `Blocked: \`${op} TABLE/DATABASE\` is irreversible. Use a reversible migration instead \u2014 rename the table (\`ALTER TABLE ... RENAME TO ..._deprecated\`) or add a soft-delete column, then drop in a separate PR after verifying no references remain.`;
|
|
93
|
+
} else if (isDestructiveFs) {
|
|
94
|
+
const match = command.match(/rm\s+-rf\s+(\S+)/i);
|
|
95
|
+
const path = match?.[1] ?? "/...";
|
|
96
|
+
reason = `Blocked: \`rm -rf ${path}\` permanently deletes files. Move to a timestamped archive path first (\`mv ${path} ${path}.bak-$(date +%s)\`), verify the archive is correct, then delete.`;
|
|
97
|
+
} else {
|
|
98
|
+
const match = command.match(/terraform\s+destroy|kubectl\s+delete\s+\S+|aws\s+\S+delete-cluster\S*/i);
|
|
99
|
+
const cmd = match?.[0] ?? "infra destroy command";
|
|
100
|
+
reason = `Blocked: \`${cmd}\` destroys live infrastructure. Use \`terraform plan -destroy\` first to preview blast radius, then require explicit human approval via HITL before executing.`;
|
|
101
|
+
}
|
|
102
|
+
return { risk: "high", blocked: true, reason };
|
|
103
|
+
}
|
|
104
|
+
const isScale = lower.includes("scale") || lower.includes("replicas") || lower.includes("resize");
|
|
105
|
+
if (isScale) {
|
|
106
|
+
return {
|
|
107
|
+
risk: "medium",
|
|
108
|
+
blocked: false,
|
|
109
|
+
reason: "Semantic Safety: Service scaling in progress."
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const tier = classifyCommandRisk(command);
|
|
113
|
+
if (tier === "deploy") {
|
|
114
|
+
return {
|
|
115
|
+
risk: "medium",
|
|
116
|
+
blocked: false,
|
|
117
|
+
reason: null
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
risk: "low",
|
|
122
|
+
blocked: false,
|
|
123
|
+
reason: null
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export {
|
|
127
|
+
analyzeSemanticRisk,
|
|
128
|
+
autopilotLevelDescription,
|
|
129
|
+
autopilotLevelPermits,
|
|
130
|
+
classifyCommandRisk,
|
|
131
|
+
getAutopilotLevel
|
|
132
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
const FEED_SIZE = 200;
|
|
3
|
+
const _feed = [];
|
|
4
|
+
const _sseClients = /* @__PURE__ */ new Set();
|
|
5
|
+
function recordActivity(event) {
|
|
6
|
+
const full = { id: randomUUID(), timestamp: Date.now(), ...event };
|
|
7
|
+
_feed.push(full);
|
|
8
|
+
if (_feed.length > FEED_SIZE) _feed.shift();
|
|
9
|
+
for (const res of _sseClients) {
|
|
10
|
+
try {
|
|
11
|
+
res.write(`data: ${JSON.stringify(full)}
|
|
12
|
+
|
|
13
|
+
`);
|
|
14
|
+
} catch {
|
|
15
|
+
_sseClients.delete(res);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function getRecentActivity(limit = 50) {
|
|
20
|
+
return _feed.slice(-Math.min(limit, FEED_SIZE)).reverse();
|
|
21
|
+
}
|
|
22
|
+
function subscribeToActivity(res) {
|
|
23
|
+
_sseClients.add(res);
|
|
24
|
+
const recent = _feed.slice(-20);
|
|
25
|
+
for (const ev of recent) {
|
|
26
|
+
try {
|
|
27
|
+
res.write(`data: ${JSON.stringify(ev)}
|
|
28
|
+
|
|
29
|
+
`);
|
|
30
|
+
} catch {
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return () => _sseClients.delete(res);
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
getRecentActivity,
|
|
38
|
+
recordActivity,
|
|
39
|
+
subscribeToActivity
|
|
40
|
+
};
|