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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +652 -272
  2. package/dist/assets.js +16 -26
  3. package/dist/boundary-flags.d.ts +73 -0
  4. package/dist/boundary-flags.js +352 -0
  5. package/dist/boundary.d.ts +18 -3
  6. package/dist/boundary.js +71 -21
  7. package/dist/cli-entry.d.ts +11 -0
  8. package/dist/cli-entry.js +48 -0
  9. package/dist/cli.js +40 -43
  10. package/dist/common.d.ts +34 -0
  11. package/dist/common.js +94 -0
  12. package/dist/http.js +78 -51
  13. package/dist/identity-cli.js +93 -46
  14. package/dist/identity-http.js +226 -94
  15. package/dist/index.d.ts +4 -4
  16. package/dist/index.js +4 -4
  17. package/dist/mcp.js +40 -5
  18. package/dist/oidc.d.ts +5 -0
  19. package/dist/oidc.js +121 -38
  20. package/dist/policy-reader.d.ts +56 -3
  21. package/dist/policy-reader.js +823 -110
  22. package/dist/projection-fields.d.ts +4 -0
  23. package/dist/projection-fields.js +144 -0
  24. package/dist/service/changes.d.ts +5 -0
  25. package/dist/service/changes.js +255 -0
  26. package/dist/service/context.d.ts +33 -0
  27. package/dist/service/context.js +2 -0
  28. package/dist/service/discovery.d.ts +6 -0
  29. package/dist/service/discovery.js +31 -0
  30. package/dist/service/explain.d.ts +12 -0
  31. package/dist/service/explain.js +126 -0
  32. package/dist/service/flags.d.ts +20 -0
  33. package/dist/service/flags.js +70 -0
  34. package/dist/service/inventory.d.ts +4 -0
  35. package/dist/service/inventory.js +62 -0
  36. package/dist/service/json.d.ts +8 -0
  37. package/dist/service/json.js +7 -0
  38. package/dist/service/operations/discovery.d.ts +5 -0
  39. package/dist/service/operations/discovery.js +90 -0
  40. package/dist/service/operations/explain-access.d.ts +3 -0
  41. package/dist/service/operations/explain-access.js +25 -0
  42. package/dist/service/operations/find-agents-with-access.d.ts +3 -0
  43. package/dist/service/operations/find-agents-with-access.js +120 -0
  44. package/dist/service/operations/get-agent-access.d.ts +3 -0
  45. package/dist/service/operations/get-agent-access.js +75 -0
  46. package/dist/service/operations/get-agent-inventory.d.ts +3 -0
  47. package/dist/service/operations/get-agent-inventory.js +42 -0
  48. package/dist/service/operations/get-changes.d.ts +3 -0
  49. package/dist/service/operations/get-changes.js +11 -0
  50. package/dist/service/operations/get-inventory-health.d.ts +3 -0
  51. package/dist/service/operations/get-inventory-health.js +81 -0
  52. package/dist/service/operations/get-resource-access.d.ts +3 -0
  53. package/dist/service/operations/get-resource-access.js +48 -0
  54. package/dist/service/operations/list-agents.d.ts +3 -0
  55. package/dist/service/operations/list-agents.js +92 -0
  56. package/dist/service/operations/list-owners-resources.d.ts +5 -0
  57. package/dist/service/operations/list-owners-resources.js +69 -0
  58. package/dist/service/ordering.d.ts +9 -0
  59. package/dist/service/ordering.js +47 -0
  60. package/dist/service/outside-guard.d.ts +68 -0
  61. package/dist/service/outside-guard.js +313 -0
  62. package/dist/service/policy.d.ts +26 -0
  63. package/dist/service/policy.js +299 -0
  64. package/dist/service/schemas.d.ts +16 -0
  65. package/dist/service/schemas.js +192 -0
  66. package/dist/service/summaries.d.ts +32 -0
  67. package/dist/service/summaries.js +247 -0
  68. package/dist/service/work.d.ts +33 -0
  69. package/dist/service/work.js +116 -0
  70. package/dist/service.d.ts +5 -5
  71. package/dist/service.js +110 -645
  72. package/dist/store/ancestors.d.ts +9 -0
  73. package/dist/store/ancestors.js +86 -0
  74. package/dist/store/discovery.d.ts +8 -0
  75. package/dist/store/discovery.js +288 -0
  76. package/dist/store/evidence.d.ts +12 -0
  77. package/dist/store/evidence.js +90 -0
  78. package/dist/store/identity-scope.d.ts +9 -0
  79. package/dist/store/identity-scope.js +71 -0
  80. package/dist/store/nodes.d.ts +24 -0
  81. package/dist/store/nodes.js +237 -0
  82. package/dist/store/projection.d.ts +19 -0
  83. package/dist/store/projection.js +242 -0
  84. package/dist/store/queries.d.ts +48 -0
  85. package/dist/store/queries.js +302 -0
  86. package/dist/store/read.d.ts +81 -0
  87. package/dist/store/read.js +135 -0
  88. package/dist/store/selection.d.ts +3 -0
  89. package/dist/store/selection.js +102 -0
  90. package/dist/store.d.ts +15 -0
  91. package/dist/store.js +102 -395
  92. package/dist/transport.d.ts +42 -2
  93. package/dist/transport.js +144 -16
  94. package/dist/types.d.ts +184 -4
  95. package/dist/types.js +52 -11
  96. package/package.json +16 -3
