@anchrd/intel-api 0.14.0 → 0.15.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.
- package/dist/adapters/cloudflare/cloudflare.js +0 -68
- package/dist/adapters/cloudflare/cloudflare.types.d.ts +0 -39
- package/dist/adapters/db/db-flows.js +1 -1
- package/dist/adapters/db/db-grants.js +1 -1
- package/dist/adapters/db/db.js +16 -113
- package/dist/bundle/bundle.js +28 -141
- package/dist/cli/cli.js +3 -9
- package/dist/http/http.js +1 -206
- package/dist/http/http.types.d.ts +0 -8
- package/dist/indexing/indexing.js +18 -89
- package/dist/intel/intel.js +4 -9
- package/dist/intel/intel.types.d.ts +0 -6
- package/dist/mcp/mcp.js +3 -292
- package/dist/mcp/mcp.types.d.ts +2 -7
- package/dist/nodes/document-links/document-links.d.ts +6 -8
- package/dist/nodes/document-links/document-links.js +8 -31
- package/dist/nodes/nodes.js +20 -886
- package/dist/nodes/nodes.types.d.ts +10 -169
- package/dist/tools/tools.js +37 -148
- package/dist/tools/tools.types.d.ts +0 -21
- package/migrations/0018_no_context_policy_at_last.sql +13 -6
- package/migrations/0019_one_name_for_the_grants.sql +52 -0
- package/package.json +2 -2
- package/dist/adapters/cloudflare-api/cloudflare-api.d.ts +0 -22
- package/dist/adapters/cloudflare-api/cloudflare-api.js +0 -306
- package/dist/adapters/cloudflare-api/cloudflare-api.types.d.ts +0 -64
- package/dist/adapters/cloudflare-api/cloudflare-api.types.js +0 -1
- package/dist/adapters/gate-applications/gate-applications.d.ts +0 -23
- package/dist/adapters/gate-applications/gate-applications.js +0 -88
- package/dist/adapters/tool-delegation/tool-delegation.d.ts +0 -22
- package/dist/adapters/tool-delegation/tool-delegation.js +0 -90
- package/dist/agent-costs/agent-costs.d.ts +0 -16
- package/dist/agent-costs/agent-costs.js +0 -105
- package/dist/agent-costs/agent-costs.types.d.ts +0 -30
- package/dist/agent-costs/agent-costs.types.js +0 -1
- package/dist/agent-runtime/agent-runtime.d.ts +0 -16
- package/dist/agent-runtime/agent-runtime.js +0 -150
- package/dist/agent-runtime/agent-runtime.types.d.ts +0 -122
- package/dist/agent-runtime/agent-runtime.types.js +0 -1
- package/dist/model-catalog/model-catalog.d.ts +0 -2
- package/dist/model-catalog/model-catalog.js +0 -99
- package/dist/model-catalog/model-catalog.types.d.ts +0 -15
- package/dist/model-catalog/model-catalog.types.js +0 -1
- package/dist/nodes/board/board.d.ts +0 -61
- package/dist/nodes/board/board.js +0 -826
- package/dist/nodes/board/board.types.d.ts +0 -38
- package/dist/nodes/board/board.types.js +0 -1
- package/migrations/0013_agents_in_the_tree.sql +0 -76
- package/migrations/0014_agent_applications.sql +0 -25
- package/migrations/0015_tools_delegated_from_a_connection.sql +0 -15
- package/migrations/0016_boards_in_the_tree.sql +0 -80
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AppendTableRowsInput, AppendTableRowsResult, ArchiveNodeInput, CreateNodeInput, DefineTableInput, DeleteTableRowsInput, DeleteTableRowsResult, Flow, FlowVersion, ListNodesInput, Node, NodeAttachment, NodeCitation, NodeDocument, NodeGraph, NodeGraphInput, NodeLink, NodeTable, NodeVersion, RedefineTableInput, ResolveNodeLinksInput, ResolveNodeLinksResult, ResourceGrant, ResourceGrantList, ResourceVerb, RevokeGrantInput, SaveAttachmentInput, SaveNodeVersionInput, SearchInput, ShareInput, ShareResult, UpdateNodeInput, UpdateTableRowsInput, UpdateTableRowsResult } from "@anchrd/intel-contract";
|
|
2
2
|
import type { SemanticIndex } from "../adapters/semantic-index/semantic-index.types.js";
|
|
3
3
|
export interface Actor {
|
|
4
4
|
id: string;
|
|
@@ -10,9 +10,6 @@ export interface NewNode {
|
|
|
10
10
|
actorId: string;
|
|
11
11
|
idempotencyKey: string;
|
|
12
12
|
auditId: string;
|
|
13
|
-
application?: {
|
|
14
|
-
id: string;
|
|
15
|
-
} | undefined;
|
|
16
13
|
}
|
|
17
14
|
export interface NewNodeVersion {
|
|
18
15
|
version: NodeVersion;
|
|
@@ -27,7 +24,7 @@ export interface NewTableVersion {
|
|
|
27
24
|
idempotencyKey: string;
|
|
28
25
|
auditId: string;
|
|
29
26
|
}
|
|
30
|
-
export type SnapshotOperation = "node.table_update" | "node.table_delete" | "node.table_redefine"
|
|
27
|
+
export type SnapshotOperation = "node.table_update" | "node.table_delete" | "node.table_redefine";
|
|
31
28
|
/**
|
|
32
29
|
* One version that replaces the readable state, written only against the state it replaces.
|
|
33
30
|
*
|
|
@@ -35,11 +32,6 @@ export type SnapshotOperation = "node.table_update" | "node.table_delete" | "nod
|
|
|
35
32
|
* read, so it has to know which state it replaces — the same optimistic concurrency the document
|
|
36
33
|
* save uses, enforced in the same statement that inserts the row (#135).
|
|
37
34
|
*
|
|
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
35
|
*/
|
|
44
36
|
export interface NewSnapshotVersion {
|
|
45
37
|
version: NodeVersion;
|
|
@@ -59,8 +51,6 @@ export interface NodeRepository {
|
|
|
59
51
|
parentId: string | null;
|
|
60
52
|
limit: number;
|
|
61
53
|
}): Promise<BoundedChildren>;
|
|
62
|
-
listVisibleAgents(actor: Actor, input: ListAgentsInput): Promise<Node[]>;
|
|
63
|
-
listVisibleAgentDefinitionKeys(actor: Actor, folderId: string): Promise<string[]>;
|
|
64
54
|
getVisible(actor: Actor, nodeId: string): Promise<Node | null>;
|
|
65
55
|
listVisibleSubtree(actor: Actor, rootId: string | null): Promise<SubtreeNode[]>;
|
|
66
56
|
can(actor: Actor, nodeId: string, verb: ResourceVerb): Promise<boolean>;
|
|
@@ -68,27 +58,6 @@ export interface NodeRepository {
|
|
|
68
58
|
findIdempotentRevocation(actorId: string, idempotencyKey: string): Promise<boolean | null>;
|
|
69
59
|
findSnapshotMetadata(actorId: string, operation: SnapshotOperation, idempotencyKey: string): Promise<Record<string, unknown> | null>;
|
|
70
60
|
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
61
|
getVersion(versionId: string): Promise<NodeVersion | null>;
|
|
93
62
|
appendVersion(input: NewNodeVersion): Promise<"saved" | "conflict">;
|
|
94
63
|
appendTableVersion(input: NewTableVersion): Promise<NodeVersion>;
|
|
@@ -207,90 +176,12 @@ export interface NodeAttachmentBody {
|
|
|
207
176
|
attachment: NodeAttachment;
|
|
208
177
|
body: ReadableStream<Uint8Array>;
|
|
209
178
|
}
|
|
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
179
|
export interface GateCaller {
|
|
255
180
|
token: string;
|
|
256
181
|
}
|
|
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<{
|
|
@@ -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"
|
|
287
|
+
kind: "document" | "attachment" | "table";
|
|
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
|
|
453
|
-
*
|
|
454
|
-
*
|
|
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
|
|
465
|
-
*
|
|
466
|
-
*
|
|
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;
|
package/dist/tools/tools.js
CHANGED
|
@@ -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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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".
|
|
41
|
-
|
|
42
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
83
|
-
const token = await accessToken(
|
|
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(
|
|
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(
|
|
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(
|
|
138
|
-
const
|
|
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
|
|
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(
|
|
198
|
-
const
|
|
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
|
|
135
|
+
return found;
|
|
204
136
|
}
|
|
205
|
-
async function call(
|
|
206
|
-
if (!
|
|
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
|
-
|
|
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(
|
|
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
|
|
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).
|
|
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
|
-
|
|
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
|
|
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(
|
|
286
|
-
const { servers } = await serverList(
|
|
287
|
-
|
|
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(
|
|
197
|
+
execute: async (actor, input) => await call(actor, input),
|
|
305
198
|
async test(actor, input) {
|
|
306
|
-
const
|
|
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(
|
|
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
|
|
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
|
};
|
|
@@ -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>;
|
|
@@ -9,10 +9,15 @@
|
|
|
9
9
|
-- ⚠️ THE reason this ticket sat open for weeks is that three earlier attempts failed against
|
|
10
10
|
-- `--remote` while passing locally, and the file that recorded them (`0009_no_context_policy.sql`)
|
|
11
11
|
-- concluded that a table rebuild with foreign keys needs "a session that runs the transaction
|
|
12
|
-
-- itself, not a migration file". That conclusion is out of date
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
12
|
+
-- itself, not a migration file". That conclusion is out of date: `0005`, `0013` and `0016` each
|
|
13
|
+
-- rebuilt this very table through an ordinary migration file, and `0016` did it against the live
|
|
14
|
+
-- database on 2026-08-08 with 120 nodes, every referencing table coming out with the count it went
|
|
15
|
+
-- in with.
|
|
16
|
+
--
|
|
17
|
+
-- ⚠️ `0013` and `0016` are GONE since #392 — they were the Agent and Board rebuilds, and the
|
|
18
|
+
-- feature was parked (#385). What they worked out is not gone with them: the recipe is in
|
|
19
|
+
-- `packages/api/CLAUDE.md`, which is where the next rebuild reads it, and the three points below
|
|
20
|
+
-- are this file's own copy of it.
|
|
16
21
|
--
|
|
17
22
|
-- ⚠️ One thing about that run is worth knowing before it is quoted as a precedent: the installation
|
|
18
23
|
-- held ZERO `node_links` rows, so the rescue below would have had nothing to rescue and the run
|
|
@@ -34,7 +39,9 @@
|
|
|
34
39
|
-- 3. `DROP TABLE` on a parent runs an implicit `DELETE FROM` first, and `node_links` is the one
|
|
35
40
|
-- child declared ON DELETE CASCADE — so that delete does not merely flag its rows, it REMOVES
|
|
36
41
|
-- them. They are carried out of the way and put back. That is a rescue, not a decision about
|
|
37
|
-
-- the data.
|
|
42
|
+
-- the data. ⚠️ The list of children is in `packages/api/CLAUDE.md` and is READ FROM THE
|
|
43
|
+
-- DATABASE, not from these files: `0011` renamed the tree and SQLite rewrote the `REFERENCES`
|
|
44
|
+
-- clauses of tables older than it, so their current shape is written down nowhere here.
|
|
38
45
|
--
|
|
39
46
|
-- ⚠️ On an empty database neither detour is visible, because nothing points at anything. That is
|
|
40
47
|
-- how `0005`'s first version passed a green suite and then failed against the first database with
|
|
@@ -56,7 +63,7 @@ DROP TABLE nodes;
|
|
|
56
63
|
CREATE TABLE nodes (
|
|
57
64
|
id TEXT PRIMARY KEY NOT NULL,
|
|
58
65
|
parent_id TEXT REFERENCES nodes(id),
|
|
59
|
-
kind TEXT NOT NULL CHECK (kind IN ('folder', 'document', 'attachment', 'table'
|
|
66
|
+
kind TEXT NOT NULL CHECK (kind IN ('folder', 'document', 'attachment', 'table')),
|
|
60
67
|
title TEXT NOT NULL CHECK (length(title) BETWEEN 1 AND 240),
|
|
61
68
|
description TEXT CHECK (description IS NULL OR length(description) <= 2000),
|
|
62
69
|
owner_id TEXT NOT NULL,
|