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,115 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual, randomBytes } from "crypto";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { DATA_DIR } from "../sensor/paths.js";
|
|
5
|
+
import logger from "../sensor/logger.js";
|
|
6
|
+
const TOKEN_RECORD_DIR = path.join(DATA_DIR, "agent-tokens");
|
|
7
|
+
const DEFAULT_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
8
|
+
let _tokenSecret = "";
|
|
9
|
+
function setAgentTokenSecret(secret) {
|
|
10
|
+
_tokenSecret = secret;
|
|
11
|
+
}
|
|
12
|
+
function sign(payload) {
|
|
13
|
+
const body = JSON.stringify(payload);
|
|
14
|
+
const sig = createHmac("sha256", _tokenSecret).update(body).digest("hex");
|
|
15
|
+
return Buffer.from(JSON.stringify({ payload, sig }), "utf8").toString("base64url");
|
|
16
|
+
}
|
|
17
|
+
function issueToken(agentId, ttlMs = DEFAULT_TTL_MS, parentAgentId, userId) {
|
|
18
|
+
if (!_tokenSecret) {
|
|
19
|
+
throw new Error("agent-identity: token secret not initialized \u2014 setAgentTokenSecret must run before issueToken");
|
|
20
|
+
}
|
|
21
|
+
const payload = {
|
|
22
|
+
agentId,
|
|
23
|
+
issuedAt: Date.now(),
|
|
24
|
+
expiresAt: Date.now() + ttlMs,
|
|
25
|
+
nonce: randomBytes(8).toString("hex"),
|
|
26
|
+
...parentAgentId ? { parentAgentId } : {},
|
|
27
|
+
...userId ? { userId } : {}
|
|
28
|
+
};
|
|
29
|
+
const token = sign(payload);
|
|
30
|
+
_recordIssuedToken(agentId, payload.issuedAt, payload.expiresAt, parentAgentId, userId);
|
|
31
|
+
return token;
|
|
32
|
+
}
|
|
33
|
+
function verifyToken(token) {
|
|
34
|
+
if (!_tokenSecret) return null;
|
|
35
|
+
let decoded;
|
|
36
|
+
try {
|
|
37
|
+
decoded = JSON.parse(Buffer.from(token, "base64url").toString("utf8"));
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (!decoded.payload?.agentId || !decoded.sig) return null;
|
|
42
|
+
const body = JSON.stringify(decoded.payload);
|
|
43
|
+
const expected = createHmac("sha256", _tokenSecret).update(body).digest("hex");
|
|
44
|
+
const expectedBuf = Buffer.from(expected, "hex");
|
|
45
|
+
let actualBuf;
|
|
46
|
+
try {
|
|
47
|
+
actualBuf = Buffer.from(decoded.sig, "hex");
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (expectedBuf.length !== actualBuf.length) return null;
|
|
52
|
+
if (!timingSafeEqual(expectedBuf, actualBuf)) return null;
|
|
53
|
+
if (Date.now() > decoded.payload.expiresAt) return null;
|
|
54
|
+
return { agentId: decoded.payload.agentId, parentAgentId: decoded.payload.parentAgentId, userId: decoded.payload.userId };
|
|
55
|
+
}
|
|
56
|
+
let _warnedUnauthenticated = false;
|
|
57
|
+
let _warnedNoSecretWithToken = false;
|
|
58
|
+
function resolveAgentIdentity() {
|
|
59
|
+
const token = process.env.MERGEN_AGENT_TOKEN;
|
|
60
|
+
if (token) {
|
|
61
|
+
const verified = verifyToken(token);
|
|
62
|
+
if (verified) return { agentId: verified.agentId, verified: true, parentAgentId: verified.parentAgentId, userId: verified.userId };
|
|
63
|
+
if (!_tokenSecret && !_warnedNoSecretWithToken) {
|
|
64
|
+
logger.warn("agent-identity: MERGEN_AGENT_TOKEN is set but no signing secret is configured on this server \u2014 cannot verify, treating as unverified");
|
|
65
|
+
_warnedNoSecretWithToken = true;
|
|
66
|
+
} else {
|
|
67
|
+
logger.warn("agent-identity: MERGEN_AGENT_TOKEN is set but invalid or expired \u2014 ignoring");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const raw = process.env.MERGEN_AGENT_ID;
|
|
71
|
+
if (raw && !_warnedUnauthenticated) {
|
|
72
|
+
logger.warn(
|
|
73
|
+
"agent-identity: MERGEN_AGENT_ID is set but unauthenticated (no valid MERGEN_AGENT_TOKEN) \u2014 it will be used for reputation/session labeling only, not for agent-profile permissions or policy conditions.agentIds matching. Run `mergen-server agent-register <profile-id>` to issue a verified token."
|
|
74
|
+
);
|
|
75
|
+
_warnedUnauthenticated = true;
|
|
76
|
+
}
|
|
77
|
+
return { agentId: raw, verified: false };
|
|
78
|
+
}
|
|
79
|
+
function _recordIssuedToken(agentId, issuedAt, expiresAt, parentAgentId, userId) {
|
|
80
|
+
try {
|
|
81
|
+
fs.mkdirSync(TOKEN_RECORD_DIR, { recursive: true, mode: 448 });
|
|
82
|
+
const file = path.join(TOKEN_RECORD_DIR, `${agentId}.json`);
|
|
83
|
+
const record = { agentId, issuedAt, expiresAt, ...parentAgentId ? { parentAgentId } : {}, ...userId ? { userId } : {} };
|
|
84
|
+
fs.writeFileSync(file, JSON.stringify(record, null, 2), { encoding: "utf8", mode: 384 });
|
|
85
|
+
} catch (err) {
|
|
86
|
+
logger.warn({ err, agentId }, "agent-identity: failed to persist issued-token record (non-fatal \u2014 token is still valid)");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function listIssuedTokenRecords() {
|
|
90
|
+
try {
|
|
91
|
+
if (!fs.existsSync(TOKEN_RECORD_DIR)) return [];
|
|
92
|
+
return fs.readdirSync(TOKEN_RECORD_DIR).filter((f) => f.endsWith(".json")).map((f) => {
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(fs.readFileSync(path.join(TOKEN_RECORD_DIR, f), "utf8"));
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}).filter((r) => r !== null).sort((a, b) => b.issuedAt - a.issuedAt);
|
|
99
|
+
} catch {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function _resetAgentIdentityForTesting() {
|
|
104
|
+
_tokenSecret = "";
|
|
105
|
+
_warnedUnauthenticated = false;
|
|
106
|
+
_warnedNoSecretWithToken = false;
|
|
107
|
+
}
|
|
108
|
+
export {
|
|
109
|
+
_resetAgentIdentityForTesting,
|
|
110
|
+
issueToken,
|
|
111
|
+
listIssuedTokenRecords,
|
|
112
|
+
resolveAgentIdentity,
|
|
113
|
+
setAgentTokenSecret,
|
|
114
|
+
verifyToken
|
|
115
|
+
};
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { fixActionToCommand } from "./causal.js";
|
|
2
|
+
import { extractCommand } from "./autonomy.js";
|
|
3
|
+
import { computeBlastRadius } from "./blast-radius.js";
|
|
4
|
+
import { deriveRollback } from "./rollback.js";
|
|
5
|
+
import { getStatsForTag } from "./calibration.js";
|
|
6
|
+
import { hasRecentOverride, dominantOverrideReason } from "./override-corpus.js";
|
|
7
|
+
import { getAutopilotLevel, autopilotLevelPermits, classifyCommandRisk, analyzeSemanticRisk } from "./action-risk.js";
|
|
8
|
+
import { postmortemStore } from "./postmortem-store.js";
|
|
9
|
+
import logger from "../sensor/logger.js";
|
|
10
|
+
function validateHypothesis(hyp, chain) {
|
|
11
|
+
const contradictions = [];
|
|
12
|
+
let coverageScore = 0;
|
|
13
|
+
const errorTexts = chain.errors.map((e) => e.message.toLowerCase());
|
|
14
|
+
const tagParts = hyp.tag.replace(/^infra_/, "").split("_");
|
|
15
|
+
const tagMatches = tagParts.filter(
|
|
16
|
+
(part) => part.length >= 4 && errorTexts.some((msg) => msg.includes(part))
|
|
17
|
+
);
|
|
18
|
+
coverageScore += tagMatches.length > 0 ? 0.3 : 0;
|
|
19
|
+
const evidenceMatched = hyp.evidence.filter((ev) => {
|
|
20
|
+
const evLower = ev.toLowerCase();
|
|
21
|
+
return chain.errors.some((e) => e.message.toLowerCase().includes(evLower.slice(0, 20))) || chain.correlatedNetwork.some(
|
|
22
|
+
(n) => n.url.toLowerCase().includes(evLower.slice(0, 20)) || (n.error ?? "").toLowerCase().includes(evLower.slice(0, 20))
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
coverageScore += Math.min(0.4, evidenceMatched.length * 0.1);
|
|
26
|
+
coverageScore += hyp.causalPath.length >= 2 ? 0.3 : 0;
|
|
27
|
+
const isNetworkHyp = /network|dns|timeout|latency|503|502|429|rate_limit/i.test(hyp.tag);
|
|
28
|
+
if (isNetworkHyp && chain.correlatedNetwork.length === 0 && chain.errors.length > 0) {
|
|
29
|
+
contradictions.push("Network hypothesis but no network events observed in window");
|
|
30
|
+
coverageScore -= 0.2;
|
|
31
|
+
}
|
|
32
|
+
const isOomHyp = /oom|memory|heap|killed/i.test(hyp.tag);
|
|
33
|
+
if (isOomHyp && chain.correlatedBackend.length === 0 && chain.errors.length > 0) {
|
|
34
|
+
const oomInErrors = chain.errors.some(
|
|
35
|
+
(e) => /oom|memory|heap|killed|enomem/i.test(e.message)
|
|
36
|
+
);
|
|
37
|
+
if (!oomInErrors) {
|
|
38
|
+
contradictions.push("OOM hypothesis but no OOM-related signals in error messages");
|
|
39
|
+
coverageScore -= 0.15;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (chain.errors.length > 0 && hyp.evidence.length === 0) {
|
|
43
|
+
contradictions.push("Hypothesis has no explicit evidence \u2014 may be a weak signal match");
|
|
44
|
+
}
|
|
45
|
+
const corpusStats = postmortemStore.tagStats().find((s) => s.tag === hyp.tag);
|
|
46
|
+
const precedentCount = corpusStats?.count ?? 0;
|
|
47
|
+
const avgMttrMs = corpusStats?.avgMttrMs ?? null;
|
|
48
|
+
if (precedentCount > 0) {
|
|
49
|
+
coverageScore += Math.min(0.2, precedentCount * 0.05);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
score: Math.max(0, Math.min(1, coverageScore)),
|
|
53
|
+
contradictions,
|
|
54
|
+
corpusPrecedent: precedentCount > 0,
|
|
55
|
+
precedentCount,
|
|
56
|
+
avgMttrMs
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function buildExecutionPlan(hyp, chain) {
|
|
60
|
+
const command = hyp.fixAction ? fixActionToCommand(hyp.fixAction) : hyp.fixHint ? extractCommand(hyp.fixHint) : null;
|
|
61
|
+
if (!command) return null;
|
|
62
|
+
const rollback = deriveRollback(command, "");
|
|
63
|
+
const rollbackCommand = rollback.type === "command" ? rollback.command : null;
|
|
64
|
+
const risk = classifyCommandRisk(command);
|
|
65
|
+
const estimatedRisk = risk === "restart" ? "low" : risk === "full" ? "high" : "medium";
|
|
66
|
+
const reversible = rollbackCommand !== null || /restart|rollout|deploy/i.test(command);
|
|
67
|
+
const steps = [
|
|
68
|
+
{ order: 1, action: "Confirm current error state", command: "triage_incident (diagnosis only)" },
|
|
69
|
+
{ order: 2, action: `Execute fix: ${hyp.fixHint?.split("\n")[0] ?? command}`, command }
|
|
70
|
+
];
|
|
71
|
+
if (rollbackCommand) {
|
|
72
|
+
steps.push({ order: 3, action: "Validate resolution", command: "validate_fix" });
|
|
73
|
+
steps.push({ order: 4, action: "Rollback if REGRESSED", command: rollbackCommand });
|
|
74
|
+
} else {
|
|
75
|
+
steps.push({ order: 3, action: "Validate resolution (manual rollback required if REGRESSED)", command: "validate_fix" });
|
|
76
|
+
}
|
|
77
|
+
const requiresApproval = estimatedRisk === "high" || !reversible;
|
|
78
|
+
return {
|
|
79
|
+
command,
|
|
80
|
+
rollbackCommand,
|
|
81
|
+
steps,
|
|
82
|
+
estimatedRisk,
|
|
83
|
+
requiresApproval,
|
|
84
|
+
reversible
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function critiqueExecutionPlan(plan, hyp, validation, service, opts) {
|
|
88
|
+
const concerns = [];
|
|
89
|
+
let verdict = "proceed";
|
|
90
|
+
const blastRadius = computeBlastRadius(plan.command);
|
|
91
|
+
const dtLabel = blastRadius.estimatedDowntimeMs != null ? `~${Math.round(blastRadius.estimatedDowntimeMs / 1e3)}s downtime` : "unknown downtime";
|
|
92
|
+
const blastSummary = `${blastRadius.scope} scope \xB7 ${dtLabel} \xB7 ${blastRadius.reversible ? "reversible" : "IRREVERSIBLE"}`;
|
|
93
|
+
if (!blastRadius.reversible) {
|
|
94
|
+
const rollbackNote = blastRadius.rollbackCommand ?? "no rollback path";
|
|
95
|
+
concerns.push(`Command is IRREVERSIBLE: ${rollbackNote}`);
|
|
96
|
+
verdict = "block";
|
|
97
|
+
}
|
|
98
|
+
if (blastRadius.dataAtRisk) {
|
|
99
|
+
concerns.push("Data mutation detected \u2014 command may modify persistent state");
|
|
100
|
+
verdict = verdict === "proceed" ? "review" : verdict;
|
|
101
|
+
}
|
|
102
|
+
if (plan.estimatedRisk === "high") {
|
|
103
|
+
concerns.push(`High-risk command tier: ${classifyCommandRisk(plan.command)}`);
|
|
104
|
+
verdict = verdict === "proceed" ? "review" : verdict;
|
|
105
|
+
}
|
|
106
|
+
const semantic = analyzeSemanticRisk(plan.command, {
|
|
107
|
+
service,
|
|
108
|
+
service_time: opts.service_time
|
|
109
|
+
});
|
|
110
|
+
if (semantic.blocked) {
|
|
111
|
+
concerns.push(semantic.reason ?? "Semantic safety block");
|
|
112
|
+
verdict = "block";
|
|
113
|
+
} else if (semantic.risk === "high") {
|
|
114
|
+
concerns.push(semantic.reason ?? "High semantic risk detected");
|
|
115
|
+
verdict = verdict === "proceed" ? "review" : verdict;
|
|
116
|
+
}
|
|
117
|
+
if (validation.contradictions.length > 0) {
|
|
118
|
+
concerns.push(...validation.contradictions.map((c) => `Validation: ${c}`));
|
|
119
|
+
if (validation.score < 0.3) {
|
|
120
|
+
verdict = "block";
|
|
121
|
+
} else if (validation.score < 0.6) {
|
|
122
|
+
verdict = verdict === "proceed" ? "review" : verdict;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const now = opts.service_time ?? {
|
|
126
|
+
dayOfWeek: (/* @__PURE__ */ new Date()).getUTCDay(),
|
|
127
|
+
hourOfDay: (/* @__PURE__ */ new Date()).getUTCHours()
|
|
128
|
+
};
|
|
129
|
+
const corpusConflict = hasRecentOverride(hyp.tag, service, now.dayOfWeek, now.hourOfDay);
|
|
130
|
+
if (corpusConflict) {
|
|
131
|
+
const reason = dominantOverrideReason(hyp.tag, service) ?? "override corpus conflict";
|
|
132
|
+
concerns.push(`Override corpus: ${reason}`);
|
|
133
|
+
verdict = verdict === "proceed" ? "review" : verdict;
|
|
134
|
+
}
|
|
135
|
+
const autopilotLevel = getAutopilotLevel();
|
|
136
|
+
const levelConflict = !autopilotLevelPermits(plan.command, autopilotLevel);
|
|
137
|
+
if (levelConflict) {
|
|
138
|
+
concerns.push(`MERGEN_AUTOPILOT_LEVEL=${autopilotLevel} does not permit ${classifyCommandRisk(plan.command)}-tier commands`);
|
|
139
|
+
verdict = "block";
|
|
140
|
+
}
|
|
141
|
+
if (validation.score < 0.2 && validation.contradictions.length === 0) {
|
|
142
|
+
concerns.push("Low validation score \u2014 hypothesis may not explain all observed signals");
|
|
143
|
+
verdict = verdict === "proceed" ? "review" : verdict;
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
verdict,
|
|
147
|
+
concerns,
|
|
148
|
+
blastRadiusSummary: blastSummary,
|
|
149
|
+
corpusConflict,
|
|
150
|
+
levelConflict
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function guardedAction(hyp, plan, critique, threshold) {
|
|
154
|
+
const execConfidence = hyp.remediationConfidence ?? hyp.confidenceScore ?? 0;
|
|
155
|
+
if (critique.verdict === "block") {
|
|
156
|
+
return {
|
|
157
|
+
verdict: "block",
|
|
158
|
+
blockReason: critique.concerns[0] ?? "Critic stage blocked execution"
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (execConfidence < threshold) {
|
|
162
|
+
return {
|
|
163
|
+
verdict: "block",
|
|
164
|
+
blockReason: `Remediation confidence ${Math.round(execConfidence * 100)}% is below ${Math.round(threshold * 100)}% threshold`
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (critique.verdict === "review" || plan.requiresApproval) {
|
|
168
|
+
return { verdict: "review", blockReason: null };
|
|
169
|
+
}
|
|
170
|
+
return { verdict: "proceed", blockReason: null };
|
|
171
|
+
}
|
|
172
|
+
function runAgentPipeline(chain, opts = {}) {
|
|
173
|
+
const threshold = opts.executionThreshold ?? 0.85;
|
|
174
|
+
const service = opts.service ?? "unknown";
|
|
175
|
+
const stages = [];
|
|
176
|
+
const errorCount = chain.errors.length;
|
|
177
|
+
const netCount = chain.correlatedNetwork.filter((n) => n.status >= 400).length;
|
|
178
|
+
stages.push({
|
|
179
|
+
name: "detector",
|
|
180
|
+
status: "pass",
|
|
181
|
+
summary: `${errorCount} console error${errorCount !== 1 ? "s" : ""}, ${netCount} network failure${netCount !== 1 ? "s" : ""}`,
|
|
182
|
+
detail: chain.chain.length > 0 ? `${chain.chain.length} causal events in timeline` : void 0
|
|
183
|
+
});
|
|
184
|
+
const topHyp = chain.hypotheses[0] ?? null;
|
|
185
|
+
if (!topHyp) {
|
|
186
|
+
stages.push({
|
|
187
|
+
name: "hypothesis",
|
|
188
|
+
status: "block",
|
|
189
|
+
summary: "No hypothesis generated \u2014 insufficient signals for root cause identification"
|
|
190
|
+
});
|
|
191
|
+
return { stages, topHypothesis: null, validation: null, plan: null, critique: null, verdict: "block", blockReason: "No hypothesis generated" };
|
|
192
|
+
}
|
|
193
|
+
const calStats = getStatsForTag(topHyp.tag);
|
|
194
|
+
const calLabel = calStats?.isEmpirical ? `calibrated (${calStats.verdicts} verdicts)` : "prior estimate";
|
|
195
|
+
const pct = Math.round((topHyp.confidenceScore ?? 0) * 100);
|
|
196
|
+
stages.push({
|
|
197
|
+
name: "hypothesis",
|
|
198
|
+
status: "pass",
|
|
199
|
+
summary: `${topHyp.confidence} (${pct}%) \u2014 ${topHyp.summary.slice(0, 80)}`,
|
|
200
|
+
detail: `[${calLabel}] ${chain.hypotheses.length} total hypotheses, ${chain.suppressedHypotheses.length} suppressed`
|
|
201
|
+
});
|
|
202
|
+
const validation = validateHypothesis(topHyp, chain);
|
|
203
|
+
const validStatus = validation.score >= 0.6 ? "pass" : validation.score >= 0.3 ? "warn" : "block";
|
|
204
|
+
stages.push({
|
|
205
|
+
name: "validator",
|
|
206
|
+
status: validStatus,
|
|
207
|
+
summary: `Coverage score ${Math.round(validation.score * 100)}%` + (validation.corpusPrecedent ? ` \xB7 ${validation.precedentCount} corpus match${validation.precedentCount !== 1 ? "es" : ""}` : " \xB7 no corpus precedent") + (validation.avgMttrMs != null ? ` \xB7 avg MTTR ${Math.round(validation.avgMttrMs / 6e4)}m` : ""),
|
|
208
|
+
detail: validation.contradictions.length > 0 ? validation.contradictions.join("; ") : void 0
|
|
209
|
+
});
|
|
210
|
+
const plan = buildExecutionPlan(topHyp, chain);
|
|
211
|
+
if (!plan) {
|
|
212
|
+
stages.push({
|
|
213
|
+
name: "planner",
|
|
214
|
+
status: "warn",
|
|
215
|
+
summary: "No executable command \u2014 fix requires manual intervention"
|
|
216
|
+
});
|
|
217
|
+
return {
|
|
218
|
+
stages,
|
|
219
|
+
topHypothesis: topHyp,
|
|
220
|
+
validation,
|
|
221
|
+
plan: null,
|
|
222
|
+
critique: null,
|
|
223
|
+
verdict: "review",
|
|
224
|
+
blockReason: "No executable command in fix hint"
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
stages.push({
|
|
228
|
+
name: "planner",
|
|
229
|
+
status: plan.estimatedRisk === "high" ? "warn" : "pass",
|
|
230
|
+
summary: `\`${plan.command}\` \xB7 risk: ${plan.estimatedRisk} \xB7 ${plan.reversible ? "reversible" : "IRREVERSIBLE"}`,
|
|
231
|
+
detail: plan.rollbackCommand ? `rollback: \`${plan.rollbackCommand}\`` : "no rollback path"
|
|
232
|
+
});
|
|
233
|
+
const critique = critiqueExecutionPlan(plan, topHyp, validation, service, opts);
|
|
234
|
+
stages.push({
|
|
235
|
+
name: "critic",
|
|
236
|
+
status: critique.verdict === "proceed" ? "pass" : critique.verdict === "review" ? "warn" : "block",
|
|
237
|
+
summary: `${critique.blastRadiusSummary} \xB7 ${critique.concerns.length === 0 ? "no concerns" : `${critique.concerns.length} concern${critique.concerns.length !== 1 ? "s" : ""}`}`,
|
|
238
|
+
detail: critique.concerns.length > 0 ? critique.concerns.slice(0, 3).join("; ") : void 0
|
|
239
|
+
});
|
|
240
|
+
const guard = guardedAction(topHyp, plan, critique, threshold);
|
|
241
|
+
stages.push({
|
|
242
|
+
name: "guard",
|
|
243
|
+
status: guard.verdict === "proceed" ? "pass" : guard.verdict === "review" ? "warn" : "block",
|
|
244
|
+
summary: guard.verdict.toUpperCase() + (guard.blockReason ? ` \u2014 ${guard.blockReason}` : "") + (guard.verdict === "proceed" ? ` (remediation confidence ${Math.round((topHyp.remediationConfidence ?? topHyp.confidenceScore ?? 0) * 100)}% \u2265 ${Math.round(threshold * 100)}%)` : "")
|
|
245
|
+
});
|
|
246
|
+
logger.debug({
|
|
247
|
+
service,
|
|
248
|
+
tag: topHyp.tag,
|
|
249
|
+
validationScore: Math.round(validation.score * 100),
|
|
250
|
+
pipelineVerdict: guard.verdict,
|
|
251
|
+
stages: stages.map((s) => `${s.name}:${s.status}`).join(",")
|
|
252
|
+
}, "agent-pipeline: complete");
|
|
253
|
+
return {
|
|
254
|
+
stages,
|
|
255
|
+
topHypothesis: topHyp,
|
|
256
|
+
validation,
|
|
257
|
+
plan,
|
|
258
|
+
critique,
|
|
259
|
+
verdict: guard.verdict,
|
|
260
|
+
blockReason: guard.blockReason
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function renderPipelineStages(stages) {
|
|
264
|
+
const icon = (s) => s === "pass" ? "\u2705" : s === "warn" ? "\u26A0\uFE0F" : "\u{1F6AB}";
|
|
265
|
+
const lines = ["### Pipeline", ""];
|
|
266
|
+
for (const stage of stages) {
|
|
267
|
+
lines.push(`${icon(stage.status)} **${stage.name}**: ${stage.summary}`);
|
|
268
|
+
if (stage.detail) lines.push(` _${stage.detail}_`);
|
|
269
|
+
}
|
|
270
|
+
return lines.join("\n");
|
|
271
|
+
}
|
|
272
|
+
const _hooks = [];
|
|
273
|
+
function registerGovernanceHook(hook) {
|
|
274
|
+
_hooks.push(hook);
|
|
275
|
+
}
|
|
276
|
+
function clearGovernanceHooks() {
|
|
277
|
+
_hooks.length = 0;
|
|
278
|
+
}
|
|
279
|
+
async function runAgentPipelineAsync(chain, opts = {}) {
|
|
280
|
+
const result = runAgentPipeline(chain, opts);
|
|
281
|
+
if (result.verdict === "block" || !result.topHypothesis || !result.plan) {
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
const ctx = {
|
|
285
|
+
chain,
|
|
286
|
+
hypothesis: result.topHypothesis,
|
|
287
|
+
plan: result.plan,
|
|
288
|
+
opts
|
|
289
|
+
};
|
|
290
|
+
for (let i = 0; i < _hooks.length; i++) {
|
|
291
|
+
const hook = _hooks[i];
|
|
292
|
+
try {
|
|
293
|
+
const hookRes = await hook(ctx);
|
|
294
|
+
const status = hookRes.verdict === "proceed" ? "pass" : hookRes.verdict === "review" ? "warn" : "block";
|
|
295
|
+
result.stages.push({
|
|
296
|
+
name: `pre-execution-hook-${i + 1}`,
|
|
297
|
+
status,
|
|
298
|
+
summary: hookRes.reason ?? `Hook ${i + 1} returned ${hookRes.verdict}`
|
|
299
|
+
});
|
|
300
|
+
if (hookRes.verdict === "block") {
|
|
301
|
+
result.verdict = "block";
|
|
302
|
+
result.blockReason = hookRes.reason ?? `Blocked by pre-execution hook ${i + 1}`;
|
|
303
|
+
break;
|
|
304
|
+
} else if (hookRes.verdict === "review") {
|
|
305
|
+
if (result.verdict === "proceed") {
|
|
306
|
+
result.verdict = "review";
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
} catch (err) {
|
|
310
|
+
logger.error({ err, hookIndex: i }, "agent-pipeline: async pre-execution hook failed");
|
|
311
|
+
result.stages.push({
|
|
312
|
+
name: `pre-execution-hook-${i + 1}`,
|
|
313
|
+
status: "warn",
|
|
314
|
+
summary: `Hook failed with error: ${err.message || err}`
|
|
315
|
+
});
|
|
316
|
+
if (result.verdict === "proceed") {
|
|
317
|
+
result.verdict = "review";
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
export {
|
|
324
|
+
clearGovernanceHooks,
|
|
325
|
+
registerGovernanceHook,
|
|
326
|
+
renderPipelineStages,
|
|
327
|
+
runAgentPipeline,
|
|
328
|
+
runAgentPipelineAsync
|
|
329
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { DATA_DIR, zeroRetentionMode } from "../sensor/paths.js";
|
|
4
|
+
import logger from "../sensor/logger.js";
|
|
5
|
+
const PROFILES_FILE = path.join(DATA_DIR, "agent-profiles.json");
|
|
6
|
+
let _profiles = [];
|
|
7
|
+
let _loaded = false;
|
|
8
|
+
function load(force = false) {
|
|
9
|
+
if (_loaded && !force) return;
|
|
10
|
+
_loaded = true;
|
|
11
|
+
if (!fs.existsSync(PROFILES_FILE)) {
|
|
12
|
+
_profiles = [];
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const raw = JSON.parse(fs.readFileSync(PROFILES_FILE, "utf8"));
|
|
17
|
+
_profiles = raw?.version === 1 && Array.isArray(raw.profiles) ? raw.profiles : [];
|
|
18
|
+
} catch {
|
|
19
|
+
_profiles = [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function persist() {
|
|
23
|
+
if (zeroRetentionMode()) return;
|
|
24
|
+
try {
|
|
25
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
26
|
+
const tmp = `${PROFILES_FILE}.tmp.${process.pid}`;
|
|
27
|
+
fs.writeFileSync(tmp, JSON.stringify({ version: 1, profiles: _profiles }), "utf8");
|
|
28
|
+
fs.renameSync(tmp, PROFILES_FILE);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
logger.warn({ err }, "agent-profiles: persist failed");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function listProfiles() {
|
|
34
|
+
load();
|
|
35
|
+
return [..._profiles];
|
|
36
|
+
}
|
|
37
|
+
function getProfile(id) {
|
|
38
|
+
load();
|
|
39
|
+
return _profiles.find((p) => p.id === id) ?? null;
|
|
40
|
+
}
|
|
41
|
+
function saveProfile(profile) {
|
|
42
|
+
load(true);
|
|
43
|
+
const idx = _profiles.findIndex((p) => p.id === profile.id);
|
|
44
|
+
if (idx >= 0) _profiles[idx] = profile;
|
|
45
|
+
else _profiles.push(profile);
|
|
46
|
+
persist();
|
|
47
|
+
}
|
|
48
|
+
function deleteProfile(id) {
|
|
49
|
+
load(true);
|
|
50
|
+
const before = _profiles.length;
|
|
51
|
+
_profiles = _profiles.filter((p) => p.id !== id);
|
|
52
|
+
if (_profiles.length < before) {
|
|
53
|
+
persist();
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
function checkAgentProfile(toolName, service, verifiedAgentId) {
|
|
59
|
+
const agentId = verifiedAgentId;
|
|
60
|
+
if (!agentId) return null;
|
|
61
|
+
load();
|
|
62
|
+
const profile = _profiles.find((p) => p.id === agentId);
|
|
63
|
+
if (!profile) return null;
|
|
64
|
+
if (profile.blockedTools.includes(toolName)) {
|
|
65
|
+
return `Agent profile "${profile.name}" (${agentId}) is not permitted to call ${toolName}`;
|
|
66
|
+
}
|
|
67
|
+
if (profile.allowedTools.length > 0 && !profile.allowedTools.includes(toolName)) {
|
|
68
|
+
return `Agent profile "${profile.name}" (${agentId}) is restricted to: ${profile.allowedTools.join(", ")}`;
|
|
69
|
+
}
|
|
70
|
+
if (service && profile.allowedServices.length > 0 && !profile.allowedServices.includes(service)) {
|
|
71
|
+
return `Agent profile "${profile.name}" (${agentId}) is not permitted to target service: ${service}`;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
checkAgentProfile,
|
|
77
|
+
deleteProfile,
|
|
78
|
+
getProfile,
|
|
79
|
+
listProfiles,
|
|
80
|
+
saveProfile
|
|
81
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const AI_AUTHOR_RE = /\b(claude|cursor|copilot|windsurf|codeium|tabnine|devin|ghostwriter|codex)\b/i;
|
|
2
|
+
const AI_SUMMARY_RE = /\b(claude|cursor|copilot|windsurf|codeium|tabnine|devin|ghostwriter|codex)\b|co-authored-by:\s*(claude|cursor|copilot|windsurf|codeium|devin)|generated (by|with) (ai|claude|cursor|copilot)|ai[- ]generated|ai[- ]written|auto[- ]generated by/i;
|
|
3
|
+
function detectAiCommit(summary, author) {
|
|
4
|
+
const text = [summary, author ?? ""].join(" ");
|
|
5
|
+
const authorMatch = author ? AI_AUTHOR_RE.exec(author) : null;
|
|
6
|
+
if (authorMatch) return { detected: true, tool: authorMatch[1].toLowerCase() };
|
|
7
|
+
const summaryMatch = AI_SUMMARY_RE.exec(text);
|
|
8
|
+
if (summaryMatch) {
|
|
9
|
+
const tool = (summaryMatch[1] ?? summaryMatch[2] ?? "ai").toLowerCase();
|
|
10
|
+
return { detected: true, tool };
|
|
11
|
+
}
|
|
12
|
+
return { detected: false, tool: null };
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
detectAiCommit
|
|
16
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { getRuleFirings, getHitlStats } from "./gate-analytics.js";
|
|
2
|
+
import { getStores } from "../storage/store-registry.js";
|
|
3
|
+
const MIN_DECISIONS_FOR_OVERRIDE_RATE = 2;
|
|
4
|
+
const MIN_TRIGGERS_FOR_NOISY = 5;
|
|
5
|
+
const NON_BLOCKING_BLUNDER_TYPES = /* @__PURE__ */ new Set(["credential_issued"]);
|
|
6
|
+
async function computeApprovalAnalytics(topN = 10) {
|
|
7
|
+
const blunders = await getStores().blunders.list();
|
|
8
|
+
const commandCounts = /* @__PURE__ */ new Map();
|
|
9
|
+
for (const b of blunders) {
|
|
10
|
+
if (NON_BLOCKING_BLUNDER_TYPES.has(b.blunderType)) continue;
|
|
11
|
+
const key = b.command ?? "(no command recorded)";
|
|
12
|
+
commandCounts.set(key, (commandCounts.get(key) ?? 0) + 1);
|
|
13
|
+
}
|
|
14
|
+
const topBlockedActions = [...commandCounts.entries()].map(([command, count]) => ({ command, count })).sort((a, b) => b.count - a.count).slice(0, topN);
|
|
15
|
+
const hitlStats = getHitlStats();
|
|
16
|
+
const overriddenRules = [];
|
|
17
|
+
const ruleLatency = [];
|
|
18
|
+
for (const [ruleId, stats] of hitlStats) {
|
|
19
|
+
const total = stats.approvals + stats.denials;
|
|
20
|
+
if (total >= MIN_DECISIONS_FOR_OVERRIDE_RATE) {
|
|
21
|
+
overriddenRules.push({ ruleId, approvals: stats.approvals, denials: stats.denials, overrideRate: Math.round(stats.approvals / total * 1e3) / 1e3 });
|
|
22
|
+
}
|
|
23
|
+
ruleLatency.push({
|
|
24
|
+
ruleId,
|
|
25
|
+
avgApprovalLatencyMs: stats.approvals > 0 ? Math.round(stats.totalApprovalLatencyMs / stats.approvals) : 0,
|
|
26
|
+
avgDenialLatencyMs: stats.denials > 0 ? Math.round(stats.totalDenialLatencyMs / stats.denials) : 0
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
overriddenRules.sort((a, b) => b.overrideRate - a.overrideRate || b.approvals - a.approvals);
|
|
30
|
+
const firings = getRuleFirings();
|
|
31
|
+
const hitlByRule = new Map(overriddenRules.map((r) => [r.ruleId, r.overrideRate]));
|
|
32
|
+
const noisyRules = [...firings.entries()].filter(([, count]) => count >= MIN_TRIGGERS_FOR_NOISY).map(([ruleId, triggerCount]) => ({ ruleId, triggerCount, overrideRate: hitlByRule.get(ruleId) ?? 0 })).sort((a, b) => b.triggerCount * b.overrideRate - a.triggerCount * a.overrideRate);
|
|
33
|
+
return {
|
|
34
|
+
topBlockedActions,
|
|
35
|
+
mostOverriddenRules: overriddenRules.slice(0, topN),
|
|
36
|
+
ruleLatency,
|
|
37
|
+
noisyRules: noisyRules.slice(0, topN)
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
computeApprovalAnalytics
|
|
42
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { buildGraph, getZone } from "./arch-graph.js";
|
|
3
|
+
const DEFAULT_BOUNDARY_RULES = [
|
|
4
|
+
{
|
|
5
|
+
fromZone: "sensor",
|
|
6
|
+
cannotImport: ["routes", "intelligence"],
|
|
7
|
+
rationale: "sensor/* is the data ingestion and storage layer. Importing routes would create a circular dependency; importing intelligence would tie storage to business logic."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
fromZone: "intelligence",
|
|
11
|
+
cannotImport: ["routes"],
|
|
12
|
+
rationale: "intelligence/* (MCP tools, analysis) must not depend on route handlers. Routes consume intelligence, not the other way around."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
fromZone: "datadog",
|
|
16
|
+
cannotImport: ["routes", "intelligence"],
|
|
17
|
+
rationale: "datadog/* is an external integration adapter. It may only depend on the sensor layer (shared data types and storage)."
|
|
18
|
+
}
|
|
19
|
+
];
|
|
20
|
+
function checkBoundaries(opts) {
|
|
21
|
+
const { srcDir, rules = DEFAULT_BOUNDARY_RULES, singleFile } = opts;
|
|
22
|
+
const graph = buildGraph(srcDir);
|
|
23
|
+
const violations = [];
|
|
24
|
+
const ruleset = /* @__PURE__ */ new Map();
|
|
25
|
+
for (const rule of rules) {
|
|
26
|
+
ruleset.set(rule.fromZone, new Set(rule.cannotImport));
|
|
27
|
+
}
|
|
28
|
+
const filesToCheck = singleFile ? [singleFile] : graph.files;
|
|
29
|
+
for (const file of filesToCheck) {
|
|
30
|
+
const fromZone = getZone(file);
|
|
31
|
+
const forbidden = ruleset.get(fromZone);
|
|
32
|
+
if (!forbidden) continue;
|
|
33
|
+
const imports = graph.forward.get(file) ?? /* @__PURE__ */ new Set();
|
|
34
|
+
for (const imported of imports) {
|
|
35
|
+
const importedZone = getZone(imported);
|
|
36
|
+
if (forbidden.has(importedZone)) {
|
|
37
|
+
const rule = rules.find((r) => r.fromZone === fromZone);
|
|
38
|
+
violations.push({
|
|
39
|
+
file,
|
|
40
|
+
fromZone,
|
|
41
|
+
importedFile: imported,
|
|
42
|
+
importedZone,
|
|
43
|
+
rule,
|
|
44
|
+
relativeFile: path.relative(srcDir, file),
|
|
45
|
+
relativeImport: path.relative(srcDir, imported)
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const filesWithViolations = new Set(violations.map((v) => v.file));
|
|
51
|
+
const cleanFiles = filesToCheck.filter((f) => getZone(f) !== "other" && !filesWithViolations.has(f)).length;
|
|
52
|
+
return {
|
|
53
|
+
violations,
|
|
54
|
+
filesChecked: filesToCheck.length,
|
|
55
|
+
rulesApplied: rules.length,
|
|
56
|
+
cleanFiles
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function formatBoundaryReport(result, srcDir) {
|
|
60
|
+
if (result.violations.length === 0) {
|
|
61
|
+
return [
|
|
62
|
+
"## Architectural Boundary Check: PASS \u2705",
|
|
63
|
+
"",
|
|
64
|
+
`${result.filesChecked} files checked \xB7 ${result.rulesApplied} rules applied \xB7 0 violations`
|
|
65
|
+
].join("\n");
|
|
66
|
+
}
|
|
67
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
68
|
+
for (const v of result.violations) {
|
|
69
|
+
const arr = byFile.get(v.relativeFile) ?? [];
|
|
70
|
+
arr.push(v);
|
|
71
|
+
byFile.set(v.relativeFile, arr);
|
|
72
|
+
}
|
|
73
|
+
const lines = [
|
|
74
|
+
`## Architectural Boundary Check: FAIL \u274C`,
|
|
75
|
+
"",
|
|
76
|
+
`${result.violations.length} violation(s) found in ${byFile.size} file(s). ${result.filesChecked} files checked \xB7 ${result.rulesApplied} rules applied.`,
|
|
77
|
+
""
|
|
78
|
+
];
|
|
79
|
+
for (const [file, vs] of byFile) {
|
|
80
|
+
lines.push(`### \`${file}\``);
|
|
81
|
+
for (const v of vs) {
|
|
82
|
+
lines.push(` - \u274C **${v.fromZone}** \u2192 **${v.importedZone}**: imports \`${v.relativeImport}\``);
|
|
83
|
+
lines.push(` > ${v.rule.rationale}`);
|
|
84
|
+
}
|
|
85
|
+
lines.push("");
|
|
86
|
+
}
|
|
87
|
+
lines.push("**Fix:** Refactor so that dependencies only flow in the permitted direction (sensor \u2190 intelligence \u2190 routes).");
|
|
88
|
+
return lines.join("\n");
|
|
89
|
+
}
|
|
90
|
+
export {
|
|
91
|
+
DEFAULT_BOUNDARY_RULES,
|
|
92
|
+
checkBoundaries,
|
|
93
|
+
formatBoundaryReport
|
|
94
|
+
};
|