@anchrd/intel-api 0.6.7 → 0.9.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 (58) hide show
  1. package/README.md +63 -3
  2. package/dist/adapters/cloudflare/cloudflare.js +102 -37
  3. package/dist/adapters/cloudflare/cloudflare.types.d.ts +20 -0
  4. package/dist/adapters/content/content.d.ts +1 -1
  5. package/dist/adapters/db/db-flows.js +148 -20
  6. package/dist/adapters/db/db-grants.d.ts +13 -2
  7. package/dist/adapters/db/db-grants.js +25 -8
  8. package/dist/adapters/db/db-indexing.d.ts +2 -2
  9. package/dist/adapters/db/db-indexing.js +26 -19
  10. package/dist/adapters/db/db.d.ts +3 -3
  11. package/dist/adapters/db/db.js +442 -118
  12. package/dist/adapters/gate-applications/gate-applications.d.ts +23 -0
  13. package/dist/adapters/gate-applications/gate-applications.js +88 -0
  14. package/dist/adapters/index-queue/index-queue.d.ts +1 -1
  15. package/dist/adapters/index-queue/index-queue.js +2 -2
  16. package/dist/adapters/semantic-index/semantic-index.types.d.ts +2 -2
  17. package/dist/adapters/tool-delegation/tool-delegation.d.ts +22 -0
  18. package/dist/adapters/tool-delegation/tool-delegation.js +90 -0
  19. package/dist/agent-runtime/agent-runtime.d.ts +16 -0
  20. package/dist/agent-runtime/agent-runtime.js +150 -0
  21. package/dist/agent-runtime/agent-runtime.types.d.ts +122 -0
  22. package/dist/bundle/bundle.d.ts +4 -0
  23. package/dist/bundle/bundle.js +1048 -0
  24. package/dist/bundle/bundle.types.d.ts +33 -0
  25. package/dist/bundle/bundle.types.js +1 -0
  26. package/dist/cli/cli.js +10 -1
  27. package/dist/flows/flows.d.ts +8 -8
  28. package/dist/flows/flows.js +158 -42
  29. package/dist/flows/flows.types.d.ts +40 -7
  30. package/dist/http/http.d.ts +1 -0
  31. package/dist/http/http.js +348 -61
  32. package/dist/http/http.types.d.ts +6 -2
  33. package/dist/indexing/indexing.js +14 -2
  34. package/dist/indexing/indexing.types.d.ts +2 -2
  35. package/dist/intel/intel.js +12 -3
  36. package/dist/intel/intel.types.d.ts +6 -2
  37. package/dist/mcp/mcp.js +519 -124
  38. package/dist/mcp/mcp.types.d.ts +11 -2
  39. package/dist/nodes/nodes.d.ts +2 -0
  40. package/dist/nodes/nodes.js +1466 -0
  41. package/dist/nodes/nodes.types.d.ts +402 -0
  42. package/dist/nodes/nodes.types.js +1 -0
  43. package/dist/tools/tool-servers/tool-servers.d.ts +46 -0
  44. package/dist/tools/tool-servers/tool-servers.js +114 -0
  45. package/dist/tools/tools.js +190 -31
  46. package/dist/tools/tools.types.d.ts +23 -1
  47. package/migrations/0011_one_name_for_the_tree.sql +53 -0
  48. package/migrations/0012_table_snapshots.sql +29 -0
  49. package/migrations/0013_agents_in_the_tree.sql +76 -0
  50. package/migrations/0014_agent_applications.sql +25 -0
  51. package/migrations/0015_tools_delegated_from_a_connection.sql +15 -0
  52. package/package.json +3 -2
  53. package/dist/knowledge/knowledge.d.ts +0 -2
  54. package/dist/knowledge/knowledge.js +0 -761
  55. package/dist/knowledge/knowledge.types.d.ts +0 -198
  56. /package/dist/{knowledge/knowledge.types.js → agent-runtime/agent-runtime.types.js} +0 -0
  57. /package/dist/{knowledge → nodes}/document-links/document-links.d.ts +0 -0
  58. /package/dist/{knowledge → nodes}/document-links/document-links.js +0 -0