package/dist/transport.js CHANGED
@@ -1,16 +1,50 @@
1
1
  import { MapQueryError } from './types.js';
2
- import { fail } from './boundary.js';
2
+ import { fail, integer } from './common.js';
3
+ import { fields, parseRequest } from './boundary.js';
4
+ // Request plumbing shared by the reference-credential server (http.ts) and the company
5
+ // sign-in server (identity-http.ts): the same headers, limits, error bodies and refusals.
3
6
  const CODES = new Map([
4
- ['MAP_UNAUTHENTICATED', 401], ['MAP_REQUEST_DENIED', 403], ['MAP_NOT_FOUND', 404],
5
- ['MAP_REQUEST_INVALID', 400], ['MAP_ARGUMENT_INVALID', 400], ['MAP_OPERATION_INVALID', 400],
6
- ['MAP_SCOPE_INVALID', 403], ['MAP_LIMIT_EXCEEDED', 413], ['MAP_REQUEST_TOO_LARGE', 413],
7
- ['MAP_RESPONSE_TOO_LARGE', 413], ['MAP_TIMEOUT', 408], ['MAP_BUSY', 503],
8
- ['MAP_RATE_LIMITED', 429], ['MAP_UNAVAILABLE', 503],
9
- ['MAP_SESSION_REQUIRED', 401], ['MAP_IDENTITY_INVALID', 401], ['MAP_IDENTITY_UNAVAILABLE', 503],
7
+ ['MAP_UNAUTHENTICATED', 401],
8
+ ['MAP_REQUEST_DENIED', 403],
9
+ ['MAP_NOT_FOUND', 404],
10
+ ['MAP_REQUEST_INVALID', 400],
11
+ ['MAP_SCOPE_INVALID', 403],
12
+ ['MAP_LIMIT_EXCEEDED', 413],
13
+ ['MAP_REQUEST_TOO_LARGE', 413],
14
+ ['MAP_RESPONSE_TOO_LARGE', 413],
15
+ // 408 while the request is still arriving; 504 once it has (see `timeoutStatus`).
16
+ ['MAP_TIMEOUT', 408],
17
+ ['MAP_BUSY', 503],
18
+ ['MAP_RATE_LIMITED', 429],
19
+ ['MAP_UNAVAILABLE', 503],
20
+ ['MAP_SESSION_REQUIRED', 401],
21
+ ['MAP_IDENTITY_INVALID', 401],
22
+ ['MAP_IDENTITY_UNAVAILABLE', 503],
10
23
  ]);
11
24
  export function safeError(error) {
12
25
  return error instanceof MapQueryError && CODES.has(error.code) ? error.code : 'MAP_UNAVAILABLE';
13
26
  }
