@anchrd/intel-api 0.6.7 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -3
- package/dist/adapters/cloudflare/cloudflare.js +50 -16
- package/dist/adapters/cloudflare/cloudflare.types.d.ts +11 -0
- package/dist/adapters/content/content.d.ts +1 -1
- package/dist/adapters/db/db-flows.js +148 -20
- package/dist/adapters/db/db-grants.d.ts +13 -2
- package/dist/adapters/db/db-grants.js +25 -8
- package/dist/adapters/db/db-indexing.d.ts +2 -2
- package/dist/adapters/db/db-indexing.js +26 -19
- package/dist/adapters/db/db.d.ts +3 -3
- package/dist/adapters/db/db.js +431 -118
- package/dist/adapters/gate-applications/gate-applications.d.ts +23 -0
- package/dist/adapters/gate-applications/gate-applications.js +66 -0
- package/dist/adapters/index-queue/index-queue.d.ts +1 -1
- package/dist/adapters/index-queue/index-queue.js +2 -2
- package/dist/adapters/semantic-index/semantic-index.types.d.ts +2 -2
- package/dist/agent-runtime/agent-runtime.d.ts +16 -0
- package/dist/agent-runtime/agent-runtime.js +76 -0
- package/dist/agent-runtime/agent-runtime.types.d.ts +57 -0
- package/dist/bundle/bundle.d.ts +4 -0
- package/dist/bundle/bundle.js +1035 -0
- package/dist/bundle/bundle.types.d.ts +33 -0
- package/dist/bundle/bundle.types.js +1 -0
- package/dist/cli/cli.js +10 -1
- package/dist/flows/flows.d.ts +8 -8
- package/dist/flows/flows.js +158 -42
- package/dist/flows/flows.types.d.ts +40 -7
- package/dist/http/http.d.ts +1 -0
- package/dist/http/http.js +324 -61
- package/dist/http/http.types.d.ts +6 -2
- package/dist/indexing/indexing.js +14 -2
- package/dist/indexing/indexing.types.d.ts +2 -2
- package/dist/intel/intel.js +12 -3
- package/dist/intel/intel.types.d.ts +6 -2
- package/dist/mcp/mcp.js +483 -124
- package/dist/mcp/mcp.types.d.ts +11 -2
- package/dist/nodes/nodes.d.ts +2 -0
- package/dist/nodes/nodes.js +1337 -0
- package/dist/nodes/nodes.types.d.ts +314 -0
- package/dist/nodes/nodes.types.js +1 -0
- package/migrations/0011_one_name_for_the_tree.sql +53 -0
- package/migrations/0012_table_snapshots.sql +29 -0
- package/migrations/0013_agents_in_the_tree.sql +76 -0
- package/migrations/0014_agent_applications.sql +25 -0
- package/package.json +3 -2
- package/dist/knowledge/knowledge.d.ts +0 -2
- package/dist/knowledge/knowledge.js +0 -761
- package/dist/knowledge/knowledge.types.d.ts +0 -198
- /package/dist/{knowledge/knowledge.types.js → agent-runtime/agent-runtime.types.js} +0 -0
- /package/dist/{knowledge → nodes}/document-links/document-links.d.ts +0 -0
- /package/dist/{knowledge → nodes}/document-links/document-links.js +0 -0
package/dist/http/http.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AppendTableRowsInput, ArchiveFlowInput, ArchiveNodeInput, CancelFlowRunInput, CompleteFlowRunStepInput, CreateAgentInput, CreateFlowInput, CreateNodeInput, DefineTableInput, DeleteTableRowsInput, ExecuteToolInput, GetFlowInput, GetFlowRunInput, GetFlowVersionInput, GetNodeInput, GetNodeVersionInput, ListAgentsInput, ListFlowRunsInput, ListFlowsInput, ListNodesInput, NodeGraphInput, PreviewFlowPublishInput, PublishFlowInput, RedefineTableInput, RelationGraphInput, ResolveNodeLinksInput, RevokeGrantInput, SaveAgentDefinitionInput, SaveAttachmentInput, SaveFlowVersionInput, SaveNodeVersionInput, SearchInput, ShareInput, StartFlowRunInput, TestToolInput, UnpublishFlowInput, UpdateFlowInput, UpdateNodeInput, UpdateTableRowsInput, } from "@anchrd/intel-contract";
|
|
2
2
|
import { Hono } from "hono";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { authorizeBearer, bearer, permits, } from "../shared/gate-authorization/gate-authorization.js";
|
|
@@ -30,6 +30,32 @@ function asActor(authorization) {
|
|
|
30
30
|
isAdmin: authorization.can("intel", "admin"),
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
+
// The exporting caller: the node actor plus whether flows may ride along in the bundle at all.
|
|
34
|
+
// Gate's `flows/read` is asked here once, so the bundle service never re-derives a capability.
|
|
35
|
+
function asBundleActor(authorization) {
|
|
36
|
+
return {
|
|
37
|
+
id: authorization.identity.id,
|
|
38
|
+
email: authorization.identity.email,
|
|
39
|
+
isAdmin: authorization.can("intel", "admin"),
|
|
40
|
+
canReadFlows: permits(authorization, "flows", "read"),
|
|
41
|
+
canCreateFlows: permits(authorization, "flows", "create"),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// The idempotency key of an import travels as a header: the body is the zip itself, so there is no
|
|
45
|
+
// JSON envelope to put it in, and a query string would drag it into logs (#137).
|
|
46
|
+
const ImportIdempotencyKey = z.string().min(8).max(200);
|
|
47
|
+
// A bundle export streamed as a download. `content-length` is deliberately absent: the zip is
|
|
48
|
+
// written while it leaves, which is the whole point of streaming it (#136).
|
|
49
|
+
function zipResponse(bundle) {
|
|
50
|
+
return new Response(bundle.stream, {
|
|
51
|
+
headers: {
|
|
52
|
+
"cache-control": "private, no-store",
|
|
53
|
+
"content-disposition": contentDisposition(bundle.filename),
|
|
54
|
+
"content-type": "application/zip",
|
|
55
|
+
"x-content-type-options": "nosniff",
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
33
59
|
function asToolActor(authorization) {
|
|
34
60
|
return {
|
|
35
61
|
id: authorization.identity.id,
|
|
@@ -52,11 +78,24 @@ function asFlowActor(authorization) {
|
|
|
52
78
|
isAdmin: authorization.can("intel", "admin"),
|
|
53
79
|
};
|
|
54
80
|
}
|
|
81
|
+
// The same `agents/run` the agent Worker asks for, resolved once at Intel's door. `intel/admin`
|
|
82
|
+
// opens it for the same reason it opens every other one — `permits` is where that lives.
|
|
83
|
+
function asAgentActor(authorization) {
|
|
84
|
+
return {
|
|
85
|
+
id: authorization.identity.id,
|
|
86
|
+
email: authorization.identity.email,
|
|
87
|
+
isAdmin: authorization.can("intel", "admin"),
|
|
88
|
+
canRun: permits(authorization, "agents", "run"),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
55
91
|
export function createHttp(deps) {
|
|
56
92
|
const app = new Hono();
|
|
57
93
|
async function authenticated(headers) {
|
|
58
94
|
const token = bearer(headers) ?? (await deps.auth?.resolve(headers))?.bearer ?? null;
|
|
59
|
-
|
|
95
|
+
if (!token)
|
|
96
|
+
return null;
|
|
97
|
+
const authorization = await authorizeBearer(deps.gate, token, deps.resource);
|
|
98
|
+
return authorization ? { authorization, token } : null;
|
|
60
99
|
}
|
|
61
100
|
app.onError((error, context) => {
|
|
62
101
|
if (error instanceof IntelError) {
|
|
@@ -71,12 +110,13 @@ export function createHttp(deps) {
|
|
|
71
110
|
return context.json(problem(500, "internal_error", "Internal server error"), 500);
|
|
72
111
|
});
|
|
73
112
|
app.use("*", async (context, next) => {
|
|
74
|
-
const
|
|
75
|
-
if (!
|
|
113
|
+
const session = await authenticated(context.req.raw.headers);
|
|
114
|
+
if (!session) {
|
|
76
115
|
context.header("WWW-Authenticate", `Bearer resource_metadata="${deps.resourceMetadataUrl}"`);
|
|
77
116
|
return context.json(problem(401, "authentication_required", "Authentication required"), 401);
|
|
78
117
|
}
|
|
79
|
-
context.set("authorization", authorization);
|
|
118
|
+
context.set("authorization", session.authorization);
|
|
119
|
+
context.set("token", session.token);
|
|
80
120
|
await next();
|
|
81
121
|
});
|
|
82
122
|
function requireCapability(context, handle, fn) {
|
|
@@ -96,7 +136,58 @@ export function createHttp(deps) {
|
|
|
96
136
|
name: identity.name ?? null,
|
|
97
137
|
});
|
|
98
138
|
});
|
|
99
|
-
|
|
139
|
+
// What this deployment is equipped to do (#190) — facts about the installation, not about the
|
|
140
|
+
// caller, which is why it needs no capability beyond being signed in. `/session` stays identity
|
|
141
|
+
// only on purpose; this is the separate answer the UI asks for instead of reading a 503 as one.
|
|
142
|
+
// It sits behind the auth middleware like everything else, so an anonymous probe still learns
|
|
143
|
+
// nothing about whether a runtime exists.
|
|
144
|
+
app.get("/capabilities", (context) => {
|
|
145
|
+
return context.json({ agentRuntime: deps.agents.available() });
|
|
146
|
+
});
|
|
147
|
+
/**
|
|
148
|
+
* The agent runtime, reached through Intel.
|
|
149
|
+
*
|
|
150
|
+
* ⚠️ This list IS the door. The runtime serves more than what stands here — `/mcp` is the agent's
|
|
151
|
+
* own MCP surface, which the portal dials with its own credential (ADR-0005 §7), and
|
|
152
|
+
* `/mail/inbound` is authorized by a webhook signature. Neither is a browser path, and neither
|
|
153
|
+
* becomes one by being reachable: anything not named below is a 404 from Hono's router, so there
|
|
154
|
+
* is no allow-list beside the routes that could go quietly out of step with them.
|
|
155
|
+
*
|
|
156
|
+
* ⚠️ `agents/run` — not `knowledge/read`. Reading an agent's page is a knowledge read; driving the
|
|
157
|
+
* agent is the capability the runtime itself insists on, and Intel refuses on the same terms
|
|
158
|
+
* BEFORE the binding is touched, so a denial never reaches the runtime at all.
|
|
159
|
+
*/
|
|
160
|
+
const runtimeRoutes = [
|
|
161
|
+
{ method: "GET", pattern: "/state", path: () => "/state" },
|
|
162
|
+
{ method: "GET", pattern: "/runs", path: () => "/runs" },
|
|
163
|
+
{
|
|
164
|
+
method: "GET",
|
|
165
|
+
pattern: "/runs/:runId",
|
|
166
|
+
path: (context) => `/runs/${encodeURIComponent(context.req.param("runId") ?? "")}`,
|
|
167
|
+
},
|
|
168
|
+
{ method: "GET", pattern: "/log", path: () => "/log" },
|
|
169
|
+
{ method: "POST", pattern: "/chat", path: () => "/chat" },
|
|
170
|
+
{ method: "POST", pattern: "/pause", path: () => "/pause" },
|
|
171
|
+
{ method: "POST", pattern: "/resume", path: () => "/resume" },
|
|
172
|
+
{ method: "POST", pattern: "/run", path: () => "/run" },
|
|
173
|
+
];
|
|
174
|
+
for (const route of runtimeRoutes) {
|
|
175
|
+
app.on(route.method, `/agents/:agentId${route.pattern}`, async (context) => {
|
|
176
|
+
const authorization = context.get("authorization");
|
|
177
|
+
// The runtime's answer is returned untouched, headers included: the chat is a stream, and a
|
|
178
|
+
// body read here to inspect it would be a stream nobody could read afterwards.
|
|
179
|
+
return await deps.agents.forward({
|
|
180
|
+
actor: asAgentActor(authorization),
|
|
181
|
+
token: context.get("token"),
|
|
182
|
+
agentId: context.req.param("agentId"),
|
|
183
|
+
method: route.method,
|
|
184
|
+
path: route.path(context),
|
|
185
|
+
...(route.method === "POST" ? { body: await context.req.text() } : {}),
|
|
186
|
+
signal: context.req.raw.signal,
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
app.get("/nodes", async (context) => {
|
|
100
191
|
const auth = requireCapability(context, "knowledge", "read");
|
|
101
192
|
const url = new URL(context.req.url);
|
|
102
193
|
const unknownQuery = [];
|
|
@@ -108,32 +199,132 @@ export function createHttp(deps) {
|
|
|
108
199
|
if (unknownQuery.length) {
|
|
109
200
|
throw new IntelError(400, "invalid_request", `Unknown query parameters: ${unknownQuery.join(", ")}`);
|
|
110
201
|
}
|
|
111
|
-
const input =
|
|
202
|
+
const input = ListNodesInput.parse({
|
|
112
203
|
parentId: url.searchParams.get("parentId"),
|
|
113
204
|
includeArchived: url.searchParams.get("includeArchived") === "true",
|
|
114
205
|
...(url.searchParams.get("archivedOnly") === "true" ? { archivedOnly: true } : {}),
|
|
115
206
|
});
|
|
116
|
-
return context.json(await deps.
|
|
207
|
+
return context.json(await deps.nodes.list(asActor(auth), input));
|
|
117
208
|
});
|
|
118
|
-
app.get("/
|
|
209
|
+
app.get("/nodes/graph", async (context) => {
|
|
119
210
|
const auth = requireCapability(context, "knowledge", "read");
|
|
120
211
|
const url = new URL(context.req.url);
|
|
121
|
-
const input =
|
|
212
|
+
const input = NodeGraphInput.parse({
|
|
122
213
|
limit: url.searchParams.has("limit") ? Number(url.searchParams.get("limit")) : undefined,
|
|
123
214
|
});
|
|
124
|
-
return context.json(await deps.
|
|
215
|
+
return context.json(await deps.nodes.graph(asActor(auth), input));
|
|
216
|
+
});
|
|
217
|
+
// ⚠️ Before `/nodes/:nodeId`, or the router would read "agents" as a node ID. Same order and same
|
|
218
|
+
// reason as `/nodes/graph` above.
|
|
219
|
+
app.get("/nodes/agents", async (context) => {
|
|
220
|
+
const auth = requireCapability(context, "knowledge", "read");
|
|
221
|
+
const url = new URL(context.req.url);
|
|
222
|
+
requireKnownQuery(url, ["parentId", "includeArchived"]);
|
|
223
|
+
const parentId = url.searchParams.get("parentId");
|
|
224
|
+
// ⚠️ The key is only set when the caller sent it. Omitted means the whole tree and `null` means
|
|
225
|
+
// the root level, so defaulting a missing parameter to `null` here would silently answer a
|
|
226
|
+
// different question than the one asked.
|
|
227
|
+
const input = ListAgentsInput.parse({
|
|
228
|
+
...(url.searchParams.has("parentId") ? { parentId: parentId === "" ? null : parentId } : {}),
|
|
229
|
+
includeArchived: url.searchParams.get("includeArchived") === "true",
|
|
230
|
+
});
|
|
231
|
+
return context.json(await deps.nodes.listAgents(asActor(auth), input));
|
|
232
|
+
});
|
|
233
|
+
/**
|
|
234
|
+
* ⚠️ The one response in Intel's HTTP surface that carries a credential (#182, D27).
|
|
235
|
+
*
|
|
236
|
+
* `applicationKey` is the Gate Application key of the new agent, in plain text, exactly once —
|
|
237
|
+
* Gate issues it once and keeps only a hash, and Intel writes it nowhere. It belongs in the agent
|
|
238
|
+
* runtime's `AGENT_APPLICATION_KEYS` secret; the answer says so rather than assuming the caller
|
|
239
|
+
* knows. The caller's own bearer is what creates the principal, so this needs `applications:write`
|
|
240
|
+
* in Gate on top of `knowledge:create` in Intel.
|
|
241
|
+
*/
|
|
242
|
+
app.post("/nodes/agents", async (context) => {
|
|
243
|
+
const auth = requireCapability(context, "knowledge", "create");
|
|
244
|
+
const input = CreateAgentInput.parse(await context.req.json().catch(() => null));
|
|
245
|
+
return context.json(await deps.nodes.createAgent(asActor(auth), input, { token: context.get("token") }), 201);
|
|
246
|
+
});
|
|
247
|
+
// ⚠️ Before `/nodes/:nodeId`, or the router would read "export" as a node ID. The root has no
|
|
248
|
+
// node to address, so the whole installation — as this caller may read it — exports here (#136).
|
|
249
|
+
app.get("/nodes/export", async (context) => {
|
|
250
|
+
const auth = requireCapability(context, "knowledge", "read");
|
|
251
|
+
return zipResponse(await deps.bundle.exportSubtree(asBundleActor(auth), null));
|
|
252
|
+
});
|
|
253
|
+
// The reverse door (#137): a zip bundle — or a naked zipped folder — lands as a new subtree.
|
|
254
|
+
// The body IS the zip, streamed to the service rather than read whole here; `knowledge/create`
|
|
255
|
+
// like every other way of making nodes, and the resource-level write check is the service's.
|
|
256
|
+
async function importBundle(context, targetNodeId) {
|
|
257
|
+
const auth = requireCapability(context, "knowledge", "create");
|
|
258
|
+
const idempotencyKey = ImportIdempotencyKey.parse(context.req.header("idempotency-key"));
|
|
259
|
+
const body = context.req.raw.body;
|
|
260
|
+
if (body === null) {
|
|
261
|
+
throw new IntelError(400, "invalid_request", "A zip request body is required");
|
|
262
|
+
}
|
|
263
|
+
const result = await deps.bundle.importBundle(asBundleActor(auth), {
|
|
264
|
+
targetNodeId,
|
|
265
|
+
zip: body,
|
|
266
|
+
idempotencyKey,
|
|
267
|
+
});
|
|
268
|
+
// A replay answers 200: nothing was created by THIS request, and the summary is the earlier
|
|
269
|
+
// import's. 201 stays the word for rows that exist because of this call.
|
|
270
|
+
return context.json(result, result.replayed ? 200 : 201);
|
|
271
|
+
}
|
|
272
|
+
// ⚠️ Before `/nodes/:nodeId`, or the router would read "import" as a node ID.
|
|
273
|
+
app.post("/nodes/import", async (context) => await importBundle(context, null));
|
|
274
|
+
app.get("/nodes/:nodeId", async (context) => {
|
|
275
|
+
const auth = requireCapability(context, "knowledge", "read");
|
|
276
|
+
return context.json(await deps.nodes.get(asActor(auth), context.req.param("nodeId")));
|
|
277
|
+
});
|
|
278
|
+
app.post("/nodes/:nodeId/import", async (context) => {
|
|
279
|
+
const input = GetNodeInput.parse({ nodeId: context.req.param("nodeId") });
|
|
280
|
+
return await importBundle(context, input.nodeId);
|
|
281
|
+
});
|
|
282
|
+
// One subtree — a folder with everything beneath it, or a single node — as a zip bundle (#136).
|
|
283
|
+
app.get("/nodes/:nodeId/export", async (context) => {
|
|
284
|
+
const auth = requireCapability(context, "knowledge", "read");
|
|
285
|
+
const input = GetNodeInput.parse({ nodeId: context.req.param("nodeId") });
|
|
286
|
+
return zipResponse(await deps.bundle.exportSubtree(asBundleActor(auth), input.nodeId));
|
|
287
|
+
});
|
|
288
|
+
app.get("/nodes/:nodeId/agent", async (context) => {
|
|
289
|
+
const auth = requireCapability(context, "knowledge", "read");
|
|
290
|
+
return context.json(await deps.nodes.getAgent(asActor(auth), { nodeId: context.req.param("nodeId") }));
|
|
291
|
+
});
|
|
292
|
+
app.post("/nodes/:nodeId/agent", async (context) => {
|
|
293
|
+
const auth = requireCapability(context, "knowledge", "write");
|
|
294
|
+
const input = SaveAgentDefinitionInput.parse(await context.req.json().catch(() => null));
|
|
295
|
+
if (input.nodeId !== context.req.param("nodeId")) {
|
|
296
|
+
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
297
|
+
}
|
|
298
|
+
return context.json(await deps.nodes.saveAgentDefinition(asActor(auth), input), 201);
|
|
299
|
+
});
|
|
300
|
+
app.get("/nodes/:nodeId/versions", async (context) => {
|
|
301
|
+
const auth = requireCapability(context, "knowledge", "read");
|
|
302
|
+
return context.json(await deps.nodes.listVersions(asActor(auth), context.req.param("nodeId")));
|
|
125
303
|
});
|
|
126
|
-
|
|
304
|
+
// The content behind one row of the list above (#147): what a version-pinned citation points at.
|
|
305
|
+
// Read capability like every other node read; the resource ACL and the node/version pairing are
|
|
306
|
+
// the service's to refuse.
|
|
307
|
+
app.get("/nodes/:nodeId/versions/:versionId/content", async (context) => {
|
|
127
308
|
const auth = requireCapability(context, "knowledge", "read");
|
|
128
|
-
|
|
309
|
+
const input = GetNodeVersionInput.parse({
|
|
310
|
+
nodeId: context.req.param("nodeId"),
|
|
311
|
+
versionId: context.req.param("versionId"),
|
|
312
|
+
});
|
|
313
|
+
return context.json(await deps.nodes.getVersion(asActor(auth), input.nodeId, input.versionId));
|
|
129
314
|
});
|
|
130
|
-
|
|
315
|
+
// The metadata half of an attachment (#148): what `node_attachment_get` answers an agent, for
|
|
316
|
+
// HTTP callers who so far could only stream the bytes. Without the `resourceUri` — an MCP
|
|
317
|
+
// resource address says nothing to an HTTP client, whose address for the bytes is the sibling
|
|
318
|
+
// route below.
|
|
319
|
+
app.get("/nodes/:nodeId/attachment/meta", async (context) => {
|
|
131
320
|
const auth = requireCapability(context, "knowledge", "read");
|
|
132
|
-
|
|
321
|
+
const input = GetNodeInput.parse({ nodeId: context.req.param("nodeId") });
|
|
322
|
+
const { node, version } = await deps.nodes.getAttachment(asActor(auth), input.nodeId);
|
|
323
|
+
return context.json({ node, version });
|
|
133
324
|
});
|
|
134
|
-
app.get("/
|
|
325
|
+
app.get("/nodes/:nodeId/attachment", async (context) => {
|
|
135
326
|
const auth = requireCapability(context, "knowledge", "read");
|
|
136
|
-
const { attachment, body } = await deps.
|
|
327
|
+
const { attachment, body } = await deps.nodes.readAttachment(asActor(auth), context.req.param("nodeId"));
|
|
137
328
|
return new Response(body, {
|
|
138
329
|
headers: {
|
|
139
330
|
"cache-control": "private, no-store",
|
|
@@ -143,111 +334,141 @@ export function createHttp(deps) {
|
|
|
143
334
|
},
|
|
144
335
|
});
|
|
145
336
|
});
|
|
146
|
-
app.get("/
|
|
337
|
+
app.get("/nodes/:nodeId/table", async (context) => {
|
|
147
338
|
const auth = requireCapability(context, "knowledge", "read");
|
|
148
|
-
return context.json(await deps.
|
|
339
|
+
return context.json(await deps.nodes.getTable(asActor(auth), context.req.param("nodeId")));
|
|
149
340
|
});
|
|
150
341
|
// Defining the header and appending rows are two routes because they are two decisions: the
|
|
151
342
|
// header is written once and is the contract, an append is the everyday write (#40).
|
|
152
|
-
app.post("/
|
|
343
|
+
app.post("/nodes/:nodeId/table", async (context) => {
|
|
153
344
|
const auth = requireCapability(context, "knowledge", "write");
|
|
154
|
-
const input =
|
|
345
|
+
const input = DefineTableInput.parse(await context.req.json().catch(() => null));
|
|
155
346
|
if (input.nodeId !== context.req.param("nodeId")) {
|
|
156
347
|
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
157
348
|
}
|
|
158
|
-
return context.json(await deps.
|
|
349
|
+
return context.json(await deps.nodes.defineTable(asActor(auth), input), 201);
|
|
159
350
|
});
|
|
160
|
-
app.post("/
|
|
351
|
+
app.post("/nodes/:nodeId/table/rows", async (context) => {
|
|
161
352
|
const auth = requireCapability(context, "knowledge", "write");
|
|
162
|
-
const input =
|
|
353
|
+
const input = AppendTableRowsInput.parse(await context.req.json().catch(() => null));
|
|
163
354
|
if (input.nodeId !== context.req.param("nodeId")) {
|
|
164
355
|
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
165
356
|
}
|
|
166
|
-
return context.json(await deps.
|
|
357
|
+
return context.json(await deps.nodes.appendTableRows(asActor(auth), input), 201);
|
|
167
358
|
});
|
|
168
|
-
|
|
359
|
+
// The three mutations beside the append (#135). Action-style POSTs like the archive and revoke
|
|
360
|
+
// routes, because a row has no address of its own to PUT to: positions live in the body, next to
|
|
361
|
+
// the `baseVersionId` that says which state they mean. Each writes one snapshot version, so the
|
|
362
|
+
// 201 is the same statement the append's 201 makes.
|
|
363
|
+
app.post("/nodes/:nodeId/table/rows/update", async (context) => {
|
|
364
|
+
const auth = requireCapability(context, "knowledge", "write");
|
|
365
|
+
const input = UpdateTableRowsInput.parse(await context.req.json().catch(() => null));
|
|
366
|
+
if (input.nodeId !== context.req.param("nodeId")) {
|
|
367
|
+
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
368
|
+
}
|
|
369
|
+
return context.json(await deps.nodes.updateTableRows(asActor(auth), input), 201);
|
|
370
|
+
});
|
|
371
|
+
app.post("/nodes/:nodeId/table/rows/delete", async (context) => {
|
|
372
|
+
const auth = requireCapability(context, "knowledge", "write");
|
|
373
|
+
const input = DeleteTableRowsInput.parse(await context.req.json().catch(() => null));
|
|
374
|
+
if (input.nodeId !== context.req.param("nodeId")) {
|
|
375
|
+
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
376
|
+
}
|
|
377
|
+
return context.json(await deps.nodes.deleteTableRows(asActor(auth), input), 201);
|
|
378
|
+
});
|
|
379
|
+
app.post("/nodes/:nodeId/table/redefine", async (context) => {
|
|
380
|
+
const auth = requireCapability(context, "knowledge", "write");
|
|
381
|
+
const input = RedefineTableInput.parse(await context.req.json().catch(() => null));
|
|
382
|
+
if (input.nodeId !== context.req.param("nodeId")) {
|
|
383
|
+
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
384
|
+
}
|
|
385
|
+
return context.json(await deps.nodes.redefineTable(asActor(auth), input), 201);
|
|
386
|
+
});
|
|
387
|
+
app.get("/nodes/:nodeId/links", async (context) => {
|
|
169
388
|
const auth = requireCapability(context, "knowledge", "read");
|
|
170
|
-
return context.json(await deps.
|
|
389
|
+
return context.json(await deps.nodes.listLinks(asActor(auth), context.req.param("nodeId")));
|
|
171
390
|
});
|
|
172
391
|
// A document link's name, for whoever is reading the text (#41). A POST because the list of IDs
|
|
173
392
|
// is the request body — putting them in a query string would put the tree's identifiers into
|
|
174
|
-
// logs and referrers. `knowledge/read` and nothing more: it answers with what this reader may
|
|
393
|
+
// logs and referrers. The `knowledge/read` capability and nothing more: it answers with what this reader may
|
|
175
394
|
// already see, and stays silent about the rest rather than saying that there is a rest.
|
|
176
395
|
//
|
|
177
396
|
// There is no route to create or delete a link any more. A relationship is written where it is
|
|
178
|
-
// meant, in the text, and `POST /
|
|
179
|
-
app.post("/
|
|
397
|
+
// meant, in the text, and `POST /nodes/:nodeId/versions` is what records it.
|
|
398
|
+
app.post("/nodes/links/resolve", async (context) => {
|
|
180
399
|
const auth = requireCapability(context, "knowledge", "read");
|
|
181
|
-
const input =
|
|
182
|
-
return context.json(await deps.
|
|
400
|
+
const input = ResolveNodeLinksInput.parse(await context.req.json().catch(() => null));
|
|
401
|
+
return context.json(await deps.nodes.resolveLinks(asActor(auth), input));
|
|
183
402
|
});
|
|
184
|
-
app.post("/
|
|
403
|
+
app.post("/nodes/search", async (context) => {
|
|
185
404
|
const auth = requireCapability(context, "knowledge", "read");
|
|
186
|
-
const input =
|
|
187
|
-
return context.json(await deps.
|
|
405
|
+
const input = SearchInput.parse(await context.req.json().catch(() => null));
|
|
406
|
+
return context.json(await deps.nodes.search(asActor(auth), input));
|
|
188
407
|
});
|
|
189
408
|
app.post("/search/reindex", async (context) => {
|
|
190
409
|
const auth = requireCapability(context, "intel", "admin");
|
|
191
410
|
z.strictObject({}).parse(await context.req.json().catch(() => null));
|
|
192
|
-
return context.json(await deps.
|
|
411
|
+
return context.json(await deps.nodes.reindex(asActor(auth)), 202);
|
|
193
412
|
});
|
|
194
|
-
app.post("/
|
|
413
|
+
app.post("/nodes", async (context) => {
|
|
195
414
|
const auth = requireCapability(context, "knowledge", "create");
|
|
196
|
-
const input =
|
|
197
|
-
return context.json(await deps.
|
|
415
|
+
const input = CreateNodeInput.parse(await context.req.json().catch(() => null));
|
|
416
|
+
return context.json(await deps.nodes.create(asActor(auth), input), 201);
|
|
198
417
|
});
|
|
199
|
-
app.post("/
|
|
418
|
+
app.post("/nodes/:nodeId/versions", async (context) => {
|
|
200
419
|
const auth = requireCapability(context, "knowledge", "write");
|
|
201
|
-
const input =
|
|
420
|
+
const input = SaveNodeVersionInput.parse(await context.req.json().catch(() => null));
|
|
202
421
|
if (input.nodeId !== context.req.param("nodeId")) {
|
|
203
422
|
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
204
423
|
}
|
|
205
|
-
return context.json(await deps.
|
|
424
|
+
return context.json(await deps.nodes.save(asActor(auth), input), 201);
|
|
206
425
|
});
|
|
207
|
-
app.post("/
|
|
426
|
+
app.post("/nodes/:nodeId/attachment", async (context) => {
|
|
208
427
|
const auth = requireCapability(context, "knowledge", "write");
|
|
209
|
-
const input =
|
|
428
|
+
const input = SaveAttachmentInput.parse(await context.req.json().catch(() => null));
|
|
210
429
|
if (input.nodeId !== context.req.param("nodeId")) {
|
|
211
430
|
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
212
431
|
}
|
|
213
|
-
return context.json(await deps.
|
|
432
|
+
return context.json(await deps.nodes.saveAttachment(asActor(auth), input), 201);
|
|
214
433
|
});
|
|
215
|
-
app.patch("/
|
|
434
|
+
app.patch("/nodes/:nodeId", async (context) => {
|
|
216
435
|
const auth = requireCapability(context, "knowledge", "write");
|
|
217
|
-
const input =
|
|
436
|
+
const input = UpdateNodeInput.parse(await context.req.json().catch(() => null));
|
|
218
437
|
if (input.nodeId !== context.req.param("nodeId")) {
|
|
219
438
|
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
220
439
|
}
|
|
221
|
-
return context.json(await deps.
|
|
440
|
+
return context.json(await deps.nodes.update(asActor(auth), input));
|
|
222
441
|
});
|
|
223
|
-
app.post("/
|
|
442
|
+
app.post("/nodes/:nodeId/archive", async (context) => {
|
|
224
443
|
const auth = requireCapability(context, "knowledge", "write");
|
|
225
|
-
const input =
|
|
444
|
+
const input = ArchiveNodeInput.parse(await context.req.json().catch(() => null));
|
|
226
445
|
if (input.nodeId !== context.req.param("nodeId")) {
|
|
227
446
|
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
228
447
|
}
|
|
229
|
-
|
|
448
|
+
// The caller's own bearer, for the one thing archiving may have to do in Gate: switch off the
|
|
449
|
+
// Application behind an agent (#182). Every other kind never reaches it.
|
|
450
|
+
return context.json(await deps.nodes.archive(asActor(auth), input, { token: context.get("token") }));
|
|
230
451
|
});
|
|
231
|
-
app.get("/
|
|
452
|
+
app.get("/nodes/:nodeId/grants", async (context) => {
|
|
232
453
|
const auth = requireCapability(context, "knowledge", "share");
|
|
233
|
-
return context.json(await deps.
|
|
454
|
+
return context.json(await deps.nodes.listGrants(asActor(auth), context.req.param("nodeId")));
|
|
234
455
|
});
|
|
235
|
-
app.post("/
|
|
456
|
+
app.post("/nodes/:nodeId/grants", async (context) => {
|
|
236
457
|
const auth = requireCapability(context, "knowledge", "share");
|
|
237
|
-
const input =
|
|
458
|
+
const input = ShareInput.parse(await context.req.json().catch(() => null));
|
|
238
459
|
if (input.resourceId !== context.req.param("nodeId")) {
|
|
239
460
|
throw new IntelError(400, "node_id_mismatch", "Path and body resource IDs differ");
|
|
240
461
|
}
|
|
241
|
-
return context.json(await deps.
|
|
462
|
+
return context.json(await deps.nodes.share(asActor(auth), input), 201);
|
|
242
463
|
});
|
|
243
|
-
app.post("/
|
|
464
|
+
app.post("/nodes/:nodeId/grants/:grantId/revoke", async (context) => {
|
|
244
465
|
const auth = requireCapability(context, "knowledge", "share");
|
|
245
|
-
const input =
|
|
466
|
+
const input = RevokeGrantInput.parse(await context.req.json().catch(() => null));
|
|
246
467
|
if (input.resourceId !== context.req.param("nodeId") ||
|
|
247
468
|
input.grantId !== context.req.param("grantId")) {
|
|
248
469
|
throw new IntelError(400, "grant_id_mismatch", "Path and body grant IDs differ");
|
|
249
470
|
}
|
|
250
|
-
return context.json(await deps.
|
|
471
|
+
return context.json(await deps.nodes.revokeGrant(asActor(auth), input));
|
|
251
472
|
});
|
|
252
473
|
app.get("/flows", async (context) => {
|
|
253
474
|
const auth = requireCapability(context, "flows", "read");
|
|
@@ -266,7 +487,7 @@ export function createHttp(deps) {
|
|
|
266
487
|
return context.json(await deps.flows.list(asFlowActor(auth), input));
|
|
267
488
|
});
|
|
268
489
|
// ⚠️ Before `/flows/:flowId`, or the router would read "graph" as a flow ID. Same order and same
|
|
269
|
-
// reason as `/
|
|
490
|
+
// reason as `/nodes/graph`.
|
|
270
491
|
app.get("/flows/graph", async (context) => {
|
|
271
492
|
const auth = requireCapability(context, "flows", "read");
|
|
272
493
|
const url = new URL(context.req.url);
|
|
@@ -287,6 +508,13 @@ export function createHttp(deps) {
|
|
|
287
508
|
const input = GetFlowInput.parse({ flowId: context.req.param("flowId") });
|
|
288
509
|
return context.json(await deps.flows.get(asFlowActor(auth), input.flowId));
|
|
289
510
|
});
|
|
511
|
+
// A single flow as the same bundle shape a node export makes: one entry plus the manifest
|
|
512
|
+
// (#136). `flows/read` like every other read of the graph.
|
|
513
|
+
app.get("/flows/:flowId/export", async (context) => {
|
|
514
|
+
const auth = requireCapability(context, "flows", "read");
|
|
515
|
+
const input = GetFlowInput.parse({ flowId: context.req.param("flowId") });
|
|
516
|
+
return zipResponse(await deps.bundle.exportFlow(asBundleActor(auth), input.flowId));
|
|
517
|
+
});
|
|
290
518
|
// What this flow calls, read out of its graph. The tree lists where a flow is filed; this lists
|
|
291
519
|
// what it runs, and a shared subflow answers both questions at once (ADR-0004 §3).
|
|
292
520
|
app.get("/flows/:flowId/calls", async (context) => {
|
|
@@ -311,7 +539,7 @@ export function createHttp(deps) {
|
|
|
311
539
|
return context.json(await deps.flows.listRequirements(asFlowActor(auth), input.flowId));
|
|
312
540
|
});
|
|
313
541
|
// A flow has no grant route of its own. It is shared through the folder it is filed in, under
|
|
314
|
-
// /
|
|
542
|
+
// /nodes/:nodeId/grants — one place answers the question for the documents and the flows in
|
|
315
543
|
// that folder alike (ADR-0004 §2).
|
|
316
544
|
app.post("/flows", async (context) => {
|
|
317
545
|
const auth = requireCapability(context, "flows", "create");
|
|
@@ -344,6 +572,22 @@ export function createHttp(deps) {
|
|
|
344
572
|
}
|
|
345
573
|
return context.json(await deps.flows.save(asFlowActor(auth), input), 201);
|
|
346
574
|
});
|
|
575
|
+
// The GET beside `POST /flows/:flowId/versions`: the same collection, read instead of appended
|
|
576
|
+
// to (#144). `flows/read` like `GET /flows/:flowId` — the history describes a flow the caller
|
|
577
|
+
// may already open.
|
|
578
|
+
app.get("/flows/:flowId/versions", async (context) => {
|
|
579
|
+
const auth = requireCapability(context, "flows", "read");
|
|
580
|
+
const input = GetFlowInput.parse({ flowId: context.req.param("flowId") });
|
|
581
|
+
return context.json(await deps.flows.listVersions(asFlowActor(auth), input.flowId));
|
|
582
|
+
});
|
|
583
|
+
app.get("/flows/:flowId/versions/:versionId", async (context) => {
|
|
584
|
+
const auth = requireCapability(context, "flows", "read");
|
|
585
|
+
const input = GetFlowVersionInput.parse({
|
|
586
|
+
flowId: context.req.param("flowId"),
|
|
587
|
+
versionId: context.req.param("versionId"),
|
|
588
|
+
});
|
|
589
|
+
return context.json(await deps.flows.getVersion(asFlowActor(auth), input));
|
|
590
|
+
});
|
|
347
591
|
// What publishing would freeze, before it is published (ADR-0004 §5). A GET because it reads and
|
|
348
592
|
// changes nothing; the `publish` capability because it answers a question only a publisher has.
|
|
349
593
|
app.get("/flows/:flowId/versions/:versionId/publish-preview", async (context) => {
|
|
@@ -362,6 +606,15 @@ export function createHttp(deps) {
|
|
|
362
606
|
}
|
|
363
607
|
return context.json(await deps.flows.publish(asFlowActor(auth), input));
|
|
364
608
|
});
|
|
609
|
+
// The reverse of publish, behind the same capability: whoever may publish may withdraw (#146).
|
|
610
|
+
app.post("/flows/:flowId/unpublish", async (context) => {
|
|
611
|
+
const auth = requireCapability(context, "flows", "publish");
|
|
612
|
+
const input = UnpublishFlowInput.parse(await context.req.json().catch(() => null));
|
|
613
|
+
if (input.flowId !== context.req.param("flowId")) {
|
|
614
|
+
throw new IntelError(400, "flow_id_mismatch", "Path and body flow IDs differ");
|
|
615
|
+
}
|
|
616
|
+
return context.json(await deps.flows.unpublish(asFlowActor(auth), input));
|
|
617
|
+
});
|
|
365
618
|
app.post("/flows/:flowId/runs", async (context) => {
|
|
366
619
|
const auth = requireCapability(context, "flows", "run");
|
|
367
620
|
const input = StartFlowRunInput.parse(await context.req.json().catch(() => null));
|
|
@@ -406,6 +659,16 @@ export function createHttp(deps) {
|
|
|
406
659
|
}
|
|
407
660
|
return context.json(await deps.flows.completeStep(asFlowActor(auth), input));
|
|
408
661
|
});
|
|
662
|
+
// `flows/run`, the same capability the start asked for: whoever may start a run may end it
|
|
663
|
+
// (#145). The resource-level `execute` is the service's question, asked against the flow.
|
|
664
|
+
app.post("/flow-runs/:runId/cancel", async (context) => {
|
|
665
|
+
const auth = requireCapability(context, "flows", "run");
|
|
666
|
+
const input = CancelFlowRunInput.parse(await context.req.json().catch(() => null));
|
|
667
|
+
if (input.runId !== context.req.param("runId")) {
|
|
668
|
+
throw new IntelError(400, "run_id_mismatch", "Path and body run IDs differ");
|
|
669
|
+
}
|
|
670
|
+
return context.json(await deps.flows.cancelRun(asFlowActor(auth), input));
|
|
671
|
+
});
|
|
409
672
|
app.get("/tools", async (context) => {
|
|
410
673
|
const auth = requireCapability(context, "tools", "read");
|
|
411
674
|
return context.json(await deps.tools.catalog(asToolActor(auth)));
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import type { GateClient } from "@anchrd/gate-sdk";
|
|
2
|
+
import type { AgentRuntimeService } from "../agent-runtime/agent-runtime.types.js";
|
|
2
3
|
import type { BrowserAuth } from "../auth/auth.types.js";
|
|
4
|
+
import type { BundleService } from "../bundle/bundle.types.js";
|
|
3
5
|
import type { FlowService } from "../flows/flows.types.js";
|
|
4
|
-
import type {
|
|
6
|
+
import type { NodeService } from "../nodes/nodes.types.js";
|
|
5
7
|
import type { ToolService } from "../tools/tools.types.js";
|
|
6
8
|
export interface HttpDeps {
|
|
7
9
|
gate: Pick<GateClient, "authorize">;
|
|
8
|
-
|
|
10
|
+
nodes: NodeService;
|
|
9
11
|
flows: FlowService;
|
|
10
12
|
tools: ToolService;
|
|
13
|
+
bundle: BundleService;
|
|
14
|
+
agents: AgentRuntimeService;
|
|
11
15
|
resource: string;
|
|
12
16
|
resourceMetadataUrl: string;
|
|
13
17
|
auth?: Pick<BrowserAuth, "resolve">;
|
|
@@ -10,7 +10,7 @@ function indexText(mediaType, content) {
|
|
|
10
10
|
catch (error) {
|
|
11
11
|
// Immutable content never becomes parsable on a retry, so treating this as transient would
|
|
12
12
|
// requeue the version until the queue gives up and would bury the real cause.
|
|
13
|
-
throw new PermanentIndexingError(`
|
|
13
|
+
throw new PermanentIndexingError(`Node version content is not a valid BlockNote document: ${error instanceof Error ? error.message : "unknown parse failure"}`);
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
async function readCanonical(deps, target) {
|
|
@@ -29,13 +29,25 @@ export function createIndexing(deps) {
|
|
|
29
29
|
const target = await deps.repository.getTarget(versionId);
|
|
30
30
|
if (!target)
|
|
31
31
|
return;
|
|
32
|
+
// ⚠️ An agent's body is a definition, not something to read: node IDs, a cron line and a model
|
|
33
|
+
// name (ADR-0005 §4). Indexing it verbatim would fill the index with identifiers and, worse,
|
|
34
|
+
// put them into the passage a searcher is shown — the definition names nodes the searcher may
|
|
35
|
+
// have no access to, and a snippet is the one place their IDs would become visible without
|
|
36
|
+
// the tree being asked. What makes an agent findable is what a person wrote about it, so the
|
|
37
|
+
// text is its description, and the FTS table indexes the title on its own (#139).
|
|
38
|
+
if (target.kind === "agent") {
|
|
39
|
+
await deps.repository.replace(target, target.description ?? "");
|
|
40
|
+
await deps.semantic?.replace(target, target.description ?? "");
|
|
41
|
+
await deps.repository.markIndexed(versionId, deps.now().toISOString());
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
32
44
|
// An attachment is one object of bytes; text kinds may be several, because a table's content
|
|
33
45
|
// is the join of its versions (#40). A missing segment fails the whole pass rather than
|
|
34
46
|
// indexing a table with a hole in it.
|
|
35
47
|
const canonical = await readCanonical(deps, target);
|
|
36
48
|
if (canonical === null) {
|
|
37
49
|
await deps.repository.markError(versionId, "content_missing", deps.now().toISOString());
|
|
38
|
-
throw new Error("
|
|
50
|
+
throw new Error("Node version content is missing");
|
|
39
51
|
}
|
|
40
52
|
try {
|
|
41
53
|
const text = typeof canonical === "string"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { DocumentConverter } from "../adapters/document-converter/document-converter.types.js";
|
|
2
2
|
import type { SemanticIndex } from "../adapters/semantic-index/semantic-index.types.js";
|
|
3
|
-
import type { ContentStore,
|
|
3
|
+
import type { ContentStore, NodeIndexRepository } from "../nodes/nodes.types.js";
|
|
4
4
|
export interface IndexingDeps {
|
|
5
|
-
repository:
|
|
5
|
+
repository: NodeIndexRepository;
|
|
6
6
|
content: ContentStore;
|
|
7
7
|
semantic?: SemanticIndex | undefined;
|
|
8
8
|
converter?: DocumentConverter | undefined;
|