@shieldaai/agent-map 0.1.0-alpha.3 → 1.0.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 (96) hide show
  1. package/README.md +652 -272
  2. package/dist/assets.js +16 -26
  3. package/dist/boundary-flags.d.ts +73 -0
  4. package/dist/boundary-flags.js +352 -0
  5. package/dist/boundary.d.ts +18 -3
  6. package/dist/boundary.js +71 -21
  7. package/dist/cli-entry.d.ts +11 -0
  8. package/dist/cli-entry.js +48 -0
  9. package/dist/cli.js +40 -43
  10. package/dist/common.d.ts +34 -0
  11. package/dist/common.js +94 -0
  12. package/dist/http.js +78 -51
  13. package/dist/identity-cli.js +93 -46
  14. package/dist/identity-http.js +226 -94
  15. package/dist/index.d.ts +4 -4
  16. package/dist/index.js +4 -4
  17. package/dist/mcp.js +40 -5
  18. package/dist/oidc.d.ts +5 -0
  19. package/dist/oidc.js +121 -38
  20. package/dist/policy-reader.d.ts +56 -3
  21. package/dist/policy-reader.js +823 -110
  22. package/dist/projection-fields.d.ts +4 -0
  23. package/dist/projection-fields.js +144 -0
  24. package/dist/service/changes.d.ts +5 -0
  25. package/dist/service/changes.js +255 -0
  26. package/dist/service/context.d.ts +33 -0
  27. package/dist/service/context.js +2 -0
  28. package/dist/service/discovery.d.ts +6 -0
  29. package/dist/service/discovery.js +31 -0
  30. package/dist/service/explain.d.ts +12 -0
  31. package/dist/service/explain.js +126 -0
  32. package/dist/service/flags.d.ts +20 -0
  33. package/dist/service/flags.js +70 -0
  34. package/dist/service/inventory.d.ts +4 -0
  35. package/dist/service/inventory.js +62 -0
  36. package/dist/service/json.d.ts +8 -0
  37. package/dist/service/json.js +7 -0
  38. package/dist/service/operations/discovery.d.ts +5 -0
  39. package/dist/service/operations/discovery.js +90 -0
  40. package/dist/service/operations/explain-access.d.ts +3 -0
  41. package/dist/service/operations/explain-access.js +25 -0
  42. package/dist/service/operations/find-agents-with-access.d.ts +3 -0
  43. package/dist/service/operations/find-agents-with-access.js +120 -0
  44. package/dist/service/operations/get-agent-access.d.ts +3 -0
  45. package/dist/service/operations/get-agent-access.js +75 -0
  46. package/dist/service/operations/get-agent-inventory.d.ts +3 -0
  47. package/dist/service/operations/get-agent-inventory.js +42 -0
  48. package/dist/service/operations/get-changes.d.ts +3 -0
  49. package/dist/service/operations/get-changes.js +11 -0
  50. package/dist/service/operations/get-inventory-health.d.ts +3 -0
  51. package/dist/service/operations/get-inventory-health.js +81 -0
  52. package/dist/service/operations/get-resource-access.d.ts +3 -0
  53. package/dist/service/operations/get-resource-access.js +48 -0
  54. package/dist/service/operations/list-agents.d.ts +3 -0
  55. package/dist/service/operations/list-agents.js +92 -0
  56. package/dist/service/operations/list-owners-resources.d.ts +5 -0
  57. package/dist/service/operations/list-owners-resources.js +69 -0
  58. package/dist/service/ordering.d.ts +9 -0
  59. package/dist/service/ordering.js +47 -0
  60. package/dist/service/outside-guard.d.ts +68 -0
  61. package/dist/service/outside-guard.js +313 -0
  62. package/dist/service/policy.d.ts +26 -0
  63. package/dist/service/policy.js +299 -0
  64. package/dist/service/schemas.d.ts +16 -0
  65. package/dist/service/schemas.js +192 -0
  66. package/dist/service/summaries.d.ts +32 -0
  67. package/dist/service/summaries.js +247 -0
  68. package/dist/service/work.d.ts +33 -0
  69. package/dist/service/work.js +116 -0
  70. package/dist/service.d.ts +5 -5
  71. package/dist/service.js +110 -645
  72. package/dist/store/ancestors.d.ts +9 -0
  73. package/dist/store/ancestors.js +86 -0
  74. package/dist/store/discovery.d.ts +8 -0
  75. package/dist/store/discovery.js +288 -0
  76. package/dist/store/evidence.d.ts +12 -0
  77. package/dist/store/evidence.js +90 -0
  78. package/dist/store/identity-scope.d.ts +9 -0
  79. package/dist/store/identity-scope.js +71 -0
  80. package/dist/store/nodes.d.ts +24 -0
  81. package/dist/store/nodes.js +237 -0
  82. package/dist/store/projection.d.ts +19 -0
  83. package/dist/store/projection.js +242 -0
  84. package/dist/store/queries.d.ts +48 -0
  85. package/dist/store/queries.js +302 -0
  86. package/dist/store/read.d.ts +81 -0
  87. package/dist/store/read.js +135 -0
  88. package/dist/store/selection.d.ts +3 -0
  89. package/dist/store/selection.js +102 -0
  90. package/dist/store.d.ts +15 -0
  91. package/dist/store.js +102 -395
  92. package/dist/transport.d.ts +42 -2
  93. package/dist/transport.js +144 -16
  94. package/dist/types.d.ts +184 -4
  95. package/dist/types.js +52 -11
  96. package/package.json +16 -3
