@anchrd/intel-api 0.14.0 → 0.16.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 (60) hide show
  1. package/dist/adapters/cloudflare/cloudflare.js +0 -68
  2. package/dist/adapters/cloudflare/cloudflare.types.d.ts +0 -39
  3. package/dist/adapters/db/db-flows.js +3 -2
  4. package/dist/adapters/db/db-grants.d.ts +1 -1
  5. package/dist/adapters/db/db-grants.js +1 -1
  6. package/dist/adapters/db/db.js +16 -113
  7. package/dist/adapters/openid/openid.js +1 -1
  8. package/dist/adapters/remote-tools/remote-tools.js +1 -1
  9. package/dist/bundle/bundle.js +31 -141
  10. package/dist/bundle/bundle.types.d.ts +1 -1
  11. package/dist/cli/cli.js +18 -11
  12. package/dist/flows/flows.d.ts +1 -1
  13. package/dist/flows/flows.js +1 -1
  14. package/dist/flows/flows.types.d.ts +4 -1
  15. package/dist/http/http.js +43 -238
  16. package/dist/http/http.types.d.ts +0 -8
  17. package/dist/indexing/indexing.js +18 -89
  18. package/dist/intel/intel.js +4 -9
  19. package/dist/intel/intel.types.d.ts +0 -6
  20. package/dist/mcp/mcp.js +84 -361
  21. package/dist/mcp/mcp.types.d.ts +2 -7
  22. package/dist/nodes/document-links/document-links.d.ts +6 -8
  23. package/dist/nodes/document-links/document-links.js +8 -31
  24. package/dist/nodes/nodes.js +24 -890
  25. package/dist/nodes/nodes.types.d.ts +14 -173
  26. package/dist/tools/tool-servers/tool-servers.d.ts +1 -1
  27. package/dist/tools/tool-servers/tool-servers.js +1 -1
  28. package/dist/tools/tools.js +37 -148
  29. package/dist/tools/tools.types.d.ts +1 -22
  30. package/migrations/0018_no_context_policy_at_last.sql +13 -6
  31. package/migrations/0019_one_name_for_the_grants.sql +52 -0
  32. package/package.json +2 -2
  33. package/dist/adapters/cloudflare-api/cloudflare-api.d.ts +0 -22
  34. package/dist/adapters/cloudflare-api/cloudflare-api.js +0 -306
  35. package/dist/adapters/cloudflare-api/cloudflare-api.types.d.ts +0 -64
  36. package/dist/adapters/cloudflare-api/cloudflare-api.types.js +0 -1
  37. package/dist/adapters/gate-applications/gate-applications.d.ts +0 -23
  38. package/dist/adapters/gate-applications/gate-applications.js +0 -88
  39. package/dist/adapters/tool-delegation/tool-delegation.d.ts +0 -22
  40. package/dist/adapters/tool-delegation/tool-delegation.js +0 -90
  41. package/dist/agent-costs/agent-costs.d.ts +0 -16
  42. package/dist/agent-costs/agent-costs.js +0 -105
  43. package/dist/agent-costs/agent-costs.types.d.ts +0 -30
  44. package/dist/agent-costs/agent-costs.types.js +0 -1
  45. package/dist/agent-runtime/agent-runtime.d.ts +0 -16
  46. package/dist/agent-runtime/agent-runtime.js +0 -150
  47. package/dist/agent-runtime/agent-runtime.types.d.ts +0 -122
  48. package/dist/agent-runtime/agent-runtime.types.js +0 -1
  49. package/dist/model-catalog/model-catalog.d.ts +0 -2
  50. package/dist/model-catalog/model-catalog.js +0 -99
  51. package/dist/model-catalog/model-catalog.types.d.ts +0 -15
  52. package/dist/model-catalog/model-catalog.types.js +0 -1
  53. package/dist/nodes/board/board.d.ts +0 -61
  54. package/dist/nodes/board/board.js +0 -826
  55. package/dist/nodes/board/board.types.d.ts +0 -38
  56. package/dist/nodes/board/board.types.js +0 -1
  57. package/migrations/0013_agents_in_the_tree.sql +0 -76
  58. package/migrations/0014_agent_applications.sql +0 -25
  59. package/migrations/0015_tools_delegated_from_a_connection.sql +0 -15
  60. package/migrations/0016_boards_in_the_tree.sql +0 -80
