@anchrd/intel-api 0.13.0 → 0.15.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 (57) hide show
  1. package/dist/adapters/cloudflare/cloudflare.js +1 -68
  2. package/dist/adapters/cloudflare/cloudflare.types.d.ts +0 -39
  3. package/dist/adapters/db/db-flows.js +1 -1
  4. package/dist/adapters/db/db-grants.js +1 -1
  5. package/dist/adapters/db/db-indexing.js +79 -0
  6. package/dist/adapters/db/db.js +81 -139
  7. package/dist/adapters/semantic-index/semantic-index.js +97 -17
  8. package/dist/adapters/semantic-index/semantic-index.types.d.ts +20 -1
  9. package/dist/bundle/bundle.js +42 -134
  10. package/dist/cli/cli.js +3 -9
  11. package/dist/http/http.js +5 -206
  12. package/dist/http/http.types.d.ts +0 -8
  13. package/dist/indexing/indexing.js +133 -55
  14. package/dist/indexing/indexing.types.d.ts +1 -0
  15. package/dist/intel/intel.js +4 -9
  16. package/dist/intel/intel.types.d.ts +0 -6
  17. package/dist/mcp/mcp.js +33 -308
  18. package/dist/mcp/mcp.types.d.ts +2 -7
  19. package/dist/nodes/document-links/document-links.d.ts +6 -8
  20. package/dist/nodes/document-links/document-links.js +8 -31
  21. package/dist/nodes/nodes.js +92 -826
  22. package/dist/nodes/nodes.types.d.ts +57 -158
  23. package/dist/tools/tools.js +37 -148
  24. package/dist/tools/tools.types.d.ts +0 -21
  25. package/migrations/0009_no_context_policy.sql +15 -0
  26. package/migrations/0017_a_vector_per_card.sql +38 -0
  27. package/migrations/0018_no_context_policy_at_last.sql +97 -0
  28. package/migrations/0019_one_name_for_the_grants.sql +52 -0
  29. package/package.json +2 -2
  30. package/dist/adapters/cloudflare-api/cloudflare-api.d.ts +0 -22
  31. package/dist/adapters/cloudflare-api/cloudflare-api.js +0 -214
  32. package/dist/adapters/cloudflare-api/cloudflare-api.types.d.ts +0 -64
  33. package/dist/adapters/cloudflare-api/cloudflare-api.types.js +0 -1
  34. package/dist/adapters/gate-applications/gate-applications.d.ts +0 -23
  35. package/dist/adapters/gate-applications/gate-applications.js +0 -88
  36. package/dist/adapters/tool-delegation/tool-delegation.d.ts +0 -22
  37. package/dist/adapters/tool-delegation/tool-delegation.js +0 -90
  38. package/dist/agent-costs/agent-costs.d.ts +0 -16
  39. package/dist/agent-costs/agent-costs.js +0 -105
  40. package/dist/agent-costs/agent-costs.types.d.ts +0 -30
  41. package/dist/agent-costs/agent-costs.types.js +0 -1
  42. package/dist/agent-runtime/agent-runtime.d.ts +0 -16
  43. package/dist/agent-runtime/agent-runtime.js +0 -150
  44. package/dist/agent-runtime/agent-runtime.types.d.ts +0 -122
  45. package/dist/agent-runtime/agent-runtime.types.js +0 -1
  46. package/dist/model-catalog/model-catalog.d.ts +0 -2
  47. package/dist/model-catalog/model-catalog.js +0 -99
  48. package/dist/model-catalog/model-catalog.types.d.ts +0 -15
  49. package/dist/model-catalog/model-catalog.types.js +0 -1
  50. package/dist/nodes/board/board.d.ts +0 -59
  51. package/dist/nodes/board/board.js +0 -528
  52. package/dist/nodes/board/board.types.d.ts +0 -31
  53. package/dist/nodes/board/board.types.js +0 -1
  54. package/migrations/0013_agents_in_the_tree.sql +0 -76
  55. package/migrations/0014_agent_applications.sql +0 -25
  56. package/migrations/0015_tools_delegated_from_a_connection.sql +0 -15
  57. package/migrations/0016_boards_in_the_tree.sql +0 -80
