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,447 @@
|
|
|
1
|
+
import { randomUUID, createHash } from "crypto";
|
|
2
|
+
import { getSql } from "./pg-client.js";
|
|
3
|
+
import { dominantCommandSignatures, overridePackKey } from "../../intelligence/override-corpus.js";
|
|
4
|
+
const DEFAULT_TENANT = "local";
|
|
5
|
+
const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
6
|
+
function rowToOverrideEvent(row) {
|
|
7
|
+
return {
|
|
8
|
+
id: String(row.id ?? ""),
|
|
9
|
+
incidentTag: String(row.incident_tag ?? ""),
|
|
10
|
+
proposedCommand: String(row.proposed_command ?? ""),
|
|
11
|
+
overrideReason: row.override_reason ?? "other",
|
|
12
|
+
note: row.note ? String(row.note) : void 0,
|
|
13
|
+
service: String(row.service ?? ""),
|
|
14
|
+
environment: String(row.environment ?? "production"),
|
|
15
|
+
dayOfWeek: Number(row.day_of_week ?? 0),
|
|
16
|
+
hourOfDay: Number(row.hour_of_day ?? 0),
|
|
17
|
+
manualAction: row.manual_action ? String(row.manual_action) : void 0,
|
|
18
|
+
outcome: row.outcome ? row.outcome : void 0,
|
|
19
|
+
rationale: row.rationale ? String(row.rationale) : void 0,
|
|
20
|
+
source: row.source === "community" || row.source === "team" ? row.source : void 0,
|
|
21
|
+
recordedAt: row.recorded_at instanceof Date ? row.recorded_at.getTime() : new Date(String(row.recorded_at ?? 0)).getTime(),
|
|
22
|
+
reviewedAt: row.reviewed_at == null ? void 0 : row.reviewed_at instanceof Date ? row.reviewed_at.getTime() : new Date(String(row.reviewed_at)).getTime(),
|
|
23
|
+
// NULL = permanent, mapped to 0 (the file store's explicit "permanent" marker).
|
|
24
|
+
expiresAt: row.expires_at == null ? 0 : row.expires_at instanceof Date ? row.expires_at.getTime() : new Date(String(row.expires_at)).getTime(),
|
|
25
|
+
actor: String(row.actor ?? "")
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
class PgOverrideCorpus {
|
|
29
|
+
async recordOverride(input, tenantId) {
|
|
30
|
+
const sql = getSql();
|
|
31
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
32
|
+
const now = /* @__PURE__ */ new Date();
|
|
33
|
+
const id = randomUUID();
|
|
34
|
+
const lastRows = await sql`
|
|
35
|
+
SELECT hash FROM override_corpus
|
|
36
|
+
WHERE tenant_id = ${tid}
|
|
37
|
+
ORDER BY recorded_at DESC, id DESC
|
|
38
|
+
LIMIT 1
|
|
39
|
+
`;
|
|
40
|
+
const prevHash = lastRows.length > 0 && lastRows[0].hash ? String(lastRows[0].hash) : "genesis_override";
|
|
41
|
+
const payload = [
|
|
42
|
+
id,
|
|
43
|
+
input.incidentTag,
|
|
44
|
+
input.proposedCommand.slice(0, 500),
|
|
45
|
+
input.overrideReason,
|
|
46
|
+
input.service,
|
|
47
|
+
input.actor
|
|
48
|
+
].join("|");
|
|
49
|
+
const hash = createHash("sha256").update(payload + prevHash).digest("hex");
|
|
50
|
+
const expiresAt = input.expiresAt === 0 ? null : new Date(input.expiresAt ?? now.getTime() + 90 * 24 * 60 * 60 * 1e3);
|
|
51
|
+
const rows = await sql`
|
|
52
|
+
INSERT INTO override_corpus (
|
|
53
|
+
id, tenant_id, incident_tag, proposed_command, override_reason,
|
|
54
|
+
note, rationale, service, environment, day_of_week, hour_of_day,
|
|
55
|
+
manual_action, actor, source, recorded_at, expires_at, hash, prev_hash
|
|
56
|
+
) VALUES (
|
|
57
|
+
${id}, ${tid},
|
|
58
|
+
${input.incidentTag},
|
|
59
|
+
${input.proposedCommand.slice(0, 500)},
|
|
60
|
+
${input.overrideReason},
|
|
61
|
+
${input.note?.slice(0, 200) ?? null},
|
|
62
|
+
${input.rationale?.slice(0, 300) ?? null},
|
|
63
|
+
${input.service},
|
|
64
|
+
${input.environment ?? "production"},
|
|
65
|
+
${now.getUTCDay()},
|
|
66
|
+
${now.getUTCHours()},
|
|
67
|
+
${input.manualAction?.slice(0, 500) ?? null},
|
|
68
|
+
${input.actor},
|
|
69
|
+
${input.source ?? null},
|
|
70
|
+
${now},
|
|
71
|
+
${expiresAt},
|
|
72
|
+
${hash},
|
|
73
|
+
${prevHash}
|
|
74
|
+
)
|
|
75
|
+
RETURNING *
|
|
76
|
+
`;
|
|
77
|
+
return rowToOverrideEvent(rows[0]);
|
|
78
|
+
}
|
|
79
|
+
async updateOutcome(id, outcome, tenantId) {
|
|
80
|
+
const sql = getSql();
|
|
81
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
82
|
+
const rows = await sql`
|
|
83
|
+
UPDATE override_corpus
|
|
84
|
+
SET outcome = ${outcome}
|
|
85
|
+
WHERE tenant_id = ${tid} AND id = ${id}
|
|
86
|
+
RETURNING 1
|
|
87
|
+
`;
|
|
88
|
+
return rows.length > 0;
|
|
89
|
+
}
|
|
90
|
+
async hasRecentOverride(incidentTag, service, dayOfWeek, hourOfDay, tenantId) {
|
|
91
|
+
const sql = getSql();
|
|
92
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
93
|
+
const rows = await sql`
|
|
94
|
+
SELECT 1 FROM override_corpus
|
|
95
|
+
WHERE tenant_id = ${tid}
|
|
96
|
+
AND incident_tag = ${incidentTag}
|
|
97
|
+
AND service = ${service}
|
|
98
|
+
AND day_of_week = ${dayOfWeek}
|
|
99
|
+
AND recorded_at > NOW() - INTERVAL '90 days'
|
|
100
|
+
AND ABS(hour_of_day - ${hourOfDay}) <= 1
|
|
101
|
+
LIMIT 1
|
|
102
|
+
`;
|
|
103
|
+
return rows.length > 0;
|
|
104
|
+
}
|
|
105
|
+
async dominantOverrideReason(incidentTag, service, tenantId) {
|
|
106
|
+
const sql = getSql();
|
|
107
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
108
|
+
const rows = await sql`
|
|
109
|
+
SELECT override_reason, COUNT(*) AS cnt
|
|
110
|
+
FROM override_corpus
|
|
111
|
+
WHERE tenant_id = ${tid} AND incident_tag = ${incidentTag} AND service = ${service}
|
|
112
|
+
GROUP BY override_reason
|
|
113
|
+
ORDER BY cnt DESC
|
|
114
|
+
LIMIT 1
|
|
115
|
+
`;
|
|
116
|
+
if (rows.length === 0) return null;
|
|
117
|
+
return rows[0].override_reason ?? null;
|
|
118
|
+
}
|
|
119
|
+
async getOverridesForTag(tag, tenantId) {
|
|
120
|
+
const sql = getSql();
|
|
121
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
122
|
+
const rows = await sql`
|
|
123
|
+
SELECT * FROM override_corpus
|
|
124
|
+
WHERE tenant_id = ${tid} AND incident_tag = ${tag}
|
|
125
|
+
ORDER BY recorded_at DESC
|
|
126
|
+
`;
|
|
127
|
+
return rows.map((r) => rowToOverrideEvent(r));
|
|
128
|
+
}
|
|
129
|
+
async getOverrideById(id, tenantId) {
|
|
130
|
+
const sql = getSql();
|
|
131
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
132
|
+
const rows = await sql`
|
|
133
|
+
SELECT * FROM override_corpus
|
|
134
|
+
WHERE tenant_id = ${tid} AND id = ${id}
|
|
135
|
+
LIMIT 1
|
|
136
|
+
`;
|
|
137
|
+
if (rows.length === 0) return null;
|
|
138
|
+
return rowToOverrideEvent(rows[0]);
|
|
139
|
+
}
|
|
140
|
+
async getAllOverrides(tenantId) {
|
|
141
|
+
const sql = getSql();
|
|
142
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
143
|
+
const rows = await sql`
|
|
144
|
+
SELECT * FROM override_corpus
|
|
145
|
+
WHERE tenant_id = ${tid}
|
|
146
|
+
ORDER BY recorded_at DESC
|
|
147
|
+
`;
|
|
148
|
+
return rows.map((r) => rowToOverrideEvent(r));
|
|
149
|
+
}
|
|
150
|
+
async compactCorpus(tenantId) {
|
|
151
|
+
const events = await this.getAllOverrides(tenantId);
|
|
152
|
+
return _compactEvents([...events]);
|
|
153
|
+
}
|
|
154
|
+
async getRulesForTag(incidentTag, service, tenantId) {
|
|
155
|
+
const rules = await this.compactCorpus(tenantId);
|
|
156
|
+
return rules.filter((r) => r.incidentTag === incidentTag && r.service === service);
|
|
157
|
+
}
|
|
158
|
+
async describeTopRule(incidentTag, service, tenantId) {
|
|
159
|
+
const rules = await this.getRulesForTag(incidentTag, service, tenantId);
|
|
160
|
+
if (rules.length === 0) return null;
|
|
161
|
+
const r = rules[0];
|
|
162
|
+
const timePart = r.dayOfWeek !== null ? ` (${DAY_NAMES[r.dayOfWeek]}${r.hourWindow ? ` ${r.hourWindow[0]}\u2013${r.hourWindow[1] - 1} UTC` : ""}, ${r.occurrences} overrides)` : ` (${r.occurrences} overrides)`;
|
|
163
|
+
return `${r.incidentTag} for ${r.service} \u2014 ${r.overrideReason}${timePart}`;
|
|
164
|
+
}
|
|
165
|
+
async getOverrideSummary(tenantId) {
|
|
166
|
+
const events = await this.getAllOverrides(tenantId);
|
|
167
|
+
return _buildOverrideSummary([...events]);
|
|
168
|
+
}
|
|
169
|
+
async compileOverrideFromSlackThread(slackThread, service, tenantId) {
|
|
170
|
+
if (!slackThread) return null;
|
|
171
|
+
const backtickCommands = [...slackThread.matchAll(/`([^`]{4,200})`/g)].map((m) => m[1].trim());
|
|
172
|
+
let proposedCommand = "";
|
|
173
|
+
let manualAction = "";
|
|
174
|
+
for (const cmd of backtickCommands) {
|
|
175
|
+
if (/^(kubectl|docker|systemctl|service|npm|yarn|pnpm|make)\s/i.test(cmd)) {
|
|
176
|
+
if (/restart|stop|scale|revert|rollback|install/i.test(cmd)) {
|
|
177
|
+
if (!proposedCommand) proposedCommand = cmd;
|
|
178
|
+
else if (!manualAction && cmd !== proposedCommand) manualAction = cmd;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (!proposedCommand) {
|
|
183
|
+
for (const line of slackThread.split("\n")) {
|
|
184
|
+
const trimmed = line.trim();
|
|
185
|
+
if (/^(kubectl|docker|systemctl|service|npm|yarn|pnpm|make)\s/i.test(trimmed)) {
|
|
186
|
+
proposedCommand = trimmed;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (!proposedCommand) return null;
|
|
192
|
+
let reason = "on-call-discretion";
|
|
193
|
+
let note = "Extracted from Slack thread discussion";
|
|
194
|
+
const lower = slackThread.toLowerCase();
|
|
195
|
+
if (lower.includes("window") || lower.includes("settlement") || lower.includes("friday") || lower.includes("batch")) {
|
|
196
|
+
reason = "batch-window";
|
|
197
|
+
note = "Override reason mapping: batch-window context discussed";
|
|
198
|
+
} else if (lower.includes("cost") || lower.includes("budget") || lower.includes("scale")) {
|
|
199
|
+
reason = "cost-constraint";
|
|
200
|
+
note = "Override reason mapping: cost constraints discussed";
|
|
201
|
+
} else if (lower.includes("cab") || lower.includes("freeze") || lower.includes("compliance")) {
|
|
202
|
+
reason = "compliance-hold";
|
|
203
|
+
note = "Override reason mapping: compliance hold discussed";
|
|
204
|
+
} else if (lower.includes("replica") || lower.includes("read") || lower.includes("primary")) {
|
|
205
|
+
reason = "prefer-read-replica";
|
|
206
|
+
note = "Override reason mapping: read-replica preference discussed";
|
|
207
|
+
} else if (lower.includes("maintenance") || lower.includes("scheduled")) {
|
|
208
|
+
reason = "maintenance-window";
|
|
209
|
+
note = "Override reason mapping: maintenance window discussed";
|
|
210
|
+
} else if (lower.includes("wrong diagnosis") || lower.includes("misidentified")) {
|
|
211
|
+
reason = "wrong-diagnosis";
|
|
212
|
+
note = "Override reason mapping: wrong root-cause discussed";
|
|
213
|
+
} else if (lower.includes("wrong fix") || lower.includes("bad command")) {
|
|
214
|
+
reason = "wrong-fix";
|
|
215
|
+
note = "Override reason mapping: wrong remediation discussed";
|
|
216
|
+
}
|
|
217
|
+
let incidentTag = "infra_db_connection_pool";
|
|
218
|
+
if (lower.includes("oom") || lower.includes("memory")) incidentTag = "infra_oom_kill";
|
|
219
|
+
else if (lower.includes("rate") || lower.includes("throttl")) incidentTag = "infra_rate_limit_cascade";
|
|
220
|
+
else if (lower.includes("cert") || lower.includes("tls")) incidentTag = "infra_certificate_expiry";
|
|
221
|
+
else if (lower.includes("disk") || lower.includes("space")) incidentTag = "infra_disk_pressure";
|
|
222
|
+
else if (lower.includes("slow") || lower.includes("query")) incidentTag = "infra_slow_query";
|
|
223
|
+
return this.recordOverride(
|
|
224
|
+
{
|
|
225
|
+
incidentTag,
|
|
226
|
+
proposedCommand,
|
|
227
|
+
overrideReason: reason,
|
|
228
|
+
note,
|
|
229
|
+
service: service ?? "unknown",
|
|
230
|
+
environment: "production",
|
|
231
|
+
manualAction: manualAction || void 0,
|
|
232
|
+
actor: "Slack NLP Parser"
|
|
233
|
+
},
|
|
234
|
+
tenantId
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
async compileOverridesFromSlackThread(slackThread, service, tenantId) {
|
|
238
|
+
const single = await this.compileOverrideFromSlackThread(slackThread, service, tenantId);
|
|
239
|
+
return single ? [single] : [];
|
|
240
|
+
}
|
|
241
|
+
async getStaleOverrides(daysThreshold = 60, tenantId) {
|
|
242
|
+
const sql = getSql();
|
|
243
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
244
|
+
const rows = await sql`
|
|
245
|
+
SELECT * FROM override_corpus
|
|
246
|
+
WHERE tenant_id = ${tid}
|
|
247
|
+
AND COALESCE(reviewed_at, recorded_at) < NOW() - (${daysThreshold} * INTERVAL '1 day')
|
|
248
|
+
ORDER BY COALESCE(reviewed_at, recorded_at) ASC
|
|
249
|
+
`;
|
|
250
|
+
return rows.map((r) => rowToOverrideEvent(r));
|
|
251
|
+
}
|
|
252
|
+
async getExpiringSoon(windowDays = 14, tenantId) {
|
|
253
|
+
const sql = getSql();
|
|
254
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
255
|
+
const rows = await sql`
|
|
256
|
+
SELECT * FROM override_corpus
|
|
257
|
+
WHERE tenant_id = ${tid}
|
|
258
|
+
AND expires_at IS NOT NULL
|
|
259
|
+
AND expires_at > NOW()
|
|
260
|
+
AND expires_at < NOW() + (${windowDays} * INTERVAL '1 day')
|
|
261
|
+
ORDER BY expires_at ASC
|
|
262
|
+
`;
|
|
263
|
+
return rows.map((r) => rowToOverrideEvent(r));
|
|
264
|
+
}
|
|
265
|
+
async markOverrideReviewed(id, tenantId) {
|
|
266
|
+
const sql = getSql();
|
|
267
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
268
|
+
const rows = await sql`
|
|
269
|
+
UPDATE override_corpus
|
|
270
|
+
SET reviewed_at = NOW()
|
|
271
|
+
WHERE tenant_id = ${tid} AND id = ${id}
|
|
272
|
+
RETURNING 1
|
|
273
|
+
`;
|
|
274
|
+
return rows.length > 0;
|
|
275
|
+
}
|
|
276
|
+
async importOverrides(entries, opts, tenantId) {
|
|
277
|
+
const sql = getSql();
|
|
278
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
279
|
+
const source = opts?.source ?? "community";
|
|
280
|
+
const actor = opts?.actor ?? source;
|
|
281
|
+
const existing = await sql`
|
|
282
|
+
SELECT incident_tag, override_reason, day_of_week, hour_of_day, service
|
|
283
|
+
FROM override_corpus WHERE tenant_id = ${tid}
|
|
284
|
+
`;
|
|
285
|
+
const existingKeys = new Set(existing.map((r) => overridePackKey({
|
|
286
|
+
incidentTag: String(r.incident_tag),
|
|
287
|
+
overrideReason: r.override_reason,
|
|
288
|
+
dayOfWeek: Number(r.day_of_week),
|
|
289
|
+
hourOfDay: Number(r.hour_of_day),
|
|
290
|
+
service: String(r.service)
|
|
291
|
+
})));
|
|
292
|
+
let imported = 0;
|
|
293
|
+
let skipped = 0;
|
|
294
|
+
for (const entry of entries) {
|
|
295
|
+
const dayOfWeek = Math.min(6, Math.max(0, Math.trunc(entry.dayOfWeek)));
|
|
296
|
+
const hourOfDay = Math.min(23, Math.max(0, Math.trunc(entry.hourOfDay)));
|
|
297
|
+
const key = overridePackKey({ ...entry, dayOfWeek, hourOfDay });
|
|
298
|
+
if (existingKeys.has(key)) {
|
|
299
|
+
skipped++;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
existingKeys.add(key);
|
|
303
|
+
const now = /* @__PURE__ */ new Date();
|
|
304
|
+
const id = randomUUID();
|
|
305
|
+
const lastRows = await sql`
|
|
306
|
+
SELECT hash FROM override_corpus
|
|
307
|
+
WHERE tenant_id = ${tid}
|
|
308
|
+
ORDER BY recorded_at DESC, id DESC
|
|
309
|
+
LIMIT 1
|
|
310
|
+
`;
|
|
311
|
+
const prevHash = lastRows.length > 0 && lastRows[0].hash ? String(lastRows[0].hash) : "genesis_override";
|
|
312
|
+
const payload = [
|
|
313
|
+
id,
|
|
314
|
+
entry.incidentTag,
|
|
315
|
+
entry.proposedCommand.slice(0, 500),
|
|
316
|
+
entry.overrideReason,
|
|
317
|
+
entry.service,
|
|
318
|
+
actor
|
|
319
|
+
].join("|");
|
|
320
|
+
const hash = createHash("sha256").update(payload + prevHash).digest("hex");
|
|
321
|
+
const expiresAt = entry.expiresInDays ? new Date(now.getTime() + entry.expiresInDays * 24 * 60 * 60 * 1e3) : null;
|
|
322
|
+
await sql`
|
|
323
|
+
INSERT INTO override_corpus (
|
|
324
|
+
id, tenant_id, incident_tag, proposed_command, override_reason,
|
|
325
|
+
note, rationale, service, environment, day_of_week, hour_of_day,
|
|
326
|
+
manual_action, actor, source, recorded_at, expires_at, hash, prev_hash
|
|
327
|
+
) VALUES (
|
|
328
|
+
${id}, ${tid},
|
|
329
|
+
${entry.incidentTag},
|
|
330
|
+
${entry.proposedCommand.slice(0, 500)},
|
|
331
|
+
${entry.overrideReason},
|
|
332
|
+
${entry.note?.slice(0, 200) ?? null},
|
|
333
|
+
${entry.rationale?.slice(0, 300) ?? null},
|
|
334
|
+
${entry.service},
|
|
335
|
+
${entry.environment ?? "production"},
|
|
336
|
+
${dayOfWeek},
|
|
337
|
+
${hourOfDay},
|
|
338
|
+
${entry.manualAction?.slice(0, 500) ?? null},
|
|
339
|
+
${actor},
|
|
340
|
+
${source},
|
|
341
|
+
${now},
|
|
342
|
+
${expiresAt},
|
|
343
|
+
${hash},
|
|
344
|
+
${prevHash}
|
|
345
|
+
)
|
|
346
|
+
`;
|
|
347
|
+
imported++;
|
|
348
|
+
}
|
|
349
|
+
return { imported, skipped };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function _compactEvents(events) {
|
|
353
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
354
|
+
for (const e of events) {
|
|
355
|
+
const key = `${e.incidentTag}\0${e.service}\0${e.overrideReason}`;
|
|
356
|
+
const list = buckets.get(key) ?? [];
|
|
357
|
+
list.push(e);
|
|
358
|
+
buckets.set(key, list);
|
|
359
|
+
}
|
|
360
|
+
const rules = [];
|
|
361
|
+
const now = Date.now();
|
|
362
|
+
for (const evs of buckets.values()) {
|
|
363
|
+
if (evs.length === 0) continue;
|
|
364
|
+
const first = evs[0];
|
|
365
|
+
const dayCounts = /* @__PURE__ */ new Map();
|
|
366
|
+
for (const e of evs) dayCounts.set(e.dayOfWeek, (dayCounts.get(e.dayOfWeek) ?? 0) + 1);
|
|
367
|
+
let dominantDay = null;
|
|
368
|
+
let dominantDayCount = 0;
|
|
369
|
+
for (const [day, count] of dayCounts) {
|
|
370
|
+
if (count > dominantDayCount) {
|
|
371
|
+
dominantDay = day;
|
|
372
|
+
dominantDayCount = count;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (dominantDay !== null && dominantDayCount / evs.length < 0.4) dominantDay = null;
|
|
376
|
+
const hoursToWindow = dominantDay !== null ? evs.filter((e) => e.dayOfWeek === dominantDay).map((e) => e.hourOfDay) : evs.map((e) => e.hourOfDay);
|
|
377
|
+
let hourWindow = null;
|
|
378
|
+
if (hoursToWindow.length > 0) {
|
|
379
|
+
const minH = Math.min(...hoursToWindow);
|
|
380
|
+
const maxH = Math.max(...hoursToWindow);
|
|
381
|
+
if (maxH - minH < 20) hourWindow = [minH, maxH + 1];
|
|
382
|
+
}
|
|
383
|
+
rules.push({
|
|
384
|
+
incidentTag: first.incidentTag,
|
|
385
|
+
service: first.service,
|
|
386
|
+
overrideReason: first.overrideReason,
|
|
387
|
+
dayOfWeek: dominantDay,
|
|
388
|
+
hourWindow,
|
|
389
|
+
occurrences: evs.length,
|
|
390
|
+
commandSignatures: dominantCommandSignatures(evs),
|
|
391
|
+
compactedAt: now
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
rules.sort((a, b) => b.occurrences - a.occurrences);
|
|
395
|
+
return rules;
|
|
396
|
+
}
|
|
397
|
+
function _buildOverrideSummary(events) {
|
|
398
|
+
const byTag = /* @__PURE__ */ new Map();
|
|
399
|
+
for (const e of events) {
|
|
400
|
+
const list = byTag.get(e.incidentTag) ?? [];
|
|
401
|
+
list.push(e);
|
|
402
|
+
byTag.set(e.incidentTag, list);
|
|
403
|
+
}
|
|
404
|
+
const out = [];
|
|
405
|
+
for (const [tag, evs] of byTag) {
|
|
406
|
+
const reasonCounts = /* @__PURE__ */ new Map();
|
|
407
|
+
const services = /* @__PURE__ */ new Set();
|
|
408
|
+
const dayCounts = /* @__PURE__ */ new Map();
|
|
409
|
+
const outcomes = { resolved: 0, escalated: 0, unresolved: 0, unknown: 0 };
|
|
410
|
+
for (const e of evs) {
|
|
411
|
+
reasonCounts.set(e.overrideReason, (reasonCounts.get(e.overrideReason) ?? 0) + 1);
|
|
412
|
+
services.add(e.service);
|
|
413
|
+
dayCounts.set(e.dayOfWeek, (dayCounts.get(e.dayOfWeek) ?? 0) + 1);
|
|
414
|
+
if (e.outcome) outcomes[e.outcome]++;
|
|
415
|
+
else outcomes.unknown++;
|
|
416
|
+
}
|
|
417
|
+
let dominant = null;
|
|
418
|
+
let bestCount = 0;
|
|
419
|
+
for (const [r, c] of reasonCounts) {
|
|
420
|
+
if (c > bestCount) {
|
|
421
|
+
dominant = r;
|
|
422
|
+
bestCount = c;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
let dominantDay = null;
|
|
426
|
+
let dominantDayCount = 0;
|
|
427
|
+
for (const [day, count] of dayCounts) {
|
|
428
|
+
if (count > dominantDayCount) {
|
|
429
|
+
dominantDay = day;
|
|
430
|
+
dominantDayCount = count;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
let timePattern = null;
|
|
434
|
+
if (dominantDay !== null && dominantDayCount / evs.length >= 0.4) {
|
|
435
|
+
const hoursOnDay = evs.filter((e) => e.dayOfWeek === dominantDay).map((e) => e.hourOfDay);
|
|
436
|
+
const minHour = Math.min(...hoursOnDay);
|
|
437
|
+
const maxHour = Math.max(...hoursOnDay);
|
|
438
|
+
timePattern = maxHour > minHour ? `${DAY_NAMES[dominantDay]} ${minHour}\u2013${maxHour} UTC` : `${DAY_NAMES[dominantDay]} ${minHour} UTC`;
|
|
439
|
+
}
|
|
440
|
+
out.push({ tag, total: evs.length, dominantReason: dominant, services: [...services], timePattern, outcomes });
|
|
441
|
+
}
|
|
442
|
+
out.sort((a, b) => b.total - a.total);
|
|
443
|
+
return out;
|
|
444
|
+
}
|
|
445
|
+
export {
|
|
446
|
+
PgOverrideCorpus
|
|
447
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { getSql } from "./pg-client.js";
|
|
3
|
+
import {
|
|
4
|
+
computeShadowReport,
|
|
5
|
+
computeShadowSlackDigest,
|
|
6
|
+
computeShadowCsv
|
|
7
|
+
} from "../../intelligence/shadow-log.js";
|
|
8
|
+
import { getStores } from "../store-registry.js";
|
|
9
|
+
import logger from "../../sensor/logger.js";
|
|
10
|
+
const DEFAULT_TENANT = "local";
|
|
11
|
+
function rowToEntry(row) {
|
|
12
|
+
return {
|
|
13
|
+
id: String(row.id ?? ""),
|
|
14
|
+
pid: String(row.pid ?? ""),
|
|
15
|
+
incidentTag: String(row.incident_tag ?? ""),
|
|
16
|
+
service: String(row.service ?? ""),
|
|
17
|
+
command: row.command ? String(row.command) : null,
|
|
18
|
+
diagnosisConfidence: Number(row.diagnosis_confidence ?? 0),
|
|
19
|
+
remediationConfidence: Number(row.remediation_confidence ?? 0),
|
|
20
|
+
wouldHaveExecuted: Boolean(row.would_have_executed),
|
|
21
|
+
skipReason: row.skip_reason ?? "no-command",
|
|
22
|
+
firedAt: row.fired_at ? Number(row.fired_at) : void 0,
|
|
23
|
+
recordedAt: Number(row.recorded_at ?? 0),
|
|
24
|
+
humanVerdict: row.human_verdict ? row.human_verdict : void 0,
|
|
25
|
+
humanNote: row.human_note ? String(row.human_note) : void 0,
|
|
26
|
+
verdictAt: row.verdict_at ? Number(row.verdict_at) : void 0,
|
|
27
|
+
overrideId: row.override_id ? String(row.override_id) : void 0,
|
|
28
|
+
runbookId: row.runbook_id ? String(row.runbook_id) : void 0
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
class PgShadowLog {
|
|
32
|
+
async recordShadow(input, tenantId) {
|
|
33
|
+
const sql = getSql();
|
|
34
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
35
|
+
const id = input.id ?? randomUUID();
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
await sql`
|
|
38
|
+
INSERT INTO shadow_log (
|
|
39
|
+
id, tenant_id, pid, incident_tag, service, command,
|
|
40
|
+
diagnosis_confidence, remediation_confidence, would_have_executed,
|
|
41
|
+
skip_reason, fired_at, recorded_at, runbook_id
|
|
42
|
+
) VALUES (
|
|
43
|
+
${id}, ${tid}, ${input.pid}, ${input.incidentTag}, ${input.service},
|
|
44
|
+
${input.command ?? null}, ${input.diagnosisConfidence}, ${input.remediationConfidence},
|
|
45
|
+
${input.wouldHaveExecuted}, ${input.skipReason}, ${input.firedAt ?? null}, ${now},
|
|
46
|
+
${input.runbookId ?? null}
|
|
47
|
+
)
|
|
48
|
+
ON CONFLICT (tenant_id, id) DO NOTHING
|
|
49
|
+
`;
|
|
50
|
+
logger.info({ id, pid: input.pid, tag: input.incidentTag, tid }, "pg-shadow-log: entry recorded");
|
|
51
|
+
return { ...input, id, recordedAt: now };
|
|
52
|
+
}
|
|
53
|
+
async recordShadowVerdict(id, verdict, opts = {}, tenantId) {
|
|
54
|
+
const sql = getSql();
|
|
55
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
56
|
+
const rows = await sql`SELECT * FROM shadow_log WHERE tenant_id = ${tid} AND id = ${id}`;
|
|
57
|
+
if (rows.length === 0) return { found: false };
|
|
58
|
+
const entry = rowToEntry(rows[0]);
|
|
59
|
+
const verdictAt = Date.now();
|
|
60
|
+
const note = opts.note?.slice(0, 200);
|
|
61
|
+
let overrideId;
|
|
62
|
+
if (verdict === "would-override" && entry.command) {
|
|
63
|
+
try {
|
|
64
|
+
const ov = await getStores().overrides.recordOverride(
|
|
65
|
+
{
|
|
66
|
+
incidentTag: entry.incidentTag,
|
|
67
|
+
proposedCommand: entry.command,
|
|
68
|
+
overrideReason: opts.overrideReason ?? "on-call-discretion",
|
|
69
|
+
note: opts.note,
|
|
70
|
+
service: entry.service,
|
|
71
|
+
environment: "production",
|
|
72
|
+
manualAction: opts.manualAction,
|
|
73
|
+
actor: opts.actor ?? "shadow-review"
|
|
74
|
+
},
|
|
75
|
+
tid
|
|
76
|
+
);
|
|
77
|
+
overrideId = ov.id;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
logger.warn({ err, id }, "pg-shadow-log: failed to record override from verdict");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
await sql`
|
|
83
|
+
UPDATE shadow_log
|
|
84
|
+
SET human_verdict = ${verdict},
|
|
85
|
+
verdict_at = ${verdictAt},
|
|
86
|
+
human_note = ${note ?? null},
|
|
87
|
+
override_id = ${overrideId ?? null}
|
|
88
|
+
WHERE tenant_id = ${tid} AND id = ${id}
|
|
89
|
+
`;
|
|
90
|
+
entry.humanVerdict = verdict;
|
|
91
|
+
entry.verdictAt = verdictAt;
|
|
92
|
+
if (note) entry.humanNote = note;
|
|
93
|
+
if (overrideId) entry.overrideId = overrideId;
|
|
94
|
+
return { found: true, entry, overrideId };
|
|
95
|
+
}
|
|
96
|
+
async updateShadowReasonByPid(pid, skipReason, tenantId) {
|
|
97
|
+
const sql = getSql();
|
|
98
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
99
|
+
await sql`
|
|
100
|
+
UPDATE shadow_log SET skip_reason = ${skipReason}
|
|
101
|
+
WHERE tenant_id = ${tid} AND pid = ${pid}
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
104
|
+
async getShadowEntries(tenantId) {
|
|
105
|
+
const sql = getSql();
|
|
106
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
107
|
+
const rows = await sql`
|
|
108
|
+
SELECT * FROM shadow_log
|
|
109
|
+
WHERE tenant_id = ${tid}
|
|
110
|
+
ORDER BY recorded_at ASC
|
|
111
|
+
`;
|
|
112
|
+
return rows.map((r) => rowToEntry(r));
|
|
113
|
+
}
|
|
114
|
+
async getShadowReport(windowDays = 30, tenantId) {
|
|
115
|
+
const entries = await this.getShadowEntries(tenantId);
|
|
116
|
+
return computeShadowReport(entries, windowDays);
|
|
117
|
+
}
|
|
118
|
+
async getShadowSlackDigest(windowDays = 7, tenantId) {
|
|
119
|
+
const entries = await this.getShadowEntries(tenantId);
|
|
120
|
+
return computeShadowSlackDigest(entries, windowDays);
|
|
121
|
+
}
|
|
122
|
+
async exportShadowCsv(tenantId) {
|
|
123
|
+
const entries = await this.getShadowEntries(tenantId);
|
|
124
|
+
return computeShadowCsv(entries);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export {
|
|
128
|
+
PgShadowLog
|
|
129
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { actionLedger } from "../../sensor/action-ledger.js";
|
|
2
|
+
class SqliteActionLedger {
|
|
3
|
+
async append(rec, _tenantId) {
|
|
4
|
+
actionLedger.appendActionRecord(rec);
|
|
5
|
+
}
|
|
6
|
+
async query(opts, _tenantId) {
|
|
7
|
+
return actionLedger.queryLedger(opts);
|
|
8
|
+
}
|
|
9
|
+
async queryCredentialUsage(opts, _tenantId) {
|
|
10
|
+
return actionLedger.queryCredentialUsage(opts);
|
|
11
|
+
}
|
|
12
|
+
async prune(_tenantId) {
|
|
13
|
+
actionLedger.prune();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export {
|
|
17
|
+
SqliteActionLedger
|
|
18
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { flushApprovals } from "../../intelligence/execution-gate.js";
|
|
2
|
+
class SqliteApprovalStore {
|
|
3
|
+
/** In-memory store — mirrors the structure of execution-gate's private _pending map. */
|
|
4
|
+
_map = /* @__PURE__ */ new Map();
|
|
5
|
+
async add(token, execution, _tenantId) {
|
|
6
|
+
this._map.set(token, execution);
|
|
7
|
+
}
|
|
8
|
+
async get(token) {
|
|
9
|
+
return Promise.resolve(this._map.get(token) ?? null);
|
|
10
|
+
}
|
|
11
|
+
async resolve(token) {
|
|
12
|
+
return Promise.resolve(this._map.delete(token));
|
|
13
|
+
}
|
|
14
|
+
async listPending(_tenantId) {
|
|
15
|
+
return Promise.resolve([...this._map.entries()]);
|
|
16
|
+
}
|
|
17
|
+
/** Remove all entries whose expiresAt has passed. */
|
|
18
|
+
async pruneExpired() {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
for (const [token, execution] of this._map) {
|
|
21
|
+
if (execution.expiresAt <= now) {
|
|
22
|
+
this._map.delete(token);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Delegates to execution-gate's flushApprovals() so the SIGTERM handler in
|
|
28
|
+
* index.ts can flush the approval state without knowing which store is active.
|
|
29
|
+
* Phase 2 will update this to serialize our own _map to disk.
|
|
30
|
+
*/
|
|
31
|
+
async flush() {
|
|
32
|
+
return Promise.resolve(flushApprovals());
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
SqliteApprovalStore
|
|
37
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
recordBlunder,
|
|
3
|
+
getBlunders,
|
|
4
|
+
getBlunderStats,
|
|
5
|
+
verifyChain,
|
|
6
|
+
isBlunderIntegrityViolated
|
|
7
|
+
} from "../../sensor/agent-blunder-store.js";
|
|
8
|
+
class SqliteBlunderStore {
|
|
9
|
+
async record(event, _tenantId) {
|
|
10
|
+
recordBlunder(event);
|
|
11
|
+
}
|
|
12
|
+
async list(_tenantId) {
|
|
13
|
+
return getBlunders();
|
|
14
|
+
}
|
|
15
|
+
async getStats(_tenantId) {
|
|
16
|
+
return getBlunderStats();
|
|
17
|
+
}
|
|
18
|
+
async verifyChain(_tenantId) {
|
|
19
|
+
return verifyChain();
|
|
20
|
+
}
|
|
21
|
+
isIntegrityViolated() {
|
|
22
|
+
return isBlunderIntegrityViolated();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
SqliteBlunderStore
|
|
27
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
recordGateEvaluation,
|
|
3
|
+
getGateHistoryForPR,
|
|
4
|
+
getGateHistory
|
|
5
|
+
} from "../../sensor/ci-gate-history.js";
|
|
6
|
+
class SqliteGateHistoryStore {
|
|
7
|
+
async record(entry, _tenantId) {
|
|
8
|
+
recordGateEvaluation(entry);
|
|
9
|
+
}
|
|
10
|
+
async listForPR(prNumber, repo, _tenantId) {
|
|
11
|
+
return getGateHistoryForPR(prNumber, repo);
|
|
12
|
+
}
|
|
13
|
+
async list(opts, _tenantId) {
|
|
14
|
+
return getGateHistory(opts);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
SqliteGateHistoryStore
|
|
19
|
+
};
|