@@ -1,18 +1,14 @@
1
1
  import { createGateClient } from "@anchrd/gate-sdk";
2
2
  import { ulid } from "ulid";
3
- import { createAgentCosts } from "../../agent-costs/agent-costs.js";
4
- import { createAgentRuntimeService } from "../../agent-runtime/agent-runtime.js";
5
3
  import { createBrowserAuth } from "../../auth/auth.js";
6
4
  import { createBundle } from "../../bundle/bundle.js";
7
5
  import { createFlows } from "../../flows/flows.js";
8
6
  import { createIndexing, PermanentIndexingError } from "../../indexing/indexing.js";
9
7
  import { createIntel } from "../../intel/intel.js";
10
- import { createModelCatalog } from "../../model-catalog/model-catalog.js";
11
8
  import { createNodes } from "../../nodes/nodes.js";
12
9
  import { IntelError } from "../../shared/intel-error/intel-error.js";
13
10
  import { sha256Hex } from "../../shared/sha256/sha256.js";
14
11
  import { createTools } from "../../tools/tools.js";
15
- import { createCloudflareApi } from "../cloudflare-api/cloudflare-api.js";
16
12
  import { createContentStore } from "../content/content.js";
17
13
  import { createNodeRepository } from "../db/db.js";
18
14
  import { createFlowRepository } from "../db/db-flows.js";
@@ -20,7 +16,6 @@ import { createNodeIndexRepository } from "../db/db-indexing.js";
20
16
  import { createOAuthClientStore } from "../db/db-oauth.js";
21
17
  import { createDocumentConverter } from "../document-converter/document-converter.js";
22
18
  import { createFlowRuntime } from "../flow-runtime/flow-runtime.js";
23
- import { createGateApplications } from "../gate-applications/gate-applications.js";
24
19
  import { createIndexQueue, IndexMessage } from "../index-queue/index-queue.js";
25
20
  import { createJsonSchemaValidator } from "../json-schema/json-schema.js";
26
21
  import { createOpenId } from "../openid/openid.js";
@@ -28,7 +23,6 @@ import { createPortalTokenStore } from "../portal-tokens/portal-tokens.js";
28
23
  import { createRemoteTools } from "../remote-tools/remote-tools.js";
29
24
  import { createSemanticIndex } from "../semantic-index/semantic-index.js";
30
25
  import { createSessionCodec } from "../session-cookie/session-cookie.js";
31
- import { createToolDelegation } from "../tool-delegation/tool-delegation.js";
32
26
  import { createToolSourcePolicy } from "../tool-source-policy/tool-source-policy.js";
33
27
  // biome-ignore lint/performance/noBarrelFile: The package entry must expose Wrangler's named Workflow class.
34
28
  export { IntelFlowWorkflow } from "./cloudflare-flow-workflow.js";
@@ -82,22 +76,10 @@ export default {
82
76
  crypto,
83
77
  now,
84
78
  });
