@anchrd/intel-api 0.6.7 → 0.9.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/README.md +63 -3
- package/dist/adapters/cloudflare/cloudflare.js +102 -37
- package/dist/adapters/cloudflare/cloudflare.types.d.ts +20 -0
- package/dist/adapters/content/content.d.ts +1 -1
- package/dist/adapters/db/db-flows.js +148 -20
- package/dist/adapters/db/db-grants.d.ts +13 -2
- package/dist/adapters/db/db-grants.js +25 -8
- package/dist/adapters/db/db-indexing.d.ts +2 -2
- package/dist/adapters/db/db-indexing.js +26 -19
- package/dist/adapters/db/db.d.ts +3 -3
- package/dist/adapters/db/db.js +442 -118
- package/dist/adapters/gate-applications/gate-applications.d.ts +23 -0
- package/dist/adapters/gate-applications/gate-applications.js +88 -0
- package/dist/adapters/index-queue/index-queue.d.ts +1 -1
- package/dist/adapters/index-queue/index-queue.js +2 -2
- package/dist/adapters/semantic-index/semantic-index.types.d.ts +2 -2
- package/dist/adapters/tool-delegation/tool-delegation.d.ts +22 -0
- package/dist/adapters/tool-delegation/tool-delegation.js +90 -0
- package/dist/agent-runtime/agent-runtime.d.ts +16 -0
- package/dist/agent-runtime/agent-runtime.js +150 -0
- package/dist/agent-runtime/agent-runtime.types.d.ts +122 -0
- package/dist/bundle/bundle.d.ts +4 -0
- package/dist/bundle/bundle.js +1048 -0
- package/dist/bundle/bundle.types.d.ts +33 -0
- package/dist/bundle/bundle.types.js +1 -0
- package/dist/cli/cli.js +10 -1
- package/dist/flows/flows.d.ts +8 -8
- package/dist/flows/flows.js +158 -42
- package/dist/flows/flows.types.d.ts +40 -7
- package/dist/http/http.d.ts +1 -0
- package/dist/http/http.js +348 -61
- package/dist/http/http.types.d.ts +6 -2
- package/dist/indexing/indexing.js +14 -2
- package/dist/indexing/indexing.types.d.ts +2 -2
- package/dist/intel/intel.js +12 -3
- package/dist/intel/intel.types.d.ts +6 -2
- package/dist/mcp/mcp.js +519 -124
- package/dist/mcp/mcp.types.d.ts +11 -2
- package/dist/nodes/nodes.d.ts +2 -0
- package/dist/nodes/nodes.js +1466 -0
- package/dist/nodes/nodes.types.d.ts +402 -0
- package/dist/nodes/nodes.types.js +1 -0
- package/dist/tools/tool-servers/tool-servers.d.ts +46 -0
- package/dist/tools/tool-servers/tool-servers.js +114 -0
- package/dist/tools/tools.js +190 -31
- package/dist/tools/tools.types.d.ts +23 -1
- package/migrations/0011_one_name_for_the_tree.sql +53 -0
- package/migrations/0012_table_snapshots.sql +29 -0
- package/migrations/0013_agents_in_the_tree.sql +76 -0
- package/migrations/0014_agent_applications.sql +25 -0
- package/migrations/0015_tools_delegated_from_a_connection.sql +15 -0
- package/package.json +3 -2
- package/dist/knowledge/knowledge.d.ts +0 -2
- package/dist/knowledge/knowledge.js +0 -761
- package/dist/knowledge/knowledge.types.d.ts +0 -198
- /package/dist/{knowledge/knowledge.types.js → agent-runtime/agent-runtime.types.js} +0 -0
- /package/dist/{knowledge → nodes}/document-links/document-links.d.ts +0 -0
- /package/dist/{knowledge → nodes}/document-links/document-links.js +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
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;
|
|
@@ -0,0 +1,88 @@
|
|
|
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,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { QueueProducer } from "./index-queue.types.js";
|
|
3
3
|
export declare const IndexMessage: z.ZodObject<{
|
|
4
|
-
type: z.ZodLiteral<"
|
|
4
|
+
type: z.ZodLiteral<"node.version">;
|
|
5
5
|
versionId: z.ZodString;
|
|
6
6
|
}, z.core.$strict>;
|
|
7
7
|
export type IndexMessage = z.infer<typeof IndexMessage>;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export const IndexMessage = z.strictObject({
|
|
3
|
-
type: z.literal("
|
|
3
|
+
type: z.literal("node.version"),
|
|
4
4
|
versionId: z.string().min(1),
|
|
5
5
|
});
|
|
6
6
|
export function createIndexQueue(queue) {
|
|
7
7
|
return {
|
|
8
8
|
async enqueue(versionId) {
|
|
9
|
-
await queue.send({ type: "
|
|
9
|
+
await queue.send({ type: "node.version", versionId });
|
|
10
10
|
},
|
|
11
11
|
};
|
|
12
12
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { NodeIndexTarget } from "../../nodes/nodes.types.js";
|
|
2
2
|
export interface WorkersAiBinding {
|
|
3
3
|
run(model: string, input: {
|
|
4
4
|
text: string[];
|
|
@@ -22,7 +22,7 @@ export interface SemanticHit {
|
|
|
22
22
|
score: number;
|
|
23
23
|
}
|
|
24
24
|
export interface SemanticIndex {
|
|
25
|
-
replace(target:
|
|
25
|
+
replace(target: NodeIndexTarget, content: string): Promise<void>;
|
|
26
26
|
search(query: string, limit: number): Promise<SemanticHit[]>;
|
|
27
27
|
}
|
|
28
28
|
export interface SemanticIndexDeps {
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
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;
|
|
@@ -0,0 +1,150 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
}
|