@shieldaai/agent-map 0.1.0-alpha.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +652 -272
- package/dist/assets.js +16 -26
- package/dist/boundary-flags.d.ts +73 -0
- package/dist/boundary-flags.js +352 -0
- package/dist/boundary.d.ts +18 -3
- package/dist/boundary.js +71 -21
- package/dist/cli-entry.d.ts +11 -0
- package/dist/cli-entry.js +48 -0
- package/dist/cli.js +40 -43
- package/dist/common.d.ts +34 -0
- package/dist/common.js +94 -0
- package/dist/http.js +78 -51
- package/dist/identity-cli.js +93 -46
- package/dist/identity-http.js +226 -94
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/mcp.js +40 -5
- package/dist/oidc.d.ts +5 -0
- package/dist/oidc.js +121 -38
- package/dist/policy-reader.d.ts +56 -3
- package/dist/policy-reader.js +823 -110
- package/dist/projection-fields.d.ts +4 -0
- package/dist/projection-fields.js +144 -0
- package/dist/service/changes.d.ts +5 -0
- package/dist/service/changes.js +255 -0
- package/dist/service/context.d.ts +33 -0
- package/dist/service/context.js +2 -0
- package/dist/service/discovery.d.ts +6 -0
- package/dist/service/discovery.js +31 -0
- package/dist/service/explain.d.ts +12 -0
- package/dist/service/explain.js +126 -0
- package/dist/service/flags.d.ts +20 -0
- package/dist/service/flags.js +70 -0
- package/dist/service/inventory.d.ts +4 -0
- package/dist/service/inventory.js +62 -0
- package/dist/service/json.d.ts +8 -0
- package/dist/service/json.js +7 -0
- package/dist/service/operations/discovery.d.ts +5 -0
- package/dist/service/operations/discovery.js +90 -0
- package/dist/service/operations/explain-access.d.ts +3 -0
- package/dist/service/operations/explain-access.js +25 -0
- package/dist/service/operations/find-agents-with-access.d.ts +3 -0
- package/dist/service/operations/find-agents-with-access.js +120 -0
- package/dist/service/operations/get-agent-access.d.ts +3 -0
- package/dist/service/operations/get-agent-access.js +75 -0
- package/dist/service/operations/get-agent-inventory.d.ts +3 -0
- package/dist/service/operations/get-agent-inventory.js +42 -0
- package/dist/service/operations/get-changes.d.ts +3 -0
- package/dist/service/operations/get-changes.js +11 -0
- package/dist/service/operations/get-inventory-health.d.ts +3 -0
- package/dist/service/operations/get-inventory-health.js +81 -0
- package/dist/service/operations/get-resource-access.d.ts +3 -0
- package/dist/service/operations/get-resource-access.js +48 -0
- package/dist/service/operations/list-agents.d.ts +3 -0
- package/dist/service/operations/list-agents.js +92 -0
- package/dist/service/operations/list-owners-resources.d.ts +5 -0
- package/dist/service/operations/list-owners-resources.js +69 -0
- package/dist/service/ordering.d.ts +9 -0
- package/dist/service/ordering.js +47 -0
- package/dist/service/outside-guard.d.ts +68 -0
- package/dist/service/outside-guard.js +313 -0
- package/dist/service/policy.d.ts +26 -0
- package/dist/service/policy.js +299 -0
- package/dist/service/schemas.d.ts +16 -0
- package/dist/service/schemas.js +192 -0
- package/dist/service/summaries.d.ts +32 -0
- package/dist/service/summaries.js +247 -0
- package/dist/service/work.d.ts +33 -0
- package/dist/service/work.js +116 -0
- package/dist/service.d.ts +5 -5
- package/dist/service.js +110 -645
- package/dist/store/ancestors.d.ts +9 -0
- package/dist/store/ancestors.js +86 -0
- package/dist/store/discovery.d.ts +8 -0
- package/dist/store/discovery.js +288 -0
- package/dist/store/evidence.d.ts +12 -0
- package/dist/store/evidence.js +90 -0
- package/dist/store/identity-scope.d.ts +9 -0
- package/dist/store/identity-scope.js +71 -0
- package/dist/store/nodes.d.ts +24 -0
- package/dist/store/nodes.js +237 -0
- package/dist/store/projection.d.ts +19 -0
- package/dist/store/projection.js +242 -0
- package/dist/store/queries.d.ts +48 -0
- package/dist/store/queries.js +302 -0
- package/dist/store/read.d.ts +81 -0
- package/dist/store/read.js +135 -0
- package/dist/store/selection.d.ts +3 -0
- package/dist/store/selection.js +102 -0
- package/dist/store.d.ts +15 -0
- package/dist/store.js +102 -395
- package/dist/transport.d.ts +42 -2
- package/dist/transport.js +144 -16
- package/dist/types.d.ts +184 -4
- package/dist/types.js +52 -11
- package/package.json +16 -3
package/README.md
CHANGED
|
@@ -1,313 +1,693 @@
|
|
|
1
|
-
#
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
`
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
`
|
|
51
|
-
|
|
52
|
-
`
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
the
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
396
|
+
npx agent-guard-map-identity --config /absolute/private/identity.json
|
|
87
397
|
```
|
|
88
398
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
The
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
`
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
| `
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
121
|
-
| `
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
and
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
`
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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 = {
|
|
491
|
+
const store: MapStore = {
|
|
492
|
+
async read() {
|
|
493
|
+
return { sources: [] };
|
|
494
|
+
},
|
|
495
|
+
};
|
|
202
496
|
const service = new MapService(store);
|
|
203
|
-
const scope = validateReadScope({
|
|
204
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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 = {
|
|
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',
|
|
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',
|
|
539
|
+
clientId: 'replace-with-registered-client-id',
|
|
540
|
+
clientSecret,
|
|
232
541
|
},
|
|
233
542
|
companyId: 'example-company',
|
|
234
|
-
bindings: [
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
|
245
|
-
|
|
246
|
-
`invalidateSessions`
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
budget.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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).
|