@@ -0,0 +1,402 @@
1
+ import type { AgentKeyRotated, AgentList, AppendTableRowsInput, AppendTableRowsResult, ArchiveNodeInput, CreateAgentInput, CreatedAgent, CreateNodeInput, DefineTableInput, DeleteTableRowsInput, DeleteTableRowsResult, Flow, FlowVersion, GetAgentInput, ListAgentsInput, ListNodesInput, Node, NodeAgent, NodeAttachment, NodeCitation, NodeDocument, NodeGraph, NodeGraphInput, NodeLink, NodeTable, NodeVersion, RedefineTableInput, ResolveNodeLinksInput, ResolveNodeLinksResult, ResourceGrant, ResourceGrantList, ResourceVerb, RevokeGrantInput, RotateAgentKeyInput, SaveAgentDefinitionInput, SaveAttachmentInput, SaveNodeVersionInput, SearchInput, ShareInput, ShareResult, UpdateNodeInput, UpdateTableRowsInput, UpdateTableRowsResult } from "@anchrd/intel-contract";
2
+ import type { SemanticIndex } from "../adapters/semantic-index/semantic-index.types.js";
3
+ export interface Actor {
4
+ id: string;
5
+ email: string;
6
+ isAdmin?: boolean;
7
+ }
8
+ export interface NewNode {
9
+ node: Node;
10
+ actorId: string;
11
+ idempotencyKey: string;
12
+ auditId: string;
13
+ application?: {
14
+ id: string;
15
+ } | undefined;
16
+ }
17
+ export interface NewNodeVersion {
18
+ version: NodeVersion;
19
+ actorId: string;
20
+ baseVersionId: string | null;
21
+ idempotencyKey: string;
22
+ auditId: string;
23
+ }
24
+ export interface NewTableVersion {
25
+ version: Omit<NodeVersion, "sequence">;
26
+ actorId: string;
27
+ idempotencyKey: string;
28
+ auditId: string;
29
+ }
30
+ export type TableSnapshotOperation = "node.table_update" | "node.table_delete" | "node.table_redefine";
31
+ export interface NewTableSnapshot {
32
+ version: NodeVersion;
33
+ actorId: string;
34
+ baseVersionId: string;
35
+ operation: TableSnapshotOperation;
36
+ metadata: Record<string, unknown>;
37
+ idempotencyKey: string;
38
+ auditId: string;
39
+ }
40
+ export interface NodeRepository {
41
+ listVisible(actor: Actor, input: ListNodesInput): Promise<{
42
+ items: Node[];
43
+ withChildren: string[];
44
+ }>;
45
+ listVisibleBounded(actor: Actor, input: {
46
+ parentId: string | null;
47
+ limit: number;
48
+ }): Promise<BoundedChildren>;
49
+ listVisibleAgents(actor: Actor, input: ListAgentsInput): Promise<Node[]>;
50
+ listVisibleAgentDefinitionKeys(actor: Actor, folderId: string): Promise<string[]>;
51
+ getVisible(actor: Actor, nodeId: string): Promise<Node | null>;
52
+ listVisibleSubtree(actor: Actor, rootId: string | null): Promise<SubtreeNode[]>;
53
+ can(actor: Actor, nodeId: string, verb: ResourceVerb): Promise<boolean>;
54
+ findIdempotentNode(actorId: string, operation: "node.create" | "node.save" | "node.append" | "node.update" | "node.archive" | "node.share" | "node.revoke" | TableSnapshotOperation, idempotencyKey: string): Promise<string | null>;
55
+ findIdempotentRevocation(actorId: string, idempotencyKey: string): Promise<boolean | null>;
56
+ insertNode(input: NewNode): Promise<Node>;
57
+ agentApplicationId(nodeId: string): Promise<string | null>;
58
+ /**
59
+ * The audit row for replacing the key of the Application an agent runs as (D29, #207).
60
+ *
61
+ * ⚠️ Its own write because it is the only mutation on an agent node that changes nothing IN the
62
+ * node — no version, no row, nothing a reader could diff afterwards. Swapping which credential an
63
+ * agent acts with is the most security-relevant thing anybody does to it, and without this it
64
+ * would be the one act on the tree that leaves no trace in Intel at all. Gate audits its side;
65
+ * Gate does not know which Intel node the principal belongs to.
66
+ *
67
+ * ⚠️ `applicationId` names the principal and is a name, not a credential. The key itself is not a
68
+ * parameter here and must never become one — audit is metadata, and this is exactly the row a
69
+ * "helpful" second field would leak it into.
70
+ */
71
+ recordAgentKeyRotation(input: {
72
+ auditId: string;
73
+ actorId: string;
74
+ nodeId: string;
75
+ applicationId: string;
76
+ occurredAt: string;
77
+ }): Promise<void>;
78
+ getVersion(versionId: string): Promise<NodeVersion | null>;
79
+ appendVersion(input: NewNodeVersion): Promise<"saved" | "conflict">;
80
+ appendTableVersion(input: NewTableVersion): Promise<NodeVersion>;
81
+ appendTableSnapshot(input: NewTableSnapshot): Promise<"saved" | "conflict">;
82
+ listVersionContentKeys(nodeId: string): Promise<string[]>;
83
+ tableHeaderContentKey(nodeId: string): Promise<string | null>;
84
+ listVersions(nodeId: string): Promise<NodeVersion[]>;
85
+ updateNode(input: {
86
+ node: Node;
87
+ baseUpdatedAt: string;
88
+ actorId: string;
89
+ idempotencyKey: string;
90
+ auditId: string;
91
+ }): Promise<"cycle" | "conflict" | Node>;
92
+ archiveNode(input: {
93
+ nodeId: string;
94
+ baseUpdatedAt: string;
95
+ archivedAt: string | null;
96
+ updatedAt: string;
97
+ actorId: string;
98
+ idempotencyKey: string;
99
+ auditId: string;
100
+ }): Promise<"conflict" | Node>;
101
+ listGrants(resourceId: string): Promise<ResourceGrant[]>;
102
+ organizationExecuteReaches(nodeId: string, exceptGrantId: string): Promise<boolean>;
103
+ listLinksVisible(actor: Actor, nodeId: string): Promise<NodeLink[]>;
104
+ resolveVisibleTitles(actor: Actor, nodeIds: string[]): Promise<Array<{
105
+ nodeId: string;
106
+ title: string;
107
+ }>>;
108
+ replaceTextLinks(input: {
109
+ sourceNodeId: string;
110
+ links: Array<{
111
+ id: string;
112
+ targetNodeId: string;
113
+ }>;
114
+ actorId: string;
115
+ auditId: string;
116
+ occurredAt: string;
117
+ }): Promise<void>;
118
+ graphVisible(actor: Actor, input: NodeGraphInput): Promise<NodeGraph>;
119
+ setGrant(input: {
120
+ grant: ResourceGrant;
121
+ actorId: string;
122
+ idempotencyKey: string;
123
+ auditId: string;
124
+ }): Promise<ResourceGrant>;
125
+ revokeGrant(input: {
126
+ resourceId: string;
127
+ grantId: string;
128
+ actorId: string;
129
+ idempotencyKey: string;
130
+ auditId: string;
131
+ occurredAt: string;
132
+ }): Promise<boolean>;
133
+ searchVisible(actor: Actor, input: SearchInput): Promise<NodeCitation[]>;
134
+ hydrateVisibleCitations(actor: Actor, nodeIds: string[], scopeId?: string): Promise<NodeCitation[]>;
135
+ listCurrentVersionIds(input: {
136
+ after: string | null;
137
+ limit: number;
138
+ }): Promise<string[]>;
139
+ importTree(input: ImportTreeInput): Promise<"created" | "replayed">;
140
+ findImportReplay(actorId: string, idempotencyKey: string): Promise<Record<string, unknown> | null>;
141
+ }
142
+ export interface ImportedLink {
143
+ id: string;
144
+ sourceNodeId: string;
145
+ targetNodeId: string;
146
+ }
147
+ /**
148
+ * Everything one bundle import creates, as one write (#137).
149
+ *
150
+ * ⚠️ The whole point of this shape is the atomicity: every row lands in one `db.batch`, which D1
151
+ * runs as one transaction, so a partial import cannot exist — either the whole subtree stands or
152
+ * nothing does. The R2 objects behind `versions` are written before and deleted again by the
153
+ * caller when the batch refuses.
154
+ *
155
+ * `nodes` must come parents before children: the tree's foreign key checks each row as it lands.
156
+ */
157
+ export interface ImportTreeInput {
158
+ nodes: Node[];
159
+ versions: NodeVersion[];
160
+ links: ImportedLink[];
161
+ flows: Flow[];
162
+ flowVersions: FlowVersion[];
163
+ actorId: string;
164
+ idempotencyKey: string;
165
+ auditId: string;
166
+ auditResourceId: string;
167
+ metadata: Record<string, unknown>;
168
+ occurredAt: string;
169
+ }
170
+ export interface ContentStore {
171
+ get(key: string): Promise<string | null>;
172
+ getBytes(key: string): Promise<ArrayBuffer | null>;
173
+ getStream(key: string): Promise<ReadableStream<Uint8Array> | null>;
174
+ put(key: string, content: string, mediaType: string): Promise<void>;
175
+ putBytes(key: string, content: ArrayBuffer, mediaType: string): Promise<void>;
176
+ delete(key: string): Promise<void>;
177
+ }
178
+ export interface NodeAttachmentBody {
179
+ attachment: NodeAttachment;
180
+ body: ReadableStream<Uint8Array>;
181
+ }
182
+ /**
183
+ * Gate's Applications surface, as the tree needs it (#182).
184
+ *
185
+ * ⚠️ Every call carries the CALLER's own Gate bearer, never a service key. Creating and switching a
186
+ * machine principal is an administrative act in Gate, gated on `applications:write` and audited
187
+ * there under the person who performed it; Intel's service key does not open that door at all
188
+ * (Gate's two-token rule). The token is therefore a parameter of the operation rather than a field
189
+ * of the actor: it must be impossible to reach for by accident from a service that has no business
190
+ * with it, and it must never end up on an audit row or in a log line.
191
+ */
192
+ export interface AgentApplications {
193
+ /**
194
+ * A new machine principal, named so a person can recognise the agent behind it.
195
+ *
196
+ * ⚠️ The returned key is plain text, the only time it exists in readable form anywhere, and it is
197
+ * gone from Gate the moment this call returns. It is passed straight back to the caller who asked
198
+ * for it and is written nowhere on the way (D27).
199
+ */
200
+ create(input: {
201
+ token: string;
202
+ name: string;
203
+ }): Promise<{
204
+ id: string;
205
+ key: string;
206
+ }>;
207
+ /**
208
+ * A replacement key for an Application that already exists, invalidating every earlier one.
209
+ *
210
+ * ⚠️ Gate issues before it revokes, so a failed rotation leaves the old key working. That is what
211
+ * makes `rotate-key` a safe repair rather than a way to lock an agent out: the only state this
212
+ * can leave behind is "new key issued, runtime never got it", and rotating again fixes it.
213
+ */
214
+ rotateKey(input: {
215
+ token: string;
216
+ applicationId: string;
217
+ }): Promise<{
218
+ key: string;
219
+ }>;
220
+ setEnabled(input: {
221
+ token: string;
222
+ applicationId: string;
223
+ enabled: boolean;
224
+ }): Promise<void>;
225
+ }
226
+ export interface GateCaller {
227
+ token: string;
228
+ }
229
+ export interface NodesDeps {
230
+ repository: NodeRepository;
231
+ content: ContentStore;
232
+ applications: AgentApplications;
233
+ agentRuntimeAvailable(): boolean;
234
+ /**
235
+ * The runtime's key store, as the tree needs it (D29, #207).
236
+ *
237
+ * ⚠️ This is where the Application key GOES, and it is the only place it goes. Gate hands it out
238
+ * once, this port carries it into the agent's Durable Object over the service binding, and the
239
+ * local variable that held it dies with the call. Nothing in this file writes it to D1, R2, an
240
+ * audit row, a log line or a response — the proof is the absence of the value in every argument
241
+ * every port was called with, not a redaction somebody could forget.
242
+ */
243
+ agentKeys: {
244
+ store(input: {
245
+ token: string;
246
+ agentId: string;
247
+ key: string;
248
+ }): Promise<void>;
249
+ };
250
+ /**
251
+ * The runtime's alarm, brought in line with the definition that was just written (#214).
252
+ *
253
+ * ⚠️ Required, not optional, and it carries no schedules. The runtime arms its alarm only when
254
+ * something tells it to look — a chat turn, a resume, a manual run, or this call — so a definition
255
+ * written without it leaves an agent whose schedules are visible in its profile and fire never.
256
+ * What travels is the agent's ID and the caller's bearer; the runtime re-reads the definition from
257
+ * Intel with the agent's own token, which is what keeps a schedule something that was written
258
+ * before it can be acted on.
259
+ */
260
+ agentSchedules: {
261
+ sync(input: {
262
+ token: string;
263
+ agentId: string;
264
+ }): Promise<void>;
265
+ };
266
+ id(): string;
267
+ now(): Date;
268
+ externalFlowCallers(actor: Actor, folderId: string): Promise<{
269
+ visible: string[];
270
+ hidden: number;
271
+ }>;
272
+ flowNodeReferences(actor: Actor, folderId: string): Promise<string[]>;
273
+ /**
274
+ * The handles of the MCP servers this actor reaches through the portal right now (D30).
275
+ *
276
+ * ⚠️ Injected as a live question, never as a stored list. Delegating a server is only allowed to
277
+ * somebody who has it themselves, and "has it" can only be answered by asking the portal at the
278
+ * moment of the save — a mirrored table here would be the permission mirror ADR-0003 removed.
279
+ */
280
+ toolServers(actor: Actor): Promise<string[]>;
281
+ hash(content: string | Uint8Array): Promise<string>;
282
+ indexing: {
283
+ enqueue(versionId: string): Promise<void>;
284
+ };
285
+ semantic?: SemanticIndex | undefined;
286
+ }
287
+ export type FolderAccess = "ok" | "missing" | "not-a-folder" | "forbidden";
288
+ export interface SubtreeNode {
289
+ node: Node;
290
+ version: {
291
+ id: string;
292
+ mediaType: string;
293
+ contentKey: string;
294
+ } | null;
295
+ }
296
+ export interface BoundedChildren {
297
+ items: Node[];
298
+ total: number;
299
+ }
300
+ export interface NodeService {
301
+ list(actor: Actor, input: ListNodesInput): Promise<{
302
+ items: Node[];
303
+ }>;
304
+ /**
305
+ * One level of the tree as this actor may see it, bounded: at most `limit` current children, and
306
+ * `total` for how many there are. Its caller is the relation graph, which draws a bounded picture
307
+ * and must not read a folder of a thousand documents to do it (#30).
308
+ *
309
+ * ⚠️ `total` counts what passed the visibility predicate and nothing else, or the size a picture
310
+ * reports would become a way of learning that something is filed here.
311
+ */
312
+ childrenBounded(actor: Actor, input: {
313
+ parentId: string | null;
314
+ limit: number;
315
+ }): Promise<BoundedChildren>;
316
+ get(actor: Actor, nodeId: string): Promise<NodeDocument>;
317
+ getVersion(actor: Actor, nodeId: string, versionId: string): Promise<NodeDocument>;
318
+ folderAccess(actor: Actor, folderId: string): Promise<FolderAccess>;
319
+ create(actor: Actor, input: CreateNodeInput): Promise<Node>;
320
+ save(actor: Actor, input: SaveNodeVersionInput): Promise<NodeDocument>;
321
+ saveAttachment(actor: Actor, input: SaveAttachmentInput): Promise<NodeDocument>;
322
+ getTable(actor: Actor, nodeId: string): Promise<NodeTable>;
323
+ defineTable(actor: Actor, input: DefineTableInput): Promise<NodeTable>;
324
+ appendTableRows(actor: Actor, input: AppendTableRowsInput): Promise<AppendTableRowsResult>;
325
+ updateTableRows(actor: Actor, input: UpdateTableRowsInput): Promise<UpdateTableRowsResult>;
326
+ deleteTableRows(actor: Actor, input: DeleteTableRowsInput): Promise<DeleteTableRowsResult>;
327
+ redefineTable(actor: Actor, input: RedefineTableInput): Promise<NodeTable>;
328
+ getAgent(actor: Actor, input: GetAgentInput): Promise<NodeAgent>;
329
+ /**
330
+ * An agent node, its first definition, and the Gate Application it runs as (#182, D29).
331
+ *
332
+ * ⚠️ The answer carries NO key. The one Gate issues goes straight into the agent runtime over the
333
+ * service binding, inside this call, and the caller never sees it — which is what makes an agent
334
+ * created through the screen able to run without a terminal step (#200, #207).
335
+ */
336
+ createAgent(actor: Actor, input: CreateAgentInput, caller: GateCaller): Promise<CreatedAgent>;
337
+ /**
338
+ * Replace the key of the Application an agent runs as, and give the new one to the runtime.
339
+ *
340
+ * ⚠️ The repair path for an agent that answers `agent_key_missing`, and the only one: Gate keeps
341
+ * a key hashed, so an agent whose Durable Object lost its key cannot be handed the old one back.
342
+ * It is a write on the agent node — `knowledge/write` and the resource ACL — not a run.
343
+ */
344
+ rotateAgentKey(actor: Actor, input: RotateAgentKeyInput, caller: GateCaller): Promise<AgentKeyRotated>;
345
+ /**
346
+ * A new definition version for an agent, and the alarm that goes with it (#214).
347
+ *
348
+ * ⚠️ It takes the caller's bearer for the same reason `createAgent` does: the runtime is told to
349
+ * re-arm over the service binding, and what travels on that call is the person behind this request
350
+ * — never a service key (D19).
351
+ */
352
+ saveAgentDefinition(actor: Actor, input: SaveAgentDefinitionInput, caller: GateCaller): Promise<NodeAgent>;
353
+ listAgents(actor: Actor, input: ListAgentsInput): Promise<AgentList>;
354
+ getAttachment(actor: Actor, nodeId: string): Promise<NodeAttachment>;
355
+ readAttachment(actor: Actor, nodeId: string): Promise<NodeAttachmentBody>;
356
+ listVersions(actor: Actor, nodeId: string): Promise<{
357
+ items: NodeVersion[];
358
+ }>;
359
+ update(actor: Actor, input: UpdateNodeInput): Promise<Node>;
360
+ /**
361
+ * Archiving and restoring, for every kind — and for an agent, its Gate Application with it (#182).
362
+ *
363
+ * ⚠️ The Gate call comes FIRST and the node row second. An archived agent whose principal is
364
+ * still live is exactly the access nobody can see any more, so that state must not be reachable
365
+ * by a write that half succeeded; the opposite order can only leave a live agent whose principal
366
+ * is off, which stops working loudly and is repaired by repeating the call.
367
+ */
368
+ archive(actor: Actor, input: ArchiveNodeInput, caller: GateCaller): Promise<Node>;
369
+ listGrants(actor: Actor, resourceId: string): Promise<ResourceGrantList>;
370
+ listLinks(actor: Actor, nodeId: string): Promise<{
371
+ items: NodeLink[];
372
+ }>;
373
+ resolveLinks(actor: Actor, input: ResolveNodeLinksInput): Promise<ResolveNodeLinksResult>;
374
+ graph(actor: Actor, input: NodeGraphInput): Promise<NodeGraph>;
375
+ visibleNode(actor: Actor, nodeId: string): Promise<Node | null>;
376
+ share(actor: Actor, input: ShareInput): Promise<ShareResult>;
377
+ revokeGrant(actor: Actor, input: RevokeGrantInput): Promise<{
378
+ revoked: boolean;
379
+ }>;
380
+ search(actor: Actor, input: SearchInput): Promise<{
381
+ items: NodeCitation[];
382
+ }>;
383
+ reindex(actor: Actor): Promise<{
384
+ queued: number;
385
+ }>;
386
+ }
387
+ export interface NodeIndexTarget {
388
+ nodeId: string;
389
+ versionId: string;
390
+ title: string;
391
+ description: string | null;
392
+ contentKeys: string[];
393
+ mediaType: string;
394
+ kind: "document" | "attachment" | "table" | "agent";
395
+ updatedAt: string;
396
+ }
397
+ export interface NodeIndexRepository {
398
+ getTarget(versionId: string): Promise<NodeIndexTarget | null>;
399
+ replace(target: NodeIndexTarget, content: string): Promise<void>;
400
+ markIndexed(versionId: string, occurredAt: string): Promise<void>;
401
+ markError(versionId: string, message: string, occurredAt: string): Promise<void>;
402
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ import { type ToolServer } from "@anchrd/intel-contract";
2
+ /**
3
+ * The portal's own directory tool. It is an ordinary entry in `tools/list`, so it is reached the
4
+ * same way every other tool is: `tools/call` with the asking user's portal token.
5
+ *
6
+ * ⚠️ Its presence in the live list is also the permission check. A deployment whose portal does not
7
+ * offer it, or a user whose Access policies hide it, gets no server list at all — and therefore
8
+ * cannot delegate anything. That is deliberate: the alternative would be inventing the server list
9
+ * out of tool names, which is exactly the guess this module exists to avoid.
10
+ */
11
+ export declare const ServerDirectoryTool = "portal_list_servers";
12
+ export interface ToolServerDirectoryAnswer {
13
+ isError: boolean;
14
+ content: unknown[];
15
+ structuredContent?: unknown;
16
+ }
17
+ /**
18
+ * The servers this user reaches: the portal's directory, confirmed against the portal's live tool
19
+ * list.
20
+ *
21
+ * Two sets come out of one answer, and keeping them apart is the whole of this function:
22
+ *
23
+ * - **`declared`** — every identifier the directory mentions at all, disabled rows included. It is
24
+ * the universe `serverOf` attributes against, and it must be as WIDE as possible. ⚠️ Narrowing it
25
+ * widens the delegation: drop `wiki_extra` because the portal marked it disabled, and the live
26
+ * tool `wiki_extra__read` falls back onto `wiki` — a tool of a server nobody delegated, handed to
27
+ * an agent that was given `wiki`. Filtering therefore happens after attribution, never before it.
28
+ * - **the returned list** — what may actually be delegated: enabled, and confirmed by at least one
29
+ * live tool. Intel cannot prove that a directory identifier is the same string the namespace uses,
30
+ * so it only ever offers handles it has just seen work.
31
+ *
32
+ * ⚠️ What remains outside Intel's reach: a directory that omits a server whose tools ARE in
33
+ * `tools/list`. Nothing in the answer distinguishes that from a tool of the enclosing namespace, and
34
+ * the portal is the authority on its own servers — so an incomplete directory widens by exactly one
35
+ * namespace level, and it is a portal defect rather than something this code can detect.
36
+ */
37
+ export interface ToolServerDirectory {
38
+ /** Every identifier the directory mentioned. Attribute against this, never against `servers`. */
39
+ declared: string[];
40
+ /** What may be offered and delegated: enabled, and confirmed by a live tool. */
41
+ servers: ToolServer[];
42
+ }
43
+ export declare function toolServersFrom(input: {
44
+ directory: ToolServerDirectoryAnswer;
45
+ toolNames: string[];
46
+ }): ToolServerDirectory | null;
@@ -0,0 +1,114 @@
1
+ import { serverOf } from "@anchrd/intel-contract";
2
+ import { z } from "zod";
3
+ /**
4
+ * The portal's own directory tool. It is an ordinary entry in `tools/list`, so it is reached the
5
+ * same way every other tool is: `tools/call` with the asking user's portal token.
6
+ *
7
+ * ⚠️ Its presence in the live list is also the permission check. A deployment whose portal does not
8
+ * offer it, or a user whose Access policies hide it, gets no server list at all — and therefore
9
+ * cannot delegate anything. That is deliberate: the alternative would be inventing the server list
10
+ * out of tool names, which is exactly the guess this module exists to avoid.
11
+ */
12
+ export const ServerDirectoryTool = "portal_list_servers";
13
+ /**
14
+ * What the portal answers about one server, read as tolerantly as possible.
15
+ *
16
+ * ⚠️ Only the shape is assumed, never the vocabulary. The portal is somebody else's product and its
17
+ * payload is not part of any contract Intel owns, so every field is optional and the answer is
18
+ * accepted from `structuredContent`, from `{ servers: [...] }` or from a JSON text block. What
19
+ * makes a row usable is not that it parsed but that its identifier is confirmed against the live
20
+ * tool list below.
21
+ */
22
+ const DirectoryRow = z.looseObject({
23
+ id: z.string().optional(),
24
+ name: z.string().optional(),
25
+ enabled: z.boolean().optional(),
26
+ });
27
+ const DirectoryRows = z.array(DirectoryRow);
28
+ const DirectoryEnvelope = z.looseObject({ servers: DirectoryRows });
29
+ /**
30
+ * Every place the portal could reasonably have put its list, tried in order. An MCP tool result is
31
+ * either structured or text, and a server that answers with `{ servers: [...] }` is as likely as
32
+ * one that answers with a bare array.
33
+ */
34
+ function rows(answer) {
35
+ const candidates = [answer.structuredContent];
36
+ for (const block of answer.content) {
37
+ if (typeof block === "object" && block !== null && "text" in block) {
38
+ const text = block.text;
39
+ if (typeof text !== "string")
40
+ continue;
41
+ try {
42
+ candidates.push(JSON.parse(text));
43
+ }
44
+ catch {
45
+ // A text block that is not JSON is prose, not a directory. Skip it rather than fail: the
46
+ // portal may well add a human sentence beside the payload.
47
+ }
48
+ }
49
+ }
50
+ for (const candidate of candidates) {
51
+ const bare = DirectoryRows.safeParse(candidate);
52
+ if (bare.success)
53
+ return bare.data;
54
+ const wrapped = DirectoryEnvelope.safeParse(candidate);
55
+ if (wrapped.success)
56
+ return wrapped.data.servers;
57
+ }
58
+ return null;
59
+ }
60
+ /**
61
+ * ⚠️ The rule itself — longest match against declared handles, never a cut at an underscore — is
62
+ * `serverOf` in `@anchrd/intel-contract`, because the tools screen has to read a name exactly the
63
+ * way the delegation does (#212). What stays here is what the rule is USED for: a tool whose prefix
64
+ * matches no declared handle belongs to no server and can never be delegated, which is what keeps
65
+ * the portal's own management tools (`portal_list_servers`, `portal_toggle_servers`) out of an
66
+ * agent's reach — the portal does not list itself as an upstream server.
67
+ */
68
+ /**
69
+ * ⚠️ A handle that would carry the portal's OWN tools is never a server anybody can be given.
70
+ *
71
+ * The portal does not list itself as an upstream, so in practice `portal` never appears in the
72
+ * directory — but "in practice" is not a guard. One row called `portal` (an upstream a customer
73
+ * happens to name that way, a future portal that describes itself) would make `portal_toggle_servers`
74
+ * a delegable tool, and an agent could then switch its delegator's MCP servers on and off. The rule
75
+ * is written as a question about reach rather than as a blocklist of one string: if delegating this
76
+ * handle would hand over `portal_list_servers`, the handle is refused.
77
+ */
78
+ function ownsTheDirectory(handle) {
79
+ return ServerDirectoryTool.startsWith(`${handle}_`);
80
+ }
81
+ export function toolServersFrom(input) {
82
+ const parsed = rows(input.directory);
83
+ if (parsed === null)
84
+ return null;
85
+ // `id` and `name` are both offered as candidates because the portal's directory names them
86
+ // separately and only one of them is the namespace. The live list decides which.
87
+ const display = new Map();
88
+ const offerable = new Set();
89
+ for (const row of parsed) {
90
+ for (const candidate of [row.name, row.id]) {
91
+ if (candidate === undefined || candidate.length === 0)
92
+ continue;
93
+ if (ownsTheDirectory(candidate))
94
+ continue;
95
+ if (!display.has(candidate))
96
+ display.set(candidate, row.name ?? row.id ?? candidate);
97
+ if (row.enabled !== false)
98
+ offerable.add(candidate);
99
+ }
100
+ }
101
+ const counts = new Map();
102
+ for (const toolName of input.toolNames) {
103
+ const handle = serverOf(toolName, display.keys());
104
+ if (handle === null || !offerable.has(handle))
105
+ continue;
106
+ counts.set(handle, (counts.get(handle) ?? 0) + 1);
107
+ }
108
+ return {
109
+ declared: [...display.keys()],
110
+ servers: [...counts.entries()]
111
+ .map(([handle, toolCount]) => ({ handle, name: display.get(handle) ?? handle, toolCount }))
112
+ .sort((a, b) => a.handle.localeCompare(b.handle)),
113
+ };
114
+ }