@anchrd/intel-api 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/cloudflare/cloudflare.js +0 -68
- package/dist/adapters/cloudflare/cloudflare.types.d.ts +0 -39
- package/dist/adapters/db/db-flows.js +1 -1
- package/dist/adapters/db/db-grants.js +1 -1
- package/dist/adapters/db/db.js +16 -113
- package/dist/bundle/bundle.js +28 -141
- package/dist/cli/cli.js +3 -9
- package/dist/http/http.js +1 -206
- package/dist/http/http.types.d.ts +0 -8
- package/dist/indexing/indexing.js +18 -89
- package/dist/intel/intel.js +4 -9
- package/dist/intel/intel.types.d.ts +0 -6
- package/dist/mcp/mcp.js +3 -292
- package/dist/mcp/mcp.types.d.ts +2 -7
- package/dist/nodes/document-links/document-links.d.ts +6 -8
- package/dist/nodes/document-links/document-links.js +8 -31
- package/dist/nodes/nodes.js +20 -886
- package/dist/nodes/nodes.types.d.ts +10 -169
- package/dist/tools/tools.js +37 -148
- package/dist/tools/tools.types.d.ts +0 -21
- package/migrations/0018_no_context_policy_at_last.sql +13 -6
- package/migrations/0019_one_name_for_the_grants.sql +52 -0
- package/package.json +2 -2
- package/dist/adapters/cloudflare-api/cloudflare-api.d.ts +0 -22
- package/dist/adapters/cloudflare-api/cloudflare-api.js +0 -306
- package/dist/adapters/cloudflare-api/cloudflare-api.types.d.ts +0 -64
- package/dist/adapters/cloudflare-api/cloudflare-api.types.js +0 -1
- package/dist/adapters/gate-applications/gate-applications.d.ts +0 -23
- package/dist/adapters/gate-applications/gate-applications.js +0 -88
- package/dist/adapters/tool-delegation/tool-delegation.d.ts +0 -22
- package/dist/adapters/tool-delegation/tool-delegation.js +0 -90
- package/dist/agent-costs/agent-costs.d.ts +0 -16
- package/dist/agent-costs/agent-costs.js +0 -105
- package/dist/agent-costs/agent-costs.types.d.ts +0 -30
- package/dist/agent-costs/agent-costs.types.js +0 -1
- package/dist/agent-runtime/agent-runtime.d.ts +0 -16
- package/dist/agent-runtime/agent-runtime.js +0 -150
- package/dist/agent-runtime/agent-runtime.types.d.ts +0 -122
- package/dist/agent-runtime/agent-runtime.types.js +0 -1
- package/dist/model-catalog/model-catalog.d.ts +0 -2
- package/dist/model-catalog/model-catalog.js +0 -99
- package/dist/model-catalog/model-catalog.types.d.ts +0 -15
- package/dist/model-catalog/model-catalog.types.js +0 -1
- package/dist/nodes/board/board.d.ts +0 -61
- package/dist/nodes/board/board.js +0 -826
- package/dist/nodes/board/board.types.d.ts +0 -38
- package/dist/nodes/board/board.types.js +0 -1
- package/migrations/0013_agents_in_the_tree.sql +0 -76
- package/migrations/0014_agent_applications.sql +0 -25
- package/migrations/0015_tools_delegated_from_a_connection.sql +0 -15
- package/migrations/0016_boards_in_the_tree.sql +0 -80
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
import { createGateClient } from "@anchrd/gate-sdk";
|
|
2
2
|
import { ulid } from "ulid";
|
|
3
|
-
import { createAgentCosts } from "../../agent-costs/agent-costs.js";
|
|
4
|
-
import { createAgentRuntimeService } from "../../agent-runtime/agent-runtime.js";
|
|
5
3
|
import { createBrowserAuth } from "../../auth/auth.js";
|
|
6
4
|
import { createBundle } from "../../bundle/bundle.js";
|
|
7
5
|
import { createFlows } from "../../flows/flows.js";
|
|
8
6
|
import { createIndexing, PermanentIndexingError } from "../../indexing/indexing.js";
|
|
9
7
|
import { createIntel } from "../../intel/intel.js";
|
|
10
|
-
import { createModelCatalog } from "../../model-catalog/model-catalog.js";
|
|
11
8
|
import { createNodes } from "../../nodes/nodes.js";
|
|
12
9
|
import { IntelError } from "../../shared/intel-error/intel-error.js";
|
|
13
10
|
import { sha256Hex } from "../../shared/sha256/sha256.js";
|
|
14
11
|
import { createTools } from "../../tools/tools.js";
|
|
15
|
-
import { createCloudflareApi } from "../cloudflare-api/cloudflare-api.js";
|
|
16
12
|
import { createContentStore } from "../content/content.js";
|
|
17
13
|
import { createNodeRepository } from "../db/db.js";
|
|
18
14
|
import { createFlowRepository } from "../db/db-flows.js";
|
|
@@ -20,7 +16,6 @@ import { createNodeIndexRepository } from "../db/db-indexing.js";
|
|
|
20
16
|
import { createOAuthClientStore } from "../db/db-oauth.js";
|
|
21
17
|
import { createDocumentConverter } from "../document-converter/document-converter.js";
|
|
22
18
|
import { createFlowRuntime } from "../flow-runtime/flow-runtime.js";
|
|
23
|
-
import { createGateApplications } from "../gate-applications/gate-applications.js";
|
|
24
19
|
import { createIndexQueue, IndexMessage } from "../index-queue/index-queue.js";
|
|
25
20
|
import { createJsonSchemaValidator } from "../json-schema/json-schema.js";
|
|
26
21
|
import { createOpenId } from "../openid/openid.js";
|
|
@@ -28,7 +23,6 @@ import { createPortalTokenStore } from "../portal-tokens/portal-tokens.js";
|
|
|
28
23
|
import { createRemoteTools } from "../remote-tools/remote-tools.js";
|
|
29
24
|
import { createSemanticIndex } from "../semantic-index/semantic-index.js";
|
|
30
25
|
import { createSessionCodec } from "../session-cookie/session-cookie.js";
|
|
31
|
-
import { createToolDelegation } from "../tool-delegation/tool-delegation.js";
|
|
32
26
|
import { createToolSourcePolicy } from "../tool-source-policy/tool-source-policy.js";
|
|
33
27
|
// biome-ignore lint/performance/noBarrelFile: The package entry must expose Wrangler's named Workflow class.
|
|
34
28
|
export { IntelFlowWorkflow } from "./cloudflare-flow-workflow.js";
|
|
@@ -82,22 +76,10 @@ export default {
|
|
|
82
76
|
crypto,
|
|
83
77
|
now,
|
|
84
78
|
});
|
|
85
|
-
// Built before the tree since D30: saving an agent has to ask the portal, live, whether the
|
|
86
|
-
// person saving reaches every server they are delegating. The dependency runs one way — the
|
|
87
|
-
// tool service knows nothing about the tree, and the delegation lookup below reads D1 directly
|
|
88
|
-
// rather than through the node service.
|
|
89
|
-
const toolDelegation = createToolDelegation({
|
|
90
|
-
db: env.DB,
|
|
91
|
-
content: contentStore,
|
|
92
|
-
id: ulid,
|
|
93
|
-
now,
|
|
94
|
-
});
|
|
95
79
|
const tools = createTools({
|
|
96
80
|
portalUrl,
|
|
97
81
|
remote: createRemoteTools({ fetch: globalThis.fetch.bind(globalThis) }),
|
|
98
82
|
tokens: portalTokens,
|
|
99
|
-
delegation: toolDelegation.resolve,
|
|
100
|
-
audit: toolDelegation.audit,
|
|
101
83
|
refresh: async (token) => {
|
|
102
84
|
try {
|
|
103
85
|
const refreshed = await oauth.refresh({
|
|
@@ -126,56 +108,9 @@ export default {
|
|
|
126
108
|
return await sha256Hex(crypto, JSON.stringify(value));
|
|
127
109
|
},
|
|
128
110
|
});
|
|
129
|
-
// ⚠️ Built before `nodes` and not after, because creating an agent has to be able to hand the
|
|
130
|
-
// runtime its key inside the same call (D29). Its `visibleNode` closes over `nodes` and is only
|
|
131
|
-
// ever called later, which is what keeps the two from being a cycle.
|
|
132
|
-
const agents = createAgentRuntimeService({
|
|
133
|
-
...(env.AGENT ? { runtime: env.AGENT } : {}),
|
|
134
|
-
...(env.AGENT_HANDOVER_SECRET ? { handoverSecret: env.AGENT_HANDOVER_SECRET } : {}),
|
|
135
|
-
// The tree's own visibility lookup, unchanged: whether somebody may reach an agent is the
|
|
136
|
-
// same question as whether they may see the node, and no second answer is invented here.
|
|
137
|
-
visibleNode: async (actor, nodeId) => await nodes.visibleNode(actor, nodeId),
|
|
138
|
-
});
|
|
139
|
-
// ⚠️ One Cloudflare read client for both of the account's answers — the gateway's cost log
|
|
140
|
-
// (#251) and the Workers AI price list (#257) — because it is one token and one account. It is
|
|
141
|
-
// absent as a whole where any of the three variables is missing: a half-configured client would
|
|
142
|
-
// fail per call with a 401 that reads like a permission problem instead of saying, once, that
|
|
143
|
-
// this deployment was never given a token.
|
|
144
|
-
const gatewayAccountId = env.AI_GATEWAY_ACCOUNT_ID?.trim();
|
|
145
|
-
const gatewayId = env.AI_GATEWAY_ID?.trim();
|
|
146
|
-
const gatewayReadToken = env.AI_GATEWAY_READ_TOKEN?.trim();
|
|
147
|
-
const cloudflareApi = gatewayAccountId && gatewayId && gatewayReadToken
|
|
148
|
-
? createCloudflareApi({
|
|
149
|
-
accountId: gatewayAccountId,
|
|
150
|
-
gatewayId,
|
|
151
|
-
token: gatewayReadToken,
|
|
152
|
-
fetch: globalThis.fetch.bind(globalThis),
|
|
153
|
-
})
|
|
154
|
-
: undefined;
|
|
155
|
-
const agentCosts = createAgentCosts({
|
|
156
|
-
...(cloudflareApi ? { gateway: cloudflareApi } : {}),
|
|
157
|
-
visibleNode: async (actor, nodeId) => await nodes.visibleNode(actor, nodeId),
|
|
158
|
-
now,
|
|
159
|
-
});
|
|
160
|
-
const models = createModelCatalog({ ...(cloudflareApi ? { cloudflare: cloudflareApi } : {}) });
|
|
161
111
|
const nodes = createNodes({
|
|
162
112
|
repository: nodeRepository,
|
|
163
113
|
content: contentStore,
|
|
164
|
-
// ⚠️ Built from `GATE_URL` alone — no service key is handed to it, and none would help. The
|
|
165
|
-
// Applications surface is admin-gated on a real principal, so every call carries the bearer
|
|
166
|
-
// of the person making it, which the tree passes in per operation (#182).
|
|
167
|
-
applications: createGateApplications({
|
|
168
|
-
fetch: globalThis.fetch.bind(globalThis),
|
|
169
|
-
gateUrl: env.GATE_URL,
|
|
170
|
-
}),
|
|
171
|
-
// The binding's presence IS the signal (#190) — no separate configuration option exists.
|
|
172
|
-
agentRuntimeAvailable: () => env.AGENT !== undefined,
|
|
173
|
-
// Where the Application key goes, and the only place it goes (D29). The same service the
|
|
174
|
-
// browser paths are proxied through, so there is one door to the runtime and not two.
|
|
175
|
-
agentKeys: { store: async (input) => await agents.storeKey(input) },
|
|
176
|
-
// The same door again (#214): a definition that was written is a schedule that has to be
|
|
177
|
-
// armed, and the runtime only looks when it is told to.
|
|
178
|
-
agentSchedules: { sync: async (input) => await agents.syncSchedules(input) },
|
|
179
114
|
id: ulid,
|
|
180
115
|
now,
|
|
181
116
|
indexing: createIndexQueue(env.INDEXING),
|
|
@@ -247,9 +182,6 @@ export default {
|
|
|
247
182
|
},
|
|
248
183
|
indexing: createIndexQueue(env.INDEXING),
|
|
249
184
|
}),
|
|
250
|
-
agents,
|
|
251
|
-
agentCosts,
|
|
252
|
-
models,
|
|
253
185
|
auth,
|
|
254
186
|
}).fetch(request);
|
|
255
187
|
},
|
|
@@ -9,17 +9,6 @@ export interface CloudflareEnv {
|
|
|
9
9
|
ASSETS?: {
|
|
10
10
|
fetch(request: Request): Promise<Response>;
|
|
11
11
|
};
|
|
12
|
-
/**
|
|
13
|
-
* The agent runtime Worker, bound as a service.
|
|
14
|
-
*
|
|
15
|
-
* ⚠️ Optional, and it has to be: a deployment may run Intel without agents, and a hard requirement
|
|
16
|
-
* here would make the whole installation fail to start over a feature nobody uses. Where it is
|
|
17
|
-
* absent the agent routes answer 503 by name; where it is present the browser reaches the runtime
|
|
18
|
-
* through Intel and never across a second origin (#178).
|
|
19
|
-
*/
|
|
20
|
-
AGENT?: {
|
|
21
|
-
fetch(request: Request): Promise<Response>;
|
|
22
|
-
};
|
|
23
12
|
DB: D1Database;
|
|
24
13
|
CONTENT: R2Bucket;
|
|
25
14
|
INDEXING: QueueProducer<IndexMessage>;
|
|
@@ -30,34 +19,6 @@ export interface CloudflareEnv {
|
|
|
30
19
|
GATE_SERVICE_KEY: string;
|
|
31
20
|
INTEL_URL: string;
|
|
32
21
|
INTEL_SESSION_SECRET: string;
|
|
33
|
-
/**
|
|
34
|
-
* The shared secret that proves a key handover reached the agent runtime through this Worker
|
|
35
|
-
* (D29, #207). The SAME value the agent Worker holds as `AGENT_HANDOVER_SECRET`.
|
|
36
|
-
*
|
|
37
|
-
* ⚠️ Optional exactly like `AGENT`: a deployment may run Intel without agents, and a hard
|
|
38
|
-
* requirement would make the whole installation fail to start over a feature nobody uses. Where
|
|
39
|
-
* it is absent, creating an agent refuses by name instead of leaving one without a key.
|
|
40
|
-
*/
|
|
41
|
-
AGENT_HANDOVER_SECRET?: string;
|
|
42
|
-
/**
|
|
43
|
-
* Where the AI Gateway lives and how to READ it (#251, #257).
|
|
44
|
-
*
|
|
45
|
-
* ⚠️ `AI_GATEWAY_READ_TOKEN` is not the agent runtime's `AI_GATEWAY_TOKEN` and must never be the
|
|
46
|
-
* same value. That one carries `AI Gateway: Run` and buys inference; this one carries
|
|
47
|
-
* `AI Gateway: Read` — plus `Workers AI: Read` for the model catalog — and buys nothing at all.
|
|
48
|
-
* One credential doing both would put a spending permission into the Worker that only reads.
|
|
49
|
-
*
|
|
50
|
-
* ⚠️ Cloudflare cannot scope `AI Gateway: Read` to a single gateway; it is account-wide, the same
|
|
51
|
-
* limitation `AI Gateway: Run` already had in #239. It is worth knowing when the token is created
|
|
52
|
-
* and is not a reason against the route: it reads and writes nothing.
|
|
53
|
-
*
|
|
54
|
-
* ⚠️ All three optional, together. A deployment that configures none of them gets a cost view that
|
|
55
|
-
* says "not configured" and a model catalog off the built-in table — both named states, neither an
|
|
56
|
-
* error. Intel without a Cloudflare token is a supported installation.
|
|
57
|
-
*/
|
|
58
|
-
AI_GATEWAY_ACCOUNT_ID?: string;
|
|
59
|
-
AI_GATEWAY_ID?: string;
|
|
60
|
-
AI_GATEWAY_READ_TOKEN?: string;
|
|
61
22
|
TOOL_SOURCE_ORIGINS: string;
|
|
62
23
|
MCP_PORTAL_URL?: string;
|
|
63
24
|
ALLOW_INSECURE_OAUTH?: string;
|
|
@@ -219,7 +219,7 @@ export function createFlowRepository(deps) {
|
|
|
219
219
|
SELECT
|
|
220
220
|
EXISTS (SELECT 1 FROM ancestors WHERE id = ?) AS in_subtree,
|
|
221
221
|
EXISTS (
|
|
222
|
-
SELECT 1 FROM
|
|
222
|
+
SELECT 1 FROM node_grants grant_row
|
|
223
223
|
JOIN ancestors ON ancestors.id = grant_row.node_id
|
|
224
224
|
WHERE grant_row.principal_type = 'organization'
|
|
225
225
|
AND grant_row.verb = 'execute'
|
package/dist/adapters/db/db.js
CHANGED
|
@@ -5,7 +5,7 @@ const grantColumns = `id, node_id, principal_type, principal_id, verb, expires_a
|
|
|
5
5
|
created_by, created_at`;
|
|
6
6
|
const linkColumns = `link.id, link.source_node_id, link.target_node_id, link.relation,
|
|
7
7
|
link.origin, link.label, link.created_by, link.created_at`;
|
|
8
|
-
// The first row of each node, in the order they arrive. A chunked kind —
|
|
8
|
+
// The first row of each node, in the order they arrive. A chunked kind — one whose index holds
|
|
9
9
|
// one row per task (#285) — answers a query once per matching card; a citation names a node, so the
|
|
10
10
|
// best of them is the one that gets to speak for it.
|
|
11
11
|
function dedupedByNode(rows) {
|
|
@@ -146,66 +146,7 @@ export function createNodeRepository(deps) {
|
|
|
146
146
|
JOIN allowed ON allowed.id = n.id
|
|
147
147
|
WHERE n.archived_at IS NOT NULL
|
|
148
148
|
ORDER BY n.archived_at DESC, lower(n.title), n.id`;
|
|
149
|
-
/**
|
|
150
|
-
* The agents this actor may see, over the whole tree or under one folder (#139).
|
|
151
|
-
*
|
|
152
|
-
* ⚠️ The same `allowed` set every other read joins against, not a second predicate written for
|
|
153
|
-
* agents. An agent is a node, and the moment its list answers from its own rule it can answer
|
|
154
|
-
* differently from `list` about the very same row.
|
|
155
|
-
*
|
|
156
|
-
* Three scopes, matching the flow list: no clause at all for the whole tree, `IS NULL` for the
|
|
157
|
-
* root level, and one folder by ID.
|
|
158
|
-
*/
|
|
159
|
-
const visibleAgents = (clause, includeArchived) => `${visibleCte}
|
|
160
|
-
SELECT ${nodeColumns}
|
|
161
|
-
FROM nodes n
|
|
162
|
-
JOIN allowed ON allowed.id = n.id
|
|
163
|
-
WHERE n.kind = 'agent' ${includeArchived ? "" : "AND n.archived_at IS NULL"} ${clause}
|
|
164
|
-
ORDER BY lower(n.title), n.id`;
|
|
165
|
-
const agentScopeOf = (parentId) => parentId === undefined
|
|
166
|
-
? { clause: "", bindings: [] }
|
|
167
|
-
: parentId === null
|
|
168
|
-
? { clause: "AND n.parent_id IS NULL", bindings: [] }
|
|
169
|
-
: { clause: "AND n.parent_id = ?", bindings: [parentId] };
|
|
170
149
|
return {
|
|
171
|
-
async listVisibleAgents(actor, input) {
|
|
172
|
-
const scope = agentScopeOf(input.parentId);
|
|
173
|
-
const result = await deps.db
|
|
174
|
-
.prepare(visibleAgents(scope.clause, input.includeArchived === true))
|
|
175
|
-
.bind(...readBindings(actor), ...scope.bindings)
|
|
176
|
-
.all();
|
|
177
|
-
return (result.results ?? []).map(mapNode);
|
|
178
|
-
},
|
|
179
|
-
/**
|
|
180
|
-
* The R2 keys of the current definitions of every agent in this folder's subtree that this
|
|
181
|
-
* actor may see — the input to the share warning (#139, ADR-0005 §2).
|
|
182
|
-
*
|
|
183
|
-
* ⚠️ Keys, never bodies and never titles. Whether a referenced node may be NAMED to whoever is
|
|
184
|
-
* sharing is the tree's question and is asked afterwards through `getVisible`, exactly as the
|
|
185
|
-
* flow side does it. Answering it here would be the second rule that #17 and #19 got wrong.
|
|
186
|
-
*
|
|
187
|
-
* ⚠️ Agents this actor may not see contribute nothing, not even a count. Their references would
|
|
188
|
-
* otherwise be attributed to a folder this actor administers, which discloses that the agent is
|
|
189
|
-
* there at all.
|
|
190
|
-
*/
|
|
191
|
-
async listVisibleAgentDefinitionKeys(actor, folderId) {
|
|
192
|
-
const result = await deps.db
|
|
193
|
-
.prepare(`${visibleCte},
|
|
194
|
-
scope(id) AS (
|
|
195
|
-
SELECT id FROM nodes WHERE id = ?
|
|
196
|
-
UNION
|
|
197
|
-
SELECT child.id FROM nodes child JOIN scope ON child.parent_id = scope.id
|
|
198
|
-
)
|
|
199
|
-
SELECT version.content_key AS content_key
|
|
200
|
-
FROM nodes n
|
|
201
|
-
JOIN allowed ON allowed.id = n.id
|
|
202
|
-
JOIN node_versions version ON version.id = n.current_version_id
|
|
203
|
-
WHERE n.kind = 'agent' AND n.archived_at IS NULL AND n.id IN (SELECT id FROM scope)
|
|
204
|
-
ORDER BY n.id`)
|
|
205
|
-
.bind(...readBindings(actor), folderId)
|
|
206
|
-
.all();
|
|
207
|
-
return (result.results ?? []).map((row) => row.content_key);
|
|
208
|
-
},
|
|
209
150
|
async listVisible(actor, input) {
|
|
210
151
|
const result = await deps.db
|
|
211
152
|
.prepare(input.archivedOnly ? archivedEverywhere : visibleChildren(false))
|
|
@@ -377,7 +318,7 @@ export function createNodeRepository(deps) {
|
|
|
377
318
|
*
|
|
378
319
|
* ⚠️ The idempotency row points at the VERSION, and the audit event points at the node, so the
|
|
379
320
|
* two are joined through the version id the snapshot writer puts into the metadata. That
|
|
380
|
-
* metadata is where a
|
|
321
|
+
* metadata is where a snapshot says which positions it touched and how many it removed —
|
|
381
322
|
* facts a replay cannot recompute from the stored document, because the document is the state
|
|
382
323
|
* AFTER the write. Without this, repeating a delete would answer with a count nobody measured.
|
|
383
324
|
*/
|
|
@@ -439,19 +380,8 @@ export function createNodeRepository(deps) {
|
|
|
439
380
|
return null;
|
|
440
381
|
return row.resource_id.startsWith("1:");
|
|
441
382
|
},
|
|
442
|
-
// ⚠️ Its own statement rather than a column on the node read: four of the five kinds can never
|
|
443
|
-
// have an Application, and the two joins every node read already carries are not going to grow
|
|
444
|
-
// a third for a question only agents ask (#182).
|
|
445
|
-
async agentApplicationId(nodeId) {
|
|
446
|
-
const row = await deps.db
|
|
447
|
-
.prepare("SELECT application_id FROM agent_applications WHERE node_id = ?")
|
|
448
|
-
.bind(nodeId)
|
|
449
|
-
.first();
|
|
450
|
-
return row?.application_id ?? null;
|
|
451
|
-
},
|
|
452
383
|
async insertNode(input) {
|
|
453
384
|
const node = input.node;
|
|
454
|
-
const application = input.application;
|
|
455
385
|
try {
|
|
456
386
|
await deps.db.batch([
|
|
457
387
|
deps.db
|
|
@@ -469,23 +399,7 @@ export function createNodeRepository(deps) {
|
|
|
469
399
|
.prepare(`INSERT INTO audit_events (
|
|
470
400
|
id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
|
|
471
401
|
) VALUES (?, ?, 'node.create', 'node', ?, ?, ?)`)
|
|
472
|
-
.bind(input.auditId, input.actorId, node.id,
|
|
473
|
-
// ⚠️ The Application ID is deliberately NOT in this metadata, and the key is not
|
|
474
|
-
// within reach of it at all. An audit event is read back by people and by the
|
|
475
|
-
// reindexer; what belongs in it is that an agent was created, not the name of the
|
|
476
|
-
// principal behind it — the mapping row below is where that is kept, once (D27).
|
|
477
|
-
JSON.stringify({ kind: node.kind, parentId: node.parentId }), node.createdAt),
|
|
478
|
-
// Last in the batch, and in the SAME batch: D1 runs a batch as one transaction, so the
|
|
479
|
-
// mapping cannot end up without its node or the node without its mapping. An agent whose
|
|
480
|
-
// principal is recorded nowhere is one that archiving would quietly fail to switch off.
|
|
481
|
-
...(application
|
|
482
|
-
? [
|
|
483
|
-
deps.db
|
|
484
|
-
.prepare(`INSERT INTO agent_applications (node_id, application_id, created_at)
|
|
485
|
-
VALUES (?, ?, ?)`)
|
|
486
|
-
.bind(node.id, application.id, node.createdAt),
|
|
487
|
-
]
|
|
488
|
-
: []),
|
|
402
|
+
.bind(input.auditId, input.actorId, node.id, JSON.stringify({ kind: node.kind, parentId: node.parentId }), node.createdAt),
|
|
489
403
|
]);
|
|
490
404
|
}
|
|
491
405
|
catch (error) {
|
|
@@ -832,7 +746,7 @@ export function createNodeRepository(deps) {
|
|
|
832
746
|
.bind(version.id, version.nodeId, version.sequence, version.contentKey, version.mediaType, version.contentHash, version.size,
|
|
833
747
|
// ⚠️ Taken from the version rather than written in as `'snapshot'`. A table's mutation
|
|
834
748
|
// marks a snapshot, because reading a table starts at the newest one and everything
|
|
835
|
-
// before it is history (#135). A
|
|
749
|
+
// before it is history (#135). A snapshot version carries the whole document by
|
|
836
750
|
// construction, so it marks nothing — the same `null` a document's version carries, and
|
|
837
751
|
// for the same reason (#285).
|
|
838
752
|
version.segment, version.createdBy, version.createdAt, version.nodeId, input.baseVersionId),
|
|
@@ -868,7 +782,7 @@ export function createNodeRepository(deps) {
|
|
|
868
782
|
},
|
|
869
783
|
async listGrants(resourceId) {
|
|
870
784
|
const result = await deps.db
|
|
871
|
-
.prepare(`SELECT ${grantColumns} FROM
|
|
785
|
+
.prepare(`SELECT ${grantColumns} FROM node_grants
|
|
872
786
|
WHERE node_id = ?
|
|
873
787
|
ORDER BY principal_type, principal_id, verb`)
|
|
874
788
|
.bind(resourceId)
|
|
@@ -887,7 +801,7 @@ export function createNodeRepository(deps) {
|
|
|
887
801
|
JOIN ancestors child ON child.parent_id = parent.id
|
|
888
802
|
)
|
|
889
803
|
SELECT 1 AS reaches
|
|
890
|
-
FROM
|
|
804
|
+
FROM node_grants grant_row
|
|
891
805
|
JOIN ancestors ON ancestors.id = grant_row.node_id
|
|
892
806
|
WHERE grant_row.id <> ?
|
|
893
807
|
AND grant_row.principal_type = 'organization'
|
|
@@ -966,17 +880,6 @@ export function createNodeRepository(deps) {
|
|
|
966
880
|
.bind(input.auditId, input.actorId, input.sourceNodeId, JSON.stringify({ targetNodeIds: input.links.map((link) => link.targetNodeId) }), input.occurredAt),
|
|
967
881
|
]);
|
|
968
882
|
},
|
|
969
|
-
async recordAgentKeyRotation(input) {
|
|
970
|
-
// ⚠️ No node row moves, so there is nothing to batch this with — the event IS the write. The
|
|
971
|
-
// key is not among the columns and must not be added to them: audit is metadata, and a row
|
|
972
|
-
// that carried the credential would outlive every other copy of it.
|
|
973
|
-
await deps.db
|
|
974
|
-
.prepare(`INSERT INTO audit_events (
|
|
975
|
-
id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
|
|
976
|
-
) VALUES (?, ?, 'agent.rotate_key', 'node', ?, ?, ?)`)
|
|
977
|
-
.bind(input.auditId, input.actorId, input.nodeId, JSON.stringify({ applicationId: input.applicationId }), input.occurredAt)
|
|
978
|
-
.run();
|
|
979
|
-
},
|
|
980
883
|
async graphVisible(actor, input) {
|
|
981
884
|
const nodeResult = await deps.db
|
|
982
885
|
.prepare(`${visibleCte}
|
|
@@ -1021,7 +924,7 @@ export function createNodeRepository(deps) {
|
|
|
1021
924
|
deps.db
|
|
1022
925
|
// The verb is part of the key, so re-granting the same verb only refreshes its expiry
|
|
1023
926
|
// and never turns one verb into another.
|
|
1024
|
-
.prepare(`INSERT INTO
|
|
927
|
+
.prepare(`INSERT INTO node_grants (
|
|
1025
928
|
id, node_id, principal_type, principal_id, verb,
|
|
1026
929
|
expires_at, created_by, created_at
|
|
1027
930
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
@@ -1031,7 +934,7 @@ export function createNodeRepository(deps) {
|
|
|
1031
934
|
deps.db
|
|
1032
935
|
.prepare(`INSERT INTO idempotency_keys (
|
|
1033
936
|
actor_id, operation, idempotency_key, resource_id, created_at
|
|
1034
|
-
) SELECT ?, 'node.share', ?, id, ? FROM
|
|
937
|
+
) SELECT ?, 'node.share', ?, id, ? FROM node_grants
|
|
1035
938
|
WHERE node_id = ? AND principal_type = ? AND principal_id = ? AND verb = ?`)
|
|
1036
939
|
.bind(input.actorId, input.idempotencyKey, grant.createdAt, grant.resourceId, principalType, principalId, grant.verb),
|
|
1037
940
|
deps.db
|
|
@@ -1041,7 +944,7 @@ export function createNodeRepository(deps) {
|
|
|
1041
944
|
.bind(input.auditId, input.actorId, grant.resourceId, JSON.stringify({ principalType, verb: grant.verb }), grant.createdAt),
|
|
1042
945
|
]);
|
|
1043
946
|
const stored = await deps.db
|
|
1044
|
-
.prepare(`SELECT ${grantColumns} FROM
|
|
947
|
+
.prepare(`SELECT ${grantColumns} FROM node_grants
|
|
1045
948
|
WHERE node_id = ? AND principal_type = ? AND principal_id = ? AND verb = ?`)
|
|
1046
949
|
.bind(grant.resourceId, principalType, principalId, grant.verb)
|
|
1047
950
|
.first();
|
|
@@ -1057,7 +960,7 @@ export function createNodeRepository(deps) {
|
|
|
1057
960
|
.first();
|
|
1058
961
|
if (replayed) {
|
|
1059
962
|
const row = await deps.db
|
|
1060
|
-
.prepare(`SELECT ${grantColumns} FROM
|
|
963
|
+
.prepare(`SELECT ${grantColumns} FROM node_grants WHERE id = ?`)
|
|
1061
964
|
.bind(replayed.resource_id)
|
|
1062
965
|
.first();
|
|
1063
966
|
if (row)
|
|
@@ -1074,11 +977,11 @@ export function createNodeRepository(deps) {
|
|
|
1074
977
|
actor_id, operation, idempotency_key, resource_id, created_at
|
|
1075
978
|
) SELECT ?, 'node.revoke', ?,
|
|
1076
979
|
(CASE WHEN EXISTS (
|
|
1077
|
-
SELECT 1 FROM
|
|
980
|
+
SELECT 1 FROM node_grants WHERE id = ? AND node_id = ?
|
|
1078
981
|
) THEN '1:' ELSE '0:' END) || ?, ?`)
|
|
1079
982
|
.bind(input.actorId, input.idempotencyKey, input.grantId, input.resourceId, input.grantId, input.occurredAt),
|
|
1080
983
|
deps.db
|
|
1081
|
-
.prepare("DELETE FROM
|
|
984
|
+
.prepare("DELETE FROM node_grants WHERE id = ? AND node_id = ?")
|
|
1082
985
|
.bind(input.grantId, input.resourceId),
|
|
1083
986
|
deps.db
|
|
1084
987
|
.prepare(`INSERT INTO audit_events (
|
|
@@ -1121,10 +1024,10 @@ export function createNodeRepository(deps) {
|
|
|
1121
1024
|
LIMIT ?`)
|
|
1122
1025
|
.bind(...readBindings(actor), ...scopeBindings(input.scopeId), query, input.limit)
|
|
1123
1026
|
.all();
|
|
1124
|
-
// ⚠️ One citation per node, and the FIRST is the one kept.
|
|
1125
|
-
//
|
|
1027
|
+
// ⚠️ One citation per node, and the FIRST is the one kept. The index is CHUNKED — a node
|
|
1028
|
+
// may hold several rows (#285) — so a query matching three of them arrives here three times;
|
|
1126
1029
|
// the rows come back in rank order, so the survivor is the best-matching card and its own
|
|
1127
|
-
// passage is what the reader is shown. Without this, one busy
|
|
1030
|
+
// passage is what the reader is shown. Without this, one busy node could fill a whole page
|
|
1128
1031
|
// of results and push every other node off it.
|
|
1129
1032
|
return dedupedByNode(result.results ?? []).map((row) => ({
|
|
1130
1033
|
nodeId: row.node_id,
|
|
@@ -1139,7 +1042,7 @@ export function createNodeRepository(deps) {
|
|
|
1139
1042
|
},
|
|
1140
1043
|
async hydrateVisibleCitations(actor, hits, scopeId) {
|
|
1141
1044
|
// One entry per node: the caller has already picked the best-scoring chunk of each, so a
|
|
1142
|
-
//
|
|
1045
|
+
// node that answered with four passages arrives as the one passage that answered best.
|
|
1143
1046
|
const unique = [...new Map(hits.map((hit) => [hit.nodeId, hit])).values()].slice(0, 100);
|
|
1144
1047
|
if (unique.length === 0)
|
|
1145
1048
|
return [];
|
package/dist/bundle/bundle.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BlockNoteMediaType, BundleImportResult, BundleManifest, BundleManifestFilename, DocumentLinkInlineType, FlowGraph, TableMediaType, } from "@anchrd/intel-contract";
|
|
2
2
|
import { Unzip, UnzipInflate, Zip, ZipDeflate, ZipPassThrough } from "fflate";
|
|
3
|
-
import { repeatedBoardId, upgradeStoredBoard } from "../nodes/board/board.js";
|
|
4
3
|
import { documentLinkTargets } from "../nodes/document-links/document-links.js";
|
|
5
4
|
import { parseCsv } from "../shared/csv/csv.js";
|
|
6
5
|
import { IntelError } from "../shared/intel-error/intel-error.js";
|
|
@@ -217,13 +216,11 @@ function nakedKindOf(name) {
|
|
|
217
216
|
if (lower.endsWith(".csv")) {
|
|
218
217
|
return { kind: "table", mediaType: TableMediaType, title: name.slice(0, -4) };
|
|
219
218
|
}
|
|
220
|
-
// ⚠️
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
// is
|
|
224
|
-
|
|
225
|
-
return { kind: "board", mediaType: BoardMediaType, title: name.slice(0, -11) };
|
|
226
|
-
}
|
|
219
|
+
// ⚠️ `.board.json` had its own rule here until #390 and deliberately does not any more. This is
|
|
220
|
+
// the NAKED-folder path — a directory somebody picked, with no manifest — so a file called
|
|
221
|
+
// `plan.board.json` is a JSON file on a disk, and keeping it as an attachment preserves it. A
|
|
222
|
+
// real export is a different matter: its manifest declares the kind, and that is where such an
|
|
223
|
+
// entry is refused by name.
|
|
227
224
|
return { kind: "attachment", mediaType: guessedMediaType(lower), title: name };
|
|
228
225
|
}
|
|
229
226
|
const KnownMediaTypes = {
|
|
@@ -402,41 +399,11 @@ export function createBundle(deps) {
|
|
|
402
399
|
},
|
|
403
400
|
};
|
|
404
401
|
}
|
|
405
|
-
// A
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
return {
|
|
411
|
-
manifest: {
|
|
412
|
-
id: node.id,
|
|
413
|
-
kind: "board",
|
|
414
|
-
title: node.title,
|
|
415
|
-
description: node.description,
|
|
416
|
-
mediaType: version?.mediaType ?? BoardMediaType,
|
|
417
|
-
path,
|
|
418
|
-
},
|
|
419
|
-
content: version === null
|
|
420
|
-
? { type: "text", load: async () => "" }
|
|
421
|
-
: { type: "text", load: textLoader(version.contentKey) },
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
if (node.kind === "agent") {
|
|
425
|
-
const path = `${directory}${uniqueName(used, base, ".json")}`;
|
|
426
|
-
return {
|
|
427
|
-
manifest: {
|
|
428
|
-
id: node.id,
|
|
429
|
-
kind: "agent",
|
|
430
|
-
title: node.title,
|
|
431
|
-
description: node.description,
|
|
432
|
-
mediaType: version?.mediaType ?? "application/json",
|
|
433
|
-
path,
|
|
434
|
-
},
|
|
435
|
-
content: version === null
|
|
436
|
-
? { type: "text", load: async () => "" }
|
|
437
|
-
: { type: "text", load: textLoader(version.contentKey) },
|
|
438
|
-
};
|
|
439
|
-
}
|
|
402
|
+
// ⚠️ A row of a kind this build no longer makes — an `agent` or a `board` written before #390
|
|
403
|
+
// — falls through to the document branch and travels as its raw body. Deliberately forgiving:
|
|
404
|
+
// refusing here would make one leftover row enough to stop a whole subtree from being exported,
|
|
405
|
+
// and the file it produces is exactly the bytes that were stored. The IMPORT is where such a
|
|
406
|
+
// bundle is turned away, by name.
|
|
440
407
|
// A document: markdown stays `.md`, a BlockNote body is JSON and says so with its name.
|
|
441
408
|
const mediaType = version?.mediaType ?? "text/markdown";
|
|
442
409
|
const extension = mediaType === BlockNoteMediaType ? ".json" : ".md";
|
|
@@ -945,103 +912,23 @@ export function createBundle(deps) {
|
|
|
945
912
|
version = versionRowFor(entry, text, TableMediaType, await deps.hash(text), "snapshot");
|
|
946
913
|
}
|
|
947
914
|
}
|
|
948
|
-
else if (entry.kind === "board") {
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
* truth here — everywhere else a task id is minted by `deps.id()` and a status list
|
|
966
|
-
* goes through `ConfigureBoardInput`, which has demanded distinct ids since #285. Two
|
|
967
|
-
* tasks under one id threw `graph.addNode` out of the board's graph view, and two
|
|
968
|
-
* statuses under one id drew the same cards in two lanes with a drop that had two
|
|
969
|
-
* targets meaning one.
|
|
970
|
-
*
|
|
971
|
-
* Outside the catch above on purpose: swallowed into `import_invalid_board` this would
|
|
972
|
-
* read as "that file is not a board", and whoever is holding the file would look for
|
|
973
|
-
* the wrong thing. `repeatedBoardId` names the id, which is the line they have to fix.
|
|
974
|
-
*
|
|
975
|
-
* ⚠️ And it names the way out, not only the fault (anchrd/intel#341). The usual holder
|
|
976
|
-
* of such a file is not its author but somebody moving a tree between installations,
|
|
977
|
-
* and the pair is in the BOARD the export came from — where `board_repair_task_ids`
|
|
978
|
-
* and `board_configure` each remove one kind of it. Told only what is wrong, they were
|
|
979
|
-
* left editing JSON by hand or leaving the board behind, and an import is all or
|
|
980
|
-
* nothing: one such board stops the whole move.
|
|
981
|
-
*/
|
|
982
|
-
const repeated = repeatedBoardId(document);
|
|
983
|
-
if (repeated !== null) {
|
|
984
|
-
throw new IntelError(400, "import_duplicate_board_id", repeated.list === "tasks"
|
|
985
|
-
? `Bundle entry names the task “${repeated.id}” twice: ${entry.path}. Repair that board with board_repair_task_ids where it came from, then export it again.`
|
|
986
|
-
: `Bundle entry names the status “${repeated.id}” twice: ${entry.path}. Repair that board with board_configure — the status list written whole, with distinct ids — where it came from, then export it again.`);
|
|
987
|
-
}
|
|
988
|
-
// ⚠️ `references` are remapped the way a document's inline links are, and `dependsOn`
|
|
989
|
-
// and `parentId` are NOT (#285). A task's references point at NODES, which the import
|
|
990
|
-
// renumbers; its dependencies and its parent point at tasks inside this very file,
|
|
991
|
-
// whose ids the import does not touch. Renumbering those would break every edge in the
|
|
992
|
-
// board for no reason.
|
|
993
|
-
const remapped = {
|
|
994
|
-
statuses: document.statuses,
|
|
995
|
-
tasks: document.tasks.map((task) => ({
|
|
996
|
-
...task,
|
|
997
|
-
references: task.references.map((reference) => idMap.get(reference) ?? reference),
|
|
998
|
-
})),
|
|
999
|
-
};
|
|
1000
|
-
const body = JSON.stringify(remapped);
|
|
1001
|
-
version = versionRowFor(entry, body, BoardMediaType, await deps.hash(body), null);
|
|
1002
|
-
// Into the same link pass as an imported document: a board's references are `text`
|
|
1003
|
-
// links, so they have to survive the import the same way.
|
|
1004
|
-
documentBodies.set(entry.newId, { mediaType: BoardMediaType, content: body });
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
else if (entry.kind === "agent") {
|
|
1008
|
-
const text = entry.body === null ? "" : decodeText(entry.body, entry.path);
|
|
1009
|
-
if (text.trim().length > 0) {
|
|
1010
|
-
let definition;
|
|
1011
|
-
try {
|
|
1012
|
-
definition = AgentDefinition.parse(JSON.parse(text));
|
|
1013
|
-
}
|
|
1014
|
-
catch {
|
|
1015
|
-
throw new IntelError(400, "import_invalid_agent", `Bundle entry is not an agent definition: ${entry.path}`);
|
|
1016
|
-
}
|
|
1017
|
-
const remapped = {
|
|
1018
|
-
...definition,
|
|
1019
|
-
/**
|
|
1020
|
-
* ⚠️ A delegation NEVER travels in a file (#208, D30). `tools.delegatedBy` names
|
|
1021
|
-
* whose portal connection an agent acts on, and the only thing that may write it is
|
|
1022
|
-
* a person saving the agent in a session Intel authorized — the save path proves
|
|
1023
|
-
* they reach every named server first. A bundle is a document somebody hands over;
|
|
1024
|
-
* carrying the field through would let a crafted file name a local user and hand an
|
|
1025
|
-
* imported agent that person's connection, with no act of theirs anywhere.
|
|
1026
|
-
*
|
|
1027
|
-
* The consequence is deliberate and mild: an imported agent arrives with no tools,
|
|
1028
|
-
* and whoever wants it to have some gives them in the profile — which is the same
|
|
1029
|
-
* act D30 made the only way in.
|
|
1030
|
-
*/
|
|
1031
|
-
tools: null,
|
|
1032
|
-
references: definition.references.map((reference) => idMap.has(reference.nodeId)
|
|
1033
|
-
? { ...reference, nodeId: idMap.get(reference.nodeId) }
|
|
1034
|
-
: reference),
|
|
1035
|
-
schedules: definition.schedules.map((schedule) => idMap.has(schedule.target.id)
|
|
1036
|
-
? {
|
|
1037
|
-
...schedule,
|
|
1038
|
-
target: { ...schedule.target, id: idMap.get(schedule.target.id) },
|
|
1039
|
-
}
|
|
1040
|
-
: schedule),
|
|
1041
|
-
};
|
|
1042
|
-
const body = JSON.stringify(remapped);
|
|
1043
|
-
version = versionRowFor(entry, body, AgentMediaType, await deps.hash(body), null);
|
|
1044
|
-
}
|
|
915
|
+
else if (entry.kind === "board" || entry.kind === "agent") {
|
|
916
|
+
/**
|
|
917
|
+
* ⚠️ Refused by name, and the name says WHY rather than what the parser tripped over.
|
|
918
|
+
*
|
|
919
|
+
* Agents and Boards were parked with #385; nothing in this build can hold either kind.
|
|
920
|
+
* A bundle that carries one is not malformed — it is a correct export of an installation
|
|
921
|
+
* that still had the feature, and the person holding it is usually moving a tree between
|
|
922
|
+
* two installations rather than the author of the file. Left to the schema they would get
|
|
923
|
+
* `unexpected enum value`, look for a broken file, and find nothing wrong with it.
|
|
924
|
+
*
|
|
925
|
+
* The refusal names the entry, the kind and the branch the feature is parked on, so the
|
|
926
|
+
* answer to "where did my board go" is in the message and not in somebody's memory.
|
|
927
|
+
*
|
|
928
|
+
* ⚠️ Only a MANIFEST can reach this, because only a manifest declares a kind. A naked
|
|
929
|
+
* folder holding a `plan.board.json` imports it as an attachment — see `kindOfFile`.
|
|
930
|
+
*/
|
|
931
|
+
throw new IntelError(400, "import_kind_parked", `Bundle entry “${entry.path}” is a ${entry.kind}, and ${entry.kind}s are not part of this installation any more (anchrd/intel#385). The bundle was exported from a version that still had them; the code is on the branch parked/agents-and-board. Remove the entry from the bundle to import the rest.`);
|
|
1045
932
|
}
|
|
1046
933
|
else if (entry.kind === "attachment") {
|
|
1047
934
|
const bytes = entry.body ?? new Uint8Array(0);
|