@datasynx/agentic-ai-cartography 2.0.0 → 2.2.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.
Files changed (60) hide show
  1. package/AGENTS.md +32 -0
  2. package/README.md +115 -6
  3. package/dist/{bookmarks-VS56KVCO.js → bookmarks-WXHE7GN7.js} +6 -3
  4. package/dist/{chunk-CJ2PITFA.js → chunk-2SZ5QHGH.js} +71 -9
  5. package/dist/chunk-2SZ5QHGH.js.map +1 -0
  6. package/dist/chunk-BNDCY2RI.js +5672 -0
  7. package/dist/chunk-BNDCY2RI.js.map +1 -0
  8. package/dist/chunk-WCR47QA2.js +277 -0
  9. package/dist/chunk-WCR47QA2.js.map +1 -0
  10. package/dist/cli.js +2346 -667
  11. package/dist/cli.js.map +1 -1
  12. package/dist/index.cjs +8406 -58089
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +2766 -68
  15. package/dist/index.d.ts +2766 -68
  16. package/dist/index.js +7977 -2587
  17. package/dist/index.js.map +1 -1
  18. package/dist/mcp-bin.js +16 -26
  19. package/dist/mcp-bin.js.map +1 -1
  20. package/dist/types-TJWXAQ2L.js +66 -0
  21. package/llms-full.txt +758 -0
  22. package/llms.txt +24 -0
  23. package/package.json +23 -8
  24. package/scripts/build-llms.mjs +89 -0
  25. package/scripts/build-mcpb.mjs +31 -0
  26. package/scripts/gen-docs.ts +123 -0
  27. package/scripts/validate-server-json.mjs +54 -0
  28. package/server.json +4 -4
  29. package/dist/chunk-CJ2PITFA.js.map +0 -1
  30. package/dist/chunk-D6SRSLBF.js +0 -48
  31. package/dist/chunk-J6FDZ6HZ.js +0 -142
  32. package/dist/chunk-J6FDZ6HZ.js.map +0 -1
  33. package/dist/chunk-UGSNG3QJ.js +0 -49
  34. package/dist/chunk-UGSNG3QJ.js.map +0 -1
  35. package/dist/chunk-W7YE6AAH.js +0 -1516
  36. package/dist/chunk-W7YE6AAH.js.map +0 -1
  37. package/dist/onnxruntime_binding-6Q6HXASN.node +0 -0
  38. package/dist/onnxruntime_binding-EKZT2NRK.node +0 -0
  39. package/dist/onnxruntime_binding-P6S7V3CI.node +0 -0
  40. package/dist/onnxruntime_binding-PJNNIIUO.node +0 -0
  41. package/dist/onnxruntime_binding-UN6SPTQK.node +0 -0
  42. package/dist/sdk-A6NLO3DJ.js +0 -12294
  43. package/dist/sdk-A6NLO3DJ.js.map +0 -1
  44. package/dist/sdk-G5D4WQZ4.js +0 -12293
  45. package/dist/sdk-G5D4WQZ4.js.map +0 -1
  46. package/dist/sdk-QSTAREST.js +0 -4869
  47. package/dist/sdk-QSTAREST.js.map +0 -1
  48. package/dist/sqlite-vec-EZN67B2V.js +0 -40
  49. package/dist/sqlite-vec-EZN67B2V.js.map +0 -1
  50. package/dist/sqlite-vec-UK5YYE5T.js +0 -39
  51. package/dist/sqlite-vec-UK5YYE5T.js.map +0 -1
  52. package/dist/transformers.node-BTYUTJK5.js +0 -42884
  53. package/dist/transformers.node-BTYUTJK5.js.map +0 -1
  54. package/dist/transformers.node-J6PRTTOX.js +0 -42883
  55. package/dist/transformers.node-J6PRTTOX.js.map +0 -1
  56. package/dist/types-JG27FR3E.js +0 -29
  57. package/dist/types-JG27FR3E.js.map +0 -1
  58. package/scripts/postinstall.mjs +0 -7
  59. /package/dist/{bookmarks-VS56KVCO.js.map → bookmarks-WXHE7GN7.js.map} +0 -0
  60. /package/dist/{chunk-D6SRSLBF.js.map → types-TJWXAQ2L.js.map} +0 -0
package/dist/index.d.ts CHANGED
@@ -4,6 +4,48 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
4
  import http from 'node:http';
5
5
  import { McpServerConfig, HookCallback } from '@anthropic-ai/claude-agent-sdk';
6
6
 
7
+ /**
8
+ * Topology diffing — pure, deterministic comparison of two discovery snapshots.
9
+ *
10
+ * This module knows nothing about the database; it operates on plain
11
+ * node/edge arrays so it can be unit-tested in isolation and reused by the
12
+ * CLI, the MCP server, and the exporter. Drift is detected on a stable
13
+ * projection of node fields (see DRIFT_FIELDS); `confidence` is reported but
14
+ * never on its own marks a node as changed.
15
+ */
16
+
17
+ /** Deterministic JSON serialization with recursively sorted object keys. */
18
+ declare function stableStringify(value: unknown): string;
19
+ interface TopologyInput {
20
+ nodes: NodeRow[];
21
+ edges: EdgeRow[];
22
+ }
23
+ interface TopologyDelta {
24
+ nodes: {
25
+ added: NodeRow[];
26
+ removed: NodeRow[];
27
+ changed: NodeChange[];
28
+ unchanged: number;
29
+ };
30
+ edges: {
31
+ added: EdgeRow[];
32
+ removed: EdgeRow[];
33
+ unchanged: number;
34
+ };
35
+ summary: {
36
+ nodesAdded: number;
37
+ nodesRemoved: number;
38
+ nodesChanged: number;
39
+ edgesAdded: number;
40
+ edgesRemoved: number;
41
+ };
42
+ }
43
+ /**
44
+ * Compute the delta from `base` to `current`. Nodes are keyed by `id`, edges by
45
+ * (source, target, relationship). Pure: same inputs always yield the same output.
46
+ */
47
+ declare function diffTopology(base: TopologyInput, current: TopologyInput): TopologyDelta;
48
+
7
49
  declare const NODE_TYPES: readonly ["host", "database_server", "database", "table", "web_service", "api_endpoint", "cache_server", "message_broker", "queue", "topic", "container", "pod", "k8s_cluster", "config_file", "saas_tool", "unknown"];
8
50
  type NodeType = typeof NODE_TYPES[number];