27
+ /** A refusal HTTP names more exactly than the code's usual status (an unmet `Expect` is 417). */
28
+ class StatusError extends MapQueryError {
29
+ status;
30
+ constructor(code, status) {
31
+ super(code);
32
+ this.status = status;
33
+ }
34
+ }
35
+ /**
36
+ * The status of a timeout. 408 says the client was too slow to send its request; once the whole
37
+ * request has been received, the time was spent by the service or its database, which is 504.
38
+ */
39
+ export const timeoutStatus = (received) => (received ? 504 : undefined);
40
+ /**
41
+ * Answers a failed request with its safe code; nothing from the error itself is sent.
42
+ * `received`: the request body had been read in full when the failure happened.
43
+ */
44
+ export function respondError(response, error, received = false) {
45
+ const code = safeError(error);
46
+ errorResponse(response, code, error instanceof StatusError ? error.status : code === 'MAP_TIMEOUT' ? timeoutStatus(received) : undefined);
47
+ }
14
48
  export function headers(response, size, mime = 'application/json') {
15
49
  response.setHeader('Content-Type', mime);
16
50
  response.setHeader('Content-Length', size);
@@ -20,7 +54,8 @@ export function headers(response, size, mime = 'application/json') {
20
54
  response.setHeader('Connection', 'close');
21
55
  response.setHeader('Content-Security-Policy', "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; frame-ancestors 'none'; form-action 'none'");
22
56
  }
23
- export function errorResponse(response, code) {
57
+ /** `status` overrides the code's usual status, for a refusal HTTP names more exactly (417). */
58
+ export function errorResponse(response, code, status) {
24
59
  if (response.writableEnded || response.destroyed)
25
60
  return;
26
61
  if (response.headersSent) {
@@ -31,7 +66,7 @@ export function errorResponse(response, code) {
31
66
  headers(response, body.length);
32
67
  if (code === 'MAP_UNAUTHENTICATED')
33
68
  response.setHeader('WWW-Authenticate', 'Bearer realm="agent-guard-reference"');
34
- response.writeHead(CODES.get(code) ?? 503);
69
+ response.writeHead(status ?? CODES.get(code) ?? 503);
35
70
  response.end(body);
36
71
  }
37
72
  export const count = (request, name) => request.rawHeaders.filter((entry, i) => i % 2 === 0 && entry.toLowerCase() === name).length;
@@ -48,13 +83,30 @@ export function readBody(request, max, signal) {
48
83
  return new Promise((resolve, reject) => {
49
84
  let size = 0;
50
85
  const chunks = [];
51
- const cleanup = () => { request.off('data', data); request.off('end', end); request.off('error', aborted); request.off('aborted', aborted); signal.removeEventListener('abort', timeout); };
52
- const stop = (code) => { cleanup(); request.pause(); reject(new MapQueryError(code)); };
53
- const data = (chunk) => { size += chunk.length; if (size > max) {
54
- stop('MAP_REQUEST_TOO_LARGE');
55
- return;
56
- } chunks.push(chunk); };
57
- const end = () => { cleanup(); resolve(Buffer.concat(chunks, size)); };
86
+ const cleanup = () => {
87
+ request.off('data', data);
88
+ request.off('end', end);
89
+ request.off('error', aborted);
90
+ request.off('aborted', aborted);
91
+ signal.removeEventListener('abort', timeout);
92
+ };
93
+ const stop = (code) => {
94
+ cleanup();
95
+ request.pause();
96
+ reject(new MapQueryError(code));
97
+ };
98
+ const data = (chunk) => {
99
+ size += chunk.length;
100
+ if (size > max) {
101
+ stop('MAP_REQUEST_TOO_LARGE');
102
+ return;
103
+ }
104
+ chunks.push(chunk);
105
+ };
106
+ const end = () => {
107
+ cleanup();
108
+ resolve(Buffer.concat(chunks, size));
109
+ };
58
110
  const aborted = () => stop('MAP_REQUEST_INVALID');
59
111
  const timeout = () => stop('MAP_TIMEOUT');
60
112
  if (signal.aborted) {
@@ -68,3 +120,79 @@ export function readBody(request, max, signal) {
68
120
  signal.addEventListener('abort', timeout, { once: true });
69
121
  });
70
122
  }
123
+ /** Per-credential or per-session rate limit: `maxRequests` in each `windowMs`. */
124
+ export function rateLimit(value, defaultMaxRequests) {
125
+ const rate = value === undefined ? {} : fields(value, ['maxRequests', 'windowMs'], ['maxRequests', 'windowMs']);
126
+ return {
127
+ maxRequests: integer(rate.maxRequests, defaultMaxRequests, 1, 10_000),
128
+ windowMs: integer(rate.windowMs, 60_000, 10, 60_000),
129
+ };
130
+ }
131
+ /**
132
+ * The JSON body of an admitted POST. Call it only after the credential (or session and CSRF)
133
+ * and rate checks have passed: it is what tells a client that sent `Expect: 100-continue` to go
134
+ * on, and a refusal before it has already answered with its own status.
135
+ */
136
+ export async function readJsonBody(request, response, limit, signal) {
137
+ if (count(request, 'content-type') !== 1 ||
138
+ !/^application\/json(?:\s*;\s*charset=utf-8)?$/i.test(request.headers['content-type'] ?? '') ||
139
+ count(request, 'content-length') > 1 ||
140
+ count(request, 'transfer-encoding') > 1 ||
141
+ request.headers['content-encoding'] !== undefined)
142
+ fail('MAP_REQUEST_INVALID');
143
+ const length = request.headers['content-length'];
144
+ if (length !== undefined && (!/^\d+$/.test(length) || !Number.isSafeInteger(Number(length))))
145
+ fail('MAP_REQUEST_INVALID');
146
+ if (length !== undefined && Number(length) > limit)
147
+ fail('MAP_REQUEST_TOO_LARGE');
148
+ const expectation = request.headers.expect;
149
+ if (expectation !== undefined) {
150
+ // Only `100-continue` can be met; anything else, or anything added to it, is 417.
151
+ if (expectation.trim().toLowerCase() !== '100-continue')
152
+ throw new StatusError('MAP_REQUEST_INVALID', 417);
153
+ // Interim responses exist only in HTTP/1.1. An HTTP/1.0 client that sends the expectation
154
+ // is not waiting for one (RFC 9110, 10.1.1): its body is read without an invitation.
155
+ if (request.httpVersionMajor === 1 && request.httpVersionMinor === 1)
156
+ response.writeContinue();
157
+ }
158
+ return parseRequest(await readBody(request, limit, signal), limit);
159
+ }
160
+ /** The `{ operation, args }` envelope of `POST /v1/query`. */
161
+ export function queryEnvelope(parsed) {
162
+ try {
163
+ const envelope = fields(parsed, ['operation', 'args'], ['operation', 'args']);
164
+ return { operation: envelope.operation, args: envelope.args };
165
+ }
166
+ catch {
167
+ return fail('MAP_REQUEST_INVALID');
168
+ }
169
+ }
170
+ /** A body that must be the empty object, as the sign-in and sign-out routes require. */
171
+ export function emptyObject(parsed) {
172
+ try {
173
+ fields(parsed, [], []);
174
+ }
175
+ catch {
176
+ fail('MAP_REQUEST_INVALID');
177
+ }
178
+ }
179
+ /** Sends one fixed viewer asset. */
180
+ export function sendAsset(response, asset) {
181
+ headers(response, asset.body.length, asset.mime);
182
+ response.writeHead(200);
183
+ response.end(asset.body);
184
+ }
185
+ /**
186
+ * What neither server speaks: a request that arrives with `Expect: 100-continue` still goes
187
+ * through the normal handler, any other expectation is 417, and tunnels, upgrades and
188
+ * malformed requests are dropped.
189
+ */
190
+ export function refuseOtherTraffic(server, handle) {
191
+ server.on('checkContinue', (request, response) => {
192
+ void handle(request, response);
193
+ });
194
+ server.on('checkExpectation', (_request, response) => errorResponse(response, 'MAP_REQUEST_INVALID', 417));
195
+ server.on('connect', (_request, socket) => socket.destroy());
196
+ server.on('upgrade', (_request, socket) => socket.destroy());
197
+ server.on('clientError', (_error, socket) => socket.destroy());
198
+ }
package/dist/types.d.ts CHANGED
@@ -15,10 +15,10 @@ export interface ReadScope {
15
15
  readonly teamIds: readonly string[];
16
16
  readonly allTeams: boolean;
17
17
  }
18
- export declare const MAP_LABELS: readonly ['GuardAgent', 'GuardDeployment', 'GuardTask', 'GuardPrincipal', 'GuardGrant', 'GuardTool', 'GuardResource', 'GuardResourceBinding', 'GuardExecutionBinding', 'GuardExternalResource', 'GuardExternalIdentity', 'GuardMcpConnection', 'GuardMcpOperation'];
19
- export type GuardLabel = typeof MAP_LABELS[number];
20
- export declare const MAP_RELATIONSHIPS: readonly ['G_INSTANCE_OF', 'G_ASSIGNED_TO', 'G_ACTS_AS', 'G_DELEGATED_FROM', 'G_DERIVED_FROM', 'G_HAS_GRANT', 'G_FOR_TOOL', 'G_TARGETS', 'G_BINDS_SELECTOR', 'G_MAPS_TO', 'G_EXECUTES_FOR', 'G_EXECUTES_TOOL', 'G_USES_IDENTITY', 'G_VIA_CONNECTION', 'G_EXPOSES_OPERATION', 'G_OPERATION_TOOL'];
21
- export type GuardRelation = typeof MAP_RELATIONSHIPS[number];
18
+ export declare const MAP_LABELS: readonly ['GuardAgent', 'GuardDeployment', 'GuardTask', 'GuardPrincipal', 'GuardGrant', 'GuardTool', 'GuardResource', 'GuardResourceBinding', 'GuardExecutionBinding', 'GuardExternalResource', 'GuardExternalIdentity', 'GuardMcpConnection', 'GuardMcpOperation', 'GuardOwner', 'GuardCredentialReference', 'GuardDirectAccess'];
19
+ export type GuardLabel = (typeof MAP_LABELS)[number];
20
+ export declare const MAP_RELATIONSHIPS: readonly ['G_INSTANCE_OF', 'G_ASSIGNED_TO', 'G_ACTS_AS', 'G_DELEGATED_FROM', 'G_DERIVED_FROM', 'G_HAS_GRANT', 'G_FOR_TOOL', 'G_TARGETS', 'G_BINDS_SELECTOR', 'G_MAPS_TO', 'G_EXECUTES_FOR', 'G_EXECUTES_TOOL', 'G_USES_IDENTITY', 'G_VIA_CONNECTION', 'G_EXPOSES_OPERATION', 'G_OPERATION_TOOL', 'G_OWNED_BY', 'G_RESOURCE_OWNED_BY', 'G_USES_CREDENTIAL', 'G_DEPENDS_ON', 'G_DIRECT_ACCESS_FOR'];
21
+ export type GuardRelation = (typeof MAP_RELATIONSHIPS)[number];
22
22
  export interface MapNode {
23
23
  readonly id: string;
24
24
  readonly entityId: string;
@@ -58,6 +58,36 @@ export interface MapPolicyEvidence {
58
58
  readonly assessment: 'unknown';
59
59
  readonly candidates: readonly MapPolicyCandidate[];
60
60
  }
61
+ /** A bucket a tool's static identity has an identity-policy candidate on, outside that tool's bindings. */
62
+ export interface MapIdentityExtraResource {
63
+ readonly resourceId: string;
64
+ readonly bucketName: string;
65
+ readonly relationship: MapPolicyCandidate['relationship'];
66
+ readonly hasCondition: boolean;
67
+ readonly conditionKeys: readonly string[];
68
+ }
69
+ /** C2: what a tool's execution identity reaches compared with what that tool's grants bind. */
70
+ export interface MapIdentityScope {
71
+ readonly executionBindingId: string;
72
+ readonly deploymentId: string | null;
73
+ readonly tool: string | null;
74
+ readonly identityId: string | null;
75
+ /** The identity's AWS account; candidates are collected per account, so only its buckets are checked. */
76
+ readonly accountId: string | null;
77
+ readonly status: 'broader_than_grants' | 'matches_grants' | 'unknown';
78
+ readonly reason: Exclude<MapPolicyEvidence['reason'], 'no_candidate' | 'unsupported_action' | 'unsupported_binding'> | 'unsupported_identity' | 'not_evaluated' | 'wildcard_grant' | 'binding_narrower_than_candidate' | 'grant_lineage_unknown' | 'truncated';
79
+ readonly observedAt: string | null;
80
+ readonly boundBuckets: readonly string[];
81
+ readonly extraResources: readonly MapIdentityExtraResource[];
82
+ readonly truncated: boolean;
83
+ /**
84
+ * `evidence_outside_visible_scope`: some extra buckets have no binding in your scope (another
85
+ * team's, or none). `bucket_list_shortened`: the list is cut; the status still stands.
86
+ */
87
+ readonly notes?: readonly string[];
88
+ readonly evidenceKind: 'iam_identity_policy_candidate';
89
+ readonly assessment: 'unknown';
90
+ }
61
91
  export interface MapProjection {
62
92
  readonly id: string;
63
93
  readonly snapshotId: string;
@@ -68,6 +98,105 @@ export interface MapProjection {
68
98
  readonly coverage: MapCoverage;
69
99
  /** Current, independently checked policy evidence; omitted for summaries/history. */
70
100
  readonly policyEvidence?: readonly MapPolicyEvidence[];
101
+ /** Current identity-scope comparison per static execution binding; only for one-agent reads. */
102
+ readonly identityScope?: readonly MapIdentityScope[];
103
+ /**
104
+ * The same comparison limited to buckets that visible bindings map to; only for
105
+ * `outsideGuard` reads. `matches_grants` here covers only those buckets.
106
+ */
107
+ readonly identityReach?: readonly MapIdentityScope[];
108
+ /**
109
+ * Outside-guard reads for a team-scoped reader: false when the projection has no
110
+ * deployment or task outside the reader's read, so `none_found` can stand. Never a count.
111
+ */
112
+ readonly outsideVisibleScope?: boolean;
113
+ /** A boundary read had no room left for delegation ancestors and loaded none. */
114
+ readonly ancestorsNotLoaded?: boolean;
115
+ /** One-agent boundary reads for a team reader: host resource ids in its scope. */
116
+ readonly inScopeTargets?: readonly string[];
117
+ /**
118
+ * Boundary reads: for each tool the agent holds a `*` grant on, up to five in-scope bindings
119
+ * of that tool marked confidential or restricted. Read in place of loading every binding.
120
+ */
121
+ readonly wildcardSensitivity?: Readonly<Record<string, readonly {
122
+ readonly id: string;
123
+ readonly sensitivity: string;
124
+ }[]>>;
125
+ /** `undeclared` reads only: activity in this projection that no declared agent accounts for. */
126
+ readonly undeclared?: MapUndeclaredActivity;
127
+ /**
128
+ * A `policy.optional` read found more task/binding/action pairs than one read evaluates and
129
+ * loaded no policy evidence. Its coverage is unknown, never `not_collected`.
130
+ */
131
+ readonly policyEvidenceNotLoaded?: boolean;
132
+ }
133
+ /** A task seen through an enrolled executor that no manifest task binding assigns to a declared agent. */
134
+ export interface MapUndeclaredTask {
135
+ readonly id: string;
136
+ readonly taskId: string;
137
+ readonly principalId: string | null;
138
+ readonly delegated: boolean | null;
139
+ readonly derived: boolean | null;
140
+ readonly revoked: boolean | null;
141
+ /** Its direct parent or source task is assigned to a declared agent: likely that agent's sub-task. */
142
+ readonly parentDeclared: boolean;
143
+ /** Tools the task holds a declared grant on, at most 32. */
144
+ readonly tools: readonly string[];
145
+ readonly toolsTruncated: boolean;
146
+ }
147
+ /** A caller principal none of whose tasks is assigned to a declared agent. */
148
+ export interface MapUndeclaredPrincipal {
149
+ readonly id: string;
150
+ readonly principalId: string;
151
+ /** Its tasks in this projection, counted up to 1,000. */
152
+ readonly tasks: number;
153
+ readonly tasksCapped: boolean;
154
+ }
155
+ /** Only for `undeclared` reads (all-teams readers). Counting stops at 10,000: `*Capped` means "at least". */
156
+ export interface MapUndeclaredActivity {
157
+ readonly tasks: number;
158
+ readonly tasksCapped: boolean;
159
+ readonly principals: number;
160
+ readonly principalsCapped: boolean;
161
+ readonly page?: {
162
+ readonly kind: 'tasks';
163
+ readonly items: readonly MapUndeclaredTask[];
164
+ } | {
165
+ readonly kind: 'principals';
166
+ readonly items: readonly MapUndeclaredPrincipal[];
167
+ };
168
+ }
169
+ /** One enrolled AWS account's collection, judged for an `unclaimedIdentities` read. */
170
+ export interface MapCloudAccount {
171
+ readonly tenantId: string;
172
+ readonly partition: string;
173
+ readonly accountId: string;
174
+ readonly status: 'complete' | 'unknown';
175
+ readonly reason: 'collection_invalid' | 'stale' | 'changed' | 'limit_exceeded' | 'claims_limit_exceeded' | null;
176
+ readonly observedAt: string | null;
177
+ /** IAM roles and users in the collection, and how many of them no declared binding names; null when unknown. */
178
+ readonly observed: number | null;
179
+ readonly unclaimed: number | null;
180
+ }
181
+ /** An IAM role or user in a complete, fresh collection whose ARN no declared execution binding names. */
182
+ export interface MapUnclaimedIdentity {
183
+ readonly arn: string;
184
+ readonly kind: 'role' | 'user';
185
+ readonly name: string;
186
+ readonly path: string;
187
+ readonly tenantId: string;
188
+ readonly partition: string;
189
+ readonly accountId: string;
190
+ }
191
+ /** Only for `unclaimedIdentities` reads (all-teams readers). */
192
+ export interface MapUnclaimedIdentities {
193
+ readonly accounts: readonly MapCloudAccount[];
194
+ /** One page over the complete accounts, ordered by ARN. */
195
+ readonly identities: readonly MapUnclaimedIdentity[];
196
+ /** Unclaimed identities across the complete accounts. */
197
+ readonly total: number;
198
+ /** Some declared execution binding names no identity, so a listed identity may be its own. */
199
+ readonly bindingsWithoutIdentity: boolean;
71
200
  }
72
201
  export interface MapSource {
73
202
  readonly id: string;
@@ -79,13 +208,64 @@ export interface MapSource {
79
208
  }
80
209
  export interface MapView {
81
210
  readonly sources: readonly MapSource[];
211
+ /** `unclaimedIdentities` reads only. */
212
+ readonly unclaimedIdentities?: MapUnclaimedIdentities;
213
+ }
214
+ /** Narrows the policy evidence of a read to the pairs its caller will show. */
215
+ export interface MapPolicySelection {
216
+ /** Only pairs of this task (native or entity id). */
217
+ readonly taskId?: string;
218
+ /** Only pairs of this tool. */
219
+ readonly tool?: string;
220
+ /** Only pairs of bindings with this exact resource reference or mapped ARN. */
221
+ readonly resourceRef?: string;
222
+ /** Only this action. */
223
+ readonly action?: string;
224
+ /** Past the pair limit, answer with `policyEvidenceNotLoaded` instead of failing the read. */
225
+ readonly optional?: boolean;
82
226
  }
83
227
  export interface MapReadRequest {
84
228
  readonly sourceId?: string;
85
229
  readonly agentId?: string;
230
+ /** With agentId: also load boundary evidence (get_agent_access). */
231
+ readonly boundary?: boolean;
232
+ /** A flags read for a page of agents (1-100 native or entity ids), with one-agent rules. */
233
+ readonly agentIds?: readonly string[];
86
234
  readonly history?: boolean;
87
235
  /** Agent/deployment index only; avoids loading task neighborhoods before selecting an agent. */
88
236
  readonly summary?: boolean;
237
+ /** Full graph for boundary flags, without policy or identity evidence reads. */
238
+ readonly flags?: boolean;
239
+ /** Full graph plus identity reach limited to the buckets visible bindings map to. */
240
+ readonly outsideGuard?: boolean;
241
+ /**
242
+ * Owner index only (list_owners): agents, resource bindings and owners, plus, for a team
243
+ * reader, the nodes that decide which bindings are visible. No policy evidence.
244
+ */
245
+ readonly owners?: boolean;
246
+ /**
247
+ * Policy evidence of the current projection. Omitted: every visible task/binding/action pair,
248
+ * at most 512 per read (more fails the read). `false`: none, so the read is not bound by
249
+ * that limit. An object narrows the pairs. Summary, history, flags, owner and discovery
250
+ * reads never load policy evidence.
251
+ */
252
+ readonly policy?: false | MapPolicySelection;
253
+ /**
254
+ * All-teams readers only. Per source, counts of tasks and caller principals that no declared
255
+ * agent accounts for; no nodes are loaded. With `sourceId` and `page`, also one page of them.
256
+ */
257
+ readonly undeclared?: boolean;
258
+ /**
259
+ * All-teams readers only, never with `sourceId`. IAM roles and users of the enrolled AWS
260
+ * collections that no declared execution binding names, one `page` at a time.
261
+ */
262
+ readonly unclaimedIdentities?: boolean;
263
+ /** `kind` is required with `undeclared` and refused with `unclaimedIdentities`. */
264
+ readonly page?: {
265
+ readonly kind?: 'tasks' | 'principals';
266
+ readonly offset: number;
267
+ readonly limit: number;
268
+ };
89
269
  }
90
270
  export interface MapStore {
91
271
  read(scope: ReadScope, request?: MapReadRequest): Promise<MapView>;
package/dist/types.js CHANGED
@@ -1,4 +1,4 @@
1
- import { immutableJson } from '@shieldaai/agent-guard';
1
+ import { immutableJson } from '@shieldaai/agent-guard/internal';
2
2
  export class MapQueryError extends Error {
3
3
  code;
4
4
  constructor(code) {
@@ -7,15 +7,52 @@ export class MapQueryError extends Error {
7
7
  this.name = 'MapQueryError';
8
8
  }
9
9
  }
10
- export const MAP_LABELS = ['GuardAgent', 'GuardDeployment', 'GuardTask', 'GuardPrincipal', 'GuardGrant',
11
- 'GuardTool', 'GuardResource', 'GuardResourceBinding', 'GuardExecutionBinding', 'GuardExternalResource',
12
- 'GuardExternalIdentity', 'GuardMcpConnection', 'GuardMcpOperation'];
13
- export const MAP_RELATIONSHIPS = ['G_INSTANCE_OF', 'G_ASSIGNED_TO', 'G_ACTS_AS', 'G_DELEGATED_FROM',
14
- 'G_DERIVED_FROM', 'G_HAS_GRANT', 'G_FOR_TOOL', 'G_TARGETS', 'G_BINDS_SELECTOR', 'G_MAPS_TO',
15
- 'G_EXECUTES_FOR', 'G_EXECUTES_TOOL', 'G_USES_IDENTITY', 'G_VIA_CONNECTION', 'G_EXPOSES_OPERATION',
16
- 'G_OPERATION_TOOL'];
10
+ export const MAP_LABELS = [
11
+ 'GuardAgent',
12
+ 'GuardDeployment',
13
+ 'GuardTask',
14
+ 'GuardPrincipal',
15
+ 'GuardGrant',
16
+ 'GuardTool',
17
+ 'GuardResource',
18
+ 'GuardResourceBinding',
19
+ 'GuardExecutionBinding',
20
+ 'GuardExternalResource',
21
+ 'GuardExternalIdentity',
22
+ 'GuardMcpConnection',
23
+ 'GuardMcpOperation',
24
+ 'GuardOwner',
25
+ 'GuardCredentialReference',
26
+ 'GuardDirectAccess',
27
+ ];
28
+ export const MAP_RELATIONSHIPS = [
29
+ 'G_INSTANCE_OF',
30
+ 'G_ASSIGNED_TO',
31
+ 'G_ACTS_AS',
32
+ 'G_DELEGATED_FROM',
33
+ 'G_DERIVED_FROM',
34
+ 'G_HAS_GRANT',
35
+ 'G_FOR_TOOL',
36
+ 'G_TARGETS',
37
+ 'G_BINDS_SELECTOR',
38
+ 'G_MAPS_TO',
39
+ 'G_EXECUTES_FOR',
40
+ 'G_EXECUTES_TOOL',
41
+ 'G_USES_IDENTITY',
42
+ 'G_VIA_CONNECTION',
43
+ 'G_EXPOSES_OPERATION',
44
+ 'G_OPERATION_TOOL',
45
+ 'G_OWNED_BY',
46
+ 'G_RESOURCE_OWNED_BY',
47
+ 'G_USES_CREDENTIAL',
48
+ 'G_DEPENDS_ON',
49
+ 'G_DIRECT_ACCESS_FOR',
50
+ ];
17
51
  export function mapIdentifier(value, code = 'MAP_REQUEST_INVALID') {
18
- if (typeof value !== 'string' || !value.trim() || [...value].length > 4096 || value.includes('\0') ||
52
+ if (typeof value !== 'string' ||
53
+ !value.trim() ||
54
+ [...value].length > 4096 ||
55
+ value.includes('\0') ||
19
56
  /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(value))
20
57
  throw new MapQueryError(code);
21
58
  return value;
@@ -23,7 +60,9 @@ export function mapIdentifier(value, code = 'MAP_REQUEST_INVALID') {
23
60
  export function validateReadScope(value) {
24
61
  try {
25
62
  const copied = immutableJson(value, 4, 1024, 64 * 1024);
26
- if (!copied || Array.isArray(copied) || typeof copied !== 'object' ||
63
+ if (!copied ||
64
+ Array.isArray(copied) ||
65
+ typeof copied !== 'object' ||
27
66
  Object.keys(copied).sort().join(',') !== 'allTeams,companyId,subject,teamIds,tenantIds')
28
67
  throw new Error();
29
68
  mapIdentifier(copied.subject);
@@ -32,7 +71,9 @@ export function validateReadScope(value) {
32
71
  throw new Error();
33
72
  for (const key of ['tenantIds', 'teamIds']) {
34
73
  const items = copied[key];
35
- if (!Array.isArray(items) || items.length > 128 || (key === 'tenantIds' && items.length === 0) ||
74
+ if (!Array.isArray(items) ||
75
+ items.length > 128 ||
76
+ (key === 'tenantIds' && items.length === 0) ||
36
77
  new Set(items).size !== items.length)
37
78
  throw new Error();
38
79
  items.forEach(item => mapIdentifier(item));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shieldaai/agent-map",
3
- "version": "0.1.0-alpha.3",
3
+ "version": "1.0.0",
4
4
  "description": "Read-only AI agent and MCP inventory queries over HTTP and MCP, with scoped access to Cartography projections.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -10,12 +10,25 @@
10
10
  ".": {
11
11
  "types": "./dist/index.d.ts",
12
12
  "import": "./dist/index.js"
13
+ },
14
+ "./service": {
15
+ "types": "./dist/service.d.ts",
16
+ "import": "./dist/service.js"
17
+ },
18
+ "./oidc": {
19
+ "types": "./dist/oidc.d.ts",
20
+ "import": "./dist/oidc.js"
21
+ },
22
+ "./identity-http": {
23
+ "types": "./dist/identity-http.d.ts",
24
+ "import": "./dist/identity-http.js"
13
25
  }
14
26
  },
15
27
  "bin": {
16
28
  "agent-guard-map": "./dist/cli.js",
17
29
  "agent-guard-map-identity": "./dist/identity-cli.js"
18
30
  },
31
+ "sideEffects": false,
19
32
  "files": [
20
33
  "dist",
21
34
  "README.md",
@@ -26,13 +39,13 @@
26
39
  },
27
40
  "dependencies": {
28
41
  "@modelcontextprotocol/server": "2.4.0",
29
- "@shieldaai/agent-guard": "0.1.0-alpha.3",
42
+ "@shieldaai/agent-guard": "1.0.0",
30
43
  "neo4j-driver": "6.3.0",
31
44
  "openid-client": "6.8.8"
32
45
  },
33
46
  "publishConfig": {
34
47
  "access": "public",
35
- "tag": "alpha"
48
+ "tag": "latest"
36
49
  },
37
50
  "repository": {
38
51
  "type": "git",