@@ -1,6 +1,5 @@
1
- import { AgentDefinition, AgentMediaType, BlockNoteMediaType, BoardDocument, BoardMediaType, BundleImportResult, BundleManifest, BundleManifestFilename, DocumentLinkInlineType, FlowGraph, TableMediaType, } from "@anchrd/intel-contract";
1
+ import { BlockNoteMediaType, BundleImportResult, BundleManifest, BundleManifestFilename, DocumentLinkInlineType, FlowGraph, TableMediaType, } from "@anchrd/intel-contract";
2
2
  import { Unzip, UnzipInflate, Zip, ZipDeflate, ZipPassThrough } from "fflate";
3
- import { repeatedBoardId, upgradeStoredBoard } from "../nodes/board/board.js";
4
3
  import { documentLinkTargets } from "../nodes/document-links/document-links.js";
5
4
  import { parseCsv } from "../shared/csv/csv.js";
6
5
  import { IntelError } from "../shared/intel-error/intel-error.js";
@@ -217,13 +216,11 @@ function nakedKindOf(name) {
217
216
  if (lower.endsWith(".csv")) {
218
217
  return { kind: "table", mediaType: TableMediaType, title: name.slice(0, -4) };
219
218
  }
220
- // ⚠️ Before any plain `.json` rule and with a compound suffix on purpose (#285). A board is not
221
- // recognisable from `.json` alone an agent definition wears the same extension — so the export
222
- // writes `.board.json` and a naked upload has to spell it out too. A file that only says `.json`
223
- // is an attachment, which is what it was before boards existed.
224
- if (lower.endsWith(".board.json")) {
225
- return { kind: "board", mediaType: BoardMediaType, title: name.slice(0, -11) };
226
- }
219
+ // ⚠️ `.board.json` had its own rule here until #390 and deliberately does not any more. This is
220
+ // the NAKED-folder patha directory somebody picked, with no manifest — so a file called
221
+ // `plan.board.json` is a JSON file on a disk, and keeping it as an attachment preserves it. A
222
+ // real export is a different matter: its manifest declares the kind, and that is where such an
223
+ // entry is refused by name.
227
224
  return { kind: "attachment", mediaType: guessedMediaType(lower), title: name };
228
225
  }
229
226
  const KnownMediaTypes = {
@@ -402,41 +399,11 @@ export function createBundle(deps) {
402
399
  },
403
400
  };
404
401
  }
405
- // A board is one JSON file, exactly like an agent's definition: the document IS the board, so
406
- // there is nothing to serialize differently for an export (#285). A board with no version yet
407
- // exports as an empty file, the same way an agent without a definition does.
408
- if (node.kind === "board") {
409
- const path = `${directory}${uniqueName(used, base, ".board.json")}`;
410
- return {
411
- manifest: {
412
- id: node.id,
413
- kind: "board",
414
- title: node.title,
415
- description: node.description,
416
- mediaType: version?.mediaType ?? BoardMediaType,
417
- path,
418
- },
419
- content: version === null
420
- ? { type: "text", load: async () => "" }
421
- : { type: "text", load: textLoader(version.contentKey) },
422
- };
423
- }
424
- if (node.kind === "agent") {
425
- const path = `${directory}${uniqueName(used, base, ".json")}`;
426
- return {
427
- manifest: {
428
- id: node.id,
429
- kind: "agent",
430
- title: node.title,
431
- description: node.description,
432
- mediaType: version?.mediaType ?? "application/json",
433
- path,
434
- },
435
- content: version === null
436
- ? { type: "text", load: async () => "" }
437
- : { type: "text", load: textLoader(version.contentKey) },
438
- };
439
- }
402
+ // ⚠️ A row of a kind this build no longer makes — an `agent` or a `board` written before #390
403
+ // falls through to the document branch and travels as its raw body. Deliberately forgiving:
404
+ // refusing here would make one leftover row enough to stop a whole subtree from being exported,
405
+ // and the file it produces is exactly the bytes that were stored. The IMPORT is where such a
406
+ // bundle is turned away, by name.
440
407
  // A document: markdown stays `.md`, a BlockNote body is JSON and says so with its name.
441
408
  const mediaType = version?.mediaType ?? "text/markdown";
442
409
  const extension = mediaType === BlockNoteMediaType ? ".json" : ".md";
