@anchrd/intel-api 0.14.0 → 0.16.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 (60) hide show
  1. package/dist/adapters/cloudflare/cloudflare.js +0 -68
  2. package/dist/adapters/cloudflare/cloudflare.types.d.ts +0 -39
  3. package/dist/adapters/db/db-flows.js +3 -2
  4. package/dist/adapters/db/db-grants.d.ts +1 -1
  5. package/dist/adapters/db/db-grants.js +1 -1
  6. package/dist/adapters/db/db.js +16 -113
  7. package/dist/adapters/openid/openid.js +1 -1
  8. package/dist/adapters/remote-tools/remote-tools.js +1 -1
  9. package/dist/bundle/bundle.js +31 -141
  10. package/dist/bundle/bundle.types.d.ts +1 -1
  11. package/dist/cli/cli.js +18 -11
  12. package/dist/flows/flows.d.ts +1 -1
  13. package/dist/flows/flows.js +1 -1
  14. package/dist/flows/flows.types.d.ts +4 -1
  15. package/dist/http/http.js +43 -238
  16. package/dist/http/http.types.d.ts +0 -8
  17. package/dist/indexing/indexing.js +18 -89
  18. package/dist/intel/intel.js +4 -9
  19. package/dist/intel/intel.types.d.ts +0 -6
  20. package/dist/mcp/mcp.js +84 -361
  21. package/dist/mcp/mcp.types.d.ts +2 -7
  22. package/dist/nodes/document-links/document-links.d.ts +6 -8
  23. package/dist/nodes/document-links/document-links.js +8 -31
  24. package/dist/nodes/nodes.js +24 -890
  25. package/dist/nodes/nodes.types.d.ts +14 -173
  26. package/dist/tools/tool-servers/tool-servers.d.ts +1 -1
  27. package/dist/tools/tool-servers/tool-servers.js +1 -1
  28. package/dist/tools/tools.js +37 -148
  29. package/dist/tools/tools.types.d.ts +1 -22
  30. package/migrations/0018_no_context_policy_at_last.sql +13 -6
  31. package/migrations/0019_one_name_for_the_grants.sql +52 -0
  32. package/package.json +2 -2
  33. package/dist/adapters/cloudflare-api/cloudflare-api.d.ts +0 -22
  34. package/dist/adapters/cloudflare-api/cloudflare-api.js +0 -306
  35. package/dist/adapters/cloudflare-api/cloudflare-api.types.d.ts +0 -64
  36. package/dist/adapters/cloudflare-api/cloudflare-api.types.js +0 -1
  37. package/dist/adapters/gate-applications/gate-applications.d.ts +0 -23
  38. package/dist/adapters/gate-applications/gate-applications.js +0 -88
  39. package/dist/adapters/tool-delegation/tool-delegation.d.ts +0 -22
  40. package/dist/adapters/tool-delegation/tool-delegation.js +0 -90
  41. package/dist/agent-costs/agent-costs.d.ts +0 -16
  42. package/dist/agent-costs/agent-costs.js +0 -105
  43. package/dist/agent-costs/agent-costs.types.d.ts +0 -30
  44. package/dist/agent-costs/agent-costs.types.js +0 -1
  45. package/dist/agent-runtime/agent-runtime.d.ts +0 -16
  46. package/dist/agent-runtime/agent-runtime.js +0 -150
  47. package/dist/agent-runtime/agent-runtime.types.d.ts +0 -122
  48. package/dist/agent-runtime/agent-runtime.types.js +0 -1
  49. package/dist/model-catalog/model-catalog.d.ts +0 -2
  50. package/dist/model-catalog/model-catalog.js +0 -99
  51. package/dist/model-catalog/model-catalog.types.d.ts +0 -15
  52. package/dist/model-catalog/model-catalog.types.js +0 -1
  53. package/dist/nodes/board/board.d.ts +0 -61
  54. package/dist/nodes/board/board.js +0 -826
  55. package/dist/nodes/board/board.types.d.ts +0 -38
  56. package/dist/nodes/board/board.types.js +0 -1
  57. package/migrations/0013_agents_in_the_tree.sql +0 -76
  58. package/migrations/0014_agent_applications.sql +0 -25
  59. package/migrations/0015_tools_delegated_from_a_connection.sql +0 -15
  60. package/migrations/0016_boards_in_the_tree.sql +0 -80
