@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.
- package/README.md +63 -3
- package/dist/adapters/cloudflare/cloudflare.js +102 -37
- package/dist/adapters/cloudflare/cloudflare.types.d.ts +20 -0
- package/dist/adapters/content/content.d.ts +1 -1
- package/dist/adapters/db/db-flows.js +148 -20
- package/dist/adapters/db/db-grants.d.ts +13 -2
- package/dist/adapters/db/db-grants.js +25 -8
- package/dist/adapters/db/db-indexing.d.ts +2 -2
- package/dist/adapters/db/db-indexing.js +26 -19
- package/dist/adapters/db/db.d.ts +3 -3
- package/dist/adapters/db/db.js +442 -118
- package/dist/adapters/gate-applications/gate-applications.d.ts +23 -0
- package/dist/adapters/gate-applications/gate-applications.js +88 -0
- package/dist/adapters/index-queue/index-queue.d.ts +1 -1
- package/dist/adapters/index-queue/index-queue.js +2 -2
- package/dist/adapters/semantic-index/semantic-index.types.d.ts +2 -2
- package/dist/adapters/tool-delegation/tool-delegation.d.ts +22 -0
- package/dist/adapters/tool-delegation/tool-delegation.js +90 -0
- package/dist/agent-runtime/agent-runtime.d.ts +16 -0
- package/dist/agent-runtime/agent-runtime.js +150 -0
- package/dist/agent-runtime/agent-runtime.types.d.ts +122 -0
- package/dist/bundle/bundle.d.ts +4 -0
- package/dist/bundle/bundle.js +1048 -0
- package/dist/bundle/bundle.types.d.ts +33 -0
- package/dist/bundle/bundle.types.js +1 -0
- package/dist/cli/cli.js +10 -1
- package/dist/flows/flows.d.ts +8 -8
- package/dist/flows/flows.js +158 -42
- package/dist/flows/flows.types.d.ts +40 -7
- package/dist/http/http.d.ts +1 -0
- package/dist/http/http.js +348 -61
- package/dist/http/http.types.d.ts +6 -2
- package/dist/indexing/indexing.js +14 -2
- package/dist/indexing/indexing.types.d.ts +2 -2
- package/dist/intel/intel.js +12 -3
- package/dist/intel/intel.types.d.ts +6 -2
- package/dist/mcp/mcp.js +519 -124
- package/dist/mcp/mcp.types.d.ts +11 -2
- package/dist/nodes/nodes.d.ts +2 -0
- package/dist/nodes/nodes.js +1466 -0
- package/dist/nodes/nodes.types.d.ts +402 -0
- package/dist/nodes/nodes.types.js +1 -0
- package/dist/tools/tool-servers/tool-servers.d.ts +46 -0
- package/dist/tools/tool-servers/tool-servers.js +114 -0
- package/dist/tools/tools.js +190 -31
- package/dist/tools/tools.types.d.ts +23 -1
- package/migrations/0011_one_name_for_the_tree.sql +53 -0
- package/migrations/0012_table_snapshots.sql +29 -0
- package/migrations/0013_agents_in_the_tree.sql +76 -0
- package/migrations/0014_agent_applications.sql +25 -0
- package/migrations/0015_tools_delegated_from_a_connection.sql +15 -0
- package/package.json +3 -2
- package/dist/knowledge/knowledge.d.ts +0 -2
- package/dist/knowledge/knowledge.js +0 -761
- package/dist/knowledge/knowledge.types.d.ts +0 -198
- /package/dist/{knowledge/knowledge.types.js → agent-runtime/agent-runtime.types.js} +0 -0
- /package/dist/{knowledge → nodes}/document-links/document-links.d.ts +0 -0
- /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
|
+
}
|