@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.js CHANGED
@@ -2562,6 +2562,7 @@ function newAnomalies(base, current) {
2562
2562
 
2563
2563
  // src/db.ts
2564
2564
  var DEFAULT_TENANT = "local";
2565
+ var SCHEMA_VERSION = 15;
2565
2566
  function normalizeTenant(raw) {
2566
2567
  if (raw == null) return DEFAULT_TENANT;
2567
2568
  const cleaned = sanitizeUntrusted(String(raw)).trim().slice(0, 128);
@@ -3966,6 +3967,14 @@ var CartographyDB = class {
3966
3967
  }
3967
3968
  return rows.length;
3968
3969
  }
3970
+ /**
3971
+ * Retention/compaction (4.7): delete audit events older than `olderThan` (ISO 8601).
3972
+ * The audit trail grows unbounded on a busy collector; this bounds it without touching
3973
+ * sessions/nodes/edges. Returns the number of events removed.
3974
+ */
3975
+ pruneEventsOlderThan(olderThan) {
3976
+ return this.db.prepare("DELETE FROM activity_events WHERE timestamp < ?").run(olderThan).changes;
3977
+ }
3969
3978
  // ── Graph queries (read-only context layer) ─────────────────────────────────
3970
3979
  /** Fetch a single node by id within a session. */
3971
3980
  getNode(sessionId, nodeId) {
@@ -4353,6 +4362,148 @@ var SqliteStoreBackend = class {
4353
4362
  }
4354
4363
  };
4355
4364
 
4365
+ // src/store/graph.ts
4366
+ function toNum(v) {
4367
+ if (typeof v === "number") return v;
4368
+ if (v && typeof v === "object" && "toNumber" in v && typeof v.toNumber === "function") {
4369
+ return v.toNumber();
4370
+ }
4371
+ return Number(v ?? 0);
4372
+ }
4373
+ var GraphStoreBackend = class {
4374
+ constructor(driver) {
4375
+ this.driver = driver;
4376
+ }
4377
+ async run(cypher, params) {
4378
+ const session = this.driver.session();
4379
+ try {
4380
+ return await session.run(cypher, params);
4381
+ } finally {
4382
+ await session.close();
4383
+ }
4384
+ }
4385
+ async upsertNode(org, node, identity, contributor) {
4386
+ const res = await this.run(
4387
+ `MERGE (n:Node {org: $org, globalId: $globalId})
4388
+ ON CREATE SET n._created = true
4389
+ ON MATCH SET n._created = false
4390
+ SET n.id = $id, n.contentHash = $contentHash, n.type = $type, n.name = $name,
4391
+ n.domain = $domain, n.owner = $owner,
4392
+ n.confidence = CASE WHEN n.confidence IS NULL OR $confidence > n.confidence THEN $confidence ELSE n.confidence END
4393
+ MERGE (c:Contributor {org: $org, globalId: $globalId, machineId: $machineId})
4394
+ SET c.hostname = $hostname, c.user = $user, c.at = $at,
4395
+ c.confidence = CASE WHEN c.confidence IS NULL OR $contribConfidence > c.confidence THEN $contribConfidence ELSE c.confidence END
4396
+ RETURN n._created AS created`,
4397
+ {
4398
+ org,
4399
+ globalId: identity.globalId,
4400
+ contentHash: identity.contentHash,
4401
+ id: node.id,
4402
+ type: node.type,
4403
+ name: node.name,
4404
+ domain: node.domain ?? null,
4405
+ owner: node.owner ?? null,
4406
+ confidence: node.confidence,
4407
+ machineId: contributor.machineId,
4408
+ hostname: contributor.hostname,
4409
+ user: contributor.user,
4410
+ at: contributor.at,
4411
+ contribConfidence: contributor.confidence
4412
+ }
4413
+ );
4414
+ return res.records[0]?.get("created") === true ? "created" : "merged";
4415
+ }
4416
+ async insertEdge(org, edge) {
4417
+ await this.run(
4418
+ `MATCH (s:Node {org: $org, id: $source})
4419
+ MATCH (t:Node {org: $org, id: $target})
4420
+ MERGE (s)-[r:DEPENDS {relationship: $rel}]->(t)
4421
+ SET r.evidence = $evidence, r.confidence = $confidence`,
4422
+ { org, source: edge.sourceId, target: edge.targetId, rel: edge.relationship, evidence: edge.evidence, confidence: edge.confidence }
4423
+ );
4424
+ }
4425
+ async getSummary(org) {
4426
+ const totals = await this.run(
4427
+ `MATCH (n:Node {org: $org})
4428
+ OPTIONAL MATCH (n)-[r:DEPENDS]->(:Node {org: $org})
4429
+ RETURN count(DISTINCT n) AS nodes, count(r) AS edges`,
4430
+ { org }
4431
+ );
4432
+ const byType = await this.run(`MATCH (n:Node {org: $org}) RETURN n.type AS k, count(*) AS c`, { org });
4433
+ const byDomain = await this.run(`MATCH (n:Node {org: $org}) RETURN coalesce(n.domain, '(none)') AS k, count(*) AS c`, { org });
4434
+ const byRel = await this.run(`MATCH (:Node {org: $org})-[r:DEPENDS]->(:Node {org: $org}) RETURN r.relationship AS k, count(*) AS c`, { org });
4435
+ const top = await this.run(
4436
+ `MATCH (n:Node {org: $org})
4437
+ OPTIONAL MATCH (n)-[r:DEPENDS]-(:Node {org: $org})
4438
+ RETURN n.id AS id, n.name AS name, n.type AS type, count(r) AS degree
4439
+ ORDER BY degree DESC, id ASC LIMIT 10`,
4440
+ { org }
4441
+ );
4442
+ const contrib = await this.run(`MATCH (c:Contributor {org: $org}) RETURN count(DISTINCT c.machineId) AS contributors`, { org });
4443
+ const counts = (r) => {
4444
+ const out = {};
4445
+ for (const rec of r.records) out[String(rec.get("k"))] = toNum(rec.get("c"));
4446
+ return out;
4447
+ };
4448
+ return {
4449
+ org,
4450
+ totals: { nodes: toNum(totals.records[0]?.get("nodes")), edges: toNum(totals.records[0]?.get("edges")) },
4451
+ nodesByType: counts(byType),
4452
+ nodesByDomain: counts(byDomain),
4453
+ edgesByRelationship: counts(byRel),
4454
+ topConnected: top.records.map((rec) => ({
4455
+ id: String(rec.get("id")),
4456
+ name: String(rec.get("name")),
4457
+ type: String(rec.get("type")),
4458
+ degree: toNum(rec.get("degree"))
4459
+ })),
4460
+ contributors: toNum(contrib.records[0]?.get("contributors"))
4461
+ };
4462
+ }
4463
+ async getContributors(globalId2) {
4464
+ const res = await this.run(
4465
+ `MATCH (c:Contributor {globalId: $globalId})
4466
+ 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`,
4467
+ { globalId: globalId2 }
4468
+ );
4469
+ return res.records.map((rec) => ({
4470
+ machineId: String(rec.get("machineId")),
4471
+ hostname: String(rec.get("hostname")),
4472
+ user: String(rec.get("user")),
4473
+ organization: rec.get("org") != null ? String(rec.get("org")) : void 0,
4474
+ at: String(rec.get("at")),
4475
+ confidence: toNum(rec.get("confidence"))
4476
+ }));
4477
+ }
4478
+ async close() {
4479
+ await this.driver.close();
4480
+ }
4481
+ };
4482
+
4483
+ // src/store/index.ts
4484
+ async function defaultNeo4jDriver(url, user, password) {
4485
+ const mod = await import("neo4j-driver");
4486
+ return mod.default.driver(url, mod.default.auth.basic(user, password));
4487
+ }
4488
+ async function openStoreBackend(db, opts = {}) {
4489
+ if (opts.backend === "graph" && opts.graphUrl) {
4490
+ try {
4491
+ const make = opts.driverFactory ?? defaultNeo4jDriver;
4492
+ const driver = await make(opts.graphUrl, opts.graphUser ?? "neo4j", opts.graphPassword ?? "");
4493
+ if (driver.verifyConnectivity) await driver.verifyConnectivity();
4494
+ logInfo("central store: graph backend active", { host: stripSensitive(opts.graphUrl) });
4495
+ return new GraphStoreBackend(driver);
4496
+ } catch (err) {
4497
+ logWarn("central store: graph backend unavailable \u2014 falling back to SQLite", {
4498
+ host: stripSensitive(opts.graphUrl),
4499
+ reason: err instanceof Error ? err.message : String(err)
4500
+ });
4501
+ return new SqliteStoreBackend(db);
4502
+ }
4503
+ }
4504
+ return new SqliteStoreBackend(db);
4505
+ }
4506
+
4356
4507
  // src/store/query.ts
4357
4508
  var NotFoundError = class extends Error {
4358
4509
  constructor(message) {
@@ -4657,7 +4808,7 @@ var ContributorSchema = z5.object({
4657
4808
  });
4658
4809
  var IngestEnvelopeSchema = z5.object({
4659
4810
  schemaVersion: z5.literal(INGEST_SCHEMA_VERSION),
4660
- org: z5.string().min(1).optional(),
4811
+ org: z5.string().min(1).max(128).optional(),
4661
4812
  items: z5.array(z5.object({
4662
4813
  contentHash: z5.string(),
4663
4814
  kind: z5.enum(["node", "edge"]),
@@ -4667,9 +4818,9 @@ var IngestEnvelopeSchema = z5.object({
4667
4818
  contributor: ContributorSchema.optional(),
4668
4819
  anonymizationLevel: z5.enum(["none", "anonymized", "full"]).optional()
4669
4820
  });
4670
- function ingestEnvelope(store, envelope, opts = {}) {
4821
+ async function ingestEnvelope(store, envelope, opts = {}) {
4671
4822
  const anonMode = opts.anonMode ?? "reject";
4672
- const org = envelope.org ?? opts.defaultOrg ?? "local";
4823
+ const org = normalizeTenant(envelope.org ?? opts.defaultOrg);
4673
4824
  const level = envelope.anonymizationLevel ?? "anonymized";
4674
4825
  const at = (/* @__PURE__ */ new Date()).toISOString();
4675
4826
  const contributor = {
@@ -4720,7 +4871,7 @@ function ingestEnvelope(store, envelope, opts = {}) {
4720
4871
  }
4721
4872
  const safe = check.node;
4722
4873
  const identity = computeIdentity(org, safe);
4723
- const outcome = store.upsertNode(org, safe, identity, { ...contributor, confidence: safe.confidence });
4874
+ const outcome = await store.upsertNode(org, safe, identity, { ...contributor, confidence: safe.confidence });
4724
4875
  accepted += 1;
4725
4876
  if (outcome === "merged") merged += 1;
4726
4877
  acceptedNodeIds.add(safe.id);
@@ -4736,7 +4887,7 @@ function ingestEnvelope(store, envelope, opts = {}) {
4736
4887
  if (acceptedNodeIds.size > 0 && (!acceptedNodeIds.has(edge.sourceId) || !acceptedNodeIds.has(edge.targetId))) {
4737
4888
  continue;
4738
4889
  }
4739
- store.insertEdge(org, edge);
4890
+ await store.insertEdge(org, edge);
4740
4891
  edges += 1;
4741
4892
  }
4742
4893
  logInfo("ingest", { org, accepted, merged, rejected, edges, violations, level, anonMode });
@@ -4745,15 +4896,24 @@ function ingestEnvelope(store, envelope, opts = {}) {
4745
4896
 
4746
4897
  // src/central/server.ts
4747
4898
  function createIngestHandler(store, opts = {}) {
4748
- return (body) => {
4899
+ const quota = opts.quota;
4900
+ return async (body) => {
4749
4901
  const parsed = IngestEnvelopeSchema.safeParse(body);
4750
4902
  if (!parsed.success) {
4751
4903
  const issues = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`);
4752
4904
  logWarn("ingest: rejected invalid envelope", { issues });
4753
4905
  return { status: 400, body: { error: "invalid envelope", issues } };
4754
4906
  }
4907
+ if (quota) {
4908
+ const org = normalizeTenant(parsed.data.org ?? opts.defaultOrg);
4909
+ const decision = quota.take(org);
4910
+ if (!decision.allowed) {
4911
+ logWarn("ingest: rate limited", { org, retryAfterSec: decision.retryAfterSec });
4912
+ return { status: 429, body: { error: "too many requests" }, headers: { "retry-after": String(decision.retryAfterSec) } };
4913
+ }
4914
+ }
4755
4915
  try {
4756
- const result = ingestEnvelope(store, parsed.data, opts);
4916
+ const result = await ingestEnvelope(store, parsed.data, opts);
4757
4917
  return { status: 200, body: result };
4758
4918
  } catch (err) {
4759
4919
  logWarn("ingest: failed", { error: err instanceof Error ? err.message : String(err) });
@@ -4762,6 +4922,39 @@ function createIngestHandler(store, opts = {}) {
4762
4922
  };
4763
4923
  }
4764
4924
 
4925
+ // src/central/quota.ts
4926
+ var DEFAULT_INGEST_QUOTA = { capacity: 120, refillMs: 6e4 };
4927
+ var MAX_KEYS = 1e4;
4928
+ var RateLimiter = class {
4929
+ constructor(cfg = DEFAULT_INGEST_QUOTA, now = () => Date.now()) {
4930
+ this.cfg = cfg;
4931
+ this.now = now;
4932
+ }
4933
+ buckets = /* @__PURE__ */ new Map();
4934
+ /** Consume one token for `key`. Returns whether the request is allowed (+ Retry-After when not). */
4935
+ take(key) {
4936
+ const t = this.now();
4937
+ const ratePerMs = this.cfg.capacity / this.cfg.refillMs;
4938
+ let b = this.buckets.get(key);
4939
+ if (!b) {
4940
+ if (this.buckets.size >= MAX_KEYS) {
4941
+ const oldest = this.buckets.keys().next().value;
4942
+ if (oldest !== void 0) this.buckets.delete(oldest);
4943
+ }
4944
+ b = { tokens: this.cfg.capacity, last: t };
4945
+ this.buckets.set(key, b);
4946
+ }
4947
+ b.tokens = Math.min(this.cfg.capacity, b.tokens + Math.max(0, t - b.last) * ratePerMs);
4948
+ b.last = t;
4949
+ if (b.tokens >= 1) {
4950
+ b.tokens -= 1;
4951
+ return { allowed: true, retryAfterSec: 0 };
4952
+ }
4953
+ const waitMs = (1 - b.tokens) / ratePerMs;
4954
+ return { allowed: false, retryAfterSec: Math.max(1, Math.ceil(waitMs / 1e3)) };
4955
+ }
4956
+ };
4957
+
4765
4958
  // src/scanners/bookmarks.ts
4766
4959
  var PERSONAL = [
4767
4960
  "facebook.",
@@ -5649,7 +5842,7 @@ async function resolveNlQuery(db, sessionId, search, raw, opts) {
5649
5842
 
5650
5843
  // src/mcp/server.ts
5651
5844
  var SERVER_NAME = "cartography";
5652
- var SERVER_VERSION = "2.6.0";
5845
+ var SERVER_VERSION = "2.8.0";
5653
5846
  var SERVICE_TYPES = NODE_TYPE_GROUPS.web;
5654
5847
  var DATA_TYPES = NODE_TYPE_GROUPS.data;
5655
5848
  var lexicalSearch = async (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
@@ -5724,9 +5917,10 @@ function createMcpServer(opts = {}) {
5724
5917
  "graph-summary",
5725
5918
  "cartography://graph/summary",
5726
5919
  { title: "Topology summary", description: "Low-token aggregate index of the whole landscape \u2014 read this first.", mimeType: "text/markdown" },
5727
- (uri) => {
5920
+ async (uri) => {
5728
5921
  if (org !== void 0) {
5729
- return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: summaryText(db.getOrgSummary(org)) }] };
5922
+ const s = opts.orgSummary ? await opts.orgSummary(org) : db.getOrgSummary(org);
5923
+ return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: summaryText(s) }] };
5730
5924
  }
5731
5925
  const sid = resolveSession();
5732
5926
  if (!sid) return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: "No discovery session found. Run discovery first." }] };
@@ -5801,8 +5995,8 @@ function createMcpServer(opts = {}) {
5801
5995
  server.registerTool(
5802
5996
  "get_summary",
5803
5997
  { title: "Get topology summary", description: "Low-token overview of the whole landscape (counts, types, domains, most-connected, anomalies).", inputSchema: {}, annotations: readOnly },
5804
- () => {
5805
- if (org !== void 0) return json(db.getOrgSummary(org));
5998
+ async () => {
5999
+ if (org !== void 0) return json(opts.orgSummary ? await opts.orgSummary(org) : db.getOrgSummary(org));
5806
6000
  const sid = resolveSession();
5807
6001
  if (!sid) return json({ error: "No discovery session found." });
5808
6002
  return json(db.getGraphSummary(sid));
@@ -6292,6 +6486,16 @@ async function runHttp(factory, opts = {}) {
6292
6486
  const httpServer = http.createServer(async (req, res) => {
6293
6487
  try {
6294
6488
  const url = req.url ?? "";
6489
+ const probePath = new URL(url || "/", "http://probe").pathname;
6490
+ if (probePath === "/healthz") {
6491
+ res.writeHead(200, { "content-type": "application/json" }).end('{"status":"ok"}');
6492
+ return;
6493
+ }
6494
+ if (probePath === "/readyz") {
6495
+ const r = opts.readiness ? opts.readiness() : { ready: true };
6496
+ res.writeHead(r.ready ? 200 : 503, { "content-type": "application/json" }).end(JSON.stringify({ status: r.ready ? "ready" : "unready" }));
6497
+ return;
6498
+ }
6295
6499
  const isIngest = url.startsWith("/ingest") && opts.onIngest !== void 0;
6296
6500
  if (!url.startsWith("/mcp") && !isIngest) {
6297
6501
  res.writeHead(404, { "content-type": "application/json" }).end('{"error":"not found"}');
@@ -6318,8 +6522,8 @@ async function runHttp(factory, opts = {}) {
6318
6522
  res.writeHead(413, { "content-type": "application/json" }).end('{"error":"payload too large"}');
6319
6523
  return;
6320
6524
  }
6321
- const out = onIngest(value);
6322
- res.writeHead(out.status, { "content-type": "application/json" }).end(JSON.stringify(out.body));
6525
+ const out = await onIngest(value);
6526
+ res.writeHead(out.status, { "content-type": "application/json", ...out.headers ?? {} }).end(JSON.stringify(out.body));
6323
6527
  return;
6324
6528
  }
6325
6529
  const sessionId = req.headers["mcp-session-id"];
@@ -11569,9 +11773,11 @@ export {
11569
11773
  CredentialConfigSchema,
11570
11774
  CsvCostSource,
11571
11775
  DEFAULT_ANOMALY_THRESHOLDS,
11776
+ DEFAULT_INGEST_QUOTA,
11572
11777
  DEFAULT_SERVER_NAME,
11573
11778
  DEFAULT_TENANT,
11574
11779
  DriftConfigSchema,
11780
+ GraphStoreBackend,
11575
11781
  INGEST_SCHEMA_VERSION,
11576
11782
  IngestEnvelopeSchema,
11577
11783
  InvalidTenantError,
@@ -11590,10 +11796,12 @@ export {
11590
11796
  ProviderRegistry,
11591
11797
  RELATION_TO_DIRECTION,
11592
11798
  ROLES,
11799
+ RateLimiter,
11593
11800
  RoleSchema,
11594
11801
  RuleCheckSchema,
11595
11802
  RulesetSchema,
11596
11803
  SCAN_ARG_PATTERNS,
11804
+ SCHEMA_VERSION,
11597
11805
  SDL,
11598
11806
  SEVERITY_WEIGHT,
11599
11807
  SHARING_LEVELS,
@@ -11741,6 +11949,7 @@ export {
11741
11949
  nodesToAssets,
11742
11950
  normalizeId,
11743
11951
  normalizeTenant,
11952
+ openStoreBackend,
11744
11953
  orgKeyPath,
11745
11954
  osUser,
11746
11955
  parseApiArgs,