@anchrd/intel-api 0.3.1 → 0.3.2
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 +11 -12
- package/dist/adapters/db/db-flows.js +367 -193
- package/dist/adapters/db/db-grants.d.ts +47 -0
- package/dist/adapters/db/db-grants.js +125 -0
- package/dist/adapters/db/db-indexing.js +14 -3
- package/dist/adapters/db/db.js +236 -202
- package/dist/flows/flows.d.ts +31 -1
- package/dist/flows/flows.js +939 -72
- package/dist/flows/flows.types.d.ts +92 -28
- package/dist/http/http.js +130 -53
- package/dist/indexing/indexing.js +14 -3
- package/dist/knowledge/document-links/document-links.d.ts +15 -0
- package/dist/knowledge/document-links/document-links.js +52 -0
- package/dist/knowledge/knowledge.js +347 -74
- package/dist/knowledge/knowledge.types.d.ts +58 -27
- package/dist/mcp/mcp.js +153 -71
- package/dist/shared/csv/csv.d.ts +13 -0
- package/dist/shared/csv/csv.js +85 -0
- package/dist/shared/gate-authorization/gate-authorization.d.ts +1 -0
- package/dist/shared/gate-authorization/gate-authorization.js +7 -0
- package/migrations/0003_folder_permissions.sql +211 -0
- package/migrations/0004_subflow_runs.sql +14 -0
- package/migrations/0005_flow_node_cleanup.sql +28 -0
- package/migrations/0005_tables_in_the_knowledge_tree.sql +39 -0
- package/migrations/0006_links_are_written_in_the_text.sql +20 -0
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AppendKnowledgeTableRowsInput, AppendKnowledgeTableRowsResult, ArchiveKnowledgeNodeInput, CreateKnowledgeNodeInput, DefineKnowledgeTableInput, KnowledgeAttachment, KnowledgeCitation, KnowledgeDocument, KnowledgeGraph, KnowledgeGraphInput, KnowledgeLink, KnowledgeNode, KnowledgeTable, KnowledgeVersion, ListKnowledgeNodesInput, ResolveKnowledgeLinksInput, ResolveKnowledgeLinksResult, ResourceGrant, ResourceGrantList, ResourceVerb, RevokeKnowledgeGrantInput, SaveKnowledgeAttachmentInput, SaveKnowledgeVersionInput, SearchKnowledgeInput, ShareKnowledgeInput, ShareKnowledgeResult, UpdateKnowledgeNodeInput } 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;
|
|
5
5
|
email: string;
|
|
6
|
-
|
|
6
|
+
isAdmin?: boolean;
|
|
7
7
|
}
|
|
8
8
|
export interface NewKnowledgeNode {
|
|
9
9
|
node: KnowledgeNode;
|
|
@@ -18,16 +18,28 @@ export interface NewKnowledgeVersion {
|
|
|
18
18
|
idempotencyKey: string;
|
|
19
19
|
auditId: string;
|
|
20
20
|
}
|
|
21
|
+
export interface NewKnowledgeTableVersion {
|
|
22
|
+
version: Omit<KnowledgeVersion, "sequence">;
|
|
23
|
+
actorId: string;
|
|
24
|
+
idempotencyKey: string;
|
|
25
|
+
auditId: string;
|
|
26
|
+
}
|
|
21
27
|
export interface KnowledgeRepository {
|
|
22
28
|
listVisible(actor: Actor, input: ListKnowledgeNodesInput): Promise<KnowledgeNode[]>;
|
|
29
|
+
listVisibleBounded(actor: Actor, input: {
|
|
30
|
+
parentId: string | null;
|
|
31
|
+
limit: number;
|
|
32
|
+
}): Promise<BoundedChildren>;
|
|
23
33
|
getVisible(actor: Actor, nodeId: string): Promise<KnowledgeNode | null>;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
findIdempotentNode(actorId: string, operation: "knowledge.create" | "knowledge.save" | "knowledge.update" | "knowledge.archive" | "knowledge.link" | "knowledge.unlink" | "knowledge.share" | "knowledge.revoke", idempotencyKey: string): Promise<string | null>;
|
|
34
|
+
can(actor: Actor, nodeId: string, verb: ResourceVerb): Promise<boolean>;
|
|
35
|
+
findIdempotentNode(actorId: string, operation: "knowledge.create" | "knowledge.save" | "knowledge.append" | "knowledge.update" | "knowledge.archive" | "knowledge.share" | "knowledge.revoke", idempotencyKey: string): Promise<string | null>;
|
|
27
36
|
findIdempotentRevocation(actorId: string, idempotencyKey: string): Promise<boolean | null>;
|
|
28
37
|
insertNode(input: NewKnowledgeNode): Promise<KnowledgeNode>;
|
|
29
38
|
getVersion(versionId: string): Promise<KnowledgeVersion | null>;
|
|
30
39
|
appendVersion(input: NewKnowledgeVersion): Promise<"saved" | "conflict">;
|
|
40
|
+
appendTableVersion(input: NewKnowledgeTableVersion): Promise<KnowledgeVersion>;
|
|
41
|
+
listVersionContentKeys(nodeId: string): Promise<string[]>;
|
|
42
|
+
firstVersionContentKey(nodeId: string): Promise<string | null>;
|
|
31
43
|
listVersions(nodeId: string): Promise<KnowledgeVersion[]>;
|
|
32
44
|
updateNode(input: {
|
|
33
45
|
node: KnowledgeNode;
|
|
@@ -46,23 +58,22 @@ export interface KnowledgeRepository {
|
|
|
46
58
|
auditId: string;
|
|
47
59
|
}): Promise<"conflict" | KnowledgeNode>;
|
|
48
60
|
listGrants(resourceId: string): Promise<ResourceGrant[]>;
|
|
61
|
+
organizationExecuteReaches(nodeId: string, exceptGrantId: string): Promise<boolean>;
|
|
49
62
|
listLinksVisible(actor: Actor, nodeId: string): Promise<KnowledgeLink[]>;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
idempotencyKey: string;
|
|
56
|
-
auditId: string;
|
|
57
|
-
}): Promise<KnowledgeLink>;
|
|
58
|
-
deleteLink(input: {
|
|
63
|
+
resolveVisibleTitles(actor: Actor, nodeIds: string[]): Promise<Array<{
|
|
64
|
+
nodeId: string;
|
|
65
|
+
title: string;
|
|
66
|
+
}>>;
|
|
67
|
+
replaceTextLinks(input: {
|
|
59
68
|
sourceNodeId: string;
|
|
60
|
-
|
|
69
|
+
links: Array<{
|
|
70
|
+
id: string;
|
|
71
|
+
targetNodeId: string;
|
|
72
|
+
}>;
|
|
61
73
|
actorId: string;
|
|
62
|
-
idempotencyKey: string;
|
|
63
74
|
auditId: string;
|
|
64
75
|
occurredAt: string;
|
|
65
|
-
}): Promise<
|
|
76
|
+
}): Promise<void>;
|
|
66
77
|
graphVisible(actor: Actor, input: KnowledgeGraphInput): Promise<KnowledgeGraph>;
|
|
67
78
|
setGrant(input: {
|
|
68
79
|
grant: ResourceGrant;
|
|
@@ -102,6 +113,11 @@ export interface KnowledgeDeps {
|
|
|
102
113
|
content: ContentStore;
|
|
103
114
|
id(): string;
|
|
104
115
|
now(): Date;
|
|
116
|
+
externalFlowCallers(actor: Actor, folderId: string): Promise<{
|
|
117
|
+
visible: string[];
|
|
118
|
+
hidden: number;
|
|
119
|
+
}>;
|
|
120
|
+
flowKnowledgeReferences(actor: Actor, folderId: string): Promise<string[]>;
|
|
105
121
|
hash(content: string | Uint8Array): Promise<string>;
|
|
106
122
|
indexing: {
|
|
107
123
|
enqueue(versionId: string): Promise<void>;
|
|
@@ -109,15 +125,34 @@ export interface KnowledgeDeps {
|
|
|
109
125
|
semantic?: SemanticIndex | undefined;
|
|
110
126
|
}
|
|
111
127
|
export type FolderAccess = "ok" | "missing" | "not-a-folder" | "forbidden";
|
|
128
|
+
export interface BoundedChildren {
|
|
129
|
+
items: KnowledgeNode[];
|
|
130
|
+
total: number;
|
|
131
|
+
}
|
|
112
132
|
export interface KnowledgeService {
|
|
113
133
|
list(actor: Actor, input: ListKnowledgeNodesInput): Promise<{
|
|
114
134
|
items: KnowledgeNode[];
|
|
115
135
|
}>;
|
|
136
|
+
/**
|
|
137
|
+
* One level of the tree as this actor may see it, bounded: at most `limit` current children, and
|
|
138
|
+
* `total` for how many there are. Its caller is the relation graph, which draws a bounded picture
|
|
139
|
+
* and must not read a folder of a thousand documents to do it (#30).
|
|
140
|
+
*
|
|
141
|
+
* ⚠️ `total` counts what passed the visibility predicate and nothing else, or the size a picture
|
|
142
|
+
* reports would become a way of learning that something is filed here.
|
|
143
|
+
*/
|
|
144
|
+
childrenBounded(actor: Actor, input: {
|
|
145
|
+
parentId: string | null;
|
|
146
|
+
limit: number;
|
|
147
|
+
}): Promise<BoundedChildren>;
|
|
116
148
|
get(actor: Actor, nodeId: string): Promise<KnowledgeDocument>;
|
|
117
149
|
folderAccess(actor: Actor, folderId: string): Promise<FolderAccess>;
|
|
118
150
|
create(actor: Actor, input: CreateKnowledgeNodeInput): Promise<KnowledgeNode>;
|
|
119
151
|
save(actor: Actor, input: SaveKnowledgeVersionInput): Promise<KnowledgeDocument>;
|
|
120
152
|
saveAttachment(actor: Actor, input: SaveKnowledgeAttachmentInput): Promise<KnowledgeDocument>;
|
|
153
|
+
getTable(actor: Actor, nodeId: string): Promise<KnowledgeTable>;
|
|
154
|
+
defineTable(actor: Actor, input: DefineKnowledgeTableInput): Promise<KnowledgeTable>;
|
|
155
|
+
appendTableRows(actor: Actor, input: AppendKnowledgeTableRowsInput): Promise<AppendKnowledgeTableRowsResult>;
|
|
121
156
|
getAttachment(actor: Actor, nodeId: string): Promise<KnowledgeAttachment>;
|
|
122
157
|
readAttachment(actor: Actor, nodeId: string): Promise<KnowledgeAttachmentBody>;
|
|
123
158
|
listVersions(actor: Actor, nodeId: string): Promise<{
|
|
@@ -125,18 +160,14 @@ export interface KnowledgeService {
|
|
|
125
160
|
}>;
|
|
126
161
|
update(actor: Actor, input: UpdateKnowledgeNodeInput): Promise<KnowledgeNode>;
|
|
127
162
|
archive(actor: Actor, input: ArchiveKnowledgeNodeInput): Promise<KnowledgeNode>;
|
|
128
|
-
listGrants(actor: Actor, resourceId: string): Promise<
|
|
129
|
-
items: ResourceGrant[];
|
|
130
|
-
}>;
|
|
163
|
+
listGrants(actor: Actor, resourceId: string): Promise<ResourceGrantList>;
|
|
131
164
|
listLinks(actor: Actor, nodeId: string): Promise<{
|
|
132
165
|
items: KnowledgeLink[];
|
|
133
166
|
}>;
|
|
134
|
-
|
|
135
|
-
deleteLink(actor: Actor, input: DeleteKnowledgeLinkInput): Promise<{
|
|
136
|
-
deleted: boolean;
|
|
137
|
-
}>;
|
|
167
|
+
resolveLinks(actor: Actor, input: ResolveKnowledgeLinksInput): Promise<ResolveKnowledgeLinksResult>;
|
|
138
168
|
graph(actor: Actor, input: KnowledgeGraphInput): Promise<KnowledgeGraph>;
|
|
139
|
-
|
|
169
|
+
visibleNode(actor: Actor, nodeId: string): Promise<KnowledgeNode | null>;
|
|
170
|
+
share(actor: Actor, input: ShareKnowledgeInput): Promise<ShareKnowledgeResult>;
|
|
140
171
|
revokeGrant(actor: Actor, input: RevokeKnowledgeGrantInput): Promise<{
|
|
141
172
|
revoked: boolean;
|
|
142
173
|
}>;
|
|
@@ -151,9 +182,9 @@ export interface KnowledgeIndexTarget {
|
|
|
151
182
|
nodeId: string;
|
|
152
183
|
versionId: string;
|
|
153
184
|
title: string;
|
|
154
|
-
|
|
185
|
+
contentKeys: string[];
|
|
155
186
|
mediaType: string;
|
|
156
|
-
kind: "document" | "attachment";
|
|
187
|
+
kind: "document" | "attachment" | "table";
|
|
157
188
|
updatedAt: string;
|
|
158
189
|
}
|
|
159
190
|
export interface KnowledgeIndexRepository {
|
package/dist/mcp/mcp.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { ArchiveKnowledgeNodeInput, CompleteFlowRunStepInput, CreateFlowInput,
|
|
1
|
+
import { AppendKnowledgeTableRowsInput, ArchiveKnowledgeNodeInput, CompleteFlowRunStepInput, CreateFlowInput, CreateKnowledgeNodeInput, DefineKnowledgeTableInput, ExecuteToolInput, GetFlowInput, GetFlowRunInput, GetKnowledgeNodeInput, GetKnowledgeTableInput, KnowledgeGraphInput, ListFlowRunsInput, ListFlowsInput, ListKnowledgeGrantsInput, ListKnowledgeNodesInput, PreviewFlowPublishInput, PublishFlowInput, RelationGraphInput, ResolveKnowledgeLinksInput, RevokeKnowledgeGrantInput, SaveFlowVersionInput, SaveKnowledgeAttachmentInput, SaveKnowledgeVersionInput, SearchKnowledgeInput, ShareKnowledgeInput, StartFlowRunInput, TestToolInput, UpdateFlowInput, UpdateKnowledgeNodeInput, } from "@anchrd/intel-contract";
|
|
2
2
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { permits } from "../shared/gate-authorization/gate-authorization.js";
|
|
5
6
|
import { IntelError } from "../shared/intel-error/intel-error.js";
|
|
6
7
|
function text(value) {
|
|
7
8
|
return { content: [{ type: "text", text: JSON.stringify(value) }], isError: false };
|
|
@@ -27,25 +28,26 @@ export async function handleMcp(request, deps) {
|
|
|
27
28
|
const actor = {
|
|
28
29
|
id: deps.authorization.identity.id,
|
|
29
30
|
email: deps.authorization.identity.email,
|
|
30
|
-
|
|
31
|
+
isAdmin: deps.authorization.can("intel", "admin"),
|
|
31
32
|
};
|
|
32
33
|
const toolActor = {
|
|
33
34
|
id: deps.authorization.identity.id,
|
|
34
35
|
email: deps.authorization.identity.email,
|
|
35
|
-
canExecute: deps.authorization
|
|
36
|
+
canExecute: permits(deps.authorization, "tools", "execute"),
|
|
36
37
|
};
|
|
37
38
|
const flowActor = {
|
|
38
39
|
id: deps.authorization.identity.id,
|
|
39
40
|
email: deps.authorization.identity.email,
|
|
40
|
-
canRun: deps.authorization
|
|
41
|
-
canApprove: deps.authorization
|
|
41
|
+
canRun: permits(deps.authorization, "flows", "run"),
|
|
42
|
+
canApprove: permits(deps.authorization, "flows", "approve"),
|
|
43
|
+
isAdmin: deps.authorization.can("intel", "admin"),
|
|
42
44
|
};
|
|
43
45
|
const server = new McpServer({ name: "intel", version: "0.1.0" });
|
|
44
46
|
const EmptyInput = z.strictObject({});
|
|
45
|
-
if (deps.authorization
|
|
47
|
+
if (permits(deps.authorization, "knowledge", "read")) {
|
|
46
48
|
server.registerTool("knowledge_list", {
|
|
47
49
|
title: "List knowledge",
|
|
48
|
-
description: "List authorized folders, documents, and
|
|
50
|
+
description: "List authorized folders, documents, attachments, and tables under one parent.",
|
|
49
51
|
inputSchema: ListKnowledgeNodesInput,
|
|
50
52
|
annotations: {
|
|
51
53
|
title: "List knowledge",
|
|
@@ -127,6 +129,18 @@ export async function handleMcp(request, deps) {
|
|
|
127
129
|
openWorldHint: false,
|
|
128
130
|
},
|
|
129
131
|
}, async (input) => text(await deps.knowledge.search(actor, input)));
|
|
132
|
+
server.registerTool("knowledge_table_get", {
|
|
133
|
+
title: "Get knowledge table",
|
|
134
|
+
description: "Read one authorized table as column names and rows.",
|
|
135
|
+
inputSchema: GetKnowledgeTableInput,
|
|
136
|
+
annotations: {
|
|
137
|
+
title: "Get knowledge table",
|
|
138
|
+
readOnlyHint: true,
|
|
139
|
+
destructiveHint: false,
|
|
140
|
+
idempotentHint: true,
|
|
141
|
+
openWorldHint: false,
|
|
142
|
+
},
|
|
143
|
+
}, async (input) => text(await deps.knowledge.getTable(actor, input.nodeId)));
|
|
130
144
|
server.registerTool("knowledge_links_list", {
|
|
131
145
|
title: "List knowledge links",
|
|
132
146
|
description: "List authorized outgoing links and backlinks for one Knowledge node.",
|
|
@@ -139,6 +153,25 @@ export async function handleMcp(request, deps) {
|
|
|
139
153
|
openWorldHint: false,
|
|
140
154
|
},
|
|
141
155
|
}, async (input) => text(await deps.knowledge.listLinks(actor, input.nodeId)));
|
|
156
|
+
// The reader's half of a document link (#41): the titles of the linked documents this caller
|
|
157
|
+
// may see. There is no tool to create or delete a link — a relationship is written in the text
|
|
158
|
+
// and `knowledge_save` is what records it, so there is only one way to make one.
|
|
159
|
+
//
|
|
160
|
+
// ⚠️ A target this caller may not reach, or one that is gone, is simply absent from the answer.
|
|
161
|
+
// The two are indistinguishable on purpose: telling them apart would confirm that a document
|
|
162
|
+
// exists somewhere they cannot look.
|
|
163
|
+
server.registerTool("knowledge_links_resolve", {
|
|
164
|
+
title: "Resolve knowledge links",
|
|
165
|
+
description: "Resolve document link targets to the titles this caller is authorized to see. Targets that are unreachable or deleted are absent from the result.",
|
|
166
|
+
inputSchema: ResolveKnowledgeLinksInput,
|
|
167
|
+
annotations: {
|
|
168
|
+
title: "Resolve knowledge links",
|
|
169
|
+
readOnlyHint: true,
|
|
170
|
+
destructiveHint: false,
|
|
171
|
+
idempotentHint: true,
|
|
172
|
+
openWorldHint: false,
|
|
173
|
+
},
|
|
174
|
+
}, async (input) => text(await deps.knowledge.resolveLinks(actor, input)));
|
|
142
175
|
server.registerTool("knowledge_graph", {
|
|
143
176
|
title: "Get knowledge graph",
|
|
144
177
|
description: "Get the authorized Knowledge nodes and explicit relationships for discovery.",
|
|
@@ -166,10 +199,10 @@ export async function handleMcp(request, deps) {
|
|
|
166
199
|
},
|
|
167
200
|
}, async () => text(await deps.knowledge.reindex(actor)));
|
|
168
201
|
}
|
|
169
|
-
if (deps.authorization
|
|
202
|
+
if (permits(deps.authorization, "knowledge", "create")) {
|
|
170
203
|
server.registerTool("knowledge_create", {
|
|
171
204
|
title: "Create knowledge",
|
|
172
|
-
description: "Create a governed folder, document, or
|
|
205
|
+
description: "Create a governed folder, document, attachment, or table node.",
|
|
173
206
|
inputSchema: CreateKnowledgeNodeInput,
|
|
174
207
|
annotations: {
|
|
175
208
|
title: "Create knowledge",
|
|
@@ -180,7 +213,7 @@ export async function handleMcp(request, deps) {
|
|
|
180
213
|
},
|
|
181
214
|
}, async (input) => text(await deps.knowledge.create(actor, input)));
|
|
182
215
|
}
|
|
183
|
-
if (deps.authorization
|
|
216
|
+
if (permits(deps.authorization, "knowledge", "write")) {
|
|
184
217
|
server.registerTool("knowledge_save", {
|
|
185
218
|
title: "Save knowledge version",
|
|
186
219
|
description: "Append an immutable content version using an optimistic base version.",
|
|
@@ -205,6 +238,34 @@ export async function handleMcp(request, deps) {
|
|
|
205
238
|
openWorldHint: false,
|
|
206
239
|
},
|
|
207
240
|
}, async (input) => text(await deps.knowledge.saveAttachment(actor, input)));
|
|
241
|
+
server.registerTool("knowledge_table_define", {
|
|
242
|
+
title: "Define knowledge table columns",
|
|
243
|
+
description: "Write the column names of an empty table. The header is the contract every append is checked against and cannot be rewritten.",
|
|
244
|
+
inputSchema: DefineKnowledgeTableInput,
|
|
245
|
+
annotations: {
|
|
246
|
+
title: "Define knowledge table columns",
|
|
247
|
+
readOnlyHint: false,
|
|
248
|
+
destructiveHint: false,
|
|
249
|
+
idempotentHint: true,
|
|
250
|
+
openWorldHint: false,
|
|
251
|
+
},
|
|
252
|
+
}, async (input) => text(await deps.knowledge.defineTable(actor, input)));
|
|
253
|
+
// The tool #40 exists for: an agent collecting findings on a schedule appends them without
|
|
254
|
+
// reading or resending what is already there, and two agents appending at once lose nothing.
|
|
255
|
+
server.registerTool("knowledge_table_append", {
|
|
256
|
+
title: "Append knowledge table rows",
|
|
257
|
+
description: "Append rows to a table without reading or resending its existing content. Each row must have exactly as many cells as the table has columns; a row that does not is rejected and nothing is written.",
|
|
258
|
+
inputSchema: AppendKnowledgeTableRowsInput,
|
|
259
|
+
annotations: {
|
|
260
|
+
title: "Append knowledge table rows",
|
|
261
|
+
readOnlyHint: false,
|
|
262
|
+
destructiveHint: false,
|
|
263
|
+
// The idempotency key makes a repeat of the same call a no-op; without one, appending
|
|
264
|
+
// twice appends twice, which is what appending means.
|
|
265
|
+
idempotentHint: true,
|
|
266
|
+
openWorldHint: false,
|
|
267
|
+
},
|
|
268
|
+
}, async (input) => text(await deps.knowledge.appendTableRows(actor, input)));
|
|
208
269
|
server.registerTool("knowledge_update", {
|
|
209
270
|
title: "Update knowledge",
|
|
210
271
|
description: "Rename, move, describe, or change the context policy of Knowledge.",
|
|
@@ -229,32 +290,8 @@ export async function handleMcp(request, deps) {
|
|
|
229
290
|
openWorldHint: false,
|
|
230
291
|
},
|
|
231
292
|
}, async (input) => text(await deps.knowledge.archive(actor, input)));
|
|
232
|
-
server.registerTool("knowledge_link_create", {
|
|
233
|
-
title: "Link knowledge",
|
|
234
|
-
description: "Create a governed relationship between two authorized Knowledge nodes.",
|
|
235
|
-
inputSchema: CreateKnowledgeLinkInput,
|
|
236
|
-
annotations: {
|
|
237
|
-
title: "Link knowledge",
|
|
238
|
-
readOnlyHint: false,
|
|
239
|
-
destructiveHint: false,
|
|
240
|
-
idempotentHint: true,
|
|
241
|
-
openWorldHint: false,
|
|
242
|
-
},
|
|
243
|
-
}, async (input) => text(await deps.knowledge.createLink(actor, input)));
|
|
244
|
-
server.registerTool("knowledge_link_delete", {
|
|
245
|
-
title: "Delete knowledge link",
|
|
246
|
-
description: "Delete one governed Knowledge relationship by ID.",
|
|
247
|
-
inputSchema: DeleteKnowledgeLinkInput,
|
|
248
|
-
annotations: {
|
|
249
|
-
title: "Delete knowledge link",
|
|
250
|
-
readOnlyHint: false,
|
|
251
|
-
destructiveHint: true,
|
|
252
|
-
idempotentHint: true,
|
|
253
|
-
openWorldHint: false,
|
|
254
|
-
},
|
|
255
|
-
}, async (input) => text(await deps.knowledge.deleteLink(actor, input)));
|
|
256
293
|
}
|
|
257
|
-
if (deps.authorization
|
|
294
|
+
if (permits(deps.authorization, "knowledge", "share")) {
|
|
258
295
|
server.registerTool("knowledge_shares_list", {
|
|
259
296
|
title: "List knowledge shares",
|
|
260
297
|
description: "List direct grants for Knowledge the caller is allowed to manage.",
|
|
@@ -292,7 +329,7 @@ export async function handleMcp(request, deps) {
|
|
|
292
329
|
},
|
|
293
330
|
}, async (input) => text(await deps.knowledge.revokeGrant(actor, input)));
|
|
294
331
|
}
|
|
295
|
-
if (deps.authorization
|
|
332
|
+
if (permits(deps.authorization, "flows", "read")) {
|
|
296
333
|
server.registerTool("flows_list", {
|
|
297
334
|
title: "List flows",
|
|
298
335
|
description: "List authorized flows, either all of them or the ones filed in one folder of the shared Knowledge tree.",
|
|
@@ -317,6 +354,45 @@ export async function handleMcp(request, deps) {
|
|
|
317
354
|
openWorldHint: false,
|
|
318
355
|
},
|
|
319
356
|
}, async (input) => text(await deps.flows.get(flowActor, input.flowId)));
|
|
357
|
+
server.registerTool("flow_calls_list", {
|
|
358
|
+
title: "List called flows",
|
|
359
|
+
description: "List the flows one flow calls, read out of its graph rather than out of where it is filed.",
|
|
360
|
+
inputSchema: GetFlowInput,
|
|
361
|
+
annotations: {
|
|
362
|
+
title: "List called flows",
|
|
363
|
+
readOnlyHint: true,
|
|
364
|
+
destructiveHint: false,
|
|
365
|
+
idempotentHint: true,
|
|
366
|
+
openWorldHint: false,
|
|
367
|
+
},
|
|
368
|
+
}, async (input) => text(await deps.flows.listCalls(flowActor, input.flowId)));
|
|
369
|
+
// ⚠️ Only nodes the requesting user may see are in the answer, placeholders included: an edge to
|
|
370
|
+
// a grey box would already say that something is there. The same rule the screen follows,
|
|
371
|
+
// because it is the same service (#19).
|
|
372
|
+
server.registerTool("flow_relation_graph", {
|
|
373
|
+
title: "Read the relation graph",
|
|
374
|
+
description: "Read what accesses what for one folder or one flow: which flow reads which document and which flow calls which flow.",
|
|
375
|
+
inputSchema: RelationGraphInput,
|
|
376
|
+
annotations: {
|
|
377
|
+
title: "Read the relation graph",
|
|
378
|
+
readOnlyHint: true,
|
|
379
|
+
destructiveHint: false,
|
|
380
|
+
idempotentHint: true,
|
|
381
|
+
openWorldHint: false,
|
|
382
|
+
},
|
|
383
|
+
}, async (input) => text(await deps.flows.relationGraph(flowActor, input)));
|
|
384
|
+
server.registerTool("flow_requirements_list", {
|
|
385
|
+
title: "List what a flow needs",
|
|
386
|
+
description: "List the Knowledge documents and MCP tools one flow's graph names. Documents the calling user cannot see are counted rather than named, and no claim is made about whether anyone may reach them: for tools that cannot be known in advance, because the catalog is a live query with each user's own portal token.",
|
|
387
|
+
inputSchema: GetFlowInput,
|
|
388
|
+
annotations: {
|
|
389
|
+
title: "List what a flow needs",
|
|
390
|
+
readOnlyHint: true,
|
|
391
|
+
destructiveHint: false,
|
|
392
|
+
idempotentHint: true,
|
|
393
|
+
openWorldHint: false,
|
|
394
|
+
},
|
|
395
|
+
}, async (input) => text(await deps.flows.listRequirements(flowActor, input.flowId)));
|
|
320
396
|
server.registerTool("flow_run_get", {
|
|
321
397
|
title: "Get flow run",
|
|
322
398
|
description: "Get durable run state and the next node an AI or person must handle.",
|
|
@@ -329,46 +405,37 @@ export async function handleMcp(request, deps) {
|
|
|
329
405
|
openWorldHint: false,
|
|
330
406
|
},
|
|
331
407
|
}, async (input) => text(await deps.flows.getRun(flowActor, input.runId)));
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
408
|
+
// ⚠️ Model context like every other tool result: what a run did, never what it produced. The run
|
|
409
|
+
// input and output are absent because a run reaches Knowledge and tools with the rights of
|
|
410
|
+
// whoever started it, and only their own runs are the calling user's to read out that way.
|
|
411
|
+
server.registerTool("flow_runs_list", {
|
|
412
|
+
title: "List flow runs",
|
|
413
|
+
description: "List the runs of one flow, newest first, with status, start, duration, what triggered them, and for a failed run the step that ended it. Optionally only the failed ones. Runs the calling user may not see are absent, and a failure inside a called flow they may not see is named by the calling step alone.",
|
|
414
|
+
inputSchema: ListFlowRunsInput,
|
|
415
|
+
annotations: {
|
|
416
|
+
title: "List flow runs",
|
|
340
417
|
readOnlyHint: true,
|
|
341
418
|
destructiveHint: false,
|
|
342
419
|
idempotentHint: true,
|
|
343
420
|
openWorldHint: false,
|
|
344
421
|
},
|
|
345
|
-
}, async (input) => text(await deps.flows.
|
|
346
|
-
server.registerTool("
|
|
347
|
-
title: "
|
|
348
|
-
description: "
|
|
349
|
-
inputSchema:
|
|
422
|
+
}, async (input) => text(await deps.flows.listRuns(flowActor, input)));
|
|
423
|
+
server.registerTool("flow_run_steps_list", {
|
|
424
|
+
title: "List flow run steps",
|
|
425
|
+
description: "List every completed step of one run, oldest first, with its outcome, branch, and the reason a failed step gives, plus the call chain the run belongs to.",
|
|
426
|
+
inputSchema: GetFlowRunInput,
|
|
350
427
|
annotations: {
|
|
351
|
-
title: "
|
|
352
|
-
readOnlyHint:
|
|
428
|
+
title: "List flow run steps",
|
|
429
|
+
readOnlyHint: true,
|
|
353
430
|
destructiveHint: false,
|
|
354
431
|
idempotentHint: true,
|
|
355
432
|
openWorldHint: false,
|
|
356
433
|
},
|
|
357
|
-
}, async (input) => text(await deps.flows.
|
|
358
|
-
server.registerTool("flow_revoke_share", {
|
|
359
|
-
title: "Revoke flow share",
|
|
360
|
-
description: "Revoke one direct Flow grant by ID.",
|
|
361
|
-
inputSchema: RevokeFlowGrantInput,
|
|
362
|
-
annotations: {
|
|
363
|
-
title: "Revoke flow share",
|
|
364
|
-
readOnlyHint: false,
|
|
365
|
-
destructiveHint: true,
|
|
366
|
-
idempotentHint: true,
|
|
367
|
-
openWorldHint: false,
|
|
368
|
-
},
|
|
369
|
-
}, async (input) => text(await deps.flows.revokeGrant(flowActor, input)));
|
|
434
|
+
}, async (input) => text(await deps.flows.listRunSteps(flowActor, input.runId)));
|
|
370
435
|
}
|
|
371
|
-
|
|
436
|
+
// A flow has no share tool of its own. Sharing happens on the folder a flow is filed in, through
|
|
437
|
+
// knowledge_share, so a narrower grant cannot sit beside the folder grant (ADR-0004 §2).
|
|
438
|
+
if (permits(deps.authorization, "flows", "create")) {
|
|
372
439
|
server.registerTool("flow_create", {
|
|
373
440
|
title: "Create flow",
|
|
374
441
|
description: "Create governed flow metadata before adding an immutable graph version.",
|
|
@@ -382,7 +449,7 @@ export async function handleMcp(request, deps) {
|
|
|
382
449
|
},
|
|
383
450
|
}, async (input) => text(await deps.flows.create(flowActor, input)));
|
|
384
451
|
}
|
|
385
|
-
if (deps.authorization
|
|
452
|
+
if (permits(deps.authorization, "flows", "write")) {
|
|
386
453
|
server.registerTool("flow_update", {
|
|
387
454
|
title: "Rename or move flow",
|
|
388
455
|
description: "Rename a flow, change its description, or move it to another folder of the shared Knowledge tree. Never changes what the flow does.",
|
|
@@ -408,7 +475,22 @@ export async function handleMcp(request, deps) {
|
|
|
408
475
|
},
|
|
409
476
|
}, async (input) => text(await deps.flows.save(flowActor, input)));
|
|
410
477
|
}
|
|
411
|
-
if (deps.authorization
|
|
478
|
+
if (permits(deps.authorization, "flows", "publish")) {
|
|
479
|
+
// The same answer the screen shows the author before publishing. Every user-visible capability
|
|
480
|
+
// has its MCP equivalent, and a freeze nobody could inspect from here would be one behavior
|
|
481
|
+
// through two doors.
|
|
482
|
+
server.registerTool("flow_publish_preview", {
|
|
483
|
+
title: "Preview flow publication",
|
|
484
|
+
description: "Show which version each sub-flow call will take and which of them publishing freezes.",
|
|
485
|
+
inputSchema: PreviewFlowPublishInput,
|
|
486
|
+
annotations: {
|
|
487
|
+
title: "Preview flow publication",
|
|
488
|
+
readOnlyHint: true,
|
|
489
|
+
destructiveHint: false,
|
|
490
|
+
idempotentHint: true,
|
|
491
|
+
openWorldHint: false,
|
|
492
|
+
},
|
|
493
|
+
}, async (input) => text(await deps.flows.previewPublish(flowActor, input)));
|
|
412
494
|
server.registerTool("flow_publish", {
|
|
413
495
|
title: "Publish flow",
|
|
414
496
|
description: "Publish a version after resolving its authorized Knowledge and tool references.",
|
|
@@ -422,7 +504,7 @@ export async function handleMcp(request, deps) {
|
|
|
422
504
|
},
|
|
423
505
|
}, async (input) => text(await deps.flows.publish(flowActor, input)));
|
|
424
506
|
}
|
|
425
|
-
if (deps.authorization
|
|
507
|
+
if (permits(deps.authorization, "flows", "run")) {
|
|
426
508
|
server.registerTool("flow_run_start", {
|
|
427
509
|
title: "Start flow run",
|
|
428
510
|
description: "Start a durable run of the published graph and return its first actionable node.",
|
|
@@ -448,7 +530,7 @@ export async function handleMcp(request, deps) {
|
|
|
448
530
|
},
|
|
449
531
|
}, async (input) => text(await deps.flows.completeStep(flowActor, input)));
|
|
450
532
|
}
|
|
451
|
-
if (deps.authorization
|
|
533
|
+
if (permits(deps.authorization, "tools", "read")) {
|
|
452
534
|
server.registerTool("tools_catalog_list", {
|
|
453
535
|
title: "List tools",
|
|
454
536
|
description: "List the MCP tools the calling user can reach through the portal, live rather than cached.",
|
|
@@ -462,7 +544,7 @@ export async function handleMcp(request, deps) {
|
|
|
462
544
|
},
|
|
463
545
|
}, async () => text(await deps.tools.catalog(toolActor)));
|
|
464
546
|
}
|
|
465
|
-
if (deps.authorization
|
|
547
|
+
if (permits(deps.authorization, "tools", "test")) {
|
|
466
548
|
server.registerTool("tools_test", {
|
|
467
549
|
title: "Test tool",
|
|
468
550
|
description: "Validate input and execute one explicitly read-only cached MCP tool with the caller's Gate connection.",
|
|
@@ -476,7 +558,7 @@ export async function handleMcp(request, deps) {
|
|
|
476
558
|
},
|
|
477
559
|
}, async (input) => text(await deps.tools.test(toolActor, input)));
|
|
478
560
|
}
|
|
479
|
-
if (deps.authorization
|
|
561
|
+
if (permits(deps.authorization, "tools", "execute")) {
|
|
480
562
|
server.registerTool("tools_execute", {
|
|
481
563
|
title: "Execute tool",
|
|
482
564
|
description: "Validate and execute a discovered MCP tool with the caller's just-in-time Gate connection.",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare function encodeCsvRow(cells: readonly string[]): string;
|
|
2
|
+
export declare function encodeCsv(rows: readonly (readonly string[])[]): string;
|
|
3
|
+
/**
|
|
4
|
+
* Every row of a CSV body, quoted fields included.
|
|
5
|
+
*
|
|
6
|
+
* ⚠️ A quoted field may contain a line break, so this cannot split on `\n` first. A row is closed
|
|
7
|
+
* by a line break that falls outside quotes and by nothing else — the reason a naive `split` reads
|
|
8
|
+
* a table with one multi-line cell as two broken rows.
|
|
9
|
+
*
|
|
10
|
+
* A trailing newline does not produce an empty final row; an empty line in the middle does produce
|
|
11
|
+
* a row with one empty cell, because that is what the file says.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseCsv(text: string): string[][];
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// The one CSV reader and writer in Intel. A table's canonical body is CSV (#40), and every surface
|
|
2
|
+
// that has to look inside one — the grid, the column check, the search index — goes through here so
|
|
3
|
+
// none of them can grow a private opinion about quoting.
|
|
4
|
+
//
|
|
5
|
+
// RFC 4180 with two deliberate narrowings: rows are separated by `\n` when written (a reader
|
|
6
|
+
// accepts `\r\n` too), and a field is quoted only when it has to be. Nothing else is escaped,
|
|
7
|
+
// because CSV has no escapes — a quote inside a quoted field is written twice.
|
|
8
|
+
function needsQuoting(cell) {
|
|
9
|
+
return (cell.includes(",") ||
|
|
10
|
+
cell.includes('"') ||
|
|
11
|
+
cell.includes("\n") ||
|
|
12
|
+
cell.includes("\r") ||
|
|
13
|
+
cell !== cell.trim());
|
|
14
|
+
}
|
|
15
|
+
export function encodeCsvRow(cells) {
|
|
16
|
+
return cells
|
|
17
|
+
.map((cell) => (needsQuoting(cell) ? `"${cell.replaceAll('"', '""')}"` : cell))
|
|
18
|
+
.join(",");
|
|
19
|
+
}
|
|
20
|
+
export function encodeCsv(rows) {
|
|
21
|
+
return rows.length === 0 ? "" : `${rows.map(encodeCsvRow).join("\n")}\n`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Every row of a CSV body, quoted fields included.
|
|
25
|
+
*
|
|
26
|
+
* ⚠️ A quoted field may contain a line break, so this cannot split on `\n` first. A row is closed
|
|
27
|
+
* by a line break that falls outside quotes and by nothing else — the reason a naive `split` reads
|
|
28
|
+
* a table with one multi-line cell as two broken rows.
|
|
29
|
+
*
|
|
30
|
+
* A trailing newline does not produce an empty final row; an empty line in the middle does produce
|
|
31
|
+
* a row with one empty cell, because that is what the file says.
|
|
32
|
+
*/
|
|
33
|
+
export function parseCsv(text) {
|
|
34
|
+
const rows = [];
|
|
35
|
+
let row = [];
|
|
36
|
+
let cell = "";
|
|
37
|
+
let quoted = false;
|
|
38
|
+
let started = false;
|
|
39
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
40
|
+
const character = text[index];
|
|
41
|
+
if (quoted) {
|
|
42
|
+
if (character === '"') {
|
|
43
|
+
if (text[index + 1] === '"') {
|
|
44
|
+
cell += '"';
|
|
45
|
+
index += 1;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
quoted = false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
cell += character;
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (character === '"' && cell === "") {
|
|
57
|
+
quoted = true;
|
|
58
|
+
started = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (character === ",") {
|
|
62
|
+
row.push(cell);
|
|
63
|
+
cell = "";
|
|
64
|
+
started = true;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (character === "\n" || character === "\r") {
|
|
68
|
+
if (character === "\r" && text[index + 1] === "\n")
|
|
69
|
+
index += 1;
|
|
70
|
+
row.push(cell);
|
|
71
|
+
rows.push(row);
|
|
72
|
+
row = [];
|
|
73
|
+
cell = "";
|
|
74
|
+
started = false;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
cell += character;
|
|
78
|
+
started = true;
|
|
79
|
+
}
|
|
80
|
+
if (started || cell !== "" || row.length > 0) {
|
|
81
|
+
row.push(cell);
|
|
82
|
+
rows.push(row);
|
|
83
|
+
}
|
|
84
|
+
return rows;
|
|
85
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Authorized, GateClient } from "@anchrd/gate-sdk";
|
|
2
2
|
export declare function bearer(headers: Headers): string | null;
|
|
3
3
|
export declare function authorize(gate: Pick<GateClient, "authorize">, headers: Headers, resource: string): Promise<Authorized | null>;
|
|
4
|
+
export declare function permits(authorization: Pick<Authorized, "can">, handle: string, fn: string): boolean;
|
|
4
5
|
export declare function authorizeBearer(gate: Pick<GateClient, "authorize">, token: string, resource: string): Promise<Authorized | null>;
|
|
@@ -13,6 +13,13 @@ export async function authorize(gate, headers, resource) {
|
|
|
13
13
|
return null;
|
|
14
14
|
return await authorizeBearer(gate, token, resource);
|
|
15
15
|
}
|
|
16
|
+
// `intel/admin` opens every door, and it has to: `share` inherits downwards only, so without a way
|
|
17
|
+
// in that needs no grant, a fresh instance has nobody who can set the first one (ADR-0004 §6). Gate
|
|
18
|
+
// learned the same lesson at three separate doors — a guard written per route is a list that goes
|
|
19
|
+
// quietly incomplete at the next one, so both surfaces ask this one function instead.
|
|
20
|
+
export function permits(authorization, handle, fn) {
|
|
21
|
+
return authorization.can(handle, fn) || authorization.can("intel", "admin");
|
|
22
|
+
}
|
|
16
23
|
export async function authorizeBearer(gate, token, resource) {
|
|
17
24
|
const result = await gate.authorize(token);
|
|
18
25
|
return result.ok && result.resource === resource ? result : null;
|