@anchrd/intel-api 0.14.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 (51) 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 +1 -1
  4. package/dist/adapters/db/db-grants.js +1 -1
  5. package/dist/adapters/db/db.js +16 -113
  6. package/dist/bundle/bundle.js +28 -141
  7. package/dist/cli/cli.js +3 -9
  8. package/dist/http/http.js +1 -206
  9. package/dist/http/http.types.d.ts +0 -8
  10. package/dist/indexing/indexing.js +18 -89
  11. package/dist/intel/intel.js +4 -9
  12. package/dist/intel/intel.types.d.ts +0 -6
  13. package/dist/mcp/mcp.js +3 -292
  14. package/dist/mcp/mcp.types.d.ts +2 -7
  15. package/dist/nodes/document-links/document-links.d.ts +6 -8
  16. package/dist/nodes/document-links/document-links.js +8 -31
  17. package/dist/nodes/nodes.js +20 -886
  18. package/dist/nodes/nodes.types.d.ts +10 -169
  19. package/dist/tools/tools.js +37 -148
  20. package/dist/tools/tools.types.d.ts +0 -21
  21. package/migrations/0018_no_context_policy_at_last.sql +13 -6
  22. package/migrations/0019_one_name_for_the_grants.sql +52 -0
  23. package/package.json +2 -2
  24. package/dist/adapters/cloudflare-api/cloudflare-api.d.ts +0 -22
  25. package/dist/adapters/cloudflare-api/cloudflare-api.js +0 -306
  26. package/dist/adapters/cloudflare-api/cloudflare-api.types.d.ts +0 -64
  27. package/dist/adapters/cloudflare-api/cloudflare-api.types.js +0 -1
  28. package/dist/adapters/gate-applications/gate-applications.d.ts +0 -23
  29. package/dist/adapters/gate-applications/gate-applications.js +0 -88
  30. package/dist/adapters/tool-delegation/tool-delegation.d.ts +0 -22
  31. package/dist/adapters/tool-delegation/tool-delegation.js +0 -90
  32. package/dist/agent-costs/agent-costs.d.ts +0 -16
  33. package/dist/agent-costs/agent-costs.js +0 -105
  34. package/dist/agent-costs/agent-costs.types.d.ts +0 -30
  35. package/dist/agent-costs/agent-costs.types.js +0 -1
  36. package/dist/agent-runtime/agent-runtime.d.ts +0 -16
  37. package/dist/agent-runtime/agent-runtime.js +0 -150
  38. package/dist/agent-runtime/agent-runtime.types.d.ts +0 -122
  39. package/dist/agent-runtime/agent-runtime.types.js +0 -1
  40. package/dist/model-catalog/model-catalog.d.ts +0 -2
  41. package/dist/model-catalog/model-catalog.js +0 -99
  42. package/dist/model-catalog/model-catalog.types.d.ts +0 -15
  43. package/dist/model-catalog/model-catalog.types.js +0 -1
  44. package/dist/nodes/board/board.d.ts +0 -61
  45. package/dist/nodes/board/board.js +0 -826
  46. package/dist/nodes/board/board.types.d.ts +0 -38
  47. package/dist/nodes/board/board.types.js +0 -1
  48. package/migrations/0013_agents_in_the_tree.sql +0 -76
  49. package/migrations/0014_agent_applications.sql +0 -25
  50. package/migrations/0015_tools_delegated_from_a_connection.sql +0 -15
  51. 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;
