@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.
- package/dist/adapters/cloudflare/cloudflare.js +1 -68
- package/dist/adapters/cloudflare/cloudflare.types.d.ts +0 -39
- package/dist/adapters/db/db-flows.js +1 -1
- package/dist/adapters/db/db-grants.js +1 -1
- package/dist/adapters/db/db-indexing.js +79 -0
- package/dist/adapters/db/db.js +81 -139
- package/dist/adapters/semantic-index/semantic-index.js +97 -17
- package/dist/adapters/semantic-index/semantic-index.types.d.ts +20 -1
- package/dist/bundle/bundle.js +42 -134
- package/dist/cli/cli.js +3 -9
- package/dist/http/http.js +5 -206
- package/dist/http/http.types.d.ts +0 -8
- package/dist/indexing/indexing.js +133 -55
- package/dist/indexing/indexing.types.d.ts +1 -0
- package/dist/intel/intel.js +4 -9
- package/dist/intel/intel.types.d.ts +0 -6
- package/dist/mcp/mcp.js +33 -308
- package/dist/mcp/mcp.types.d.ts +2 -7
- package/dist/nodes/document-links/document-links.d.ts +6 -8
- package/dist/nodes/document-links/document-links.js +8 -31
- package/dist/nodes/nodes.js +92 -826
- package/dist/nodes/nodes.types.d.ts +57 -158
- package/dist/tools/tools.js +37 -148
- package/dist/tools/tools.types.d.ts +0 -21
- package/migrations/0009_no_context_policy.sql +15 -0
- package/migrations/0017_a_vector_per_card.sql +38 -0
- package/migrations/0018_no_context_policy_at_last.sql +97 -0
- package/migrations/0019_one_name_for_the_grants.sql +52 -0
- package/package.json +2 -2
- package/dist/adapters/cloudflare-api/cloudflare-api.d.ts +0 -22
- package/dist/adapters/cloudflare-api/cloudflare-api.js +0 -214
- package/dist/adapters/cloudflare-api/cloudflare-api.types.d.ts +0 -64
- package/dist/adapters/cloudflare-api/cloudflare-api.types.js +0 -1
- package/dist/adapters/gate-applications/gate-applications.d.ts +0 -23
- package/dist/adapters/gate-applications/gate-applications.js +0 -88
- package/dist/adapters/tool-delegation/tool-delegation.d.ts +0 -22
- package/dist/adapters/tool-delegation/tool-delegation.js +0 -90
- package/dist/agent-costs/agent-costs.d.ts +0 -16
- package/dist/agent-costs/agent-costs.js +0 -105
- package/dist/agent-costs/agent-costs.types.d.ts +0 -30
- package/dist/agent-costs/agent-costs.types.js +0 -1
- package/dist/agent-runtime/agent-runtime.d.ts +0 -16
- package/dist/agent-runtime/agent-runtime.js +0 -150
- package/dist/agent-runtime/agent-runtime.types.d.ts +0 -122
- package/dist/agent-runtime/agent-runtime.types.js +0 -1
- package/dist/model-catalog/model-catalog.d.ts +0 -2
- package/dist/model-catalog/model-catalog.js +0 -99
- package/dist/model-catalog/model-catalog.types.d.ts +0 -15
- package/dist/model-catalog/model-catalog.types.js +0 -1
- package/dist/nodes/board/board.d.ts +0 -59
- package/dist/nodes/board/board.js +0 -528
- package/dist/nodes/board/board.types.d.ts +0 -31
- package/dist/nodes/board/board.types.js +0 -1
- package/migrations/0013_agents_in_the_tree.sql +0 -76
- package/migrations/0014_agent_applications.sql +0 -25
- package/migrations/0015_tools_delegated_from_a_connection.sql +0 -15
- package/migrations/0016_boards_in_the_tree.sql +0 -80
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
-- #76. `context_policy` leaves the contract, the UI and every MCP answer. The COLUMN stays.
|
|
2
2
|
--
|
|
3
|
+
-- ⚠️ HISTORY, and no longer an explanation of what is possible. The column was finally dropped by
|
|
4
|
+
-- `0018_no_context_policy_at_last.sql` (#86), through an ordinary migration file — the very thing
|
|
5
|
+
-- the conclusion at the bottom of this file says cannot be done. What changed is not D1 but the
|
|
6
|
+
-- recipe: `0005`, `0013` and `0016` worked out that the new table has to be created under the FINAL
|
|
7
|
+
-- name instead of renamed into place, and that `node_links` has to be carried out of the way
|
|
8
|
+
-- because it is the one child declared ON DELETE CASCADE. Read on for the three failures that
|
|
9
|
+
-- produced the lessons; read `0018` for the shape that works.
|
|
10
|
+
--
|
|
3
11
|
-- ⚠️ That is not the intent, it is what D1 permits. SQLite cannot drop a column a CHECK names, and
|
|
4
12
|
-- this one names itself. The way around it is a table rebuild, and `knowledge_nodes` carries six
|
|
5
13
|
-- foreign keys, one of them from itself.
|
|
@@ -22,6 +30,13 @@
|
|
|
22
30
|
-- A table rebuild with foreign keys is therefore not possible inside a migration file. It needs a
|
|
23
31
|
-- session that drives the transaction itself.
|
|
24
32
|
--
|
|
33
|
+
-- ⚠️ THAT CONCLUSION WAS WRONG, and the way it was wrong is worth more than the conclusion. The
|
|
34
|
+
-- three lessons above are all correct; what they did not contain was the fourth — do not RENAME at
|
|
35
|
+
-- all. Create the new table under the final name and insert the rows back under the name the
|
|
36
|
+
-- children have referenced the whole time, and there is nothing left for a deferred check to
|
|
37
|
+
-- complain about at COMMIT. `0005` found it, `0013` and `0016` repeated it, and `0018` used it to
|
|
38
|
+
-- drop this column at last (#86).
|
|
39
|
+
--
|
|
25
40
|
-- What holds instead: the column sits in D1, nothing reads it, and `db.ts` writes a fixed value on
|
|
26
41
|
-- insert because it is NOT NULL without a DEFAULT. The contract does not know it — for every
|
|
27
42
|
-- consumer it is gone. What remains is one dead column, and that is the price of Intel running.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
-- anchrd/intel#301: one vector per board card, and the record of which vectors a node has.
|
|
2
|
+
--
|
|
3
|
+
-- Vectorize is keyed by node id (`adapters/semantic-index`), so a board was ONE vector holding the
|
|
4
|
+
-- whole document while the full-text half had held one row per card since #285. "Where do I stand
|
|
5
|
+
-- with X" is the question a board exists to answer and it is an imprecise one: lexically it reaches
|
|
6
|
+
-- a card only where the searched word is written on it verbatim. A vector id therefore becomes
|
|
7
|
+
-- `<node id>#<task id>` for a board card and stays the bare node id for every other kind — no
|
|
8
|
+
-- existing vector changes its name, so no installation re-embeds its whole tree to get this.
|
|
9
|
+
--
|
|
10
|
+
-- This table is the D1 side of that index and nothing more: which vectors a node has, what text
|
|
11
|
+
-- each one was made from, and the passage a searcher is shown when that vector is the hit. It is
|
|
12
|
+
-- DERIVED like `node_fts` beside it, and `reindex` empties it so that a rebuild really rebuilds.
|
|
13
|
+
--
|
|
14
|
+
-- ⚠️ `fingerprint` is what keeps a board of three hundred cards from costing three hundred
|
|
15
|
+
-- embeddings per save. The indexing pass compares it against the text it is about to embed and
|
|
16
|
+
-- upserts only what changed; a row is written ONLY AFTER the upsert it describes succeeded. That is
|
|
17
|
+
-- why a fingerprint is stored rather than a timestamp: a pass that dies between the two leaves a
|
|
18
|
+
-- row missing, never a row claiming a vector that was never written, so the next pass repairs
|
|
19
|
+
-- itself instead of trusting a lie.
|
|
20
|
+
--
|
|
21
|
+
-- ⚠️ `chunk_key` is `''` for a node that has exactly one vector — every kind but `board`. NULL
|
|
22
|
+
-- would be the honest spelling and is the wrong one: NULLs are distinct from one another inside a
|
|
23
|
+
-- SQLite primary key, so two rows for the same node could both exist and neither would be found by
|
|
24
|
+
-- the other's write.
|
|
25
|
+
--
|
|
26
|
+
-- ⚠️ No foreign key on `node_id`, deliberately, and for the same reason `node_fts` has none: a
|
|
27
|
+
-- derived index must not be able to make a write to `nodes` fail, and every rebuild of `nodes`
|
|
28
|
+
-- (0005, 0013, 0016, 0018) has to carry each declared child through the detour those files describe.
|
|
29
|
+
-- A row that outlives its node is invisible anyway — hydration joins `nodes` and drops what is
|
|
30
|
+
-- archived or gone, exactly as it does for `node_fts`.
|
|
31
|
+
CREATE TABLE node_vectors (
|
|
32
|
+
node_id TEXT NOT NULL,
|
|
33
|
+
chunk_key TEXT NOT NULL,
|
|
34
|
+
version_id TEXT NOT NULL,
|
|
35
|
+
fingerprint TEXT NOT NULL,
|
|
36
|
+
passage TEXT NOT NULL,
|
|
37
|
+
PRIMARY KEY (node_id, chunk_key)
|
|
38
|
+
);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
-- #86: `context_policy` leaves `nodes` for good.
|
|
2
|
+
--
|
|
3
|
+
-- #76 removed it from the contract, the UI and every MCP answer — for every consumer it has been
|
|
4
|
+
-- gone since. The COLUMN stayed because SQLite cannot drop one a CHECK names, and `db.ts` has been
|
|
5
|
+
-- writing the fixed value `'relevant'` into it ever since so that inserting a node works at all.
|
|
6
|
+
-- A dead column plus a line of code serving it: harmless, and ballast the next reader has to be
|
|
7
|
+
-- told about.
|
|
8
|
+
--
|
|
9
|
+
-- ⚠️ THE reason this ticket sat open for weeks is that three earlier attempts failed against
|
|
10
|
+
-- `--remote` while passing locally, and the file that recorded them (`0009_no_context_policy.sql`)
|
|
11
|
+
-- concluded that a table rebuild with foreign keys needs "a session that runs the transaction
|
|
12
|
+
-- itself, not a migration file". That conclusion is out of date: `0005`, `0013` and `0016` each
|
|
13
|
+
-- rebuilt this very table through an ordinary migration file, and `0016` did it against the live
|
|
14
|
+
-- database on 2026-08-08 with 120 nodes, every referencing table coming out with the count it went
|
|
15
|
+
-- in with.
|
|
16
|
+
--
|
|
17
|
+
-- ⚠️ `0013` and `0016` are GONE since #392 — they were the Agent and Board rebuilds, and the
|
|
18
|
+
-- feature was parked (#385). What they worked out is not gone with them: the recipe is in
|
|
19
|
+
-- `packages/api/CLAUDE.md`, which is where the next rebuild reads it, and the three points below
|
|
20
|
+
-- are this file's own copy of it.
|
|
21
|
+
--
|
|
22
|
+
-- ⚠️ One thing about that run is worth knowing before it is quoted as a precedent: the installation
|
|
23
|
+
-- held ZERO `node_links` rows, so the rescue below would have had nothing to rescue and the run
|
|
24
|
+
-- would have passed without exercising it at all. The one row it did carry was created for the
|
|
25
|
+
-- purpose, minutes before, by linking two throwaway documents. The proof was arranged, not found —
|
|
26
|
+
-- and the next person rebuilding this table has to arrange it again, because the installation still
|
|
27
|
+
-- has almost no links.
|
|
28
|
+
--
|
|
29
|
+
-- What the three of them worked out, and what this file copies rather than rediscovers:
|
|
30
|
+
--
|
|
31
|
+
-- 1. `PRAGMA foreign_keys = OFF` is IGNORED by D1 over the HTTP API. Locally miniflare obeys it,
|
|
32
|
+
-- so the integration test was green while the real database answered `FOREIGN KEY constraint
|
|
33
|
+
-- failed`. That is why the proof of a migration here is a run against `--remote`.
|
|
34
|
+
-- 2. The new table is created under the FINAL name rather than built beside the old one and
|
|
35
|
+
-- renamed over it. `ALTER TABLE ... RENAME` makes references FOLLOW the rename, so renaming
|
|
36
|
+
-- the old table out of the way quietly re-points every other table at a table about to be
|
|
37
|
+
-- dropped. Inserting the rows again under the name the children have referenced all along is
|
|
38
|
+
-- what settles them.
|
|
39
|
+
-- 3. `DROP TABLE` on a parent runs an implicit `DELETE FROM` first, and `node_links` is the one
|
|
40
|
+
-- child declared ON DELETE CASCADE — so that delete does not merely flag its rows, it REMOVES
|
|
41
|
+
-- them. They are carried out of the way and put back. That is a rescue, not a decision about
|
|
42
|
+
-- the data. ⚠️ The list of children is in `packages/api/CLAUDE.md` and is READ FROM THE
|
|
43
|
+
-- DATABASE, not from these files: `0011` renamed the tree and SQLite rewrote the `REFERENCES`
|
|
44
|
+
-- clauses of tables older than it, so their current shape is written down nowhere here.
|
|
45
|
+
--
|
|
46
|
+
-- ⚠️ On an empty database neither detour is visible, because nothing points at anything. That is
|
|
47
|
+
-- how `0005`'s first version passed a green suite and then failed against the first database with
|
|
48
|
+
-- content in it, and why the proof for this file is a row count of every referencing table before
|
|
49
|
+
-- and after rather than a migration that merely ran.
|
|
50
|
+
PRAGMA defer_foreign_keys = TRUE;
|
|
51
|
+
|
|
52
|
+
-- Plain holding tables on purpose: no keys, no CHECKs, no foreign keys, and the column set taken
|
|
53
|
+
-- from whatever the live table has. Anything enforced here would only be enforced a second time on
|
|
54
|
+
-- the way back in, and a holding table that can reject a row is a holding table that can lose one.
|
|
55
|
+
CREATE TABLE nodes_carry AS SELECT * FROM nodes;
|
|
56
|
+
CREATE TABLE node_links_carry AS SELECT * FROM node_links;
|
|
57
|
+
|
|
58
|
+
DROP TABLE nodes;
|
|
59
|
+
|
|
60
|
+
-- The same table as `0016` left it, minus one column. Nothing else about it changes: same keys,
|
|
61
|
+
-- same CHECKs, same six kinds — a rebuild is the only way to drop the column, not an invitation to
|
|
62
|
+
-- change anything else while the table is open.
|
|
63
|
+
CREATE TABLE nodes (
|
|
64
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
65
|
+
parent_id TEXT REFERENCES nodes(id),
|
|
66
|
+
kind TEXT NOT NULL CHECK (kind IN ('folder', 'document', 'attachment', 'table')),
|
|
67
|
+
title TEXT NOT NULL CHECK (length(title) BETWEEN 1 AND 240),
|
|
68
|
+
description TEXT CHECK (description IS NULL OR length(description) <= 2000),
|
|
69
|
+
owner_id TEXT NOT NULL,
|
|
70
|
+
current_version_id TEXT,
|
|
71
|
+
created_at TEXT NOT NULL,
|
|
72
|
+
updated_at TEXT NOT NULL,
|
|
73
|
+
archived_at TEXT
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
-- Columns named on both sides rather than `SELECT *`: the holding table still HAS `context_policy`,
|
|
77
|
+
-- and a positional insert would either fail or, worse, shift every value one place to the left.
|
|
78
|
+
INSERT INTO nodes (
|
|
79
|
+
id, parent_id, kind, title, description, owner_id,
|
|
80
|
+
current_version_id, created_at, updated_at, archived_at
|
|
81
|
+
)
|
|
82
|
+
SELECT
|
|
83
|
+
id, parent_id, kind, title, description, owner_id,
|
|
84
|
+
current_version_id, created_at, updated_at, archived_at
|
|
85
|
+
FROM nodes_carry;
|
|
86
|
+
|
|
87
|
+
-- `OR IGNORE` because whether the cascade above actually fired is SQLite's business, not this
|
|
88
|
+
-- migration's: if it did, this puts the rows back; if it did not, each one is already present under
|
|
89
|
+
-- the same primary key and this is a no-op. Either way `node_links` ends up holding exactly what it
|
|
90
|
+
-- held before, which is the only outcome this statement is permitted to have.
|
|
91
|
+
INSERT OR IGNORE INTO node_links SELECT * FROM node_links_carry;
|
|
92
|
+
|
|
93
|
+
DROP TABLE nodes_carry;
|
|
94
|
+
DROP TABLE node_links_carry;
|
|
95
|
+
|
|
96
|
+
CREATE INDEX nodes_parent_idx ON nodes(parent_id, archived_at, title);
|
|
97
|
+
CREATE INDEX nodes_owner_idx ON nodes(owner_id, archived_at);
|
|
@@ -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.
|
|
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.
|
|
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,214 +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
|
-
* The gateway's log entry, read tolerantly.
|
|
41
|
-
*
|
|
42
|
-
* ⚠️ `metadata` arrives as an object on some responses and as a JSON string on others, and neither
|
|
43
|
-
* is documented as the one shape. Both are accepted; anything else means the call is unattributed,
|
|
44
|
-
* which is a state the caller can see rather than a parse failure that blanks the whole window.
|
|
45
|
-
*/
|
|
46
|
-
const LogEntry = z.object({
|
|
47
|
-
cost: z.number().nullish(),
|
|
48
|
-
model: z.string().nullish(),
|
|
49
|
-
created_at: z.string().nullish(),
|
|
50
|
-
metadata: z.union([z.string(), z.record(z.string(), z.unknown())]).nullish(),
|
|
51
|
-
});
|
|
52
|
-
/**
|
|
53
|
-
* ⚠️ `result` is required, only its contents may be null. An optional field would let ANY JSON body
|
|
54
|
-
* parse as an empty page — and an empty page reads as "this agent cost nothing", which is the one
|
|
55
|
-
* answer this whole path exists to avoid giving by accident.
|
|
56
|
-
*/
|
|
57
|
-
const LogResponse = z.object({
|
|
58
|
-
success: z.boolean().nullish(),
|
|
59
|
-
result: z.array(LogEntry).nullable(),
|
|
60
|
-
});
|
|
61
|
-
function readMetadata(raw) {
|
|
62
|
-
if (typeof raw === "string") {
|
|
63
|
-
try {
|
|
64
|
-
const parsed = JSON.parse(raw);
|
|
65
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
66
|
-
? parsed
|
|
67
|
-
: {};
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
return {};
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return raw && typeof raw === "object" && !Array.isArray(raw)
|
|
74
|
-
? raw
|
|
75
|
-
: {};
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* ⚠️ Cloudflare's `properties` are a list of `{property_id, value}` pairs, not fields, and `value`
|
|
79
|
-
* is a string for the scalars and an array of `{unit, price, currency}` for the price. The units are
|
|
80
|
-
* the provider's own wording — "per M input tokens" — so they are matched loosely and never parsed
|
|
81
|
-
* as a contract: an unrecognised unit costs a missing price, and a missing price shows nothing,
|
|
82
|
-
* which is the behaviour #257 asks for anyway.
|
|
83
|
-
*/
|
|
84
|
-
const PriceEntry = z.object({
|
|
85
|
-
unit: z.string(),
|
|
86
|
-
price: z.union([z.number(), z.string()]),
|
|
87
|
-
currency: z.string().nullish(),
|
|
88
|
-
});
|
|
89
|
-
const ModelProperty = z.object({
|
|
90
|
-
property_id: z.string(),
|
|
91
|
-
value: z.union([z.string(), z.number(), z.boolean(), z.array(PriceEntry)]),
|
|
92
|
-
});
|
|
93
|
-
const ModelEntry = z.object({
|
|
94
|
-
name: z.string(),
|
|
95
|
-
properties: z.array(ModelProperty).nullish(),
|
|
96
|
-
});
|
|
97
|
-
// Required for the same reason `LogResponse.result` is: a body this reader does not recognise must
|
|
98
|
-
// not come out as "the account offers no models".
|
|
99
|
-
const ModelResponse = z.object({
|
|
100
|
-
success: z.boolean().nullish(),
|
|
101
|
-
result: z.array(ModelEntry).nullable(),
|
|
102
|
-
});
|
|
103
|
-
function propertyOf(properties, id) {
|
|
104
|
-
return properties.find((property) => property.property_id === id)?.value;
|
|
105
|
-
}
|
|
106
|
-
function priceFor(entries, side) {
|
|
107
|
-
const found = entries.find((entry) => {
|
|
108
|
-
const unit = entry.unit.toLowerCase();
|
|
109
|
-
return unit.includes(side) && unit.includes("token") && /\bm\b|million/.test(unit);
|
|
110
|
-
});
|
|
111
|
-
if (!found)
|
|
112
|
-
return undefined;
|
|
113
|
-
const value = typeof found.price === "string" ? Number(found.price) : found.price;
|
|
114
|
-
return Number.isFinite(value) ? value : undefined;
|
|
115
|
-
}
|
|
116
|
-
function readModel(entry) {
|
|
117
|
-
const properties = entry.properties ?? [];
|
|
118
|
-
const context = Number(propertyOf(properties, "context_window"));
|
|
119
|
-
const rawPrice = propertyOf(properties, "price");
|
|
120
|
-
const prices = Array.isArray(rawPrice) ? rawPrice : [];
|
|
121
|
-
const input = priceFor(prices, "input");
|
|
122
|
-
const output = priceFor(prices, "output");
|
|
123
|
-
return {
|
|
124
|
-
name: entry.name,
|
|
125
|
-
contextTokens: Number.isFinite(context) && context > 0 ? context : null,
|
|
126
|
-
// ⚠️ Both halves or neither. A model shown with an input price and no output price reads as if
|
|
127
|
-
// its answers were free, which is a worse statement than saying nothing.
|
|
128
|
-
price: input !== undefined && output !== undefined
|
|
129
|
-
? { inputPerMillion: input, outputPerMillion: output }
|
|
130
|
-
: null,
|
|
131
|
-
functionCalling: String(propertyOf(properties, "function_calling") ?? "") === "true",
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
export function createCloudflareApi(deps) {
|
|
135
|
-
/**
|
|
136
|
-
* ⚠️ Neither the URL nor the body of a refusal is quoted onward. The URL carries the account id
|
|
137
|
-
* and the body carries whatever Cloudflare wrote about a token; this message is read by a person
|
|
138
|
-
* on a screen and by a model through the MCP surface alike. The status is kept, because it is the
|
|
139
|
-
* whole of what an operator can act on: 401/403 is the token's permissions, 404 is the gateway id,
|
|
140
|
-
* 429 is a limit.
|
|
141
|
-
*/
|
|
142
|
-
async function get(path, query) {
|
|
143
|
-
const url = new URL(`${ApiOrigin}${path}`);
|
|
144
|
-
for (const [key, value] of Object.entries(query))
|
|
145
|
-
url.searchParams.set(key, value);
|
|
146
|
-
let response;
|
|
147
|
-
try {
|
|
148
|
-
response = await deps.fetch(url, {
|
|
149
|
-
headers: { authorization: `Bearer ${deps.token}`, accept: "application/json" },
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
throw new IntelError(502, "cloudflare_api_unreachable", "The Cloudflare API did not answer");
|
|
154
|
-
}
|
|
155
|
-
if (!response.ok) {
|
|
156
|
-
throw new IntelError(502, "cloudflare_api_refused", `The Cloudflare API refused this read (HTTP ${response.status})`);
|
|
157
|
-
}
|
|
158
|
-
return await response.json().catch(() => null);
|
|
159
|
-
}
|
|
160
|
-
return {
|
|
161
|
-
async gatewayCalls(query) {
|
|
162
|
-
const calls = [];
|
|
163
|
-
let partial = false;
|
|
164
|
-
for (let page = 1; page <= MaxPages; page += 1) {
|
|
165
|
-
const body = await get(`/accounts/${encodeURIComponent(deps.accountId)}/ai-gateway/gateways/${encodeURIComponent(deps.gatewayId)}/logs`, {
|
|
166
|
-
page: String(page),
|
|
167
|
-
per_page: String(PerPage),
|
|
168
|
-
start_date: query.since.toISOString(),
|
|
169
|
-
end_date: query.until.toISOString(),
|
|
170
|
-
order_by: "created_at",
|
|
171
|
-
order_by_direction: "desc",
|
|
172
|
-
// ⚠️ Only documented scalar parameters travel. The endpoint also takes a `filters` array
|
|
173
|
-
// whose query encoding Cloudflare documents nowhere — neither the reference nor the
|
|
174
|
-
// curl example shows it — so a guess at it would either be ignored (a slow read) or
|
|
175
|
-
// rejected (no read at all), and there is no way to tell those apart from the status.
|
|
176
|
-
// The agent is therefore picked out below, from the metadata the runtime stamped.
|
|
177
|
-
});
|
|
178
|
-
const parsed = LogResponse.safeParse(body);
|
|
179
|
-
// A shape this reader cannot make sense of is a failure, not an empty window: an empty
|
|
180
|
-
// window reads as "this agent cost nothing".
|
|
181
|
-
if (!parsed.success) {
|
|
182
|
-
throw new IntelError(502, "cloudflare_api_unreadable", "The Cloudflare API answered in a shape this version does not understand");
|
|
183
|
-
}
|
|
184
|
-
const entries = parsed.data.result ?? [];
|
|
185
|
-
for (const entry of entries) {
|
|
186
|
-
const metadata = readMetadata(entry.metadata);
|
|
187
|
-
if (metadata.agentId !== query.agentId)
|
|
188
|
-
continue;
|
|
189
|
-
calls.push({
|
|
190
|
-
runId: typeof metadata.runId === "string" ? metadata.runId : null,
|
|
191
|
-
model: entry.model ?? "",
|
|
192
|
-
cost: entry.cost ?? 0,
|
|
193
|
-
at: entry.created_at ?? query.until.toISOString(),
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
if (entries.length < PerPage)
|
|
197
|
-
return { calls, partial: false };
|
|
198
|
-
partial = page === MaxPages;
|
|
199
|
-
}
|
|
200
|
-
return { calls, partial };
|
|
201
|
-
},
|
|
202
|
-
async workersAiModels() {
|
|
203
|
-
const body = await get(`/accounts/${encodeURIComponent(deps.accountId)}/ai/models/search`, {
|
|
204
|
-
per_page: String(ModelPerPage),
|
|
205
|
-
hide_experimental: "true",
|
|
206
|
-
});
|
|
207
|
-
const parsed = ModelResponse.safeParse(body);
|
|
208
|
-
if (!parsed.success) {
|
|
209
|
-
throw new IntelError(502, "cloudflare_api_unreadable", "The Cloudflare API answered in a shape this version does not understand");
|
|
210
|
-
}
|
|
211
|
-
return (parsed.data.result ?? []).map(readModel);
|
|
212
|
-
},
|
|
213
|
-
};
|
|
214
|
-
}
|
|
@@ -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
|
-
}
|