@@ -1,4 +1,7 @@
1
- import type { AddBoardTaskInput, AgentKeyRotated, AgentList, AppendTableRowsInput, AppendTableRowsResult, ArchiveNodeInput, BoardTaskResult, ConfigureBoardInput, ConfigureBoardResult, CreateAgentInput, CreatedAgent, CreateNodeInput, DefineTableInput, DeleteBoardTaskInput, DeleteBoardTaskResult, DeleteTableRowsInput, DeleteTableRowsResult, Flow, FlowVersion, GetAgentInput, GetBoardInput, ListAgentsInput, ListNodesInput, MoveBoardTaskInput, Node, NodeAgent, NodeAttachment, NodeBoard, NodeCitation, NodeDocument, NodeGraph, NodeGraphInput, NodeLink, NodeTable, NodeVersion, RedefineTableInput, RepairBoardTaskIdsInput, RepairBoardTaskIdsResult, ResolveNodeLinksInput, ResolveNodeLinksResult, ResourceGrant, ResourceGrantList, ResourceVerb, RevokeGrantInput, RotateAgentKeyInput, SaveAgentDefinitionInput, SaveAttachmentInput, SaveNodeVersionInput, SearchInput, ShareInput, ShareResult, UpdateBoardTaskInput, UpdateNodeInput, UpdateTableRowsInput, UpdateTableRowsResult } from "@anchrd/intel-contract";
1
+ import type { Flow, FlowVersion } from "@anchrd/intel-contract/flow";
2
+ import type { ArchiveNodeInput, CreateNodeInput, ListNodesInput, Node, NodeAttachment, NodeCitation, NodeDocument, NodeGraph, NodeGraphInput, NodeKind, NodeLink, NodeTable, NodeVersion, ResolveNodeLinksInput, ResolveNodeLinksResult, SaveAttachmentInput, SaveNodeVersionInput, SearchInput, UpdateNodeInput } from "@anchrd/intel-contract/node";
3
+ import type { ResourceGrant, ResourceGrantList, ResourceVerb, RevokeGrantInput, ShareInput, ShareResult } from "@anchrd/intel-contract/share";
4
+ import type { AppendTableRowsInput, AppendTableRowsResult, DefineTableInput, DeleteTableRowsInput, DeleteTableRowsResult, RedefineTableInput, UpdateTableRowsInput, UpdateTableRowsResult } from "@anchrd/intel-contract/table";
2
5
  import type { SemanticIndex } from "../adapters/semantic-index/semantic-index.types.js";
