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,223 @@
|
|
|
1
|
+
const NO_CAPS = {
|
|
2
|
+
hitlApproval: false,
|
|
3
|
+
overrideCorpusEnforce: false,
|
|
4
|
+
ciGate: false,
|
|
5
|
+
credentialBrokering: false,
|
|
6
|
+
agentIam: false
|
|
7
|
+
};
|
|
8
|
+
const CANONICAL_PLANS = {
|
|
9
|
+
free: {
|
|
10
|
+
id: "free",
|
|
11
|
+
name: "Free (OSS)",
|
|
12
|
+
rank: 0,
|
|
13
|
+
tagline: "Local-first logs and traces for developers",
|
|
14
|
+
seats: "1 developer",
|
|
15
|
+
maxServices: 1,
|
|
16
|
+
bufferSize: 2e3,
|
|
17
|
+
backendObservability: false,
|
|
18
|
+
analyzeCreditsPerMonth: 10,
|
|
19
|
+
overageCentsPerCredit: 0,
|
|
20
|
+
seatCap: 1,
|
|
21
|
+
overageCentsPerSeat: 0,
|
|
22
|
+
includedToolCallsPerMonth: 1e3,
|
|
23
|
+
overageCentsPerThousandToolCalls: 0,
|
|
24
|
+
// hard stop at limit — no paid overage on Free
|
|
25
|
+
capabilities: { ...NO_CAPS },
|
|
26
|
+
ctaUrl: "https://mergen.app/pricing",
|
|
27
|
+
priceDescription: "Free forever"
|
|
28
|
+
},
|
|
29
|
+
starter: {
|
|
30
|
+
id: "starter",
|
|
31
|
+
name: "Starter",
|
|
32
|
+
rank: 1,
|
|
33
|
+
tagline: "Shared policies, workspace collaboration, and safety for small AI teams",
|
|
34
|
+
seats: "5 included seats (+$15 / additional seat)",
|
|
35
|
+
maxServices: 5,
|
|
36
|
+
bufferSize: 1e4,
|
|
37
|
+
backendObservability: true,
|
|
38
|
+
analyzeCreditsPerMonth: 500,
|
|
39
|
+
overageCentsPerCredit: 10,
|
|
40
|
+
seatCap: 5,
|
|
41
|
+
overageCentsPerSeat: 1500,
|
|
42
|
+
// $15.00/mo per seat beyond 5
|
|
43
|
+
includedToolCallsPerMonth: 1e4,
|
|
44
|
+
overageCentsPerThousandToolCalls: 500,
|
|
45
|
+
// $5 / 1,000 calls over
|
|
46
|
+
// Basic multi-cloud credential brokering + shared override sync across
|
|
47
|
+
// environments — the first step above the free local-only gate.
|
|
48
|
+
capabilities: { ...NO_CAPS, overrideCorpusEnforce: true, credentialBrokering: true },
|
|
49
|
+
ctaUrl: "https://mergen.app/pricing#starter",
|
|
50
|
+
priceDescription: "$79 / month founding price ($149 regular)"
|
|
51
|
+
},
|
|
52
|
+
team: {
|
|
53
|
+
id: "team",
|
|
54
|
+
name: "Team",
|
|
55
|
+
rank: 2,
|
|
56
|
+
tagline: "Slack approvals, RBAC, CI/CD gates, and runtime visualization for scaling companies",
|
|
57
|
+
seats: "20 included seats (+$20 / additional seat)",
|
|
58
|
+
maxServices: 25,
|
|
59
|
+
bufferSize: 5e4,
|
|
60
|
+
backendObservability: true,
|
|
61
|
+
analyzeCreditsPerMonth: 1e3,
|
|
62
|
+
overageCentsPerCredit: 5,
|
|
63
|
+
seatCap: 20,
|
|
64
|
+
overageCentsPerSeat: 2e3,
|
|
65
|
+
// $20.00/mo per seat beyond 20
|
|
66
|
+
includedToolCallsPerMonth: 1e5,
|
|
67
|
+
// 100K tool calls
|
|
68
|
+
overageCentsPerThousandToolCalls: 500,
|
|
69
|
+
// $5 / 1,000 calls over
|
|
70
|
+
// HITL Slack approval loops (OIDC/JWKS-verified approver identity) and
|
|
71
|
+
// the CI/CD PR diff gate both unlock here — this is the launch tier's
|
|
72
|
+
// top rung, so it carries every governance primitive except full
|
|
73
|
+
// enterprise IAM/SSO federation.
|
|
74
|
+
capabilities: { hitlApproval: true, overrideCorpusEnforce: true, ciGate: true, credentialBrokering: true, agentIam: false },
|
|
75
|
+
ctaUrl: "https://mergen.app/pricing#team",
|
|
76
|
+
priceDescription: "$399 / month founding price ($499 regular)"
|
|
77
|
+
},
|
|
78
|
+
// Platform and Enterprise are intentionally held back from the launch
|
|
79
|
+
// pricing page (three-tier funnel: Free/Starter/Team only) to avoid
|
|
80
|
+
// overloading early-stage adoption with procurement-heavy tiers — but the
|
|
81
|
+
// ladder stays defined here since existing legacy-alias customers
|
|
82
|
+
// (solo_power, pay_as_you_go) resolve to them, and they're the natural
|
|
83
|
+
// next rung once a team outgrows Team's seat/quota ceiling.
|
|
84
|
+
platform: {
|
|
85
|
+
id: "platform",
|
|
86
|
+
name: "Platform",
|
|
87
|
+
rank: 3,
|
|
88
|
+
tagline: "Unlimited scale on top of Team\u2019s governance primitives",
|
|
89
|
+
seats: "Unlimited",
|
|
90
|
+
maxServices: Infinity,
|
|
91
|
+
bufferSize: 2e5,
|
|
92
|
+
backendObservability: true,
|
|
93
|
+
analyzeCreditsPerMonth: 5e3,
|
|
94
|
+
overageCentsPerCredit: 2,
|
|
95
|
+
seatCap: Infinity,
|
|
96
|
+
overageCentsPerSeat: 0,
|
|
97
|
+
includedToolCallsPerMonth: 25e4,
|
|
98
|
+
overageCentsPerThousandToolCalls: 600,
|
|
99
|
+
// $6 / 1,000 calls over
|
|
100
|
+
capabilities: { hitlApproval: true, overrideCorpusEnforce: true, ciGate: true, credentialBrokering: true, agentIam: false },
|
|
101
|
+
ctaUrl: "https://mergen.app/pricing#platform",
|
|
102
|
+
priceDescription: "$10,000 / month base"
|
|
103
|
+
},
|
|
104
|
+
enterprise: {
|
|
105
|
+
id: "enterprise",
|
|
106
|
+
name: "Enterprise",
|
|
107
|
+
rank: 4,
|
|
108
|
+
tagline: "VPC deployment, SSO/IAM federation, and strict policy engine for regulated environments",
|
|
109
|
+
seats: "Custom",
|
|
110
|
+
maxServices: Infinity,
|
|
111
|
+
bufferSize: 1e6,
|
|
112
|
+
backendObservability: true,
|
|
113
|
+
analyzeCreditsPerMonth: 0,
|
|
114
|
+
overageCentsPerCredit: 0,
|
|
115
|
+
seatCap: Infinity,
|
|
116
|
+
overageCentsPerSeat: 0,
|
|
117
|
+
includedToolCallsPerMonth: 0,
|
|
118
|
+
// unlimited — custom-contracted, not metered
|
|
119
|
+
overageCentsPerThousandToolCalls: 0,
|
|
120
|
+
// Full Agent IAM — SSO federation + enterprise identity/credential governance.
|
|
121
|
+
capabilities: { hitlApproval: true, overrideCorpusEnforce: true, ciGate: true, credentialBrokering: true, agentIam: true },
|
|
122
|
+
ctaUrl: "https://mergen.app/pricing#enterprise",
|
|
123
|
+
priceDescription: "Custom ACV ($100K+ / year)"
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const PLAN_ORDER = ["free", "starter", "team", "platform", "enterprise"];
|
|
127
|
+
const LEGACY_ALIASES = {
|
|
128
|
+
solo_starter: "starter",
|
|
129
|
+
solo_pro: "team",
|
|
130
|
+
solo_power: "platform",
|
|
131
|
+
pay_as_you_go: "enterprise"
|
|
132
|
+
};
|
|
133
|
+
function buildPlans() {
|
|
134
|
+
const all = { ...CANONICAL_PLANS };
|
|
135
|
+
for (const [aliasId, canonicalId] of Object.entries(LEGACY_ALIASES)) {
|
|
136
|
+
const base = CANONICAL_PLANS[canonicalId];
|
|
137
|
+
all[aliasId] = {
|
|
138
|
+
...base,
|
|
139
|
+
capabilities: { ...base.capabilities },
|
|
140
|
+
id: aliasId,
|
|
141
|
+
name: `${base.name} (Legacy)`
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return all;
|
|
145
|
+
}
|
|
146
|
+
const PLANS = buildPlans();
|
|
147
|
+
function getPlan(planId) {
|
|
148
|
+
return PLANS[planId ?? ""] ?? PLANS["free"];
|
|
149
|
+
}
|
|
150
|
+
function getPlanRank(planId) {
|
|
151
|
+
return getPlan(planId).rank;
|
|
152
|
+
}
|
|
153
|
+
function planMeetsMin(planId, minPlanId) {
|
|
154
|
+
return getPlanRank(planId) >= getPlanRank(minPlanId);
|
|
155
|
+
}
|
|
156
|
+
function planHasCapability(planId, cap) {
|
|
157
|
+
return getPlan(planId).capabilities[cap] === true;
|
|
158
|
+
}
|
|
159
|
+
const LOWEST_PAID_PLAN = "starter";
|
|
160
|
+
function minPlanForGate(gate) {
|
|
161
|
+
if (gate === "free" || gate === "all") return "free";
|
|
162
|
+
if (gate === "pro") return LOWEST_PAID_PLAN;
|
|
163
|
+
return PLANS[gate] ? gate : "free";
|
|
164
|
+
}
|
|
165
|
+
function planAllowsGate(planId, gate) {
|
|
166
|
+
return planMeetsMin(planId, minPlanForGate(gate));
|
|
167
|
+
}
|
|
168
|
+
function planAllowsTier(planId, tier) {
|
|
169
|
+
return planAllowsGate(planId, tier);
|
|
170
|
+
}
|
|
171
|
+
function estimateToolCallOverageCents(planId, toolCallsThisMonth) {
|
|
172
|
+
const plan = getPlan(planId);
|
|
173
|
+
const included = plan.includedToolCallsPerMonth;
|
|
174
|
+
const rate = plan.overageCentsPerThousandToolCalls;
|
|
175
|
+
if (included === 0 || rate === 0) return 0;
|
|
176
|
+
const overage = Math.max(0, toolCallsThisMonth - included);
|
|
177
|
+
return Math.ceil(overage / 1e3 * rate);
|
|
178
|
+
}
|
|
179
|
+
function estimateSeatOverageCents(planId, activeSeats) {
|
|
180
|
+
const plan = getPlan(planId);
|
|
181
|
+
const cap = plan.seatCap;
|
|
182
|
+
const rate = plan.overageCentsPerSeat;
|
|
183
|
+
if (!isFinite(cap) || rate === 0) return 0;
|
|
184
|
+
return Math.max(0, activeSeats - cap) * rate;
|
|
185
|
+
}
|
|
186
|
+
const CAPABILITY_LABELS = {
|
|
187
|
+
hitlApproval: "HITL approve/deny holds",
|
|
188
|
+
overrideCorpusEnforce: "Override-corpus enforcement (BLOCK/HOLD)",
|
|
189
|
+
ciGate: "CI/CD blast-radius gate",
|
|
190
|
+
credentialBrokering: "Ephemeral multi-cloud credential brokering",
|
|
191
|
+
agentIam: "Agent IAM: SSO federation + enterprise credential governance"
|
|
192
|
+
};
|
|
193
|
+
const CAPABILITY_KEYS = Object.keys(CAPABILITY_LABELS);
|
|
194
|
+
function describeEntitlement(planId) {
|
|
195
|
+
const plan = getPlan(planId);
|
|
196
|
+
const unlocked = CAPABILITY_KEYS.filter((cap) => plan.capabilities[cap]).map((cap) => CAPABILITY_LABELS[cap]);
|
|
197
|
+
const nextPlanId = PLAN_ORDER.find((id) => PLANS[id].rank === plan.rank + 1);
|
|
198
|
+
const upgradeLines = [];
|
|
199
|
+
if (nextPlanId) {
|
|
200
|
+
const next = getPlan(nextPlanId);
|
|
201
|
+
const newlyUnlocked = CAPABILITY_KEYS.filter((cap) => next.capabilities[cap] && !plan.capabilities[cap]).map((cap) => CAPABILITY_LABELS[cap]);
|
|
202
|
+
const suffix = newlyUnlocked.length > 0 ? ` \u2014 unlocks ${newlyUnlocked.join(", ")}` : "";
|
|
203
|
+
upgradeLines.push(
|
|
204
|
+
`> **Upgrade to ${next.name}** (${next.priceDescription})${suffix}`,
|
|
205
|
+
`> ${next.ctaUrl}`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return { unlocked, upgradeLines };
|
|
209
|
+
}
|
|
210
|
+
export {
|
|
211
|
+
PLANS,
|
|
212
|
+
PLAN_ORDER,
|
|
213
|
+
describeEntitlement,
|
|
214
|
+
estimateSeatOverageCents,
|
|
215
|
+
estimateToolCallOverageCents,
|
|
216
|
+
getPlan,
|
|
217
|
+
getPlanRank,
|
|
218
|
+
minPlanForGate,
|
|
219
|
+
planAllowsGate,
|
|
220
|
+
planAllowsTier,
|
|
221
|
+
planHasCapability,
|
|
222
|
+
planMeetsMin
|
|
223
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { getRecords, getStats } from "./calibration.js";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import { DATA_DIR } from "../sensor/paths.js";
|
|
5
|
+
const LEARNING_RATE = 0.1;
|
|
6
|
+
const EPOCHS = 200;
|
|
7
|
+
const MIN_SAMPLES = 10;
|
|
8
|
+
const CACHE_TTL_MS = 5 * 6e4;
|
|
9
|
+
const FEDERATED_FILE = path.join(DATA_DIR, "federated-calibration.json");
|
|
10
|
+
const _cache = /* @__PURE__ */ new Map();
|
|
11
|
+
let _federatedCache = null;
|
|
12
|
+
let _federatedLoadedAt = 0;
|
|
13
|
+
function sigmoid(z) {
|
|
14
|
+
return 1 / (1 + Math.exp(-z));
|
|
15
|
+
}
|
|
16
|
+
function getFederatedParams(tag) {
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
if (!_federatedCache || now - _federatedLoadedAt > CACHE_TTL_MS) {
|
|
19
|
+
try {
|
|
20
|
+
if (fs.existsSync(FEDERATED_FILE)) {
|
|
21
|
+
const raw = fs.readFileSync(FEDERATED_FILE, "utf8");
|
|
22
|
+
_federatedCache = JSON.parse(raw);
|
|
23
|
+
_federatedLoadedAt = now;
|
|
24
|
+
} else {
|
|
25
|
+
_federatedCache = { tags: {} };
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
_federatedCache = { tags: {} };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (_federatedCache && _federatedCache.tags && _federatedCache.tags[tag]) {
|
|
32
|
+
const val = _federatedCache.tags[tag];
|
|
33
|
+
return {
|
|
34
|
+
A: val.A,
|
|
35
|
+
B: val.B,
|
|
36
|
+
n: val.n,
|
|
37
|
+
holdoutAccuracy: val.accuracy
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
function fitPlattParams(samples, y_pos, y_neg) {
|
|
43
|
+
let A = -1;
|
|
44
|
+
let B = 0;
|
|
45
|
+
for (let epoch = 0; epoch < EPOCHS; epoch++) {
|
|
46
|
+
let dA = 0, dB = 0;
|
|
47
|
+
for (const { score, isCorrect } of samples) {
|
|
48
|
+
const p = sigmoid(A * score + B);
|
|
49
|
+
const y = isCorrect ? y_pos : y_neg;
|
|
50
|
+
const err = p - y;
|
|
51
|
+
dA += err * score;
|
|
52
|
+
dB += err;
|
|
53
|
+
}
|
|
54
|
+
A -= LEARNING_RATE * dA / samples.length;
|
|
55
|
+
B -= LEARNING_RATE * dB / samples.length;
|
|
56
|
+
}
|
|
57
|
+
return { A, B };
|
|
58
|
+
}
|
|
59
|
+
function fitPlatt(samples) {
|
|
60
|
+
if (samples.length < MIN_SAMPLES) return null;
|
|
61
|
+
const N_pos = samples.filter((s) => s.isCorrect).length;
|
|
62
|
+
const N_neg = samples.length - N_pos;
|
|
63
|
+
const y_pos = (N_pos + 1) / (N_pos + 2);
|
|
64
|
+
const y_neg = 1 / (N_neg + 2);
|
|
65
|
+
const { A, B } = fitPlattParams(samples, y_pos, y_neg);
|
|
66
|
+
let looCorrect = 0;
|
|
67
|
+
for (let i = 0; i < samples.length; i++) {
|
|
68
|
+
const subset = samples.slice(0, i).concat(samples.slice(i + 1));
|
|
69
|
+
const sub_N_pos = subset.filter((s) => s.isCorrect).length;
|
|
70
|
+
const sub_N_neg = subset.length - sub_N_pos;
|
|
71
|
+
const sub_y_pos = (sub_N_pos + 1) / (sub_N_pos + 2);
|
|
72
|
+
const sub_y_neg = 1 / (sub_N_neg + 2);
|
|
73
|
+
const { A: subA, B: subB } = fitPlattParams(subset, sub_y_pos, sub_y_neg);
|
|
74
|
+
const target = samples[i];
|
|
75
|
+
const p = sigmoid(subA * target.score + subB);
|
|
76
|
+
const predictedCorrect = p >= 0.5;
|
|
77
|
+
if (predictedCorrect === target.isCorrect) {
|
|
78
|
+
looCorrect++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const holdoutAccuracy = looCorrect / samples.length;
|
|
82
|
+
return { A, B, n: samples.length, holdoutAccuracy };
|
|
83
|
+
}
|
|
84
|
+
function scoreOfRecord(r) {
|
|
85
|
+
return r.numericScore ?? r.confidenceScore ?? 0;
|
|
86
|
+
}
|
|
87
|
+
function getParams(tag) {
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
const cached = _cache.get(tag);
|
|
90
|
+
if (cached && now - cached.fittedAt < CACHE_TTL_MS) return cached.params;
|
|
91
|
+
const records = getRecords().filter(
|
|
92
|
+
(r) => r.verdict !== null && (r.numericScore !== void 0 || r.confidenceScore !== void 0)
|
|
93
|
+
);
|
|
94
|
+
let samples;
|
|
95
|
+
if (tag === "global") {
|
|
96
|
+
samples = records.map((r) => ({
|
|
97
|
+
score: scoreOfRecord(r),
|
|
98
|
+
isCorrect: r.verdict === "correct" || r.verdict === "partial"
|
|
99
|
+
}));
|
|
100
|
+
} else {
|
|
101
|
+
samples = records.filter((r) => r.tag === tag).map((r) => ({
|
|
102
|
+
score: scoreOfRecord(r),
|
|
103
|
+
isCorrect: r.verdict === "correct" || r.verdict === "partial"
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
const params = fitPlatt(samples);
|
|
107
|
+
if (params) _cache.set(tag, { params, fittedAt: now });
|
|
108
|
+
return params;
|
|
109
|
+
}
|
|
110
|
+
function plattScale(rawScore, tag) {
|
|
111
|
+
if (tag) {
|
|
112
|
+
const tagParams = getParams(tag);
|
|
113
|
+
if (tagParams) {
|
|
114
|
+
return {
|
|
115
|
+
calibrated: Math.min(0.99, Math.max(0.01, sigmoid(tagParams.A * rawScore + tagParams.B))),
|
|
116
|
+
source: "tag-platt",
|
|
117
|
+
n: tagParams.n
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (tag) {
|
|
122
|
+
const fedParams = getFederatedParams(tag);
|
|
123
|
+
if (fedParams) {
|
|
124
|
+
return {
|
|
125
|
+
calibrated: Math.min(0.99, Math.max(0.01, sigmoid(fedParams.A * rawScore + fedParams.B))),
|
|
126
|
+
source: "federated-platt",
|
|
127
|
+
n: fedParams.n
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const globalParams = getParams("global");
|
|
132
|
+
if (globalParams) {
|
|
133
|
+
return {
|
|
134
|
+
calibrated: Math.min(0.99, Math.max(0.01, sigmoid(globalParams.A * rawScore + globalParams.B))),
|
|
135
|
+
source: "global-platt",
|
|
136
|
+
n: globalParams.n
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (tag) {
|
|
140
|
+
const stats = getStats().find((s) => s.tag === tag);
|
|
141
|
+
if (stats?.trusted) {
|
|
142
|
+
return { calibrated: stats.accuracy, source: "empirical", n: stats.verdicts };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { calibrated: rawScore, source: "raw", n: 0 };
|
|
146
|
+
}
|
|
147
|
+
function invalidatePlattCache() {
|
|
148
|
+
_cache.clear();
|
|
149
|
+
}
|
|
150
|
+
function getPlattDiagnostics() {
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
return [..._cache.entries()].filter(([, v]) => now - v.fittedAt < CACHE_TTL_MS).map(([tag, { params }]) => ({
|
|
153
|
+
tag,
|
|
154
|
+
A: params.A,
|
|
155
|
+
B: params.B,
|
|
156
|
+
n: params.n,
|
|
157
|
+
holdoutAccuracy: params.holdoutAccuracy
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
export {
|
|
161
|
+
getPlattDiagnostics,
|
|
162
|
+
invalidatePlattCache,
|
|
163
|
+
plattScale
|
|
164
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
import { DATA_DIR, zeroRetentionMode } from "../sensor/paths.js";
|
|
5
|
+
import logger from "../sensor/logger.js";
|
|
6
|
+
const PROPOSALS_FILE = path.join(DATA_DIR, "policy-proposals.json");
|
|
7
|
+
let _proposals = null;
|
|
8
|
+
function load() {
|
|
9
|
+
if (_proposals) return _proposals;
|
|
10
|
+
if (zeroRetentionMode() || !fs.existsSync(PROPOSALS_FILE)) {
|
|
11
|
+
_proposals = [];
|
|
12
|
+
return _proposals;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
_proposals = JSON.parse(fs.readFileSync(PROPOSALS_FILE, "utf8"));
|
|
16
|
+
} catch (err) {
|
|
17
|
+
logger.warn({ err }, "policy-proposals: file unreadable \u2014 starting empty");
|
|
18
|
+
_proposals = [];
|
|
19
|
+
}
|
|
20
|
+
return _proposals;
|
|
21
|
+
}
|
|
22
|
+
function persist() {
|
|
23
|
+
if (zeroRetentionMode()) return;
|
|
24
|
+
try {
|
|
25
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
26
|
+
const tmp = `${PROPOSALS_FILE}.tmp.${process.pid}`;
|
|
27
|
+
fs.writeFileSync(tmp, JSON.stringify(_proposals ?? [], null, 2));
|
|
28
|
+
fs.renameSync(tmp, PROPOSALS_FILE);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
logger.warn({ err }, "policy-proposals: persist failed");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function stageProposal(ruleHash, rule, sourceOccurrences) {
|
|
34
|
+
if (rule.action !== "warn") {
|
|
35
|
+
throw new Error(`policy-proposals: refusing to stage a non-HOLD proposal (action=${rule.action})`);
|
|
36
|
+
}
|
|
37
|
+
const list = load();
|
|
38
|
+
const existing = list.find((p) => p.ruleHash === ruleHash && p.status !== "rejected");
|
|
39
|
+
if (existing) return null;
|
|
40
|
+
const proposal = {
|
|
41
|
+
id: randomUUID(),
|
|
42
|
+
ruleHash,
|
|
43
|
+
rule,
|
|
44
|
+
sourceOccurrences,
|
|
45
|
+
status: "proposed",
|
|
46
|
+
proposedAt: Date.now(),
|
|
47
|
+
decidedAt: null
|
|
48
|
+
};
|
|
49
|
+
list.push(proposal);
|
|
50
|
+
persist();
|
|
51
|
+
logger.info({ id: proposal.id, ruleId: rule.id }, "policy-proposals: staged HOLD-only proposal");
|
|
52
|
+
return proposal;
|
|
53
|
+
}
|
|
54
|
+
function getProposals(status) {
|
|
55
|
+
const list = load();
|
|
56
|
+
return status ? list.filter((p) => p.status === status) : [...list];
|
|
57
|
+
}
|
|
58
|
+
function getProposal(id) {
|
|
59
|
+
return load().find((p) => p.id === id) ?? null;
|
|
60
|
+
}
|
|
61
|
+
function markProposalDecided(id, status) {
|
|
62
|
+
const list = load();
|
|
63
|
+
const p = list.find((x) => x.id === id);
|
|
64
|
+
if (!p || p.status !== "proposed") return null;
|
|
65
|
+
p.status = status;
|
|
66
|
+
p.decidedAt = Date.now();
|
|
67
|
+
persist();
|
|
68
|
+
logger.info({ id, status }, "policy-proposals: proposal decided");
|
|
69
|
+
return p;
|
|
70
|
+
}
|
|
71
|
+
function _resetProposalsForTesting() {
|
|
72
|
+
_proposals = null;
|
|
73
|
+
try {
|
|
74
|
+
fs.unlinkSync(PROPOSALS_FILE);
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export {
|
|
79
|
+
_resetProposalsForTesting,
|
|
80
|
+
getProposal,
|
|
81
|
+
getProposals,
|
|
82
|
+
markProposalDecided,
|
|
83
|
+
stageProposal
|
|
84
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { getStores } from "../storage/store-registry.js";
|
|
2
|
+
import { loadEnterprisePolicy } from "./enterprise-policy-engine.js";
|
|
3
|
+
import { synthesizeRulesFromCorpus } from "./corpus-to-policy.js";
|
|
4
|
+
import logger from "../sensor/logger.js";
|
|
5
|
+
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
6
|
+
const MIN_CLUSTER_SIZE = 5;
|
|
7
|
+
function commandPrefix(cmd) {
|
|
8
|
+
if (!cmd) return "(unknown)";
|
|
9
|
+
return cmd.trim().split(/\s+/).slice(0, 3).join(" ");
|
|
10
|
+
}
|
|
11
|
+
function isCoveredByPolicy(prefix) {
|
|
12
|
+
const policy = loadEnterprisePolicy();
|
|
13
|
+
const lowerPrefix = prefix.toLowerCase();
|
|
14
|
+
for (const rule of policy.rules) {
|
|
15
|
+
const cmds = rule.conditions.commands ?? [];
|
|
16
|
+
if (cmds.some((c) => lowerPrefix.includes(c.toLowerCase()) || c.toLowerCase().includes(lowerPrefix.split(" ")[0]))) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
async function computePolicySuggestions() {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
const cutoff = now - THIRTY_DAYS_MS;
|
|
25
|
+
const recent = (await getStores().blunders.list()).filter((b) => b.recordedAt >= cutoff);
|
|
26
|
+
const clusters = /* @__PURE__ */ new Map();
|
|
27
|
+
for (const b of recent) {
|
|
28
|
+
const prefix = commandPrefix(b.command);
|
|
29
|
+
const entry = clusters.get(prefix) ?? { count: 0, lastSeenAt: 0, services: /* @__PURE__ */ new Set() };
|
|
30
|
+
entry.count++;
|
|
31
|
+
entry.lastSeenAt = Math.max(entry.lastSeenAt, b.recordedAt);
|
|
32
|
+
if (b.service) entry.services.add(b.service);
|
|
33
|
+
clusters.set(prefix, entry);
|
|
34
|
+
}
|
|
35
|
+
const suggestions = [];
|
|
36
|
+
for (const [prefix, data] of clusters) {
|
|
37
|
+
if (data.count < MIN_CLUSTER_SIZE) continue;
|
|
38
|
+
if (prefix === "(unknown)") continue;
|
|
39
|
+
const alreadyCovered = isCoveredByPolicy(prefix);
|
|
40
|
+
const slug = prefix.replace(/[^a-z0-9]+/gi, "_").toLowerCase().slice(0, 40);
|
|
41
|
+
suggestions.push({
|
|
42
|
+
commandPrefix: prefix,
|
|
43
|
+
occurrences: data.count,
|
|
44
|
+
lastSeenAt: data.lastSeenAt,
|
|
45
|
+
services: [...data.services],
|
|
46
|
+
alreadyCovered,
|
|
47
|
+
suggestedRule: {
|
|
48
|
+
id: `auto_suggest_${slug}`,
|
|
49
|
+
name: `Block: ${prefix}`,
|
|
50
|
+
action: "block",
|
|
51
|
+
reason: `Auto-suggested: this command pattern was blocked ${data.count} times in the last 30 days.`,
|
|
52
|
+
conditions: { commands: [prefix.split(" ")[0]] }
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const corpusSynthesized = synthesizeRulesFromCorpus();
|
|
57
|
+
for (const { rule, sourceOccurrences, compactedRule } of corpusSynthesized) {
|
|
58
|
+
suggestions.push({
|
|
59
|
+
commandPrefix: compactedRule.incidentTag,
|
|
60
|
+
occurrences: sourceOccurrences,
|
|
61
|
+
lastSeenAt: compactedRule.compactedAt,
|
|
62
|
+
services: compactedRule.service !== "unknown" ? [compactedRule.service] : [],
|
|
63
|
+
alreadyCovered: false,
|
|
64
|
+
// synthesizeRulesFromCorpus already filters covered ones
|
|
65
|
+
source: "corpus",
|
|
66
|
+
suggestedRule: {
|
|
67
|
+
id: rule.id,
|
|
68
|
+
name: rule.name,
|
|
69
|
+
// Corpus-synthesized rules are only ever 'warn' or 'block' (never 'pass');
|
|
70
|
+
// narrow away 'pass' to satisfy the suggestion type.
|
|
71
|
+
action: rule.action,
|
|
72
|
+
reason: rule.reason,
|
|
73
|
+
conditions: rule.conditions
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return suggestions.sort((a, b) => b.occurrences - a.occurrences);
|
|
78
|
+
}
|
|
79
|
+
async function postSuggestionsToSlack(suggestions) {
|
|
80
|
+
const token = process.env.MERGEN_SLACK_BOT_TOKEN;
|
|
81
|
+
const channel = process.env.MERGEN_SLACK_DIGEST_CHANNEL ?? process.env.MERGEN_SLACK_CHANNEL;
|
|
82
|
+
if (!token || !channel) return;
|
|
83
|
+
const uncovered = suggestions.filter((s) => !s.alreadyCovered);
|
|
84
|
+
if (uncovered.length === 0) return;
|
|
85
|
+
const lines = uncovered.slice(0, 5).map(
|
|
86
|
+
(s) => `\u2022 \`${s.commandPrefix}\` \u2014 blocked *${s.occurrences}\xD7* in 30 days (services: ${s.services.join(", ") || "unknown"})`
|
|
87
|
+
);
|
|
88
|
+
const payload = {
|
|
89
|
+
channel,
|
|
90
|
+
text: `\u{1F4CB} Mergen Policy Suggestions \u2014 ${uncovered.length} uncovered pattern${uncovered.length !== 1 ? "s" : ""}`,
|
|
91
|
+
blocks: [
|
|
92
|
+
{
|
|
93
|
+
type: "header",
|
|
94
|
+
text: { type: "plain_text", text: "\u{1F4CB} Mergen Policy Suggestions", emoji: true }
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
type: "section",
|
|
98
|
+
text: {
|
|
99
|
+
type: "mrkdwn",
|
|
100
|
+
text: `*${uncovered.length}* command pattern${uncovered.length !== 1 ? "s" : ""} have been blocked repeatedly but have no named policy rule. Consider formalising them:
|
|
101
|
+
|
|
102
|
+
${lines.join("\n")}`
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
type: "section",
|
|
107
|
+
text: {
|
|
108
|
+
type: "mrkdwn",
|
|
109
|
+
text: `View all suggestions and generated rule JSON: \`GET /policy-suggestions\`
|
|
110
|
+
Add a rule: \`POST /policies/rules\``
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
};
|
|
115
|
+
try {
|
|
116
|
+
await fetch("https://slack.com/api/chat.postMessage", {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: { "Content-Type": "application/json; charset=utf-8", Authorization: `Bearer ${token}` },
|
|
119
|
+
body: JSON.stringify(payload),
|
|
120
|
+
signal: AbortSignal.timeout(8e3)
|
|
121
|
+
});
|
|
122
|
+
} catch (err) {
|
|
123
|
+
logger.warn({ err }, "policy-suggester: Slack post failed");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function msUntilNextMonday0900Utc() {
|
|
127
|
+
const now = /* @__PURE__ */ new Date();
|
|
128
|
+
const day = now.getUTCDay();
|
|
129
|
+
const daysUntilMonday = day === 1 ? 7 : (8 - day) % 7 || 7;
|
|
130
|
+
const next = new Date(Date.UTC(
|
|
131
|
+
now.getUTCFullYear(),
|
|
132
|
+
now.getUTCMonth(),
|
|
133
|
+
now.getUTCDate() + daysUntilMonday,
|
|
134
|
+
9,
|
|
135
|
+
5,
|
|
136
|
+
0,
|
|
137
|
+
0
|
|
138
|
+
));
|
|
139
|
+
return Math.max(next.getTime() - Date.now(), 0);
|
|
140
|
+
}
|
|
141
|
+
function scheduleNext() {
|
|
142
|
+
const delay = msUntilNextMonday0900Utc();
|
|
143
|
+
setTimeout(async () => {
|
|
144
|
+
const suggestions = await computePolicySuggestions();
|
|
145
|
+
void postSuggestionsToSlack(suggestions);
|
|
146
|
+
scheduleNext();
|
|
147
|
+
}, delay).unref();
|
|
148
|
+
}
|
|
149
|
+
function startPolicySuggesterCron() {
|
|
150
|
+
setTimeout(async () => {
|
|
151
|
+
const suggestions = await computePolicySuggestions();
|
|
152
|
+
if (suggestions.length > 0) {
|
|
153
|
+
logger.info({ count: suggestions.length }, "policy-suggester: startup scan complete");
|
|
154
|
+
}
|
|
155
|
+
scheduleNext();
|
|
156
|
+
}, 3e4).unref();
|
|
157
|
+
}
|
|
158
|
+
export {
|
|
159
|
+
computePolicySuggestions,
|
|
160
|
+
startPolicySuggesterCron
|
|
161
|
+
};
|