@@ -219,7 +219,7 @@ export function createFlowRepository(deps) {
219
219
  SELECT
220
220
  EXISTS (SELECT 1 FROM ancestors WHERE id = ?) AS in_subtree,
221
221
  EXISTS (
222
- SELECT 1 FROM tree_grants grant_row
222
+ SELECT 1 FROM node_grants grant_row
223
223
  JOIN ancestors ON ancestors.id = grant_row.node_id
224
224
  WHERE grant_row.principal_type = 'organization'
225
225
  AND grant_row.verb = 'execute'
@@ -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,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";
@@ -945,103 +912,23 @@ export function createBundle(deps) {
945
912
  version = versionRowFor(entry, text, TableMediaType, await deps.hash(text), "snapshot");
946
913
  }
947
914
  }
948
- else if (entry.kind === "board") {
949
- const text = entry.body === null ? "" : decodeText(entry.body, entry.path);
950
- if (text.trim().length > 0) {
951
- let document;
952
- try {
953
- // Same upgrade the stored read does (anchrd/intel#311): a bundle exported before
954
- // `terminal` existed is exactly an old board in a file, and importing it must not be
955
- // the one way to lose a board.
956
- document = BoardDocument.parse(upgradeStoredBoard(JSON.parse(text)));
957
- }
958
- catch {
959
- throw new IntelError(400, "import_invalid_board", `Bundle entry is not a board: ${entry.path}`);
960
- }
961
- /**
962
- * ⚠️ Refused by name, and refused rather than folded (anchrd/intel#321).
963
- *
964
- * A board arriving in a file is the ONE place a document somebody else wrote becomes
965
- * truth here — everywhere else a task id is minted by `deps.id()` and a status list
966
- * goes through `ConfigureBoardInput`, which has demanded distinct ids since #285. Two
967
- * tasks under one id threw `graph.addNode` out of the board's graph view, and two
968
- * statuses under one id drew the same cards in two lanes with a drop that had two
969
- * targets meaning one.
970
- *
971
- * Outside the catch above on purpose: swallowed into `import_invalid_board` this would
972
- * read as "that file is not a board", and whoever is holding the file would look for
973
- * the wrong thing. `repeatedBoardId` names the id, which is the line they have to fix.
974
- *
975
- * ⚠️ And it names the way out, not only the fault (anchrd/intel#341). The usual holder
976
- * of such a file is not its author but somebody moving a tree between installations,
977
- * and the pair is in the BOARD the export came from — where `board_repair_task_ids`
978
- * and `board_configure` each remove one kind of it. Told only what is wrong, they were
979
- * left editing JSON by hand or leaving the board behind, and an import is all or
980
- * nothing: one such board stops the whole move.
981
- */
982
- const repeated = repeatedBoardId(document);
983
- if (repeated !== null) {
984
- throw new IntelError(400, "import_duplicate_board_id", repeated.list === "tasks"
985
- ? `Bundle entry names the task “${repeated.id}” twice: ${entry.path}. Repair that board with board_repair_task_ids where it came from, then export it again.`
986
- : `Bundle entry names the status “${repeated.id}” twice: ${entry.path}. Repair that board with board_configure — the status list written whole, with distinct ids — where it came from, then export it again.`);
987
- }
988
- // ⚠️ `references` are remapped the way a document's inline links are, and `dependsOn`
989
- // and `parentId` are NOT (#285). A task's references point at NODES, which the import
990
- // renumbers; its dependencies and its parent point at tasks inside this very file,
991
- // whose ids the import does not touch. Renumbering those would break every edge in the
992
- // board for no reason.
993
- const remapped = {
994
- statuses: document.statuses,
995
- tasks: document.tasks.map((task) => ({
996
- ...task,
997
- references: task.references.map((reference) => idMap.get(reference) ?? reference),
998
- })),
999
- };
1000
- const body = JSON.stringify(remapped);
1001
- version = versionRowFor(entry, body, BoardMediaType, await deps.hash(body), null);
1002
- // Into the same link pass as an imported document: a board's references are `text`
1003
- // links, so they have to survive the import the same way.
1004
- documentBodies.set(entry.newId, { mediaType: BoardMediaType, content: body });
1005
- }
1006
- }
1007
- else if (entry.kind === "agent") {
1008
- const text = entry.body === null ? "" : decodeText(entry.body, entry.path);
1009
- if (text.trim().length > 0) {
1010
- let definition;
1011
- try {
1012
- definition = AgentDefinition.parse(JSON.parse(text));
1013
- }
1014
- catch {
1015
- throw new IntelError(400, "import_invalid_agent", `Bundle entry is not an agent definition: ${entry.path}`);
1016
- }
1017
- const remapped = {
1018
- ...definition,
1019
- /**
1020
- * ⚠️ A delegation NEVER travels in a file (#208, D30). `tools.delegatedBy` names
1021
- * whose portal connection an agent acts on, and the only thing that may write it is
1022
- * a person saving the agent in a session Intel authorized — the save path proves
1023
- * they reach every named server first. A bundle is a document somebody hands over;
1024
- * carrying the field through would let a crafted file name a local user and hand an
1025
- * imported agent that person's connection, with no act of theirs anywhere.
1026
- *
1027
- * The consequence is deliberate and mild: an imported agent arrives with no tools,
1028
- * and whoever wants it to have some gives them in the profile — which is the same
1029
- * act D30 made the only way in.
1030
- */
1031
- tools: null,
1032
- references: definition.references.map((reference) => idMap.has(reference.nodeId)
1033
- ? { ...reference, nodeId: idMap.get(reference.nodeId) }
1034
- : reference),
1035
- schedules: definition.schedules.map((schedule) => idMap.has(schedule.target.id)
1036
- ? {
1037
- ...schedule,
1038
- target: { ...schedule.target, id: idMap.get(schedule.target.id) },
1039
- }
1040
- : schedule),
1041
- };
1042
- const body = JSON.stringify(remapped);
1043
- version = versionRowFor(entry, body, AgentMediaType, await deps.hash(body), null);
1044
- }
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.`);
1045
932
  }
1046
933
  else if (entry.kind === "attachment") {
1047
934
  const bytes = entry.body ?? new Uint8Array(0);