@shieldaai/agent-map 0.1.0-alpha.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +652 -272
  2. package/dist/assets.js +16 -26
  3. package/dist/boundary-flags.d.ts +73 -0
  4. package/dist/boundary-flags.js +352 -0
  5. package/dist/boundary.d.ts +18 -3
  6. package/dist/boundary.js +71 -21
  7. package/dist/cli-entry.d.ts +11 -0
  8. package/dist/cli-entry.js +48 -0
  9. package/dist/cli.js +40 -43
  10. package/dist/common.d.ts +34 -0
  11. package/dist/common.js +94 -0
  12. package/dist/http.js +78 -51
  13. package/dist/identity-cli.js +93 -46
  14. package/dist/identity-http.js +226 -94
  15. package/dist/index.d.ts +4 -4
  16. package/dist/index.js +4 -4
  17. package/dist/mcp.js +40 -5
  18. package/dist/oidc.d.ts +5 -0
  19. package/dist/oidc.js +121 -38
  20. package/dist/policy-reader.d.ts +56 -3
  21. package/dist/policy-reader.js +823 -110
  22. package/dist/projection-fields.d.ts +4 -0
  23. package/dist/projection-fields.js +144 -0
  24. package/dist/service/changes.d.ts +5 -0
  25. package/dist/service/changes.js +255 -0
  26. package/dist/service/context.d.ts +33 -0
  27. package/dist/service/context.js +2 -0
  28. package/dist/service/discovery.d.ts +6 -0
  29. package/dist/service/discovery.js +31 -0
  30. package/dist/service/explain.d.ts +12 -0
  31. package/dist/service/explain.js +126 -0
  32. package/dist/service/flags.d.ts +20 -0
  33. package/dist/service/flags.js +70 -0
  34. package/dist/service/inventory.d.ts +4 -0
  35. package/dist/service/inventory.js +62 -0
  36. package/dist/service/json.d.ts +8 -0
  37. package/dist/service/json.js +7 -0
  38. package/dist/service/operations/discovery.d.ts +5 -0
  39. package/dist/service/operations/discovery.js +90 -0
  40. package/dist/service/operations/explain-access.d.ts +3 -0
  41. package/dist/service/operations/explain-access.js +25 -0
  42. package/dist/service/operations/find-agents-with-access.d.ts +3 -0
  43. package/dist/service/operations/find-agents-with-access.js +120 -0
  44. package/dist/service/operations/get-agent-access.d.ts +3 -0
  45. package/dist/service/operations/get-agent-access.js +75 -0
  46. package/dist/service/operations/get-agent-inventory.d.ts +3 -0
  47. package/dist/service/operations/get-agent-inventory.js +42 -0
  48. package/dist/service/operations/get-changes.d.ts +3 -0
  49. package/dist/service/operations/get-changes.js +11 -0
  50. package/dist/service/operations/get-inventory-health.d.ts +3 -0
  51. package/dist/service/operations/get-inventory-health.js +81 -0
  52. package/dist/service/operations/get-resource-access.d.ts +3 -0
  53. package/dist/service/operations/get-resource-access.js +48 -0
  54. package/dist/service/operations/list-agents.d.ts +3 -0
  55. package/dist/service/operations/list-agents.js +92 -0
  56. package/dist/service/operations/list-owners-resources.d.ts +5 -0
  57. package/dist/service/operations/list-owners-resources.js +69 -0
  58. package/dist/service/ordering.d.ts +9 -0
  59. package/dist/service/ordering.js +47 -0
  60. package/dist/service/outside-guard.d.ts +68 -0
  61. package/dist/service/outside-guard.js +313 -0
  62. package/dist/service/policy.d.ts +26 -0
  63. package/dist/service/policy.js +299 -0
  64. package/dist/service/schemas.d.ts +16 -0
  65. package/dist/service/schemas.js +192 -0
  66. package/dist/service/summaries.d.ts +32 -0
  67. package/dist/service/summaries.js +247 -0
  68. package/dist/service/work.d.ts +33 -0
  69. package/dist/service/work.js +116 -0
  70. package/dist/service.d.ts +5 -5
  71. package/dist/service.js +110 -645
  72. package/dist/store/ancestors.d.ts +9 -0
  73. package/dist/store/ancestors.js +86 -0
  74. package/dist/store/discovery.d.ts +8 -0
  75. package/dist/store/discovery.js +288 -0
  76. package/dist/store/evidence.d.ts +12 -0
  77. package/dist/store/evidence.js +90 -0
  78. package/dist/store/identity-scope.d.ts +9 -0
  79. package/dist/store/identity-scope.js +71 -0
  80. package/dist/store/nodes.d.ts +24 -0
  81. package/dist/store/nodes.js +237 -0
  82. package/dist/store/projection.d.ts +19 -0
  83. package/dist/store/projection.js +242 -0
  84. package/dist/store/queries.d.ts +48 -0
  85. package/dist/store/queries.js +302 -0
  86. package/dist/store/read.d.ts +81 -0
  87. package/dist/store/read.js +135 -0
  88. package/dist/store/selection.d.ts +3 -0
  89. package/dist/store/selection.js +102 -0
  90. package/dist/store.d.ts +15 -0
  91. package/dist/store.js +102 -395
  92. package/dist/transport.d.ts +42 -2
  93. package/dist/transport.js +144 -16
  94. package/dist/types.d.ts +184 -4
  95. package/dist/types.js +52 -11
  96. package/package.json +16 -3