@@ -926,101 +893,42 @@ export function createBundle(deps) {
926
893
  if (header.length === 0 || header.every((column) => column.trim().length === 0)) {
927
894
  throw new IntelError(400, "import_invalid_table", `Bundle entry is not a table with a header row: ${entry.path}`);
928
895
  }
896
+ // ⚠️ Distinct column names, read the same way `DefineTableInput` and
897
+ // `RedefineTableInput` read them — case-insensitively (#322). Both refuse a duplicate,
898
+ // and the import used to walk past both: a table could only enter the tree this way
899
+ // with two columns of one name.
900
+ //
901
+ // The consumer assumes the opposite and says so. `packages/ui/src/node-table` keys its
902
+ // header cells and every row cell with the column name and carries the comment
903
+ // "Column names are distinct by contract (`DefineTableInput`)" — true for the two write
904
+ // paths, and false for this one. Two equal names are two React keys with one value, in
905
+ // the header AND in every row.
906
+ const seen = new Set(header.map((column) => column.trim().toLowerCase()));
907
+ if (seen.size !== header.length) {
908
+ throw new IntelError(400, "import_invalid_table", `Bundle entry has two columns of the same name: ${entry.path}`);
909
+ }
929
910
  // The whole file as one snapshot: the same shape a definition or a redefine writes
930
911
  // (#135), so reading starts here and nothing older is expected to exist.
931
912
  version = versionRowFor(entry, text, TableMediaType, await deps.hash(text), "snapshot");
932
913
  }
933
914
  }
