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,117 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
const IMPORT_RE = /(?:import|export)(?:[\s\S]*?\bfrom\b)?\s+['"]([^'"]+)['"]/g;
|
|
4
|
+
function extractImports(source) {
|
|
5
|
+
const paths = [];
|
|
6
|
+
let m;
|
|
7
|
+
IMPORT_RE.lastIndex = 0;
|
|
8
|
+
while ((m = IMPORT_RE.exec(source)) !== null) {
|
|
9
|
+
paths.push(m[1]);
|
|
10
|
+
}
|
|
11
|
+
return paths;
|
|
12
|
+
}
|
|
13
|
+
function resolveImport(rawPath, importerDir) {
|
|
14
|
+
if (!rawPath.startsWith(".")) return null;
|
|
15
|
+
const withoutExt = rawPath.replace(/\.(js|ts|mjs)$/, "");
|
|
16
|
+
const candidates = [
|
|
17
|
+
path.resolve(importerDir, withoutExt + ".ts"),
|
|
18
|
+
path.resolve(importerDir, withoutExt, "index.ts")
|
|
19
|
+
];
|
|
20
|
+
for (const c of candidates) {
|
|
21
|
+
if (fs.existsSync(c)) return c;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
function collectTsFiles(dir, acc = []) {
|
|
26
|
+
if (!fs.existsSync(dir)) return acc;
|
|
27
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
28
|
+
const full = path.join(dir, entry.name);
|
|
29
|
+
if (entry.isDirectory()) {
|
|
30
|
+
if (["dist", "node_modules", "__stubs__", "evals"].includes(entry.name)) continue;
|
|
31
|
+
collectTsFiles(full, acc);
|
|
32
|
+
} else if (entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts")) {
|
|
33
|
+
acc.push(full);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return acc;
|
|
37
|
+
}
|
|
38
|
+
const _cache = /* @__PURE__ */ new Map();
|
|
39
|
+
function buildGraph(srcDir) {
|
|
40
|
+
const cached = _cache.get(srcDir);
|
|
41
|
+
if (cached && Date.now() - cached.builtAt < 6e4) return cached;
|
|
42
|
+
const files = collectTsFiles(srcDir).sort();
|
|
43
|
+
const forward = /* @__PURE__ */ new Map();
|
|
44
|
+
const reverse = /* @__PURE__ */ new Map();
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
forward.set(file, /* @__PURE__ */ new Set());
|
|
47
|
+
reverse.set(file, /* @__PURE__ */ new Set());
|
|
48
|
+
}
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
let source;
|
|
51
|
+
try {
|
|
52
|
+
source = fs.readFileSync(file, "utf8");
|
|
53
|
+
} catch {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const imports = extractImports(source);
|
|
57
|
+
for (const raw of imports) {
|
|
58
|
+
const resolved = resolveImport(raw, path.dirname(file));
|
|
59
|
+
if (!resolved || !forward.has(resolved)) continue;
|
|
60
|
+
forward.get(file).add(resolved);
|
|
61
|
+
reverse.get(resolved).add(file);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const graph = { root: srcDir, forward, reverse, files, builtAt: Date.now() };
|
|
65
|
+
_cache.set(srcDir, graph);
|
|
66
|
+
return graph;
|
|
67
|
+
}
|
|
68
|
+
function invalidateGraph(srcDir) {
|
|
69
|
+
_cache.delete(srcDir);
|
|
70
|
+
}
|
|
71
|
+
function queryGraph(graph, query) {
|
|
72
|
+
const { file, direction, maxDepth = 3 } = query;
|
|
73
|
+
const adjacency = direction === "depends-on" ? graph.forward : graph.reverse;
|
|
74
|
+
const direct = [...adjacency.get(file) ?? []];
|
|
75
|
+
const visited = /* @__PURE__ */ new Set([file]);
|
|
76
|
+
const frontier = [...direct];
|
|
77
|
+
const transitive = [];
|
|
78
|
+
let depth = 1;
|
|
79
|
+
while (frontier.length > 0 && depth < maxDepth) {
|
|
80
|
+
depth++;
|
|
81
|
+
const next = [];
|
|
82
|
+
for (const f of frontier) {
|
|
83
|
+
if (visited.has(f)) continue;
|
|
84
|
+
visited.add(f);
|
|
85
|
+
transitive.push(f);
|
|
86
|
+
const neighbors = adjacency.get(f) ?? /* @__PURE__ */ new Set();
|
|
87
|
+
for (const n of neighbors) {
|
|
88
|
+
if (!visited.has(n)) next.push(n);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
frontier.length = 0;
|
|
92
|
+
frontier.push(...next);
|
|
93
|
+
}
|
|
94
|
+
return { file, direction, direct, transitive, depth };
|
|
95
|
+
}
|
|
96
|
+
function getZone(filePath) {
|
|
97
|
+
const p = filePath.replace(/\\/g, "/");
|
|
98
|
+
if (p.includes("/sensor/")) return "sensor";
|
|
99
|
+
if (p.includes("/intelligence/")) return "intelligence";
|
|
100
|
+
if (p.includes("/routes/")) return "routes";
|
|
101
|
+
if (p.includes("/datadog/")) return "datadog";
|
|
102
|
+
return "other";
|
|
103
|
+
}
|
|
104
|
+
const ZONE_DISPLAY = {
|
|
105
|
+
sensor: "Sensor (data ingestion / storage)",
|
|
106
|
+
intelligence: "Intelligence (MCP tools / analysis)",
|
|
107
|
+
routes: "Routes (Express handlers)",
|
|
108
|
+
datadog: "Datadog (external integration)",
|
|
109
|
+
other: "Other"
|
|
110
|
+
};
|
|
111
|
+
export {
|
|
112
|
+
ZONE_DISPLAY,
|
|
113
|
+
buildGraph,
|
|
114
|
+
getZone,
|
|
115
|
+
invalidateGraph,
|
|
116
|
+
queryGraph
|
|
117
|
+
};
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { recordExecutionAudit } from "../sensor/audit-log.js";
|
|
5
|
+
import { hasPermission } from "../sensor/rbac.js";
|
|
6
|
+
import { getStores } from "../storage/store-registry.js";
|
|
7
|
+
import logger from "../sensor/logger.js";
|
|
8
|
+
import { SAFETY_POLICY_FILE } from "../sensor/paths.js";
|
|
9
|
+
const DEFAULT_SAFETY_POLICY = {
|
|
10
|
+
// version 2: replaced overbroad bare-word 'delete' and 'truncate' with precise
|
|
11
|
+
// multi-word patterns ('delete from', 'truncate table') that don't false-positive
|
|
12
|
+
// on service names like 'delete-cache' or make targets like 'truncate-logs'.
|
|
13
|
+
// Database service names (postgres, mysql, redis, mongo) remain as they correctly
|
|
14
|
+
// block commands that target known production data stores by name.
|
|
15
|
+
version: 2,
|
|
16
|
+
blockedKeywords: [
|
|
17
|
+
"rm -rf",
|
|
18
|
+
"drop table",
|
|
19
|
+
"drop database",
|
|
20
|
+
"truncate table",
|
|
21
|
+
"delete from",
|
|
22
|
+
"kubectl delete",
|
|
23
|
+
"production-db",
|
|
24
|
+
"postgres",
|
|
25
|
+
"mysql",
|
|
26
|
+
"redis",
|
|
27
|
+
"mongo"
|
|
28
|
+
],
|
|
29
|
+
blockedServices: [
|
|
30
|
+
"payments",
|
|
31
|
+
"auth-service",
|
|
32
|
+
"database"
|
|
33
|
+
]
|
|
34
|
+
};
|
|
35
|
+
let _safetyPolicy = null;
|
|
36
|
+
let _safetyPolicyWatcherStarted = false;
|
|
37
|
+
function _watchSafetyPolicy() {
|
|
38
|
+
if (_safetyPolicyWatcherStarted) return;
|
|
39
|
+
_safetyPolicyWatcherStarted = true;
|
|
40
|
+
fs.watchFile(SAFETY_POLICY_FILE, { interval: 5e3, persistent: false }, () => {
|
|
41
|
+
_safetyPolicy = null;
|
|
42
|
+
logger.info({ path: SAFETY_POLICY_FILE }, "autonomy: safety-policy.json changed \u2014 reloading on next call");
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function _resetSafetyPolicyForTesting(override) {
|
|
46
|
+
_safetyPolicy = override ?? null;
|
|
47
|
+
}
|
|
48
|
+
function loadSafetyPolicy(force = false) {
|
|
49
|
+
if (_safetyPolicy && !force) return _safetyPolicy;
|
|
50
|
+
_watchSafetyPolicy();
|
|
51
|
+
if (!fs.existsSync(SAFETY_POLICY_FILE)) {
|
|
52
|
+
try {
|
|
53
|
+
fs.mkdirSync(path.dirname(SAFETY_POLICY_FILE), { recursive: true });
|
|
54
|
+
fs.writeFileSync(SAFETY_POLICY_FILE, JSON.stringify(DEFAULT_SAFETY_POLICY, null, 2), "utf8");
|
|
55
|
+
logger.info({ path: SAFETY_POLICY_FILE }, "autonomy: created default safety-policy.json");
|
|
56
|
+
} catch (err) {
|
|
57
|
+
logger.warn({ err }, "autonomy: failed to write default safety policy");
|
|
58
|
+
}
|
|
59
|
+
_safetyPolicy = DEFAULT_SAFETY_POLICY;
|
|
60
|
+
return _safetyPolicy;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const raw = fs.readFileSync(SAFETY_POLICY_FILE, "utf8");
|
|
64
|
+
const parsed = JSON.parse(raw);
|
|
65
|
+
if (parsed && Array.isArray(parsed.blockedKeywords) && Array.isArray(parsed.blockedServices)) {
|
|
66
|
+
if ((parsed.version ?? 0) < DEFAULT_SAFETY_POLICY.version) {
|
|
67
|
+
logger.info(
|
|
68
|
+
{ oldVersion: parsed.version ?? 0, newVersion: DEFAULT_SAFETY_POLICY.version, path: SAFETY_POLICY_FILE },
|
|
69
|
+
"autonomy: safety-policy.json is outdated \u2014 upgrading to current defaults"
|
|
70
|
+
);
|
|
71
|
+
_safetyPolicy = DEFAULT_SAFETY_POLICY;
|
|
72
|
+
try {
|
|
73
|
+
fs.writeFileSync(SAFETY_POLICY_FILE, JSON.stringify(DEFAULT_SAFETY_POLICY, null, 2), "utf8");
|
|
74
|
+
} catch (err) {
|
|
75
|
+
logger.warn({ err }, "autonomy: failed to write upgraded safety policy");
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
_safetyPolicy = parsed;
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
_safetyPolicy = DEFAULT_SAFETY_POLICY;
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
logger.warn({ err }, "autonomy: failed to load safety policy \u2014 using defaults");
|
|
85
|
+
_safetyPolicy = DEFAULT_SAFETY_POLICY;
|
|
86
|
+
}
|
|
87
|
+
return _safetyPolicy;
|
|
88
|
+
}
|
|
89
|
+
function checkSafetyPolicy(command) {
|
|
90
|
+
const policy = loadSafetyPolicy();
|
|
91
|
+
const normalized = normalizeWhitespace(command).toLowerCase();
|
|
92
|
+
for (const keyword of policy.blockedKeywords) {
|
|
93
|
+
if (normalized.includes(keyword.toLowerCase())) {
|
|
94
|
+
return {
|
|
95
|
+
allowed: false,
|
|
96
|
+
blockReason: `Command matches safety-policy blocked keyword/pattern: '${keyword}'`,
|
|
97
|
+
matchedRule: `safety_keyword:${keyword}`
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const service of policy.blockedServices) {
|
|
102
|
+
if (normalized.includes(service.toLowerCase())) {
|
|
103
|
+
return {
|
|
104
|
+
allowed: false,
|
|
105
|
+
blockReason: `Command targets safety-policy blocked service: '${service}'`,
|
|
106
|
+
matchedRule: `safety_service:${service}`
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { allowed: true };
|
|
111
|
+
}
|
|
112
|
+
const ALLOWED_COMMANDS = [
|
|
113
|
+
// Package managers
|
|
114
|
+
{ pattern: /^npm (install|ci|rebuild|run \S+|update \S+)(\s|$)/i, description: "npm install / ci / rebuild / run / update" },
|
|
115
|
+
{ pattern: /^yarn (install|run \S+|add \S+)(\s|$)/i, description: "yarn install / run / add" },
|
|
116
|
+
{ pattern: /^pnpm (install|run \S+|update \S+|add \S+)(\s|$)/i, description: "pnpm install / run / update / add" },
|
|
117
|
+
// pip install: only allow package-name installs. Block URL schemes (https://, git+),
|
|
118
|
+
// and flags that write to arbitrary paths (--target, --prefix).
|
|
119
|
+
{ pattern: /^pip3? install (?!https?:\/\/|git\+|--target[\s=]|--prefix[\s=])[\w.-]/i, description: "pip install <package-name> (no URL or path installs)" },
|
|
120
|
+
// Git — safe, read-side / restore operations only
|
|
121
|
+
{ pattern: /^git checkout [a-f0-9]{4,40}(\s|$)/i, description: "git checkout <sha>" },
|
|
122
|
+
{ pattern: /^git fetch(\s|$)/i, description: "git fetch" },
|
|
123
|
+
{ pattern: /^git stash (push|pop)(\s|$)/i, description: "git stash push / pop" },
|
|
124
|
+
{ pattern: /^git pull --ff-only(\s|$)/i, description: "git pull --ff-only" },
|
|
125
|
+
// Container orchestration
|
|
126
|
+
{ pattern: /^docker (restart|stop|start|logs) \S+/i, description: "docker restart/stop/start/logs <container>" },
|
|
127
|
+
{ pattern: /^kubectl rollout (restart|undo) /i, description: "kubectl rollout restart / undo" },
|
|
128
|
+
{ pattern: /^kubectl scale deployment /i, description: "kubectl scale deployment" },
|
|
129
|
+
// Service control
|
|
130
|
+
{ pattern: /^systemctl (restart|stop|start|reload) \S+/i, description: "systemctl restart/stop/start/reload" },
|
|
131
|
+
{ pattern: /^service \S+ (restart|stop|start)/i, description: "service <name> restart/stop/start" },
|
|
132
|
+
// Build — restricted to known-safe targets only. Makefile targets can execute
|
|
133
|
+
// arbitrary shell code, so we permit only a fixed set rather than \S+ (any target).
|
|
134
|
+
{ pattern: /^make (build|test|install|restart|reload|start|stop|clean|lint|check)(\s|$)/i, description: "make <build|test|install|restart|reload|start|stop|clean|lint|check>" }
|
|
135
|
+
];
|
|
136
|
+
const ALLOWED_COMMAND_DESCRIPTIONS = ALLOWED_COMMANDS.map((r) => r.description);
|
|
137
|
+
function normalizeWhitespace(cmd) {
|
|
138
|
+
return cmd.trim().replace(/\s+/g, " ");
|
|
139
|
+
}
|
|
140
|
+
const SHELL_METACHAR_RE = /[;&|`$(){}[\]<>\\!\n\r]/;
|
|
141
|
+
function checkAllowlist(command) {
|
|
142
|
+
const normalized = normalizeWhitespace(command);
|
|
143
|
+
if (SHELL_METACHAR_RE.test(normalized)) {
|
|
144
|
+
return {
|
|
145
|
+
allowed: false,
|
|
146
|
+
blockReason: "Command contains shell metacharacter \u2014 possible injection attempt"
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const match = ALLOWED_COMMANDS.find((r) => r.pattern.test(normalized));
|
|
150
|
+
if (match) return { allowed: true, matchedRule: match.description };
|
|
151
|
+
return {
|
|
152
|
+
allowed: false,
|
|
153
|
+
blockReason: `Command does not match any allowed pattern. Allowed forms: ${ALLOWED_COMMAND_DESCRIPTIONS.join(", ")}`
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const MAX_OUTPUT_BYTES = 16 * 1024;
|
|
157
|
+
const EXEC_TIMEOUT_MS = 6e4;
|
|
158
|
+
function validateCwd(raw) {
|
|
159
|
+
if (!raw) return void 0;
|
|
160
|
+
const resolved = path.resolve(raw);
|
|
161
|
+
const safePrefixes = [process.cwd(), os.homedir()];
|
|
162
|
+
const isUnderSafeRoot = safePrefixes.some(
|
|
163
|
+
(p) => resolved === p || resolved.startsWith(p + path.sep)
|
|
164
|
+
);
|
|
165
|
+
if (!isUnderSafeRoot) {
|
|
166
|
+
logger.warn({ cwd: raw, resolved }, "autonomy: rejected cwd outside safe root");
|
|
167
|
+
return void 0;
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
if (!fs.statSync(resolved).isDirectory()) return void 0;
|
|
171
|
+
} catch {
|
|
172
|
+
return void 0;
|
|
173
|
+
}
|
|
174
|
+
return resolved;
|
|
175
|
+
}
|
|
176
|
+
function _auditExecution(cmd, result, actor = "unknown") {
|
|
177
|
+
try {
|
|
178
|
+
recordExecutionAudit({
|
|
179
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
180
|
+
event: "autonomy.execute",
|
|
181
|
+
actor,
|
|
182
|
+
cmd: cmd.slice(0, 500),
|
|
183
|
+
ok: result.ok,
|
|
184
|
+
exitCode: result.exitCode ?? -1,
|
|
185
|
+
durationMs: result.durationMs,
|
|
186
|
+
blocked: result.blocked,
|
|
187
|
+
blockReason: result.blockReason ?? "",
|
|
188
|
+
timedOut: result.timedOut
|
|
189
|
+
});
|
|
190
|
+
} catch {
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async function executeRemediation(command, opts = {}) {
|
|
194
|
+
const start = Date.now();
|
|
195
|
+
const actor = opts.actor ?? "unknown";
|
|
196
|
+
if (!hasPermission(actor, "responder")) {
|
|
197
|
+
const result = {
|
|
198
|
+
ok: false,
|
|
199
|
+
exitCode: null,
|
|
200
|
+
stdout: "",
|
|
201
|
+
stderr: "",
|
|
202
|
+
durationMs: 0,
|
|
203
|
+
timedOut: false,
|
|
204
|
+
blocked: true,
|
|
205
|
+
blockReason: `Actor '${actor}' does not have the 'responder' role required for fix execution`
|
|
206
|
+
};
|
|
207
|
+
_auditExecution(command, result, actor);
|
|
208
|
+
logger.warn({ actor, command }, "autonomy: command blocked by RBAC (insufficient role)");
|
|
209
|
+
await getStores().blunders.record({ blunderType: "rbac_block", command, blockReason: result.blockReason, service: null, tag: null, actor, pid: null, confidenceScore: null });
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
const { allowed, matchedRule, blockReason } = checkAllowlist(command);
|
|
213
|
+
if (!allowed) {
|
|
214
|
+
const result = {
|
|
215
|
+
ok: false,
|
|
216
|
+
exitCode: null,
|
|
217
|
+
stdout: "",
|
|
218
|
+
stderr: "",
|
|
219
|
+
durationMs: Date.now() - start,
|
|
220
|
+
timedOut: false,
|
|
221
|
+
blocked: true,
|
|
222
|
+
blockReason
|
|
223
|
+
};
|
|
224
|
+
_auditExecution(command, result, actor);
|
|
225
|
+
logger.warn({ command, blockReason }, "autonomy: command blocked by allowlist");
|
|
226
|
+
const blunderType = typeof blockReason === "string" && /inject/i.test(blockReason) ? "injection_attempt" : "allowlist_block";
|
|
227
|
+
await getStores().blunders.record({ blunderType, command, blockReason: blockReason ?? "", service: null, tag: null, actor, pid: null, confidenceScore: null });
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
const safetyCheck = checkSafetyPolicy(command);
|
|
231
|
+
if (!safetyCheck.allowed) {
|
|
232
|
+
const result = {
|
|
233
|
+
ok: false,
|
|
234
|
+
exitCode: null,
|
|
235
|
+
stdout: "",
|
|
236
|
+
stderr: "",
|
|
237
|
+
durationMs: Date.now() - start,
|
|
238
|
+
timedOut: false,
|
|
239
|
+
blocked: true,
|
|
240
|
+
blockReason: safetyCheck.blockReason
|
|
241
|
+
};
|
|
242
|
+
_auditExecution(command, result, actor);
|
|
243
|
+
logger.warn({ command, blockReason: safetyCheck.blockReason }, "autonomy: command blocked by safety policy");
|
|
244
|
+
await getStores().blunders.record({
|
|
245
|
+
blunderType: "pipeline_block",
|
|
246
|
+
command,
|
|
247
|
+
blockReason: safetyCheck.blockReason ?? "",
|
|
248
|
+
service: null,
|
|
249
|
+
tag: null,
|
|
250
|
+
actor,
|
|
251
|
+
pid: null,
|
|
252
|
+
confidenceScore: null,
|
|
253
|
+
triggeredRules: safetyCheck.matchedRule ? [safetyCheck.matchedRule] : null
|
|
254
|
+
});
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
logger.debug({ command, matchedRule }, "autonomy: command passed allowlist");
|
|
258
|
+
if (opts.dryRun) {
|
|
259
|
+
const result = {
|
|
260
|
+
ok: true,
|
|
261
|
+
exitCode: 0,
|
|
262
|
+
stdout: `[dry-run] would execute: ${command}`,
|
|
263
|
+
stderr: "",
|
|
264
|
+
durationMs: 0,
|
|
265
|
+
timedOut: false,
|
|
266
|
+
blocked: false
|
|
267
|
+
};
|
|
268
|
+
_auditExecution(command, result, actor);
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
const safeCwd = validateCwd(opts.cwd) ?? process.cwd();
|
|
272
|
+
logger.info({ command, cwd: safeCwd }, "autonomy: executing remediation command");
|
|
273
|
+
const [bin, ...cmdArgs] = normalizeWhitespace(command).split(" ").filter(Boolean);
|
|
274
|
+
const execEnv = {
|
|
275
|
+
PATH: process.env.PATH ?? "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
276
|
+
HOME: process.env.HOME ?? os.homedir(),
|
|
277
|
+
USER: process.env.USER ?? os.userInfo().username,
|
|
278
|
+
LOGNAME: process.env.LOGNAME ?? os.userInfo().username,
|
|
279
|
+
TMPDIR: process.env.TMPDIR ?? os.tmpdir(),
|
|
280
|
+
NO_COLOR: "1",
|
|
281
|
+
TERM: "dumb"
|
|
282
|
+
};
|
|
283
|
+
const { spawnSandboxed } = await import("./sandbox.js");
|
|
284
|
+
const { child: proc, sandboxed } = await spawnSandboxed(bin, cmdArgs, {
|
|
285
|
+
cwd: safeCwd,
|
|
286
|
+
env: execEnv,
|
|
287
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
288
|
+
});
|
|
289
|
+
return new Promise((resolve) => {
|
|
290
|
+
let stdout = "";
|
|
291
|
+
let stderr = "";
|
|
292
|
+
let timedOut = false;
|
|
293
|
+
let watcher = null;
|
|
294
|
+
if (!sandboxed && proc.pid) {
|
|
295
|
+
const rootPid = proc.pid;
|
|
296
|
+
void import("./process-tree.js").then(({ watchDescendants, killTree }) => {
|
|
297
|
+
watcher = watchDescendants(rootPid, {
|
|
298
|
+
onSpawn: (descendant) => {
|
|
299
|
+
const check = checkSafetyPolicy(descendant.command);
|
|
300
|
+
if (check.allowed) return;
|
|
301
|
+
logger.error(
|
|
302
|
+
{ command, descendantPid: descendant.pid, descendantCommand: descendant.command },
|
|
303
|
+
"autonomy: descendant process matched a destructive pattern \u2014 terminating the process tree (post-spawn detect-and-kill, not pre-exec)"
|
|
304
|
+
);
|
|
305
|
+
void killTree(rootPid);
|
|
306
|
+
void getStores().blunders.record({
|
|
307
|
+
blunderType: "pipeline_block",
|
|
308
|
+
command: descendant.command,
|
|
309
|
+
blockReason: `Descendant of autonomous remediation command matched a destructive pattern (post-spawn detect-and-kill): ${check.blockReason ?? ""}`,
|
|
310
|
+
service: null,
|
|
311
|
+
tag: "process_tree_descendant",
|
|
312
|
+
actor,
|
|
313
|
+
pid: String(descendant.pid),
|
|
314
|
+
confidenceScore: null
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}).catch(() => {
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
proc.stdout?.on("data", (chunk) => {
|
|
322
|
+
if (stdout.length < MAX_OUTPUT_BYTES) stdout += chunk.toString("utf8");
|
|
323
|
+
});
|
|
324
|
+
proc.stderr?.on("data", (chunk) => {
|
|
325
|
+
if (stderr.length < MAX_OUTPUT_BYTES) stderr += chunk.toString("utf8");
|
|
326
|
+
});
|
|
327
|
+
const timer = setTimeout(() => {
|
|
328
|
+
timedOut = true;
|
|
329
|
+
proc.kill("SIGTERM");
|
|
330
|
+
setTimeout(() => proc.kill("SIGKILL"), 2e3);
|
|
331
|
+
}, EXEC_TIMEOUT_MS);
|
|
332
|
+
proc.on("close", (code) => {
|
|
333
|
+
clearTimeout(timer);
|
|
334
|
+
watcher?.stop();
|
|
335
|
+
const result = {
|
|
336
|
+
ok: code === 0 && !timedOut,
|
|
337
|
+
exitCode: code,
|
|
338
|
+
stdout: stdout.slice(0, MAX_OUTPUT_BYTES),
|
|
339
|
+
stderr: stderr.slice(0, MAX_OUTPUT_BYTES),
|
|
340
|
+
durationMs: Date.now() - start,
|
|
341
|
+
timedOut,
|
|
342
|
+
blocked: false
|
|
343
|
+
};
|
|
344
|
+
_auditExecution(command, result, actor);
|
|
345
|
+
logger.info(
|
|
346
|
+
{ exitCode: code, durationMs: result.durationMs, timedOut, actor, sandboxed },
|
|
347
|
+
"autonomy: command completed"
|
|
348
|
+
);
|
|
349
|
+
resolve(result);
|
|
350
|
+
});
|
|
351
|
+
proc.on("error", (err) => {
|
|
352
|
+
clearTimeout(timer);
|
|
353
|
+
watcher?.stop();
|
|
354
|
+
const result = {
|
|
355
|
+
ok: false,
|
|
356
|
+
exitCode: null,
|
|
357
|
+
stdout,
|
|
358
|
+
stderr: stderr + "\n" + String(err),
|
|
359
|
+
durationMs: Date.now() - start,
|
|
360
|
+
timedOut: false,
|
|
361
|
+
blocked: false
|
|
362
|
+
};
|
|
363
|
+
_auditExecution(command, result, actor);
|
|
364
|
+
resolve(result);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
const CLI_PREFIXES = /^(npm|yarn|pnpm|pip3?|python3?|node|git|docker|kubectl|make|cargo|go|brew|apt|systemctl|service)\s/i;
|
|
369
|
+
function extractCommand(fixHint) {
|
|
370
|
+
if (!fixHint) return null;
|
|
371
|
+
const backtickMatches = [...fixHint.matchAll(/`([^`]{4,200})`/g)];
|
|
372
|
+
for (const m of backtickMatches) {
|
|
373
|
+
const candidate = m[1].trim();
|
|
374
|
+
if (CLI_PREFIXES.test(candidate)) return candidate;
|
|
375
|
+
}
|
|
376
|
+
const prompt = fixHint.match(/\$\s+([^\n]{4,200})/);
|
|
377
|
+
if (prompt) {
|
|
378
|
+
const candidate = prompt[1].trim();
|
|
379
|
+
if (CLI_PREFIXES.test(candidate)) return candidate;
|
|
380
|
+
}
|
|
381
|
+
const lines = fixHint.split("\n");
|
|
382
|
+
for (const line of lines) {
|
|
383
|
+
const trimmed = line.trim();
|
|
384
|
+
if (CLI_PREFIXES.test(trimmed)) return trimmed;
|
|
385
|
+
}
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
export {
|
|
389
|
+
ALLOWED_COMMAND_DESCRIPTIONS,
|
|
390
|
+
DEFAULT_SAFETY_POLICY,
|
|
391
|
+
_resetSafetyPolicyForTesting,
|
|
392
|
+
checkSafetyPolicy,
|
|
393
|
+
executeRemediation,
|
|
394
|
+
extractCommand,
|
|
395
|
+
loadSafetyPolicy,
|
|
396
|
+
validateCwd
|
|
397
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { issueFederationToken } from "./oidc-issuer.js";
|
|
2
|
+
import { createAzureProxySession } from "./azure-proxy-sessions.js";
|
|
3
|
+
const FEDERATED_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
|
|
4
|
+
const DEFAULT_FEDERATION_AUDIENCE = "api://AzureADTokenExchange";
|
|
5
|
+
async function requestAzureCredential(agentId, scopeName, scopeConfig, requestedDurationSeconds) {
|
|
6
|
+
const federationToken = await issueFederationToken({
|
|
7
|
+
agentId,
|
|
8
|
+
audience: scopeConfig.federationAudience ?? DEFAULT_FEDERATION_AUDIENCE,
|
|
9
|
+
// Azure access tokens are typically ~3600s regardless of what's asked
|
|
10
|
+
// for; the federation JWT itself only needs to live long enough to be
|
|
11
|
+
// exchanged, so it doesn't need to match the credential's own duration.
|
|
12
|
+
ttlSeconds: 300
|
|
13
|
+
});
|
|
14
|
+
const tokenUrl = `https://login.microsoftonline.com/${encodeURIComponent(scopeConfig.tenantId)}/oauth2/v2.0/token`;
|
|
15
|
+
const body = new URLSearchParams({
|
|
16
|
+
grant_type: "client_credentials",
|
|
17
|
+
client_id: scopeConfig.clientId,
|
|
18
|
+
client_assertion_type: FEDERATED_ASSERTION_TYPE,
|
|
19
|
+
client_assertion: federationToken,
|
|
20
|
+
scope: scopeConfig.azureScope
|
|
21
|
+
});
|
|
22
|
+
const res = await fetch(tokenUrl, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
25
|
+
body: body.toString(),
|
|
26
|
+
signal: AbortSignal.timeout(1e4)
|
|
27
|
+
});
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
const text = await res.text().catch(() => "");
|
|
30
|
+
throw new Error(`azure-broker: ${tokenUrl} returned ${res.status}: ${text.slice(0, 500)}`);
|
|
31
|
+
}
|
|
32
|
+
const tokenResponse = await res.json();
|
|
33
|
+
const cappedSeconds = Math.min(
|
|
34
|
+
tokenResponse.expires_in,
|
|
35
|
+
requestedDurationSeconds ?? scopeConfig.maxDurationSeconds,
|
|
36
|
+
scopeConfig.maxDurationSeconds
|
|
37
|
+
);
|
|
38
|
+
const { proxyToken, proxyPath } = createAzureProxySession({
|
|
39
|
+
azureAccessToken: tokenResponse.access_token,
|
|
40
|
+
azureApiBase: scopeConfig.azureApiBase,
|
|
41
|
+
expiresInSeconds: cappedSeconds,
|
|
42
|
+
scopeName,
|
|
43
|
+
agentId
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
provider: "azure",
|
|
47
|
+
proxyToken,
|
|
48
|
+
proxyPath,
|
|
49
|
+
clientId: scopeConfig.clientId,
|
|
50
|
+
expiration: new Date(Date.now() + cappedSeconds * 1e3),
|
|
51
|
+
scope: scopeName,
|
|
52
|
+
durationEnforced: true
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
requestAzureCredential
|
|
57
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { randomBytes } from "crypto";
|
|
2
|
+
import logger from "../sensor/logger.js";
|
|
3
|
+
const _sessions = /* @__PURE__ */ new Map();
|
|
4
|
+
const SWEEP_INTERVAL_MS = 5 * 6e4;
|
|
5
|
+
setInterval(() => {
|
|
6
|
+
const now = Date.now();
|
|
7
|
+
for (const [token, session] of _sessions) {
|
|
8
|
+
if (now > session.expiresAt) _sessions.delete(token);
|
|
9
|
+
}
|
|
10
|
+
}, SWEEP_INTERVAL_MS).unref();
|
|
11
|
+
function createAzureProxySession(params) {
|
|
12
|
+
const proxyToken = randomBytes(32).toString("hex");
|
|
13
|
+
_sessions.set(proxyToken, {
|
|
14
|
+
azureAccessToken: params.azureAccessToken,
|
|
15
|
+
azureApiBase: params.azureApiBase,
|
|
16
|
+
expiresAt: Date.now() + params.expiresInSeconds * 1e3,
|
|
17
|
+
scopeName: params.scopeName,
|
|
18
|
+
agentId: params.agentId,
|
|
19
|
+
revoked: false
|
|
20
|
+
});
|
|
21
|
+
logger.info(
|
|
22
|
+
{ scope: params.scopeName, agentId: params.agentId, expiresInSeconds: params.expiresInSeconds },
|
|
23
|
+
"azure-proxy-sessions: session created"
|
|
24
|
+
);
|
|
25
|
+
return { proxyToken, proxyPath: `/credentials/azure-proxy/${proxyToken}` };
|
|
26
|
+
}
|
|
27
|
+
function lookupAzureProxySession(proxyToken) {
|
|
28
|
+
const session = _sessions.get(proxyToken);
|
|
29
|
+
if (!session) return { status: "not_found" };
|
|
30
|
+
if (session.revoked) return { status: "revoked" };
|
|
31
|
+
if (Date.now() > session.expiresAt) return { status: "expired" };
|
|
32
|
+
return { status: "ok", session };
|
|
33
|
+
}
|
|
34
|
+
function revokeAzureProxySession(proxyToken) {
|
|
35
|
+
const session = _sessions.get(proxyToken);
|
|
36
|
+
if (!session) return false;
|
|
37
|
+
session.revoked = true;
|
|
38
|
+
logger.info({ scope: session.scopeName, agentId: session.agentId }, "azure-proxy-sessions: session revoked");
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
function _resetAzureProxySessionsForTesting() {
|
|
42
|
+
_sessions.clear();
|
|
43
|
+
}
|
|
44
|
+
export {
|
|
45
|
+
_resetAzureProxySessionsForTesting,
|
|
46
|
+
createAzureProxySession,
|
|
47
|
+
lookupAzureProxySession,
|
|
48
|
+
revokeAzureProxySession
|
|
49
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../__stubs__/closed-source.js';
|