@anchrd/intel-api 0.6.6 → 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.
Files changed (51) hide show
  1. package/README.md +44 -3
  2. package/dist/adapters/cloudflare/cloudflare.js +50 -16
  3. package/dist/adapters/cloudflare/cloudflare.types.d.ts +11 -0
  4. package/dist/adapters/content/content.d.ts +1 -1
  5. package/dist/adapters/db/db-flows.js +161 -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 +448 -119
  12. package/dist/adapters/gate-applications/gate-applications.d.ts +23 -0
  13. package/dist/adapters/gate-applications/gate-applications.js +66 -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/agent-runtime/agent-runtime.d.ts +16 -0
  18. package/dist/agent-runtime/agent-runtime.js +76 -0
  19. package/dist/agent-runtime/agent-runtime.types.d.ts +57 -0
  20. package/dist/bundle/bundle.d.ts +4 -0
  21. package/dist/bundle/bundle.js +1035 -0
  22. package/dist/bundle/bundle.types.d.ts +33 -0
  23. package/dist/bundle/bundle.types.js +1 -0
  24. package/dist/cli/cli.js +10 -1
  25. package/dist/flows/flows.d.ts +8 -8
  26. package/dist/flows/flows.js +158 -42
  27. package/dist/flows/flows.types.d.ts +40 -7
  28. package/dist/http/http.d.ts +1 -0
  29. package/dist/http/http.js +329 -63
  30. package/dist/http/http.types.d.ts +6 -2
  31. package/dist/indexing/indexing.js +14 -2
  32. package/dist/indexing/indexing.types.d.ts +2 -2
  33. package/dist/intel/intel.js +12 -3
  34. package/dist/intel/intel.types.d.ts +6 -2
  35. package/dist/mcp/mcp.js +483 -124
  36. package/dist/mcp/mcp.types.d.ts +11 -2
  37. package/dist/nodes/nodes.d.ts +2 -0
  38. package/dist/nodes/nodes.js +1337 -0
  39. package/dist/nodes/nodes.types.d.ts +314 -0
  40. package/dist/nodes/nodes.types.js +1 -0
  41. package/migrations/0011_one_name_for_the_tree.sql +53 -0
  42. package/migrations/0012_table_snapshots.sql +29 -0
  43. package/migrations/0013_agents_in_the_tree.sql +76 -0
  44. package/migrations/0014_agent_applications.sql +25 -0
  45. package/package.json +3 -2
  46. package/dist/knowledge/knowledge.d.ts +0 -2
  47. package/dist/knowledge/knowledge.js +0 -761
  48. package/dist/knowledge/knowledge.types.d.ts +0 -198
  49. /package/dist/{knowledge/knowledge.types.js → agent-runtime/agent-runtime.types.js} +0 -0
  50. /package/dist/{knowledge → nodes}/document-links/document-links.d.ts +0 -0
  51. /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, 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,42 +136,195 @@ 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 = [];
103
194
  url.searchParams.forEach((_value, key) => {
104
- if (key !== "parentId" && key !== "includeArchived")
195
+ if (key !== "parentId" && key !== "includeArchived" && key !== "archivedOnly") {
105
196
  unknownQuery.push(key);
197
+ }
106
198
  });
107
199
  if (unknownQuery.length) {
108
200
  throw new IntelError(400, "invalid_request", `Unknown query parameters: ${unknownQuery.join(", ")}`);
109
201
  }
110
- const input = ListKnowledgeNodesInput.parse({
202
+ const input = ListNodesInput.parse({
111
203
  parentId: url.searchParams.get("parentId"),
112
204
  includeArchived: url.searchParams.get("includeArchived") === "true",
205
+ ...(url.searchParams.get("archivedOnly") === "true" ? { archivedOnly: true } : {}),
113
206
  });
114
- return context.json(await deps.knowledge.list(asActor(auth), input));
207
+ return context.json(await deps.nodes.list(asActor(auth), input));
115
208
  });
116
- app.get("/knowledge/graph", async (context) => {
209
+ app.get("/nodes/graph", async (context) => {
117
210
  const auth = requireCapability(context, "knowledge", "read");
118
211
  const url = new URL(context.req.url);
119
- const input = KnowledgeGraphInput.parse({
212
+ const input = NodeGraphInput.parse({
120
213
  limit: url.searchParams.has("limit") ? Number(url.searchParams.get("limit")) : undefined,
121
214
  });
122
- return context.json(await deps.knowledge.graph(asActor(auth), input));
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")));
123
303
  });
124
- app.get("/knowledge/:nodeId", async (context) => {
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) => {
125
308
  const auth = requireCapability(context, "knowledge", "read");
126
- return context.json(await deps.knowledge.get(asActor(auth), context.req.param("nodeId")));
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));
127
314
  });
128
- app.get("/knowledge/:nodeId/versions", async (context) => {
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) => {
129
320
  const auth = requireCapability(context, "knowledge", "read");
130
- return context.json(await deps.knowledge.listVersions(asActor(auth), context.req.param("nodeId")));
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 });
131
324
  });
132
- app.get("/knowledge/:nodeId/attachment", async (context) => {
325
+ app.get("/nodes/:nodeId/attachment", async (context) => {
133
326
  const auth = requireCapability(context, "knowledge", "read");
134
- const { attachment, body } = await deps.knowledge.readAttachment(asActor(auth), context.req.param("nodeId"));
327
+ const { attachment, body } = await deps.nodes.readAttachment(asActor(auth), context.req.param("nodeId"));
135
328
  return new Response(body, {
136
329
  headers: {
137
330
  "cache-control": "private, no-store",
@@ -141,116 +334,146 @@ export function createHttp(deps) {
141
334
  },
142
335
  });
143
336
  });
144
- app.get("/knowledge/:nodeId/table", async (context) => {
337
+ app.get("/nodes/:nodeId/table", async (context) => {
145
338
  const auth = requireCapability(context, "knowledge", "read");
146
- return context.json(await deps.knowledge.getTable(asActor(auth), context.req.param("nodeId")));
339
+ return context.json(await deps.nodes.getTable(asActor(auth), context.req.param("nodeId")));
147
340
  });
148
341
  // Defining the header and appending rows are two routes because they are two decisions: the
149
342
  // header is written once and is the contract, an append is the everyday write (#40).
150
- app.post("/knowledge/:nodeId/table", async (context) => {
343
+ app.post("/nodes/:nodeId/table", async (context) => {
151
344
  const auth = requireCapability(context, "knowledge", "write");
152
- const input = DefineKnowledgeTableInput.parse(await context.req.json().catch(() => null));
345
+ const input = DefineTableInput.parse(await context.req.json().catch(() => null));
153
346
  if (input.nodeId !== context.req.param("nodeId")) {
154
347
  throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
155
348
  }
156
- return context.json(await deps.knowledge.defineTable(asActor(auth), input), 201);
349
+ return context.json(await deps.nodes.defineTable(asActor(auth), input), 201);
157
350
  });
158
- app.post("/knowledge/:nodeId/table/rows", async (context) => {
351
+ app.post("/nodes/:nodeId/table/rows", async (context) => {
159
352
  const auth = requireCapability(context, "knowledge", "write");
160
- const input = AppendKnowledgeTableRowsInput.parse(await context.req.json().catch(() => null));
353
+ const input = AppendTableRowsInput.parse(await context.req.json().catch(() => null));
161
354
  if (input.nodeId !== context.req.param("nodeId")) {
162
355
  throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
163
356
  }
164
- return context.json(await deps.knowledge.appendTableRows(asActor(auth), input), 201);
357
+ return context.json(await deps.nodes.appendTableRows(asActor(auth), input), 201);
165
358
  });
166
- app.get("/knowledge/:nodeId/links", async (context) => {
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) => {
167
388
  const auth = requireCapability(context, "knowledge", "read");
168
- return context.json(await deps.knowledge.listLinks(asActor(auth), context.req.param("nodeId")));
389
+ return context.json(await deps.nodes.listLinks(asActor(auth), context.req.param("nodeId")));
169
390
  });
170
391
  // A document link's name, for whoever is reading the text (#41). A POST because the list of IDs
171
392
  // is the request body — putting them in a query string would put the tree's identifiers into
172
- // 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
173
394
  // already see, and stays silent about the rest rather than saying that there is a rest.
174
395
  //
175
396
  // There is no route to create or delete a link any more. A relationship is written where it is
176
- // meant, in the text, and `POST /knowledge/:nodeId/versions` is what records it.
177
- app.post("/knowledge/links/resolve", async (context) => {
397
+ // meant, in the text, and `POST /nodes/:nodeId/versions` is what records it.
398
+ app.post("/nodes/links/resolve", async (context) => {
178
399
  const auth = requireCapability(context, "knowledge", "read");
179
- const input = ResolveKnowledgeLinksInput.parse(await context.req.json().catch(() => null));
180
- return context.json(await deps.knowledge.resolveLinks(asActor(auth), input));
400
+ const input = ResolveNodeLinksInput.parse(await context.req.json().catch(() => null));
401
+ return context.json(await deps.nodes.resolveLinks(asActor(auth), input));
181
402
  });
182
- app.post("/knowledge/search", async (context) => {
403
+ app.post("/nodes/search", async (context) => {
183
404
  const auth = requireCapability(context, "knowledge", "read");
184
- const input = SearchKnowledgeInput.parse(await context.req.json().catch(() => null));
185
- return context.json(await deps.knowledge.search(asActor(auth), input));
405
+ const input = SearchInput.parse(await context.req.json().catch(() => null));
406
+ return context.json(await deps.nodes.search(asActor(auth), input));
186
407
  });
187
408
  app.post("/search/reindex", async (context) => {
188
409
  const auth = requireCapability(context, "intel", "admin");
189
410
  z.strictObject({}).parse(await context.req.json().catch(() => null));
190
- return context.json(await deps.knowledge.reindex(asActor(auth)), 202);
411
+ return context.json(await deps.nodes.reindex(asActor(auth)), 202);
191
412
  });
192
- app.post("/knowledge", async (context) => {
413
+ app.post("/nodes", async (context) => {
193
414
  const auth = requireCapability(context, "knowledge", "create");
194
- const input = CreateKnowledgeNodeInput.parse(await context.req.json().catch(() => null));
195
- return context.json(await deps.knowledge.create(asActor(auth), input), 201);
415
+ const input = CreateNodeInput.parse(await context.req.json().catch(() => null));
416
+ return context.json(await deps.nodes.create(asActor(auth), input), 201);
196
417
  });
197
- app.post("/knowledge/:nodeId/versions", async (context) => {
418
+ app.post("/nodes/:nodeId/versions", async (context) => {
198
419
  const auth = requireCapability(context, "knowledge", "write");
199
- const input = SaveKnowledgeVersionInput.parse(await context.req.json().catch(() => null));
420
+ const input = SaveNodeVersionInput.parse(await context.req.json().catch(() => null));
200
421
  if (input.nodeId !== context.req.param("nodeId")) {
201
422
  throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
202
423
  }
203
- return context.json(await deps.knowledge.save(asActor(auth), input), 201);
424
+ return context.json(await deps.nodes.save(asActor(auth), input), 201);
204
425
  });
205
- app.post("/knowledge/:nodeId/attachment", async (context) => {
426
+ app.post("/nodes/:nodeId/attachment", async (context) => {
206
427
  const auth = requireCapability(context, "knowledge", "write");
207
- const input = SaveKnowledgeAttachmentInput.parse(await context.req.json().catch(() => null));
428
+ const input = SaveAttachmentInput.parse(await context.req.json().catch(() => null));
208
429
  if (input.nodeId !== context.req.param("nodeId")) {
209
430
  throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
210
431
  }
211
- return context.json(await deps.knowledge.saveAttachment(asActor(auth), input), 201);
432
+ return context.json(await deps.nodes.saveAttachment(asActor(auth), input), 201);
212
433
  });
213
- app.patch("/knowledge/:nodeId", async (context) => {
434
+ app.patch("/nodes/:nodeId", async (context) => {
214
435
  const auth = requireCapability(context, "knowledge", "write");
215
- const input = UpdateKnowledgeNodeInput.parse(await context.req.json().catch(() => null));
436
+ const input = UpdateNodeInput.parse(await context.req.json().catch(() => null));
216
437
  if (input.nodeId !== context.req.param("nodeId")) {
217
438
  throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
218
439
  }
219
- return context.json(await deps.knowledge.update(asActor(auth), input));
440
+ return context.json(await deps.nodes.update(asActor(auth), input));
220
441
  });
221
- app.post("/knowledge/:nodeId/archive", async (context) => {
442
+ app.post("/nodes/:nodeId/archive", async (context) => {
222
443
  const auth = requireCapability(context, "knowledge", "write");
223
- const input = ArchiveKnowledgeNodeInput.parse(await context.req.json().catch(() => null));
444
+ const input = ArchiveNodeInput.parse(await context.req.json().catch(() => null));
224
445
  if (input.nodeId !== context.req.param("nodeId")) {
225
446
  throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
226
447
  }
227
- return context.json(await deps.knowledge.archive(asActor(auth), input));
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") }));
228
451
  });
229
- app.get("/knowledge/:nodeId/grants", async (context) => {
452
+ app.get("/nodes/:nodeId/grants", async (context) => {
230
453
  const auth = requireCapability(context, "knowledge", "share");
231
- return context.json(await deps.knowledge.listGrants(asActor(auth), context.req.param("nodeId")));
454
+ return context.json(await deps.nodes.listGrants(asActor(auth), context.req.param("nodeId")));
232
455
  });
233
- app.post("/knowledge/:nodeId/grants", async (context) => {
456
+ app.post("/nodes/:nodeId/grants", async (context) => {
234
457
  const auth = requireCapability(context, "knowledge", "share");
235
- const input = ShareKnowledgeInput.parse(await context.req.json().catch(() => null));
458
+ const input = ShareInput.parse(await context.req.json().catch(() => null));
236
459
  if (input.resourceId !== context.req.param("nodeId")) {
237
460
  throw new IntelError(400, "node_id_mismatch", "Path and body resource IDs differ");
238
461
  }
239
- return context.json(await deps.knowledge.share(asActor(auth), input), 201);
462
+ return context.json(await deps.nodes.share(asActor(auth), input), 201);
240
463
  });
241
- app.post("/knowledge/:nodeId/grants/:grantId/revoke", async (context) => {
464
+ app.post("/nodes/:nodeId/grants/:grantId/revoke", async (context) => {
242
465
  const auth = requireCapability(context, "knowledge", "share");
243
- const input = RevokeKnowledgeGrantInput.parse(await context.req.json().catch(() => null));
466
+ const input = RevokeGrantInput.parse(await context.req.json().catch(() => null));
244
467
  if (input.resourceId !== context.req.param("nodeId") ||
245
468
  input.grantId !== context.req.param("grantId")) {
246
469
  throw new IntelError(400, "grant_id_mismatch", "Path and body grant IDs differ");
247
470
  }
248
- return context.json(await deps.knowledge.revokeGrant(asActor(auth), input));
471
+ return context.json(await deps.nodes.revokeGrant(asActor(auth), input));
249
472
  });
250
473
  app.get("/flows", async (context) => {
251
474
  const auth = requireCapability(context, "flows", "read");
252
475
  const url = new URL(context.req.url);
253
- requireKnownQuery(url, ["parentId", "includeArchived"]);
476
+ requireKnownQuery(url, ["parentId", "includeArchived", "archivedOnly"]);
254
477
  // Absent means "every flow I may see"; present but empty means the root of the shared tree.
255
478
  // Without that distinction a tree level and a full list would be the same request.
256
479
  const parentId = url.searchParams.get("parentId");
@@ -259,11 +482,12 @@ export function createHttp(deps) {
259
482
  const input = ListFlowsInput.parse({
260
483
  ...(url.searchParams.has("parentId") ? { parentId: parentId === "" ? null : parentId } : {}),
261
484
  ...(url.searchParams.get("includeArchived") === "true" ? { includeArchived: true } : {}),
485
+ ...(url.searchParams.get("archivedOnly") === "true" ? { archivedOnly: true } : {}),
262
486
  });
263
487
  return context.json(await deps.flows.list(asFlowActor(auth), input));
264
488
  });
265
489
  // ⚠️ Before `/flows/:flowId`, or the router would read "graph" as a flow ID. Same order and same
266
- // reason as `/knowledge/graph`.
490
+ // reason as `/nodes/graph`.
267
491
  app.get("/flows/graph", async (context) => {
268
492
  const auth = requireCapability(context, "flows", "read");
269
493
  const url = new URL(context.req.url);
@@ -284,6 +508,13 @@ export function createHttp(deps) {
284
508
  const input = GetFlowInput.parse({ flowId: context.req.param("flowId") });
285
509
  return context.json(await deps.flows.get(asFlowActor(auth), input.flowId));
286
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
+ });
287
518
  // What this flow calls, read out of its graph. The tree lists where a flow is filed; this lists
288
519
  // what it runs, and a shared subflow answers both questions at once (ADR-0004 §3).
289
520
  app.get("/flows/:flowId/calls", async (context) => {
@@ -308,7 +539,7 @@ export function createHttp(deps) {
308
539
  return context.json(await deps.flows.listRequirements(asFlowActor(auth), input.flowId));
309
540
  });
310
541
  // A flow has no grant route of its own. It is shared through the folder it is filed in, under
311
- // /knowledge/:nodeId/grants — one place answers the question for the documents and the flows in
542
+ // /nodes/:nodeId/grants — one place answers the question for the documents and the flows in
312
543
  // that folder alike (ADR-0004 §2).
313
544
  app.post("/flows", async (context) => {
314
545
  const auth = requireCapability(context, "flows", "create");
@@ -341,6 +572,22 @@ export function createHttp(deps) {
341
572
  }
342
573
  return context.json(await deps.flows.save(asFlowActor(auth), input), 201);
343
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
+ });
344
591
  // What publishing would freeze, before it is published (ADR-0004 §5). A GET because it reads and
345
592
  // changes nothing; the `publish` capability because it answers a question only a publisher has.
346
593
  app.get("/flows/:flowId/versions/:versionId/publish-preview", async (context) => {
@@ -359,6 +606,15 @@ export function createHttp(deps) {
359
606
  }
360
607
  return context.json(await deps.flows.publish(asFlowActor(auth), input));
361
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
+ });
362
618
  app.post("/flows/:flowId/runs", async (context) => {
363
619
  const auth = requireCapability(context, "flows", "run");
364
620
  const input = StartFlowRunInput.parse(await context.req.json().catch(() => null));
@@ -403,6 +659,16 @@ export function createHttp(deps) {
403
659
  }
404
660
  return context.json(await deps.flows.completeStep(asFlowActor(auth), input));
405
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
+ });
406
672
  app.get("/tools", async (context) => {
407
673
  const auth = requireCapability(context, "tools", "read");
408
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 { 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">;
@@ -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(`Knowledge version content is not a valid BlockNote document: ${error instanceof Error ? error.message : "unknown parse failure"}`);
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("Knowledge version content is missing");
50
+ throw new Error("Node version content is missing");
39
51
  }
40
52
  try {
41
53
  const text = typeof canonical === "string"