@anchrd/intel-api 0.3.0 → 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.
@@ -1,9 +1,9 @@
1
- import type { ArchiveKnowledgeNodeInput, CreateKnowledgeLinkInput, CreateKnowledgeNodeInput, DeleteKnowledgeLinkInput, KnowledgeAttachment, KnowledgeCitation, KnowledgeDocument, KnowledgeGraph, KnowledgeGraphInput, KnowledgeLink, KnowledgeNode, KnowledgeVersion, ListKnowledgeNodesInput, ResourceGrant, RevokeKnowledgeGrantInput, SaveKnowledgeAttachmentInput, SaveKnowledgeVersionInput, SearchKnowledgeInput, ShareKnowledgeInput, UpdateKnowledgeNodeInput } from "@anchrd/intel-contract";
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
- canReindex?: boolean;
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
- canEdit(actor: Actor, nodeId: string): Promise<boolean>;
25
- canManage(actor: Actor, nodeId: string): Promise<boolean>;
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
- getLinkVisible(actor: Actor, linkId: string): Promise<KnowledgeLink | null>;
51
- findLinkVisible(actor: Actor, input: Pick<CreateKnowledgeLinkInput, "sourceNodeId" | "targetNodeId" | "relation">): Promise<KnowledgeLink | null>;
52
- insertLink(input: {
53
- link: KnowledgeLink;
54
- actorId: string;
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
- linkId: string;
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<boolean>;
76
+ }): Promise<void>;
66
77
  graphVisible(actor: Actor, input: KnowledgeGraphInput): Promise<KnowledgeGraph>;
67
78
  setGrant(input: {
68
79
  grant: ResourceGrant;
@@ -102,20 +113,46 @@ 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>;
108
124
  };
109
125
  semantic?: SemanticIndex | undefined;
110
126
  }
