@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
@@ -0,0 +1,52 @@
1
+ -- #392: the grant table takes the tree's name, and the table nobody reads goes.
2
+ --
3
+ -- Two changes to the same subject, in one file because they are one subject. After Agents and Board
4
+ -- were parked (#385) `tree_grants` is the ONLY grant table left, and it stands beside
5
+ -- `node_versions`, `node_links`, `node_vectors` and `node_index_state` under a name from a different
6
+ -- vocabulary. `node_grants` is the name somebody would guess without looking.
7
+ --
8
+ -- ⚠️ It also closes a split that ran through four layers. D1 said `grants`, the contract says
9
+ -- `ListGrantsInput`/`RevokeGrantInput`, HTTP says `/nodes/:nodeId/grants` — and the MCP surface
10
+ -- alone said `share`. anchrd/intel#396 pulls the tools onto `grant`; this is the other half of the
11
+ -- same move, and afterwards the same thing has the same name everywhere.
12
+ --
13
+ -- ⚠️ A rename is safe HERE and would not be one table up. `ALTER TABLE ... RENAME` makes every
14
+ -- `REFERENCES` clause pointing AT the renamed table follow it — which is the trap the `nodes`
15
+ -- rebuild exists to avoid (see `packages/api/CLAUDE.md`). `tree_grants` is a child: nothing points
16
+ -- at it, so nothing can follow it anywhere. The direction is what makes the difference, not the
17
+ -- statement.
18
+ --
19
+ -- ⚠️ Indexes do NOT follow a rename. They keep working — an index is bound to its table, not to its
20
+ -- table's name — but they keep the old name in `sqlite_master` and would be the last place
21
+ -- `tree_` survives. That is the lesson `0011` wrote down, and dropping and recreating one is free:
22
+ -- an index holds no rows of its own and nothing points at it.
23
+ ALTER TABLE tree_grants RENAME TO node_grants;
24
+
25
+ DROP INDEX tree_grants_principal_idx;
26
+ DROP INDEX tree_grants_node_idx;
27
+
28
+ CREATE INDEX node_grants_principal_idx ON node_grants(
29
+ principal_type,
30
+ principal_id,
31
+ verb,
32
+ expires_at
33
+ );
34
+
35
+ CREATE INDEX node_grants_node_idx ON node_grants(node_id, verb);
36
+
37
+ -- `resource_grants` has been dead since `0003`, which said so in its own header:
38
+ --
39
+ -- > `resource_grants` is deliberately left in place and untouched. The previous version of the
40
+ -- > Worker reads it and keeps answering correctly until it is replaced; nothing in this version
41
+ -- > reads it any more. Once no old Worker is left, the table holds only history and can be
42
+ -- > dropped.
43
+ --
44
+ -- There is no old Worker. The condition has been met for five migrations and nobody announced the
45
+ -- moment, because nothing forced one — and a customer should not receive a schema carrying a table
46
+ -- no code has read since `0003`.
47
+ --
48
+ -- ⚠️ Dropped here rather than removed from `0000`, and that is deliberate. `0003` reads it: it is
49
+ -- the source every `tree_grants` row was migrated FROM. Taking it out of `0000` would leave `0003`
50
+ -- selecting from a table that never existed, so the history has to keep it and the end of the chain
51
+ -- is where it can go. `0001` also deletes rows from it — same reason, same answer.
52
+ DROP TABLE resource_grants;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anchrd/intel-api",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -43,7 +43,7 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@anchrd/gate-sdk": "^0.7.0",
46
- "@anchrd/intel-contract": "^0.12.0",
46
+ "@anchrd/intel-contract": "^0.13.0",
47
47
  "@cfworker/json-schema": "^4.1.1",
48
48
  "@modelcontextprotocol/sdk": "^1.30.0",
49
49
  "fflate": "^0.8.3",
@@ -1,22 +0,0 @@
1
- import type { CloudflareAccountApi } from "./cloudflare-api.types.js";
2
- /**
3
- * ⚠️ `Authorization: Bearer`, and NOT `cf-aig-authorization`. The two hosts take different headers
4
- * and the agent runtime uses the other one: `gateway.ai.cloudflare.com` reads
5
- * `cf-aig-authorization`, this REST host reads the plain header. Swapping them produces a 401 that
6
- * says nothing about which of the two was wrong.
7
- */
8
- export interface CloudflareApiDeps {
9
- accountId: string;
10
- gatewayId: string;
11
- /**
12
- * A Cloudflare API token, read-only by intent.
13
- *
14
- * ⚠️ It is NOT the `AI_GATEWAY_TOKEN` the agent runtime holds. That one carries
15
- * `AI Gateway: Run` and buys inference; this one carries `AI Gateway: Read` (and, for the model
16
- * catalog, `Workers AI: Read`) and buys nothing at all. One credential for both would put a
17
- * spending permission into the Worker that only ever reads.
18
- */
19
- token: string;
20
- fetch: typeof fetch;
21
- }
22
- export declare function createCloudflareApi(deps: CloudflareApiDeps): CloudflareAccountApi;
@@ -1,306 +0,0 @@
1
- import { z } from "zod";
2
- import { IntelError } from "../../shared/intel-error/intel-error.js";
3
- const ApiOrigin = "https://api.cloudflare.com/client/v4";
4
- /**
5
- * How many pages of gateway log are read before the answer is declared a floor.
6
- *
7
- * A run is several model turns and a five-minute schedule is 8 640 runs a month, so "read
8
- * everything" is not on the table. Twenty pages of fifty is the last thirty days of a busy agent
9
- * or the last few days of a very busy one; past that the screen says "at least this much" rather
10
- * than a number nobody can check.
11
- *
12
- * ⚠️ `PerPage` is Cloudflare's ceiling, not a chosen number. The endpoint answers `per_page=100`
13
- * with `HTTP 400` and `Number must be less than or equal to 50`, the adapter turns that into
14
- * `cloudflare_api_refused`, and the screen says `unreadable` — so the whole cost view read nothing,
15
- * ever, and looked like an outage while doing it (#294). Raising it back is not an optimisation;
16
- * it switches the feature off. `MaxPages` carries the reach instead: the product of the two is what
17
- * "the last thousand calls" means, and lowering one without raising the other halves the window
18
- * silently.
19
- */
20
- const MaxPages = 20;
21
- const PerPage = 50;
22
- /**
23
- * ⚠️ Cloudflare's ceiling on `/ai/models/search`, and a DIFFERENT number from `PerPage` above —
24
- * the limit belongs to the endpoint, not to the account. Neither may be copied onto the other's
25
- * call: 50 on the model catalog halves it, 100 on the log is refused outright.
26
- *
27
- * And the two fail in opposite ways, of which this is the worse one. The log endpoint REFUSES with
28
- * `HTTP 400 Number must be less than or equal to 50` — loud, and found in a day (#294). This one
29
- * IGNORES: measured against the live account (#297), `per_page=200` and `per_page=1000` both answer
30
- * `HTTP 200` with no error and `result_info.per_page: 100`.
31
- *
32
- * ⚠️ `result_info.total_count` cannot be used to notice a short answer either. The same account
33
- * reports `total_count: 286` and returns 61 entries on page 1, with page 2 empty — a reader that
34
- * paginated on that figure would loop over empty pages and call the result partial. The truthful
35
- * signal is a page that came back FULL, which the log reader already uses and this one does not
36
- * yet (anchrd/intel#330).
37
- */
38
- const ModelPerPage = 100;
39
- /**
40
- * How many pages of the model catalog are read before the reader gives up.
41
- *
42
- * ⚠️ It is a runaway brake and not a window, which is the opposite of `MaxPages` above. The log has
43
- * more calls than anybody wants to read; the catalog is finite and small — 61 entries on this
44
- * account — so the loop normally ends on the first short page and this number is never reached.
45
- * What it guards against is an endpoint that answers a full page forever, and a loop inside a
46
- * request nobody is watching.
47
- */
48
- const ModelMaxPages = 10;
49
- /**
50
- * The gateway's log entry, read tolerantly.
51
- *
52
- * ⚠️ `metadata` arrives as an object on some responses and as a JSON string on others, and neither
53
- * is documented as the one shape. Both are accepted; anything else means the call is unattributed,
54
- * which is a state the caller can see rather than a parse failure that blanks the whole window.
55
- */
56
- const LogEntry = z.object({
57
- cost: z.number().nullish(),
58
- model: z.string().nullish(),
59
- created_at: z.string().nullish(),
60
- metadata: z.union([z.string(), z.record(z.string(), z.unknown())]).nullish(),
61
- });
62
- /**
63
- * ⚠️ `result` is required, only its contents may be null. An optional field would let ANY JSON body
64
- * parse as an empty page — and an empty page reads as "this agent cost nothing", which is the one
65
- * answer this whole path exists to avoid giving by accident.
66
- */
67
- const LogResponse = z.object({
68
- success: z.boolean().nullish(),
69
- result: z.array(LogEntry).nullable(),
70
- });
71
- function readMetadata(raw) {
72
- if (typeof raw === "string") {
73
- try {
74
- const parsed = JSON.parse(raw);
75
- return parsed && typeof parsed === "object" && !Array.isArray(parsed)
76
- ? parsed
77
- : {};
78
- }
79
- catch {
80
- return {};
81
- }
82
- }
83
- return raw && typeof raw === "object" && !Array.isArray(raw)
84
- ? raw
85
- : {};
86
- }
87
- /**
88
- * ⚠️ Cloudflare's `properties` are a list of `{property_id, value}` pairs, not fields, and `value`
89
- * is a string for the scalars and an array of `{unit, price, currency}` for the price. The units are
90
- * the provider's own wording — "per M input tokens" — so they are matched loosely and never parsed
91
- * as a contract: an unrecognised unit costs a missing price, and a missing price shows nothing,
92
- * which is the behaviour #257 asks for anyway.
93
- */
94
- const PriceEntry = z.object({
95
- unit: z.string(),
96
- price: z.union([z.number(), z.string()]),
97
- currency: z.string().nullish(),
98
- });
99
- const ModelProperty = z.object({
100
- property_id: z.string(),
101
- value: z.union([z.string(), z.number(), z.boolean(), z.array(PriceEntry)]),
102
- });
103
- const ModelEntry = z.object({
104
- name: z.string(),
105
- properties: z.array(ModelProperty).nullish(),
106
- });
107
- // Required for the same reason `LogResponse.result` is: a body this reader does not recognise must
108
- // not come out as "the account offers no models".
109
- const ModelResponse = z.object({
110
- success: z.boolean().nullish(),
111
- result: z.array(ModelEntry).nullable(),
112
- });
113
- function propertyOf(properties, id) {
114
- return properties.find((property) => property.property_id === id)?.value;
115
- }
116
- function priceFor(entries, side) {
117
- const found = entries.find((entry) => {
118
- const unit = entry.unit.toLowerCase();
119
- return unit.includes(side) && unit.includes("token") && /\bm\b|million/.test(unit);
120
- });
121
- if (!found)
122
- return undefined;
123
- const value = typeof found.price === "string" ? Number(found.price) : found.price;
124
- return Number.isFinite(value) ? value : undefined;
125
- }
126
- function readModel(entry) {
127
- const properties = entry.properties ?? [];
128
- const context = Number(propertyOf(properties, "context_window"));
129
- const rawPrice = propertyOf(properties, "price");
130
- const prices = Array.isArray(rawPrice) ? rawPrice : [];
131
- const input = priceFor(prices, "input");
132
- const output = priceFor(prices, "output");
133
- return {
134
- name: entry.name,
135
- contextTokens: Number.isFinite(context) && context > 0 ? context : null,
136
- // ⚠️ Both halves or neither. A model shown with an input price and no output price reads as if
137
- // its answers were free, which is a worse statement than saying nothing.
138
- price: input !== undefined && output !== undefined
139
- ? { inputPerMillion: input, outputPerMillion: output }
140
- : null,
141
- functionCalling: String(propertyOf(properties, "function_calling") ?? "") === "true",
142
- };
143
- }
144
- export function createCloudflareApi(deps) {
145
- /**
146
- * ⚠️ Neither the URL nor the body of a refusal is quoted onward. The URL carries the account id
147
- * and the body carries whatever Cloudflare wrote about a token; this message is read by a person
148
- * on a screen and by a model through the MCP surface alike. The status is kept, because it is the
149
- * whole of what an operator can act on: 401/403 is the token's permissions, 404 is the gateway id,
150
- * 429 is a limit.
151
- */
152
- async function get(path, query) {
153
- const url = new URL(`${ApiOrigin}${path}`);
154
- for (const [key, value] of Object.entries(query))
155
- url.searchParams.set(key, value);
156
- let response;
157
- try {
158
- response = await deps.fetch(url, {
159
- headers: { authorization: `Bearer ${deps.token}`, accept: "application/json" },
160
- });
161
- }
162
- catch {
163
- throw new IntelError(502, "cloudflare_api_unreachable", "The Cloudflare API did not answer");
164
- }
165
- if (!response.ok) {
166
- throw new IntelError(502, "cloudflare_api_refused", `The Cloudflare API refused this read (HTTP ${response.status})`);
167
- }
168
- return await response.json().catch(() => null);
169
- }
170
- return {
171
- async gatewayCalls(query) {
172
- const logPath = `/accounts/${encodeURIComponent(deps.accountId)}/ai-gateway/gateways/${encodeURIComponent(deps.gatewayId)}/logs`;
173
- /**
174
- * One window of the log, optionally cut to this agent by the gateway itself.
175
- *
176
- * The encoding Cloudflare documents nowhere, measured against the live gateway (#274):
177
- * `filters` is a URL-encoded JSON array, and `value` is an ARRAY even for a single value.
178
- *
179
- * filters=[{"key":"metadata.value","operator":"eq","value":["<id>"]}] → 200, filtered
180
- * value as a scalar string → 400 `Expected array, received string`
181
- * filters as a JSON object → 400 `Expected array, received object`
182
- * filters[0][key]=… (brackets) and filters.0.key=… (dots) → 200, SILENTLY IGNORED
183
- *
184
- * ⚠️ The last line is why this was never guessed at: two of the five spellings answer 200
185
- * with the whole unfiltered window, which reads exactly like a filter that matched
186
- * everything. Two filters combine with AND (measured with `metadata.key` plus `runId`).
187
- */
188
- async function read(pages, filtered) {
189
- const calls = [];
190
- let partial = false;
191
- for (let index = 1; index <= pages; index += 1) {
192
- const body = await get(logPath, {
193
- page: String(index),
194
- per_page: String(PerPage),
195
- start_date: query.since.toISOString(),
196
- end_date: query.until.toISOString(),
197
- order_by: "created_at",
198
- order_by_direction: "desc",
199
- ...(filtered
200
- ? {
201
- filters: JSON.stringify([
202
- { key: "metadata.value", operator: "eq", value: [query.agentId] },
203
- ]),
204
- }
205
- : {}),
206
- });
207
- const parsed = LogResponse.safeParse(body);
208
- // A shape this reader cannot make sense of is a failure, not an empty window: an empty
209
- // window reads as "this agent cost nothing".
210
- if (!parsed.success) {
211
- throw new IntelError(502, "cloudflare_api_unreadable", "The Cloudflare API answered in a shape this version does not understand");
212
- }
213
- const entries = parsed.data.result ?? [];
214
- // ⚠️ The local check stays even under the server-side filter, and it is no longer belt
215
- // and braces: it is the only thing that could notice the filter matching the wrong rows.
216
- for (const entry of entries) {
217
- const metadata = readMetadata(entry.metadata);
218
- if (metadata.agentId !== query.agentId)
219
- continue;
220
- calls.push({
221
- runId: typeof metadata.runId === "string" ? metadata.runId : null,
222
- model: entry.model ?? "",
223
- cost: entry.cost ?? 0,
224
- at: entry.created_at ?? query.until.toISOString(),
225
- });
226
- }
227
- if (entries.length < PerPage)
228
- return { calls, partial: false };
229
- partial = index === pages;
230
- }
231
- return { calls, partial };
232
- }
233
- const answer = await read(MaxPages, true);
234
- /**
235
- * ⚠️ Anything at all is taken at face value, and that is a decision with a hole in it. A
236
- * filter that degraded PARTLY — matching some of this agent's rows and not others — returns
237
- * here, and the under-count is then handed out as `partial: false`, which claims to be a
238
- * total. The probe below cannot see that case, because the probe is only reached when the
239
- * filtered read found nothing. It is the price of not reading the whole log twice on every
240
- * request, and it is worth knowing before somebody reads `partial: false` as "complete".
241
- */
242
- if (answer.calls.length > 0)
243
- return answer;
244
- /**
245
- * ⚠️ Zero is the ONE answer this reader may not take at face value, and the reason is the
246
- * way `filters` fails. Measured: `cached` filtered to `["false"]` answers 200 with zero rows
247
- * although every row in that window carries `cached: false` — a value the index does not
248
- * match empties the page instead of being refused. So the day metadata stops being indexed
249
- * the way it is today, this would report "cost nothing" for an agent that spent money, under
250
- * `status: "read"`. That is precisely the sentence #251 exists for.
251
- *
252
- * One unfiltered page is the probe: if the newest calls contain any of this agent's, the
253
- * filter is lying and the honest answer is that the log could not be read. It costs one
254
- * request and only in the empty case, which is a fresh agent or a quiet window — and in the
255
- * case it replaces, a quiet agent on a busy gateway, it costs 2 requests where reading the
256
- * whole log unfiltered used to cost 20.
257
- *
258
- * ⚠️ It is a probe and not a proof: an agent whose only calls are older than the newest
259
- * `PerPage` of the whole gateway is invisible to it, and that case still reports zero. What
260
- * it buys is that a filter which stopped working ENTIRELY cannot pass as silence.
261
- *
262
- * ⚠️ A probe that cannot run leaves the answer standing, deliberately. The read this is
263
- * checking SUCCEEDED and said "no calls"; letting a second opinion that never arrived turn
264
- * that into `unreadable` would report an outage for the most ordinary state there is — a
265
- * fresh agent — because one extra request met a 429. The guard is a second opinion, not the
266
- * answer.
267
- */
268
- const probe = await read(1, false).catch(() => null);
269
- if (probe === null || probe.calls.length === 0)
270
- return answer;
271
- throw new IntelError(502, "cloudflare_api_unreadable", "The Cloudflare API answered no calls for this agent while its own log holds some");
272
- },
273
- async workersAiModels() {
274
- const models = [];
275
- for (let index = 1; index <= ModelMaxPages; index += 1) {
276
- const body = await get(`/accounts/${encodeURIComponent(deps.accountId)}/ai/models/search`, {
277
- page: String(index),
278
- per_page: String(ModelPerPage),
279
- hide_experimental: "true",
280
- });
281
- const parsed = ModelResponse.safeParse(body);
282
- if (!parsed.success) {
283
- throw new IntelError(502, "cloudflare_api_unreadable", "The Cloudflare API answered in a shape this version does not understand");
284
- }
285
- const entries = parsed.data.result ?? [];
286
- models.push(...entries.map(readModel));
287
- // ⚠️ A SHORT page is the end, and a full one is not a reason to believe the answer was
288
- // complete (#330). Before this the reader took page one and stopped: a model past position
289
- // 100 was simply absent from the select, `liveStatus` still said `read`, and nothing said
290
- // the list was shorter than the account. A missing entry has no `source` to mark stale
291
- // with, so the per-entry labelling that covers a fallback cannot cover this at all.
292
- //
293
- // ⚠️ `result_info.total_count` is NOT the test, which is why it is not parsed. Measured on
294
- // the live account (#297): it reports 286 while page one returns 61 and page two is empty.
295
- // A reader that paginated on that figure would loop over empty pages and call the result
296
- // partial. The full page is the only honest signal.
297
- if (entries.length < ModelPerPage)
298
- return models;
299
- }
300
- // Ten full pages is not a big account, it is an endpoint that stopped ending. Refusing is the
301
- // honest answer: the catalog service falls back to its built-in table and the screen says the
302
- // figures may be old — better than a list that is silently missing whatever came after.
303
- throw new IntelError(502, "cloudflare_api_unreadable", "The Cloudflare API kept answering full pages of models past the read limit");
304
- },
305
- };
306
- }
@@ -1,64 +0,0 @@
1
- /**
2
- * The two things Intel reads out of the Cloudflare account, and nothing else.
3
- *
4
- * ⚠️ Both are READS, and the port says so by having no other verb. The token behind it is
5
- * account-wide — Cloudflare offers no per-gateway scope for `AI Gateway: Read`, the same limitation
6
- * `AI Gateway: Run` already had in #239 — so the narrowness has to come from what this interface
7
- * can express rather than from what the credential allows.
8
- */
9
- export interface CloudflareAccountApi {
10
- /**
11
- * The gateway's own log lines for one agent, within a window.
12
- *
13
- * ⚠️ `cost` here is the **debit from the Cloudflare balance**, 1:1 — measured on 2026-08-07
14
- * against the running installation: balance $19.77 + spend $0.23 = the $20.00 that was loaded.
15
- * Cloudflare takes its 5 % when the balance is topped up and passes inference through without a
16
- * markup, so this number means "what this costs us" and needs no conversion. A reader who
17
- * multiplied it by anything would be inventing a second, wrong price.
18
- */
19
- gatewayCalls(query: GatewayCallQuery): Promise<GatewayCallPage>;
20
- /**
21
- * What Cloudflare currently charges for the models it serves itself (#257).
22
- *
23
- * ⚠️ Workers AI only. Cloudflare publishes no price list for the Anthropic models it resells
24
- * through Unified Billing, so those figures have no live source and stay a table — which is the
25
- * whole reason the catalog says, per entry, where its numbers came from.
26
- */
27
- workersAiModels(): Promise<WorkersAiModel[]>;
28
- }
29
- export interface GatewayCallQuery {
30
- /** The value stamped as `cf-aig-metadata.agentId` by the agent runtime. */
31
- agentId: string;
32
- since: Date;
33
- until: Date;
34
- }
35
- export interface GatewayCall {
36
- /** From `cf-aig-metadata.runId`. `null` for a call made before the stamp existed. */
37
- runId: string | null;
38
- model: string;
39
- /** US dollars, as billed. */
40
- cost: number;
41
- at: string;
42
- }
43
- export interface GatewayCallPage {
44
- calls: GatewayCall[];
45
- /**
46
- * The window was cut off at the page cap, so every total built from it is a floor rather than a
47
- * total.
48
- *
49
- * ⚠️ It exists so the screen can say "at least". A sum that silently stopped counting is the same
50
- * failure as a missing number pretending to be zero, only harder to notice.
51
- */
52
- partial: boolean;
53
- }
54
- export interface WorkersAiModel {
55
- /** The full `@cf/...` id, exactly as a definition names it. */
56
- name: string;
57
- contextTokens: number | null;
58
- price: {
59
- inputPerMillion: number;
60
- outputPerMillion: number;
61
- } | null;
62
- /** Whether this model can call a tool at all. An agent is a tool loop; one that cannot is useless. */
63
- functionCalling: boolean;
64
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,23 +0,0 @@
1
- import type { AgentApplications } from "../../nodes/nodes.types.js";
2
- /**
3
- * Intel's door to Gate's Applications surface (`anchrd/gate#223`/`#224`, Gate 0.10.x).
4
- *
5
- * ⚠️ This is the one Gate call Intel makes with the CALLER's bearer instead of its service key, and
6
- * that is not a shortcut — the service key does not open this door. A Gate service key authorizes
7
- * `/api/v1/authorization` and `/api/v1/schema` and nothing else (Gate's two-token rule); the
8
- * Applications routes are admin-gated and resolve a real principal, so the person creating an agent
9
- * needs `applications:write` in Gate and their act is audited in Gate under their own name. A
10
- * service key here would have made Intel the author of every machine principal an installation ever
11
- * grew, which is the opposite of what an audit trail is for.
12
- *
13
- * ⚠️ Nothing this module receives from Gate is logged, wrapped into a message, or returned other
14
- * than through the one typed answer below. `create` is handed a plain-text key, and the shortest
15
- * path from here to a leak is an error that quotes the response body — so no refusal names anything
16
- * but the status Gate answered with.
17
- */
18
- export interface GateApplicationsDeps {
19
- fetch: (url: string, init?: RequestInit) => Promise<Response>;
20
- gateUrl: string;
21
- timeoutMs?: number;
22
- }
23
- export declare function createGateApplications(deps: GateApplicationsDeps): AgentApplications;
@@ -1,88 +0,0 @@
1
- import { z } from "zod";
2
- import { IntelError } from "../../shared/intel-error/intel-error.js";
3
- // Tolerant on purpose, unlike Intel's own contracts: this is somebody else's wire format, and a
4
- // field Gate adds tomorrow must not stop an installation from creating an agent. Only what Intel
5
- // actually reads is named.
6
- const CreatedApplication = z.object({ id: z.string().min(1), key: z.string().min(1) });
7
- // The same tolerance, for the same reason. Only the key is read: the ID is the one Intel asked with.
8
- const RotatedApplication = z.object({ key: z.string().min(1) });
9
- const DefaultTimeoutMs = 10_000;
10
- export function createGateApplications(deps) {
11
- const base = deps.gateUrl.replace(/\/+$/, "");
12
- const timeoutMs = deps.timeoutMs ?? DefaultTimeoutMs;
13
- // Gate's refusals, translated once. A 401/403 is the caller's missing `applications:write` and is
14
- // permanent until somebody acts in Gate, so it is answered as a refusal rather than as an outage;
15
- // everything else — a 5xx, a timeout, a DNS failure — is "Gate did not answer", and the caller is
16
- // told that no agent was created rather than left to guess.
17
- function refusal(status) {
18
- if (status === 401 || status === 403) {
19
- return new IntelError(403, "agent_application_forbidden", "Gate refused this account the management of applications — creating an agent needs the applications permission in Gate");
20
- }
21
- return new IntelError(502, "agent_application_unavailable", `Gate could not manage this agent's application (${status})`);
22
- }
23
- async function call(path, token, body) {
24
- try {
25
- return await deps.fetch(`${base}/api/v1/applications${path}`, {
26
- method: "POST",
27
- headers: {
28
- "content-type": "application/json",
29
- authorization: `Bearer ${token}`,
30
- },
31
- body: JSON.stringify(body),
32
- signal: AbortSignal.timeout(timeoutMs),
33
- });
34
- }
35
- catch {
36
- // ⚠️ The caught error is dropped rather than described. A fetch failure carries the URL, and
37
- // the URL is the one place the bearer could still be if a caller ever put it in a query.
38
- throw new IntelError(502, "agent_application_unavailable", "Gate did not answer, so no agent application was created or changed");
39
- }
40
- }
41
- return {
42
- async create(input) {
43
- const response = await call("", input.token, { name: input.name });
44
- if (!response.ok)
45
- throw refusal(response.status);
46
- const parsed = CreatedApplication.safeParse(await response.json().catch(() => null));
47
- if (!parsed.success) {
48
- // ⚠️ A 2xx Intel cannot read means a principal MAY exist in Gate that Intel cannot record.
49
- // Nothing has been written on this side yet, so the agent does not come into being; the
50
- // operator finds an unused application in Gate's list rather than an agent that half works.
51
- throw new IntelError(502, "agent_application_unavailable", "Gate answered the application creation in a shape Intel cannot read");
52
- }
53
- return { id: parsed.data.id, key: parsed.data.key };
54
- },
55
- async rotateKey(input) {
56
- // Gate issues the replacement FIRST and only then revokes what was there, so a rotation that
57
- // fails leaves the old key working rather than locking the agent out (`applications.ts` in
58
- // `anchrd/gate`). Intel relies on that: the handover to the runtime happens after this call,
59
- // and until it succeeds the agent keeps running on the key it had.
60
- const response = await call(`/${encodeURIComponent(input.applicationId)}/rotate-key`, input.token, {});
61
- if (response.status === 404) {
62
- throw new IntelError(502, "agent_application_missing", "Gate does not know this agent's application any more");
63
- }
64
- if (!response.ok)
65
- throw refusal(response.status);
66
- const parsed = RotatedApplication.safeParse(await response.json().catch(() => null));
67
- if (!parsed.success) {
68
- // ⚠️ A 2xx Intel cannot read means the OLD key is already revoked in Gate and the new one is
69
- // lost. The agent is broken either way, so the caller is told the rotation failed and rotates
70
- // again — which is safe, because rotating twice is just another new key.
71
- throw new IntelError(502, "agent_application_unavailable", "Gate answered the key rotation in a shape Intel cannot read");
72
- }
73
- return { key: parsed.data.key };
74
- },
75
- async setEnabled(input) {
76
- // Both routes are idempotent in Gate, which is what lets a retried archive heal a run that
77
- // failed between the two writes instead of needing a repair path of its own.
78
- const response = await call(`/${encodeURIComponent(input.applicationId)}/${input.enabled ? "enable" : "disable"}`, input.token, {});
79
- // A 404 is the one status worth separating: the Application behind this agent is gone from
80
- // Gate, and telling the operator that is more use than a generic outage they would retry.
81
- if (response.status === 404) {
82
- throw new IntelError(502, "agent_application_missing", "Gate does not know this agent's application any more");
83
- }
84
- if (!response.ok)
85
- throw refusal(response.status);
86
- },
87
- };
88
- }
@@ -1,22 +0,0 @@
1
- import type { ContentStore } from "../../nodes/nodes.types.js";
2
- import type { ToolAuditEvent, ToolDelegation } from "../../tools/tools.types.js";
3
- import type { D1Database } from "../db/db.types.js";
4
- /**
5
- * The two D1 statements the delegated tool path needs (D30): who is acting, and a record that they
6
- * did.
7
- *
8
- * ⚠️ Its own adapter rather than a method on the node repository, and deliberately so. The question
9
- * is asked with a **Gate Application id** and no Intel actor at all — there is no ACL to apply,
10
- * because the answer is not "may you read this agent" but "which agent are you". Hanging it off the
11
- * tree's repository would put an unauthorized read next to authorized ones, which is the shape a
12
- * later reader copies by accident.
13
- */
14
- export declare function createToolDelegation(deps: {
15
- db: D1Database;
16
- content: ContentStore;
17
- id(): string;
18
- now(): Date;
19
- }): {
20
- resolve(applicationId: string): Promise<ToolDelegation | null>;
21
- audit(event: ToolAuditEvent): Promise<void>;
22
- };