934
- else if (entry.kind === "board") {
935
- const text = entry.body === null ? "" : decodeText(entry.body, entry.path);
936
- if (text.trim().length > 0) {
937
- let document;
938
- try {
939
- // Same upgrade the stored read does (anchrd/intel#311): a bundle exported before
940
- // `terminal` existed is exactly an old board in a file, and importing it must not be
941
- // the one way to lose a board.
942
- document = BoardDocument.parse(upgradeStoredBoard(JSON.parse(text)));
943
- }
944
- catch {
945
- throw new IntelError(400, "import_invalid_board", `Bundle entry is not a board: ${entry.path}`);
946
- }
947
- /**
948
- * ⚠️ Refused by name, and refused rather than folded (anchrd/intel#321).
949
- *
950
- * A board arriving in a file is the ONE place a document somebody else wrote becomes
951
- * truth here — everywhere else a task id is minted by `deps.id()` and a status list
952
- * goes through `ConfigureBoardInput`, which has demanded distinct ids since #285. Two
953
- * tasks under one id threw `graph.addNode` out of the board's graph view, and two
954
- * statuses under one id drew the same cards in two lanes with a drop that had two
955
- * targets meaning one.
956
- *
957
- * Outside the catch above on purpose: swallowed into `import_invalid_board` this would
958
- * read as "that file is not a board", and whoever is holding the file would look for
959
- * the wrong thing. `repeatedBoardId` names the id, which is the line they have to fix.
960
- */
961
- const repeated = repeatedBoardId(document);
962
- if (repeated !== null) {
963
- throw new IntelError(400, "import_duplicate_board_id", repeated.list === "tasks"
964
- ? `Bundle entry names the task “${repeated.id}” twice: ${entry.path}`
965
- : `Bundle entry names the status “${repeated.id}” twice: ${entry.path}`);
966
- }
967
- // ⚠️ `references` are remapped the way a document's inline links are, and `dependsOn`
968
- // and `parentId` are NOT (#285). A task's references point at NODES, which the import
969
- // renumbers; its dependencies and its parent point at tasks inside this very file,
970
- // whose ids the import does not touch. Renumbering those would break every edge in the
971
- // board for no reason.
972
- const remapped = {
973
- statuses: document.statuses,
974
- tasks: document.tasks.map((task) => ({
975
- ...task,
976
- references: task.references.map((reference) => idMap.get(reference) ?? reference),
977
- })),
978
- };
979
- const body = JSON.stringify(remapped);
980
- version = versionRowFor(entry, body, BoardMediaType, await deps.hash(body), null);
981
- // Into the same link pass as an imported document: a board's references are `text`
982
- // links, so they have to survive the import the same way.
983
- documentBodies.set(entry.newId, { mediaType: BoardMediaType, content: body });
984
- }
985
- }
986
- else if (entry.kind === "agent") {
987
- const text = entry.body === null ? "" : decodeText(entry.body, entry.path);
988
- if (text.trim().length > 0) {
989
- let definition;
990
- try {
991
- definition = AgentDefinition.parse(JSON.parse(text));
992
- }
993
- catch {
994
- throw new IntelError(400, "import_invalid_agent", `Bundle entry is not an agent definition: ${entry.path}`);
995
- }
996
- const remapped = {
997
- ...definition,
998
- /**
999
- * ⚠️ A delegation NEVER travels in a file (#208, D30). `tools.delegatedBy` names
1000
- * whose portal connection an agent acts on, and the only thing that may write it is
1001
- * a person saving the agent in a session Intel authorized — the save path proves
1002
- * they reach every named server first. A bundle is a document somebody hands over;
1003
- * carrying the field through would let a crafted file name a local user and hand an
1004
- * imported agent that person's connection, with no act of theirs anywhere.
1005
- *
1006
- * The consequence is deliberate and mild: an imported agent arrives with no tools,
1007
- * and whoever wants it to have some gives them in the profile — which is the same
1008
- * act D30 made the only way in.
1009
- */
1010
- tools: null,
1011
- references: definition.references.map((reference) => idMap.has(reference.nodeId)
1012
- ? { ...reference, nodeId: idMap.get(reference.nodeId) }
1013
- : reference),
1014
- schedules: definition.schedules.map((schedule) => idMap.has(schedule.target.id)
1015
- ? {
1016
- ...schedule,
1017
- target: { ...schedule.target, id: idMap.get(schedule.target.id) },
1018
- }
1019
- : schedule),
1020
- };
1021
- const body = JSON.stringify(remapped);
1022
- version = versionRowFor(entry, body, AgentMediaType, await deps.hash(body), null);
1023
- }
915
+ else if (entry.kind === "board" || entry.kind === "agent") {
916
+ /**
917
+ * ⚠️ Refused by name, and the name says WHY rather than what the parser tripped over.
918
+ *
919
+ * Agents and Boards were parked with #385; nothing in this build can hold either kind.
920
+ * A bundle that carries one is not malformed — it is a correct export of an installation
921
+ * that still had the feature, and the person holding it is usually moving a tree between
922
+ * two installations rather than the author of the file. Left to the schema they would get
923
+ * `unexpected enum value`, look for a broken file, and find nothing wrong with it.
924
+ *
925
+ * The refusal names the entry, the kind and the branch the feature is parked on, so the
926
+ * answer to "where did my board go" is in the message and not in somebody's memory.
927
+ *
928
+ * ⚠️ Only a MANIFEST can reach this, because only a manifest declares a kind. A naked
929
+ * folder holding a `plan.board.json` imports it as an attachment — see `kindOfFile`.
930
+ */
931
+ throw new IntelError(400, "import_kind_parked", `Bundle entry “${entry.path}” is a ${entry.kind}, and ${entry.kind}s are not part of this installation any more (anchrd/intel#385). The bundle was exported from a version that still had them; the code is on the branch parked/agents-and-board. Remove the entry from the bundle to import the rest.`);
1024
932
  }
1025
933
  else if (entry.kind === "attachment") {
1026
934
  const bytes = entry.body ?? new Uint8Array(0);
package/dist/cli/cli.js CHANGED
@@ -12,15 +12,9 @@ const interfaces = [
12
12
  functions: ["read", "create", "write", "publish", "run", "share"],
13
13
  },
14
14
  { handle: "tools", functions: ["read", "test", "execute", "admin"] },
15
- // Declared here, and not by a service of its own, because Gate scopes the rules it returns to the
16
- // service whose key asked (least privilege). The agent runtime authorizes its callers with this
17
- // installation's Gate service key against the same audience Intel uses, so a handle registered
18
- // anywhere else would never appear in the rules that `can("agents", "run")` reads.
19
- //
20
- // Without it there is no state between "administrator" and "not at all": the runtime falls back to
21
- // `intel:admin`, which is the first-grant exit in the same sense as everywhere else, not the way a
22
- // team lets a colleague talk to an agent.
23
- { handle: "agents", functions: ["run"] },
15
+ // ⚠️ `agents:run` is deliberately NOT here any more (#390). `bootstrap` declares, it never
16
+ // revokes, so an installation that has the interface keeps it as a harmless leftover — but a new
17
+ // one is not asked to decide about a permission nothing reads.
24
18
  // `mcp:connect` means the same thing at every MCP service of this installation. A separate name
25
19
  // for the same thing forces the operator to check, service by service, which permission carries
26
20
  // portal access.
package/dist/http/http.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AddBoardTaskInput, AppendTableRowsInput, ArchiveFlowInput, ArchiveNodeInput, CancelFlowRunInput, CompleteFlowRunStepInput, ConfigureBoardInput, CreateAgentInput, CreateFlowInput, CreateNodeInput, DefineTableInput, DeleteBoardTaskInput, DeleteTableRowsInput, ExecuteToolInput, GetFlowInput, GetFlowRunInput, GetFlowVersionInput, GetNodeInput, GetNodeVersionInput, ListAgentsInput, ListFlowRunsInput, ListFlowsInput, ListNodesInput, MoveBoardTaskInput, NodeGraphInput, PreviewFlowPublishInput, PublishFlowInput, RedefineTableInput, RelationGraphInput, ResolveNodeLinksInput, RevokeGrantInput, RotateAgentKeyInput, SaveAgentDefinitionInput, SaveAttachmentInput, SaveFlowVersionInput, SaveNodeVersionInput, SearchInput, ShareInput, StartFlowRunInput, TestToolInput, UnpublishFlowInput, UpdateBoardTaskInput, UpdateFlowInput, UpdateNodeInput, UpdateTableRowsInput, } from "@anchrd/intel-contract";
1
+ import { AppendTableRowsInput, ArchiveFlowInput, ArchiveNodeInput, CancelFlowRunInput, CompleteFlowRunStepInput, CreateFlowInput, CreateNodeInput, DefineTableInput, DeleteTableRowsInput, ExecuteToolInput, GetFlowInput, GetFlowRunInput, GetFlowVersionInput, GetNodeInput, GetNodeVersionInput, ListFlowRunsInput, ListFlowsInput, ListNodesInput, NodeGraphInput, PreviewFlowPublishInput, PublishFlowInput, RedefineTableInput, RelationGraphInput, ResolveNodeLinksInput, RevokeGrantInput, 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";
@@ -78,16 +78,6 @@ function asFlowActor(authorization) {
78
78
  isAdmin: authorization.can("intel", "admin"),
79
79
  };
80
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
- }
91
81
  export function createHttp(deps) {
92
82
  const app = new Hono();
93
83
  async function authenticated(headers) {
@@ -136,94 +126,6 @@ export function createHttp(deps) {
136
126
  name: identity.name ?? null,
137
127
  });
138
128
  });
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
- * What the models on offer cost and how much they hold (#257).
149
- *
150
- * ⚠️ Intel asks Cloudflare, never the browser. The account endpoint needs a token and a token does
151
- * not belong in a SPA — which is the sentence the hard-coded table in `packages/ui` used to carry
152
- * as the reason it stayed hard-coded. This route is the answer to it: the figures cross the wire,
153
- * the credential does not.
154
- *
155
- * ⚠️ No capability beyond being signed in. It is a fact about the installation and about nobody's
156
- * data — the same footing `/capabilities` stands on.
157
- *
158
- * ⚠️ The `cache-control` is the caching the ticket asks for, and it is the caching that matters:
159
- * the waste it names is "one fetch every time the select is opened", and that fetch is the
160
- * browser's. A Worker isolate has nowhere durable to keep a table between requests, and inventing
161
- * storage for a list of prices would cost more than the request it saved.
162
- */
163
- app.get("/models", async (context) => {
164
- context.header("cache-control", "private, max-age=3600");
165
- return context.json(await deps.models.list());
166
- });
167
- /**
168
- * The agent runtime, reached through Intel.
169
- *
170
- * ⚠️ This list IS the door. The runtime serves more than what stands here — `/mcp` is the agent's
171
- * own MCP surface, which the portal dials with its own credential (ADR-0005 §7), and
172
- * `/mail/inbound` is authorized by a webhook signature. Neither is a browser path, and neither
173
- * becomes one by being reachable: anything not named below is a 404 from Hono's router, so there
174
- * is no allow-list beside the routes that could go quietly out of step with them.
175
- *
176
- * ⚠️ `agents/run` — not `knowledge/read`. Reading an agent's page is a knowledge read; driving the
177
- * agent is the capability the runtime itself insists on, and Intel refuses on the same terms
178
- * BEFORE the binding is touched, so a denial never reaches the runtime at all.
179
- */
180
- const runtimeRoutes = [
181
- { method: "GET", pattern: "/state", path: () => "/state" },
182
- { method: "GET", pattern: "/runs", path: () => "/runs" },
183
- {
184
- method: "GET",
185
- pattern: "/runs/:runId",
186
- path: (context) => `/runs/${encodeURIComponent(context.req.param("runId") ?? "")}`,
187
- },
188
- { method: "GET", pattern: "/log", path: () => "/log" },
189
- { method: "POST", pattern: "/chat", path: () => "/chat" },
190
- { method: "POST", pattern: "/pause", path: () => "/pause" },
191
- { method: "POST", pattern: "/resume", path: () => "/resume" },
192
- { method: "POST", pattern: "/run", path: () => "/run" },
193
- ];
194
- /**
195
- * What this agent has cost — answered by Intel itself, not forwarded (#251).
196
- *
197
- * ⚠️ It stands under `/agents/:agentId` and is NOT in `runtimeRoutes`, which is the whole point of
198
- * writing it out here. The runtime has no idea what anything costs: the figures come from
199
- * Cloudflare's AI Gateway log, which needs an `AI Gateway: Read` token, and that token belongs in
200
- * Intel rather than in the Worker that executes model-driven tool calls — the runtime's own
201
- * `CLAUDE.md` keeps the number of credentials there deliberately small. The runtime's part is the
202
- * stamp on the model call and nothing more.
203
- *
204
- * ⚠️ `agents/run`, matching the run list this stands beside. What an agent spends is as sensitive
205
- * as what it did, and the two must not be reachable on different terms.
206
- */
207
- app.get("/agents/:agentId/costs", async (context) => {
208
- const authorization = context.get("authorization");
209
- return context.json(await deps.agentCosts.read(asAgentActor(authorization), context.req.param("agentId")));
210
- });
211
- for (const route of runtimeRoutes) {
212
- app.on(route.method, `/agents/:agentId${route.pattern}`, async (context) => {
213
- const authorization = context.get("authorization");
214
- // The runtime's answer is returned untouched, headers included: the chat is a stream, and a
215
- // body read here to inspect it would be a stream nobody could read afterwards.
216
- return await deps.agents.forward({
217
- actor: asAgentActor(authorization),
218
- token: context.get("token"),
219
- agentId: context.req.param("agentId"),
220
- method: route.method,
221
- path: route.path(context),
222
- ...(route.method === "POST" ? { body: await context.req.text() } : {}),
223
- signal: context.req.raw.signal,
224
- });
225
- });
226
- }
227
129
  app.get("/nodes", async (context) => {
228
130
  const auth = requireCapability(context, "knowledge", "read");
229
131
  const url = new URL(context.req.url);
@@ -251,51 +153,6 @@ export function createHttp(deps) {
251
153
  });
252
154
  return context.json(await deps.nodes.graph(asActor(auth), input));
253
155
  });
