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,107 @@
|
|
|
1
|
+
import { getSql } from "./pg-client.js";
|
|
2
|
+
const DEFAULT_TENANT = "local";
|
|
3
|
+
function rowToPending(row) {
|
|
4
|
+
return {
|
|
5
|
+
pid: String(row.pid ?? ""),
|
|
6
|
+
command: String(row.command ?? ""),
|
|
7
|
+
tier: row.tier ?? "restart",
|
|
8
|
+
service: String(row.service ?? ""),
|
|
9
|
+
remediationConfidence: Number(row.remediation_confidence ?? 0),
|
|
10
|
+
requestedAt: row.requested_at instanceof Date ? row.requested_at.getTime() : new Date(String(row.requested_at ?? 0)).getTime(),
|
|
11
|
+
expiresAt: row.expires_at instanceof Date ? row.expires_at.getTime() : new Date(String(row.expires_at ?? 0)).getTime(),
|
|
12
|
+
cwd: row.cwd ? String(row.cwd) : void 0,
|
|
13
|
+
blastRadius: row.blast_radius ? row.blast_radius : void 0
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
class PgApprovalStore {
|
|
17
|
+
async add(token, execution, tenantId = DEFAULT_TENANT) {
|
|
18
|
+
const sql = getSql();
|
|
19
|
+
const lastRows = await sql`
|
|
20
|
+
SELECT hash FROM pending_approvals
|
|
21
|
+
WHERE tenant_id = ${tenantId}
|
|
22
|
+
ORDER BY requested_at DESC, token DESC
|
|
23
|
+
LIMIT 1
|
|
24
|
+
`;
|
|
25
|
+
const prevHash = lastRows.length > 0 && lastRows[0].hash ? String(lastRows[0].hash) : "genesis_approvals";
|
|
26
|
+
const crypto = await import("crypto");
|
|
27
|
+
const payload = [
|
|
28
|
+
token,
|
|
29
|
+
execution.pid,
|
|
30
|
+
execution.command,
|
|
31
|
+
execution.tier,
|
|
32
|
+
execution.service,
|
|
33
|
+
String(execution.remediationConfidence),
|
|
34
|
+
String(execution.requestedAt)
|
|
35
|
+
].join("|");
|
|
36
|
+
const hash = crypto.createHash("sha256").update(payload + prevHash).digest("hex");
|
|
37
|
+
await sql`
|
|
38
|
+
INSERT INTO pending_approvals (
|
|
39
|
+
token, tenant_id, pid, command, tier, service,
|
|
40
|
+
remediation_confidence, requested_at, expires_at, cwd, blast_radius,
|
|
41
|
+
hash, prev_hash
|
|
42
|
+
) VALUES (
|
|
43
|
+
${token},
|
|
44
|
+
${tenantId},
|
|
45
|
+
${execution.pid},
|
|
46
|
+
${execution.command},
|
|
47
|
+
${execution.tier},
|
|
48
|
+
${execution.service},
|
|
49
|
+
${execution.remediationConfidence},
|
|
50
|
+
${new Date(execution.requestedAt)},
|
|
51
|
+
${new Date(execution.expiresAt)},
|
|
52
|
+
${execution.cwd ?? null},
|
|
53
|
+
${execution.blastRadius ? sql.json(execution.blastRadius) : null},
|
|
54
|
+
${hash},
|
|
55
|
+
${prevHash}
|
|
56
|
+
)
|
|
57
|
+
ON CONFLICT (token) DO NOTHING
|
|
58
|
+
`;
|
|
59
|
+
}
|
|
60
|
+
async get(token) {
|
|
61
|
+
const sql = getSql();
|
|
62
|
+
const rows = await sql`
|
|
63
|
+
SELECT * FROM pending_approvals
|
|
64
|
+
WHERE token = ${token}
|
|
65
|
+
AND resolved_at IS NULL
|
|
66
|
+
AND expires_at > NOW()
|
|
67
|
+
`;
|
|
68
|
+
if (rows.length === 0) return null;
|
|
69
|
+
return rowToPending(rows[0]);
|
|
70
|
+
}
|
|
71
|
+
async resolve(token) {
|
|
72
|
+
const sql = getSql();
|
|
73
|
+
const rows = await sql`
|
|
74
|
+
UPDATE pending_approvals
|
|
75
|
+
SET resolved_at = NOW(), resolution = 'resolved'
|
|
76
|
+
WHERE token = ${token} AND resolved_at IS NULL
|
|
77
|
+
RETURNING 1
|
|
78
|
+
`;
|
|
79
|
+
return rows.length > 0;
|
|
80
|
+
}
|
|
81
|
+
async listPending(tenantId = DEFAULT_TENANT) {
|
|
82
|
+
const sql = getSql();
|
|
83
|
+
const rows = await sql`
|
|
84
|
+
SELECT * FROM pending_approvals
|
|
85
|
+
WHERE tenant_id = ${tenantId} AND resolved_at IS NULL AND expires_at > NOW()
|
|
86
|
+
ORDER BY requested_at ASC
|
|
87
|
+
`;
|
|
88
|
+
return rows.map((r) => [
|
|
89
|
+
String(r.token ?? ""),
|
|
90
|
+
rowToPending(r)
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
async pruneExpired() {
|
|
94
|
+
const sql = getSql();
|
|
95
|
+
await sql`
|
|
96
|
+
UPDATE pending_approvals
|
|
97
|
+
SET resolved_at = NOW(), resolution = 'expired'
|
|
98
|
+
WHERE expires_at < NOW() AND resolved_at IS NULL
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
async flush() {
|
|
102
|
+
return Promise.resolve();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export {
|
|
106
|
+
PgApprovalStore
|
|
107
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "crypto";
|
|
2
|
+
import { getSql } from "./pg-client.js";
|
|
3
|
+
import { MAX_BLUNDERS, NON_BLOCKING_BLUNDER_TYPES } from "../../sensor/agent-blunder-store.js";
|
|
4
|
+
const DEFAULT_TENANT = "local";
|
|
5
|
+
const GENESIS_HASH = "0".repeat(64);
|
|
6
|
+
function _hashableContent(event, previousHash) {
|
|
7
|
+
const content = {
|
|
8
|
+
id: event.id,
|
|
9
|
+
recordedAt: event.recordedAt,
|
|
10
|
+
blunderType: event.blunderType,
|
|
11
|
+
command: event.command,
|
|
12
|
+
blockReason: event.blockReason,
|
|
13
|
+
service: event.service,
|
|
14
|
+
tag: event.tag,
|
|
15
|
+
actor: event.actor,
|
|
16
|
+
pid: event.pid,
|
|
17
|
+
confidenceScore: event.confidenceScore
|
|
18
|
+
};
|
|
19
|
+
if (event.triggeredRules !== void 0) content.triggeredRules = event.triggeredRules;
|
|
20
|
+
return previousHash + JSON.stringify(content);
|
|
21
|
+
}
|
|
22
|
+
function _computeHash(event, previousHash) {
|
|
23
|
+
return createHash("sha256").update(_hashableContent(event, previousHash)).digest("hex");
|
|
24
|
+
}
|
|
25
|
+
function rowToBlunder(row) {
|
|
26
|
+
const triggeredRaw = row.triggered_rules;
|
|
27
|
+
let triggeredRules;
|
|
28
|
+
if (triggeredRaw === null || triggeredRaw === void 0) {
|
|
29
|
+
triggeredRules = null;
|
|
30
|
+
} else if (Array.isArray(triggeredRaw)) {
|
|
31
|
+
triggeredRules = triggeredRaw;
|
|
32
|
+
} else {
|
|
33
|
+
try {
|
|
34
|
+
triggeredRules = JSON.parse(String(triggeredRaw));
|
|
35
|
+
} catch {
|
|
36
|
+
triggeredRules = null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
id: String(row.id ?? ""),
|
|
41
|
+
recordedAt: Number(row.recorded_at ?? 0),
|
|
42
|
+
blunderType: row.blunder_type ?? "allowlist_block",
|
|
43
|
+
command: row.command ? String(row.command) : null,
|
|
44
|
+
blockReason: String(row.block_reason ?? ""),
|
|
45
|
+
service: row.service ? String(row.service) : null,
|
|
46
|
+
tag: row.tag ? String(row.tag) : null,
|
|
47
|
+
actor: row.actor ? String(row.actor) : null,
|
|
48
|
+
pid: row.pid ? String(row.pid) : null,
|
|
49
|
+
confidenceScore: row.confidence_score != null ? Number(row.confidence_score) : null,
|
|
50
|
+
triggeredRules,
|
|
51
|
+
previousHash: String(row.previous_hash ?? ""),
|
|
52
|
+
hash: String(row.hash ?? "")
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
class PgBlunderStore {
|
|
56
|
+
async record(event, tenantId) {
|
|
57
|
+
const sql = getSql();
|
|
58
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
59
|
+
await sql.begin("SERIALIZABLE", async (tx) => {
|
|
60
|
+
const id = event.id ?? randomUUID();
|
|
61
|
+
const existing = await tx`SELECT 1 FROM agent_blunders WHERE id = ${id} AND tenant_id = ${tid} LIMIT 1`;
|
|
62
|
+
if (existing.length > 0) return;
|
|
63
|
+
const lastRows = await tx`
|
|
64
|
+
SELECT hash FROM agent_blunders
|
|
65
|
+
WHERE tenant_id = ${tid}
|
|
66
|
+
ORDER BY recorded_at ASC, id ASC
|
|
67
|
+
OFFSET (SELECT GREATEST(COUNT(*) - 1, 0) FROM agent_blunders WHERE tenant_id = ${tid})
|
|
68
|
+
LIMIT 1
|
|
69
|
+
`;
|
|
70
|
+
const previousHash = lastRows.length > 0 ? String(lastRows[0].hash) : GENESIS_HASH;
|
|
71
|
+
const recordedAt = event.recordedAt ?? Date.now();
|
|
72
|
+
const triggeredRules = event.triggeredRules !== void 0 ? event.triggeredRules : null;
|
|
73
|
+
const base = { ...event, id, recordedAt, triggeredRules };
|
|
74
|
+
const hash = _computeHash(base, previousHash);
|
|
75
|
+
await tx`
|
|
76
|
+
INSERT INTO agent_blunders (
|
|
77
|
+
id, tenant_id, recorded_at, blunder_type, command, block_reason,
|
|
78
|
+
service, tag, actor, pid, confidence_score, triggered_rules,
|
|
79
|
+
previous_hash, hash
|
|
80
|
+
) VALUES (
|
|
81
|
+
${id}, ${tid}, ${recordedAt},
|
|
82
|
+
${event.blunderType}, ${event.command ?? null}, ${event.blockReason},
|
|
83
|
+
${event.service ?? null}, ${event.tag ?? null}, ${event.actor ?? null},
|
|
84
|
+
${event.pid ?? null}, ${event.confidenceScore ?? null},
|
|
85
|
+
${triggeredRules != null ? JSON.stringify(triggeredRules) : null},
|
|
86
|
+
${previousHash}, ${hash}
|
|
87
|
+
)
|
|
88
|
+
ON CONFLICT (id, tenant_id) DO NOTHING
|
|
89
|
+
`;
|
|
90
|
+
await tx`
|
|
91
|
+
DELETE FROM agent_blunders
|
|
92
|
+
WHERE tenant_id = ${tid}
|
|
93
|
+
AND recorded_at = (
|
|
94
|
+
SELECT recorded_at FROM agent_blunders
|
|
95
|
+
WHERE tenant_id = ${tid}
|
|
96
|
+
ORDER BY recorded_at ASC, id ASC
|
|
97
|
+
LIMIT 1
|
|
98
|
+
)
|
|
99
|
+
AND (SELECT COUNT(*) FROM agent_blunders WHERE tenant_id = ${tid}) > ${MAX_BLUNDERS}
|
|
100
|
+
`;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
async list(tenantId) {
|
|
104
|
+
const sql = getSql();
|
|
105
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
106
|
+
const rows = await sql`
|
|
107
|
+
SELECT * FROM agent_blunders
|
|
108
|
+
WHERE tenant_id = ${tid}
|
|
109
|
+
ORDER BY recorded_at ASC, id ASC
|
|
110
|
+
`;
|
|
111
|
+
return rows.map((r) => rowToBlunder(r));
|
|
112
|
+
}
|
|
113
|
+
async getStats(tenantId) {
|
|
114
|
+
const sql = getSql();
|
|
115
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
const ms7 = 7 * 24 * 60 * 60 * 1e3;
|
|
118
|
+
const ms30 = 30 * 24 * 60 * 60 * 1e3;
|
|
119
|
+
const nonBlockingTypes = [...NON_BLOCKING_BLUNDER_TYPES];
|
|
120
|
+
const [totalRows, typeRows, recent7Rows, recent30Rows] = await Promise.all([
|
|
121
|
+
sql`SELECT COUNT(*) AS cnt FROM agent_blunders WHERE tenant_id = ${tid} AND blunder_type != ALL(${nonBlockingTypes})`,
|
|
122
|
+
sql`SELECT blunder_type, COUNT(*) AS cnt FROM agent_blunders WHERE tenant_id = ${tid} GROUP BY blunder_type`,
|
|
123
|
+
sql`SELECT COUNT(*) AS cnt FROM agent_blunders WHERE tenant_id = ${tid} AND recorded_at >= ${now - ms7} AND blunder_type != ALL(${nonBlockingTypes})`,
|
|
124
|
+
sql`SELECT COUNT(*) AS cnt FROM agent_blunders WHERE tenant_id = ${tid} AND recorded_at >= ${now - ms30} AND blunder_type != ALL(${nonBlockingTypes})`
|
|
125
|
+
]);
|
|
126
|
+
const byType = {};
|
|
127
|
+
for (const row of typeRows) {
|
|
128
|
+
byType[String(row.blunder_type)] = Number(row.cnt);
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
total: Number(totalRows[0].cnt),
|
|
132
|
+
byType,
|
|
133
|
+
last7Days: Number(recent7Rows[0].cnt),
|
|
134
|
+
last30Days: Number(recent30Rows[0].cnt)
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
async verifyChain(tenantId) {
|
|
138
|
+
const hmacProtected = false;
|
|
139
|
+
const blunders = await this.list(tenantId);
|
|
140
|
+
if (blunders.length === 0) {
|
|
141
|
+
return { valid: true, verified: 0, note: "Log is empty \u2014 nothing to verify", tamperEvidenceLevel: "none", hmacProtected };
|
|
142
|
+
}
|
|
143
|
+
const firstV2Idx = blunders.findIndex((b) => !!b.hash);
|
|
144
|
+
if (firstV2Idx === -1) {
|
|
145
|
+
return {
|
|
146
|
+
valid: true,
|
|
147
|
+
verified: 0,
|
|
148
|
+
truncated: false,
|
|
149
|
+
note: "No cryptographically-verified entries \u2014 all entries predate hash-chain migration. Chain integrity cannot be confirmed.",
|
|
150
|
+
tamperEvidenceLevel: "none",
|
|
151
|
+
hmacProtected
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const anchor = blunders[firstV2Idx];
|
|
155
|
+
const truncated = anchor.previousHash !== GENESIS_HASH;
|
|
156
|
+
let expectedPrev = anchor.previousHash;
|
|
157
|
+
let seenFirstV2 = false;
|
|
158
|
+
for (let i = firstV2Idx; i < blunders.length; i++) {
|
|
159
|
+
const b = blunders[i];
|
|
160
|
+
if (!b.hash) {
|
|
161
|
+
return { valid: false, firstInvalidIdx: i, reason: `entry at index ${i} (id=${b.id}) is missing hash field after v2 section began \u2014 possible injection`, tamperEvidenceLevel: "hash-chain", hmacProtected };
|
|
162
|
+
}
|
|
163
|
+
if (!seenFirstV2) {
|
|
164
|
+
seenFirstV2 = true;
|
|
165
|
+
} else {
|
|
166
|
+
if (b.previousHash !== expectedPrev) {
|
|
167
|
+
return { valid: false, firstInvalidIdx: i, reason: `previousHash mismatch at index ${i}: expected ${expectedPrev.slice(0, 8)}\u2026 got ${b.previousHash.slice(0, 8)}\u2026`, tamperEvidenceLevel: "hash-chain", hmacProtected };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const { hash: _h, previousHash: _p, ...rest } = b;
|
|
171
|
+
const recomputed = _computeHash(rest, b.previousHash);
|
|
172
|
+
if (recomputed !== b.hash) {
|
|
173
|
+
return { valid: false, firstInvalidIdx: i, reason: `hash mismatch at index ${i} (id=${b.id}): content was modified after recording`, tamperEvidenceLevel: "hash-chain", hmacProtected };
|
|
174
|
+
}
|
|
175
|
+
expectedPrev = b.hash;
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
valid: true,
|
|
179
|
+
truncated,
|
|
180
|
+
verifiedFrom: anchor.id,
|
|
181
|
+
verified: blunders.length - firstV2Idx,
|
|
182
|
+
tamperEvidenceLevel: "hash-chain",
|
|
183
|
+
hmacProtected
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/** Always false in Postgres mode — DB integrity mechanisms replace the HMAC sidecar. */
|
|
187
|
+
isIntegrityViolated() {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
export {
|
|
192
|
+
PgBlunderStore
|
|
193
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { getSql } from "./pg-client.js";
|
|
3
|
+
import { MAX_GATE_HISTORY } from "../../sensor/ci-gate-history.js";
|
|
4
|
+
import logger from "../../sensor/logger.js";
|
|
5
|
+
const DEFAULT_TENANT = "local";
|
|
6
|
+
function rowToEntry(row) {
|
|
7
|
+
const reasonsRaw = row.reasons;
|
|
8
|
+
let reasons = [];
|
|
9
|
+
if (Array.isArray(reasonsRaw)) {
|
|
10
|
+
reasons = reasonsRaw;
|
|
11
|
+
} else if (typeof reasonsRaw === "string") {
|
|
12
|
+
try {
|
|
13
|
+
reasons = JSON.parse(reasonsRaw);
|
|
14
|
+
} catch {
|
|
15
|
+
reasons = [];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
id: String(row.id ?? ""),
|
|
20
|
+
recordedAt: Number(row.recorded_at ?? 0),
|
|
21
|
+
prNumber: row.pr_number != null ? Number(row.pr_number) : null,
|
|
22
|
+
repo: row.repo ? String(row.repo) : null,
|
|
23
|
+
service: row.service ? String(row.service) : null,
|
|
24
|
+
actor: row.actor ? String(row.actor) : null,
|
|
25
|
+
verdict: row.verdict ?? "pass",
|
|
26
|
+
riskScore: Number(row.risk_score ?? 0),
|
|
27
|
+
reasons,
|
|
28
|
+
sha: row.sha ? String(row.sha) : null
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
class PgGateHistoryStore {
|
|
32
|
+
async record(entry, tenantId) {
|
|
33
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
34
|
+
const id = entry.id ?? randomUUID();
|
|
35
|
+
const recordedAt = entry.recordedAt ?? Date.now();
|
|
36
|
+
try {
|
|
37
|
+
const sql = getSql();
|
|
38
|
+
await sql.begin(async (tx) => {
|
|
39
|
+
const existing = await tx`SELECT 1 FROM ci_gate_history WHERE id = ${id} AND tenant_id = ${tid} LIMIT 1`;
|
|
40
|
+
if (existing.length > 0) return;
|
|
41
|
+
await tx`
|
|
42
|
+
INSERT INTO ci_gate_history (
|
|
43
|
+
id, tenant_id, recorded_at, pr_number, repo, service, actor, verdict, risk_score, reasons, sha
|
|
44
|
+
) VALUES (
|
|
45
|
+
${id}, ${tid}, ${recordedAt},
|
|
46
|
+
${entry.prNumber ?? null}, ${entry.repo ?? null}, ${entry.service ?? null}, ${entry.actor ?? null},
|
|
47
|
+
${entry.verdict}, ${entry.riskScore ?? 0}, ${JSON.stringify(entry.reasons ?? [])}, ${entry.sha ?? null}
|
|
48
|
+
)
|
|
49
|
+
ON CONFLICT (id, tenant_id) DO NOTHING
|
|
50
|
+
`;
|
|
51
|
+
await tx`
|
|
52
|
+
DELETE FROM ci_gate_history
|
|
53
|
+
WHERE tenant_id = ${tid}
|
|
54
|
+
AND recorded_at = (
|
|
55
|
+
SELECT recorded_at FROM ci_gate_history
|
|
56
|
+
WHERE tenant_id = ${tid}
|
|
57
|
+
ORDER BY recorded_at ASC, id ASC
|
|
58
|
+
LIMIT 1
|
|
59
|
+
)
|
|
60
|
+
AND (SELECT COUNT(*) FROM ci_gate_history WHERE tenant_id = ${tid}) > ${MAX_GATE_HISTORY}
|
|
61
|
+
`;
|
|
62
|
+
});
|
|
63
|
+
} catch (err) {
|
|
64
|
+
logger.warn({ err }, "pg-ci-gate-history: record failed (non-fatal)");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async listForPR(prNumber, repo, tenantId) {
|
|
68
|
+
const sql = getSql();
|
|
69
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
70
|
+
const rows = repo ? await sql`SELECT * FROM ci_gate_history WHERE tenant_id = ${tid} AND pr_number = ${prNumber} AND repo = ${repo} ORDER BY recorded_at ASC` : await sql`SELECT * FROM ci_gate_history WHERE tenant_id = ${tid} AND pr_number = ${prNumber} ORDER BY recorded_at ASC`;
|
|
71
|
+
return rows.map((r) => rowToEntry(r));
|
|
72
|
+
}
|
|
73
|
+
async list(opts, tenantId) {
|
|
74
|
+
const sql = getSql();
|
|
75
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
76
|
+
const limit = opts?.limit ?? 100;
|
|
77
|
+
const rows = await sql`SELECT * FROM ci_gate_history WHERE tenant_id = ${tid} ORDER BY recorded_at DESC LIMIT ${limit}`;
|
|
78
|
+
return rows.map((r) => rowToEntry(r));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export {
|
|
82
|
+
PgGateHistoryStore
|
|
83
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import postgres from "postgres";
|
|
2
|
+
let _sql = null;
|
|
3
|
+
function getSql() {
|
|
4
|
+
if (!_sql) {
|
|
5
|
+
const url = process.env.MERGEN_PG_URL;
|
|
6
|
+
if (!url) throw new Error("MERGEN_PG_URL is not set");
|
|
7
|
+
_sql = postgres(url, {
|
|
8
|
+
max: 10,
|
|
9
|
+
idle_timeout: 30,
|
|
10
|
+
connect_timeout: 10
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
return _sql;
|
|
14
|
+
}
|
|
15
|
+
async function closeSql() {
|
|
16
|
+
if (_sql) {
|
|
17
|
+
await _sql.end();
|
|
18
|
+
_sql = null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
closeSql,
|
|
23
|
+
getSql
|
|
24
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { getSql } from "./pg-client.js";
|
|
2
|
+
const DEFAULT_TENANT = "local";
|
|
3
|
+
const DEFAULT_RETENTION_HOURS = 72;
|
|
4
|
+
class PgEventStore {
|
|
5
|
+
async init() {
|
|
6
|
+
}
|
|
7
|
+
async push(event, tenantId) {
|
|
8
|
+
const sql = getSql();
|
|
9
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
10
|
+
const type = event.type;
|
|
11
|
+
const ts = event.timestamp ?? Date.now();
|
|
12
|
+
let level = null;
|
|
13
|
+
if (event.type === "console") {
|
|
14
|
+
level = event.level ?? null;
|
|
15
|
+
} else if (event.type === "network") {
|
|
16
|
+
const status = event.status;
|
|
17
|
+
level = status != null ? String(status) : null;
|
|
18
|
+
}
|
|
19
|
+
await sql`
|
|
20
|
+
INSERT INTO events (tenant_id, type, level, data, ts)
|
|
21
|
+
VALUES (${tid}, ${type}, ${level}, ${sql.json(event)}, ${ts})
|
|
22
|
+
`;
|
|
23
|
+
}
|
|
24
|
+
async query(opts) {
|
|
25
|
+
const sql = getSql();
|
|
26
|
+
const tid = opts.tenantId ?? DEFAULT_TENANT;
|
|
27
|
+
const limit = opts.limit ?? 500;
|
|
28
|
+
const rows = await sql`
|
|
29
|
+
SELECT data FROM events
|
|
30
|
+
WHERE tenant_id = ${tid}
|
|
31
|
+
${opts.since != null ? sql`AND ts >= ${opts.since}` : sql``}
|
|
32
|
+
${opts.level != null ? sql`AND level = ${opts.level}` : sql``}
|
|
33
|
+
${opts.type != null ? sql`AND type = ${opts.type}` : sql``}
|
|
34
|
+
ORDER BY ts DESC
|
|
35
|
+
LIMIT ${limit}
|
|
36
|
+
`;
|
|
37
|
+
return rows.map((r) => r.data);
|
|
38
|
+
}
|
|
39
|
+
async size() {
|
|
40
|
+
const sql = getSql();
|
|
41
|
+
const rows = await sql`SELECT COUNT(*)::int AS cnt FROM events`;
|
|
42
|
+
return rows[0]?.cnt ?? 0;
|
|
43
|
+
}
|
|
44
|
+
async clear(tenantId) {
|
|
45
|
+
const sql = getSql();
|
|
46
|
+
if (tenantId) {
|
|
47
|
+
await sql`DELETE FROM events WHERE tenant_id = ${tenantId}`;
|
|
48
|
+
} else {
|
|
49
|
+
await sql`DELETE FROM events`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async pruneOld() {
|
|
53
|
+
const sql = getSql();
|
|
54
|
+
const hours = Number(process.env.MERGEN_RETENTION_HOURS ?? DEFAULT_RETENTION_HOURS);
|
|
55
|
+
await sql`
|
|
56
|
+
DELETE FROM events
|
|
57
|
+
WHERE inserted_at < NOW() - (${hours} || ' hours')::interval
|
|
58
|
+
`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
PgEventStore
|
|
63
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { getSql } from "./pg-client.js";
|
|
2
|
+
const DEFAULT_TENANT = "local";
|
|
3
|
+
function toMs(value) {
|
|
4
|
+
if (!value) return 0;
|
|
5
|
+
if (value instanceof Date) return value.getTime();
|
|
6
|
+
return new Date(value).getTime();
|
|
7
|
+
}
|
|
8
|
+
function toMsNullable(value) {
|
|
9
|
+
if (!value) return null;
|
|
10
|
+
if (value instanceof Date) return value.getTime();
|
|
11
|
+
return new Date(value).getTime();
|
|
12
|
+
}
|
|
13
|
+
function rowToIncident(row) {
|
|
14
|
+
return {
|
|
15
|
+
pid: String(row.pid ?? ""),
|
|
16
|
+
hypothesis: String(row.hypothesis ?? ""),
|
|
17
|
+
tag: String(row.tag ?? ""),
|
|
18
|
+
status: row.status ?? "open",
|
|
19
|
+
assignee: row.assignee ? String(row.assignee) : null,
|
|
20
|
+
notes: Array.isArray(row.notes) ? row.notes : (() => {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(String(row.notes ?? "[]"));
|
|
23
|
+
} catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
})(),
|
|
27
|
+
sha: row.sha ? String(row.sha) : null,
|
|
28
|
+
environment: row.environment ? String(row.environment) : null,
|
|
29
|
+
service: row.service ? String(row.service) : null,
|
|
30
|
+
cluster: row.cluster ? String(row.cluster) : null,
|
|
31
|
+
confidence: Number(row.confidence ?? 0),
|
|
32
|
+
createdAt: toMs(row.created_at),
|
|
33
|
+
updatedAt: toMs(row.updated_at),
|
|
34
|
+
acknowledgedBy: row.acknowledged_by ? String(row.acknowledged_by) : null,
|
|
35
|
+
resolvedAt: toMsNullable(row.resolved_at),
|
|
36
|
+
resolvedAutonomously: Boolean(row.resolved_autonomously),
|
|
37
|
+
causallyCorrect: Boolean(row.causally_correct),
|
|
38
|
+
contextBriefViewedAt: toMsNullable(row.context_brief_viewed_at)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
class PgIncidentStore {
|
|
42
|
+
async init() {
|
|
43
|
+
}
|
|
44
|
+
async upsert(pid, fields, tenantId) {
|
|
45
|
+
const sql = getSql();
|
|
46
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
47
|
+
const notes = JSON.stringify(fields.notes ?? []);
|
|
48
|
+
const rows = await sql`
|
|
49
|
+
INSERT INTO incidents (
|
|
50
|
+
pid, tenant_id, hypothesis, tag, status, assignee, notes,
|
|
51
|
+
sha, environment, service, cluster, confidence,
|
|
52
|
+
acknowledged_by, resolved_at, resolved_autonomously, causally_correct
|
|
53
|
+
) VALUES (
|
|
54
|
+
${pid}, ${tid},
|
|
55
|
+
${fields.hypothesis ?? ""},
|
|
56
|
+
${fields.tag ?? ""},
|
|
57
|
+
${fields.status ?? "open"},
|
|
58
|
+
${fields.assignee ?? null},
|
|
59
|
+
${notes}::jsonb,
|
|
60
|
+
${fields.sha ?? null},
|
|
61
|
+
${fields.environment ?? null},
|
|
62
|
+
${fields.service ?? null},
|
|
63
|
+
${fields.cluster ?? null},
|
|
64
|
+
${fields.confidence ?? 0},
|
|
65
|
+
${fields.acknowledgedBy ?? null},
|
|
66
|
+
${fields.resolvedAt ? new Date(fields.resolvedAt) : null},
|
|
67
|
+
${fields.resolvedAutonomously ?? false},
|
|
68
|
+
${fields.causallyCorrect ?? false}
|
|
69
|
+
)
|
|
70
|
+
ON CONFLICT (tenant_id, pid) DO UPDATE SET
|
|
71
|
+
hypothesis = EXCLUDED.hypothesis,
|
|
72
|
+
tag = EXCLUDED.tag,
|
|
73
|
+
status = EXCLUDED.status,
|
|
74
|
+
assignee = EXCLUDED.assignee,
|
|
75
|
+
notes = EXCLUDED.notes,
|
|
76
|
+
sha = EXCLUDED.sha,
|
|
77
|
+
environment = EXCLUDED.environment,
|
|
78
|
+
service = EXCLUDED.service,
|
|
79
|
+
cluster = EXCLUDED.cluster,
|
|
80
|
+
confidence = EXCLUDED.confidence,
|
|
81
|
+
updated_at = NOW(),
|
|
82
|
+
acknowledged_by = EXCLUDED.acknowledged_by,
|
|
83
|
+
resolved_at = EXCLUDED.resolved_at,
|
|
84
|
+
resolved_autonomously = EXCLUDED.resolved_autonomously,
|
|
85
|
+
causally_correct = EXCLUDED.causally_correct
|
|
86
|
+
RETURNING *
|
|
87
|
+
`;
|
|
88
|
+
const incident = rowToIncident(rows[0]);
|
|
89
|
+
if (incident.service) {
|
|
90
|
+
await this.updateServiceEdges(incident.service, incident.updatedAt, void 0, tid);
|
|
91
|
+
}
|
|
92
|
+
return incident;
|
|
93
|
+
}
|
|
94
|
+
async get(pid, tenantId) {
|
|
95
|
+
const sql = getSql();
|
|
96
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
97
|
+
const rows = await sql`
|
|
98
|
+
SELECT * FROM incidents WHERE tenant_id = ${tid} AND pid = ${pid}
|
|
99
|
+
`;
|
|
100
|
+
if (rows.length === 0) return null;
|
|
101
|
+
return rowToIncident(rows[0]);
|
|
102
|
+
}
|
|
103
|
+
async list(status, limit = 50, tenantId) {
|
|
104
|
+
const sql = getSql();
|
|
105
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
106
|
+
const rows = await sql`
|
|
107
|
+
SELECT * FROM incidents
|
|
108
|
+
WHERE tenant_id = ${tid}
|
|
109
|
+
${status != null ? sql`AND status = ${status}` : sql``}
|
|
110
|
+
ORDER BY updated_at DESC
|
|
111
|
+
LIMIT ${limit}
|
|
112
|
+
`;
|
|
113
|
+
return rows.map((r) => rowToIncident(r));
|
|
114
|
+
}
|
|
115
|
+
async addNote(pid, note, author, tenantId) {
|
|
116
|
+
const inc = await this.get(pid, tenantId);
|
|
117
|
+
if (!inc) return null;
|
|
118
|
+
const entry = author ? `[${author}] ${note}` : note;
|
|
119
|
+
return this.upsert(pid, { notes: [...inc.notes, entry] }, tenantId);
|
|
120
|
+
}
|
|
121
|
+
async markContextViewed(pid, tenantId) {
|
|
122
|
+
const sql = getSql();
|
|
123
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
124
|
+
await sql`
|
|
125
|
+
UPDATE incidents
|
|
126
|
+
SET context_brief_viewed_at = NOW()
|
|
127
|
+
WHERE tenant_id = ${tid} AND pid = ${pid} AND context_brief_viewed_at IS NULL
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
130
|
+
async coOccurringServices(service, windowMs = 10 * 60 * 1e3, limit = 4, tenantId) {
|
|
131
|
+
const sql = getSql();
|
|
132
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
133
|
+
const intervalSec = Math.round(windowMs / 1e3);
|
|
134
|
+
const rows = await sql`
|
|
135
|
+
SELECT other.service, COUNT(*) AS cnt
|
|
136
|
+
FROM incidents AS base
|
|
137
|
+
JOIN incidents AS other
|
|
138
|
+
ON other.service != base.service
|
|
139
|
+
AND other.service IS NOT NULL
|
|
140
|
+
AND other.created_at BETWEEN base.created_at - (${intervalSec} || ' seconds')::interval
|
|
141
|
+
AND base.created_at + (${intervalSec} || ' seconds')::interval
|
|
142
|
+
WHERE base.tenant_id = ${tid}
|
|
143
|
+
AND other.tenant_id = ${tid}
|
|
144
|
+
AND base.service = ${service}
|
|
145
|
+
GROUP BY other.service
|
|
146
|
+
ORDER BY cnt DESC
|
|
147
|
+
LIMIT ${limit}
|
|
148
|
+
`;
|
|
149
|
+
return rows.map((r) => ({ service: String(r.service ?? ""), count: Number(r.cnt ?? 0) }));
|
|
150
|
+
}
|
|
151
|
+
async updateServiceEdges(service, at, windowMs = 10 * 60 * 1e3, tenantId) {
|
|
152
|
+
const sql = getSql();
|
|
153
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
154
|
+
const coServices = await this.coOccurringServices(service, windowMs, 20, tid);
|
|
155
|
+
const lastAt = new Date(at);
|
|
156
|
+
for (const { service: target } of coServices) {
|
|
157
|
+
for (const [src, tgt] of [[service, target], [target, service]]) {
|
|
158
|
+
await sql`
|
|
159
|
+
INSERT INTO service_edges (tenant_id, source, target, weight, last_incident_at)
|
|
160
|
+
VALUES (${tid}, ${src}, ${tgt}, 1, ${lastAt})
|
|
161
|
+
ON CONFLICT (tenant_id, source, target)
|
|
162
|
+
DO UPDATE SET
|
|
163
|
+
weight = service_edges.weight + 1,
|
|
164
|
+
last_incident_at = EXCLUDED.last_incident_at
|
|
165
|
+
`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async getInteractionGraph(service, tenantId) {
|
|
170
|
+
const sql = getSql();
|
|
171
|
+
const tid = tenantId ?? DEFAULT_TENANT;
|
|
172
|
+
const rows = await sql`
|
|
173
|
+
SELECT source, target, weight, last_incident_at
|
|
174
|
+
FROM service_edges
|
|
175
|
+
WHERE tenant_id = ${tid}
|
|
176
|
+
${service != null ? sql`AND (source = ${service} OR target = ${service})` : sql``}
|
|
177
|
+
ORDER BY weight DESC
|
|
178
|
+
LIMIT ${service != null ? 100 : 200}
|
|
179
|
+
`;
|
|
180
|
+
return rows.map((r) => ({
|
|
181
|
+
source: String(r.source ?? ""),
|
|
182
|
+
target: String(r.target ?? ""),
|
|
183
|
+
weight: Number(r.weight ?? 0),
|
|
184
|
+
lastIncidentAt: toMs(r.last_incident_at)
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
export {
|
|
189
|
+
PgIncidentStore
|
|
190
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { getSql } from "./pg-client.js";
|
|
5
|
+
async function runMigrations() {
|
|
6
|
+
const sql = getSql();
|
|
7
|
+
await sql`
|
|
8
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
9
|
+
id SERIAL PRIMARY KEY,
|
|
10
|
+
name TEXT NOT NULL UNIQUE,
|
|
11
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
12
|
+
)
|
|
13
|
+
`;
|
|
14
|
+
const migrationsDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "migrations");
|
|
15
|
+
const files = fs.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
|
|
16
|
+
for (const file of files) {
|
|
17
|
+
const applied = await sql`SELECT 1 FROM _migrations WHERE name = ${file}`;
|
|
18
|
+
if (applied.length > 0) continue;
|
|
19
|
+
const content = fs.readFileSync(path.join(migrationsDir, file), "utf8");
|
|
20
|
+
await sql.unsafe(content);
|
|
21
|
+
await sql`INSERT INTO _migrations (name) VALUES (${file})`;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
runMigrations
|
|
26
|
+
};
|