@anchrd/intel-api 0.6.7 → 0.9.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 +63 -3
- package/dist/adapters/cloudflare/cloudflare.js +102 -37
- package/dist/adapters/cloudflare/cloudflare.types.d.ts +20 -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 +442 -118
- package/dist/adapters/gate-applications/gate-applications.d.ts +23 -0
- package/dist/adapters/gate-applications/gate-applications.js +88 -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/adapters/tool-delegation/tool-delegation.d.ts +22 -0
- package/dist/adapters/tool-delegation/tool-delegation.js +90 -0
- package/dist/agent-runtime/agent-runtime.d.ts +16 -0
- package/dist/agent-runtime/agent-runtime.js +150 -0
- package/dist/agent-runtime/agent-runtime.types.d.ts +122 -0
- package/dist/bundle/bundle.d.ts +4 -0
- package/dist/bundle/bundle.js +1048 -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 +348 -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 +519 -124
- package/dist/mcp/mcp.types.d.ts +11 -2
- package/dist/nodes/nodes.d.ts +2 -0
- package/dist/nodes/nodes.js +1466 -0
- package/dist/nodes/nodes.types.d.ts +402 -0
- package/dist/nodes/nodes.types.js +1 -0
- package/dist/tools/tool-servers/tool-servers.d.ts +46 -0
- package/dist/tools/tool-servers/tool-servers.js +114 -0
- package/dist/tools/tools.js +190 -31
- package/dist/tools/tools.types.d.ts +23 -1
- 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/migrations/0015_tools_delegated_from_a_connection.sql +15 -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, RotateAgentKeyInput, 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,149 @@ 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));
|
|
125
216
|
});
|
|
126
|
-
|
|
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) => {
|
|
127
220
|
const auth = requireCapability(context, "knowledge", "read");
|
|
128
|
-
|
|
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
|
+
* ⚠️ NO response in Intel's HTTP surface carries a credential any more (D29, #207).
|
|
235
|
+
*
|
|
236
|
+
* Creating an agent still mints its Gate Application with the caller's own bearer — so this needs
|
|
237
|
+
* `applications:write` in Gate on top of `knowledge:create` in Intel — but the key Gate issues
|
|
238
|
+
* once goes straight into the agent runtime over the service binding, inside this request. The
|
|
239
|
+
* answer is the same shape every agent read has. Until #207 it carried the key in plain text and
|
|
240
|
+
* a person had to put it into a Worker secret by hand, which is why an agent created here could
|
|
241
|
+
* never run (#200).
|
|
242
|
+
*/
|
|
243
|
+
app.post("/nodes/agents", async (context) => {
|
|
244
|
+
const auth = requireCapability(context, "knowledge", "create");
|
|
245
|
+
const input = CreateAgentInput.parse(await context.req.json().catch(() => null));
|
|
246
|
+
return context.json(await deps.nodes.createAgent(asActor(auth), input, { token: context.get("token") }), 201);
|
|
247
|
+
});
|
|
248
|
+
/**
|
|
249
|
+
* The repair path, and the only one an agent that lost its key has (D29, #207).
|
|
250
|
+
*
|
|
251
|
+
* ⚠️ `knowledge/write`, not `agents/run`. Which principal an agent acts as is a property of the
|
|
252
|
+
* agent, changed by whoever may change the agent; being allowed to press "Run now" is a different
|
|
253
|
+
* question and answers a different one. The resource ACL on the node is checked by the service.
|
|
254
|
+
*
|
|
255
|
+
* ⚠️ Before `/nodes/:nodeId`, like every other literal segment under `/nodes`.
|
|
256
|
+
*/
|
|
257
|
+
app.post("/nodes/agents/:nodeId/rotate-key", async (context) => {
|
|
258
|
+
const auth = requireCapability(context, "knowledge", "write");
|
|
259
|
+
const input = RotateAgentKeyInput.parse({ nodeId: context.req.param("nodeId") });
|
|
260
|
+
return context.json(await deps.nodes.rotateAgentKey(asActor(auth), input, { token: context.get("token") }));
|
|
129
261
|
});
|
|
130
|
-
|
|
262
|
+
// ⚠️ Before `/nodes/:nodeId`, or the router would read "export" as a node ID. The root has no
|
|
263
|
+
// node to address, so the whole installation — as this caller may read it — exports here (#136).
|
|
264
|
+
app.get("/nodes/export", async (context) => {
|
|
131
265
|
const auth = requireCapability(context, "knowledge", "read");
|
|
132
|
-
return
|
|
266
|
+
return zipResponse(await deps.bundle.exportSubtree(asBundleActor(auth), null));
|
|
133
267
|
});
|
|
134
|
-
|
|
268
|
+
// The reverse door (#137): a zip bundle — or a naked zipped folder — lands as a new subtree.
|
|
269
|
+
// The body IS the zip, streamed to the service rather than read whole here; `knowledge/create`
|
|
270
|
+
// like every other way of making nodes, and the resource-level write check is the service's.
|
|
271
|
+
async function importBundle(context, targetNodeId) {
|
|
272
|
+
const auth = requireCapability(context, "knowledge", "create");
|
|
273
|
+
const idempotencyKey = ImportIdempotencyKey.parse(context.req.header("idempotency-key"));
|
|
274
|
+
const body = context.req.raw.body;
|
|
275
|
+
if (body === null) {
|
|
276
|
+
throw new IntelError(400, "invalid_request", "A zip request body is required");
|
|
277
|
+
}
|
|
278
|
+
const result = await deps.bundle.importBundle(asBundleActor(auth), {
|
|
279
|
+
targetNodeId,
|
|
280
|
+
zip: body,
|
|
281
|
+
idempotencyKey,
|
|
282
|
+
});
|
|
283
|
+
// A replay answers 200: nothing was created by THIS request, and the summary is the earlier
|
|
284
|
+
// import's. 201 stays the word for rows that exist because of this call.
|
|
285
|
+
return context.json(result, result.replayed ? 200 : 201);
|
|
286
|
+
}
|
|
287
|
+
// ⚠️ Before `/nodes/:nodeId`, or the router would read "import" as a node ID.
|
|
288
|
+
app.post("/nodes/import", async (context) => await importBundle(context, null));
|
|
289
|
+
app.get("/nodes/:nodeId", async (context) => {
|
|
135
290
|
const auth = requireCapability(context, "knowledge", "read");
|
|
136
|
-
|
|
291
|
+
return context.json(await deps.nodes.get(asActor(auth), context.req.param("nodeId")));
|
|
292
|
+
});
|
|
293
|
+
app.post("/nodes/:nodeId/import", async (context) => {
|
|
294
|
+
const input = GetNodeInput.parse({ nodeId: context.req.param("nodeId") });
|
|
295
|
+
return await importBundle(context, input.nodeId);
|
|
296
|
+
});
|
|
297
|
+
// One subtree — a folder with everything beneath it, or a single node — as a zip bundle (#136).
|
|
298
|
+
app.get("/nodes/:nodeId/export", async (context) => {
|
|
299
|
+
const auth = requireCapability(context, "knowledge", "read");
|
|
300
|
+
const input = GetNodeInput.parse({ nodeId: context.req.param("nodeId") });
|
|
301
|
+
return zipResponse(await deps.bundle.exportSubtree(asBundleActor(auth), input.nodeId));
|
|
302
|
+
});
|
|
303
|
+
app.get("/nodes/:nodeId/agent", async (context) => {
|
|
304
|
+
const auth = requireCapability(context, "knowledge", "read");
|
|
305
|
+
return context.json(await deps.nodes.getAgent(asActor(auth), { nodeId: context.req.param("nodeId") }));
|
|
306
|
+
});
|
|
307
|
+
app.post("/nodes/:nodeId/agent", async (context) => {
|
|
308
|
+
const auth = requireCapability(context, "knowledge", "write");
|
|
309
|
+
const input = SaveAgentDefinitionInput.parse(await context.req.json().catch(() => null));
|
|
310
|
+
if (input.nodeId !== context.req.param("nodeId")) {
|
|
311
|
+
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
312
|
+
}
|
|
313
|
+
// ⚠️ The caller's own bearer travels on, like it does when an agent is created: writing a
|
|
314
|
+
// definition also arms what it schedules, and that call is made as the person saving (#214).
|
|
315
|
+
return context.json(await deps.nodes.saveAgentDefinition(asActor(auth), input, { token: context.get("token") }), 201);
|
|
316
|
+
});
|
|
317
|
+
app.get("/nodes/:nodeId/versions", async (context) => {
|
|
318
|
+
const auth = requireCapability(context, "knowledge", "read");
|
|
319
|
+
return context.json(await deps.nodes.listVersions(asActor(auth), context.req.param("nodeId")));
|
|
320
|
+
});
|
|
321
|
+
// The content behind one row of the list above (#147): what a version-pinned citation points at.
|
|
322
|
+
// Read capability like every other node read; the resource ACL and the node/version pairing are
|
|
323
|
+
// the service's to refuse.
|
|
324
|
+
app.get("/nodes/:nodeId/versions/:versionId/content", async (context) => {
|
|
325
|
+
const auth = requireCapability(context, "knowledge", "read");
|
|
326
|
+
const input = GetNodeVersionInput.parse({
|
|
327
|
+
nodeId: context.req.param("nodeId"),
|
|
328
|
+
versionId: context.req.param("versionId"),
|
|
329
|
+
});
|
|
330
|
+
return context.json(await deps.nodes.getVersion(asActor(auth), input.nodeId, input.versionId));
|
|
331
|
+
});
|
|
332
|
+
// The metadata half of an attachment (#148): what `node_attachment_get` answers an agent, for
|
|
333
|
+
// HTTP callers who so far could only stream the bytes. Without the `resourceUri` — an MCP
|
|
334
|
+
// resource address says nothing to an HTTP client, whose address for the bytes is the sibling
|
|
335
|
+
// route below.
|
|
336
|
+
app.get("/nodes/:nodeId/attachment/meta", async (context) => {
|
|
337
|
+
const auth = requireCapability(context, "knowledge", "read");
|
|
338
|
+
const input = GetNodeInput.parse({ nodeId: context.req.param("nodeId") });
|
|
339
|
+
const { node, version } = await deps.nodes.getAttachment(asActor(auth), input.nodeId);
|
|
340
|
+
return context.json({ node, version });
|
|
341
|
+
});
|
|
342
|
+
app.get("/nodes/:nodeId/attachment", async (context) => {
|
|
343
|
+
const auth = requireCapability(context, "knowledge", "read");
|
|
344
|
+
const { attachment, body } = await deps.nodes.readAttachment(asActor(auth), context.req.param("nodeId"));
|
|
137
345
|
return new Response(body, {
|
|
138
346
|
headers: {
|
|
139
347
|
"cache-control": "private, no-store",
|
|
@@ -143,111 +351,141 @@ export function createHttp(deps) {
|
|
|
143
351
|
},
|
|
144
352
|
});
|
|
145
353
|
});
|
|
146
|
-
app.get("/
|
|
354
|
+
app.get("/nodes/:nodeId/table", async (context) => {
|
|
147
355
|
const auth = requireCapability(context, "knowledge", "read");
|
|
148
|
-
return context.json(await deps.
|
|
356
|
+
return context.json(await deps.nodes.getTable(asActor(auth), context.req.param("nodeId")));
|
|
149
357
|
});
|
|
150
358
|
// Defining the header and appending rows are two routes because they are two decisions: the
|
|
151
359
|
// header is written once and is the contract, an append is the everyday write (#40).
|
|
152
|
-
app.post("/
|
|
360
|
+
app.post("/nodes/:nodeId/table", async (context) => {
|
|
153
361
|
const auth = requireCapability(context, "knowledge", "write");
|
|
154
|
-
const input =
|
|
362
|
+
const input = DefineTableInput.parse(await context.req.json().catch(() => null));
|
|
155
363
|
if (input.nodeId !== context.req.param("nodeId")) {
|
|
156
364
|
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
157
365
|
}
|
|
158
|
-
return context.json(await deps.
|
|
366
|
+
return context.json(await deps.nodes.defineTable(asActor(auth), input), 201);
|
|
159
367
|
});
|
|
160
|
-
app.post("/
|
|
368
|
+
app.post("/nodes/:nodeId/table/rows", async (context) => {
|
|
161
369
|
const auth = requireCapability(context, "knowledge", "write");
|
|
162
|
-
const input =
|
|
370
|
+
const input = AppendTableRowsInput.parse(await context.req.json().catch(() => null));
|
|
163
371
|
if (input.nodeId !== context.req.param("nodeId")) {
|
|
164
372
|
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
165
373
|
}
|
|
166
|
-
return context.json(await deps.
|
|
374
|
+
return context.json(await deps.nodes.appendTableRows(asActor(auth), input), 201);
|
|
167
375
|
});
|
|
168
|
-
|
|
376
|
+
// The three mutations beside the append (#135). Action-style POSTs like the archive and revoke
|
|
377
|
+
// routes, because a row has no address of its own to PUT to: positions live in the body, next to
|
|
378
|
+
// the `baseVersionId` that says which state they mean. Each writes one snapshot version, so the
|
|
379
|
+
// 201 is the same statement the append's 201 makes.
|
|
380
|
+
app.post("/nodes/:nodeId/table/rows/update", async (context) => {
|
|
381
|
+
const auth = requireCapability(context, "knowledge", "write");
|
|
382
|
+
const input = UpdateTableRowsInput.parse(await context.req.json().catch(() => null));
|
|
383
|
+
if (input.nodeId !== context.req.param("nodeId")) {
|
|
384
|
+
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
385
|
+
}
|
|
386
|
+
return context.json(await deps.nodes.updateTableRows(asActor(auth), input), 201);
|
|
387
|
+
});
|
|
388
|
+
app.post("/nodes/:nodeId/table/rows/delete", async (context) => {
|
|
389
|
+
const auth = requireCapability(context, "knowledge", "write");
|
|
390
|
+
const input = DeleteTableRowsInput.parse(await context.req.json().catch(() => null));
|
|
391
|
+
if (input.nodeId !== context.req.param("nodeId")) {
|
|
392
|
+
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
393
|
+
}
|
|
394
|
+
return context.json(await deps.nodes.deleteTableRows(asActor(auth), input), 201);
|
|
395
|
+
});
|
|
396
|
+
app.post("/nodes/:nodeId/table/redefine", async (context) => {
|
|
397
|
+
const auth = requireCapability(context, "knowledge", "write");
|
|
398
|
+
const input = RedefineTableInput.parse(await context.req.json().catch(() => null));
|
|
399
|
+
if (input.nodeId !== context.req.param("nodeId")) {
|
|
400
|
+
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
401
|
+
}
|
|
402
|
+
return context.json(await deps.nodes.redefineTable(asActor(auth), input), 201);
|
|
403
|
+
});
|
|
404
|
+
app.get("/nodes/:nodeId/links", async (context) => {
|
|
169
405
|
const auth = requireCapability(context, "knowledge", "read");
|
|
170
|
-
return context.json(await deps.
|
|
406
|
+
return context.json(await deps.nodes.listLinks(asActor(auth), context.req.param("nodeId")));
|
|
171
407
|
});
|
|
172
408
|
// A document link's name, for whoever is reading the text (#41). A POST because the list of IDs
|
|
173
409
|
// 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
|
|
410
|
+
// logs and referrers. The `knowledge/read` capability and nothing more: it answers with what this reader may
|
|
175
411
|
// already see, and stays silent about the rest rather than saying that there is a rest.
|
|
176
412
|
//
|
|
177
413
|
// 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("/
|
|
414
|
+
// meant, in the text, and `POST /nodes/:nodeId/versions` is what records it.
|
|
415
|
+
app.post("/nodes/links/resolve", async (context) => {
|
|
180
416
|
const auth = requireCapability(context, "knowledge", "read");
|
|
181
|
-
const input =
|
|
182
|
-
return context.json(await deps.
|
|
417
|
+
const input = ResolveNodeLinksInput.parse(await context.req.json().catch(() => null));
|
|
418
|
+
return context.json(await deps.nodes.resolveLinks(asActor(auth), input));
|
|
183
419
|
});
|
|
184
|
-
app.post("/
|
|
420
|
+
app.post("/nodes/search", async (context) => {
|
|
185
421
|
const auth = requireCapability(context, "knowledge", "read");
|
|
186
|
-
const input =
|
|
187
|
-
return context.json(await deps.
|
|
422
|
+
const input = SearchInput.parse(await context.req.json().catch(() => null));
|
|
423
|
+
return context.json(await deps.nodes.search(asActor(auth), input));
|
|
188
424
|
});
|
|
189
425
|
app.post("/search/reindex", async (context) => {
|
|
190
426
|
const auth = requireCapability(context, "intel", "admin");
|
|
191
427
|
z.strictObject({}).parse(await context.req.json().catch(() => null));
|
|
192
|
-
return context.json(await deps.
|
|
428
|
+
return context.json(await deps.nodes.reindex(asActor(auth)), 202);
|
|
193
429
|
});
|
|
194
|
-
app.post("/
|
|
430
|
+
app.post("/nodes", async (context) => {
|
|
195
431
|
const auth = requireCapability(context, "knowledge", "create");
|
|
196
|
-
const input =
|
|
197
|
-
return context.json(await deps.
|
|
432
|
+
const input = CreateNodeInput.parse(await context.req.json().catch(() => null));
|
|
433
|
+
return context.json(await deps.nodes.create(asActor(auth), input), 201);
|
|
198
434
|
});
|
|
199
|
-
app.post("/
|
|
435
|
+
app.post("/nodes/:nodeId/versions", async (context) => {
|
|
200
436
|
const auth = requireCapability(context, "knowledge", "write");
|
|
201
|
-
const input =
|
|
437
|
+
const input = SaveNodeVersionInput.parse(await context.req.json().catch(() => null));
|
|
202
438
|
if (input.nodeId !== context.req.param("nodeId")) {
|
|
203
439
|
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
204
440
|
}
|
|
205
|
-
return context.json(await deps.
|
|
441
|
+
return context.json(await deps.nodes.save(asActor(auth), input), 201);
|
|
206
442
|
});
|
|
207
|
-
app.post("/
|
|
443
|
+
app.post("/nodes/:nodeId/attachment", async (context) => {
|
|
208
444
|
const auth = requireCapability(context, "knowledge", "write");
|
|
209
|
-
const input =
|
|
445
|
+
const input = SaveAttachmentInput.parse(await context.req.json().catch(() => null));
|
|
210
446
|
if (input.nodeId !== context.req.param("nodeId")) {
|
|
211
447
|
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
212
448
|
}
|
|
213
|
-
return context.json(await deps.
|
|
449
|
+
return context.json(await deps.nodes.saveAttachment(asActor(auth), input), 201);
|
|
214
450
|
});
|
|
215
|
-
app.patch("/
|
|
451
|
+
app.patch("/nodes/:nodeId", async (context) => {
|
|
216
452
|
const auth = requireCapability(context, "knowledge", "write");
|
|
217
|
-
const input =
|
|
453
|
+
const input = UpdateNodeInput.parse(await context.req.json().catch(() => null));
|
|
218
454
|
if (input.nodeId !== context.req.param("nodeId")) {
|
|
219
455
|
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
220
456
|
}
|
|
221
|
-
return context.json(await deps.
|
|
457
|
+
return context.json(await deps.nodes.update(asActor(auth), input));
|
|
222
458
|
});
|
|
223
|
-
app.post("/
|
|
459
|
+
app.post("/nodes/:nodeId/archive", async (context) => {
|
|
224
460
|
const auth = requireCapability(context, "knowledge", "write");
|
|
225
|
-
const input =
|
|
461
|
+
const input = ArchiveNodeInput.parse(await context.req.json().catch(() => null));
|
|
226
462
|
if (input.nodeId !== context.req.param("nodeId")) {
|
|
227
463
|
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
228
464
|
}
|
|
229
|
-
|
|
465
|
+
// The caller's own bearer, for the one thing archiving may have to do in Gate: switch off the
|
|
466
|
+
// Application behind an agent (#182). Every other kind never reaches it.
|
|
467
|
+
return context.json(await deps.nodes.archive(asActor(auth), input, { token: context.get("token") }));
|
|
230
468
|
});
|
|
231
|
-
app.get("/
|
|
469
|
+
app.get("/nodes/:nodeId/grants", async (context) => {
|
|
232
470
|
const auth = requireCapability(context, "knowledge", "share");
|
|
233
|
-
return context.json(await deps.
|
|
471
|
+
return context.json(await deps.nodes.listGrants(asActor(auth), context.req.param("nodeId")));
|
|
234
472
|
});
|
|
235
|
-
app.post("/
|
|
473
|
+
app.post("/nodes/:nodeId/grants", async (context) => {
|
|
236
474
|
const auth = requireCapability(context, "knowledge", "share");
|
|
237
|
-
const input =
|
|
475
|
+
const input = ShareInput.parse(await context.req.json().catch(() => null));
|
|
238
476
|
if (input.resourceId !== context.req.param("nodeId")) {
|
|
239
477
|
throw new IntelError(400, "node_id_mismatch", "Path and body resource IDs differ");
|
|
240
478
|
}
|
|
241
|
-
return context.json(await deps.
|
|
479
|
+
return context.json(await deps.nodes.share(asActor(auth), input), 201);
|
|
242
480
|
});
|
|
243
|
-
app.post("/
|
|
481
|
+
app.post("/nodes/:nodeId/grants/:grantId/revoke", async (context) => {
|
|
244
482
|
const auth = requireCapability(context, "knowledge", "share");
|
|
245
|
-
const input =
|
|
483
|
+
const input = RevokeGrantInput.parse(await context.req.json().catch(() => null));
|
|
246
484
|
if (input.resourceId !== context.req.param("nodeId") ||
|
|
247
485
|
input.grantId !== context.req.param("grantId")) {
|
|
248
486
|
throw new IntelError(400, "grant_id_mismatch", "Path and body grant IDs differ");
|
|
249
487
|
}
|
|
250
|
-
return context.json(await deps.
|
|
488
|
+
return context.json(await deps.nodes.revokeGrant(asActor(auth), input));
|
|
251
489
|
});
|
|
252
490
|
app.get("/flows", async (context) => {
|
|
253
491
|
const auth = requireCapability(context, "flows", "read");
|
|
@@ -266,7 +504,7 @@ export function createHttp(deps) {
|
|
|
266
504
|
return context.json(await deps.flows.list(asFlowActor(auth), input));
|
|
267
505
|
});
|
|
268
506
|
// ⚠️ Before `/flows/:flowId`, or the router would read "graph" as a flow ID. Same order and same
|
|
269
|
-
// reason as `/
|
|
507
|
+
// reason as `/nodes/graph`.
|
|
270
508
|
app.get("/flows/graph", async (context) => {
|
|
271
509
|
const auth = requireCapability(context, "flows", "read");
|
|
272
510
|
const url = new URL(context.req.url);
|
|
@@ -287,6 +525,13 @@ export function createHttp(deps) {
|
|
|
287
525
|
const input = GetFlowInput.parse({ flowId: context.req.param("flowId") });
|
|
288
526
|
return context.json(await deps.flows.get(asFlowActor(auth), input.flowId));
|
|
289
527
|
});
|
|
528
|
+
// A single flow as the same bundle shape a node export makes: one entry plus the manifest
|
|
529
|
+
// (#136). `flows/read` like every other read of the graph.
|
|
530
|
+
app.get("/flows/:flowId/export", async (context) => {
|
|
531
|
+
const auth = requireCapability(context, "flows", "read");
|
|
532
|
+
const input = GetFlowInput.parse({ flowId: context.req.param("flowId") });
|
|
533
|
+
return zipResponse(await deps.bundle.exportFlow(asBundleActor(auth), input.flowId));
|
|
534
|
+
});
|
|
290
535
|
// What this flow calls, read out of its graph. The tree lists where a flow is filed; this lists
|
|
291
536
|
// what it runs, and a shared subflow answers both questions at once (ADR-0004 §3).
|
|
292
537
|
app.get("/flows/:flowId/calls", async (context) => {
|
|
@@ -311,7 +556,7 @@ export function createHttp(deps) {
|
|
|
311
556
|
return context.json(await deps.flows.listRequirements(asFlowActor(auth), input.flowId));
|
|
312
557
|
});
|
|
313
558
|
// A flow has no grant route of its own. It is shared through the folder it is filed in, under
|
|
314
|
-
// /
|
|
559
|
+
// /nodes/:nodeId/grants — one place answers the question for the documents and the flows in
|
|
315
560
|
// that folder alike (ADR-0004 §2).
|
|
316
561
|
app.post("/flows", async (context) => {
|
|
317
562
|
const auth = requireCapability(context, "flows", "create");
|
|
@@ -344,6 +589,22 @@ export function createHttp(deps) {
|
|
|
344
589
|
}
|
|
345
590
|
return context.json(await deps.flows.save(asFlowActor(auth), input), 201);
|
|
346
591
|
});
|
|
592
|
+
// The GET beside `POST /flows/:flowId/versions`: the same collection, read instead of appended
|
|
593
|
+
// to (#144). `flows/read` like `GET /flows/:flowId` — the history describes a flow the caller
|
|
594
|
+
// may already open.
|
|
595
|
+
app.get("/flows/:flowId/versions", async (context) => {
|
|
596
|
+
const auth = requireCapability(context, "flows", "read");
|
|
597
|
+
const input = GetFlowInput.parse({ flowId: context.req.param("flowId") });
|
|
598
|
+
return context.json(await deps.flows.listVersions(asFlowActor(auth), input.flowId));
|
|
599
|
+
});
|
|
600
|
+
app.get("/flows/:flowId/versions/:versionId", async (context) => {
|
|
601
|
+
const auth = requireCapability(context, "flows", "read");
|
|
602
|
+
const input = GetFlowVersionInput.parse({
|
|
603
|
+
flowId: context.req.param("flowId"),
|
|
604
|
+
versionId: context.req.param("versionId"),
|
|
605
|
+
});
|
|
606
|
+
return context.json(await deps.flows.getVersion(asFlowActor(auth), input));
|
|
607
|
+
});
|
|
347
608
|
// What publishing would freeze, before it is published (ADR-0004 §5). A GET because it reads and
|
|
348
609
|
// changes nothing; the `publish` capability because it answers a question only a publisher has.
|
|
349
610
|
app.get("/flows/:flowId/versions/:versionId/publish-preview", async (context) => {
|
|
@@ -362,6 +623,15 @@ export function createHttp(deps) {
|
|
|
362
623
|
}
|
|
363
624
|
return context.json(await deps.flows.publish(asFlowActor(auth), input));
|
|
364
625
|
});
|
|
626
|
+
// The reverse of publish, behind the same capability: whoever may publish may withdraw (#146).
|
|
627
|
+
app.post("/flows/:flowId/unpublish", async (context) => {
|
|
628
|
+
const auth = requireCapability(context, "flows", "publish");
|
|
629
|
+
const input = UnpublishFlowInput.parse(await context.req.json().catch(() => null));
|
|
630
|
+
if (input.flowId !== context.req.param("flowId")) {
|
|
631
|
+
throw new IntelError(400, "flow_id_mismatch", "Path and body flow IDs differ");
|
|
632
|
+
}
|
|
633
|
+
return context.json(await deps.flows.unpublish(asFlowActor(auth), input));
|
|
634
|
+
});
|
|
365
635
|
app.post("/flows/:flowId/runs", async (context) => {
|
|
366
636
|
const auth = requireCapability(context, "flows", "run");
|
|
367
637
|
const input = StartFlowRunInput.parse(await context.req.json().catch(() => null));
|
|
@@ -406,10 +676,27 @@ export function createHttp(deps) {
|
|
|
406
676
|
}
|
|
407
677
|
return context.json(await deps.flows.completeStep(asFlowActor(auth), input));
|
|
408
678
|
});
|
|
679
|
+
// `flows/run`, the same capability the start asked for: whoever may start a run may end it
|
|
680
|
+
// (#145). The resource-level `execute` is the service's question, asked against the flow.
|
|
681
|
+
app.post("/flow-runs/:runId/cancel", async (context) => {
|
|
682
|
+
const auth = requireCapability(context, "flows", "run");
|
|
683
|
+
const input = CancelFlowRunInput.parse(await context.req.json().catch(() => null));
|
|
684
|
+
if (input.runId !== context.req.param("runId")) {
|
|
685
|
+
throw new IntelError(400, "run_id_mismatch", "Path and body run IDs differ");
|
|
686
|
+
}
|
|
687
|
+
return context.json(await deps.flows.cancelRun(asFlowActor(auth), input));
|
|
688
|
+
});
|
|
409
689
|
app.get("/tools", async (context) => {
|
|
410
690
|
const auth = requireCapability(context, "tools", "read");
|
|
411
691
|
return context.json(await deps.tools.catalog(asToolActor(auth)));
|
|
412
692
|
});
|
|
693
|
+
// ⚠️ Registered before `/tools/test` and `/tools/execute` and behind `tools/read`, not a new
|
|
694
|
+
// capability: it answers strictly less than `/tools` does — the servers behind the same live
|
|
695
|
+
// list, with no schemas (D30).
|
|
696
|
+
app.get("/tools/servers", async (context) => {
|
|
697
|
+
const auth = requireCapability(context, "tools", "read");
|
|
698
|
+
return context.json(await deps.tools.servers(asToolActor(auth)));
|
|
699
|
+
});
|
|
413
700
|
app.post("/tools/test", async (context) => {
|
|
414
701
|
const auth = requireCapability(context, "tools", "test");
|
|
415
702
|
const input = TestToolInput.parse(await context.req.json().catch(() => null));
|
|
@@ -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">;
|