254
- // ⚠️ Before `/nodes/:nodeId`, or the router would read "agents" as a node ID. Same order and same
255
- // reason as `/nodes/graph` above.
256
- app.get("/nodes/agents", async (context) => {
257
- const auth = requireCapability(context, "knowledge", "read");
258
- const url = new URL(context.req.url);
259
- requireKnownQuery(url, ["parentId", "includeArchived"]);
260
- const parentId = url.searchParams.get("parentId");
261
- // ⚠️ The key is only set when the caller sent it. Omitted means the whole tree and `null` means
262
- // the root level, so defaulting a missing parameter to `null` here would silently answer a
263
- // different question than the one asked.
264
- const input = ListAgentsInput.parse({
265
- ...(url.searchParams.has("parentId") ? { parentId: parentId === "" ? null : parentId } : {}),
266
- includeArchived: url.searchParams.get("includeArchived") === "true",
267
- });
268
- return context.json(await deps.nodes.listAgents(asActor(auth), input));
269
- });
270
- /**
271
- * ⚠️ NO response in Intel's HTTP surface carries a credential any more (D29, #207).
272
- *
273
- * Creating an agent still mints its Gate Application with the caller's own bearer — so this needs
274
- * `applications:write` in Gate on top of `knowledge:create` in Intel — but the key Gate issues
275
- * once goes straight into the agent runtime over the service binding, inside this request. The
276
- * answer is the same shape every agent read has. Until #207 it carried the key in plain text and
277
- * a person had to put it into a Worker secret by hand, which is why an agent created here could
278
- * never run (#200).
279
- */
280
- app.post("/nodes/agents", async (context) => {
281
- const auth = requireCapability(context, "knowledge", "create");
282
- const input = CreateAgentInput.parse(await context.req.json().catch(() => null));
283
- return context.json(await deps.nodes.createAgent(asActor(auth), input, { token: context.get("token") }), 201);
284
- });
285
- /**
286
- * The repair path, and the only one an agent that lost its key has (D29, #207).
287
- *
288
- * ⚠️ `knowledge/write`, not `agents/run`. Which principal an agent acts as is a property of the
289
- * agent, changed by whoever may change the agent; being allowed to press "Run now" is a different
290
- * question and answers a different one. The resource ACL on the node is checked by the service.
291
- *
292
- * ⚠️ Before `/nodes/:nodeId`, like every other literal segment under `/nodes`.
293
- */
294
- app.post("/nodes/agents/:nodeId/rotate-key", async (context) => {
295
- const auth = requireCapability(context, "knowledge", "write");
296
- const input = RotateAgentKeyInput.parse({ nodeId: context.req.param("nodeId") });
297
- return context.json(await deps.nodes.rotateAgentKey(asActor(auth), input, { token: context.get("token") }));
298
- });
299
156
  // ⚠️ Before `/nodes/:nodeId`, or the router would read "export" as a node ID. The root has no
