@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.
Files changed (58) hide show
  1. package/README.md +63 -3
  2. package/dist/adapters/cloudflare/cloudflare.js +102 -37
  3. package/dist/adapters/cloudflare/cloudflare.types.d.ts +20 -0
  4. package/dist/adapters/content/content.d.ts +1 -1
  5. package/dist/adapters/db/db-flows.js +148 -20
  6. package/dist/adapters/db/db-grants.d.ts +13 -2
  7. package/dist/adapters/db/db-grants.js +25 -8
  8. package/dist/adapters/db/db-indexing.d.ts +2 -2
  9. package/dist/adapters/db/db-indexing.js +26 -19
  10. package/dist/adapters/db/db.d.ts +3 -3
  11. package/dist/adapters/db/db.js +442 -118
  12. package/dist/adapters/gate-applications/gate-applications.d.ts +23 -0
  13. package/dist/adapters/gate-applications/gate-applications.js +88 -0
  14. package/dist/adapters/index-queue/index-queue.d.ts +1 -1
  15. package/dist/adapters/index-queue/index-queue.js +2 -2
  16. package/dist/adapters/semantic-index/semantic-index.types.d.ts +2 -2
  17. package/dist/adapters/tool-delegation/tool-delegation.d.ts +22 -0
  18. package/dist/adapters/tool-delegation/tool-delegation.js +90 -0
  19. package/dist/agent-runtime/agent-runtime.d.ts +16 -0
  20. package/dist/agent-runtime/agent-runtime.js +150 -0
  21. package/dist/agent-runtime/agent-runtime.types.d.ts +122 -0
  22. package/dist/bundle/bundle.d.ts +4 -0
  23. package/dist/bundle/bundle.js +1048 -0
  24. package/dist/bundle/bundle.types.d.ts +33 -0
  25. package/dist/bundle/bundle.types.js +1 -0
  26. package/dist/cli/cli.js +10 -1
  27. package/dist/flows/flows.d.ts +8 -8
  28. package/dist/flows/flows.js +158 -42
  29. package/dist/flows/flows.types.d.ts +40 -7
  30. package/dist/http/http.d.ts +1 -0
  31. package/dist/http/http.js +348 -61
  32. package/dist/http/http.types.d.ts +6 -2
  33. package/dist/indexing/indexing.js +14 -2
  34. package/dist/indexing/indexing.types.d.ts +2 -2
  35. package/dist/intel/intel.js +12 -3
  36. package/dist/intel/intel.types.d.ts +6 -2
  37. package/dist/mcp/mcp.js +519 -124
  38. package/dist/mcp/mcp.types.d.ts +11 -2
  39. package/dist/nodes/nodes.d.ts +2 -0
  40. package/dist/nodes/nodes.js +1466 -0
  41. package/dist/nodes/nodes.types.d.ts +402 -0
  42. package/dist/nodes/nodes.types.js +1 -0
  43. package/dist/tools/tool-servers/tool-servers.d.ts +46 -0
  44. package/dist/tools/tool-servers/tool-servers.js +114 -0
  45. package/dist/tools/tools.js +190 -31
  46. package/dist/tools/tools.types.d.ts +23 -1
  47. package/migrations/0011_one_name_for_the_tree.sql +53 -0
  48. package/migrations/0012_table_snapshots.sql +29 -0
  49. package/migrations/0013_agents_in_the_tree.sql +76 -0
  50. package/migrations/0014_agent_applications.sql +25 -0
  51. package/migrations/0015_tools_delegated_from_a_connection.sql +15 -0
  52. package/package.json +3 -2
  53. package/dist/knowledge/knowledge.d.ts +0 -2
  54. package/dist/knowledge/knowledge.js +0 -761
  55. package/dist/knowledge/knowledge.types.d.ts +0 -198
  56. /package/dist/{knowledge/knowledge.types.js → agent-runtime/agent-runtime.types.js} +0 -0
  57. /package/dist/{knowledge → nodes}/document-links/document-links.d.ts +0 -0
  58. /package/dist/{knowledge → nodes}/document-links/document-links.js +0 -0
