@anchrd/intel-api 0.2.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 +96 -0
- package/bin/intel.mjs +134 -0
- package/dist/adapters/cloudflare/cloudflare-flow-workflow.d.ts +21 -0
- package/dist/adapters/cloudflare/cloudflare-flow-workflow.js +39 -0
- package/dist/adapters/cloudflare/cloudflare.d.ts +8 -0
- package/dist/adapters/cloudflare/cloudflare.js +200 -0
- package/dist/adapters/cloudflare/cloudflare.types.d.ts +29 -0
- package/dist/adapters/cloudflare/cloudflare.types.js +1 -0
- package/dist/adapters/content/content.d.ts +3 -0
- package/dist/adapters/content/content.js +22 -0
- package/dist/adapters/content/content.types.d.ts +14 -0
- package/dist/adapters/content/content.types.js +1 -0
- package/dist/adapters/db/db-flows.d.ts +6 -0
- package/dist/adapters/db/db-flows.js +466 -0
- package/dist/adapters/db/db-indexing.d.ts +3 -0
- package/dist/adapters/db/db-indexing.js +70 -0
- package/dist/adapters/db/db-oauth.d.ts +3 -0
- package/dist/adapters/db/db-oauth.js +26 -0
- package/dist/adapters/db/db-tools.d.ts +6 -0
- package/dist/adapters/db/db-tools.js +200 -0
- package/dist/adapters/db/db.d.ts +6 -0
- package/dist/adapters/db/db.js +755 -0
- package/dist/adapters/db/db.types.d.ts +16 -0
- package/dist/adapters/db/db.types.js +1 -0
- package/dist/adapters/document-converter/document-converter.d.ts +2 -0
- package/dist/adapters/document-converter/document-converter.js +25 -0
- package/dist/adapters/document-converter/document-converter.types.d.ts +9 -0
- package/dist/adapters/document-converter/document-converter.types.js +1 -0
- package/dist/adapters/flow-runtime/flow-runtime.d.ts +3 -0
- package/dist/adapters/flow-runtime/flow-runtime.js +22 -0
- package/dist/adapters/flow-runtime/flow-runtime.types.d.ts +19 -0
- package/dist/adapters/flow-runtime/flow-runtime.types.js +1 -0
- package/dist/adapters/index-queue/index-queue.d.ts +10 -0
- package/dist/adapters/index-queue/index-queue.js +12 -0
- package/dist/adapters/index-queue/index-queue.types.d.ts +11 -0
- package/dist/adapters/index-queue/index-queue.types.js +1 -0
- package/dist/adapters/json-schema/json-schema.d.ts +2 -0
- package/dist/adapters/json-schema/json-schema.js +31 -0
- package/dist/adapters/openid/openid.d.ts +5 -0
- package/dist/adapters/openid/openid.js +159 -0
- package/dist/adapters/portal-tokens/portal-tokens.d.ts +8 -0
- package/dist/adapters/portal-tokens/portal-tokens.js +71 -0
- package/dist/adapters/remote-tools/remote-tools.d.ts +5 -0
- package/dist/adapters/remote-tools/remote-tools.js +109 -0
- package/dist/adapters/semantic-index/semantic-index.d.ts +2 -0
- package/dist/adapters/semantic-index/semantic-index.js +57 -0
- package/dist/adapters/semantic-index/semantic-index.types.d.ts +32 -0
- package/dist/adapters/semantic-index/semantic-index.types.js +1 -0
- package/dist/adapters/session-cookie/session-cookie.d.ts +5 -0
- package/dist/adapters/session-cookie/session-cookie.js +81 -0
- package/dist/adapters/tool-source-policy/tool-source-policy.d.ts +1 -0
- package/dist/adapters/tool-source-policy/tool-source-policy.js +22 -0
- package/dist/auth/auth.d.ts +2 -0
- package/dist/auth/auth.js +176 -0
- package/dist/auth/auth.types.d.ts +104 -0
- package/dist/auth/auth.types.js +1 -0
- package/dist/build/build.d.ts +2 -0
- package/dist/build/build.js +125 -0
- package/dist/build/build.types.d.ts +17 -0
- package/dist/build/build.types.js +1 -0
- package/dist/cli/cli.d.ts +4 -0
- package/dist/cli/cli.js +141 -0
- package/dist/cli/cli.types.d.ts +14 -0
- package/dist/cli/cli.types.js +1 -0
- package/dist/flows/flows.d.ts +4 -0
- package/dist/flows/flows.js +409 -0
- package/dist/flows/flows.types.d.ts +108 -0
- package/dist/flows/flows.types.js +1 -0
- package/dist/http/http.d.ts +8 -0
- package/dist/http/http.js +286 -0
- package/dist/http/http.types.d.ts +14 -0
- package/dist/http/http.types.js +1 -0
- package/dist/indexing/indexing.d.ts +6 -0
- package/dist/indexing/indexing.js +48 -0
- package/dist/indexing/indexing.types.d.ts +10 -0
- package/dist/indexing/indexing.types.js +1 -0
- package/dist/intel/intel.d.ts +3 -0
- package/dist/intel/intel.js +77 -0
- package/dist/intel/intel.types.d.ts +14 -0
- package/dist/intel/intel.types.js +1 -0
- package/dist/knowledge/knowledge.d.ts +2 -0
- package/dist/knowledge/knowledge.js +479 -0
- package/dist/knowledge/knowledge.types.d.ts +162 -0
- package/dist/knowledge/knowledge.types.js +1 -0
- package/dist/mcp/mcp.d.ts +2 -0
- package/dist/mcp/mcp.js +484 -0
- package/dist/mcp/mcp.types.d.ts +10 -0
- package/dist/mcp/mcp.types.js +1 -0
- package/dist/prepare/prepare.d.ts +4 -0
- package/dist/prepare/prepare.js +16 -0
- package/dist/prepare/prepare.types.d.ts +9 -0
- package/dist/prepare/prepare.types.js +1 -0
- package/dist/shared/gate-authorization/gate-authorization.d.ts +4 -0
- package/dist/shared/gate-authorization/gate-authorization.js +19 -0
- package/dist/shared/intel-error/intel-error.d.ts +5 -0
- package/dist/shared/intel-error/intel-error.js +10 -0
- package/dist/shared/problem-details/problem-details.d.ts +8 -0
- package/dist/shared/problem-details/problem-details.js +4 -0
- package/dist/shared/safe-return-path/safe-return-path.d.ts +2 -0
- package/dist/shared/safe-return-path/safe-return-path.js +11 -0
- package/dist/shared/sha256/sha256.d.ts +1 -0
- package/dist/shared/sha256/sha256.js +9 -0
- package/dist/tools/tools.d.ts +2 -0
- package/dist/tools/tools.js +107 -0
- package/dist/tools/tools.types.d.ts +56 -0
- package/dist/tools/tools.types.js +1 -0
- package/examples/branding/favicon.svg +5 -0
- package/examples/branding/logo.svg +5 -0
- package/examples/branding/theme.css +16 -0
- package/examples/dev.vars.example +7 -0
- package/examples/intel.json +9 -0
- package/migrations/0000_intel_foundation.sql +203 -0
- package/migrations/0001_portal_native_tools.sql +21 -0
- package/package.json +62 -0
package/dist/cli/cli.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { createBuild } from "../build/build.js";
|
|
2
|
+
import { createPrepare } from "../prepare/prepare.js";
|
|
3
|
+
const interfaces = [
|
|
4
|
+
{ handle: "intel", functions: ["use", "admin"] },
|
|
5
|
+
{ handle: "knowledge", functions: ["read", "create", "write", "share"] },
|
|
6
|
+
{
|
|
7
|
+
handle: "flows",
|
|
8
|
+
functions: ["read", "create", "write", "publish", "run", "approve", "share"],
|
|
9
|
+
},
|
|
10
|
+
{ handle: "tools", functions: ["read", "test", "execute", "admin"] },
|
|
11
|
+
{ handle: "intel-tools", functions: ["connect"] },
|
|
12
|
+
];
|
|
13
|
+
const usage = `Usage: intel <prepare|bootstrap|build|doctor|reindex>
|
|
14
|
+
intel prepare Copy versioned D1 migrations to .intel/migrations
|
|
15
|
+
intel bootstrap Idempotently declare Intel interfaces in Gate
|
|
16
|
+
intel build Apply intel.json and atomically build the customer UI
|
|
17
|
+
intel doctor Check packages, environment, migrations, UI, and Gate interfaces
|
|
18
|
+
intel reindex Request a full authorized search-index rebuild`;
|
|
19
|
+
export function createCli(deps) {
|
|
20
|
+
const envValue = (name) => deps.env[name]?.trim() || null;
|
|
21
|
+
const fail = (message) => {
|
|
22
|
+
deps.log(`Error: ${message}`);
|
|
23
|
+
return 1;
|
|
24
|
+
};
|
|
25
|
+
const within = async (work, milliseconds = 10_000) => {
|
|
26
|
+
let timer;
|
|
27
|
+
try {
|
|
28
|
+
return await Promise.race([
|
|
29
|
+
work,
|
|
30
|
+
new Promise((_resolve, reject) => {
|
|
31
|
+
timer = setTimeout(() => reject(new Error("operation timed out")), milliseconds);
|
|
32
|
+
}),
|
|
33
|
+
]);
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
if (timer)
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
async function bootstrap() {
|
|
41
|
+
const gateUrl = envValue("GATE_URL");
|
|
42
|
+
const serviceKey = envValue("GATE_SERVICE_KEY") ?? envValue("GATE_KEY");
|
|
43
|
+
if (!gateUrl || !serviceKey) {
|
|
44
|
+
return fail("GATE_URL and GATE_SERVICE_KEY must be set.");
|
|
45
|
+
}
|
|
46
|
+
const gate = deps.createGateInterfaces(gateUrl, serviceKey);
|
|
47
|
+
for (const entry of interfaces)
|
|
48
|
+
await gate.set(entry.handle, [...entry.functions]);
|
|
49
|
+
deps.log(`Declared ${interfaces.length} Intel interfaces in Gate. No grants were changed.`);
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
async function doctor() {
|
|
53
|
+
const failures = [];
|
|
54
|
+
const gateUrl = envValue("GATE_URL");
|
|
55
|
+
const intelUrl = envValue("INTEL_URL");
|
|
56
|
+
const serviceKey = envValue("GATE_SERVICE_KEY") ?? envValue("GATE_KEY");
|
|
57
|
+
const sessionSecret = envValue("INTEL_SESSION_SECRET");
|
|
58
|
+
const toolSourceOrigins = envValue("TOOL_SOURCE_ORIGINS");
|
|
59
|
+
const portalUrl = envValue("MCP_PORTAL_URL");
|
|
60
|
+
if (!gateUrl)
|
|
61
|
+
failures.push("GATE_URL is missing");
|
|
62
|
+
if (!intelUrl)
|
|
63
|
+
failures.push("INTEL_URL is missing");
|
|
64
|
+
if (!serviceKey)
|
|
65
|
+
failures.push("GATE_SERVICE_KEY is missing");
|
|
66
|
+
if (!toolSourceOrigins)
|
|
67
|
+
failures.push("TOOL_SOURCE_ORIGINS is missing");
|
|
68
|
+
// Tools are optional for a deployment that only uses Knowledge and Flows, so a missing portal
|
|
69
|
+
// is reported as a warning rather than a failure.
|
|
70
|
+
if (!portalUrl) {
|
|
71
|
+
deps.log("Note: MCP_PORTAL_URL is unset, so the Tools area stays empty.");
|
|
72
|
+
}
|
|
73
|
+
else if (toolSourceOrigins && !toolSourceOrigins.includes(new URL(portalUrl).origin)) {
|
|
74
|
+
failures.push("MCP_PORTAL_URL origin is missing from TOOL_SOURCE_ORIGINS");
|
|
75
|
+
}
|
|
76
|
+
if (!sessionSecret || new TextEncoder().encode(sessionSecret).byteLength < 32) {
|
|
77
|
+
failures.push("INTEL_SESSION_SECRET must contain at least 32 bytes");
|
|
78
|
+
}
|
|
79
|
+
if (!(await deps.resolveUi()))
|
|
80
|
+
failures.push("@anchrd/intel-ui is not installed");
|
|
81
|
+
if (!(await deps.resolveMigrations()))
|
|
82
|
+
failures.push("Intel migrations are not installed");
|
|
83
|
+
if (gateUrl && serviceKey) {
|
|
84
|
+
try {
|
|
85
|
+
const current = await within(deps.createGateInterfaces(gateUrl, serviceKey).list());
|
|
86
|
+
for (const expected of interfaces) {
|
|
87
|
+
const actual = current.find((entry) => entry.handle === expected.handle);
|
|
88
|
+
const missing = expected.functions.filter((fn) => !actual?.functions.includes(fn));
|
|
89
|
+
if (missing.length)
|
|
90
|
+
failures.push(`Gate interface ${expected.handle} misses ${missing}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
failures.push("Gate interface check failed");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (failures.length) {
|
|
98
|
+
for (const failure of failures)
|
|
99
|
+
deps.log(`FAIL ${failure}`);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
deps.log("OK Intel packages, configuration, and Gate interfaces are ready.");
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
async function reindex() {
|
|
106
|
+
const intelUrl = envValue("INTEL_URL")?.replace(/\/+$/, "");
|
|
107
|
+
const token = envValue("INTEL_OPERATOR_TOKEN");
|
|
108
|
+
if (!intelUrl || !token) {
|
|
109
|
+
return fail("INTEL_URL and a short-lived INTEL_OPERATOR_TOKEN must be set.");
|
|
110
|
+
}
|
|
111
|
+
const response = await deps.fetch(`${intelUrl}/api/v1/search/reindex`, {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
114
|
+
body: "{}",
|
|
115
|
+
signal: AbortSignal.timeout(10_000),
|
|
116
|
+
});
|
|
117
|
+
if (!response.ok)
|
|
118
|
+
return fail(`reindex responded with ${response.status}`);
|
|
119
|
+
deps.log("Intel accepted the reindex request.");
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
async run(argv) {
|
|
124
|
+
switch (argv[0]) {
|
|
125
|
+
case "prepare":
|
|
126
|
+
return await createPrepare(deps).prepare();
|
|
127
|
+
case "bootstrap":
|
|
128
|
+
return await bootstrap();
|
|
129
|
+
case "build":
|
|
130
|
+
return await createBuild(deps).build();
|
|
131
|
+
case "doctor":
|
|
132
|
+
return await doctor();
|
|
133
|
+
case "reindex":
|
|
134
|
+
return await reindex();
|
|
135
|
+
default:
|
|
136
|
+
deps.log(usage);
|
|
137
|
+
return argv[0] ? 1 : 0;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BuildDeps } from "../build/build.types.js";
|
|
2
|
+
import type { PrepareDeps } from "../prepare/prepare.types.js";
|
|
3
|
+
export interface GateInterfaces {
|
|
4
|
+
list(): Promise<Array<{
|
|
5
|
+
handle: string;
|
|
6
|
+
functions: string[];
|
|
7
|
+
}>>;
|
|
8
|
+
set(handle: string, functions: string[]): Promise<unknown>;
|
|
9
|
+
}
|
|
10
|
+
export interface CliDeps extends BuildDeps, PrepareDeps {
|
|
11
|
+
env: Record<string, string | undefined>;
|
|
12
|
+
fetch: typeof fetch;
|
|
13
|
+
createGateInterfaces(url: string, serviceKey: string): GateInterfaces;
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FlowGraph } from "@anchrd/intel-contract";
|
|
2
|
+
import type { CompiledFlow, FlowDeps, FlowService } from "./flows.types.js";
|
|
3
|
+
export declare function compileFlow(graph: FlowGraph): CompiledFlow;
|
|
4
|
+
export declare function createFlows(deps: FlowDeps): FlowService;
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { IntelError } from "../shared/intel-error/intel-error.js";
|
|
2
|
+
function invalid(detail) {
|
|
3
|
+
throw new IntelError(400, "flow_graph_invalid", detail);
|
|
4
|
+
}
|
|
5
|
+
export function compileFlow(graph) {
|
|
6
|
+
const nodes = new Map();
|
|
7
|
+
for (const node of graph.nodes) {
|
|
8
|
+
if (nodes.has(node.id))
|
|
9
|
+
invalid(`Duplicate node ID: ${node.id}`);
|
|
10
|
+
nodes.set(node.id, node);
|
|
11
|
+
}
|
|
12
|
+
const edgeIds = new Set();
|
|
13
|
+
const incoming = new Map();
|
|
14
|
+
const outgoing = new Map();
|
|
15
|
+
for (const edge of graph.edges) {
|
|
16
|
+
if (edgeIds.has(edge.id))
|
|
17
|
+
invalid(`Duplicate edge ID: ${edge.id}`);
|
|
18
|
+
edgeIds.add(edge.id);
|
|
19
|
+
if (!nodes.has(edge.source) || !nodes.has(edge.target))
|
|
20
|
+
invalid(`Dangling edge: ${edge.id}`);
|
|
21
|
+
incoming.set(edge.target, (incoming.get(edge.target) ?? 0) + 1);
|
|
22
|
+
outgoing.set(edge.source, [...(outgoing.get(edge.source) ?? []), edge]);
|
|
23
|
+
}
|
|
24
|
+
const triggers = graph.nodes.filter((node) => node.kind === "trigger");
|
|
25
|
+
if (triggers.length !== 1)
|
|
26
|
+
invalid("A flow requires exactly one trigger");
|
|
27
|
+
if (!graph.nodes.some((node) => node.kind === "output"))
|
|
28
|
+
invalid("A flow requires an output");
|
|
29
|
+
const trigger = triggers[0];
|
|
30
|
+
if (!trigger)
|
|
31
|
+
invalid("A flow requires exactly one trigger");
|
|
32
|
+
for (const node of graph.nodes) {
|
|
33
|
+
const parents = incoming.get(node.id) ?? 0;
|
|
34
|
+
const children = outgoing.get(node.id) ?? [];
|
|
35
|
+
if (node.kind === "trigger" && parents !== 0)
|
|
36
|
+
invalid("The trigger cannot have an incoming edge");
|
|
37
|
+
if (node.kind !== "trigger" && parents !== 1) {
|
|
38
|
+
invalid(`Node ${node.id} requires exactly one incoming edge`);
|
|
39
|
+
}
|
|
40
|
+
if (node.kind === "output" && children.length !== 0)
|
|
41
|
+
invalid(`Output ${node.id} must be terminal`);
|
|
42
|
+
if (node.kind === "condition" || node.kind === "approval") {
|
|
43
|
+
if (children.length < 2)
|
|
44
|
+
invalid(`Node ${node.id} requires at least two branches`);
|
|
45
|
+
const handles = new Set(children.map((edge) => edge.sourceHandle));
|
|
46
|
+
if (handles.has(null) || handles.size !== children.length) {
|
|
47
|
+
invalid(`Node ${node.id} requires unique branch handles`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (node.kind !== "output") {
|
|
51
|
+
if (children.length !== 1)
|
|
52
|
+
invalid(`Node ${node.id} requires exactly one outgoing edge`);
|
|
53
|
+
if (children[0]?.sourceHandle !== null)
|
|
54
|
+
invalid(`Node ${node.id} cannot define a branch handle`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const visited = new Set();
|
|
58
|
+
const active = new Set();
|
|
59
|
+
function visit(nodeId) {
|
|
60
|
+
if (active.has(nodeId))
|
|
61
|
+
invalid("Flow graphs cannot contain cycles");
|
|
62
|
+
if (visited.has(nodeId))
|
|
63
|
+
return;
|
|
64
|
+
active.add(nodeId);
|
|
65
|
+
for (const edge of outgoing.get(nodeId) ?? [])
|
|
66
|
+
visit(edge.target);
|
|
67
|
+
active.delete(nodeId);
|
|
68
|
+
visited.add(nodeId);
|
|
69
|
+
}
|
|
70
|
+
visit(trigger.id);
|
|
71
|
+
if (visited.size !== graph.nodes.length)
|
|
72
|
+
invalid("Every node must be reachable from the trigger");
|
|
73
|
+
return { graph, triggerId: trigger.id };
|
|
74
|
+
}
|
|
75
|
+
function nodeFor(version, nodeId) {
|
|
76
|
+
return nodeId ? (version.graph.nodes.find((node) => node.id === nodeId) ?? null) : null;
|
|
77
|
+
}
|
|
78
|
+
export function createFlows(deps) {
|
|
79
|
+
async function requireFlow(actor, flowId) {
|
|
80
|
+
const flow = await deps.repository.getVisible(actor, flowId);
|
|
81
|
+
if (!flow || flow.archivedAt)
|
|
82
|
+
throw new IntelError(404, "flow_not_found", "Flow was not found");
|
|
83
|
+
return flow;
|
|
84
|
+
}
|
|
85
|
+
// Every surface maps the Gate capability into the actor, so the rule lives here once instead of
|
|
86
|
+
// being restated per surface. Checked before any storage access.
|
|
87
|
+
function requireRun(actor) {
|
|
88
|
+
if (!actor.canRun) {
|
|
89
|
+
throw new IntelError(403, "flow_run_forbidden", "Flow run permission is required");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function requireEdit(actor, flowId) {
|
|
93
|
+
const flow = await requireFlow(actor, flowId);
|
|
94
|
+
if (!(await deps.repository.canEdit(actor, flowId))) {
|
|
95
|
+
throw new IntelError(403, "flow_edit_forbidden", "Flow cannot be edited");
|
|
96
|
+
}
|
|
97
|
+
return flow;
|
|
98
|
+
}
|
|
99
|
+
async function requireManage(actor, flowId) {
|
|
100
|
+
const flow = await requireFlow(actor, flowId);
|
|
101
|
+
if (!(await deps.repository.canManage(actor, flowId))) {
|
|
102
|
+
throw new IntelError(403, "flow_manage_forbidden", "Flow sharing cannot be managed");
|
|
103
|
+
}
|
|
104
|
+
return flow;
|
|
105
|
+
}
|
|
106
|
+
async function requireVersion(versionId, flowId) {
|
|
107
|
+
const version = await deps.repository.getVersion(versionId);
|
|
108
|
+
if (!version || (flowId && version.flowId !== flowId)) {
|
|
109
|
+
throw new IntelError(404, "flow_version_not_found", "Flow version was not found");
|
|
110
|
+
}
|
|
111
|
+
return version;
|
|
112
|
+
}
|
|
113
|
+
async function step(actor, run) {
|
|
114
|
+
await requireFlow(actor, run.flowId);
|
|
115
|
+
const version = await requireVersion(run.versionId, run.flowId);
|
|
116
|
+
return { run, node: nodeFor(version, run.currentNodeId) };
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
async list(actor) {
|
|
120
|
+
return { items: await deps.repository.listVisible(actor) };
|
|
121
|
+
},
|
|
122
|
+
async get(actor, flowId) {
|
|
123
|
+
const flow = await requireFlow(actor, flowId);
|
|
124
|
+
return {
|
|
125
|
+
flow,
|
|
126
|
+
version: flow.currentVersionId
|
|
127
|
+
? await requireVersion(flow.currentVersionId, flow.id)
|
|
128
|
+
: null,
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
async create(actor, input) {
|
|
132
|
+
const replayed = await deps.repository.findIdempotent(actor.id, "flows.create", input.idempotencyKey);
|
|
133
|
+
if (replayed)
|
|
134
|
+
return await requireFlow(actor, replayed);
|
|
135
|
+
const occurredAt = deps.now().toISOString();
|
|
136
|
+
return await deps.repository.insertFlow({
|
|
137
|
+
flow: {
|
|
138
|
+
id: deps.id(),
|
|
139
|
+
title: input.title,
|
|
140
|
+
description: input.description,
|
|
141
|
+
ownerId: actor.id,
|
|
142
|
+
currentVersionId: null,
|
|
143
|
+
publishedVersionId: null,
|
|
144
|
+
createdAt: occurredAt,
|
|
145
|
+
updatedAt: occurredAt,
|
|
146
|
+
archivedAt: null,
|
|
147
|
+
},
|
|
148
|
+
actorId: actor.id,
|
|
149
|
+
idempotencyKey: input.idempotencyKey,
|
|
150
|
+
auditId: deps.id(),
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
async save(actor, input) {
|
|
154
|
+
const replayed = await deps.repository.findIdempotent(actor.id, "flows.save", input.idempotencyKey);
|
|
155
|
+
if (replayed) {
|
|
156
|
+
const version = await requireVersion(replayed, input.flowId);
|
|
157
|
+
return { flow: await requireFlow(actor, input.flowId), version };
|
|
158
|
+
}
|
|
159
|
+
const flow = await requireEdit(actor, input.flowId);
|
|
160
|
+
compileFlow(input.graph);
|
|
161
|
+
if (flow.currentVersionId !== input.baseVersionId) {
|
|
162
|
+
throw new IntelError(409, "flow_version_conflict", "Flow changed since it was loaded");
|
|
163
|
+
}
|
|
164
|
+
const previous = flow.currentVersionId
|
|
165
|
+
? await requireVersion(flow.currentVersionId, flow.id)
|
|
166
|
+
: null;
|
|
167
|
+
const occurredAt = deps.now().toISOString();
|
|
168
|
+
const version = {
|
|
169
|
+
id: deps.id(),
|
|
170
|
+
flowId: flow.id,
|
|
171
|
+
sequence: (previous?.sequence ?? 0) + 1,
|
|
172
|
+
graph: input.graph,
|
|
173
|
+
createdBy: actor.id,
|
|
174
|
+
createdAt: occurredAt,
|
|
175
|
+
};
|
|
176
|
+
const result = await deps.repository.insertVersion({
|
|
177
|
+
version,
|
|
178
|
+
baseVersionId: input.baseVersionId,
|
|
179
|
+
actorId: actor.id,
|
|
180
|
+
idempotencyKey: input.idempotencyKey,
|
|
181
|
+
auditId: deps.id(),
|
|
182
|
+
});
|
|
183
|
+
if (result === "conflict") {
|
|
184
|
+
throw new IntelError(409, "flow_version_conflict", "Flow changed since it was loaded");
|
|
185
|
+
}
|
|
186
|
+
return { flow: await requireFlow(actor, flow.id), version };
|
|
187
|
+
},
|
|
188
|
+
async publish(actor, input) {
|
|
189
|
+
const flow = await requireEdit(actor, input.flowId);
|
|
190
|
+
const replayed = await deps.repository.findIdempotent(actor.id, "flows.publish", input.idempotencyKey);
|
|
191
|
+
if (replayed)
|
|
192
|
+
return await requireFlow(actor, flow.id);
|
|
193
|
+
const version = await requireVersion(input.versionId, flow.id);
|
|
194
|
+
compileFlow(version.graph);
|
|
195
|
+
for (const node of version.graph.nodes) {
|
|
196
|
+
if (node.kind === "knowledge") {
|
|
197
|
+
for (const resourceId of node.configuration.resourceIds) {
|
|
198
|
+
if (!(await deps.knowledgeExists(actor, resourceId))) {
|
|
199
|
+
throw new IntelError(409, "flow_knowledge_unavailable", `Knowledge reference is unavailable: ${resourceId}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (node.kind === "tool") {
|
|
204
|
+
const fingerprint = await deps.toolFingerprint(actor, node.configuration.toolName);
|
|
205
|
+
if (!fingerprint) {
|
|
206
|
+
throw new IntelError(409, "flow_tool_unavailable", `Tool is unavailable: ${node.label}`);
|
|
207
|
+
}
|
|
208
|
+
if (node.configuration.fingerprint !== fingerprint) {
|
|
209
|
+
throw new IntelError(409, "flow_tool_schema_changed", `Review the current schema before publishing: ${node.label}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const published = await deps.repository.publish({
|
|
214
|
+
flowId: flow.id,
|
|
215
|
+
versionId: version.id,
|
|
216
|
+
actorId: actor.id,
|
|
217
|
+
idempotencyKey: input.idempotencyKey,
|
|
218
|
+
auditId: deps.id(),
|
|
219
|
+
occurredAt: deps.now().toISOString(),
|
|
220
|
+
});
|
|
221
|
+
if (!published)
|
|
222
|
+
throw new IntelError(409, "flow_publish_conflict", "Flow could not be published");
|
|
223
|
+
return await requireFlow(actor, flow.id);
|
|
224
|
+
},
|
|
225
|
+
async start(actor, input) {
|
|
226
|
+
requireRun(actor);
|
|
227
|
+
const replayed = await deps.repository.findIdempotent(actor.id, "flows.run", input.idempotencyKey);
|
|
228
|
+
if (replayed) {
|
|
229
|
+
const run = await deps.repository.getRunVisible(actor, replayed);
|
|
230
|
+
if (run) {
|
|
231
|
+
if (run.status === "running" || run.status === "queued")
|
|
232
|
+
await deps.runtime.start(run.id);
|
|
233
|
+
return await step(actor, run);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const flow = await requireFlow(actor, input.flowId);
|
|
237
|
+
if (!flow.publishedVersionId) {
|
|
238
|
+
throw new IntelError(409, "flow_not_published", "Flow has no published version");
|
|
239
|
+
}
|
|
240
|
+
const version = await requireVersion(flow.publishedVersionId, flow.id);
|
|
241
|
+
const compiled = compileFlow(version.graph);
|
|
242
|
+
const first = version.graph.edges.find((edge) => edge.source === compiled.triggerId)?.target;
|
|
243
|
+
if (!first)
|
|
244
|
+
invalid("The trigger requires an outgoing edge");
|
|
245
|
+
// The flow may have been built by someone with wider portal access. Naming the missing tools
|
|
246
|
+
// before the first step beats failing halfway through with a portal error the user cannot place.
|
|
247
|
+
const missing = await deps.unavailableTools(actor, version.graph.nodes
|
|
248
|
+
.filter((node) => node.kind === "tool")
|
|
249
|
+
.map((node) => (node.kind === "tool" ? node.configuration.toolName : "")));
|
|
250
|
+
if (missing.length) {
|
|
251
|
+
throw new IntelError(403, "flow_tools_unavailable", `You cannot reach these tools: ${missing.join(", ")}`);
|
|
252
|
+
}
|
|
253
|
+
const occurredAt = deps.now().toISOString();
|
|
254
|
+
const run = {
|
|
255
|
+
id: deps.id(),
|
|
256
|
+
flowId: flow.id,
|
|
257
|
+
versionId: version.id,
|
|
258
|
+
status: "running",
|
|
259
|
+
currentNodeId: first,
|
|
260
|
+
input: input.input,
|
|
261
|
+
output: null,
|
|
262
|
+
error: null,
|
|
263
|
+
initiatedBy: actor.id,
|
|
264
|
+
createdAt: occurredAt,
|
|
265
|
+
updatedAt: occurredAt,
|
|
266
|
+
completedAt: null,
|
|
267
|
+
};
|
|
268
|
+
const inserted = await deps.repository.insertRun({
|
|
269
|
+
run,
|
|
270
|
+
actorId: actor.id,
|
|
271
|
+
idempotencyKey: input.idempotencyKey,
|
|
272
|
+
auditId: deps.id(),
|
|
273
|
+
});
|
|
274
|
+
await deps.runtime.start(inserted.id);
|
|
275
|
+
return await step(actor, inserted);
|
|
276
|
+
},
|
|
277
|
+
async getRun(actor, runId) {
|
|
278
|
+
const run = await deps.repository.getRunVisible(actor, runId);
|
|
279
|
+
if (!run)
|
|
280
|
+
throw new IntelError(404, "flow_run_not_found", "Flow run was not found");
|
|
281
|
+
return await step(actor, run);
|
|
282
|
+
},
|
|
283
|
+
async completeStep(actor, input) {
|
|
284
|
+
requireRun(actor);
|
|
285
|
+
const replayed = await deps.repository.findIdempotent(actor.id, "flows.complete", input.idempotencyKey);
|
|
286
|
+
if (replayed) {
|
|
287
|
+
const run = await deps.repository.getRunVisible(actor, replayed);
|
|
288
|
+
if (run)
|
|
289
|
+
return await step(actor, run);
|
|
290
|
+
}
|
|
291
|
+
const current = await deps.repository.getRunVisible(actor, input.runId);
|
|
292
|
+
if (!current)
|
|
293
|
+
throw new IntelError(404, "flow_run_not_found", "Flow run was not found");
|
|
294
|
+
if (current.status !== "running" && current.status !== "waiting") {
|
|
295
|
+
throw new IntelError(409, "flow_run_terminal", "Flow run is already terminal");
|
|
296
|
+
}
|
|
297
|
+
if (current.currentNodeId !== input.nodeId) {
|
|
298
|
+
throw new IntelError(409, "flow_step_conflict", "A different step is currently active");
|
|
299
|
+
}
|
|
300
|
+
const version = await requireVersion(current.versionId, current.flowId);
|
|
301
|
+
const node = nodeFor(version, input.nodeId);
|
|
302
|
+
if (!node) {
|
|
303
|
+
throw new IntelError(500, "flow_version_corrupt", "Current run node is missing from the immutable version");
|
|
304
|
+
}
|
|
305
|
+
if (node.kind === "approval" && !actor.canApprove) {
|
|
306
|
+
throw new IntelError(403, "flow_approval_forbidden", "Approval permission is required");
|
|
307
|
+
}
|
|
308
|
+
const occurredAt = deps.now().toISOString();
|
|
309
|
+
let status = "running";
|
|
310
|
+
let currentNodeId = null;
|
|
311
|
+
let output = current.output;
|
|
312
|
+
let error = null;
|
|
313
|
+
let completedAt = null;
|
|
314
|
+
if (input.outcome === "failed") {
|
|
315
|
+
status = "failed";
|
|
316
|
+
error = input.error ?? "Flow step failed";
|
|
317
|
+
completedAt = occurredAt;
|
|
318
|
+
}
|
|
319
|
+
else if (node.kind === "output") {
|
|
320
|
+
status = "completed";
|
|
321
|
+
output = input.output;
|
|
322
|
+
completedAt = occurredAt;
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
const edges = version.graph.edges.filter((edge) => edge.source === node.id);
|
|
326
|
+
const edge = node.kind === "condition" || node.kind === "approval"
|
|
327
|
+
? edges.find((candidate) => candidate.sourceHandle === input.branch)
|
|
328
|
+
: edges[0];
|
|
329
|
+
if (!edge) {
|
|
330
|
+
throw new IntelError(400, "flow_branch_invalid", "A valid branch is required");
|
|
331
|
+
}
|
|
332
|
+
currentNodeId = edge.target;
|
|
333
|
+
}
|
|
334
|
+
const next = {
|
|
335
|
+
...current,
|
|
336
|
+
status,
|
|
337
|
+
currentNodeId,
|
|
338
|
+
output,
|
|
339
|
+
error,
|
|
340
|
+
updatedAt: occurredAt,
|
|
341
|
+
completedAt,
|
|
342
|
+
};
|
|
343
|
+
const advanced = await deps.repository.advanceRun({
|
|
344
|
+
run: next,
|
|
345
|
+
expectedNodeId: input.nodeId,
|
|
346
|
+
actorId: actor.id,
|
|
347
|
+
stepId: deps.id(),
|
|
348
|
+
outcome: input.outcome,
|
|
349
|
+
branch: input.branch,
|
|
350
|
+
output: input.output,
|
|
351
|
+
error: input.error,
|
|
352
|
+
idempotencyKey: input.idempotencyKey,
|
|
353
|
+
auditId: deps.id(),
|
|
354
|
+
});
|
|
355
|
+
if (advanced === "conflict") {
|
|
356
|
+
throw new IntelError(409, "flow_step_conflict", "Flow step was already completed");
|
|
357
|
+
}
|
|
358
|
+
await deps.runtime.signal(next.id);
|
|
359
|
+
return await step(actor, next);
|
|
360
|
+
},
|
|
361
|
+
async listGrants(actor, flowId) {
|
|
362
|
+
await requireManage(actor, flowId);
|
|
363
|
+
return { items: await deps.repository.listGrants(flowId) };
|
|
364
|
+
},
|
|
365
|
+
async share(actor, input) {
|
|
366
|
+
await requireManage(actor, input.resourceId);
|
|
367
|
+
const replayedId = await deps.repository.findIdempotent(actor.id, "flows.share", input.idempotencyKey);
|
|
368
|
+
if (replayedId) {
|
|
369
|
+
const replayed = (await deps.repository.listGrants(input.resourceId)).find((grant) => grant.id === replayedId);
|
|
370
|
+
if (replayed)
|
|
371
|
+
return replayed;
|
|
372
|
+
}
|
|
373
|
+
const principal = input.principal.type === "email"
|
|
374
|
+
? { type: "email", email: input.principal.email.toLowerCase() }
|
|
375
|
+
: input.principal;
|
|
376
|
+
const createdAt = deps.now().toISOString();
|
|
377
|
+
return await deps.repository.setGrant({
|
|
378
|
+
grant: {
|
|
379
|
+
id: deps.id(),
|
|
380
|
+
resourceId: input.resourceId,
|
|
381
|
+
principal,
|
|
382
|
+
role: input.role,
|
|
383
|
+
expiresAt: input.expiresAt,
|
|
384
|
+
createdBy: actor.id,
|
|
385
|
+
createdAt,
|
|
386
|
+
},
|
|
387
|
+
actorId: actor.id,
|
|
388
|
+
idempotencyKey: input.idempotencyKey,
|
|
389
|
+
auditId: deps.id(),
|
|
390
|
+
});
|
|
391
|
+
},
|
|
392
|
+
async revokeGrant(actor, input) {
|
|
393
|
+
await requireManage(actor, input.resourceId);
|
|
394
|
+
const replayed = await deps.repository.findIdempotentRevocation(actor.id, input.idempotencyKey);
|
|
395
|
+
if (replayed !== null)
|
|
396
|
+
return { revoked: replayed };
|
|
397
|
+
return {
|
|
398
|
+
revoked: await deps.repository.revokeGrant({
|
|
399
|
+
flowId: input.resourceId,
|
|
400
|
+
grantId: input.grantId,
|
|
401
|
+
actorId: actor.id,
|
|
402
|
+
idempotencyKey: input.idempotencyKey,
|
|
403
|
+
auditId: deps.id(),
|
|
404
|
+
occurredAt: deps.now().toISOString(),
|
|
405
|
+
}),
|
|
406
|
+
};
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { CompleteFlowRunStepInput, CreateFlowInput, Flow, FlowDocument, FlowGraph, FlowRun, FlowRunStep, FlowVersion, PublishFlowInput, ResourceGrant, RevokeFlowGrantInput, SaveFlowVersionInput, ShareFlowInput, StartFlowRunInput } from "@anchrd/intel-contract";
|
|
2
|
+
export interface FlowActor {
|
|
3
|
+
id: string;
|
|
4
|
+
email: string;
|
|
5
|
+
canRun: boolean;
|
|
6
|
+
canApprove: boolean;
|
|
7
|
+
}
|
|
8
|
+
export type FlowOperation = "flows.create" | "flows.save" | "flows.publish" | "flows.run" | "flows.complete" | "flows.share" | "flows.revoke";
|
|
9
|
+
export interface FlowRepository {
|
|
10
|
+
listVisible(actor: FlowActor): Promise<Flow[]>;
|
|
11
|
+
getVisible(actor: FlowActor, flowId: string): Promise<Flow | null>;
|
|
12
|
+
canEdit(actor: FlowActor, flowId: string): Promise<boolean>;
|
|
13
|
+
canManage(actor: FlowActor, flowId: string): Promise<boolean>;
|
|
14
|
+
findIdempotent(actorId: string, operation: FlowOperation, key: string): Promise<string | null>;
|
|
15
|
+
findIdempotentRevocation(actorId: string, key: string): Promise<boolean | null>;
|
|
16
|
+
insertFlow(input: {
|
|
17
|
+
flow: Flow;
|
|
18
|
+
actorId: string;
|
|
19
|
+
idempotencyKey: string;
|
|
20
|
+
auditId: string;
|
|
21
|
+
}): Promise<Flow>;
|
|
22
|
+
getVersion(versionId: string): Promise<FlowVersion | null>;
|
|
23
|
+
insertVersion(input: {
|
|
24
|
+
version: FlowVersion;
|
|
25
|
+
baseVersionId: string | null;
|
|
26
|
+
actorId: string;
|
|
27
|
+
idempotencyKey: string;
|
|
28
|
+
auditId: string;
|
|
29
|
+
}): Promise<"saved" | "conflict">;
|
|
30
|
+
publish(input: {
|
|
31
|
+
flowId: string;
|
|
32
|
+
versionId: string;
|
|
33
|
+
actorId: string;
|
|
34
|
+
idempotencyKey: string;
|
|
35
|
+
auditId: string;
|
|
36
|
+
occurredAt: string;
|
|
37
|
+
}): Promise<boolean>;
|
|
38
|
+
listGrants(flowId: string): Promise<ResourceGrant[]>;
|
|
39
|
+
setGrant(input: {
|
|
40
|
+
grant: ResourceGrant;
|
|
41
|
+
actorId: string;
|
|
42
|
+
idempotencyKey: string;
|
|
43
|
+
auditId: string;
|
|
44
|
+
}): Promise<ResourceGrant>;
|
|
45
|
+
revokeGrant(input: {
|
|
46
|
+
flowId: string;
|
|
47
|
+
grantId: string;
|
|
48
|
+
actorId: string;
|
|
49
|
+
idempotencyKey: string;
|
|
50
|
+
auditId: string;
|
|
51
|
+
occurredAt: string;
|
|
52
|
+
}): Promise<boolean>;
|
|
53
|
+
insertRun(input: {
|
|
54
|
+
run: FlowRun;
|
|
55
|
+
actorId: string;
|
|
56
|
+
idempotencyKey: string;
|
|
57
|
+
auditId: string;
|
|
58
|
+
}): Promise<FlowRun>;
|
|
59
|
+
getRunVisible(actor: FlowActor, runId: string): Promise<FlowRun | null>;
|
|
60
|
+
advanceRun(input: {
|
|
61
|
+
run: FlowRun;
|
|
62
|
+
expectedNodeId: string;
|
|
63
|
+
actorId: string;
|
|
64
|
+
stepId: string;
|
|
65
|
+
outcome: "completed" | "failed";
|
|
66
|
+
branch: string | null;
|
|
67
|
+
output: unknown;
|
|
68
|
+
error: string | null;
|
|
69
|
+
idempotencyKey: string;
|
|
70
|
+
auditId: string;
|
|
71
|
+
}): Promise<"advanced" | "conflict">;
|
|
72
|
+
}
|
|
73
|
+
export interface FlowRuntime {
|
|
74
|
+
start(runId: string): Promise<void>;
|
|
75
|
+
signal(runId: string): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
export interface FlowDeps {
|
|
78
|
+
repository: FlowRepository;
|
|
79
|
+
runtime: FlowRuntime;
|
|
80
|
+
id(): string;
|
|
81
|
+
now(): Date;
|
|
82
|
+
knowledgeExists(actor: FlowActor, resourceId: string): Promise<boolean>;
|
|
83
|
+
toolFingerprint(actor: FlowActor, toolName: string): Promise<string | null>;
|
|
84
|
+
unavailableTools(actor: FlowActor, toolNames: string[]): Promise<string[]>;
|
|
85
|
+
}
|
|
86
|
+
export interface FlowService {
|
|
87
|
+
list(actor: FlowActor): Promise<{
|
|
88
|
+
items: Flow[];
|
|
89
|
+
}>;
|
|
90
|
+
get(actor: FlowActor, flowId: string): Promise<FlowDocument>;
|
|
91
|
+
create(actor: FlowActor, input: CreateFlowInput): Promise<Flow>;
|
|
92
|
+
save(actor: FlowActor, input: SaveFlowVersionInput): Promise<FlowDocument>;
|
|
93
|
+
publish(actor: FlowActor, input: PublishFlowInput): Promise<Flow>;
|
|
94
|
+
start(actor: FlowActor, input: StartFlowRunInput): Promise<FlowRunStep>;
|
|
95
|
+
getRun(actor: FlowActor, runId: string): Promise<FlowRunStep>;
|
|
96
|
+
completeStep(actor: FlowActor, input: CompleteFlowRunStepInput): Promise<FlowRunStep>;
|
|
97
|
+
listGrants(actor: FlowActor, flowId: string): Promise<{
|
|
98
|
+
items: ResourceGrant[];
|
|
99
|
+
}>;
|
|
100
|
+
share(actor: FlowActor, input: ShareFlowInput): Promise<ResourceGrant>;
|
|
101
|
+
revokeGrant(actor: FlowActor, input: RevokeFlowGrantInput): Promise<{
|
|
102
|
+
revoked: boolean;
|
|
103
|
+
}>;
|
|
104
|
+
}
|
|
105
|
+
export interface CompiledFlow {
|
|
106
|
+
graph: FlowGraph;
|
|
107
|
+
triggerId: string;
|
|
108
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|