@anchrd/intel-api 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/cloudflare/cloudflare.js +0 -68
- package/dist/adapters/cloudflare/cloudflare.types.d.ts +0 -39
- package/dist/adapters/db/db-flows.js +1 -1
- package/dist/adapters/db/db-grants.js +1 -1
- package/dist/adapters/db/db.js +16 -113
- package/dist/bundle/bundle.js +28 -141
- package/dist/cli/cli.js +3 -9
- package/dist/http/http.js +1 -206
- package/dist/http/http.types.d.ts +0 -8
- package/dist/indexing/indexing.js +18 -89
- package/dist/intel/intel.js +4 -9
- package/dist/intel/intel.types.d.ts +0 -6
- package/dist/mcp/mcp.js +3 -292
- package/dist/mcp/mcp.types.d.ts +2 -7
- package/dist/nodes/document-links/document-links.d.ts +6 -8
- package/dist/nodes/document-links/document-links.js +8 -31
- package/dist/nodes/nodes.js +20 -886
- package/dist/nodes/nodes.types.d.ts +10 -169
- package/dist/tools/tools.js +37 -148
- package/dist/tools/tools.types.d.ts +0 -21
- package/migrations/0018_no_context_policy_at_last.sql +13 -6
- package/migrations/0019_one_name_for_the_grants.sql +52 -0
- package/package.json +2 -2
- package/dist/adapters/cloudflare-api/cloudflare-api.d.ts +0 -22
- package/dist/adapters/cloudflare-api/cloudflare-api.js +0 -306
- package/dist/adapters/cloudflare-api/cloudflare-api.types.d.ts +0 -64
- package/dist/adapters/cloudflare-api/cloudflare-api.types.js +0 -1
- package/dist/adapters/gate-applications/gate-applications.d.ts +0 -23
- package/dist/adapters/gate-applications/gate-applications.js +0 -88
- package/dist/adapters/tool-delegation/tool-delegation.d.ts +0 -22
- package/dist/adapters/tool-delegation/tool-delegation.js +0 -90
- package/dist/agent-costs/agent-costs.d.ts +0 -16
- package/dist/agent-costs/agent-costs.js +0 -105
- package/dist/agent-costs/agent-costs.types.d.ts +0 -30
- package/dist/agent-costs/agent-costs.types.js +0 -1
- package/dist/agent-runtime/agent-runtime.d.ts +0 -16
- package/dist/agent-runtime/agent-runtime.js +0 -150
- package/dist/agent-runtime/agent-runtime.types.d.ts +0 -122
- package/dist/agent-runtime/agent-runtime.types.js +0 -1
- package/dist/model-catalog/model-catalog.d.ts +0 -2
- package/dist/model-catalog/model-catalog.js +0 -99
- package/dist/model-catalog/model-catalog.types.d.ts +0 -15
- package/dist/model-catalog/model-catalog.types.js +0 -1
- package/dist/nodes/board/board.d.ts +0 -61
- package/dist/nodes/board/board.js +0 -826
- package/dist/nodes/board/board.types.d.ts +0 -38
- package/dist/nodes/board/board.types.js +0 -1
- package/migrations/0013_agents_in_the_tree.sql +0 -76
- package/migrations/0014_agent_applications.sql +0 -25
- package/migrations/0015_tools_delegated_from_a_connection.sql +0 -15
- package/migrations/0016_boards_in_the_tree.sql +0 -80
package/dist/cli/cli.js
CHANGED
|
@@ -12,15 +12,9 @@ const interfaces = [
|
|
|
12
12
|
functions: ["read", "create", "write", "publish", "run", "share"],
|
|
13
13
|
},
|
|
14
14
|
{ handle: "tools", functions: ["read", "test", "execute", "admin"] },
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// anywhere else would never appear in the rules that `can("agents", "run")` reads.
|
|
19
|
-
//
|
|
20
|
-
// Without it there is no state between "administrator" and "not at all": the runtime falls back to
|
|
21
|
-
// `intel:admin`, which is the first-grant exit in the same sense as everywhere else, not the way a
|
|
22
|
-
// team lets a colleague talk to an agent.
|
|
23
|
-
{ handle: "agents", functions: ["run"] },
|
|
15
|
+
// ⚠️ `agents:run` is deliberately NOT here any more (#390). `bootstrap` declares, it never
|
|
16
|
+
// revokes, so an installation that has the interface keeps it as a harmless leftover — but a new
|
|
17
|
+
// one is not asked to decide about a permission nothing reads.
|
|
24
18
|
// `mcp:connect` means the same thing at every MCP service of this installation. A separate name
|
|
25
19
|
// for the same thing forces the operator to check, service by service, which permission carries
|
|
26
20
|
// portal access.
|
package/dist/http/http.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AppendTableRowsInput, ArchiveFlowInput, ArchiveNodeInput, CancelFlowRunInput, CompleteFlowRunStepInput, CreateFlowInput, CreateNodeInput, DefineTableInput, DeleteTableRowsInput, ExecuteToolInput, GetFlowInput, GetFlowRunInput, GetFlowVersionInput, GetNodeInput, GetNodeVersionInput, ListFlowRunsInput, ListFlowsInput, ListNodesInput, NodeGraphInput, PreviewFlowPublishInput, PublishFlowInput, RedefineTableInput, RelationGraphInput, ResolveNodeLinksInput, RevokeGrantInput, 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";
|
|
@@ -78,16 +78,6 @@ function asFlowActor(authorization) {
|
|
|
78
78
|
isAdmin: authorization.can("intel", "admin"),
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
|
-
// The same `agents/run` the agent Worker asks for, resolved once at Intel's door. `intel/admin`
|
|
82
|
-
// opens it for the same reason it opens every other one — `permits` is where that lives.
|
|
83
|
-
function asAgentActor(authorization) {
|
|
84
|
-
return {
|
|
85
|
-
id: authorization.identity.id,
|
|
86
|
-
email: authorization.identity.email,
|
|
87
|
-
isAdmin: authorization.can("intel", "admin"),
|
|
88
|
-
canRun: permits(authorization, "agents", "run"),
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
81
|
export function createHttp(deps) {
|
|
92
82
|
const app = new Hono();
|
|
93
83
|
async function authenticated(headers) {
|
|
@@ -136,94 +126,6 @@ export function createHttp(deps) {
|
|
|
136
126
|
name: identity.name ?? null,
|
|
137
127
|
});
|
|
138
128
|
});
|
|
139
|
-
// What this deployment is equipped to do (#190) — facts about the installation, not about the
|
|
140
|
-
// caller, which is why it needs no capability beyond being signed in. `/session` stays identity
|
|
141
|
-
// only on purpose; this is the separate answer the UI asks for instead of reading a 503 as one.
|
|
142
|
-
// It sits behind the auth middleware like everything else, so an anonymous probe still learns
|
|
143
|
-
// nothing about whether a runtime exists.
|
|
144
|
-
app.get("/capabilities", (context) => {
|
|
145
|
-
return context.json({ agentRuntime: deps.agents.available() });
|
|
146
|
-
});
|
|
147
|
-
/**
|
|
148
|
-
* What the models on offer cost and how much they hold (#257).
|
|
149
|
-
*
|
|
150
|
-
* ⚠️ Intel asks Cloudflare, never the browser. The account endpoint needs a token and a token does
|
|
151
|
-
* not belong in a SPA — which is the sentence the hard-coded table in `packages/ui` used to carry
|
|
152
|
-
* as the reason it stayed hard-coded. This route is the answer to it: the figures cross the wire,
|
|
153
|
-
* the credential does not.
|
|
154
|
-
*
|
|
155
|
-
* ⚠️ No capability beyond being signed in. It is a fact about the installation and about nobody's
|
|
156
|
-
* data — the same footing `/capabilities` stands on.
|
|
157
|
-
*
|
|
158
|
-
* ⚠️ The `cache-control` is the caching the ticket asks for, and it is the caching that matters:
|
|
159
|
-
* the waste it names is "one fetch every time the select is opened", and that fetch is the
|
|
160
|
-
* browser's. A Worker isolate has nowhere durable to keep a table between requests, and inventing
|
|
161
|
-
* storage for a list of prices would cost more than the request it saved.
|
|
162
|
-
*/
|
|
163
|
-
app.get("/models", async (context) => {
|
|
164
|
-
context.header("cache-control", "private, max-age=3600");
|
|
165
|
-
return context.json(await deps.models.list());
|
|
166
|
-
});
|
|
167
|
-
/**
|
|
168
|
-
* The agent runtime, reached through Intel.
|
|
169
|
-
*
|
|
170
|
-
* ⚠️ This list IS the door. The runtime serves more than what stands here — `/mcp` is the agent's
|
|
171
|
-
* own MCP surface, which the portal dials with its own credential (ADR-0005 §7), and
|
|
172
|
-
* `/mail/inbound` is authorized by a webhook signature. Neither is a browser path, and neither
|
|
173
|
-
* becomes one by being reachable: anything not named below is a 404 from Hono's router, so there
|
|
174
|
-
* is no allow-list beside the routes that could go quietly out of step with them.
|
|
175
|
-
*
|
|
176
|
-
* ⚠️ `agents/run` — not `knowledge/read`. Reading an agent's page is a knowledge read; driving the
|
|
177
|
-
* agent is the capability the runtime itself insists on, and Intel refuses on the same terms
|
|
178
|
-
* BEFORE the binding is touched, so a denial never reaches the runtime at all.
|
|
179
|
-
*/
|
|
180
|
-
const runtimeRoutes = [
|
|
181
|
-
{ method: "GET", pattern: "/state", path: () => "/state" },
|
|
182
|
-
{ method: "GET", pattern: "/runs", path: () => "/runs" },
|
|
183
|
-
{
|
|
184
|
-
method: "GET",
|
|
185
|
-
pattern: "/runs/:runId",
|
|
186
|
-
path: (context) => `/runs/${encodeURIComponent(context.req.param("runId") ?? "")}`,
|
|
187
|
-
},
|
|
188
|
-
{ method: "GET", pattern: "/log", path: () => "/log" },
|
|
189
|
-
{ method: "POST", pattern: "/chat", path: () => "/chat" },
|
|
190
|
-
{ method: "POST", pattern: "/pause", path: () => "/pause" },
|
|
191
|
-
{ method: "POST", pattern: "/resume", path: () => "/resume" },
|
|
192
|
-
{ method: "POST", pattern: "/run", path: () => "/run" },
|
|
193
|
-
];
|
|
194
|
-
/**
|
|
195
|
-
* What this agent has cost — answered by Intel itself, not forwarded (#251).
|
|
196
|
-
*
|
|
197
|
-
* ⚠️ It stands under `/agents/:agentId` and is NOT in `runtimeRoutes`, which is the whole point of
|
|
198
|
-
* writing it out here. The runtime has no idea what anything costs: the figures come from
|
|
199
|
-
* Cloudflare's AI Gateway log, which needs an `AI Gateway: Read` token, and that token belongs in
|
|
200
|
-
* Intel rather than in the Worker that executes model-driven tool calls — the runtime's own
|
|
201
|
-
* `CLAUDE.md` keeps the number of credentials there deliberately small. The runtime's part is the
|
|
202
|
-
* stamp on the model call and nothing more.
|
|
203
|
-
*
|
|
204
|
-
* ⚠️ `agents/run`, matching the run list this stands beside. What an agent spends is as sensitive
|
|
205
|
-
* as what it did, and the two must not be reachable on different terms.
|
|
206
|
-
*/
|
|
207
|
-
app.get("/agents/:agentId/costs", async (context) => {
|
|
208
|
-
const authorization = context.get("authorization");
|
|
209
|
-
return context.json(await deps.agentCosts.read(asAgentActor(authorization), context.req.param("agentId")));
|
|
210
|
-
});
|
|
211
|
-
for (const route of runtimeRoutes) {
|
|
212
|
-
app.on(route.method, `/agents/:agentId${route.pattern}`, async (context) => {
|
|
213
|
-
const authorization = context.get("authorization");
|
|
214
|
-
// The runtime's answer is returned untouched, headers included: the chat is a stream, and a
|
|
215
|
-
// body read here to inspect it would be a stream nobody could read afterwards.
|
|
216
|
-
return await deps.agents.forward({
|
|
217
|
-
actor: asAgentActor(authorization),
|
|
218
|
-
token: context.get("token"),
|
|
219
|
-
agentId: context.req.param("agentId"),
|
|
220
|
-
method: route.method,
|
|
221
|
-
path: route.path(context),
|
|
222
|
-
...(route.method === "POST" ? { body: await context.req.text() } : {}),
|
|
223
|
-
signal: context.req.raw.signal,
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
129
|
app.get("/nodes", async (context) => {
|
|
228
130
|
const auth = requireCapability(context, "knowledge", "read");
|
|
229
131
|
const url = new URL(context.req.url);
|
|
@@ -251,51 +153,6 @@ export function createHttp(deps) {
|
|
|
251
153
|
});
|
|
252
154
|
return context.json(await deps.nodes.graph(asActor(auth), input));
|
|
253
155
|
});
|
|
254
|
-
// ⚠️ Before `/nodes/:nodeId`, or the router would read "agents" as a node ID. Same order and same
|
|
255
|
-
// reason as `/nodes/graph` above.
|
|
256
|
-
app.get("/nodes/agents", async (context) => {
|
|
257
|
-
const auth = requireCapability(context, "knowledge", "read");
|
|
258
|
-
const url = new URL(context.req.url);
|
|
259
|
-
requireKnownQuery(url, ["parentId", "includeArchived"]);
|
|
260
|
-
const parentId = url.searchParams.get("parentId");
|
|
261
|
-
// ⚠️ The key is only set when the caller sent it. Omitted means the whole tree and `null` means
|
|
262
|
-
// the root level, so defaulting a missing parameter to `null` here would silently answer a
|
|
263
|
-
// different question than the one asked.
|
|
264
|
-
const input = ListAgentsInput.parse({
|
|
265
|
-
...(url.searchParams.has("parentId") ? { parentId: parentId === "" ? null : parentId } : {}),
|
|
266
|
-
includeArchived: url.searchParams.get("includeArchived") === "true",
|
|
267
|
-
});
|
|
268
|
-
return context.json(await deps.nodes.listAgents(asActor(auth), input));
|
|
269
|
-
});
|
|
270
|
-
/**
|
|
271
|
-
* ⚠️ NO response in Intel's HTTP surface carries a credential any more (D29, #207).
|
|
272
|
-
*
|
|
273
|
-
* Creating an agent still mints its Gate Application with the caller's own bearer — so this needs
|
|
274
|
-
* `applications:write` in Gate on top of `knowledge:create` in Intel — but the key Gate issues
|
|
275
|
-
* once goes straight into the agent runtime over the service binding, inside this request. The
|
|
276
|
-
* answer is the same shape every agent read has. Until #207 it carried the key in plain text and
|
|
277
|
-
* a person had to put it into a Worker secret by hand, which is why an agent created here could
|
|
278
|
-
* never run (#200).
|
|
279
|
-
*/
|
|
280
|
-
app.post("/nodes/agents", async (context) => {
|
|
281
|
-
const auth = requireCapability(context, "knowledge", "create");
|
|
282
|
-
const input = CreateAgentInput.parse(await context.req.json().catch(() => null));
|
|
283
|
-
return context.json(await deps.nodes.createAgent(asActor(auth), input, { token: context.get("token") }), 201);
|
|
284
|
-
});
|
|
285
|
-
/**
|
|
286
|
-
* The repair path, and the only one an agent that lost its key has (D29, #207).
|
|
287
|
-
*
|
|
288
|
-
* ⚠️ `knowledge/write`, not `agents/run`. Which principal an agent acts as is a property of the
|
|
289
|
-
* agent, changed by whoever may change the agent; being allowed to press "Run now" is a different
|
|
290
|
-
* question and answers a different one. The resource ACL on the node is checked by the service.
|
|
291
|
-
*
|
|
292
|
-
* ⚠️ Before `/nodes/:nodeId`, like every other literal segment under `/nodes`.
|
|
293
|
-
*/
|
|
294
|
-
app.post("/nodes/agents/:nodeId/rotate-key", async (context) => {
|
|
295
|
-
const auth = requireCapability(context, "knowledge", "write");
|
|
296
|
-
const input = RotateAgentKeyInput.parse({ nodeId: context.req.param("nodeId") });
|
|
297
|
-
return context.json(await deps.nodes.rotateAgentKey(asActor(auth), input, { token: context.get("token") }));
|
|
298
|
-
});
|
|
299
156
|
// ⚠️ Before `/nodes/:nodeId`, or the router would read "export" as a node ID. The root has no
|
|
300
157
|
// node to address, so the whole installation — as this caller may read it — exports here (#136).
|
|
301
158
|
app.get("/nodes/export", async (context) => {
|
|
@@ -337,20 +194,6 @@ export function createHttp(deps) {
|
|
|
337
194
|
const input = GetNodeInput.parse({ nodeId: context.req.param("nodeId") });
|
|
338
195
|
return zipResponse(await deps.bundle.exportSubtree(asBundleActor(auth), input.nodeId));
|
|
339
196
|
});
|
|
340
|
-
app.get("/nodes/:nodeId/agent", async (context) => {
|
|
341
|
-
const auth = requireCapability(context, "knowledge", "read");
|
|
342
|
-
return context.json(await deps.nodes.getAgent(asActor(auth), { nodeId: context.req.param("nodeId") }));
|
|
343
|
-
});
|
|
344
|
-
app.post("/nodes/:nodeId/agent", async (context) => {
|
|
345
|
-
const auth = requireCapability(context, "knowledge", "write");
|
|
346
|
-
const input = SaveAgentDefinitionInput.parse(await context.req.json().catch(() => null));
|
|
347
|
-
if (input.nodeId !== context.req.param("nodeId")) {
|
|
348
|
-
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
349
|
-
}
|
|
350
|
-
// ⚠️ The caller's own bearer travels on, like it does when an agent is created: writing a
|
|
351
|
-
// definition also arms what it schedules, and that call is made as the person saving (#214).
|
|
352
|
-
return context.json(await deps.nodes.saveAgentDefinition(asActor(auth), input, { token: context.get("token") }), 201);
|
|
353
|
-
});
|
|
354
197
|
app.get("/nodes/:nodeId/versions", async (context) => {
|
|
355
198
|
const auth = requireCapability(context, "knowledge", "read");
|
|
356
199
|
return context.json(await deps.nodes.listVersions(asActor(auth), context.req.param("nodeId")));
|
|
@@ -438,54 +281,6 @@ export function createHttp(deps) {
|
|
|
438
281
|
}
|
|
439
282
|
return context.json(await deps.nodes.redefineTable(asActor(auth), input), 201);
|
|
440
283
|
});
|
|
441
|
-
// The board (#285). One read and five task operations, all action-style POSTs for the same reason
|
|
442
|
-
// the table mutations are: what they address is a task ID in the body, and none of them has an
|
|
443
|
-
// address of its own to PUT to. Every one writes a version, so they all answer 201 like the
|
|
444
|
-
// table's do.
|
|
445
|
-
app.get("/nodes/:nodeId/board", async (context) => {
|
|
446
|
-
const auth = requireCapability(context, "knowledge", "read");
|
|
447
|
-
return context.json(await deps.nodes.getBoard(asActor(auth), { nodeId: context.req.param("nodeId") }));
|
|
448
|
-
});
|
|
449
|
-
app.post("/nodes/:nodeId/board/configure", async (context) => {
|
|
450
|
-
const auth = requireCapability(context, "knowledge", "write");
|
|
451
|
-
const input = ConfigureBoardInput.parse(await context.req.json().catch(() => null));
|
|
452
|
-
if (input.nodeId !== context.req.param("nodeId")) {
|
|
453
|
-
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
454
|
-
}
|
|
455
|
-
return context.json(await deps.nodes.configureBoard(asActor(auth), input), 201);
|
|
456
|
-
});
|
|
457
|
-
app.post("/nodes/:nodeId/board/tasks", async (context) => {
|
|
458
|
-
const auth = requireCapability(context, "knowledge", "write");
|
|
459
|
-
const input = AddBoardTaskInput.parse(await context.req.json().catch(() => null));
|
|
460
|
-
if (input.nodeId !== context.req.param("nodeId")) {
|
|
461
|
-
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
462
|
-
}
|
|
463
|
-
return context.json(await deps.nodes.addBoardTask(asActor(auth), input), 201);
|
|
464
|
-
});
|
|
465
|
-
app.post("/nodes/:nodeId/board/tasks/update", async (context) => {
|
|
466
|
-
const auth = requireCapability(context, "knowledge", "write");
|
|
467
|
-
const input = UpdateBoardTaskInput.parse(await context.req.json().catch(() => null));
|
|
468
|
-
if (input.nodeId !== context.req.param("nodeId")) {
|
|
469
|
-
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
470
|
-
}
|
|
471
|
-
return context.json(await deps.nodes.updateBoardTask(asActor(auth), input), 201);
|
|
472
|
-
});
|
|
473
|
-
app.post("/nodes/:nodeId/board/tasks/move", async (context) => {
|
|
474
|
-
const auth = requireCapability(context, "knowledge", "write");
|
|
475
|
-
const input = MoveBoardTaskInput.parse(await context.req.json().catch(() => null));
|
|
476
|
-
if (input.nodeId !== context.req.param("nodeId")) {
|
|
477
|
-
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
478
|
-
}
|
|
479
|
-
return context.json(await deps.nodes.moveBoardTask(asActor(auth), input), 201);
|
|
480
|
-
});
|
|
481
|
-
app.post("/nodes/:nodeId/board/tasks/delete", async (context) => {
|
|
482
|
-
const auth = requireCapability(context, "knowledge", "write");
|
|
483
|
-
const input = DeleteBoardTaskInput.parse(await context.req.json().catch(() => null));
|
|
484
|
-
if (input.nodeId !== context.req.param("nodeId")) {
|
|
485
|
-
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
486
|
-
}
|
|
487
|
-
return context.json(await deps.nodes.deleteBoardTask(asActor(auth), input), 201);
|
|
488
|
-
});
|
|
489
284
|
// ⚠️ There is deliberately no route for the sixth board operation. `board_repair_task_ids`
|
|
490
285
|
// (anchrd/intel#341) is MCP-only until a screen calls it: the routes here are what the browser
|
|
491
286
|
// reaches, nothing in `intel-data-provider` asks for it, and a route with no caller is the thing
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import type { GateClient } from "@anchrd/gate-sdk";
|
|
2
|
-
import type { AgentCostsService } from "../agent-costs/agent-costs.types.js";
|
|
3
|
-
import type { AgentRuntimeService } from "../agent-runtime/agent-runtime.types.js";
|
|
4
2
|
import type { BrowserAuth } from "../auth/auth.types.js";
|
|
5
3
|
import type { BundleService } from "../bundle/bundle.types.js";
|
|
6
4
|
import type { FlowService } from "../flows/flows.types.js";
|
|
7
|
-
import type { ModelCatalogService } from "../model-catalog/model-catalog.types.js";
|
|
8
5
|
import type { NodeService } from "../nodes/nodes.types.js";
|
|
9
6
|
import type { ToolService } from "../tools/tools.types.js";
|
|
10
7
|
export interface HttpDeps {
|
|
@@ -13,11 +10,6 @@ export interface HttpDeps {
|
|
|
13
10
|
flows: FlowService;
|
|
14
11
|
tools: ToolService;
|
|
15
12
|
bundle: BundleService;
|
|
16
|
-
agents: AgentRuntimeService;
|
|
17
|
-
/** What an agent has cost, read out of the AI Gateway log rather than computed here (#251). */
|
|
18
|
-
agentCosts: AgentCostsService;
|
|
19
|
-
/** What the models on offer cost, read from Cloudflare rather than typed out here (#257). */
|
|
20
|
-
models: ModelCatalogService;
|
|
21
13
|
resource: string;
|
|
22
14
|
resourceMetadataUrl: string;
|
|
23
15
|
auth?: Pick<BrowserAuth, "resolve">;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { upgradeStoredBoard } from "../nodes/board/board.js";
|
|
1
|
+
import { BlockNoteDocument, BlockNoteMediaType } from "@anchrd/intel-contract";
|
|
3
2
|
export class PermanentIndexingError extends Error {
|
|
4
3
|
}
|
|
5
|
-
// The key under which a node
|
|
6
|
-
// the bare node id in the index itself, which is why nothing written before anchrd/intel#301 has
|
|
7
|
-
// be renamed or embedded again.
|
|
4
|
+
// The key under which a node with exactly one vector is filed — since #390 that is every kind. It
|
|
5
|
+
// is the bare node id in the index itself, which is why nothing written before anchrd/intel#301 has
|
|
6
|
+
// to be renamed or embedded again.
|
|
8
7
|
const wholeNodeChunkKey = "";
|
|
9
8
|
// What the pass hands to `purgeVectors` at its end: the names it has just written, in case the node
|
|
10
9
|
// was archived while it worked and the record of them was refused (anchrd/intel#348).
|
|
@@ -21,58 +20,6 @@ function indexText(mediaType, content) {
|
|
|
21
20
|
throw new PermanentIndexingError(`Node version content is not a valid BlockNote document: ${error instanceof Error ? error.message : "unknown parse failure"}`);
|
|
22
21
|
}
|
|
23
22
|
}
|
|
24
|
-
/**
|
|
25
|
-
* One passage per task, never one per board (#285) — and since anchrd/intel#301 one VECTOR per task
|
|
26
|
-
* too, which is what `key` carries.
|
|
27
|
-
*
|
|
28
|
-
* ⚠️ This is the whole reason a board is chunked at all. "Where do I stand with X" has to land on a
|
|
29
|
-
* CARD: indexed as one blob, a board of three hundred tasks matches on any of them and answers with
|
|
30
|
-
* whichever thousand characters the snippet function happened to cut — text from a task that may
|
|
31
|
-
* have nothing to do with the question. Each chunk carries the task's own title, which is the
|
|
32
|
-
* column the FTS table weights highest.
|
|
33
|
-
*
|
|
34
|
-
* ⚠️ The status list is not indexed, and neither is a task's own status. "Backlog" and "Done" appear
|
|
35
|
-
* on every board in the installation, so they are the words most likely to match and the least
|
|
36
|
-
* likely to mean anything — and in the semantic half they would drag every card of every board
|
|
37
|
-
* towards every question phrased as a state.
|
|
38
|
-
*
|
|
39
|
-
* ⚠️ A task on the `archived` shelf is left out of BOTH halves (anchrd/intel#301). That shelf is
|
|
40
|
-
* what a board has instead of deleting a card (`ArchivedBoardStatusId`), so it is the same statement
|
|
41
|
-
* `archived_at` makes about a node one level up — and an archived node has never been searchable.
|
|
42
|
-
* Answering "where do I stand with X" out of a card somebody swept away is the failure the ticket
|
|
43
|
-
* names. A card in a `terminal` status is NOT swept: "we finished it" is an answer to where the work
|
|
44
|
-
* stands, and the only thing that says a card no longer counts is the shelf.
|
|
45
|
-
*/
|
|
46
|
-
function boardChunks(content) {
|
|
47
|
-
let parsed;
|
|
48
|
-
try {
|
|
49
|
-
parsed = JSON.parse(content);
|
|
50
|
-
}
|
|
51
|
-
catch (error) {
|
|
52
|
-
throw new PermanentIndexingError(`Node version content is not valid JSON: ${error instanceof Error ? error.message : "unknown parse failure"}`);
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* ⚠️ Upgraded first, exactly like `parseStoredBoard` — this is the SECOND place a stored board
|
|
56
|
-
* body is parsed, and it reads bodies nobody has rewritten since (anchrd/intel#318).
|
|
57
|
-
*
|
|
58
|
-
* `reindex` walks every current version in the installation, so an old board arrives here as it
|
|
59
|
-
* was written: without `terminal` (#311) or with a repeated `dependsOn`. Without the upgrade the
|
|
60
|
-
* refusal below is PERMANENT — the version is marked failed and never retried — so that board
|
|
61
|
-
* would simply stop being findable, quietly, while its screen still worked. A rule added at one
|
|
62
|
-
* parse site and not the other is how a fix looks complete and is not.
|
|
63
|
-
*/
|
|
64
|
-
const board = BoardDocument.safeParse(upgradeStoredBoard(parsed));
|
|
65
|
-
if (!board.success) {
|
|
66
|
-
throw new PermanentIndexingError("Node version content is not a valid board document");
|
|
67
|
-
}
|
|
68
|
-
return board.data.tasks
|
|
69
|
-
.filter((task) => task.status !== ArchivedBoardStatusId)
|
|
70
|
-
.map((task) => ({
|
|
71
|
-
key: task.id,
|
|
72
|
-
title: task.title,
|
|
73
|
-
text: [task.title, ...task.labels, task.description].filter(Boolean).join("\n\n"),
|
|
74
|
-
}));
|
|
75
|
-
}
|
|
76
23
|
/**
|
|
77
24
|
* What one chunk is embedded as (anchrd/intel#301).
|
|
78
25
|
*
|
|
@@ -85,9 +32,9 @@ function boardChunks(content) {
|
|
|
85
32
|
* compare against. That cost is one embedding per node, paid when that node is next saved or when
|
|
86
33
|
* an administrator runs `reindex`, and never per card.
|
|
87
34
|
*
|
|
88
|
-
* ⚠️
|
|
89
|
-
*
|
|
90
|
-
*
|
|
35
|
+
* ⚠️ Only the WHOLE-node chunk is prefixed, and the distinction is what kept a rename cheap while
|
|
36
|
+
* a node could hold many chunks: a per-chunk prefix would have put the node's title into every
|
|
37
|
+
* fingerprint, so renaming it once cost one embedding per chunk.
|
|
91
38
|
*/
|
|
92
39
|
function embeddedText(target, chunk) {
|
|
93
40
|
return chunk.key === wholeNodeChunkKey ? `${target.title}\n\n${chunk.text}` : chunk.text;
|
|
@@ -95,14 +42,13 @@ function embeddedText(target, chunk) {
|
|
|
95
42
|
/**
|
|
96
43
|
* The vectors this node needs, minus the ones it already has (anchrd/intel#301).
|
|
97
44
|
*
|
|
98
|
-
* ⚠️ This is the answer to "
|
|
99
|
-
* document
|
|
100
|
-
*
|
|
101
|
-
* was embedded is what tells an untouched card from a changed one.
|
|
45
|
+
* ⚠️ This is the answer to "an unchanged chunk must not be embedded again". A save writes the
|
|
46
|
+
* whole document, so without this every save would re-embed everything it holds. The fingerprint of
|
|
47
|
+
* the exact text that was embedded is what tells an untouched chunk from a changed one.
|
|
102
48
|
*
|
|
103
49
|
* ⚠️ The comparison is against D1 and deliberately not against Vectorize. Vectorize writes are
|
|
104
50
|
* asynchronous — a vector upserted a moment ago is not readable yet — so a check made there would
|
|
105
|
-
* answer about the save before last and re-embed a
|
|
51
|
+
* answer about the save before last and re-embed a node that had just been saved twice.
|
|
106
52
|
*
|
|
107
53
|
* ⚠️ The record is written only after the upsert returned, in the caller. A pass that dies in
|
|
108
54
|
* between leaves the record short, so the next one embeds again; the opposite order would leave a
|
|
@@ -127,9 +73,9 @@ async function replaceVectors(deps, target, chunks) {
|
|
|
127
73
|
const stale = stored.map((record) => record.chunkKey).filter((key) => !wanted.has(key));
|
|
128
74
|
// ⚠️ The one-time sweep of the vector this node had BEFORE it was chunked. An empty record with a
|
|
129
75
|
// chunked node means this is the first pass since anchrd/intel#301 (or since `reindex` emptied
|
|
130
|
-
// the record), and the whole-
|
|
76
|
+
// the record), and the whole-node vector written under the bare node id is still sitting there —
|
|
131
77
|
// matching questions and answering with the wrong card's passage. Once a record exists this
|
|
132
|
-
// branch is never taken again, so it costs one call per
|
|
78
|
+
// branch is never taken again, so it costs one call per node rather than one per save.
|
|
133
79
|
if (stored.length === 0 && !wanted.has(wholeNodeChunkKey))
|
|
134
80
|
stale.push(wholeNodeChunkKey);
|
|
135
81
|
await semantic.upsert(target, changed);
|
|
@@ -207,22 +153,6 @@ export function createIndexing(deps) {
|
|
|
207
153
|
await purgeVectors(deps, versionId);
|
|
208
154
|
return;
|
|
209
155
|
}
|
|
210
|
-
// ⚠️ An agent's body is a definition, not something to read: node IDs, a cron line and a model
|
|
211
|
-
// name (ADR-0005 §4). Indexing it verbatim would fill the index with identifiers and, worse,
|
|
212
|
-
// put them into the passage a searcher is shown — the definition names nodes the searcher may
|
|
213
|
-
// have no access to, and a snippet is the one place their IDs would become visible without
|
|
214
|
-
// the tree being asked. What makes an agent findable is what a person wrote about it, so the
|
|
215
|
-
// text is its description, and the FTS table indexes the title on its own (#139).
|
|
216
|
-
if (target.kind === "agent") {
|
|
217
|
-
const chunks = [
|
|
218
|
-
{ key: wholeNodeChunkKey, title: target.title, text: target.description ?? "" },
|
|
219
|
-
];
|
|
220
|
-
await deps.repository.replace(target, chunks);
|
|
221
|
-
await replaceVectors(deps, target, chunks);
|
|
222
|
-
await purgeVectors(deps, versionId, writtenKeys(chunks));
|
|
223
|
-
await deps.repository.markIndexed(versionId, deps.now().toISOString());
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
156
|
// An attachment is one object of bytes; text kinds may be several, because a table's content
|
|
227
157
|
// is the join of its versions (#40). A missing segment fails the whole pass rather than
|
|
228
158
|
// indexing a table with a hole in it.
|
|
@@ -240,12 +170,11 @@ export function createIndexing(deps) {
|
|
|
240
170
|
if (text === undefined) {
|
|
241
171
|
throw new PermanentIndexingError(`No document converter is configured for ${target.mediaType}`);
|
|
242
172
|
}
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
: [{ key: wholeNodeChunkKey, title: target.title, text }];
|
|
173
|
+
// ⚠️ One chunk per node since #390, and the LIST is what stays. Both indexes read it, so
|
|
174
|
+
// they cannot come to disagree about what a node is made of — which is the property that
|
|
175
|
+
// let a board be one passage and one vector per card (#285, anchrd/intel#301) without
|
|
176
|
+
// either half being told about it separately.
|
|
177
|
+
const chunks = [{ key: wholeNodeChunkKey, title: target.title, text }];
|
|
249
178
|
await deps.repository.replace(target, chunks);
|
|
250
179
|
await replaceVectors(deps, target, chunks);
|
|
251
180
|
// ⚠️ Asked again, at the END of a pass that started on a live node (anchrd/intel#348). The
|
package/dist/intel/intel.js
CHANGED
|
@@ -72,9 +72,6 @@ export function createIntel(deps) {
|
|
|
72
72
|
flows: deps.flows,
|
|
73
73
|
tools: deps.tools,
|
|
74
74
|
bundle: deps.bundle,
|
|
75
|
-
agents: deps.agents,
|
|
76
|
-
agentCosts: deps.agentCosts,
|
|
77
|
-
models: deps.models,
|
|
78
75
|
resource,
|
|
79
76
|
resourceMetadataUrl,
|
|
80
77
|
...(deps.auth ? { auth: deps.auth } : {}),
|
|
@@ -87,13 +84,11 @@ export function createIntel(deps) {
|
|
|
87
84
|
}
|
|
88
85
|
return await handleMcp(context.req.raw, {
|
|
89
86
|
authorization,
|
|
90
|
-
// ⚠️ The very token this call was authorized with, so
|
|
91
|
-
// caller and not as Intel. There is no cookie path on `/mcp`, so `bearer` is the
|
|
92
|
-
// `authorize` just used — read again rather than passed out of it, because a
|
|
93
|
-
// returned a credential beside its answer would be one somebody logs.
|
|
87
|
+
// ⚠️ The very token this call was authorized with, so a tool that has to speak to Gate does
|
|
88
|
+
// it as the caller and not as Intel. There is no cookie path on `/mcp`, so `bearer` is the
|
|
89
|
+
// same value `authorize` just used — read again rather than passed out of it, because a
|
|
90
|
+
// function that returned a credential beside its answer would be one somebody logs.
|
|
94
91
|
bearer: bearer(context.req.raw.headers) ?? "",
|
|
95
|
-
agents: deps.agents,
|
|
96
|
-
agentCosts: deps.agentCosts,
|
|
97
92
|
flows: deps.flows,
|
|
98
93
|
nodes: deps.nodes,
|
|
99
94
|
tools: deps.tools,
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import type { GateClient } from "@anchrd/gate-sdk";
|
|
2
|
-
import type { AgentCostsService } from "../agent-costs/agent-costs.types.js";
|
|
3
|
-
import type { AgentRuntimeService } from "../agent-runtime/agent-runtime.types.js";
|
|
4
2
|
import type { BrowserAuth } from "../auth/auth.types.js";
|
|
5
3
|
import type { BundleService } from "../bundle/bundle.types.js";
|
|
6
4
|
import type { FlowService } from "../flows/flows.types.js";
|
|
7
|
-
import type { ModelCatalogService } from "../model-catalog/model-catalog.types.js";
|
|
8
5
|
import type { NodeService } from "../nodes/nodes.types.js";
|
|
9
6
|
import type { ToolService } from "../tools/tools.types.js";
|
|
10
7
|
export interface IntelDeps {
|
|
@@ -15,8 +12,5 @@ export interface IntelDeps {
|
|
|
15
12
|
flows: FlowService;
|
|
16
13
|
tools: ToolService;
|
|
17
14
|
bundle: BundleService;
|
|
18
|
-
agents: AgentRuntimeService;
|
|
19
|
-
agentCosts: AgentCostsService;
|
|
20
|
-
models: ModelCatalogService;
|
|
21
15
|
auth?: BrowserAuth;
|
|
22
16
|
}
|