@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.
- package/dist/adapters/cloudflare/cloudflare.js +0 -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.js +16 -113
- package/dist/bundle/bundle.js +28 -141
- package/dist/cli/cli.js +3 -9
- package/dist/http/http.js +1 -206
- package/dist/http/http.types.d.ts +0 -8
- package/dist/indexing/indexing.js +18 -89
- package/dist/intel/intel.js +4 -9
- package/dist/intel/intel.types.d.ts +0 -6
- package/dist/mcp/mcp.js +3 -292
- 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 +20 -886
- package/dist/nodes/nodes.types.d.ts +10 -169
- package/dist/tools/tools.js +37 -148
- package/dist/tools/tools.types.d.ts +0 -21
- package/migrations/0018_no_context_policy_at_last.sql +13 -6
- 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 -306
- 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 -61
- package/dist/nodes/board/board.js +0 -826
- package/dist/nodes/board/board.types.d.ts +0 -38
- 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,90 +0,0 @@
|
|
|
1
|
-
import { AgentDefinition } from "@anchrd/intel-contract";
|
|
2
|
-
import { reportUnexpectedError } from "../../shared/report-unexpected-error/report-unexpected-error.js";
|
|
3
|
-
/**
|
|
4
|
-
* The two D1 statements the delegated tool path needs (D30): who is acting, and a record that they
|
|
5
|
-
* did.
|
|
6
|
-
*
|
|
7
|
-
* ⚠️ Its own adapter rather than a method on the node repository, and deliberately so. The question
|
|
8
|
-
* is asked with a **Gate Application id** and no Intel actor at all — there is no ACL to apply,
|
|
9
|
-
* because the answer is not "may you read this agent" but "which agent are you". Hanging it off the
|
|
10
|
-
* tree's repository would put an unauthorized read next to authorized ones, which is the shape a
|
|
11
|
-
* later reader copies by accident.
|
|
12
|
-
*/
|
|
13
|
-
export function createToolDelegation(deps) {
|
|
14
|
-
return {
|
|
15
|
-
/**
|
|
16
|
-
* ⚠️ `null` has exactly ONE meaning here and it is a strong claim: "this caller is not an
|
|
17
|
-
* agent", after which the tool service treats them as an ordinary person with a portal
|
|
18
|
-
* connection of their own. Every other outcome — an archived agent, an agent with no definition
|
|
19
|
-
* version yet, a body R2 lost, a body that will not parse, a definition with no `tools` — is an
|
|
20
|
-
* agent that delegates NOTHING, and is answered as an empty delegation. That is the difference
|
|
21
|
-
* between "act as yourself" and "act on nobody's connection", and only one of them is safe to
|
|
22
|
-
* fall into by accident.
|
|
23
|
-
*
|
|
24
|
-
* The joins are therefore LEFT: a row in `agent_applications` is already the whole answer to
|
|
25
|
-
* "is this an agent", and losing it to a missing version would turn a fresh agent into a user.
|
|
26
|
-
*/
|
|
27
|
-
async resolve(applicationId) {
|
|
28
|
-
const row = await deps.db
|
|
29
|
-
.prepare(`SELECT a.node_id AS node_id, n.archived_at AS archived_at,
|
|
30
|
-
v.content_key AS content_key
|
|
31
|
-
FROM agent_applications a
|
|
32
|
-
LEFT JOIN nodes n ON n.id = a.node_id
|
|
33
|
-
LEFT JOIN node_versions v ON v.id = n.current_version_id
|
|
34
|
-
WHERE a.application_id = ?`)
|
|
35
|
-
.bind(applicationId)
|
|
36
|
-
.first();
|
|
37
|
-
if (!row)
|
|
38
|
-
return null;
|
|
39
|
-
const nothing = { agentId: row.node_id, delegatedBy: "", servers: [] };
|
|
40
|
-
// Archiving an agent switches its Gate Application off, but Gate's own state is not what
|
|
41
|
-
// Intel is allowed to depend on: an archived agent reaches nothing through Intel from the
|
|
42
|
-
// moment it is archived, whatever another system still thinks.
|
|
43
|
-
if (row.archived_at !== null || row.content_key === null)
|
|
44
|
-
return nothing;
|
|
45
|
-
const body = await deps.content.get(row.content_key);
|
|
46
|
-
if (body === null) {
|
|
47
|
-
reportUnexpectedError(new Error(`agent ${row.node_id} has no readable definition body`));
|
|
48
|
-
return nothing;
|
|
49
|
-
}
|
|
50
|
-
let parsed;
|
|
51
|
-
try {
|
|
52
|
-
parsed = JSON.parse(body);
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
parsed = null;
|
|
56
|
-
}
|
|
57
|
-
const definition = AgentDefinition.safeParse(parsed);
|
|
58
|
-
if (!definition.success) {
|
|
59
|
-
reportUnexpectedError(new Error(`agent ${row.node_id} has a definition Intel cannot parse`));
|
|
60
|
-
return nothing;
|
|
61
|
-
}
|
|
62
|
-
const tools = definition.data.tools;
|
|
63
|
-
if (!tools)
|
|
64
|
-
return nothing;
|
|
65
|
-
return {
|
|
66
|
-
agentId: row.node_id,
|
|
67
|
-
delegatedBy: tools.delegatedBy,
|
|
68
|
-
servers: tools.servers,
|
|
69
|
-
};
|
|
70
|
-
},
|
|
71
|
-
async audit(event) {
|
|
72
|
-
// ⚠️ The agent's node is the resource and the Application is the actor, which is the only
|
|
73
|
-
// pairing that reads correctly later: `actor_id` is what authenticated, `resource_id` is what
|
|
74
|
-
// a person searches for. The tool and the server are metadata, and the ARGUMENTS are absent —
|
|
75
|
-
// an append-only table is the last place a payload should be copied to.
|
|
76
|
-
await deps.db
|
|
77
|
-
.prepare(`INSERT INTO audit_events (
|
|
78
|
-
id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
|
|
79
|
-
) VALUES (?, ?, 'tool.execute', 'agent', ?, ?, ?)`)
|
|
80
|
-
.bind(deps.id(), event.applicationId, event.agentId, JSON.stringify({
|
|
81
|
-
tool: event.tool,
|
|
82
|
-
server: event.server,
|
|
83
|
-
// Both principals, which is the whole point of the row: who acted, and whose portal
|
|
84
|
-
// connection made it possible (D30).
|
|
85
|
-
delegatedBy: event.delegatedBy,
|
|
86
|
-
}), deps.now().toISOString())
|
|
87
|
-
.run();
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { AgentCostsDeps, AgentCostsService } from "./agent-costs.types.js";
|
|
2
|
-
/**
|
|
3
|
-
* What this agent has cost, read out of Cloudflare's AI Gateway log rather than computed here.
|
|
4
|
-
*
|
|
5
|
-
* ⚠️ Nothing in this file multiplies tokens by a price, and nothing may start to. The gateway
|
|
6
|
-
* publishes the billed figure per call and it is the debit from the account balance, 1:1 — measured
|
|
7
|
-
* on 2026-08-07 (#251). A price table beside it would be a second, self-maintained answer to a
|
|
8
|
-
* question that already has a first one, and the day the two disagree the wrong one is the one
|
|
9
|
-
* on screen.
|
|
10
|
-
*
|
|
11
|
-
* ⚠️ The three checks below are in the same order as the runtime proxy's, and for the same reasons:
|
|
12
|
-
* the capability first so a refusal costs no storage read; the resource ACL second, because seeing
|
|
13
|
-
* an agent and seeing its bill are the same question; the reader last, so nobody learns whether this
|
|
14
|
-
* deployment holds a Cloudflare token before they are allowed to ask.
|
|
15
|
-
*/
|
|
16
|
-
export declare function createAgentCosts(deps: AgentCostsDeps): AgentCostsService;
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { IntelError } from "../shared/intel-error/intel-error.js";
|
|
2
|
-
/**
|
|
3
|
-
* The windows the profile shows, longest last.
|
|
4
|
-
*
|
|
5
|
-
* Two, and both of them, because they answer different questions: thirty days is what an agent
|
|
6
|
-
* costs to keep, seven is whether that changed. One number alone cannot say "this got more
|
|
7
|
-
* expensive last week".
|
|
8
|
-
*/
|
|
9
|
-
const Windows = [7, 30];
|
|
10
|
-
const DayMs = 24 * 60 * 60 * 1_000;
|
|
11
|
-
/**
|
|
12
|
-
* What this agent has cost, read out of Cloudflare's AI Gateway log rather than computed here.
|
|
13
|
-
*
|
|
14
|
-
* ⚠️ Nothing in this file multiplies tokens by a price, and nothing may start to. The gateway
|
|
15
|
-
* publishes the billed figure per call and it is the debit from the account balance, 1:1 — measured
|
|
16
|
-
* on 2026-08-07 (#251). A price table beside it would be a second, self-maintained answer to a
|
|
17
|
-
* question that already has a first one, and the day the two disagree the wrong one is the one
|
|
18
|
-
* on screen.
|
|
19
|
-
*
|
|
20
|
-
* ⚠️ The three checks below are in the same order as the runtime proxy's, and for the same reasons:
|
|
21
|
-
* the capability first so a refusal costs no storage read; the resource ACL second, because seeing
|
|
22
|
-
* an agent and seeing its bill are the same question; the reader last, so nobody learns whether this
|
|
23
|
-
* deployment holds a Cloudflare token before they are allowed to ask.
|
|
24
|
-
*/
|
|
25
|
-
export function createAgentCosts(deps) {
|
|
26
|
-
return {
|
|
27
|
-
async read(actor, agentId) {
|
|
28
|
-
if (!actor.canRun) {
|
|
29
|
-
throw new IntelError(403, "permission_required", "Permission required");
|
|
30
|
-
}
|
|
31
|
-
const node = await deps.visibleNode(actor, agentId);
|
|
32
|
-
// Not found and not visible answer alike, as everywhere else in the tree.
|
|
33
|
-
if (node?.kind !== "agent") {
|
|
34
|
-
throw new IntelError(404, "agent_not_found", "Agent was not found");
|
|
35
|
-
}
|
|
36
|
-
if (!deps.gateway)
|
|
37
|
-
return empty("not_configured");
|
|
38
|
-
const until = deps.now();
|
|
39
|
-
const longest = Windows[Windows.length - 1] ?? 30;
|
|
40
|
-
const since = new Date(until.getTime() - longest * DayMs);
|
|
41
|
-
let page;
|
|
42
|
-
try {
|
|
43
|
-
page = await deps.gateway.gatewayCalls({ agentId, since, until });
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
// ⚠️ Swallowed on purpose, and named rather than rethrown. The run list beside these figures
|
|
47
|
-
// is answered by the runtime and is perfectly readable; letting a Cloudflare outage take the
|
|
48
|
-
// whole screen down would hide the tokens too — and the tokens are the half that never
|
|
49
|
-
// depended on Cloudflare being reachable.
|
|
50
|
-
return empty("unreadable");
|
|
51
|
-
}
|
|
52
|
-
return {
|
|
53
|
-
status: "read",
|
|
54
|
-
currency: "USD",
|
|
55
|
-
runs: perRun(page.calls),
|
|
56
|
-
windows: Windows.map((days) => window(days, page.calls, until)),
|
|
57
|
-
partial: page.partial,
|
|
58
|
-
};
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
function empty(status) {
|
|
63
|
-
return {
|
|
64
|
-
status,
|
|
65
|
-
currency: "USD",
|
|
66
|
-
// ⚠️ Empty, and the status is what stops it reading as "free". Nothing here invents a zero:
|
|
67
|
-
// `runs: []` with `status: "read"` really does mean this agent has cost nothing yet.
|
|
68
|
-
runs: [],
|
|
69
|
-
windows: Windows.map((days) => ({ days, cost: 0, calls: 0, models: [] })),
|
|
70
|
-
partial: false,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
function perRun(calls) {
|
|
74
|
-
const byRun = new Map();
|
|
75
|
-
for (const call of calls) {
|
|
76
|
-
// A call the runtime could not stamp — anything from before #251 — belongs to no run and is
|
|
77
|
-
// dropped from the per-run view. It still counts towards the windows below: it is the agent's
|
|
78
|
-
// money either way, and only its place in the list is unknown.
|
|
79
|
-
if (!call.runId)
|
|
80
|
-
continue;
|
|
81
|
-
const known = byRun.get(call.runId);
|
|
82
|
-
if (known) {
|
|
83
|
-
known.cost += call.cost;
|
|
84
|
-
known.calls += 1;
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
byRun.set(call.runId, { runId: call.runId, cost: call.cost, calls: 1 });
|
|
88
|
-
}
|
|
89
|
-
return [...byRun.values()];
|
|
90
|
-
}
|
|
91
|
-
function window(days, calls, until) {
|
|
92
|
-
const from = until.getTime() - days * DayMs;
|
|
93
|
-
const inside = calls.filter((call) => {
|
|
94
|
-
const at = Date.parse(call.at);
|
|
95
|
-
// An unparseable timestamp counts towards the longest window rather than being dropped: the
|
|
96
|
-
// money was spent, and a total that quietly omits it is the wrong kind of wrong.
|
|
97
|
-
return Number.isNaN(at) ? days === Windows[Windows.length - 1] : at >= from;
|
|
98
|
-
});
|
|
99
|
-
return {
|
|
100
|
-
days,
|
|
101
|
-
cost: inside.reduce((total, call) => total + call.cost, 0),
|
|
102
|
-
calls: inside.length,
|
|
103
|
-
models: [...new Set(inside.map((call) => call.model).filter((model) => model.length > 0))],
|
|
104
|
-
};
|
|
105
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { AgentCosts, Node } from "@anchrd/intel-contract";
|
|
2
|
-
import type { CloudflareAccountApi } from "../adapters/cloudflare-api/cloudflare-api.types.js";
|
|
3
|
-
import type { AgentActor } from "../agent-runtime/agent-runtime.types.js";
|
|
4
|
-
import type { Actor } from "../nodes/nodes.types.js";
|
|
5
|
-
export interface AgentCostsDeps {
|
|
6
|
-
/**
|
|
7
|
-
* Absent where the deployment configured no account id, gateway id or read token.
|
|
8
|
-
*
|
|
9
|
-
* ⚠️ Absent is a NAMED state and never an error: an installation that never wants Intel to hold a
|
|
10
|
-
* Cloudflare token is a supported installation, and it should read "not configured" rather than
|
|
11
|
-
* a 502 somebody spends an afternoon on.
|
|
12
|
-
*/
|
|
13
|
-
gateway?: Pick<CloudflareAccountApi, "gatewayCalls">;
|
|
14
|
-
/**
|
|
15
|
-
* The tree's own visibility lookup, unchanged on the way through — the same one the runtime proxy
|
|
16
|
-
* uses. What an agent spends is as sensitive as what it does, and neither answer may be kinder
|
|
17
|
-
* than the other.
|
|
18
|
-
*/
|
|
19
|
-
visibleNode(actor: Actor, nodeId: string): Promise<Node | null>;
|
|
20
|
-
now(): Date;
|
|
21
|
-
}
|
|
22
|
-
export interface AgentCostsService {
|
|
23
|
-
/**
|
|
24
|
-
* ⚠️ The answer shape lives in `@anchrd/intel-contract` and not here, because the browser parses
|
|
25
|
-
* it. A second declaration in this package is how the screen and the service start disagreeing
|
|
26
|
-
* about what `status` can say — and `status` is the field that separates "cost nothing" from "not
|
|
27
|
-
* known" (#251).
|
|
28
|
-
*/
|
|
29
|
-
read(actor: AgentActor, agentId: string): Promise<AgentCosts>;
|
|
30
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { AgentRuntimeDeps, AgentRuntimeService } from "./agent-runtime.types.js";
|
|
2
|
-
/**
|
|
3
|
-
* Intel's door to the agent runtime.
|
|
4
|
-
*
|
|
5
|
-
* ⚠️ The browser never talks to the runtime. It talks to Intel, and Intel forwards — one origin for
|
|
6
|
-
* the page, one session cookie, no CORS, and no Gate token in browser JavaScript. What travels on
|
|
7
|
-
* the forwarded call is the caller's OWN bearer, which the runtime then checks for `agents/run`
|
|
8
|
-
* exactly as it would from any other client. Intel impersonates nobody: there is no service key on
|
|
9
|
-
* this path, and a request that arrives without a usable session never reaches the binding.
|
|
10
|
-
*
|
|
11
|
-
* ⚠️ The three checks below are in this order on purpose. The capability comes first, so a caller
|
|
12
|
-
* who may not drive agents at all causes no storage read; the resource ACL comes second, because it
|
|
13
|
-
* is the question the runtime cannot answer; the binding comes last, so nobody learns whether this
|
|
14
|
-
* deployment has a runtime before they are allowed to ask.
|
|
15
|
-
*/
|
|
16
|
-
export declare function createAgentRuntimeService(deps: AgentRuntimeDeps): AgentRuntimeService;
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { ProblemDetails } from "@anchrd/intel-contract";
|
|
2
|
-
import { IntelError } from "../shared/intel-error/intel-error.js";
|
|
3
|
-
/**
|
|
4
|
-
* The host a service binding never resolves.
|
|
5
|
-
*
|
|
6
|
-
* A `Request` needs an absolute URL and a binding routes on the path alone, so this name exists
|
|
7
|
-
* only to make one. `.internal` is not a resolvable TLD: a call that ever left this Worker by
|
|
8
|
-
* mistake fails to connect rather than reaching a stranger who registered the domain.
|
|
9
|
-
*/
|
|
10
|
-
const RuntimeOrigin = "https://intel-agent.internal";
|
|
11
|
-
/**
|
|
12
|
-
* Intel's door to the agent runtime.
|
|
13
|
-
*
|
|
14
|
-
* ⚠️ The browser never talks to the runtime. It talks to Intel, and Intel forwards — one origin for
|
|
15
|
-
* the page, one session cookie, no CORS, and no Gate token in browser JavaScript. What travels on
|
|
16
|
-
* the forwarded call is the caller's OWN bearer, which the runtime then checks for `agents/run`
|
|
17
|
-
* exactly as it would from any other client. Intel impersonates nobody: there is no service key on
|
|
18
|
-
* this path, and a request that arrives without a usable session never reaches the binding.
|
|
19
|
-
*
|
|
20
|
-
* ⚠️ The three checks below are in this order on purpose. The capability comes first, so a caller
|
|
21
|
-
* who may not drive agents at all causes no storage read; the resource ACL comes second, because it
|
|
22
|
-
* is the question the runtime cannot answer; the binding comes last, so nobody learns whether this
|
|
23
|
-
* deployment has a runtime before they are allowed to ask.
|
|
24
|
-
*/
|
|
25
|
-
export function createAgentRuntimeService(deps) {
|
|
26
|
-
async function reach(actor, agentId) {
|
|
27
|
-
if (!actor.canRun) {
|
|
28
|
-
throw new IntelError(403, "permission_required", "Permission required");
|
|
29
|
-
}
|
|
30
|
-
const node = await deps.visibleNode(actor, agentId);
|
|
31
|
-
// Not found and not visible answer alike, as everywhere else in the tree: the difference would
|
|
32
|
-
// tell somebody an agent exists that they may not see.
|
|
33
|
-
if (node?.kind !== "agent") {
|
|
34
|
-
throw new IntelError(404, "agent_not_found", "Agent was not found");
|
|
35
|
-
}
|
|
36
|
-
if (!deps.runtime) {
|
|
37
|
-
throw new IntelError(503, "agent_runtime_not_configured", "This installation has no agent runtime");
|
|
38
|
-
}
|
|
39
|
-
return deps.runtime;
|
|
40
|
-
}
|
|
41
|
-
async function forward(input) {
|
|
42
|
-
const runtime = await reach(input.actor, input.agentId);
|
|
43
|
-
const headers = new Headers({ authorization: `Bearer ${input.token}` });
|
|
44
|
-
if (input.body !== undefined)
|
|
45
|
-
headers.set("content-type", "application/json");
|
|
46
|
-
return await runtime.fetch(new Request(`${RuntimeOrigin}/agents/${encodeURIComponent(input.agentId)}${input.path}`, {
|
|
47
|
-
method: input.method,
|
|
48
|
-
headers,
|
|
49
|
-
...(input.body === undefined ? {} : { body: input.body }),
|
|
50
|
-
...(input.signal ? { signal: input.signal } : {}),
|
|
51
|
-
}));
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* The two routes Intel dials itself, rather than on a browser's behalf: `/key` and
|
|
55
|
-
* `/schedules/sync`.
|
|
56
|
-
*
|
|
57
|
-
* ⚠️ Neither goes through `forward`, and that is deliberate. `forward` asks `canRun` — the
|
|
58
|
-
* question for somebody driving an agent — while both of these belong to *maintaining* one, which
|
|
59
|
-
* Intel has already authorized with `knowledge/create` or `knowledge/write` plus the resource ACL.
|
|
60
|
-
* What travels instead is the caller's own bearer beside the handover secret: the bearer says who
|
|
61
|
-
* is behind the call, the secret says it came down Intel's binding at all. The runtime cannot
|
|
62
|
-
* check Intel's resource ACL — it knows Gate and nothing about the tree — so without the secret a
|
|
63
|
-
* published runtime hostname would open both routes to anybody holding `knowledge/write` (D29).
|
|
64
|
-
*/
|
|
65
|
-
async function handover(call) {
|
|
66
|
-
if (!deps.runtime) {
|
|
67
|
-
throw new IntelError(503, "agent_runtime_not_configured", "This installation has no agent runtime");
|
|
68
|
-
}
|
|
69
|
-
const handoverSecret = deps.handoverSecret?.trim();
|
|
70
|
-
if (!handoverSecret) {
|
|
71
|
-
// ⚠️ Refused here rather than dialled and refused there. The runtime would answer 403, and
|
|
72
|
-
// a 403 reads like "this person may not" — which would send an operator looking at Gate
|
|
73
|
-
// permissions for a secret that was never set.
|
|
74
|
-
throw new IntelError(503, call.unconfigured.code, call.unconfigured.detail);
|
|
75
|
-
}
|
|
76
|
-
return await deps.runtime.fetch(new Request(`${RuntimeOrigin}/agents/${encodeURIComponent(call.agentId)}${call.path}`, {
|
|
77
|
-
method: "POST",
|
|
78
|
-
headers: {
|
|
79
|
-
authorization: `Bearer ${call.token}`,
|
|
80
|
-
"content-type": "application/json",
|
|
81
|
-
// ⚠️ Its own header, never folded into `authorization`: that one already carries the
|
|
82
|
-
// caller's Gate bearer on this very request, and two credentials in one header is how
|
|
83
|
-
// one of them ends up read as the other.
|
|
84
|
-
"x-intel-agent-handover": handoverSecret,
|
|
85
|
-
},
|
|
86
|
-
body: call.body,
|
|
87
|
-
}));
|
|
88
|
-
}
|
|
89
|
-
return {
|
|
90
|
-
forward,
|
|
91
|
-
async syncSchedules(input) {
|
|
92
|
-
const response = await handover({
|
|
93
|
-
agentId: input.agentId,
|
|
94
|
-
path: "/schedules/sync",
|
|
95
|
-
token: input.token,
|
|
96
|
-
// No schedules travel: the runtime re-reads the definition from Intel itself (#214).
|
|
97
|
-
body: "{}",
|
|
98
|
-
unconfigured: {
|
|
99
|
-
code: "agent_schedule_sync_not_configured",
|
|
100
|
-
detail: "This installation has no shared secret for reaching the agent runtime, so schedules cannot be armed",
|
|
101
|
-
},
|
|
102
|
-
});
|
|
103
|
-
if (response.ok)
|
|
104
|
-
return;
|
|
105
|
-
// ⚠️ Loud, and named after the state it leaves behind rather than after the call that failed.
|
|
106
|
-
// What the caller has to act on is not "the runtime answered 502" but "this agent's schedules
|
|
107
|
-
// are not armed", and the sentence says the repair: write the definition again.
|
|
108
|
-
throw new IntelError(502, "agent_schedules_not_armed", `The definition was saved, but the agent runtime did not arm its schedules (${response.status}). Saving the definition again arms them.`);
|
|
109
|
-
},
|
|
110
|
-
async storeKey(input) {
|
|
111
|
-
const response = await handover({
|
|
112
|
-
agentId: input.agentId,
|
|
113
|
-
path: "/key",
|
|
114
|
-
token: input.token,
|
|
115
|
-
body: JSON.stringify({ key: input.key }),
|
|
116
|
-
unconfigured: {
|
|
117
|
-
code: "agent_key_handover_not_configured",
|
|
118
|
-
detail: "This installation has no shared secret for handing an agent its application key",
|
|
119
|
-
},
|
|
120
|
-
});
|
|
121
|
-
if (response.ok)
|
|
122
|
-
return;
|
|
123
|
-
// ⚠️ The runtime's answer is read for its status and thrown away. Its body cannot be quoted
|
|
124
|
-
// here the way `request` quotes it: the call that produced it carried a credential, and an
|
|
125
|
-
// echoed body is the shortest path from a refusal to a key in a log. What the caller is told
|
|
126
|
-
// is that the agent has no usable key — which is the only thing they can act on anyway.
|
|
127
|
-
throw new IntelError(502, "agent_key_not_stored", `The agent runtime did not take this agent's application key (${response.status}), so the agent cannot run yet`);
|
|
128
|
-
},
|
|
129
|
-
// A plain fact, not a probe: the binding is either configured or it is not, and nothing is
|
|
130
|
-
// dialled to answer. Unlike `reach` this stands before any per-agent check — it names no agent.
|
|
131
|
-
available: () => deps.runtime !== undefined,
|
|
132
|
-
async request(input) {
|
|
133
|
-
const response = await forward(input);
|
|
134
|
-
const body = await response.json().catch(() => null);
|
|
135
|
-
if (!response.ok) {
|
|
136
|
-
const problem = ProblemDetails.safeParse(body);
|
|
137
|
-
// ⚠️ The runtime's own words, and only those. Its refusals are written to be read — by a
|
|
138
|
-
// person on the Log tab and by a model on the MCP surface alike — while anything it could
|
|
139
|
-
// not explain arrives as a generic 500 body. Nothing about the forwarded call is added
|
|
140
|
-
// here: the URL, the binding and above all the bearer stay out of every message.
|
|
141
|
-
throw new IntelError(response.status, problem.success
|
|
142
|
-
? (problem.data.code ?? "agent_runtime_refused")
|
|
143
|
-
: "agent_runtime_refused", problem.success
|
|
144
|
-
? (problem.data.detail ?? problem.data.title)
|
|
145
|
-
: "The agent runtime refused this call");
|
|
146
|
-
}
|
|
147
|
-
return body;
|
|
148
|
-
},
|
|
149
|
-
};
|
|
150
|
-
}
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import type { Node } from "@anchrd/intel-contract";
|
|
2
|
-
import type { Actor } from "../nodes/nodes.types.js";
|
|
3
|
-
/**
|
|
4
|
-
* The agent runtime Worker, as Intel is allowed to see it.
|
|
5
|
-
*
|
|
6
|
-
* One method, because that is all a service binding is. The Cloudflare adapter hands the binding
|
|
7
|
-
* over unchanged; nothing below this line knows it is one, so the whole seam is testable with a
|
|
8
|
-
* plain object that records what it was asked.
|
|
9
|
-
*/
|
|
10
|
-
export interface AgentRuntimeBinding {
|
|
11
|
-
fetch(request: Request): Promise<Response>;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Who is driving the agent.
|
|
15
|
-
*
|
|
16
|
-
* ⚠️ `canRun` is the caller's Gate capability, resolved once at the door — never something a body,
|
|
17
|
-
* an MCP argument or a definition may say. It is spelled beside the actor rather than derived here
|
|
18
|
-
* for the same reason `FlowActor` spells `canRun`: the service must not need a Gate client.
|
|
19
|
-
*/
|
|
20
|
-
export interface AgentActor extends Actor {
|
|
21
|
-
canRun: boolean;
|
|
22
|
-
}
|
|
23
|
-
export interface AgentRuntimeDeps {
|
|
24
|
-
/**
|
|
25
|
-
* The shared secret that proves a key handover came down this binding (D29, #207).
|
|
26
|
-
*
|
|
27
|
-
* ⚠️ The SAME value the agent Worker holds as `AGENT_HANDOVER_SECRET`, and it travels on exactly
|
|
28
|
-
* one route. It is not a substitute for the caller's bearer, which travels beside it: the bearer
|
|
29
|
-
* says who is asking, this says the request came through Intel at all. The runtime cannot check
|
|
30
|
-
* Intel's resource ACLs — it knows Gate and nothing about the tree — so without this a published
|
|
31
|
-
* runtime hostname would let anybody with `knowledge/write` write the key of any agent.
|
|
32
|
-
*
|
|
33
|
-
* Absent, `storeKey` refuses by name rather than dialling the binding and being refused there:
|
|
34
|
-
* the deployment is misconfigured, and saying so beats a 403 that reads like a permission problem.
|
|
35
|
-
*/
|
|
36
|
-
handoverSecret?: string;
|
|
37
|
-
/**
|
|
38
|
-
* Absent where a deployment runs no agent Worker. Every call then refuses by name instead of
|
|
39
|
-
* failing somewhere unreadable — a customer may deploy Intel without agents.
|
|
40
|
-
*/
|
|
41
|
-
runtime?: AgentRuntimeBinding;
|
|
42
|
-
/**
|
|
43
|
-
* The tree's own visibility lookup, unchanged on the way through. It is the check the runtime
|
|
44
|
-
* cannot make: the runtime knows Gate capabilities and nothing about Intel's resource ACLs, so
|
|
45
|
-
* "may this person drive agents at all" is its question and "may they reach THIS agent" is ours.
|
|
46
|
-
*/
|
|
47
|
-
visibleNode(actor: Actor, nodeId: string): Promise<Node | null>;
|
|
48
|
-
}
|
|
49
|
-
export interface ForwardInput {
|
|
50
|
-
actor: AgentActor;
|
|
51
|
-
/** The caller's own Gate bearer, passed on unchanged. Never a service key (D19). */
|
|
52
|
-
token: string;
|
|
53
|
-
agentId: string;
|
|
54
|
-
method: "GET" | "POST";
|
|
55
|
-
/** The path below `/agents/:agentId`, exactly as the runtime mounts it. */
|
|
56
|
-
path: string;
|
|
57
|
-
body?: string;
|
|
58
|
-
signal?: AbortSignal;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* One agent's Application key, on its way from Gate into the agent's Durable Object (D29, #207).
|
|
62
|
-
*
|
|
63
|
-
* ⚠️ It exists as a parameter and never as a field of anything stored. The value is in one local
|
|
64
|
-
* variable from the moment Gate answers until this call returns, exactly as it was before D29 — what
|
|
65
|
-
* changed is where it goes: into the runtime instead of into a response the browser reads.
|
|
66
|
-
*/
|
|
67
|
-
export interface StoreKeyInput {
|
|
68
|
-
/** The caller's own Gate bearer, passed on unchanged. Never a service key (D19). */
|
|
69
|
-
token: string;
|
|
70
|
-
agentId: string;
|
|
71
|
-
key: string;
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Which agent's alarm has to be brought in line with the definition Intel just wrote (#214).
|
|
75
|
-
*
|
|
76
|
-
* ⚠️ It carries no schedules. The runtime re-reads the definition from Intel itself, with the
|
|
77
|
-
* agent's own token, so this is a "look again" and never a second copy of the document — a schedule
|
|
78
|
-
* pushed down this call would be one the agent could act on without it ever having been written.
|
|
79
|
-
*/
|
|
80
|
-
export interface SyncSchedulesInput {
|
|
81
|
-
/** The caller's own Gate bearer, passed on unchanged. Never a service key (D19). */
|
|
82
|
-
token: string;
|
|
83
|
-
agentId: string;
|
|
84
|
-
}
|
|
85
|
-
export interface AgentRuntimeService {
|
|
86
|
-
/** The browser's way through: the runtime's answer, body and headers untouched. */
|
|
87
|
-
forward(input: ForwardInput): Promise<Response>;
|
|
88
|
-
/**
|
|
89
|
-
* Hands an agent the key of the Gate Application it runs as.
|
|
90
|
-
*
|
|
91
|
-
* ⚠️ The one door here that does NOT ask `canRun`, and that is deliberate. Its two callers —
|
|
92
|
-
* creating an agent and rotating its key — have already decided who may act: `knowledge/create`
|
|
93
|
-
* with a writable parent, or `knowledge/write` with the resource ACL of the agent node. Requiring
|
|
94
|
-
* `agents/run` on top would mean a person who may create agents but never run one creates keyless
|
|
95
|
-
* agents, which is #200 arrived at from the other side. The runtime makes the same distinction at
|
|
96
|
-
* its own door.
|
|
97
|
-
*
|
|
98
|
-
* ⚠️ It fails loudly. A handover that quietly did not happen is an agent that exists, looks
|
|
99
|
-
* right, and dies at its first run — the whole failure this ticket removes.
|
|
100
|
-
*/
|
|
101
|
-
storeKey(input: StoreKeyInput): Promise<void>;
|
|
102
|
-
/**
|
|
103
|
-
* Arms the agent's alarm for whatever the definition Intel just wrote schedules (#214).
|
|
104
|
-
*
|
|
105
|
-
* ⚠️ Like `storeKey` and unlike everything else here, it does NOT ask `canRun`. Its two callers —
|
|
106
|
-
* creating an agent and saving its definition — have already decided who may act: `knowledge/
|
|
107
|
-
* create` with a writable parent, or `knowledge/write` with the resource ACL of the agent node.
|
|
108
|
-
* Requiring `agents/run` on top would mean an editor who may not drive agents saves a schedule
|
|
109
|
-
* that is never armed, which is the failure this exists to remove.
|
|
110
|
-
*
|
|
111
|
-
* ⚠️ It fails loudly, for the same reason `storeKey` does. A sync that quietly did not happen is
|
|
112
|
-
* an agent whose profile shows a schedule that will never fire.
|
|
113
|
-
*/
|
|
114
|
-
syncSchedules(input: SyncSchedulesInput): Promise<void>;
|
|
115
|
-
/** The same door for a caller that wants the answer, not the response — Intel MCP uses this. */
|
|
116
|
-
request(input: ForwardInput): Promise<unknown>;
|
|
117
|
-
/**
|
|
118
|
-
* Whether this deployment has a runtime at all (#190). The answer `/capabilities` gives to an
|
|
119
|
-
* authenticated caller so the UI can stop offering agents instead of guessing from a 503.
|
|
120
|
-
*/
|
|
121
|
-
available(): boolean;
|
|
122
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The figures this package knows without asking anybody, and the ONLY place they are written down.
|
|
3
|
-
*
|
|
4
|
-
* ⚠️ It moved here out of `packages/ui` with #257, and the move is the point rather than the
|
|
5
|
-
* tidying. The comment it used to carry accused itself correctly — *"it is a copy and it ages
|
|
6
|
-
* silently"* — and named the reason it stayed a copy: the Cloudflare endpoint needs a token, and a
|
|
7
|
-
* token does not belong in a SPA. It belongs here. What the browser gets is the answer, never the
|
|
8
|
-
* credential, and the Workers AI half of this table is overwritten by the live one on every read.
|
|
9
|
-
*
|
|
10
|
-
* ⚠️ The Anthropic entries have no live source and will not get one. Cloudflare resells those models
|
|
11
|
-
* through Unified Billing and publishes no price list for them, so they stay `builtin` even on a
|
|
12
|
-
* perfectly healthy read — which is exactly why `source` is per entry and not per response.
|
|
13
|
-
*
|
|
14
|
-
* ⚠️ Workers AI models without `function_calling` are deliberately absent from the live merge. An
|
|
15
|
-
* agent is a tool loop; a model that cannot call a tool cannot run one.
|
|
16
|
-
*
|
|
17
|
-
* Collected 2026-08-06 from `GET /accounts/{id}/ai/models/search` and from Anthropic's model list.
|
|
18
|
-
*/
|
|
19
|
-
const builtin = [
|
|
20
|
-
entry("anthropic", "claude-opus-5", "Opus 5", 1_000_000, 5, 25),
|
|
21
|
-
entry("anthropic", "claude-sonnet-5", "Sonnet 5", 1_000_000, 3, 15),
|
|
22
|
-
entry("anthropic", "claude-sonnet-4", "Sonnet 4", 200_000, 3, 15),
|
|
23
|
-
entry("anthropic", "claude-haiku-4-5", "Haiku 4.5", 200_000, 1, 5),
|
|
24
|
-
entry("workers-ai", "@cf/openai/gpt-oss-120b", "GPT-OSS 120B", 128_000, 0.35, 0.75),
|
|
25
|
-
entry("workers-ai", "@cf/openai/gpt-oss-20b", "GPT-OSS 20B", 128_000, 0.2, 0.3),
|
|
26
|
-
entry("workers-ai", "@cf/moonshotai/kimi-k2.6", "Kimi K2.6", 262_144, 0.95, 4),
|
|
27
|
-
entry("workers-ai", "@cf/moonshotai/kimi-k2.7-code", "Kimi K2.7 Code", 262_144, 0.95, 4),
|
|
28
|
-
entry("workers-ai", "@cf/zai-org/glm-5.2", "GLM 5.2", 262_144, 1.4, 4.4),
|
|
29
|
-
entry("workers-ai", "@cf/zai-org/glm-4.7-flash", "GLM 4.7 Flash", 131_072, 0.0605, 0.4),
|
|
30
|
-
entry("workers-ai", "@cf/google/gemma-4-26b-a4b-it", "Gemma 4 26B", 256_000, 0.1, 0.3),
|
|
31
|
-
entry("workers-ai", "@cf/nvidia/nemotron-3-120b-a12b", "Nemotron 3 120B", 256_000, 0.5, 1.5),
|
|
32
|
-
entry("workers-ai", "@cf/meta/llama-4-scout-17b-16e-instruct", "Llama 4 Scout", 131_000, 0.27, 0.85),
|
|
33
|
-
entry("workers-ai", "@cf/meta/llama-3.3-70b-instruct-fp8-fast", "Llama 3.3 70B Fast", 24_000, 0.293, 2.253),
|
|
34
|
-
entry("workers-ai", "@cf/mistralai/mistral-small-3.1-24b-instruct", "Small 3.1 24B", 128_000, 0.351, 0.555),
|
|
35
|
-
entry("workers-ai", "@cf/ibm-granite/granite-4.0-h-micro", "Granite 4.0 Micro", 131_000, 0.017, 0.112),
|
|
36
|
-
entry("workers-ai", "@cf/qwen/qwen3-30b-a3b-fp8", "Qwen3 30B", 32_768, 0.0509, 0.335),
|
|
37
|
-
];
|
|
38
|
-
function entry(provider, model, name, contextTokens, inputPerMillion, outputPerMillion) {
|
|
39
|
-
return {
|
|
40
|
-
provider,
|
|
41
|
-
model,
|
|
42
|
-
name,
|
|
43
|
-
contextTokens,
|
|
44
|
-
price: { inputPerMillion, outputPerMillion },
|
|
45
|
-
source: "builtin",
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* The model's own name, taken from its id rather than invented.
|
|
50
|
-
*
|
|
51
|
-
* A live Workers AI model that is not in the table above has no written-out name anywhere, and
|
|
52
|
-
* "@cf/qwen/qwen3-30b-a3b-fp8" prettified by any rule short enough to write here comes out wrong —
|
|
53
|
-
* so the last segment of the id is used as it stands. A raw name is easier to recognise as raw than
|
|
54
|
-
* a wrong one is to recognise as wrong.
|
|
55
|
-
*/
|
|
56
|
-
function nameFromId(model) {
|
|
57
|
-
const segments = model.split("/");
|
|
58
|
-
return segments[segments.length - 1] ?? model;
|
|
59
|
-
}
|
|
60
|
-
export function createModelCatalog(deps) {
|
|
61
|
-
return {
|
|
62
|
-
async list() {
|
|
63
|
-
if (!deps.cloudflare)
|
|
64
|
-
return { entries: builtin, liveStatus: "not_configured" };
|
|
65
|
-
try {
|
|
66
|
-
const live = await deps.cloudflare.workersAiModels();
|
|
67
|
-
const merged = new Map(builtin.map((known) => [`${known.provider}:${known.model}`, known]));
|
|
68
|
-
for (const model of live) {
|
|
69
|
-
// ⚠️ A model that cannot call a tool is skipped rather than listed without figures. It
|
|
70
|
-
// would be an offer that produces a broken agent for whoever took it.
|
|
71
|
-
if (!model.functionCalling)
|
|
72
|
-
continue;
|
|
73
|
-
const key = `workers-ai:${model.name}`;
|
|
74
|
-
const known = merged.get(key);
|
|
75
|
-
merged.set(key, {
|
|
76
|
-
provider: "workers-ai",
|
|
77
|
-
model: model.name,
|
|
78
|
-
name: known?.name ?? nameFromId(model.name),
|
|
79
|
-
// ⚠️ The live figure wins, and a live figure that is MISSING wins too — falling back to
|
|
80
|
-
// the table for one of the two halves would produce an entry that is half fresh and
|
|
81
|
-
// half years old, labelled fresh. Either the account answered for this model or it did
|
|
82
|
-
// not.
|
|
83
|
-
contextTokens: model.contextTokens,
|
|
84
|
-
price: model.price,
|
|
85
|
-
source: "cloudflare",
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
return { entries: [...merged.values()], liveStatus: "read" };
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
// ⚠️ Named, not thrown. The select has to render either way — an agent whose model cannot be
|
|
92
|
-
// chosen is an agent nobody can repair from that screen — and the alternative to a stale
|
|
93
|
-
// price here is no price at all, which helps nobody choose. What the caller gets is the
|
|
94
|
-
// table plus the reason it is the table, and the screen says so.
|
|
95
|
-
return { entries: builtin, liveStatus: "unreadable" };
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
|
-
}
|