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,140 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { compactCorpus } from "./override-corpus.js";
|
|
3
|
+
import { loadEnterprisePolicy, saveEnterprisePolicy } from "./enterprise-policy-engine.js";
|
|
4
|
+
import { stageProposal } from "./policy-proposals.js";
|
|
5
|
+
import logger from "../sensor/logger.js";
|
|
6
|
+
const MIN_OCCURRENCES = 3;
|
|
7
|
+
function _actionForReason(reason) {
|
|
8
|
+
switch (reason) {
|
|
9
|
+
case "batch-window":
|
|
10
|
+
case "maintenance-window":
|
|
11
|
+
case "compliance-hold":
|
|
12
|
+
return "block";
|
|
13
|
+
case "wrong-fix":
|
|
14
|
+
case "wrong-diagnosis":
|
|
15
|
+
case "cost-constraint":
|
|
16
|
+
case "on-call-discretion":
|
|
17
|
+
case "prefer-read-replica":
|
|
18
|
+
case "other":
|
|
19
|
+
return "warn";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function _humanReason(reason) {
|
|
23
|
+
const labels = {
|
|
24
|
+
"batch-window": "batch settlement window",
|
|
25
|
+
"maintenance-window": "maintenance window",
|
|
26
|
+
"compliance-hold": "compliance hold",
|
|
27
|
+
"wrong-fix": "incorrect fix applied previously",
|
|
28
|
+
"wrong-diagnosis": "incorrect root cause diagnosis",
|
|
29
|
+
"cost-constraint": "cost ceiling constraint",
|
|
30
|
+
"on-call-discretion": "on-call discretion override",
|
|
31
|
+
"prefer-read-replica": "read-replica preferred",
|
|
32
|
+
"other": "team override"
|
|
33
|
+
};
|
|
34
|
+
return labels[reason] ?? reason;
|
|
35
|
+
}
|
|
36
|
+
function _ruleHash(rule) {
|
|
37
|
+
const key = JSON.stringify({
|
|
38
|
+
tag: rule.incidentTag,
|
|
39
|
+
svc: rule.service,
|
|
40
|
+
reason: rule.overrideReason,
|
|
41
|
+
day: rule.dayOfWeek,
|
|
42
|
+
hour: rule.hourWindow
|
|
43
|
+
});
|
|
44
|
+
return createHash("sha256").update(key).digest("hex").slice(0, 16);
|
|
45
|
+
}
|
|
46
|
+
function _isAlreadyCovered(hash) {
|
|
47
|
+
const policy = loadEnterprisePolicy();
|
|
48
|
+
return policy.rules.some((r) => r.id === `corpus_auto_${hash}`);
|
|
49
|
+
}
|
|
50
|
+
function synthesizeRulesFromCorpus(scope) {
|
|
51
|
+
const compacted = compactCorpus().filter(
|
|
52
|
+
(cr) => !scope || cr.incidentTag === scope.incidentTag && cr.service === scope.service
|
|
53
|
+
);
|
|
54
|
+
const results = [];
|
|
55
|
+
for (const cr of compacted) {
|
|
56
|
+
if (cr.occurrences < MIN_OCCURRENCES) continue;
|
|
57
|
+
if (cr.commandSignatures.length === 0) {
|
|
58
|
+
logger.warn(
|
|
59
|
+
{ incidentTag: cr.incidentTag, service: cr.service },
|
|
60
|
+
"corpus-to-policy: skipping synthesis \u2014 no command signatures could be extracted from this bucket"
|
|
61
|
+
);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const hash = _ruleHash(cr);
|
|
65
|
+
if (_isAlreadyCovered(hash)) continue;
|
|
66
|
+
const action = _actionForReason(cr.overrideReason);
|
|
67
|
+
const humanLabel = _humanReason(cr.overrideReason);
|
|
68
|
+
const conditions = {
|
|
69
|
+
commands: cr.commandSignatures
|
|
70
|
+
};
|
|
71
|
+
if (cr.dayOfWeek !== null) {
|
|
72
|
+
conditions.daysOfWeek = [cr.dayOfWeek];
|
|
73
|
+
}
|
|
74
|
+
if (cr.hourWindow !== null) {
|
|
75
|
+
conditions.hourWindow = cr.hourWindow;
|
|
76
|
+
}
|
|
77
|
+
conditions.actorType = "ai";
|
|
78
|
+
const dayLabel = cr.dayOfWeek !== null ? ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][cr.dayOfWeek] : null;
|
|
79
|
+
const hourLabel = cr.hourWindow ? `${String(cr.hourWindow[0]).padStart(2, "0")}:00\u2013${String(cr.hourWindow[1]).padStart(2, "0")}:00 UTC` : null;
|
|
80
|
+
const timeDesc = [dayLabel, hourLabel].filter(Boolean).join(" ");
|
|
81
|
+
const commandsDesc = cr.commandSignatures.map((s) => `"${s}"`).join(", ");
|
|
82
|
+
const rule = {
|
|
83
|
+
id: `corpus_auto_${hash}`,
|
|
84
|
+
name: `[Auto] ${cr.service !== "unknown" ? cr.service + ": " : ""}${humanLabel}${timeDesc ? " (" + timeDesc + ")" : ""}`,
|
|
85
|
+
description: `Auto-synthesized from ${cr.occurrences} override events. Tag: ${cr.incidentTag}. Reason: ${humanLabel}. Scoped to commands: ${commandsDesc}.`,
|
|
86
|
+
action,
|
|
87
|
+
reason: `Corpus-derived policy: this action was overridden ${cr.occurrences} time(s) due to ${humanLabel}${timeDesc ? " during " + timeDesc : ""}.`,
|
|
88
|
+
conditions
|
|
89
|
+
};
|
|
90
|
+
results.push({ rule, sourceOccurrences: cr.occurrences, compactedRule: cr });
|
|
91
|
+
logger.info(
|
|
92
|
+
{ ruleId: rule.id, occurrences: cr.occurrences, action },
|
|
93
|
+
"corpus-to-policy: synthesized new policy rule from override corpus"
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return results;
|
|
97
|
+
}
|
|
98
|
+
function _activateRule(rule) {
|
|
99
|
+
const policy = loadEnterprisePolicy();
|
|
100
|
+
if (policy.rules.some((r) => r.id === rule.id)) return;
|
|
101
|
+
saveEnterprisePolicy({ ...policy, rules: [...policy.rules, rule] });
|
|
102
|
+
}
|
|
103
|
+
function autoActivateReviewedRules(incidentTag, service) {
|
|
104
|
+
const staged = synthesizeRulesFromCorpus({ incidentTag, service });
|
|
105
|
+
for (const { rule } of staged) {
|
|
106
|
+
_activateRule(rule);
|
|
107
|
+
logger.info(
|
|
108
|
+
{ ruleId: rule.id, incidentTag, service },
|
|
109
|
+
"corpus-to-policy: auto-activated rule into live policy after operator review"
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return staged;
|
|
113
|
+
}
|
|
114
|
+
function autoCorpusProposeEnabled() {
|
|
115
|
+
return process.env.MERGEN_AUTO_CORPUS_PROPOSE !== "false";
|
|
116
|
+
}
|
|
117
|
+
function proposeRulesFromCorpus(scope) {
|
|
118
|
+
if (!autoCorpusProposeEnabled()) return [];
|
|
119
|
+
const synthesized = synthesizeRulesFromCorpus(scope);
|
|
120
|
+
const proposals = [];
|
|
121
|
+
for (const { rule, sourceOccurrences } of synthesized) {
|
|
122
|
+
const holdRule = { ...rule, action: "warn" };
|
|
123
|
+
const staged = stageProposal(rule.id, holdRule, sourceOccurrences);
|
|
124
|
+
if (staged) proposals.push(staged);
|
|
125
|
+
}
|
|
126
|
+
if (proposals.length > 0) {
|
|
127
|
+
logger.info({ count: proposals.length }, "corpus-to-policy: staged HOLD-only policy proposals");
|
|
128
|
+
}
|
|
129
|
+
return proposals;
|
|
130
|
+
}
|
|
131
|
+
function activateProposedRule(rule) {
|
|
132
|
+
_activateRule(rule);
|
|
133
|
+
}
|
|
134
|
+
export {
|
|
135
|
+
activateProposedRule,
|
|
136
|
+
autoActivateReviewedRules,
|
|
137
|
+
autoCorpusProposeEnabled,
|
|
138
|
+
proposeRulesFromCorpus,
|
|
139
|
+
synthesizeRulesFromCorpus
|
|
140
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts";
|
|
4
|
+
import { DATA_DIR } from "../sensor/paths.js";
|
|
5
|
+
import { getStores } from "../storage/store-registry.js";
|
|
6
|
+
import { getTraceId } from "./trace-context.js";
|
|
7
|
+
import logger from "../sensor/logger.js";
|
|
8
|
+
import { requestGcpCredential } from "./gcp-broker.js";
|
|
9
|
+
import { requestAzureCredential } from "./azure-broker.js";
|
|
10
|
+
const CREDENTIAL_SCOPES_FILE = path.join(DATA_DIR, "credential-scopes.json");
|
|
11
|
+
function loadScopes() {
|
|
12
|
+
try {
|
|
13
|
+
const raw = JSON.parse(fs.readFileSync(CREDENTIAL_SCOPES_FILE, "utf8"));
|
|
14
|
+
return raw.scopes ?? {};
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function providerOf(scopeConfig) {
|
|
20
|
+
return scopeConfig.provider ?? "aws";
|
|
21
|
+
}
|
|
22
|
+
const STS_MIN_DURATION_SECONDS = 900;
|
|
23
|
+
const STS_MAX_DURATION_SECONDS = 43200;
|
|
24
|
+
function sessionNameFor(agentId, taskId) {
|
|
25
|
+
const raw = `mergen-${agentId}-${taskId}`.replace(/[^\w+=,.@-]/g, "_");
|
|
26
|
+
return raw.slice(0, 64);
|
|
27
|
+
}
|
|
28
|
+
async function requestAwsCredential(request, scopeConfig) {
|
|
29
|
+
const requested = request.durationSeconds ?? scopeConfig.maxDurationSeconds;
|
|
30
|
+
const durationSeconds = Math.min(
|
|
31
|
+
Math.max(requested, STS_MIN_DURATION_SECONDS),
|
|
32
|
+
Math.min(scopeConfig.maxDurationSeconds, STS_MAX_DURATION_SECONDS)
|
|
33
|
+
);
|
|
34
|
+
const client = new STSClient({ region: scopeConfig.region });
|
|
35
|
+
const response = await client.send(new AssumeRoleCommand({
|
|
36
|
+
RoleArn: scopeConfig.roleArn,
|
|
37
|
+
RoleSessionName: sessionNameFor(request.agentId, request.taskId),
|
|
38
|
+
DurationSeconds: durationSeconds
|
|
39
|
+
}));
|
|
40
|
+
const creds = response.Credentials;
|
|
41
|
+
if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken || !creds.Expiration) {
|
|
42
|
+
throw new Error("credential-broker: AssumeRole succeeded but returned incomplete credentials");
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
provider: "aws",
|
|
46
|
+
accessKeyId: creds.AccessKeyId,
|
|
47
|
+
secretAccessKey: creds.SecretAccessKey,
|
|
48
|
+
sessionToken: creds.SessionToken,
|
|
49
|
+
expiration: creds.Expiration,
|
|
50
|
+
roleArn: scopeConfig.roleArn,
|
|
51
|
+
scope: request.scope,
|
|
52
|
+
durationEnforced: true
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async function requestEphemeralCredential(request) {
|
|
56
|
+
const scopes = loadScopes();
|
|
57
|
+
const scopeConfig = scopes[request.scope];
|
|
58
|
+
if (!scopeConfig || scopeConfig.mergenTenantId !== void 0 && scopeConfig.mergenTenantId !== request.callerTenantId) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`credential-broker: unknown scope "${request.scope}" \u2014 configure it in ${CREDENTIAL_SCOPES_FILE} first`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const provider = providerOf(scopeConfig);
|
|
64
|
+
let cred;
|
|
65
|
+
if (provider === "gcp") {
|
|
66
|
+
cred = await requestGcpCredential(request.agentId, request.scope, scopeConfig, request.durationSeconds);
|
|
67
|
+
} else if (provider === "azure") {
|
|
68
|
+
cred = await requestAzureCredential(request.agentId, request.scope, scopeConfig, request.durationSeconds);
|
|
69
|
+
} else {
|
|
70
|
+
cred = await requestAwsCredential(request, scopeConfig);
|
|
71
|
+
}
|
|
72
|
+
await getStores().blunders.record({
|
|
73
|
+
blunderType: "credential_issued",
|
|
74
|
+
command: `credential:${request.scope}`,
|
|
75
|
+
blockReason: `Ephemeral ${provider} credential issued, expires ${cred.expiration.toISOString()}`,
|
|
76
|
+
service: "credential-broker",
|
|
77
|
+
tag: "credential_broker",
|
|
78
|
+
actor: "agent",
|
|
79
|
+
pid: null,
|
|
80
|
+
confidenceScore: null,
|
|
81
|
+
triggeredRules: null,
|
|
82
|
+
agentId: request.agentId,
|
|
83
|
+
traceId: getTraceId()
|
|
84
|
+
});
|
|
85
|
+
logger.info(
|
|
86
|
+
{ agentId: request.agentId, scope: request.scope, provider, expiresAt: cred.expiration },
|
|
87
|
+
"credential-broker: ephemeral credential issued"
|
|
88
|
+
);
|
|
89
|
+
try {
|
|
90
|
+
void getStores().actionLedger.append({
|
|
91
|
+
agentId: request.agentId,
|
|
92
|
+
tool: "request_credential",
|
|
93
|
+
command: `credential:${request.scope}`,
|
|
94
|
+
resource: request.scope,
|
|
95
|
+
verdict: "pass",
|
|
96
|
+
reason: `Ephemeral ${provider} credential issued`,
|
|
97
|
+
credentialIssued: true,
|
|
98
|
+
credentialScope: request.scope,
|
|
99
|
+
environment: process.env.MERGEN_ENVIRONMENT ?? null,
|
|
100
|
+
traceId: getTraceId()
|
|
101
|
+
}, request.callerTenantId);
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
return cred;
|
|
105
|
+
}
|
|
106
|
+
function listConfiguredScopes(callerTenantId) {
|
|
107
|
+
return Object.entries(loadScopes()).filter(([, cfg]) => cfg.mergenTenantId === void 0 || cfg.mergenTenantId === callerTenantId).map(([name]) => name);
|
|
108
|
+
}
|
|
109
|
+
export {
|
|
110
|
+
listConfiguredScopes,
|
|
111
|
+
requestEphemeralCredential
|
|
112
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getStores } from "../storage/store-registry.js";
|
|
2
|
+
const DEFAULT_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
3
|
+
async function detectCredentialMisuse(opts = {}) {
|
|
4
|
+
const windowMs = opts.windowMs ?? DEFAULT_WINDOW_MS;
|
|
5
|
+
const from = Date.now() - windowMs;
|
|
6
|
+
const rows = await getStores().actionLedger.queryCredentialUsage({ agentId: opts.agentId, from }, opts.tenantId);
|
|
7
|
+
const groups = /* @__PURE__ */ new Map();
|
|
8
|
+
for (const r of rows) {
|
|
9
|
+
const scope = r.credentialScope ?? "unknown";
|
|
10
|
+
const env = r.environment ?? "unknown";
|
|
11
|
+
const key = `${r.agentId ?? ""}\0${scope}`;
|
|
12
|
+
let g = groups.get(key);
|
|
13
|
+
if (!g) {
|
|
14
|
+
g = { agentId: r.agentId, scope, envs: /* @__PURE__ */ new Set(), count: 0, firstTs: r.ts, lastTs: r.ts };
|
|
15
|
+
groups.set(key, g);
|
|
16
|
+
}
|
|
17
|
+
g.envs.add(env);
|
|
18
|
+
g.count++;
|
|
19
|
+
if (r.ts < g.firstTs) g.firstTs = r.ts;
|
|
20
|
+
if (r.ts > g.lastTs) g.lastTs = r.ts;
|
|
21
|
+
}
|
|
22
|
+
const findings = [];
|
|
23
|
+
for (const g of groups.values()) {
|
|
24
|
+
if (g.envs.size >= 2) {
|
|
25
|
+
findings.push({
|
|
26
|
+
agentId: g.agentId,
|
|
27
|
+
scope: g.scope,
|
|
28
|
+
environments: [...g.envs].sort(),
|
|
29
|
+
issuances: g.count,
|
|
30
|
+
firstTs: g.firstTs,
|
|
31
|
+
lastTs: g.lastTs
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return findings.sort((a, b) => b.lastTs - a.lastTs);
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
detectCredentialMisuse
|
|
39
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
function fingerprintConsole(e) {
|
|
3
|
+
const msg = e.args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ").trim().slice(0, 120);
|
|
4
|
+
return `${e.level}::${msg}`;
|
|
5
|
+
}
|
|
6
|
+
function fingerprintNetwork(n) {
|
|
7
|
+
const url = n.url.split("?")[0];
|
|
8
|
+
return `${n.method}::${url}::${n.status}`;
|
|
9
|
+
}
|
|
10
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
11
|
+
function captureSnapshot(errors, warnings, networkFailures) {
|
|
12
|
+
return {
|
|
13
|
+
capturedAt: Date.now(),
|
|
14
|
+
errors: errors.map((e) => ({
|
|
15
|
+
fingerprint: fingerprintConsole(e),
|
|
16
|
+
message: e.args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ").slice(0, 200),
|
|
17
|
+
level: e.level,
|
|
18
|
+
timestamp: e.timestamp
|
|
19
|
+
})),
|
|
20
|
+
warnings: warnings.map((e) => ({
|
|
21
|
+
fingerprint: fingerprintConsole(e),
|
|
22
|
+
message: e.args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ").slice(0, 200),
|
|
23
|
+
level: e.level,
|
|
24
|
+
timestamp: e.timestamp
|
|
25
|
+
})),
|
|
26
|
+
networkFailures: networkFailures.map((n) => ({
|
|
27
|
+
fingerprint: fingerprintNetwork(n),
|
|
28
|
+
summary: `${n.method} ${n.url.split("?")[0]} \u2192 ${n.status || "NET_ERR"}`,
|
|
29
|
+
status: n.status,
|
|
30
|
+
timestamp: n.timestamp
|
|
31
|
+
}))
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function diffSnapshots(baseline, current) {
|
|
35
|
+
const baseErrFPs = new Set(baseline.errors.map((e) => e.fingerprint));
|
|
36
|
+
const baseNetFPs = new Set(baseline.networkFailures.map((n) => n.fingerprint));
|
|
37
|
+
const curErrFPs = new Set(current.errors.map((e) => e.fingerprint));
|
|
38
|
+
const curNetFPs = new Set(current.networkFailures.map((n) => n.fingerprint));
|
|
39
|
+
const resolved = baseline.errors.filter((e) => !curErrFPs.has(e.fingerprint));
|
|
40
|
+
const persisted = baseline.errors.filter((e) => curErrFPs.has(e.fingerprint));
|
|
41
|
+
const newErrors = current.errors.filter((e) => !baseErrFPs.has(e.fingerprint));
|
|
42
|
+
const resolvedNetworkFailures = baseline.networkFailures.filter((n) => !curNetFPs.has(n.fingerprint));
|
|
43
|
+
const newNetworkFailures = current.networkFailures.filter((n) => !baseNetFPs.has(n.fingerprint));
|
|
44
|
+
return {
|
|
45
|
+
resolved,
|
|
46
|
+
persisted,
|
|
47
|
+
newErrors,
|
|
48
|
+
resolvedNetworkFailures,
|
|
49
|
+
newNetworkFailures,
|
|
50
|
+
isFixed: persisted.length === 0 && newErrors.length === 0 && baseline.errors.length > 0
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function startDebugSession(description, baseline, targetComponent) {
|
|
54
|
+
const session = {
|
|
55
|
+
id: randomUUID(),
|
|
56
|
+
description,
|
|
57
|
+
targetComponent,
|
|
58
|
+
baseline,
|
|
59
|
+
iterations: [],
|
|
60
|
+
startedAt: Date.now(),
|
|
61
|
+
resolved: false
|
|
62
|
+
};
|
|
63
|
+
sessions.set(session.id, session);
|
|
64
|
+
return session;
|
|
65
|
+
}
|
|
66
|
+
function checkpointSession(sessionId, current, note) {
|
|
67
|
+
const session = sessions.get(sessionId);
|
|
68
|
+
if (!session) return null;
|
|
69
|
+
const diff = diffSnapshots(session.baseline, current);
|
|
70
|
+
session.iterations.push({
|
|
71
|
+
index: session.iterations.length + 1,
|
|
72
|
+
note,
|
|
73
|
+
timestamp: Date.now(),
|
|
74
|
+
diff
|
|
75
|
+
});
|
|
76
|
+
if (diff.isFixed) session.resolved = true;
|
|
77
|
+
return { session, diff };
|
|
78
|
+
}
|
|
79
|
+
function endDebugSession(sessionId, current) {
|
|
80
|
+
const session = sessions.get(sessionId);
|
|
81
|
+
if (!session) return null;
|
|
82
|
+
const diff = diffSnapshots(session.baseline, current);
|
|
83
|
+
session.endedAt = Date.now();
|
|
84
|
+
session.resolved = diff.isFixed;
|
|
85
|
+
sessions.delete(sessionId);
|
|
86
|
+
return { session, diff };
|
|
87
|
+
}
|
|
88
|
+
function getSession(sessionId) {
|
|
89
|
+
return sessions.get(sessionId);
|
|
90
|
+
}
|
|
91
|
+
function listActiveSessions() {
|
|
92
|
+
return Array.from(sessions.values());
|
|
93
|
+
}
|
|
94
|
+
function clearAllSessions() {
|
|
95
|
+
const count = sessions.size;
|
|
96
|
+
sessions.clear();
|
|
97
|
+
return count;
|
|
98
|
+
}
|
|
99
|
+
export {
|
|
100
|
+
captureSnapshot,
|
|
101
|
+
checkpointSession,
|
|
102
|
+
clearAllSessions,
|
|
103
|
+
diffSnapshots,
|
|
104
|
+
endDebugSession,
|
|
105
|
+
getSession,
|
|
106
|
+
listActiveSessions,
|
|
107
|
+
startDebugSession
|
|
108
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const LOW_ACCURACY_THRESHOLD = 0.7;
|
|
2
|
+
function blendEscalation(input) {
|
|
3
|
+
const { similarity, calibration } = input;
|
|
4
|
+
const reasons = [];
|
|
5
|
+
let hold = false;
|
|
6
|
+
let confidence = 1;
|
|
7
|
+
if (similarity.recommendation === "hold") {
|
|
8
|
+
hold = true;
|
|
9
|
+
confidence -= 0.4;
|
|
10
|
+
reasons.push(similarity.rationale);
|
|
11
|
+
}
|
|
12
|
+
if (calibration && !calibration.trusted) {
|
|
13
|
+
hold = true;
|
|
14
|
+
confidence -= 0.3;
|
|
15
|
+
reasons.push(
|
|
16
|
+
`Calibration for '${calibration.tag}' is not yet trusted (${calibration.verdicts} verdicts, ${Math.round(calibration.accuracy * 100)}% accuracy).`
|
|
17
|
+
);
|
|
18
|
+
} else if (calibration && calibration.accuracy < LOW_ACCURACY_THRESHOLD) {
|
|
19
|
+
hold = true;
|
|
20
|
+
confidence -= 0.2;
|
|
21
|
+
reasons.push(
|
|
22
|
+
`Calibration accuracy for '${calibration.tag}' is ${Math.round(calibration.accuracy * 100)}%, below the ${Math.round(LOW_ACCURACY_THRESHOLD * 100)}% confidence floor.`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
if (reasons.length === 0) {
|
|
26
|
+
reasons.push("No similar past actions with negative outcomes, and calibration for this tag is trusted.");
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
recommendation: hold ? "hold" : "none",
|
|
30
|
+
confidence: Math.max(0, Math.min(1, confidence)),
|
|
31
|
+
reasons
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export {
|
|
35
|
+
blendEscalation
|
|
36
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
function summarizeOverride(e) {
|
|
2
|
+
return {
|
|
3
|
+
source: "override",
|
|
4
|
+
recordedAt: e.recordedAt,
|
|
5
|
+
service: e.service,
|
|
6
|
+
summary: `${e.overrideReason}${e.note ? `: ${e.note}` : ""} \u2014 proposed \`${e.proposedCommand.slice(0, 80)}\``,
|
|
7
|
+
outcome: e.outcome ?? null
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function summarizeShadow(e) {
|
|
11
|
+
const cmd = e.command ? `\`${e.command.slice(0, 80)}\`` : "no command";
|
|
12
|
+
return {
|
|
13
|
+
source: "shadow",
|
|
14
|
+
recordedAt: e.recordedAt,
|
|
15
|
+
service: e.service,
|
|
16
|
+
summary: `would have run: ${cmd} (${e.skipReason})`,
|
|
17
|
+
outcome: e.humanVerdict ?? null
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function buildDecisionMemory(tag, service, overrides, shadowEntries, topRule) {
|
|
21
|
+
const entries = [
|
|
22
|
+
...overrides.map(summarizeOverride),
|
|
23
|
+
...shadowEntries.map(summarizeShadow)
|
|
24
|
+
].sort((a, b) => b.recordedAt - a.recordedAt);
|
|
25
|
+
const overrideOutcomes = { resolved: 0, escalated: 0, unresolved: 0, unknown: 0 };
|
|
26
|
+
for (const e of overrides) {
|
|
27
|
+
if (e.outcome) overrideOutcomes[e.outcome]++;
|
|
28
|
+
else overrideOutcomes.unknown++;
|
|
29
|
+
}
|
|
30
|
+
const shadowOutcomes = { wouldApprove: 0, wouldOverride: 0, unreviewed: 0 };
|
|
31
|
+
for (const e of shadowEntries) {
|
|
32
|
+
if (e.humanVerdict === "would-approve") shadowOutcomes.wouldApprove++;
|
|
33
|
+
else if (e.humanVerdict === "would-override") shadowOutcomes.wouldOverride++;
|
|
34
|
+
else shadowOutcomes.unreviewed++;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
tag,
|
|
38
|
+
service,
|
|
39
|
+
totalEntries: entries.length,
|
|
40
|
+
entries,
|
|
41
|
+
topRule,
|
|
42
|
+
outcomeBreakdown: { overrides: overrideOutcomes, shadow: shadowOutcomes }
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
buildDecisionMemory
|
|
47
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { commandSignature } from "./override-corpus.js";
|
|
2
|
+
import { normalizeForMatching } from "./normalize.js";
|
|
3
|
+
const TOP_N = 5;
|
|
4
|
+
const MIN_SIMILARITY = 0.34;
|
|
5
|
+
const MIN_MATCHES_FOR_RECOMMENDATION = 2;
|
|
6
|
+
const NEGATIVE_OUTCOMES = /* @__PURE__ */ new Set(["escalated", "unresolved", "would-override"]);
|
|
7
|
+
function tokenSet(text) {
|
|
8
|
+
return new Set(normalizeForMatching(text).split(" ").filter(Boolean));
|
|
9
|
+
}
|
|
10
|
+
function jaccard(a, b) {
|
|
11
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
12
|
+
let intersection = 0;
|
|
13
|
+
for (const t of a) if (b.has(t)) intersection++;
|
|
14
|
+
const union = a.size + b.size - intersection;
|
|
15
|
+
return union === 0 ? 0 : intersection / union;
|
|
16
|
+
}
|
|
17
|
+
function scoreCandidate(proposedTokens, proposedSig, candidateCommand, candidateService, service) {
|
|
18
|
+
const reasons = [];
|
|
19
|
+
let score = 0;
|
|
20
|
+
const candidateSig = commandSignature(candidateCommand);
|
|
21
|
+
if (candidateSig && candidateSig === proposedSig) {
|
|
22
|
+
score += 0.5;
|
|
23
|
+
reasons.push("same command signature");
|
|
24
|
+
}
|
|
25
|
+
const overlap = jaccard(proposedTokens, tokenSet(candidateCommand));
|
|
26
|
+
if (overlap > 0) {
|
|
27
|
+
score += overlap * 0.4;
|
|
28
|
+
if (overlap >= 0.3) reasons.push(`${Math.round(overlap * 100)}% command text overlap`);
|
|
29
|
+
}
|
|
30
|
+
if (service && candidateService === service) {
|
|
31
|
+
score += 0.1;
|
|
32
|
+
reasons.push("same service");
|
|
33
|
+
}
|
|
34
|
+
return { similarity: Math.min(1, score), matchReasons: reasons };
|
|
35
|
+
}
|
|
36
|
+
function findSimilarDecisions(proposedCommand, service, overrides, shadowEntries) {
|
|
37
|
+
const proposedTokens = tokenSet(proposedCommand);
|
|
38
|
+
const proposedSig = commandSignature(proposedCommand);
|
|
39
|
+
const candidates = [];
|
|
40
|
+
for (const e of overrides) {
|
|
41
|
+
const { similarity, matchReasons } = scoreCandidate(proposedTokens, proposedSig, e.proposedCommand, e.service, service);
|
|
42
|
+
if (similarity < MIN_SIMILARITY) continue;
|
|
43
|
+
candidates.push({
|
|
44
|
+
source: "override",
|
|
45
|
+
recordedAt: e.recordedAt,
|
|
46
|
+
service: e.service,
|
|
47
|
+
command: e.proposedCommand,
|
|
48
|
+
outcome: e.outcome ?? null,
|
|
49
|
+
similarity,
|
|
50
|
+
matchReasons
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
for (const e of shadowEntries) {
|
|
54
|
+
if (!e.command) continue;
|
|
55
|
+
const { similarity, matchReasons } = scoreCandidate(proposedTokens, proposedSig, e.command, e.service, service);
|
|
56
|
+
if (similarity < MIN_SIMILARITY) continue;
|
|
57
|
+
candidates.push({
|
|
58
|
+
source: "shadow",
|
|
59
|
+
recordedAt: e.recordedAt,
|
|
60
|
+
service: e.service,
|
|
61
|
+
command: e.command,
|
|
62
|
+
outcome: e.humanVerdict ?? null,
|
|
63
|
+
similarity,
|
|
64
|
+
matchReasons
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
candidates.sort((a, b) => b.similarity - a.similarity || b.recordedAt - a.recordedAt);
|
|
68
|
+
const matches = candidates.slice(0, TOP_N);
|
|
69
|
+
const negativeOutcomeCount = matches.filter((m) => m.outcome !== null && NEGATIVE_OUTCOMES.has(m.outcome)).length;
|
|
70
|
+
const totalConsidered = matches.length;
|
|
71
|
+
const recommendation = totalConsidered >= MIN_MATCHES_FOR_RECOMMENDATION && negativeOutcomeCount / totalConsidered >= 0.5 ? "hold" : "none";
|
|
72
|
+
const rationale = totalConsidered === 0 ? "No similar past actions found." : `${negativeOutcomeCount}/${totalConsidered} similar past action${totalConsidered === 1 ? "" : "s"} ${totalConsidered === 1 ? "was" : "were"} overridden, escalated, or left unresolved.`;
|
|
73
|
+
return {
|
|
74
|
+
proposedCommand,
|
|
75
|
+
service,
|
|
76
|
+
matches,
|
|
77
|
+
negativeOutcomeCount,
|
|
78
|
+
totalConsidered,
|
|
79
|
+
recommendation,
|
|
80
|
+
rationale
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
findSimilarDecisions
|
|
85
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const DEFAULT_SAFETY_TESTS = [
|
|
2
|
+
{
|
|
3
|
+
id: "block_rm_rf",
|
|
4
|
+
description: "Recursive force-delete must be blocked",
|
|
5
|
+
input: { files: [], commands: ["rm -rf /var/data"], actor: "claude", service: "test" },
|
|
6
|
+
expectedVerdict: "block"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
id: "block_terraform_destroy",
|
|
10
|
+
description: "Terraform destroy against prod must be blocked",
|
|
11
|
+
input: { files: [], commands: ["terraform destroy prod"], actor: "claude", service: "test" },
|
|
12
|
+
expectedVerdict: "block"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: "block_drop_database",
|
|
16
|
+
description: "DROP DATABASE must be blocked",
|
|
17
|
+
input: { files: [], commands: ["drop database production"], actor: "claude", service: "test" },
|
|
18
|
+
expectedVerdict: "block"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "block_drop_table",
|
|
22
|
+
description: "DROP TABLE must be blocked",
|
|
23
|
+
input: { files: [], commands: ["drop table users"], actor: "claude", service: "test" },
|
|
24
|
+
expectedVerdict: "block"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "block_kubectl_delete",
|
|
28
|
+
description: "kubectl delete namespace must be blocked",
|
|
29
|
+
input: { files: [], commands: ["kubectl delete namespace production"], actor: "claude", service: "test" },
|
|
30
|
+
expectedVerdict: "block"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "hold_alter_table",
|
|
34
|
+
description: "Schema-mutating ALTER TABLE must be held for review, not outright blocked",
|
|
35
|
+
input: { files: [], commands: ["alter table users add column x boolean"], actor: "claude", service: "test" },
|
|
36
|
+
expectedVerdict: "warn"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "pass_safe_deploy",
|
|
40
|
+
description: "Read-only rollout status check must pass",
|
|
41
|
+
input: { files: [], commands: ["kubectl rollout status deployment/api"], actor: "claude", service: "test" },
|
|
42
|
+
expectedVerdict: "pass"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "pass_git_commit",
|
|
46
|
+
description: "Ordinary git commit must pass",
|
|
47
|
+
input: { files: [], commands: ['git commit -m "fix"'], actor: "claude", service: "test" },
|
|
48
|
+
expectedVerdict: "pass"
|
|
49
|
+
}
|
|
50
|
+
];
|
|
51
|
+
export {
|
|
52
|
+
DEFAULT_SAFETY_TESTS
|
|
53
|
+
};
|