@anchrd/intel-api 0.6.7 → 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.
- package/README.md +44 -3
- package/dist/adapters/cloudflare/cloudflare.js +50 -16
- package/dist/adapters/cloudflare/cloudflare.types.d.ts +11 -0
- package/dist/adapters/content/content.d.ts +1 -1
- package/dist/adapters/db/db-flows.js +148 -20
- package/dist/adapters/db/db-grants.d.ts +13 -2
- package/dist/adapters/db/db-grants.js +25 -8
- package/dist/adapters/db/db-indexing.d.ts +2 -2
- package/dist/adapters/db/db-indexing.js +26 -19
- package/dist/adapters/db/db.d.ts +3 -3
- package/dist/adapters/db/db.js +431 -118
- package/dist/adapters/gate-applications/gate-applications.d.ts +23 -0
- package/dist/adapters/gate-applications/gate-applications.js +66 -0
- package/dist/adapters/index-queue/index-queue.d.ts +1 -1
- package/dist/adapters/index-queue/index-queue.js +2 -2
- package/dist/adapters/semantic-index/semantic-index.types.d.ts +2 -2
- package/dist/agent-runtime/agent-runtime.d.ts +16 -0
- package/dist/agent-runtime/agent-runtime.js +76 -0
- package/dist/agent-runtime/agent-runtime.types.d.ts +57 -0
- package/dist/bundle/bundle.d.ts +4 -0
- package/dist/bundle/bundle.js +1035 -0
- package/dist/bundle/bundle.types.d.ts +33 -0
- package/dist/bundle/bundle.types.js +1 -0
- package/dist/cli/cli.js +10 -1
- package/dist/flows/flows.d.ts +8 -8
- package/dist/flows/flows.js +158 -42
- package/dist/flows/flows.types.d.ts +40 -7
- package/dist/http/http.d.ts +1 -0
- package/dist/http/http.js +324 -61
- package/dist/http/http.types.d.ts +6 -2
- package/dist/indexing/indexing.js +14 -2
- package/dist/indexing/indexing.types.d.ts +2 -2
- package/dist/intel/intel.js +12 -3
- package/dist/intel/intel.types.d.ts +6 -2
- package/dist/mcp/mcp.js +483 -124
- package/dist/mcp/mcp.types.d.ts +11 -2
- package/dist/nodes/nodes.d.ts +2 -0
- package/dist/nodes/nodes.js +1337 -0
- package/dist/nodes/nodes.types.d.ts +314 -0
- package/dist/nodes/nodes.types.js +1 -0
- package/migrations/0011_one_name_for_the_tree.sql +53 -0
- package/migrations/0012_table_snapshots.sql +29 -0
- package/migrations/0013_agents_in_the_tree.sql +76 -0
- package/migrations/0014_agent_applications.sql +25 -0
- package/package.json +3 -2
- package/dist/knowledge/knowledge.d.ts +0 -2
- package/dist/knowledge/knowledge.js +0 -761
- package/dist/knowledge/knowledge.types.d.ts +0 -198
- /package/dist/{knowledge/knowledge.types.js → agent-runtime/agent-runtime.types.js} +0 -0
- /package/dist/{knowledge → nodes}/document-links/document-links.d.ts +0 -0
- /package/dist/{knowledge → nodes}/document-links/document-links.js +0 -0
|
@@ -0,0 +1,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
|
|
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.");
|
package/dist/flows/flows.d.ts
CHANGED
|
@@ -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
|
|
7
|
-
kind:
|
|
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
|
|
13
|
-
type
|
|
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
|
|
21
|
+
export declare function treeLinkNodes(graph: FlowGraph): TreeLinkNode[];
|
|
22
22
|
export declare function toolNodes(graph: FlowGraph): ToolStepNode[];
|
|
23
|
-
export declare function resourceIdOf(node:
|
|
23
|
+
export declare function resourceIdOf(node: TreeLinkNode): string;
|
|
24
24
|
export declare function calleeIds(graph: FlowGraph): string[];
|
|
25
25
|
/**
|
|
26
|
-
* The
|
|
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
|
-
|
|
32
|
+
nodes: string[];
|
|
33
33
|
tools: string[];
|
|
34
34
|
};
|
|
35
35
|
export declare function compileFlow(graph: FlowGraph): CompiledFlow;
|
package/dist/flows/flows.js
CHANGED
|
@@ -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
|
|
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
|
|
26
|
-
return graph.nodes.filter((node) =>
|
|
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
|
|
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
|
|
54
|
+
const nodes = [];
|
|
55
55
|
const tools = [];
|
|
56
|
-
for (const node of
|
|
57
|
-
if (!
|
|
58
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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, "
|
|
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.
|
|
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
|
|
527
|
+
async function reachableNodes(actor, resourceIds) {
|
|
528
528
|
const reachable = [];
|
|
529
529
|
for (const resourceId of new Set(resourceIds)) {
|
|
530
|
-
const node = await deps.
|
|
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
|
|
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
|
|
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(
|
|
550
|
+
const wanted = [...new Set(treeLinkNodes({ ...graph, nodes: attached }).map(resourceIdOf))];
|
|
551
551
|
if (wanted.length) {
|
|
552
|
-
const reachable = await
|
|
552
|
+
const reachable = await reachableNodes(actor, wanted);
|
|
553
553
|
if (reachable.length !== wanted.length) {
|
|
554
|
-
throw new IntelError(403, "
|
|
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;
|
|
814
|
-
// tree and its ACLs are
|
|
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
|
|
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
|
|
856
|
-
if (!
|
|
857
|
-
const found = await deps.
|
|
858
|
-
|
|
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
|
|
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.
|
|
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
|
|
947
|
+
for (const node of treeLinkNodes(version.graph)) {
|
|
925
948
|
const resourceId = resourceIdOf(node);
|
|
926
|
-
const target = await
|
|
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
|
-
//
|
|
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
|
-
|
|
1021
|
-
|
|
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
|
|
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
|
|
1055
|
+
const reachable = await reachableNodes(actor, referenced.nodes);
|
|
1033
1056
|
return {
|
|
1034
1057
|
flowId: flow.id,
|
|
1035
1058
|
versionId: version.id,
|
|
1036
|
-
|
|
1037
|
-
|
|
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(
|
|
1198
|
-
const reachable = new Set((await
|
|
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, "
|
|
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
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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;
|