@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.
Files changed (58) hide show
  1. package/README.md +63 -3
  2. package/dist/adapters/cloudflare/cloudflare.js +102 -37
  3. package/dist/adapters/cloudflare/cloudflare.types.d.ts +20 -0
  4. package/dist/adapters/content/content.d.ts +1 -1
  5. package/dist/adapters/db/db-flows.js +148 -20
  6. package/dist/adapters/db/db-grants.d.ts +13 -2
  7. package/dist/adapters/db/db-grants.js +25 -8
  8. package/dist/adapters/db/db-indexing.d.ts +2 -2
  9. package/dist/adapters/db/db-indexing.js +26 -19
  10. package/dist/adapters/db/db.d.ts +3 -3
  11. package/dist/adapters/db/db.js +442 -118
  12. package/dist/adapters/gate-applications/gate-applications.d.ts +23 -0
  13. package/dist/adapters/gate-applications/gate-applications.js +88 -0
  14. package/dist/adapters/index-queue/index-queue.d.ts +1 -1
  15. package/dist/adapters/index-queue/index-queue.js +2 -2
  16. package/dist/adapters/semantic-index/semantic-index.types.d.ts +2 -2
  17. package/dist/adapters/tool-delegation/tool-delegation.d.ts +22 -0
  18. package/dist/adapters/tool-delegation/tool-delegation.js +90 -0
  19. package/dist/agent-runtime/agent-runtime.d.ts +16 -0
  20. package/dist/agent-runtime/agent-runtime.js +150 -0
  21. package/dist/agent-runtime/agent-runtime.types.d.ts +122 -0
  22. package/dist/bundle/bundle.d.ts +4 -0
  23. package/dist/bundle/bundle.js +1048 -0
  24. package/dist/bundle/bundle.types.d.ts +33 -0
  25. package/dist/bundle/bundle.types.js +1 -0
  26. package/dist/cli/cli.js +10 -1
  27. package/dist/flows/flows.d.ts +8 -8
  28. package/dist/flows/flows.js +158 -42
  29. package/dist/flows/flows.types.d.ts +40 -7
  30. package/dist/http/http.d.ts +1 -0
  31. package/dist/http/http.js +348 -61
  32. package/dist/http/http.types.d.ts +6 -2
  33. package/dist/indexing/indexing.js +14 -2
  34. package/dist/indexing/indexing.types.d.ts +2 -2
  35. package/dist/intel/intel.js +12 -3
  36. package/dist/intel/intel.types.d.ts +6 -2
  37. package/dist/mcp/mcp.js +519 -124
  38. package/dist/mcp/mcp.types.d.ts +11 -2
  39. package/dist/nodes/nodes.d.ts +2 -0
  40. package/dist/nodes/nodes.js +1466 -0
  41. package/dist/nodes/nodes.types.d.ts +402 -0
  42. package/dist/nodes/nodes.types.js +1 -0
  43. package/dist/tools/tool-servers/tool-servers.d.ts +46 -0
  44. package/dist/tools/tool-servers/tool-servers.js +114 -0
  45. package/dist/tools/tools.js +190 -31
  46. package/dist/tools/tools.types.d.ts +23 -1
  47. package/migrations/0011_one_name_for_the_tree.sql +53 -0
  48. package/migrations/0012_table_snapshots.sql +29 -0
  49. package/migrations/0013_agents_in_the_tree.sql +76 -0
  50. package/migrations/0014_agent_applications.sql +25 -0
  51. package/migrations/0015_tools_delegated_from_a_connection.sql +15 -0
  52. package/package.json +3 -2
  53. package/dist/knowledge/knowledge.d.ts +0 -2
  54. package/dist/knowledge/knowledge.js +0 -761
  55. package/dist/knowledge/knowledge.types.d.ts +0 -198
  56. /package/dist/{knowledge/knowledge.types.js → agent-runtime/agent-runtime.types.js} +0 -0
  57. /package/dist/{knowledge → nodes}/document-links/document-links.d.ts +0 -0
  58. /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<"knowledge.version">;
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("knowledge.version"),
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: "knowledge.version", versionId });
9
+ await queue.send({ type: "node.version", versionId });
10
10
  },
11
11
  };
12
12
  }
@@ -1,4 +1,4 @@
1
- import type { KnowledgeIndexTarget } from "../../knowledge/knowledge.types.js";
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: KnowledgeIndexTarget, content: string): Promise<void>;
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
+ }
@@ -0,0 +1,4 @@
1
+ import type { BundleDeps, BundleService } from "./bundle.types.js";
2
+ export declare const MaxImportZipBytes: number;
3
+ export declare const MaxImportEntryBytes: number;
4
+ export declare function createBundle(deps: BundleDeps): BundleService;