@datasynx/agentic-ai-cartography 2.3.0 → 2.5.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.
@@ -1,9 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ AuthorizationError,
3
4
  CartographyDB,
4
5
  RulesetSchema,
6
+ SqliteCredentialStore,
5
7
  assertSafeBind,
6
- checkBearer,
8
+ authorize,
9
+ bearerToken,
7
10
  cloudAwsScanner,
8
11
  cloudAzureScanner,
9
12
  cloudGcpScanner,
@@ -17,10 +20,11 @@ import {
17
20
  keyMetaOf,
18
21
  normalizeTenant,
19
22
  redactValue,
23
+ resolvePrincipal,
20
24
  sanitizeUntrusted,
21
25
  stableStringify,
22
26
  stripSensitive
23
- } from "./chunk-7QEBFMN4.js";
27
+ } from "./chunk-GA4427LB.js";
24
28
  import {
25
29
  EdgeSchema,
26
30
  NODE_TYPES,
@@ -29,7 +33,7 @@ import {
29
33
  SECURITY_METADATA_KEYS,
30
34
  SEVERITIES,
31
35
  defaultConfig
32
- } from "./chunk-WCR47QA2.js";
36
+ } from "./chunk-QQOQBE2A.js";
33
37
  import {
34
38
  IS_WIN,
35
39
  PLATFORM,
@@ -965,6 +969,41 @@ var StdoutSink = class {
965
969
 
966
970
  // src/sinks/webhook.ts
967
971
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
972
+ async function postJson(opts) {
973
+ const doFetch = opts.fetchImpl ?? (typeof fetch === "function" ? fetch : void 0);
974
+ if (!doFetch) {
975
+ logWarn("sink unavailable: global fetch missing", { sink: opts.sinkName });
976
+ return;
977
+ }
978
+ if (!opts.url) {
979
+ logWarn("sink unavailable: no url configured", { sink: opts.sinkName });
980
+ return;
981
+ }
982
+ if (!isSecureWebhookUrl(opts.url)) {
983
+ logWarn("sink refused: insecure scheme (use https:// or a loopback host)", {
984
+ sink: opts.sinkName,
985
+ host: stripSensitive(opts.url)
986
+ });
987
+ return;
988
+ }
989
+ try {
990
+ const res = await doFetch(opts.url, {
991
+ method: "POST",
992
+ headers: { "content-type": "application/json", ...opts.headers ?? {} },
993
+ body: JSON.stringify(opts.body),
994
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 1e4)
995
+ });
996
+ if (!res.ok) {
997
+ logError("sink delivery failed", { sink: opts.sinkName, host: stripSensitive(opts.url), status: res.status });
998
+ }
999
+ } catch (err) {
1000
+ logError("sink delivery failed", {
1001
+ sink: opts.sinkName,
1002
+ host: stripSensitive(opts.url),
1003
+ reason: err instanceof Error ? err.message : String(err)
1004
+ });
1005
+ }
1006
+ }
968
1007
  function isSecureWebhookUrl(url, env = process.env) {
969
1008
  if (env.CARTOGRAPHY_ALLOW_INSECURE_SYNC === "1") return true;
970
1009
  let parsed;
@@ -983,59 +1022,177 @@ var WebhookSink = class {
983
1022
  }
984
1023
  name = "webhook";
985
1024
  async emit(alert) {
986
- if (typeof fetch !== "function") {
987
- logWarn("webhook sink unavailable: global fetch missing", { sink: this.name });
988
- return;
989
- }
990
1025
  const { url, token, timeoutMs } = this.opts;
991
- if (!url) {
992
- logWarn("webhook sink unavailable: no url configured", { sink: this.name });
993
- return;
994
- }
995
- if (!isSecureWebhookUrl(url)) {
996
- logWarn("webhook sink refused: insecure scheme (use https:// or a loopback host)", {
997
- sink: this.name,
998
- host: stripSensitive(url)
999
- });
1000
- return;
1001
- }
1002
- try {
1003
- const res = await fetch(url, {
1004
- method: "POST",
1005
- headers: {
1006
- "content-type": "application/json",
1007
- ...token ? { authorization: `Bearer ${token}` } : {}
1008
- },
1009
- body: JSON.stringify(redactValue(alert)),
1010
- signal: AbortSignal.timeout(timeoutMs ?? 1e4)
1011
- });
1012
- if (!res.ok) {
1013
- logError("webhook sink failed", { sink: this.name, host: stripSensitive(url), status: res.status });
1026
+ await postJson({
1027
+ url,
1028
+ body: redactValue(alert),
1029
+ ...token ? { headers: { authorization: `Bearer ${token}` } } : {},
1030
+ ...timeoutMs !== void 0 ? { timeoutMs } : {},
1031
+ sinkName: this.name
1032
+ });
1033
+ }
1034
+ };
1035
+
1036
+ // src/sinks/providers.ts
1037
+ var MAX_ITEMS = 20;
1038
+ var SEVERITY_EMOJI = { info: "\u{1F7E2}", warning: "\u{1F7E1}", critical: "\u{1F534}" };
1039
+ function headline(alert) {
1040
+ const s = alert.summary;
1041
+ return `${s.nodesAdded}+ / ${s.nodesRemoved}- / ${s.nodesChanged}~ nodes, ${s.edgesAdded}+ / ${s.edgesRemoved}- edges`;
1042
+ }
1043
+ function itemLine(it) {
1044
+ const sec = it.securityFields?.length ? ` [security: ${it.securityFields.join(", ")}]` : "";
1045
+ const fields = it.changedFields?.length ? ` (${it.changedFields.join(", ")})` : "";
1046
+ return `${it.severity.toUpperCase()} \xB7 ${it.kind} \xB7 ${it.label}${fields}${sec}`;
1047
+ }
1048
+ function bodyText(alert) {
1049
+ const lines = alert.items.slice(0, MAX_ITEMS).map(itemLine);
1050
+ const more = alert.items.length > MAX_ITEMS ? [`\u2026and ${alert.items.length - MAX_ITEMS} more`] : [];
1051
+ return [headline(alert), "", ...lines, ...more].join("\n");
1052
+ }
1053
+ function formatSlack(alert) {
1054
+ const title = `${SEVERITY_EMOJI[alert.severity]} Topology drift \u2014 ${alert.severity}`;
1055
+ return {
1056
+ text: `${title}: ${headline(alert)}`,
1057
+ blocks: [
1058
+ { type: "header", text: { type: "plain_text", text: title, emoji: true } },
1059
+ { type: "section", text: { type: "mrkdwn", text: "```" + bodyText(alert) + "```" } },
1060
+ { type: "context", elements: [{ type: "mrkdwn", text: `base ${alert.base.sessionId} \u2192 current ${alert.current.sessionId} \xB7 ${alert.generatedAt}` }] }
1061
+ ]
1062
+ };
1063
+ }
1064
+ var PD_SEVERITY = {
1065
+ info: "info",
1066
+ warning: "warning",
1067
+ critical: "critical"
1068
+ };
1069
+ function formatPagerDuty(alert, routingKey) {
1070
+ return {
1071
+ routing_key: routingKey,
1072
+ event_action: "trigger",
1073
+ // Stable per base→current pair so repeated alerts for the same delta de-duplicate.
1074
+ dedup_key: `cartograph-drift:${alert.base.sessionId}:${alert.current.sessionId}`,
1075
+ payload: {
1076
+ summary: `Cartograph topology drift (${alert.severity}): ${headline(alert)}`,
1077
+ source: "cartograph",
1078
+ severity: PD_SEVERITY[alert.severity],
1079
+ timestamp: alert.generatedAt,
1080
+ custom_details: {
1081
+ summary: alert.summary,
1082
+ items: alert.items.slice(0, MAX_ITEMS).map((it) => ({
1083
+ kind: it.kind,
1084
+ ref: it.ref,
1085
+ severity: it.severity,
1086
+ ...it.changedFields ? { changedFields: it.changedFields } : {},
1087
+ ...it.securityFields ? { securityFields: it.securityFields } : {}
1088
+ }))
1014
1089
  }
1015
- } catch (err) {
1016
- logError("webhook sink failed", {
1017
- sink: this.name,
1018
- host: stripSensitive(url),
1019
- reason: err instanceof Error ? err.message : String(err)
1020
- });
1021
1090
  }
1091
+ };
1092
+ }
1093
+ function formatJira(alert, opts) {
1094
+ return {
1095
+ fields: {
1096
+ project: { key: opts.project },
1097
+ issuetype: { name: opts.issueType ?? "Task" },
1098
+ summary: `Cartograph topology drift (${alert.severity}): ${headline(alert)}`,
1099
+ description: bodyText(alert) + `
1100
+
1101
+ base ${alert.base.sessionId} \u2192 current ${alert.current.sessionId}
1102
+ generated ${alert.generatedAt}`
1103
+ }
1104
+ };
1105
+ }
1106
+
1107
+ // src/sinks/provider-sink.ts
1108
+ var PAGERDUTY_ENQUEUE_URL = "https://events.pagerduty.com/v2/enqueue";
1109
+ function deliver(name, url, body, opts, headers) {
1110
+ return postJson({
1111
+ url,
1112
+ body,
1113
+ sinkName: name,
1114
+ ...headers ? { headers } : {},
1115
+ ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
1116
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
1117
+ });
1118
+ }
1119
+ var SlackSink = class {
1120
+ constructor(opts) {
1121
+ this.opts = opts;
1122
+ }
1123
+ name = "slack";
1124
+ async emit(alert) {
1125
+ await deliver(this.name, this.opts.url, formatSlack(redactValue(alert)), this.opts);
1126
+ }
1127
+ };
1128
+ var PagerDutySink = class {
1129
+ constructor(opts) {
1130
+ this.opts = opts;
1131
+ }
1132
+ name = "pagerduty";
1133
+ async emit(alert) {
1134
+ const body = formatPagerDuty(redactValue(alert), this.opts.routingKey);
1135
+ await deliver(this.name, this.opts.url || PAGERDUTY_ENQUEUE_URL, body, this.opts);
1136
+ }
1137
+ };
1138
+ var JiraSink = class {
1139
+ constructor(opts) {
1140
+ this.opts = opts;
1141
+ }
1142
+ name = "jira";
1143
+ async emit(alert) {
1144
+ const body = formatJira(redactValue(alert), {
1145
+ project: this.opts.project,
1146
+ ...this.opts.issueType ? { issueType: this.opts.issueType } : {}
1147
+ });
1148
+ const auth = Buffer.from(`${this.opts.email}:${this.opts.token}`).toString("base64");
1149
+ const base = this.opts.url.replace(/\/+$/, "");
1150
+ await deliver(this.name, `${base}/rest/api/2/issue`, body, this.opts, { authorization: `Basic ${auth}` });
1022
1151
  }
1023
1152
  };
1024
1153
 
1025
1154
  // src/sinks/index.ts
1026
1155
  function buildSinks(drift) {
1027
1156
  const configs = drift?.sinks && drift.sinks.length > 0 ? drift.sinks : [{ type: "stdout" }];
1157
+ const envSecret = process.env.CARTOGRAPHY_DRIFT_TOKEN;
1028
1158
  const sinks = [];
1029
1159
  for (const s of configs) {
1030
- if (s.type === "webhook") {
1031
- if (!s.url) continue;
1032
- sinks.push(new WebhookSink({
1033
- url: s.url,
1034
- token: s.token ?? process.env.CARTOGRAPHY_DRIFT_TOKEN,
1035
- timeoutMs: s.timeoutMs
1036
- }));
1037
- } else {
1038
- sinks.push(new StdoutSink());
1160
+ const timeoutMs = s.timeoutMs;
1161
+ switch (s.type) {
1162
+ case "webhook":
1163
+ if (!s.url) {
1164
+ logWarn("drift sink skipped: webhook requires a url", { sink: s.type });
1165
+ break;
1166
+ }
1167
+ sinks.push(new WebhookSink({ url: s.url, token: s.token ?? envSecret, timeoutMs }));
1168
+ break;
1169
+ case "slack":
1170
+ if (!s.url) {
1171
+ logWarn("drift sink skipped: slack requires a webhook url", { sink: s.type });
1172
+ break;
1173
+ }
1174
+ sinks.push(new SlackSink({ url: s.url, timeoutMs }));
1175
+ break;
1176
+ case "pagerduty": {
1177
+ const routingKey = s.routingKey ?? s.token ?? envSecret;
1178
+ if (!routingKey) {
1179
+ logWarn("drift sink skipped: pagerduty requires a routingKey (or CARTOGRAPHY_DRIFT_TOKEN)", { sink: s.type });
1180
+ break;
1181
+ }
1182
+ sinks.push(new PagerDutySink({ url: s.url ?? PAGERDUTY_ENQUEUE_URL, routingKey, timeoutMs }));
1183
+ break;
1184
+ }
1185
+ case "jira": {
1186
+ const token = s.token ?? envSecret;
1187
+ if (!s.url || !s.email || !s.project || !token) {
1188
+ logWarn("drift sink skipped: jira requires url, email, project and a token", { sink: s.type });
1189
+ break;
1190
+ }
1191
+ sinks.push(new JiraSink({ url: s.url, email: s.email, token, project: s.project, issueType: s.issueType, timeoutMs }));
1192
+ break;
1193
+ }
1194
+ default:
1195
+ sinks.push(new StdoutSink());
1039
1196
  }
1040
1197
  }
1041
1198
  return sinks.length > 0 ? sinks : [new StdoutSink()];
@@ -1381,7 +1538,7 @@ async function executeNlQuery(db, sessionId, search, intent, opts = {}) {
1381
1538
 
1382
1539
  // src/mcp/server.ts
1383
1540
  var SERVER_NAME = "cartography";
1384
- var SERVER_VERSION = "2.3.0";
1541
+ var SERVER_VERSION = "2.5.0";
1385
1542
  var SERVICE_TYPES = NODE_TYPE_GROUPS.web;
1386
1543
  var DATA_TYPES = NODE_TYPE_GROUPS.data;
1387
1544
  var lexicalSearch = async (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
@@ -1783,6 +1940,14 @@ function createMcpServer(opts = {}) {
1783
1940
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
1784
1941
  },
1785
1942
  async (args) => {
1943
+ if (opts.principal) {
1944
+ try {
1945
+ authorize(opts.principal, "discovery");
1946
+ } catch (err) {
1947
+ if (err instanceof AuthorizationError) return json({ error: `forbidden: role '${opts.principal.role}' may not run discovery (operator required)` });
1948
+ throw err;
1949
+ }
1950
+ }
1786
1951
  let sid = resolveSession();
1787
1952
  if (args.update) {
1788
1953
  if (!sid) return json({ error: "No session to update; run discovery first." });
@@ -1882,6 +2047,9 @@ import { randomUUID } from "crypto";
1882
2047
  import http from "http";
1883
2048
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1884
2049
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2050
+ function samePrincipal(a, b) {
2051
+ return a.subject === b.subject && a.tenant === b.tenant && a.role === b.role;
2052
+ }
1885
2053
  async function runStdio(server) {
1886
2054
  const transport = new StdioServerTransport();
1887
2055
  await server.connect(transport);
@@ -1926,6 +2094,14 @@ async function runHttp(factory, opts = {}) {
1926
2094
  assertSafeBind({ host, port, ...opts.allowedHosts ? { allowedHosts: opts.allowedHosts } : {}, ...opts.token ? { token: opts.token } : {} });
1927
2095
  const allowedHosts = opts.allowedHosts ?? defaultAllowedHosts(host, port);
1928
2096
  const token = opts.token;
2097
+ const authStore = opts.auth?.store;
2098
+ const defaultTenant = opts.defaultTenant;
2099
+ const resolveAuth = (header) => resolvePrincipal(bearerToken(header), {
2100
+ ...authStore ? { store: authStore } : {},
2101
+ ...token ? { sharedToken: token } : {},
2102
+ ...defaultTenant ? { defaultTenant } : {},
2103
+ ...opts.auth?.required ? { required: true } : {}
2104
+ });
1929
2105
  const transports = /* @__PURE__ */ new Map();
1930
2106
  const httpServer = http.createServer(async (req, res) => {
1931
2107
  try {
@@ -1935,7 +2111,8 @@ async function runHttp(factory, opts = {}) {
1935
2111
  res.writeHead(404, { "content-type": "application/json" }).end('{"error":"not found"}');
1936
2112
  return;
1937
2113
  }
1938
- if (!checkBearer(req.headers["authorization"], token)) {
2114
+ const principal = resolveAuth(req.headers["authorization"]);
2115
+ if (!principal) {
1939
2116
  res.writeHead(401, { "content-type": "application/json", "www-authenticate": "Bearer" }).end('{"error":"unauthorized"}');
1940
2117
  return;
1941
2118
  }
@@ -1962,8 +2139,12 @@ async function runHttp(factory, opts = {}) {
1962
2139
  const sessionId = req.headers["mcp-session-id"];
1963
2140
  const existing = sessionId ? transports.get(sessionId) : void 0;
1964
2141
  if (existing) {
2142
+ if (!samePrincipal(existing.principal, principal)) {
2143
+ res.writeHead(403, { "content-type": "application/json" }).end('{"error":"session belongs to a different principal"}');
2144
+ return;
2145
+ }
1965
2146
  const body2 = req.method === "POST" ? await readJsonBody(req) : void 0;
1966
- await existing.handleRequest(req, res, body2);
2147
+ await existing.transport.handleRequest(req, res, body2);
1967
2148
  return;
1968
2149
  }
1969
2150
  if (req.method !== "POST") {
@@ -1977,13 +2158,13 @@ async function runHttp(factory, opts = {}) {
1977
2158
  allowedHosts,
1978
2159
  ...opts.allowedOrigins ? { allowedOrigins: opts.allowedOrigins } : {},
1979
2160
  onsessioninitialized: (id) => {
1980
- transports.set(id, transport);
2161
+ transports.set(id, { transport, principal });
1981
2162
  }
1982
2163
  });
1983
2164
  transport.onclose = () => {
1984
2165
  if (transport.sessionId) transports.delete(transport.sessionId);
1985
2166
  };
1986
- await factory().connect(transport);
2167
+ await factory(principal).connect(transport);
1987
2168
  await transport.handleRequest(req, res, body);
1988
2169
  } catch (err) {
1989
2170
  process.stderr.write(`[cartography-mcp] HTTP request failed: ${err instanceof Error ? err.message : String(err)}
@@ -2271,7 +2452,7 @@ function revalidateAnonymized(node, level, mode) {
2271
2452
 
2272
2453
  // src/central/ingest.ts
2273
2454
  var INGEST_SCHEMA_VERSION = 1;
2274
- var MAX_ITEMS = 5e4;
2455
+ var MAX_ITEMS2 = 5e4;
2275
2456
  var ContributorSchema = z3.object({
2276
2457
  machineId: z3.string().min(1),
2277
2458
  hostname: z3.string().default("unknown"),
@@ -2285,7 +2466,7 @@ var IngestEnvelopeSchema = z3.object({
2285
2466
  contentHash: z3.string(),
2286
2467
  kind: z3.enum(["node", "edge"]),
2287
2468
  payload: z3.unknown()
2288
- })).max(MAX_ITEMS),
2469
+ })).max(MAX_ITEMS2),
2289
2470
  // Extensions (forward-compatible; 2.11 does not yet send these).
2290
2471
  contributor: ContributorSchema.optional(),
2291
2472
  anonymizationLevel: z3.enum(["none", "anonymized", "full"]).optional()
@@ -2405,7 +2586,8 @@ function parseMcpArgs(argv) {
2405
2586
  else if (a === "--anon-mode") {
2406
2587
  const m = argv[++i];
2407
2588
  if (m === "reject" || m === "strip") opts.anonMode = m;
2408
- } else if (a === "--help" || a === "-h") opts.help = true;
2589
+ } else if (a === "--auth-required") opts.authRequired = true;
2590
+ else if (a === "--help" || a === "-h") opts.help = true;
2409
2591
  }
2410
2592
  return opts;
2411
2593
  }
@@ -2420,8 +2602,21 @@ async function startMcp(opts = {}) {
2420
2602
  const discovery = localDiscoveryFn(void 0, plugins);
2421
2603
  const tenant = normalizeTenant(opts.tenant);
2422
2604
  const serverMode = opts.serverMode === true;
2423
- const org = serverMode ? tenant : void 0;
2424
- const factory = () => createMcpServer({ db, session: opts.session ?? "latest", tenant, search, discovery, ...org !== void 0 ? { org } : {} });
2605
+ const authStore = new SqliteCredentialStore(db);
2606
+ const rbacActive = authStore.count() > 0;
2607
+ const factory = (principal) => {
2608
+ const scopeTenant = rbacActive && principal ? principal.tenant : tenant;
2609
+ const orgArg = serverMode ? scopeTenant : void 0;
2610
+ return createMcpServer({
2611
+ db,
2612
+ session: opts.session ?? "latest",
2613
+ tenant: scopeTenant,
2614
+ search,
2615
+ discovery,
2616
+ ...principal ? { principal } : {},
2617
+ ...orgArg !== void 0 ? { org: orgArg } : {}
2618
+ });
2619
+ };
2425
2620
  const transport = serverMode ? "http" : opts.transport;
2426
2621
  if (transport === "http") {
2427
2622
  const port = opts.port ?? 3737;
@@ -2436,6 +2631,8 @@ async function startMcp(opts = {}) {
2436
2631
  await runHttp(factory, {
2437
2632
  port,
2438
2633
  host,
2634
+ auth: { store: authStore, ...opts.authRequired ? { required: true } : {} },
2635
+ defaultTenant: tenant,
2439
2636
  ...opts.allowedHosts ? { allowedHosts: opts.allowedHosts } : {},
2440
2637
  ...token ? { token } : {},
2441
2638
  ...onIngest ? { onIngest } : {}
@@ -2462,4 +2659,4 @@ export {
2462
2659
  parseMcpArgs,
2463
2660
  startMcp
2464
2661
  };
2465
- //# sourceMappingURL=chunk-B2AKONVW.js.map
2662
+ //# sourceMappingURL=chunk-RYQ4KQCK.js.map