85
- // Built before the tree since D30: saving an agent has to ask the portal, live, whether the
86
- // person saving reaches every server they are delegating. The dependency runs one way — the
87
- // tool service knows nothing about the tree, and the delegation lookup below reads D1 directly
88
- // rather than through the node service.
89
- const toolDelegation = createToolDelegation({
90
- db: env.DB,
91
- content: contentStore,
92
- id: ulid,
93
- now,
94
- });
95
79
  const tools = createTools({
96
80
  portalUrl,
97
81
  remote: createRemoteTools({ fetch: globalThis.fetch.bind(globalThis) }),
98
82
  tokens: portalTokens,
99
- delegation: toolDelegation.resolve,
100
- audit: toolDelegation.audit,
101
83
  refresh: async (token) => {
102
84
  try {
103
85
  const refreshed = await oauth.refresh({
@@ -126,56 +108,9 @@ export default {
126
108
  return await sha256Hex(crypto, JSON.stringify(value));
127
109
  },
128
110
  });
129
- // ⚠️ Built before `nodes` and not after, because creating an agent has to be able to hand the
130
- // runtime its key inside the same call (D29). Its `visibleNode` closes over `nodes` and is only
131
- // ever called later, which is what keeps the two from being a cycle.
132
- const agents = createAgentRuntimeService({
133
- ...(env.AGENT ? { runtime: env.AGENT } : {}),
134
- ...(env.AGENT_HANDOVER_SECRET ? { handoverSecret: env.AGENT_HANDOVER_SECRET } : {}),
135
- // The tree's own visibility lookup, unchanged: whether somebody may reach an agent is the
136
- // same question as whether they may see the node, and no second answer is invented here.
137
- visibleNode: async (actor, nodeId) => await nodes.visibleNode(actor, nodeId),
138
- });
139
- // ⚠️ One Cloudflare read client for both of the account's answers — the gateway's cost log
140
- // (#251) and the Workers AI price list (#257) — because it is one token and one account. It is
141
- // absent as a whole where any of the three variables is missing: a half-configured client would
142
- // fail per call with a 401 that reads like a permission problem instead of saying, once, that
143
- // this deployment was never given a token.
144
- const gatewayAccountId = env.AI_GATEWAY_ACCOUNT_ID?.trim();
145
- const gatewayId = env.AI_GATEWAY_ID?.trim();
146
- const gatewayReadToken = env.AI_GATEWAY_READ_TOKEN?.trim();
147
- const cloudflareApi = gatewayAccountId && gatewayId && gatewayReadToken
148
- ? createCloudflareApi({
149
- accountId: gatewayAccountId,
150
- gatewayId,
151
- token: gatewayReadToken,
152
- fetch: globalThis.fetch.bind(globalThis),
153
- })
154
- : undefined;
155
- const agentCosts = createAgentCosts({
156
- ...(cloudflareApi ? { gateway: cloudflareApi } : {}),
157
- visibleNode: async (actor, nodeId) => await nodes.visibleNode(actor, nodeId),
158
- now,
159
- });
160
- const models = createModelCatalog({ ...(cloudflareApi ? { cloudflare: cloudflareApi } : {}) });
161
111
  const nodes = createNodes({
162
112
  repository: nodeRepository,
163
113
  content: contentStore,
164
- // ⚠️ Built from `GATE_URL` alone — no service key is handed to it, and none would help. The
165
- // Applications surface is admin-gated on a real principal, so every call carries the bearer
166
- // of the person making it, which the tree passes in per operation (#182).
167
- applications: createGateApplications({
168
- fetch: globalThis.fetch.bind(globalThis),
169
- gateUrl: env.GATE_URL,
170
- }),
171
- // The binding's presence IS the signal (#190) — no separate configuration option exists.
172
- agentRuntimeAvailable: () => env.AGENT !== undefined,
173
- // Where the Application key goes, and the only place it goes (D29). The same service the
174
- // browser paths are proxied through, so there is one door to the runtime and not two.
175
- agentKeys: { store: async (input) => await agents.storeKey(input) },
176
- // The same door again (#214): a definition that was written is a schedule that has to be
177
- // armed, and the runtime only looks when it is told to.
178
- agentSchedules: { sync: async (input) => await agents.syncSchedules(input) },
179
114
  id: ulid,
180
115
  now,
181
116
  indexing: createIndexQueue(env.INDEXING),
@@ -247,9 +182,6 @@ export default {
247
182
  },
248
183
  indexing: createIndexQueue(env.INDEXING),
249
184
  }),
250
- agents,
251
- agentCosts,
252
- models,
253
185
  auth,
254
186
  }).fetch(request);
255
187
  },
@@ -9,17 +9,6 @@ export interface CloudflareEnv {
9
9
  ASSETS?: {
10
10
  fetch(request: Request): Promise<Response>;
11
11
  };
12
- /**
13
- * The agent runtime Worker, bound as a service.
14
- *
15
- * ⚠️ Optional, and it has to be: a deployment may run Intel without agents, and a hard requirement
16
- * here would make the whole installation fail to start over a feature nobody uses. Where it is
17
- * absent the agent routes answer 503 by name; where it is present the browser reaches the runtime
18
- * through Intel and never across a second origin (#178).
19
- */
20
- AGENT?: {
21
- fetch(request: Request): Promise<Response>;
22
- };
23
12
  DB: D1Database;
24
13
  CONTENT: R2Bucket;
25
14
  INDEXING: QueueProducer<IndexMessage>;
@@ -30,34 +19,6 @@ export interface CloudflareEnv {
30
19
  GATE_SERVICE_KEY: string;
31
20
  INTEL_URL: string;
32
21
  INTEL_SESSION_SECRET: string;
33
- /**
34
- * The shared secret that proves a key handover reached the agent runtime through this Worker
35
- * (D29, #207). The SAME value the agent Worker holds as `AGENT_HANDOVER_SECRET`.
36
- *
37
- * ⚠️ Optional exactly like `AGENT`: a deployment may run Intel without agents, and a hard
38
- * requirement would make the whole installation fail to start over a feature nobody uses. Where
39
- * it is absent, creating an agent refuses by name instead of leaving one without a key.
40
- */
41
- AGENT_HANDOVER_SECRET?: string;
42
- /**
43
- * Where the AI Gateway lives and how to READ it (#251, #257).
44
- *
45
- * ⚠️ `AI_GATEWAY_READ_TOKEN` is not the agent runtime's `AI_GATEWAY_TOKEN` and must never be the
46
- * same value. That one carries `AI Gateway: Run` and buys inference; this one carries
47
- * `AI Gateway: Read` — plus `Workers AI: Read` for the model catalog — and buys nothing at all.
48
- * One credential doing both would put a spending permission into the Worker that only reads.
49
- *
50
- * ⚠️ Cloudflare cannot scope `AI Gateway: Read` to a single gateway; it is account-wide, the same
51
- * limitation `AI Gateway: Run` already had in #239. It is worth knowing when the token is created
52
- * and is not a reason against the route: it reads and writes nothing.
53
- *
54
- * ⚠️ All three optional, together. A deployment that configures none of them gets a cost view that
55
- * says "not configured" and a model catalog off the built-in table — both named states, neither an
56
- * error. Intel without a Cloudflare token is a supported installation.
57
- */
58
- AI_GATEWAY_ACCOUNT_ID?: string;
59
- AI_GATEWAY_ID?: string;
60
- AI_GATEWAY_READ_TOKEN?: string;
61
22
  TOOL_SOURCE_ORIGINS: string;
62
23
  MCP_PORTAL_URL?: string;
63
24
  ALLOW_INSECURE_OAUTH?: string;
@@ -1,4 +1,5 @@
1
- import { Flow, FlowGraph, FlowRun, FlowVersion, } from "@anchrd/intel-contract";
1
+ import { Flow, FlowGraph, FlowVersion, } from "@anchrd/intel-contract/flow";
2
+ import { FlowRun } from "@anchrd/intel-contract/flow-run";
2
3
  import { calleeIds, treeLinkKinds } from "../../flows/flows.js";
3
4
  import { flowCallable, flowInSubtree, flowInSubtreeBindings, flowVerbBindings, flowVerbQuery, readableOrRunnableCte, subtreeBindings, subtreeCte, } from "./db-grants.js";
4
5
  const flowColumnNames = [
@@ -219,7 +220,7 @@ export function createFlowRepository(deps) {
219
220
  SELECT
220
221
  EXISTS (SELECT 1 FROM ancestors WHERE id = ?) AS in_subtree,
221
222
  EXISTS (
222
- SELECT 1 FROM tree_grants grant_row
223
+ SELECT 1 FROM node_grants grant_row
223
224
  JOIN ancestors ON ancestors.id = grant_row.node_id
224
225
  WHERE grant_row.principal_type = 'organization'
225
226
  AND grant_row.verb = 'execute'
@@ -1,4 +1,4 @@
1
- import type { ResourceVerb } from "@anchrd/intel-contract";
1
+ import type { ResourceVerb } from "@anchrd/intel-contract/share";
2
2
  export interface GrantActor {
3
3
  id: string;
4
4
  email: string;
@@ -5,7 +5,7 @@ const principalMatch = `(
5
5
  )`;
6
6
  function grantExists(nodeColumn) {
7
7
  return `EXISTS (
8
- SELECT 1 FROM tree_grants grant_row
8
+ SELECT 1 FROM node_grants grant_row
9
9
  WHERE grant_row.node_id = ${nodeColumn}
10
10
  AND ${principalMatch}
11
11
  AND grant_row.verb = ?
@@ -5,7 +5,7 @@ const grantColumns = `id, node_id, principal_type, principal_id, verb, expires_a
5
5
  created_by, created_at`;
6
6
  const linkColumns = `link.id, link.source_node_id, link.target_node_id, link.relation,
7
7
  link.origin, link.label, link.created_by, link.created_at`;
8
- // The first row of each node, in the order they arrive. A chunked kind — a board, whose index holds
8
+ // The first row of each node, in the order they arrive. A chunked kind — one whose index holds
9
9
  // one row per task (#285) — answers a query once per matching card; a citation names a node, so the
10
10
  // best of them is the one that gets to speak for it.
11
11
  function dedupedByNode(rows) {
@@ -146,66 +146,7 @@ export function createNodeRepository(deps) {
146
146
  JOIN allowed ON allowed.id = n.id
147
147
  WHERE n.archived_at IS NOT NULL
148
148
  ORDER BY n.archived_at DESC, lower(n.title), n.id`;
149
- /**
150
- * The agents this actor may see, over the whole tree or under one folder (#139).
151
- *
152
- * ⚠️ The same `allowed` set every other read joins against, not a second predicate written for
153
- * agents. An agent is a node, and the moment its list answers from its own rule it can answer
154
- * differently from `list` about the very same row.
155
- *
156
- * Three scopes, matching the flow list: no clause at all for the whole tree, `IS NULL` for the
157
- * root level, and one folder by ID.
158
- */
159
- const visibleAgents = (clause, includeArchived) => `${visibleCte}
160
- SELECT ${nodeColumns}
161
- FROM nodes n
162
- JOIN allowed ON allowed.id = n.id
163
- WHERE n.kind = 'agent' ${includeArchived ? "" : "AND n.archived_at IS NULL"} ${clause}
164
- ORDER BY lower(n.title), n.id`;
165
- const agentScopeOf = (parentId) => parentId === undefined
166
- ? { clause: "", bindings: [] }
167
- : parentId === null
168
- ? { clause: "AND n.parent_id IS NULL", bindings: [] }
169
- : { clause: "AND n.parent_id = ?", bindings: [parentId] };
170
149
  return {
171
- async listVisibleAgents(actor, input) {
172
- const scope = agentScopeOf(input.parentId);
173
- const result = await deps.db
174
- .prepare(visibleAgents(scope.clause, input.includeArchived === true))
175
- .bind(...readBindings(actor), ...scope.bindings)
176
- .all();
177
- return (result.results ?? []).map(mapNode);
178
- },
179
- /**
180
- * The R2 keys of the current definitions of every agent in this folder's subtree that this
181
- * actor may see — the input to the share warning (#139, ADR-0005 §2).
182
- *
183
- * ⚠️ Keys, never bodies and never titles. Whether a referenced node may be NAMED to whoever is
184
- * sharing is the tree's question and is asked afterwards through `getVisible`, exactly as the
185
- * flow side does it. Answering it here would be the second rule that #17 and #19 got wrong.
186
- *
187
- * ⚠️ Agents this actor may not see contribute nothing, not even a count. Their references would
188
- * otherwise be attributed to a folder this actor administers, which discloses that the agent is
189
- * there at all.
190
- */
191
- async listVisibleAgentDefinitionKeys(actor, folderId) {
192
- const result = await deps.db
193
- .prepare(`${visibleCte},
194
- scope(id) AS (
195
- SELECT id FROM nodes WHERE id = ?
196
- UNION
197
- SELECT child.id FROM nodes child JOIN scope ON child.parent_id = scope.id
198
- )
199
- SELECT version.content_key AS content_key
200
- FROM nodes n
201
- JOIN allowed ON allowed.id = n.id
202
- JOIN node_versions version ON version.id = n.current_version_id
203
- WHERE n.kind = 'agent' AND n.archived_at IS NULL AND n.id IN (SELECT id FROM scope)
204
- ORDER BY n.id`)
205
- .bind(...readBindings(actor), folderId)
206
- .all();
207
- return (result.results ?? []).map((row) => row.content_key);
208
- },
209
150
  async listVisible(actor, input) {
210
151
  const result = await deps.db
211
152
  .prepare(input.archivedOnly ? archivedEverywhere : visibleChildren(false))
@@ -377,7 +318,7 @@ export function createNodeRepository(deps) {
377
318
  *
378
319
  * ⚠️ The idempotency row points at the VERSION, and the audit event points at the node, so the
379
320
  * two are joined through the version id the snapshot writer puts into the metadata. That
380
- * metadata is where a board operation says which task it touched and how many it removed —
321
+ * metadata is where a snapshot says which positions it touched and how many it removed —
381
322
  * facts a replay cannot recompute from the stored document, because the document is the state
382
323
  * AFTER the write. Without this, repeating a delete would answer with a count nobody measured.
383
324
  */
@@ -439,19 +380,8 @@ export function createNodeRepository(deps) {
439
380
  return null;
440
381
  return row.resource_id.startsWith("1:");
441
382
  },
442
- // ⚠️ Its own statement rather than a column on the node read: four of the five kinds can never
443
- // have an Application, and the two joins every node read already carries are not going to grow
444
- // a third for a question only agents ask (#182).
445
- async agentApplicationId(nodeId) {
446
- const row = await deps.db
447
- .prepare("SELECT application_id FROM agent_applications WHERE node_id = ?")
448
- .bind(nodeId)
449
- .first();
450
- return row?.application_id ?? null;
451
- },
452
383
  async insertNode(input) {
453
384
  const node = input.node;
454
- const application = input.application;
455
385
  try {
456
386
  await deps.db.batch([
457
387
  deps.db
@@ -469,23 +399,7 @@ export function createNodeRepository(deps) {
469
399
  .prepare(`INSERT INTO audit_events (
470
400
  id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
471
401
  ) VALUES (?, ?, 'node.create', 'node', ?, ?, ?)`)
472
- .bind(input.auditId, input.actorId, node.id,
473
- // ⚠️ The Application ID is deliberately NOT in this metadata, and the key is not
474
- // within reach of it at all. An audit event is read back by people and by the
475
- // reindexer; what belongs in it is that an agent was created, not the name of the
476
- // principal behind it — the mapping row below is where that is kept, once (D27).
477
- JSON.stringify({ kind: node.kind, parentId: node.parentId }), node.createdAt),
478
- // Last in the batch, and in the SAME batch: D1 runs a batch as one transaction, so the
479
- // mapping cannot end up without its node or the node without its mapping. An agent whose
480
- // principal is recorded nowhere is one that archiving would quietly fail to switch off.
481
- ...(application
482
- ? [
483
- deps.db
484
- .prepare(`INSERT INTO agent_applications (node_id, application_id, created_at)
485
- VALUES (?, ?, ?)`)
486
- .bind(node.id, application.id, node.createdAt),
487
- ]
488
- : []),
402
+ .bind(input.auditId, input.actorId, node.id, JSON.stringify({ kind: node.kind, parentId: node.parentId }), node.createdAt),
489
403
  ]);
490
404
  }
491
405
  catch (error) {
@@ -832,7 +746,7 @@ export function createNodeRepository(deps) {
832
746
  .bind(version.id, version.nodeId, version.sequence, version.contentKey, version.mediaType, version.contentHash, version.size,
833
747
  // ⚠️ Taken from the version rather than written in as `'snapshot'`. A table's mutation
834
748
  // marks a snapshot, because reading a table starts at the newest one and everything
835
- // before it is history (#135). A board's version carries the whole board by
749
+ // before it is history (#135). A snapshot version carries the whole document by
836
750
  // construction, so it marks nothing — the same `null` a document's version carries, and
837
751
  // for the same reason (#285).
838
752
  version.segment, version.createdBy, version.createdAt, version.nodeId, input.baseVersionId),
@@ -868,7 +782,7 @@ export function createNodeRepository(deps) {
868
782
  },
869
783
  async listGrants(resourceId) {
870
784
  const result = await deps.db
871
- .prepare(`SELECT ${grantColumns} FROM tree_grants
785
+ .prepare(`SELECT ${grantColumns} FROM node_grants
872
786
  WHERE node_id = ?
873
787
  ORDER BY principal_type, principal_id, verb`)
874
788
  .bind(resourceId)
@@ -887,7 +801,7 @@ export function createNodeRepository(deps) {
887
801
  JOIN ancestors child ON child.parent_id = parent.id
888
802
  )
889
803
  SELECT 1 AS reaches
890
- FROM tree_grants grant_row
804
+ FROM node_grants grant_row
891
805
  JOIN ancestors ON ancestors.id = grant_row.node_id
892
806
  WHERE grant_row.id <> ?
893
807
  AND grant_row.principal_type = 'organization'
@@ -966,17 +880,6 @@ export function createNodeRepository(deps) {
966
880
  .bind(input.auditId, input.actorId, input.sourceNodeId, JSON.stringify({ targetNodeIds: input.links.map((link) => link.targetNodeId) }), input.occurredAt),
967
881
  ]);
968
882
  },
969
- async recordAgentKeyRotation(input) {
970
- // ⚠️ No node row moves, so there is nothing to batch this with — the event IS the write. The
971
- // key is not among the columns and must not be added to them: audit is metadata, and a row
972
- // that carried the credential would outlive every other copy of it.
973
- await deps.db
974
- .prepare(`INSERT INTO audit_events (
975
- id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
976
- ) VALUES (?, ?, 'agent.rotate_key', 'node', ?, ?, ?)`)
977
- .bind(input.auditId, input.actorId, input.nodeId, JSON.stringify({ applicationId: input.applicationId }), input.occurredAt)
978
- .run();
979
- },
980
883
  async graphVisible(actor, input) {
981
884
  const nodeResult = await deps.db
982
885
  .prepare(`${visibleCte}
@@ -1021,7 +924,7 @@ export function createNodeRepository(deps) {
1021
924
  deps.db
1022
925
  // The verb is part of the key, so re-granting the same verb only refreshes its expiry
1023
926
  // and never turns one verb into another.
1024
- .prepare(`INSERT INTO tree_grants (
927
+ .prepare(`INSERT INTO node_grants (
1025
928
  id, node_id, principal_type, principal_id, verb,
1026
929
  expires_at, created_by, created_at
1027
930
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
@@ -1031,7 +934,7 @@ export function createNodeRepository(deps) {
1031
934
  deps.db
1032
935
  .prepare(`INSERT INTO idempotency_keys (
1033
936
  actor_id, operation, idempotency_key, resource_id, created_at
1034
- ) SELECT ?, 'node.share', ?, id, ? FROM tree_grants
937
+ ) SELECT ?, 'node.share', ?, id, ? FROM node_grants
1035
938
  WHERE node_id = ? AND principal_type = ? AND principal_id = ? AND verb = ?`)
1036
939
  .bind(input.actorId, input.idempotencyKey, grant.createdAt, grant.resourceId, principalType, principalId, grant.verb),
1037
940
  deps.db
@@ -1041,7 +944,7 @@ export function createNodeRepository(deps) {
1041
944
  .bind(input.auditId, input.actorId, grant.resourceId, JSON.stringify({ principalType, verb: grant.verb }), grant.createdAt),
1042
945
  ]);
1043
946
  const stored = await deps.db
1044
- .prepare(`SELECT ${grantColumns} FROM tree_grants
947
+ .prepare(`SELECT ${grantColumns} FROM node_grants
1045
948
  WHERE node_id = ? AND principal_type = ? AND principal_id = ? AND verb = ?`)
1046
949
  .bind(grant.resourceId, principalType, principalId, grant.verb)
1047
950
  .first();
@@ -1057,7 +960,7 @@ export function createNodeRepository(deps) {
1057
960
  .first();
1058
961
  if (replayed) {
1059
962
  const row = await deps.db
1060
- .prepare(`SELECT ${grantColumns} FROM tree_grants WHERE id = ?`)
963
+ .prepare(`SELECT ${grantColumns} FROM node_grants WHERE id = ?`)
1061
964
  .bind(replayed.resource_id)
1062
965
  .first();
1063
966
  if (row)
@@ -1074,11 +977,11 @@ export function createNodeRepository(deps) {
1074
977
  actor_id, operation, idempotency_key, resource_id, created_at
1075
978
  ) SELECT ?, 'node.revoke', ?,
1076
979
  (CASE WHEN EXISTS (
1077
- SELECT 1 FROM tree_grants WHERE id = ? AND node_id = ?
980
+ SELECT 1 FROM node_grants WHERE id = ? AND node_id = ?
1078
981
  ) THEN '1:' ELSE '0:' END) || ?, ?`)
1079
982
  .bind(input.actorId, input.idempotencyKey, input.grantId, input.resourceId, input.grantId, input.occurredAt),
1080
983
  deps.db
1081
- .prepare("DELETE FROM tree_grants WHERE id = ? AND node_id = ?")
984
+ .prepare("DELETE FROM node_grants WHERE id = ? AND node_id = ?")
1082
985
  .bind(input.grantId, input.resourceId),
1083
986
  deps.db
1084
987
  .prepare(`INSERT INTO audit_events (
@@ -1121,10 +1024,10 @@ export function createNodeRepository(deps) {
1121
1024
  LIMIT ?`)
1122
1025
  .bind(...readBindings(actor), ...scopeBindings(input.scopeId), query, input.limit)
1123
1026
  .all();
1124
- // ⚠️ One citation per node, and the FIRST is the one kept. A board holds one index row per
1125
- // task (#285), so a query matching three cards of the same board arrives here three times;
1027
+ // ⚠️ One citation per node, and the FIRST is the one kept. The index is CHUNKED a node
1028
+ // may hold several rows (#285) so a query matching three of them arrives here three times;
1126
1029
  // the rows come back in rank order, so the survivor is the best-matching card and its own
1127
- // passage is what the reader is shown. Without this, one busy board could fill a whole page
1030
+ // passage is what the reader is shown. Without this, one busy node could fill a whole page
1128
1031
  // of results and push every other node off it.
1129
1032
  return dedupedByNode(result.results ?? []).map((row) => ({
1130
1033
  nodeId: row.node_id,
@@ -1139,7 +1042,7 @@ export function createNodeRepository(deps) {
1139
1042
  },
1140
1043
  async hydrateVisibleCitations(actor, hits, scopeId) {
1141
1044
  // One entry per node: the caller has already picked the best-scoring chunk of each, so a
1142
- // board that answered with four cards arrives as the one card that answered best.
1045
+ // node that answered with four passages arrives as the one passage that answered best.
1143
1046
  const unique = [...new Map(hits.map((hit) => [hit.nodeId, hit])).values()].slice(0, 100);
1144
1047
  if (unique.length === 0)
1145
1048
  return [];
@@ -1,4 +1,4 @@
1
- import { ToolSourceUrl } from "@anchrd/intel-contract";
1
+ import { ToolSourceUrl } from "@anchrd/intel-contract/tool";
2
2
  import * as client from "openid-client";
3
3
  import { z } from "zod";
4
4
  const ProtectedResource = z.looseObject({
@@ -1,4 +1,4 @@
1
- import { ToolTestResult } from "@anchrd/intel-contract";
1
+ import { ToolTestResult } from "@anchrd/intel-contract/tool";
2
2
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
3
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
4
  import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker";