@anchrd/intel-api 0.6.7 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +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 +431 -118
  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 +324 -61
  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,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,132 @@ export function createHttp(deps) {
108
199
  if (unknownQuery.length) {
109
200
  throw new IntelError(400, "invalid_request", `Unknown query parameters: ${unknownQuery.join(", ")}`);
110
201
  }
111
- const input = 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));
216
+ });
217
+ // ⚠️ Before `/nodes/:nodeId`, or the router would read "agents" as a node ID. Same order and same
218
+ // reason as `/nodes/graph` above.
219
+ app.get("/nodes/agents", async (context) => {
220
+ const auth = requireCapability(context, "knowledge", "read");
221
+ const url = new URL(context.req.url);
222
+ requireKnownQuery(url, ["parentId", "includeArchived"]);
223
+ const parentId = url.searchParams.get("parentId");
224
+ // ⚠️ The key is only set when the caller sent it. Omitted means the whole tree and `null` means
225
+ // the root level, so defaulting a missing parameter to `null` here would silently answer a
226
+ // different question than the one asked.
227
+ const input = ListAgentsInput.parse({
228
+ ...(url.searchParams.has("parentId") ? { parentId: parentId === "" ? null : parentId } : {}),
229
+ includeArchived: url.searchParams.get("includeArchived") === "true",
230
+ });
231
+ return context.json(await deps.nodes.listAgents(asActor(auth), input));
232
+ });
233
+ /**
234
+ * ⚠️ The one response in Intel's HTTP surface that carries a credential (#182, D27).
235
+ *
236
+ * `applicationKey` is the Gate Application key of the new agent, in plain text, exactly once —
237
+ * Gate issues it once and keeps only a hash, and Intel writes it nowhere. It belongs in the agent
238
+ * runtime's `AGENT_APPLICATION_KEYS` secret; the answer says so rather than assuming the caller
239
+ * knows. The caller's own bearer is what creates the principal, so this needs `applications:write`
240
+ * in Gate on top of `knowledge:create` in Intel.
241
+ */
242
+ app.post("/nodes/agents", async (context) => {
243
+ const auth = requireCapability(context, "knowledge", "create");
244
+ const input = CreateAgentInput.parse(await context.req.json().catch(() => null));
245
+ return context.json(await deps.nodes.createAgent(asActor(auth), input, { token: context.get("token") }), 201);
246
+ });
247
+ // ⚠️ Before `/nodes/:nodeId`, or the router would read "export" as a node ID. The root has no
248
+ // node to address, so the whole installation — as this caller may read it — exports here (#136).
249
+ app.get("/nodes/export", async (context) => {
250
+ const auth = requireCapability(context, "knowledge", "read");
251
+ return zipResponse(await deps.bundle.exportSubtree(asBundleActor(auth), null));
252
+ });
253
+ // The reverse door (#137): a zip bundle — or a naked zipped folder — lands as a new subtree.
254
+ // The body IS the zip, streamed to the service rather than read whole here; `knowledge/create`
255
+ // like every other way of making nodes, and the resource-level write check is the service's.
256
+ async function importBundle(context, targetNodeId) {
257
+ const auth = requireCapability(context, "knowledge", "create");
258
+ const idempotencyKey = ImportIdempotencyKey.parse(context.req.header("idempotency-key"));
259
+ const body = context.req.raw.body;
260
+ if (body === null) {
261
+ throw new IntelError(400, "invalid_request", "A zip request body is required");
262
+ }
263
+ const result = await deps.bundle.importBundle(asBundleActor(auth), {
264
+ targetNodeId,
265
+ zip: body,
266
+ idempotencyKey,
267
+ });
268
+ // A replay answers 200: nothing was created by THIS request, and the summary is the earlier
269
+ // import's. 201 stays the word for rows that exist because of this call.
270
+ return context.json(result, result.replayed ? 200 : 201);
271
+ }
272
+ // ⚠️ Before `/nodes/:nodeId`, or the router would read "import" as a node ID.
273
+ app.post("/nodes/import", async (context) => await importBundle(context, null));
274
+ app.get("/nodes/:nodeId", async (context) => {
275
+ const auth = requireCapability(context, "knowledge", "read");
276
+ return context.json(await deps.nodes.get(asActor(auth), context.req.param("nodeId")));
277
+ });
278
+ app.post("/nodes/:nodeId/import", async (context) => {
279
+ const input = GetNodeInput.parse({ nodeId: context.req.param("nodeId") });
280
+ return await importBundle(context, input.nodeId);
281
+ });
282
+ // One subtree — a folder with everything beneath it, or a single node — as a zip bundle (#136).
283
+ app.get("/nodes/:nodeId/export", async (context) => {
284
+ const auth = requireCapability(context, "knowledge", "read");
285
+ const input = GetNodeInput.parse({ nodeId: context.req.param("nodeId") });
286
+ return zipResponse(await deps.bundle.exportSubtree(asBundleActor(auth), input.nodeId));
287
+ });
288
+ app.get("/nodes/:nodeId/agent", async (context) => {
289
+ const auth = requireCapability(context, "knowledge", "read");
290
+ return context.json(await deps.nodes.getAgent(asActor(auth), { nodeId: context.req.param("nodeId") }));
291
+ });
292
+ app.post("/nodes/:nodeId/agent", async (context) => {
293
+ const auth = requireCapability(context, "knowledge", "write");
294
+ const input = SaveAgentDefinitionInput.parse(await context.req.json().catch(() => null));
295
+ if (input.nodeId !== context.req.param("nodeId")) {
296
+ throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
297
+ }
298
+ return context.json(await deps.nodes.saveAgentDefinition(asActor(auth), input), 201);
299
+ });
300
+ app.get("/nodes/:nodeId/versions", async (context) => {
301
+ const auth = requireCapability(context, "knowledge", "read");
302
+ return context.json(await deps.nodes.listVersions(asActor(auth), context.req.param("nodeId")));
125
303
  });
126
- 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) => {
127
308
  const auth = requireCapability(context, "knowledge", "read");
128
- 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));
129
314
  });
130
- 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) => {
131
320
  const auth = requireCapability(context, "knowledge", "read");
132
- 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 });
133
324
  });
134
- app.get("/knowledge/:nodeId/attachment", async (context) => {
325
+ app.get("/nodes/:nodeId/attachment", async (context) => {
135
326
  const auth = requireCapability(context, "knowledge", "read");
136
- 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"));
137
328
  return new Response(body, {
138
329
  headers: {
139
330
  "cache-control": "private, no-store",
@@ -143,111 +334,141 @@ export function createHttp(deps) {
143
334
  },
144
335
  });
145
336
  });
146
- app.get("/knowledge/:nodeId/table", async (context) => {
337
+ app.get("/nodes/:nodeId/table", async (context) => {
147
338
  const auth = requireCapability(context, "knowledge", "read");
148
- 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")));
149
340
  });
150
341
  // Defining the header and appending rows are two routes because they are two decisions: the
151
342
  // header is written once and is the contract, an append is the everyday write (#40).
152
- app.post("/knowledge/:nodeId/table", async (context) => {
343
+ app.post("/nodes/:nodeId/table", async (context) => {
153
344
  const auth = requireCapability(context, "knowledge", "write");
154
- const input = DefineKnowledgeTableInput.parse(await context.req.json().catch(() => null));
345
+ const input = DefineTableInput.parse(await context.req.json().catch(() => null));
155
346
  if (input.nodeId !== context.req.param("nodeId")) {
156
347
  throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
157
348
  }
158
- return context.json(await deps.knowledge.defineTable(asActor(auth), input), 201);
349
+ return context.json(await deps.nodes.defineTable(asActor(auth), input), 201);
159
350
  });
160
- app.post("/knowledge/:nodeId/table/rows", async (context) => {
351
+ app.post("/nodes/:nodeId/table/rows", async (context) => {
161
352
  const auth = requireCapability(context, "knowledge", "write");
162
- const input = AppendKnowledgeTableRowsInput.parse(await context.req.json().catch(() => null));
353
+ const input = AppendTableRowsInput.parse(await context.req.json().catch(() => null));
163
354
  if (input.nodeId !== context.req.param("nodeId")) {
164
355
  throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
165
356
  }
166
- return context.json(await deps.knowledge.appendTableRows(asActor(auth), input), 201);
357
+ return context.json(await deps.nodes.appendTableRows(asActor(auth), input), 201);
167
358
  });
168
- 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) => {
169
388
  const auth = requireCapability(context, "knowledge", "read");
170
- 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")));
171
390
  });
172
391
  // A document link's name, for whoever is reading the text (#41). A POST because the list of IDs
173
392
  // is the request body — putting them in a query string would put the tree's identifiers into
174
- // logs and referrers. `knowledge/read` and nothing more: it answers with what this reader may
393
+ // logs and referrers. The `knowledge/read` capability and nothing more: it answers with what this reader may
175
394
  // already see, and stays silent about the rest rather than saying that there is a rest.
176
395
  //
177
396
  // There is no route to create or delete a link any more. A relationship is written where it is
178
- // meant, in the text, and `POST /knowledge/:nodeId/versions` is what records it.
179
- 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) => {
180
399
  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));
400
+ const input = ResolveNodeLinksInput.parse(await context.req.json().catch(() => null));
401
+ return context.json(await deps.nodes.resolveLinks(asActor(auth), input));
183
402
  });
184
- app.post("/knowledge/search", async (context) => {
403
+ app.post("/nodes/search", async (context) => {
185
404
  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));
405
+ const input = SearchInput.parse(await context.req.json().catch(() => null));
406
+ return context.json(await deps.nodes.search(asActor(auth), input));
188
407
  });
189
408
  app.post("/search/reindex", async (context) => {
190
409
  const auth = requireCapability(context, "intel", "admin");
191
410
  z.strictObject({}).parse(await context.req.json().catch(() => null));
192
- return context.json(await deps.knowledge.reindex(asActor(auth)), 202);
411
+ return context.json(await deps.nodes.reindex(asActor(auth)), 202);
193
412
  });
194
- app.post("/knowledge", async (context) => {
413
+ app.post("/nodes", async (context) => {
195
414
  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);
415
+ const input = CreateNodeInput.parse(await context.req.json().catch(() => null));
416
+ return context.json(await deps.nodes.create(asActor(auth), input), 201);
198
417
  });
199
- app.post("/knowledge/:nodeId/versions", async (context) => {
418
+ app.post("/nodes/:nodeId/versions", async (context) => {
200
419
  const auth = requireCapability(context, "knowledge", "write");
201
- const input = SaveKnowledgeVersionInput.parse(await context.req.json().catch(() => null));
420
+ const input = SaveNodeVersionInput.parse(await context.req.json().catch(() => null));
202
421
  if (input.nodeId !== context.req.param("nodeId")) {
203
422
  throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
204
423
  }
205
- return context.json(await deps.knowledge.save(asActor(auth), input), 201);
424
+ return context.json(await deps.nodes.save(asActor(auth), input), 201);
206
425
  });
207
- app.post("/knowledge/:nodeId/attachment", async (context) => {
426
+ app.post("/nodes/:nodeId/attachment", async (context) => {
208
427
  const auth = requireCapability(context, "knowledge", "write");
209
- const input = SaveKnowledgeAttachmentInput.parse(await context.req.json().catch(() => null));
428
+ const input = SaveAttachmentInput.parse(await context.req.json().catch(() => null));
210
429
  if (input.nodeId !== context.req.param("nodeId")) {
211
430
  throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
212
431
  }
213
- return context.json(await deps.knowledge.saveAttachment(asActor(auth), input), 201);
432
+ return context.json(await deps.nodes.saveAttachment(asActor(auth), input), 201);
214
433
  });
215
- app.patch("/knowledge/:nodeId", async (context) => {
434
+ app.patch("/nodes/:nodeId", async (context) => {
216
435
  const auth = requireCapability(context, "knowledge", "write");
217
- const input = UpdateKnowledgeNodeInput.parse(await context.req.json().catch(() => null));
436
+ const input = UpdateNodeInput.parse(await context.req.json().catch(() => null));
218
437
  if (input.nodeId !== context.req.param("nodeId")) {
219
438
  throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
220
439
  }
221
- return context.json(await deps.knowledge.update(asActor(auth), input));
440
+ return context.json(await deps.nodes.update(asActor(auth), input));
222
441
  });
223
- app.post("/knowledge/:nodeId/archive", async (context) => {
442
+ app.post("/nodes/:nodeId/archive", async (context) => {
224
443
  const auth = requireCapability(context, "knowledge", "write");
225
- const input = ArchiveKnowledgeNodeInput.parse(await context.req.json().catch(() => null));
444
+ const input = ArchiveNodeInput.parse(await context.req.json().catch(() => null));
226
445
  if (input.nodeId !== context.req.param("nodeId")) {
227
446
  throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
228
447
  }
229
- 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") }));
230
451
  });
231
- app.get("/knowledge/:nodeId/grants", async (context) => {
452
+ app.get("/nodes/:nodeId/grants", async (context) => {
232
453
  const auth = requireCapability(context, "knowledge", "share");
233
- 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")));
234
455
  });
235
- app.post("/knowledge/:nodeId/grants", async (context) => {
456
+ app.post("/nodes/:nodeId/grants", async (context) => {
236
457
  const auth = requireCapability(context, "knowledge", "share");
237
- const input = ShareKnowledgeInput.parse(await context.req.json().catch(() => null));
458
+ const input = ShareInput.parse(await context.req.json().catch(() => null));
238
459
  if (input.resourceId !== context.req.param("nodeId")) {
239
460
  throw new IntelError(400, "node_id_mismatch", "Path and body resource IDs differ");
240
461
  }
241
- return context.json(await deps.knowledge.share(asActor(auth), input), 201);
462
+ return context.json(await deps.nodes.share(asActor(auth), input), 201);
242
463
  });
243
- app.post("/knowledge/:nodeId/grants/:grantId/revoke", async (context) => {
464
+ app.post("/nodes/:nodeId/grants/:grantId/revoke", async (context) => {
244
465
  const auth = requireCapability(context, "knowledge", "share");
245
- const input = RevokeKnowledgeGrantInput.parse(await context.req.json().catch(() => null));
466
+ const input = RevokeGrantInput.parse(await context.req.json().catch(() => null));
246
467
  if (input.resourceId !== context.req.param("nodeId") ||
247
468
  input.grantId !== context.req.param("grantId")) {
248
469
  throw new IntelError(400, "grant_id_mismatch", "Path and body grant IDs differ");
249
470
  }
250
- return context.json(await deps.knowledge.revokeGrant(asActor(auth), input));
471
+ return context.json(await deps.nodes.revokeGrant(asActor(auth), input));
251
472
  });
252
473
  app.get("/flows", async (context) => {
253
474
  const auth = requireCapability(context, "flows", "read");
@@ -266,7 +487,7 @@ export function createHttp(deps) {
266
487
  return context.json(await deps.flows.list(asFlowActor(auth), input));
267
488
  });
268
489
  // ⚠️ Before `/flows/:flowId`, or the router would read "graph" as a flow ID. Same order and same
269
- // reason as `/knowledge/graph`.
490
+ // reason as `/nodes/graph`.
270
491
  app.get("/flows/graph", async (context) => {
271
492
  const auth = requireCapability(context, "flows", "read");
272
493
  const url = new URL(context.req.url);
@@ -287,6 +508,13 @@ export function createHttp(deps) {
287
508
  const input = GetFlowInput.parse({ flowId: context.req.param("flowId") });
288
509
  return context.json(await deps.flows.get(asFlowActor(auth), input.flowId));
289
510
  });
511
+ // A single flow as the same bundle shape a node export makes: one entry plus the manifest
512
+ // (#136). `flows/read` like every other read of the graph.
513
+ app.get("/flows/:flowId/export", async (context) => {
514
+ const auth = requireCapability(context, "flows", "read");
515
+ const input = GetFlowInput.parse({ flowId: context.req.param("flowId") });
516
+ return zipResponse(await deps.bundle.exportFlow(asBundleActor(auth), input.flowId));
517
+ });
290
518
  // What this flow calls, read out of its graph. The tree lists where a flow is filed; this lists
291
519
  // what it runs, and a shared subflow answers both questions at once (ADR-0004 §3).
292
520
  app.get("/flows/:flowId/calls", async (context) => {
@@ -311,7 +539,7 @@ export function createHttp(deps) {
311
539
  return context.json(await deps.flows.listRequirements(asFlowActor(auth), input.flowId));
312
540
  });
313
541
  // A flow has no grant route of its own. It is shared through the folder it is filed in, under
314
- // /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
315
543
  // that folder alike (ADR-0004 §2).
316
544
  app.post("/flows", async (context) => {
317
545
  const auth = requireCapability(context, "flows", "create");
@@ -344,6 +572,22 @@ export function createHttp(deps) {
344
572
  }
345
573
  return context.json(await deps.flows.save(asFlowActor(auth), input), 201);
346
574
  });
575
+ // The GET beside `POST /flows/:flowId/versions`: the same collection, read instead of appended
576
+ // to (#144). `flows/read` like `GET /flows/:flowId` — the history describes a flow the caller
577
+ // may already open.
578
+ app.get("/flows/:flowId/versions", async (context) => {
579
+ const auth = requireCapability(context, "flows", "read");
580
+ const input = GetFlowInput.parse({ flowId: context.req.param("flowId") });
581
+ return context.json(await deps.flows.listVersions(asFlowActor(auth), input.flowId));
582
+ });
583
+ app.get("/flows/:flowId/versions/:versionId", async (context) => {
584
+ const auth = requireCapability(context, "flows", "read");
585
+ const input = GetFlowVersionInput.parse({
586
+ flowId: context.req.param("flowId"),
587
+ versionId: context.req.param("versionId"),
588
+ });
589
+ return context.json(await deps.flows.getVersion(asFlowActor(auth), input));
590
+ });
347
591
  // What publishing would freeze, before it is published (ADR-0004 §5). A GET because it reads and
348
592
  // changes nothing; the `publish` capability because it answers a question only a publisher has.
349
593
  app.get("/flows/:flowId/versions/:versionId/publish-preview", async (context) => {
@@ -362,6 +606,15 @@ export function createHttp(deps) {
362
606
  }
363
607
  return context.json(await deps.flows.publish(asFlowActor(auth), input));
364
608
  });
609
+ // The reverse of publish, behind the same capability: whoever may publish may withdraw (#146).
610
+ app.post("/flows/:flowId/unpublish", async (context) => {
611
+ const auth = requireCapability(context, "flows", "publish");
612
+ const input = UnpublishFlowInput.parse(await context.req.json().catch(() => null));
613
+ if (input.flowId !== context.req.param("flowId")) {
614
+ throw new IntelError(400, "flow_id_mismatch", "Path and body flow IDs differ");
615
+ }
616
+ return context.json(await deps.flows.unpublish(asFlowActor(auth), input));
617
+ });
365
618
  app.post("/flows/:flowId/runs", async (context) => {
366
619
  const auth = requireCapability(context, "flows", "run");
367
620
  const input = StartFlowRunInput.parse(await context.req.json().catch(() => null));
@@ -406,6 +659,16 @@ export function createHttp(deps) {
406
659
  }
407
660
  return context.json(await deps.flows.completeStep(asFlowActor(auth), input));
408
661
  });
662
+ // `flows/run`, the same capability the start asked for: whoever may start a run may end it
663
+ // (#145). The resource-level `execute` is the service's question, asked against the flow.
664
+ app.post("/flow-runs/:runId/cancel", async (context) => {
665
+ const auth = requireCapability(context, "flows", "run");
666
+ const input = CancelFlowRunInput.parse(await context.req.json().catch(() => null));
667
+ if (input.runId !== context.req.param("runId")) {
668
+ throw new IntelError(400, "run_id_mismatch", "Path and body run IDs differ");
669
+ }
670
+ return context.json(await deps.flows.cancelRun(asFlowActor(auth), input));
671
+ });
409
672
  app.get("/tools", async (context) => {
410
673
  const auth = requireCapability(context, "tools", "read");
411
674
  return context.json(await deps.tools.catalog(asToolActor(auth)));
@@ -1,13 +1,17 @@
1
1
  import type { GateClient } from "@anchrd/gate-sdk";
2
+ import type { AgentRuntimeService } from "../agent-runtime/agent-runtime.types.js";
2
3
  import type { BrowserAuth } from "../auth/auth.types.js";
4
+ import type { BundleService } from "../bundle/bundle.types.js";
3
5
  import type { FlowService } from "../flows/flows.types.js";
4
- import type { 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"
@@ -1,8 +1,8 @@
1
1
  import type { DocumentConverter } from "../adapters/document-converter/document-converter.types.js";
2
2
  import type { SemanticIndex } from "../adapters/semantic-index/semantic-index.types.js";
3
- import type { ContentStore, KnowledgeIndexRepository } from "../knowledge/knowledge.types.js";
3
+ import type { ContentStore, NodeIndexRepository } from "../nodes/nodes.types.js";
4
4
  export interface IndexingDeps {
5
- repository: KnowledgeIndexRepository;
5
+ repository: NodeIndexRepository;
6
6
  content: ContentStore;
7
7
  semantic?: SemanticIndex | undefined;
8
8
  converter?: DocumentConverter | undefined;