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,231 @@
|
|
|
1
|
+
import logger from "../sensor/logger.js";
|
|
2
|
+
const SESSION_RING_SIZE = 20;
|
|
3
|
+
const SESSION_EXPIRY_MS = 30 * 60 * 1e3;
|
|
4
|
+
const _sessions = /* @__PURE__ */ new Map();
|
|
5
|
+
function _getOrCreate(sessionId) {
|
|
6
|
+
let s = _sessions.get(sessionId);
|
|
7
|
+
if (!s) {
|
|
8
|
+
s = { calls: [], contaminatedCallsRemaining: 0, contaminationSource: "", lastActivity: Date.now() };
|
|
9
|
+
_sessions.set(sessionId, s);
|
|
10
|
+
void _hydrateContamination(sessionId);
|
|
11
|
+
}
|
|
12
|
+
s.lastActivity = Date.now();
|
|
13
|
+
return s;
|
|
14
|
+
}
|
|
15
|
+
const _pruneHandle = setInterval(() => {
|
|
16
|
+
const cutoff = Date.now() - SESSION_EXPIRY_MS;
|
|
17
|
+
for (const [id, s] of _sessions) {
|
|
18
|
+
if (s.lastActivity < cutoff) _sessions.delete(id);
|
|
19
|
+
}
|
|
20
|
+
}, 60 * 60 * 1e3);
|
|
21
|
+
_pruneHandle.unref();
|
|
22
|
+
const THREAT_SEQUENCES = [
|
|
23
|
+
{
|
|
24
|
+
steps: [/env|printenv|cat\s+\.env/i, /curl\b|wget\b|nc\b|netcat\b/i],
|
|
25
|
+
label: "credential_exfil_chain"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
steps: [/rm.*log|delete.*log|clear.*log/i, /rm\s+-rf/i],
|
|
29
|
+
label: "cover_tracks_chain"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
steps: [/select.*from|describe\s+table|show\s+tables/i, /drop\s+table|truncate\s+table/i],
|
|
33
|
+
label: "db_recon_destroy_chain"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
steps: [/kubectl\s+get|kubectl\s+describe/i, /kubectl\s+delete|terraform\s+destroy/i],
|
|
37
|
+
label: "infra_recon_destroy_chain"
|
|
38
|
+
},
|
|
39
|
+
// git history read → exfil: agent dumps commit history or secrets then ships them out
|
|
40
|
+
{
|
|
41
|
+
steps: [/git\s+log|git\s+show|git\s+diff\b/i, /curl\b|wget\b|nc\b|netcat\b/i],
|
|
42
|
+
label: "git_history_exfil_chain"
|
|
43
|
+
},
|
|
44
|
+
// AWS identity probe → privilege escalation via role assumption or IAM mutation
|
|
45
|
+
{
|
|
46
|
+
steps: [/aws\s+sts\s+get-caller-identity|aws\s+iam\s+list/i, /aws\s+sts\s+assume-role|aws\s+iam\s+attach|aws\s+iam\s+put/i],
|
|
47
|
+
label: "aws_priv_esc_chain"
|
|
48
|
+
},
|
|
49
|
+
// /etc/passwd or /etc/shadow read → exfil of system credentials
|
|
50
|
+
{
|
|
51
|
+
steps: [/cat\s+\/etc\/passwd|cat\s+\/etc\/shadow|\/etc\/shadow/i, /curl\b|wget\b|nc\b|netcat\b/i],
|
|
52
|
+
label: "system_cred_exfil_chain"
|
|
53
|
+
},
|
|
54
|
+
// setuid/world-writable chmod → execute as privileged user
|
|
55
|
+
{
|
|
56
|
+
steps: [/chmod\s+(777|[0-9]*[47][0-9]*s|[ugo]\+s)/i, /\.\/|\bbash\b|\bsh\b|\bexec\b/i],
|
|
57
|
+
label: "suid_escalation_chain"
|
|
58
|
+
}
|
|
59
|
+
];
|
|
60
|
+
function recordSessionCall(sessionId, toolName, commandArg, verdict) {
|
|
61
|
+
const s = _getOrCreate(sessionId);
|
|
62
|
+
s.calls.push({ toolName, commandArg, verdict, ts: Date.now() });
|
|
63
|
+
if (s.calls.length > SESSION_RING_SIZE) s.calls.shift();
|
|
64
|
+
if (s.contaminatedCallsRemaining > 0) s.contaminatedCallsRemaining--;
|
|
65
|
+
}
|
|
66
|
+
function detectSequenceThreat(sessionId, currentCommandArg) {
|
|
67
|
+
const s = _sessions.get(sessionId);
|
|
68
|
+
if (!s || s.calls.length === 0) return { threat: false, label: null };
|
|
69
|
+
const history = s.calls.map((c) => c.commandArg);
|
|
70
|
+
for (const seq of THREAT_SEQUENCES) {
|
|
71
|
+
if (seq.steps.length < 2) continue;
|
|
72
|
+
const lastStep = seq.steps[seq.steps.length - 1];
|
|
73
|
+
if (!lastStep.test(currentCommandArg)) continue;
|
|
74
|
+
let stepIdx = seq.steps.length - 2;
|
|
75
|
+
for (let i = history.length - 1; i >= 0 && stepIdx >= 0; i--) {
|
|
76
|
+
if (seq.steps[stepIdx].test(history[i])) stepIdx--;
|
|
77
|
+
}
|
|
78
|
+
if (stepIdx < 0) {
|
|
79
|
+
logger.warn({ sessionId, label: seq.label }, "session-threat-tracker: multi-turn threat sequence detected");
|
|
80
|
+
return { threat: true, label: seq.label };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { threat: false, label: null };
|
|
84
|
+
}
|
|
85
|
+
function markContaminated(sessionId, source, callCount) {
|
|
86
|
+
const s = _getOrCreate(sessionId);
|
|
87
|
+
s.contaminatedCallsRemaining = callCount;
|
|
88
|
+
s.contaminationSource = source;
|
|
89
|
+
logger.info({ sessionId, source, callCount }, "session-threat-tracker: session marked contaminated");
|
|
90
|
+
void _persistContamination(sessionId, source, callCount);
|
|
91
|
+
}
|
|
92
|
+
async function _persistContamination(sessionId, source, remaining) {
|
|
93
|
+
try {
|
|
94
|
+
const { agentContextStore } = await import("../sensor/agent-context-store.js");
|
|
95
|
+
const expiresAt = Date.now() + SESSION_EXPIRY_MS;
|
|
96
|
+
agentContextStore.store(
|
|
97
|
+
sessionId,
|
|
98
|
+
"contamination_v1",
|
|
99
|
+
JSON.stringify({ source, remaining, expiresAt }),
|
|
100
|
+
SESSION_EXPIRY_MS
|
|
101
|
+
);
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function _hydrateContamination(sessionId) {
|
|
106
|
+
try {
|
|
107
|
+
const { agentContextStore } = await import("../sensor/agent-context-store.js");
|
|
108
|
+
const entries = agentContextStore.recall(sessionId, "contamination_v1", 1);
|
|
109
|
+
if (entries.length === 0) return;
|
|
110
|
+
const { source, remaining, expiresAt } = JSON.parse(entries[0].value);
|
|
111
|
+
if (Date.now() > expiresAt || remaining <= 0) return;
|
|
112
|
+
const s = _getOrCreate(sessionId);
|
|
113
|
+
s.contaminatedCallsRemaining = remaining;
|
|
114
|
+
s.contaminationSource = source;
|
|
115
|
+
logger.info({ sessionId, remaining }, "session-threat-tracker: contamination state restored from store");
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function isSessionContaminated(sessionId) {
|
|
120
|
+
const s = _sessions.get(sessionId);
|
|
121
|
+
return s ? s.contaminatedCallsRemaining > 0 : false;
|
|
122
|
+
}
|
|
123
|
+
function getContaminationSource(sessionId) {
|
|
124
|
+
const s = _sessions.get(sessionId);
|
|
125
|
+
return s && s.contaminatedCallsRemaining > 0 ? s.contaminationSource || null : null;
|
|
126
|
+
}
|
|
127
|
+
const _reputations = /* @__PURE__ */ new Map();
|
|
128
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
129
|
+
const GENERIC_AGENT_ID = "agent";
|
|
130
|
+
function _defaultReputation() {
|
|
131
|
+
return { blockCount24h: 0, injectionAttempts: 0, sequenceThreats: 0, scrutinyTier: "normal", windowStart: Date.now() };
|
|
132
|
+
}
|
|
133
|
+
function _loadReputation(agentId) {
|
|
134
|
+
if (!_reputations.has(agentId)) _reputations.set(agentId, _defaultReputation());
|
|
135
|
+
return _reputations.get(agentId);
|
|
136
|
+
}
|
|
137
|
+
function _computeTier(rep) {
|
|
138
|
+
if (rep.injectionAttempts >= 2 || rep.sequenceThreats >= 1) return "high";
|
|
139
|
+
if (rep.blockCount24h >= 3) return "elevated";
|
|
140
|
+
return "normal";
|
|
141
|
+
}
|
|
142
|
+
function updateAgentReputation(agentId, eventType) {
|
|
143
|
+
if (!agentId || agentId === GENERIC_AGENT_ID) return;
|
|
144
|
+
const rep = _loadReputation(agentId);
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
if (now - rep.windowStart > DAY_MS) {
|
|
147
|
+
rep.blockCount24h = 0;
|
|
148
|
+
rep.windowStart = now;
|
|
149
|
+
}
|
|
150
|
+
if (eventType === "block") rep.blockCount24h++;
|
|
151
|
+
if (eventType === "injection") rep.injectionAttempts++;
|
|
152
|
+
if (eventType === "sequence") rep.sequenceThreats++;
|
|
153
|
+
rep.scrutinyTier = _computeTier(rep);
|
|
154
|
+
void _persistReputation(agentId, rep);
|
|
155
|
+
}
|
|
156
|
+
function getAgentScrutinyTier(agentId) {
|
|
157
|
+
if (!agentId || agentId === GENERIC_AGENT_ID) return "normal";
|
|
158
|
+
return _loadReputation(agentId).scrutinyTier;
|
|
159
|
+
}
|
|
160
|
+
async function _persistReputation(agentId, rep) {
|
|
161
|
+
try {
|
|
162
|
+
const { agentContextStore } = await import("../sensor/agent-context-store.js");
|
|
163
|
+
agentContextStore.store(agentId, "reputation_v1", JSON.stringify(rep), 0);
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function hydrateReputation(agentId) {
|
|
168
|
+
if (!agentId || agentId === GENERIC_AGENT_ID || _reputations.has(agentId)) return;
|
|
169
|
+
try {
|
|
170
|
+
const { agentContextStore } = await import("../sensor/agent-context-store.js");
|
|
171
|
+
const entries = agentContextStore.recall(agentId, "reputation_v1", 1);
|
|
172
|
+
if (entries.length > 0) {
|
|
173
|
+
const rep = JSON.parse(entries[0].value);
|
|
174
|
+
_reputations.set(agentId, rep);
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
|
|
180
|
+
"analyze_runtime",
|
|
181
|
+
"get_recent_logs",
|
|
182
|
+
"get_network_activity",
|
|
183
|
+
"get_unified_timeline",
|
|
184
|
+
"validate_fix",
|
|
185
|
+
"clear_buffer"
|
|
186
|
+
]);
|
|
187
|
+
function _resetSessionsForTesting() {
|
|
188
|
+
_sessions.clear();
|
|
189
|
+
_reputations.clear();
|
|
190
|
+
}
|
|
191
|
+
function getSessionCallRing(sessionId) {
|
|
192
|
+
return _sessions.get(sessionId)?.calls ?? [];
|
|
193
|
+
}
|
|
194
|
+
function getSessionContaminationState(sessionId) {
|
|
195
|
+
const s = _sessions.get(sessionId);
|
|
196
|
+
return {
|
|
197
|
+
contaminated: (s?.contaminatedCallsRemaining ?? 0) > 0,
|
|
198
|
+
source: s?.contaminationSource ?? "",
|
|
199
|
+
callsRemaining: s?.contaminatedCallsRemaining ?? 0
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function getDetectedSequences(sessionId) {
|
|
203
|
+
const s = _sessions.get(sessionId);
|
|
204
|
+
if (!s || s.calls.length < 2) return [];
|
|
205
|
+
const detected = [];
|
|
206
|
+
const history = s.calls.map((c) => c.commandArg);
|
|
207
|
+
for (const seq of THREAT_SEQUENCES) {
|
|
208
|
+
if (seq.steps.length < 2) continue;
|
|
209
|
+
let stepIdx = seq.steps.length - 1;
|
|
210
|
+
for (let i = history.length - 1; i >= 0 && stepIdx >= 0; i--) {
|
|
211
|
+
if (seq.steps[stepIdx].test(history[i])) stepIdx--;
|
|
212
|
+
}
|
|
213
|
+
if (stepIdx < 0) detected.push(seq.label);
|
|
214
|
+
}
|
|
215
|
+
return detected;
|
|
216
|
+
}
|
|
217
|
+
export {
|
|
218
|
+
READ_ONLY_TOOLS,
|
|
219
|
+
_resetSessionsForTesting,
|
|
220
|
+
detectSequenceThreat,
|
|
221
|
+
getAgentScrutinyTier,
|
|
222
|
+
getContaminationSource,
|
|
223
|
+
getDetectedSequences,
|
|
224
|
+
getSessionCallRing,
|
|
225
|
+
getSessionContaminationState,
|
|
226
|
+
hydrateReputation,
|
|
227
|
+
isSessionContaminated,
|
|
228
|
+
markContaminated,
|
|
229
|
+
recordSessionCall,
|
|
230
|
+
updateAgentReputation
|
|
231
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { getStores } from "../storage/store-registry.js";
|
|
2
|
+
import logger from "../sensor/logger.js";
|
|
3
|
+
const SYSTEM_TENANT = process.env.MERGEN_SYSTEM_TENANT_ID;
|
|
4
|
+
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
5
|
+
function msUntilNextMonday0900Utc() {
|
|
6
|
+
const now = /* @__PURE__ */ new Date();
|
|
7
|
+
const day = now.getUTCDay();
|
|
8
|
+
const daysUntilMonday = day === 1 ? 7 : (8 - day) % 7 || 7;
|
|
9
|
+
const next = new Date(Date.UTC(
|
|
10
|
+
now.getUTCFullYear(),
|
|
11
|
+
now.getUTCMonth(),
|
|
12
|
+
now.getUTCDate() + daysUntilMonday,
|
|
13
|
+
9,
|
|
14
|
+
0,
|
|
15
|
+
0,
|
|
16
|
+
0
|
|
17
|
+
));
|
|
18
|
+
return Math.max(next.getTime() - Date.now(), 0);
|
|
19
|
+
}
|
|
20
|
+
async function postDigest() {
|
|
21
|
+
const token = process.env.MERGEN_SLACK_BOT_TOKEN;
|
|
22
|
+
const channel = process.env.MERGEN_SLACK_DIGEST_CHANNEL ?? process.env.MERGEN_SLACK_CHANNEL;
|
|
23
|
+
if (!token || !channel) {
|
|
24
|
+
logger.debug("shadow-digest-cron: no Slack token or channel \u2014 skipping digest");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const blocks = await getStores().shadowLog.getShadowSlackDigest(7, SYSTEM_TENANT);
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch("https://slack.com/api/chat.postMessage", {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: {
|
|
32
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
33
|
+
Authorization: `Bearer ${token}`
|
|
34
|
+
},
|
|
35
|
+
body: JSON.stringify({ channel, blocks })
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
logger.warn({ status: res.status }, "shadow-digest-cron: HTTP error posting to Slack");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const json = await res.json();
|
|
42
|
+
if (!json.ok) {
|
|
43
|
+
logger.warn({ slackError: json.error }, "shadow-digest-cron: Slack API error");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
logger.info({ channel }, "shadow-digest-cron: weekly digest posted");
|
|
47
|
+
} catch (err) {
|
|
48
|
+
logger.warn({ err }, "shadow-digest-cron: failed to post digest");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function startShadowDigestCron() {
|
|
52
|
+
const isShadowMode = process.env.MERGEN_SHADOW_MODE === "true";
|
|
53
|
+
const isAutopilot = process.env.MERGEN_AUTOPILOT === "true";
|
|
54
|
+
const hasSlackToken = !!process.env.MERGEN_SLACK_BOT_TOKEN;
|
|
55
|
+
const hasChannel = !!(process.env.MERGEN_SLACK_DIGEST_CHANNEL ?? process.env.MERGEN_SLACK_CHANNEL);
|
|
56
|
+
if (!isShadowMode && !isAutopilot || !hasSlackToken || !hasChannel) {
|
|
57
|
+
return () => {
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const delay = msUntilNextMonday0900Utc();
|
|
61
|
+
logger.info(
|
|
62
|
+
{ nextFireInHours: Math.round(delay / 36e5) },
|
|
63
|
+
"shadow-digest-cron: scheduled (Monday 09:00 UTC)"
|
|
64
|
+
);
|
|
65
|
+
let intervalHandle = null;
|
|
66
|
+
const timeoutHandle = setTimeout(() => {
|
|
67
|
+
void postDigest();
|
|
68
|
+
intervalHandle = setInterval(() => {
|
|
69
|
+
void postDigest();
|
|
70
|
+
}, SEVEN_DAYS_MS);
|
|
71
|
+
intervalHandle.unref();
|
|
72
|
+
}, delay);
|
|
73
|
+
timeoutHandle.unref();
|
|
74
|
+
return () => {
|
|
75
|
+
clearTimeout(timeoutHandle);
|
|
76
|
+
if (intervalHandle) clearInterval(intervalHandle);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export {
|
|
80
|
+
startShadowDigestCron
|
|
81
|
+
};
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { lockAndExecute } from "../sensor/file-lock.js";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
import { SHADOW_LOG_FILE, DATA_DIR } from "../sensor/paths.js";
|
|
5
|
+
import { recordOverride } from "./override-corpus.js";
|
|
6
|
+
import logger from "../sensor/logger.js";
|
|
7
|
+
const MAX_ENTRIES = 500;
|
|
8
|
+
let _entries = [];
|
|
9
|
+
let _loaded = false;
|
|
10
|
+
function load(force = false) {
|
|
11
|
+
if (_loaded && !force) return;
|
|
12
|
+
if (!fs.existsSync(SHADOW_LOG_FILE)) {
|
|
13
|
+
_entries = [];
|
|
14
|
+
_loaded = true;
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const raw = fs.readFileSync(SHADOW_LOG_FILE, "utf8");
|
|
19
|
+
const parsed = JSON.parse(raw);
|
|
20
|
+
if (parsed?.version === 1 && Array.isArray(parsed.entries)) {
|
|
21
|
+
_entries = parsed.entries.slice(-MAX_ENTRIES);
|
|
22
|
+
} else {
|
|
23
|
+
_entries = [];
|
|
24
|
+
}
|
|
25
|
+
_loaded = true;
|
|
26
|
+
} catch (err) {
|
|
27
|
+
logger.warn({ err }, "shadow-log: failed to load \u2014 starting fresh");
|
|
28
|
+
_entries = [];
|
|
29
|
+
_loaded = true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
let _tmpCounter = 0;
|
|
33
|
+
function persist() {
|
|
34
|
+
try {
|
|
35
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
36
|
+
const payload = { version: 1, entries: _entries };
|
|
37
|
+
_tmpCounter = _tmpCounter + 1 >>> 0;
|
|
38
|
+
const tmp = `${SHADOW_LOG_FILE}.tmp.${process.pid}.${Date.now().toString(36)}.${_tmpCounter.toString(36)}`;
|
|
39
|
+
fs.writeFileSync(tmp, JSON.stringify(payload), "utf8");
|
|
40
|
+
fs.renameSync(tmp, SHADOW_LOG_FILE);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
logger.warn({ err }, "shadow-log: failed to persist");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function recordShadow(input) {
|
|
46
|
+
const entry = {
|
|
47
|
+
...input,
|
|
48
|
+
id: input.id ?? randomUUID(),
|
|
49
|
+
recordedAt: Date.now()
|
|
50
|
+
};
|
|
51
|
+
return lockAndExecute(`${SHADOW_LOG_FILE}.lock`, () => {
|
|
52
|
+
load(true);
|
|
53
|
+
_entries.push(entry);
|
|
54
|
+
if (_entries.length > MAX_ENTRIES) _entries = _entries.slice(-MAX_ENTRIES);
|
|
55
|
+
persist();
|
|
56
|
+
logger.info(
|
|
57
|
+
{ id: entry.id, tag: entry.incidentTag, skip: entry.skipReason, wouldRun: entry.wouldHaveExecuted },
|
|
58
|
+
"shadow-log: entry recorded"
|
|
59
|
+
);
|
|
60
|
+
return entry;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function recordShadowVerdict(id, verdict, opts = {}) {
|
|
64
|
+
return lockAndExecute(`${SHADOW_LOG_FILE}.lock`, () => {
|
|
65
|
+
load(true);
|
|
66
|
+
const entry = _entries.find((e) => e.id === id);
|
|
67
|
+
if (!entry) return { found: false };
|
|
68
|
+
entry.humanVerdict = verdict;
|
|
69
|
+
entry.verdictAt = Date.now();
|
|
70
|
+
if (opts.note) entry.humanNote = opts.note.slice(0, 200);
|
|
71
|
+
let overrideId;
|
|
72
|
+
if (verdict === "would-override" && entry.command) {
|
|
73
|
+
const ov = recordOverride({
|
|
74
|
+
incidentTag: entry.incidentTag,
|
|
75
|
+
proposedCommand: entry.command,
|
|
76
|
+
overrideReason: opts.overrideReason ?? "on-call-discretion",
|
|
77
|
+
note: opts.note,
|
|
78
|
+
service: entry.service,
|
|
79
|
+
environment: "production",
|
|
80
|
+
manualAction: opts.manualAction,
|
|
81
|
+
actor: opts.actor ?? "shadow-review"
|
|
82
|
+
});
|
|
83
|
+
overrideId = ov.id;
|
|
84
|
+
entry.overrideId = ov.id;
|
|
85
|
+
}
|
|
86
|
+
persist();
|
|
87
|
+
return { found: true, entry, overrideId };
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function updateShadowReasonByPid(pid, skipReason) {
|
|
91
|
+
lockAndExecute(`${SHADOW_LOG_FILE}.lock`, () => {
|
|
92
|
+
load(true);
|
|
93
|
+
const entry = _entries.find((e) => e.pid === pid);
|
|
94
|
+
if (entry) {
|
|
95
|
+
entry.skipReason = skipReason;
|
|
96
|
+
persist();
|
|
97
|
+
logger.info({ pid, skipReason }, "shadow-log: entry updated by pid");
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function getShadowLog() {
|
|
102
|
+
load(true);
|
|
103
|
+
return _entries;
|
|
104
|
+
}
|
|
105
|
+
function computeShadowReport(entries, windowDays = 30) {
|
|
106
|
+
const cutoff = Date.now() - windowDays * 24 * 60 * 60 * 1e3;
|
|
107
|
+
const window = entries.filter((e) => e.recordedAt >= cutoff);
|
|
108
|
+
const wouldHaveExecuted = window.filter((e) => e.wouldHaveExecuted).length;
|
|
109
|
+
const skippedByLowConfidence = window.filter(
|
|
110
|
+
(e) => e.skipReason === "confidence-below-threshold" || e.skipReason === "remediation-below-threshold"
|
|
111
|
+
).length;
|
|
112
|
+
const skippedByOverrideCorpus = window.filter((e) => e.skipReason === "override-corpus").length;
|
|
113
|
+
const skippedNoCommand = window.filter((e) => e.skipReason === "no-command").length;
|
|
114
|
+
const reviewed = window.filter((e) => e.humanVerdict !== void 0);
|
|
115
|
+
const approved = reviewed.filter((e) => e.humanVerdict === "would-approve").length;
|
|
116
|
+
const overrode = reviewed.filter((e) => e.humanVerdict === "would-override").length;
|
|
117
|
+
const approvalRate = reviewed.length >= 3 ? approved / reviewed.length : null;
|
|
118
|
+
const interventionRate = reviewed.length >= 3 ? overrode / reviewed.length : null;
|
|
119
|
+
const annotationCoverage = window.length > 0 ? reviewed.length / window.length : 0;
|
|
120
|
+
const safetyBlocked = window.filter((e) => e.skipReason === "blocked-by-safety-filter");
|
|
121
|
+
const annotatedSafetyBlocked = safetyBlocked.filter((e) => e.humanVerdict !== void 0);
|
|
122
|
+
const safetyBlockedApproved = annotatedSafetyBlocked.filter((e) => e.humanVerdict === "would-approve").length;
|
|
123
|
+
const gateFalsePositiveRate = annotatedSafetyBlocked.length >= 3 ? safetyBlockedApproved / annotatedSafetyBlocked.length : null;
|
|
124
|
+
let recommendation;
|
|
125
|
+
if (window.length === 0) {
|
|
126
|
+
recommendation = "No shadow entries yet. Enable MERGEN_SHADOW_MODE=true to start collecting recommendations.";
|
|
127
|
+
} else if (approvalRate === null) {
|
|
128
|
+
recommendation = `${reviewed.length} of ${window.length} entries reviewed. Review more shadow entries to get a reliable approval rate.`;
|
|
129
|
+
} else if (approvalRate >= 0.85) {
|
|
130
|
+
recommendation = `${Math.round(approvalRate * 100)}% approval rate over ${windowDays} days. Consider enabling MERGEN_AUTOPILOT_LEVEL=restarts.`;
|
|
131
|
+
} else if (approvalRate >= 0.7) {
|
|
132
|
+
recommendation = `${Math.round(approvalRate * 100)}% approval rate. Review override reasons before enabling autopilot \u2014 some patterns may need corpus entries.`;
|
|
133
|
+
} else {
|
|
134
|
+
recommendation = `${Math.round(approvalRate * 100)}% approval rate \u2014 below threshold. Review wrong recommendations and add override corpus entries for recurring patterns.`;
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
windowDays,
|
|
138
|
+
total: window.length,
|
|
139
|
+
wouldHaveExecuted,
|
|
140
|
+
skippedByLowConfidence,
|
|
141
|
+
skippedByOverrideCorpus,
|
|
142
|
+
skippedNoCommand,
|
|
143
|
+
humanReviewed: reviewed.length,
|
|
144
|
+
humanApproved: approved,
|
|
145
|
+
humanOverrode: overrode,
|
|
146
|
+
approvalRate,
|
|
147
|
+
interventionRate,
|
|
148
|
+
gateFalsePositiveRate,
|
|
149
|
+
annotationCoverage,
|
|
150
|
+
recommendation
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function getShadowReport(windowDays = 30) {
|
|
154
|
+
load();
|
|
155
|
+
return computeShadowReport(_entries, windowDays);
|
|
156
|
+
}
|
|
157
|
+
function computeShadowSlackDigest(entries, windowDays = 7) {
|
|
158
|
+
const report = computeShadowReport(entries, windowDays);
|
|
159
|
+
const ratePct = report.approvalRate !== null ? `${Math.round(report.approvalRate * 100)}%` : "n/a";
|
|
160
|
+
const reviewedNote = report.humanReviewed > 0 ? ` | ${report.humanApproved} approved / ${report.humanOverrode} overrode` : "";
|
|
161
|
+
const cutoff = Date.now() - windowDays * 24 * 60 * 60 * 1e3;
|
|
162
|
+
const pending = entries.filter((e) => e.recordedAt >= cutoff && e.wouldHaveExecuted && e.humanVerdict === void 0).slice(-5).reverse();
|
|
163
|
+
const entryBlocks = pending.flatMap((e) => {
|
|
164
|
+
const diagPct = Math.round(e.diagnosisConfidence * 100);
|
|
165
|
+
const remPct = Math.round(e.remediationConfidence * 100);
|
|
166
|
+
const cmd = e.command ? `\`${e.command}\`` : "_no command_";
|
|
167
|
+
return [
|
|
168
|
+
{ type: "divider" },
|
|
169
|
+
{
|
|
170
|
+
type: "section",
|
|
171
|
+
text: {
|
|
172
|
+
type: "mrkdwn",
|
|
173
|
+
text: `*${e.service}* \xB7 \`${e.incidentTag}\`
|
|
174
|
+
Diagnosis: ${diagPct}% \xB7 Remediation: ${remPct}%
|
|
175
|
+
Would have run: ${cmd}`
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
type: "actions",
|
|
180
|
+
elements: [
|
|
181
|
+
{
|
|
182
|
+
type: "button",
|
|
183
|
+
text: { type: "plain_text", text: "\u{1F44D} Would approve", emoji: true },
|
|
184
|
+
action_id: `digest_approve_${e.id}`,
|
|
185
|
+
value: JSON.stringify({ id: e.id }),
|
|
186
|
+
style: "primary"
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
type: "button",
|
|
190
|
+
text: { type: "plain_text", text: "\u270B Would override", emoji: true },
|
|
191
|
+
action_id: `digest_override_${e.id}`,
|
|
192
|
+
value: JSON.stringify({ id: e.id }),
|
|
193
|
+
style: "danger"
|
|
194
|
+
}
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
];
|
|
198
|
+
});
|
|
199
|
+
return {
|
|
200
|
+
blocks: [
|
|
201
|
+
{
|
|
202
|
+
type: "header",
|
|
203
|
+
text: { type: "plain_text", text: "\u{1F50D} Mergen Shadow Mode \u2014 Weekly Summary" }
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
type: "section",
|
|
207
|
+
fields: [
|
|
208
|
+
{ type: "mrkdwn", text: `*Recommendations (${windowDays}d)*
|
|
209
|
+
${report.total}` },
|
|
210
|
+
{ type: "mrkdwn", text: `*Would-execute (high confidence)*
|
|
211
|
+
${report.wouldHaveExecuted}` },
|
|
212
|
+
{ type: "mrkdwn", text: `*Human approval rate*
|
|
213
|
+
${ratePct}${reviewedNote}` },
|
|
214
|
+
{ type: "mrkdwn", text: `*Override corpus blocks*
|
|
215
|
+
${report.skippedByOverrideCorpus}` }
|
|
216
|
+
]
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
type: "section",
|
|
220
|
+
text: { type: "mrkdwn", text: `*Next step:* ${report.recommendation}` }
|
|
221
|
+
},
|
|
222
|
+
...pending.length > 0 ? [
|
|
223
|
+
{
|
|
224
|
+
type: "section",
|
|
225
|
+
text: { type: "mrkdwn", text: `*${pending.length} pending review${pending.length > 1 ? "s" : ""}* \u2014 click below to calibrate:` }
|
|
226
|
+
},
|
|
227
|
+
...entryBlocks
|
|
228
|
+
] : [],
|
|
229
|
+
{
|
|
230
|
+
type: "actions",
|
|
231
|
+
elements: [
|
|
232
|
+
{
|
|
233
|
+
type: "button",
|
|
234
|
+
text: { type: "plain_text", text: "View full shadow report" },
|
|
235
|
+
url: "http://127.0.0.1:3000/shadow-report"
|
|
236
|
+
}
|
|
237
|
+
]
|
|
238
|
+
}
|
|
239
|
+
]
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function getShadowSlackDigest(windowDays = 7) {
|
|
243
|
+
load();
|
|
244
|
+
return computeShadowSlackDigest(_entries, windowDays);
|
|
245
|
+
}
|
|
246
|
+
function computeShadowCsv(entries) {
|
|
247
|
+
const header = "id,pid,incidentTag,service,command,diagnosisConfidence,remediationConfidence,wouldHaveExecuted,skipReason,recordedAt,humanVerdict,verdictAt,humanNote";
|
|
248
|
+
const escape = (v) => {
|
|
249
|
+
if (/[",\r\n]/.test(v)) return `"${v.replace(/"/g, '""')}"`;
|
|
250
|
+
return v;
|
|
251
|
+
};
|
|
252
|
+
const rows = entries.map((e) => [
|
|
253
|
+
e.id,
|
|
254
|
+
e.pid,
|
|
255
|
+
e.incidentTag,
|
|
256
|
+
e.service,
|
|
257
|
+
e.command ?? "",
|
|
258
|
+
e.diagnosisConfidence,
|
|
259
|
+
e.remediationConfidence,
|
|
260
|
+
e.wouldHaveExecuted ? "true" : "false",
|
|
261
|
+
e.skipReason,
|
|
262
|
+
new Date(e.recordedAt).toISOString(),
|
|
263
|
+
e.humanVerdict ?? "",
|
|
264
|
+
e.verdictAt ? new Date(e.verdictAt).toISOString() : "",
|
|
265
|
+
e.humanNote ?? ""
|
|
266
|
+
].map((c) => escape(String(c))).join(","));
|
|
267
|
+
return [header, ...rows].join("\n") + (rows.length > 0 ? "\n" : "");
|
|
268
|
+
}
|
|
269
|
+
function exportShadowCsv() {
|
|
270
|
+
load();
|
|
271
|
+
return computeShadowCsv(_entries);
|
|
272
|
+
}
|
|
273
|
+
export {
|
|
274
|
+
computeShadowCsv,
|
|
275
|
+
computeShadowReport,
|
|
276
|
+
computeShadowSlackDigest,
|
|
277
|
+
exportShadowCsv,
|
|
278
|
+
getShadowLog,
|
|
279
|
+
getShadowReport,
|
|
280
|
+
getShadowSlackDigest,
|
|
281
|
+
recordShadow,
|
|
282
|
+
recordShadowVerdict,
|
|
283
|
+
updateShadowReasonByPid
|
|
284
|
+
};
|