@anchrd/intel-api 0.7.0 → 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 +30 -11
- package/dist/adapters/cloudflare/cloudflare.js +71 -40
- package/dist/adapters/cloudflare/cloudflare.types.d.ts +9 -0
- package/dist/adapters/db/db.js +11 -0
- package/dist/adapters/gate-applications/gate-applications.js +22 -0
- 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.js +74 -0
- package/dist/agent-runtime/agent-runtime.types.d.ts +65 -0
- package/dist/bundle/bundle.js +13 -0
- package/dist/http/http.js +32 -8
- package/dist/mcp/mcp.js +48 -12
- package/dist/nodes/nodes.js +152 -23
- package/dist/nodes/nodes.types.d.ts +94 -6
- 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/0015_tools_delegated_from_a_connection.sql +15 -0
- package/package.json +2 -2
package/dist/http/http.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AppendTableRowsInput, ArchiveFlowInput, ArchiveNodeInput, CancelFlowRunInput, CompleteFlowRunStepInput, CreateAgentInput, CreateFlowInput, CreateNodeInput, DefineTableInput, DeleteTableRowsInput, ExecuteToolInput, GetFlowInput, GetFlowRunInput, GetFlowVersionInput, GetNodeInput, GetNodeVersionInput, ListAgentsInput, ListFlowRunsInput, ListFlowsInput, ListNodesInput, NodeGraphInput, PreviewFlowPublishInput, PublishFlowInput, RedefineTableInput, RelationGraphInput, ResolveNodeLinksInput, RevokeGrantInput, SaveAgentDefinitionInput, SaveAttachmentInput, SaveFlowVersionInput, SaveNodeVersionInput, SearchInput, ShareInput, StartFlowRunInput, TestToolInput, UnpublishFlowInput, UpdateFlowInput, UpdateNodeInput, UpdateTableRowsInput, } from "@anchrd/intel-contract";
|
|
1
|
+
import { AppendTableRowsInput, ArchiveFlowInput, ArchiveNodeInput, CancelFlowRunInput, CompleteFlowRunStepInput, CreateAgentInput, CreateFlowInput, CreateNodeInput, DefineTableInput, DeleteTableRowsInput, ExecuteToolInput, GetFlowInput, GetFlowRunInput, GetFlowVersionInput, GetNodeInput, GetNodeVersionInput, ListAgentsInput, ListFlowRunsInput, ListFlowsInput, ListNodesInput, NodeGraphInput, PreviewFlowPublishInput, PublishFlowInput, RedefineTableInput, RelationGraphInput, ResolveNodeLinksInput, RevokeGrantInput, RotateAgentKeyInput, SaveAgentDefinitionInput, SaveAttachmentInput, SaveFlowVersionInput, SaveNodeVersionInput, SearchInput, ShareInput, StartFlowRunInput, TestToolInput, UnpublishFlowInput, UpdateFlowInput, UpdateNodeInput, UpdateTableRowsInput, } from "@anchrd/intel-contract";
|
|
2
2
|
import { Hono } from "hono";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { authorizeBearer, bearer, permits, } from "../shared/gate-authorization/gate-authorization.js";
|
|
@@ -231,19 +231,34 @@ export function createHttp(deps) {
|
|
|
231
231
|
return context.json(await deps.nodes.listAgents(asActor(auth), input));
|
|
232
232
|
});
|
|
233
233
|
/**
|
|
234
|
-
* ⚠️
|
|
234
|
+
* ⚠️ NO response in Intel's HTTP surface carries a credential any more (D29, #207).
|
|
235
235
|
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
236
|
+
* Creating an agent still mints its Gate Application with the caller's own bearer — so this needs
|
|
237
|
+
* `applications:write` in Gate on top of `knowledge:create` in Intel — but the key Gate issues
|
|
238
|
+
* once goes straight into the agent runtime over the service binding, inside this request. The
|
|
239
|
+
* answer is the same shape every agent read has. Until #207 it carried the key in plain text and
|
|
240
|
+
* a person had to put it into a Worker secret by hand, which is why an agent created here could
|
|
241
|
+
* never run (#200).
|
|
241
242
|
*/
|
|
242
243
|
app.post("/nodes/agents", async (context) => {
|
|
243
244
|
const auth = requireCapability(context, "knowledge", "create");
|
|
244
245
|
const input = CreateAgentInput.parse(await context.req.json().catch(() => null));
|
|
245
246
|
return context.json(await deps.nodes.createAgent(asActor(auth), input, { token: context.get("token") }), 201);
|
|
246
247
|
});
|
|
248
|
+
/**
|
|
249
|
+
* The repair path, and the only one an agent that lost its key has (D29, #207).
|
|
250
|
+
*
|
|
251
|
+
* ⚠️ `knowledge/write`, not `agents/run`. Which principal an agent acts as is a property of the
|
|
252
|
+
* agent, changed by whoever may change the agent; being allowed to press "Run now" is a different
|
|
253
|
+
* question and answers a different one. The resource ACL on the node is checked by the service.
|
|
254
|
+
*
|
|
255
|
+
* ⚠️ Before `/nodes/:nodeId`, like every other literal segment under `/nodes`.
|
|
256
|
+
*/
|
|
257
|
+
app.post("/nodes/agents/:nodeId/rotate-key", async (context) => {
|
|
258
|
+
const auth = requireCapability(context, "knowledge", "write");
|
|
259
|
+
const input = RotateAgentKeyInput.parse({ nodeId: context.req.param("nodeId") });
|
|
260
|
+
return context.json(await deps.nodes.rotateAgentKey(asActor(auth), input, { token: context.get("token") }));
|
|
261
|
+
});
|
|
247
262
|
// ⚠️ Before `/nodes/:nodeId`, or the router would read "export" as a node ID. The root has no
|
|
248
263
|
// node to address, so the whole installation — as this caller may read it — exports here (#136).
|
|
249
264
|
app.get("/nodes/export", async (context) => {
|
|
@@ -295,7 +310,9 @@ export function createHttp(deps) {
|
|
|
295
310
|
if (input.nodeId !== context.req.param("nodeId")) {
|
|
296
311
|
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
297
312
|
}
|
|
298
|
-
|
|
313
|
+
// ⚠️ The caller's own bearer travels on, like it does when an agent is created: writing a
|
|
314
|
+
// definition also arms what it schedules, and that call is made as the person saving (#214).
|
|
315
|
+
return context.json(await deps.nodes.saveAgentDefinition(asActor(auth), input, { token: context.get("token") }), 201);
|
|
299
316
|
});
|
|
300
317
|
app.get("/nodes/:nodeId/versions", async (context) => {
|
|
301
318
|
const auth = requireCapability(context, "knowledge", "read");
|
|
@@ -673,6 +690,13 @@ export function createHttp(deps) {
|
|
|
673
690
|
const auth = requireCapability(context, "tools", "read");
|
|
674
691
|
return context.json(await deps.tools.catalog(asToolActor(auth)));
|
|
675
692
|
});
|
|
693
|
+
// ⚠️ Registered before `/tools/test` and `/tools/execute` and behind `tools/read`, not a new
|
|
694
|
+
// capability: it answers strictly less than `/tools` does — the servers behind the same live
|
|
695
|
+
// list, with no schemas (D30).
|
|
696
|
+
app.get("/tools/servers", async (context) => {
|
|
697
|
+
const auth = requireCapability(context, "tools", "read");
|
|
698
|
+
return context.json(await deps.tools.servers(asToolActor(auth)));
|
|
699
|
+
});
|
|
676
700
|
app.post("/tools/test", async (context) => {
|
|
677
701
|
const auth = requireCapability(context, "tools", "test");
|
|
678
702
|
const input = TestToolInput.parse(await context.req.json().catch(() => null));
|
package/dist/mcp/mcp.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AppendTableRowsInput, ArchiveFlowInput, ArchiveNodeInput, CancelFlowRunInput, CompleteFlowRunStepInput, CreateAgentInput, CreateFlowInput, CreateNodeInput, DefineTableInput, DeleteTableRowsInput, ExecuteToolInput, GetAgentInput, GetFlowInput, GetFlowRunInput, GetFlowVersionInput, GetNodeInput, GetNodeVersionInput, GetTableInput, IntelId, ListAgentsInput, ListFlowRunsInput, ListFlowsInput, ListGrantsInput, ListNodesInput, NodeGraphInput, PauseAgentInput, PreviewFlowPublishInput, PublishFlowInput, RedefineTableInput, RelationGraphInput, ResolveNodeLinksInput, RevokeGrantInput, RunAgentNowInput, SaveAgentDefinitionInput, SaveAttachmentInput, SaveFlowVersionInput, SaveNodeVersionInput, SearchInput, ShareInput, StartFlowRunInput, TestToolInput, UnpublishFlowInput, UpdateFlowInput, UpdateNodeInput, UpdateTableRowsInput, } from "@anchrd/intel-contract";
|
|
1
|
+
import { AppendTableRowsInput, ArchiveFlowInput, ArchiveNodeInput, CancelFlowRunInput, CompleteFlowRunStepInput, CreateAgentInput, CreateFlowInput, CreateNodeInput, DefineTableInput, DeleteTableRowsInput, ExecuteToolInput, GetAgentInput, GetFlowInput, GetFlowRunInput, GetFlowVersionInput, GetNodeInput, GetNodeVersionInput, GetTableInput, IntelId, ListAgentsInput, ListFlowRunsInput, ListFlowsInput, ListGrantsInput, ListNodesInput, NodeGraphInput, PauseAgentInput, PreviewFlowPublishInput, PublishFlowInput, RedefineTableInput, RelationGraphInput, ResolveNodeLinksInput, RevokeGrantInput, RotateAgentKeyInput, RunAgentNowInput, SaveAgentDefinitionInput, SaveAttachmentInput, SaveFlowVersionInput, SaveNodeVersionInput, SearchInput, ShareInput, StartFlowRunInput, TestToolInput, UnpublishFlowInput, UpdateFlowInput, UpdateNodeInput, UpdateTableRowsInput, } from "@anchrd/intel-contract";
|
|
2
2
|
import { McpServer, ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
4
4
|
import { z } from "zod";
|
|
@@ -342,19 +342,18 @@ export async function handleMcp(request, deps) {
|
|
|
342
342
|
},
|
|
343
343
|
}, async (input) => text(await deps.nodes.create(actor, input)));
|
|
344
344
|
/**
|
|
345
|
-
* ⚠️
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
* read answer with `applicationId` alone, which names the principal without authenticating it.
|
|
345
|
+
* ⚠️ There is no longer ANY tool result in Intel that contains a secret, and that is the whole
|
|
346
|
+
* of D29 seen from here. Until #207 this one tool answered with the Application key in plain
|
|
347
|
+
* text — a deliberate exception, because the result WAS the credential and a redacted answer
|
|
348
|
+
* would have meant "an agent you can create but never run". The exception is gone rather than
|
|
349
|
+
* weakened: the key now travels from Gate into the agent runtime over Intel's service binding
|
|
350
|
+
* inside this call, so there is nothing left to hand to a model. `agent_create`, `agent_get`,
|
|
351
|
+
* `agent_list` and every node read answer with `applicationId` alone, which names the principal
|
|
352
|
+
* without authenticating it.
|
|
354
353
|
*/
|
|
355
354
|
server.registerTool("agent_create", {
|
|
356
355
|
title: "Create agent",
|
|
357
|
-
description: "Create an agent node with its first definition, and the Gate application it runs as. The definition names nodes and their role, schedules and a model — never accounts, secrets or channels. The
|
|
356
|
+
description: "Create an agent node with its first definition, and the Gate application it runs as. The definition names nodes and their role, schedules and a model — never accounts, secrets or channels. The agent's application key is handed to the agent runtime internally and never appears in this result; the agent can run straight away. A repeated idempotency key returns the existing agent and mints nothing.",
|
|
358
357
|
inputSchema: CreateAgentInput,
|
|
359
358
|
annotations: {
|
|
360
359
|
title: "Create agent",
|
|
@@ -378,7 +377,29 @@ export async function handleMcp(request, deps) {
|
|
|
378
377
|
idempotentHint: true,
|
|
379
378
|
openWorldHint: false,
|
|
380
379
|
},
|
|
381
|
-
}, async (input) => text(await deps.nodes.saveAgentDefinition(actor, input)));
|
|
380
|
+
}, async (input) => text(await deps.nodes.saveAgentDefinition(actor, input, { token: deps.bearer })));
|
|
381
|
+
/**
|
|
382
|
+
* The MCP half of the "Replace key" action in the agent profile (D29, #207).
|
|
383
|
+
*
|
|
384
|
+
* ⚠️ `knowledge/write` like `agent_update`, not `agents/run`: which principal an agent acts as
|
|
385
|
+
* is a property of the agent, changed by whoever may change the agent. And the result names the
|
|
386
|
+
* principal and the moment — never the key, which went to the runtime and nowhere else.
|
|
387
|
+
*/
|
|
388
|
+
server.registerTool("agent_rotate_key", {
|
|
389
|
+
title: "Replace an agent's application key",
|
|
390
|
+
description: "Issue a new Gate application key for an agent and give it to the agent runtime. The previous key stops working immediately. Use it when a run fails with agent_key_missing, or when a key may have been exposed. The key itself is never returned.",
|
|
391
|
+
inputSchema: RotateAgentKeyInput,
|
|
392
|
+
annotations: {
|
|
393
|
+
title: "Replace an agent's application key",
|
|
394
|
+
readOnlyHint: false,
|
|
395
|
+
// The old key stops working the moment this runs, and anything still using it breaks.
|
|
396
|
+
destructiveHint: true,
|
|
397
|
+
// Asking twice issues twice, and the second answer invalidates the first key.
|
|
398
|
+
idempotentHint: false,
|
|
399
|
+
// The principal lives in Gate, which is a system outside Intel.
|
|
400
|
+
openWorldHint: true,
|
|
401
|
+
},
|
|
402
|
+
}, async (input) => text(await deps.nodes.rotateAgentKey(actor, input, { token: deps.bearer })));
|
|
382
403
|
registerWithAlias(server, { name: "node_save", deprecated: "knowledge_save" }, {
|
|
383
404
|
title: "Save node version",
|
|
384
405
|
description: "Append an immutable content version using an optimistic base version.",
|
|
@@ -927,6 +948,21 @@ export async function handleMcp(request, deps) {
|
|
|
927
948
|
openWorldHint: false,
|
|
928
949
|
},
|
|
929
950
|
}, async () => text(await deps.tools.catalog(toolActor)));
|
|
951
|
+
// The MCP twin of the picker in the agent profile (D30): whole servers, so an agent can be
|
|
952
|
+
// given tools from a client instead of from the screen. Assigning and removing is
|
|
953
|
+
// `agent_update` — a delegation is a field of the definition, not a surface of its own.
|
|
954
|
+
server.registerTool("tools_servers_list", {
|
|
955
|
+
title: "List tool servers",
|
|
956
|
+
description: "List the MCP servers the calling user reaches through the portal, as the portal itself names them. These are the handles an agent definition may delegate.",
|
|
957
|
+
inputSchema: EmptyInput,
|
|
958
|
+
annotations: {
|
|
959
|
+
title: "List tool servers",
|
|
960
|
+
readOnlyHint: true,
|
|
961
|
+
destructiveHint: false,
|
|
962
|
+
idempotentHint: true,
|
|
963
|
+
openWorldHint: false,
|
|
964
|
+
},
|
|
965
|
+
}, async () => text(await deps.tools.servers(toolActor)));
|
|
930
966
|
}
|
|
931
967
|
if (permits(deps.authorization, "tools", "test")) {
|
|
932
968
|
server.registerTool("tools_test", {
|
package/dist/nodes/nodes.js
CHANGED
|
@@ -327,6 +327,34 @@ export function createNodes(deps) {
|
|
|
327
327
|
throw new IntelError(500, "content_missing", "Version content is missing");
|
|
328
328
|
return { node, version, definition: parseStoredDefinition(body), applicationId };
|
|
329
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* The one place a caller's asked-for definition becomes the definition Intel stores (D30).
|
|
332
|
+
*
|
|
333
|
+
* ⚠️ `delegatedBy` is ADDED here, from the session, and could not have arrived any other way:
|
|
334
|
+
* `AgentDefinitionInput` has no field for it, so a body naming somebody else is a parse error at
|
|
335
|
+
* the boundary rather than a value this function has to remember to ignore. Adding it here is
|
|
336
|
+
* safe in exactly one direction — the writer must prove below that they reach every named server
|
|
337
|
+
* themselves, so the delegation can only ever move to somebody who already had it.
|
|
338
|
+
*
|
|
339
|
+
* ⚠️ The check is a live `tools/list` through the portal, before anything is written. A server the
|
|
340
|
+
* saver cannot reach is refused BY NAME — a silent drop would leave a person looking at a saved
|
|
341
|
+
* agent that quietly has one tool fewer than they picked.
|
|
342
|
+
*/
|
|
343
|
+
async function delegationOf(actor, definition) {
|
|
344
|
+
const tools = definition.tools;
|
|
345
|
+
if (!tools)
|
|
346
|
+
return { ...definition, tools: null };
|
|
347
|
+
const servers = [...new Set(tools.servers)];
|
|
348
|
+
if (servers.length === 0) {
|
|
349
|
+
return { ...definition, tools: { delegatedBy: actor.id, servers: [] } };
|
|
350
|
+
}
|
|
351
|
+
const reachable = new Set(await deps.toolServers(actor));
|
|
352
|
+
const missing = servers.filter((server) => !reachable.has(server));
|
|
353
|
+
if (missing.length > 0) {
|
|
354
|
+
throw new IntelError(403, "tool_server_not_delegatable", `You do not reach these MCP servers, so you cannot delegate them: ${missing.join(", ")}`);
|
|
355
|
+
}
|
|
356
|
+
return { ...definition, tools: { delegatedBy: actor.id, servers } };
|
|
357
|
+
}
|
|
330
358
|
/**
|
|
331
359
|
* One immutable definition version, written exactly the way a document's content is (ADR-0005 §1).
|
|
332
360
|
*
|
|
@@ -694,10 +722,12 @@ export function createNodes(deps) {
|
|
|
694
722
|
* would make the second write look like a replay of the first. A caller who repeats the
|
|
695
723
|
* request gets the agent that already exists rather than a second one beside it.
|
|
696
724
|
*
|
|
697
|
-
* ⚠️ A REPLAY MAKES NO SECOND APPLICATION
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
* agent
|
|
725
|
+
* ⚠️ A REPLAY MAKES NO SECOND APPLICATION. It still returns the agent that exists — that promise
|
|
726
|
+
* is not weakened — but a repeat that minted a fresh principal would leave the installation with
|
|
727
|
+
* two machine accounts for one agent, one of which nobody would ever switch off, and would
|
|
728
|
+
* replace a working agent's key with one it never asked for. Gate is not asked at all on that
|
|
729
|
+
* path, and no key travels either. What a replay DOES do is arm the agent's schedules again
|
|
730
|
+
* (#214) — that is idempotent, and it is the repair for a create whose arming failed.
|
|
701
731
|
*
|
|
702
732
|
* ⚠️ Gate is asked BEFORE anything is written, and that ordering IS the answer to "what if Gate
|
|
703
733
|
* is down". Nothing exists yet at that moment, so a Gate that does not answer leaves no node, no
|
|
@@ -722,14 +752,21 @@ export function createNodes(deps) {
|
|
|
722
752
|
const definitionKey = `${input.idempotencyKey}:definition`;
|
|
723
753
|
const existingId = await deps.repository.findIdempotentNode(actor.id, "node.create", input.idempotencyKey);
|
|
724
754
|
if (existingId) {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
755
|
+
const existing = await agentOf(await requireVisible(actor, existingId));
|
|
756
|
+
// ⚠️ A replay arms as well, and that is what makes a create whose sync failed repairable
|
|
757
|
+
// (#214): repeating it with the same key mints nothing, returns this agent, and puts its
|
|
758
|
+
// alarm right. Without it the only repair left would be a fresh key — a second agent.
|
|
759
|
+
if ((existing.definition?.schedules.length ?? 0) > 0) {
|
|
760
|
+
await deps.agentSchedules.sync({ token: caller.token, agentId: existing.node.id });
|
|
761
|
+
}
|
|
762
|
+
return existing;
|
|
729
763
|
}
|
|
730
764
|
if (input.parentId !== null && !(await deps.repository.can(actor, input.parentId, "write"))) {
|
|
731
765
|
throw new IntelError(403, "node_forbidden", "Parent folder cannot be edited");
|
|
732
766
|
}
|
|
767
|
+
// Before Gate, for the same reason the runtime check is before Gate: a delegation the saver
|
|
768
|
+
// cannot back must not leave a machine principal behind for an agent that was never created.
|
|
769
|
+
const definition = await delegationOf(actor, input.definition);
|
|
733
770
|
const nodeId = deps.id();
|
|
734
771
|
// The Application's name is what a person reads in Gate's list, so it has to be enough to
|
|
735
772
|
// recognise the agent by. Title alone would leave two agents called "Research" indis-
|
|
@@ -738,6 +775,22 @@ export function createNodes(deps) {
|
|
|
738
775
|
token: caller.token,
|
|
739
776
|
name: `Intel agent ${input.title} (${nodeId})`,
|
|
740
777
|
});
|
|
778
|
+
// ⚠️ The handover happens BEFORE the node is written, for the same reason Gate is asked before
|
|
779
|
+
// it: at this moment nothing exists on Intel's side, so a runtime that does not take the key
|
|
780
|
+
// leaves no node, no version, no R2 object and no audit event — the agent simply does not come
|
|
781
|
+
// into being and the caller repeats the request. The other order would produce exactly the
|
|
782
|
+
// agent #200 is about: a node that looks finished and dies at its first run. What it can leave
|
|
783
|
+
// behind is a Durable Object holding a key for a node id that will never exist, which is
|
|
784
|
+
// ciphertext nobody can address — ULIDs are not reused.
|
|
785
|
+
try {
|
|
786
|
+
await deps.agentKeys.store({ token: caller.token, agentId: nodeId, key: application.key });
|
|
787
|
+
}
|
|
788
|
+
catch (error) {
|
|
789
|
+
await deps.applications
|
|
790
|
+
.setEnabled({ token: caller.token, applicationId: application.id, enabled: false })
|
|
791
|
+
.catch(() => undefined);
|
|
792
|
+
throw error;
|
|
793
|
+
}
|
|
741
794
|
const timestamp = deps.now().toISOString();
|
|
742
795
|
let node;
|
|
743
796
|
try {
|
|
@@ -766,21 +819,95 @@ export function createNodes(deps) {
|
|
|
766
819
|
.catch(() => undefined);
|
|
767
820
|
throw error;
|
|
768
821
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
822
|
+
// ⚠️ No key in the answer, and no field one could travel in (D29). `application.key` has been
|
|
823
|
+
// in one local variable since Gate returned it, went into the runtime, and is out of reach of
|
|
824
|
+
// every surface above this line.
|
|
825
|
+
const created = (await writeAgentVersion(actor, node, definition, null, definitionKey));
|
|
826
|
+
/**
|
|
827
|
+
* ⚠️ Only when this definition actually schedules something — unlike the save below, which
|
|
828
|
+
* arms unconditionally. A brand-new agent has no earlier definition, so there is no alarm that
|
|
829
|
+
* could be left standing: no schedules means there is nothing to arm and nothing to clear, and
|
|
830
|
+
* the screen creates every agent that way (the profile is where schedules are added).
|
|
831
|
+
*
|
|
832
|
+
* ⚠️ It fails loudly, and the agent stays. Everything is written by now and nothing here can
|
|
833
|
+
* roll an agent back, so the caller is told the one thing they can act on: the schedules are
|
|
834
|
+
* not armed. The repair is repeating the create with the SAME idempotency key — which returns
|
|
835
|
+
* this agent, mints no second Application, and arms it. A fresh key would create a second
|
|
836
|
+
* agent beside this one, which is why the message says "again" and not "retry".
|
|
837
|
+
*/
|
|
838
|
+
if (definition.schedules.length > 0) {
|
|
839
|
+
await deps.agentSchedules.sync({ token: caller.token, agentId: node.id });
|
|
840
|
+
}
|
|
841
|
+
return created;
|
|
782
842
|
},
|
|
783
|
-
|
|
843
|
+
/**
|
|
844
|
+
* A new key for the Application this agent runs as, handed straight to the runtime (D29, #207).
|
|
845
|
+
*
|
|
846
|
+
* ⚠️ A write on the agent node, not a run: `knowledge/write` at the door and the resource ACL
|
|
847
|
+
* here. Whoever may change what an agent does may change which key it does it with; being
|
|
848
|
+
* allowed to press "Run now" says nothing about being allowed to decide its principal.
|
|
849
|
+
*
|
|
850
|
+
* ⚠️ Gate first, runtime second, and nothing in between. Gate issues the replacement before it
|
|
851
|
+
* revokes the old one, so a rotation that fails at Gate changes nothing at all; one that fails
|
|
852
|
+
* at the handover leaves the agent with a key it no longer has — named, repairable, and fixed by
|
|
853
|
+
* rotating again. The alternative order cannot exist: the runtime has nothing to store until
|
|
854
|
+
* Gate has answered.
|
|
855
|
+
*
|
|
856
|
+
* ⚠️ The key is in one local variable and in no answer. This is the operation that exists BECAUSE
|
|
857
|
+
* Gate hands a key out exactly once — reading one back is not something any repair can do.
|
|
858
|
+
*/
|
|
859
|
+
async rotateAgentKey(actor, input, caller) {
|
|
860
|
+
const node = await requireVisible(actor, input.nodeId);
|
|
861
|
+
if (node.kind !== "agent") {
|
|
862
|
+
throw new IntelError(409, "not_an_agent", "Only agents have an application key");
|
|
863
|
+
}
|
|
864
|
+
if (!(await deps.repository.can(actor, node.id, "write"))) {
|
|
865
|
+
throw new IntelError(403, "node_forbidden", "This agent cannot be edited");
|
|
866
|
+
}
|
|
867
|
+
if (!deps.agentRuntimeAvailable()) {
|
|
868
|
+
throw new IntelError(503, "agent_runtime_not_configured", "This installation has no agent runtime, so there is nothing to give a key to");
|
|
869
|
+
}
|
|
870
|
+
const applicationId = await deps.repository.agentApplicationId(node.id);
|
|
871
|
+
if (applicationId === null) {
|
|
872
|
+
// An agent written before #182, restored from a bundle, or imported. Giving it a principal
|
|
873
|
+
// is an act in Gate, and inventing one here would make Intel the author of a machine
|
|
874
|
+
// account nobody asked for.
|
|
875
|
+
throw new IntelError(409, "agent_application_missing", "This agent has no Gate application, so there is no key to replace");
|
|
876
|
+
}
|
|
877
|
+
const rotated = await deps.applications.rotateKey({ token: caller.token, applicationId });
|
|
878
|
+
await deps.agentKeys.store({ token: caller.token, agentId: node.id, key: rotated.key });
|
|
879
|
+
const rotatedAt = deps.now().toISOString();
|
|
880
|
+
// ⚠️ Written LAST, and only once both sides took the change. An event ahead of the runtime
|
|
881
|
+
// would record a rotation that may not have arrived, and this row is the only trace the act
|
|
882
|
+
// leaves in Intel — nothing about the node itself changes, so a reader who diffed versions
|
|
883
|
+
// would see an agent that silently began acting with a different credential.
|
|
884
|
+
await deps.repository.recordAgentKeyRotation({
|
|
885
|
+
auditId: deps.id(),
|
|
886
|
+
actorId: actor.id,
|
|
887
|
+
nodeId: node.id,
|
|
888
|
+
applicationId,
|
|
889
|
+
occurredAt: rotatedAt,
|
|
890
|
+
});
|
|
891
|
+
return { nodeId: node.id, applicationId, rotatedAt };
|
|
892
|
+
},
|
|
893
|
+
/**
|
|
894
|
+
* A new definition version, and the alarm that has to match it (#214).
|
|
895
|
+
*
|
|
896
|
+
* ⚠️ The runtime is told to re-arm after EVERY definition write, not only after one that names a
|
|
897
|
+
* schedule. The alternative — comparing against the definition that was there before — needs the
|
|
898
|
+
* previous document read back out of R2, and it is wrong in exactly the direction that matters:
|
|
899
|
+
* a save that removes the LAST schedule has nothing to compare against in the new definition and
|
|
900
|
+
* is precisely the write that has to clear the alarm. One binding call per save buys the rule
|
|
901
|
+
* "what is written is what is armed" without a comparison anybody could get backwards.
|
|
902
|
+
*
|
|
903
|
+
* ⚠️ Written first, armed second, and the definition STAYS when the arming fails. It has to: the
|
|
904
|
+
* runtime reads the definition back out of Intel to arm it, so there is no order in which it
|
|
905
|
+
* could be armed before it exists. What the caller gets instead of silence is
|
|
906
|
+
* `agent_schedules_not_armed` — the version is in the history, the profile shows it, and saving
|
|
907
|
+
* again arms it. Rolling the version back would be worse than the unarmed alarm: it would throw
|
|
908
|
+
* away an edit somebody made because a Worker was briefly unreachable.
|
|
909
|
+
*/
|
|
910
|
+
async saveAgentDefinition(actor, input, caller) {
|
|
784
911
|
const existingId = await deps.repository.findIdempotentNode(actor.id, "node.save", input.idempotencyKey);
|
|
785
912
|
if (existingId)
|
|
786
913
|
return await agentOf(await requireVisible(actor, input.nodeId));
|
|
@@ -794,7 +921,9 @@ export function createNodes(deps) {
|
|
|
794
921
|
if (node.currentVersionId !== input.baseVersionId) {
|
|
795
922
|
throw new IntelError(409, "version_conflict", "A newer version already exists");
|
|
796
923
|
}
|
|
797
|
-
|
|
924
|
+
const saved = await writeAgentVersion(actor, node, await delegationOf(actor, input.definition), input.baseVersionId, input.idempotencyKey);
|
|
925
|
+
await deps.agentSchedules.sync({ token: caller.token, agentId: node.id });
|
|
926
|
+
return saved;
|
|
798
927
|
},
|
|
799
928
|
async saveAttachment(actor, input) {
|
|
800
929
|
const existingId = await deps.repository.findIdempotentNode(actor.id, "node.save", input.idempotencyKey);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentList, AppendTableRowsInput, AppendTableRowsResult, ArchiveNodeInput, CreateAgentInput, CreatedAgent, CreateNodeInput, DefineTableInput, DeleteTableRowsInput, DeleteTableRowsResult, Flow, FlowVersion, GetAgentInput, ListAgentsInput, ListNodesInput, Node, NodeAgent, NodeAttachment, NodeCitation, NodeDocument, NodeGraph, NodeGraphInput, NodeLink, NodeTable, NodeVersion, RedefineTableInput, ResolveNodeLinksInput, ResolveNodeLinksResult, ResourceGrant, ResourceGrantList, ResourceVerb, RevokeGrantInput, SaveAgentDefinitionInput, SaveAttachmentInput, SaveNodeVersionInput, SearchInput, ShareInput, ShareResult, UpdateNodeInput, UpdateTableRowsInput, UpdateTableRowsResult } from "@anchrd/intel-contract";
|
|
1
|
+
import type { AgentKeyRotated, AgentList, AppendTableRowsInput, AppendTableRowsResult, ArchiveNodeInput, CreateAgentInput, CreatedAgent, CreateNodeInput, DefineTableInput, DeleteTableRowsInput, DeleteTableRowsResult, Flow, FlowVersion, GetAgentInput, ListAgentsInput, ListNodesInput, Node, NodeAgent, NodeAttachment, NodeCitation, NodeDocument, NodeGraph, NodeGraphInput, NodeLink, NodeTable, NodeVersion, RedefineTableInput, ResolveNodeLinksInput, ResolveNodeLinksResult, ResourceGrant, ResourceGrantList, ResourceVerb, RevokeGrantInput, RotateAgentKeyInput, SaveAgentDefinitionInput, SaveAttachmentInput, SaveNodeVersionInput, SearchInput, ShareInput, ShareResult, UpdateNodeInput, UpdateTableRowsInput, UpdateTableRowsResult } from "@anchrd/intel-contract";
|
|
2
2
|
import type { SemanticIndex } from "../adapters/semantic-index/semantic-index.types.js";
|
|
3
3
|
export interface Actor {
|
|
4
4
|
id: string;
|
|
@@ -55,6 +55,26 @@ export interface NodeRepository {
|
|
|
55
55
|
findIdempotentRevocation(actorId: string, idempotencyKey: string): Promise<boolean | null>;
|
|
56
56
|
insertNode(input: NewNode): Promise<Node>;
|
|
57
57
|
agentApplicationId(nodeId: string): Promise<string | null>;
|
|
58
|
+
/**
|
|
59
|
+
* The audit row for replacing the key of the Application an agent runs as (D29, #207).
|
|
60
|
+
*
|
|
61
|
+
* ⚠️ Its own write because it is the only mutation on an agent node that changes nothing IN the
|
|
62
|
+
* node — no version, no row, nothing a reader could diff afterwards. Swapping which credential an
|
|
63
|
+
* agent acts with is the most security-relevant thing anybody does to it, and without this it
|
|
64
|
+
* would be the one act on the tree that leaves no trace in Intel at all. Gate audits its side;
|
|
65
|
+
* Gate does not know which Intel node the principal belongs to.
|
|
66
|
+
*
|
|
67
|
+
* ⚠️ `applicationId` names the principal and is a name, not a credential. The key itself is not a
|
|
68
|
+
* parameter here and must never become one — audit is metadata, and this is exactly the row a
|
|
69
|
+
* "helpful" second field would leak it into.
|
|
70
|
+
*/
|
|
71
|
+
recordAgentKeyRotation(input: {
|
|
72
|
+
auditId: string;
|
|
73
|
+
actorId: string;
|
|
74
|
+
nodeId: string;
|
|
75
|
+
applicationId: string;
|
|
76
|
+
occurredAt: string;
|
|
77
|
+
}): Promise<void>;
|
|
58
78
|
getVersion(versionId: string): Promise<NodeVersion | null>;
|
|
59
79
|
appendVersion(input: NewNodeVersion): Promise<"saved" | "conflict">;
|
|
60
80
|
appendTableVersion(input: NewTableVersion): Promise<NodeVersion>;
|
|
@@ -184,6 +204,19 @@ export interface AgentApplications {
|
|
|
184
204
|
id: string;
|
|
185
205
|
key: string;
|
|
186
206
|
}>;
|
|
207
|
+
/**
|
|
208
|
+
* A replacement key for an Application that already exists, invalidating every earlier one.
|
|
209
|
+
*
|
|
210
|
+
* ⚠️ Gate issues before it revokes, so a failed rotation leaves the old key working. That is what
|
|
211
|
+
* makes `rotate-key` a safe repair rather than a way to lock an agent out: the only state this
|
|
212
|
+
* can leave behind is "new key issued, runtime never got it", and rotating again fixes it.
|
|
213
|
+
*/
|
|
214
|
+
rotateKey(input: {
|
|
215
|
+
token: string;
|
|
216
|
+
applicationId: string;
|
|
217
|
+
}): Promise<{
|
|
218
|
+
key: string;
|
|
219
|
+
}>;
|
|
187
220
|
setEnabled(input: {
|
|
188
221
|
token: string;
|
|
189
222
|
applicationId: string;
|
|
@@ -198,6 +231,38 @@ export interface NodesDeps {
|
|
|
198
231
|
content: ContentStore;
|
|
199
232
|
applications: AgentApplications;
|
|
200
233
|
agentRuntimeAvailable(): boolean;
|
|
234
|
+
/**
|
|
235
|
+
* The runtime's key store, as the tree needs it (D29, #207).
|
|
236
|
+
*
|
|
237
|
+
* ⚠️ This is where the Application key GOES, and it is the only place it goes. Gate hands it out
|
|
238
|
+
* once, this port carries it into the agent's Durable Object over the service binding, and the
|
|
239
|
+
* local variable that held it dies with the call. Nothing in this file writes it to D1, R2, an
|
|
240
|
+
* audit row, a log line or a response — the proof is the absence of the value in every argument
|
|
241
|
+
* every port was called with, not a redaction somebody could forget.
|
|
242
|
+
*/
|
|
243
|
+
agentKeys: {
|
|
244
|
+
store(input: {
|
|
245
|
+
token: string;
|
|
246
|
+
agentId: string;
|
|
247
|
+
key: string;
|
|
248
|
+
}): Promise<void>;
|
|
249
|
+
};
|
|
250
|
+
/**
|
|
251
|
+
* The runtime's alarm, brought in line with the definition that was just written (#214).
|
|
252
|
+
*
|
|
253
|
+
* ⚠️ Required, not optional, and it carries no schedules. The runtime arms its alarm only when
|
|
254
|
+
* something tells it to look — a chat turn, a resume, a manual run, or this call — so a definition
|
|
255
|
+
* written without it leaves an agent whose schedules are visible in its profile and fire never.
|
|
256
|
+
* What travels is the agent's ID and the caller's bearer; the runtime re-reads the definition from
|
|
257
|
+
* Intel with the agent's own token, which is what keeps a schedule something that was written
|
|
258
|
+
* before it can be acted on.
|
|
259
|
+
*/
|
|
260
|
+
agentSchedules: {
|
|
261
|
+
sync(input: {
|
|
262
|
+
token: string;
|
|
263
|
+
agentId: string;
|
|
264
|
+
}): Promise<void>;
|
|
265
|
+
};
|
|
201
266
|
id(): string;
|
|
202
267
|
now(): Date;
|
|
203
268
|
externalFlowCallers(actor: Actor, folderId: string): Promise<{
|
|
@@ -205,6 +270,14 @@ export interface NodesDeps {
|
|
|
205
270
|
hidden: number;
|
|
206
271
|
}>;
|
|
207
272
|
flowNodeReferences(actor: Actor, folderId: string): Promise<string[]>;
|
|
273
|
+
/**
|
|
274
|
+
* The handles of the MCP servers this actor reaches through the portal right now (D30).
|
|
275
|
+
*
|
|
276
|
+
* ⚠️ Injected as a live question, never as a stored list. Delegating a server is only allowed to
|
|
277
|
+
* somebody who has it themselves, and "has it" can only be answered by asking the portal at the
|
|
278
|
+
* moment of the save — a mirrored table here would be the permission mirror ADR-0003 removed.
|
|
279
|
+
*/
|
|
280
|
+
toolServers(actor: Actor): Promise<string[]>;
|
|
208
281
|
hash(content: string | Uint8Array): Promise<string>;
|
|
209
282
|
indexing: {
|
|
210
283
|
enqueue(versionId: string): Promise<void>;
|
|
@@ -254,14 +327,29 @@ export interface NodeService {
|
|
|
254
327
|
redefineTable(actor: Actor, input: RedefineTableInput): Promise<NodeTable>;
|
|
255
328
|
getAgent(actor: Actor, input: GetAgentInput): Promise<NodeAgent>;
|
|
256
329
|
/**
|
|
257
|
-
* An agent node, its first definition, and the Gate Application it runs as (#182).
|
|
330
|
+
* An agent node, its first definition, and the Gate Application it runs as (#182, D29).
|
|
258
331
|
*
|
|
259
|
-
* ⚠️ The answer carries
|
|
260
|
-
*
|
|
261
|
-
*
|
|
332
|
+
* ⚠️ The answer carries NO key. The one Gate issues goes straight into the agent runtime over the
|
|
333
|
+
* service binding, inside this call, and the caller never sees it — which is what makes an agent
|
|
334
|
+
* created through the screen able to run without a terminal step (#200, #207).
|
|
262
335
|
*/
|
|
263
336
|
createAgent(actor: Actor, input: CreateAgentInput, caller: GateCaller): Promise<CreatedAgent>;
|
|
264
|
-
|
|
337
|
+
/**
|
|
338
|
+
* Replace the key of the Application an agent runs as, and give the new one to the runtime.
|
|
339
|
+
*
|
|
340
|
+
* ⚠️ The repair path for an agent that answers `agent_key_missing`, and the only one: Gate keeps
|
|
341
|
+
* a key hashed, so an agent whose Durable Object lost its key cannot be handed the old one back.
|
|
342
|
+
* It is a write on the agent node — `knowledge/write` and the resource ACL — not a run.
|
|
343
|
+
*/
|
|
344
|
+
rotateAgentKey(actor: Actor, input: RotateAgentKeyInput, caller: GateCaller): Promise<AgentKeyRotated>;
|
|
345
|
+
/**
|
|
346
|
+
* A new definition version for an agent, and the alarm that goes with it (#214).
|
|
347
|
+
*
|
|
348
|
+
* ⚠️ It takes the caller's bearer for the same reason `createAgent` does: the runtime is told to
|
|
349
|
+
* re-arm over the service binding, and what travels on that call is the person behind this request
|
|
350
|
+
* — never a service key (D19).
|
|
351
|
+
*/
|
|
352
|
+
saveAgentDefinition(actor: Actor, input: SaveAgentDefinitionInput, caller: GateCaller): Promise<NodeAgent>;
|
|
265
353
|
listAgents(actor: Actor, input: ListAgentsInput): Promise<AgentList>;
|
|
266
354
|
getAttachment(actor: Actor, nodeId: string): Promise<NodeAttachment>;
|
|
267
355
|
readAttachment(actor: Actor, nodeId: string): Promise<NodeAttachmentBody>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type ToolServer } from "@anchrd/intel-contract";
|
|
2
|
+
/**
|
|
3
|
+
* The portal's own directory tool. It is an ordinary entry in `tools/list`, so it is reached the
|
|
4
|
+
* same way every other tool is: `tools/call` with the asking user's portal token.
|
|
5
|
+
*
|
|
6
|
+
* ⚠️ Its presence in the live list is also the permission check. A deployment whose portal does not
|
|
7
|
+
* offer it, or a user whose Access policies hide it, gets no server list at all — and therefore
|
|
8
|
+
* cannot delegate anything. That is deliberate: the alternative would be inventing the server list
|
|
9
|
+
* out of tool names, which is exactly the guess this module exists to avoid.
|
|
10
|
+
*/
|
|
11
|
+
export declare const ServerDirectoryTool = "portal_list_servers";
|
|
12
|
+
export interface ToolServerDirectoryAnswer {
|
|
13
|
+
isError: boolean;
|
|
14
|
+
content: unknown[];
|
|
15
|
+
structuredContent?: unknown;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The servers this user reaches: the portal's directory, confirmed against the portal's live tool
|
|
19
|
+
* list.
|
|
20
|
+
*
|
|
21
|
+
* Two sets come out of one answer, and keeping them apart is the whole of this function:
|
|
22
|
+
*
|
|
23
|
+
* - **`declared`** — every identifier the directory mentions at all, disabled rows included. It is
|
|
24
|
+
* the universe `serverOf` attributes against, and it must be as WIDE as possible. ⚠️ Narrowing it
|
|
25
|
+
* widens the delegation: drop `wiki_extra` because the portal marked it disabled, and the live
|
|
26
|
+
* tool `wiki_extra__read` falls back onto `wiki` — a tool of a server nobody delegated, handed to
|
|
27
|
+
* an agent that was given `wiki`. Filtering therefore happens after attribution, never before it.
|
|
28
|
+
* - **the returned list** — what may actually be delegated: enabled, and confirmed by at least one
|
|
29
|
+
* live tool. Intel cannot prove that a directory identifier is the same string the namespace uses,
|
|
30
|
+
* so it only ever offers handles it has just seen work.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ What remains outside Intel's reach: a directory that omits a server whose tools ARE in
|
|
33
|
+
* `tools/list`. Nothing in the answer distinguishes that from a tool of the enclosing namespace, and
|
|
34
|
+
* the portal is the authority on its own servers — so an incomplete directory widens by exactly one
|
|
35
|
+
* namespace level, and it is a portal defect rather than something this code can detect.
|
|
36
|
+
*/
|
|
37
|
+
export interface ToolServerDirectory {
|
|
38
|
+
/** Every identifier the directory mentioned. Attribute against this, never against `servers`. */
|
|
39
|
+
declared: string[];
|
|
40
|
+
/** What may be offered and delegated: enabled, and confirmed by a live tool. */
|
|
41
|
+
servers: ToolServer[];
|
|
42
|
+
}
|
|
43
|
+
export declare function toolServersFrom(input: {
|
|
44
|
+
directory: ToolServerDirectoryAnswer;
|
|
45
|
+
toolNames: string[];
|
|
46
|
+
}): ToolServerDirectory | null;
|