9
51
  /**
@@ -21,6 +63,22 @@ declare const NODE_TYPE_GROUPS: {
21
63
  };
22
64
  declare const EDGE_RELATIONSHIPS: readonly ["connects_to", "reads_from", "writes_to", "calls", "contains", "depends_on"];
23
65
  type EdgeRelationship = typeof EDGE_RELATIONSHIPS[number];
66
+ /** Billing period the amount covers. Cross-period rollup is bucketed, never normalized (5.1). */
67
+ declare const COST_PERIODS: readonly ["hourly", "daily", "monthly", "yearly"];
68
+ type CostPeriod = typeof COST_PERIODS[number];
69
+ /** Attributed cost for one billing period (3.3). Amount is in `currency` per `period`. */
70
+ declare const CostEntrySchema: z.ZodObject<{
71
+ amount: z.ZodNumber;
72
+ currency: z.ZodString;
73
+ period: z.ZodEnum<{
74
+ hourly: "hourly";
75
+ daily: "daily";
76
+ monthly: "monthly";
77
+ yearly: "yearly";
78
+ }>;
79
+ source: z.ZodOptional<z.ZodString>;
80
+ }, z.core.$strip>;
81
+ type CostEntry = z.infer<typeof CostEntrySchema>;
24
82
  declare const NodeSchema: z.ZodObject<{
25
83
  id: z.ZodString;
26
84
  type: z.ZodEnum<{
@@ -49,6 +107,18 @@ declare const NodeSchema: z.ZodObject<{
49
107
  domain: z.ZodOptional<z.ZodString>;
50
108
  subDomain: z.ZodOptional<z.ZodString>;
51
109
  qualityScore: z.ZodOptional<z.ZodNumber>;
110
+ owner: z.ZodOptional<z.ZodString>;
111
+ cost: z.ZodOptional<z.ZodObject<{
112
+ amount: z.ZodNumber;
113
+ currency: z.ZodString;
114
+ period: z.ZodEnum<{
115
+ hourly: "hourly";
116
+ daily: "daily";
117
+ monthly: "monthly";
118
+ yearly: "yearly";
119
+ }>;
120
+ source: z.ZodOptional<z.ZodString>;
121
+ }, z.core.$strip>>;
52
122
  }, z.core.$strip>;
53
123
  type DiscoveryNode = z.infer<typeof NodeSchema>;
54
124
  declare const EdgeSchema: z.ZodObject<{
@@ -66,6 +136,32 @@ declare const EdgeSchema: z.ZodObject<{
66
136
  confidence: z.ZodDefault<z.ZodNumber>;
67
137
  }, z.core.$strip>;
68
138
  type DiscoveryEdge = z.infer<typeof EdgeSchema>;
139
+ /**
140
+ * Per-employee sharing levels, ordered most-private → least-private:
141
+ * - `none` — share nothing (the opt-in default; nothing leaves the machine).
142
+ * - `anonymized` — pseudonymize identifying fields (host/user/path/private-IP) via
143
+ * an org-keyed, admin-reversible HMAC while preserving topology shape.
144
+ * - `full` — share the raw record verbatim.
145
+ */
146
+ declare const SHARING_LEVELS: readonly ["none", "anonymized", "full"];
147
+ declare const SharingLevelSchema: z.ZodEnum<{
148
+ none: "none";
149
+ anonymized: "anonymized";
150
+ full: "full";
151
+ }>;
152
+ type SharingLevel = typeof SHARING_LEVELS[number];
153
+ /**
154
+ * A resolved sharing policy: the global `defaultLevel` (the `'*'` row) plus
155
+ * remembered pattern overrides (glob over the node id). The most-specific
156
+ * matching override wins; the default applies when nothing matches.
157
+ */
158
+ interface SharingPolicy {
159
+ defaultLevel: SharingLevel;
160
+ overrides: {
161
+ pattern: string;
162
+ level: SharingLevel;
163
+ }[];
164
+ }
69
165
  declare const DataAssetSchema: z.ZodObject<{
70
166
  id: z.ZodString;
71
167
  name: z.ZodString;
@@ -116,6 +212,13 @@ interface NodeRow extends DiscoveryNode {
116
212
  discoveredAt: string;
117
213
  depth: number;
118
214
  pathId?: string;
215
+ /**
216
+ * Org-scoped human-readable global identity (`{tenant}:{normalizedId}`); the
217
+ * same logical resource collapses to one `globalId` across machines (2.9).
218
+ */
219
+ globalId?: string;
220
+ /** Secondary dedup key (sha256 over type + name + key-meta) that catches `id` drift between machines (2.9). */
221
+ contentHash?: string;
119
222
  }
120
223
  interface EdgeRow extends DiscoveryEdge {
121
224
  id: string;
@@ -129,19 +232,681 @@ interface SessionRow {
129
232
  startedAt: string;
130
233
  completedAt?: string;
131
234
  config: string;
235
+ /** Human-friendly, deterministically-derived label (e.g. "infra+data · 42 nodes · 2026-06-11"). */
236
+ name?: string;
237
+ /** Tenant/organization partition this session belongs to. Defaults to `'local'`. */
238
+ tenant: string;
239
+ /**
240
+ * Source attribution captured at session creation (2.9). Local-only — these
241
+ * identifying fields never leave the machine; off-machine sharing (2.11) and
242
+ * anonymization/consent (2.10) are deferred.
243
+ */
244
+ hostname?: string;
245
+ user?: string;
246
+ machineId?: string;
247
+ /**
248
+ * Raw `--org` / `config.organization` value as supplied (provenance). The
249
+ * normalized form is {@link tenant} — the org-scope partition introduced by 2.8.
250
+ */
251
+ organization?: string;
252
+ /**
253
+ * ISO 8601 UTC timestamp of the last in-place rescan of this session (2.1).
254
+ * `undefined`/NULL until the session is rescanned via incremental discovery —
255
+ * the freshness signal for scheduled discovery (2.5) to build on.
256
+ */
257
+ lastScannedAt?: string;
258
+ }
259
+ /**
260
+ * One observation of a logical node from a single machine. Accumulated in the
261
+ * `node_contributors` table (keyed by `(global_id, machine_id)`); never anonymized
262
+ * in 2.9 (that is 2.10) and never transmitted off-machine in 2.9 (that is 2.11).
263
+ */
264
+ interface Contributor {
265
+ machineId: string;
266
+ hostname: string;
267
+ user: string;
268
+ /** Effective org-scope of the contribution (the session's tenant). */
269
+ organization?: string;
270
+ /** ISO 8601 UTC timestamp of the contributing observation. */
271
+ at: string;
272
+ /** Confidence of the observation that produced this contribution (0–1). */
273
+ confidence: number;
274
+ }
275
+ /**
276
+ * Node fields whose change marks a node as `changed` in a topology diff.
277
+ * `confidence` is deliberately excluded — it fluctuates between scans (noise)
278
+ * and is reported separately as `confidenceDelta` rather than triggering drift.
279
+ */
280
+ declare const DRIFT_FIELDS: readonly ["type", "name", "domain", "subDomain", "qualityScore", "metadata", "tags", "owner", "cost"];
281
+ type DriftField = typeof DRIFT_FIELDS[number];
282
+ interface NodeChange {
283
+ id: string;
284
+ before: NodeRow;
285
+ after: NodeRow;
286
+ /** Which of DRIFT_FIELDS differ between `before` and `after`. */
287
+ changedFields: DriftField[];
288
+ /** Informational confidence delta (after − before); does not itself trigger drift. */
289
+ confidenceDelta: number;
290
+ }
291
+ declare const ANOMALY_KINDS: readonly ["orphan", "shadow-it"];
292
+ type AnomalyKind = typeof ANOMALY_KINDS[number];
293
+ declare const ANOMALY_SEVERITIES: readonly ["low", "medium", "high"];
294
+ type AnomalySeverity = typeof ANOMALY_SEVERITIES[number];
295
+ /** A standing structural anomaly within a single topology snapshot. Deterministic. */
296
+ interface Anomaly {
297
+ /** The flagged node, structured id "{type}:{id}" — never raw free-text. */
298
+ nodeId: string;
299
+ kind: AnomalyKind;
300
+ severity: AnomalySeverity;
301
+ /** Stable, human-readable explanation built only from nodeId + numeric scores. */
302
+ reason: string;
303
+ }
304
+ /** Resolved anomaly thresholds (defaults in `DEFAULT_ANOMALY_THRESHOLDS` unless overridden by config). */
305
+ interface AnomalyThresholds {
306
+ /** Degree at or below which a node is a weak-link orphan candidate (0 = isolated). */
307
+ orphanWeakDegree: number;
308
+ /** Confidence (0–1) below which an undomained node is shadow-IT. */
309
+ shadowConfidence: number;
310
+ /** qualityScore (0–100) below which an undomained node is shadow-IT. */
311
+ shadowQuality: number;
312
+ }
313
+ interface AnomalyConfig extends AnomalyThresholds {
314
+ /** When false, the engine short-circuits to an empty array (rollback flag). */
315
+ enabled: boolean;
316
+ }
317
+ /**
318
+ * Default anomaly thresholds. Defined here (not in `anomaly.ts`) so `defaultConfig`
319
+ * can reference them without a runtime cycle; `anomaly.ts` re-exports this constant.
320
+ */
321
+ declare const DEFAULT_ANOMALY_THRESHOLDS: AnomalyThresholds;
322
+ interface TopologyDiff {
323
+ base: {
324
+ sessionId: string;
325
+ startedAt: string;
326
+ nodeCount: number;
327
+ edgeCount: number;
328
+ };
329
+ current: {
330
+ sessionId: string;
331
+ startedAt: string;
332
+ nodeCount: number;
333
+ edgeCount: number;
334
+ };
335
+ nodes: {
336
+ added: NodeRow[];
337
+ removed: NodeRow[];
338
+ changed: NodeChange[];
339
+ unchanged: number;
340
+ };
341
+ edges: {
342
+ added: EdgeRow[];
343
+ removed: EdgeRow[];
344
+ unchanged: number;
345
+ };
346
+ summary: {
347
+ nodesAdded: number;
348
+ nodesRemoved: number;
349
+ nodesChanged: number;
350
+ edgesAdded: number;
351
+ edgesRemoved: number;
352
+ };
353
+ /** Standing anomalies in base vs current, plus those newly appearing in current (3.6). */
354
+ anomalies: {
355
+ base: Anomaly[];
356
+ current: Anomaly[];
357
+ added: Anomaly[];
358
+ };
359
+ }
360
+ /** Severity rank, ascending. `maxSeverity` and threshold filtering rely on this order. */
361
+ declare const SEVERITIES: readonly ["info", "warning", "critical"];
362
+ type Severity$1 = typeof SEVERITIES[number];
363
+ /**
364
+ * Free-form metadata keys (case-insensitive) whose change escalates a node-changed
365
+ * item to `critical`. Security-/exposure-relevant signals live only in the
366
+ * free-form `metadata` blob (there are no first-class security node fields).
367
+ */
368
+ declare const SECURITY_METADATA_KEYS: readonly ["publicexposure", "public", "exposed", "iamrole", "role", "encryption", "encrypted", "tls", "tlsenabled", "ports", "openports", "auth", "authentication"];
369
+ type DriftItemKind = 'node-added' | 'node-removed' | 'node-changed' | 'edge-added' | 'edge-removed';
370
+ interface DriftAlertItem {
371
+ kind: DriftItemKind;
372
+ /** Node id, or "source -rel-> target" for edges. */
373
+ ref: string;
374
+ /** Human-readable node/edge name for display. */
375
+ label: string;
376
+ nodeType?: NodeType;
377
+ severity: Severity$1;
378
+ /** Present for node-changed; subset of DRIFT_FIELDS that differ. */
379
+ changedFields?: DriftField[];
380
+ /** Present for node-changed; metadata keys that triggered escalation. */
381
+ securityFields?: string[];
382
+ }
383
+ interface DriftAlert {
384
+ base: TopologyDiff['base'];
385
+ current: TopologyDiff['current'];
386
+ summary: TopologyDiff['summary'];
387
+ /** Overall severity = max severity across items (info when no items). */
388
+ severity: Severity$1;
389
+ items: DriftAlertItem[];
390
+ /** ISO-8601 UTC generation time. */
391
+ generatedAt: string;
392
+ }
393
+ /** One configured drift sink. `url` is required when `type === 'webhook'`. */
394
+ interface DriftSinkConfig {
395
+ type: 'stdout' | 'webhook';
396
+ /** Required when type === 'webhook'. */
397
+ url?: string;
398
+ /** Optional bearer token; falls back to CARTOGRAPHY_DRIFT_TOKEN. */
399
+ token?: string;
400
+ timeoutMs?: number;
401
+ }
402
+ /**
403
+ * Opt-in drift-alerting block on {@link CartographyConfig}. Absent → the runner
404
+ * defaults to a single `stdout` sink at `minSeverity: 'info'` (everything stays
405
+ * local; no outbound traffic unless a `webhook` sink is explicitly configured).
406
+ */
407
+ interface DriftConfig {
408
+ /** Items below this severity are dropped before dispatch. Default 'info'. */
409
+ minSeverity: Severity$1;
410
+ sinks: DriftSinkConfig[];
411
+ }
412
+ /** Validate an externally-supplied drift block (CLI/env/future file loader). */
413
+ declare const DriftConfigSchema: z.ZodObject<{
414
+ minSeverity: z.ZodDefault<z.ZodEnum<{
415
+ info: "info";
416
+ warning: "warning";
417
+ critical: "critical";
418
+ }>>;
419
+ sinks: z.ZodDefault<z.ZodArray<z.ZodObject<{
420
+ type: z.ZodEnum<{
421
+ stdout: "stdout";
422
+ webhook: "webhook";
423
+ }>;
424
+ url: z.ZodOptional<z.ZodString>;
425
+ token: z.ZodOptional<z.ZodString>;
426
+ timeoutMs: z.ZodOptional<z.ZodNumber>;
427
+ }, z.core.$strip>>>;
428
+ }, z.core.$strip>;
429
+ /** Machine-readable result formats shared by `discover` (#67) and `schedule`. */
430
+ declare const OUTPUT_FORMATS: readonly ["text", "json", "stream-json"];
431
+ type OutputFormat = typeof OUTPUT_FORMATS[number];
432
+ /**
433
+ * A recurring-discovery schedule, read from a JSON config file. The `cron`
434
+ * string is a 5-field expression (min hour dom month dow) validated by
435
+ * `parseCron` in `schedule.ts`; the Zod schema only enforces non-emptiness so
436
+ * the config layer stays decoupled from the cron grammar.
437
+ */
438
+ declare const ScheduleConfigSchema: z.ZodObject<{
439
+ cron: z.ZodString;
440
+ entryPoints: z.ZodOptional<z.ZodArray<z.ZodString>>;
441
+ outputFormat: z.ZodDefault<z.ZodEnum<{
442
+ text: "text";
443
+ json: "json";
444
+ "stream-json": "stream-json";
445
+ }>>;
446
+ dbPath: z.ZodOptional<z.ZodString>;
447
+ }, z.core.$strict>;
448
+ type ScheduleConfig = z.infer<typeof ScheduleConfigSchema>;
449
+ /**
450
+ * Outbound central-DB connection (2.11). The first egress path Cartograph has:
451
+ * after a scan, consented, policy-transformed deltas are pushed to this ingest
452
+ * endpoint over bearer-auth HTTPS. Presence of `url` *is* the feature flag — when
453
+ * absent the entire sync pipeline short-circuits and nothing ever networks.
454
+ *
455
+ * `.strict()` so a typo'd key in `config.json` fails loudly. The `token` is an
456
+ * opaque secret (never logged, never serialized into a payload); `org` is forwarded
457
+ * as a header so the central side (2.12) can scope ingest by tenant.
458
+ */
459
+ declare const CentralDbConfigSchema: z.ZodObject<{
460
+ url: z.ZodString;
461
+ token: z.ZodString;
462
+ org: z.ZodOptional<z.ZodString>;
463
+ batchSize: z.ZodOptional<z.ZodNumber>;
464
+ }, z.core.$strict>;
465
+ type CentralDbConfig = z.infer<typeof CentralDbConfigSchema>;
466
+ /**
467
+ * Read a {@link CentralDbConfig} from environment variables
468
+ * (`CARTOGRAPHY_CENTRAL_URL`/`_TOKEN`/`_ORG`), letting CI / secret-managers inject
469
+ * the token without a file. Returns a partial — only the keys actually present —
470
+ * so it composes field-wise over a `config.json` block. Never throws.
471
+ */
472
+ declare function centralDbFromEnv(env?: NodeJS.ProcessEnv): Partial<CentralDbConfig>;
473
+ /**
474
+ * Lifecycle status of one queued share item (2.11):
475
+ * - `pending` — new/unmatched, awaiting the employee's explicit review.
476
+ * - `approved` — cleared to leave (by `sync review`, or auto by a remembered rule).
477
+ * - `shared` — successfully pushed to the central ingest endpoint.
478
+ * - `withheld` — explicitly suppressed; never leaves.
479
+ *
480
+ * The load-bearing privacy invariant: only `approved` rows are ever pushed.
481
+ */
482
+ declare const PENDING_STATUSES: readonly ["pending", "approved", "shared", "withheld"];
483
+ type PendingStatus = typeof PENDING_STATUSES[number];
484
+ /**
485
+ * One row of the `pending_shares` review queue (2.11). `payload` is the *already
486
+ * policy-transformed* (anonymized/dropped) projection from `previewShare` — never
487
+ * raw node data for `anonymized`/`none` items — so what is queued is exactly what
488
+ * may leave. Keyed by `contentHash` (a hash of that transformed payload).
489
+ */
490
+ interface PendingShareRow {
491
+ contentHash: string;
492
+ sessionId: string;
493
+ nodeId?: string;
494
+ kind: 'node' | 'edge';
495
+ /** Policy-transformed payload (the exact bytes a push would send). */
496
+ payload: unknown;
497
+ status: PendingStatus;
498
+ /** Who decided: `'user'` (interactive review) or `'rule'` (remembered policy). */
499
+ decidedBy?: 'user' | 'rule';
500
+ createdAt: string;
501
+ decidedAt?: string;
502
+ sharedAt?: string;
503
+ }
504
+ /**
505
+ * Top-level shape of a `cartography.config.json` file. `.strict()` rejects
506
+ * unknown keys so typos fail loudly rather than being silently ignored. WS 2.11
507
+ * (central-org sync) extends this same schema with a `centralDb` block.
508
+ */
509
+ declare const ConfigFileSchema: z.ZodObject<{
510
+ schedule: z.ZodOptional<z.ZodObject<{
511
+ cron: z.ZodString;
512
+ entryPoints: z.ZodOptional<z.ZodArray<z.ZodString>>;
513
+ outputFormat: z.ZodDefault<z.ZodEnum<{
514
+ text: "text";
515
+ json: "json";
516
+ "stream-json": "stream-json";
517
+ }>>;
518
+ dbPath: z.ZodOptional<z.ZodString>;
519
+ }, z.core.$strict>>;
520
+ entryPoints: z.ZodOptional<z.ZodArray<z.ZodString>>;
521
+ dbPath: z.ZodOptional<z.ZodString>;
522
+ organization: z.ZodOptional<z.ZodString>;
523
+ centralDb: z.ZodOptional<z.ZodObject<{
524
+ url: z.ZodString;
525
+ token: z.ZodString;
526
+ org: z.ZodOptional<z.ZodString>;
527
+ batchSize: z.ZodOptional<z.ZodNumber>;
528
+ }, z.core.$strict>>;
529
+ }, z.core.$strict>;
530
+ type ConfigFile = z.infer<typeof ConfigFileSchema>;
531
+ /**
532
+ * One persisted scheduled-discovery run (2.5). Records what changed between this
533
+ * run's session and the prior one, with the full {@link TopologyDelta} for audit
534
+ * and the summary counts for fast querying. `baseSessionId` is `undefined` on the
535
+ * very first run (no prior topology — everything is `added`).
536
+ */
537
+ interface DriftRunRow {
538
+ id: string;
539
+ sessionId: string;
540
+ baseSessionId?: string;
541
+ /** ISO 8601 UTC timestamp this run was recorded. */
542
+ ranAt: string;
543
+ summary: {
544
+ nodesAdded: number;
545
+ nodesRemoved: number;
546
+ nodesChanged: number;
547
+ edgesAdded: number;
548
+ edgesRemoved: number;
549
+ };
550
+ delta: TopologyDelta;
132
551
  }
552
+ /**
553
+ * Agent backend selectable via `--provider` / `CARTOGRAPHY_PROVIDER`. Defined here
554
+ * (in the dependency-free types module) and re-exported from `providers/types.ts`
555
+ * so `defaultConfig` can reference it without a runtime cycle.
556
+ */
557
+ type ProviderName = 'claude' | 'openai' | 'ollama';
133
558
  interface CartographyConfig {
134
559
  maxDepth: number;
135
560
  maxTurns: number;
136
561
  entryPoints: string[];
562
+ /** Agent backend. Defaults to `'claude'`; selected by `--provider` / `CARTOGRAPHY_PROVIDER`. */
563
+ provider: ProviderName;
564
+ /** Lead/discovery model. Back-compat alias for `models.lead` (kept in sync by defaultConfig). */
137
565
  agentModel: string;
566
+ /** Model roles: `lead` drives discovery, `fast` powers cheaper helper tasks (e.g. chat). */
567
+ models: {
568
+ lead: string;
569
+ fast: string;
570
+ };
138
571
  organization?: string;
139
572
  outputDir: string;
140
573
  dbPath: string;
141
574
  verbose: boolean;
575
+ /** Max characters of a single scan-tool response returned to the agent (guards the context window). */
576
+ maxToolResponseBytes: number;
577
+ /** Explicit allowlist of scanner plugin package names to load (opt-in / consent-first). Default `[]`. */
578
+ plugins: string[];
579
+ /**
580
+ * Optional recurring-discovery schedule (2.5), populated from a config file by
581
+ * `loadConfig`. `undefined` for every existing/CLI caller — additive only.
582
+ */
583
+ schedule?: ScheduleConfig;
584
+ /**
585
+ * Optional central-DB outbound sync target (2.11). `undefined` for every caller
586
+ * unless configured via `config.json` (`centralDb` block), the
587
+ * `CARTOGRAPHY_CENTRAL_*` env vars, or an explicit override. Absent = the sync
588
+ * pipeline is fully inert (no classify, no queue, no push).
589
+ */
590
+ centralDb?: CentralDbConfig;
591
+ /**
592
+ * Optional anomaly-detection thresholds (3.6). `undefined` for every existing
593
+ * caller — `defaultConfig` populates it from `DEFAULT_ANOMALY_THRESHOLDS`, and the
594
+ * engine falls back to those defaults when absent (optional-deps-degrade).
595
+ */
596
+ anomaly?: AnomalyConfig;
597
+ /**
598
+ * Optional drift-alerting block (3.1). `undefined` for every existing/CLI caller
599
+ * (additive only); when absent the drift runner defaults to a local `stdout` sink.
600
+ * No outbound traffic unless an operator configures a `webhook` sink.
601
+ */
602
+ drift?: DriftConfig;
142
603
  }
604
+ /** Default lead (discovery) model. */
605
+ declare const DEFAULT_LEAD_MODEL = "claude-sonnet-4-5-20250929";
606
+ /** Default fast model for helper tasks (chat, summaries). */
607
+ declare const DEFAULT_FAST_MODEL = "claude-haiku-4-5-20251001";
143
608
  declare function defaultConfig(overrides?: Partial<CartographyConfig>): CartographyConfig;
144
609
 
610
+ /**
611
+ * Compliance scoring (3.4) — schemas + types.
612
+ *
613
+ * Rulesets are **declarative data** (a serializable `RuleCheck` expression tree)
614
+ * interpreted by a single trusted engine — never executable predicate code. `field`
615
+ * and `pattern` are closed enums, so a ruleset can neither reach arbitrary node
616
+ * properties nor inject a ReDoS-prone regex. Every bundled ruleset is
617
+ * `RulesetSchema.parse`d at module load (fail-fast on malformed data).
618
+ */
619
+
620
+ /** Which nodes a rule applies to. Empty scope (no groups/types) = all nodes. */
621
+ declare const RuleScopeSchema: z.ZodObject<{
622
+ groups: z.ZodOptional<z.ZodArray<z.ZodEnum<{
623
+ [x: string]: string;
624
+ }>>>;
625
+ types: z.ZodOptional<z.ZodArray<z.ZodEnum<{
626
+ host: "host";
627
+ database_server: "database_server";
628
+ database: "database";
629
+ table: "table";
630
+ web_service: "web_service";
631
+ api_endpoint: "api_endpoint";
632
+ cache_server: "cache_server";
633
+ message_broker: "message_broker";
634
+ queue: "queue";
635
+ topic: "topic";
636
+ container: "container";
637
+ pod: "pod";
638
+ k8s_cluster: "k8s_cluster";
639
+ config_file: "config_file";
640
+ saas_tool: "saas_tool";
641
+ unknown: "unknown";
642
+ }>>>;
643
+ }, z.core.$strip>;
644
+ type RuleScope = z.infer<typeof RuleScopeSchema>;
645
+ declare const ConditionSchema: z.ZodObject<{
646
+ field: z.ZodEnum<{
647
+ type: "type";
648
+ name: "name";
649
+ domain: "domain";
650
+ subDomain: "subDomain";
651
+ qualityScore: "qualityScore";
652
+ tags: "tags";
653
+ owner: "owner";
654
+ confidence: "confidence";
655
+ metadataKeys: "metadataKeys";
656
+ metadataValues: "metadataValues";
657
+ }>;
658
+ op: z.ZodEnum<{
659
+ includes: "includes";
660
+ present: "present";
661
+ absent: "absent";
662
+ lt: "lt";
663
+ lte: "lte";
664
+ gt: "gt";
665
+ gte: "gte";
666
+ eq: "eq";
667
+ matches: "matches";
668
+ }>;
669
+ value: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
670
+ pattern: z.ZodOptional<z.ZodEnum<{
671
+ dsn_with_credentials: "dsn_with_credentials";
672
+ owner_key: "owner_key";
673
+ public_exposure: "public_exposure";
674
+ }>>;
675
+ }, z.core.$strip>;
676
+ type Condition = z.infer<typeof ConditionSchema>;
677
+ /** A serializable check: a leaf Condition or an all/any/not combinator. */
678
+ type RuleCheck = Condition | {
679
+ all: RuleCheck[];
680
+ } | {
681
+ any: RuleCheck[];
682
+ } | {
683
+ not: RuleCheck;
684
+ };
685
+ declare const RuleCheckSchema: z.ZodType<RuleCheck>;
686
+ declare const SeveritySchema: z.ZodEnum<{
687
+ low: "low";
688
+ medium: "medium";
689
+ high: "high";
690
+ critical: "critical";
691
+ }>;
692
+ type Severity = z.infer<typeof SeveritySchema>;
693
+ /** Severity → score weight (decision #5). */
694
+ declare const SEVERITY_WEIGHT: Record<Severity, number>;
695
+ declare const ComplianceRuleSchema: z.ZodObject<{
696
+ id: z.ZodString;
697
+ control: z.ZodString;
698
+ framework: z.ZodEnum<{
699
+ CIS: "CIS";
700
+ SOC2: "SOC2";
701
+ ISO27001: "ISO27001";
702
+ baseline: "baseline";
703
+ }>;
704
+ title: z.ZodString;
705
+ severity: z.ZodEnum<{
706
+ low: "low";
707
+ medium: "medium";
708
+ high: "high";
709
+ critical: "critical";
710
+ }>;
711
+ rationale: z.ZodString;
712
+ scope: z.ZodObject<{
713
+ groups: z.ZodOptional<z.ZodArray<z.ZodEnum<{
714
+ [x: string]: string;
715
+ }>>>;
716
+ types: z.ZodOptional<z.ZodArray<z.ZodEnum<{
717
+ host: "host";
718
+ database_server: "database_server";
719
+ database: "database";
720
+ table: "table";
721
+ web_service: "web_service";
722
+ api_endpoint: "api_endpoint";
723
+ cache_server: "cache_server";
724
+ message_broker: "message_broker";
725
+ queue: "queue";
726
+ topic: "topic";
727
+ container: "container";
728
+ pod: "pod";
729
+ k8s_cluster: "k8s_cluster";
730
+ config_file: "config_file";
731
+ saas_tool: "saas_tool";
732
+ unknown: "unknown";
733
+ }>>>;
734
+ }, z.core.$strip>;
735
+ applicableWhen: z.ZodOptional<z.ZodType<RuleCheck, unknown, z.core.$ZodTypeInternals<RuleCheck, unknown>>>;
736
+ check: z.ZodType<RuleCheck, unknown, z.core.$ZodTypeInternals<RuleCheck, unknown>>;
737
+ }, z.core.$strip>;
738
+ type ComplianceRule = z.infer<typeof ComplianceRuleSchema>;
739
+ declare const RulesetSchema: z.ZodObject<{
740
+ name: z.ZodString;
741
+ version: z.ZodString;
742
+ framework: z.ZodString;
743
+ description: z.ZodString;
744
+ rules: z.ZodArray<z.ZodObject<{
745
+ id: z.ZodString;
746
+ control: z.ZodString;
747
+ framework: z.ZodEnum<{
748
+ CIS: "CIS";
749
+ SOC2: "SOC2";
750
+ ISO27001: "ISO27001";
751
+ baseline: "baseline";
752
+ }>;
753
+ title: z.ZodString;
754
+ severity: z.ZodEnum<{
755
+ low: "low";
756
+ medium: "medium";
757
+ high: "high";
758
+ critical: "critical";
759
+ }>;
760
+ rationale: z.ZodString;
761
+ scope: z.ZodObject<{
762
+ groups: z.ZodOptional<z.ZodArray<z.ZodEnum<{
763
+ [x: string]: string;
764
+ }>>>;
765
+ types: z.ZodOptional<z.ZodArray<z.ZodEnum<{
766
+ host: "host";
767
+ database_server: "database_server";
768
+ database: "database";
769
+ table: "table";
770
+ web_service: "web_service";
771
+ api_endpoint: "api_endpoint";
772
+ cache_server: "cache_server";
773
+ message_broker: "message_broker";
774
+ queue: "queue";
775
+ topic: "topic";
776
+ container: "container";
777
+ pod: "pod";
778
+ k8s_cluster: "k8s_cluster";
779
+ config_file: "config_file";
780
+ saas_tool: "saas_tool";
781
+ unknown: "unknown";
782
+ }>>>;
783
+ }, z.core.$strip>;
784
+ applicableWhen: z.ZodOptional<z.ZodType<RuleCheck, unknown, z.core.$ZodTypeInternals<RuleCheck, unknown>>>;
785
+ check: z.ZodType<RuleCheck, unknown, z.core.$ZodTypeInternals<RuleCheck, unknown>>;
786
+ }, z.core.$strip>>;
787
+ }, z.core.$strip>;
788
+ type Ruleset = z.infer<typeof RulesetSchema>;
789
+ declare const ControlResultSchema: z.ZodObject<{
790
+ ruleId: z.ZodString;
791
+ control: z.ZodString;
792
+ framework: z.ZodString;
793
+ severity: z.ZodEnum<{
794
+ low: "low";
795
+ medium: "medium";
796
+ high: "high";
797
+ critical: "critical";
798
+ }>;
799
+ status: z.ZodEnum<{
800
+ pass: "pass";
801
+ fail: "fail";
802
+ not_applicable: "not_applicable";
803
+ }>;
804
+ applicableCount: z.ZodNumber;
805
+ passedCount: z.ZodNumber;
806
+ failingNodeIds: z.ZodArray<z.ZodString>;
807
+ }, z.core.$strip>;
808
+ type ControlResult = z.infer<typeof ControlResultSchema>;
809
+ declare const ComplianceReportSchema: z.ZodObject<{
810
+ rulesetName: z.ZodString;
811
+ rulesetVersion: z.ZodString;
812
+ generatedAt: z.ZodString;
813
+ score: z.ZodNullable<z.ZodNumber>;
814
+ status: z.ZodEnum<{
815
+ pass: "pass";
816
+ fail: "fail";
817
+ not_applicable: "not_applicable";
818
+ }>;
819
+ totals: z.ZodObject<{
820
+ rules: z.ZodNumber;
821
+ applicable: z.ZodNumber;
822
+ passed: z.ZodNumber;
823
+ failed: z.ZodNumber;
824
+ notApplicable: z.ZodNumber;
825
+ }, z.core.$strip>;
826
+ bySeverity: z.ZodRecord<z.ZodEnum<{
827
+ low: "low";
828
+ medium: "medium";
829
+ high: "high";
830
+ critical: "critical";
831
+ }>, z.ZodObject<{
832
+ passed: z.ZodNumber;
833
+ failed: z.ZodNumber;
834
+ }, z.core.$strip>>;
835
+ controls: z.ZodArray<z.ZodObject<{
836
+ ruleId: z.ZodString;
837
+ control: z.ZodString;
838
+ framework: z.ZodString;
839
+ severity: z.ZodEnum<{
840
+ low: "low";
841
+ medium: "medium";
842
+ high: "high";
843
+ critical: "critical";
844
+ }>;
845
+ status: z.ZodEnum<{
846
+ pass: "pass";
847
+ fail: "fail";
848
+ not_applicable: "not_applicable";
849
+ }>;
850
+ applicableCount: z.ZodNumber;
851
+ passedCount: z.ZodNumber;
852
+ failingNodeIds: z.ZodArray<z.ZodString>;
853
+ }, z.core.$strip>>;
854
+ gaps: z.ZodArray<z.ZodObject<{
855
+ ruleId: z.ZodString;
856
+ control: z.ZodString;
857
+ severity: z.ZodEnum<{
858
+ low: "low";
859
+ medium: "medium";
860
+ high: "high";
861
+ critical: "critical";
862
+ }>;
863
+ title: z.ZodString;
864
+ nodeIds: z.ZodArray<z.ZodString>;
865
+ }, z.core.$strip>>;
866
+ }, z.core.$strip>;
867
+ type ComplianceReport = z.infer<typeof ComplianceReportSchema>;
868
+
869
+ /** Attribution applied by an enrichment pass (3.3). `null` clears the field; `undefined` leaves it unchanged. */
870
+ interface NodeAttribution {
871
+ owner?: string | null;
872
+ cost?: CostEntry | null;
873
+ }
874
+
875
+ /** Default tenant for single-user / pre-migration installs. */
876
+ declare const DEFAULT_TENANT = "local";
877
+ /**
878
+ * Normalize an untrusted tenant id: strip invisible/control characters, trim,
879
+ * cap length, and enforce a conservative key charset. Falls back to DEFAULT_TENANT
880
+ * when the input is missing or invalid. The tenant is a data-scoping partition key
881
+ * (not an auth boundary — RBAC is Phase 4), so the rule is shared by the DB and MCP
882
+ * layers via this one helper.
883
+ */
884
+ declare function normalizeTenant(raw?: string | null): string;
885
+ /**
886
+ * Deterministic, pure normalization of a node id so the same logical resource
887
+ * yields the same key across machines: trim, lowercase, collapse internal
888
+ * whitespace, strip a single trailing default port (:80/:443).
889
+ */
890
+ declare function normalizeId(id: string): string;
891
+ /**
892
+ * Extract only the stable key-meta subset (`host`/`port`/`path`/`url`) from a
893
+ * metadata blob (pure). Never the whole blob, which carries machine-local noise
894
+ * such as timestamps and absolute paths.
895
+ */
896
+ declare function keyMetaOf(metadata: Record<string, unknown>): Record<string, unknown>;
897
+ /**
898
+ * Secondary dedup key: sha256 over type + normalized name + sorted key-meta.
899
+ * Catches `id` drift between machines for the same logical resource. Truncated
900
+ * to 32 hex chars (128 bits) — ample for collision resistance at org scale.
901
+ */
902
+ declare function contentHash(type: string, name: string, keyMeta: Record<string, unknown>): string;
903
+ /**
904
+ * Human-readable primary identity: org-scoped normalized id (`{org}:{normalizedId}`).
905
+ * `org` is the session's normalized tenant (2.8's partition) — the `'_'` sentinel
906
+ * isolates org-less sessions into their own namespace so two organizations never
907
+ * collapse onto one logical node even with identical infra.
908
+ */
909
+ declare function globalId(organization: string | undefined, id: string): string;
145
910
  interface ConnectionRow extends Connection {
146
911
  sessionId: string;
147
912
  createdAt: string;
@@ -162,7 +927,61 @@ interface GraphSummary {
162
927
  type: string;
163
928
  degree: number;
164
929
  }>;
930
+ /** Standing structural anomalies (orphans / shadow-IT), computed at read time (3.6). */
931
+ anomalies: Anomaly[];
932
+ /** Distinct machines that contributed to this session's nodes (2.9 attribution). */
933
+ contributors: number;
934
+ /** Cost rolled up by domain, bucketed by (currency, period) — never FX-normalized (3.3). */
935
+ costByDomain: Array<{
936
+ domain: string;
937
+ currency: string;
938
+ period: string;
939
+ total: number;
940
+ nodes: number;
941
+ }>;
942
+ /** Cost rolled up by owner, bucketed by (currency, period) (3.3). */
943
+ costByOwner: Array<{
944
+ owner: string;
945
+ currency: string;
946
+ period: string;
947
+ total: number;
948
+ nodes: number;
949
+ }>;
950
+ /** Nodes carrying a cost vs. total — attribution coverage (3.3). */
951
+ costCoverage: {
952
+ withCost: number;
953
+ total: number;
954
+ };
955
+ }
956
+ /**
957
+ * Org-wide aggregate index (2.12) — the central-collector analogue of
958
+ * {@link GraphSummary}. Scoped to a single tenant (`org`) rather than one session,
959
+ * so it merges every machine's contribution into one organization-wide topology.
960
+ */
961
+ interface OrgSummary {
962
+ org: string;
963
+ totals: {
964
+ nodes: number;
965
+ edges: number;
966
+ };
967
+ nodesByType: Record<string, number>;
968
+ nodesByDomain: Record<string, number>;
969
+ edgesByRelationship: Record<string, number>;
970
+ topConnected: Array<{
971
+ id: string;
972
+ name: string;
973
+ type: string;
974
+ degree: number;
975
+ }>;
976
+ /** Distinct machines that contributed to this org's nodes (2.9 attribution, org-wide). */
977
+ contributors: number;
165
978
  }
979
+ /**
980
+ * Derive a deterministic, human-friendly session label from its graph summary,
981
+ * e.g. `"infra+data · 42 nodes · 2026-06-11"`. Pure: same summary + timestamp
982
+ * always yields the same name. No LLM call.
983
+ */
984
+ declare function deriveSessionName(summary: GraphSummary, startedAt: string): string;
166
985
  /** Result of a recursive dependency traversal. */
167
986
  interface TraversalResult {
168
987
  root?: NodeRow;
@@ -185,6 +1004,8 @@ interface EventRow {
185
1004
  targetType?: string;
186
1005
  port?: number;
187
1006
  durationMs?: number;
1007
+ command?: string;
1008
+ resultBytes?: number;
188
1009
  }
189
1010
  interface TaskRow {
190
1011
  id: string;
@@ -210,7 +1031,12 @@ interface WorkflowRow {
210
1031
  }
211
1032
  declare class CartographyDB {
212
1033
  private db;
213
- constructor(dbPath: string);
1034
+ /** 3.6 anomaly settings; defaults apply when no `anomaly` config is supplied. */
1035
+ private readonly anomalyEnabled;
1036
+ private readonly anomalyThresholds;
1037
+ constructor(dbPath: string, opts?: {
1038
+ anomaly?: AnomalyConfig;
1039
+ });
214
1040
  private migrate;
215
1041
  close(): void;
216
1042
  /**
@@ -219,26 +1045,105 @@ declare class CartographyDB {
219
1045
  * virtual table. Prefer the typed methods above for everything else.
220
1046
  */
221
1047
  rawConnection(): Database.Database;
222
- createSession(mode: 'discover', config: CartographyConfig): string;
1048
+ /**
1049
+ * Create a discovery session, stamping its tenant from (in precedence order)
1050
+ * the explicit `tenantId` arg → `config.organization` → DEFAULT_TENANT. The
1051
+ * tenant is normalized once here; every child row written under this session
1052
+ * inherits it via {@link tenantOf}.
1053
+ */
1054
+ createSession(mode: 'discover', config: CartographyConfig, tenantId?: string): string;
1055
+ /** The tenant that owns a session (DEFAULT_TENANT if the session is unknown). */
1056
+ private tenantOf;
223
1057
  endSession(id: string): void;
224
1058
  getSession(id: string): SessionRow | undefined;
225
- getLatestSession(mode?: string): SessionRow | undefined;
226
- getSessions(): SessionRow[];
1059
+ /**
1060
+ * Resolve the newest session, optionally constrained to a `mode` and/or
1061
+ * `tenantId`. Omitting `tenantId` preserves the original (unscoped) behavior;
1062
+ * passing one returns only that tenant's sessions, which is how the tenant
1063
+ * boundary is enforced at session resolution.
1064
+ */
1065
+ getLatestSession(mode?: string, tenantId?: string): SessionRow | undefined;
1066
+ getSessions(tenantId?: string): SessionRow[];
227
1067
  private mapSession;
228
- upsertNode(sessionId: string, node: DiscoveryNode, depth?: number): void;
1068
+ /** Record that a session was (re-)scanned now (ISO 8601 UTC). */
1069
+ touchSession(id: string): void;
1070
+ /** Set (or clear) a session's human-friendly name. */
1071
+ setSessionName(id: string, name: string): void;
1072
+ /**
1073
+ * Compare two discovery sessions and report drift (added/removed/changed nodes
1074
+ * and added/removed edges). Read-only; no schema changes. Throws if either
1075
+ * session id does not exist.
1076
+ */
1077
+ diffSessions(baseId: string, currentId: string): TopologyDiff;
1078
+ /**
1079
+ * Score a session against a compliance ruleset (3.4) — a thin wrapper over the
1080
+ * pure `scoreTopology` engine (mirrors `diffSessions`). Throws only when the
1081
+ * session id is unknown; the engine never throws on data shape.
1082
+ */
1083
+ scoreSession(sessionId: string, ruleset: Ruleset, opts?: {
1084
+ now?: string;
1085
+ }): ComplianceReport;
1086
+ /**
1087
+ * The most recent session of `mode` (and optionally `tenantId`) other than
1088
+ * `excludeId`. Used by scheduled discovery to pick an unambiguous diff base
1089
+ * (newest-first by `rowid`), avoiding the `getLatestSession` just-created-session
1090
+ * ambiguity. Returns `undefined` when no such prior session exists.
1091
+ */
1092
+ getPreviousSession(excludeId: string, mode?: string, tenantId?: string): SessionRow | undefined;
1093
+ /**
1094
+ * Persist one scheduled-discovery drift run: the summary counts plus the full
1095
+ * {@link TopologyDelta} (for audit/replay). Returns the generated row id.
1096
+ * `ranAt` is stamped now (ISO 8601 UTC).
1097
+ */
1098
+ recordDriftRun(sessionId: string, baseSessionId: string | undefined, delta: TopologyDelta): string;
1099
+ /** Recent drift runs, newest-first (`LIMIT`, default 50). */
1100
+ getDriftRuns(limit?: number): DriftRunRow[];
1101
+ /** The most recent drift run, or `undefined` when none has been recorded. */
1102
+ getLatestDriftRun(): DriftRunRow | undefined;
1103
+ private mapDriftRun;
1104
+ upsertNode(sessionId: string, node: DiscoveryNode, depth?: number, attribution?: Contributor): void;
1105
+ /** All contributors that observed a logical node, ordered by observation time. */
1106
+ getContributors(globalId: string): Contributor[];
229
1107
  getNodes(sessionId: string, opts?: {
230
1108
  limit?: number;
231
1109
  offset?: number;
232
1110
  }): NodeRow[];
233
1111
  getNodeCount(sessionId: string): number;
234
1112
  private mapNode;
1113
+ /**
1114
+ * Update only the cost/owner of an existing node, without touching any other
1115
+ * field (unlike upsertNode's INSERT OR REPLACE) — the idempotent enrichment
1116
+ * primitive (3.3). `undefined` leaves a field unchanged; `null` clears it.
1117
+ * No-op (returns false) if the node is absent. Cost is re-validated before write.
1118
+ */
1119
+ enrichNodeAttribution(sessionId: string, nodeId: string, attr: NodeAttribution): boolean;
235
1120
  deleteNode(sessionId: string, nodeId: string): void;
236
1121
  insertEdge(sessionId: string, edge: DiscoveryEdge): void;
1122
+ /**
1123
+ * Delete every edge matching the logical key (source, target, relationship)
1124
+ * within a session. `insertEdge` writes a random PK, so logical identity is the
1125
+ * only way to prune an edge that disappeared while both endpoints survived.
1126
+ */
1127
+ deleteEdgeByKey(sessionId: string, sourceId: string, targetId: string, relationship: string): void;
1128
+ /**
1129
+ * Apply a precomputed {@link TopologyDelta} to one session in a single
1130
+ * transaction (2.1 incremental discovery): prune removed nodes (cascading their
1131
+ * edges via {@link deleteNode}), upsert added/changed nodes, delete removed edges
1132
+ * by logical key, insert added edges, and stamp `last_scanned_at`. Unchanged rows
1133
+ * are left untouched (stable `discovered_at`).
1134
+ *
1135
+ * `attribution` (2.9) is forwarded to every added/changed node's upsert so a
1136
+ * rescan keeps/appends the running machine's contributor instead of leaving it
1137
+ * out. Unchanged nodes are not re-upserted, so their existing contributors
1138
+ * survive the rescan. `NodeRow extends DiscoveryNode`, so the delta rows pass
1139
+ * straight into `upsertNode` with no mapper.
1140
+ */
1141
+ applyTopologyDelta(sessionId: string, delta: TopologyDelta, attribution?: Contributor): void;
237
1142
  getEdges(sessionId: string, opts?: {
238
1143
  limit?: number;
239
1144
  offset?: number;
240
1145
  }): EdgeRow[];
241
- insertEvent(sessionId: string, event: Pick<EventRow, 'eventType' | 'process' | 'pid' | 'target' | 'targetType' | 'port'>, taskId?: string): void;
1146
+ insertEvent(sessionId: string, event: Pick<EventRow, 'eventType' | 'process' | 'pid' | 'target' | 'targetType' | 'port'> & Partial<Pick<EventRow, 'command' | 'resultBytes'>>, taskId?: string): void;
242
1147
  getEvents(sessionId: string, since?: string): EventRow[];
243
1148
  startTask(sessionId: string, description?: string): string;
244
1149
  endCurrentTask(sessionId: string): void;
@@ -253,6 +1158,63 @@ declare class CartographyDB {
253
1158
  deleteConnection(sessionId: string, connectionId: string): void;
254
1159
  setApproval(pattern: string, action: 'save' | 'ignore' | 'auto'): void;
255
1160
  getApproval(pattern: string): string | undefined;
1161
+ /**
1162
+ * Set (or replace) the sharing level for a pattern. The `'*'` pattern is the
1163
+ * global default; any other pattern is an override (glob over the node id).
1164
+ * Validated via {@link SharingLevelSchema} before write; `created_at` is ISO UTC.
1165
+ */
1166
+ setSharingLevel(pattern: string, level: SharingLevel): void;
1167
+ /**
1168
+ * The full sharing policy: the `'*'` row resolves to `defaultLevel` (`'none'`
1169
+ * when absent — the opt-in floor), every other row becomes an override. The
1170
+ * glob-precedence resolution itself lives in `src/sharing.ts` so it is unit
1171
+ * testable in isolation; this returns the raw policy it consumes.
1172
+ */
1173
+ getSharingPolicy(): SharingPolicy;
1174
+ /** Remove a pattern override. The global default (`'*'`) cannot be cleared this way. */
1175
+ clearSharingOverride(pattern: string): void;
1176
+ /**
1177
+ * Persist the encrypted plaintext behind a pseudonym token. Idempotent: the
1178
+ * token is deterministic, so repeated writes `INSERT OR REPLACE` and never grow
1179
+ * the table. `ciphertext` is base64(iv ‖ tag ‖ AES-256-GCM(plaintext)).
1180
+ */
1181
+ saveReversal(token: string, ciphertext: string): void;
1182
+ /** Read the stored ciphertext for a pseudonym token (admin reversal path). */
1183
+ getReversal(token: string): string | undefined;
1184
+ /**
1185
+ * Enqueue one proposed share item. Idempotent via `INSERT OR IGNORE` on the
1186
+ * `content_hash` PK: re-classifying the same (transformed) item never duplicates
1187
+ * a row nor resets an existing decision. `payload` is the already-policy-
1188
+ * transformed projection (the exact bytes a push would send) — never raw node
1189
+ * data for `anonymized`/`none` items.
1190
+ */
1191
+ enqueuePending(item: {
1192
+ contentHash: string;
1193
+ sessionId: string;
1194
+ nodeId?: string;
1195
+ kind: 'node' | 'edge';
1196
+ payload: unknown;
1197
+ status: PendingStatus;
1198
+ decidedBy?: 'user' | 'rule';
1199
+ }): void;
1200
+ /** Queued share items, optionally filtered by status and/or session. */
1201
+ getPendingShares(filter?: {
1202
+ status?: PendingStatus;
1203
+ sessionId?: string;
1204
+ }): PendingShareRow[];
1205
+ /** Queue size by status (every status key present, zero-filled). */
1206
+ countPendingByStatus(): Record<PendingStatus, number>;
1207
+ /** `content_hash` values already pushed (status `shared`) — for re-share suppression. */
1208
+ getSharedHashes(): Set<string>;
1209
+ /**
1210
+ * Transition one queued item. Stamps `decided_at` on any non-`pending` status and
1211
+ * `shared_at` when moving to `shared`. `decidedBy` records the actor (`'user'` or
1212
+ * `'rule'`) for the audit trail.
1213
+ */
1214
+ setPendingStatus(contentHash: string, status: PendingStatus, decidedBy?: 'user' | 'rule'): void;
1215
+ /** Approved items cleared to push (FIFO), optionally capped by `limit`. */
1216
+ getApprovedShares(limit?: number): PendingShareRow[];
1217
+ private mapPendingShare;
256
1218
  /**
257
1219
  * Delete a session and all its associated data (nodes, edges, events, tasks, workflows, connections).
258
1220
  */
@@ -286,6 +1248,55 @@ declare class CartographyDB {
286
1248
  }): TraversalResult;
287
1249
  /** Lightweight aggregate index of the whole topology — the progressive-disclosure summary. */
288
1250
  getGraphSummary(sessionId: string): GraphSummary;
1251
+ /**
1252
+ * Resolve (creating once) the synthetic collector session that owns every
1253
+ * central node/edge for a tenant. Central nodes are merged by `(org, global_id)`,
1254
+ * not by session, so they live under a single deterministic session id
1255
+ * (`central:{org}`) — this satisfies the existing `(id, session_id)` node PK and
1256
+ * the `session_id` foreign key without a destructive schema change. Idempotent.
1257
+ */
1258
+ ensureCentralSession(org: string): string;
1259
+ /**
1260
+ * Find an existing central node within a tenant by its primary identity
1261
+ * (`global_id`), returning its stored `id` so a merge keeps a single row.
1262
+ */
1263
+ findCentralNodeIdByGlobalId(org: string, gid: string): string | undefined;
1264
+ /**
1265
+ * Secondary merge lookup: an existing central node in the tenant whose
1266
+ * `content_hash` matches (catches `id` drift between machines for the same
1267
+ * logical resource). Returns its stored `id` and `global_id`.
1268
+ */
1269
+ findCentralNodeByContentHash(org: string, ch: string): {
1270
+ id: string;
1271
+ globalId: string;
1272
+ } | undefined;
1273
+ /**
1274
+ * Merge one incoming node into the central store for a tenant and append the
1275
+ * contributor (2.12). Resolves identity by `(tenant, global_id)` primary, then
1276
+ * `(tenant, content_hash)` secondary; on a hit it keeps the existing row's id
1277
+ * (so the logical node stays a single row), unions tags, keeps the higher
1278
+ * confidence, and merges metadata (incoming values win on key conflict). The
1279
+ * incoming `globalId`/`contentHash` are precomputed by the merge core so they
1280
+ * are consistent with what the lookups used. Returns whether a new row was
1281
+ * created or an existing one was merged. Runs in one transaction.
1282
+ */
1283
+ upsertCentralNode(org: string, node: DiscoveryNode, identity: {
1284
+ globalId: string;
1285
+ contentHash: string;
1286
+ }, contributor: Contributor): 'created' | 'merged';
1287
+ /** Insert an edge into the central store for a tenant (idempotent on logical key). */
1288
+ insertCentralEdge(org: string, edge: DiscoveryEdge): void;
1289
+ /** A central node by tenant + stored id (the merge target after identity resolution). */
1290
+ getCentralNode(org: string, sessionId: string, nodeId: string): NodeRow | undefined;
1291
+ /** All contributors for a logical (global_id) node across an org. */
1292
+ getContributorsByGlobalId(gid: string): Contributor[];
1293
+ /**
1294
+ * Org-wide aggregate summary (2.12) — the central analogue of
1295
+ * {@link getGraphSummary}, scoped `WHERE tenant = ?` so it merges every machine's
1296
+ * contribution into one organization-wide view. Cross-tenant isolation is
1297
+ * structural: org A's rows never appear in org B's counts.
1298
+ */
1299
+ getOrgSummary(org: string): OrgSummary;
289
1300
  getStats(sessionId: string): {
290
1301
  nodes: number;
291
1302
  edges: number;
@@ -295,78 +1306,471 @@ declare class CartographyDB {
295
1306
  }
296
1307
 
297
1308
  /**
298
- * The Cartography MCP server the package's primary, LLM-agnostic interface.
1309
+ * `StoreBackend` the persistence seam for the central collector (2.12).
299
1310
  *
300
- * It exposes the discovered infrastructure topology as Model Context Protocol
301
- * **Resources** (read-only context, progressive disclosure), a small set of query
302
- * **Tools** (parameterized lookups), and reusable **Prompts**. Any MCP host —
303
- * Claude Code, Cursor, Cline, Windsurf, the Vercel AI SDK, LangGraph — can drive
304
- * it; the package never needs to know which model is in use.
1311
+ * The central collector merges consented discovery deltas from many machines into
1312
+ * one organization-wide, tenant-partitioned topology. Today there is exactly one
1313
+ * shipping implementation, {@link SqliteStoreBackend}, which wraps the existing
1314
+ * `CartographyDB`. The interface exists so a graph/Postgres backend (4.3) can be
1315
+ * dropped in without touching the ingest orchestration the ingest core
1316
+ * (`src/central/ingest.ts`) talks only to this interface, never to SQLite directly.
1317
+ *
1318
+ * The contract is deliberately minimal: an org-scoped node upsert (merge-by-identity
1319
+ * with contributor attribution), an org-scoped edge insert, an org-wide summary, and
1320
+ * a contributor read for audit/tests. Every method is tenant-scoped by `org` — there
1321
+ * is no un-scoped path, so cross-tenant isolation is structural.
305
1322
  */
306
1323
 
307
- /** A pluggable search backend; defaults to lexical search, can be upgraded to semantic. */
308
- type SearchFn = (db: CartographyDB, sessionId: string, query: string, opts: {
309
- types?: readonly string[];
310
- limit: number;
311
- }) => Promise<Array<{
312
- node: NodeRow;
313
- score?: number;
314
- }>>;
315
- /** A pluggable discovery backend invoked by the `run_discovery` tool. */
316
- type DiscoveryFn = (db: CartographyDB, sessionId: string, opts: {
317
- hint?: string;
318
- }) => Promise<{
319
- nodes: number;
320
- edges: number;
321
- }>;
322
- interface CreateMcpServerOptions {
323
- /** Database instance. If omitted, one is opened at `config.dbPath`. */
324
- db?: CartographyDB;
325
- /** Path to the SQLite catalog (used when `db` is not provided). */
326
- dbPath?: string;
327
- /** Session to serve: a session id, or `'latest'` (default) for the newest discovery. */
328
- session?: string | 'latest';
329
- /** Semantic/lexical search backend. Defaults to lexical `searchNodes`. */
330
- search?: SearchFn;
331
- /** Discovery backend for `run_discovery`/`refresh`. Optional. */
332
- discovery?: DiscoveryFn;
1324
+ /** Precomputed merge identity for an incoming node (from the merge core). */
1325
+ interface NodeIdentity {
1326
+ /** Primary merge key — org-scoped normalized id (`{org}:{normalizedId}`). */
1327
+ globalId: string;
1328
+ /** Secondary merge key — content hash that catches `id` drift between machines. */
1329
+ contentHash: string;
333
1330
  }
334
1331
  /**
335
- * Build a fully-configured Cartography MCP server. Call `.connect(transport)` to run it.
1332
+ * A provider-agnostic central store. All operations are scoped to a single tenant
1333
+ * (`org`); there is no cross-tenant read or write path.
336
1334
  */
337
- declare function createMcpServer(opts?: CreateMcpServerOptions): McpServer;
1335
+ interface StoreBackend {
1336
+ /**
1337
+ * Merge one incoming node under `org`, keyed by `identity.globalId` (primary) then
1338
+ * `identity.contentHash` (secondary), and append/update the `contributor`. Returns
1339
+ * `'created'` when this is the first observation of the logical node, `'merged'`
1340
+ * when it collapsed onto an existing one.
1341
+ */
1342
+ upsertNode(org: string, node: DiscoveryNode, identity: NodeIdentity, contributor: Contributor): 'created' | 'merged';
1343
+ /** Insert an edge under `org` (idempotent on the logical `(source, target, relationship)` key). */
1344
+ insertEdge(org: string, edge: DiscoveryEdge): void;
1345
+ /** Org-wide aggregate summary (merged counts across all contributors). */
1346
+ getSummary(org: string): OrgSummary;
1347
+ /** Contributors for a merged logical node (test/audit helper). */
1348
+ getContributors(globalId: string): Contributor[];
1349
+ /** Release any underlying resources. */
1350
+ close(): void;
1351
+ }
338
1352
 
339
1353
  /**
340
- * Transport bindings for the Cartography MCP server.
1354
+ * `SqliteStoreBackend` the default (and currently only) {@link StoreBackend}
1355
+ * implementation for the central collector (2.12).
341
1356
  *
342
- * - **stdio**: the local-first default zero network, every client supports it.
343
- * - **Streamable HTTP**: a single `/mcp` endpoint for team/remote use, bound to
344
- * localhost with DNS-rebinding protection. The deprecated SSE transport is not used.
1357
+ * It is a thin adapter over `CartographyDB`: the schema, migrations, and merge SQL
1358
+ * all live in `src/db.ts` (the single owner of the catalog), so this class only
1359
+ * forwards the org-scoped central operations. Constructing it adds no new state and
1360
+ * no new schema — the zero-config local SQLite path is byte-for-byte unchanged.
1361
+ *
1362
+ * A graph/Postgres backend (4.3) implements the same interface without changing the
1363
+ * ingest orchestration that consumes it.
345
1364
  */
346
1365
 
347
- /** Connect a server over stdio (resolves when the transport closes). */
348
- declare function runStdio(server: McpServer): Promise<void>;
349
- interface HttpOptions {
350
- port?: number;
351
- host?: string;
352
- /** Extra allowed Host headers (defaults to localhost:port variants). */
353
- allowedHosts?: string[];
354
- /** Allowed Origin headers (defaults to none → same-origin only). */
355
- allowedOrigins?: string[];
1366
+ declare class SqliteStoreBackend implements StoreBackend {
1367
+ private readonly db;
1368
+ constructor(db: CartographyDB);
1369
+ upsertNode(org: string, node: DiscoveryNode, identity: NodeIdentity, contributor: Contributor): 'created' | 'merged';
1370
+ insertEdge(org: string, edge: DiscoveryEdge): void;
1371
+ getSummary(org: string): OrgSummary;
1372
+ getContributors(globalId: string): Contributor[];
1373
+ /**
1374
+ * No-op: the wrapped `CartographyDB` is owned by the caller (it is shared with the
1375
+ * read-side MCP server in server-mode), so the backend never closes it. The caller
1376
+ * closes the `CartographyDB` directly.
1377
+ */
1378
+ close(): void;
356
1379
  }
1380
+
357
1381
  /**
358
- * Start a Streamable HTTP server. A fresh MCP server instance is created per
359
- * session via `factory`, so multiple clients can connect concurrently.
1382
+ * Global-identity merge core for the central collector (2.12) pure, no I/O.
1383
+ *
1384
+ * The merge *keys* (`normalizeId`, `contentHash`, `keyMetaOf`, `globalId`) already
1385
+ * exist in `src/db.ts` from the 2.9 identity work; this module re-exports them so
1386
+ * the central collector has one import surface, and adds {@link computeIdentity} —
1387
+ * the small adapter that turns one incoming `DiscoveryNode` into the precomputed
1388
+ * `(globalId, contentHash)` pair the {@link StoreBackend} merge consumes.
1389
+ *
1390
+ * The merge *resolution* (primary by `(org, globalId)`, secondary by
1391
+ * `(org, contentHash)`, contributor union, max-confidence) lives in the store
1392
+ * (`CartographyDB.upsertCentralNode`) because it needs the existing row — keeping
1393
+ * the SQL next to the schema. This module is the deterministic key derivation that
1394
+ * both the client (push) and the server (ingest) must agree on.
360
1395
  */
361
- declare function runHttp(factory: () => McpServer, opts?: HttpOptions): Promise<http.Server>;
1396
+
1397
+ /**
1398
+ * Derive the precomputed merge identity for one incoming node under `org`:
1399
+ * - `globalId` = `{org}:{normalizeId(node.id)}` (primary key; the org-scope ensures
1400
+ * two organizations never collapse onto one logical node).
1401
+ * - `contentHash` = sha256 over `type + normalized name + sorted key-meta` (secondary
1402
+ * key; catches `id` drift between machines for the same logical resource).
1403
+ *
1404
+ * Pure and deterministic: the same node + org always yields the same identity, on
1405
+ * any machine. This is the contract the client (2.11 push) and the server (ingest)
1406
+ * share so a content hash computed on the client matches the one the server recomputes.
1407
+ */
1408
+ declare function computeIdentity(org: string, node: DiscoveryNode): NodeIdentity;
1409
+
1410
+ /**
1411
+ * Server-side anonymization re-validation for the central collector (2.12).
1412
+ *
1413
+ * **Don't trust the client.** The client (2.10) is supposed to pseudonymize
1414
+ * identifying fragments before pushing at the `anonymized` sharing level, but the
1415
+ * collector independently re-checks every incoming payload and rejects or scrubs any
1416
+ * un-anonymized identifying fragment it finds. This is defense-in-depth: a buggy,
1417
+ * outdated, or malicious client cannot leak raw identifiers into the org-wide store.
1418
+ *
1419
+ * The walker mirrors `redactValue`/`pseudonymize` (it rewrites only leaf strings;
1420
+ * structure is invariant). What it flags at the `anonymized` level:
1421
+ * - **private-ip** — RFC-1918 / loopback IPv4 (`src/anonymize.ts:PRIVATE_IP` + 127/8).
1422
+ * - **absolute-path**— POSIX (`/home/alice/...`) or Windows (`C:\Users\alice\...`) paths.
1423
+ * - **username** — the user segment of `/home/<u>`, `/Users/<u>`, `C:\Users\<u>`.
1424
+ * - **hostname** — multi-label FQDNs (`db-01.internal.acme.lan`), AND the known
1425
+ * 2.10 residual: **bare single-label internal hostnames** (e.g. `db-prod-01`) that
1426
+ * the client's multi-label-only HOSTNAME regex never tokenizes. A token already in
1427
+ * `anon:{kind}:{base32}` form is recognized as anonymized and never flagged.
1428
+ *
1429
+ * At the `none` / `full` levels nothing is claimed to be anonymized, so re-validation
1430
+ * is a no-op (returns no violations): the employee consented to share at that level.
1431
+ */
1432
+
1433
+ /** The anonymization level claimed by an ingest envelope (mirrors `SharingLevel`). */
1434
+ type AnonymizationLevel = 'none' | 'anonymized' | 'full';
1435
+ /** A detected un-anonymized identifying fragment, with its location for logging. */
1436
+ interface AnonViolation {
1437
+ /** JSON path to the offending leaf, e.g. `metadata.host`. */
1438
+ path: string;
1439
+ kind: 'hostname' | 'username' | 'absolute-path' | 'private-ip';
1440
+ }
1441
+ /**
1442
+ * Recursively collect every anonymization violation in a value at the given level.
1443
+ * Pure. At `none`/`full` returns `[]` (no anonymization is claimed). Object keys are
1444
+ * schema, not data, so only values are inspected; `path` accumulates the location.
1445
+ */
1446
+ declare function findAnonViolations(value: unknown, level: AnonymizationLevel, path?: string): AnonViolation[];
1447
+ /**
1448
+ * Re-validate one node against its claimed anonymization level (2.12).
1449
+ *
1450
+ * - `mode: 'reject'` (default, strict) — returns the node unchanged plus the
1451
+ * violations; the caller drops a node with any violation and never persists it.
1452
+ * - `mode: 'strip'` (lenient) — returns a sanitized node (offending leaves masked to
1453
+ * `***`) plus the violations (for logging), so the topology shape survives while
1454
+ * the identifying fragments do not.
1455
+ *
1456
+ * Pure: no I/O, no logging (the caller logs). At `none`/`full` it is a pass-through.
1457
+ */
1458
+ declare function revalidateAnonymized(node: DiscoveryNode, level: AnonymizationLevel, mode: 'reject' | 'strip'): {
1459
+ node: DiscoveryNode;
1460
+ violations: AnonViolation[];
1461
+ };
1462
+
1463
+ /**
1464
+ * Ingest orchestration for the central collector (2.12).
1465
+ *
1466
+ * Consumes the 2.11 PUSH ENVELOPE verbatim —
1467
+ * `{ schemaVersion: 1, org?, items: [{ contentHash, kind: 'node'|'edge', payload }] }`
1468
+ * — and merges it into the tenant-partitioned central store. The pipeline is:
1469
+ *
1470
+ * 1. **Validate** the envelope shape (zod). A malformed envelope is rejected whole;
1471
+ * nothing is persisted (the HTTP layer turns this into a 400).
1472
+ * 2. **Re-validate anonymization** per node (`revalidateAnonymized`) — the client is
1473
+ * not trusted. In `reject` mode a node with any identifying fragment is dropped
1474
+ * and counted; in `strip` mode it is scrubbed and persisted. Either way a
1475
+ * structured WARN is logged with the violation path + action.
1476
+ * 3. **Merge** each node by `(org, globalId)` primary + `(org, contentHash)` secondary
1477
+ * with a contributor union (`store.upsertNode`), and insert edges (`store.insertEdge`)
1478
+ * only when both endpoints survived re-validation.
1479
+ * 4. **Log** one structured INFO with the per-ingest counts.
1480
+ *
1481
+ * The contributor `at` is stamped server-side (`new Date().toISOString()`) — never
1482
+ * trusted from the client. The envelope MAY carry a `contributor`/`anonymizationLevel`
1483
+ * extension (2.9/2.10); absent, the collector derives a conservative default and
1484
+ * re-validates at the `anonymized` level (the safe assumption).
1485
+ */
1486
+
1487
+ /** Wire-format version this collector accepts (the 2.11 `PUSH_SCHEMA_VERSION`). */
1488
+ declare const INGEST_SCHEMA_VERSION: 1;
1489
+ /**
1490
+ * The ingest envelope — the 2.11 push contract, plus optional `contributor` /
1491
+ * `anonymizationLevel` extension fields (read when present; never required). `org`
1492
+ * is optional in the wire format; the collector falls back to its `defaultOrg`.
1493
+ */
1494
+ declare const IngestEnvelopeSchema: z.ZodObject<{
1495
+ schemaVersion: z.ZodLiteral<1>;
1496
+ org: z.ZodOptional<z.ZodString>;
1497
+ items: z.ZodArray<z.ZodObject<{
1498
+ contentHash: z.ZodString;
1499
+ kind: z.ZodEnum<{
1500
+ node: "node";
1501
+ edge: "edge";
1502
+ }>;
1503
+ payload: z.ZodUnknown;
1504
+ }, z.core.$strip>>;
1505
+ contributor: z.ZodOptional<z.ZodObject<{
1506
+ machineId: z.ZodString;
1507
+ hostname: z.ZodDefault<z.ZodString>;
1508
+ user: z.ZodDefault<z.ZodString>;
1509
+ confidence: z.ZodDefault<z.ZodNumber>;
1510
+ }, z.core.$strip>>;
1511
+ anonymizationLevel: z.ZodOptional<z.ZodEnum<{
1512
+ none: "none";
1513
+ anonymized: "anonymized";
1514
+ full: "full";
1515
+ }>>;
1516
+ }, z.core.$strip>;
1517
+ type IngestEnvelope = z.infer<typeof IngestEnvelopeSchema>;
1518
+ /** Per-ingest outcome counts (the 200 response body). */
1519
+ interface IngestResult {
1520
+ org: string;
1521
+ /** Nodes persisted (created + merged). */
1522
+ accepted: number;
1523
+ /** Nodes that collapsed onto an existing logical node. */
1524
+ merged: number;
1525
+ /** Nodes dropped for anonymization violations (reject mode). */
1526
+ rejected: number;
1527
+ /** Edges persisted. */
1528
+ edges: number;
1529
+ /** Total anonymization violations detected across all nodes. */
1530
+ violations: number;
1531
+ }
1532
+ interface IngestOptions {
1533
+ /** `reject` (default) drops nodes with violations; `strip` scrubs and keeps them. */
1534
+ anonMode?: 'reject' | 'strip';
1535
+ /** Tenant used when the envelope omits `org`. */
1536
+ defaultOrg?: string;
1537
+ }
1538
+ /**
1539
+ * Ingest one validated envelope into the store. Returns the outcome counts.
1540
+ * The caller (HTTP handler) wraps this in try/catch; the store's per-node upsert is
1541
+ * itself transactional, so a single bad node never half-writes a row.
1542
+ */
1543
+ declare function ingestEnvelope(store: StoreBackend, envelope: IngestEnvelope, opts?: IngestOptions): IngestResult;
1544
+
1545
+ /**
1546
+ * The central-collector ingest HTTP surface (2.12).
1547
+ *
1548
+ * {@link createIngestHandler} produces the request handler that the Streamable-HTTP
1549
+ * transport mounts at `POST /ingest` when server-mode is on. It is deliberately a
1550
+ * pure `(body) => { status, body }` function with no socket of its own, so it can be
1551
+ * unit-tested directly without binding a port (the transport owns auth, the
1552
+ * non-loopback host allowlist, and the body size cap — see `src/mcp/transports.ts`).
1553
+ *
1554
+ * Validation is fail-closed: a malformed envelope yields 400 (no writes); an internal
1555
+ * error yields 500; only a valid envelope runs `ingestEnvelope` and returns 200 with
1556
+ * the {@link IngestResult}. The bearer token is enforced by the transport before this
1557
+ * handler ever runs, so the handler never sees (and never logs) the token.
1558
+ */
1559
+
1560
+ /** A transport-agnostic HTTP-ish response: a status code and a JSON-serializable body. */
1561
+ interface IngestResponse {
1562
+ status: number;
1563
+ body: unknown;
1564
+ }
1565
+ type IngestHandler = (body: unknown) => IngestResponse;
1566
+ /**
1567
+ * Build the `/ingest` handler over a {@link StoreBackend}. The handler validates the
1568
+ * 2.11 push envelope, runs ingest (re-validating anonymization first), and maps the
1569
+ * outcome to an HTTP status:
1570
+ * - 400 — envelope failed `IngestEnvelopeSchema` (no writes).
1571
+ * - 500 — ingest threw (the store's per-node transaction rolls that node back).
1572
+ * - 200 — {@link IngestResult}.
1573
+ */
1574
+ declare function createIngestHandler(store: StoreBackend, opts?: IngestOptions): IngestHandler;
1575
+
1576
+ /**
1577
+ * Org-key lifecycle for the 2.10 anonymization layer.
1578
+ *
1579
+ * The org key is the single secret that makes pseudonyms deterministic across
1580
+ * machines (so a central merge collapses the same host to one token) and
1581
+ * admin-reversible (only the key holder can invert a pseudonym). It lives on disk
1582
+ * at `~/.cartography/org-key` (mode 0600), **never** in the SQLite catalog, never
1583
+ * in any log line, and never in an export. The returned key is HKDF-namespaced by
1584
+ * `organization`, so two organizations sharing one machine derive non-colliding
1585
+ * keys (and therefore non-colliding tokens + isolated reversal maps).
1586
+ *
1587
+ * Zero new dependencies: HMAC/AES/HKDF/randomBytes all come from `node:crypto`.
1588
+ */
1589
+ interface OrgKeyOptions {
1590
+ /** Organization namespace; absent ⇒ the `'default'` namespace. */
1591
+ organization?: string;
1592
+ /** Override the on-disk key path (tests / non-default data dirs). */
1593
+ keyPath?: string;
1594
+ }
1595
+ /** Default org-key file path: `~/.cartography/org-key`. */
1596
+ declare function orgKeyPath(home?: string): string;
1597
+ /**
1598
+ * Load (or lazily create) the org key, HKDF-namespaced by `organization`. The
1599
+ * on-disk secret is a single random value; per-org keys are derived from it so
1600
+ * the same file serves multiple orgs without collision. Pure of side effects
1601
+ * beyond the lazy file creation; never logs the key material.
1602
+ */
1603
+ declare function loadOrgKey(opts?: OrgKeyOptions): Buffer;
1604
+ /**
1605
+ * Rotate the org key: overwrite the on-disk secret with fresh random bytes and
1606
+ * return the new derived key. Reversal entries written under the old key become
1607
+ * unrecoverable by design — callers are warned at the stderr level.
1608
+ */
1609
+ declare function rotateOrgKey(opts?: OrgKeyOptions): Buffer;
1610
+ /** Subkey for HMAC pseudonym tokens (distinct domain from the reversal cipher key). */
1611
+ declare function hmacKey(orgKey: Buffer): Buffer;
1612
+ /** Subkey for AES-256-GCM reversal-map encryption (distinct domain from the HMAC key). */
1613
+ declare function reversalKey(orgKey: Buffer): Buffer;
1614
+
1615
+ /**
1616
+ * Org-keyed, admin-reversible pseudonymization (2.10 `anonymized` sharing level).
1617
+ *
1618
+ * Identifying fragments — private IPs, file paths, `user@host` pairs, and bare
1619
+ * hostnames — are replaced by a deterministic token
1620
+ * `anon:{kind}:{base32(first 12 bytes of HMAC-SHA256(hmacKey, plaintext))}`
1621
+ * so the same input + org key always yields the same token (a central merge then
1622
+ * collapses the same host across machines). The token is one-way for anyone
1623
+ * without the org key (HMAC preimage resistance + a 32-byte secret); an admin
1624
+ * holding the key can invert it via an AES-256-GCM reversal map.
1625
+ *
1626
+ * Topology shape is preserved: {@link pseudonymize} mirrors `redactValue`
1627
+ * (`src/tools.ts`) — it walks structure, rewriting only leaf strings; it never
1628
+ * adds, removes, or reorders keys/array elements.
1629
+ *
1630
+ * Zero new dependencies — all crypto is `node:crypto`.
1631
+ */
1632
+
1633
+ /** The kinds of identifying fragment we tokenize. The prefix keeps namespaces legible + non-colliding. */
1634
+ type FragmentKind = 'host' | 'user' | 'path' | 'ip';
1635
+ /**
1636
+ * RFC-1918 private IPv4 ranges: 10/8, 172.16/12, 192.168/16. Only private IPs are
1637
+ * pseudonymized — public IPs are not identifying of an employee's machine and are
1638
+ * left intact (so topology against public infra still reads).
1639
+ */
1640
+ declare const PRIVATE_IP: RegExp;
1641
+ /**
1642
+ * Deterministic token for one identifying fragment. When `db` is supplied, the
1643
+ * plaintext is AES-256-GCM-encrypted under `reversalKey(orgKey)` and persisted so
1644
+ * an admin can later invert the token. Idempotent: the token is deterministic, so
1645
+ * re-encrypting the same fragment `INSERT OR REPLACE`s the same row.
1646
+ */
1647
+ declare function pseudonymizeFragment(plaintext: string, kind: FragmentKind, orgKey: Buffer, db?: CartographyDB): string;
1648
+ /**
1649
+ * Pseudonymize the identifying fragments inside a single string — private IPs,
1650
+ * file paths, `user@host` pairs, and bare hostnames — leaving structure-irrelevant
1651
+ * text intact. Order matters: paths and IPs are matched before hostnames so an IP
1652
+ * or a path segment is never mis-tokenized as a host.
1653
+ */
1654
+ declare function pseudonymizeString(s: string, orgKey: Buffer, db?: CartographyDB): string;
1655
+ /**
1656
+ * Recursive, structure-preserving walker — same shape as `redactValue`
1657
+ * (`src/tools.ts`): strings → {@link pseudonymizeString}, arrays → map,
1658
+ * objects → per-value recurse, primitives → unchanged. Object keys are left
1659
+ * verbatim (they are schema, not data), so topology shape is invariant.
1660
+ */
1661
+ declare function pseudonymize(value: unknown, orgKey: Buffer, db?: CartographyDB): unknown;
1662
+ /**
1663
+ * Admin reversal: decrypt the original plaintext behind a pseudonym token, or
1664
+ * `undefined` when the token is unknown or the ciphertext fails GCM authentication
1665
+ * (tampered or produced under a different / rotated org key).
1666
+ */
1667
+ declare function reversePseudonym(token: string, orgKey: Buffer, db: CartographyDB): string | undefined;
1668
+
1669
+ /**
1670
+ * Sharing classifier + pre-send preview (2.10).
1671
+ *
1672
+ * Resolves the effective sharing level for each node (a PERSONAL-host hard floor
1673
+ * over the persisted policy), applies that level (`none` drops, `anonymized`
1674
+ * pseudonymizes, `full` keeps raw), and builds {@link previewShare} — the exact,
1675
+ * topology-shape-preserving payload that *would* leave the machine. This is the
1676
+ * privacy gate 2.11/2.12 build on; nothing here performs any network I/O.
1677
+ */
1678
+
1679
+ /**
1680
+ * Resolve the policy-level for a node id: the most-specific matching override
1681
+ * wins (fewest wildcards, then longest pattern); ties and no-match fall through
1682
+ * to `defaultLevel`. The `'*'`/`'**'` rows are the global default and always lose
1683
+ * to any narrower match. Pure and deterministic.
1684
+ */
1685
+ declare function resolveSharingLevel(nodeId: string, policy: SharingPolicy): SharingLevel;
1686
+ /**
1687
+ * The effective level for a node: a PERSONAL-host hard floor (single-sourced from
1688
+ * the bookmarks never-share list) forces `'none'` regardless of policy; otherwise
1689
+ * the policy's resolved level for the node id applies.
1690
+ */
1691
+ declare function resolveEffectiveLevel(node: DiscoveryNode, policy: SharingPolicy): SharingLevel;
1692
+ /**
1693
+ * Apply a sharing level to one node:
1694
+ * - `none` → `null` (the node is dropped, never leaves).
1695
+ * - `anonymized` → a clone with `id`/`name` pseudonymized and `metadata` walked
1696
+ * (structure-preserving); identifying fragments tokenized.
1697
+ * - `full` → a structural clone, verbatim.
1698
+ *
1699
+ * The same deterministic `pseudonymizeString` is applied to the id here and by
1700
+ * {@link previewShare} when remapping edges, so endpoints always resolve.
1701
+ */
1702
+ declare function applySharingLevel(node: DiscoveryNode, level: SharingLevel, orgKey: Buffer, db?: CartographyDB): DiscoveryNode | null;
1703
+ interface SharePreviewEntry {
1704
+ /** The original (un-anonymized) node, for the operator's side-by-side disclosure. */
1705
+ node: DiscoveryNode;
1706
+ level: SharingLevel;
1707
+ /** What would actually leave: the transformed node, or `null` when dropped. */
1708
+ payload: DiscoveryNode | null;
1709
+ }
1710
+ interface SharePreview {
1711
+ nodes: SharePreviewEntry[];
1712
+ /** Edges that would leave, with endpoints remapped to surviving (transformed) ids. */
1713
+ edges: {
1714
+ sourceId: string;
1715
+ targetId: string;
1716
+ relationship: string;
1717
+ }[];
1718
+ /** Original ids of nodes dropped at level `none`. */
1719
+ droppedNodeIds: string[];
1720
+ }
1721
+ /**
1722
+ * Build the exact payload that would leave for a session: resolve each node's
1723
+ * effective level, apply it, and remap edge endpoints through the same id
1724
+ * transform. An edge survives only when both endpoints survive — so when no node
1725
+ * is dropped the node count, edge count, and relationship multiset are identical
1726
+ * before/after; when some are dropped the result is a well-defined subgraph.
1727
+ *
1728
+ * `opts.persistReversal` (default false) controls whether anonymized fragments are
1729
+ * written to the reversal map — pure preview need not persist; an actual
1730
+ * pre-send transform should, so an admin can later invert.
1731
+ */
1732
+ declare function previewShare(db: CartographyDB, sessionId: string, orgKey: Buffer, policy: SharingPolicy, opts?: {
1733
+ persistReversal?: boolean;
1734
+ }): SharePreview;
1735
+ /**
1736
+ * The seam 2.11 will use to suppress `ask_user` re-prompts: true when the node id
1737
+ * is already covered by the persisted policy — either a matching override or a
1738
+ * non-`none` default — so a matched item is never re-prompted.
1739
+ */
1740
+ declare function isRemembered(policy: SharingPolicy, nodeId: string): boolean;
362
1741
 
363
1742
  /**
364
1743
  * Cross-platform utilities for Linux, macOS, and Windows.
365
1744
  * Centralizes all OS-specific logic so scanning tools work everywhere.
366
1745
  */
367
1746
  type Platform = 'linux' | 'darwin' | 'win32';
1747
+ /** OS hostname, with a stable fallback so attribution is never empty. */
1748
+ declare function hostname(): string;
1749
+ /** Current OS username, falling back to USER/USERNAME env, then a sentinel. */
1750
+ declare function osUser(): string;
1751
+ /**
1752
+ * Stable, privacy-respecting per-install identifier: a random UUID v4 cached at
1753
+ * `~/.cartography/machine-id` (mode 0600). No hardware fingerprinting, no command
1754
+ * execution — allowlist-safe. If the file cannot be read or written (read-only
1755
+ * home, missing HOME in CI), it degrades to a process-stable ephemeral UUID with a
1756
+ * warning (optional-deps-degrade house style) rather than throwing. The id never
1757
+ * leaves the machine in 2.9; it is opaque and non-identifying.
1758
+ */
1759
+ declare function machineId(): string;
368
1760
  declare function safeEnv(): NodeJS.ProcessEnv;
369
1761
 
1762
+ /**
1763
+ * Remove orphaned temp files from previous bookmark/history scans.
1764
+ * Call at startup to prevent /tmp accumulation after crashes.
1765
+ */
1766
+ declare function cleanupTempFiles(): number;
1767
+ interface BookmarkHost {
1768
+ hostname: string;
1769
+ port: number;
1770
+ protocol: 'http' | 'https';
1771
+ source: string;
1772
+ }
1773
+
370
1774
  /**
371
1775
  * Scanner plugin contract.
372
1776
  *
@@ -389,6 +1793,19 @@ interface ScanContext {
389
1793
  timeout?: number;
390
1794
  env?: NodeJS.ProcessEnv;
391
1795
  }) => string;
1796
+ /**
1797
+ * Injectable seam: resolve a command to its path ('' if absent). Defaults to the
1798
+ * real `commandExists` when omitted. Lets scanners run deterministically in tests.
1799
+ */
1800
+ commandExists?: (cmd: string) => string;
1801
+ /** Injectable seam: raw listening-port output. Defaults to `scanListeningPorts`. */
1802
+ scanListeningPorts?: () => string;
1803
+ /** Injectable seam: raw established-connection output (3.2). Defaults to `scanEstablishedConnections`. */
1804
+ scanEstablishedConnections?: () => string;
1805
+ /** Injectable seam: cross-platform file search (3.2). Defaults to `findFiles`. */
1806
+ findFiles?: (dirs: string[], patterns: string[], maxDepth: number, limit: number) => string;
1807
+ /** Injectable seam: browser-bookmark host source. Defaults to `scanAllBookmarks`. */
1808
+ scanBookmarks?: () => Promise<BookmarkHost[]>;
392
1809
  }
393
1810
  interface ScanResult {
394
1811
  /** Deterministically classified nodes. */
@@ -416,23 +1833,402 @@ interface Scanner {
416
1833
  declare class ScannerRegistry {
417
1834
  private scanners;
418
1835
  register(scanner: Scanner): this;
1836
+ /**
1837
+ * Register a {@link Scanner} produced by an external plugin package. Validates
1838
+ * the shape (throws `ZodError` on mismatch), namespaces the id to
1839
+ * `plugin:<pkg>:<id>` (avoiding collisions with built-ins and across plugins),
1840
+ * wraps `scan()` so the scanner's `ctx.run` is gated by its declared
1841
+ * `allowedCommands` intersected with the central read-only allowlist
1842
+ * ({@link checkReadOnly}), and freezes the wrapper. Reuses the duplicate-id
1843
+ * guard in {@link register}.
1844
+ *
1845
+ * The gated runner delegates the actual execution to the host-supplied
1846
+ * `ctx.run` (the platform runner), so the global read-only floor still applies
1847
+ * and `allowedCommands` is a *second*, scanner-scoped least-privilege boundary.
1848
+ * A command runs only if its leading executable is declared AND the whole
1849
+ * command passes `checkReadOnly`; otherwise the runner returns `''` (the
1850
+ * documented "blocked → ''" contract).
1851
+ */
1852
+ registerExternal(pkg: string, scanner: Scanner): this;
419
1853
  get(id: string): Scanner | undefined;
420
1854
  list(): Scanner[];
421
1855
  /** Scanners whose `platforms` include the given platform. */
422
1856
  forPlatform(platform: Platform): Scanner[];
423
1857
  }
424
1858
 
1859
+ /**
1860
+ * Hostname substrings that indicate a personal site — never catalogued, and the
1861
+ * hard floor for the 2.10 sharing policy (a personal host is forced to `'none'`
1862
+ * regardless of policy). Single-sourced here and shared via {@link isPersonalHost}
1863
+ * so the never-share list never forks.
1864
+ */
1865
+ declare const PERSONAL: string[];
1866
+ /** True when `host` matches a PERSONAL substring (case-insensitive). */
1867
+ declare function isPersonalHost(host: string): boolean;
425
1868
  declare const bookmarksScanner: Scanner;
426
1869
 
1870
+ /**
1871
+ * The Cartography MCP server — the package's primary, LLM-agnostic interface.
1872
+ *
1873
+ * It exposes the discovered infrastructure topology as Model Context Protocol
1874
+ * **Resources** (read-only context, progressive disclosure), a small set of query
1875
+ * **Tools** (parameterized lookups), and reusable **Prompts**. Any MCP host —
1876
+ * Claude Code, Cursor, Cline, Windsurf, the Vercel AI SDK, LangGraph — can drive
1877
+ * it; the package never needs to know which model is in use.
1878
+ */
1879
+
1880
+ /** A pluggable search backend; defaults to lexical search, can be upgraded to semantic. */
1881
+ type SearchFn = (db: CartographyDB, sessionId: string, query: string, opts: {
1882
+ types?: readonly string[];
1883
+ limit: number;
1884
+ }) => Promise<Array<{
1885
+ node: NodeRow;
1886
+ score?: number;
1887
+ }>>;
1888
+ /** A pluggable discovery backend invoked by the `run_discovery` tool. */
1889
+ type DiscoveryFn = (db: CartographyDB, sessionId: string, opts: {
1890
+ hint?: string;
1891
+ mode?: 'replace' | 'update';
1892
+ }) => Promise<{
1893
+ nodes: number;
1894
+ edges: number;
1895
+ delta?: TopologyDelta;
1896
+ }>;
1897
+ interface CreateMcpServerOptions {
1898
+ /** Database instance. If omitted, one is opened at `config.dbPath`. */
1899
+ db?: CartographyDB;
1900
+ /** Path to the SQLite catalog (used when `db` is not provided). */
1901
+ dbPath?: string;
1902
+ /** Session to serve: a session id, or `'latest'` (default) for the newest discovery. */
1903
+ session?: string | 'latest';
1904
+ /**
1905
+ * Tenant/organization whose topology this server serves. Defaults to DEFAULT_TENANT
1906
+ * (`'local'`). Session resolution is scoped to this tenant, so a server bound to one
1907
+ * tenant never surfaces another tenant's sessions/nodes/edges. This is a data-scoping
1908
+ * partition, not an authorization boundary (RBAC is Phase 4).
1909
+ */
1910
+ tenant?: string;
1911
+ /** Semantic/lexical search backend. Defaults to lexical `searchNodes`. */
1912
+ search?: SearchFn;
1913
+ /** Discovery backend for `run_discovery`/`refresh`. Optional. */
1914
+ discovery?: DiscoveryFn;
1915
+ /**
1916
+ * Central-collector org (2.12). When set, `get_summary` returns the org-wide,
1917
+ * cross-machine merged summary (`db.getOrgSummary(org)`) instead of the single-
1918
+ * session view. Used by server-mode; unset preserves the local single-session
1919
+ * behaviour exactly. The org is normalized to a tenant.
1920
+ */
1921
+ org?: string;
1922
+ }
1923
+ /**
1924
+ * Build a fully-configured Cartography MCP server. Call `.connect(transport)` to run it.
1925
+ */
1926
+ declare function createMcpServer(opts?: CreateMcpServerOptions): McpServer;
1927
+
1928
+ /**
1929
+ * Transport bindings for the Cartography MCP server.
1930
+ *
1931
+ * - **stdio**: the local-first default — zero network, every client supports it.
1932
+ * - **Streamable HTTP**: a single `/mcp` endpoint for team/remote use, bound to
1933
+ * localhost with DNS-rebinding protection. The deprecated SSE transport is not used.
1934
+ */
1935
+
1936
+ /** Connect a server over stdio (resolves when the transport closes). */
1937
+ declare function runStdio(server: McpServer): Promise<void>;
1938
+ interface HttpOptions {
1939
+ port?: number;
1940
+ host?: string;
1941
+ /** Extra allowed Host headers (defaults to localhost:port variants). */
1942
+ allowedHosts?: string[];
1943
+ /** Allowed Origin headers (defaults to none → same-origin only). */
1944
+ allowedOrigins?: string[];
1945
+ /**
1946
+ * Shared secret required in the `Authorization: Bearer <token>` header.
1947
+ * Mandatory when binding a non-loopback host; optional (and enforced when
1948
+ * present) on loopback.
1949
+ */
1950
+ token?: string;
1951
+ /**
1952
+ * Central-collector ingest hook (2.12). When set, an authenticated `POST /ingest`
1953
+ * **write** route is exposed: it inherits the *same* bearer auth and non-loopback
1954
+ * host-allowlist guards as `/mcp` (the route adds no separate hardening path), size-
1955
+ * caps the body, parses JSON, and returns the hook's `{ status, body }`. When unset,
1956
+ * `/ingest` 404s exactly like any other path — the collector stays dark by default.
1957
+ */
1958
+ onIngest?: (body: unknown) => {
1959
+ status: number;
1960
+ body: unknown;
1961
+ };
1962
+ }
1963
+ declare function runHttp(factory: () => McpServer, opts?: HttpOptions): Promise<http.Server>;
1964
+
427
1965
  declare const installedAppsScanner: Scanner;
428
1966
 
1967
+ /** Well-known listening ports → node type + service name. */
1968
+ declare const PORT_MAP: Record<number, {
1969
+ type: NodeType;
1970
+ service: string;
1971
+ }>;
429
1972
  /** Extract distinct listening port numbers from ss/lsof/PowerShell output. */
430
1973
  declare function extractListeningPorts(raw: string): number[];
431
1974
  declare const portsScanner: Scanner;
432
1975
 
1976
+ /**
1977
+ * AWS infrastructure scanner.
1978
+ *
1979
+ * Read-only AWS CLI discovery (`describe`/`list` actions, JSON output) mapped to
1980
+ * deterministic nodes. Detection is a cheap `commandExists('aws')` check, so an
1981
+ * absent CLI skips the scanner without throwing. Every command stays within the
1982
+ * read-only allowlist's `aws` gate and uses `--output json` for stable parsing.
1983
+ */
1984
+
1985
+ declare const cloudAwsScanner: Scanner;
1986
+
1987
+ /**
1988
+ * Google Cloud Platform scanner.
1989
+ *
1990
+ * Read-only `gcloud … list` discovery with `--format=json` for stable parsing,
1991
+ * mapped to deterministic nodes. Detection is `commandExists('gcloud')`; an
1992
+ * absent CLI skips the scanner. Every command stays within the read-only
1993
+ * allowlist's `gcloud` gate.
1994
+ */
1995
+
1996
+ declare const cloudGcpScanner: Scanner;
1997
+
1998
+ /**
1999
+ * Azure infrastructure scanner.
2000
+ *
2001
+ * Read-only `az … list/show` discovery with `--output json` for stable parsing,
2002
+ * mapped to deterministic nodes. Detection is `commandExists('az')`; an absent
2003
+ * CLI skips the scanner. Subscription / resource-group scoping comes from the
2004
+ * hint. Every command stays within the read-only allowlist's `az` gate.
2005
+ */
2006
+
2007
+ declare const cloudAzureScanner: Scanner;
2008
+
2009
+ /**
2010
+ * Kubernetes scanner.
2011
+ *
2012
+ * Read-only `kubectl get … -o json` discovery mapped to deterministic nodes and
2013
+ * edges. Detection is `commandExists('kubectl')`; an absent CLI skips the
2014
+ * scanner. The cluster node anchors `contains` edges to its hosts and pods, and
2015
+ * a `connects_to` edge is emitted from a service to a running pod when the pod's
2016
+ * labels match the service's selector — only when both endpoints are in the
2017
+ * result set (the driver prunes dangling edges anyway). Pod enumeration is
2018
+ * bounded to keep `run_discovery` latency predictable on large clusters.
2019
+ */
2020
+
2021
+ declare const k8sScanner: Scanner;
2022
+
2023
+ /**
2024
+ * Local database scanner.
2025
+ *
2026
+ * Probes locally-installed DB clients (read-only) and discovers SQLite files in
2027
+ * app-data directories, mapping reachable servers to deterministic nodes. Each
2028
+ * client is only probed when present (`commandExists`), so an absent CLI never
2029
+ * throws and never produces a node. The expensive home-directory / config-file
2030
+ * find is opt-in via the `deep` hint token, keeping the default deterministic
2031
+ * `run_discovery` path fast. Connection strings are credential-redacted before
2032
+ * persistence.
2033
+ */
2034
+
2035
+ declare const databasesScanner: Scanner;
2036
+
2037
+ /**
2038
+ * Established-connections scanner (3.2).
2039
+ *
2040
+ * Reads the host's live TCP connections (read-only) and infers `connects_to`
2041
+ * edges from a single local `host:localhost` node to any recognized listening
2042
+ * service (`${type}:localhost:${port}` from {@link PORT_MAP}). The server-side
2043
+ * node is only kept when `portsScanner` also mapped that port in the same run —
2044
+ * the in-batch endpoint gate in `runLocalDiscovery` drops edges to ports nothing
2045
+ * is listening on (graceful degradation, never an error).
2046
+ */
2047
+
2048
+ interface EstablishedConn {
2049
+ localPort: number;
2050
+ remoteHost: string;
2051
+ remotePort: number;
2052
+ }
2053
+ /**
2054
+ * Parse established connections from `ss -tnp`, `lsof -nP`, or PowerShell output
2055
+ * (pure, host-independent). Deduplicates and silently ignores unparseable rows.
2056
+ *
2057
+ * Recognized shapes:
2058
+ * - ss: `ESTAB 0 0 127.0.0.1:54321 127.0.0.1:5432 …`
2059
+ * - lsof: `node 1 u … TCP 127.0.0.1:54321->127.0.0.1:5432 (ESTABLISHED)`
2060
+ * - PS: `127.0.0.1:54321 -> 127.0.0.1:5432`
2061
+ */
2062
+ declare function parseEstablished(raw: string): EstablishedConn[];
2063
+ declare const connectionsScanner: Scanner;
2064
+
2065
+ /**
2066
+ * Service & reverse-proxy config scanner (3.2).
2067
+ *
2068
+ * Infers *declared* dependency edges from local config without running any service:
2069
+ * - connection-string env vars (`DATABASE_URL`, `REDIS_URL`, …) → `reads_from` /
2070
+ * `depends_on` to `${type}:localhost:${port}`, confidence `connection-string` (0.6);
2071
+ * - nginx `upstream` / `proxy_pass` host:port → `depends_on`, confidence `config-declared` (0.7);
2072
+ * - docker-compose `depends_on:` → `container:<svc> depends_on container:<dep>`, 0.7.
2073
+ *
2074
+ * **Security boundary:** connection strings routinely embed credentials and
2075
+ * `sanitizeUntrusted` (applied later in `insertEdge`) is NOT a credential filter, so
2076
+ * this scanner redacts userinfo / `password=` *before* the value reaches `evidence`.
2077
+ */
2078
+
2079
+ /**
2080
+ * Replace credentials in a connection string with `****` (the security boundary).
2081
+ * Strips the `user:pass@` userinfo and any `password=`/`pwd=` query token.
2082
+ */
2083
+ declare function redactConnectionString(url: string): string;
2084
+ /**
2085
+ * Parse nginx `upstream { server host:port; }` and `proxy_pass http://host:port`
2086
+ * directives into `{ host, port }` targets (pure, host-independent).
2087
+ */
2088
+ declare function parseNginxUpstreams(raw: string): {
2089
+ host: string;
2090
+ port: number;
2091
+ }[];
2092
+ /**
2093
+ * Parse docker-compose `depends_on:` blocks (both list and map form) into a
2094
+ * `{ service, dependsOn[] }` array (pure). Indentation-based, dependency-free.
2095
+ */
2096
+ declare function parseComposeDeps(raw: string): {
2097
+ service: string;
2098
+ dependsOn: string[];
2099
+ }[];
2100
+ /**
2101
+ * Map a connection-string env var to a candidate edge (pure). Returns the
2102
+ * relationship, the target service port, and a **credential-redacted** evidence
2103
+ * string — or null when the scheme/port is not recognized.
2104
+ */
2105
+ declare function parseConnectionString(name: string, url: string): {
2106
+ relationship: EdgeRelationship;
2107
+ service: string;
2108
+ port: number;
2109
+ evidence: string;
2110
+ } | null;
2111
+ declare const serviceConfigScanner: Scanner;
2112
+
2113
+ /**
2114
+ * Confidence rubric for inferred dependency edges (3.2).
2115
+ *
2116
+ * A single source of truth so every scanner that infers an edge draws its
2117
+ * `confidence` from the same strictly-ordered tier set, and downstream consumers
2118
+ * (compliance 3.4, anomaly 3.6) can rank evidence quality consistently. All values
2119
+ * are in `(0,1]`; confidence is never fabricated — an absent evidence source yields
2120
+ * no edge rather than a low-confidence guess.
2121
+ */
2122
+ /** Evidence tiers for inferred dependency edges, strongest first. */
2123
+ type EvidenceKind = 'established-connection' | 'config-declared' | 'connection-string' | 'co-location';
2124
+ /** Single source of truth for edge confidence. Strictly ordered, all in (0,1]. */
2125
+ declare const CONFIDENCE: Record<EvidenceKind, number>;
2126
+ /** Build a self-describing, auditable evidence string with an ISO-8601 UTC stamp. */
2127
+ declare function evidenceLine(kind: EvidenceKind, detail: string): string;
2128
+
2129
+ /**
2130
+ * Shared helpers for the cloud / k8s / database scanners.
2131
+ *
2132
+ * These keep the provider scanners (`cloud-aws`, `cloud-gcp`, `cloud-azure`,
2133
+ * `k8s`, `databases`) small and consistent: JSON parsing that never throws,
2134
+ * provider-parameter extraction from the single `ScanContext.hint` channel
2135
+ * (validated against the same strict patterns the agent tools use), and the
2136
+ * `=== KEY ===` report formatter so the structured + raw-report output shape
2137
+ * stays identical to the legacy agent-tool reports.
2138
+ */
2139
+ /**
2140
+ * Parse JSON CLI output. Returns `undefined` for empty output, our sentinel
2141
+ * placeholders (`(error or not available)`, `(skipped …)`), or malformed JSON —
2142
+ * it never throws, so a scanner degrades to "no nodes" instead of crashing the
2143
+ * discovery run.
2144
+ */
2145
+ declare function safeJson<T = unknown>(raw: string): T | undefined;
2146
+ /** Provider parameters parsed out of a {@link ScanContext.hint}. */
2147
+ interface ScanHintParams {
2148
+ namespace?: string;
2149
+ region?: string;
2150
+ profile?: string;
2151
+ project?: string;
2152
+ subscription?: string;
2153
+ resourceGroup?: string;
2154
+ /** Leftover non-`key=value` terms (back-compat with the installed-apps hint use). */
2155
+ free: string;
2156
+ }
2157
+ /**
2158
+ * Parse `key=value` provider parameters from a `ScanContext.hint`, validating
2159
+ * each value against its strict pattern via {@link assertSafeScanArg} (throws on
2160
+ * an injection payload — even a read-only one the allowlist would otherwise
2161
+ * permit). Unknown tokens flow through to {@link ScanHintParams.free} unchanged.
2162
+ */
2163
+ declare function parseScanHint(hint: string | undefined): ScanHintParams;
2164
+ /** Render `=== KEY ===\n…` report sections (the legacy agent-tool report shape). */
2165
+ declare function buildReport(sections: Array<[string, string]>): string;
2166
+
2167
+ /** The only surface a plugin's `register()` touches. */
2168
+ interface ScannerPluginApi {
2169
+ /** Register a scanner the host will namespace, command-gate, and freeze. */
2170
+ registerScanner(scanner: Scanner): void;
2171
+ }
2172
+ /** The default-exported shape a `@datasynx/scanner-*` package implements. */
2173
+ interface ScannerPlugin {
2174
+ /** Human-readable plugin name (informational; the package name is the identity). */
2175
+ name: string;
2176
+ /** Register the plugin's scanners through the host-provided API. */
2177
+ register(api: ScannerPluginApi): void;
2178
+ }
2179
+ /**
2180
+ * Identity helper a plugin author default-exports. It only narrows the type so
2181
+ * editors guide authoring; it adds no runtime behaviour.
2182
+ */
2183
+ declare function definePlugin(plugin: ScannerPlugin): ScannerPlugin;
2184
+ /**
2185
+ * Zod shape validating a {@link Scanner} produced by an external plugin.
2186
+ * `platforms` uses the concrete `Platform` literals so an invalid platform is
2187
+ * rejected at registration; `detect`/`scan` are validated as callables only
2188
+ * (their async signatures cannot be expressed in zod) — behavioural safety comes
2189
+ * from the gated `ctx.run` wrapper plus the per-scanner runtime try/catch.
2190
+ */
2191
+ declare const ScannerShape: z.ZodObject<{
2192
+ id: z.ZodString;
2193
+ title: z.ZodString;
2194
+ platforms: z.ZodUnion<readonly [z.ZodLiteral<"all">, z.ZodArray<z.ZodEnum<{
2195
+ linux: "linux";
2196
+ darwin: "darwin";
2197
+ win32: "win32";
2198
+ }>>]>;
2199
+ allowedCommands: z.ZodOptional<z.ZodArray<z.ZodString>>;
2200
+ detect: z.ZodCustom<(ctx: ScanContext) => boolean | Promise<boolean>, (ctx: ScanContext) => boolean | Promise<boolean>>;
2201
+ scan: z.ZodCustom<(ctx: ScanContext) => Promise<ScanResult>, (ctx: ScanContext) => Promise<ScanResult>>;
2202
+ }, z.core.$strip>;
2203
+ /**
2204
+ * Validate an unknown value as a {@link Scanner}; throws `ZodError` on mismatch.
2205
+ * Returns the original value (the parsed result discards the live function
2206
+ * references), so the caller keeps the real `detect`/`scan` closures.
2207
+ */
2208
+ declare function validateScanner(value: unknown): Scanner;
2209
+
433
2210
  /** A registry pre-loaded with the built-in deterministic scanners. */
434
2211
  declare function defaultRegistry(): ScannerRegistry;
435
2212
 
2213
+ /**
2214
+ * Opt-in scanner-plugin loader.
2215
+ *
2216
+ * Resolves the explicitly configured plugin package names (consent-first: no
2217
+ * filesystem auto-scan), dynamically `import()`s each in its own try/catch, and
2218
+ * registers its scanners through {@link ScannerRegistry.registerExternal}
2219
+ * (validated, namespaced, command-gated, frozen). One bad plugin — a
2220
+ * non-resolvable name, a missing/invalid default export, a `register()` that
2221
+ * throws, or a malformed scanner that fails zod validation — is logged via
2222
+ * `logWarn` and skipped. It never aborts discovery (optional-deps degrade
2223
+ * pattern, mirroring `src/semantic/search.ts`).
2224
+ */
2225
+
2226
+ /**
2227
+ * Load each configured plugin package into the given registry. Returns the
2228
+ * package names that loaded and registered successfully (for audit/reporting).
2229
+ */
2230
+ declare function loadPlugins(registry: ScannerRegistry, pkgs: readonly string[]): Promise<string[]>;
2231
+
436
2232
  /**
437
2233
  * Deterministic, LLM-free local discovery.
438
2234
  *
@@ -446,18 +2242,48 @@ declare function defaultRegistry(): ScannerRegistry;
446
2242
  interface LocalDiscoveryOptions {
447
2243
  hint?: string;
448
2244
  registry?: ScannerRegistry;
2245
+ /**
2246
+ * Opt-in scanner plugin package names to load (consent-first). Honoured only
2247
+ * when no explicit `registry` is supplied — an injected registry is used
2248
+ * verbatim, so tests and advanced callers stay in full control.
2249
+ */
2250
+ plugins?: string[];
449
2251
  /** Called after each scanner with a short progress line. */
450
2252
  onProgress?: (line: string) => void;
2253
+ /**
2254
+ * Override individual {@link ScanContext} fields. Production omits this and the
2255
+ * real platform helpers are used; tests inject deterministic sources
2256
+ * (`commandExists`, `scanListeningPorts`, `scanBookmarks`) so the built-in
2257
+ * scanners run against known inputs without depending on the host.
2258
+ */
2259
+ ctx?: Partial<ScanContext>;
2260
+ /**
2261
+ * `'replace'` (default) preserves today's append-all behavior: every scanned
2262
+ * node/edge is upserted/inserted into the session. `'update'` (2.1 incremental
2263
+ * discovery) reads the session's prior state, diffs it against the current scan,
2264
+ * and applies only the delta — upserting changed/added, pruning removed (nodes
2265
+ * and edges), and stamping `last_scanned_at`. Same `session_id` either way.
2266
+ */
2267
+ mode?: 'replace' | 'update';
451
2268
  }
452
- declare function runLocalDiscovery(db: CartographyDB, sessionId: string, opts?: LocalDiscoveryOptions): Promise<{
2269
+ interface LocalDiscoveryResult {
453
2270
  nodes: number;
454
2271
  edges: number;
455
2272
  scanners: string[];
456
- }>;
457
- /** Adapter matching the MCP `DiscoveryFn` signature. */
458
- declare function localDiscoveryFn(registry?: ScannerRegistry): (db: CartographyDB, sessionId: string, opts: {
2273
+ /** Present only in `'update'` mode: the delta applied to the session (2.1). */
2274
+ delta?: TopologyDelta;
2275
+ }
2276
+ declare function runLocalDiscovery(db: CartographyDB, sessionId: string, opts?: LocalDiscoveryOptions): Promise<LocalDiscoveryResult>;
2277
+ /**
2278
+ * Adapter matching the MCP `DiscoveryFn` signature. When no explicit `registry`
2279
+ * is provided, the opt-in `plugins` list is loaded onto the default registry on
2280
+ * each discovery run (so late-installed plugins are picked up).
2281
+ */
2282
+ declare function localDiscoveryFn(registry?: ScannerRegistry, plugins?: string[]): (db: CartographyDB, sessionId: string, opts: {
459
2283
  hint?: string;
2284
+ mode?: "replace" | "update";
460
2285
  }) => Promise<{
2286
+ delta?: TopologyDelta | undefined;
461
2287
  nodes: number;
462
2288
  edges: number;
463
2289
  }>;
@@ -520,12 +2346,175 @@ declare class VectorStore {
520
2346
  * or return nothing.
521
2347
  */
522
2348
 
2349
+ /** Options for {@link createSemanticSearch}. */
2350
+ interface SemanticSearchOptions {
2351
+ /** Logger for mode/degradation diagnostics (stderr). No-op if omitted. */
2352
+ log?: (msg: string) => void;
2353
+ }
523
2354
  /**
524
2355
  * Build a {@link SearchFn} that prefers semantic (vector) search and falls back to
525
2356
  * lexical. Pass an explicit embedder, or let it lazily load the local transformer
526
- * (returns a lexical-only function if none is available).
2357
+ * (returns a lexical-only function if none is available). Pass `opts.log` to record
2358
+ * which mode was resolved — semantic readiness or the reason it degraded to lexical.
2359
+ */
2360
+ declare function createSemanticSearch(db: CartographyDB, embedder?: EmbeddingProvider, opts?: SemanticSearchOptions): Promise<SearchFn>;
2361
+
2362
+ /**
2363
+ * Types for the `install` harness — the layer that writes Cartography's MCP
2364
+ * server entry into each host's native config file (JSON / TOML / YAML).
2365
+ *
2366
+ * A {@link ClientSpec} is a declarative description of one host: where its config
2367
+ * lives per OS and scope, what serialization format it uses, and how to splice a
2368
+ * server entry into that host's particular schema (`mcpServers`, `servers`,
2369
+ * `context_servers`, `[mcp_servers]`, `extensions`, …). The merge engine and CLI
2370
+ * are generic; all host-specific knowledge lives in specs.
2371
+ */
2372
+ type ConfigFormat = 'json' | 'toml' | 'yaml';
2373
+ type Scope = 'global' | 'project';
2374
+ type OsKind = 'mac' | 'win' | 'linux';
2375
+ /** A transport-agnostic description of the Cartography MCP server to register. */
2376
+ interface ServerEntry {
2377
+ /** stdio command (mutually exclusive with `url`). */
2378
+ command?: string;
2379
+ args?: string[];
2380
+ env?: Record<string, string>;
2381
+ /** Streamable HTTP endpoint (mutually exclusive with `command`). */
2382
+ url?: string;
2383
+ }
2384
+ /** Everything a spec needs to resolve a config path; injectable for tests. */
2385
+ interface ResolveContext {
2386
+ scope: Scope;
2387
+ os: OsKind;
2388
+ /** User home directory. */
2389
+ home: string;
2390
+ /** Project/working directory (for project-scoped configs). */
2391
+ cwd: string;
2392
+ /** Environment variables (for `%APPDATA%` etc. on Windows). */
2393
+ env: Record<string, string | undefined>;
2394
+ }
2395
+ interface ClientSpec {
2396
+ /** Stable id used on the CLI, e.g. `claude-code`. */
2397
+ id: string;
2398
+ /** Human label, e.g. `Claude Code`. */
2399
+ label: string;
2400
+ format: ConfigFormat;
2401
+ /** Resolve the absolute config path for the given scope/OS, or undefined if unsupported. */
2402
+ path(ctx: ResolveContext): string | undefined;
2403
+ /** Pure: return a new config object with `entry` spliced in under `serverName`. */
2404
+ apply(existing: Record<string, unknown>, serverName: string, entry: ServerEntry): Record<string, unknown>;
2405
+ /** Optional caveat surfaced to the user (e.g. "uses `servers`, not `mcpServers`"). */
2406
+ note?: string;
2407
+ }
2408
+
2409
+ /**
2410
+ * Format-agnostic (de)serialization for host config files. JSON keeps 2-space
2411
+ * indentation; TOML uses smol-toml; YAML uses the `yaml` package. Empty or
2412
+ * whitespace-only input parses to an empty object so a fresh install starts clean.
2413
+ */
2414
+
2415
+ declare function parseConfig(text: string, format: ConfigFormat): Record<string, unknown>;
2416
+ declare function serializeConfig(obj: Record<string, unknown>, format: ConfigFormat): string;
2417
+
2418
+ /**
2419
+ * Idempotent deep-merge of plain objects. Used by client specs to splice a server
2420
+ * entry into an existing config without clobbering unrelated keys. Arrays and
2421
+ * scalars from `source` replace those in `target`; nested plain objects merge
2422
+ * recursively. `source` is never mutated; `target` is cloned.
2423
+ */
2424
+ declare function deepMerge<T extends Record<string, unknown>>(target: T, source: Record<string, unknown>): T;
2425
+
2426
+ /**
2427
+ * Shared entry-shape helpers. Most hosts accept the Claude-Desktop-style object
2428
+ * (`command`/`args`/`env` for stdio, `url`/`type` for HTTP); specs that diverge
2429
+ * (VS Code `type`, Zed `source`, Codex TOML, …) compose or override these.
2430
+ */
2431
+
2432
+ /** The common `{ command, args, env }` | `{ type:'http', url }` server object. */
2433
+ declare function mcpServerObject(entry: ServerEntry): Record<string, unknown>;
2434
+
2435
+ /**
2436
+ * The canonical Cartography MCP server entry every client spec receives. Kept in
2437
+ * one place so the npx invocation (and any future packaging change) is defined
2438
+ * exactly once and reused across all hosts.
2439
+ */
2440
+
2441
+ declare const PACKAGE_NAME = "@datasynx/agentic-ai-cartography";
2442
+ declare const MCP_BIN = "cartography-mcp";
2443
+ declare const DEFAULT_SERVER_NAME = "cartography";
2444
+ interface EntryOptions {
2445
+ /** `http` produces a `url` entry; otherwise stdio via npx. */
2446
+ transport?: 'stdio' | 'http';
2447
+ /** HTTP endpoint (used when transport === 'http'). */
2448
+ url?: string;
2449
+ /** Extra environment variables to inject. */
2450
+ env?: Record<string, string>;
2451
+ /** Extra package arguments appended after the bin name (e.g. `--db`, `--session`). */
2452
+ packageArgs?: string[];
2453
+ }
2454
+ /** Build the default server entry (stdio via `npx` unless an HTTP url is given). */
2455
+ declare function defaultServerEntry(opts?: EntryOptions): ServerEntry;
2456
+
2457
+ /**
2458
+ * Generic install planning/applying. `planInstall` is pure relative to a provided
2459
+ * context (reads the existing file, computes the merged result, never writes) so
2460
+ * it powers both `--dry-run` and the real write in `applyInstall`.
2461
+ */
2462
+
2463
+ interface InstallPlan {
2464
+ client: string;
2465
+ label: string;
2466
+ path: string;
2467
+ format: ConfigFormat;
2468
+ /** Existing file contents ('' when the file does not exist). */
2469
+ before: string;
2470
+ /** Contents that would be written. */
2471
+ after: string;
2472
+ fileExists: boolean;
2473
+ changed: boolean;
2474
+ note?: string;
2475
+ }
2476
+ /** Detect the current OS kind. */
2477
+ declare function currentOs(): OsKind;
2478
+ /** Build a real resolve context from the running environment. */
2479
+ declare function defaultContext(scope: Scope): ResolveContext;
2480
+ interface PlanOptions {
2481
+ serverName?: string;
2482
+ entry: ServerEntry;
2483
+ }
2484
+ /** Compute what installing `entry` into `spec` would change. Reads the config file but never writes. */
2485
+ declare function planInstall(spec: ClientSpec, ctx: ResolveContext, opts: PlanOptions): InstallPlan;
2486
+ /** Write a plan's result to disk, creating parent directories as needed. */
2487
+ declare function applyInstall(plan: InstallPlan): void;
2488
+ /** A minimal line-oriented diff for `--dry-run` output. */
2489
+ declare function renderDiff(before: string, after: string): string;
2490
+
2491
+ /**
2492
+ * Registry of supported MCP hosts. The merge engine and CLI are generic; every
2493
+ * host-specific detail (config path, format, schema key) lives in a `ClientSpec`
2494
+ * here. New hosts are added by appending a spec — no engine changes required.
527
2495
  */
528
- declare function createSemanticSearch(db: CartographyDB, embedder?: EmbeddingProvider): Promise<SearchFn>;
2496
+
2497
+ /** All registered clients, in display order. Extended by later milestones. */
2498
+ declare const CLIENTS: ClientSpec[];
2499
+ declare function getClient(id: string): ClientSpec | undefined;
2500
+ declare function listClients(): ReadonlyArray<Pick<ClientSpec, 'id' | 'label' | 'format' | 'note'>>;
2501
+
2502
+ /**
2503
+ * One-click install deeplinks for hosts that support them. Cursor expects a
2504
+ * **Base64**-encoded server config; VS Code expects **URL-encoded** JSON — mixing
2505
+ * the two encodings is a classic mistake, so each has its own helper.
2506
+ */
2507
+
2508
+ /** `cursor://…/mcp/install?name=<name>&config=<base64 JSON of the server config>`. */
2509
+ declare function cursorDeeplink(name: string, entry: ServerEntry): string;
2510
+ interface VscodeDeeplinkOptions {
2511
+ /** Target VS Code Insiders (`vscode-insiders://`). */
2512
+ insiders?: boolean;
2513
+ }
2514
+ /** `vscode://mcp/install?<URL-encoded JSON>` where the JSON is `{ name, ...serverConfig }`. */
2515
+ declare function vscodeDeeplink(name: string, entry: ServerEntry, opts?: VscodeDeeplinkOptions): string;
2516
+ /** A `code --add-mcp '<json>'` CLI one-liner (alternative to the deeplink). */
2517
+ declare function codeAddMcpCommand(name: string, entry: ServerEntry): string;
529
2518
 
530
2519
  /**
531
2520
  * Circuit breaker for sequential CLI scans.
@@ -542,8 +2531,66 @@ declare function createScanRunner(runFn: (cmd: string, opts?: {
542
2531
  interface CartographyToolsOptions {
543
2532
  /** Called when the agent needs a human answer. Return the user's response. */
544
2533
  onAskUser?: (question: string, context?: string) => Promise<string>;
2534
+ /** Max characters of a single tool response (sanitized + truncated). Default 100 000. */
2535
+ maxResponseBytes?: number;
545
2536
  }
2537
+ /**
2538
+ * Sanitize untrusted scan output (strip invisible/control characters) and cap it
2539
+ * at `max` characters so a single verbose scan can never blow the agent's context
2540
+ * window. Appends an explicit truncation notice when clamped.
2541
+ */
2542
+ declare function clampText(raw: string, max: number): string;
546
2543
  declare function stripSensitive(target: string): string;
2544
+ /**
2545
+ * Strict patterns for the cloud/k8s scan parameters that get spliced into a
2546
+ * shell command. `run()` re-checks the final command against the read-only
2547
+ * allowlist, but values are validated here first so an injection payload never
2548
+ * reaches the shell — not even a read-only one (e.g. `; cat ~/.ssh/id_rsa`)
2549
+ * that the allowlist would otherwise permit and which could disclose files.
2550
+ */
2551
+ declare const SCAN_ARG_PATTERNS: {
2552
+ readonly 'k8s-namespace': RegExp;
2553
+ readonly 'aws-region': RegExp;
2554
+ readonly 'aws-profile': RegExp;
2555
+ readonly 'gcp-project': RegExp;
2556
+ readonly 'azure-subscription': RegExp;
2557
+ readonly 'azure-resource-group': RegExp;
2558
+ };
2559
+ type ScanArgKind = keyof typeof SCAN_ARG_PATTERNS;
2560
+ /** Throw if `value` fails the strict pattern for `kind`; otherwise return it. */
2561
+ declare function assertSafeScanArg(kind: ScanArgKind, value: string): string;
2562
+ /** Redact `user:password@` credentials embedded in any URL/DSN-like string. */
2563
+ declare function redactSecrets(value: string): string;
2564
+ /** Recursively redact secrets from arbitrary metadata before persistence. */
2565
+ declare function redactValue(value: unknown): unknown;
2566
+ /** The MCP text-content shape every tool handler returns. */
2567
+ interface ToolResult {
2568
+ content: {
2569
+ type: 'text';
2570
+ text: string;
2571
+ }[];
2572
+ }
2573
+ /**
2574
+ * Provider-neutral tool definition: a zod input shape plus a handler that returns
2575
+ * the MCP text-content shape. This is the single source of truth for the discovery
2576
+ * tools — the Claude provider wraps these via the SDK `tool()` factory, while
2577
+ * OpenAI/Ollama convert `inputShape` to JSON Schema and call `handler` directly.
2578
+ */
2579
+ interface AgentTool {
2580
+ name: string;
2581
+ description: string;
2582
+ /** Raw zod shape (ZodRawShape) for the tool input. */
2583
+ inputShape: z.ZodRawShape;
2584
+ /** Host-side gating hints (never a security boundary). */
2585
+ annotations: Record<string, boolean>;
2586
+ handler: (args: Record<string, unknown>) => Promise<ToolResult>;
2587
+ }
2588
+ /**
2589
+ * Build the discovery tool handlers with **no** SDK dependency. Returns the same
2590
+ * handler bodies used by the Claude path, exposed neutrally so any provider can
2591
+ * reuse them. `buildCartographyToolDefinitions` wraps these for the Claude SDK.
2592
+ */
2593
+ declare function buildCartographyToolHandlers(db: CartographyDB, sessionId: string, opts?: CartographyToolsOptions): Promise<AgentTool[]>;
547
2594
  declare function createCartographyTools(db: CartographyDB, sessionId: string, opts?: CartographyToolsOptions): Promise<McpServerConfig>;
548
2595
 
549
2596
  declare const safetyHook: HookCallback;
@@ -602,16 +2649,667 @@ type DiscoveryEvent = {
602
2649
  type AskUserFn = (question: string, context?: string) => Promise<string>;
603
2650
  declare function runDiscovery(config: CartographyConfig, db: CartographyDB, sessionId: string, onEvent?: (event: DiscoveryEvent) => void, onAskUser?: AskUserFn, hint?: string): Promise<void>;
604
2651
 
2652
+ /** Inputs a provider needs to run one discovery session. */
2653
+ interface AgentRunContext {
2654
+ config: CartographyConfig;
2655
+ db: CartographyDB;
2656
+ sessionId: string;
2657
+ systemPrompt: string;
2658
+ initialPrompt: string;
2659
+ onAskUser?: AskUserFn;
2660
+ /** Wall-clock deadline (epoch ms). Providers MUST stop and emit `done` past this. */
2661
+ deadlineMs: number;
2662
+ }
2663
+ /**
2664
+ * A provider-neutral agent backend. Yields the existing `DiscoveryEvent` stream.
2665
+ * Implementations MUST: enforce `config.maxTurns`, honor `ctx.deadlineMs`, route
2666
+ * every shell command through `checkReadOnly` + `run()`, and write an audit row per
2667
+ * executed tool so the audit trail is identical across providers.
2668
+ */
2669
+ interface AgentProvider {
2670
+ readonly name: ProviderName;
2671
+ /** Throw a clear Error if the optional dep / API key / CLI is missing. */
2672
+ ensureAvailable(config: CartographyConfig): Promise<void>;
2673
+ run(ctx: AgentRunContext): AsyncIterable<DiscoveryEvent>;
2674
+ }
2675
+ type ProviderFactory = () => AgentProvider;
2676
+ /** Registry of provider factories; the single source of truth for valid provider names. */
2677
+ declare class ProviderRegistry {
2678
+ private readonly factories;
2679
+ register(name: ProviderName, factory: ProviderFactory): void;
2680
+ has(name: string): name is ProviderName;
2681
+ resolve(name: ProviderName): AgentProvider;
2682
+ names(): ProviderName[];
2683
+ }
2684
+
2685
+ declare function createDefaultRegistry(): ProviderRegistry;
2686
+ declare const defaultProviderRegistry: ProviderRegistry;
2687
+
2688
+ declare function createClaudeProvider(): AgentProvider;
2689
+
2690
+ declare function createOpenAIProvider(): AgentProvider;
2691
+
2692
+ declare function createOllamaProvider(): AgentProvider;
2693
+
2694
+ interface JsonSchema {
2695
+ type: 'object';
2696
+ properties: Record<string, unknown>;
2697
+ required: string[];
2698
+ additionalProperties: boolean;
2699
+ }
2700
+ /** Convert a flat ZodRawShape used by the discovery tools to a JSON Schema object. */
2701
+ declare function shapeToJsonSchema(shape: z.ZodRawShape): JsonSchema;
2702
+
2703
+ declare function createBashTool(): AgentTool;
2704
+
2705
+ /**
2706
+ * Drift classification + dispatch (3.1).
2707
+ *
2708
+ * Layered on top of the pure topology delta (`diffTopology` → `db.diffSessions`):
2709
+ * `classifyDrift` turns a `TopologyDiff` into a severity-ranked `DriftAlert`
2710
+ * (deterministic, DB-agnostic, exhaustively unit-testable), and `runDrift` is the
2711
+ * one-shot runner — the seam scheduled discovery (2.5) calls after a scan — that
2712
+ * resolves the latest two sessions, classifies, threshold-filters, fans out to the
2713
+ * configured sinks (one sink's failure never aborts the others), and writes an
2714
+ * audit event. No new tables, no migration; read-only except the audit row.
2715
+ */
2716
+
2717
+ /** Return the highest severity among items; `info` for an empty list. */
2718
+ declare function maxSeverity(items: DriftAlertItem[]): Severity$1;
2719
+ /**
2720
+ * Names of the metadata keys whose value changed and which warrant escalation to
2721
+ * `critical`. Empty unless the node-changed touched `metadata` and a
2722
+ * security-relevant key (case-insensitive) actually differs before→after.
2723
+ */
2724
+ declare function securityRelevantChange(change: NodeChange): string[];
2725
+ /**
2726
+ * Classify a topology diff into a severity-tagged alert. Pure & deterministic
2727
+ * (only `generatedAt` depends on `now`). Rules:
2728
+ * - node-removed, edge-removed → warning (lost capability / connectivity)
2729
+ * - node-changed touching a security metadata → critical
2730
+ * key
2731
+ * - node-changed (other fields) → info
2732
+ * - node-added, edge-added → info
2733
+ * Overall severity = maxSeverity(items).
2734
+ */
2735
+ declare function classifyDrift(diff: TopologyDiff, now?: Date): DriftAlert;
2736
+ /** Drop items strictly below `min`; recompute the overall severity over what's kept. */
2737
+ declare function filterBySeverity(alert: DriftAlert, min: Severity$1): DriftAlert;
2738
+ interface RunDriftOptions {
2739
+ base?: string;
2740
+ current?: string;
2741
+ minSeverity?: Severity$1;
2742
+ }
2743
+ /**
2744
+ * Resolve base/current (newest-first `getSessions`, like the CLI/MCP surfaces),
2745
+ * classify, filter below `minSeverity`, dispatch to all configured sinks
2746
+ * (`Promise.allSettled` — one sink failure never aborts the others), and record an
2747
+ * audit event. Returns the alert, or `null` when fewer than two sessions exist
2748
+ * (graceful no-op). The seam scheduled discovery (2.5) calls post-scan.
2749
+ */
2750
+ declare function runDrift(db: CartographyDB, config: CartographyConfig, opts?: RunDriftOptions): Promise<DriftAlert | null>;
2751
+
2752
+ /**
2753
+ * Anomaly detection (3.6) — pure, deterministic flagging of standing structural risk.
2754
+ *
2755
+ * Knows nothing about the database; operates on plain `NodeRow[]` + an in-memory
2756
+ * degree map so it can be unit-tested in isolation and reused by the CLI, the MCP
2757
+ * server, and the scheduled-scan alert path (3.1). Same topology always yields the
2758
+ * same anomalies with identical `reason` strings (mirrors `deriveSessionName`). No
2759
+ * LLM, no I/O, no dependency. `reason` interpolates only the structured `nodeId` and
2760
+ * numeric scores — never raw node name/metadata (prompt-injection safe).
2761
+ */
2762
+
2763
+ /** Flag zero/weak-degree nodes. degree 0 → high; 1..orphanWeakDegree → low. */
2764
+ declare function detectOrphans(nodes: NodeRow[], degree: ReadonlyMap<string, number>, thresholds?: AnomalyThresholds): Anomaly[];
2765
+ /** Flag unmanaged-type or undomained low-confidence/quality nodes. */
2766
+ declare function detectShadowIt(nodes: NodeRow[], thresholds?: AnomalyThresholds): Anomaly[];
2767
+ /** Aggregate + stable sort (nodeId, then kind). Returns [] for an empty topology. */
2768
+ declare function detectAnomalies(nodes: NodeRow[], degree: ReadonlyMap<string, number>, thresholds?: AnomalyThresholds): Anomaly[];
2769
+ /** Anomalies present in `current` but absent in `base`, keyed by (nodeId, kind). Pure & order-stable. */
2770
+ declare function newAnomalies(base: Anomaly[], current: Anomaly[]): Anomaly[];
2771
+
2772
+ /**
2773
+ * Natural-language topology query resolver (3.5) — deterministic, LLM-free.
2774
+ *
2775
+ * Turns a plain-English question ("services that depend on the payments DB") into a
2776
+ * structured intent, then composes the existing primitives — the injected `SearchFn`
2777
+ * (semantic↔lexical degradation inherited for free) + `db.getDependencies` + a
2778
+ * post-traversal type filter — and echoes the parsed intent for explainability.
2779
+ * No LLM, no new exec path, read-only. The NL string is sanitized + length-clamped
2780
+ * (ReDoS-safe: only linear, anchored patterns).
2781
+ */
2782
+
2783
+ /** The relationship the operator asked about, in resolver-native terms. */
2784
+ type NlRelation = 'depends-on' | 'depended-on-by' | 'connected-to' | 'list';
2785
+ /** Maps a parsed relation to the `db.getDependencies` direction. Single source of truth. */
2786
+ declare const RELATION_TO_DIRECTION: {
2787
+ readonly 'depends-on': "downstream";
2788
+ readonly 'depended-on-by': "upstream";
2789
+ readonly 'connected-to': "both";
2790
+ readonly list: undefined;
2791
+ };
2792
+ /** Structured intent parsed from a natural-language question. */
2793
+ interface NlIntent {
2794
+ readonly query: string;
2795
+ readonly subjectQuery: string;
2796
+ readonly relation: NlRelation;
2797
+ readonly direction?: 'downstream' | 'upstream' | 'both';
2798
+ /** Concrete node types the *result* set is restricted to (post-traversal), if any. */
2799
+ readonly typeFilter?: readonly NodeType[];
2800
+ /** True when no relation pattern matched and we degraded to a search-only list. */
2801
+ readonly degraded: boolean;
2802
+ }
2803
+ interface NlQueryResult {
2804
+ readonly intent: NlIntent;
2805
+ readonly anchors: Array<{
2806
+ node: NodeRow;
2807
+ score?: number;
2808
+ }>;
2809
+ readonly nodes: Array<NodeRow & {
2810
+ depth?: number;
2811
+ }>;
2812
+ readonly paths: EdgeRow[];
2813
+ }
2814
+ interface NlQueryOptions {
2815
+ readonly maxDepth?: number;
2816
+ readonly anchorLimit?: number;
2817
+ }
2818
+ /** Parse a natural-language question into a structured {@link NlIntent}. Pure & deterministic. */
2819
+ declare function parseNlQuery(raw: string): NlIntent;
2820
+ /** Execute a parsed intent: anchor via `search`, traverse, then filter results by type. */
2821
+ declare function executeNlQuery(db: CartographyDB, sessionId: string, search: SearchFn, intent: NlIntent, opts?: NlQueryOptions): Promise<NlQueryResult>;
2822
+ /** Convenience: parse + execute in one call. */
2823
+ declare function resolveNlQuery(db: CartographyDB, sessionId: string, search: SearchFn, raw: string, opts?: NlQueryOptions): Promise<NlQueryResult>;
2824
+
2825
+ /**
2826
+ * Cost attribution (3.3) — turn the topology into a FinOps lens.
2827
+ *
2828
+ * A pluggable `CostSource` yields `{ owner, cost }` keyed by node id; `enrichCosts`
2829
+ * applies it to a session via targeted, idempotent UPDATEs (never `INSERT OR
2830
+ * REPLACE`, so other node fields are untouched). The first source is `CsvCostSource`
2831
+ * — deterministic, provider-agnostic, no new dependency. Live billing-API sources
2832
+ * (AWS Cost Explorer, GCP/Azure) are future implementations of the same interface.
2833
+ */
2834
+
2835
+ /** One attribution line keyed to a node. `cost`/`owner` may be partially present. */
2836
+ interface CostRecord {
2837
+ nodeId: string;
2838
+ owner?: string;
2839
+ cost?: CostEntry;
2840
+ }
2841
+ /** A pluggable provider of cost/owner attribution, keyed by node id. */
2842
+ interface CostSource {
2843
+ readonly id: string;
2844
+ /** Attribution keyed by node id. An absent/unauthorized source resolves to an empty map (degrade). */
2845
+ fetch(): Promise<Map<string, CostRecord>>;
2846
+ }
2847
+ interface EnrichResult {
2848
+ source: string;
2849
+ total: number;
2850
+ matched: number;
2851
+ unmatched: number;
2852
+ unmatchedIds: string[];
2853
+ }
2854
+ /** How a CSV row is resolved to a node id when no explicit `nodeId` column is given. */
2855
+ type MatchStrategy = 'nodeId' | 'name' | 'tag';
2856
+ interface CsvCostSourceOptions {
2857
+ filePath: string;
2858
+ match?: MatchStrategy;
2859
+ db?: CartographyDB;
2860
+ sessionId?: string;
2861
+ }
2862
+ /**
2863
+ * Parse a cost CSV (`nodeId,owner,amount,currency,period[,source]`, header required)
2864
+ * into validated `CostRecord[]`. Each row's cost fields are validated via
2865
+ * `CostEntrySchema`; a malformed row is skipped with a `logWarn` (counts only, no
2866
+ * owner PII) — the batch never aborts.
2867
+ */
2868
+ declare function parseCostCsv(text: string): CostRecord[];
2869
+ /** CSV-backed cost source. Resolves row→node ids per the chosen `MatchStrategy`. */
2870
+ declare class CsvCostSource implements CostSource {
2871
+ private readonly opts;
2872
+ readonly id: string;
2873
+ constructor(opts: CsvCostSourceOptions);
2874
+ fetch(): Promise<Map<string, CostRecord>>;
2875
+ }
2876
+ /**
2877
+ * Idempotent post-pass: apply a source's attribution to a session via targeted
2878
+ * UPDATEs. Re-running with the same source yields the same DB state. Unmatched
2879
+ * rows (no such node id) are reported, never written.
2880
+ */
2881
+ declare function enrichCosts(db: CartographyDB, sessionId: string, source: CostSource): Promise<EnrichResult>;
2882
+
605
2883
  declare function generateTopologyMermaid(nodes: NodeRow[], edges: EdgeRow[]): string;
606
2884
  declare function generateDependencyMermaid(nodes: NodeRow[], edges: EdgeRow[]): string;
2885
+ /**
2886
+ * Render a topology diff as a Mermaid graph: added nodes/edges in green, removed
2887
+ * in red, changed in amber. Endpoints of added/removed edges that are otherwise
2888
+ * unchanged are drawn as neutral "context" nodes so every edge has both ends.
2889
+ */
2890
+ declare function generateDiffMermaid(diff: TopologyDiff): string;
607
2891
  declare function exportBackstageYAML(nodes: NodeRow[], edges: EdgeRow[], org?: string): string;
608
2892
  declare function exportJSON(db: CartographyDB, sessionId: string): string;
609
2893
  declare function exportDiscoveryApp(nodes: NodeRow[], edges: EdgeRow[], options?: {
610
2894
  theme?: 'light' | 'dark';
611
2895
  }): string;
612
2896
  declare function exportJGF(nodes: NodeRow[], edges: EdgeRow[]): string;
2897
+ /**
2898
+ * Cost rolled up by domain and owner as CSV — the FinOps export. One block per
2899
+ * scope; rows are bucketed by `(currency, period)` so mixed currencies are never
2900
+ * summed into one figure.
2901
+ */
2902
+ declare function exportCostCSV(summary: GraphSummary): string;
2903
+ /** The same cost rollup as JSON for programmatic consumers. */
2904
+ declare function exportCostSummary(summary: GraphSummary): string;
2905
+ /**
2906
+ * Render a `ComplianceReport` as `json`, `markdown`, or `mermaid`. The Mermaid form
2907
+ * reuses the diff exporter's severity-coloured `classDef` pattern: one node per gap
2908
+ * coloured by severity, with a `"✓ No compliance gaps"` empty state.
2909
+ */
2910
+ declare function exportComplianceReport(report: ComplianceReport, format: 'json' | 'markdown' | 'mermaid'): string;
613
2911
  declare function exportAll(db: CartographyDB, sessionId: string, outputDir: string, formats?: string[]): void;
614
2912
 
2913
+ /**
2914
+ * Compliance scoring engine (3.4) — pure, deterministic, DB-free.
2915
+ *
2916
+ * `scoreTopology({nodes, edges}, ruleset)` mirrors `diffTopology`'s shape: plain
2917
+ * arrays in, a structured `ComplianceReport` out. The engine is the only trusted
2918
+ * evaluator of the declarative `RuleCheck` DSL — no `eval`, no ruleset-supplied
2919
+ * code or regex. Iteration order is stabilised (nodes + rules sorted by id) so the
2920
+ * report is byte-stable for a fixed `now`.
2921
+ */
2922
+
2923
+ interface ComplianceInput {
2924
+ nodes: NodeRow[];
2925
+ edges: EdgeRow[];
2926
+ }
2927
+ declare function evaluateCheck(node: NodeRow, check: RuleCheck): boolean;
2928
+ declare function evaluateRule(input: ComplianceInput, rule: ComplianceRule): ControlResult;
2929
+ /**
2930
+ * Score a topology against a ruleset. `score = round(100 × Σweight(passed applicable) /
2931
+ * Σweight(applicable))`, weighted by severity, with not-applicable controls excluded
2932
+ * from the denominator. `score = null` / `status = 'not_applicable'` when nothing applies.
2933
+ */
2934
+ declare function scoreTopology(input: ComplianceInput, ruleset: Ruleset, opts?: {
2935
+ now?: string;
2936
+ }): ComplianceReport;
2937
+ /** Validate raw ruleset data (used by the registry and any future import path). */
2938
+ declare function loadRuleset(raw: unknown): Ruleset;
2939
+
2940
+ /**
2941
+ * Bundled ruleset registry (3.4). All rulesets are plain data, validated at load.
2942
+ */
2943
+
2944
+ /** Resolve a bundled ruleset by name, or `undefined` (callers degrade, never throw). */
2945
+ declare function getRuleset(name: string): Ruleset | undefined;
2946
+ /** List the bundled rulesets (name/version/framework/rule count) for help + degrade messages. */
2947
+ declare function listRulesets(): Array<{
2948
+ name: string;
2949
+ version: string;
2950
+ framework: string;
2951
+ ruleCount: number;
2952
+ }>;
2953
+
2954
+ /**
2955
+ * Plain-text compliance report formatter (3.4). Colour-free so it lives outside
2956
+ * `cli.ts` (which owns the colour helpers and is excluded from coverage).
2957
+ */
2958
+
2959
+ /** Render a `ComplianceReport` as a plain-text summary with `✓`/`✗` markers. */
2960
+ declare function formatComplianceText(report: ComplianceReport): string;
2961
+
2962
+ /**
2963
+ * A destination for a classified drift alert. Implementations are the *only*
2964
+ * outbound surface of the drift feature; they must degrade gracefully and must
2965
+ * not throw for transient failures — log and resolve so the runner can continue
2966
+ * dispatching to the remaining sinks.
2967
+ */
2968
+ interface DriftSink {
2969
+ /** Stable identifier for logging/audit (e.g. 'stdout', 'webhook'). */
2970
+ readonly name: string;
2971
+ /** Dispatch one alert. Never throws for transient failures. */
2972
+ emit(alert: DriftAlert): Promise<void>;
2973
+ }
2974
+
2975
+ /**
2976
+ * Default sink: writes one severity-tagged, credential-redacted JSON line to
2977
+ * **stdout** (the data channel) and a one-line diagnostic to **stderr**, keeping
2978
+ * the stdout/stderr discipline the rest of the CLI follows. Fully local — no
2979
+ * network egress.
2980
+ */
2981
+ declare class StdoutSink implements DriftSink {
2982
+ readonly name = "stdout";
2983
+ emit(alert: DriftAlert): Promise<void>;
2984
+ }
2985
+
2986
+ interface WebhookSinkOptions {
2987
+ url: string;
2988
+ token?: string;
2989
+ timeoutMs?: number;
2990
+ }
2991
+ /**
2992
+ * Outbound sink: POSTs the alert as JSON to an operator-configured endpoint. The
2993
+ * first and only outbound network surface of the drift feature — off by default
2994
+ * (constructed only when a `webhook` sink is explicitly configured). Hardened:
2995
+ * - the body is **always** `redactValue(alert)`, so no `user:password@` secret
2996
+ * leaves the process;
2997
+ * - only `stripSensitive(url)` (host:port) is ever logged — never the full URL,
2998
+ * the bearer token, or the body;
2999
+ * - the target must be `https:` (SSRF / plaintext-exfil guard) — a plaintext
3000
+ * `http:` URL is refused unless the host is loopback or the documented
3001
+ * `CARTOGRAPHY_ALLOW_INSECURE_SYNC=1` escape hatch is set (mirrors `pushDeltas`);
3002
+ * - degrades to a logged no-op when `fetch` is unavailable or the URL is empty;
3003
+ * - never throws (the runner owns retry/abort policy).
3004
+ */
3005
+ declare class WebhookSink implements DriftSink {
3006
+ private readonly opts;
3007
+ readonly name = "webhook";
3008
+ constructor(opts: WebhookSinkOptions);
3009
+ emit(alert: DriftAlert): Promise<void>;
3010
+ }
3011
+
3012
+ /**
3013
+ * Construct sinks from config. Absent/empty config → `[new StdoutSink()]` (the
3014
+ * local default). A webhook sink's token falls back to `CARTOGRAPHY_DRIFT_TOKEN`
3015
+ * when not given explicitly (mirroring `CARTOGRAPHY_HTTP_TOKEN`). A webhook entry
3016
+ * without a url is skipped defensively (the schema already rejects it).
3017
+ */
3018
+ declare function buildSinks(drift?: DriftConfig): DriftSink[];
3019
+
3020
+ /** Raised when a config file cannot be read, parsed, or validated. */
3021
+ declare class ConfigError extends Error {
3022
+ constructor(message: string);
3023
+ }
3024
+ /**
3025
+ * Read and validate a JSON config file at `path`, returning a fully-resolved
3026
+ * {@link CartographyConfig}. Merges the file's `schedule`/`entryPoints`/`dbPath`/
3027
+ * `organization` into `defaultConfig` so every existing config invariant (e.g.
3028
+ * `agentModel === models.lead`) is preserved.
3029
+ *
3030
+ * Precedence for the shared `entryPoints`/`dbPath`: a value inside the `schedule`
3031
+ * block wins over the same file-level key (the schedule block is the more specific
3032
+ * intent for a scheduled run).
3033
+ *
3034
+ * @throws {ConfigError} when the file is missing/unreadable, the JSON is malformed,
3035
+ * or the content fails schema validation (including unknown keys via `.strict()`).
3036
+ */
3037
+ declare function loadConfig(path: string): CartographyConfig;
3038
+ /**
3039
+ * Lower-level reader: parse + validate a config file into its typed shape without
3040
+ * resolving it against `defaultConfig`. Useful for callers (e.g. WS 2.11) that need
3041
+ * the raw file shape rather than a merged runtime config.
3042
+ *
3043
+ * @throws {ConfigError} on missing file, malformed JSON, or schema-validation failure.
3044
+ */
3045
+ declare function readConfigFile(path: string): ConfigFile;
3046
+
3047
+ /**
3048
+ * Scheduled discovery (2.5).
3049
+ *
3050
+ * A thin, dependency-free cron driver over the existing read-only discovery and
3051
+ * drift machinery. Two pure pieces — {@link parseCron} and {@link nextRun} — own
3052
+ * a minimal 5-field cron grammar (UTC, no NPM dependency); {@link runOnce} runs a
3053
+ * single deterministic local scan and returns the topology drift relative to the
3054
+ * prior run. `runOnce` never invokes the Claude/agent loop and needs no API key.
3055
+ *
3056
+ * Reuse, not reimplementation: discovery is `runLocalDiscovery` and diffing is the
3057
+ * pure `diffTopology` engine (surfaced here via the 2.1 incremental `mode:'update'`
3058
+ * rescan, which already returns the delta). This module adds scheduling + per-run
3059
+ * persistence around them.
3060
+ */
3061
+
3062
+ /** Parsed allowed-value sets for each cron field (all UTC). */
3063
+ interface CronFields {
3064
+ /** 0–59 */
3065
+ minute: Set<number>;
3066
+ /** 0–23 */
3067
+ hour: Set<number>;
3068
+ /** 1–31 */
3069
+ dom: Set<number>;
3070
+ /** 1–12 */
3071
+ month: Set<number>;
3072
+ /** 0–6 (Sunday = 0) */
3073
+ dow: Set<number>;
3074
+ }
3075
+ /**
3076
+ * Parse a 5-field cron expression (`minute hour dom month dow`, UTC) into its
3077
+ * matching value sets. Grammar per field: star, star-slash-n, `a`, `a-b`,
3078
+ * `a-b`-slash-n, `a`-slash-n, and comma lists of those. Day-of-week `7`
3079
+ * normalizes to `0` (Sunday).
3080
+ *
3081
+ * @throws {RangeError} when the expression is not exactly 5 fields, or any field
3082
+ * is out of range / non-numeric / malformed.
3083
+ */
3084
+ declare function parseCron(expr: string): CronFields;
3085
+ /**
3086
+ * The earliest scheduled instant strictly after `after` (UTC, second/ms truncated).
3087
+ * Deterministic and pure: same `expr` + `after` always yields the same Date.
3088
+ *
3089
+ * @throws {RangeError} when `expr` is invalid, or no match is found within ~4 years.
3090
+ */
3091
+ declare function nextRun(expr: string, after: Date): Date;
3092
+ interface ScheduledRunResult {
3093
+ /** The session this run scanned (reused in place across runs). */
3094
+ sessionId: string;
3095
+ /** The prior session used as the diff base, or `undefined` on the first run. */
3096
+ baseSessionId?: string;
3097
+ /** Topology drift this run observed. */
3098
+ delta: TopologyDelta;
3099
+ /** Node count after the scan. */
3100
+ nodes: number;
3101
+ /** Edge count after the scan. */
3102
+ edges: number;
3103
+ /** Ids of the scanners that ran. */
3104
+ scanners: string[];
3105
+ }
3106
+ /**
3107
+ * Run one deterministic local-discovery pass and return its topology drift.
3108
+ *
3109
+ * Reuses the most recent prior `discover` session for this config's tenant and
3110
+ * rescans it in place via the 2.1 incremental `mode:'update'` path (same session
3111
+ * id, prunes vanished entities, stamps `last_scanned_at`), which already returns
3112
+ * the delta from the pure `diffTopology` engine. When no prior session exists, it
3113
+ * creates a fresh session, replace-scans it, and reports the whole topology as
3114
+ * `added` (diffed against an empty base).
3115
+ *
3116
+ * Read-only and API-key-free. Does **not** persist the drift run — the caller does
3117
+ * (so tests can inspect the delta first). All progress goes to stderr.
3118
+ */
3119
+ declare function runOnce(cfg: CartographyConfig, db: CartographyDB): Promise<ScheduledRunResult>;
3120
+
3121
+ /**
3122
+ * Share classifier (2.11) — pure, DB-agnostic.
3123
+ *
3124
+ * Buckets the items of a {@link SharePreview} (the 2.10 policy-transformed payload)
3125
+ * into `share` / `withhold` / `pending` against the persisted {@link SharingPolicy}
3126
+ * and the set of already-shared content hashes:
3127
+ *
3128
+ * - A node whose transformed payload is `null` (effective level `none`, incl. the
3129
+ * PERSONAL-host hard floor) is **withheld** — it never leaves.
3130
+ * - A node already pushed (its `shareHash` ∈ `sharedHashes`) is dropped (no bucket).
3131
+ * - A surviving node covered by the policy ({@link isRemembered}) is **share** — the
3132
+ * employee's policy is the standing consent, so it auto-approves with no re-prompt.
3133
+ * - A surviving node *not* covered by any policy rule/default is **pending** — new /
3134
+ * unmatched, queued for explicit review. Nothing in this bucket ever leaves until
3135
+ * the employee approves it (the load-bearing privacy invariant).
3136
+ *
3137
+ * Edges follow their endpoints: an edge is shareable only when both endpoints
3138
+ * survive (already guaranteed by `previewShare`, which drops dangling edges) and
3139
+ * both endpoint nodes land in `share`. Otherwise the edge is withheld.
3140
+ *
3141
+ * The transform/anonymization itself is **not** reimplemented here — it is consumed
3142
+ * from `previewShare`. This module only routes the already-transformed items.
3143
+ */
3144
+
3145
+ /** One classified, ready-to-queue item carrying its outgoing (transformed) payload. */
3146
+ interface ClassifiedItem {
3147
+ contentHash: string;
3148
+ kind: 'node' | 'edge';
3149
+ /** Original node id (for `node` items); the remapped source id for `edge` items. */
3150
+ nodeId?: string;
3151
+ /** The exact transformed bytes that would leave the machine. */
3152
+ payload: unknown;
3153
+ }
3154
+ interface ClassifyResult {
3155
+ /** Covered by policy → auto-approve (no re-prompt). */
3156
+ share: ClassifiedItem[];
3157
+ /** Effective level `none` / PERSONAL floor → suppressed; never leaves. */
3158
+ withhold: ClassifiedItem[];
3159
+ /** New / unmatched → queued for explicit human review. */
3160
+ pending: ClassifiedItem[];
3161
+ }
3162
+ interface ClassifyInput {
3163
+ preview: SharePreview;
3164
+ policy: SharingPolicy;
3165
+ /** `content_hash` values already pushed (status `shared`) — suppress re-share. */
3166
+ sharedHashes: ReadonlySet<string>;
3167
+ }
3168
+ /**
3169
+ * Route a {@link SharePreview} into share / withhold / pending buckets. Pure and
3170
+ * deterministic: same inputs always yield the same buckets. The payloads carried
3171
+ * here are the transformed ones from the preview, so anything routed to `share` is
3172
+ * already anonymized per policy.
3173
+ */
3174
+ declare function classify(input: ClassifyInput): ClassifyResult;
3175
+
3176
+ /**
3177
+ * Stable content hashing for the central-DB share queue (2.11).
3178
+ *
3179
+ * The hash is computed over the *policy-transformed* payload (what `previewShare`
3180
+ * produces — already anonymized/dropped), so two scans that yield the same outgoing
3181
+ * bytes map to the same `pending_shares` row (idempotent enqueue) and the same
3182
+ * server-side dedup key. It is deterministic via `stableStringify` (recursively
3183
+ * key-sorted JSON), so key ordering never perturbs the hash.
3184
+ *
3185
+ * Zero new dependencies — `node:crypto` + the existing `stableStringify`.
3186
+ */
3187
+ /** A node or edge projected to its outgoing (already-transformed) shape. */
3188
+ type ShareKind = 'node' | 'edge';
3189
+ /**
3190
+ * sha256 (hex) over the canonical JSON of `{ kind, payload }`. `payload` is the
3191
+ * transformed projection — for `anonymized`/`none` items it is the anonymized form,
3192
+ * never the raw record — so the hash is stable across scans and identical to the
3193
+ * value the central side can dedup on.
3194
+ */
3195
+ declare function shareHash(kind: ShareKind, payload: unknown): string;
3196
+
3197
+ /**
3198
+ * Outbound push client (2.11) — Cartograph's first egress path.
3199
+ *
3200
+ * Pushes consented, policy-transformed deltas to the central ingest endpoint over
3201
+ * bearer-auth HTTPS. It is the inverse of the inbound MCP auth in
3202
+ * `src/mcp/transports.ts`: instead of validating a bearer token, it *sends* one.
3203
+ *
3204
+ * Load-bearing privacy invariant: this function only ever sends the items it is
3205
+ * handed, which the caller (`sync push`) draws exclusively from `getApprovedShares()`
3206
+ * (status `approved`). The hard guards below additionally make a no-config / insecure
3207
+ * / empty send impossible. The bearer token is NEVER logged and NEVER placed in the
3208
+ * payload; error logging routes the URL through `stripSensitive`.
3209
+ *
3210
+ * Network is Node 20+ global `fetch`, injectable via `fetchImpl` so tests never hit
3211
+ * the network. Zero new dependencies.
3212
+ */
3213
+
3214
+ /** One item to push. `payload` is the already-policy-transformed (anonymized) projection. */
3215
+ interface PushItem {
3216
+ contentHash: string;
3217
+ kind: 'node' | 'edge';
3218
+ payload: unknown;
3219
+ }
3220
+ interface PushResult {
3221
+ /** Items the server acknowledged. */
3222
+ sent: number;
3223
+ /** Batches POSTed. */
3224
+ batches: number;
3225
+ /** Items in batches that ultimately failed (left for a later retry). */
3226
+ failed: number;
3227
+ /** content hashes the server acknowledged (caller flips these to `shared`). */
3228
+ sentHashes: string[];
3229
+ }
3230
+ interface PushOptions {
3231
+ /** Injectable fetch (tests). Defaults to Node global `fetch`. */
3232
+ fetchImpl?: typeof fetch;
3233
+ /** Items per batch. Defaults to `config.centralDb.batchSize ?? 100`. */
3234
+ batchSize?: number;
3235
+ /** Max retries per batch on 5xx / network errors. Default 4. */
3236
+ maxRetries?: number;
3237
+ /** Per-request timeout (ms). Default 15000. */
3238
+ timeoutMs?: number;
3239
+ /** Preview only — never networks. */
3240
+ dryRun?: boolean;
3241
+ /** Log sink (stderr by default); the token never reaches it. */
3242
+ log?: (line: string) => void;
3243
+ /** Sleep hook (tests inject a no-op to skip real backoff). */
3244
+ sleep?: (ms: number) => Promise<void>;
3245
+ }
3246
+ /** Wire-format version of the push envelope (the contract WS 2.12 ingests). */
3247
+ declare const PUSH_SCHEMA_VERSION: 1;
3248
+ /**
3249
+ * Push approved deltas. Returns counts and the acknowledged content hashes.
3250
+ *
3251
+ * Hard guards (the no-leak guarantee):
3252
+ * - missing `centralDb.url`/`token` → throws (nothing sent, fetch never called).
3253
+ * - non-`https:` URL → throws, unless `CARTOGRAPHY_ALLOW_INSECURE_SYNC === '1'`
3254
+ * (test-only, documented as unsafe; never the default).
3255
+ * - empty `items` → returns zeros without any network call.
3256
+ */
3257
+ declare function pushDeltas(config: CartographyConfig, items: PushItem[], opts?: PushOptions): Promise<PushResult>;
3258
+
3259
+ /**
3260
+ * Central-DB sync orchestration (2.11) — the post-scan enqueue glue.
3261
+ *
3262
+ * After a (manual or scheduled) scan, {@link runSyncClassify} resolves the
3263
+ * employee's persisted sharing policy (2.10), builds the policy-transformed payload
3264
+ * via `previewShare` (already anonymized/dropped — nothing raw for `anonymized`/
3265
+ * `none`), classifies it against the already-shared set, and enqueues the result
3266
+ * into `pending_shares`:
3267
+ *
3268
+ * - `share` → enqueued `approved` with `decided_by='rule'` (the policy *is* the
3269
+ * standing consent; never re-prompted).
3270
+ * - `pending` → enqueued `pending` for explicit review (nothing leaves until approved).
3271
+ * - `withhold`→ recorded `withheld` for the audit/`sync status` suppression count.
3272
+ *
3273
+ * Short-circuits to a no-op when `centralDb` is unconfigured, so an install without
3274
+ * sync configured never writes to the queue. All writes run in one transaction.
3275
+ */
3276
+
3277
+ interface SyncClassifyResult {
3278
+ enqueued: number;
3279
+ autoShared: number;
3280
+ withheld: number;
3281
+ }
3282
+ interface SyncClassifyOptions {
3283
+ /** Override the org-key path / namespace (tests). Defaults to `config.organization`. */
3284
+ orgKey?: Buffer;
3285
+ }
3286
+ /**
3287
+ * Classify a session's topology against the persisted policy and enqueue the
3288
+ * result. Returns counts for `pending` (`enqueued`), auto-approved (`autoShared`),
3289
+ * and suppressed (`withheld`) items. No-op (all zeros) when sync is unconfigured.
3290
+ */
3291
+ declare function runSyncClassify(db: CartographyDB, sessionId: string, config: CartographyConfig, opts?: SyncClassifyOptions): SyncClassifyResult;
3292
+
3293
+ /**
3294
+ * Sanitization of untrusted text before it enters the catalog or an LLM context
3295
+ * window. Discovery ingests text from sources outside our control — browser
3296
+ * bookmark titles, command output, scanner reports — which can carry hidden
3297
+ * prompt-injection payloads using invisible Unicode (zero-width spaces,
3298
+ * bidi/format controls, soft hyphens) or stray control characters.
3299
+ *
3300
+ * `sanitizeUntrusted` strips those while preserving ordinary whitespace
3301
+ * (tab/0x09, line feed/0x0A, carriage return/0x0D) and NFC-normalizes the
3302
+ * result. It is a no-op for normal ASCII/printable text.
3303
+ *
3304
+ * The set of stripped code points is defined numerically (below) rather than as
3305
+ * a regex of literal invisible characters, so the source stays pure ASCII and
3306
+ * auditable.
3307
+ */
3308
+ /** Strip invisible/control characters and NFC-normalize untrusted text. */
3309
+ declare function sanitizeUntrusted(text: string): string;
3310
+ /** Recursively apply `sanitizeUntrusted` to every string in an arbitrary value. */
3311
+ declare function sanitizeValue(value: unknown): unknown;
3312
+
615
3313
  /**
616
3314
  * Hex Grid Engine — flat-top axial coordinate system.
617
3315
  * Reference: https://www.redblobgames.com/grids/hexagons/
@@ -705,13 +3403,13 @@ declare function buildMapData(nodes: NodeRow[], edges: EdgeRow[], options?: {
705
3403
  theme?: 'light' | 'dark';
706
3404
  }): CartographyMapData;
707
3405
 
708
- declare function checkPrerequisites(): void;
709
-
710
3406
  /**
711
- * Remove orphaned temp files from previous bookmark/history scans.
712
- * Call at startup to prevent /tmp accumulation after crashes.
3407
+ * Provider-aware preflight. Defaults to `'claude'` so existing zero-arg callers are
3408
+ * unaffected. For non-Claude providers, checks only what each backend needs from the
3409
+ * environment; deeper reachability (e.g. Ollama host) is deferred to the provider's
3410
+ * `ensureAvailable`, which degrades at run with an actionable message.
713
3411
  */
714
- declare function cleanupTempFiles(): number;
3412
+ declare function checkPrerequisites(provider?: ProviderName): void;
715
3413
 
716
3414
  /**
717
3415
  * Structured logging for enterprise observability.
@@ -731,4 +3429,4 @@ declare function logInfo(message: string, context?: Record<string, unknown>): vo
731
3429
  declare function logWarn(message: string, context?: Record<string, unknown>): void;
732
3430
  declare function logError(message: string, context?: Record<string, unknown>): void;
733
3431
 
734
- export { type CartographyConfig, CartographyDB, type CartographyMapData, type Cluster, ClusterSchema, type Connection, ConnectionSchema, type CreateMcpServerOptions, DOMAIN_COLORS, DOMAIN_PALETTE, type DataAsset, DataAssetSchema, type DiscoveryEdge, type DiscoveryEvent, type DiscoveryFn, type DiscoveryNode, EDGE_RELATIONSHIPS, type EdgeRelationship, type EdgeRow, EdgeSchema, type EmbeddingProvider, type GraphSummary, type HttpOptions, type LocalDiscoveryOptions, type LogEntry, type LogLevel, NODE_TYPES, NODE_TYPE_GROUPS, type NodeRow, NodeSchema, type NodeType, type PolicyResult, type ScanContext, type ScanResult, type Scanner, ScannerRegistry, type SearchFn, type SessionRow, type ShellKind, type TraversalResult, VectorStore, assertReadOnly, assignColors, bookmarksScanner, buildMapData, checkPrerequisites, checkReadOnly, cleanupTempFiles, computeCentroid, computeClusterBounds, createCartographyTools, createHashEmbedder, createLocalEmbedder, createMcpServer, createScanRunner, createSemanticSearch, defaultConfig, defaultRegistry, edgesToConnections, exportAll, exportBackstageYAML, exportDiscoveryApp, exportJGF, exportJSON, extractListeningPorts, generateDependencyMermaid, generateTopologyMermaid, groupByDomain, hexCorners, hexDistance, hexNeighbors, hexRing, hexSpiral, hexToPixel, installedAppsScanner, isReadOnlyCommand, layoutClusters, localDiscoveryFn, log, logDebug, logError, logInfo, logWarn, nodesToAssets, pixelToHex, portsScanner, runDiscovery, runHttp, runLocalDiscovery, runStdio, safeEnv, safetyHook, setVerbose, shadeVariant, splitSegments, stripSensitive };
3432
+ export { ANOMALY_KINDS, ANOMALY_SEVERITIES, type AgentProvider, type AgentRunContext, type AgentTool, type Anomaly, type AnomalyConfig, type AnomalyKind, type AnomalySeverity, type AnomalyThresholds, type AnonViolation, type AnonymizationLevel, type AskUserFn, CLIENTS, CONFIDENCE, COST_PERIODS, type CartographyConfig, CartographyDB, type CartographyMapData, type CentralDbConfig, CentralDbConfigSchema, type ClassifiedItem, type ClassifyInput, type ClassifyResult, type ClientSpec, type Cluster, ClusterSchema, type ComplianceInput, type ComplianceReport, ComplianceReportSchema, type ComplianceRule, ComplianceRuleSchema, type Condition, ConditionSchema, ConfigError, type ConfigFile, ConfigFileSchema, type ConfigFormat, type Connection, ConnectionSchema, type Contributor, type ControlResult, ControlResultSchema, type CostEntry, CostEntrySchema, type CostPeriod, type CostRecord, type CostSource, type CreateMcpServerOptions, type CronFields, CsvCostSource, type CsvCostSourceOptions, DEFAULT_ANOMALY_THRESHOLDS, DEFAULT_FAST_MODEL, DEFAULT_LEAD_MODEL, DEFAULT_SERVER_NAME, DEFAULT_TENANT, DOMAIN_COLORS, DOMAIN_PALETTE, DRIFT_FIELDS, type DataAsset, DataAssetSchema, type DiscoveryEdge, type DiscoveryEvent, type DiscoveryFn, type DiscoveryNode, type DriftAlert, type DriftAlertItem, type DriftConfig, DriftConfigSchema, type DriftField, type DriftItemKind, type DriftRunRow, type DriftSink, type DriftSinkConfig, EDGE_RELATIONSHIPS, type EdgeRelationship, type EdgeRow, EdgeSchema, type EmbeddingProvider, type EnrichResult, type EntryOptions, type EstablishedConn, type EvidenceKind, type FragmentKind, type GraphSummary, type HttpOptions, INGEST_SCHEMA_VERSION, type IngestEnvelope, IngestEnvelopeSchema, type IngestHandler, type IngestOptions, type IngestResponse, type IngestResult, type InstallPlan, type LocalDiscoveryOptions, type LocalDiscoveryResult, type LogEntry, type LogLevel, MCP_BIN, type MatchStrategy, NODE_TYPES, NODE_TYPE_GROUPS, type NlIntent, type NlQueryOptions, type NlQueryResult, type NlRelation, type NodeAttribution, type NodeChange, type NodeIdentity, type NodeRow, NodeSchema, type NodeType, OUTPUT_FORMATS, type OrgKeyOptions, type OrgSummary, type OsKind, type OutputFormat, PACKAGE_NAME, PENDING_STATUSES, PERSONAL, PORT_MAP, PRIVATE_IP, PUSH_SCHEMA_VERSION, type PendingShareRow, type PendingStatus, type PlanOptions, type PolicyResult, type ProviderFactory, type ProviderName, ProviderRegistry, type PushItem, type PushOptions, type PushResult, RELATION_TO_DIRECTION, type ResolveContext, type RuleCheck, RuleCheckSchema, type RuleScope, type Ruleset, RulesetSchema, type RunDriftOptions, SCAN_ARG_PATTERNS, SECURITY_METADATA_KEYS, SEVERITIES, SEVERITY_WEIGHT, SHARING_LEVELS, type ScanArgKind, type ScanContext, type ScanHintParams, type ScanResult, type Scanner, type ScannerPlugin, type ScannerPluginApi, ScannerRegistry, ScannerShape, type ScheduleConfig, ScheduleConfigSchema, type ScheduledRunResult, type Scope, type SearchFn, type SemanticSearchOptions, type ServerEntry, type SessionRow, type Severity, type SharePreview, type SharePreviewEntry, type SharingLevel, SharingLevelSchema, type SharingPolicy, type ShellKind, SqliteStoreBackend, StdoutSink, type StoreBackend, type SyncClassifyOptions, type SyncClassifyResult, type ToolResult, type TopologyDelta, type TopologyDiff, type TopologyInput, type TraversalResult, VectorStore, WebhookSink, type WebhookSinkOptions, applyInstall, applySharingLevel, assertReadOnly, assertSafeScanArg, assignColors, bookmarksScanner, buildCartographyToolHandlers, buildMapData, buildReport, buildSinks, centralDbFromEnv, checkPrerequisites, checkReadOnly, clampText, classify, classifyDrift, cleanupTempFiles, cloudAwsScanner, cloudAzureScanner, cloudGcpScanner, codeAddMcpCommand, computeCentroid, computeClusterBounds, computeIdentity, connectionsScanner, contentHash, createBashTool, createCartographyTools, createClaudeProvider, createDefaultRegistry, createHashEmbedder, createIngestHandler, createLocalEmbedder, createMcpServer, createOllamaProvider, createOpenAIProvider, createScanRunner, createSemanticSearch, currentOs, cursorDeeplink, databasesScanner, deepMerge, defaultConfig, defaultContext, defaultProviderRegistry, defaultRegistry, defaultServerEntry, definePlugin, deriveSessionName, detectAnomalies, detectOrphans, detectShadowIt, diffTopology, edgesToConnections, enrichCosts, evaluateCheck, evaluateRule, evidenceLine, executeNlQuery, exportAll, exportBackstageYAML, exportComplianceReport, exportCostCSV, exportCostSummary, exportDiscoveryApp, exportJGF, exportJSON, extractListeningPorts, filterBySeverity, findAnonViolations, formatComplianceText, generateDependencyMermaid, generateDiffMermaid, generateTopologyMermaid, getClient, getRuleset, globalId, groupByDomain, hexCorners, hexDistance, hexNeighbors, hexRing, hexSpiral, hexToPixel, hmacKey, hostname, ingestEnvelope, installedAppsScanner, isPersonalHost, isReadOnlyCommand, isRemembered, k8sScanner, keyMetaOf, layoutClusters, listClients, listRulesets, loadConfig, loadOrgKey, loadPlugins, loadRuleset, localDiscoveryFn, log, logDebug, logError, logInfo, logWarn, machineId, maxSeverity, mcpServerObject, newAnomalies, nextRun, nodesToAssets, normalizeId, normalizeTenant, orgKeyPath, osUser, parseComposeDeps, parseConfig, parseConnectionString, parseCostCsv, parseCron, parseEstablished, parseNginxUpstreams, parseNlQuery, parseScanHint, pixelToHex, planInstall, portsScanner, previewShare, pseudonymize, pseudonymizeFragment, pseudonymizeString, pushDeltas, readConfigFile, redactConnectionString, redactSecrets, redactValue, renderDiff, resolveEffectiveLevel, resolveNlQuery, resolveSharingLevel, revalidateAnonymized, reversalKey, reversePseudonym, rotateOrgKey, runDiscovery, runDrift, runHttp, runLocalDiscovery, runOnce, runStdio, runSyncClassify, safeEnv, safeJson, safetyHook, sanitizeUntrusted, sanitizeValue, scoreTopology, securityRelevantChange, serializeConfig, serviceConfigScanner, setVerbose, shadeVariant, shapeToJsonSchema, shareHash, splitSegments, stableStringify, stripSensitive, validateScanner, vscodeDeeplink };