@anchrd/intel-api 0.15.0 → 0.16.1
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/dist/adapters/db/db-flows.js +2 -1
- package/dist/adapters/db/db-grants.d.ts +1 -1
- package/dist/adapters/openid/openid.js +22 -4
- package/dist/adapters/remote-tools/remote-tools.js +1 -1
- package/dist/bundle/bundle.js +4 -1
- package/dist/bundle/bundle.types.d.ts +1 -1
- package/dist/cli/cli.js +15 -2
- package/dist/flows/flows.d.ts +1 -1
- package/dist/flows/flows.js +1 -1
- package/dist/flows/flows.types.d.ts +4 -1
- package/dist/http/http.js +43 -33
- package/dist/indexing/indexing.js +1 -1
- package/dist/intel/intel.js +10 -1
- package/dist/mcp/mcp.js +82 -70
- package/dist/nodes/document-links/document-links.js +1 -1
- package/dist/nodes/nodes.js +8 -8
- package/dist/nodes/nodes.types.d.ts +6 -6
- package/dist/tools/tool-servers/tool-servers.d.ts +1 -1
- package/dist/tools/tool-servers/tool-servers.js +1 -1
- package/dist/tools/tools.types.d.ts +1 -1
- package/package.json +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Flow, FlowGraph,
|
|
1
|
+
import { Flow, FlowGraph, FlowVersion, } from "@anchrd/intel-contract/flow";
|
|
2
|
+
import { FlowRun } from "@anchrd/intel-contract/flow-run";
|
|
2
3
|
import { calleeIds, treeLinkKinds } from "../../flows/flows.js";
|
|
3
4
|
import { flowCallable, flowInSubtree, flowInSubtreeBindings, flowVerbBindings, flowVerbQuery, readableOrRunnableCte, subtreeBindings, subtreeCte, } from "./db-grants.js";
|
|
4
5
|
const flowColumnNames = [
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ToolSourceUrl } from "@anchrd/intel-contract";
|
|
1
|
+
import { ToolSourceUrl } from "@anchrd/intel-contract/tool";
|
|
2
2
|
import * as client from "openid-client";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
const ProtectedResource = z.looseObject({
|
|
@@ -162,9 +162,20 @@ export function createOpenId(deps) {
|
|
|
162
162
|
// one with a 404, which surfaced as a bare 500 on /auth/connect (#93). The fallback repeats the
|
|
163
163
|
// operation once with the OAuth document; when both fail, the second error is thrown because it
|
|
164
164
|
// belongs to the attempt that got further for the issuer that needed the fallback at all.
|
|
165
|
-
async function withAlgorithmFallback(run) {
|
|
165
|
+
async function withAlgorithmFallback(run, usable = () => true) {
|
|
166
166
|
try {
|
|
167
|
-
|
|
167
|
+
const first = await run();
|
|
168
|
+
// ⚠️ A THROW is not the only way the OIDC document can be the wrong one (#425). Cloudflare
|
|
169
|
+
// Access answers `/.well-known/openid-configuration` with **200** and a two-field stub —
|
|
170
|
+
// `issuer` and `jwks_uri`, nothing else — while the endpoints live only under RFC 8414's
|
|
171
|
+
// `oauth-authorization-server`. Discovery therefore SUCCEEDS, the fallback never runs, and
|
|
172
|
+
// the missing endpoint surfaces minutes later inside `buildAuthorizationUrl` as
|
|
173
|
+
// `authorization server metadata does not contain a valid "as.authorization_endpoint"` — a
|
|
174
|
+
// 500 on `/auth/connect` with nothing in it pointing at discovery.
|
|
175
|
+
//
|
|
176
|
+
// So the question is not "did it fail" but "is what came back usable". An answer that parses
|
|
177
|
+
// and carries nothing is the same situation as a 404, and it gets the same second attempt.
|
|
178
|
+
return usable(first) ? first : await run("oauth2");
|
|
168
179
|
}
|
|
169
180
|
catch {
|
|
170
181
|
return await run("oauth2");
|
|
@@ -174,7 +185,14 @@ export function createOpenId(deps) {
|
|
|
174
185
|
const key = `${issuer}#${clientId}`;
|
|
175
186
|
let configuration = configurations.get(key);
|
|
176
187
|
if (!configuration) {
|
|
177
|
-
configuration = withAlgorithmFallback((algorithm) => client.discovery(new URL(issuer), clientId, { token_endpoint_auth_method: "none" }, client.None(), { ...options(issuer), ...(algorithm ? { algorithm } : {}) })
|
|
188
|
+
configuration = withAlgorithmFallback((algorithm) => client.discovery(new URL(issuer), clientId, { token_endpoint_auth_method: "none" }, client.None(), { ...options(issuer), ...(algorithm ? { algorithm } : {}) }),
|
|
189
|
+
// The two endpoints every flow here needs. Asking for exactly them rather than for a long
|
|
190
|
+
// list keeps the check honest: a server that publishes these two is usable, and one that
|
|
191
|
+
// publishes neither is the stub case above, whatever else it carries.
|
|
192
|
+
(resolved) => {
|
|
193
|
+
const metadata = resolved.serverMetadata();
|
|
194
|
+
return Boolean(metadata.authorization_endpoint && metadata.token_endpoint);
|
|
195
|
+
});
|
|
178
196
|
configurations.set(key, configuration);
|
|
179
197
|
void configuration.catch(() => configurations.delete(key));
|
|
180
198
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ToolTestResult } from "@anchrd/intel-contract";
|
|
1
|
+
import { ToolTestResult } from "@anchrd/intel-contract/tool";
|
|
2
2
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
3
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4
4
|
import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker";
|
package/dist/bundle/bundle.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BundleImportResult, BundleManifest, BundleManifestFilename, } from "@anchrd/intel-contract/bundle";
|
|
2
|
+
import { FlowGraph } from "@anchrd/intel-contract/flow";
|
|
3
|
+
import { BlockNoteMediaType, DocumentLinkInlineType, } from "@anchrd/intel-contract/node";
|
|
4
|
+
import { TableMediaType } from "@anchrd/intel-contract/table";
|
|
2
5
|
import { Unzip, UnzipInflate, Zip, ZipDeflate, ZipPassThrough } from "fflate";
|
|
3
6
|
import { documentLinkTargets } from "../nodes/document-links/document-links.js";
|
|
4
7
|
import { parseCsv } from "../shared/csv/csv.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BundleImportResult, BundleManifest } from "@anchrd/intel-contract";
|
|
1
|
+
import type { BundleImportResult, BundleManifest } from "@anchrd/intel-contract/bundle";
|
|
2
2
|
import type { FlowRepository } from "../flows/flows.types.js";
|
|
3
3
|
import type { Actor, ContentStore, NodeRepository } from "../nodes/nodes.types.js";
|
|
4
4
|
export interface BundleActor extends Actor {
|
package/dist/cli/cli.js
CHANGED
|
@@ -2,7 +2,20 @@ import { createBuild } from "../build/build.js";
|
|
|
2
2
|
import { createPrepare } from "../prepare/prepare.js";
|
|
3
3
|
const interfaces = [
|
|
4
4
|
{ handle: "intel", functions: ["use", "admin"] },
|
|
5
|
-
|
|
5
|
+
// ⚠️ `nodes`, not `knowledge` — and the rename cost something, which is why it needed its own
|
|
6
|
+
// ticket (#152) five months after #125 renamed everything else. This handle is only half Intel's:
|
|
7
|
+
// `bootstrap` DECLARES it in Gate and deliberately changes no grant, so to Gate a renamed handle
|
|
8
|
+
// is a NEW one. Every grant on the old name keeps pointing at the old name, and everybody loses
|
|
9
|
+
// access at once, silently, until an operator hands the permissions out again.
|
|
10
|
+
//
|
|
11
|
+
// It was done in the one window where that is free: the installation on anchrd.sh was being
|
|
12
|
+
// rebuilt anyway (#385, anchrd/core#93) and there is no other. Doing it later would mean doing it
|
|
13
|
+
// to somebody.
|
|
14
|
+
//
|
|
15
|
+
// ⚠️ The old `knowledge` interface stays declared in Gate — bootstrap declares, it never revokes.
|
|
16
|
+
// It is a harmless leftover on installations that have it, and nothing in Intel asks about it any
|
|
17
|
+
// more; removing it is an act in Gate, by hand, when somebody is sure nothing else uses it.
|
|
18
|
+
{ handle: "nodes", functions: ["read", "create", "write", "share"] },
|
|
6
19
|
// ⚠️ No `approve`. The approval node is gone (#73), and this list is what Intel declares to Gate:
|
|
7
20
|
// a function nobody asks about is a permission an operator has to decide on for no reason. An
|
|
8
21
|
// installation that already granted it keeps a harmless leftover — bootstrap declares, it does
|
|
@@ -118,7 +131,7 @@ export function createCli(deps) {
|
|
|
118
131
|
if (!intelUrl || !token) {
|
|
119
132
|
return fail("INTEL_URL and a short-lived INTEL_OPERATOR_TOKEN must be set.");
|
|
120
133
|
}
|
|
121
|
-
const response = await deps.fetch(`${intelUrl}/api/v1/
|
|
134
|
+
const response = await deps.fetch(`${intelUrl}/api/v1/nodes/reindex`, {
|
|
122
135
|
method: "POST",
|
|
123
136
|
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
124
137
|
body: "{}",
|
package/dist/flows/flows.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FlowGraph, FlowNode } from "@anchrd/intel-contract";
|
|
1
|
+
import type { FlowGraph, FlowNode } from "@anchrd/intel-contract/flow";
|
|
2
2
|
import type { CompiledFlow, FlowDeps, FlowService } from "./flows.types.js";
|
|
3
3
|
type SubflowNode = Extract<FlowNode, {
|
|
4
4
|
kind: "subflow";
|
package/dist/flows/flows.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { flowNodeLayer } from "@anchrd/intel-contract";
|
|
1
|
+
import { flowNodeLayer } from "@anchrd/intel-contract/flow";
|
|
2
2
|
import { IntelError } from "../shared/intel-error/intel-error.js";
|
|
3
3
|
import { plainTitle } from "../shared/plain-title/plain-title.js";
|
|
4
4
|
function invalid(detail) {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type { ArchiveFlowInput,
|
|
1
|
+
import type { ArchiveFlowInput, CreateFlowInput, Flow, FlowDocument, FlowGraph, FlowPublishPreview, FlowRequirements, FlowValidation, FlowVersion, FlowVersionList, FlowVersionSummary, GetFlowVersionInput, ListFlowsInput, PreviewFlowPublishInput, PublishFlowInput, RelationGraph, RelationGraphInput, SaveFlowVersionInput, UnpublishFlowInput, UpdateFlowInput } from "@anchrd/intel-contract/flow";
|
|
2
|
+
import type { CancelFlowRunInput, CompleteFlowRunStepInput, FlowRun, FlowRunHistory, FlowRunList, FlowRunStep, ListFlowRunsInput, StartFlowRunInput } from "@anchrd/intel-contract/flow-run";
|
|
3
|
+
import type { Node } from "@anchrd/intel-contract/node";
|
|
4
|
+
import type { ResourceVerb } from "@anchrd/intel-contract/share";
|
|
2
5
|
export type FlowPrincipal = Pick<FlowActor, "id" | "email" | "isAdmin">;
|
|
3
6
|
export type FlowCallReach = "subtree" | "library" | "out-of-reach";
|
|
4
7
|
export interface FlowRunChainEntry {
|
package/dist/http/http.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ArchiveFlowInput, CreateFlowInput, GetFlowInput, GetFlowVersionInput, ListFlowsInput, PreviewFlowPublishInput, PublishFlowInput, RelationGraphInput, SaveFlowVersionInput, UnpublishFlowInput, UpdateFlowInput, } from "@anchrd/intel-contract/flow";
|
|
2
|
+
import { CancelFlowRunInput, CompleteFlowRunStepInput, GetFlowRunInput, ListFlowRunsInput, StartFlowRunInput, } from "@anchrd/intel-contract/flow-run";
|
|
3
|
+
import { ArchiveNodeInput, CreateNodeInput, GetNodeInput, GetNodeVersionInput, ListNodesInput, NodeGraphInput, ResolveNodeLinksInput, SaveAttachmentInput, SaveNodeVersionInput, SearchInput, UpdateNodeInput, } from "@anchrd/intel-contract/node";
|
|
4
|
+
import { RevokeGrantInput, ShareInput } from "@anchrd/intel-contract/share";
|
|
5
|
+
import { AppendTableRowsInput, DefineTableInput, DeleteTableRowsInput, RedefineTableInput, UpdateTableRowsInput, } from "@anchrd/intel-contract/table";
|
|
6
|
+
import { ExecuteToolInput, TestToolInput } from "@anchrd/intel-contract/tool";
|
|
2
7
|
import { Hono } from "hono";
|
|
3
8
|
import { z } from "zod";
|
|
4
9
|
import { authorizeBearer, bearer, permits, } from "../shared/gate-authorization/gate-authorization.js";
|
|
@@ -119,15 +124,20 @@ export function createHttp(deps) {
|
|
|
119
124
|
// The caller's own identity, so the shell can name who is signed in. It needs no capability —
|
|
120
125
|
// the answer is the token's own subject — and it returns nothing but id, email, and name.
|
|
121
126
|
app.get("/session", (context) => {
|
|
122
|
-
const
|
|
127
|
+
const authorization = context.get("authorization");
|
|
128
|
+
const { identity } = authorization;
|
|
123
129
|
return context.json({
|
|
124
130
|
id: identity.id,
|
|
125
131
|
email: identity.email,
|
|
126
132
|
name: identity.name ?? null,
|
|
133
|
+
// ⚠️ What the shell may DRAW, never what it may do (#416). Every admin route below still asks
|
|
134
|
+
// `requireCapability` for itself, and a browser that lied here would gain nothing but a
|
|
135
|
+
// button that answers 403.
|
|
136
|
+
isAdmin: authorization.can("intel", "admin"),
|
|
127
137
|
});
|
|
128
138
|
});
|
|
129
139
|
app.get("/nodes", async (context) => {
|
|
130
|
-
const auth = requireCapability(context, "
|
|
140
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
131
141
|
const url = new URL(context.req.url);
|
|
132
142
|
const unknownQuery = [];
|
|
133
143
|
url.searchParams.forEach((_value, key) => {
|
|
@@ -146,7 +156,7 @@ export function createHttp(deps) {
|
|
|
146
156
|
return context.json(await deps.nodes.list(asActor(auth), input));
|
|
147
157
|
});
|
|
148
158
|
app.get("/nodes/graph", async (context) => {
|
|
149
|
-
const auth = requireCapability(context, "
|
|
159
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
150
160
|
const url = new URL(context.req.url);
|
|
151
161
|
const input = NodeGraphInput.parse({
|
|
152
162
|
limit: url.searchParams.has("limit") ? Number(url.searchParams.get("limit")) : undefined,
|
|
@@ -156,14 +166,14 @@ export function createHttp(deps) {
|
|
|
156
166
|
// ⚠️ Before `/nodes/:nodeId`, or the router would read "export" as a node ID. The root has no
|
|
157
167
|
// node to address, so the whole installation — as this caller may read it — exports here (#136).
|
|
158
168
|
app.get("/nodes/export", async (context) => {
|
|
159
|
-
const auth = requireCapability(context, "
|
|
169
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
160
170
|
return zipResponse(await deps.bundle.exportSubtree(asBundleActor(auth), null));
|
|
161
171
|
});
|
|
162
172
|
// The reverse door (#137): a zip bundle — or a naked zipped folder — lands as a new subtree.
|
|
163
173
|
// The body IS the zip, streamed to the service rather than read whole here; `knowledge/create`
|
|
164
174
|
// like every other way of making nodes, and the resource-level write check is the service's.
|
|
165
175
|
async function importBundle(context, targetNodeId) {
|
|
166
|
-
const auth = requireCapability(context, "
|
|
176
|
+
const auth = requireCapability(context, "nodes", "create");
|
|
167
177
|
const idempotencyKey = ImportIdempotencyKey.parse(context.req.header("idempotency-key"));
|
|
168
178
|
const body = context.req.raw.body;
|
|
169
179
|
if (body === null) {
|
|
@@ -181,7 +191,7 @@ export function createHttp(deps) {
|
|
|
181
191
|
// ⚠️ Before `/nodes/:nodeId`, or the router would read "import" as a node ID.
|
|
182
192
|
app.post("/nodes/import", async (context) => await importBundle(context, null));
|
|
183
193
|
app.get("/nodes/:nodeId", async (context) => {
|
|
184
|
-
const auth = requireCapability(context, "
|
|
194
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
185
195
|
return context.json(await deps.nodes.get(asActor(auth), context.req.param("nodeId")));
|
|
186
196
|
});
|
|
187
197
|
app.post("/nodes/:nodeId/import", async (context) => {
|
|
@@ -190,19 +200,19 @@ export function createHttp(deps) {
|
|
|
190
200
|
});
|
|
191
201
|
// One subtree — a folder with everything beneath it, or a single node — as a zip bundle (#136).
|
|
192
202
|
app.get("/nodes/:nodeId/export", async (context) => {
|
|
193
|
-
const auth = requireCapability(context, "
|
|
203
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
194
204
|
const input = GetNodeInput.parse({ nodeId: context.req.param("nodeId") });
|
|
195
205
|
return zipResponse(await deps.bundle.exportSubtree(asBundleActor(auth), input.nodeId));
|
|
196
206
|
});
|
|
197
207
|
app.get("/nodes/:nodeId/versions", async (context) => {
|
|
198
|
-
const auth = requireCapability(context, "
|
|
208
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
199
209
|
return context.json(await deps.nodes.listVersions(asActor(auth), context.req.param("nodeId")));
|
|
200
210
|
});
|
|
201
211
|
// The content behind one row of the list above (#147): what a version-pinned citation points at.
|
|
202
212
|
// Read capability like every other node read; the resource ACL and the node/version pairing are
|
|
203
213
|
// the service's to refuse.
|
|
204
214
|
app.get("/nodes/:nodeId/versions/:versionId/content", async (context) => {
|
|
205
|
-
const auth = requireCapability(context, "
|
|
215
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
206
216
|
const input = GetNodeVersionInput.parse({
|
|
207
217
|
nodeId: context.req.param("nodeId"),
|
|
208
218
|
versionId: context.req.param("versionId"),
|
|
@@ -214,13 +224,13 @@ export function createHttp(deps) {
|
|
|
214
224
|
// resource address says nothing to an HTTP client, whose address for the bytes is the sibling
|
|
215
225
|
// route below.
|
|
216
226
|
app.get("/nodes/:nodeId/attachment/meta", async (context) => {
|
|
217
|
-
const auth = requireCapability(context, "
|
|
227
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
218
228
|
const input = GetNodeInput.parse({ nodeId: context.req.param("nodeId") });
|
|
219
229
|
const { node, version } = await deps.nodes.getAttachment(asActor(auth), input.nodeId);
|
|
220
230
|
return context.json({ node, version });
|
|
221
231
|
});
|
|
222
232
|
app.get("/nodes/:nodeId/attachment", async (context) => {
|
|
223
|
-
const auth = requireCapability(context, "
|
|
233
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
224
234
|
const { attachment, body } = await deps.nodes.readAttachment(asActor(auth), context.req.param("nodeId"));
|
|
225
235
|
return new Response(body, {
|
|
226
236
|
headers: {
|
|
@@ -232,13 +242,13 @@ export function createHttp(deps) {
|
|
|
232
242
|
});
|
|
233
243
|
});
|
|
234
244
|
app.get("/nodes/:nodeId/table", async (context) => {
|
|
235
|
-
const auth = requireCapability(context, "
|
|
245
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
236
246
|
return context.json(await deps.nodes.getTable(asActor(auth), context.req.param("nodeId")));
|
|
237
247
|
});
|
|
238
248
|
// Defining the header and appending rows are two routes because they are two decisions: the
|
|
239
249
|
// header is written once and is the contract, an append is the everyday write (#40).
|
|
240
250
|
app.post("/nodes/:nodeId/table", async (context) => {
|
|
241
|
-
const auth = requireCapability(context, "
|
|
251
|
+
const auth = requireCapability(context, "nodes", "write");
|
|
242
252
|
const input = DefineTableInput.parse(await context.req.json().catch(() => null));
|
|
243
253
|
if (input.nodeId !== context.req.param("nodeId")) {
|
|
244
254
|
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
@@ -246,7 +256,7 @@ export function createHttp(deps) {
|
|
|
246
256
|
return context.json(await deps.nodes.defineTable(asActor(auth), input), 201);
|
|
247
257
|
});
|
|
248
258
|
app.post("/nodes/:nodeId/table/rows", async (context) => {
|
|
249
|
-
const auth = requireCapability(context, "
|
|
259
|
+
const auth = requireCapability(context, "nodes", "write");
|
|
250
260
|
const input = AppendTableRowsInput.parse(await context.req.json().catch(() => null));
|
|
251
261
|
if (input.nodeId !== context.req.param("nodeId")) {
|
|
252
262
|
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
@@ -258,7 +268,7 @@ export function createHttp(deps) {
|
|
|
258
268
|
// the `baseVersionId` that says which state they mean. Each writes one snapshot version, so the
|
|
259
269
|
// 201 is the same statement the append's 201 makes.
|
|
260
270
|
app.post("/nodes/:nodeId/table/rows/update", async (context) => {
|
|
261
|
-
const auth = requireCapability(context, "
|
|
271
|
+
const auth = requireCapability(context, "nodes", "write");
|
|
262
272
|
const input = UpdateTableRowsInput.parse(await context.req.json().catch(() => null));
|
|
263
273
|
if (input.nodeId !== context.req.param("nodeId")) {
|
|
264
274
|
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
@@ -266,15 +276,15 @@ export function createHttp(deps) {
|
|
|
266
276
|
return context.json(await deps.nodes.updateTableRows(asActor(auth), input), 201);
|
|
267
277
|
});
|
|
268
278
|
app.post("/nodes/:nodeId/table/rows/delete", async (context) => {
|
|
269
|
-
const auth = requireCapability(context, "
|
|
279
|
+
const auth = requireCapability(context, "nodes", "write");
|
|
270
280
|
const input = DeleteTableRowsInput.parse(await context.req.json().catch(() => null));
|
|
271
281
|
if (input.nodeId !== context.req.param("nodeId")) {
|
|
272
282
|
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
273
283
|
}
|
|
274
284
|
return context.json(await deps.nodes.deleteTableRows(asActor(auth), input), 201);
|
|
275
285
|
});
|
|
276
|
-
app.
|
|
277
|
-
const auth = requireCapability(context, "
|
|
286
|
+
app.patch("/nodes/:nodeId/table", async (context) => {
|
|
287
|
+
const auth = requireCapability(context, "nodes", "write");
|
|
278
288
|
const input = RedefineTableInput.parse(await context.req.json().catch(() => null));
|
|
279
289
|
if (input.nodeId !== context.req.param("nodeId")) {
|
|
280
290
|
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
@@ -286,7 +296,7 @@ export function createHttp(deps) {
|
|
|
286
296
|
// reaches, nothing in `intel-data-provider` asks for it, and a route with no caller is the thing
|
|
287
297
|
// YAGNI is about. The screen is anchrd/intel#358, and it brings this route with it.
|
|
288
298
|
app.get("/nodes/:nodeId/links", async (context) => {
|
|
289
|
-
const auth = requireCapability(context, "
|
|
299
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
290
300
|
return context.json(await deps.nodes.listLinks(asActor(auth), context.req.param("nodeId")));
|
|
291
301
|
});
|
|
292
302
|
// A document link's name, for whoever is reading the text (#41). A POST because the list of IDs
|
|
@@ -297,27 +307,27 @@ export function createHttp(deps) {
|
|
|
297
307
|
// There is no route to create or delete a link any more. A relationship is written where it is
|
|
298
308
|
// meant, in the text, and `POST /nodes/:nodeId/versions` is what records it.
|
|
299
309
|
app.post("/nodes/links/resolve", async (context) => {
|
|
300
|
-
const auth = requireCapability(context, "
|
|
310
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
301
311
|
const input = ResolveNodeLinksInput.parse(await context.req.json().catch(() => null));
|
|
302
312
|
return context.json(await deps.nodes.resolveLinks(asActor(auth), input));
|
|
303
313
|
});
|
|
304
314
|
app.post("/nodes/search", async (context) => {
|
|
305
|
-
const auth = requireCapability(context, "
|
|
315
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
306
316
|
const input = SearchInput.parse(await context.req.json().catch(() => null));
|
|
307
317
|
return context.json(await deps.nodes.search(asActor(auth), input));
|
|
308
318
|
});
|
|
309
|
-
app.post("/
|
|
319
|
+
app.post("/nodes/reindex", async (context) => {
|
|
310
320
|
const auth = requireCapability(context, "intel", "admin");
|
|
311
321
|
z.strictObject({}).parse(await context.req.json().catch(() => null));
|
|
312
322
|
return context.json(await deps.nodes.reindex(asActor(auth)), 202);
|
|
313
323
|
});
|
|
314
324
|
app.post("/nodes", async (context) => {
|
|
315
|
-
const auth = requireCapability(context, "
|
|
325
|
+
const auth = requireCapability(context, "nodes", "create");
|
|
316
326
|
const input = CreateNodeInput.parse(await context.req.json().catch(() => null));
|
|
317
327
|
return context.json(await deps.nodes.create(asActor(auth), input), 201);
|
|
318
328
|
});
|
|
319
329
|
app.post("/nodes/:nodeId/versions", async (context) => {
|
|
320
|
-
const auth = requireCapability(context, "
|
|
330
|
+
const auth = requireCapability(context, "nodes", "write");
|
|
321
331
|
const input = SaveNodeVersionInput.parse(await context.req.json().catch(() => null));
|
|
322
332
|
if (input.nodeId !== context.req.param("nodeId")) {
|
|
323
333
|
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
@@ -325,7 +335,7 @@ export function createHttp(deps) {
|
|
|
325
335
|
return context.json(await deps.nodes.save(asActor(auth), input), 201);
|
|
326
336
|
});
|
|
327
337
|
app.post("/nodes/:nodeId/attachment", async (context) => {
|
|
328
|
-
const auth = requireCapability(context, "
|
|
338
|
+
const auth = requireCapability(context, "nodes", "write");
|
|
329
339
|
const input = SaveAttachmentInput.parse(await context.req.json().catch(() => null));
|
|
330
340
|
if (input.nodeId !== context.req.param("nodeId")) {
|
|
331
341
|
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
@@ -333,7 +343,7 @@ export function createHttp(deps) {
|
|
|
333
343
|
return context.json(await deps.nodes.saveAttachment(asActor(auth), input), 201);
|
|
334
344
|
});
|
|
335
345
|
app.patch("/nodes/:nodeId", async (context) => {
|
|
336
|
-
const auth = requireCapability(context, "
|
|
346
|
+
const auth = requireCapability(context, "nodes", "write");
|
|
337
347
|
const input = UpdateNodeInput.parse(await context.req.json().catch(() => null));
|
|
338
348
|
if (input.nodeId !== context.req.param("nodeId")) {
|
|
339
349
|
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
@@ -341,21 +351,21 @@ export function createHttp(deps) {
|
|
|
341
351
|
return context.json(await deps.nodes.update(asActor(auth), input));
|
|
342
352
|
});
|
|
343
353
|
app.post("/nodes/:nodeId/archive", async (context) => {
|
|
344
|
-
const auth = requireCapability(context, "
|
|
354
|
+
const auth = requireCapability(context, "nodes", "write");
|
|
345
355
|
const input = ArchiveNodeInput.parse(await context.req.json().catch(() => null));
|
|
346
356
|
if (input.nodeId !== context.req.param("nodeId")) {
|
|
347
357
|
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
348
358
|
}
|
|
349
359
|
// The caller's own bearer, for the one thing archiving may have to do in Gate: switch off the
|
|
350
360
|
// Application behind an agent (#182). Every other kind never reaches it.
|
|
351
|
-
return context.json(await deps.nodes.archive(asActor(auth), input
|
|
361
|
+
return context.json(await deps.nodes.archive(asActor(auth), input));
|
|
352
362
|
});
|
|
353
363
|
app.get("/nodes/:nodeId/grants", async (context) => {
|
|
354
|
-
const auth = requireCapability(context, "
|
|
364
|
+
const auth = requireCapability(context, "nodes", "share");
|
|
355
365
|
return context.json(await deps.nodes.listGrants(asActor(auth), context.req.param("nodeId")));
|
|
356
366
|
});
|
|
357
367
|
app.post("/nodes/:nodeId/grants", async (context) => {
|
|
358
|
-
const auth = requireCapability(context, "
|
|
368
|
+
const auth = requireCapability(context, "nodes", "share");
|
|
359
369
|
const input = ShareInput.parse(await context.req.json().catch(() => null));
|
|
360
370
|
if (input.resourceId !== context.req.param("nodeId")) {
|
|
361
371
|
throw new IntelError(400, "node_id_mismatch", "Path and body resource IDs differ");
|
|
@@ -363,7 +373,7 @@ export function createHttp(deps) {
|
|
|
363
373
|
return context.json(await deps.nodes.share(asActor(auth), input), 201);
|
|
364
374
|
});
|
|
365
375
|
app.post("/nodes/:nodeId/grants/:grantId/revoke", async (context) => {
|
|
366
|
-
const auth = requireCapability(context, "
|
|
376
|
+
const auth = requireCapability(context, "nodes", "share");
|
|
367
377
|
const input = RevokeGrantInput.parse(await context.req.json().catch(() => null));
|
|
368
378
|
if (input.resourceId !== context.req.param("nodeId") ||
|
|
369
379
|
input.grantId !== context.req.param("grantId")) {
|
|
@@ -552,7 +562,7 @@ export function createHttp(deps) {
|
|
|
552
562
|
const input = GetFlowRunInput.parse({ runId: context.req.param("runId") });
|
|
553
563
|
return context.json(await deps.flows.listRunSteps(asFlowActor(auth), input.runId));
|
|
554
564
|
});
|
|
555
|
-
app.post("/flow-runs/:runId/complete", async (context) => {
|
|
565
|
+
app.post("/flow-runs/:runId/steps/complete", async (context) => {
|
|
556
566
|
const auth = requireCapability(context, "flows", "run");
|
|
557
567
|
const input = CompleteFlowRunStepInput.parse(await context.req.json().catch(() => null));
|
|
558
568
|
if (input.runId !== context.req.param("runId")) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BlockNoteDocument, BlockNoteMediaType } from "@anchrd/intel-contract";
|
|
1
|
+
import { BlockNoteDocument, BlockNoteMediaType } from "@anchrd/intel-contract/node";
|
|
2
2
|
export class PermanentIndexingError extends Error {
|
|
3
3
|
}
|
|
4
4
|
// The key under which a node with exactly one vector is filed — since #390 that is every kind. It
|
package/dist/intel/intel.js
CHANGED
|
@@ -60,7 +60,16 @@ export function createIntel(deps) {
|
|
|
60
60
|
if (error instanceof IntelError) {
|
|
61
61
|
return context.redirect(`/tools?connectError=${encodeURIComponent(error.code)}`);
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
// ⚠️ Everything else used to be rethrown, and the outer handler turned it into a bare 500
|
|
64
|
+
// with a generic body (#425). For an API that is the right answer; for a route a BROWSER is
|
|
65
|
+
// sent to it is a dead end — the reader gets JSON instead of a screen, and the Tools page
|
|
66
|
+
// never learns that the attempt happened at all, so it says "no access" about a sign-in
|
|
67
|
+
// that crashed.
|
|
68
|
+
//
|
|
69
|
+
// The trace still goes to the log, which is the operator's channel; what changes is that
|
|
70
|
+
// the reader is returned to a screen that can say "this broke" instead of "you may not".
|
|
71
|
+
reportUnexpectedError(error);
|
|
72
|
+
return context.redirect("/tools?connectError=portal_sign_in_failed");
|
|
64
73
|
}
|
|
65
74
|
});
|
|
66
75
|
app.get("/auth/callback", async (context) => await browserAuth.callback(new URL(context.req.url), context.req.raw.headers));
|
package/dist/mcp/mcp.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { IdempotencyKey, IntelId } from "@anchrd/intel-contract";
|
|
2
|
+
import { ArchiveFlowInput, CreateFlowInput, GetFlowInput, GetFlowVersionInput, ListFlowsInput, PreviewFlowPublishInput, PublishFlowInput, RelationGraphInput, SaveFlowVersionInput, UnpublishFlowInput, UpdateFlowInput, } from "@anchrd/intel-contract/flow";
|
|
3
|
+
import { CancelFlowRunInput, CompleteFlowRunStepInput, GetFlowRunInput, ListFlowRunsInput, StartFlowRunInput, } from "@anchrd/intel-contract/flow-run";
|
|
4
|
+
import { ArchiveNodeInput, CreateNodeInput, GetNodeInput, GetNodeVersionInput, ListNodesInput, NodeGraphInput, ResolveNodeLinksInput, SaveAttachmentInput, SaveNodeVersionInput, SearchInput, UpdateNodeInput, } from "@anchrd/intel-contract/node";
|
|
5
|
+
import { ListGrantsInput, RevokeGrantInput, ShareInput } from "@anchrd/intel-contract/share";
|
|
6
|
+
import { AppendTableRowsInput, DefineTableInput, DeleteTableRowsInput, GetTableInput, RedefineTableInput, UpdateTableRowsInput, } from "@anchrd/intel-contract/table";
|
|
7
|
+
import { ExecuteToolInput, TestToolInput } from "@anchrd/intel-contract/tool";
|
|
2
8
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
9
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
4
10
|
import { z } from "zod";
|
|
@@ -65,14 +71,20 @@ export async function handleMcp(request, deps) {
|
|
|
65
71
|
const EmptyInput = z.strictObject({});
|
|
66
72
|
// `null` is the root: the whole tree as this caller may read it. The field is required rather
|
|
67
73
|
// than defaulted so "everything" is always said, never fallen into.
|
|
68
|
-
const ExportManifestInput = z.strictObject({
|
|
74
|
+
const ExportManifestInput = z.strictObject({
|
|
75
|
+
nodeId: IntelId.nullable().describe('Folder or node to plan the export of, or `null` for the whole tree as this caller may read it. Required rather than defaulted, so "everything" is always said and never fallen into.'),
|
|
76
|
+
});
|
|
69
77
|
// The zip travels inline as base64, which is MCP's one way of carrying bytes. ~14M characters is
|
|
70
78
|
// ~10 MB of zip — the same order as the attachment inline limit, and for the same isolate-memory
|
|
71
79
|
// reason. Bigger bundles take the HTTP door, which streams.
|
|
72
80
|
const ImportBundleInput = z.strictObject({
|
|
73
|
-
nodeId: IntelId.nullable(),
|
|
74
|
-
zipBase64: z
|
|
75
|
-
|
|
81
|
+
nodeId: IntelId.nullable().describe("Folder the bundle lands under, or `null` for the top level. The bundle's own structure is kept beneath it."),
|
|
82
|
+
zipBase64: z
|
|
83
|
+
.string()
|
|
84
|
+
.min(1)
|
|
85
|
+
.max(14_000_000)
|
|
86
|
+
.describe("The zip bundle, base64-encoded. Around 10 MB of zip; a bigger bundle takes the HTTP door, which streams instead of holding it in memory."),
|
|
87
|
+
idempotencyKey: IdempotencyKey,
|
|
76
88
|
});
|
|
77
89
|
function decodeZipBase64(value) {
|
|
78
90
|
if (value.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value)) {
|
|
@@ -105,7 +117,7 @@ export async function handleMcp(request, deps) {
|
|
|
105
117
|
email: deps.authorization.identity.email,
|
|
106
118
|
name: deps.authorization.identity.name ?? null,
|
|
107
119
|
}));
|
|
108
|
-
if (permits(deps.authorization, "
|
|
120
|
+
if (permits(deps.authorization, "nodes", "read")) {
|
|
109
121
|
server.registerTool("node_list", {
|
|
110
122
|
title: "List nodes",
|
|
111
123
|
description: "List authorized folders, documents, attachments, and tables under one parent.",
|
|
@@ -130,7 +142,7 @@ export async function handleMcp(request, deps) {
|
|
|
130
142
|
openWorldHint: false,
|
|
131
143
|
},
|
|
132
144
|
}, async (input) => text(await deps.nodes.get(actor, input.nodeId)));
|
|
133
|
-
server.registerTool("
|
|
145
|
+
server.registerTool("node_version_list", {
|
|
134
146
|
title: "List node versions",
|
|
135
147
|
description: "List immutable versions of one authorized node.",
|
|
136
148
|
inputSchema: GetNodeInput,
|
|
@@ -219,7 +231,7 @@ export async function handleMcp(request, deps) {
|
|
|
219
231
|
openWorldHint: false,
|
|
220
232
|
},
|
|
221
233
|
}, async (input) => text(await deps.nodes.getTable(actor, input.nodeId)));
|
|
222
|
-
server.registerTool("
|
|
234
|
+
server.registerTool("node_link_list", {
|
|
223
235
|
title: "List node links",
|
|
224
236
|
description: "List authorized outgoing links and backlinks for one node.",
|
|
225
237
|
inputSchema: GetNodeInput,
|
|
@@ -233,12 +245,12 @@ export async function handleMcp(request, deps) {
|
|
|
233
245
|
}, async (input) => text(await deps.nodes.listLinks(actor, input.nodeId)));
|
|
234
246
|
// The reader's half of a document link (#41): the titles of the linked documents this caller
|
|
235
247
|
// may see. There is no tool to create or delete a link — a relationship is written in the text
|
|
236
|
-
// and `
|
|
248
|
+
// and `node_version_create` is what records it, so there is only one way to make one.
|
|
237
249
|
//
|
|
238
250
|
// ⚠️ A target this caller may not reach, or one that is gone, is simply absent from the answer.
|
|
239
251
|
// The two are indistinguishable on purpose: telling them apart would confirm that a document
|
|
240
252
|
// exists somewhere they cannot look.
|
|
241
|
-
server.registerTool("
|
|
253
|
+
server.registerTool("node_link_resolve", {
|
|
242
254
|
title: "Resolve node links",
|
|
243
255
|
description: "Resolve document link targets to the titles this caller is authorized to see. Targets that are unreachable or deleted are absent from the result.",
|
|
244
256
|
inputSchema: ResolveNodeLinksInput,
|
|
@@ -268,12 +280,12 @@ export async function handleMcp(request, deps) {
|
|
|
268
280
|
// resource payload travels inline as base64 and would have to fit in Worker memory twice, the
|
|
269
281
|
// same limit that caps `node_attachment_get` at 10 MB — a whole subtree is the case that limit
|
|
270
282
|
// exists for. HTTP `GET /nodes/:nodeId/export` streams the real archive.
|
|
271
|
-
server.registerTool("
|
|
272
|
-
title: "
|
|
283
|
+
server.registerTool("node_export", {
|
|
284
|
+
title: "Export nodes",
|
|
273
285
|
description: "Plan a bundle export: the manifest of one authorized subtree (or the whole tree when nodeId is null) with each entry's bundle path, kind, and media type. Content bytes are fetched per node; the binary zip is served over HTTP.",
|
|
274
286
|
inputSchema: ExportManifestInput,
|
|
275
287
|
annotations: {
|
|
276
|
-
title: "
|
|
288
|
+
title: "Export nodes",
|
|
277
289
|
readOnlyHint: true,
|
|
278
290
|
destructiveHint: false,
|
|
279
291
|
idempotentHint: true,
|
|
@@ -295,7 +307,7 @@ export async function handleMcp(request, deps) {
|
|
|
295
307
|
},
|
|
296
308
|
}, async () => text(await deps.nodes.reindex(actor)));
|
|
297
309
|
}
|
|
298
|
-
if (permits(deps.authorization, "
|
|
310
|
+
if (permits(deps.authorization, "nodes", "create")) {
|
|
299
311
|
server.registerTool("node_create", {
|
|
300
312
|
title: "Create node",
|
|
301
313
|
description: "Create a governed folder, document, attachment, or table node.",
|
|
@@ -309,25 +321,25 @@ export async function handleMcp(request, deps) {
|
|
|
309
321
|
},
|
|
310
322
|
}, async (input) => text(await deps.nodes.create(actor, input)));
|
|
311
323
|
}
|
|
312
|
-
if (permits(deps.authorization, "
|
|
313
|
-
server.registerTool("
|
|
314
|
-
title: "
|
|
324
|
+
if (permits(deps.authorization, "nodes", "write")) {
|
|
325
|
+
server.registerTool("node_version_create", {
|
|
326
|
+
title: "Create node version",
|
|
315
327
|
description: "Append an immutable content version using an optimistic base version.",
|
|
316
328
|
inputSchema: SaveNodeVersionInput,
|
|
317
329
|
annotations: {
|
|
318
|
-
title: "
|
|
330
|
+
title: "Create node version",
|
|
319
331
|
readOnlyHint: false,
|
|
320
332
|
destructiveHint: false,
|
|
321
333
|
idempotentHint: true,
|
|
322
334
|
openWorldHint: false,
|
|
323
335
|
},
|
|
324
336
|
}, async (input) => text(await deps.nodes.save(actor, input)));
|
|
325
|
-
server.registerTool("
|
|
326
|
-
title: "
|
|
337
|
+
server.registerTool("node_attachment_create", {
|
|
338
|
+
title: "Create node attachment version",
|
|
327
339
|
description: "Append immutable base64 file bytes to an attachment node.",
|
|
328
340
|
inputSchema: SaveAttachmentInput,
|
|
329
341
|
annotations: {
|
|
330
|
-
title: "
|
|
342
|
+
title: "Create node attachment version",
|
|
331
343
|
readOnlyHint: false,
|
|
332
344
|
destructiveHint: false,
|
|
333
345
|
idempotentHint: true,
|
|
@@ -338,12 +350,12 @@ export async function handleMcp(request, deps) {
|
|
|
338
350
|
// the export — or a naked zipped folder — lands as a new subtree under the target. Same
|
|
339
351
|
// service as HTTP, so authorization, limits, remapping, and the all-or-nothing write cannot
|
|
340
352
|
// differ by surface; only the transport differs (inline base64 here, a streamed body there).
|
|
341
|
-
server.registerTool("
|
|
342
|
-
title: "Import
|
|
353
|
+
server.registerTool("node_import", {
|
|
354
|
+
title: "Import nodes",
|
|
343
355
|
description: "Import a zip bundle (base64) as a new subtree under an authorized target folder — null files at the root. Kinds come from manifest.json when present, from file extensions otherwise. Always creates new nodes; links and flow references between bundled entries are rewritten to the new IDs. Bundles beyond ~10 MB take the streaming HTTP route POST /nodes/:nodeId/import.",
|
|
344
356
|
inputSchema: ImportBundleInput,
|
|
345
357
|
annotations: {
|
|
346
|
-
title: "Import
|
|
358
|
+
title: "Import nodes",
|
|
347
359
|
readOnlyHint: false,
|
|
348
360
|
destructiveHint: false,
|
|
349
361
|
idempotentHint: true,
|
|
@@ -354,12 +366,12 @@ export async function handleMcp(request, deps) {
|
|
|
354
366
|
zip: decodeZipBase64(input.zipBase64),
|
|
355
367
|
idempotencyKey: input.idempotencyKey,
|
|
356
368
|
})));
|
|
357
|
-
server.registerTool("
|
|
358
|
-
title: "
|
|
369
|
+
server.registerTool("node_table_create", {
|
|
370
|
+
title: "Create node table columns",
|
|
359
371
|
description: "Write the column names of an empty table. The header is the contract every append is checked against and cannot be rewritten.",
|
|
360
372
|
inputSchema: DefineTableInput,
|
|
361
373
|
annotations: {
|
|
362
|
-
title: "
|
|
374
|
+
title: "Create node table columns",
|
|
363
375
|
readOnlyHint: false,
|
|
364
376
|
destructiveHint: false,
|
|
365
377
|
idempotentHint: true,
|
|
@@ -368,12 +380,12 @@ export async function handleMcp(request, deps) {
|
|
|
368
380
|
}, async (input) => text(await deps.nodes.defineTable(actor, input)));
|
|
369
381
|
// The tool #40 exists for: an agent collecting findings on a schedule appends them without
|
|
370
382
|
// reading or resending what is already there, and two agents appending at once lose nothing.
|
|
371
|
-
server.registerTool("
|
|
372
|
-
title: "
|
|
383
|
+
server.registerTool("node_table_row_create", {
|
|
384
|
+
title: "Create node table rows",
|
|
373
385
|
description: "Append rows to a table without reading or resending its existing content. Each row must have exactly as many cells as the table has columns; a row that does not is rejected and nothing is written.",
|
|
374
386
|
inputSchema: AppendTableRowsInput,
|
|
375
387
|
annotations: {
|
|
376
|
-
title: "
|
|
388
|
+
title: "Create node table rows",
|
|
377
389
|
readOnlyHint: false,
|
|
378
390
|
destructiveHint: false,
|
|
379
391
|
// The idempotency key makes a repeat of the same call a no-op; without one, appending
|
|
@@ -386,7 +398,7 @@ export async function handleMcp(request, deps) {
|
|
|
386
398
|
// rows — they carry no IDs on purpose — so every mutation names the version the positions were
|
|
387
399
|
// read from and is refused with `version_conflict` when the table moved on. No `knowledge_*`
|
|
388
400
|
// alias: these tools are new under the post-#125 naming and never carried the old prefix.
|
|
389
|
-
server.registerTool("
|
|
401
|
+
server.registerTool("node_table_row_update", {
|
|
390
402
|
title: "Update node table rows",
|
|
391
403
|
description: "Replace rows at the given zero-based positions. Requires the baseVersionId the positions were read from (see node_table_get); answers version_conflict and writes nothing when the table has changed since. Each replacement row must match the column count.",
|
|
392
404
|
inputSchema: UpdateTableRowsInput,
|
|
@@ -399,7 +411,7 @@ export async function handleMcp(request, deps) {
|
|
|
399
411
|
openWorldHint: false,
|
|
400
412
|
},
|
|
401
413
|
}, async (input) => text(await deps.nodes.updateTableRows(actor, input)));
|
|
402
|
-
server.registerTool("
|
|
414
|
+
server.registerTool("node_table_row_delete", {
|
|
403
415
|
title: "Delete node table rows",
|
|
404
416
|
description: "Remove rows at the given zero-based positions. Requires the baseVersionId the positions were read from; answers version_conflict and writes nothing when the table has changed since.",
|
|
405
417
|
inputSchema: DeleteTableRowsInput,
|
|
@@ -411,12 +423,12 @@ export async function handleMcp(request, deps) {
|
|
|
411
423
|
openWorldHint: false,
|
|
412
424
|
},
|
|
413
425
|
}, async (input) => text(await deps.nodes.deleteTableRows(actor, input)));
|
|
414
|
-
server.registerTool("
|
|
415
|
-
title: "
|
|
426
|
+
server.registerTool("node_table_update", {
|
|
427
|
+
title: "Update node table columns",
|
|
416
428
|
description: "Change the header of a defined table through an explicit column mapping: each new column names the current column that fills it (rename or keep), names none to start empty (add), and a current column no entry names is removed with its cells. Requires the baseVersionId the mapping was read from. Defining over an existing header without a mapping stays refused.",
|
|
417
429
|
inputSchema: RedefineTableInput,
|
|
418
430
|
annotations: {
|
|
419
|
-
title: "
|
|
431
|
+
title: "Update node table columns",
|
|
420
432
|
readOnlyHint: false,
|
|
421
433
|
destructiveHint: true,
|
|
422
434
|
idempotentHint: true,
|
|
@@ -437,53 +449,53 @@ export async function handleMcp(request, deps) {
|
|
|
437
449
|
}, async (input) => text(await deps.nodes.update(actor, input)));
|
|
438
450
|
server.registerTool("node_archive", {
|
|
439
451
|
title: "Archive node",
|
|
440
|
-
description: "Archive or restore one node
|
|
452
|
+
description: "Archive or restore one node. Archiving hides it from listings and search and takes its vectors out of the index; nothing is deleted and every version stays readable, so restoring is the same call with `archived: false`.",
|
|
441
453
|
inputSchema: ArchiveNodeInput,
|
|
442
454
|
annotations: {
|
|
443
455
|
title: "Archive node",
|
|
444
456
|
readOnlyHint: false,
|
|
445
457
|
destructiveHint: true,
|
|
446
458
|
idempotentHint: true,
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
|
|
459
|
+
// ⚠️ `true` until #418, and the reason is worth keeping: archiving an agent used to
|
|
460
|
+
// switch its Gate Application off (#182), which was the one node mutation that reached
|
|
461
|
+
// outside. With the agents parked nothing here leaves the installation — and an
|
|
462
|
+
// openWorldHint that is wrong in the cautious direction is still wrong, because it is
|
|
463
|
+
// the field a client decides on.
|
|
464
|
+
openWorldHint: false,
|
|
450
465
|
},
|
|
451
|
-
},
|
|
452
|
-
// The caller's own bearer, exactly as the runtime proxy uses it and for the same reason:
|
|
453
|
-
// switching a machine principal is an act in Gate, performed by the person asking.
|
|
454
|
-
async (input) => text(await deps.nodes.archive(actor, input, { token: deps.bearer })));
|
|
466
|
+
}, async (input) => text(await deps.nodes.archive(actor, input)));
|
|
455
467
|
}
|
|
456
|
-
if (permits(deps.authorization, "
|
|
457
|
-
server.registerTool("
|
|
458
|
-
title: "List node
|
|
468
|
+
if (permits(deps.authorization, "nodes", "share")) {
|
|
469
|
+
server.registerTool("node_grant_list", {
|
|
470
|
+
title: "List node grants",
|
|
459
471
|
description: "List direct grants for nodes the caller is allowed to manage.",
|
|
460
472
|
inputSchema: ListGrantsInput,
|
|
461
473
|
annotations: {
|
|
462
|
-
title: "List node
|
|
474
|
+
title: "List node grants",
|
|
463
475
|
readOnlyHint: true,
|
|
464
476
|
destructiveHint: false,
|
|
465
477
|
idempotentHint: true,
|
|
466
478
|
openWorldHint: false,
|
|
467
479
|
},
|
|
468
480
|
}, async (input) => text(await deps.nodes.listGrants(actor, input.resourceId)));
|
|
469
|
-
server.registerTool("
|
|
470
|
-
title: "
|
|
481
|
+
server.registerTool("node_grant_create", {
|
|
482
|
+
title: "Grant node access",
|
|
471
483
|
description: "Grant inherited node access to a Gate user, verified email, or the organization.",
|
|
472
484
|
inputSchema: ShareInput,
|
|
473
485
|
annotations: {
|
|
474
|
-
title: "
|
|
486
|
+
title: "Grant node access",
|
|
475
487
|
readOnlyHint: false,
|
|
476
488
|
destructiveHint: false,
|
|
477
489
|
idempotentHint: true,
|
|
478
490
|
openWorldHint: false,
|
|
479
491
|
},
|
|
480
492
|
}, async (input) => text(await deps.nodes.share(actor, input)));
|
|
481
|
-
server.registerTool("
|
|
482
|
-
title: "Revoke node
|
|
493
|
+
server.registerTool("node_grant_revoke", {
|
|
494
|
+
title: "Revoke node grant",
|
|
483
495
|
description: "Revoke one direct node grant by ID.",
|
|
484
496
|
inputSchema: RevokeGrantInput,
|
|
485
497
|
annotations: {
|
|
486
|
-
title: "Revoke node
|
|
498
|
+
title: "Revoke node grant",
|
|
487
499
|
readOnlyHint: false,
|
|
488
500
|
destructiveHint: true,
|
|
489
501
|
idempotentHint: true,
|
|
@@ -492,7 +504,7 @@ export async function handleMcp(request, deps) {
|
|
|
492
504
|
}, async (input) => text(await deps.nodes.revokeGrant(actor, input)));
|
|
493
505
|
}
|
|
494
506
|
if (permits(deps.authorization, "flows", "read")) {
|
|
495
|
-
server.registerTool("
|
|
507
|
+
server.registerTool("flow_list", {
|
|
496
508
|
title: "List flows",
|
|
497
509
|
description: "List authorized flows, either all of them or the ones filed in one folder of the shared tree.",
|
|
498
510
|
inputSchema: ListFlowsInput,
|
|
@@ -519,7 +531,7 @@ export async function handleMcp(request, deps) {
|
|
|
519
531
|
// Named like `knowledge_versions_list`, because it is the same question about the other kind of
|
|
520
532
|
// thing in the tree. This pair is what turns the `versionId` that `flow_publish` and
|
|
521
533
|
// `flow_publish_preview` demand into something an MCP client can actually obtain (#144).
|
|
522
|
-
server.registerTool("
|
|
534
|
+
server.registerTool("flow_version_list", {
|
|
523
535
|
title: "List flow versions",
|
|
524
536
|
description: "List the immutable versions of one authorized flow, oldest first, marking which of them is published. Metadata only; flow_version_get loads a version's graph.",
|
|
525
537
|
inputSchema: GetFlowInput,
|
|
@@ -543,7 +555,7 @@ export async function handleMcp(request, deps) {
|
|
|
543
555
|
openWorldHint: false,
|
|
544
556
|
},
|
|
545
557
|
}, async (input) => text(await deps.flows.getVersion(flowActor, input)));
|
|
546
|
-
server.registerTool("
|
|
558
|
+
server.registerTool("flow_call_list", {
|
|
547
559
|
title: "List called flows",
|
|
548
560
|
description: "List the flows one flow calls, read out of its graph rather than out of where it is filed.",
|
|
549
561
|
inputSchema: GetFlowInput,
|
|
@@ -558,7 +570,7 @@ export async function handleMcp(request, deps) {
|
|
|
558
570
|
// ⚠️ Only nodes the requesting user may see are in the answer, placeholders included: an edge to
|
|
559
571
|
// a grey box would already say that something is there. The same rule the screen follows,
|
|
560
572
|
// because it is the same service (#19).
|
|
561
|
-
server.registerTool("
|
|
573
|
+
server.registerTool("flow_graph", {
|
|
562
574
|
title: "Read the relation graph",
|
|
563
575
|
description: "Read what accesses what for one folder or one flow: which flow reads which document and which flow calls which flow.",
|
|
564
576
|
inputSchema: RelationGraphInput,
|
|
@@ -582,7 +594,7 @@ export async function handleMcp(request, deps) {
|
|
|
582
594
|
openWorldHint: false,
|
|
583
595
|
},
|
|
584
596
|
}, async (input) => text(await deps.flows.validate(flowActor, input.flowId)));
|
|
585
|
-
server.registerTool("
|
|
597
|
+
server.registerTool("flow_requirement_list", {
|
|
586
598
|
title: "List what a flow needs",
|
|
587
599
|
description: "List the documents and MCP tools one flow's graph names. Documents the calling user cannot see are counted rather than named, and no claim is made about whether anyone may reach them: for tools that cannot be known in advance, because the catalog is a live query with each user's own portal token.",
|
|
588
600
|
inputSchema: GetFlowInput,
|
|
@@ -609,7 +621,7 @@ export async function handleMcp(request, deps) {
|
|
|
609
621
|
// ⚠️ Model context like every other tool result: what a run did, never what it produced. The run
|
|
610
622
|
// input and output are absent because a run reaches nodes and tools with the rights of
|
|
611
623
|
// whoever started it, and only their own runs are the calling user's to read out that way.
|
|
612
|
-
server.registerTool("
|
|
624
|
+
server.registerTool("flow_run_list", {
|
|
613
625
|
title: "List flow runs",
|
|
614
626
|
description: "List the runs of one flow, newest first, with status, start, duration, what triggered them, and for a failed run the step that ended it. Optionally only the failed ones. Runs the calling user may not see are absent, and a failure inside a called flow they may not see is named by the calling step alone.",
|
|
615
627
|
inputSchema: ListFlowRunsInput,
|
|
@@ -621,7 +633,7 @@ export async function handleMcp(request, deps) {
|
|
|
621
633
|
openWorldHint: false,
|
|
622
634
|
},
|
|
623
635
|
}, async (input) => text(await deps.flows.listRuns(flowActor, input)));
|
|
624
|
-
server.registerTool("
|
|
636
|
+
server.registerTool("flow_run_step_list", {
|
|
625
637
|
title: "List flow run steps",
|
|
626
638
|
description: "List every completed step of one run, oldest first, with its outcome, branch, and the reason a failed step gives, plus the call chain the run belongs to.",
|
|
627
639
|
inputSchema: GetFlowRunInput,
|
|
@@ -635,7 +647,7 @@ export async function handleMcp(request, deps) {
|
|
|
635
647
|
}, async (input) => text(await deps.flows.listRunSteps(flowActor, input.runId)));
|
|
636
648
|
}
|
|
637
649
|
// A flow has no share tool of its own. Sharing happens on the folder a flow is filed in, through
|
|
638
|
-
//
|
|
650
|
+
// node_grant_create, so a narrower grant cannot sit beside the folder grant (ADR-0004 §2).
|
|
639
651
|
if (permits(deps.authorization, "flows", "create")) {
|
|
640
652
|
server.registerTool("flow_create", {
|
|
641
653
|
title: "Create flow",
|
|
@@ -677,12 +689,12 @@ export async function handleMcp(request, deps) {
|
|
|
677
689
|
openWorldHint: false,
|
|
678
690
|
},
|
|
679
691
|
}, async (input) => text(await deps.flows.archive(flowActor, input)));
|
|
680
|
-
server.registerTool("
|
|
681
|
-
title: "
|
|
692
|
+
server.registerTool("flow_version_create", {
|
|
693
|
+
title: "Create flow version",
|
|
682
694
|
description: "Append a validated immutable flow graph version with optimistic concurrency.",
|
|
683
695
|
inputSchema: SaveFlowVersionInput,
|
|
684
696
|
annotations: {
|
|
685
|
-
title: "
|
|
697
|
+
title: "Create flow version",
|
|
686
698
|
readOnlyHint: false,
|
|
687
699
|
destructiveHint: false,
|
|
688
700
|
idempotentHint: true,
|
|
@@ -746,7 +758,7 @@ export async function handleMcp(request, deps) {
|
|
|
746
758
|
openWorldHint: false,
|
|
747
759
|
},
|
|
748
760
|
}, async (input) => text(await deps.flows.start(flowActor, input)));
|
|
749
|
-
server.registerTool("
|
|
761
|
+
server.registerTool("flow_run_step_complete", {
|
|
750
762
|
title: "Complete flow step",
|
|
751
763
|
description: "Submit a node result or semantic branch to advance a durable run without persisting credentials.",
|
|
752
764
|
inputSchema: CompleteFlowRunStepInput,
|
|
@@ -774,7 +786,7 @@ export async function handleMcp(request, deps) {
|
|
|
774
786
|
}, async (input) => text(await deps.flows.cancelRun(flowActor, input)));
|
|
775
787
|
}
|
|
776
788
|
if (permits(deps.authorization, "tools", "read")) {
|
|
777
|
-
server.registerTool("
|
|
789
|
+
server.registerTool("tool_list", {
|
|
778
790
|
title: "List tools",
|
|
779
791
|
description: "List the MCP tools the calling user can reach through the portal, live rather than cached.",
|
|
780
792
|
inputSchema: EmptyInput,
|
|
@@ -787,9 +799,9 @@ export async function handleMcp(request, deps) {
|
|
|
787
799
|
},
|
|
788
800
|
}, async () => text(await deps.tools.catalog(toolActor)));
|
|
789
801
|
// The servers behind the same live list (D30), as the portal itself names them.
|
|
790
|
-
server.registerTool("
|
|
802
|
+
server.registerTool("tool_server_list", {
|
|
791
803
|
title: "List tool servers",
|
|
792
|
-
description: "List the MCP servers the calling user reaches through the portal, as the portal itself names them.
|
|
804
|
+
description: "List the MCP servers the calling user reaches through the portal, as the portal itself names them. The handle is the prefix on every tool name from that server, which is how tool_execute is routed.",
|
|
793
805
|
inputSchema: EmptyInput,
|
|
794
806
|
annotations: {
|
|
795
807
|
title: "List tool servers",
|
|
@@ -801,7 +813,7 @@ export async function handleMcp(request, deps) {
|
|
|
801
813
|
}, async () => text(await deps.tools.servers(toolActor)));
|
|
802
814
|
}
|
|
803
815
|
if (permits(deps.authorization, "tools", "test")) {
|
|
804
|
-
server.registerTool("
|
|
816
|
+
server.registerTool("tool_test", {
|
|
805
817
|
title: "Test tool",
|
|
806
818
|
description: "Validate input and execute one explicitly read-only cached MCP tool with the caller's Gate connection.",
|
|
807
819
|
inputSchema: TestToolInput,
|
|
@@ -815,7 +827,7 @@ export async function handleMcp(request, deps) {
|
|
|
815
827
|
}, async (input) => text(await deps.tools.test(toolActor, input)));
|
|
816
828
|
}
|
|
817
829
|
if (permits(deps.authorization, "tools", "execute")) {
|
|
818
|
-
server.registerTool("
|
|
830
|
+
server.registerTool("tool_execute", {
|
|
819
831
|
title: "Execute tool",
|
|
820
832
|
description: "Validate and execute a discovered MCP tool with the caller's just-in-time Gate connection.",
|
|
821
833
|
inputSchema: ExecuteToolInput,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BlockNoteDocument, BlockNoteMediaType, DocumentLinkInlineType, } from "@anchrd/intel-contract";
|
|
1
|
+
import { BlockNoteDocument, BlockNoteMediaType, DocumentLinkInlineType, } from "@anchrd/intel-contract/node";
|
|
2
2
|
function isRecord(value) {
|
|
3
3
|
return typeof value === "object" && value !== null;
|
|
4
4
|
}
|
package/dist/nodes/nodes.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TableMediaType
|
|
1
|
+
import { TableMediaType } from "@anchrd/intel-contract/table";
|
|
2
2
|
import { encodeCsv, parseCsv } from "../shared/csv/csv.js";
|
|
3
3
|
import { IntelError } from "../shared/intel-error/intel-error.js";
|
|
4
4
|
import { plainTitle } from "../shared/plain-title/plain-title.js";
|
|
@@ -318,8 +318,8 @@ export function createNodes(deps) {
|
|
|
318
318
|
* there, and what was added is.
|
|
319
319
|
*
|
|
320
320
|
* ⚠️ Only targets this actor may see become links. The author can only insert what the picker
|
|
321
|
-
* offers them, but `
|
|
322
|
-
* turn the graph into a place where the existence of an unreachable document can be confirmed by
|
|
321
|
+
* offers them, but `node_version_create` takes any content over MCP, and an unfiltered write
|
|
322
|
+
* would turn the graph into a place where the existence of an unreachable document can be confirmed by
|
|
323
323
|
* anyone who guesses its ID. The filter is `resolveVisibleTitles`, the same lookup the reader's
|
|
324
324
|
* side goes through — one rule, not two.
|
|
325
325
|
*
|
|
@@ -801,17 +801,17 @@ export function createNodes(deps) {
|
|
|
801
801
|
*
|
|
802
802
|
* ⚠️ Until #390 this also switched an agent's Gate Application off with the node, and the ORDER
|
|
803
803
|
* of the two writes was the whole safety argument. With the agents parked there is no second
|
|
804
|
-
* system to keep in step,
|
|
805
|
-
*
|
|
804
|
+
* system to keep in step, so #418 took the caller's bearer back out — a parameter kept "in
|
|
805
|
+
* case" is a parameter every surface has to find a value for, and both of them were passing a
|
|
806
|
+
* token nothing read.
|
|
806
807
|
*/
|
|
807
|
-
async archive(actor, input
|
|
808
|
+
async archive(actor, input) {
|
|
808
809
|
const current = await requireVisible(actor, input.nodeId);
|
|
809
810
|
if (!(await deps.repository.can(actor, current.id, "write"))) {
|
|
810
811
|
throw new IntelError(403, "node_forbidden", "This node cannot be edited");
|
|
811
812
|
}
|
|
812
813
|
const replayedId = await deps.repository.findIdempotentNode(actor.id, "node.archive", input.idempotencyKey);
|
|
813
|
-
// A replay
|
|
814
|
-
// about a request that is not happening.
|
|
814
|
+
// A replay does nothing a second time and answers with what the first run made.
|
|
815
815
|
if (replayedId)
|
|
816
816
|
return await requireVisible(actor, replayedId);
|
|
817
817
|
const updatedAt = deps.now().toISOString();
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Flow, FlowVersion } from "@anchrd/intel-contract/flow";
|
|
2
|
+
import type { ArchiveNodeInput, CreateNodeInput, ListNodesInput, Node, NodeAttachment, NodeCitation, NodeDocument, NodeGraph, NodeGraphInput, NodeKind, NodeLink, NodeTable, NodeVersion, ResolveNodeLinksInput, ResolveNodeLinksResult, SaveAttachmentInput, SaveNodeVersionInput, SearchInput, UpdateNodeInput } from "@anchrd/intel-contract/node";
|
|
3
|
+
import type { ResourceGrant, ResourceGrantList, ResourceVerb, RevokeGrantInput, ShareInput, ShareResult } from "@anchrd/intel-contract/share";
|
|
4
|
+
import type { AppendTableRowsInput, AppendTableRowsResult, DefineTableInput, DeleteTableRowsInput, DeleteTableRowsResult, RedefineTableInput, UpdateTableRowsInput, UpdateTableRowsResult } from "@anchrd/intel-contract/table";
|
|
2
5
|
import type { SemanticIndex } from "../adapters/semantic-index/semantic-index.types.js";
|
|
3
6
|
export interface Actor {
|
|
4
7
|
id: string;
|
|
@@ -176,9 +179,6 @@ export interface NodeAttachmentBody {
|
|
|
176
179
|
attachment: NodeAttachment;
|
|
177
180
|
body: ReadableStream<Uint8Array>;
|
|
178
181
|
}
|
|
179
|
-
export interface GateCaller {
|
|
180
|
-
token: string;
|
|
181
|
-
}
|
|
182
182
|
export interface NodesDeps {
|
|
183
183
|
repository: NodeRepository;
|
|
184
184
|
content: ContentStore;
|
|
@@ -258,7 +258,7 @@ export interface NodeService {
|
|
|
258
258
|
* by a write that half succeeded; the opposite order can only leave a live agent whose principal
|
|
259
259
|
* is off, which stops working loudly and is repaired by repeating the call.
|
|
260
260
|
*/
|
|
261
|
-
archive(actor: Actor, input: ArchiveNodeInput
|
|
261
|
+
archive(actor: Actor, input: ArchiveNodeInput): Promise<Node>;
|
|
262
262
|
listGrants(actor: Actor, resourceId: string): Promise<ResourceGrantList>;
|
|
263
263
|
listLinks(actor: Actor, nodeId: string): Promise<{
|
|
264
264
|
items: NodeLink[];
|
|
@@ -284,7 +284,7 @@ export interface NodeIndexTarget {
|
|
|
284
284
|
description: string | null;
|
|
285
285
|
contentKeys: string[];
|
|
286
286
|
mediaType: string;
|
|
287
|
-
kind:
|
|
287
|
+
kind: Exclude<NodeKind, "folder">;
|
|
288
288
|
updatedAt: string;
|
|
289
289
|
}
|
|
290
290
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ToolServer } from "@anchrd/intel-contract";
|
|
1
|
+
import { type ToolServer } from "@anchrd/intel-contract/tool";
|
|
2
2
|
/**
|
|
3
3
|
* The portal's own directory tool. It is an ordinary entry in `tools/list`, so it is reached the
|
|
4
4
|
* same way every other tool is: `tools/call` with the asking user's portal token.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExecuteToolInput, TestToolInput, ToolCapability, ToolCatalog, ToolServerCatalog, ToolTestResult } from "@anchrd/intel-contract";
|
|
1
|
+
import type { ExecuteToolInput, TestToolInput, ToolCapability, ToolCatalog, ToolServerCatalog, ToolTestResult } from "@anchrd/intel-contract/tool";
|
|
2
2
|
export interface ToolActor {
|
|
3
3
|
id: string;
|
|
4
4
|
email: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anchrd/intel-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
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.7.0",
|
|
46
|
-
"@anchrd/intel-contract": "^0.
|
|
46
|
+
"@anchrd/intel-contract": "^0.14.0",
|
|
47
47
|
"@cfworker/json-schema": "^4.1.1",
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
49
49
|
"fflate": "^0.8.3",
|