@datasynx/agentic-ai-cartography 2.6.0 → 2.8.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/dist/index.cjs CHANGED
@@ -45,9 +45,11 @@ __export(src_exports, {
45
45
  CredentialConfigSchema: () => CredentialConfigSchema,
46
46
  CsvCostSource: () => CsvCostSource,
47
47
  DEFAULT_ANOMALY_THRESHOLDS: () => DEFAULT_ANOMALY_THRESHOLDS,
48
+ DEFAULT_INGEST_QUOTA: () => DEFAULT_INGEST_QUOTA,
48
49
  DEFAULT_SERVER_NAME: () => DEFAULT_SERVER_NAME,
49
50
  DEFAULT_TENANT: () => DEFAULT_TENANT,
50
51
  DriftConfigSchema: () => DriftConfigSchema,
52
+ GraphStoreBackend: () => GraphStoreBackend,
51
53
  INGEST_SCHEMA_VERSION: () => INGEST_SCHEMA_VERSION,
52
54
  IngestEnvelopeSchema: () => IngestEnvelopeSchema,
53
55
  InvalidTenantError: () => InvalidTenantError,
@@ -66,10 +68,12 @@ __export(src_exports, {
66
68
  ProviderRegistry: () => ProviderRegistry,
67
69
  RELATION_TO_DIRECTION: () => RELATION_TO_DIRECTION,
68
70
  ROLES: () => ROLES,
71
+ RateLimiter: () => RateLimiter,
69
72
  RoleSchema: () => RoleSchema,
70
73
  RuleCheckSchema: () => RuleCheckSchema,
71
74
  RulesetSchema: () => RulesetSchema,
72
75
  SCAN_ARG_PATTERNS: () => SCAN_ARG_PATTERNS,
76
+ SCHEMA_VERSION: () => SCHEMA_VERSION,
73
77
  SDL: () => SDL,
74
78
  SEVERITY_WEIGHT: () => SEVERITY_WEIGHT,
75
79
  SHARING_LEVELS: () => SHARING_LEVELS,
@@ -217,6 +221,7 @@ __export(src_exports, {
217
221
  nodesToAssets: () => nodesToAssets,
218
222
  normalizeId: () => normalizeId,
219
223
  normalizeTenant: () => normalizeTenant,
224
+ openStoreBackend: () => openStoreBackend,
220
225
  orgKeyPath: () => orgKeyPath,
221
226
  osUser: () => osUser,
222
227
  parseApiArgs: () => parseApiArgs,
@@ -2850,6 +2855,7 @@ function newAnomalies(base, current) {
2850
2855
 
2851
2856
  // src/db.ts
2852
2857
  var DEFAULT_TENANT = "local";
2858
+ var SCHEMA_VERSION = 15;
2853
2859
  function normalizeTenant(raw) {
2854
2860
  if (raw == null) return DEFAULT_TENANT;
2855
2861
  const cleaned = sanitizeUntrusted(String(raw)).trim().slice(0, 128);
@@ -4254,6 +4260,14 @@ var CartographyDB = class {
4254
4260
  }
4255
4261
  return rows.length;
4256
4262
  }
4263
+ /**
4264
+ * Retention/compaction (4.7): delete audit events older than `olderThan` (ISO 8601).
4265
+ * The audit trail grows unbounded on a busy collector; this bounds it without touching
4266
+ * sessions/nodes/edges. Returns the number of events removed.
4267
+ */
4268
+ pruneEventsOlderThan(olderThan) {
4269
+ return this.db.prepare("DELETE FROM activity_events WHERE timestamp < ?").run(olderThan).changes;
4270
+ }
4257
4271
  // ── Graph queries (read-only context layer) ─────────────────────────────────
4258
4272
  /** Fetch a single node by id within a session. */
4259
4273
  getNode(sessionId, nodeId) {
@@ -4641,6 +4655,148 @@ var SqliteStoreBackend = class {
4641
4655
  }
4642
4656
  };
4643
4657
 
4658
+ // src/store/graph.ts
4659
+ function toNum(v) {
4660
+ if (typeof v === "number") return v;
4661
+ if (v && typeof v === "object" && "toNumber" in v && typeof v.toNumber === "function") {
4662
+ return v.toNumber();
4663
+ }
4664
+ return Number(v ?? 0);
4665
+ }
4666
+ var GraphStoreBackend = class {
4667
+ constructor(driver) {
4668
+ this.driver = driver;
4669
+ }
4670
+ async run(cypher, params) {
4671
+ const session = this.driver.session();
4672
+ try {
4673
+ return await session.run(cypher, params);
4674
+ } finally {
4675
+ await session.close();
4676
+ }
4677
+ }
4678
+ async upsertNode(org, node, identity, contributor) {
4679
+ const res = await this.run(
4680
+ `MERGE (n:Node {org: $org, globalId: $globalId})
4681
+ ON CREATE SET n._created = true
4682
+ ON MATCH SET n._created = false
4683
+ SET n.id = $id, n.contentHash = $contentHash, n.type = $type, n.name = $name,
4684
+ n.domain = $domain, n.owner = $owner,
4685
+ n.confidence = CASE WHEN n.confidence IS NULL OR $confidence > n.confidence THEN $confidence ELSE n.confidence END
4686
+ MERGE (c:Contributor {org: $org, globalId: $globalId, machineId: $machineId})
4687
+ SET c.hostname = $hostname, c.user = $user, c.at = $at,
4688
+ c.confidence = CASE WHEN c.confidence IS NULL OR $contribConfidence > c.confidence THEN $contribConfidence ELSE c.confidence END
4689
+ RETURN n._created AS created`,
4690
+ {
4691
+ org,
4692
+ globalId: identity.globalId,
4693
+ contentHash: identity.contentHash,
4694
+ id: node.id,
4695
+ type: node.type,
4696
+ name: node.name,
4697
+ domain: node.domain ?? null,
4698
+ owner: node.owner ?? null,
4699
+ confidence: node.confidence,
4700
+ machineId: contributor.machineId,
4701
+ hostname: contributor.hostname,
4702
+ user: contributor.user,
4703
+ at: contributor.at,
4704
+ contribConfidence: contributor.confidence
4705
+ }
4706
+ );
4707
+ return res.records[0]?.get("created") === true ? "created" : "merged";
4708
+ }
4709
+ async insertEdge(org, edge) {
4710
+ await this.run(
4711
+ `MATCH (s:Node {org: $org, id: $source})
4712
+ MATCH (t:Node {org: $org, id: $target})
4713
+ MERGE (s)-[r:DEPENDS {relationship: $rel}]->(t)
4714
+ SET r.evidence = $evidence, r.confidence = $confidence`,
4715
+ { org, source: edge.sourceId, target: edge.targetId, rel: edge.relationship, evidence: edge.evidence, confidence: edge.confidence }
4716
+ );
4717
+ }
4718
+ async getSummary(org) {
4719
+ const totals = await this.run(
4720
+ `MATCH (n:Node {org: $org})
4721
+ OPTIONAL MATCH (n)-[r:DEPENDS]->(:Node {org: $org})
4722
+ RETURN count(DISTINCT n) AS nodes, count(r) AS edges`,
4723
+ { org }
4724
+ );
4725
+ const byType = await this.run(`MATCH (n:Node {org: $org}) RETURN n.type AS k, count(*) AS c`, { org });
4726
+ const byDomain = await this.run(`MATCH (n:Node {org: $org}) RETURN coalesce(n.domain, '(none)') AS k, count(*) AS c`, { org });
4727
+ const byRel = await this.run(`MATCH (:Node {org: $org})-[r:DEPENDS]->(:Node {org: $org}) RETURN r.relationship AS k, count(*) AS c`, { org });
4728
+ const top = await this.run(
4729
+ `MATCH (n:Node {org: $org})
4730
+ OPTIONAL MATCH (n)-[r:DEPENDS]-(:Node {org: $org})
4731
+ RETURN n.id AS id, n.name AS name, n.type AS type, count(r) AS degree
4732
+ ORDER BY degree DESC, id ASC LIMIT 10`,
4733
+ { org }
4734
+ );
4735
+ const contrib = await this.run(`MATCH (c:Contributor {org: $org}) RETURN count(DISTINCT c.machineId) AS contributors`, { org });
4736
+ const counts = (r) => {
4737
+ const out = {};
4738
+ for (const rec of r.records) out[String(rec.get("k"))] = toNum(rec.get("c"));
4739
+ return out;
4740
+ };
4741
+ return {
4742
+ org,
4743
+ totals: { nodes: toNum(totals.records[0]?.get("nodes")), edges: toNum(totals.records[0]?.get("edges")) },
4744
+ nodesByType: counts(byType),
4745
+ nodesByDomain: counts(byDomain),
4746
+ edgesByRelationship: counts(byRel),
4747
+ topConnected: top.records.map((rec) => ({
4748
+ id: String(rec.get("id")),
4749
+ name: String(rec.get("name")),
4750
+ type: String(rec.get("type")),
4751
+ degree: toNum(rec.get("degree"))
4752
+ })),
4753
+ contributors: toNum(contrib.records[0]?.get("contributors"))
4754
+ };
4755
+ }
4756
+ async getContributors(globalId2) {
4757
+ const res = await this.run(
4758
+ `MATCH (c:Contributor {globalId: $globalId})
4759
+ RETURN c.machineId AS machineId, c.hostname AS hostname, c.user AS user, c.org AS org, c.at AS at, c.confidence AS confidence`,
4760
+ { globalId: globalId2 }
4761
+ );
4762
+ return res.records.map((rec) => ({
4763
+ machineId: String(rec.get("machineId")),
4764
+ hostname: String(rec.get("hostname")),
4765
+ user: String(rec.get("user")),
4766
+ organization: rec.get("org") != null ? String(rec.get("org")) : void 0,
4767
+ at: String(rec.get("at")),
4768
+ confidence: toNum(rec.get("confidence"))
4769
+ }));
4770
+ }
4771
+ async close() {
4772
+ await this.driver.close();
4773
+ }
4774
+ };
4775
+
4776
+ // src/store/index.ts
4777
+ async function defaultNeo4jDriver(url, user, password) {
4778
+ const mod = await import("neo4j-driver");
4779
+ return mod.default.driver(url, mod.default.auth.basic(user, password));
4780
+ }
4781
+ async function openStoreBackend(db, opts = {}) {
4782
+ if (opts.backend === "graph" && opts.graphUrl) {
4783
+ try {
4784
+ const make = opts.driverFactory ?? defaultNeo4jDriver;
4785
+ const driver = await make(opts.graphUrl, opts.graphUser ?? "neo4j", opts.graphPassword ?? "");
4786
+ if (driver.verifyConnectivity) await driver.verifyConnectivity();
4787
+ logInfo("central store: graph backend active", { host: stripSensitive(opts.graphUrl) });
4788
+ return new GraphStoreBackend(driver);
4789
+ } catch (err) {
4790
+ logWarn("central store: graph backend unavailable \u2014 falling back to SQLite", {
4791
+ host: stripSensitive(opts.graphUrl),
4792
+ reason: err instanceof Error ? err.message : String(err)
4793
+ });
4794
+ return new SqliteStoreBackend(db);
4795
+ }
4796
+ }
4797
+ return new SqliteStoreBackend(db);
4798
+ }
4799
+
4644
4800
  // src/store/query.ts
4645
4801
  var NotFoundError = class extends Error {
4646
4802
  constructor(message) {
@@ -4945,7 +5101,7 @@ var ContributorSchema = import_zod5.z.object({
4945
5101
  });
4946
5102
  var IngestEnvelopeSchema = import_zod5.z.object({
4947
5103
  schemaVersion: import_zod5.z.literal(INGEST_SCHEMA_VERSION),
4948
- org: import_zod5.z.string().min(1).optional(),
5104
+ org: import_zod5.z.string().min(1).max(128).optional(),
4949
5105
  items: import_zod5.z.array(import_zod5.z.object({
4950
5106
  contentHash: import_zod5.z.string(),
4951
5107
  kind: import_zod5.z.enum(["node", "edge"]),
@@ -4955,9 +5111,9 @@ var IngestEnvelopeSchema = import_zod5.z.object({
4955
5111
  contributor: ContributorSchema.optional(),
4956
5112
  anonymizationLevel: import_zod5.z.enum(["none", "anonymized", "full"]).optional()
4957
5113
  });
4958
- function ingestEnvelope(store, envelope, opts = {}) {
5114
+ async function ingestEnvelope(store, envelope, opts = {}) {
4959
5115
  const anonMode = opts.anonMode ?? "reject";
4960
- const org = envelope.org ?? opts.defaultOrg ?? "local";
5116
+ const org = normalizeTenant(envelope.org ?? opts.defaultOrg);
4961
5117
  const level = envelope.anonymizationLevel ?? "anonymized";
4962
5118
  const at = (/* @__PURE__ */ new Date()).toISOString();
4963
5119
  const contributor = {
@@ -5008,7 +5164,7 @@ function ingestEnvelope(store, envelope, opts = {}) {
5008
5164
  }
5009
5165
  const safe = check.node;
5010
5166
  const identity = computeIdentity(org, safe);
5011
- const outcome = store.upsertNode(org, safe, identity, { ...contributor, confidence: safe.confidence });
5167
+ const outcome = await store.upsertNode(org, safe, identity, { ...contributor, confidence: safe.confidence });
5012
5168
  accepted += 1;
5013
5169
  if (outcome === "merged") merged += 1;
5014
5170
  acceptedNodeIds.add(safe.id);
@@ -5024,7 +5180,7 @@ function ingestEnvelope(store, envelope, opts = {}) {
5024
5180
  if (acceptedNodeIds.size > 0 && (!acceptedNodeIds.has(edge.sourceId) || !acceptedNodeIds.has(edge.targetId))) {
5025
5181
  continue;
5026
5182
  }
5027
- store.insertEdge(org, edge);
5183
+ await store.insertEdge(org, edge);
5028
5184
  edges += 1;
5029
5185
  }
5030
5186
  logInfo("ingest", { org, accepted, merged, rejected, edges, violations, level, anonMode });
@@ -5033,15 +5189,24 @@ function ingestEnvelope(store, envelope, opts = {}) {
5033
5189
 
5034
5190
  // src/central/server.ts
5035
5191
  function createIngestHandler(store, opts = {}) {
5036
- return (body) => {
5192
+ const quota = opts.quota;
5193
+ return async (body) => {
5037
5194
  const parsed = IngestEnvelopeSchema.safeParse(body);
5038
5195
  if (!parsed.success) {
5039
5196
  const issues = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`);
5040
5197
  logWarn("ingest: rejected invalid envelope", { issues });
5041
5198
  return { status: 400, body: { error: "invalid envelope", issues } };
5042
5199
  }
5200
+ if (quota) {
5201
+ const org = normalizeTenant(parsed.data.org ?? opts.defaultOrg);
5202
+ const decision = quota.take(org);
5203
+ if (!decision.allowed) {
5204
+ logWarn("ingest: rate limited", { org, retryAfterSec: decision.retryAfterSec });
5205
+ return { status: 429, body: { error: "too many requests" }, headers: { "retry-after": String(decision.retryAfterSec) } };
5206
+ }
5207
+ }
5043
5208
  try {
5044
- const result = ingestEnvelope(store, parsed.data, opts);
5209
+ const result = await ingestEnvelope(store, parsed.data, opts);
5045
5210
  return { status: 200, body: result };
5046
5211
  } catch (err) {
5047
5212
  logWarn("ingest: failed", { error: err instanceof Error ? err.message : String(err) });
@@ -5050,6 +5215,39 @@ function createIngestHandler(store, opts = {}) {
5050
5215
  };
5051
5216
  }
5052
5217
 
5218
+ // src/central/quota.ts
5219
+ var DEFAULT_INGEST_QUOTA = { capacity: 120, refillMs: 6e4 };
5220
+ var MAX_KEYS = 1e4;
5221
+ var RateLimiter = class {
5222
+ constructor(cfg = DEFAULT_INGEST_QUOTA, now = () => Date.now()) {
5223
+ this.cfg = cfg;
5224
+ this.now = now;
5225
+ }
5226
+ buckets = /* @__PURE__ */ new Map();
5227
+ /** Consume one token for `key`. Returns whether the request is allowed (+ Retry-After when not). */
5228
+ take(key) {
5229
+ const t = this.now();
5230
+ const ratePerMs = this.cfg.capacity / this.cfg.refillMs;
5231
+ let b = this.buckets.get(key);
5232
+ if (!b) {
5233
+ if (this.buckets.size >= MAX_KEYS) {
5234
+ const oldest = this.buckets.keys().next().value;
5235
+ if (oldest !== void 0) this.buckets.delete(oldest);
5236
+ }
5237
+ b = { tokens: this.cfg.capacity, last: t };
5238
+ this.buckets.set(key, b);
5239
+ }
5240
+ b.tokens = Math.min(this.cfg.capacity, b.tokens + Math.max(0, t - b.last) * ratePerMs);
5241
+ b.last = t;
5242
+ if (b.tokens >= 1) {
5243
+ b.tokens -= 1;
5244
+ return { allowed: true, retryAfterSec: 0 };
5245
+ }
5246
+ const waitMs = (1 - b.tokens) / ratePerMs;
5247
+ return { allowed: false, retryAfterSec: Math.max(1, Math.ceil(waitMs / 1e3)) };
5248
+ }
5249
+ };
5250
+
5053
5251
  // src/scanners/bookmarks.ts
5054
5252
  var PERSONAL = [
5055
5253
  "facebook.",
@@ -5937,7 +6135,7 @@ async function resolveNlQuery(db, sessionId, search, raw, opts) {
5937
6135
 
5938
6136
  // src/mcp/server.ts
5939
6137
  var SERVER_NAME = "cartography";
5940
- var SERVER_VERSION = "2.6.0";
6138
+ var SERVER_VERSION = "2.8.0";
5941
6139
  var SERVICE_TYPES = NODE_TYPE_GROUPS.web;
5942
6140
  var DATA_TYPES = NODE_TYPE_GROUPS.data;
5943
6141
  var lexicalSearch = async (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
@@ -6012,9 +6210,10 @@ function createMcpServer(opts = {}) {
6012
6210
  "graph-summary",
6013
6211
  "cartography://graph/summary",
6014
6212
  { title: "Topology summary", description: "Low-token aggregate index of the whole landscape \u2014 read this first.", mimeType: "text/markdown" },
6015
- (uri) => {
6213
+ async (uri) => {
6016
6214
  if (org !== void 0) {
6017
- return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: summaryText(db.getOrgSummary(org)) }] };
6215
+ const s = opts.orgSummary ? await opts.orgSummary(org) : db.getOrgSummary(org);
6216
+ return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: summaryText(s) }] };
6018
6217
  }
6019
6218
  const sid = resolveSession();
6020
6219
  if (!sid) return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: "No discovery session found. Run discovery first." }] };
@@ -6089,8 +6288,8 @@ function createMcpServer(opts = {}) {
6089
6288
  server.registerTool(
6090
6289
  "get_summary",
6091
6290
  { title: "Get topology summary", description: "Low-token overview of the whole landscape (counts, types, domains, most-connected, anomalies).", inputSchema: {}, annotations: readOnly },
6092
- () => {
6093
- if (org !== void 0) return json(db.getOrgSummary(org));
6291
+ async () => {
6292
+ if (org !== void 0) return json(opts.orgSummary ? await opts.orgSummary(org) : db.getOrgSummary(org));
6094
6293
  const sid = resolveSession();
6095
6294
  if (!sid) return json({ error: "No discovery session found." });
6096
6295
  return json(db.getGraphSummary(sid));
@@ -6580,6 +6779,16 @@ async function runHttp(factory, opts = {}) {
6580
6779
  const httpServer = import_node_http.default.createServer(async (req, res) => {
6581
6780
  try {
6582
6781
  const url = req.url ?? "";
6782
+ const probePath = new URL(url || "/", "http://probe").pathname;
6783
+ if (probePath === "/healthz") {
6784
+ res.writeHead(200, { "content-type": "application/json" }).end('{"status":"ok"}');
6785
+ return;
6786
+ }
6787
+ if (probePath === "/readyz") {
6788
+ const r = opts.readiness ? opts.readiness() : { ready: true };
6789
+ res.writeHead(r.ready ? 200 : 503, { "content-type": "application/json" }).end(JSON.stringify({ status: r.ready ? "ready" : "unready" }));
6790
+ return;
6791
+ }
6583
6792
  const isIngest = url.startsWith("/ingest") && opts.onIngest !== void 0;
6584
6793
  if (!url.startsWith("/mcp") && !isIngest) {
6585
6794
  res.writeHead(404, { "content-type": "application/json" }).end('{"error":"not found"}');
@@ -6606,8 +6815,8 @@ async function runHttp(factory, opts = {}) {
6606
6815
  res.writeHead(413, { "content-type": "application/json" }).end('{"error":"payload too large"}');
6607
6816
  return;
6608
6817
  }
6609
- const out = onIngest(value);
6610
- res.writeHead(out.status, { "content-type": "application/json" }).end(JSON.stringify(out.body));
6818
+ const out = await onIngest(value);
6819
+ res.writeHead(out.status, { "content-type": "application/json", ...out.headers ?? {} }).end(JSON.stringify(out.body));
6611
6820
  return;
6612
6821
  }
6613
6822
  const sessionId = req.headers["mcp-session-id"];
@@ -11859,9 +12068,11 @@ function checkClaudePrerequisites() {
11859
12068
  CredentialConfigSchema,
11860
12069
  CsvCostSource,
11861
12070
  DEFAULT_ANOMALY_THRESHOLDS,
12071
+ DEFAULT_INGEST_QUOTA,
11862
12072
  DEFAULT_SERVER_NAME,
11863
12073
  DEFAULT_TENANT,
11864
12074
  DriftConfigSchema,
12075
+ GraphStoreBackend,
11865
12076
  INGEST_SCHEMA_VERSION,
11866
12077
  IngestEnvelopeSchema,
11867
12078
  InvalidTenantError,
@@ -11880,10 +12091,12 @@ function checkClaudePrerequisites() {
11880
12091
  ProviderRegistry,
11881
12092
  RELATION_TO_DIRECTION,
11882
12093
  ROLES,
12094
+ RateLimiter,
11883
12095
  RoleSchema,
11884
12096
  RuleCheckSchema,
11885
12097
  RulesetSchema,
11886
12098
  SCAN_ARG_PATTERNS,
12099
+ SCHEMA_VERSION,
11887
12100
  SDL,
11888
12101
  SEVERITY_WEIGHT,
11889
12102
  SHARING_LEVELS,
@@ -12031,6 +12244,7 @@ function checkClaudePrerequisites() {
12031
12244
  nodesToAssets,
12032
12245
  normalizeId,
12033
12246
  normalizeTenant,
12247
+ openStoreBackend,
12034
12248
  orgKeyPath,
12035
12249
  osUser,
12036
12250
  parseApiArgs,