@anchrd/intel-api 0.6.6 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +44 -3
  2. package/dist/adapters/cloudflare/cloudflare.js +50 -16
  3. package/dist/adapters/cloudflare/cloudflare.types.d.ts +11 -0
  4. package/dist/adapters/content/content.d.ts +1 -1
  5. package/dist/adapters/db/db-flows.js +161 -20
  6. package/dist/adapters/db/db-grants.d.ts +13 -2
  7. package/dist/adapters/db/db-grants.js +25 -8
  8. package/dist/adapters/db/db-indexing.d.ts +2 -2
  9. package/dist/adapters/db/db-indexing.js +26 -19
  10. package/dist/adapters/db/db.d.ts +3 -3
  11. package/dist/adapters/db/db.js +448 -119
  12. package/dist/adapters/gate-applications/gate-applications.d.ts +23 -0
  13. package/dist/adapters/gate-applications/gate-applications.js +66 -0
  14. package/dist/adapters/index-queue/index-queue.d.ts +1 -1
  15. package/dist/adapters/index-queue/index-queue.js +2 -2
  16. package/dist/adapters/semantic-index/semantic-index.types.d.ts +2 -2
  17. package/dist/agent-runtime/agent-runtime.d.ts +16 -0
  18. package/dist/agent-runtime/agent-runtime.js +76 -0
  19. package/dist/agent-runtime/agent-runtime.types.d.ts +57 -0
  20. package/dist/bundle/bundle.d.ts +4 -0
  21. package/dist/bundle/bundle.js +1035 -0
  22. package/dist/bundle/bundle.types.d.ts +33 -0
  23. package/dist/bundle/bundle.types.js +1 -0
  24. package/dist/cli/cli.js +10 -1
  25. package/dist/flows/flows.d.ts +8 -8
  26. package/dist/flows/flows.js +158 -42
  27. package/dist/flows/flows.types.d.ts +40 -7
  28. package/dist/http/http.d.ts +1 -0
  29. package/dist/http/http.js +329 -63
  30. package/dist/http/http.types.d.ts +6 -2
  31. package/dist/indexing/indexing.js +14 -2
  32. package/dist/indexing/indexing.types.d.ts +2 -2
  33. package/dist/intel/intel.js +12 -3
  34. package/dist/intel/intel.types.d.ts +6 -2
  35. package/dist/mcp/mcp.js +483 -124
  36. package/dist/mcp/mcp.types.d.ts +11 -2
  37. package/dist/nodes/nodes.d.ts +2 -0
  38. package/dist/nodes/nodes.js +1337 -0
  39. package/dist/nodes/nodes.types.d.ts +314 -0
  40. package/dist/nodes/nodes.types.js +1 -0
  41. package/migrations/0011_one_name_for_the_tree.sql +53 -0
  42. package/migrations/0012_table_snapshots.sql +29 -0
  43. package/migrations/0013_agents_in_the_tree.sql +76 -0
  44. package/migrations/0014_agent_applications.sql +25 -0
  45. package/package.json +3 -2
  46. package/dist/knowledge/knowledge.d.ts +0 -2
  47. package/dist/knowledge/knowledge.js +0 -761
  48. package/dist/knowledge/knowledge.types.d.ts +0 -198
  49. /package/dist/{knowledge/knowledge.types.js → agent-runtime/agent-runtime.types.js} +0 -0
  50. /package/dist/{knowledge → nodes}/document-links/document-links.d.ts +0 -0
  51. /package/dist/{knowledge → nodes}/document-links/document-links.js +0 -0
@@ -0,0 +1,33 @@
1
+ import type { BundleImportResult, BundleManifest } from "@anchrd/intel-contract";
2
+ import type { FlowRepository } from "../flows/flows.types.js";
3
+ import type { Actor, ContentStore, NodeRepository } from "../nodes/nodes.types.js";
4
+ export interface BundleActor extends Actor {
5
+ canReadFlows: boolean;
6
+ canCreateFlows?: boolean;
7
+ }
8
+ export interface BundleDeps {
9
+ repository: Pick<NodeRepository, "getVisible" | "listVisibleSubtree" | "listVersionContentKeys" | "can" | "importTree" | "findImportReplay" | "resolveVisibleTitles">;
10
+ flows: Pick<FlowRepository, "listVisible" | "getVisible" | "getVersions">;
11
+ content: Pick<ContentStore, "get" | "getStream" | "put" | "putBytes" | "delete">;
12
+ id(): string;
13
+ now(): Date;
14
+ hash(content: string | Uint8Array): Promise<string>;
15
+ indexing: {
16
+ enqueue(versionId: string): Promise<void>;
17
+ };
18
+ }
19
+ export interface BundleExport {
20
+ filename: string;
21
+ stream: ReadableStream<Uint8Array>;
22
+ }
23
+ export interface BundleImportInput {
24
+ targetNodeId: string | null;
25
+ zip: ReadableStream<Uint8Array> | Uint8Array;
26
+ idempotencyKey: string;
27
+ }
28
+ export interface BundleService {
29
+ exportSubtree(actor: BundleActor, rootId: string | null): Promise<BundleExport>;
30
+ exportFlow(actor: BundleActor, flowId: string): Promise<BundleExport>;
31
+ manifest(actor: BundleActor, rootId: string | null): Promise<BundleManifest>;
32
+ importBundle(actor: BundleActor, input: BundleImportInput): Promise<BundleImportResult>;
33
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli/cli.js CHANGED
@@ -12,6 +12,15 @@ const interfaces = [
12
12
  functions: ["read", "create", "write", "publish", "run", "share"],
13
13
  },
14
14
  { handle: "tools", functions: ["read", "test", "execute", "admin"] },
15
+ // Declared here, and not by a service of its own, because Gate scopes the rules it returns to the
16
+ // service whose key asked (least privilege). The agent runtime authorizes its callers with this
17
+ // installation's Gate service key against the same audience Intel uses, so a handle registered
18
+ // anywhere else would never appear in the rules that `can("agents", "run")` reads.
19
+ //
20
+ // Without it there is no state between "administrator" and "not at all": the runtime falls back to
21
+ // `intel:admin`, which is the first-grant exit in the same sense as everywhere else, not the way a
22
+ // team lets a colleague talk to an agent.
23
+ { handle: "agents", functions: ["run"] },
15
24
  // `mcp:connect` means the same thing at every MCP service of this installation. A separate name
16
25
  // for the same thing forces the operator to check, service by service, which permission carries
17
26
  // portal access.
@@ -72,7 +81,7 @@ export function createCli(deps) {
72
81
  failures.push("GATE_SERVICE_KEY is missing");
73
82
  if (!toolSourceOrigins)
74
83
  failures.push("TOOL_SOURCE_ORIGINS is missing");
75
- // Tools are optional for a deployment that only uses Knowledge and Flows, so a missing portal
84
+ // Tools are optional for a deployment that only uses nodes and Flows, so a missing portal
76
85
  // is reported as a warning rather than a failure.
77
86
  if (!portalUrl) {
78
87
  deps.log("Note: MCP_PORTAL_URL is unset, so the Tools area stays empty.");
@@ -3,14 +3,14 @@ import type { CompiledFlow, FlowDeps, FlowService } from "./flows.types.js";
3
3
  type SubflowNode = Extract<FlowNode, {
4
4
  kind: "subflow";
5
5
  }>;
6
- type KnowledgeStepNode = Extract<FlowNode, {
7
- kind: KnowledgeLinkKind;
6
+ type TreeLinkNode = Extract<FlowNode, {
7
+ kind: TreeLinkKind;
8
8
  }>;
9
9
  type ToolStepNode = Extract<FlowNode, {
10
10
  kind: "tool";
11
11
  }>;
12
- declare const knowledgeLinkKinds: readonly ["folder", "document", "upload", "table"];
13
- type KnowledgeLinkKind = (typeof knowledgeLinkKinds)[number];
12
+ export declare const treeLinkKinds: readonly ["folder", "document", "upload", "table"];
13
+ type TreeLinkKind = (typeof treeLinkKinds)[number];
14
14
  /**
15
15
  * One place reads a graph for each kind of step it contains, and everything else is derived from
16
16
  * these three. The publish-time rule, the freeze, the sidebar, the relation graph, the requirements
@@ -18,18 +18,18 @@ type KnowledgeLinkKind = (typeof knowledgeLinkKinds)[number];
18
18
  * beside them would drift quietly, because both would keep returning something plausible.
19
19
  */
20
20
  export declare function subflowNodes(graph: FlowGraph): SubflowNode[];
21
- export declare function knowledgeNodes(graph: FlowGraph): KnowledgeStepNode[];
21
+ export declare function treeLinkNodes(graph: FlowGraph): TreeLinkNode[];
22
22
  export declare function toolNodes(graph: FlowGraph): ToolStepNode[];
23
- export declare function resourceIdOf(node: KnowledgeStepNode): string;
23
+ export declare function resourceIdOf(node: TreeLinkNode): string;
24
24
  export declare function calleeIds(graph: FlowGraph): string[];
25
25
  /**
26
- * The Knowledge documents a graph names and the tools it calls, flattened and without repetition.
26
+ * The documents a graph names and the tools it calls, flattened and without repetition.
27
27
  * The requirements list, the publish-time check and the run's first tool check read this one answer,
28
28
  * so they cannot disagree about what a flow touches. A caller that needs to know *which step* names
29
29
  * a document reads the node lists above instead — the relation graph draws exactly that edge.
30
30
  */
31
31
  export declare function graphReferences(graph: FlowGraph): {
32
- knowledge: string[];
32
+ nodes: string[];
33
33
  tools: string[];
34
34
  };
35
35
  export declare function compileFlow(graph: FlowGraph): CompiledFlow;
@@ -9,7 +9,7 @@ const MaxCallDepth = 20;
9
9
  // The four link kinds that name something in the shared tree (D25). `tool` is a link too, but it
10
10
  // names a portal tool rather than a resource, and every rule about reachability applies to these
11
11
  // four and not to it.
12
- const knowledgeLinkKinds = ["folder", "document", "upload", "table"];
12
+ export const treeLinkKinds = ["folder", "document", "upload", "table"];
13
13
  /**
14
14
  * One place reads a graph for each kind of step it contains, and everything else is derived from
15
15
  * these three. The publish-time rule, the freeze, the sidebar, the relation graph, the requirements
@@ -22,8 +22,8 @@ export function subflowNodes(graph) {
22
22
  // ⚠️ Kept as one list although it is now four kinds: every caller asks "what does this graph read
23
23
  // from the tree", never "which of the four". Splitting the callers to match the split in the schema
24
24
  // would multiply four ways at each of the six call sites for no answer anyone wants.
25
- export function knowledgeNodes(graph) {
26
- return graph.nodes.filter((node) => knowledgeLinkKinds.includes(node.kind));
25
+ export function treeLinkNodes(graph) {
26
+ return graph.nodes.filter((node) => treeLinkKinds.includes(node.kind));
27
27
  }
28
28
  export function toolNodes(graph) {
29
29
  return graph.nodes.filter((node) => node.kind === "tool");
@@ -45,29 +45,29 @@ export function calleeIds(graph) {
45
45
  return ids;
46
46
  }
47
47
  /**
48
- * The Knowledge documents a graph names and the tools it calls, flattened and without repetition.
48
+ * The documents a graph names and the tools it calls, flattened and without repetition.
49
49
  * The requirements list, the publish-time check and the run's first tool check read this one answer,
50
50
  * so they cannot disagree about what a flow touches. A caller that needs to know *which step* names
51
51
  * a document reads the node lists above instead — the relation graph draws exactly that edge.
52
52
  */
53
53
  export function graphReferences(graph) {
54
- const knowledge = [];
54
+ const nodes = [];
55
55
  const tools = [];
56
- for (const node of knowledgeNodes(graph)) {
57
- if (!knowledge.includes(node.configuration.resourceId)) {
58
- knowledge.push(node.configuration.resourceId);
56
+ for (const node of treeLinkNodes(graph)) {
57
+ if (!nodes.includes(node.configuration.resourceId)) {
58
+ nodes.push(node.configuration.resourceId);
59
59
  }
60
60
  }
61
61
  for (const node of toolNodes(graph)) {
62
62
  if (!tools.includes(node.configuration.toolName))
63
63
  tools.push(node.configuration.toolName);
64
64
  }
65
- return { knowledge, tools };
65
+ return { nodes, tools };
66
66
  }
67
67
  // ⚠️ The reason, in the words the person can act on, and not one word more. How many documents a
68
68
  // step cannot reach is something they may know; which ones they are is the very thing the ACL is
69
69
  // keeping from them, so the step is named and the documents are only counted (#17, #19).
70
- function knowledgeStepDetail(label, missing) {
70
+ function treeLinkStepDetail(label, missing) {
71
71
  return missing === 1
72
72
  ? `One document this step needs is not readable for you: ${label}`
73
73
  : `${missing} documents this step needs are not readable for you: ${label}`;
@@ -454,7 +454,7 @@ export function createFlows(deps) {
454
454
  }
455
455
  return { problems, version, first };
456
456
  }
457
- // A flow's parent is a Knowledge folder, so the answer comes from Knowledge rather than from a
457
+ // A flow's parent is a folder of the tree, so the answer comes from the node service rather than from a
458
458
  // second permission model here. `null` is the root and needs no permission of its own — the same
459
459
  // as creating a folder at the root does.
460
460
  async function requireFolder(actor, parentId) {
@@ -468,7 +468,7 @@ export function createFlows(deps) {
468
468
  throw new IntelError(409, "parent_not_folder", "A flow's parent must be a folder");
469
469
  }
470
470
  if (access === "forbidden") {
471
- throw new IntelError(403, "knowledge_forbidden", "Destination folder cannot be edited");
471
+ throw new IntelError(403, "node_forbidden", "Destination folder cannot be edited");
472
472
  }
473
473
  }
474
474
  async function requireVersion(versionId, flowId) {
@@ -520,26 +520,26 @@ export function createFlows(deps) {
520
520
  // The documents of a list this actor may reach, named. Distinct IDs only, so a reference written
521
521
  // twice is one document; an ID that is missing from the answer is one they cannot reach.
522
522
  //
523
- // ⚠️ It is `deps.visibleKnowledge` and nothing else, which is what keeps the authorization check,
523
+ // ⚠️ It is `deps.visibleNodes` and nothing else, which is what keeps the authorization check,
524
524
  // the requirements list and the relation graph on one rule. Only `id` and `title` travel onwards:
525
525
  // the node also carries `parentId`, `ownerId` and its version, and where a document sits in the
526
526
  // tree is not part of the question "what does this flow need" (#17, #19).
527
- async function reachableKnowledge(actor, resourceIds) {
527
+ async function reachableNodes(actor, resourceIds) {
528
528
  const reachable = [];
529
529
  for (const resourceId of new Set(resourceIds)) {
530
- const node = await deps.visibleKnowledge(actor, resourceId);
530
+ const node = await deps.visibleNodes(actor, resourceId);
531
531
  if (node)
532
532
  reachable.push({ id: node.id, title: node.title });
533
533
  }
534
534
  return reachable;
535
535
  }
536
- // ⚠️ ADR-0004 §4, and the reason a subflow is not a way around anything. Before a Knowledge or
536
+ // ⚠️ ADR-0004 §4, and the reason a subflow is not a way around anything. Before a node or
537
537
  // Tool step is handed to whoever will carry it out — first run, retry and resume alike — the
538
538
  // current Gate identity and the resource ACL are asked again. A called flow's steps come through
539
539
  // this same function, so a document the user may not read stays unreadable however deep the call
540
540
  // sits, and a flow grant keeps protecting the procedure rather than the data.
541
541
  // ⚠️ Reads what hangs off the step, not the step itself (D25). A document or a tool is a link on a
542
- // `context` edge now, and a run never stands on one — so asking "is this node a knowledge step"
542
+ // `context` edge now, and a run never stands on one — so asking "is this node a tree link"
543
543
  // would ask about a node the run can no longer reach, and every check here would silently pass.
544
544
  async function requireNodeAuthorized(actor, node, graph) {
545
545
  if (!node)
@@ -547,11 +547,11 @@ export function createFlows(deps) {
547
547
  const attached = graph.edges
548
548
  .filter((edge) => edge.kind === "context" && edge.source === node.id)
549
549
  .flatMap((edge) => graph.nodes.filter((candidate) => candidate.id === edge.target));
550
- const wanted = [...new Set(knowledgeNodes({ ...graph, nodes: attached }).map(resourceIdOf))];
550
+ const wanted = [...new Set(treeLinkNodes({ ...graph, nodes: attached }).map(resourceIdOf))];
551
551
  if (wanted.length) {
552
- const reachable = await reachableKnowledge(actor, wanted);
552
+ const reachable = await reachableNodes(actor, wanted);
553
553
  if (reachable.length !== wanted.length) {
554
- throw new IntelError(403, "flow_knowledge_forbidden", knowledgeStepDetail(node.label, wanted.length - reachable.length));
554
+ throw new IntelError(403, "flow_node_forbidden", treeLinkStepDetail(node.label, wanted.length - reachable.length));
555
555
  }
556
556
  }
557
557
  const toolNames = attached
@@ -809,9 +809,32 @@ export function createFlows(deps) {
809
809
  : null,
810
810
  };
811
811
  },
812
+ // The history of one flow, oldest first (#144). `requireFlow`, the same door as `get`: the
813
+ // history describes a flow this actor may already open, and it holds nothing a reader of the
814
+ // current graph could not learn — only when each edit happened and which one is live.
815
+ async listVersions(actor, flowId) {
816
+ const flow = await requireFlow(actor, flowId);
817
+ const items = await deps.repository.listVersions(flow.id);
818
+ return {
819
+ flowId: flow.id,
820
+ // Marked here, off the flow row already in hand: `published_version_id` is the flow's one
821
+ // answer, and asking storage to join it in would be a second place that could disagree.
822
+ items: items.map((version) => ({
823
+ ...version,
824
+ published: version.id === flow.publishedVersionId,
825
+ })),
826
+ };
827
+ },
828
+ // One version with its graph (#144). The flow resolves first, so a version ID belonging to a
829
+ // flow this actor may not see answers exactly like one that does not exist — `requireVersion`
830
+ // already refuses a version filed under another flow, in the same words.
831
+ async getVersion(actor, input) {
832
+ await requireFlow(actor, input.flowId);
833
+ return await requireVersion(input.versionId, input.flowId);
834
+ },
812
835
  // What accesses what, for one level of the shared tree (#19). Flows answers it because the edges
813
- // live in flow graphs; Knowledge answers which nodes exist and who may see them, because the
814
- // tree and its ACLs are Knowledge's (ADR-0004 §1).
836
+ // live in flow graphs; The node service answers which nodes exist and who may see them, because the
837
+ // tree and its ACLs are the node service's (ADR-0004 §1).
815
838
  //
816
839
  // ⚠️ Every node passes an authorization before it is drawn, and a node that fails it is left out
817
840
  // entirely — no placeholder, no count, no edge. An edge to a grey box would already say that
@@ -821,7 +844,7 @@ export function createFlows(deps) {
821
844
  const nodes = new Map();
822
845
  const edges = [];
823
846
  const dropped = new Set();
824
- const knowledgeSeen = new Map();
847
+ const nodesSeen = new Map();
825
848
  const flowSeen = new Map();
826
849
  // The level being drawn, what was actually read of it, and how much of it the bound left
827
850
  // behind. `undefined` is the single-flow scope, which has no level and reads no list.
@@ -852,17 +875,17 @@ export function createFlows(deps) {
852
875
  // it — because the run path asks it too, and a document being archived must not start
853
876
  // refusing steps under a message that names the wrong reason. What a drawing leaves out and
854
877
  // what a run refuses are two decisions; only the first belongs to #19.
855
- async function knowledgeNode(nodeId) {
856
- if (!knowledgeSeen.has(nodeId)) {
857
- const found = await deps.visibleKnowledge(actor, nodeId);
858
- knowledgeSeen.set(nodeId, found && !found.archivedAt
878
+ async function seenNode(nodeId) {
879
+ if (!nodesSeen.has(nodeId)) {
880
+ const found = await deps.visibleNodes(actor, nodeId);
881
+ nodesSeen.set(nodeId, found && !found.archivedAt
859
882
  ? {
860
883
  node: { id: found.id, kind: found.kind, title: found.title, inScope: false },
861
884
  parentId: found.parentId,
862
885
  }
863
886
  : null);
864
887
  }
865
- return knowledgeSeen.get(nodeId) ?? null;
888
+ return nodesSeen.get(nodeId) ?? null;
866
889
  }
867
890
  // The rows are already in hand — every callee of the level was read in one statement below —
868
891
  // so this is the archived rule and nothing else.
@@ -884,7 +907,7 @@ export function createFlows(deps) {
884
907
  scopeFlows = [await requireFlow(actor, input.scope.flowId)];
885
908
  }
886
909
  else {
887
- const children = await deps.knowledgeChildren(actor, input.scope.folderId, input.limit);
910
+ const children = await deps.nodeChildren(actor, input.scope.folderId, input.limit);
888
911
  unread += children.total - children.items.length;
889
912
  for (const item of children.items) {
890
913
  levelIds.add(item.id);
@@ -921,9 +944,9 @@ export function createFlows(deps) {
921
944
  // The node lists rather than a walk of `graph.nodes` with two `kind` tests: an edge has to
922
945
  // name the step it comes from, so this is the one caller that needs the nodes themselves
923
946
  // and not the flattened `graphReferences`.
924
- for (const node of knowledgeNodes(version.graph)) {
947
+ for (const node of treeLinkNodes(version.graph)) {
925
948
  const resourceId = resourceIdOf(node);
926
- const target = await knowledgeNode(resourceId);
949
+ const target = await seenNode(resourceId);
927
950
  if (!target || !place(target.node, target.parentId))
928
951
  continue;
929
952
  edges.push({
@@ -979,7 +1002,7 @@ export function createFlows(deps) {
979
1002
  if (callee && !callee.archivedAt)
980
1003
  items.push(callee);
981
1004
  }
982
- // Ein aufgerufener Flow ruft selbst welche: dieselbe Frage, eine Ebene tiefer (#59).
1005
+ // A called flow calls flows of its own: the same question, one level deeper (#59).
983
1006
  return { items, withCalls: await callableCallers(actor, items) };
984
1007
  },
985
1008
  // "What this flow needs", straight out of the graph: no arithmetic, nothing that can go stale,
@@ -1017,24 +1040,24 @@ export function createFlows(deps) {
1017
1040
  return {
1018
1041
  flowId: flow.id,
1019
1042
  versionId: null,
1020
- knowledge: [],
1021
- hiddenKnowledge: 0,
1043
+ nodes: [],
1044
+ hiddenNodes: 0,
1022
1045
  tools: [],
1023
1046
  };
1024
1047
  }
1025
1048
  const version = await requireVersion(versionId, flow.id);
1026
1049
  const referenced = graphReferences(version.graph);
1027
- // ⚠️ The same lookup a Knowledge step passes through, not a second rule written for a list.
1050
+ // ⚠️ The same lookup a tree link passes through, not a second rule written for a list.
1028
1051
  // What it hands back is named; the difference between what was asked for and what came back
1029
1052
  // is a number, because a title is exactly what someone without access may not learn (#17,
1030
1053
  // #19). Tool names are not filtered: they come from a graph this actor may already read, and
1031
1054
  // whether the portal offers them is a question only their own token can answer.
1032
- const knowledge = await reachableKnowledge(actor, referenced.knowledge);
1055
+ const reachable = await reachableNodes(actor, referenced.nodes);
1033
1056
  return {
1034
1057
  flowId: flow.id,
1035
1058
  versionId: version.id,
1036
- knowledge,
1037
- hiddenKnowledge: referenced.knowledge.length - knowledge.length,
1059
+ nodes: reachable,
1060
+ hiddenNodes: referenced.nodes.length - reachable.length,
1038
1061
  tools: referenced.tools,
1039
1062
  };
1040
1063
  },
@@ -1194,11 +1217,11 @@ export function createFlows(deps) {
1194
1217
  return await requireFlow(actor, flow.id);
1195
1218
  const version = await requireVersion(input.versionId, flow.id);
1196
1219
  compileFlow(version.graph);
1197
- const wanted = [...new Set(knowledgeNodes(version.graph).map(resourceIdOf))];
1198
- const reachable = new Set((await reachableKnowledge(actor, wanted)).map((reference) => reference.id));
1220
+ const wanted = [...new Set(treeLinkNodes(version.graph).map(resourceIdOf))];
1221
+ const reachable = new Set((await reachableNodes(actor, wanted)).map((reference) => reference.id));
1199
1222
  for (const resourceId of wanted) {
1200
1223
  if (!reachable.has(resourceId)) {
1201
- throw new IntelError(409, "flow_knowledge_unavailable", `Knowledge reference is unavailable: ${resourceId}`);
1224
+ throw new IntelError(409, "flow_node_unavailable", `Node reference is unavailable: ${resourceId}`);
1202
1225
  }
1203
1226
  }
1204
1227
  for (const node of version.graph.nodes) {
@@ -1272,6 +1295,41 @@ export function createFlows(deps) {
1272
1295
  throw new IntelError(409, "flow_publish_conflict", "Flow could not be published");
1273
1296
  return await requireFlow(actor, flow.id);
1274
1297
  },
1298
+ /**
1299
+ * Withdraw the publication (#146) — the operation the refusal in `knowledge.ts` has promised
1300
+ * all along ("Change or unpublish them before narrowing the folder"). `published_version_id`
1301
+ * goes back to null and nothing else moves: every version is kept, and republishing any of
1302
+ * them is one `publish` away.
1303
+ *
1304
+ * What it means downstream is already decided by the checks that exist: new starts answer
1305
+ * `flow_not_published`, a caller's `validate` reports `flow_subflow_not_published` for the
1306
+ * dead call, and runs in flight keep their immutable version — publication decides what may
1307
+ * START, never what happens to what is already going (#112). The narrowing guard reads only
1308
+ * published graphs, so an unpublished caller frees its library folder.
1309
+ */
1310
+ async unpublish(actor, input) {
1311
+ const flow = await requireEdit(actor, input.flowId);
1312
+ const replayed = await deps.repository.findIdempotent(actor.id, "flows.unpublish", input.idempotencyKey);
1313
+ if (replayed)
1314
+ return await requireFlow(actor, flow.id);
1315
+ if (!flow.publishedVersionId) {
1316
+ throw new IntelError(409, "flow_not_published", "Flow has no published version");
1317
+ }
1318
+ const withdrawn = await deps.repository.unpublish({
1319
+ flowId: flow.id,
1320
+ versionId: flow.publishedVersionId,
1321
+ actorId: actor.id,
1322
+ idempotencyKey: input.idempotencyKey,
1323
+ auditId: deps.id(),
1324
+ occurredAt: deps.now().toISOString(),
1325
+ });
1326
+ // The write matched nothing: another writer withdrew the publication first. The state is
1327
+ // what this call asked for, but saying "done" would hide that the race happened.
1328
+ if (!withdrawn) {
1329
+ throw new IntelError(409, "flow_not_published", "Flow has no published version");
1330
+ }
1331
+ return await requireFlow(actor, flow.id);
1332
+ },
1275
1333
  async start(actor, input) {
1276
1334
  requireRun(actor);
1277
1335
  // ⚠️ The whole guarantee of a subflow sits on this line staying where it is. A called run is
@@ -1362,7 +1420,7 @@ export function createFlows(deps) {
1362
1420
  * from what opening a single run would say.
1363
1421
  *
1364
1422
  * What comes back is what a run did, never what it produced: `FlowRunSummary` carries no input
1365
- * and no output, because a run reaches Knowledge and tools with the rights of whoever started it.
1423
+ * and no output, because a run reaches nodes and tools with the rights of whoever started it.
1366
1424
  */
1367
1425
  async listRuns(actor, input) {
1368
1426
  // The history of a flow that has since been archived stays readable — the runs happened, and
@@ -1482,6 +1540,64 @@ export function createFlows(deps) {
1482
1540
  trail: await trailFor(run, version),
1483
1541
  };
1484
1542
  },
1543
+ /**
1544
+ * End a run on purpose (#145). Until this existed the only way off a parked manual step was
1545
+ * `completeStep` with `outcome: "failed"`, which recorded a step failure that never happened.
1546
+ * Cancelling writes no step row at all: the run ends, the step history stays true.
1547
+ *
1548
+ * ⚠️ Whoever may start a run may end it: the same Gate capability and the same `execute` the
1549
+ * start asked for, asked again now — a grant revoked since the start must not keep the holder
1550
+ * of an old run in control of it. Deliberately NOT `initiatedBy === actor.id`: a second person
1551
+ * with `execute` on the flow may end a colleague's stuck run, exactly as they could have
1552
+ * started the same run themselves.
1553
+ *
1554
+ * The child runs of a cancelled caller are cancelled with it, in the same write. A caller
1555
+ * standing on a subflow node is waiting for its child by design; ending the caller alone would
1556
+ * leave the child working for a result nobody will ever read.
1557
+ */
1558
+ async cancelRun(actor, input) {
1559
+ requireRun(actor);
1560
+ const replayed = await deps.repository.findIdempotent(actor.id, "flows.cancel", input.idempotencyKey);
1561
+ if (replayed) {
1562
+ const run = await deps.repository.getRunVisible(actor, replayed);
1563
+ if (run)
1564
+ return await step(actor, run);
1565
+ }
1566
+ const current = await deps.repository.getRunVisible(actor, input.runId);
1567
+ if (!current)
1568
+ throw new IntelError(404, "flow_run_not_found", "Flow run was not found");
1569
+ await requireExecute(actor, current.flowId);
1570
+ if (current.status !== "running" && current.status !== "queued") {
1571
+ throw new IntelError(409, "flow_run_terminal", "Flow run is already terminal");
1572
+ }
1573
+ const occurredAt = deps.now().toISOString();
1574
+ const result = await deps.repository.cancelRun({
1575
+ runId: current.id,
1576
+ flowId: current.flowId,
1577
+ actorId: actor.id,
1578
+ occurredAt,
1579
+ idempotencyKey: input.idempotencyKey,
1580
+ auditId: deps.id(),
1581
+ });
1582
+ if (result === "conflict") {
1583
+ throw new IntelError(409, "flow_run_terminal", "Flow run is already terminal");
1584
+ }
1585
+ // Every durable wait among the ended runs is woken, children included: each workflow reads
1586
+ // its terminal status and finishes now instead of stalling out on its own timeout.
1587
+ for (const cancelledId of result.cancelled) {
1588
+ await deps.runtime.signal(cancelledId);
1589
+ }
1590
+ const version = await requireVersion(current.versionId, current.flowId);
1591
+ // ⚠️ `runState`, not `step`: the write has happened, and asking permission again AFTER a
1592
+ // write is what turned completed work into a 404 for its own caller (#112).
1593
+ return await runState(actor, {
1594
+ ...current,
1595
+ status: "cancelled",
1596
+ currentNodeId: null,
1597
+ updatedAt: occurredAt,
1598
+ completedAt: occurredAt,
1599
+ }, version);
1600
+ },
1485
1601
  async completeStep(actor, input) {
1486
1602
  requireRun(actor);
1487
1603
  const replayed = await deps.repository.findIdempotent(actor.id, "flows.complete", input.idempotencyKey);
@@ -1,4 +1,4 @@
1
- import type { ArchiveFlowInput, CompleteFlowRunStepInput, CreateFlowInput, Flow, FlowDocument, FlowGraph, FlowPublishPreview, FlowRequirements, FlowRun, FlowRunHistory, FlowRunList, FlowRunStep, FlowValidation, FlowVersion, KnowledgeNode, ListFlowRunsInput, ListFlowsInput, PreviewFlowPublishInput, PublishFlowInput, RelationGraph, RelationGraphInput, ResourceVerb, SaveFlowVersionInput, StartFlowRunInput, UpdateFlowInput } from "@anchrd/intel-contract";
1
+ import type { ArchiveFlowInput, CancelFlowRunInput, CompleteFlowRunStepInput, CreateFlowInput, Flow, FlowDocument, FlowGraph, FlowPublishPreview, FlowRequirements, FlowRun, FlowRunHistory, FlowRunList, FlowRunStep, FlowValidation, FlowVersion, FlowVersionList, FlowVersionSummary, GetFlowVersionInput, ListFlowRunsInput, ListFlowsInput, Node, PreviewFlowPublishInput, PublishFlowInput, RelationGraph, RelationGraphInput, ResourceVerb, SaveFlowVersionInput, StartFlowRunInput, UnpublishFlowInput, UpdateFlowInput } from "@anchrd/intel-contract";
2
2
  export type FlowPrincipal = Pick<FlowActor, "id" | "email" | "isAdmin">;
3
3
  export type FlowCallReach = "subtree" | "library" | "out-of-reach";
4
4
  export interface FlowRunChainEntry {
@@ -40,7 +40,7 @@ export interface FlowActor {
40
40
  isAdmin?: boolean;
41
41
  }
42
42
  export type FlowVerb = Extract<ResourceVerb, "read" | "write" | "execute">;
43
- export type FlowOperation = "flows.create" | "flows.update" | "flows.archive" | "flows.save" | "flows.publish" | "flows.run" | "flows.complete";
43
+ export type FlowOperation = "flows.create" | "flows.update" | "flows.archive" | "flows.save" | "flows.publish" | "flows.run" | "flows.complete" | "flows.cancel" | "flows.unpublish";
44
44
  export interface BoundedLevel<T> {
45
45
  items: T[];
46
46
  total: number;
@@ -57,7 +57,7 @@ export interface FlowRepository {
57
57
  visible: string[];
58
58
  hidden: number;
59
59
  }>;
60
- knowledgeReferences(actor: FlowPrincipal, folderId: string): Promise<string[]>;
60
+ nodeReferences(actor: FlowPrincipal, folderId: string): Promise<string[]>;
61
61
  publishedCallees(flowId: string): Promise<{
62
62
  title: string;
63
63
  calleeIds: string[];
@@ -86,6 +86,7 @@ export interface FlowRepository {
86
86
  auditId: string;
87
87
  }): Promise<"conflict" | Flow>;
88
88
  getVersion(versionId: string): Promise<FlowVersion | null>;
89
+ listVersions(flowId: string): Promise<Omit<FlowVersionSummary, "published">[]>;
89
90
  getVersions(versionIds: string[]): Promise<FlowVersion[]>;
90
91
  insertVersion(input: {
91
92
  version: FlowVersion;
@@ -102,6 +103,14 @@ export interface FlowRepository {
102
103
  auditId: string;
103
104
  occurredAt: string;
104
105
  }): Promise<boolean>;
106
+ unpublish(input: {
107
+ flowId: string;
108
+ versionId: string;
109
+ actorId: string;
110
+ idempotencyKey: string;
111
+ auditId: string;
112
+ occurredAt: string;
113
+ }): Promise<boolean>;
105
114
  insertRun(input: {
106
115
  run: FlowRun;
107
116
  actorId: string;
@@ -115,6 +124,26 @@ export interface FlowRepository {
115
124
  visibleCallRuns(actor: FlowActor, sites: FlowCallSite[]): Promise<FlowVisibleCall[]>;
116
125
  findChildRun(parentRunId: string, parentNodeId: string): Promise<FlowRun | null>;
117
126
  runChain(runId: string): Promise<FlowRunChainEntry[]>;
127
+ /**
128
+ * End a run and every non-terminal run below it in one write (#145). The recursion follows
129
+ * `parent_run_id`, so cancelling a caller cannot leave a called run working for a result nobody
130
+ * will ever read. No step row is written for any of them: cancelling is not a step outcome, and
131
+ * inventing a failed step is exactly what this operation exists to replace.
132
+ *
133
+ * `"conflict"` means the run was already terminal when the write arrived — the caller's status
134
+ * check raced another writer — and nothing was changed. `cancelled` names every run this write
135
+ * ended, so the service can wake each of their durable waits.
136
+ */
137
+ cancelRun(input: {
138
+ runId: string;
139
+ flowId: string;
140
+ actorId: string;
141
+ occurredAt: string;
142
+ idempotencyKey: string;
143
+ auditId: string;
144
+ }): Promise<"conflict" | {
145
+ cancelled: string[];
146
+ }>;
118
147
  advanceRun(input: {
119
148
  run: FlowRun;
120
149
  expectedNodeId: string;
@@ -140,25 +169,25 @@ export interface FlowDeps {
140
169
  folderAccess(actor: FlowActor, folderId: string): Promise<FolderAccess>;
141
170
  /**
142
171
  * The children of one folder of the shared tree as this actor may see them, without their bodies.
143
- * `null` is the root. Knowledge answers, for the same reason `folderAccess` does.
172
+ * `null` is the root. The node service answers, for the same reason `folderAccess` does.
144
173
  *
145
174
  * ⚠️ `limit` bounds the read, not its result: its one caller draws a bounded picture, and a folder
146
175
  * of a thousand documents must not be loaded whole for it (#30). What the bound left behind comes
147
176
  * back as `total`, counted among the nodes this actor may see and no others — a number that
148
177
  * included the rest would say that the rest is there.
149
178
  */
150
- knowledgeChildren(actor: FlowActor, folderId: string | null, limit: number): Promise<BoundedLevel<KnowledgeNode>>;
179
+ nodeChildren(actor: FlowActor, folderId: string | null, limit: number): Promise<BoundedLevel<Node>>;
151
180
  /**
152
181
  * One node as this actor may see it. `null` means "this actor cannot reach it", never "it is
153
182
  * gone" — the relation graph must then leave it out altogether rather than draw a placeholder,
154
183
  * because the edge alone would already give away that it exists (#19).
155
184
  *
156
185
  * ⚠️ Three jobs, one door, on purpose. The relation graph draws what comes back here; the
157
- * requirements list names it; and every Knowledge step of every run is authorized by it, nested
186
+ * requirements list names it; and every tree link of every run is authorized by it, nested
158
187
  * calls included (ADR-0004 §4). A second lookup beside it is how a drawing or a message ends up
159
188
  * kinder than the door it describes — which is what #17 and #19 were sent back for.
160
189
  */
161
- visibleKnowledge(actor: FlowActor, nodeId: string): Promise<KnowledgeNode | null>;
190
+ visibleNodes(actor: FlowActor, nodeId: string): Promise<Node | null>;
162
191
  toolFingerprint(actor: FlowActor, toolName: string): Promise<string | null>;
163
192
  unavailableTools(actor: FlowActor, toolNames: string[]): Promise<string[]>;
164
193
  }
@@ -169,6 +198,8 @@ export interface FlowService {
169
198
  withCalls: string[];
170
199
  }>;
171
200
  get(actor: FlowActor, flowId: string): Promise<FlowDocument>;
201
+ listVersions(actor: FlowActor, flowId: string): Promise<FlowVersionList>;
202
+ getVersion(actor: FlowActor, input: GetFlowVersionInput): Promise<FlowVersion>;
172
203
  listCalls(actor: FlowActor, flowId: string): Promise<{
173
204
  items: Flow[];
174
205
  withCalls: string[];
@@ -182,11 +213,13 @@ export interface FlowService {
182
213
  save(actor: FlowActor, input: SaveFlowVersionInput): Promise<FlowDocument>;
183
214
  previewPublish(actor: FlowActor, input: PreviewFlowPublishInput): Promise<FlowPublishPreview>;
184
215
  publish(actor: FlowActor, input: PublishFlowInput): Promise<Flow>;
216
+ unpublish(actor: FlowActor, input: UnpublishFlowInput): Promise<Flow>;
185
217
  start(actor: FlowActor, input: StartFlowRunInput): Promise<FlowRunStep>;
186
218
  getRun(actor: FlowActor, runId: string): Promise<FlowRunStep>;
187
219
  listRuns(actor: FlowActor, input: ListFlowRunsInput): Promise<FlowRunList>;
188
220
  listRunSteps(actor: FlowActor, runId: string): Promise<FlowRunHistory>;
189
221
  completeStep(actor: FlowActor, input: CompleteFlowRunStepInput): Promise<FlowRunStep>;
222
+ cancelRun(actor: FlowActor, input: CancelFlowRunInput): Promise<FlowRunStep>;
190
223
  }
191
224
  export interface CompiledFlow {
192
225
  graph: FlowGraph;
@@ -4,5 +4,6 @@ import type { HttpDeps } from "./http.types.js";
4
4
  export declare function createHttp(deps: HttpDeps): Hono<{
5
5
  Variables: {
6
6
  authorization: Authorized;
7
+ token: string;
7
8
  };
8
9
  }, import("hono/types").BlankSchema, "/">;