127
+ export type FolderAccess = "ok" | "missing" | "not-a-folder" | "forbidden";
128
+ export interface BoundedChildren {
129
+ items: KnowledgeNode[];
130
+ total: number;
131
+ }
111
132
  export interface KnowledgeService {
112
133
  list(actor: Actor, input: ListKnowledgeNodesInput): Promise<{
113
134
  items: KnowledgeNode[];
114
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>;
115
148
  get(actor: Actor, nodeId: string): Promise<KnowledgeDocument>;
149
+ folderAccess(actor: Actor, folderId: string): Promise<FolderAccess>;
116
150
  create(actor: Actor, input: CreateKnowledgeNodeInput): Promise<KnowledgeNode>;
117
151
  save(actor: Actor, input: SaveKnowledgeVersionInput): Promise<KnowledgeDocument>;
118
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>;
119
156
  getAttachment(actor: Actor, nodeId: string): Promise<KnowledgeAttachment>;
120
157
  readAttachment(actor: Actor, nodeId: string): Promise<KnowledgeAttachmentBody>;
121
158
  listVersions(actor: Actor, nodeId: string): Promise<{
@@ -123,18 +160,14 @@ export interface KnowledgeService {
123
160
  }>;
124
161
  update(actor: Actor, input: UpdateKnowledgeNodeInput): Promise<KnowledgeNode>;
125
162
  archive(actor: Actor, input: ArchiveKnowledgeNodeInput): Promise<KnowledgeNode>;
126
- listGrants(actor: Actor, resourceId: string): Promise<{
127
- items: ResourceGrant[];
128
- }>;
163
+ listGrants(actor: Actor, resourceId: string): Promise<ResourceGrantList>;
129
164
  listLinks(actor: Actor, nodeId: string): Promise<{
130
165
  items: KnowledgeLink[];
131
166
  }>;
132
- createLink(actor: Actor, input: CreateKnowledgeLinkInput): Promise<KnowledgeLink>;
133
- deleteLink(actor: Actor, input: DeleteKnowledgeLinkInput): Promise<{
134
- deleted: boolean;
135
- }>;
167
+ resolveLinks(actor: Actor, input: ResolveKnowledgeLinksInput): Promise<ResolveKnowledgeLinksResult>;
136
168
  graph(actor: Actor, input: KnowledgeGraphInput): Promise<KnowledgeGraph>;
137
- share(actor: Actor, input: ShareKnowledgeInput): Promise<ResourceGrant>;
169
+ visibleNode(actor: Actor, nodeId: string): Promise<KnowledgeNode | null>;
170
+ share(actor: Actor, input: ShareKnowledgeInput): Promise<ShareKnowledgeResult>;
138
171
  revokeGrant(actor: Actor, input: RevokeKnowledgeGrantInput): Promise<{
139
172
  revoked: boolean;
140
173
  }>;
@@ -149,9 +182,9 @@ export interface KnowledgeIndexTarget {
149
182
  nodeId: string;
150
183
  versionId: string;
151
184
  title: string;
152
- contentKey: string;
185
+ contentKeys: string[];
153
186
  mediaType: string;
154
- kind: "document" | "attachment";
187
+ kind: "document" | "attachment" | "table";
155
188
  updatedAt: string;
156
189
  }
157
190
  export interface KnowledgeIndexRepository {
package/dist/mcp/mcp.js CHANGED
@@ -1,7 +1,8 @@
1
- import { ArchiveKnowledgeNodeInput, CompleteFlowRunStepInput, CreateFlowInput, CreateKnowledgeLinkInput, CreateKnowledgeNodeInput, DeleteKnowledgeLinkInput, ExecuteToolInput, GetFlowInput, GetFlowRunInput, GetKnowledgeNodeInput, KnowledgeGraphInput, ListFlowGrantsInput, ListKnowledgeGrantsInput, ListKnowledgeNodesInput, PublishFlowInput, RevokeFlowGrantInput, RevokeKnowledgeGrantInput, SaveFlowVersionInput, SaveKnowledgeAttachmentInput, SaveKnowledgeVersionInput, SearchKnowledgeInput, ShareFlowInput, ShareKnowledgeInput, StartFlowRunInput, TestToolInput, UpdateKnowledgeNodeInput, } from "@anchrd/intel-contract";
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
- canReindex: deps.authorization.can("intel", "admin"),
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.can("tools", "execute"),
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.can("flows", "run"),
41
- canApprove: deps.authorization.can("flows", "approve"),
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.can("knowledge", "read")) {
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 attachments under one parent.",
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.can("knowledge", "create")) {
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 attachment metadata node.",
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.can("knowledge", "write")) {
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.can("knowledge", "share")) {
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,11 +329,11 @@ 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.can("flows", "read")) {
332
+ if (permits(deps.authorization, "flows", "read")) {
296
333
  server.registerTool("flows_list", {
297
334
  title: "List flows",
298
- description: "List authorized company flows by title and description.",
299
- inputSchema: EmptyInput,
335
+ description: "List authorized flows, either all of them or the ones filed in one folder of the shared Knowledge tree.",
336
+ inputSchema: ListFlowsInput,
300
337
  annotations: {
301
338
  title: "List flows",
302
339
  readOnlyHint: true,
@@ -304,7 +341,7 @@ export async function handleMcp(request, deps) {
304
341
  idempotentHint: true,
305
342
  openWorldHint: false,
306
343
  },
307
- }, async () => text(await deps.flows.list(flowActor)));
344
+ }, async (input) => text(await deps.flows.list(flowActor, input)));
308
345
  server.registerTool("flow_get", {
309
346
  title: "Get flow",
310
347
  description: "Load one authorized flow and its current immutable graph on demand.",
@@ -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
- if (deps.authorization.can("flows", "share")) {
334
- server.registerTool("flow_shares_list", {
335
- title: "List flow shares",
336
- description: "List direct grants for a Flow the caller is allowed to manage.",
337
- inputSchema: ListFlowGrantsInput,
338
- annotations: {
339
- title: "List flow shares",
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.listGrants(flowActor, input.resourceId)));
346
- server.registerTool("flow_share", {
347
- title: "Share flow",
348
- description: "Grant Flow access to a Gate user, verified email, or the organization.",
349
- inputSchema: ShareFlowInput,
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: "Share flow",
352
- readOnlyHint: false,
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.share(flowActor, input)));
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
- if (deps.authorization.can("flows", "create")) {
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,19 @@ 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.can("flows", "write")) {
452
+ if (permits(deps.authorization, "flows", "write")) {
453
+ server.registerTool("flow_update", {
454
+ title: "Rename or move flow",
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.",
456
+ inputSchema: UpdateFlowInput,
457
+ annotations: {
458
+ title: "Rename or move flow",
459
+ readOnlyHint: false,
460
+ destructiveHint: false,
461
+ idempotentHint: true,
462
+ openWorldHint: false,
463
+ },
464
+ }, async (input) => text(await deps.flows.update(flowActor, input)));
386
465
  server.registerTool("flow_save", {
387
466
  title: "Save flow",
388
467
  description: "Append a validated immutable flow graph version with optimistic concurrency.",
@@ -396,7 +475,22 @@ export async function handleMcp(request, deps) {
396
475
  },
397
476
  }, async (input) => text(await deps.flows.save(flowActor, input)));
398
477
  }
399
- if (deps.authorization.can("flows", "publish")) {
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)));
400
494
  server.registerTool("flow_publish", {
401
495
  title: "Publish flow",
402
496
  description: "Publish a version after resolving its authorized Knowledge and tool references.",
@@ -410,7 +504,7 @@ export async function handleMcp(request, deps) {
410
504
  },
411
505
  }, async (input) => text(await deps.flows.publish(flowActor, input)));
412
506
  }
413
- if (deps.authorization.can("flows", "run")) {
507
+ if (permits(deps.authorization, "flows", "run")) {
414
508
  server.registerTool("flow_run_start", {
415
509
  title: "Start flow run",
416
510
  description: "Start a durable run of the published graph and return its first actionable node.",
@@ -436,7 +530,7 @@ export async function handleMcp(request, deps) {
436
530
  },
437
531
  }, async (input) => text(await deps.flows.completeStep(flowActor, input)));
438
532
  }
439
- if (deps.authorization.can("tools", "read")) {
533
+ if (permits(deps.authorization, "tools", "read")) {
440
534
  server.registerTool("tools_catalog_list", {
441
535
  title: "List tools",
442
536
  description: "List the MCP tools the calling user can reach through the portal, live rather than cached.",
@@ -450,7 +544,7 @@ export async function handleMcp(request, deps) {
450
544
  },
451
545
  }, async () => text(await deps.tools.catalog(toolActor)));
452
546
  }
453
- if (deps.authorization.can("tools", "test")) {
547
+ if (permits(deps.authorization, "tools", "test")) {
454
548
  server.registerTool("tools_test", {
455
549
  title: "Test tool",
456
550
  description: "Validate input and execute one explicitly read-only cached MCP tool with the caller's Gate connection.",
@@ -464,7 +558,7 @@ export async function handleMcp(request, deps) {
464
558
  },
465
559
  }, async (input) => text(await deps.tools.test(toolActor, input)));
466
560
  }
467
- if (deps.authorization.can("tools", "execute")) {
561
+ if (permits(deps.authorization, "tools", "execute")) {
468
562
  server.registerTool("tools_execute", {
469
563
  title: "Execute tool",
470
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
+ }