package/dist/http/http.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AppendKnowledgeTableRowsInput, ArchiveFlowInput, ArchiveKnowledgeNodeInput, CompleteFlowRunStepInput, CreateFlowInput, CreateKnowledgeNodeInput, DefineKnowledgeTableInput, ExecuteToolInput, GetFlowInput, GetFlowRunInput, KnowledgeGraphInput, ListFlowRunsInput, ListFlowsInput, ListKnowledgeNodesInput, PreviewFlowPublishInput, PublishFlowInput, RelationGraphInput, ResolveKnowledgeLinksInput, RevokeKnowledgeGrantInput, SaveFlowVersionInput, SaveKnowledgeAttachmentInput, SaveKnowledgeVersionInput, SearchKnowledgeInput, ShareKnowledgeInput, StartFlowRunInput, TestToolInput, UpdateFlowInput, UpdateKnowledgeNodeInput, } from "@anchrd/intel-contract";
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
- return token ? await authorizeBearer(deps.gate, token, deps.resource) : null;
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 authorization = await authenticated(context.req.raw.headers);
75
- if (!authorization) {
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
- app.get("/knowledge", async (context) => {
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 = ListKnowledgeNodesInput.parse({
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.knowledge.list(asActor(auth), input));
207
+ return context.json(await deps.nodes.list(asActor(auth), input));
117
208
  });
118
- app.get("/knowledge/graph", async (context) => {
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 = KnowledgeGraphInput.parse({
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.knowledge.graph(asActor(auth), input));
215
+ return context.json(await deps.nodes.graph(asActor(auth), input));
125
216
  });
126
- app.get("/knowledge/:nodeId", async (context) => {
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
- return context.json(await deps.knowledge.get(asActor(auth), context.req.param("nodeId")));
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
- app.get("/knowledge/:nodeId/versions", async (context) => {
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 context.json(await deps.knowledge.listVersions(asActor(auth), context.req.param("nodeId")));
266
+ return zipResponse(await deps.bundle.exportSubtree(asBundleActor(auth), null));
133
267
  });
134
- app.get("/knowledge/:nodeId/attachment", async (context) => {
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
- const { attachment, body } = await deps.knowledge.readAttachment(asActor(auth), context.req.param("nodeId"));
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("/knowledge/:nodeId/table", async (context) => {
354
+ app.get("/nodes/:nodeId/table", async (context) => {
147
355
  const auth = requireCapability(context, "knowledge", "read");
148
- return context.json(await deps.knowledge.getTable(asActor(auth), context.req.param("nodeId")));
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("/knowledge/:nodeId/table", async (context) => {
360
+ app.post("/nodes/:nodeId/table", async (context) => {
153
361
  const auth = requireCapability(context, "knowledge", "write");
154
- const input = DefineKnowledgeTableInput.parse(await context.req.json().catch(() => null));
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.knowledge.defineTable(asActor(auth), input), 201);
366
+ return context.json(await deps.nodes.defineTable(asActor(auth), input), 201);
159
367
  });
160
- app.post("/knowledge/:nodeId/table/rows", async (context) => {
368
+ app.post("/nodes/:nodeId/table/rows", async (context) => {
161
369
  const auth = requireCapability(context, "knowledge", "write");
162
- const input = AppendKnowledgeTableRowsInput.parse(await context.req.json().catch(() => null));
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.knowledge.appendTableRows(asActor(auth), input), 201);
374
+ return context.json(await deps.nodes.appendTableRows(asActor(auth), input), 201);
167
375
  });
168
- app.get("/knowledge/:nodeId/links", async (context) => {
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.knowledge.listLinks(asActor(auth), context.req.param("nodeId")));
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 /knowledge/:nodeId/versions` is what records it.
179
- app.post("/knowledge/links/resolve", async (context) => {
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 = ResolveKnowledgeLinksInput.parse(await context.req.json().catch(() => null));
182
- return context.json(await deps.knowledge.resolveLinks(asActor(auth), input));
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("/knowledge/search", async (context) => {
420
+ app.post("/nodes/search", async (context) => {
185
421
  const auth = requireCapability(context, "knowledge", "read");
186
- const input = SearchKnowledgeInput.parse(await context.req.json().catch(() => null));
187
- return context.json(await deps.knowledge.search(asActor(auth), input));
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.knowledge.reindex(asActor(auth)), 202);
428
+ return context.json(await deps.nodes.reindex(asActor(auth)), 202);
193
429
  });
194
- app.post("/knowledge", async (context) => {
430
+ app.post("/nodes", async (context) => {
195
431
  const auth = requireCapability(context, "knowledge", "create");
196
- const input = CreateKnowledgeNodeInput.parse(await context.req.json().catch(() => null));
197
- return context.json(await deps.knowledge.create(asActor(auth), input), 201);
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("/knowledge/:nodeId/versions", async (context) => {
435
+ app.post("/nodes/:nodeId/versions", async (context) => {
200
436
  const auth = requireCapability(context, "knowledge", "write");
201
- const input = SaveKnowledgeVersionInput.parse(await context.req.json().catch(() => null));
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.knowledge.save(asActor(auth), input), 201);
441
+ return context.json(await deps.nodes.save(asActor(auth), input), 201);
206
442
  });
207
- app.post("/knowledge/:nodeId/attachment", async (context) => {
443
+ app.post("/nodes/:nodeId/attachment", async (context) => {
208
444
  const auth = requireCapability(context, "knowledge", "write");
209
- const input = SaveKnowledgeAttachmentInput.parse(await context.req.json().catch(() => null));
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.knowledge.saveAttachment(asActor(auth), input), 201);
449
+ return context.json(await deps.nodes.saveAttachment(asActor(auth), input), 201);
214
450
  });
215
- app.patch("/knowledge/:nodeId", async (context) => {
451
+ app.patch("/nodes/:nodeId", async (context) => {
216
452
  const auth = requireCapability(context, "knowledge", "write");
217
- const input = UpdateKnowledgeNodeInput.parse(await context.req.json().catch(() => null));
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.knowledge.update(asActor(auth), input));
457
+ return context.json(await deps.nodes.update(asActor(auth), input));
222
458
  });
223
- app.post("/knowledge/:nodeId/archive", async (context) => {
459
+ app.post("/nodes/:nodeId/archive", async (context) => {
224
460
  const auth = requireCapability(context, "knowledge", "write");
225
- const input = ArchiveKnowledgeNodeInput.parse(await context.req.json().catch(() => null));
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
- return context.json(await deps.knowledge.archive(asActor(auth), input));
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("/knowledge/:nodeId/grants", async (context) => {
469
+ app.get("/nodes/:nodeId/grants", async (context) => {
232
470
  const auth = requireCapability(context, "knowledge", "share");
233
- return context.json(await deps.knowledge.listGrants(asActor(auth), context.req.param("nodeId")));
471
+ return context.json(await deps.nodes.listGrants(asActor(auth), context.req.param("nodeId")));
234
472
  });
235
- app.post("/knowledge/:nodeId/grants", async (context) => {
473
+ app.post("/nodes/:nodeId/grants", async (context) => {
236
474
  const auth = requireCapability(context, "knowledge", "share");
237
- const input = ShareKnowledgeInput.parse(await context.req.json().catch(() => null));
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.knowledge.share(asActor(auth), input), 201);
479
+ return context.json(await deps.nodes.share(asActor(auth), input), 201);
242
480
  });
243
- app.post("/knowledge/:nodeId/grants/:grantId/revoke", async (context) => {
481
+ app.post("/nodes/:nodeId/grants/:grantId/revoke", async (context) => {
244
482
  const auth = requireCapability(context, "knowledge", "share");
245
- const input = RevokeKnowledgeGrantInput.parse(await context.req.json().catch(() => null));
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.knowledge.revokeGrant(asActor(auth), input));
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 `/knowledge/graph`.
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
- // /knowledge/:nodeId/grants — one place answers the question for the documents and the flows in
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 { KnowledgeService } from "../knowledge/knowledge.types.js";
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
- knowledge: KnowledgeService;
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">;