@anchrd/intel-api 0.21.0 → 0.22.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.
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createGateClient } from "@anchrd/gate-sdk";
|
|
2
|
+
import { serverOf } from "@anchrd/intel-contract/tool";
|
|
2
3
|
import { ulid } from "ulid";
|
|
3
4
|
import { createBrowserAuth } from "../../auth/auth.js";
|
|
4
5
|
import { createBundle } from "../../bundle/bundle.js";
|
|
@@ -156,13 +157,60 @@ export default {
|
|
|
156
157
|
// requirements list and every tree link of every run read this one answer, so none of
|
|
157
158
|
// them can be kinder than the others.
|
|
158
159
|
visibleNodes: async (actor, nodeId) => await nodes.visibleNode(actor, nodeId),
|
|
159
|
-
|
|
160
|
+
/**
|
|
161
|
+
* ⚠️ The fingerprint of a whole SURFACE, not of one tool (#489). A step names a server and
|
|
162
|
+
* optionally the functions of it it may use, so what publishing freezes is that set — every
|
|
163
|
+
* covered function's own fingerprint, sorted so the order the portal happens to answer in
|
|
164
|
+
* cannot change the result, and hashed into one value.
|
|
165
|
+
*
|
|
166
|
+
* `null` in three cases, and all three have to refuse a publish: the catalog is unreachable,
|
|
167
|
+
* the server offers this user nothing, or a function the step explicitly allows is gone. The
|
|
168
|
+
* last one matters most — a narrowed step whose function disappeared is not a step that may
|
|
169
|
+
* quietly fall back to the rest of the server.
|
|
170
|
+
*/
|
|
171
|
+
toolSurfaceFingerprint: async (actor, server, allow) => {
|
|
160
172
|
const catalog = await tools
|
|
161
173
|
.catalog({ id: actor.id, email: actor.email, canExecute: true })
|
|
162
174
|
.catch(() => null);
|
|
163
|
-
|
|
175
|
+
if (!catalog)
|
|
176
|
+
return null;
|
|
177
|
+
const reachable = new Map(catalog.items.map((item) => [item.name, item.fingerprint]));
|
|
178
|
+
const covered = allow === null
|
|
179
|
+
? [...reachable.keys()].filter((name) => serverOf(name, [server]) !== null)
|
|
180
|
+
: allow;
|
|
181
|
+
if (covered.length === 0)
|
|
182
|
+
return null;
|
|
183
|
+
const parts = [];
|
|
184
|
+
for (const name of [...new Set(covered)].sort()) {
|
|
185
|
+
const fingerprint = reachable.get(name);
|
|
186
|
+
if (!fingerprint)
|
|
187
|
+
return null;
|
|
188
|
+
parts.push(`${name}:${fingerprint}`);
|
|
189
|
+
}
|
|
190
|
+
// ⚠️ The handle and the narrowing mode are part of what is hashed, not just the covered
|
|
191
|
+
// functions. Without them two different surfaces could collide: a server whose only
|
|
192
|
+
// function is allowed explicitly would hash the same as the same server left open, and the
|
|
193
|
+
// difference between "this one function" and "whatever this server offers" is exactly what
|
|
194
|
+
// publishing is meant to freeze.
|
|
195
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode([server, allow === null ? "*" : "allow", ...parts].join("\n")));
|
|
196
|
+
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
197
|
+
},
|
|
198
|
+
// Which of the named servers this user does NOT reach. A server is reached when the live
|
|
199
|
+
// catalog carries at least one function of it — the same live answer the tool surface uses,
|
|
200
|
+
// never a mirrored table.
|
|
201
|
+
// ⚠️ The catalog error is NOT caught here, and that is the whole point. Answering an
|
|
202
|
+
// unreadable portal with "all of them are missing" turns an outage into a sentence about
|
|
203
|
+
// PERMISSIONS — the reader goes asking for access they already have, while the portal is
|
|
204
|
+
// simply down. `tools.catalog` already tells the two apart (`tool_catalog_unreadable`,
|
|
205
|
+
// `portal_not_connected`), and letting that answer through is the same rule the tool surface
|
|
206
|
+
// keeps one screen up: a sentence more specific than the evidence sends the reader looking in
|
|
207
|
+
// the wrong place.
|
|
208
|
+
unavailableServers: async (actor, servers) => {
|
|
209
|
+
if (servers.length === 0)
|
|
210
|
+
return [];
|
|
211
|
+
const catalog = await tools.catalog({ id: actor.id, email: actor.email, canExecute: true });
|
|
212
|
+
return servers.filter((server) => !catalog.items.some((item) => serverOf(item.name, [server]) !== null));
|
|
164
213
|
},
|
|
165
|
-
unavailableTools: async (actor, toolNames) => await tools.unavailable({ id: actor.id, email: actor.email, canExecute: true }, toolNames),
|
|
166
214
|
});
|
|
167
215
|
return await createIntel({
|
|
168
216
|
baseUrl: env.INTEL_URL,
|
package/dist/flows/flows.d.ts
CHANGED
|
@@ -23,14 +23,18 @@ export declare function toolNodes(graph: FlowGraph): ToolStepNode[];
|
|
|
23
23
|
export declare function resourceIdOf(node: TreeLinkNode): string;
|
|
24
24
|
export declare function calleeIds(graph: FlowGraph): string[];
|
|
25
25
|
/**
|
|
26
|
-
* The documents a graph names and the
|
|
26
|
+
* The documents a graph names and the tool SERVERS it reaches for, 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
|
+
*
|
|
31
|
+
* ⚠️ Servers, not functions, since #489. A step no longer names one function, so "what does this
|
|
32
|
+
* flow need" cannot be answered with a function list any more — and answering it with the functions
|
|
33
|
+
* a step MIGHT call would be a guess about a choice that is made while the flow runs.
|
|
30
34
|
*/
|
|
31
35
|
export declare function graphReferences(graph: FlowGraph): {
|
|
32
36
|
nodes: string[];
|
|
33
|
-
|
|
37
|
+
servers: string[];
|
|
34
38
|
};
|
|
35
39
|
export declare function compileFlow(graph: FlowGraph): CompiledFlow;
|
|
36
40
|
export declare function createFlows(deps: FlowDeps): FlowService;
|
package/dist/flows/flows.js
CHANGED
|
@@ -46,24 +46,28 @@ export function calleeIds(graph) {
|
|
|
46
46
|
return ids;
|
|
47
47
|
}
|
|
48
48
|
/**
|
|
49
|
-
* The documents a graph names and the
|
|
49
|
+
* The documents a graph names and the tool SERVERS it reaches for, flattened and without repetition.
|
|
50
50
|
* The requirements list, the publish-time check and the run's first tool check read this one answer,
|
|
51
51
|
* so they cannot disagree about what a flow touches. A caller that needs to know *which step* names
|
|
52
52
|
* a document reads the node lists above instead — the relation graph draws exactly that edge.
|
|
53
|
+
*
|
|
54
|
+
* ⚠️ Servers, not functions, since #489. A step no longer names one function, so "what does this
|
|
55
|
+
* flow need" cannot be answered with a function list any more — and answering it with the functions
|
|
56
|
+
* a step MIGHT call would be a guess about a choice that is made while the flow runs.
|
|
53
57
|
*/
|
|
54
58
|
export function graphReferences(graph) {
|
|
55
59
|
const nodes = [];
|
|
56
|
-
const
|
|
60
|
+
const servers = [];
|
|
57
61
|
for (const node of treeLinkNodes(graph)) {
|
|
58
62
|
if (!nodes.includes(node.configuration.resourceId)) {
|
|
59
63
|
nodes.push(node.configuration.resourceId);
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
for (const node of toolNodes(graph)) {
|
|
63
|
-
if (!
|
|
64
|
-
|
|
67
|
+
if (!servers.includes(node.configuration.server))
|
|
68
|
+
servers.push(node.configuration.server);
|
|
65
69
|
}
|
|
66
|
-
return { nodes,
|
|
70
|
+
return { nodes, servers };
|
|
67
71
|
}
|
|
68
72
|
// ⚠️ The reason, in the words the person can act on, and not one word more. How many documents a
|
|
69
73
|
// step cannot reach is something they may know; which ones they are is the very thing the ACL is
|
|
@@ -76,10 +80,14 @@ function treeLinkStepDetail(label, missing) {
|
|
|
76
80
|
// ⚠️ For tools this is the only honest moment there is. The catalog is a live tools/list with the
|
|
77
81
|
// requesting user's own token (ADR-0003), so nobody can be told in advance what someone else would
|
|
78
82
|
// see — but the person in front of the failure can be told exactly where to go.
|
|
83
|
+
// ⚠️ Servers, not tools, since #489 — and the sentence says so. A step names a server, so a person
|
|
84
|
+
// told "you do not have access to this tool: notion" would go looking for a function by that name
|
|
85
|
+
// and find none. What they can act on is the server: it is what the portal grants and what they can
|
|
86
|
+
// ask to be granted.
|
|
79
87
|
function toolStepDetail(missing) {
|
|
80
88
|
return missing.length === 1
|
|
81
|
-
? `You do not
|
|
82
|
-
: `You do not
|
|
89
|
+
? `You do not reach this tool server in the portal: ${missing.join(", ")}`
|
|
90
|
+
: `You do not reach these tool servers in the portal: ${missing.join(", ")}`;
|
|
83
91
|
}
|
|
84
92
|
export function compileFlow(graph) {
|
|
85
93
|
const nodes = new Map();
|
|
@@ -439,11 +447,11 @@ export function createFlows(deps) {
|
|
|
439
447
|
// The one consequence to know about: `start` throws the FIRST entry, so it now names the first
|
|
440
448
|
// missing tool rather than all of them — the same as it has always done for several
|
|
441
449
|
// unpublished sub-flows below. `validate` is what lists every reason at once, and it does.
|
|
442
|
-
for (const
|
|
450
|
+
for (const server of await deps.unavailableServers(actor, graphReferences(version.graph).servers)) {
|
|
443
451
|
problems.push({
|
|
444
452
|
status: 403,
|
|
445
453
|
code: "flow_tools_unavailable",
|
|
446
|
-
detail: toolStepDetail([
|
|
454
|
+
detail: toolStepDetail([server]),
|
|
447
455
|
});
|
|
448
456
|
}
|
|
449
457
|
try {
|
|
@@ -597,12 +605,12 @@ export function createFlows(deps) {
|
|
|
597
605
|
async function requireToolsAuthorized(actor, node, graph) {
|
|
598
606
|
if (!node)
|
|
599
607
|
return;
|
|
600
|
-
const
|
|
608
|
+
const servers = attachedNodes(node, graph)
|
|
601
609
|
.filter((candidate) => candidate.kind === "tool")
|
|
602
|
-
.map((candidate) => candidate.configuration.
|
|
603
|
-
if (!
|
|
610
|
+
.map((candidate) => candidate.configuration.server);
|
|
611
|
+
if (!servers.length)
|
|
604
612
|
return;
|
|
605
|
-
const missing = await deps.
|
|
613
|
+
const missing = await deps.unavailableServers(actor, [...new Set(servers)]);
|
|
606
614
|
if (missing.length) {
|
|
607
615
|
throw new IntelError(403, "flow_tools_unavailable", toolStepDetail(missing));
|
|
608
616
|
}
|
|
@@ -773,6 +781,19 @@ export function createFlows(deps) {
|
|
|
773
781
|
let changed = false;
|
|
774
782
|
const nodes = [];
|
|
775
783
|
for (const node of graph.nodes) {
|
|
784
|
+
// A tool step is frozen the same way a sub-flow call is: what it may reach is pinned at the
|
|
785
|
+
// moment of publishing (#489). The check above has already refused an unreachable server and
|
|
786
|
+
// a surface that moved since a previous publish, so this only writes down what it confirmed.
|
|
787
|
+
if (node.kind === "tool") {
|
|
788
|
+
const fingerprint = await deps.toolSurfaceFingerprint(actor, node.configuration.server, node.configuration.allow);
|
|
789
|
+
if (fingerprint !== null && fingerprint !== node.configuration.fingerprint) {
|
|
790
|
+
nodes.push({ ...node, configuration: { ...node.configuration, fingerprint } });
|
|
791
|
+
changed = true;
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
nodes.push(node);
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
776
797
|
if (node.kind !== "subflow") {
|
|
777
798
|
nodes.push(node);
|
|
778
799
|
continue;
|
|
@@ -1085,7 +1106,7 @@ export function createFlows(deps) {
|
|
|
1085
1106
|
versionId: null,
|
|
1086
1107
|
nodes: [],
|
|
1087
1108
|
hiddenNodes: 0,
|
|
1088
|
-
|
|
1109
|
+
servers: [],
|
|
1089
1110
|
};
|
|
1090
1111
|
}
|
|
1091
1112
|
const version = await requireVersion(versionId, flow.id);
|
|
@@ -1101,7 +1122,7 @@ export function createFlows(deps) {
|
|
|
1101
1122
|
versionId: version.id,
|
|
1102
1123
|
nodes: reachable,
|
|
1103
1124
|
hiddenNodes: referenced.nodes.length - reachable.length,
|
|
1104
|
-
|
|
1125
|
+
servers: referenced.servers,
|
|
1105
1126
|
};
|
|
1106
1127
|
},
|
|
1107
1128
|
async create(actor, input) {
|
|
@@ -1311,11 +1332,24 @@ export function createFlows(deps) {
|
|
|
1311
1332
|
}
|
|
1312
1333
|
for (const node of version.graph.nodes) {
|
|
1313
1334
|
if (node.kind === "tool") {
|
|
1314
|
-
|
|
1335
|
+
// ⚠️ The fingerprint covers the whole surface this step may use — the server and, when
|
|
1336
|
+
// the step narrows it, exactly those functions with their schemas (#489). Freezing one
|
|
1337
|
+
// function was enough while a step named one; a step that may use any function of a
|
|
1338
|
+
// server would otherwise silently inherit functions the provider added AFTER publishing.
|
|
1339
|
+
const fingerprint = await deps.toolSurfaceFingerprint(actor, node.configuration.server, node.configuration.allow);
|
|
1315
1340
|
if (!fingerprint) {
|
|
1316
|
-
throw new IntelError(409, "flow_tool_unavailable", `Tool is unavailable: ${node.label}`);
|
|
1341
|
+
throw new IntelError(409, "flow_tool_unavailable", `Tool server is unavailable: ${node.label}`);
|
|
1317
1342
|
}
|
|
1318
|
-
|
|
1343
|
+
// ⚠️ A step that has never been published carries no fingerprint, and that is not a
|
|
1344
|
+
// mismatch — it is the state every draft starts in. Publishing is what FREEZES the
|
|
1345
|
+
// surface, exactly as it turns a `latest` sub-flow call into a pinned one, and `freeze`
|
|
1346
|
+
// below writes the value in.
|
|
1347
|
+
//
|
|
1348
|
+
// Demanding it up front would mean the editor had to produce it, and nothing hands it
|
|
1349
|
+
// one: the value is derived from the asking user's own live catalog. Requiring it would
|
|
1350
|
+
// make every tool step unpublishable rather than safe.
|
|
1351
|
+
if (node.configuration.fingerprint !== null &&
|
|
1352
|
+
node.configuration.fingerprint !== fingerprint) {
|
|
1319
1353
|
throw new IntelError(409, "flow_tool_schema_changed", `Review the current schema before publishing: ${node.label}`);
|
|
1320
1354
|
}
|
|
1321
1355
|
}
|
|
@@ -209,8 +209,8 @@ export interface FlowDeps {
|
|
|
209
209
|
* kinder than the door it describes — which is what #17 and #19 were sent back for.
|
|
210
210
|
*/
|
|
211
211
|
visibleNodes(actor: FlowActor, nodeId: string): Promise<Node | null>;
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
toolSurfaceFingerprint(actor: FlowActor, server: string, allow: string[] | null): Promise<string | null>;
|
|
213
|
+
unavailableServers(actor: FlowActor, servers: string[]): Promise<string[]>;
|
|
214
214
|
}
|
|
215
215
|
export type FolderAccess = "ok" | "missing" | "not-a-folder" | "forbidden";
|
|
216
216
|
export interface FlowService {
|
package/dist/tools/tools.js
CHANGED
|
@@ -203,11 +203,5 @@ export function createTools(deps) {
|
|
|
203
203
|
}
|
|
204
204
|
return await call(actor, input);
|
|
205
205
|
},
|
|
206
|
-
async unavailable(actor, names) {
|
|
207
|
-
if (names.length === 0)
|
|
208
|
-
return [];
|
|
209
|
-
const available = new Set((await capabilities(actor)).map((capability) => capability.name));
|
|
210
|
-
return names.filter((name) => !available.has(name));
|
|
211
|
-
},
|
|
212
206
|
};
|
|
213
207
|
}
|
|
@@ -53,5 +53,4 @@ export interface ToolService {
|
|
|
53
53
|
servers(actor: ToolActor): Promise<ToolServerCatalog>;
|
|
54
54
|
execute(actor: ToolActor, input: ExecuteToolInput): Promise<ToolTestResult>;
|
|
55
55
|
test(actor: ToolActor, input: TestToolInput): Promise<ToolTestResult>;
|
|
56
|
-
unavailable(actor: ToolActor, names: string[]): Promise<string[]>;
|
|
57
56
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anchrd/intel-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@anchrd/gate-sdk": "^0.15.0",
|
|
46
|
-
"@anchrd/intel-contract": "^0.
|
|
46
|
+
"@anchrd/intel-contract": "^0.18.0",
|
|
47
47
|
"@cfworker/json-schema": "^4.1.1",
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
49
49
|
"fflate": "^0.8.3",
|