@@ -0,0 +1,313 @@
1
+ import { s3BucketName, shortenScopeList } from '../common.js';
2
+ import { MapQueryError, } from '../types.js';
3
+ import { jsonList, jsonObject } from './json.js';
4
+ import { compare, ordered } from './ordering.js';
5
+ import { deployments } from './summaries.js';
6
+ import { connected, number, QueryWork, string } from './work.js';
7
+ /** A declared target names a host resource; name it only when that resource is in the reader's scope. */
8
+ export function targetVisibility(projection, scope, work) {
9
+ if (scope.allTeams)
10
+ return () => true;
11
+ work.step(projection.nodes.length);
12
+ const visible = new Set([
13
+ ...projection.nodes
14
+ .filter(node => node.label === 'GuardExternalResource')
15
+ .map(node => string(node, 'host_resource_id'))
16
+ .filter((id) => id !== null),
17
+ ...(projection.inScopeTargets ?? []),
18
+ ]);
19
+ return target => visible.has(target);
20
+ }
21
+ /**
22
+ * Host-declared paths that act without the guard, per deployment in the agent's focus.
23
+ * Manifest v3 sets `direct_access_status`; older manifests leave it absent, which reads as
24
+ * `unknown` and never as `none_declared`.
25
+ */
26
+ export function directAccess(projection, graph, scope, work) {
27
+ const visibleTarget = targetVisibility(projection, scope, work);
28
+ return ordered(graph.nodes
29
+ .filter(node => node.label === 'GuardDeployment')
30
+ .map(deployment => {
31
+ const entries = connected(projection, new Set([deployment.id]), 'G_DIRECT_ACCESS_FOR', work, true);
32
+ const declared = string(deployment, 'direct_access_status');
33
+ // An entry without the v3 status would be inconsistent; report what was found.
34
+ const status = entries.length ? 'found' : declared === 'none_declared' ? 'none_declared' : 'unknown';
35
+ return {
36
+ id: deployment.entityId,
37
+ deploymentId: string(deployment, 'deployment_id'),
38
+ status,
39
+ evidenceKind: 'declared',
40
+ entries: ordered(entries.map(entry => {
41
+ const target = string(entry, 'target_ref');
42
+ const targetStatus = target === null ? 'none' : visibleTarget(target) ? 'visible' : 'outside_visible_scope';
43
+ return {
44
+ id: entry.entityId,
45
+ accessId: string(entry, 'access_id'),
46
+ kind: string(entry, 'kind'),
47
+ targetRef: targetStatus === 'visible' ? target : null,
48
+ targetStatus,
49
+ description: string(entry, 'description'),
50
+ };
51
+ })),
52
+ };
53
+ }));
54
+ }
55
+ /** `data` with each identity-scope list cut to `keep` entries, or null if nothing would change. */
56
+ export function shortenIdentityScope(data, keep) {
57
+ const outside = data.outsideGuard;
58
+ const records = outside?.identityScope;
59
+ if (!Array.isArray(records))
60
+ return null;
61
+ const shortened = records.map(record => shortenScopeList(record, keep));
62
+ if (shortened.every((record, index) => record === records[index]))
63
+ return null;
64
+ return { ...data, outsideGuard: { ...outside, identityScope: jsonList(shortened) } };
65
+ }
66
+ export const reachIndex = new WeakMap();
67
+ /**
68
+ * Outside-guard reach over the whole visible projection: any declared direct path or any
69
+ * evaluated tool identity can touch any resource, so `none_found` needs every visible
70
+ * deployment to have declared its paths and every visible identity to be evaluated.
71
+ */
72
+ export function outsideGuardIndex(projection, work) {
73
+ const existing = reachIndex.get(projection);
74
+ if (existing)
75
+ return existing;
76
+ work.step(projection.nodes.length);
77
+ let undeclaredDeployments = 0, untargetedPaths = 0, undeclaredIdentities = 0;
78
+ const identityAccounts = new Set();
79
+ const directByTarget = new Map();
80
+ // The tools each deployment has an execution binding for, read once per deployment.
81
+ const executed = new Map();
82
+ const executedTools = (deployment) => {
83
+ let tools = executed.get(deployment.id);
84
+ if (!tools) {
85
+ tools = new Set(connected(projection, new Set([deployment.id]), 'G_EXECUTES_FOR', work, true).map(item => string(item, 'tool')));
86
+ executed.set(deployment.id, tools);
87
+ }
88
+ return tools;
89
+ };
90
+ for (const node of projection.nodes) {
91
+ if (node.label === 'GuardDeployment') {
92
+ const status = string(node, 'direct_access_status');
93
+ // `found` with no entry in the read is inconsistent, so it is not a declaration either.
94
+ const entries = connected(projection, new Set([node.id]), 'G_DIRECT_ACCESS_FOR', work, true).length;
95
+ if (!(status === 'none_declared' || (status === 'found' && entries > 0)))
96
+ undeclaredDeployments++;
97
+ }
98
+ if (node.label === 'GuardDirectAccess') {
99
+ const target = string(node, 'target_ref');
100
+ if (target === null)
101
+ untargetedPaths++;
102
+ else {
103
+ const paths = directByTarget.get(target);
104
+ if (paths)
105
+ paths.push(node);
106
+ else
107
+ directByTarget.set(target, [node]);
108
+ }
109
+ }
110
+ if (node.label === 'GuardTask') {
111
+ // A granted tool runs under some identity on every deployment the task is assigned to;
112
+ // a deployment without an execution binding for it leaves that identity undeclared.
113
+ const granted = new Set(connected(projection, new Set([node.id]), 'G_HAS_GRANT', work).map(item => string(item, 'tool')));
114
+ if (granted.size) {
115
+ const deployments = connected(projection, new Set([node.id]), 'G_ASSIGNED_TO', work);
116
+ work.step(granted.size * deployments.length);
117
+ if (!deployments.length || deployments.some(item => [...granted].some(tool => !executedTools(item).has(tool))))
118
+ undeclaredIdentities++;
119
+ }
120
+ }
121
+ }
122
+ const identitiesByBucket = new Map();
123
+ let identityUnknown = 0;
124
+ // A visible execution binding the reach read never reported on was not evaluated.
125
+ if (projection.identityReach !== undefined) {
126
+ const reported = new Set(projection.identityReach.map(record => record.executionBindingId));
127
+ for (const node of projection.nodes)
128
+ if (node.label === 'GuardExecutionBinding' && !reported.has(node.entityId))
129
+ identityUnknown++;
130
+ }
131
+ // A target the read cannot match may be any resource, so it counts like no target.
132
+ const hostIds = new Set(projection.nodes
133
+ .filter(node => node.label === 'GuardExternalResource')
134
+ .map(node => string(node, 'host_resource_id'))
135
+ .filter((id) => id !== null));
136
+ let unresolvedPaths = 0;
137
+ for (const [target, paths] of directByTarget)
138
+ if (!hostIds.has(target))
139
+ unresolvedPaths += paths.length;
140
+ for (const record of projection.identityReach ?? []) {
141
+ work.step(1 + record.extraResources.length);
142
+ // Only a complete comparison counts: any other status, or a record whose list was cut
143
+ // (it may reach buckets it no longer names), leaves reach unknown.
144
+ if (!(record.status === 'matches_grants' || record.status === 'broader_than_grants') || record.truncated)
145
+ identityUnknown++;
146
+ identityAccounts.add(record.accountId ?? null);
147
+ for (const extra of record.extraResources) {
148
+ const reached = identitiesByBucket.get(extra.bucketName);
149
+ if (reached)
150
+ reached.push({ record, extra });
151
+ else
152
+ identitiesByBucket.set(extra.bucketName, [{ record, extra }]);
153
+ }
154
+ }
155
+ const result = {
156
+ undeclaredDeployments,
157
+ untargetedPaths,
158
+ unresolvedPaths,
159
+ undeclaredIdentities,
160
+ directByTarget,
161
+ identitiesByBucket,
162
+ identityUnknown,
163
+ identityCollected: projection.identityReach !== undefined,
164
+ identityAccounts,
165
+ };
166
+ reachIndex.set(projection, result);
167
+ return result;
168
+ }
169
+ /** Entries listed per resource in get_resource_access before the list is marked truncated. */
170
+ export const REACH_LIST_LIMIT = 100;
171
+ /**
172
+ * `detail` builds the (bounded) lists for one resource; the listing only needs the status,
173
+ * which comes from index lookups.
174
+ */
175
+ export function outsideGuardReach(projection, resource, scope, work, detail = false) {
176
+ const index = outsideGuardIndex(projection, work);
177
+ const targets = connected(projection, new Set([resource.id]), 'G_MAPS_TO', work);
178
+ // The collector writes one target per binding; anything else is malformed and never none_found.
179
+ const malformed = targets.length !== 1;
180
+ const hosts = targets.map(node => string(node, 'host_resource_id')).filter((id) => id !== null);
181
+ const accounts = new Set(targets.filter(node => string(node, 'provider') === 'aws').map(node => string(node, 'account_id')));
182
+ const buckets = targets
183
+ .filter(node => string(node, 'provider') === 'aws')
184
+ .map(node => s3BucketName(string(node, 'arn')))
185
+ .filter((name) => name !== null);
186
+ work.step(1 + hosts.length + buckets.length);
187
+ const reached = hosts.some(id => index.directByTarget.has(id)) || buckets.some(name => index.identitiesByBucket.has(name));
188
+ let directAccess = [], identities = [], directAccessTruncated = false, identitiesTruncated = false;
189
+ if (detail) {
190
+ const paths = hosts
191
+ .flatMap(id => index.directByTarget.get(id) ?? [])
192
+ .sort((a, b) => compare(a.entityId, b.entityId));
193
+ const reaching = buckets
194
+ .flatMap(name => index.identitiesByBucket.get(name) ?? [])
195
+ .sort((a, b) => compare(a.record.executionBindingId, b.record.executionBindingId) ||
196
+ compare(a.extra.bucketName, b.extra.bucketName) ||
197
+ compare(a.extra.relationship, b.extra.relationship));
198
+ work.step(paths.length + reaching.length);
199
+ directAccessTruncated = paths.length > REACH_LIST_LIMIT;
200
+ identitiesTruncated = reaching.length > REACH_LIST_LIMIT;
201
+ directAccess = paths.slice(0, REACH_LIST_LIMIT).map(node => ({
202
+ id: node.entityId,
203
+ accessId: string(node, 'access_id'),
204
+ deploymentId: string(node, 'deployment_id'),
205
+ kind: string(node, 'kind'),
206
+ description: string(node, 'description'),
207
+ evidenceKind: 'declared',
208
+ }));
209
+ identities = reaching.slice(0, REACH_LIST_LIMIT).map(({ record, extra }) => jsonObject({
210
+ id: record.executionBindingId,
211
+ executionBindingId: record.executionBindingId,
212
+ deploymentId: record.deploymentId,
213
+ tool: record.tool,
214
+ identityId: record.identityId,
215
+ relationship: extra.relationship,
216
+ hasCondition: extra.hasCondition,
217
+ conditionKeys: extra.conditionKeys,
218
+ evidenceKind: 'iam_identity_policy_candidate',
219
+ }));
220
+ }
221
+ const unknownReasons = [];
222
+ if (malformed)
223
+ unknownReasons.push('binding_ambiguous');
224
+ // Candidates exist only for buckets the AWS collection observed; an unresolved bucket was
225
+ // never compared with any identity.
226
+ if (targets.some(node => string(node, 'provider') === 'aws' && string(node, 'cloud_status') !== 'resolved'))
227
+ unknownReasons.push('resource_not_collected');
228
+ if (index.undeclaredDeployments)
229
+ unknownReasons.push('direct_access_undeclared');
230
+ if (index.untargetedPaths)
231
+ unknownReasons.push('direct_access_untargeted');
232
+ if (index.unresolvedPaths)
233
+ unknownReasons.push('direct_access_target_unresolved');
234
+ if (!buckets.length)
235
+ unknownReasons.push('identity_scope_not_collected_for_resource');
236
+ else if (!index.identityCollected)
237
+ unknownReasons.push('identity_scope_not_collected');
238
+ else {
239
+ if (index.identityUnknown)
240
+ unknownReasons.push('identity_scope_unknown');
241
+ // Candidates are collected per account: an identity elsewhere was never compared with
242
+ // this bucket, and a cross-account bucket policy can still let it in.
243
+ if ([...index.identityAccounts].some(account => account === null || !accounts.has(account)) || accounts.has(null))
244
+ unknownReasons.push('identity_scope_other_account');
245
+ if (index.undeclaredIdentities)
246
+ unknownReasons.push('execution_identity_undeclared');
247
+ }
248
+ // Paths and identities of teams this reader cannot see may reach the resource too.
249
+ if (!scope.allTeams && projection.outsideVisibleScope !== false)
250
+ unknownReasons.push('outside_visible_scope');
251
+ const status = reached ? 'yes' : unknownReasons.length ? 'unknown' : 'none_found';
252
+ return { status, unknownReasons, directAccess, directAccessTruncated, identities, identitiesTruncated };
253
+ }
254
+ /**
255
+ * Reach with its own work budget, shared by every row of one response: if it runs out, that
256
+ * row and every later one read `unknown` (`reach_not_evaluated`) and the response still
257
+ * answers. Rows judged before that keep their status: each used a complete index.
258
+ */
259
+ export function boundedReach(remaining, maxBytes) {
260
+ const work = new QueryWork(Math.max(1, remaining - 25), maxBytes);
261
+ let exhausted = false;
262
+ return (projection, resource, scope, detail = false) => {
263
+ if (!exhausted)
264
+ try {
265
+ return outsideGuardReach(projection, resource, scope, work, detail);
266
+ }
267
+ catch (error) {
268
+ const code = error instanceof MapQueryError ? error.code : '';
269
+ if (code !== 'MAP_LIMIT_EXCEEDED' && code !== 'MAP_TIMEOUT')
270
+ throw error;
271
+ exhausted = true;
272
+ }
273
+ return {
274
+ status: 'unknown',
275
+ unknownReasons: ['reach_not_evaluated'],
276
+ directAccess: [],
277
+ directAccessTruncated: false,
278
+ identities: [],
279
+ identitiesTruncated: false,
280
+ };
281
+ };
282
+ }
283
+ export const OUTSIDE_GUARD_LIMITATION = 'Only S3 identity policies and host declarations of this source are checked, within your visible scope; none_found is not proof that no path exists.';
284
+ /**
285
+ * C2 per execution binding in the agent's focus. A store that did not run the reverse identity
286
+ * read reports every binding as `unknown` rather than leaving it out.
287
+ */
288
+ export function identityScope(projection, graph) {
289
+ const executions = graph.nodes.filter(node => node.label === 'GuardExecutionBinding');
290
+ const known = new Map((projection.identityScope ?? []).map(item => [item.executionBindingId, item]));
291
+ return ordered(executions.map(execution => {
292
+ const item = known.get(execution.entityId);
293
+ return jsonObject({
294
+ id: execution.entityId,
295
+ ...(item ?? {
296
+ executionBindingId: execution.entityId,
297
+ deploymentId: null,
298
+ tool: string(execution, 'tool'),
299
+ identityId: null,
300
+ accountId: null,
301
+ status: 'unknown',
302
+ // The store did not evaluate this binding (not run, or more work than allowed).
303
+ reason: 'not_evaluated',
304
+ observedAt: null,
305
+ boundBuckets: [],
306
+ extraResources: [],
307
+ truncated: false,
308
+ evidenceKind: 'iam_identity_policy_candidate',
309
+ assessment: 'unknown',
310
+ }),
311
+ });
312
+ }));
313
+ }
@@ -0,0 +1,26 @@
1
+ import { type JsonObject, type MapNode, type MapPolicyEvidence, type MapProjection } from '../types.js';
2
+ import { QueryWork } from './work.js';
3
+ export declare const POLICY_ACTIONS: Readonly<Record<string, string>>;
4
+ export declare const POLICY_REASONS: Readonly<{
5
+ unavailable: string[];
6
+ not_collected: string[];
7
+ }>;
8
+ export declare const POLICY_FIELDS: string;
9
+ export declare function policyObject(value: unknown): value is Record<string, unknown>;
10
+ export declare function policyTime(value: unknown): boolean;
11
+ export declare function policyCoverage(records: readonly MapPolicyEvidence[]): JsonObject;
12
+ /**
13
+ * Coverage for a selection of one projection's policy evidence. When the store found more
14
+ * task/binding/action pairs than one read evaluates it loaded none: that is unknown, never
15
+ * "not collected".
16
+ */
17
+ export declare function policyCoverageOf(projection: MapProjection, records: readonly MapPolicyEvidence[]): JsonObject;
18
+ export declare function selectedPolicy(projection: MapProjection, work: QueryWork, options?: {
19
+ nodes?: readonly MapNode[];
20
+ task?: MapNode;
21
+ executions?: readonly MapNode[];
22
+ resources?: readonly MapNode[];
23
+ action?: string;
24
+ }): readonly MapPolicyEvidence[];
25
+ /** The projection's policy evidence, validated once per query. */
26
+ export declare function policyRecords(projection: MapProjection, work: QueryWork): readonly MapPolicyEvidence[];
@@ -0,0 +1,299 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { immutableJson } from '@shieldaai/agent-guard/internal';
3
+ import { compareCodePoints, fail } from '../common.js';
4
+ import { mapIdentifier, } from '../types.js';
5
+ import { connected, QueryWork, string, strings } from './work.js';
6
+ export const POLICY_ACTIONS = Object.freeze({
7
+ 's3:GetObject': 'CAN_READ',
8
+ 's3:PutObject': 'CAN_WRITE',
9
+ 's3:ListBucket': 'CAN_LIST',
10
+ });
11
+ export const POLICY_REASONS = Object.freeze({
12
+ unavailable: [
13
+ 'binding_ambiguous',
14
+ 'cloud_unresolved',
15
+ 'provider_missing',
16
+ 'collection_invalid',
17
+ 'stale',
18
+ 'changed',
19
+ 'boundary_unresolved',
20
+ 'unsupported_policy',
21
+ 'evidence_invalid',
22
+ ],
23
+ not_collected: [
24
+ 'unsupported_action',
25
+ 'identity_not_static',
26
+ 'unsupported_binding',
27
+ 'cloud_not_collected',
28
+ 'collection_missing',
29
+ 'disabled',
30
+ ],
31
+ });
32
+ export const POLICY_FIELDS = [
33
+ 'taskId',
34
+ 'deploymentId',
35
+ 'executionBindingId',
36
+ 'resourceBindingId',
37
+ 'identityId',
38
+ 'resourceId',
39
+ 'action',
40
+ 'status',
41
+ 'reason',
42
+ 'observedAt',
43
+ 'assessment',
44
+ 'candidates',
45
+ ]
46
+ .sort()
47
+ .join(',');
48
+ export function policyObject(value) {
49
+ return !!value && typeof value === 'object' && !Array.isArray(value);
50
+ }
51
+ export function policyTime(value) {
52
+ if (typeof value !== 'string')
53
+ return false;
54
+ const instant = Date.parse(value);
55
+ return (Number.isFinite(instant) &&
56
+ instant >= 0 &&
57
+ instant <= 253_402_300_799_999 &&
58
+ new Date(instant).toISOString() === value);
59
+ }
60
+ /**
61
+ * A copy made by `immutableJson` is plain JSON; the checks in `validatedPolicyEvidence` are what
62
+ * establish that each entry has the record's shape, so nothing reads a field before they run.
63
+ */
64
+ const policyList = (copy) => copy;
65
+ /** Custom stores receive the same closed output contract and binding-path checks. */
66
+ function validatedPolicyEvidence(projection, work) {
67
+ const rawInput = projection.policyEvidence;
68
+ if (rawInput === undefined)
69
+ return [];
70
+ if (!Array.isArray(rawInput))
71
+ fail('MAP_UNAVAILABLE');
72
+ if (rawInput.length > 512)
73
+ fail('MAP_LIMIT_EXCEEDED');
74
+ let input;
75
+ try {
76
+ input = policyList(immutableJson(rawInput, 8, 100_000, 4 * 1024 * 1024));
77
+ }
78
+ catch {
79
+ return fail('MAP_UNAVAILABLE');
80
+ }
81
+ work.step(projection.nodes.length);
82
+ const entities = new Map(projection.nodes.map(node => [node.entityId, node]));
83
+ if (input.length && entities.size !== projection.nodes.length)
84
+ fail('MAP_UNAVAILABLE');
85
+ const seen = new Set(), result = [];
86
+ let materializedBytes = 0;
87
+ const link = (source, target, kind) => {
88
+ const targets = work.index(projection).outgoing.get(source.id)?.get(kind) ?? [];
89
+ work.step(targets.length);
90
+ return targets.includes(target.id);
91
+ };
92
+ const exactLink = (source, target, kind) => {
93
+ const targets = work.index(projection).outgoing.get(source.id)?.get(kind) ?? [];
94
+ work.step(targets.length);
95
+ return targets.length === 1 && targets[0] === target.id;
96
+ };
97
+ // Tasks may have several grants and grants several selectors, but the
98
+ // particular pair proving this record must not have duplicate relationships.
99
+ const uniqueLink = (source, target, kind) => {
100
+ const targets = work.index(projection).outgoing.get(source.id)?.get(kind) ?? [];
101
+ work.step(targets.length);
102
+ return targets.filter(id => id === target.id).length === 1;
103
+ };
104
+ const endpoint = (id, label) => {
105
+ mapIdentifier(id, 'MAP_UNAVAILABLE');
106
+ const node = entities.get(id);
107
+ if (!node || node.label !== label)
108
+ fail('MAP_UNAVAILABLE');
109
+ return node;
110
+ };
111
+ for (const raw of input) {
112
+ work.step();
113
+ if (!policyObject(raw) ||
114
+ Object.keys(raw).sort().join(',') !== POLICY_FIELDS ||
115
+ raw.assessment !== 'unknown' ||
116
+ typeof raw.action !== 'string' ||
117
+ (raw.observedAt !== null && !policyTime(raw.observedAt)))
118
+ fail('MAP_UNAVAILABLE');
119
+ mapIdentifier(raw.action, 'MAP_UNAVAILABLE');
120
+ // Generic host actions carry no cloud-policy inference. Keep their explicit
121
+ // unsupported observation readable without admitting a cloud candidate.
122
+ const genericAction = !/^[a-z0-9][a-z0-9-]*:[A-Za-z][A-Za-z0-9]*$/.test(raw.action);
123
+ if (genericAction) {
124
+ if (raw.status !== 'not_collected' || raw.reason !== 'unsupported_action')
125
+ fail('MAP_UNAVAILABLE');
126
+ }
127
+ if (!Array.isArray(raw.candidates) || raw.candidates.length > 1)
128
+ fail('MAP_UNAVAILABLE');
129
+ if (raw.status === 'complete') {
130
+ if (!policyTime(raw.observedAt) ||
131
+ !POLICY_ACTIONS[raw.action] ||
132
+ raw.reason !== (raw.candidates.length ? null : 'no_candidate'))
133
+ fail('MAP_UNAVAILABLE');
134
+ }
135
+ else if ((raw.status !== 'unavailable' && raw.status !== 'not_collected') ||
136
+ !POLICY_REASONS[raw.status].includes(raw.reason) ||
137
+ raw.candidates.length)
138
+ fail('MAP_UNAVAILABLE');
139
+ for (const candidate of raw.candidates) {
140
+ if (!policyObject(candidate) ||
141
+ Object.keys(candidate).sort().join(',') !== 'conditionKeys,hasCondition,relationship,resourceScope' ||
142
+ candidate.relationship !== POLICY_ACTIONS[raw.action] ||
143
+ candidate.resourceScope !== 'bucket_summary' ||
144
+ typeof candidate.hasCondition !== 'boolean' ||
145
+ !Array.isArray(candidate.conditionKeys) ||
146
+ candidate.conditionKeys.length > 128)
147
+ fail('MAP_UNAVAILABLE');
148
+ for (const key of candidate.conditionKeys) {
149
+ work.step();
150
+ mapIdentifier(key, 'MAP_UNAVAILABLE');
151
+ if (Buffer.byteLength(key, 'utf8') > 256 || /[\u0000-\u001f]/.test(key))
152
+ fail('MAP_UNAVAILABLE');
153
+ }
154
+ if (candidate.hasCondition !== !!candidate.conditionKeys.length ||
155
+ JSON.stringify(candidate.conditionKeys) !==
156
+ JSON.stringify([...new Set(candidate.conditionKeys)].sort(compareCodePoints)))
157
+ fail('MAP_UNAVAILABLE');
158
+ }
159
+ const task = endpoint(raw.taskId, 'GuardTask'), deployment = endpoint(raw.deploymentId, 'GuardDeployment');
160
+ const execution = endpoint(raw.executionBindingId, 'GuardExecutionBinding'), resource = endpoint(raw.resourceBindingId, 'GuardResourceBinding');
161
+ const tool = string(execution, 'tool');
162
+ if (!tool ||
163
+ string(resource, 'tool') !== tool ||
164
+ !strings(resource, 'actions').includes(raw.action) ||
165
+ !link(task, deployment, 'G_ASSIGNED_TO') ||
166
+ !link(execution, deployment, 'G_EXECUTES_FOR'))
167
+ fail('MAP_UNAVAILABLE');
168
+ const tools = connected(projection, new Set([execution.id]), 'G_EXECUTES_TOOL', work).filter(node => node.label === 'GuardTool' && string(node, 'name') === tool);
169
+ const grants = connected(projection, new Set([task.id]), 'G_HAS_GRANT', work).filter(node => node.label === 'GuardGrant' && string(node, 'tool') === tool);
170
+ const selectors = connected(projection, new Set([resource.id]), 'G_BINDS_SELECTOR', work).filter(node => node.label === 'GuardResource' &&
171
+ string(node, 'tool') === tool &&
172
+ string(node, 'resource_id') === string(resource, 'resource_ref'));
173
+ if (!grants.some(grant => strings(grant, 'resources').includes(string(resource, 'resource_ref') ?? '') &&
174
+ (raw.status !== 'complete' || uniqueLink(task, grant, 'G_HAS_GRANT')) &&
175
+ tools.some(toolNode => (raw.status === 'complete' ? exactLink : link)(grant, toolNode, 'G_FOR_TOOL')) &&
176
+ selectors.some(selector => (raw.status === 'complete' ? uniqueLink : link)(grant, selector, 'G_TARGETS'))))
177
+ fail('MAP_UNAVAILABLE');
178
+ for (const [key, label, relation, owner] of [
179
+ ['identityId', 'GuardExternalIdentity', 'G_USES_IDENTITY', execution],
180
+ ['resourceId', 'GuardExternalResource', 'G_MAPS_TO', resource],
181
+ ]) {
182
+ if (raw[key] === null) {
183
+ if (raw.status === 'complete')
184
+ fail('MAP_UNAVAILABLE');
185
+ continue;
186
+ }
187
+ const target = endpoint(raw[key], label);
188
+ if (!(raw.status === 'complete' ? exactLink(owner, target, relation) : link(owner, target, relation)))
189
+ fail('MAP_UNAVAILABLE');
190
+ }
191
+ if (genericAction &&
192
+ (raw.resourceId === null || string(endpoint(raw.resourceId, 'GuardExternalResource'), 'provider') !== 'host'))
193
+ fail('MAP_UNAVAILABLE');
194
+ if (raw.status === 'complete') {
195
+ const agents = connected(projection, new Set([deployment.id]), 'G_INSTANCE_OF', work).filter(node => node.label === 'GuardAgent');
196
+ if (agents.length !== 1 ||
197
+ !exactLink(deployment, agents[0], 'G_INSTANCE_OF') ||
198
+ string(execution, 'identity_selection') !== 'static' ||
199
+ !exactLink(task, deployment, 'G_ASSIGNED_TO') ||
200
+ !exactLink(execution, deployment, 'G_EXECUTES_FOR') ||
201
+ tools.length !== 1 ||
202
+ !exactLink(execution, tools[0], 'G_EXECUTES_TOOL') ||
203
+ selectors.length !== 1 ||
204
+ !exactLink(resource, selectors[0], 'G_BINDS_SELECTOR'))
205
+ fail('MAP_UNAVAILABLE');
206
+ }
207
+ const identity = JSON.stringify([
208
+ raw.taskId,
209
+ raw.deploymentId,
210
+ raw.executionBindingId,
211
+ raw.resourceBindingId,
212
+ raw.action,
213
+ ]);
214
+ if (seen.has(identity))
215
+ fail('MAP_UNAVAILABLE');
216
+ seen.add(identity);
217
+ try {
218
+ const copy = policyList([immutableJson(raw, 6, 1024, 64 * 1024)])[0];
219
+ materializedBytes += Buffer.byteLength(JSON.stringify(copy));
220
+ if (materializedBytes > 4 * 1024 * 1024)
221
+ fail('MAP_LIMIT_EXCEEDED');
222
+ result.push(copy);
223
+ }
224
+ catch {
225
+ fail('MAP_LIMIT_EXCEEDED');
226
+ }
227
+ }
228
+ return result;
229
+ }
230
+ export function policyCoverage(records) {
231
+ const complete = records.filter(item => item.status === 'complete').length;
232
+ const observed = records.flatMap(item => (item.observedAt === null ? [] : [item.observedAt])).sort();
233
+ return {
234
+ status: !records.length || records.every(item => item.status === 'not_collected')
235
+ ? 'not_collected'
236
+ : complete === records.length
237
+ ? 'complete'
238
+ : complete
239
+ ? 'partial'
240
+ : 'unavailable',
241
+ observedAt: observed[0] ?? null,
242
+ assessment: 'unknown',
243
+ };
244
+ }
245
+ /**
246
+ * Coverage for a selection of one projection's policy evidence. When the store found more
247
+ * task/binding/action pairs than one read evaluates it loaded none: that is unknown, never
248
+ * "not collected".
249
+ */
250
+ export function policyCoverageOf(projection, records) {
251
+ return projection.policyEvidenceNotLoaded === true
252
+ ? { status: 'unknown', reason: 'limit_exceeded', observedAt: null, assessment: 'unknown' }
253
+ : policyCoverage(records);
254
+ }
255
+ export function selectedPolicy(projection, work, options = {}) {
256
+ const ids = options.nodes ? new Set(options.nodes.map(node => node.entityId)) : undefined;
257
+ const executions = options.executions ? new Set(options.executions.map(node => node.entityId)) : undefined;
258
+ const resources = options.resources ? new Set(options.resources.map(node => node.entityId)) : undefined;
259
+ const records = options.task
260
+ ? policyRecordsOfTask(projection, options.task.entityId, work)
261
+ : policyRecords(projection, work);
262
+ return records.filter(record => {
263
+ work.step();
264
+ return ((!ids ||
265
+ [
266
+ record.taskId,
267
+ record.deploymentId,
268
+ record.executionBindingId,
269
+ record.resourceBindingId,
270
+ record.identityId,
271
+ record.resourceId,
272
+ ].every(id => id === null || ids.has(id))) &&
273
+ (!options.task || record.taskId === options.task.entityId) &&
274
+ (!executions || executions.has(record.executionBindingId)) &&
275
+ (!resources || resources.has(record.resourceBindingId)) &&
276
+ (!options.action || record.action === options.action));
277
+ });
278
+ }
279
+ /** The projection's policy evidence, validated once per query. */
280
+ export function policyRecords(projection, work) {
281
+ return work.cached(projection, 'policy', () => validatedPolicyEvidence(projection, work));
282
+ }
283
+ /** Validated policy records of one task, in their original order; indexed once per projection. */
284
+ function policyRecordsOfTask(projection, taskEntityId, work) {
285
+ const index = work.cached(projection, 'policyByTask', () => {
286
+ const records = policyRecords(projection, work);
287
+ work.step(records.length);
288
+ const byTask = new Map();
289
+ for (const record of records) {
290
+ const items = byTask.get(record.taskId);
291
+ if (items)
292
+ items.push(record);
293
+ else
294
+ byTask.set(record.taskId, [record]);
295
+ }
296
+ return byTask;
297
+ });
298
+ return index.get(taskEntityId) ?? [];
299
+ }
@@ -0,0 +1,16 @@
1
+ import { type JsonObject, type JsonValue } from '../types.js';
2
+ export declare const MAP_OPERATIONS: readonly ['list_agents', 'get_agent_access', 'explain_access', 'find_agents_with_access', 'get_changes', 'get_inventory_health', 'list_owners', 'list_resources', 'get_resource_access', 'get_agent_inventory', 'list_undeclared_activity', 'list_unclaimed_identities'];
3
+ export type MapOperation = (typeof MAP_OPERATIONS)[number];
4
+ export type QueryArguments = Readonly<Record<string, JsonValue>>;
5
+ /** What each operation answers, in one sentence. Shown to MCP clients as the tool description. */
6
+ export declare const MAP_OPERATION_DESCRIPTIONS: Readonly<Record<MapOperation, string>>;
7
+ export declare const sections: readonly ['overview', 'owners', 'deployments', 'tasks', 'permissions', 'resources', 'connections', 'credentials', 'relationships', 'tools', 'bindings', 'identities', 'policies'];
8
+ export type InventorySection = (typeof sections)[number];
9
+ export declare const MAP_INPUT_SCHEMAS: Readonly<Record<MapOperation, JsonObject>>;
10
+ /** The arguments of one operation, copied and checked against its closed schema. */
11
+ export declare function validateQueryArguments(operation: unknown, value: unknown): QueryArguments;
12
+ /** The page an operation was asked for; 50 rows from the start unless the caller says otherwise. */
13
+ export declare function pageArguments(args: QueryArguments): {
14
+ offset: number;
15
+ limit: number;
16
+ };