3
6
  export interface Actor {
4
7
  id: string;
@@ -10,9 +13,6 @@ export interface NewNode {
10
13
  actorId: string;
11
14
  idempotencyKey: string;
12
15
  auditId: string;
13
- application?: {
14
- id: string;
15
- } | undefined;
16
16
  }
17
17
  export interface NewNodeVersion {
18
18
  version: NodeVersion;
@@ -27,7 +27,7 @@ export interface NewTableVersion {
27
27
  idempotencyKey: string;
28
28
  auditId: string;
29
29
  }
30
- export type SnapshotOperation = "node.table_update" | "node.table_delete" | "node.table_redefine" | "node.board_configure" | "node.board_task_add" | "node.board_task_update" | "node.board_task_move" | "node.board_task_delete" | "node.board_task_repair";
30
+ export type SnapshotOperation = "node.table_update" | "node.table_delete" | "node.table_redefine";
31
31
  /**
32
32
  * One version that replaces the readable state, written only against the state it replaces.
33
33
  *
@@ -35,11 +35,6 @@ export type SnapshotOperation = "node.table_update" | "node.table_delete" | "nod
35
35
  * read, so it has to know which state it replaces — the same optimistic concurrency the document
36
36
  * save uses, enforced in the same statement that inserts the row (#135).
37
37
  *
38
- * ⚠️ A board uses this too, and its `baseVersionId` is NOT the caller's (#285). The caller of a task
39
- * operation never sends one; the service reads the current version, applies the change by task id
40
- * and passes what it read. A refusal here therefore means "somebody wrote in between", which the
41
- * service answers by reading again and re-applying, so two agents moving two different tasks both
42
- * get through instead of one of them meeting `version_conflict`.
43
38
  */
44
39
  export interface NewSnapshotVersion {
45
40
  version: NodeVersion;
@@ -59,8 +54,6 @@ export interface NodeRepository {
59
54
  parentId: string | null;
60
55
  limit: number;
61
56
  }): Promise<BoundedChildren>;
62
- listVisibleAgents(actor: Actor, input: ListAgentsInput): Promise<Node[]>;
63
- listVisibleAgentDefinitionKeys(actor: Actor, folderId: string): Promise<string[]>;
64
57
  getVisible(actor: Actor, nodeId: string): Promise<Node | null>;
65
58
  listVisibleSubtree(actor: Actor, rootId: string | null): Promise<SubtreeNode[]>;
66
59
  can(actor: Actor, nodeId: string, verb: ResourceVerb): Promise<boolean>;
@@ -68,27 +61,6 @@ export interface NodeRepository {
68
61
  findIdempotentRevocation(actorId: string, idempotencyKey: string): Promise<boolean | null>;
69
62
  findSnapshotMetadata(actorId: string, operation: SnapshotOperation, idempotencyKey: string): Promise<Record<string, unknown> | null>;
70
63
  insertNode(input: NewNode): Promise<Node>;
71
- agentApplicationId(nodeId: string): Promise<string | null>;
72
- /**
73
- * The audit row for replacing the key of the Application an agent runs as (D29, #207).
74
- *
75
- * ⚠️ Its own write because it is the only mutation on an agent node that changes nothing IN the
76
- * node — no version, no row, nothing a reader could diff afterwards. Swapping which credential an
77
- * agent acts with is the most security-relevant thing anybody does to it, and without this it
78
- * would be the one act on the tree that leaves no trace in Intel at all. Gate audits its side;
79
- * Gate does not know which Intel node the principal belongs to.
80
- *
81
- * ⚠️ `applicationId` names the principal and is a name, not a credential. The key itself is not a
82
- * parameter here and must never become one — audit is metadata, and this is exactly the row a
83
- * "helpful" second field would leak it into.
84
- */
85
- recordAgentKeyRotation(input: {
86
- auditId: string;
87
- actorId: string;
88
- nodeId: string;
89
- applicationId: string;
90
- occurredAt: string;
91
- }): Promise<void>;
92
64
  getVersion(versionId: string): Promise<NodeVersion | null>;
93
65
  appendVersion(input: NewNodeVersion): Promise<"saved" | "conflict">;
94
66
  appendTableVersion(input: NewTableVersion): Promise<NodeVersion>;
@@ -207,90 +179,9 @@ export interface NodeAttachmentBody {
207
179
  attachment: NodeAttachment;
208
180
  body: ReadableStream<Uint8Array>;
209
181
  }
210
- /**
211
- * Gate's Applications surface, as the tree needs it (#182).
212
- *
213
- * ⚠️ Every call carries the CALLER's own Gate bearer, never a service key. Creating and switching a
214
- * machine principal is an administrative act in Gate, gated on `applications:write` and audited
215
- * there under the person who performed it; Intel's service key does not open that door at all
216
- * (Gate's two-token rule). The token is therefore a parameter of the operation rather than a field
217
- * of the actor: it must be impossible to reach for by accident from a service that has no business
218
- * with it, and it must never end up on an audit row or in a log line.
219
- */
220
- export interface AgentApplications {
221
- /**
222
- * A new machine principal, named so a person can recognise the agent behind it.
223
- *
224
- * ⚠️ The returned key is plain text, the only time it exists in readable form anywhere, and it is
225
- * gone from Gate the moment this call returns. It is passed straight back to the caller who asked
226
- * for it and is written nowhere on the way (D27).
227
- */
228
- create(input: {
229
- token: string;
230
- name: string;
231
- }): Promise<{
232
- id: string;
233
- key: string;
234
- }>;
235
- /**
236
- * A replacement key for an Application that already exists, invalidating every earlier one.
237
- *
238
- * ⚠️ Gate issues before it revokes, so a failed rotation leaves the old key working. That is what
239
- * makes `rotate-key` a safe repair rather than a way to lock an agent out: the only state this
240
- * can leave behind is "new key issued, runtime never got it", and rotating again fixes it.
241
- */
242
- rotateKey(input: {
243
- token: string;
244
- applicationId: string;
245
- }): Promise<{
246
- key: string;
247
- }>;
248
- setEnabled(input: {
249
- token: string;
250
- applicationId: string;
251
- enabled: boolean;
252
- }): Promise<void>;
253
- }
254
- export interface GateCaller {
255
- token: string;
256
- }
257
182
  export interface NodesDeps {
258
183
  repository: NodeRepository;
259
184
  content: ContentStore;
260
- applications: AgentApplications;
261
- agentRuntimeAvailable(): boolean;
262
- /**
263
- * The runtime's key store, as the tree needs it (D29, #207).
264
- *
265
- * ⚠️ This is where the Application key GOES, and it is the only place it goes. Gate hands it out
266
- * once, this port carries it into the agent's Durable Object over the service binding, and the
267
- * local variable that held it dies with the call. Nothing in this file writes it to D1, R2, an
268
- * audit row, a log line or a response — the proof is the absence of the value in every argument
269
- * every port was called with, not a redaction somebody could forget.
270
- */
271
- agentKeys: {
272
- store(input: {
273
- token: string;
274
- agentId: string;
275
- key: string;
276
- }): Promise<void>;
277
- };
278
- /**
279
- * The runtime's alarm, brought in line with the definition that was just written (#214).
280
- *
281
- * ⚠️ Required, not optional, and it carries no schedules. The runtime arms its alarm only when
282
- * something tells it to look — a chat turn, a resume, a manual run, or this call — so a definition
283
- * written without it leaves an agent whose schedules are visible in its profile and fire never.
284
- * What travels is the agent's ID and the caller's bearer; the runtime re-reads the definition from
285
- * Intel with the agent's own token, which is what keeps a schedule something that was written
286
- * before it can be acted on.
287
- */
288
- agentSchedules: {
289
- sync(input: {
290
- token: string;
291
- agentId: string;
292
- }): Promise<void>;
293
- };
294
185
  id(): string;
295
186
  now(): Date;
296
187
  externalFlowCallers(actor: Actor, folderId: string): Promise<{
@@ -353,56 +244,6 @@ export interface NodeService {
353
244
  updateTableRows(actor: Actor, input: UpdateTableRowsInput): Promise<UpdateTableRowsResult>;
354
245
  deleteTableRows(actor: Actor, input: DeleteTableRowsInput): Promise<DeleteTableRowsResult>;
355
246
  redefineTable(actor: Actor, input: RedefineTableInput): Promise<NodeTable>;
356
- /**
357
- * A board and the five operations that change it (#285).
358
- *
359
- * ⚠️ Not one "save the board" call, deliberately. Every write here names a TASK ID and the server
360
- * applies it to the document it just read, so two callers touching two different tasks both get
361
- * through — the same problem `appendTableRows` solves for a table, arrived at from the other side.
362
- * A whole-document write would put every concurrent editor into `version_conflict` and force each
363
- * of them to read a board of thousands of tasks before changing one word on one card.
364
- */
365
- getBoard(actor: Actor, input: GetBoardInput): Promise<NodeBoard>;
366
- configureBoard(actor: Actor, input: ConfigureBoardInput): Promise<ConfigureBoardResult>;
367
- addBoardTask(actor: Actor, input: AddBoardTaskInput): Promise<BoardTaskResult>;
368
- updateBoardTask(actor: Actor, input: UpdateBoardTaskInput): Promise<BoardTaskResult>;
369
- moveBoardTask(actor: Actor, input: MoveBoardTaskInput): Promise<BoardTaskResult>;
370
- deleteBoardTask(actor: Actor, input: DeleteBoardTaskInput): Promise<DeleteBoardTaskResult>;
371
- /**
372
- * The sixth, and the only one that is not about somebody's project (anchrd/intel#341).
373
- *
374
- * ⚠️ It names no task id, because a board that needs it holds two entries under one — the pair a
375
- * bundle written before anchrd/intel#321 could carry in. Everything else here is unreachable for
376
- * that second entry: the five operations above address a task by id and would take, write or
377
- * remove both at once.
378
- */
379
- repairBoardTaskIds(actor: Actor, input: RepairBoardTaskIdsInput): Promise<RepairBoardTaskIdsResult>;
380
- getAgent(actor: Actor, input: GetAgentInput): Promise<NodeAgent>;
381
- /**
382
- * An agent node, its first definition, and the Gate Application it runs as (#182, D29).
383
- *
384
- * ⚠️ The answer carries NO key. The one Gate issues goes straight into the agent runtime over the
385
- * service binding, inside this call, and the caller never sees it — which is what makes an agent
386
- * created through the screen able to run without a terminal step (#200, #207).
387
- */
388
- createAgent(actor: Actor, input: CreateAgentInput, caller: GateCaller): Promise<CreatedAgent>;
389
- /**
390
- * Replace the key of the Application an agent runs as, and give the new one to the runtime.
391
- *
392
- * ⚠️ The repair path for an agent that answers `agent_key_missing`, and the only one: Gate keeps
393
- * a key hashed, so an agent whose Durable Object lost its key cannot be handed the old one back.
394
- * It is a write on the agent node — `knowledge/write` and the resource ACL — not a run.
395
- */
396
- rotateAgentKey(actor: Actor, input: RotateAgentKeyInput, caller: GateCaller): Promise<AgentKeyRotated>;
397
- /**
398
- * A new definition version for an agent, and the alarm that goes with it (#214).
399
- *
400
- * ⚠️ It takes the caller's bearer for the same reason `createAgent` does: the runtime is told to
401
- * re-arm over the service binding, and what travels on that call is the person behind this request
402
- * — never a service key (D19).
403
- */
404
- saveAgentDefinition(actor: Actor, input: SaveAgentDefinitionInput, caller: GateCaller): Promise<NodeAgent>;
405
- listAgents(actor: Actor, input: ListAgentsInput): Promise<AgentList>;
406
247
  getAttachment(actor: Actor, nodeId: string): Promise<NodeAttachment>;
407
248
  readAttachment(actor: Actor, nodeId: string): Promise<NodeAttachmentBody>;
408
249
  listVersions(actor: Actor, nodeId: string): Promise<{
@@ -417,7 +258,7 @@ export interface NodeService {
417
258
  * by a write that half succeeded; the opposite order can only leave a live agent whose principal
418
259
  * is off, which stops working loudly and is repaired by repeating the call.
419
260
  */
420
- archive(actor: Actor, input: ArchiveNodeInput, caller: GateCaller): Promise<Node>;
261
+ archive(actor: Actor, input: ArchiveNodeInput): Promise<Node>;
421
262
  listGrants(actor: Actor, resourceId: string): Promise<ResourceGrantList>;
422
263
  listLinks(actor: Actor, nodeId: string): Promise<{
423
264
  items: NodeLink[];
@@ -443,16 +284,15 @@ export interface NodeIndexTarget {
443
284
  description: string | null;
444
285
  contentKeys: string[];
445
286
  mediaType: string;
446
- kind: "document" | "attachment" | "table" | "agent" | "board";
287
+ kind: Exclude<NodeKind, "folder">;
447
288
  updatedAt: string;
448
289
  }
449
290
  /**
450
291
  * One passage of a node, as the full-text index holds it.
451
292
  *
452
- * ⚠️ A list, and most kinds put exactly one in it. A board puts one per TASK (#285): "where do I
453
- * stand with X" has to find a card, and a board indexed as one blob would answer with the whole
454
- * board every time — the passage a searcher is shown would be whichever thousand characters the
455
- * snippet happened to cut, from a task that may have nothing to do with the question.
293
+ * ⚠️ A list, and every kind left after #390 puts exactly one in it. It stays a list because the
294
+ * chunked shape is what `node_fts` and `node_vectors` are built around (#285, anchrd/intel#301)
295
+ * the one thing that would have to be rebuilt to unpick it.
456
296
  *
457
297
  * `title` is what the passage is called rather than what the node is called: for a task it is the
458
298
  * task's own title, which is the text the FTS table weights highest.
@@ -461,9 +301,10 @@ export interface IndexChunk {
461
301
  /**
462
302
  * What this passage is called inside its node (anchrd/intel#301).
463
303
  *
464
- * `""` for a node that is one passage every kind but `board` and the task's own id for a card.
465
- * The full-text index does not store it; the vector index is named by it, which is how a semantic
466
- * hit can say WHICH card answered instead of only which board.
304
+ * `""` for a node that is one passage, which since #390 is every kind. The full-text index does
305
+ * not store it; the vector index is NAMED by it, and that is why it stays: the name written
306
+ * before anchrd/intel#301 is the bare node id, so nothing an installation already holds has to be
307
+ * renamed or embedded again.
467
308
  */
468
309
  key: string;
469
310
  title: string;
@@ -1,4 +1,4 @@
1
- import { type ToolServer } from "@anchrd/intel-contract";
1
+ import { type ToolServer } from "@anchrd/intel-contract/tool";
2
2
  /**
3
3
  * The portal's own directory tool. It is an ordinary entry in `tools/list`, so it is reached the
4
4
  * same way every other tool is: `tools/call` with the asking user's portal token.
@@ -1,4 +1,4 @@
1
- import { serverOf } from "@anchrd/intel-contract";
1
+ import { serverOf } from "@anchrd/intel-contract/tool";
2
2
  import { z } from "zod";
3
3
  /**
4
4
  * The portal's own directory tool. It is an ordinary entry in `tools/list`, so it is reached the
@@ -1,4 +1,3 @@
1
- import { serverOf } from "@anchrd/intel-contract";
2
1
  import { IntelError } from "../shared/intel-error/intel-error.js";
3
2
  import { reportUnexpectedError } from "../shared/report-unexpected-error/report-unexpected-error.js";
4
3
  import { ServerDirectoryTool, toolServersFrom, } from "./tool-servers/tool-servers.js";
@@ -7,18 +6,14 @@ const MaxTools = 1_000;
7
6
  const MaxResultBytes = 1_000_000;
8
7
  // Refresh slightly early so a call cannot start with a token that expires mid-flight.
9
8
  const RefreshWindowMs = 30_000;
10
- /**
11
- * An agent that delegates nothing no servers, or no delegator to act for.
12
- *
13
- * ⚠️ It is answered without touching the token store at all. `delegatedBy` is empty for an archived
14
- * agent, one whose definition Intel could not read, and one that was never given tools; asking the
15
- * store for the connection of user "" would be a lookup that can only ever fail, on a path that has
16
- * already decided the answer.
9
+ /*
10
+ * ⚠️ Until #390 this file answered two kinds of caller: a person, and an agent acting on somebody
11
+ * else's portal connection (D30). The second one is gone with the agents, and with it the
12
+ * delegation lookup, the cut against the portal's server directory, and the audit row that named
13
+ * both principals. What is left is the rule the delegation was an exception to: the token is read
14
+ * per actor and never shared one operator token for everybody would make every catalog the same
15
+ * one and the portal's Access policies decorative (ADR-0003).
17
16
  */
18
- function delegatesNothing(who) {
19
- return (who.delegation !== null &&
20
- (who.delegation.servers.length === 0 || who.delegation.delegatedBy.length === 0));
21
- }
22
17
  export function createTools(deps) {
23
18
  function portal() {
24
19
  if (!deps.portalUrl || !deps.sourceAllowed(deps.portalUrl)) {
@@ -26,23 +21,10 @@ export function createTools(deps) {
26
21
  }
27
22
  return deps.portalUrl;
28
23
  }
29
- // One lookup per request, before anything else happens: everything below has to know whether it
30
- // is answering a person or an agent, and a second lookup could answer differently mid-call.
31
- async function acting(actor) {
32
- const delegation = await deps.delegation(actor.id);
33
- return {
34
- actor,
35
- connectionOf: delegation?.delegatedBy ?? actor.id,
36
- delegation,
37
- };
38
- }
39
24
  // Authorization for tools lives entirely in the portal, so "may this user act" reduces to "does
40
- // this user have a usable portal token". ⚠️ The token is read per actor and never shared: one
41
- // operator token for everybody would make every catalog the same one and the portal's Access
42
- // policies decorative (ADR-0003). For an agent the actor IS somebody else — the delegator — which
43
- // is the whole of D30 and the reason this takes an `Acting` rather than a `ToolActor`.
44
- async function accessToken(who) {
45
- const stored = await deps.tokens.read(who.connectionOf);
25
+ // this user have a usable portal token".
26
+ async function accessToken(actor) {
27
+ const stored = await deps.tokens.read(actor.id);
46
28
  if (!stored) {
47
29
  throw new IntelError(401, "portal_not_connected", "The portal has not signed this user in yet");
48
30
  }
@@ -58,10 +40,10 @@ export function createTools(deps) {
58
40
  // A token that cannot be renewed is dropped: leaving it would keep failing every call with a
59
41
  // stale credential. The browser answers this by signing in silently again (#60); an MCP
60
42
  // client sees the code and repeats its own authorization.
61
- await deps.tokens.clear(who.connectionOf);
43
+ await deps.tokens.clear(actor.id);
62
44
  throw new IntelError(401, "portal_reconnect_required", "The portal sign-in for this user has expired");
63
45
  }
64
- await deps.tokens.write(who.connectionOf, refreshed);
46
+ await deps.tokens.write(actor.id, refreshed);
65
47
  return refreshed.accessToken;
66
48
  }
67
49
  /**
@@ -79,8 +61,8 @@ export function createTools(deps) {
79
61
  * and the reason is in the log" — the same rule `adapters/json-schema` learned the hard way in
80
62
  * #229, where a catch-all that renamed the failure hid it for weeks.
81
63
  */
82
- async function remoteTools(who) {
83
- const token = await accessToken(who);
64
+ async function remoteTools(actor) {
65
+ const token = await accessToken(actor);
84
66
  const remote = await deps.remote
85
67
  .list(portal(), token, AbortSignal.timeout(CallTimeoutMs))
86
68
  .catch((error) => {
@@ -108,13 +90,13 @@ export function createTools(deps) {
108
90
  * be indistinguishable from "you have no servers", and the difference decides whether a screen
109
91
  * says "connect something" or "this portal cannot be delegated from".
110
92
  */
111
- async function serverList(who, toolNames) {
93
+ async function serverList(actor, toolNames) {
112
94
  if (!toolNames.includes(ServerDirectoryTool)) {
113
95
  throw new IntelError(502, "tool_servers_unavailable", `The portal does not offer ${ServerDirectoryTool}, so its servers cannot be listed`);
114
96
  }
115
97
  const answer = await deps.remote.call({
116
98
  url: portal(),
117
- accessToken: await accessToken(who),
99
+ accessToken: await accessToken(actor),
118
100
  name: ServerDirectoryTool,
119
101
  arguments: {},
120
102
  signal: AbortSignal.timeout(CallTimeoutMs),
@@ -128,40 +110,9 @@ export function createTools(deps) {
128
110
  /**
129
111
  * One live tools/list is both the catalog and the authorization answer: the portal only returns
130
112
  * what this user may reach. Nothing here is cached as a permission.
131
- *
132
- * ⚠️ For an agent the list is cut to the delegated servers, and the cut is made against the
133
- * portal's own directory rather than against the tool names — `tool-servers.ts` says why the
134
- * namespace alone is not enough to attribute a tool. The second return value is the attribution
135
- * itself, so the audit below names the server the cut actually used rather than guessing again.
136
113
  */
137
- async function capabilities(who) {
138
- const delegated = who.delegation;
139
- const serverOfTool = new Map();
140
- if (delegatesNothing(who))
141
- return { items: [], serverOfTool };
142
- let remote = await remoteTools(who);
143
- if (delegated) {
144
- const directory = await serverList(who, remote.map((tool) => tool.name));
145
- // ⚠️ Attributed against every identifier the portal DECLARED — including rows it marked
146
- // disabled — and only then checked against the delegation. Attributing against the offerable
147
- // subset (or against the delegation itself) would widen it: with `wiki` delegated and
148
- // `wiki_extra` merely declared, `wiki_extra__read` starts with `wiki_` and would be handed
149
- // over as a `wiki` tool. The longest match over the widest declared set is the only reading
150
- // that no missing row can loosen.
151
- const delegatedSet = new Set(delegated.servers);
152
- // ⚠️ And the attribution is then required to land on a server that is still OFFERABLE — the
153
- // narrow set, enabled and confirmed. Wide for attribution, narrow for permission: a portal
154
- // that switches a server off takes it away from the agent here, which is revocation reaching
155
- // an agent without anybody touching its definition (D30).
156
- const offerable = new Set(directory.servers.map((server) => server.handle));
157
- remote = remote.filter((tool) => {
158
- const handle = serverOf(tool.name, directory.declared);
159
- if (handle === null || !delegatedSet.has(handle) || !offerable.has(handle))
160
- return false;
161
- serverOfTool.set(tool.name, handle);
162
- return true;
163
- });
164
- }
114
+ async function capabilities(actor) {
115
+ const remote = await remoteTools(actor);
165
116
  const items = [];
166
117
  for (const tool of remote) {
167
118
  items.push({
@@ -174,63 +125,27 @@ export function createTools(deps) {
174
125
  }),
175
126
  });
176
127
  }
177
- return { items, serverOfTool };
178
- }
179
- /**
180
- * ⚠️ The refusal that costs nothing. A delegated caller naming a tool outside every delegated
181
- * namespace is turned away here — before the token store, before `tools/list`, before the portal
182
- * hears anything at all. The authoritative cut still happens in `capabilities`, against the
183
- * portal's directory; this one exists so the portal is never touched on behalf of a call that was
184
- * always going to be refused, and so the refusal can name the tool and the servers (D30).
185
- */
186
- function requireDelegated(who, name) {
187
- const delegated = who.delegation;
188
- if (!delegated)
189
- return;
190
- if (delegatesNothing(who)) {
191
- throw new IntelError(403, "tool_not_delegated", `${name} is not available: this agent has no delegated MCP servers`);
192
- }
193
- if (serverOf(name, delegated.servers) === null) {
194
- throw new IntelError(403, "tool_not_delegated", `${name} is not part of this agent's delegated servers (${delegated.servers.join(", ")})`);
195
- }
128
+ return items;
196
129
  }
197
- async function requireCapability(who, name) {
198
- const reachable = await capabilities(who);
199
- const found = reachable.items.find((capability) => capability.name === name);
130
+ async function requireCapability(actor, name) {
131
+ const found = (await capabilities(actor)).find((capability) => capability.name === name);
200
132
  if (!found) {
201
133
  throw new IntelError(404, "tool_not_available", `Tool ${name} is not available to you`);
202
134
  }
203
- return { capability: found, server: reachable.serverOfTool.get(name) };
135
+ return found;
204
136
  }
205
- async function call(who, input) {
206
- if (!who.actor.canExecute) {
137
+ async function call(actor, input) {
138
+ if (!actor.canExecute) {
207
139
  throw new IntelError(403, "tool_execute_forbidden", "Tool execution permission is required");
208
140
  }
209
- requireDelegated(who, input.name);
210
- const { capability, server } = await requireCapability(who, input.name);
141
+ const capability = await requireCapability(actor, input.name);
211
142
  const validation = deps.validate(capability.inputSchema, input.arguments);
212
143
  if (!validation.valid) {
213
144
  throw new IntelError(400, "tool_arguments_invalid", validation.detail ?? "Invalid arguments");
214
145
  }
215
- // ⚠️ Written before the call, not after it: an audit that only records what succeeded is a
216
- // record of the harmless half. Both principals are named — the agent that acted and the person
217
- // whose connection carried it (D30) — and no argument is.
218
- //
219
- // ⚠️ The server is the one the CUT used, carried out of `capabilities`, not a second guess made
220
- // against the delegated handles. Re-deriving it here would name `wiki` for a tool the cut
221
- // attributed to `wiki_extra`, and an audit row that names the wrong system is worse than none.
222
- if (who.delegation) {
223
- await deps.audit({
224
- agentId: who.delegation.agentId,
225
- applicationId: who.actor.id,
226
- delegatedBy: who.delegation.delegatedBy,
227
- server: server ?? "",
228
- tool: capability.name,
229
- });
230
- }
231
146
  const result = await deps.remote.call({
232
147
  url: portal(),
233
- accessToken: await accessToken(who),
148
+ accessToken: await accessToken(actor),
234
149
  name: capability.name,
235
150
  arguments: input.arguments,
236
151
  signal: AbortSignal.timeout(CallTimeoutMs),
@@ -250,23 +165,13 @@ export function createTools(deps) {
250
165
  }
251
166
  return {
252
167
  async catalog(actor) {
253
- const who = await acting(actor);
254
- if (delegatesNothing(who))
255
- return { portalConnected: true, items: [] };
256
- const stored = await deps.tokens.read(who.connectionOf);
168
+ const stored = await deps.tokens.read(actor.id);
257
169
  // No portal sign-in yet is a normal state, not an error: the browser answers it by running
258
- // the silent sign-in and asking again (#60). For an agent it is the ordinary shape of
259
- // revocation — the delegator disconnected, so the agent reaches nothing (D30).
170
+ // the silent sign-in and asking again (#60).
260
171
  if (!stored)
261
172
  return { portalConnected: false, items: [] };
262
173
  try {
263
- const { items, serverOfTool } = await capabilities(who);
264
- // ⚠️ Named only for a delegated caller, and it costs nothing there: the attribution had to
265
- // be made anyway to cut the list. For an ordinary user it would mean a second portal
266
- // request per call — `serverList` is not on their path — and their screen never asks the
267
- // question. The runtime's is the one that does (#289).
268
- const reached = who.delegation ? [...new Set(serverOfTool.values())].sort() : undefined;
269
- return { portalConnected: true, items, ...(reached ? { reached } : {}) };
174
+ return { portalConnected: true, items: await capabilities(actor) };
270
175
  }
271
176
  catch (error) {
272
177
  if (disconnected(error))
@@ -275,25 +180,13 @@ export function createTools(deps) {
275
180
  }
276
181
  },
277
182
  async servers(actor) {
278
- const who = await acting(actor);
279
- if (delegatesNothing(who))
280
- return { portalConnected: true, items: [] };
281
- const stored = await deps.tokens.read(who.connectionOf);
183
+ const stored = await deps.tokens.read(actor.id);
282
184
  if (!stored)
283
185
  return { portalConnected: false, items: [] };
284
186
  try {
285
- const remote = await remoteTools(who);
286
- const { servers } = await serverList(who, remote.map((tool) => tool.name));
287
- // ⚠️ An agent sees its own delegation, never the delegator's whole shelf. Its catalog is cut
288
- // anyway, so the uncut list would grant nothing — it would only tell an agent, and through
289
- // it a model, which other systems the person it acts for is connected to.
290
- const delegated = who.delegation;
291
- return {
292
- portalConnected: true,
293
- items: delegated
294
- ? servers.filter((server) => delegated.servers.includes(server.handle))
295
- : servers,
296
- };
187
+ const remote = await remoteTools(actor);
188
+ const { servers } = await serverList(actor, remote.map((tool) => tool.name));
189
+ return { portalConnected: true, items: servers };
297
190
  }
298
191
  catch (error) {
299
192
  if (disconnected(error))
@@ -301,23 +194,19 @@ export function createTools(deps) {
301
194
  throw error;
302
195
  }
303
196
  },
304
- execute: async (actor, input) => await call(await acting(actor), input),
197
+ execute: async (actor, input) => await call(actor, input),
305
198
  async test(actor, input) {
306
- const who = await acting(actor);
307
- requireDelegated(who, input.name);
308
- const { capability } = await requireCapability(who, input.name);
199
+ const capability = await requireCapability(actor, input.name);
309
200
  if (capability.annotations.readOnlyHint !== true ||
310
201
  capability.annotations.destructiveHint === true) {
311
202
  throw new IntelError(409, "tool_test_unsafe", "Only explicitly read-only, non-destructive tools can run in the test surface");
312
203
  }
313
- return await call(who, input);
204
+ return await call(actor, input);
314
205
  },
315
206
  async unavailable(actor, names) {
316
207
  if (names.length === 0)
317
208
  return [];
318
- const who = await acting(actor);
319
- const reachable = await capabilities(who);
320
- const available = new Set(reachable.items.map((capability) => capability.name));
209
+ const available = new Set((await capabilities(actor)).map((capability) => capability.name));
321
210
  return names.filter((name) => !available.has(name));
322
211
  },
323
212
  };
@@ -1,4 +1,4 @@
1
- import type { ExecuteToolInput, TestToolInput, ToolCapability, ToolCatalog, ToolServerCatalog, ToolTestResult } from "@anchrd/intel-contract";
1
+ import type { ExecuteToolInput, TestToolInput, ToolCapability, ToolCatalog, ToolServerCatalog, ToolTestResult } from "@anchrd/intel-contract/tool";
2
2
  export interface ToolActor {
3
3
  id: string;
4
4
  email: string;
@@ -35,25 +35,6 @@ export interface RemoteTools {
35
35
  signal: AbortSignal;
36
36
  }): Promise<ToolTestResult>;
37
37
  }
38
- /**
39
- * What an agent's definition delegates, resolved from the Gate Application the caller authenticated
40
- * as (D30). `null` from the port means "this caller is not an agent" — the ordinary user path.
41
- *
42
- * ⚠️ Read fresh on every call and never cached as a permission. It is the same rule the catalog
43
- * follows: the answer has to be able to change between two runs without anybody editing anything.
44
- */
45
- export interface ToolDelegation {
46
- agentId: string;
47
- delegatedBy: string;
48
- servers: string[];
49
- }
50
- export interface ToolAuditEvent {
51
- agentId: string;
52
- applicationId: string;
53
- delegatedBy: string;
54
- server: string;
55
- tool: string;
56
- }
57
38
  export interface ToolDeps {
58
39
  portalUrl: string | null;
59
40
  remote: RemoteTools;
@@ -66,8 +47,6 @@ export interface ToolDeps {
66
47
  valid: boolean;
67
48
  detail?: string;
68
49
  };
69
- delegation(applicationId: string): Promise<ToolDelegation | null>;
70
- audit(event: ToolAuditEvent): Promise<void>;
71
50
  }
72
51
  export interface ToolService {
73
52
  catalog(actor: ToolActor): Promise<ToolCatalog>;