@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,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
  },
@@ -259,6 +191,7 @@ export default {
259
191
  content: createContentStore(env.CONTENT),
260
192
  semantic: env.AI && env.SEARCH ? createSemanticIndex({ ai: env.AI, index: env.SEARCH }) : undefined,
261
193
  converter: env.AI ? createDocumentConverter(env.AI) : undefined,
194
+ hash: async (content) => await sha256Hex(crypto, content),
262
195
  now: () => new Date(),
263
196
  });
264
197
  for (const message of batch.messages) {
@@ -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 = ?
@@ -1,3 +1,7 @@
1
+ // How much of a chunk is kept as the passage a searcher is shown. The same 480 characters
2
+ // `hydrateVisibleCitations` cuts out of the full-text row, so a citation reads the same length
3
+ // whether it came from a card's own vector or from the fallback beside it.
4
+ const maxPassageCharacters = 480;
1
5
  function mapTarget(row, contentKeys) {
2
6
  return {
3
7
  nodeId: row.node_id,
@@ -68,6 +72,81 @@ export function createNodeIndexRepository(db) {
68
72
  .bind(target.versionId, target.nodeId, chunk.title, chunk.text, target.nodeId, target.versionId)),
69
73
  ]);
70
74
  },
75
+ /**
76
+ * ⚠️ `archived_at IS NOT NULL` carries this on its own, and it is the whole guard
77
+ * (anchrd/intel#348). `getTarget` above answers null for two reasons and only one of them means
78
+ * "purge": the other is a queue message for a version a later save superseded, and Cloudflare
79
+ * Queues deliver at least once, so those arrive as a matter of course. Acting on one would empty
80
+ * the vector index of a live board behind the back of the save that had just filled it — and
81
+ * nothing would report it, because a search that finds less is not an error.
82
+ *
83
+ * ⚠️ Deliberately NOT `current_version_id = v.id` as well, although that is what would make this
84
+ * the mirror image of `getTarget`. Symmetry is not a reason to write a condition: once the node
85
+ * IS archived, every version of it says the same thing about that node's vectors, so the extra
86
+ * clause could only ever refuse a purge that was right. A condition no test can turn red is a
87
+ * condition the next reader has to guess the purpose of.
88
+ */
89
+ async archivedNodeId(versionId) {
90
+ const row = await db
91
+ .prepare(`SELECT n.id AS node_id
92
+ FROM node_versions v
93
+ JOIN nodes n ON n.id = v.node_id
94
+ WHERE v.id = ? AND n.archived_at IS NOT NULL`)
95
+ .bind(versionId)
96
+ .first();
97
+ return row?.node_id ?? null;
98
+ },
99
+ async listVectors(nodeId) {
100
+ const result = await db
101
+ .prepare(`SELECT chunk_key, fingerprint, passage FROM node_vectors WHERE node_id = ?`)
102
+ .bind(nodeId)
103
+ .all();
104
+ return (result.results ?? []).map((row) => ({
105
+ chunkKey: row.chunk_key,
106
+ fingerprint: row.fingerprint,
107
+ passage: row.passage,
108
+ }));
109
+ },
110
+ /**
111
+ * The record of every vector this node has, replacing the record it had.
112
+ *
113
+ * ⚠️ The same delete-then-insert in ONE batch as `replace` above, and the same guard in front of
114
+ * every statement: a pass for a version that is no longer current, or for a node that has been
115
+ * archived meanwhile, writes nothing at all rather than half a record. What must not happen is
116
+ * the delete landing without the inserts — the next pass would then re-embed the whole board,
117
+ * which is exactly the cost anchrd/intel#301 exists to avoid.
118
+ */
119
+ async replaceVectors(target, records) {
120
+ await db.batch([
121
+ db
122
+ .prepare(`DELETE FROM node_vectors WHERE node_id = ? AND EXISTS (
123
+ SELECT 1 FROM nodes
124
+ WHERE id = ? AND current_version_id = ? AND archived_at IS NULL
125
+ )`)
126
+ .bind(target.nodeId, target.nodeId, target.versionId),
127
+ ...records.map((record) => db
128
+ .prepare(`INSERT INTO node_vectors (node_id, chunk_key, version_id, fingerprint, passage)
129
+ SELECT ?, ?, ?, ?, ? WHERE EXISTS (
130
+ SELECT 1 FROM nodes
131
+ WHERE id = ? AND current_version_id = ? AND archived_at IS NULL
132
+ )`)
133
+ .bind(target.nodeId, record.chunkKey, target.versionId, record.fingerprint, record.passage.slice(0, maxPassageCharacters), target.nodeId, target.versionId)),
134
+ ]);
135
+ },
136
+ /**
137
+ * The record of one node, gone — because its vectors are (anchrd/intel#348).
138
+ *
139
+ * ⚠️ No guard at all, and the absence is deliberate in both directions. There is no
140
+ * `archived_at IS NOT NULL`: the state this write exists to prevent is a row that outlives the
141
+ * vector it names, and a node restored between the deletion and this statement would keep
142
+ * exactly that — a record claiming vectors that are gone, which the next pass reads as "already
143
+ * embedded" and skips, leaving those cards silently unfindable. And there is no version
144
+ * condition: `archivedNodeId` has already established which node this is, from that same
145
+ * version, and a second reading of a moving row is a second answer rather than a safer one.
146
+ */
147
+ async deleteVectors(nodeId) {
148
+ await db.prepare("DELETE FROM node_vectors WHERE node_id = ?").bind(nodeId).run();
149
+ },
71
150
  async markIndexed(versionId, occurredAt) {
72
151
  await db
73
152
  .prepare(`UPDATE node_index_state