@shieldaai/agent-map 0.0.0-stage → 0.1.0-alpha.2

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.
@@ -0,0 +1,683 @@
1
+ import { canonicalJson, immutableJson } from '@shieldaai/agent-guard';
2
+ import { MapQueryError, mapIdentifier, validateReadScope } from './types.js';
3
+ export const MAP_OPERATIONS = ['list_agents', 'get_agent_access', 'explain_access',
4
+ 'find_agents_with_access', 'get_changes', 'get_inventory_health'];
5
+ const idSchema = { type: 'string', minLength: 1, maxLength: 4096 };
6
+ const limitSchema = { type: 'integer', minimum: 1, maximum: 100 };
7
+ const schema = (properties, required = []) => immutableJson({ type: 'object', additionalProperties: false, properties, required });
8
+ export const MAP_INPUT_SCHEMAS = Object.freeze({
9
+ list_agents: schema({ search: { type: 'string', maxLength: 256 }, sourceId: idSchema,
10
+ ownerId: idSchema, environment: idSchema, limit: limitSchema }),
11
+ get_agent_access: schema({ sourceId: idSchema, agentId: idSchema }, ['sourceId', 'agentId']),
12
+ explain_access: schema({ sourceId: idSchema, taskId: idSchema, tool: idSchema, resourceId: idSchema }, ['sourceId', 'taskId', 'tool', 'resourceId']),
13
+ find_agents_with_access: schema({ resourceRef: idSchema,
14
+ action: { type: 'string', minLength: 3, maxLength: 256, pattern: '^[a-z0-9][a-z0-9-]*:[A-Za-z][A-Za-z0-9]*$' },
15
+ sourceId: idSchema, limit: limitSchema }, ['resourceRef', 'action']),
16
+ get_changes: schema({ sourceId: idSchema, since: idSchema }, ['sourceId']),
17
+ get_inventory_health: schema({ sourceId: idSchema }),
18
+ });
19
+ function fail(code = 'MAP_REQUEST_INVALID') { throw new MapQueryError(code); }
20
+ function integer(value, fallback, maximum, minimum = 1) {
21
+ const chosen = value === undefined ? fallback : value;
22
+ if (typeof chosen !== 'number' || !Number.isSafeInteger(chosen) || chosen < minimum || chosen > maximum)
23
+ fail('MAP_CONFIG_INVALID');
24
+ return chosen;
25
+ }
26
+ export function validateQueryArguments(operation, value) {
27
+ if (typeof operation !== 'string' || !MAP_OPERATIONS.includes(operation))
28
+ fail();
29
+ let copied;
30
+ try {
31
+ copied = immutableJson(value, 4, 512, 64 * 1024);
32
+ }
33
+ catch {
34
+ return fail();
35
+ }
36
+ if (!copied || typeof copied !== 'object' || Array.isArray(copied))
37
+ fail();
38
+ const args = copied;
39
+ const definition = MAP_INPUT_SCHEMAS[operation];
40
+ const properties = definition.properties;
41
+ const required = definition.required;
42
+ if (Object.keys(args).some(key => !Object.hasOwn(properties, key)) || required.some(key => !Object.hasOwn(args, key)))
43
+ fail();
44
+ for (const [key, item] of Object.entries(args)) {
45
+ if (key === 'limit') {
46
+ if (typeof item !== 'number' || !Number.isSafeInteger(item) || item < 1 || item > 100)
47
+ fail();
48
+ }
49
+ else if (key === 'search') {
50
+ if (typeof item !== 'string' || [...item].length > 256 || item.includes('\0') ||
51
+ /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(item))
52
+ fail();
53
+ }
54
+ else {
55
+ mapIdentifier(item);
56
+ if (key === 'action' && (typeof item !== 'string' || item.length > 256 || !/^[a-z0-9][a-z0-9-]*:[A-Za-z][A-Za-z0-9]*$/.test(item)))
57
+ fail();
58
+ }
59
+ }
60
+ return args;
61
+ }
62
+ const string = (node, key) => typeof node.properties[key] === 'string' ? node.properties[key] : null;
63
+ const number = (node, key) => typeof node.properties[key] === 'number' ? node.properties[key] : null;
64
+ const strings = (node, key) => Array.isArray(node.properties[key]) ? node.properties[key].filter((v) => typeof v === 'string') : [];
65
+ const matching = (node, id, native) => node.entityId === id || string(node, native) === id;
66
+ const unique = (values) => [...new Set(values)];
67
+ /** Count authorized work and elapsed monotonic time, including synchronous projection. */
68
+ class QueryWork {
69
+ #deadline;
70
+ #indices = new WeakMap();
71
+ #policy = new WeakMap();
72
+ #remaining = 200_000;
73
+ #evidenceBytes;
74
+ #evidenceCount = 0;
75
+ constructor(timeout, maxBytes) { this.#deadline = performance.now() + timeout; this.#evidenceBytes = maxBytes; }
76
+ step(amount = 1) {
77
+ if (performance.now() >= this.#deadline)
78
+ fail('MAP_TIMEOUT');
79
+ this.#remaining -= amount;
80
+ if (this.#remaining < 0)
81
+ fail('MAP_LIMIT_EXCEEDED');
82
+ }
83
+ index(projection) {
84
+ this.step();
85
+ const existing = this.#indices.get(projection);
86
+ if (existing)
87
+ return existing;
88
+ this.step(projection.nodes.length + projection.edges.length);
89
+ const result = { nodes: new Map(), outgoing: new Map(), incoming: new Map() };
90
+ for (const node of projection.nodes)
91
+ result.nodes.set(node.id, node);
92
+ const add = (map, from, kind, to) => {
93
+ let kinds = map.get(from);
94
+ if (!kinds) {
95
+ kinds = new Map();
96
+ map.set(from, kinds);
97
+ }
98
+ const items = kinds.get(kind);
99
+ if (items)
100
+ items.push(to);
101
+ else
102
+ kinds.set(kind, [to]);
103
+ };
104
+ for (const edge of projection.edges) {
105
+ add(result.outgoing, edge.source, edge.kind, edge.target);
106
+ add(result.incoming, edge.target, edge.kind, edge.source);
107
+ }
108
+ this.#indices.set(projection, result);
109
+ this.step();
110
+ return result;
111
+ }
112
+ evidence(value) {
113
+ this.step();
114
+ if (++this.#evidenceCount > 1000)
115
+ fail('MAP_LIMIT_EXCEEDED');
116
+ let copy;
117
+ try {
118
+ copy = immutableJson(value, 24, 25_000, this.#evidenceBytes);
119
+ }
120
+ catch {
121
+ return fail('MAP_LIMIT_EXCEEDED');
122
+ }
123
+ this.#evidenceBytes -= Buffer.byteLength(JSON.stringify(copy));
124
+ this.step();
125
+ return copy;
126
+ }
127
+ policy(projection) {
128
+ const existing = this.#policy.get(projection);
129
+ if (existing)
130
+ return existing;
131
+ const result = validatedPolicyEvidence(projection, this);
132
+ this.#policy.set(projection, result);
133
+ return result;
134
+ }
135
+ }
136
+ function connected(projection, ids, kind, work, reverse = false) {
137
+ const index = work.index(projection), selected = new Set();
138
+ work.step(ids.size);
139
+ for (const id of ids) {
140
+ const targets = (reverse ? index.incoming : index.outgoing).get(id)?.get(kind) ?? [];
141
+ work.step(targets.length);
142
+ for (const target of targets)
143
+ selected.add(target);
144
+ }
145
+ return [...selected].flatMap(id => index.nodes.get(id) ? [index.nodes.get(id)] : []);
146
+ }
147
+ const nodeIds = (nodes) => new Set(nodes.map(node => node.id));
148
+ function deployments(projection, agent, work) {
149
+ return connected(projection, new Set([agent.id]), 'G_INSTANCE_OF', work, true).filter(node => node.label === 'GuardDeployment');
150
+ }
151
+ function agentSummary(source, projection, agent, work) {
152
+ const items = deployments(projection, agent, work).map(node => ({ id: node.entityId,
153
+ deploymentId: string(node, 'deployment_id'), environment: string(node, 'environment') }));
154
+ return { id: agent.entityId, agentId: string(agent, 'agent_id'), sourceId: source.id,
155
+ name: string(agent, 'name'), ownerId: string(agent, 'owner_id'), teamId: string(agent, 'team_id'),
156
+ environments: unique(items.map(item => item.environment).filter((item) => item !== null)).sort(),
157
+ deployments: items };
158
+ }
159
+ function taskSummary(node) {
160
+ return { id: node.entityId, taskId: string(node, 'task_id'), principalId: string(node, 'principal_id'),
161
+ revoked: node.properties.revoked ?? null, sensitive: node.properties.sensitive ?? null,
162
+ usage: { calls: number(node, 'calls'), concurrent: number(node, 'concurrent'), spent: number(node, 'spent'), reserved: number(node, 'reserved') },
163
+ budget: { maxCalls: number(node, 'max_calls'), maxConcurrent: number(node, 'max_concurrent'), maxCost: number(node, 'max_cost'), deadline: number(node, 'deadline') } };
164
+ }
165
+ function grantSummary(node) {
166
+ return { id: node.entityId, taskId: string(node, 'task_id'), tool: string(node, 'tool'),
167
+ resources: strings(node, 'resources'), evidenceKind: 'declared', assessment: 'unknown' };
168
+ }
169
+ function receipt(source, projection = source.current) {
170
+ return { sourceId: source.id, tenantId: source.tenantId, profile: source.profile,
171
+ projectionId: projection.id, snapshotId: projection.snapshotId, generatedAt: projection.generatedAt,
172
+ coverage: projection.coverage };
173
+ }
174
+ function findSource(view, id) {
175
+ const source = view.sources.find(item => item.id === id);
176
+ if (!source)
177
+ fail('MAP_NOT_FOUND');
178
+ return source;
179
+ }
180
+ function findAgent(projection, id) {
181
+ const agent = projection.nodes.find(node => node.label === 'GuardAgent' && matching(node, id, 'agent_id'));
182
+ if (!agent)
183
+ fail('MAP_NOT_FOUND');
184
+ return agent;
185
+ }
186
+ /** Keep the selected agent's neighborhood explicit; never expand through shared tools to other agents. */
187
+ function focus(projection, agent, work) {
188
+ const deployment = deployments(projection, agent, work);
189
+ const tasks = connected(projection, nodeIds(deployment), 'G_ASSIGNED_TO', work, true);
190
+ const executions = connected(projection, nodeIds(deployment), 'G_EXECUTES_FOR', work, true);
191
+ const grants = connected(projection, nodeIds(tasks), 'G_HAS_GRANT', work);
192
+ const resources = connected(projection, nodeIds(grants), 'G_TARGETS', work);
193
+ const resourceBindings = connected(projection, nodeIds(resources), 'G_BINDS_SELECTOR', work, true);
194
+ const tools = [...connected(projection, nodeIds(grants), 'G_FOR_TOOL', work), ...connected(projection, nodeIds(executions), 'G_EXECUTES_TOOL', work)];
195
+ const connections = connected(projection, nodeIds(executions), 'G_VIA_CONNECTION', work), toolIds = nodeIds(tools);
196
+ const operations = connected(projection, nodeIds(connections), 'G_EXPOSES_OPERATION', work).filter(operation => connected(projection, new Set([operation.id]), 'G_OPERATION_TOOL', work).some(tool => toolIds.has(tool.id)));
197
+ const selected = nodeIds([agent, ...deployment, ...tasks, ...executions, ...grants, ...resources, ...resourceBindings,
198
+ ...tools, ...connections, ...operations, ...connected(projection, nodeIds(tasks), 'G_ACTS_AS', work),
199
+ ...connected(projection, nodeIds(executions), 'G_USES_IDENTITY', work), ...connected(projection, nodeIds(resourceBindings), 'G_MAPS_TO', work)]);
200
+ work.step(projection.nodes.length + projection.edges.length);
201
+ return { nodes: projection.nodes.filter(node => selected.has(node.id)),
202
+ edges: projection.edges.filter(edge => selected.has(edge.source) && selected.has(edge.target)) };
203
+ }
204
+ const POLICY_ACTIONS = Object.freeze({
205
+ 's3:GetObject': 'CAN_READ', 's3:PutObject': 'CAN_WRITE', 's3:ListBucket': 'CAN_LIST',
206
+ });
207
+ const POLICY_REASONS = Object.freeze({
208
+ unavailable: ['binding_ambiguous', 'cloud_unresolved', 'provider_missing', 'collection_invalid',
209
+ 'stale', 'changed', 'boundary_unresolved', 'unsupported_policy', 'evidence_invalid'],
210
+ not_collected: ['unsupported_action', 'identity_not_static', 'unsupported_binding',
211
+ 'cloud_not_collected', 'collection_missing', 'disabled'],
212
+ });
213
+ const POLICY_FIELDS = ['taskId', 'deploymentId', 'executionBindingId', 'resourceBindingId',
214
+ 'identityId', 'resourceId', 'action', 'status', 'reason', 'observedAt', 'assessment', 'candidates'].sort().join(',');
215
+ function policyObject(value) {
216
+ return !!value && typeof value === 'object' && !Array.isArray(value);
217
+ }
218
+ function policyTime(value) {
219
+ if (typeof value !== 'string')
220
+ return false;
221
+ const instant = Date.parse(value);
222
+ return Number.isFinite(instant) && instant >= 0 && instant <= 253_402_300_799_999 && new Date(instant).toISOString() === value;
223
+ }
224
+ function comparePolicyKeys(a, b) {
225
+ let left = 0, right = 0;
226
+ while (left < a.length && right < b.length) {
227
+ const ac = a.codePointAt(left), bc = b.codePointAt(right);
228
+ if (ac !== bc)
229
+ return ac - bc;
230
+ left += ac > 0xffff ? 2 : 1;
231
+ right += bc > 0xffff ? 2 : 1;
232
+ }
233
+ return (a.length - left) - (b.length - right);
234
+ }
235
+ /** Custom stores receive the same closed output contract and binding-path checks. */
236
+ function validatedPolicyEvidence(projection, work) {
237
+ const rawInput = projection.policyEvidence;
238
+ if (rawInput === undefined)
239
+ return [];
240
+ if (!Array.isArray(rawInput))
241
+ fail('MAP_UNAVAILABLE');
242
+ if (rawInput.length > 512)
243
+ fail('MAP_LIMIT_EXCEEDED');
244
+ let input;
245
+ try {
246
+ input = immutableJson(rawInput, 8, 100_000, 4 * 1024 * 1024);
247
+ }
248
+ catch {
249
+ return fail('MAP_UNAVAILABLE');
250
+ }
251
+ work.step(projection.nodes.length);
252
+ const entities = new Map(projection.nodes.map(node => [node.entityId, node]));
253
+ if (input.length && entities.size !== projection.nodes.length)
254
+ fail('MAP_UNAVAILABLE');
255
+ const seen = new Set(), result = [];
256
+ let materializedBytes = 0;
257
+ const link = (source, target, kind) => {
258
+ const targets = work.index(projection).outgoing.get(source.id)?.get(kind) ?? [];
259
+ work.step(targets.length);
260
+ return targets.includes(target.id);
261
+ };
262
+ const exactLink = (source, target, kind) => {
263
+ const targets = work.index(projection).outgoing.get(source.id)?.get(kind) ?? [];
264
+ work.step(targets.length);
265
+ return targets.length === 1 && targets[0] === target.id;
266
+ };
267
+ // Tasks may have several grants and grants several selectors, but the
268
+ // particular pair proving this record must not have duplicate relationships.
269
+ const uniqueLink = (source, target, kind) => {
270
+ const targets = work.index(projection).outgoing.get(source.id)?.get(kind) ?? [];
271
+ work.step(targets.length);
272
+ return targets.filter(id => id === target.id).length === 1;
273
+ };
274
+ const endpoint = (id, label) => {
275
+ mapIdentifier(id, 'MAP_UNAVAILABLE');
276
+ const node = entities.get(id);
277
+ if (!node || node.label !== label)
278
+ fail('MAP_UNAVAILABLE');
279
+ return node;
280
+ };
281
+ for (const raw of input) {
282
+ work.step();
283
+ if (!policyObject(raw) || Object.keys(raw).sort().join(',') !== POLICY_FIELDS ||
284
+ raw.assessment !== 'unknown' || typeof raw.action !== 'string' || raw.action.length > 4096 ||
285
+ !/^[a-z0-9][a-z0-9-]*:[A-Za-z][A-Za-z0-9]*$/.test(raw.action) ||
286
+ (raw.observedAt !== null && !policyTime(raw.observedAt)))
287
+ fail('MAP_UNAVAILABLE');
288
+ if (!Array.isArray(raw.candidates) || raw.candidates.length > 1)
289
+ fail('MAP_UNAVAILABLE');
290
+ if (raw.status === 'complete') {
291
+ if (!policyTime(raw.observedAt) || !POLICY_ACTIONS[raw.action] ||
292
+ raw.reason !== (raw.candidates.length ? null : 'no_candidate'))
293
+ fail('MAP_UNAVAILABLE');
294
+ }
295
+ else if ((raw.status !== 'unavailable' && raw.status !== 'not_collected') ||
296
+ !POLICY_REASONS[raw.status].includes(raw.reason) || raw.candidates.length)
297
+ fail('MAP_UNAVAILABLE');
298
+ for (const candidate of raw.candidates) {
299
+ if (!policyObject(candidate) || Object.keys(candidate).sort().join(',') !== 'conditionKeys,hasCondition,relationship,resourceScope' ||
300
+ candidate.relationship !== POLICY_ACTIONS[raw.action] || candidate.resourceScope !== 'bucket_summary' ||
301
+ typeof candidate.hasCondition !== 'boolean' || !Array.isArray(candidate.conditionKeys) || candidate.conditionKeys.length > 128)
302
+ fail('MAP_UNAVAILABLE');
303
+ for (const key of candidate.conditionKeys) {
304
+ work.step();
305
+ mapIdentifier(key, 'MAP_UNAVAILABLE');
306
+ if (Buffer.byteLength(key, 'utf8') > 256 || /[\u0000-\u001f]/.test(key))
307
+ fail('MAP_UNAVAILABLE');
308
+ }
309
+ if (candidate.hasCondition !== !!candidate.conditionKeys.length ||
310
+ JSON.stringify(candidate.conditionKeys) !== JSON.stringify([...new Set(candidate.conditionKeys)].sort(comparePolicyKeys)))
311
+ fail('MAP_UNAVAILABLE');
312
+ }
313
+ const task = endpoint(raw.taskId, 'GuardTask'), deployment = endpoint(raw.deploymentId, 'GuardDeployment');
314
+ const execution = endpoint(raw.executionBindingId, 'GuardExecutionBinding'), resource = endpoint(raw.resourceBindingId, 'GuardResourceBinding');
315
+ const tool = string(execution, 'tool');
316
+ if (!tool || string(resource, 'tool') !== tool || !strings(resource, 'actions').includes(raw.action) ||
317
+ !link(task, deployment, 'G_ASSIGNED_TO') || !link(execution, deployment, 'G_EXECUTES_FOR'))
318
+ fail('MAP_UNAVAILABLE');
319
+ const tools = connected(projection, new Set([execution.id]), 'G_EXECUTES_TOOL', work).filter(node => node.label === 'GuardTool' && string(node, 'name') === tool);
320
+ const grants = connected(projection, new Set([task.id]), 'G_HAS_GRANT', work).filter(node => node.label === 'GuardGrant' && string(node, 'tool') === tool);
321
+ const selectors = connected(projection, new Set([resource.id]), 'G_BINDS_SELECTOR', work).filter(node => node.label === 'GuardResource' && string(node, 'tool') === tool && string(node, 'resource_id') === string(resource, 'resource_ref'));
322
+ if (!grants.some(grant => strings(grant, 'resources').includes(string(resource, 'resource_ref') ?? '') &&
323
+ (raw.status !== 'complete' || uniqueLink(task, grant, 'G_HAS_GRANT')) &&
324
+ tools.some(toolNode => (raw.status === 'complete' ? exactLink : link)(grant, toolNode, 'G_FOR_TOOL')) &&
325
+ selectors.some(selector => (raw.status === 'complete' ? uniqueLink : link)(grant, selector, 'G_TARGETS'))))
326
+ fail('MAP_UNAVAILABLE');
327
+ for (const [key, label, relation, owner] of [
328
+ ['identityId', 'GuardExternalIdentity', 'G_USES_IDENTITY', execution],
329
+ ['resourceId', 'GuardExternalResource', 'G_MAPS_TO', resource],
330
+ ]) {
331
+ if (raw[key] === null) {
332
+ if (raw.status === 'complete')
333
+ fail('MAP_UNAVAILABLE');
334
+ continue;
335
+ }
336
+ const target = endpoint(raw[key], label);
337
+ if (!(raw.status === 'complete' ? exactLink(owner, target, relation) : link(owner, target, relation)))
338
+ fail('MAP_UNAVAILABLE');
339
+ }
340
+ if (raw.status === 'complete') {
341
+ const agents = connected(projection, new Set([deployment.id]), 'G_INSTANCE_OF', work).filter(node => node.label === 'GuardAgent');
342
+ if (agents.length !== 1 || !exactLink(deployment, agents[0], 'G_INSTANCE_OF') ||
343
+ string(execution, 'identity_selection') !== 'static' ||
344
+ !exactLink(task, deployment, 'G_ASSIGNED_TO') || !exactLink(execution, deployment, 'G_EXECUTES_FOR') ||
345
+ tools.length !== 1 || !exactLink(execution, tools[0], 'G_EXECUTES_TOOL') || selectors.length !== 1 ||
346
+ !exactLink(resource, selectors[0], 'G_BINDS_SELECTOR'))
347
+ fail('MAP_UNAVAILABLE');
348
+ }
349
+ const identity = JSON.stringify([raw.taskId, raw.deploymentId, raw.executionBindingId, raw.resourceBindingId, raw.action]);
350
+ if (seen.has(identity))
351
+ fail('MAP_UNAVAILABLE');
352
+ seen.add(identity);
353
+ try {
354
+ const copy = immutableJson(raw, 6, 1024, 64 * 1024);
355
+ materializedBytes += Buffer.byteLength(JSON.stringify(copy));
356
+ if (materializedBytes > 4 * 1024 * 1024)
357
+ fail('MAP_LIMIT_EXCEEDED');
358
+ result.push(copy);
359
+ }
360
+ catch {
361
+ fail('MAP_LIMIT_EXCEEDED');
362
+ }
363
+ }
364
+ return result;
365
+ }
366
+ function policyCoverage(records) {
367
+ const complete = records.filter(item => item.status === 'complete').length;
368
+ const observed = records.flatMap(item => item.observedAt === null ? [] : [item.observedAt]).sort();
369
+ return { status: !records.length || records.every(item => item.status === 'not_collected') ? 'not_collected' :
370
+ complete === records.length ? 'complete' : complete ? 'partial' : 'unavailable',
371
+ observedAt: observed[0] ?? null, assessment: 'unknown' };
372
+ }
373
+ function selectedPolicy(projection, work, options = {}) {
374
+ const ids = options.nodes ? new Set(options.nodes.map(node => node.entityId)) : undefined;
375
+ const executions = options.executions ? new Set(options.executions.map(node => node.entityId)) : undefined;
376
+ const resources = options.resources ? new Set(options.resources.map(node => node.entityId)) : undefined;
377
+ return work.policy(projection).filter(record => {
378
+ work.step();
379
+ return (!ids || [record.taskId, record.deploymentId, record.executionBindingId, record.resourceBindingId,
380
+ record.identityId, record.resourceId].every(id => id === null || ids.has(id))) &&
381
+ (!options.task || record.taskId === options.task.entityId) && (!executions || executions.has(record.executionBindingId)) &&
382
+ (!resources || resources.has(record.resourceBindingId)) && (!options.action || record.action === options.action);
383
+ });
384
+ }
385
+ function knownBlockers(task, tool, now) {
386
+ const blockers = [];
387
+ if (task.properties.revoked === true)
388
+ blockers.push('task_revoked');
389
+ const deadline = number(task, 'deadline');
390
+ if (deadline !== null && now >= deadline)
391
+ blockers.push('deadline_expired');
392
+ const calls = number(task, 'calls'), maxCalls = number(task, 'max_calls');
393
+ if (calls !== null && maxCalls !== null && calls >= maxCalls)
394
+ blockers.push('call_budget_exhausted');
395
+ const concurrent = number(task, 'concurrent'), maxConcurrent = number(task, 'max_concurrent');
396
+ if (concurrent !== null && maxConcurrent !== null && concurrent >= maxConcurrent)
397
+ blockers.push('concurrency_full_at_capture');
398
+ const spent = number(task, 'spent'), reserved = number(task, 'reserved'), maxCost = number(task, 'max_cost');
399
+ const cost = tool ? number(tool, 'max_cost') : null;
400
+ if (spent !== null && reserved !== null && maxCost !== null && cost !== null && spent + reserved + cost > maxCost)
401
+ blockers.push('cost_budget_exceeded');
402
+ if (tool?.properties.control_plane === true && task.properties.allow_control_plane === false)
403
+ blockers.push('control_plane_disabled');
404
+ return blockers;
405
+ }
406
+ function externalEvidence(projection, task, toolName, resourceId, work, policySelection = {}) {
407
+ const deployment = connected(projection, new Set([task.id]), 'G_ASSIGNED_TO', work);
408
+ const executions = connected(projection, nodeIds(deployment), 'G_EXECUTES_FOR', work, true)
409
+ .filter(node => string(node, 'tool') === toolName);
410
+ work.step(projection.nodes.length);
411
+ const resources = projection.nodes.filter(node => node.label === 'GuardResourceBinding' &&
412
+ string(node, 'tool') === toolName && string(node, 'resource_ref') === resourceId &&
413
+ (!policySelection.resourceBindingId || node.entityId === policySelection.resourceBindingId));
414
+ const policy = selectedPolicy(projection, work, { task, executions, resources,
415
+ ...(policySelection.action ? { action: policySelection.action } : {}) });
416
+ return {
417
+ executionBindings: executions.map(node => ({ id: node.entityId, ...node.properties,
418
+ identities: connected(projection, new Set([node.id]), 'G_USES_IDENTITY', work).map(identity => ({ ...identity.properties })),
419
+ connections: connected(projection, new Set([node.id]), 'G_VIA_CONNECTION', work).map(connection => ({ ...connection.properties })) })),
420
+ resourceBindings: resources.map(node => ({ id: node.entityId, ...node.properties,
421
+ targets: connected(projection, new Set([node.id]), 'G_MAPS_TO', work).map(target => ({ ...target.properties })) })),
422
+ policyEvidence: policy, policyCoverage: policyCoverage(policy),
423
+ assessment: 'unknown',
424
+ limitations: ['Host bindings and provider inventory are evidence, not effective cloud permissions.',
425
+ 'Container matches do not establish exact object or prefix access; missing evidence does not establish denial.'],
426
+ };
427
+ }
428
+ const COMMON_UNKNOWN = ['live_authorization', 'request_arguments_and_resolution', 'custom_policy', 'backend_authorization', 'action_approval'];
429
+ function explain(projection, scope, task, toolName, resourceId, now, work, policySelection = {}) {
430
+ const index = work.index(projection), nodes = index.nodes;
431
+ work.step(projection.nodes.length);
432
+ const tool = projection.nodes.find(node => node.label === 'GuardTool' && string(node, 'name') === toolName);
433
+ const blockers = [], matched = [], chain = [];
434
+ const unknown = [...COMMON_UNKNOWN];
435
+ if (!scope.allTeams)
436
+ unknown.push('ancestor_evidence_outside_visible_scope');
437
+ if (!tool || tool.properties.known_metadata === false || tool.properties.registered === false)
438
+ blockers.push('tool_unregistered_at_capture');
439
+ else if (tool.properties.registered !== true)
440
+ unknown.push('tool_registration');
441
+ if (tool?.properties.approved === false)
442
+ blockers.push('tool_metadata_unapproved');
443
+ else if (tool?.properties.approved !== true)
444
+ unknown.push('tool_metadata_approval');
445
+ const visited = new Set();
446
+ let current = task;
447
+ for (let depth = 0; current; depth++) {
448
+ work.step();
449
+ if (depth >= 16 || visited.has(current.id)) {
450
+ unknown.push('delegation_depth_or_cycle');
451
+ break;
452
+ }
453
+ visited.add(current.id);
454
+ chain.push({ id: current.entityId, taskId: string(current, 'task_id') });
455
+ blockers.push(...knownBlockers(current, tool, now));
456
+ const grants = connected(projection, new Set([current.id]), 'G_HAS_GRANT', work).filter(node => node.label === 'GuardGrant' && string(node, 'tool') === toolName &&
457
+ strings(node, 'resources').some(resource => resource === resourceId || resource === '*'));
458
+ if (!grants.length)
459
+ blockers.push(current.id === task.id ? 'declared_grant_missing' : 'ancestor_declared_grant_missing');
460
+ matched.push(...grants.map(grantSummary));
461
+ const parents = index.outgoing.get(current.id)?.get('G_DELEGATED_FROM') ?? [];
462
+ work.step(parents.length);
463
+ if (parents.length > 1) {
464
+ unknown.push('delegation_evidence_ambiguous');
465
+ break;
466
+ }
467
+ current = parents[0] ? nodes.get(parents[0]) : undefined;
468
+ }
469
+ if (task.properties.sensitive === true)
470
+ unknown.push('sensitive_egress_and_recipient_checks');
471
+ return { taskId: string(task, 'task_id'), tool: toolName, resourceId,
472
+ assessment: blockers.length ? 'blocked' : 'unknown', evidenceKind: 'declared_and_observed',
473
+ blockers: unique(blockers), unknownChecks: unique(unknown), delegation: chain, matchedGrants: matched,
474
+ externalEvidence: externalEvidence(projection, task, toolName, resourceId, work, policySelection),
475
+ limitations: ['Explanation only; no action was prepared or executed.', 'A clear exported gate does not establish effective permission.'] };
476
+ }
477
+ const IGNORE_DIFF = new Set(['lastupdated', 'manifest_observed_at', 'manifest_revision', 'observed_at', 'capture_observed_at', 'cloud_observed_at']);
478
+ function nodeState(node, projection, work) {
479
+ const index = work.index(projection);
480
+ const properties = Object.fromEntries(Object.entries(node.properties).filter(([key]) => !IGNORE_DIFF.has(key)));
481
+ const edges = [...(index.outgoing.get(node.id)?.entries() ?? [])].flatMap(([kind, targets]) => {
482
+ work.step(targets.length);
483
+ return targets.map(target => ({ kind, target: index.nodes.get(target)?.entityId ?? null }));
484
+ })
485
+ .sort((a, b) => `${a.kind}:${a.target}`.localeCompare(`${b.kind}:${b.target}`));
486
+ return canonicalJson({ properties, edges });
487
+ }
488
+ function freshness(observedAt, now, staleSeconds) {
489
+ const captured = typeof observedAt === 'string' ? Date.parse(observedAt) : NaN, age = now - captured;
490
+ return { stale: !Number.isFinite(captured) || age < -60_000 || age > staleSeconds * 1000,
491
+ ageSeconds: Number.isFinite(captured) ? Math.max(0, Math.floor(age / 1000)) : null };
492
+ }
493
+ function changeSummary(node) {
494
+ return { entityId: node.entityId, label: node.label,
495
+ name: string(node, 'name') ?? string(node, 'task_id') ?? string(node, 'agent_id') ?? string(node, 'deployment_id') ?? string(node, 'tool') ?? string(node, 'binding_id') ?? string(node, 'connection_id') ?? node.entityId };
496
+ }
497
+ /** One read-only evidence layer shared by HTTP, MCP and the browser. */
498
+ export class MapService {
499
+ #store;
500
+ #clock;
501
+ #maxBytes;
502
+ #maxNodes;
503
+ #maxEdges;
504
+ #timeout;
505
+ #staleSeconds;
506
+ constructor(store, options = {}) {
507
+ if (!store || typeof store.read !== 'function' || (options.clock !== undefined && typeof options.clock !== 'function'))
508
+ fail('MAP_CONFIG_INVALID');
509
+ this.#store = store;
510
+ this.#clock = options.clock ?? Date.now;
511
+ this.#maxBytes = integer(options.maxResponseBytes, 1024 * 1024, 8 * 1024 * 1024, 128);
512
+ this.#maxNodes = integer(options.maxGraphNodes, 500, 2000);
513
+ this.#maxEdges = integer(options.maxGraphEdges, 1000, 5000);
514
+ this.#timeout = integer(options.queryTimeoutMs, 5000, 10_000, 10);
515
+ this.#staleSeconds = integer(options.staleAfterSeconds, 900, 86400);
516
+ }
517
+ async query(inputScope, inputOperation, inputArgs) {
518
+ const scope = validateReadScope(inputScope);
519
+ const args = validateQueryArguments(inputOperation, inputArgs);
520
+ const operation = inputOperation;
521
+ const work = new QueryWork(this.#timeout, this.#maxBytes);
522
+ let now;
523
+ try {
524
+ now = this.#clock();
525
+ }
526
+ catch {
527
+ return fail('MAP_UNAVAILABLE');
528
+ }
529
+ if (!Number.isSafeInteger(now) || now < 0 || now > 253_402_300_799_999)
530
+ fail('MAP_UNAVAILABLE');
531
+ const request = { ...(args.sourceId ? { sourceId: args.sourceId } : {}),
532
+ ...(operation === 'list_agents' ? { summary: true } : {}),
533
+ ...(operation === 'get_agent_access' ? { agentId: args.agentId } : {}),
534
+ ...(operation === 'get_changes' ? { history: true } : {}) };
535
+ let timer;
536
+ let view;
537
+ try {
538
+ view = await Promise.race([this.#store.read(scope, request), new Promise((_resolve, reject) => {
539
+ timer = setTimeout(() => reject(new MapQueryError('MAP_TIMEOUT')), this.#timeout);
540
+ })]);
541
+ }
542
+ catch (error) {
543
+ if (error instanceof MapQueryError && ['MAP_NOT_FOUND', 'MAP_LIMIT_EXCEEDED', 'MAP_UNAVAILABLE', 'MAP_TIMEOUT', 'MAP_SCOPE_INVALID'].includes(error.code))
544
+ throw error;
545
+ return fail('MAP_UNAVAILABLE');
546
+ }
547
+ finally {
548
+ if (timer)
549
+ clearTimeout(timer);
550
+ }
551
+ work.step(view.sources.length);
552
+ let data;
553
+ let graph;
554
+ if (operation === 'list_agents') {
555
+ const search = typeof args.search === 'string' ? args.search.toLocaleLowerCase('en-US') : '';
556
+ const agents = view.sources.flatMap(source => {
557
+ work.step(source.current.nodes.length);
558
+ return source.current.nodes.filter(node => node.label === 'GuardAgent').map(agent => agentSummary(source, source.current, agent, work));
559
+ })
560
+ .filter(agent => (!search || [agent.name, agent.agentId].some(value => typeof value === 'string' && value.toLocaleLowerCase('en-US').includes(search))) &&
561
+ (args.ownerId === undefined || agent.ownerId === args.ownerId) &&
562
+ (args.environment === undefined || agent.environments.includes(args.environment)))
563
+ .sort((a, b) => `${a.name}:${a.sourceId}:${a.id}`.localeCompare(`${b.name}:${b.sourceId}:${b.id}`));
564
+ const limit = args.limit ?? 50;
565
+ data = { agents: agents.slice(0, limit), truncated: agents.length > limit };
566
+ }
567
+ else if (operation === 'get_agent_access') {
568
+ const source = findSource(view, args.sourceId), projection = source.current;
569
+ const agent = findAgent(projection, args.agentId);
570
+ graph = focus(projection, agent, work);
571
+ const policy = selectedPolicy(projection, work, { nodes: graph.nodes });
572
+ data = { agent: agentSummary(source, projection, agent, work),
573
+ deployments: graph.nodes.filter(node => node.label === 'GuardDeployment').map(node => ({ id: node.entityId, deploymentId: string(node, 'deployment_id'), environment: string(node, 'environment') })),
574
+ tasks: graph.nodes.filter(node => node.label === 'GuardTask').map(taskSummary),
575
+ grants: graph.nodes.filter(node => node.label === 'GuardGrant').map(grantSummary),
576
+ connections: graph.nodes.filter(node => node.label === 'GuardMcpConnection').map(node => ({ id: node.entityId, ...node.properties })),
577
+ bindings: graph.nodes.filter(node => ['GuardExecutionBinding', 'GuardResourceBinding'].includes(node.label)).map(node => ({ id: node.entityId, kind: node.label, ...node.properties })),
578
+ policyEvidence: policy, policyCoverage: policyCoverage(policy),
579
+ receipt: receipt(source), assessment: 'unknown',
580
+ limitations: ['Declared and observed evidence only; current executor admission is required.', 'External object and prefix bindings retain their original scope.'] };
581
+ }
582
+ else if (operation === 'explain_access') {
583
+ const source = findSource(view, args.sourceId);
584
+ const task = source.current.nodes.find(node => node.label === 'GuardTask' && matching(node, args.taskId, 'task_id'));
585
+ if (!task)
586
+ fail('MAP_NOT_FOUND');
587
+ data = { ...explain(source.current, scope, task, args.tool, args.resourceId, now, work), receipt: receipt(source) };
588
+ }
589
+ else if (operation === 'find_agents_with_access') {
590
+ const candidates = [];
591
+ const limit = args.limit ?? 50;
592
+ let truncated = false;
593
+ candidateSearch: for (const source of view.sources)
594
+ for (const agent of source.current.nodes.filter(node => node.label === 'GuardAgent')) {
595
+ work.step();
596
+ const selected = focus(source.current, agent, work), selectedProjection = { ...source.current, ...selected };
597
+ const bindings = selected.nodes.filter(node => node.label === 'GuardResourceBinding' && strings(node, 'actions').includes(args.action));
598
+ const evidence = [];
599
+ for (const binding of bindings) {
600
+ work.step();
601
+ const external = connected(selectedProjection, new Set([binding.id]), 'G_MAPS_TO', work)[0];
602
+ if (string(binding, 'resource_ref') !== args.resourceRef && (!external || string(external, 'arn') !== args.resourceRef))
603
+ continue;
604
+ const toolName = string(binding, 'tool'), resource = string(binding, 'resource_ref');
605
+ if (!toolName || !resource)
606
+ continue;
607
+ for (const task of selected.nodes.filter(node => node.label === 'GuardTask')) {
608
+ work.step();
609
+ if (!connected(selectedProjection, new Set([task.id]), 'G_HAS_GRANT', work).some(grant => string(grant, 'tool') === toolName && strings(grant, 'resources').some(item => item === '*' || item === resource)))
610
+ continue;
611
+ if (candidates.length >= limit) {
612
+ truncated = true;
613
+ break candidateSearch;
614
+ }
615
+ const deployment = connected(selectedProjection, new Set([task.id]), 'G_ASSIGNED_TO', work);
616
+ const executions = connected(selectedProjection, nodeIds(deployment), 'G_EXECUTES_FOR', work, true).filter(node => string(node, 'tool') === toolName);
617
+ const policy = selectedPolicy(source.current, work, { task, executions, resources: [binding], action: args.action });
618
+ evidence.push(work.evidence({ taskId: string(task, 'task_id'), tool: toolName, resourceRef: resource, action: args.action,
619
+ scope: string(binding, 'scope'), externalResource: external ? { ...external.properties } : null,
620
+ executionBindings: executions.map(node => ({ id: node.entityId, ...node.properties,
621
+ identities: connected(selectedProjection, new Set([node.id]), 'G_USES_IDENTITY', work).map(identity => ({ ...identity.properties })) })),
622
+ policyEvidence: policy, policyCoverage: policyCoverage(policy),
623
+ explanation: explain(source.current, scope, task, toolName, resource, now, work, { action: args.action, resourceBindingId: binding.entityId }) }));
624
+ }
625
+ }
626
+ if (evidence.length)
627
+ candidates.push({ agent: agentSummary(source, source.current, agent, work), evidence,
628
+ assessment: 'unknown', limitations: ['Potential access candidates; no effective permission decision.', 'Cloud containment is not exact object authorization.'] });
629
+ }
630
+ data = { candidates, truncated };
631
+ }
632
+ else if (operation === 'get_changes') {
633
+ const source = findSource(view, args.sourceId), current = source.current;
634
+ const since = args.since ?? 'previous';
635
+ const previous = [current.id, current.snapshotId].includes(since) ? current :
636
+ source.previous && (since === 'previous' || [source.previous.id, source.previous.snapshotId].includes(since)) ? source.previous : null;
637
+ const added = [], removed = [], changed = [];
638
+ if (previous) {
639
+ const before = new Map(previous.nodes.map(node => [node.entityId, node])), after = new Map(current.nodes.map(node => [node.entityId, node]));
640
+ for (const [id, node] of after) {
641
+ work.step();
642
+ if (!before.has(id))
643
+ added.push(changeSummary(node));
644
+ else if (nodeState(node, current, work) !== nodeState(before.get(id), previous, work))
645
+ changed.push(changeSummary(node));
646
+ }
647
+ for (const [id, node] of before)
648
+ if (!after.has(id))
649
+ removed.push(changeSummary(node));
650
+ }
651
+ data = { sourceId: source.id, from: previous ? receipt(source, previous) : null, to: receipt(source),
652
+ changes: { added, removed, changed }, coverageGap: previous === null, policyHistory: 'not_collected',
653
+ limitations: ['Net changes in authorized retained snapshots only; not an invocation or intermediate-event log.',
654
+ 'Current IAM policy observations are separate from retained Guard snapshots; policy history is not collected.'] };
655
+ }
656
+ else {
657
+ data = { sources: view.sources.map(source => {
658
+ work.step();
659
+ return { id: source.id, sourceId: source.sourceId, tenantId: source.tenantId, profile: source.profile,
660
+ generatedAt: source.current.generatedAt, snapshotId: source.current.snapshotId, projectionId: source.current.id,
661
+ ...freshness(source.current.generatedAt, now, this.#staleSeconds),
662
+ coverage: { ...Object.fromEntries(Object.entries(source.current.coverage).filter(([component]) => component !== 'policy').map(([component, status]) => {
663
+ work.step();
664
+ return [component, { ...status, ...freshness(status.observedAt, now, this.#staleSeconds) }];
665
+ })), policy: policyCoverage(work.policy(source.current)) } };
666
+ }), visibility: scope.allTeams ? 'enrolled_sources' : 'visible_agent_sources',
667
+ limitations: ['Collection freshness is separate from current execution authority.'] };
668
+ }
669
+ if (graph && (graph.nodes.length > this.#maxNodes || graph.edges.length > this.#maxEdges))
670
+ fail('MAP_LIMIT_EXCEEDED');
671
+ work.step();
672
+ const result = { operation, observedAt: new Date(now).toISOString(), data, ...(graph ? { graph } : {}) };
673
+ let copied;
674
+ try {
675
+ copied = immutableJson(result, 32, 100_000, this.#maxBytes);
676
+ }
677
+ catch {
678
+ return fail('MAP_LIMIT_EXCEEDED');
679
+ }
680
+ work.step();
681
+ return copied;
682
+ }
683
+ }