package/README.md CHANGED
@@ -1,313 +1,693 @@
1
- # Agent Guard reference map
1
+ # @shieldaai/agent-map
2
+
3
+ A read-only inventory service for AI agents. It answers questions such as "which agents
4
+ exist and who owns them", "what was this agent granted", "which agents could touch this
5
+ resource" and "what changed since the last snapshot", over HTTP, over MCP and for a
6
+ browser viewer. It reads a Neo4j graph and never prepares, approves or executes anything.
7
+
8
+ **Experimental.** The package shares the Agent Guard version number, but its API and
9
+ graph model are not covered by the 1.x compatibility promise and may change in a minor
10
+ release.
11
+
12
+ What it shows is evidence: declared grants, host observations and cloud policy
13
+ candidates. None of it is a decision that access is allowed, and a missing fact is
14
+ reported as `unknown`, never as "none".
15
+
16
+ ## Before you start
17
+
18
+ You need three things. Only the first is on npm.
19
+
20
+ 1. **Node.js 22.13 or later** on a POSIX host (Linux or macOS). The launchers need
21
+ non-blocking, no-follow file opens; native Windows is not supported.
22
+ 2. **Neo4j 5.23 or later**, reachable over verified TLS (`neo4j+s://` or `bolt+s://`), or
23
+ over plain `bolt://` on a literal loopback address only.
24
+ 3. **A graph filled by the Agent Guard Cartography collector.** The collector is a Python
25
+ program in the Agent Guard repository and is **not published to npm**. It reads an
26
+ executor's inventory and a host manifest and publishes immutable snapshots that this
27
+ service reads. Follow the
28
+ [collector README](https://github.com/ShieldaAI/agent-guard/blob/main/integrations/cartography/README.md) and the
29
+ [setup guide](https://github.com/ShieldaAI/agent-guard/blob/main/docs/guides/agent-cartography.md).
30
+
31
+ Without a collector-filled graph the service starts and answers, with nothing in it. The
32
+ [quick start](#quick-start) below needs neither Neo4j nor the collector: it runs the
33
+ service over an in-memory store so you can see the interface first.
34
+
35
+ The map lists only agents that a person declared in a host manifest. It does not scan
36
+ infrastructure, code, networks or identity providers. See
37
+ [how agents get onto the map](https://github.com/ShieldaAI/agent-guard/blob/main/docs/guides/agent-cartography.md#how-agents-get-onto-the-map).
38
+
39
+ ## Capacity
40
+
41
+ Sized for roughly **150 declared agents per reader** with the default limits, and
42
+ roughly **800** with the store's read limits raised to their maximum (`maxNodes: 10000`,
43
+ `maxEdges: 25000`, `maxBytes: 16777216`; see [Limits](#limits)). These are estimates,
44
+ not measurements: the node limit divided by about a dozen graph nodes per agent. An
45
+ agent with more tasks, grants or bindings counts for more.
46
+
47
+ Every operation answers from one bounded read of the graph the reader may see. Paging
48
+ pages that read's result; it does not reach past the read. A larger fleet fails with
49
+ `MAP_LIMIT_EXCEEDED` instead of returning part of it. `list_agents` reads fewer nodes per
50
+ agent and so reaches further; `get_changes` reads two snapshots within the same limits
51
+ and reaches about half as far. [Limits](#limits) gives the estimate for each kind of read
52
+ and says what was tested.
53
+
54
+ Values above the default limits are accepted, but they have only been exercised against
55
+ a fake database driver in this package's tests, never against Neo4j. The 5-second
56
+ transaction deadline does not grow with them. Before you rely on raised limits, run your
57
+ largest reads against your own database and check that they finish in time.
58
+
59
+ ## Quick start
2
60
 
3
- Query AI agent and MCP inventory, snapshot changes and access explanations over
4
- scoped HTTP and MCP interfaces. See the
5
- [package selection guide](https://github.com/ShieldaAI/agent-guard/blob/main/docs/package-guide.md)
6
- for tool authorization and model response validation.
61
+ ```sh
62
+ mkdir map-try && cd map-try
63
+ npm init -y
64
+ npm install @shieldaai/agent-map
65
+ ```
66
+
67
+ Save this as `try.mjs` and run `node try.mjs`. It starts the loopback service over an
68
+ in-memory store with one agent, asks it one question over HTTP and stops.
69
+
70
+ ```js
71
+ import { MapService, createMapToken, startMapServer } from '@shieldaai/agent-map';
72
+
73
+ // A store is anything with `read(scope, request)`. This one holds one declared agent in memory.
74
+ const projection = {
75
+ id: 'projection-1',
76
+ snapshotId: 'snapshot-1',
77
+ generatedAt: new Date().toISOString(),
78
+ status: 'published',
79
+ nodes: [
80
+ {
81
+ id: 'node-1',
82
+ entityId: 'agent:support',
83
+ label: 'GuardAgent',
84
+ sourceId: 'demo',
85
+ projectionId: 'projection-1',
86
+ properties: { agent_id: 'support-assistant', name: 'Support assistant', team_id: 'support' },
87
+ },
88
+ ],
89
+ edges: [],
90
+ coverage: { tasks: { status: 'complete' } },
91
+ };
92
+ const store = {
93
+ async read() {
94
+ return {
95
+ sources: [
96
+ { id: 'demo', tenantId: 'engineering', sourceId: 'demo', profile: 'host', current: projection, previous: null },
97
+ ],
98
+ };
99
+ },
100
+ };
101
+
102
+ const token = createMapToken();
103
+ const server = await startMapServer({
104
+ service: new MapService(store),
105
+ credentials: [
106
+ {
107
+ token,
108
+ scope: { subject: 'me', companyId: 'example', tenantIds: ['engineering'], teamIds: [], allTeams: true },
109
+ },
110
+ ],
111
+ });
112
+ const response = await fetch(`${server.url}/v1/query`, {
113
+ method: 'POST',
114
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
115
+ body: JSON.stringify({ operation: 'list_agents', args: {} }),
116
+ });
117
+ const { data } = await response.json();
118
+ console.log(data.agents.map(agent => `${agent.name} (${agent.lifecycleState}, ${agent.ownershipStatus})`));
119
+ await server.close();
120
+ ```
7
121
 
8
- Optional read-only reference service for the immutable Agent Guard projections
9
- published by the [Cartography collector](../../integrations/cartography/README.md).
10
- It supplies the same evidence to HTTP, MCP clients and the
11
- [React/Cytoscape viewer](../../apps/agent-map/README.md). It never prepares or
12
- executes an action. Declared grants and cloud associations are not effective
13
- permission decisions.
122
+ It prints `[ 'Support assistant (unknown, unknown)' ]`: the agent is listed, and the
123
+ lifecycle and ownership nobody declared read `unknown`.
124
+
125
+ ### Run it against Neo4j
126
+
127
+ Write a configuration file (see [Configuration](#configuration)), keep the secrets in the
128
+ environment, and start the launcher:
129
+
130
+ ```json
131
+ {
132
+ "neo4j": {
133
+ "uri": "bolt://127.0.0.1:7687",
134
+ "username": "agent_map_reader",
135
+ "passwordEnv": "AGENT_MAP_NEO4J_PASSWORD"
136
+ },
137
+ "credentials": [
138
+ {
139
+ "tokenEnv": "AGENT_MAP_READER_TOKEN",
140
+ "scope": {
141
+ "subject": "platform-reader",
142
+ "companyId": "example-company",
143
+ "tenantIds": ["engineering"],
144
+ "teamIds": ["platform"],
145
+ "allTeams": false
146
+ }
147
+ }
148
+ ]
149
+ }
150
+ ```
14
151
 
15
- Install the service with `npm install @shieldaai/agent-map@alpha`. It includes the
16
- `agent-guard-map` and `agent-guard-map-identity` commands. Viewer assets and the
17
- Python collector are separate repository components and are not bundled in this
18
- npm package. The commands require host-owned configuration and credentials.
152
+ ```sh
153
+ export AGENT_MAP_NEO4J_PASSWORD='...'
154
+ export AGENT_MAP_READER_TOKEN="$(node -e "import('@shieldaai/agent-map').then(m => console.log(m.createMapToken()))")"
155
+ npx agent-guard-map --config ./map.json
156
+ ```
19
157
 
20
- ## Run the loopback bearer service
158
+ The launcher prints `{"url":"http://127.0.0.1:8789","mode":"read-only"}` and serves
159
+ `POST /v1/query` and `POST /mcp` on that loopback address. Send the token as
160
+ `Authorization: Bearer <token>`:
21
161
 
22
162
  ```sh
23
- npm ci --ignore-scripts
24
- npm run build
25
- node packages/agent-map/dist/cli.js --config deployment/cartography/reference.example.json
163
+ curl -s http://127.0.0.1:8789/v1/query \
164
+ -H "Authorization: Bearer $AGENT_MAP_READER_TOKEN" \
165
+ -H 'Content-Type: application/json' \
166
+ -d '{"operation":"list_agents","args":{}}'
26
167
  ```
27
168
 
28
- Review the example's company, tenant and team identifiers, Neo4j address and
29
- username before use. Provide `AGENT_MAP_NEO4J_PASSWORD` and
30
- `AGENT_MAP_READER_TOKEN` through your host's secret environment. Generate the
31
- reader token with `createMapToken()` and provision it through your trusted host;
32
- do not commit it or include it in a URL. The launcher prints only its loopback URL
33
- and mode. Open that URL and enter the reference token. The viewer keeps it in
34
- memory until disconnect/reload and never sends it to a different origin. The
35
- clearly labeled synthetic demo uses only browser-local fixture data.
36
-
37
- The launcher requires nonblocking file-open support, as provided by the pilot's
38
- POSIX hosts. Its configuration must resolve to a regular file of at most 128 KiB;
39
- symlinks to regular files remain supported. The file must belong to the service user or root, with group and other write access disabled. Protect the configuration path and its parent directories from replacement. It checks the opened descriptor and
40
- limits bytes read even if the file grows. Named pipes and other nonregular inputs
41
- fail startup with the same safe diagnostic. Native Windows is outside the
42
- [pilot's supported host profile](../../deployment/cartography/RUNBOOK.md#prerequisites-and-layout).
43
-
44
- Viewer asset hosting on both HTTP and HTTPS also requires nonblocking and
45
- no-follow file-open support, as provided by the POSIX pilot hosts. The fixed
46
- `index.html`, `app.js` and `app.css` entries must each be regular files of at most
47
- 4 MiB. The loader checks each opened descriptor, rejects a symlink replacing an
48
- asset, and bounds reads even if the file grows. Assets must belong to the service user or root, with group and other write access disabled. Keep the entire asset path in trusted directories. It captures the bytes at startup;
49
- later path replacements do not change the served assets. Starting an API without
50
- `viewerDirectory` does not require these file-open capabilities.
51
-
52
- `agm_v1` reference credentials are independent of executor/task and observer
53
- credentials. Each credential is mapped to an immutable `ReadScope` by the host:
54
- `subject`, `companyId`, `tenantIds`, `teamIds`, `allTeams`. Neither HTTP nor MCP
55
- arguments can override this scope. A team reader can see only agents explicitly
56
- assigned to an allowed team and their attached evidence. `allTeams: true` is a
57
- separate explicit grant, including unassigned/core inventory. It must not be
58
- inferred from an empty team list. Restart with a revised credential registry to
59
- rotate or revoke these bearer credentials.
60
-
61
- The server binds `127.0.0.1` only and requires an exact Host header. Browser
62
- requests must use its same origin. It provides no CORS, arbitrary Cypher,
63
- database credentials, execution tools or administration routes. For company
64
- browser access, use the separate HTTPS identity endpoint below. Do not expose
65
- the loopback listener directly through an unchecked proxy.
66
-
67
- Use a dedicated Neo4j reader account where database privileges are available.
68
- The driver's READ session mode is not a privilege boundary. Neo4j Community
69
- does not establish database-level team isolation; the service's fixed queries
70
- and scope checks enforce its reference interface. The database remains a
71
- trusted service accessible only to collector and query hosts. Remote database
72
- connections require verified TLS (`neo4j+s` or `bolt+s`). Unencrypted connections
73
- are accepted only with direct `bolt://` and a literal loopback address. Plaintext
74
- `neo4j://` routing is rejected because advertised peers can leave loopback.
75
-
76
- ## Run the company HTTPS viewer
77
-
78
- The separate `agent-guard-map-identity` launcher serves the viewer and scoped
79
- queries over HTTPS with confidential OIDC sign-in. Follow the
80
- [identity deployment guide](../../deployment/cartography/IDENTITY.md) to register
81
- the provider, provision private configuration/TLS files and environment secrets,
82
- and adapt [identity.example.json](../../deployment/cartography/identity.example.json).
83
- After building and provisioning the private config:
169
+ Neither launcher connects to Neo4j at startup: an unreachable or misconfigured database
170
+ shows as `MAP_UNAVAILABLE` on queries. For browser access by company staff, use the
171
+ second launcher, `agent-guard-map-identity`, which serves HTTPS with OIDC sign-in; see
172
+ [Company sign-in](#company-sign-in).
173
+
174
+ ## Operations
175
+
176
+ Twelve read-only operations, the same over HTTP (`POST /v1/query` with
177
+ `{ "operation": "...", "args": {...} }`) and as MCP tools (`POST /mcp` on the loopback
178
+ service). A result has `operation`, `observedAt`, `data` and, for `get_agent_access`
179
+ only, an optional `graph`.
180
+
181
+ "Any reader" means any credential or signed-in user, within its own scope: a team reader
182
+ sees only agents assigned to its teams and what is attached to them.
183
+
184
+ | Operation | What it answers | Who can call it |
185
+ | --- | --- | --- |
186
+ | `list_agents` | Which declared agents exist, with owner, team, lifecycle and environments. Filters: `search`, `sourceId`, `ownerId`, `teamId`, `environment`, `ownershipStatus`, `lifecycleState`, `flag`, `flagStatus`; `includeFlags` adds each agent's flag. | Any reader |
187
+ | `get_agent_access` | One agent's tasks, declared grants, bindings, MCP connections, IAM policy evidence, [boundary flags](#boundary-flags) and access [outside the guard](#outside-the-guard), with a graph when it is small enough. Needs `sourceId`, `agentId`. | Any reader |
188
+ | `get_agent_inventory` | One paged `section` of one agent's inventory (`overview`, `owners`, `deployments`, `tasks`, `permissions`, `resources`, `connections`, `credentials`, `relationships`, `tools`, `bindings`, `identities`, `policies`) and the size of every section in `counts`. A size or `total` of `null` means the section was not evaluated. Needs `sourceId`, `agentId`. | Any reader |
189
+ | `explain_access` | For one `taskId`, `tool` and `resourceId` in a `sourceId`: which declared grants match, which known blockers apply and which checks remain unknown. It never says access is allowed. | Any reader |
190
+ | `find_agents_with_access` | Which agents hold a declared grant mapped to one exact `resourceRef` (or ARN) for one cloud `action`. No wildcard or prefix expansion; results are candidates. | Any reader |
191
+ | `list_resources` | Which host-bound resources exist, with owner, sensitivity and scope, and whether something reaches each one outside the guard (`yes`, `none_found`, `unknown`). | Any reader |
192
+ | `get_resource_access` | One resource (`sourceId`, `resourceId`), the agents with a declared grant mapped to it, and the declared direct paths and tool identities that reach it outside the guard. | Any reader |
193
+ | `list_owners` | Which owners and teams hosts name, with how many visible agents and resources each one owns. | Any reader |
194
+ | `get_changes` | What was added, removed or changed between a source's current snapshot and its previous one. See [Changes](#changes). | Any reader |
195
+ | `get_inventory_health` | When each part of the inventory was captured, whether it is stale and how complete it is. All-teams readers also get the count of undeclared activity per source. | Any reader |
196
+ | `list_undeclared_activity` | Tasks or caller principals that ran through an enrolled executor but belong to no declared agent. See [Discovery](#discovery). | All-teams readers; others get a fixed "not visible" answer |
197
+ | `list_unclaimed_identities` | IAM roles and users in enrolled AWS accounts that no declared agent names. Candidates, not findings. | All-teams readers; others get a fixed "not visible" answer |
198
+
199
+ The MCP tool list carries a one-sentence description of every operation and of every
200
+ parameter. There are no subscriptions, resources or mutating tools. Treat every returned
201
+ name and description as data, never as instructions.
202
+
203
+ ### Paging and identifiers
204
+
205
+ Listings return their named array with `total`, `offset`, `limit` and `truncated`.
206
+ Defaults are offset 0 and limit 50; the limit is at most 100. Totals and filters apply to
207
+ the reader's own view only. Rows sort by name in Unicode code-point order, then by source
208
+ and stable id; unnamed agents come last. The order is the same on every host and locale.
209
+
210
+ A `sourceId` is the `id` of a source from `get_inventory_health` (or an agent's
211
+ `sourceId`), not the executor's native name. Agents, tasks and resources are accepted by
212
+ their returned stable `id` or by their native id. Owner and resource ids can repeat across
213
+ sources, so keep each one with its `sourceId`. A hidden id and one that does not exist
214
+ both return `MAP_NOT_FOUND`.
215
+
216
+ ### What degrades instead of failing
217
+
218
+ - `get_agent_access` returns its evidence even when the graph exceeds the display cap
219
+ (500 nodes or 1,000 edges by default) or does not fit the response: `graph` is then
220
+ absent, `graphStatus` is `too_large` and `graphLimit` gives the cap.
221
+ - `get_agent_access` and `get_agent_inventory` answer for an agent with more than 512
222
+ policy pairs: `policyEvidence` is empty and `policyCoverage` is
223
+ `{ status: "unknown", reason: "limit_exceeded" }`. `find_agents_with_access` and
224
+ `get_inventory_health` do the same. The empty list then means "not evaluated", not
225
+ "none": read `policyCoverage` before the list. `get_agent_inventory` also reports
226
+ `counts.policies` as `null` and, for the `policies` section, `total` as `null` (never
227
+ 0); `truncated` stays `false` because no further page exists.
228
+ - `find_agents_with_access` that runs out of its work budget returns the candidates it
229
+ completed with `truncated: true` and `truncatedReason: "work_limit"`.
230
+ - `list_agents` with `includeFlags` whose flag read is too large or too slow still lists
231
+ the agents, with every flag `unknown` and `flagsUnavailable` saying why.
232
+ - Health answers when the undeclared-activity count cannot be read; the count is then
233
+ `{ status: "unknown", reason }`.
234
+
235
+ ## Changes
236
+
237
+ `get_changes` compares the current snapshot of a `sourceId` with its previous retained
238
+ one. Optional arguments: `since` (`previous` by default), `agentId`, `resourceId`,
239
+ `changeKind`, `offset`, `limit`.
240
+
241
+ - Each entry in `details` has `kind` (`added`, `removed`, `changed`), its `categories`,
242
+ the `changedFields`, the `before` and `after` properties and relationships, and the
243
+ visible agents it affects (at most 100, with the total).
244
+ - `changeKind` is `all`, `permissions`, `ownership`, `bindings`, `outside_guard` or
245
+ `inventory`. `outside_guard` covers declared direct paths and a deployment's declaration
246
+ status, and carries `evidenceKind: declared`. `inventory` is every change in no other
247
+ category, such as a name, a purpose or a lifecycle state.
248
+ - With an explicit `offset` or `limit`, `changeSummaryScope` is `page`: the `changes`
249
+ summary, relationship changes and affected agents describe that page. Without paging
250
+ they describe every retained change and `details` holds the first 50.
251
+ - `coverageGap: true` means there is no previous snapshot to compare with.
252
+ - A renamed agent appears under its current name. A change of display name is not a
253
+ relationship change. Capture times alone are not changes.
254
+
255
+ These are net differences between two snapshots, not an event log. They do not show that
256
+ effective access was gained or lost, and policy history is not collected
257
+ (`policyHistory: "not_collected"`).
258
+
259
+ ## Discovery
260
+
261
+ The map lists declared agents only. Two operations point at what a manifest may be
262
+ missing. They never create an agent and never grant or block anything. Both are for
263
+ readers with `allTeams: true`, because what they list belongs to no team. A team reader
264
+ gets `{ visibility: "all_teams_required", status: "not_visible", note }` and nothing
265
+ else: no names, no counts, and the same answer whether or not anything would be listed.
266
+
267
+ **Undeclared activity.** A task is undeclared when no manifest task binding assigns it to
268
+ a deployment of a declared agent; a caller principal is undeclared when none of its tasks
269
+ is assigned.
270
+
271
+ - `get_inventory_health` adds `undeclaredActivity` to each source:
272
+ `{ status, tasks, tasksIsMinimum, principals, principalsIsMinimum, manifest }`. `status`
273
+ is `complete`, or `partial` when counting stopped at 10,000. `manifest: "not_collected"`
274
+ marks a source enrolled without a manifest, where every task counts.
275
+ - `list_undeclared_activity` (`sourceId`, optional `kind` of `tasks` or `principals`)
276
+ returns `items`, `total`, `totalIsMinimum`, paging fields, the same `counts` and a
277
+ snapshot `receipt`. A task item has `taskId`, `principalId`, `delegated`, `derived`,
278
+ `revoked`, `parentDeclared` and up to 32 `tools`. `parentDeclared: true` usually means a
279
+ sub-task that only lacks its own binding.
280
+ - Only activity through an enrolled executor is seen. An agent that does not use the
281
+ guard leaves nothing here.
282
+
283
+ **Unclaimed cloud identities.** `list_unclaimed_identities` reads the AWS collections in
284
+ the graph (see the [AWS guide](https://github.com/ShieldaAI/agent-guard/blob/main/deployment/cartography/AWS.md)).
285
+
286
+ - `identities` are IAM roles and users whose exact ARN no declared execution binding
287
+ names. Each has `arn`, `kind`, `name`, `path`, `tenantId`, `partition`, `accountId` and
288
+ `candidate: true`. Most roles are not agents; these are candidates to review.
289
+ - `accounts` has one entry per enrolled account, `complete` or `unknown` with a `reason`
290
+ (`stale`, `collection_invalid`, `changed`, `limit_exceeded`, `claims_limit_exceeded`).
291
+ An unknown account lists none of its identities.
292
+ - `status` is `complete`, `partial` or `unknown`. With `unknown`, `total` is `null` and
293
+ the list is empty; unknown is never an empty list presented as "none".
294
+ - Role assumption, instance profiles and federation are not followed. At most 64
295
+ collections, 50,000 identities per account and 10,000 declared ARNs are compared.
296
+
297
+ ## Boundary flags
298
+
299
+ `get_agent_access` returns `boundaryFlags` with one flag, `dangerous_three`: whether one
300
+ of the agent's live tasks can read private data, read outside content and send data out.
301
+
302
+ | Status | Meaning |
303
+ | --- | --- |
304
+ | `exposed` | All three apply and a declared sending tool runs without approval. |
305
+ | `mitigated` | All three apply, but every possible sender needs approval or is refused after outside content. |
306
+ | `unknown` | The declarations do not settle at least one of the three. |
307
+ | `absent` | At least one of the three is known not to apply. |
308
+
309
+ - Each of the three (`privateData`, `outsideContent`, `externalSend`) is `present`,
310
+ `absent` or `unknown`, with evidence naming the task, tool and resource binding.
311
+ - An agent's status is that of its worst live task, ranked exposed, unknown, mitigated,
312
+ absent, so a mitigated task never hides one that might be exposed.
313
+ - Only an explicit `readsUntrusted: false` or `sendsExternal: false` rules a tool out.
314
+ Older inventories, unregistered tools, delegation ancestors outside the reader's scope
315
+ and chains deeper than 16 ancestors make the affected part `unknown`; `notes` says
316
+ which.
317
+ - A task counts as live only if no delegation ancestor or handoff source is revoked and
318
+ none has expired.
319
+ - `list_agents` adds the status with `includeFlags: true` (one extra read of the returned
320
+ page) and filters with `flag: "dangerous_three"` (everything but `absent`) or
321
+ `flagStatus`.
322
+
323
+ The flag is declared evidence, not proof that data can leave.
324
+
325
+ ## Outside the guard
326
+
327
+ `get_agent_access` also returns `outsideGuard`:
328
+
329
+ - `directAccess`: per deployment, the paths the host declared that act without the guard
330
+ (a shell, a database client, an SDK). The status is `found`, `none_declared` (the host's
331
+ own claim, not a check) or `unknown` (the host has not said).
332
+ - `identityScope`: for each tool that runs under a static AWS identity, the collected S3
333
+ buckets that identity has a policy candidate on but the tool's grants do not bind
334
+ (`broader_than_grants`), or `matches_grants`, or `unknown` with a reason. A record lists
335
+ at most 100 extra buckets; a cut list is marked `truncated` and keeps its status.
336
+
337
+ `list_resources` and `get_resource_access` report the same evidence from the resource's
338
+ side. Only S3 identity policies and host declarations are checked. Bucket policies, SCPs
339
+ and permission boundaries are not, so the buckets are candidates, and `none_found` is not
340
+ proof that no path exists.
341
+
342
+ ## IAM policy evidence
343
+
344
+ Where the collector has AWS evidence, `get_agent_access`, `get_agent_inventory`
345
+ (`policies` section), `explain_access` and `find_agents_with_access` return
346
+ `policyEvidence`: one record per task, binding and action, with `status` (`complete`,
347
+ `unavailable`, `not_collected`), a `reason`, an observation time and `candidates`, an
348
+ array with at most one entry.
349
+
350
+ - Three S3 mappings are supported: `s3:GetObject`, `s3:PutObject`, `s3:ListBucket`.
351
+ - A candidate is bucket-level evidence (`resourceScope: "bucket_summary"`) with a
352
+ condition flag and the names of its condition keys. It does not authorize an exact
353
+ object or prefix, and an empty result does not prove denial.
354
+ - No raw policy, condition value or unrelated provider data is returned.
355
+ - Evidence older than the snapshot's enrolled age limit, capped by `policyMaxAgeSeconds`
356
+ (two hours by default), is reported `stale`, not used.
357
+ - At most 512 task/binding/action pairs are evaluated per read, each operation narrowed to
358
+ what it shows. `explain_access` fails with `MAP_LIMIT_EXCEEDED` past that; the others
359
+ answer with coverage `unknown` (see above).
360
+
361
+ `assessment` is always `unknown`: this metadata never changes what the executor admits.
362
+
363
+ ## Scopes and privacy rules
364
+
365
+ Every caller has a `ReadScope` chosen by the host: `subject`, `companyId`, `tenantIds`,
366
+ `teamIds`, `allTeams`. Request arguments cannot change it.
367
+
368
+ - **Team readers** see only agents explicitly assigned to one of their teams, and what is
369
+ attached to those agents. Shared tools, principals, resources and MCP connections do not
370
+ reveal other teams' entities.
371
+ - **All-teams readers** (`allTeams: true`) also see unassigned inventory and the discovery
372
+ operations. It is an explicit grant and is never inferred from an empty team list.
373
+ - A hidden id and an id that does not exist return the same `MAP_NOT_FOUND`.
374
+ - For a team reader, a declared direct path whose target is outside its scope is listed
375
+ without the target; identity-scope buckets that no visible binding maps are mentioned in a
376
+ note, never named or counted; and explanations always keep a generic "ancestors outside your scope"
377
+ uncertainty.
378
+ - History is filtered by current ownership, so moving an agent between teams does not
379
+ expose its old team's evidence.
380
+ - Only allowlisted properties leave the store. Credential entries are opaque references
381
+ with lifecycle metadata; no secret, token or raw policy is stored or returned.
382
+ - Error responses carry a code and nothing else: no path, name, query or value.
383
+
384
+ The service provides no arbitrary Cypher, no CORS, no administration route and no way to
385
+ write. The Neo4j driver's read mode is not a privilege boundary, and Neo4j Community has
386
+ no per-team isolation: use a dedicated reader account, and treat the database as a
387
+ trusted service reachable only by the collector and this service.
388
+
389
+ ## Company sign-in
390
+
391
+ `agent-guard-map-identity` serves the viewer and `POST /v1/query` over HTTPS with OIDC
392
+ sign-in (authorization code flow with S256 PKCE and RS256 ID tokens). It has no `/mcp`
393
+ route.
84
394
 
85
395
  ```sh
86
- node packages/agent-map/dist/identity-cli.js --config /absolute/private/identity.json
396
+ npx agent-guard-map-identity --config /absolute/private/identity.json
87
397
  ```
88
398
 
89
- The installed package exposes the equivalent `agent-guard-map-identity` command.
90
- The provider must support authorization code flow with S256 PKCE and RS256 ID
91
- tokens. Each exact issuer `sub` has an explicit host-owned scope; provider groups
92
- and browser arguments cannot grant access. The endpoint owns TLS, requires exact
93
- Host/Origin, rejects forwarded identity/authorization headers and provides no
94
- CORS. It does not wrap or reconfigure the loopback service.
95
-
96
- The browser uses opaque Secure/HttpOnly cookies and CSRF-protected same-origin
97
- POSTs. It receives no provider tokens or scope claims. Sessions last at most 15
98
- minutes, capped by ID token expiry, and are not refreshed. Sign out ends the
99
- local session only; the IdP SSO session may remain active. Bindings, secrets and
100
- TLS files reload on restart, which invalidates all sessions. There is no
101
- distributed session store or HA mode. A registered live company IdP and a
102
- trusted-browser end-to-end check remain rollout gates; see the guide for their
103
- status and the complete configuration/operational limits.
104
-
105
- ## Shared operations
106
-
107
- On the loopback service, POST `/v1/query` with `Authorization: Bearer <reference-token>`, content type
108
- `application/json` and `{ "operation": "list_agents", "args": {} }`.
109
- Results contain `operation`, `observedAt`, `data` and an optional focused `graph`.
110
- Source IDs are the graph source IDs returned by the service, not native source
111
- names. Native agent/task IDs and returned stable entity IDs are accepted where
112
- documented by the schemas. No pagination cursor exposes a broader scope.
113
-
114
- | Operation | Arguments | Evidence |
399
+ - Each exact issuer `sub` is bound to a scope in the configuration. Provider groups and
400
+ browser arguments cannot grant access.
401
+ - The browser gets an opaque Secure, HttpOnly session cookie and a CSRF value. It
402
+ receives no provider token. Sessions last at most 15 minutes, capped by the ID token's
403
+ expiry, and are not refreshed. Sign-out ends the map session only.
404
+ - The endpoint owns TLS, requires the exact Host and Origin, rejects forwarded identity
405
+ headers and reads only its own two `__Host-agm_*` cookies.
406
+ - The certificate must match its key, be valid and cover the public host name. Once it
407
+ expires, requests are refused: renew and restart before then. A restart reloads
408
+ bindings and secrets and ends every session. There is no shared session store.
409
+ - An identity provider clock up to five seconds ahead is tolerated
410
+ (`oidc.clockToleranceSeconds`, 0 to 60). Expiry is never extended.
411
+
412
+ Registration, file permissions and the full option list are in the
413
+ [identity guide](https://github.com/ShieldaAI/agent-guard/blob/main/deployment/cartography/IDENTITY.md), with an
414
+ [example configuration](https://github.com/ShieldaAI/agent-guard/blob/main/deployment/cartography/identity.example.json).
415
+
416
+ ## Configuration
417
+
418
+ ### `agent-guard-map --config FILE`
419
+
420
+ The file is strict JSON (no comments, no duplicate keys), at most 128 KiB, a regular file
421
+ owned by the service user or root and not writable by group or others. This launcher
422
+ follows a symbolic link at the configuration path (a mounted configuration is often one)
423
+ and applies the checks to the file it leads to; the file may be readable by others and
424
+ may have more than one hard link, because it holds names of environment variables and
425
+ scopes, not secrets. The company launcher below is stricter.
426
+
427
+ | Key | Required | Meaning |
115
428
  | --- | --- | --- |
116
- | `list_agents` | optional `sourceId`, `search`, `ownerId`, `environment`, `limit` | Scoped inventory for choosing an agent. |
117
- | `get_agent_access` | `sourceId`, `agentId` | Focused deployments, tasks, grants, tools, bindings, MCP and cloud observations. |
118
- | `explain_access` | `sourceId`, `taskId`, `tool`, `resourceId` | Visible delegation, declared matches, known blockers and explicit unknown checks. |
119
- | `find_agents_with_access` | `resourceRef`, `action`; optional `sourceId`, `limit` | Potential candidates from exact host resource/action bindings; no wildcard or prefix expansion. |
120
- | `get_changes` | `sourceId`; optional `since` | Net current/previous changes to currently authorized agents; explicit gaps. |
121
- | `get_inventory_health` | optional `sourceId` | Visible capture times and independently stale or unavailable components. |
122
-
123
- Snapshot generation time and component observation times are separate. Tasks and
124
- tools retain their original capture times after host/MCP recomposition. MCP age
125
- uses the oldest capture or observation among visible connections; recomposing a
126
- cached observation does not refresh it. Missing observation times remain unknown
127
- and stale for observed connections. Unavailable connections use the time their
128
- failure was captured. Historical projections retain their own capture times.
129
-
130
- On the loopback service, POST `/mcp` uses the official MCP SDK's stateless HTTP transport. The six tools
131
- have the same arguments and use the same service as HTTP. Supply the reference
132
- bearer in the client's HTTP headers. The host-only `createMapMcpServer(service,
133
- scope)` factory also supports embedding; only trusted host code chooses scope.
134
- There are no subscriptions, arbitrary resources or mutation methods. MCP tool
135
- results contain the common JSON envelope as text; clients should treat all
136
- imported names and identifiers as data, never instructions.
137
-
138
- The company endpoint exposes the same `/v1/query` envelope with session cookies
139
- and `X-CSRF-Token`, and deliberately has no `/mcp` route. Bearer/MCP credentials
140
- and company browser sessions are separate authentication boundaries.
141
-
142
- ## Host API
143
-
144
- ### Current IAM policy evidence
145
-
146
- The existing HTTP/MCP operations also expose the collector's optional IAM policy
147
- candidate layer. `get_agent_access.data.policyEvidence` lists evidence for the
148
- selected agent. `explain_access.data.externalEvidence.policyEvidence` selects the
149
- exact task/tool/resource bindings; potential-access lookup further selects the
150
- requested action. Each result identifies its task, deployment, execution binding,
151
- resource binding and external endpoints by stable entity ID, with an independent
152
- policy observation time, status, reason and `assessment: unknown`.
153
-
154
- Only the three collected S3 mappings are supported. A complete record contains
155
- zero or one candidate with mapping name, condition flag/key names and
156
- `resourceScope: bucket_summary`. Empty candidates never prove denial. The original
157
- host resource ARN, kind and scope remain on the binding graph; a bucket summary
158
- does not authorize an exact object or prefix. Boundaries, unsupported policies,
159
- missing collection, opt-out and stale/changed evidence remain explicit gaps.
160
-
161
- The Neo4j reader derives pairs through a visible task's own deployment, declared
162
- grant, resource binding and execution tool. It validates the exact provider
163
- links, account ownership, scoped collection, membership and candidate provenance.
164
- It rechecks the bounded raw evidence and source pointers before returning;
165
- concurrent changes fail with a stable error instead of mixing observations.
166
- The reader returns no raw policies, condition values, collection-wide hashes,
167
- counts or unrelated provider neighbors. Generic Cartography `CAN_*` edges do not
168
- fill missing owned evidence.
169
-
170
- Policy age uses the tighter of the snapshot's enrolled cloud `maxAgeSeconds` and
171
- the host's `Neo4jMapStoreOptions.policyMaxAgeSeconds` (default 7,200; maximum
172
- 86,400). A newer AWS capture makes old Guard provider joins unavailable until the
173
- next successful Guard collection. `get_inventory_health` reports policy coverage
174
- separately. `get_changes` returns `policyHistory: not_collected`: live candidates
175
- are not attached to historical inventory snapshots or used to invent policy diffs.
176
-
177
- The reader caps each request at 512 task/binding/action pairs and fails overflow
178
- without returning partial evidence. Fixed read queries, row/byte limits, the
179
- five-second transaction deadline and the existing response limits still apply.
180
- Custom `MapStore` implementations may omit `MapProjection.policyEvidence`; supplied
181
- records receive closed-shape and binding-path validation at the service boundary.
182
- Complete evidence requires one owning agent and unambiguous tool/identity/binding
183
- links, including unique task/grant and grant/selector pairs. Distinct valid grants
184
- and resource selectors remain supported; ambiguous evidence may be reported as
185
- unavailable with no candidates.
186
- This metadata never changes executor admission.
187
-
188
- Sources superseded through the host's [lifecycle command](../../deployment/cartography/SOURCE-LIFECYCLE.md)
189
- are excluded from normal reads, including explicit source requests and history.
190
- A read spanning supersession fails rather than returning its old view. The new
191
- enrollment generation has its own source identity; supersession does not rewrite
192
- the executor ledger or change agent permissions.
193
-
194
- This self-contained example demonstrates the public contract without requiring
195
- a database. For real inventory, replace the store with `createNeo4jMapStore` and
196
- close it when the host shuts down.
429
+ | `neo4j.uri` | yes | `neo4j+s://`, `bolt+s://`, or `bolt://` on a loopback address. Plain `neo4j://` is refused. |
430
+ | `neo4j.username` | yes | The reader account. |
431
+ | `neo4j.passwordEnv` | yes | Name of the environment variable that holds the password. |
432
+ | `neo4j.database` | no | Database name; `neo4j` by default. |
433
+ | `neo4j.maxNodes`, `neo4j.maxEdges`, `neo4j.maxBytes` | no | Per-read limits; see [Limits](#limits). |
434
+ | `credentials[]` | yes | 1 to 128 entries of `{ "tokenEnv", "scope" }`. `tokenEnv` names the environment variable that holds an `agm_v1.` token from `createMapToken()`; `scope` is the reader's `ReadScope`. |
435
+ | `port` | no | Loopback port; 8789 by default. |
436
+ | `viewerDirectory` | no | Directory with the viewer's `index.html`, `app.js`, `demo.js` and `app.css` (all four required, each at most 4 MiB, same ownership rule, no symbolic links). The viewer is built from the Agent Guard repository; it is not in this package. |
437
+
438
+ The server binds `127.0.0.1` only and requires an exact Host header. Do not expose it
439
+ through a proxy that does not check both. To rotate or revoke a token, change the
440
+ environment and restart.
441
+
442
+ `--help` prints the usage and `--version` the package version. A startup failure prints
443
+ one line, `MAP_START_FAILED <code>: <hint>` (followed by a usage line when an argument is
444
+ wrong), and exits with status 1; see [Error codes](#error-codes).
445
+
446
+ ### `agent-guard-map-identity --config FILE`
447
+
448
+ The same `neo4j` block, plus `publicOrigin`, `tls` (`keyFile`, `certFile`), `oidc`
449
+ (`issuer`, `clientId`, `clientSecretEnv`, and optional `clientAuthMethod`,
450
+ `endpointOrigins`, `caFile`, `requestTimeoutMs`, `maxResponseBytes`,
451
+ `clockToleranceSeconds`), `companyId` and `bindings` (`subject`, `scope`). Optional:
452
+ `listenHost`, `port`, `viewerDirectory`, `sessionTtlMs`, `loginTtlMs`, `maxSessions`,
453
+ `maxLoginTransactions`, `requestTimeoutMs`, `maxRequestBytes`, `maxResponseBytes`,
454
+ `maxConcurrentRequests`, `rateLimit`. Unlike the reference launcher, this one does not
455
+ follow symbolic links and does not accept files owned by root: the configuration, the
456
+ TLS key and certificate and the optional CA file must each be a regular file owned by
457
+ the service user, with one hard link, not a symbolic link and not writable by group or
458
+ others. The configuration and the TLS key must also be private to the service user (no
459
+ group or other access). Viewer assets keep the rule given for `viewerDirectory` above.
460
+ Details are in the [identity guide](https://github.com/ShieldaAI/agent-guard/blob/main/deployment/cartography/IDENTITY.md).
461
+
462
+ ### Options in code
463
+
464
+ | Where | Option | Default | Range |
465
+ | --- | --- | --- | --- |
466
+ | `MapService` | `maxResponseBytes` | 1 MiB | 128 bytes to 8 MiB |
467
+ | | `maxGraphNodes` / `maxGraphEdges` | 500 / 1,000 | up to 2,000 / 5,000 |
468
+ | | `queryTimeoutMs` | 5,000 | 10 to 10,000 |
469
+ | | `staleAfterSeconds` | 900 | 1 to 86,400 |
470
+ | `createNeo4jMapStore` | `timeoutMs` | 5,000 | up to 5,000 |
471
+ | | `maxSources` | 64 | up to 64 |
472
+ | | `maxNodes` / `maxEdges` / `maxBytes` | 2,000 / 5,000 / 4 MiB | up to 10,000 / 25,000 / 16 MiB (above the defaults: not tested against Neo4j, see [Limits](#limits)) |
473
+ | | `policyMaxAgeSeconds` | 7,200 | up to 86,400 |
474
+ | `startMapServer` | `port` | any free port | 0 to 65,535 |
475
+ | | `maxRequestBytes` | 32 KiB | 128 bytes to 64 KiB |
476
+ | | `maxResponseBytes` | 2 MiB | 128 bytes to 2 MiB |
477
+ | | `requestTimeoutMs` | 10,000 | 10 to 30,000 |
478
+ | | `maxConcurrentRequests` | 4 | 1 to 32 |
479
+ | | `rateLimit` | 120 requests per 60 s per credential | up to 10,000 per window of 10 ms to 60 s |
480
+
481
+ A value outside its range is `MAP_CONFIG_INVALID`.
482
+
483
+ ## Use it from code
484
+
485
+ The service takes any `MapStore`. This example needs no database; for real inventory use
486
+ `createNeo4jMapStore` and close it when the host shuts down.
197
487
 
198
488
  ```ts
199
489
  import { MapService, validateReadScope, type MapStore } from '@shieldaai/agent-map';
200
490
 
201
- const store: MapStore = { async read() { return { sources: [] }; } };
491
+ const store: MapStore = {
492
+ async read() {
493
+ return { sources: [] };
494
+ },
495
+ };
202
496
  const service = new MapService(store);
203
- const scope = validateReadScope({ subject: 'reader', companyId: 'example',
204
- tenantIds: ['engineering'], teamIds: ['platform'], allTeams: false });
497
+ const scope = validateReadScope({
498
+ subject: 'reader',
499
+ companyId: 'example',
500
+ tenantIds: ['engineering'],
501
+ teamIds: ['platform'],
502
+ allTeams: false,
503
+ });
205
504
  const response = await service.query(scope, 'list_agents', {});
206
505
  console.log(response.data);
207
506
  ```
208
507
 
209
- To embed the company endpoint, load TLS material and secrets in trusted host
210
- code. This self-contained function uses an empty store and is not invoked by the
211
- example. Replace its paths, registered issuer/client and subject binding before
212
- calling it; the host API requires an absolute viewer directory and loaded PEM
213
- strings. The CLI adds the private-file checks described in the deployment guide.
508
+ A custom `MapStore` is a trusted adapter: it must apply the reader's scope itself. The
509
+ service validates what it is given (allowlisted properties, the shape and binding path of
510
+ policy evidence) but does not turn an unscoped store into a scoped one. `MapReadRequest`
511
+ tells a store what a read needs (`summary`, `owners`, `policy`, `boundary` and so on); a
512
+ store that ignores it and returns the whole scoped graph is still correct.
513
+
514
+ To embed the company endpoint, load the TLS material and secrets in trusted host code.
515
+ The host API takes loaded PEM strings and an absolute viewer directory; the launcher adds
516
+ the private-file checks.
214
517
 
215
518
  ```ts
216
519
  import { readFile } from 'node:fs/promises';
217
- import { MapService, startMapIdentityServer,
218
- type MapIdentityServer, type MapStore } from '@shieldaai/agent-map';
520
+ import { MapService, startMapIdentityServer, type MapIdentityServer, type MapStore } from '@shieldaai/agent-map';
219
521
 
220
522
  export async function startCompanyReference(clientSecret: string): Promise<MapIdentityServer> {
221
- const store: MapStore = { async read() { return { sources: [] }; } };
523
+ const store: MapStore = {
524
+ async read() {
525
+ return { sources: [] };
526
+ },
527
+ };
222
528
  return startMapIdentityServer({
223
529
  service: new MapService(store),
224
- publicOrigin: 'https://localhost:8788', listenHost: '127.0.0.1', port: 8788,
530
+ publicOrigin: 'https://localhost:8788',
531
+ listenHost: '127.0.0.1',
532
+ port: 8788,
225
533
  tls: {
226
534
  key: await readFile('/absolute/private/tls/server-key.pem', 'utf8'),
227
535
  cert: await readFile('/absolute/private/tls/server-chain.pem', 'utf8'),
228
536
  },
229
537
  oidc: {
230
538
  issuer: 'https://login.example.invalid',
231
- clientId: 'replace-with-registered-client-id', clientSecret,
539
+ clientId: 'replace-with-registered-client-id',
540
+ clientSecret,
232
541
  },
233
542
  companyId: 'example-company',
234
- bindings: [{
235
- subject: 'replace-with-exact-idp-sub',
236
- scope: { subject: 'platform-reader', companyId: 'example-company',
237
- tenantIds: ['engineering'], teamIds: ['platform'], allTeams: false },
238
- }],
239
- viewerDirectory: '/absolute/path/to/agent-guard/apps/agent-map/dist',
543
+ bindings: [
544
+ {
545
+ subject: 'replace-with-exact-idp-sub',
546
+ scope: {
547
+ subject: 'platform-reader',
548
+ companyId: 'example-company',
549
+ tenantIds: ['engineering'],
550
+ teamIds: ['platform'],
551
+ allTeams: false,
552
+ },
553
+ },
554
+ ],
555
+ viewerDirectory: '/absolute/path/to/viewer/dist',
240
556
  });
241
557
  }
242
558
  ```
243
559
 
244
- `bindings[].subject` is the exact IdP `sub`; `scope.subject` is a local alias and
245
- can differ. The returned server exposes `replaceBindings`, `revokeSubject` and
246
- `invalidateSessions` only to host code. Each invalidates all current sessions
247
- and login transactions; revocation also removes that binding. Persist policy
248
- changes in the host before restarting. Call `close()` during shutdown and close
249
- any real Neo4j store. No HTTP administration endpoints are exposed.
250
-
251
- Custom `MapStore` implementations are trusted adapters and must implement the
252
- same visibility boundary as `Neo4jMapStore`; the service does not turn an
253
- unscoped store into a scoped one. The included store filters all node/edge
254
- queries before limits or materialization. Shared tools, principals, resources
255
- and MCP connections do not reveal other teams' attached entities. Historical
256
- membership is intersected with current authorized agent ownership, including
257
- task ownership, so moving an agent/task between teams cannot expose its old
258
- team's evidence. Hidden and nonexistent IDs return the same `MAP_NOT_FOUND`.
259
- Team responses omit source-wide revision counters and hidden ancestor/family
260
- identifiers. Explanations always retain a generic ancestor-scope uncertainty.
261
-
262
- ## Limits and semantics
263
-
264
- Loopback defaults: 32 KiB HTTP request, 1 MiB service result (2 MiB transport ceiling),
265
- 500 focused nodes/1,000 edges, 64 sources and 2,000 nodes/5,000 edges per store
266
- read, 5-second graph/service deadline, 10-second HTTP deadline, four concurrent
267
- HTTP requests and 120 requests/minute per credential plus a global quota.
268
- Streaming graph reads cap materialized nodes/edges at 4 MiB. Synchronous service
269
- processing shares the query deadline and has a 200,000-unit work budget; potential
270
- access evidence is limited to 1,000 entries and the cumulative response-byte
271
- budget. Configuration has hard upper bounds. Oversized authorized inventories fail
272
- closed instead of returning a misleading partial map. List operations return
273
- up to 100 rows and explicitly indicate truncation.
274
-
275
- The company endpoint has separate browser/provider limits, including a default
276
- 600 requests/minute per session and globally. Its complete defaults and hard
277
- ceilings are in the [identity guide](../../deployment/cartography/IDENTITY.md).
278
- The MapService/store limits and evidence semantics below apply to both endpoints.
279
-
280
- The store uses fixed parameterized read-only templates in one transaction,
281
- allowlisted properties and relationships, bounded graph paths, and a publication
282
- pointer recheck. It never traverses into arbitrary provider policy data. A
283
- pointer change fails the query for retry rather than mixing generations.
284
- Current/previous comparisons are net differences, not an audit history; missing
285
- or retired previous evidence is explicit. Collection times and global revision
286
- changes alone are not entity changes. Health describes published inventory,
287
- not collector process health. Source-wide publication timing is visible to
288
- authorized readers of that source.
289
-
290
- Blockers describe exported gates at capture/evaluation time. A result without
291
- blockers remains `unknown`: live budgets, arguments, custom policy, approvals,
292
- delegation outside scope and backend authorization still require evaluation.
293
- Potential-access matches retain exact/prefix, static/dynamic, cloud freshness
294
- and MCP lifecycle limitations. The graph is never used in executor admission.
295
-
296
- ## Validation
297
-
298
- ```sh
299
- npm test
300
- npm run test:pack
301
- cd integrations/cartography
302
- .venv/bin/python scripts/test-disposable-neo4j.py -- -q
303
- ```
304
-
305
- JavaScript checks cover schemas, immutable scope, query bounds, safe HTTP,
306
- official SDK HTTP/MCP equivalence, disposable HTTPS/OIDC exchange, cookie/CSRF
307
- and revocation boundaries, private launcher files and viewer lifecycle behavior.
308
- The packed-package check compiles both Host API examples and exercises the
309
- installed identity boundary. Disposable Neo4j checks
310
- exercise actual published graphs, cross-company/tenant/team isolation, shared
311
- MCP metadata, moved ownership, hidden IDs, current/previous visibility and
312
- absence of database mutations. Never run the integration suite against a
313
- shared or production database.
560
+ `bindings[].subject` is the exact `sub` from the identity provider; `scope.subject` is a
561
+ local alias. The returned server offers `replaceBindings`, `revokeSubject` and
562
+ `invalidateSessions` to host code only; each ends every current session. Call `close()`
563
+ on shutdown.
564
+
565
+ Other exports: `startMapServer` and `createMapToken` (the loopback service),
566
+ `createMapMcpServer(service, scope)` (an MCP server for a scope that trusted host code
567
+ chooses), `validateReadScope`, `MAP_OPERATIONS`, `MAP_INPUT_SCHEMAS` and
568
+ `validateQueryArguments`.
569
+
570
+ `@shieldaai/agent-map/service` exports the service alone, without the Neo4j store or the
571
+ servers: `MapService`, `MAP_OPERATIONS`, `MAP_INPUT_SCHEMAS`, `validateQueryArguments`
572
+ and four names that only this subpath exports, not the package root:
573
+ `MAP_OPERATION_DESCRIPTIONS` (one sentence per operation) and the closed vocabularies
574
+ `MAP_LABELS`, `MAP_RELATIONSHIPS` and `MAP_PROPERTIES`. The subpath does not load
575
+ `neo4j-driver`, `openid-client` or an HTTP server, but it is not free of Node built-ins:
576
+ it imports `node:buffer`, and through `@shieldaai/agent-guard` also `node:crypto` and
577
+ `node:util/types`. A browser bundle must supply stand-ins for those three, as the viewer
578
+ build does.
579
+
580
+ ## Limits
581
+
582
+ | Limit | Default | Notes |
583
+ | --- | --- | --- |
584
+ | Nodes, edges and bytes per store read | 2,000 / 5,000 / 4 MiB | Across all of a reader's sources, and both snapshots for `get_changes`. Raise with `maxNodes`, `maxEdges`, `maxBytes`, up to 10,000 / 25,000 / 16 MiB; see the note on raised limits below. |
585
+ | Sources per read | 64 | |
586
+ | Store transaction and service deadline | 5 s each | Does not move when the read limits are raised. |
587
+ | Service work per query | 200,000 units | One budget for the main answer. Four optional parts each count against a separate budget of the same size, so running out there degrades that part instead of failing the answer: the candidate search of `find_agents_with_access`, the page flags of `list_agents` with `includeFlags`, the flag of `get_agent_access`, and outside-guard reach in `list_resources` and `get_resource_access`. |
588
+ | Response size | 1 MiB (service), 2 MiB (HTTP) | Past it, optional detail goes first (identity-scope bucket lists, then the graph); then the answer fails. |
589
+ | Graph in `get_agent_access` | 500 nodes / 1,000 edges | Past it the evidence is returned without the graph. |
590
+ | Rows per page | 50, at most 100 | |
591
+ | Policy pairs per read | 512 | |
592
+ | Candidates in `find_agents_with_access` | 50, at most 100; 1,000 evidence entries | |
593
+ | HTTP request body | 32 KiB | |
594
+ | HTTP request deadline | 10 s | |
595
+ | Concurrent HTTP requests / store reads | 4 / 8 | |
596
+ | Requests per credential | 120 per minute | Plus a global quota. The company endpoint allows 600 per session. |
597
+ | Company session | 15 minutes | Never refreshed. |
598
+
599
+ How many agents each kind of read reaches. These are estimates, not measurements: the
600
+ node limit (2,000 by default, 10,000 at the maximum) divided by the nodes that read loads
601
+ per agent, for a fleet with about a dozen graph nodes per agent (one or two deployments
602
+ and tasks, their grants, bindings, tools and identities). Your figure is your own node
603
+ count per agent, and the edge and byte limits can be reached first.
604
+
605
+ | Read | Loads | Estimate, default limits | Estimate, maximum limits |
606
+ | --- | --- | --- | --- |
607
+ | `list_agents`, also with `includeFlags` | Agents, deployments, owners | about 900 | about 4,500 |
608
+ | `list_owners`, all-teams reader | Agents, resource bindings, owners | about 1,900 | about 9,000 |
609
+ | `list_owners`, team reader | The above plus what decides which bindings are visible | about 250 | about 1,200 |
610
+ | `list_resources`, `get_resource_access`, `find_agents_with_access`, `explain_access`, `get_inventory_health`, `list_agents` with `flag` or `flagStatus` | The whole visible graph | about 150 | about 800 |
611
+ | `get_changes` | The whole visible graph of two snapshots | about 75 | about 400 |
612
+ | `get_agent_access`, `get_agent_inventory` | The selected agent's own neighborhood | not bound by fleet size | not bound by fleet size |
613
+ | `list_undeclared_activity`, `list_unclaimed_identities` | Counts and one page, read in the database | not bound by fleet size | not bound by fleet size |
614
+
615
+ What was tested: `test/capacity.test.mjs` runs the listing operations (`list_agents`,
616
+ `list_owners`, `list_resources`, `get_resource_access`, `find_agents_with_access`,
617
+ `get_inventory_health`) for an all-teams reader over a synthetic fleet of 800 agents in a
618
+ graph of about 9,000 nodes and 8,800 relationships, with the limits at their maximum.
619
+ That test uses a fake database driver. It shows that the store's bookkeeping and the
620
+ service's work budget hold at that size; it says nothing about how long Neo4j takes.
621
+ `get_resource_access` walks every visible agent, so the service work budget can stop it
622
+ before the node limit does.
623
+
624
+ Raised limits: values above the defaults are accepted up to the maximum, but no read
625
+ above the defaults has been run against a Neo4j database. The transaction deadline stays
626
+ at 5 seconds and records are fetched 32 at a time whatever the limits are, so a read of
627
+ 10,000 nodes and 25,000 relationships makes about a thousand round trips inside that
628
+ deadline. A host that raises the limits should load its largest expected fleet into its
629
+ own database and confirm that `list_resources`, `get_inventory_health`,
630
+ `find_agents_with_access` and `get_changes` answer without `MAP_TIMEOUT`, for a team
631
+ reader as well as an all-teams reader.
632
+
633
+ Raise the three store limits together: a read holds up to `maxBytes` in memory and up to
634
+ eight reads run at once.
635
+
636
+ Other things to know:
637
+
638
+ - A read is one transaction over fixed, parameterized queries. If a new snapshot is
639
+ published while it runs, the read fails with `MAP_UNAVAILABLE` rather than mix two
640
+ snapshots; retry it.
641
+ - Snapshot time and component capture times are separate. Recomposing a snapshot does not
642
+ make an old MCP or cloud observation fresh.
643
+ - Health describes the published inventory, not whether the collector process is running.
644
+ - A source retired through the host's
645
+ [lifecycle command](https://github.com/ShieldaAI/agent-guard/blob/main/deployment/cartography/SOURCE-LIFECYCLE.md) is no longer read.
646
+ - A result of `explain_access` without blockers is still `unknown`: live budgets, request
647
+ arguments, custom policy, approvals and backend authorization are not evaluated here.
648
+
649
+ ## Error codes
650
+
651
+ An HTTP error is a JSON body `{ "error": "<code>" }` and nothing else. An MCP tool error
652
+ carries the same code. `service.query` and `store.read` throw a `MapQueryError` whose
653
+ `code` is one of these.
654
+
655
+ | Code | HTTP status | Meaning |
656
+ | --- | --- | --- |
657
+ | `MAP_REQUEST_INVALID` | 400 | The request is not valid JSON, has an unknown operation or argument, or a wrong value. Also sent with status 417 for an `Expect` header other than `100-continue`. |
658
+ | `MAP_UNAUTHENTICATED` | 401 | The bearer token is missing or not accepted. |
659
+ | `MAP_SESSION_REQUIRED` | 401 | The company session is missing, ended or not signed in. |
660
+ | `MAP_IDENTITY_INVALID` | 401 | Company sign-in could not be completed. |
661
+ | `MAP_REQUEST_DENIED` | 403 | Wrong host, origin, route, method or CSRF value. |
662
+ | `MAP_SCOPE_INVALID` | 403 | The scope given to the service is not a valid `ReadScope`. |
663
+ | `MAP_NOT_FOUND` | 404 | The source, agent, task or resource does not exist or is outside the reader's scope. |
664
+ | `MAP_TIMEOUT` | 408 or 504 | 408: the request did not arrive in time. 504: the request arrived, and the read (or, at sign-in, the identity provider) ran out of time. |
665
+ | `MAP_LIMIT_EXCEEDED` | 413 | The answer needs more than one bounded read, or more work or bytes than allowed. Narrow the request. |
666
+ | `MAP_REQUEST_TOO_LARGE` | 413 | The request body is over the limit. |
667
+ | `MAP_RESPONSE_TOO_LARGE` | 413 | The response is over the transport limit. |
668
+ | `MAP_RATE_LIMITED` | 429 | Too many requests; wait and retry. |
669
+ | `MAP_BUSY` | 503 | Too many requests in progress, or the server is stopping. |
670
+ | `MAP_UNAVAILABLE` | 503 | The database is unreachable, returned something malformed, or a snapshot was published during the read. |
671
+ | `MAP_IDENTITY_UNAVAILABLE` | 503 | The identity provider did not answer. |
672
+ | `MAP_CONFIG_INVALID` | none | Thrown by constructors and launchers for an option out of range or a file that fails its checks. |
673
+
674
+ Startup failures print `MAP_START_FAILED <code>: <hint>` with one of
675
+ `MAP_ARGUMENT_INVALID` (followed by a usage line), `MAP_CONFIG_NOT_FOUND`,
676
+ `MAP_CONFIG_UNREADABLE`, `MAP_CONFIG_INVALID`, `MAP_CREDENTIAL_REQUIRED` (an environment
677
+ variable the configuration names is empty), `MAP_SCOPE_INVALID`, `MAP_LISTEN_FAILED`,
678
+ `MAP_IDENTITY_INVALID`, `MAP_IDENTITY_UNAVAILABLE` or `MAP_UNAVAILABLE` (a required
679
+ service or capability is unavailable). Any other failure prints
680
+ `MAP_START_FAILED: unexpected failure during startup.` with no code. No path, name, value
681
+ or secret is printed.
682
+
683
+ ## Related
684
+
685
+ - [Viewer](https://github.com/ShieldaAI/agent-guard/blob/main/apps/agent-map/README.md): the browser interface for this service.
686
+ - [Operations runbook](https://github.com/ShieldaAI/agent-guard/blob/main/deployment/cartography/RUNBOOK.md): collecting, publishing and
687
+ serving in one place.
688
+ - [Package guide](https://github.com/ShieldaAI/agent-guard/blob/main/docs/package-guide.md): which Agent Guard package does what.
689
+ - [Risk map](https://github.com/ShieldaAI/agent-guard/blob/main/docs/risks.md): which risks the map helps with, and which it does not.
690
+
691
+ ## License
692
+
693
+ Apache-2.0. See [LICENSE](./LICENSE).