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,708 @@
|
|
|
1
|
+
import { randomUUID, createHash } from "crypto";
|
|
2
|
+
import { hostname } from "os";
|
|
3
|
+
import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { evaluateEnterprisePolicy, loadEnterprisePolicy } from "./enterprise-policy-engine.js";
|
|
5
|
+
import { isPolicyDryRun } from "./execution-mode.js";
|
|
6
|
+
import { getTraceId } from "./trace-context.js";
|
|
7
|
+
import { computeRiskScore, getRiskHoldThreshold } from "./risk-score.js";
|
|
8
|
+
import { verifyNoExecve } from "./ebpf-verify.js";
|
|
9
|
+
import { findSimilarIncidents } from "./incident-similarity.js";
|
|
10
|
+
import { parseShellCommand } from "./shell-ast.js";
|
|
11
|
+
import { detectScriptInvocation, detectScriptInvocationFromRawText, evaluateScriptTrust } from "./script-trust.js";
|
|
12
|
+
import { computeBlastRadius, mostSevereBlast } from "./blast-radius.js";
|
|
13
|
+
import { getStores } from "../storage/store-registry.js";
|
|
14
|
+
import {
|
|
15
|
+
recordGateBlock,
|
|
16
|
+
recordGatePass,
|
|
17
|
+
recordGateCoverage,
|
|
18
|
+
recordGateEvent,
|
|
19
|
+
recordHitlHold
|
|
20
|
+
} from "./gate-analytics.js";
|
|
21
|
+
import { trackBlock, trackSuccessfulCall } from "../sensor/bypass-tracker.js";
|
|
22
|
+
import { recordActivity } from "./activity-feed.js";
|
|
23
|
+
import { checkAgentProfile } from "./agent-profiles.js";
|
|
24
|
+
import { resolveAgentIdentity } from "./agent-identity.js";
|
|
25
|
+
import {
|
|
26
|
+
detectSequenceThreat,
|
|
27
|
+
recordSessionCall,
|
|
28
|
+
markContaminated,
|
|
29
|
+
isSessionContaminated,
|
|
30
|
+
getContaminationSource,
|
|
31
|
+
updateAgentReputation,
|
|
32
|
+
getAgentScrutinyTier,
|
|
33
|
+
READ_ONLY_TOOLS
|
|
34
|
+
} from "./session-threat-tracker.js";
|
|
35
|
+
import logger from "../sensor/logger.js";
|
|
36
|
+
import { assertGateHeartbeatFresh } from "../sensor/gate-heartbeat.js";
|
|
37
|
+
import { registerBypassBlock, checkAndConsumeBypass } from "./bypass.js";
|
|
38
|
+
import { holdToolCall } from "./hitl-hold.js";
|
|
39
|
+
import { detectInjection, extractAllStrings, getSuggestedAlternative } from "./gate-decision.js";
|
|
40
|
+
import {
|
|
41
|
+
setBypassSecret,
|
|
42
|
+
registerBypassBlock as registerBypassBlock2,
|
|
43
|
+
approveBypass,
|
|
44
|
+
checkAndConsumeBypass as checkAndConsumeBypass2,
|
|
45
|
+
getPendingBypasses,
|
|
46
|
+
getPendingBypassDetail,
|
|
47
|
+
invalidateBypassToken,
|
|
48
|
+
persistBypasses,
|
|
49
|
+
loadBypasses
|
|
50
|
+
} from "./bypass.js";
|
|
51
|
+
import {
|
|
52
|
+
denyStaleHoldsOnStartup,
|
|
53
|
+
approveToolCall,
|
|
54
|
+
approveToolCallConstrained,
|
|
55
|
+
denyToolCall,
|
|
56
|
+
getPendingHolds
|
|
57
|
+
} from "./hitl-hold.js";
|
|
58
|
+
let _warnedNoAgentId = false;
|
|
59
|
+
function _resolveAgentId() {
|
|
60
|
+
const { agentId } = resolveAgentIdentity();
|
|
61
|
+
if (agentId && agentId !== "agent") return agentId;
|
|
62
|
+
const fp = createHash("sha256").update(`${hostname()}:${process.ppid ?? 0}`).digest("hex").slice(0, 8);
|
|
63
|
+
if (!_warnedNoAgentId) {
|
|
64
|
+
logger.info({ fingerprint: `env_${fp}` }, "tool-guard: no agent identity configured \u2014 using process fingerprint for reputation tracking");
|
|
65
|
+
_warnedNoAgentId = true;
|
|
66
|
+
}
|
|
67
|
+
return `env_${fp}`;
|
|
68
|
+
}
|
|
69
|
+
async function _checkScriptTrustForRaw(raw) {
|
|
70
|
+
const parsed = parseShellCommand(raw);
|
|
71
|
+
if (!parsed) {
|
|
72
|
+
const heuristic = detectScriptInvocationFromRawText(raw);
|
|
73
|
+
if (!heuristic) return { escalate: false };
|
|
74
|
+
const trust = await evaluateScriptTrust(heuristic.scriptPath, process.cwd());
|
|
75
|
+
if (!trust.trusted) {
|
|
76
|
+
return {
|
|
77
|
+
escalate: true,
|
|
78
|
+
reason: `Script invocation \`${heuristic.interpreter} ${heuristic.scriptPath}\` (matched via raw-text fallback \u2014 command could not be fully structurally parsed) is not trusted: ${trust.reasons.join("; ")}`
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return { escalate: false };
|
|
82
|
+
}
|
|
83
|
+
for (const cmd of parsed) {
|
|
84
|
+
const invocation = detectScriptInvocation(cmd);
|
|
85
|
+
if (!invocation) continue;
|
|
86
|
+
const trust = await evaluateScriptTrust(invocation.scriptPath, process.cwd());
|
|
87
|
+
if (!trust.trusted) {
|
|
88
|
+
return {
|
|
89
|
+
escalate: true,
|
|
90
|
+
reason: `Script invocation \`${invocation.interpreter} ${invocation.scriptPath}\` is not trusted: ${trust.reasons.join("; ")}`
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { escalate: false };
|
|
95
|
+
}
|
|
96
|
+
async function _checkScriptTrustEscalation(argStrings, toolName) {
|
|
97
|
+
const candidates = argStrings.filter((s) => s && s.length > 0);
|
|
98
|
+
const toScan = candidates.length > 0 ? candidates : [toolName];
|
|
99
|
+
for (const raw of toScan) {
|
|
100
|
+
const res = await _checkScriptTrustForRaw(raw);
|
|
101
|
+
if (res.escalate) return res;
|
|
102
|
+
}
|
|
103
|
+
return { escalate: false };
|
|
104
|
+
}
|
|
105
|
+
async function applyGate(toolName, args, next, port, opts) {
|
|
106
|
+
try {
|
|
107
|
+
return await applyGateInner(toolName, args, next, port, opts);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
logger.error({ err, toolName }, "tool-guard: applyGate threw an unexpected error \u2014 failing closed");
|
|
110
|
+
await getStores().blunders.record({
|
|
111
|
+
blunderType: "pipeline_block",
|
|
112
|
+
command: toolName,
|
|
113
|
+
blockReason: `Gate threw an unexpected error and failed closed: ${err instanceof Error ? err.message : String(err)}`,
|
|
114
|
+
service: "mcp",
|
|
115
|
+
tag: "gate_internal_error",
|
|
116
|
+
actor: "agent",
|
|
117
|
+
pid: null,
|
|
118
|
+
confidenceScore: null
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
content: [{
|
|
122
|
+
type: "text",
|
|
123
|
+
text: [
|
|
124
|
+
`\u{1F6AB} **Mergen fail-closed: the local execution gate hit an internal error while evaluating this call.**`,
|
|
125
|
+
``,
|
|
126
|
+
`**Tool:** \`${toolName}\``,
|
|
127
|
+
``,
|
|
128
|
+
`_The tool call was not executed. This event has been logged to the Agent Blunder Log._`
|
|
129
|
+
].join("\n")
|
|
130
|
+
}],
|
|
131
|
+
isError: true
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function applyGateInner(toolName, args, next, port, opts) {
|
|
136
|
+
const t0 = Date.now();
|
|
137
|
+
const heartbeat = assertGateHeartbeatFresh();
|
|
138
|
+
if (!heartbeat.ok) {
|
|
139
|
+
recordGateBlock(["gate_heartbeat_stale"]);
|
|
140
|
+
await getStores().blunders.record({
|
|
141
|
+
blunderType: "pipeline_block",
|
|
142
|
+
command: toolName,
|
|
143
|
+
blockReason: `Gate heartbeat fail-closed: ${heartbeat.reason}`,
|
|
144
|
+
service: "mcp",
|
|
145
|
+
tag: "gate_heartbeat",
|
|
146
|
+
actor: "agent",
|
|
147
|
+
pid: null,
|
|
148
|
+
confidenceScore: null,
|
|
149
|
+
triggeredRules: ["gate_heartbeat_stale"]
|
|
150
|
+
});
|
|
151
|
+
logger.error({ toolName, reason: heartbeat.reason }, "tool-guard: gate heartbeat stale \u2014 failing closed");
|
|
152
|
+
return {
|
|
153
|
+
content: [{
|
|
154
|
+
type: "text",
|
|
155
|
+
text: [
|
|
156
|
+
`\u{1F6AB} **Mergen fail-closed: local execution gate heartbeat is not fresh.**`,
|
|
157
|
+
``,
|
|
158
|
+
`**Tool:** \`${toolName}\``,
|
|
159
|
+
`**Why:** ${heartbeat.reason}`,
|
|
160
|
+
``,
|
|
161
|
+
`_Restart Mergen or restore the gate heartbeat before executing agent actions._`
|
|
162
|
+
].join("\n")
|
|
163
|
+
}],
|
|
164
|
+
isError: true
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const argsSnapshot = JSON.stringify(args ?? {});
|
|
168
|
+
const argsObj = args && typeof args === "object" ? args : {};
|
|
169
|
+
const commandArg = argsObj.command ?? argsObj.cmd ?? argsObj.fix ?? "";
|
|
170
|
+
const allArgStrings = extractAllStrings(args);
|
|
171
|
+
const agentId = _resolveAgentId();
|
|
172
|
+
const sessionId = agentId;
|
|
173
|
+
const identity = opts?.verifiedAgentId ? void 0 : resolveAgentIdentity();
|
|
174
|
+
const verifiedAgentId = opts?.verifiedAgentId ?? (identity?.verified ? identity.agentId : void 0);
|
|
175
|
+
const agentIdLabel = opts?.verifiedAgentId ?? identity?.agentId ?? void 0;
|
|
176
|
+
const parentAgentId = opts?.verifiedAgentId ? opts?.verifiedParentAgentId : identity?.parentAgentId;
|
|
177
|
+
const traceId = getTraceId();
|
|
178
|
+
const { threat: sequenceThreat, label: sequenceLabel } = detectSequenceThreat(sessionId, commandArg || argsSnapshot.slice(0, 200));
|
|
179
|
+
if (sequenceThreat) {
|
|
180
|
+
updateAgentReputation(agentId, "sequence");
|
|
181
|
+
recordGateBlock(["sequence_threat"]);
|
|
182
|
+
await getStores().blunders.record({
|
|
183
|
+
blunderType: "pipeline_block",
|
|
184
|
+
command: toolName,
|
|
185
|
+
blockReason: `Multi-turn threat sequence detected: ${sequenceLabel}`,
|
|
186
|
+
service: "mcp",
|
|
187
|
+
tag: "sequence_threat",
|
|
188
|
+
actor: agentId,
|
|
189
|
+
pid: null,
|
|
190
|
+
confidenceScore: null,
|
|
191
|
+
triggeredRules: ["sequence_threat"]
|
|
192
|
+
});
|
|
193
|
+
recordSessionCall(sessionId, toolName, commandArg, "BLOCK");
|
|
194
|
+
recordActivity({ toolName, commandArg, verdict: "BLOCK", triggeredRules: ["sequence_threat"], ruleNames: ["Multi-Turn Threat Sequence"] });
|
|
195
|
+
recordGateEvent({
|
|
196
|
+
ts: Date.now(),
|
|
197
|
+
toolName,
|
|
198
|
+
command: commandArg || null,
|
|
199
|
+
actor: "agent",
|
|
200
|
+
agentId: agentIdLabel ?? null,
|
|
201
|
+
service: "mcp",
|
|
202
|
+
environment: process.env.MERGEN_ENVIRONMENT ?? null,
|
|
203
|
+
verdict: "block",
|
|
204
|
+
triggeredRules: ["sequence_threat"],
|
|
205
|
+
guidedAlternative: null,
|
|
206
|
+
reason: `Multi-turn threat sequence detected: ${sequenceLabel}`,
|
|
207
|
+
traceId
|
|
208
|
+
});
|
|
209
|
+
return {
|
|
210
|
+
content: [{
|
|
211
|
+
type: "text",
|
|
212
|
+
text: [
|
|
213
|
+
`\u{1F6AB} **Mergen blocked this tool call: multi-turn threat sequence detected.**`,
|
|
214
|
+
``,
|
|
215
|
+
`**Tool:** \`${toolName}\``,
|
|
216
|
+
`**Pattern:** \`${sequenceLabel}\``,
|
|
217
|
+
`**Why:** The sequence of recent tool calls matches a known multi-step attack chain.`,
|
|
218
|
+
``,
|
|
219
|
+
`_This event has been logged to the Agent Blunder Log._`
|
|
220
|
+
].join("\n")
|
|
221
|
+
}],
|
|
222
|
+
isError: true
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
const injectionMatch = detectInjection(argsSnapshot);
|
|
226
|
+
if (injectionMatch) {
|
|
227
|
+
updateAgentReputation(agentId, "injection");
|
|
228
|
+
markContaminated(sessionId, injectionMatch, 5);
|
|
229
|
+
recordGateBlock(["injection_attempt"]);
|
|
230
|
+
await getStores().blunders.record({
|
|
231
|
+
blunderType: "injection_attempt",
|
|
232
|
+
command: toolName,
|
|
233
|
+
blockReason: `Prompt injection pattern detected in tool arguments: ${injectionMatch}`,
|
|
234
|
+
service: "mcp",
|
|
235
|
+
tag: "injection",
|
|
236
|
+
actor: agentId,
|
|
237
|
+
pid: null,
|
|
238
|
+
confidenceScore: null,
|
|
239
|
+
triggeredRules: ["injection_attempt"]
|
|
240
|
+
});
|
|
241
|
+
logger.warn({ toolName, injectionMatch }, "tool-guard: prompt injection attempt detected in args");
|
|
242
|
+
recordSessionCall(sessionId, toolName, commandArg, "BLOCK");
|
|
243
|
+
recordActivity({ toolName, commandArg, verdict: "BLOCK", triggeredRules: ["injection_attempt"], ruleNames: ["Prompt Injection"] });
|
|
244
|
+
recordGateEvent({
|
|
245
|
+
ts: Date.now(),
|
|
246
|
+
toolName,
|
|
247
|
+
command: commandArg || null,
|
|
248
|
+
actor: "agent",
|
|
249
|
+
agentId: agentIdLabel ?? null,
|
|
250
|
+
service: "mcp",
|
|
251
|
+
environment: process.env.MERGEN_ENVIRONMENT ?? null,
|
|
252
|
+
verdict: "block",
|
|
253
|
+
triggeredRules: ["injection_attempt"],
|
|
254
|
+
guidedAlternative: null,
|
|
255
|
+
reason: `Prompt injection pattern detected in tool arguments: ${injectionMatch}`,
|
|
256
|
+
traceId
|
|
257
|
+
});
|
|
258
|
+
return {
|
|
259
|
+
content: [{
|
|
260
|
+
type: "text",
|
|
261
|
+
text: `\u{1F6AB} **Mergen blocked this tool call: prompt injection pattern detected.**
|
|
262
|
+
|
|
263
|
+
**Tool:** \`${toolName}\`
|
|
264
|
+
|
|
265
|
+
_This event has been logged to the Agent Blunder Log._`
|
|
266
|
+
}],
|
|
267
|
+
isError: true
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
if (checkAndConsumeBypass(toolName, commandArg)) {
|
|
271
|
+
const hardEval = evaluateEnterprisePolicy({
|
|
272
|
+
files: [toolName],
|
|
273
|
+
commands: [toolName, ...allArgStrings],
|
|
274
|
+
actor: "agent",
|
|
275
|
+
service: "mcp",
|
|
276
|
+
timestamp: Date.now(),
|
|
277
|
+
environment: process.env.MERGEN_ENVIRONMENT ?? void 0,
|
|
278
|
+
repo: process.env.MERGEN_REPO ?? void 0,
|
|
279
|
+
agentId: verifiedAgentId
|
|
280
|
+
});
|
|
281
|
+
if (hardEval.verdict === "block") {
|
|
282
|
+
const hardReason = hardEval.reasons.join("; ");
|
|
283
|
+
recordGateBlock(hardEval.triggeredRules);
|
|
284
|
+
await getStores().blunders.record({
|
|
285
|
+
blunderType: "pipeline_block",
|
|
286
|
+
command: toolName,
|
|
287
|
+
blockReason: `Hard policy block \u2014 bypass cannot override: ${hardReason}`,
|
|
288
|
+
service: "mcp",
|
|
289
|
+
tag: "tool_guard",
|
|
290
|
+
actor: "agent",
|
|
291
|
+
pid: null,
|
|
292
|
+
confidenceScore: null,
|
|
293
|
+
triggeredRules: hardEval.triggeredRules
|
|
294
|
+
});
|
|
295
|
+
logger.warn({ toolName, reason: hardReason }, "tool-guard: hard block rule rejected bypass approval");
|
|
296
|
+
recordGateEvent({
|
|
297
|
+
ts: Date.now(),
|
|
298
|
+
toolName,
|
|
299
|
+
command: commandArg || null,
|
|
300
|
+
actor: "agent",
|
|
301
|
+
agentId: agentIdLabel ?? null,
|
|
302
|
+
service: "mcp",
|
|
303
|
+
environment: process.env.MERGEN_ENVIRONMENT ?? null,
|
|
304
|
+
verdict: "block",
|
|
305
|
+
triggeredRules: hardEval.triggeredRules,
|
|
306
|
+
guidedAlternative: null,
|
|
307
|
+
reason: `Hard policy block \u2014 bypass cannot override: ${hardReason}`,
|
|
308
|
+
traceId
|
|
309
|
+
});
|
|
310
|
+
return {
|
|
311
|
+
content: [{
|
|
312
|
+
type: "text",
|
|
313
|
+
text: [
|
|
314
|
+
`\u{1F6AB} **Hard policy block \u2014 this action cannot be bypassed by operator approval.**`,
|
|
315
|
+
``,
|
|
316
|
+
`**Tool:** \`${toolName}\``,
|
|
317
|
+
`**Why:** ${hardReason}`,
|
|
318
|
+
``,
|
|
319
|
+
`_Hard blocks are immutable safety guardrails. Modify \`~/.mergen/enterprise-policy.json\` to change the rule itself._`
|
|
320
|
+
].join("\n")
|
|
321
|
+
}],
|
|
322
|
+
isError: true
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
logger.info({ toolName, commandArg }, "tool-guard: approved bypass consumed, tool call passing");
|
|
326
|
+
recordGatePass();
|
|
327
|
+
trackSuccessfulCall(toolName);
|
|
328
|
+
recordGateEvent({
|
|
329
|
+
ts: Date.now(),
|
|
330
|
+
toolName,
|
|
331
|
+
command: commandArg || null,
|
|
332
|
+
actor: "agent",
|
|
333
|
+
agentId: agentIdLabel ?? null,
|
|
334
|
+
service: "mcp",
|
|
335
|
+
environment: process.env.MERGEN_ENVIRONMENT ?? null,
|
|
336
|
+
verdict: "pass",
|
|
337
|
+
triggeredRules: [],
|
|
338
|
+
guidedAlternative: null,
|
|
339
|
+
reason: "operator bypass approved",
|
|
340
|
+
traceId
|
|
341
|
+
});
|
|
342
|
+
return next();
|
|
343
|
+
}
|
|
344
|
+
const profileBlock = checkAgentProfile(toolName, void 0, verifiedAgentId);
|
|
345
|
+
if (profileBlock) {
|
|
346
|
+
recordGateBlock([]);
|
|
347
|
+
await getStores().blunders.record({
|
|
348
|
+
blunderType: "rbac_block",
|
|
349
|
+
command: toolName,
|
|
350
|
+
blockReason: profileBlock,
|
|
351
|
+
service: "mcp",
|
|
352
|
+
tag: "agent_profile",
|
|
353
|
+
actor: agentIdLabel ?? "agent",
|
|
354
|
+
pid: null,
|
|
355
|
+
confidenceScore: null,
|
|
356
|
+
triggeredRules: ["agent_profile"]
|
|
357
|
+
});
|
|
358
|
+
recordActivity({ toolName, commandArg, verdict: "BLOCK", triggeredRules: ["agent_profile"], ruleNames: ["Agent Profile Block"] });
|
|
359
|
+
recordGateEvent({
|
|
360
|
+
ts: Date.now(),
|
|
361
|
+
toolName,
|
|
362
|
+
command: commandArg || null,
|
|
363
|
+
actor: "agent",
|
|
364
|
+
agentId: agentIdLabel ?? null,
|
|
365
|
+
service: "mcp",
|
|
366
|
+
environment: process.env.MERGEN_ENVIRONMENT ?? null,
|
|
367
|
+
verdict: "block",
|
|
368
|
+
triggeredRules: ["agent_profile"],
|
|
369
|
+
guidedAlternative: null,
|
|
370
|
+
reason: profileBlock,
|
|
371
|
+
traceId
|
|
372
|
+
});
|
|
373
|
+
return {
|
|
374
|
+
content: [{
|
|
375
|
+
type: "text",
|
|
376
|
+
text: `\u{1F6AB} **Mergen agent profile gate blocked this tool call.**
|
|
377
|
+
|
|
378
|
+
**Tool:** \`${toolName}\`
|
|
379
|
+
**Why:** ${profileBlock}
|
|
380
|
+
|
|
381
|
+
_Adjust permissions at: mergen-server agent-update ${verifiedAgentId ?? ""}_`
|
|
382
|
+
}],
|
|
383
|
+
isError: true
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
const evaluation = evaluateEnterprisePolicy({
|
|
387
|
+
files: [toolName],
|
|
388
|
+
commands: [toolName, ...allArgStrings],
|
|
389
|
+
actor: "agent",
|
|
390
|
+
service: "mcp",
|
|
391
|
+
timestamp: Date.now(),
|
|
392
|
+
environment: process.env.MERGEN_ENVIRONMENT ?? void 0,
|
|
393
|
+
repo: process.env.MERGEN_REPO ?? void 0,
|
|
394
|
+
agentId: verifiedAgentId
|
|
395
|
+
});
|
|
396
|
+
const evalMs = Date.now() - t0;
|
|
397
|
+
if (evalMs > 10) {
|
|
398
|
+
logger.warn({ evalMs, toolName }, "tool-guard: policy evaluation exceeded 10ms target");
|
|
399
|
+
}
|
|
400
|
+
recordGateCoverage(toolName, evaluation.triggeredRules);
|
|
401
|
+
if (evaluation.verdict === "pass") {
|
|
402
|
+
const blastCandidates = (allArgStrings.length > 0 ? allArgStrings : [commandArg || toolName]).map((s) => computeBlastRadius(s));
|
|
403
|
+
const blast = mostSevereBlast(blastCandidates);
|
|
404
|
+
if (!blast.reversible && blast.dataAtRisk && blast.scope === "data-destructive") {
|
|
405
|
+
evaluation.verdict = "warn";
|
|
406
|
+
evaluation.triggeredRules.push("blast_radius_gate");
|
|
407
|
+
evaluation.reasons.push(`Auto-hold: irreversible data operation detected (scope: ${blast.scope})`);
|
|
408
|
+
logger.info({ toolName, scope: blast.scope }, "tool-guard: blast radius gate upgraded pass \u2192 hold");
|
|
409
|
+
} else {
|
|
410
|
+
const contaminated = isSessionContaminated(sessionId);
|
|
411
|
+
if (contaminated) {
|
|
412
|
+
const source = getContaminationSource(sessionId);
|
|
413
|
+
evaluation.verdict = "warn";
|
|
414
|
+
evaluation.triggeredRules.push("contamination_hold");
|
|
415
|
+
evaluation.reasons.push(`Session contaminated by prior injection attempt (pattern: ${source}). Human review required.`);
|
|
416
|
+
logger.info({ sessionId, toolName }, "tool-guard: contamination hold \u2014 upgrading PASS to HOLD");
|
|
417
|
+
} else {
|
|
418
|
+
const scriptTrust = await _checkScriptTrustEscalation(allArgStrings, toolName);
|
|
419
|
+
if (scriptTrust.escalate) {
|
|
420
|
+
evaluation.verdict = "warn";
|
|
421
|
+
evaluation.triggeredRules.push("script_trust_hold");
|
|
422
|
+
evaluation.reasons.push(scriptTrust.reason ?? "Untrusted script invocation requires human review.");
|
|
423
|
+
logger.info({ toolName, reason: scriptTrust.reason }, "tool-guard: script-trust check upgraded pass \u2192 hold");
|
|
424
|
+
} else {
|
|
425
|
+
const tier = getAgentScrutinyTier(agentId);
|
|
426
|
+
if (tier === "high" && !READ_ONLY_TOOLS.has(toolName)) {
|
|
427
|
+
evaluation.verdict = "warn";
|
|
428
|
+
evaluation.triggeredRules.push("high_scrutiny_hold");
|
|
429
|
+
evaluation.reasons.push(`Agent ${agentId} is under high scrutiny (repeated injection/sequence threats). Human review required.`);
|
|
430
|
+
logger.info({ agentId, toolName }, "tool-guard: high-scrutiny hold applied");
|
|
431
|
+
} else if (tier === "elevated" && !blast.reversible && blast.scope !== "unknown") {
|
|
432
|
+
evaluation.verdict = "warn";
|
|
433
|
+
evaluation.triggeredRules.push("elevated_scrutiny_hold");
|
|
434
|
+
evaluation.reasons.push(`Agent ${agentId} is under elevated scrutiny (repeated blocks). Non-reversible command requires human review.`);
|
|
435
|
+
logger.info({ agentId, toolName, scope: blast.scope }, "tool-guard: elevated-scrutiny hold applied");
|
|
436
|
+
} else {
|
|
437
|
+
const riskThreshold = getRiskHoldThreshold();
|
|
438
|
+
let riskEscalated = false;
|
|
439
|
+
if (riskThreshold !== null) {
|
|
440
|
+
const similarIncidentCount = findSimilarIncidents({ commandText: commandArg || toolName }).length;
|
|
441
|
+
const risk = computeRiskScore({
|
|
442
|
+
blastReversible: blast.reversible,
|
|
443
|
+
blastDataAtRisk: blast.dataAtRisk,
|
|
444
|
+
blastScope: blast.scope,
|
|
445
|
+
contaminated: false,
|
|
446
|
+
// already checked false to reach this branch
|
|
447
|
+
scrutinyTier: getAgentScrutinyTier(agentId),
|
|
448
|
+
similarIncidentCount
|
|
449
|
+
}, riskThreshold);
|
|
450
|
+
if (risk.escalateToHold) {
|
|
451
|
+
riskEscalated = true;
|
|
452
|
+
evaluation.verdict = "warn";
|
|
453
|
+
evaluation.triggeredRules.push("risk_score_hold");
|
|
454
|
+
evaluation.reasons.push(`Auto-hold: composite risk score ${risk.riskScore} met the configured threshold (${riskThreshold}). Human review required.`);
|
|
455
|
+
logger.info({ toolName, riskScore: risk.riskScore, threshold: riskThreshold }, "tool-guard: risk score upgraded pass \u2192 hold");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (!riskEscalated) {
|
|
459
|
+
recordGatePass();
|
|
460
|
+
trackSuccessfulCall(toolName);
|
|
461
|
+
recordSessionCall(sessionId, toolName, commandArg, "PASS");
|
|
462
|
+
recordActivity({ toolName, commandArg, verdict: "PASS", triggeredRules: [], ruleNames: [] });
|
|
463
|
+
recordGateEvent({
|
|
464
|
+
ts: Date.now(),
|
|
465
|
+
toolName,
|
|
466
|
+
command: commandArg || null,
|
|
467
|
+
actor: "agent",
|
|
468
|
+
agentId: agentIdLabel ?? null,
|
|
469
|
+
service: "mcp",
|
|
470
|
+
environment: process.env.MERGEN_ENVIRONMENT ?? null,
|
|
471
|
+
verdict: "pass",
|
|
472
|
+
triggeredRules: [],
|
|
473
|
+
guidedAlternative: null,
|
|
474
|
+
traceId
|
|
475
|
+
});
|
|
476
|
+
return next();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const reason = evaluation.reasons.join("; ");
|
|
484
|
+
const alternative = getSuggestedAlternative(evaluation.triggeredRules, commandArg);
|
|
485
|
+
if (isPolicyDryRun()) {
|
|
486
|
+
const gateEventVerdict = evaluation.verdict === "block" ? "block" : "hold";
|
|
487
|
+
logger.info(
|
|
488
|
+
{ toolName, verdict: evaluation.verdict, reason, triggeredRules: evaluation.triggeredRules },
|
|
489
|
+
`tool-guard: [DRY RUN] would have ${evaluation.verdict} \u2014 passing through`
|
|
490
|
+
);
|
|
491
|
+
await getStores().blunders.record({
|
|
492
|
+
blunderType: "pipeline_block",
|
|
493
|
+
command: toolName,
|
|
494
|
+
blockReason: `[DRY RUN] ${reason}`,
|
|
495
|
+
service: "mcp",
|
|
496
|
+
tag: "policy_dry_run",
|
|
497
|
+
actor: "agent",
|
|
498
|
+
pid: null,
|
|
499
|
+
confidenceScore: null,
|
|
500
|
+
triggeredRules: evaluation.triggeredRules,
|
|
501
|
+
agentId: agentIdLabel ?? null,
|
|
502
|
+
parentAgentId: parentAgentId ?? null,
|
|
503
|
+
traceId
|
|
504
|
+
});
|
|
505
|
+
recordGateEvent({
|
|
506
|
+
ts: Date.now(),
|
|
507
|
+
toolName,
|
|
508
|
+
command: commandArg || null,
|
|
509
|
+
actor: "agent",
|
|
510
|
+
agentId: agentIdLabel ?? null,
|
|
511
|
+
service: "mcp",
|
|
512
|
+
environment: process.env.MERGEN_ENVIRONMENT ?? null,
|
|
513
|
+
verdict: gateEventVerdict,
|
|
514
|
+
triggeredRules: evaluation.triggeredRules,
|
|
515
|
+
guidedAlternative: alternative,
|
|
516
|
+
reason: reason || null,
|
|
517
|
+
traceId
|
|
518
|
+
});
|
|
519
|
+
return next();
|
|
520
|
+
}
|
|
521
|
+
if (evaluation.verdict === "block") {
|
|
522
|
+
updateAgentReputation(agentId, "block");
|
|
523
|
+
recordGateBlock(evaluation.triggeredRules);
|
|
524
|
+
trackBlock(toolName, evaluation.triggeredRules);
|
|
525
|
+
await getStores().blunders.record({
|
|
526
|
+
blunderType: "pipeline_block",
|
|
527
|
+
command: toolName,
|
|
528
|
+
blockReason: reason,
|
|
529
|
+
service: "mcp",
|
|
530
|
+
tag: "tool_guard",
|
|
531
|
+
actor: "agent",
|
|
532
|
+
pid: null,
|
|
533
|
+
confidenceScore: null,
|
|
534
|
+
triggeredRules: evaluation.triggeredRules,
|
|
535
|
+
agentId: agentIdLabel ?? null,
|
|
536
|
+
parentAgentId: parentAgentId ?? null,
|
|
537
|
+
traceId
|
|
538
|
+
});
|
|
539
|
+
logger.warn({ toolName, reason }, "tool-guard: tool call blocked by local policy");
|
|
540
|
+
const ebpfCommandHint = (commandArg || toolName).trim().split(/\s+/)[0] || toolName;
|
|
541
|
+
void verifyNoExecve(ebpfCommandHint).then((verified) => {
|
|
542
|
+
if (verified === "verified-executed") {
|
|
543
|
+
logger.error({ toolName, commandHint: ebpfCommandHint, reason }, "ebpf-verify: BLOCKED command executed anyway \u2014 gate bypass or race condition, investigate immediately");
|
|
544
|
+
} else if (verified === "verified-not-executed") {
|
|
545
|
+
logger.info({ toolName, commandHint: ebpfCommandHint }, "ebpf-verify: confirmed the blocked execve never occurred");
|
|
546
|
+
}
|
|
547
|
+
}).catch(() => {
|
|
548
|
+
});
|
|
549
|
+
recordSessionCall(sessionId, toolName, commandArg, "BLOCK");
|
|
550
|
+
recordGateEvent({
|
|
551
|
+
ts: Date.now(),
|
|
552
|
+
toolName,
|
|
553
|
+
command: commandArg || null,
|
|
554
|
+
actor: "agent",
|
|
555
|
+
agentId: agentIdLabel ?? null,
|
|
556
|
+
service: "mcp",
|
|
557
|
+
environment: process.env.MERGEN_ENVIRONMENT ?? null,
|
|
558
|
+
verdict: "block",
|
|
559
|
+
triggeredRules: evaluation.triggeredRules,
|
|
560
|
+
guidedAlternative: alternative,
|
|
561
|
+
reason: reason || null,
|
|
562
|
+
traceId
|
|
563
|
+
});
|
|
564
|
+
const bypassToken = registerBypassBlock(toolName, commandArg, evaluation.triggeredRules);
|
|
565
|
+
logger.info(
|
|
566
|
+
{ bypassToken, toolName, commandArg },
|
|
567
|
+
`tool-guard: bypass approval required \u2014 operator can run: mergen approve ${bypassToken}`
|
|
568
|
+
);
|
|
569
|
+
const ruleNames = loadEnterprisePolicy().rules.filter((r) => evaluation.triggeredRules.includes(r.id)).map((r) => r.name);
|
|
570
|
+
recordActivity({ toolName, commandArg, verdict: "BLOCK", triggeredRules: evaluation.triggeredRules, ruleNames });
|
|
571
|
+
return {
|
|
572
|
+
content: [{
|
|
573
|
+
type: "text",
|
|
574
|
+
text: [
|
|
575
|
+
`\u{1F6AB} **Mergen policy gate blocked this tool call.**`,
|
|
576
|
+
``,
|
|
577
|
+
`**Tool:** \`${toolName}\``,
|
|
578
|
+
`**Why:** ${reason}`,
|
|
579
|
+
``,
|
|
580
|
+
`**What to do instead:** ${alternative}`,
|
|
581
|
+
``,
|
|
582
|
+
`_Action logged to the Agent Blunder Log. An operator must approve this action via the Mergen terminal or Slack. To modify this policy: \`~/.mergen/enterprise-policy.json\`._`
|
|
583
|
+
].join("\n")
|
|
584
|
+
}],
|
|
585
|
+
isError: true
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
recordSessionCall(sessionId, toolName, commandArg, "HOLD");
|
|
589
|
+
const token = randomUUID();
|
|
590
|
+
const heldAt = Date.now();
|
|
591
|
+
logger.info({ toolName, token, reason }, "tool-guard: tool call held for HITL approval");
|
|
592
|
+
recordGateEvent({
|
|
593
|
+
ts: heldAt,
|
|
594
|
+
toolName,
|
|
595
|
+
command: commandArg || null,
|
|
596
|
+
actor: "agent",
|
|
597
|
+
agentId: agentIdLabel ?? null,
|
|
598
|
+
service: "mcp",
|
|
599
|
+
environment: process.env.MERGEN_ENVIRONMENT ?? null,
|
|
600
|
+
verdict: "hold",
|
|
601
|
+
triggeredRules: evaluation.triggeredRules,
|
|
602
|
+
guidedAlternative: alternative,
|
|
603
|
+
reason: reason || null,
|
|
604
|
+
traceId
|
|
605
|
+
});
|
|
606
|
+
recordHitlHold();
|
|
607
|
+
const holdRuleNames = loadEnterprisePolicy().rules.filter((r) => evaluation.triggeredRules.includes(r.id)).map((r) => r.name);
|
|
608
|
+
recordActivity({ toolName, commandArg, verdict: "HOLD", triggeredRules: evaluation.triggeredRules, ruleNames: holdRuleNames });
|
|
609
|
+
return holdToolCall({
|
|
610
|
+
token,
|
|
611
|
+
toolName,
|
|
612
|
+
argsSnapshot,
|
|
613
|
+
reason,
|
|
614
|
+
triggeredRules: evaluation.triggeredRules,
|
|
615
|
+
heldAt,
|
|
616
|
+
commandArg,
|
|
617
|
+
suggestedAlt: alternative,
|
|
618
|
+
port
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
let _gateInstalled = false;
|
|
622
|
+
function isGateInstalled() {
|
|
623
|
+
return _gateInstalled;
|
|
624
|
+
}
|
|
625
|
+
function _resetGateInstalledForTesting() {
|
|
626
|
+
_gateInstalled = false;
|
|
627
|
+
}
|
|
628
|
+
const _installedGatedHandlers = /* @__PURE__ */ new WeakMap();
|
|
629
|
+
function createGuardedServer(server, port) {
|
|
630
|
+
if (server._toolHandlersInitialized) {
|
|
631
|
+
throw new Error(
|
|
632
|
+
"createGuardedServer: a tool was already registered on this McpServer before the gate was installed \u2014 the tools/call dispatch handler is already active and unpatched. Call createGuardedServer(mcp, port) before any registerTool/tool/registerToolTask call, never after."
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
const lowLevel = server.server;
|
|
636
|
+
const originalSetRequestHandler = lowLevel.setRequestHandler.bind(lowLevel);
|
|
637
|
+
lowLevel.setRequestHandler = (schema, handler) => {
|
|
638
|
+
if (schema !== CallToolRequestSchema) {
|
|
639
|
+
return originalSetRequestHandler(schema, handler);
|
|
640
|
+
}
|
|
641
|
+
const gated = (request, extra) => {
|
|
642
|
+
const r = request;
|
|
643
|
+
const toolName = r?.params?.name ?? "unknown_tool";
|
|
644
|
+
const args = r?.params?.arguments;
|
|
645
|
+
return applyGate(toolName, args, () => Promise.resolve(handler(request, extra)), port);
|
|
646
|
+
};
|
|
647
|
+
_installedGatedHandlers.set(server, gated);
|
|
648
|
+
return originalSetRequestHandler(schema, gated);
|
|
649
|
+
};
|
|
650
|
+
_gateInstalled = true;
|
|
651
|
+
return server;
|
|
652
|
+
}
|
|
653
|
+
async function assertGateCoversRegisteredTools(server) {
|
|
654
|
+
if (!isGateInstalled()) {
|
|
655
|
+
throw new Error("assertGateCoversRegisteredTools: createGuardedServer was never called on this server.");
|
|
656
|
+
}
|
|
657
|
+
const registeredTools = server._registeredTools ?? {};
|
|
658
|
+
const toolNames = Object.keys(registeredTools);
|
|
659
|
+
if (toolNames.length === 0) {
|
|
660
|
+
throw new Error(
|
|
661
|
+
`assertGateCoversRegisteredTools: no tools are registered on this server \u2014 registerTools() appears to have registered nothing. Refusing to start with an empty, unverifiable tool set. If you are building this repository from source without the closed-source intelligence/tools.ts (the proprietary MCP tool registry \u2014 see intelligence/LICENSE), this is expected: registerTools() resolves to a no-op stub (__stubs__/closed-source.ts) and the server correctly refuses to boot with zero verifiable tools rather than silently running ungoverned. See this repo's CONTRIBUTING.md, "Running the server locally", for what an external contributor can do without the private layer.`
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
const installedHandler = _installedGatedHandlers.get(server);
|
|
665
|
+
if (!installedHandler) {
|
|
666
|
+
throw new Error(
|
|
667
|
+
"assertGateCoversRegisteredTools: no tools/call dispatch handler is installed \u2014 setToolRequestHandlers() may never have run despite tools being present in _registeredTools."
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
const syntheticRequest = {
|
|
671
|
+
params: { name: toolNames[0], arguments: { command: "terraform destroy prod-mergen-selftest" } }
|
|
672
|
+
};
|
|
673
|
+
let result;
|
|
674
|
+
try {
|
|
675
|
+
result = await installedHandler(syntheticRequest, { signal: new AbortController().signal });
|
|
676
|
+
} catch (err) {
|
|
677
|
+
throw new Error(
|
|
678
|
+
`assertGateCoversRegisteredTools: self-test call raised instead of being cleanly blocked by the gate: ${err instanceof Error ? err.message : String(err)}`
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
const isBlocked = !!(result && typeof result === "object" && result.isError === true);
|
|
682
|
+
if (!isBlocked) {
|
|
683
|
+
throw new Error(
|
|
684
|
+
'assertGateCoversRegisteredTools: a synthetic "terraform destroy" call was NOT blocked by the live tools/call dispatch handler \u2014 the gate is not enforcing on this server. Refusing to start.'
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
export {
|
|
689
|
+
_resetGateInstalledForTesting,
|
|
690
|
+
applyGate,
|
|
691
|
+
approveBypass,
|
|
692
|
+
approveToolCall,
|
|
693
|
+
approveToolCallConstrained,
|
|
694
|
+
assertGateCoversRegisteredTools,
|
|
695
|
+
checkAndConsumeBypass2 as checkAndConsumeBypass,
|
|
696
|
+
createGuardedServer,
|
|
697
|
+
denyStaleHoldsOnStartup,
|
|
698
|
+
denyToolCall,
|
|
699
|
+
getPendingBypassDetail,
|
|
700
|
+
getPendingBypasses,
|
|
701
|
+
getPendingHolds,
|
|
702
|
+
invalidateBypassToken,
|
|
703
|
+
isGateInstalled,
|
|
704
|
+
loadBypasses,
|
|
705
|
+
persistBypasses,
|
|
706
|
+
registerBypassBlock2 as registerBypassBlock,
|
|
707
|
+
setBypassSecret
|
|
708
|
+
};
|