300
157
  // node to address, so the whole installation — as this caller may read it — exports here (#136).
301
158
  app.get("/nodes/export", async (context) => {
@@ -337,20 +194,6 @@ export function createHttp(deps) {
337
194
  const input = GetNodeInput.parse({ nodeId: context.req.param("nodeId") });
338
195
  return zipResponse(await deps.bundle.exportSubtree(asBundleActor(auth), input.nodeId));
339
196
  });
340
- app.get("/nodes/:nodeId/agent", async (context) => {
341
- const auth = requireCapability(context, "knowledge", "read");
342
- return context.json(await deps.nodes.getAgent(asActor(auth), { nodeId: context.req.param("nodeId") }));
343
- });
344
- app.post("/nodes/:nodeId/agent", async (context) => {
345
- const auth = requireCapability(context, "knowledge", "write");
346
- const input = SaveAgentDefinitionInput.parse(await context.req.json().catch(() => null));
347
- if (input.nodeId !== context.req.param("nodeId")) {
348
- throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
349
- }
350
- // ⚠️ The caller's own bearer travels on, like it does when an agent is created: writing a
351
- // definition also arms what it schedules, and that call is made as the person saving (#214).
352
- return context.json(await deps.nodes.saveAgentDefinition(asActor(auth), input, { token: context.get("token") }), 201);
353
- });
354
197
  app.get("/nodes/:nodeId/versions", async (context) => {
355
198
  const auth = requireCapability(context, "knowledge", "read");
356
199
  return context.json(await deps.nodes.listVersions(asActor(auth), context.req.param("nodeId")));
@@ -438,54 +281,10 @@ export function createHttp(deps) {
438
281
  }
439
282
  return context.json(await deps.nodes.redefineTable(asActor(auth), input), 201);
440
283
  });
441
- // The board (#285). One read and five task operations, all action-style POSTs for the same reason
442
- // the table mutations are: what they address is a task ID in the body, and none of them has an
443
- // address of its own to PUT to. Every one writes a version, so they all answer 201 like the
444
- // table's do.
445
- app.get("/nodes/:nodeId/board", async (context) => {
446
- const auth = requireCapability(context, "knowledge", "read");
447
- return context.json(await deps.nodes.getBoard(asActor(auth), { nodeId: context.req.param("nodeId") }));
448
- });
449
- app.post("/nodes/:nodeId/board/configure", async (context) => {
450
- const auth = requireCapability(context, "knowledge", "write");
451
- const input = ConfigureBoardInput.parse(await context.req.json().catch(() => null));
452
- if (input.nodeId !== context.req.param("nodeId")) {
453
- throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
454
- }
455
- return context.json(await deps.nodes.configureBoard(asActor(auth), input), 201);
456
- });
457
- app.post("/nodes/:nodeId/board/tasks", async (context) => {
458
- const auth = requireCapability(context, "knowledge", "write");
459
- const input = AddBoardTaskInput.parse(await context.req.json().catch(() => null));
460
- if (input.nodeId !== context.req.param("nodeId")) {
461
- throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
462
- }
463
- return context.json(await deps.nodes.addBoardTask(asActor(auth), input), 201);
464
- });
465
- app.post("/nodes/:nodeId/board/tasks/update", async (context) => {
466
- const auth = requireCapability(context, "knowledge", "write");
467
- const input = UpdateBoardTaskInput.parse(await context.req.json().catch(() => null));
468
- if (input.nodeId !== context.req.param("nodeId")) {
469
- throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
470
- }
471
- return context.json(await deps.nodes.updateBoardTask(asActor(auth), input), 201);
472
- });
473
- app.post("/nodes/:nodeId/board/tasks/move", async (context) => {
474
- const auth = requireCapability(context, "knowledge", "write");
475
- const input = MoveBoardTaskInput.parse(await context.req.json().catch(() => null));
476
- if (input.nodeId !== context.req.param("nodeId")) {
477
- throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
478
- }
479
- return context.json(await deps.nodes.moveBoardTask(asActor(auth), input), 201);
480
- });
481
- app.post("/nodes/:nodeId/board/tasks/delete", async (context) => {
482
- const auth = requireCapability(context, "knowledge", "write");
483
- const input = DeleteBoardTaskInput.parse(await context.req.json().catch(() => null));
484
- if (input.nodeId !== context.req.param("nodeId")) {
485
- throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
486
- }
487
- return context.json(await deps.nodes.deleteBoardTask(asActor(auth), input), 201);
488
- });
284
+ // ⚠️ There is deliberately no route for the sixth board operation. `board_repair_task_ids`
285
+ // (anchrd/intel#341) is MCP-only until a screen calls it: the routes here are what the browser
286
+ // reaches, nothing in `intel-data-provider` asks for it, and a route with no caller is the thing
287
+ // YAGNI is about. The screen is anchrd/intel#358, and it brings this route with it.
489
288
  app.get("/nodes/:nodeId/links", async (context) => {
490
289
  const auth = requireCapability(context, "knowledge", "read");
491
290
  return context.json(await deps.nodes.listLinks(asActor(auth), context.req.param("nodeId")));
@@ -1,10 +1,7 @@
1
1
  import type { GateClient } from "@anchrd/gate-sdk";
2
- import type { AgentCostsService } from "../agent-costs/agent-costs.types.js";
3
- import type { AgentRuntimeService } from "../agent-runtime/agent-runtime.types.js";
4
2
  import type { BrowserAuth } from "../auth/auth.types.js";
5
3
  import type { BundleService } from "../bundle/bundle.types.js";
6
4
  import type { FlowService } from "../flows/flows.types.js";
7
- import type { ModelCatalogService } from "../model-catalog/model-catalog.types.js";
8
5
  import type { NodeService } from "../nodes/nodes.types.js";
9
6
  import type { ToolService } from "../tools/tools.types.js";
10
7
  export interface HttpDeps {
@@ -13,11 +10,6 @@ export interface HttpDeps {
13
10
  flows: FlowService;
14
11
  tools: ToolService;
15
12
  bundle: BundleService;
16
- agents: AgentRuntimeService;
17
- /** What an agent has cost, read out of the AI Gateway log rather than computed here (#251). */
18
- agentCosts: AgentCostsService;
19
- /** What the models on offer cost, read from Cloudflare rather than typed out here (#257). */
20
- models: ModelCatalogService;
21
13
  resource: string;
22
14
  resourceMetadataUrl: string;
23
15
  auth?: Pick<BrowserAuth, "resolve">;