@anchrd/intel-contract 0.12.0 → 0.14.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.
@@ -1,6 +1,18 @@
1
1
  import { z } from "zod";
2
2
  export const IntelId = z.string().min(1).max(128);
3
3
  export const IsoDateTime = z.iso.datetime({ offset: true });
4
+ // ⚠️ One described primitive rather than the same sentence on twenty-odd fields (#398). The word
5
+ // `idempotencyKey` means exactly the same thing everywhere it appears, and a reader who learns it
6
+ // once at `node_create` should not have to re-read it at `flow_publish`. Twenty copies of one
7
+ // sentence is also twenty places for it to drift, and `tools/list` would carry every copy.
8
+ //
9
+ // The description says what the caller has to DO, because that is the part a model gets wrong: it
10
+ // invents a fresh key on the retry and creates the thing twice.
11
+ export const IdempotencyKey = z
12
+ .string()
13
+ .min(8)
14
+ .max(200)
15
+ .describe("A caller-chosen key that makes this call safe to retry: repeating the same key returns the first result instead of doing the work twice. Reuse the SAME key when retrying one attempt, and a new one for a genuinely new call.");
4
16
  // The languages whose UI catalog ships inside intel-ui. They live here rather than only in the UI
5
17
  // because `intel build` has to know them: a built-in language may be chosen as `ui.defaultLanguage`
6
18
  // without the customer listing a copy under `ui.languages` that would rot at every UI update.
@@ -17,1810 +29,25 @@ export const ProblemDetails = z.strictObject({
17
29
  instance: z.string().optional(),
18
30
  code: z.string().optional(),
19
31
  });
20
- // Who the caller is, as Gate resolved it from the bearer. Deliberately identity only: no
21
- // capabilities, no token, nothing a surface could mistake for a permission.
32
+ // Who the caller is, as Gate resolved it from the bearer: no token, and no capability list.
33
+ //
34
+ // ⚠️ `isAdmin` is the one exception, and it is a DRAWING instruction rather than a permission
35
+ // (#416). MCP hides what a caller may not do by never registering the tool, so a model never sees a
36
+ // door it cannot open; the browser has no equivalent and would have to offer an admin action to
37
+ // everybody and answer with a 403. One boolean is what the shell needs to not do that.
38
+ //
39
+ // Two things keep it from becoming a capability list by degrees: it says what the shell may DRAW,
40
+ // never what the server may do — every door still asks Gate itself — and it is one field with one
41
+ // consumer. A second one is a second reason, written down when it exists.
22
42
  export const SessionUser = z.strictObject({
23
43
  id: IntelId,
24
44
  email: z.email(),
25
45
  name: z.string().min(1).max(240).nullable(),
46
+ isAdmin: z.boolean(),
26
47
  });
27
- // What this installation is equipped to do — deployment facts, never the caller's permissions
28
- // (those stay behind each door, where `/session` deliberately does not carry them). `agentRuntime`
29
- // says whether an agent Worker is bound at all (#190): without it the UI offers no "New agent" and
30
- // an agent node explains itself instead of rendering views that could only end in a 503.
31
- export const IntelCapabilities = z.strictObject({
32
- agentRuntime: z.boolean(),
33
- });
34
- // The fourth kind is `table` (#40), the fifth is `agent` (#139) and the sixth is `board` (#285).
35
- // Each is a kind of node, not a kind of thing: it hangs in the same folder tree, inherits the same
36
- // folder grants, carries the same immutable versions and the same R2 body as a document
37
- // (ADR-0004 §1, ADR-0005 §1). Only the media type and the operations below differ.
38
- //
39
- // ⚠️ `agent` being optional is load-bearing (ADR-0005 §1): an installation without a single agent
40
- // node is complete, not unfinished, and nothing here asks anyone to classify a document as a skill
41
- // or a policy in order to file it. The same holds for `board`: it is a file somebody may make, not
42
- // a place the tree grows a special corner for — which is exactly why a board is one node carrying
43
- // its tasks and not a folder that only tasks may live in (#285).
44
- export const NodeKind = z.enum(["folder", "document", "attachment", "table", "agent", "board"]);
45
- // ⚠️ There is no `ContextPolicy`, and it is not coming back in this shape (#76). It said whether a
46
- // document should be pinned into a context, be found by relevance, or be named explicitly — an
47
- // instruction to a retrieval Intel does not perform. Intel hands out references and the agent
48
- // fetches what it needs (D24), so nothing here could ever have read it, and nothing did.
49
- //
50
- // Semantic search stays: as an MCP tool the agent calls, over everything or over one area.
51
- // One verb per grant, granted independently (ADR-0004 §2). Not a ladder: seeing a process must be
52
- // separable from being allowed to start it, and `execute` is meaningful only where a flow can live.
53
- export const ResourceVerb = z.enum(["read", "write", "execute", "share"]);
54
- export const SharePrincipal = z.discriminatedUnion("type", [
55
- z.strictObject({ type: z.literal("user"), id: IntelId }),
56
- z.strictObject({ type: z.literal("email"), email: z.email() }),
57
- z.strictObject({ type: z.literal("organization") }),
58
- ]);
59
- export const Node = z.strictObject({
60
- id: IntelId,
61
- parentId: IntelId.nullable(),
62
- kind: NodeKind,
63
- title: z.string().min(1).max(240),
64
- description: z.string().max(2_000).nullable(),
65
- ownerId: IntelId,
66
- currentVersionId: IntelId.nullable(),
67
- createdAt: IsoDateTime,
68
- updatedAt: IsoDateTime,
69
- archivedAt: IsoDateTime.nullable(),
70
- });
71
- // What one table version carries (#135). An `append` holds only the rows one write added; a
72
- // `snapshot` holds the complete table — header and every row — so reading starts at the newest
73
- // snapshot and everything before it is history rather than content. Defining a table writes the
74
- // first snapshot; updating, deleting, and redefining write the later ones. Documents and
75
- // attachments carry `null`: each of their versions is complete by construction, and the word would
76
- // say nothing about them.
77
- export const NodeVersionSegment = z.enum(["append", "snapshot"]);
78
- export const NodeVersion = z.strictObject({
79
- id: IntelId,
80
- nodeId: IntelId,
81
- sequence: z.number().int().positive(),
82
- contentKey: z.string().min(1),
83
- mediaType: z.string().min(1).max(160),
84
- contentHash: z.string().regex(/^[a-f0-9]{64}$/),
85
- size: z.number().int().nonnegative(),
86
- segment: NodeVersionSegment.nullable(),
87
- createdBy: IntelId,
88
- createdAt: IsoDateTime,
89
- });
90
- export const ListNodesInput = z.strictObject({
91
- parentId: IntelId.nullable().default(null),
92
- includeArchived: z.boolean().default(false),
93
- // ⚠️ Overrides `parentId` rather than narrowing beside it: what is archived is asked for across
94
- // the whole tree, because that is the only useful question. Somebody looking for what they threw
95
- // away does not know which folder it was in — if they did, they would not be looking (#113).
96
- // A separate flag rather than a third state on `includeArchived`, so no existing caller changes
97
- // meaning — and `.optional()` rather than `.default(false)` for the same reason `ListFlowsInput`
98
- // carries it that way: a default makes the field required in the PARSED type, and every existing
99
- // caller would have to answer a question it is not asking.
100
- archivedOnly: z.boolean().optional(),
101
- });
102
- export const GetNodeInput = z.strictObject({ nodeId: IntelId });
103
- // One pinned version of one node (#147). Both IDs, always: a version ID alone would let anyone
104
- // holding an ID read content whose node-level ACL they never passed, and a citation names both.
105
- export const GetNodeVersionInput = z.strictObject({ nodeId: IntelId, versionId: IntelId });
106
- export const ListGrantsInput = z.strictObject({ resourceId: IntelId });
107
- export const CreateNodeInput = z.strictObject({
108
- parentId: IntelId.nullable().default(null),
109
- kind: NodeKind,
110
- title: z.string().trim().min(1).max(240),
111
- description: z.string().trim().max(2_000).nullable().default(null),
112
- idempotencyKey: z.string().min(8).max(200),
113
- });
114
- export const SaveNodeVersionInput = z.strictObject({
115
- nodeId: IntelId,
116
- baseVersionId: IntelId.nullable(),
117
- content: z.string().max(10_000_000),
118
- mediaType: z.string().min(1).max(160).default("text/markdown"),
119
- idempotencyKey: z.string().min(8).max(200),
120
- });
121
- export const SaveAttachmentInput = z.strictObject({
122
- nodeId: IntelId,
123
- baseVersionId: IntelId.nullable(),
124
- contentBase64: z.string().min(1).max(20_000_000),
125
- mediaType: z.string().min(1).max(160),
126
- idempotencyKey: z.string().min(8).max(200),
127
- });
128
- export const UpdateNodeInput = z
129
- .strictObject({
130
- nodeId: IntelId,
131
- baseUpdatedAt: IsoDateTime,
132
- title: z.string().trim().min(1).max(240).optional(),
133
- description: z.string().trim().max(2_000).nullable().optional(),
134
- parentId: IntelId.nullable().optional(),
135
- idempotencyKey: z.string().min(8).max(200),
136
- })
137
- .refine((input) => input.title !== undefined || input.description !== undefined || input.parentId !== undefined, { message: "At least one change is required" });
138
- export const ArchiveNodeInput = z.strictObject({
139
- nodeId: IntelId,
140
- baseUpdatedAt: IsoDateTime,
141
- archived: z.boolean(),
142
- idempotencyKey: z.string().min(8).max(200),
143
- });
144
- // ⚠️ `withChildren` belongs to the LEVEL, not to the node (#59). Whether something has children
145
- // THIS reader may see is not a property of the thing — two readers get different answers. As a
146
- // field on the node, every other place that returns a node would have to compute it as well or
147
- // lie; as a list beside the entries it costs only the one answer that needs it.
148
- export const NodeList = z.strictObject({
149
- items: z.array(Node),
150
- withChildren: z.array(IntelId).default([]),
151
- });
152
- export const NodeVersionList = z.strictObject({ items: z.array(NodeVersion) });
153
- export const NodeDocument = z.strictObject({
154
- node: Node,
155
- version: NodeVersion.nullable(),
156
- content: z.string().nullable(),
157
- });
158
- export const NodeAttachment = z.strictObject({
159
- node: Node,
160
- version: NodeVersion,
161
- resourceUri: z.string().regex(/^intel:\/\/nodes\/[^/]+\/attachment$/),
162
- });
163
- // A table is CSV, and CSV is the whole format: it is what is stored, what is downloaded and what a
164
- // machine reads. There is no second representation to keep in step with it (#40).
165
- export const TableMediaType = "text/csv";
166
- // A column name is the contract between the table and everyone who appends to it, so it is trimmed,
167
- // non-empty and bounded like a title. Cells are not: a cell is text, and text is what CSV carries.
168
- export const TableColumn = z.string().trim().min(1).max(120);
169
- export const TableCell = z.string().max(4_000);
170
- export const TableRow = z.array(TableCell).min(1).max(64);
171
- // Writing the header, once. The columns are the contract (#40's comment), which is why this refuses
172
- // on a table that already has one: changing the header would silently reinterpret every row that
173
- // was appended under the old one.
174
- export const DefineTableInput = z.strictObject({
175
- nodeId: IntelId,
176
- columns: z
177
- .array(TableColumn)
178
- .min(1)
179
- .max(64)
180
- .refine((columns) => new Set(columns.map((column) => column.toLowerCase())).size === columns.length, { error: "Column names must be distinct" }),
181
- idempotencyKey: z.string().min(8).max(200),
182
- });
183
- // ⚠️ No `baseVersionId`, and that absence is the feature. A document replaces its content and needs
184
- // to know which content it replaces; an append adds to the end and cannot collide with a second
185
- // append, so demanding a base version would invent a conflict that does not exist and force the
186
- // caller to read the whole table first — the exact cost #40 exists to remove.
187
- export const AppendTableRowsInput = z.strictObject({
188
- nodeId: IntelId,
189
- rows: z.array(TableRow).min(1).max(1_000),
190
- idempotencyKey: z.string().min(8).max(200),
191
- });
192
- export const GetTableInput = z.strictObject({ nodeId: IntelId });
193
- // The table as a grid rather than as text: the server owns the one CSV reader, so no surface has to
194
- // grow a second one that would disagree with it about quoting.
195
- export const NodeTable = z.strictObject({
196
- node: Node,
197
- columns: z.array(z.string()),
198
- rows: z.array(z.array(z.string())),
199
- // The newest append, or `null` while the table has no header yet.
200
- versionId: IntelId.nullable(),
201
- });
202
- export const AppendTableRowsResult = z.strictObject({
203
- node: Node,
204
- version: NodeVersion,
205
- appended: z.number().int().positive(),
206
- });
207
- // A row's address is its position among the table's current rows, counted from zero and without the
208
- // header. Deliberately not an ID: rows carry no identity of their own (#135, and the same decision
209
- // the grid documents), so every mutation instead pins the state its positions refer to.
210
- export const TableRowPosition = z.number().int().nonnegative();
211
- const distinctPositions = { error: "Row positions must be distinct" };
212
- // Replacing rows in place (#135). `baseVersionId` is the version the caller read the positions
213
- // from — required, never nullable, because a position into a table one has not read is a guess.
214
- // A table that moved on since answers `version_conflict` rather than editing the wrong rows; that
215
- // is the same optimistic concurrency the document save uses, and the deliberate opposite of
216
- // `append`, which needs no base because it collides with nothing.
217
- export const UpdateTableRowsInput = z.strictObject({
218
- nodeId: IntelId,
219
- baseVersionId: IntelId,
220
- updates: z
221
- .array(z.strictObject({ position: TableRowPosition, row: TableRow }))
222
- .min(1)
223
- .max(1_000)
224
- .refine((updates) => new Set(updates.map((update) => update.position)).size === updates.length, distinctPositions),
225
- idempotencyKey: z.string().min(8).max(200),
226
- });
227
- export const UpdateTableRowsResult = z.strictObject({
228
- node: Node,
229
- version: NodeVersion,
230
- updated: z.number().int().positive(),
231
- });
232
- export const DeleteTableRowsInput = z.strictObject({
233
- nodeId: IntelId,
234
- baseVersionId: IntelId,
235
- positions: z
236
- .array(TableRowPosition)
237
- .min(1)
238
- .max(1_000)
239
- .refine((positions) => new Set(positions).size === positions.length, distinctPositions),
240
- idempotencyKey: z.string().min(8).max(200),
241
- });
242
- export const DeleteTableRowsResult = z.strictObject({
243
- node: Node,
244
- version: NodeVersion,
245
- deleted: z.number().int().positive(),
246
- });
247
- // One entry per column the table will have afterwards, in order. `source` names the current column
248
- // whose cells fill it; `null` adds an empty column, and a current column no entry names is removed
249
- // together with its cells. Renaming is naming a source under a new name.
250
- export const RedefineTableColumn = z.strictObject({
251
- name: TableColumn,
252
- source: TableColumn.nullable().default(null),
253
- });
254
- // Changing the header of a table that has one (#135). The mapping is explicit because it is the
255
- // whole difference to the blind re-definition `defineTable` keeps refusing: without it a new header
256
- // would silently reinterpret every stored row under names nobody matched to the old ones.
257
- export const RedefineTableInput = z.strictObject({
258
- nodeId: IntelId,
259
- baseVersionId: IntelId,
260
- columns: z
261
- .array(RedefineTableColumn)
262
- .min(1)
263
- .max(64)
264
- .refine((columns) => new Set(columns.map((column) => column.name.toLowerCase())).size === columns.length, { error: "Column names must be distinct" })
265
- .refine((columns) => {
266
- const sources = columns.map((column) => column.source).filter((source) => source !== null);
267
- return new Set(sources).size === sources.length;
268
- }, { error: "A current column can fill only one new column" }),
269
- idempotencyKey: z.string().min(8).max(200),
270
- });
271
- // ── The board (#285) ─────────────────────────────────────────────────────────────────────────────
272
- //
273
- // A board's body is a whole project board — the status list and every task — stored as one
274
- // validated JSON version in R2, exactly the way an agent's definition is. It is a kind of node and
275
- // not a kind of thing (see `NodeKind`): it hangs in a folder, inherits that folder's grants, has
276
- // versions and is exported like everything else. Its own media type exists so a reader can tell a
277
- // board from prose without parsing it.
278
- export const BoardMediaType = "application/vnd.anchrd.board+json";
279
- // ⚠️ `archived` belongs to EVERY status list and cannot be configured away (#285). It is the shelf
280
- // tasks are swept onto, so that being finished with a task never has to mean deleting it — and
281
- // getting one back is an explicit move to another status, never an undelete.
282
- export const ArchivedBoardStatusId = "archived";
283
- // A status id is referenced by every task that sits in that column, so it is machine-shaped and
284
- // bounded rather than free text. Renaming a column changes its LABEL; the id stays, and no task has
285
- // to be rewritten to follow it.
286
- export const BoardStatusId = z.string().regex(/^[a-z0-9][a-z0-9_]{0,39}$/);
287
- export const BoardStatus = z.strictObject({
288
- id: BoardStatusId,
289
- label: z.string().trim().min(1).max(60),
290
- // ⚠️ Server-assigned from the position in `ConfigureBoardInput.statuses`, never sent. Two columns
291
- // both claiming position 3 is a board no surface could draw, and it is a state nobody has to be
292
- // able to reason about if it cannot be expressed.
293
- order: z.number().int().nonnegative(),
294
- /**
295
- * Whether standing in this column means the work is finished (anchrd/intel#311).
296
- *
297
- * It answers the one question #285 left open — when a `dependsOn` is satisfied — and it is the
298
- * ONLY answer to it. There is deliberately no second reading anywhere: a surface that decided
299
- * "done" for itself would decide it differently the first time somebody reconfigured a board.
300
- *
301
- * ⚠️ It is a property of the status, not a position in the list, and that distinction is the
302
- * whole ticket. #286 first read "the last column before `archived`" as done, reasoning by symmetry
303
- * with the server's rule that a new task lands in the first column that is not `archived`. But a
304
- * status list is configurable on purpose, so `… done → blocked → archived` makes "blocked" mean
305
- * finished — silently, with a wrong blocked marker as the only symptom.
306
- *
307
- * ⚠️ Several columns may carry it. "Done" and a cancelled-like column are both ends of the work,
308
- * and nothing waiting on a cancelled task is still blocked by it.
309
- */
310
- terminal: z.boolean(),
311
- });
312
- // What a board starts out with. Five columns, of which the last one is the fixed `archived` shelf.
313
- // `done` and the shelf are where work ends; the three before them are not (anchrd/intel#311).
314
- export const BoardDefaultStatuses = [
315
- { id: "backlog", label: "Backlog", order: 0, terminal: false },
316
- { id: "in_progress", label: "In progress", order: 1, terminal: false },
317
- { id: "review", label: "Review", order: 2, terminal: false },
318
- { id: "done", label: "Done", order: 3, terminal: true },
319
- { id: ArchivedBoardStatusId, label: "Archived", order: 4, terminal: true },
320
- ];
321
- /**
322
- * Who a task is on: a person Gate knows, or an agent node in this installation (#285).
323
- *
324
- * ⚠️ A `user` id is deliberately NOT validated against Intel's own id shape. Identity is Gate's
325
- * (see the product boundary), so a rule here would be Intel inventing one about somebody else's
326
- * identifier — the same reason `GateApplicationId` is a plain bounded string.
327
- */
328
- export const BoardAssignee = z.discriminatedUnion("type", [
329
- z.strictObject({ type: z.literal("user"), id: z.string().min(1).max(255) }),
330
- z.strictObject({ type: z.literal("agent"), nodeId: IntelId }),
331
- ]);
332
- /**
333
- * A task's place among the others, as a fractional index (#285).
334
- *
335
- * ⚠️ Server-assigned, and a caller can never send one. A move names its NEIGHBOURS and the server
336
- * mints a key between theirs, so moving one task writes one task and renumbers nothing — the whole
337
- * reason a board is not addressed by position the way a table's rows are (`TableRowPosition`).
338
- * A hand-written key could collide, and two tasks sharing a key have no defined order at all.
339
- *
340
- * ⚠️ This CHARACTER CLASS is the truth about a stored key, not `fractional-indexing`, and the two
341
- * are deliberately not the same set (anchrd/intel#359). A board that arrived through a bundle may
342
- * spell `"0"`, `"a00"`, `"zzz"` or `"A"` — all of them legal here, none of them readable by that
343
- * library. Narrowing the regex to what it reads would be a rule on the STORED document, so a board
344
- * carrying one would stop parsing in all four places a board body is read (the board read, the
345
- * indexer, the link reader, the bundle import) — unreadable, unsearchable and unmovable at once,
346
- * over a value its owner never wrote. That is the trap #311, #318 and #321 each walked into from a
347
- * different side.
348
- *
349
- * ⚠️ What every consumer may therefore rely on is exactly what stands here: keys are non-empty
350
- * strings over `[0-9A-Za-z]` and are ORDERED BY STRING COMPARISON. Nothing may assume more — and
351
- * `packages/api`'s `orderBetween` is the one place that asks `fractional-indexing` for a key and
352
- * carries on without it when it refuses a bound.
353
- */
354
- // ⚠️ Named because a consumer has to be able to stay inside it. The repair of a repeated task id
355
- // (anchrd/intel#341) mints a key beside an existing one, and a key one character too long would be
356
- // written and then refused by the very next read — the whole board lost over a repair.
357
- export const MaxBoardTaskOrderLength = 64;
358
- export const BoardTaskOrder = z
359
- .string()
360
- .regex(new RegExp(`^[0-9A-Za-z]{1,${MaxBoardTaskOrderLength}}$`));
361
- export const BoardTaskId = IntelId;
362
- export const BoardTaskLabel = z.string().trim().min(1).max(60);
363
- // A day, not an instant. A task is due on a date; giving it a time zone would make the same task
364
- // due on two different days depending on who is looking at it.
365
- export const BoardTaskDate = z.iso.date();
366
- // Markdown, and capped: a task's description is a card, and what needs more than this is a document
367
- // the task can point at through `references`.
368
- export const BoardTaskDescription = z.string().max(20_000);
369
- /**
370
- * The tasks one task waits for (#285), each of them at most once (anchrd/intel#318).
371
- *
372
- * ⚠️ Board-internal only, enforced on the write path: a dependency on a task in another board would
373
- * hang this node on a file that can change without anyone here noticing. Across boards the link is
374
- * `references`, which points at the board NODE and lands in the link graph.
375
- *
376
- * ⚠️ Refused rather than folded together, the same shape as the status ids in `ConfigureBoardInput`.
377
- * A repeat carries no information — but that is a fact about the value, not about the answer: a
378
- * caller handed back a shorter list than it sent is told nothing, and composes the same one again.
379
- * The refusal names the mistake once, and the caller is holding the list it has to fix. (The one
380
- * place a repeat is folded instead is `upgradeStoredBoard`, where there is no caller to tell.)
381
- *
382
- * ⚠️ It sits on the STORED task as well as on the two inputs, so a consumer may rely on it rather
383
- * than defend against it — `createBoardGraph` mints one edge key per pair, and a second one threw
384
- * the whole graph view off the screen for everybody looking at that board.
385
- */
386
- export const BoardTaskDependsOn = z
387
- .array(BoardTaskId)
388
- .max(64)
389
- .refine((ids) => new Set(ids).size === ids.length, {
390
- error: "A task can be named only once in dependsOn",
391
- });
392
- /**
393
- * A task's labels, each of them at most once (anchrd/intel#318).
394
- *
395
- * ⚠️ The same rule the detail panel has always applied to what a person types — it refuses to add a
396
- * label the task already carries — stated where every surface meets it, because the MCP write path
397
- * did not. A repeated label draws the same chip twice on the card, with two remove buttons of which
398
- * either takes both away, and weights that word higher in the search text (`indexing.ts`).
399
- */
400
- export const BoardTaskLabels = z
401
- .array(BoardTaskLabel)
402
- .max(32)
403
- .refine((labels) => new Set(labels).size === labels.length, {
404
- error: "A label can be named only once",
405
- });
406
- /**
407
- * The Intel nodes a task points at, each of them at most once (anchrd/intel#318).
408
- *
409
- * They land in the link graph as `text` links, the same way a document's inline links do, so what a
410
- * board points at is visible from the other side too.
411
- *
412
- * ⚠️ Distinct for the same reason as `labels`: the picker in the detail panel already refuses one
413
- * the task holds, and the link graph counts a repeat once anyway (`ON CONFLICT DO NOTHING`), so a
414
- * duplicate is a second row in the panel and nothing else — which is exactly the kind of value that
415
- * has no reading and should not be storable.
416
- */
417
- export const BoardTaskReferences = z
418
- .array(IntelId)
419
- .max(64)
420
- .refine((ids) => new Set(ids).size === ids.length, {
421
- error: "A node can be referenced only once",
422
- });
423
- export const BoardTask = z.strictObject({
424
- id: BoardTaskId,
425
- title: z.string().trim().min(1).max(240),
426
- status: BoardStatusId,
427
- assignee: BoardAssignee.nullable(),
428
- labels: BoardTaskLabels,
429
- startDate: BoardTaskDate.nullable(),
430
- dueDate: BoardTaskDate.nullable(),
431
- // ⚠️ The whole hierarchy in one field, deliberately: epic, task and subtask are a DEPTH and not a
432
- // type (#285). A `kind` beside it would allow a subtask under nothing and an epic under an epic,
433
- // and every surface would then need its own opinion about which combinations mean anything.
434
- parentId: BoardTaskId.nullable(),
435
- dependsOn: BoardTaskDependsOn,
436
- order: BoardTaskOrder,
437
- description: BoardTaskDescription,
438
- references: BoardTaskReferences,
439
- });
440
- // The whole board, as it is stored and as it is read. There is no second representation to keep in
441
- // step with it — this document is the file.
442
- export const BoardDocument = z.strictObject({
443
- statuses: z.array(BoardStatus).min(1).max(32),
444
- tasks: z.array(BoardTask).max(5_000),
445
- });
446
- // How deep `parentId` may nest. Five is epic → task → subtask with room left over; without a bound
447
- // a chain of a thousand tasks would be a valid board that no view can draw and no walk can afford.
448
- export const BoardMaxTaskDepth = 5;
449
- export const GetBoardInput = z.strictObject({ nodeId: IntelId });
450
- // A board as it is read. `versionId` is `null` while nothing has been written yet — the same answer
451
- // a table gives before its header exists — and the document is then the defaults.
452
- export const NodeBoard = z.strictObject({
453
- node: Node,
454
- board: BoardDocument,
455
- versionId: IntelId.nullable(),
456
- });
457
- export const BoardStatusInput = z.strictObject({
458
- id: BoardStatusId,
459
- label: z.string().trim().min(1).max(60),
460
- /**
461
- * Whether this column means finished (anchrd/intel#311).
462
- *
463
- * ⚠️ Optional, and the absence is not the same as `false`. A caller who says nothing gets the
464
- * server's answer — `false` for an ordinary column, `true` for the shelf, which cannot be
465
- * anything else. Making it a required boolean would force every caller that only wanted to rename
466
- * a column to restate the whole board's notion of done, and getting one entry wrong there is a
467
- * silent change to what counts as blocked.
468
- */
469
- terminal: z.boolean().optional(),
470
- });
471
- // The status list, written whole and in the order it should be drawn — never a patch. Adding,
472
- // renaming and reordering are all this one call, and `archived` has to be in what it is given.
473
- export const ConfigureBoardInput = z.strictObject({
474
- nodeId: IntelId,
475
- statuses: z
476
- .array(BoardStatusInput)
477
- .min(1)
478
- .max(32)
479
- .refine((statuses) => new Set(statuses.map((status) => status.id)).size === statuses.length, {
480
- error: "Status ids must be distinct",
481
- })
482
- .refine((statuses) => statuses.some((status) => status.id === ArchivedBoardStatusId), {
483
- error: `The "${ArchivedBoardStatusId}" status cannot be removed`,
484
- })
485
- // ⚠️ Refused rather than corrected, the same way removing the shelf is refused. A task swept
486
- // onto `archived` is finished with, and a board that could declare the shelf non-terminal would
487
- // hold every archived task open as a blocker forever. Only an EXPLICIT `false` is refused —
488
- // saying nothing is fine and means the server's `true` (anchrd/intel#311).
489
- .refine((statuses) => statuses.find((status) => status.id === ArchivedBoardStatusId)?.terminal !== false, { error: `The "${ArchivedBoardStatusId}" status is always terminal` }),
490
- idempotencyKey: z.string().min(8).max(200),
491
- });
492
- /**
493
- * A new task (#285).
494
- *
495
- * ⚠️ No `baseVersionId`, on this and on every other task operation, and that absence is the
496
- * feature. A board is addressed by stable task id and never by position, so two agents touching two
497
- * different tasks cannot collide — demanding a base version would invent the `version_conflict`
498
- * that #285 exists to remove, and force every caller to read the whole board first.
499
- */
500
- export const AddBoardTaskInput = z.strictObject({
501
- nodeId: IntelId,
502
- title: z.string().trim().min(1).max(240),
503
- // Omitted means the first status that is not `archived`: a new task belongs on the board, not on
504
- // the shelf.
505
- status: BoardStatusId.optional(),
506
- assignee: BoardAssignee.nullable().default(null),
507
- labels: BoardTaskLabels.default([]),
508
- startDate: BoardTaskDate.nullable().default(null),
509
- dueDate: BoardTaskDate.nullable().default(null),
510
- parentId: BoardTaskId.nullable().default(null),
511
- dependsOn: BoardTaskDependsOn.default([]),
512
- description: BoardTaskDescription.default(""),
513
- references: BoardTaskReferences.default([]),
514
- // Where among its neighbours it goes. Both absent puts it last in its column.
515
- afterTaskId: BoardTaskId.nullable().default(null),
516
- beforeTaskId: BoardTaskId.nullable().default(null),
517
- idempotencyKey: z.string().min(8).max(200),
518
- });
519
- /**
520
- * What a task says about itself.
521
- *
522
- * ⚠️ Deliberately no `status`, no `parentId` and no `order`: where a task SITS is a move, and a
523
- * move is the operation that has to mint an order key and re-check the two cycle rules. Folding
524
- * both into one call would mean every field edit pays for those checks and every move could quietly
525
- * rewrite a description.
526
- */
527
- export const UpdateBoardTaskInput = z
528
- .strictObject({
529
- nodeId: IntelId,
530
- taskId: BoardTaskId,
531
- title: z.string().trim().min(1).max(240).optional(),
532
- assignee: BoardAssignee.nullable().optional(),
533
- labels: BoardTaskLabels.optional(),
534
- startDate: BoardTaskDate.nullable().optional(),
535
- dueDate: BoardTaskDate.nullable().optional(),
536
- dependsOn: BoardTaskDependsOn.optional(),
537
- description: BoardTaskDescription.optional(),
538
- references: BoardTaskReferences.optional(),
539
- idempotencyKey: z.string().min(8).max(200),
540
- })
541
- .refine((input) => input.title !== undefined ||
542
- input.assignee !== undefined ||
543
- input.labels !== undefined ||
544
- input.startDate !== undefined ||
545
- input.dueDate !== undefined ||
546
- input.dependsOn !== undefined ||
547
- input.description !== undefined ||
548
- input.references !== undefined, { error: "At least one change is required" });
549
- // Where a task sits: its column, its parent, its place among its neighbours. Archiving is this call
550
- // with `status: "archived"` — there is no separate verb, because it is not a separate act.
551
- export const MoveBoardTaskInput = z
552
- .strictObject({
553
- nodeId: IntelId,
554
- taskId: BoardTaskId,
555
- status: BoardStatusId.optional(),
556
- parentId: BoardTaskId.nullable().optional(),
557
- afterTaskId: BoardTaskId.nullable().default(null),
558
- beforeTaskId: BoardTaskId.nullable().default(null),
559
- idempotencyKey: z.string().min(8).max(200),
560
- })
561
- .refine((input) => input.status !== undefined ||
562
- input.parentId !== undefined ||
563
- input.afterTaskId !== null ||
564
- input.beforeTaskId !== null, { error: "A move needs a status, a parent or a neighbour" });
565
- // ⚠️ Deleting cascades to every descendant, and the count comes back so a surface can warn BEFORE
566
- // asking. See `DeleteBoardTaskResult`.
567
- export const DeleteBoardTaskInput = z.strictObject({
568
- nodeId: IntelId,
569
- taskId: BoardTaskId,
570
- idempotencyKey: z.string().min(8).max(200),
571
- });
572
- /**
573
- * The way out of a board whose `tasks[]` names one id twice (anchrd/intel#341).
574
- *
575
- * ⚠️ It names no task, and that is not an oversight. Such a board holds a pair the caller cannot
576
- * tell apart — every other operation here addresses a task BY id (#285), so the one thing nobody
577
- * can say is "the second of the two". The board is what is named, and the server does the one thing
578
- * that removes the ambiguity: the first entry under an id keeps it, every later one gets a freshly
579
- * minted id and keeps everything else it carries.
580
- *
581
- * ⚠️ It exists because the pair is deliberately NOT folded away on read (`upgradeStoredBoard`,
582
- * `repeatedBoardId`): two tasks under one id are two whole tasks, and a fold on the read is written
583
- * back by the next save of any kind. So somebody has to ask for the repair, and this is the asking.
584
- */
585
- export const RepairBoardTaskIdsInput = z.strictObject({
586
- nodeId: IntelId,
587
- idempotencyKey: z.string().min(8).max(200),
588
- });
589
- // The one task that was written, not the whole board: a board can hold thousands of tasks, and
590
- // answering a one-card edit with all of them would make every write pay for the read.
591
- export const BoardTaskResult = z.strictObject({
592
- node: Node,
593
- version: NodeVersion,
594
- task: BoardTask,
595
- });
596
- // `deleted` counts the task AND every descendant that went with it, so a caller can say what
597
- // happened rather than "done".
598
- export const DeleteBoardTaskResult = z.strictObject({
599
- node: Node,
600
- version: NodeVersion,
601
- deleted: z.number().int().positive(),
602
- });
603
- export const ConfigureBoardResult = z.strictObject({
604
- node: Node,
605
- version: NodeVersion,
606
- statuses: z.array(BoardStatus),
607
- });
608
- /**
609
- * What the repair did, task by task (anchrd/intel#341).
610
- *
611
- * ⚠️ `previousId` is the id the entry shared, and it still names a task on this board — the FIRST
612
- * entry under it, the one that kept it. That is what makes the repair readable: nothing that
613
- * pointed at that id moved, so a caller can see which of the two the board's `parentId` and
614
- * `dependsOn` edges have been meaning all along, and move them with `board_task_move` and
615
- * `board_task_update` if they meant the other one.
616
- *
617
- * ⚠️ At least one entry, because a board with nothing to repair is refused rather than answered
618
- * with an empty list and a new version that changed nothing.
619
- */
620
- export const RepairBoardTaskIdsResult = z.strictObject({
621
- node: Node,
622
- version: NodeVersion,
623
- renumbered: z
624
- .array(z.strictObject({ previousId: BoardTaskId, task: BoardTask }))
625
- .min(1)
626
- .max(5_000),
627
- });
628
- // ── The agent definition (#139, ADR-0005 §4) ─────────────────────────────────────────────────────
629
- //
630
- // An agent's body is a definition, stored as an immutable version in R2 exactly like a document's.
631
- // Its own media type exists so a reader can tell a definition from prose without parsing it.
632
- export const AgentMediaType = "application/vnd.anchrd.agent+json";
633
- // ⚠️ The role lives on the AGENT, never on the node it names, and that is the whole difference to
634
- // the removed `context_policy` (ADR-0005 §2, #76). The same folder can be the system message for
635
- // one agent and nothing but search space for another; a node has no opinion about how it is used.
636
- // Any future field on a node saying how it should be loaded is `context_policy` under a new name.
637
- //
638
- // system-message prepended verbatim by the runtime
639
- // semantic-context search space; the agent searches it when it decides to
640
- // memory write target — ordinary Knowledge, versioned and readable like everything else
641
- export const AgentReferenceRole = z.enum(["system-message", "semantic-context", "memory"]);
642
- export const AgentReference = z.strictObject({ nodeId: IntelId, role: AgentReferenceRole });
643
- /**
644
- * Which kinds of node each role can actually be given (#255).
645
- *
646
- * ⚠️ Not every role takes every kind, and the reasons are about what the runtime DOES with a
647
- * reference rather than about tidiness:
648
- *
649
- * `memory` is a folder because the agent WRITES there — `agent_remember` creates a note inside
650
- * it. A single document as memory would mean the agent overwrites the document it was given.
651
- *
652
- * `semantic-context` is a folder because it is a search SPACE, searched per folder by
653
- * `loop/scoped-search`. A single document is not a narrower search space; reading it whole is a
654
- * different behaviour, and one that gets named before it is introduced, not slipped in.
655
- *
656
- * `system-message` reads single nodes already and takes a document or a table as well as a
657
- * folder. A document is the natural case — a skill somebody wrote as ordinary text — and a table
658
- * is the same read: the runtime asks intel for the node and prepends its content, which for a
659
- * table is its CSV.
660
- *
661
- * ⚠️ `folder` stays on `system-message` although a folder carries no content of its own. Every
662
- * definition written before #255 could only name folders, and taking the combination away here
663
- * would refuse the next save of an agent that has been working for months — "existing definitions
664
- * stay valid" is not only about reading them.
665
- *
666
- * ⚠️ This is the ONE place the rule lives. The screen offers what it says and the write path
667
- * refuses what it forbids; a surface that made its own list would eventually disagree with the
668
- * other, and the one that matters is whichever runs last.
669
- */
670
- export const AgentReferenceKinds = {
671
- "system-message": ["folder", "document", "table"],
672
- "semantic-context": ["folder"],
673
- memory: ["folder"],
674
- };
675
- export function agentReferenceAccepts(role, kind) {
676
- return AgentReferenceKinds[role].includes(kind);
677
- }
678
- /** The roles a node of this kind may be given — the same rule, read from the other side. */
679
- export function agentReferenceRolesFor(kind) {
680
- return AgentReferenceRole.options.filter((role) => agentReferenceAccepts(role, kind));
681
- }
682
- // A `document` target means the content of that document is the instruction — a "skill" somebody
683
- // wrote as ordinary text; a `flow` target means a run is started through Intel MCP and worked step
684
- // by step. Both are references, so nothing in here goes stale (ADR-0005 §4).
685
- //
686
- // ⚠️ Intel stores a schedule as a declared fact and never fires it. The alarm lives in the runtime
687
- // (ADR-0005 §3); Intel gains no scheduler, which is D24 confirmed rather than bent.
688
- export const AgentScheduleTarget = z.strictObject({
689
- kind: z.enum(["document", "flow"]),
690
- id: IntelId,
691
- });
692
- /**
693
- * ⚠️ `timezone` is what the cron expression is READ IN, and it belongs to the schedule rather than
694
- * to whoever is looking at it (#228). "Every morning at eight" means eight o'clock where the person
695
- * who wrote it sits — in Berlin that is 06:00 UTC in summer and 07:00 in winter, and a field that
696
- * does not carry the zone cannot express that difference. A UTC cron is an hour wrong twice a year
697
- * and nobody sees why.
698
- *
699
- * The UI suggests the reader's own zone when a schedule is created, but it is not a per-user
700
- * setting: an agent's schedule would otherwise move whenever its owner travelled, and it would mean
701
- * different times to two people reading the same definition. What is stored is the answer.
702
- *
703
- * ⚠️ The default is `"UTC"`, and it is load-bearing rather than tidy: every definition written
704
- * before this field parses to it and therefore keeps firing exactly when it did. A default of
705
- * "whatever the writer's browser says" would silently move every existing schedule at the next save.
706
- *
707
- * The name is validated against this runtime's own tz database rather than a pattern. A regular
708
- * expression would accept `Mars/Olympus`, and the failure would surface inside a Durable Object
709
- * alarm — the place where nobody is watching.
710
- */
711
- const IanaTimezone = z
712
- .string()
713
- .trim()
714
- .min(1)
715
- .max(64)
716
- .refine((zone) => {
717
- try {
718
- new Intl.DateTimeFormat("en-US", { timeZone: zone });
719
- return true;
720
- }
721
- catch {
722
- return false;
723
- }
724
- }, { message: "must be an IANA timezone name this runtime knows, for example Europe/Berlin" });
725
- export const AgentSchedule = z.strictObject({
726
- cron: z.string().trim().min(1).max(120),
727
- timezone: IanaTimezone.default("UTC"),
728
- target: AgentScheduleTarget,
729
- });
730
- export const AgentModel = z.strictObject({
731
- provider: z.enum(["workers-ai", "anthropic"]),
732
- model: z.string().trim().min(1).max(120),
733
- });
734
- /**
735
- * One MCP server as the portal names it. The handle is what the portal puts in front of every tool
736
- * that server offers (`notion_notion-search` belongs to `notion`), and it is the only identifier
737
- * Intel can both store and recognise again in a live `tools/list`.
738
- *
739
- * ⚠️ A handle is never invented from a tool name. Which servers exist is the portal's answer
740
- * (`portal_list_servers`), and the prefix is only used to attribute a tool to a server that answer
741
- * already named — see `packages/api/src/tools/tool-servers` for why splitting on the underscore
742
- * alone would be ambiguous.
743
- */
744
- export const ToolServerHandle = z
745
- .string()
746
- .trim()
747
- .min(1)
748
- .max(120)
749
- .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "A server handle is the portal's own identifier");
750
- const ToolServerHandles = z.array(ToolServerHandle).max(32).default([]);
751
- /**
752
- * What a caller may ASK for: whole MCP servers, and nothing about who delegates them (D30).
753
- *
754
- * ⚠️ The absence of `delegatedBy` is the point, and it is why the write shape differs from the read
755
- * shape at all. Intel writes that field from the session it authorized; a caller who could name
756
- * somebody else would be handing an agent a portal connection they do not have, and the agent would
757
- * act on it unattended. Leaving the field out of the input makes that structural instead of a
758
- * runtime overwrite: a body carrying it is a parse error at the boundary, on every surface, and no
759
- * screen ever has to invent a value it has no business knowing.
760
- */
761
- export const AgentToolSelection = z.strictObject({ servers: ToolServerHandles });
762
- /**
763
- * What is STORED and read back: the selection plus whose portal connection it came from (D30).
764
- *
765
- * ⚠️ This is a selection, not a permission. Nothing here grants anything: whether a server is
766
- * reachable is still decided by one live `tools/list` with `delegatedBy`'s own portal token, so a
767
- * delegator who loses the server or the connection takes it away from the agent at the next run
768
- * with no edit to this document.
769
- *
770
- * ⚠️ Read shape only. It appears in `AgentDefinition` and never in an input — see
771
- * `AgentToolSelection` for why the two are deliberately different documents rather than one schema
772
- * with an optional field.
773
- */
774
- export const AgentToolDelegation = z.strictObject({
775
- delegatedBy: IntelId,
776
- servers: ToolServerHandles,
777
- });
778
- /**
779
- * ⚠️ No accounts, no secrets and no channels — and the reason is mechanical rather than tidy
780
- * (ADR-0005 §4): this body is read, shared, exported and put into model context, so a secret in it
781
- * is a secret in a citation. Identity is Gate's, accounts are the portal's, channels are runtime
782
- * configuration.
783
- *
784
- * ⚠️ `tools` is the one correction to that list (D30), and it is narrower than it looks. What is
785
- * stored is a **selection of whole servers plus who delegated them**, never a mirrored permission
786
- * and never a catalog: the catalog stays a live `tools/list` made with the delegator's token at the
787
- * moment the agent runs. ADR-0005 §4's "no tools in the definition" forbade the mirror, and the
788
- * mirror is still forbidden — a tool name, a schema or an account in here would be the thing that
789
- * line was written against.
790
- *
791
- * ⚠️ Strict on purpose, and deliberately stricter than the runtime's own reader
792
- * (`packages/agent/src/definition/definition.ts`, which is `z.object`). Intel is the writer: an
793
- * unknown field here is a caller's mistake and is refused at the boundary. The runtime is the
794
- * reader and released separately, so it must keep starting agents when Intel adds a field
795
- * tomorrow. The asymmetry is the point, not an oversight.
796
- */
797
- const AgentBody = {
798
- references: z.array(AgentReference).max(200).default([]),
799
- schedules: z.array(AgentSchedule).max(50).default([]),
800
- model: AgentModel,
801
- };
802
- export const AgentDefinition = z.strictObject({
803
- ...AgentBody,
804
- // `null` is "this agent has no tools", and it is also what every definition written before D30
805
- // parses to. An empty `servers` list means the same thing and is kept as its own state so
806
- // removing the last server does not have to erase who was delegating.
807
- tools: AgentToolDelegation.nullable().default(null),
808
- });
809
- /**
810
- * The same document as `AgentDefinition`, minus the one field a caller may not write.
811
- *
812
- * ⚠️ Two schemas rather than one, and the split is load-bearing (#208, D30). Everything an agent IS
813
- * comes from whoever edits it; **whose portal connection it acts on** does not, because that is an
814
- * authority the editor would be granting to themselves. So the write shape simply has no place to
815
- * put it: `{ tools: { servers: [...] } }` is what a screen or an MCP client sends, Intel adds
816
- * `delegatedBy` from the session, and a body that tries to name one is refused by the strict object
817
- * before any of it is read. The reading shape keeps the field because a reader must be able to see
818
- * whose connection an agent runs on.
819
- */
820
- export const AgentDefinitionInput = z.strictObject({
821
- ...AgentBody,
822
- tools: AgentToolSelection.nullable().default(null),
823
- });
824
- export const SaveAgentDefinitionInput = z.strictObject({
825
- nodeId: IntelId,
826
- baseVersionId: IntelId.nullable(),
827
- definition: AgentDefinitionInput,
828
- idempotencyKey: z.string().min(8).max(200),
829
- });
830
- export const GetAgentInput = z.strictObject({ nodeId: IntelId });
831
- // Switching an agent off and on again, and starting one run by hand. All three name only the agent
832
- // and — for a run — which of the targets it already schedules.
833
- //
834
- // ⚠️ Intel holds none of this. Whether an agent is paused is state of its Durable Object, not a
835
- // field of the definition: a definition is versioned, shared and read into model context (ADR-0005
836
- // §4), so every pause would otherwise be a new version and would tell the agent it is switched off.
837
- // These inputs are what Intel accepts and passes on, nothing that Intel stores.
838
- export const PauseAgentInput = z.strictObject({ nodeId: IntelId });
839
- export const RunAgentNowInput = z.strictObject({
840
- nodeId: IntelId,
841
- target: AgentScheduleTarget,
842
- });
843
- /**
844
- * What one agent has actually cost, read out of Cloudflare's AI Gateway log (#251).
845
- *
846
- * ⚠️ Intel computes none of this from tokens and a price table. The gateway publishes the billed
847
- * figure per call, and that figure is the debit from the Cloudflare balance 1:1 — Cloudflare takes
848
- * its 5 % when the balance is loaded and passes inference through unchanged (measured 2026-08-07).
849
- * A second, self-maintained answer beside it would be wrong on the day the two disagreed, and the
850
- * wrong one would be the one on screen.
851
- *
852
- * ⚠️ `status` travels with the numbers and may never be dropped. `runs: []` means "cost nothing"
853
- * only when `status` is `read`; under `not_configured` or `unreadable` it means "not known", and a
854
- * screen that renders the two alike reports an outage as a saving.
855
- */
856
- export const AgentCostStatus = z.enum(["read", "not_configured", "unreadable"]);
857
- export const AgentRunCost = z.strictObject({
858
- runId: z.string(),
859
- cost: z.number(),
860
- calls: z.number(),
861
- });
862
- export const AgentCostWindow = z.strictObject({
863
- days: z.number(),
864
- cost: z.number(),
865
- calls: z.number(),
866
- // Which models produced this figure. It is here so the model select can say the number is about
867
- // the PAST (#257) — a reader who switched model would otherwise take it for a forecast.
868
- models: z.array(z.string()),
869
- });
870
- export const AgentCosts = z.strictObject({
871
- status: AgentCostStatus,
872
- currency: z.literal("USD"),
873
- runs: z.array(AgentRunCost),
874
- windows: z.array(AgentCostWindow),
875
- // The read hit its page limit, so every total above is a floor rather than a total.
876
- partial: z.boolean(),
877
- });
878
- /**
879
- * What the models on offer cost and how much they hold (#257).
880
- *
881
- * ⚠️ `source` is per ENTRY and not per response, and that is not over-engineering. Cloudflare
882
- * publishes figures for the models it serves itself and none at all for the Anthropic models it
883
- * resells through Unified Billing — so a perfectly healthy read still leaves half the list on a
884
- * written-out table, and one flag for the whole answer would call either the read stale or the
885
- * table live.
886
- */
887
- export const ModelPrice = z.strictObject({
888
- inputPerMillion: z.number(),
889
- outputPerMillion: z.number(),
890
- });
891
- export const ModelCatalogEntry = z.strictObject({
892
- provider: z.enum(["workers-ai", "anthropic"]),
893
- model: z.string(),
894
- name: z.string(),
895
- contextTokens: z.number().nullable(),
896
- // `null` where this installation has no figure. Never zero and never a guess — an invented number
897
- // is a false statement about money.
898
- price: ModelPrice.nullable(),
899
- source: z.enum(["cloudflare", "builtin"]),
900
- });
901
- export const ModelCatalog = z.strictObject({
902
- entries: z.array(ModelCatalogEntry),
903
- liveStatus: z.enum(["read", "not_configured", "unreadable"]),
904
- });
905
- // ⚠️ Three states, not two, and the same three the flow list makes: omitted is the whole tree,
906
- // `null` is the root level, an ID is that folder. "Which agents may I use" is a question about the
907
- // tree rather than about one folder, so the useful answer has to be reachable without knowing where
908
- // somebody filed them.
909
- export const ListAgentsInput = z.strictObject({
910
- parentId: IntelId.nullable().optional(),
911
- includeArchived: z.boolean().default(false),
912
- });
913
- export const CreateAgentInput = z.strictObject({
914
- parentId: IntelId.nullable().default(null),
915
- title: z.string().trim().min(1).max(240),
916
- description: z.string().trim().max(2_000).nullable().default(null),
917
- definition: AgentDefinitionInput,
918
- idempotencyKey: z.string().min(8).max(200),
919
- });
920
- // The ID of the Gate Application an agent runs as. Deliberately NOT an `IntelId`: it is Better
921
- // Auth's user ID, minted in Gate and only ever handed back to Gate, so validating it against
922
- // Intel's own ID shape would be Intel inventing a rule about somebody else's identifier.
923
- export const GateApplicationId = z.string().min(1).max(255);
924
- // The definition is `null` exactly while the node exists and no version has been written yet — the
925
- // same window in which a document's content is `null`.
926
- //
927
- // ⚠️ `applicationId` names the machine principal, it does not authenticate it (#182, D27). That is
928
- // why the ID may be stored, listed and drawn while the key may not: one is a name, the other is the
929
- // credential, and Gate hands the credential out exactly once and keeps only its hash. `null` means
930
- // this agent has no Application — an agent node written before #182, restored from a bundle, or
931
- // imported from another installation. Such an agent is not switched with its node, and giving it a
932
- // principal is an operator's act in Gate.
933
- export const NodeAgent = z.strictObject({
934
- node: Node,
935
- version: NodeVersion.nullable(),
936
- definition: AgentDefinition.nullable(),
937
- applicationId: GateApplicationId.nullable(),
938
- });
939
- /**
940
- * ⚠️ There is NO key field in this file, and adding one back would be the regression (D29, #207).
941
- *
942
- * Until #207 the create answer carried the Application key in plain text, once, and a person had to
943
- * carry it into a Worker secret by hand — which is why an agent created through the screen could
944
- * never run (#200). The key now goes from Gate straight into the agent runtime over Intel's service
945
- * binding and is encrypted into that agent's Durable Object; it reaches no browser, no MCP tool
946
- * result and no response body at all. `NodeAgent` is a `z.strictObject`, so a field named `key`
947
- * added anywhere in this file is a parse error at the boundary rather than a leak somebody has to
948
- * spot in review.
949
- *
950
- * What `POST /nodes/agents` and `agent_create` answer is therefore exactly what every read answers:
951
- * the node, its first definition, and the `applicationId` that NAMES the principal without
952
- * authenticating it.
953
- */
954
- export const CreatedAgent = NodeAgent;
955
- // Which agent's key is being replaced. `nodeId` and not the Application ID: this addresses an agent
956
- // in Intel's tree, and the Application behind it is Intel's to look up — a caller naming the
957
- // principal directly would be rotating a key for an agent nobody checked they may edit.
958
- export const RotateAgentKeyInput = z.strictObject({ nodeId: IntelId });
959
- /**
960
- * What replacing an agent's key answers.
961
- *
962
- * ⚠️ No key, and that is the whole shape of D29: Intel asks Gate for a new one, hands it to the
963
- * runtime over the service binding, and forgets it inside the same call. What the caller gets is
964
- * the fact that it happened, so a screen can say so — `applicationId` names the principal whose key
965
- * was replaced, which is a name and not a credential.
966
- */
967
- export const AgentKeyRotated = z.strictObject({
968
- nodeId: IntelId,
969
- applicationId: GateApplicationId,
970
- rotatedAt: IsoDateTime,
971
- });
972
- export const AgentList = z.strictObject({ items: z.array(Node) });
973
- // ⚠️ Kept for what is already stored, not for what is written. Relations were picked in a dialog
974
- // until #41; a link is now made where it is meant — in the text — and every link written from now
975
- // on is a `references`. Rewriting the old rows would destroy a distinction somebody chose on
976
- // purpose, and dropping the column would destroy it with them, so both stay readable.
977
- export const NodeLinkRelation = z.enum(["references", "related", "depends_on", "implements"]);
978
- // Where the link came from. `text` links are derived from a document's content and are rewritten
979
- // whenever it is saved; `manual` links were made in the dialog #41 removed and are now history.
980
- //
981
- // ⚠️ This is provenance, never a second sort of relationship. Nothing offers the reader a choice
982
- // between them, and nothing may start writing `manual` again — that would be the two ways of saying
983
- // one thing that #41 exists to end. It exists so that saving a document cannot delete a link
984
- // somebody made before there was another way to make one.
985
- export const NodeLinkOrigin = z.enum(["text", "manual"]);
986
- export const NodeLink = z.strictObject({
987
- id: IntelId,
988
- sourceNodeId: IntelId,
989
- targetNodeId: IntelId,
990
- relation: NodeLinkRelation,
991
- origin: NodeLinkOrigin,
992
- label: z.string().trim().min(1).max(120).nullable(),
993
- createdBy: IntelId,
994
- createdAt: IsoDateTime,
995
- });
996
- export const NodeLinkList = z.strictObject({ items: z.array(NodeLink) });
997
- // The inline element a document link is, inside a BlockNote document (#41).
998
- //
999
- // ⚠️ The ID and nothing else. No title and no path travel with it: a stored title would go stale
1000
- // the moment the target is renamed, a stored path the moment it is moved — and either one would
1001
- // put a name the reader may not see into a document they may.
1002
- export const DocumentLinkInlineType = "documentLink";
1003
- export const ResolveNodeLinksInput = z.strictObject({
1004
- nodeIds: z.array(IntelId).min(1).max(200),
1005
- });
1006
- export const ResolvedNodeLink = z.strictObject({
1007
- nodeId: IntelId,
1008
- title: z.string().min(1).max(240),
1009
- });
1010
- // ⚠️ Only what the asking reader may see is in here, and an unreachable target is simply absent —
1011
- // never a row with an empty title, never a count, never a "restricted" marker. The list is what one
1012
- // side of a document link is drawn from, and an entry that says "something is here" is exactly the
1013
- // leak this schema has to make impossible to write by accident. Deleted and unreadable therefore
1014
- // look identical from the outside, which is the point.
1015
- export const ResolveNodeLinksResult = z.strictObject({
1016
- items: z.array(ResolvedNodeLink),
1017
- });
1018
- export const NodeGraphInput = z.strictObject({
1019
- limit: z.number().int().min(1).max(500).default(250),
1020
- });
1021
- export const NodeGraph = z.strictObject({
1022
- nodes: z.array(Node),
1023
- links: z.array(NodeLink),
1024
- });
1025
- export const BlockNoteMediaType = "application/vnd.anchrd.intel.blocknote+json";
1026
- export const BlockNoteDocument = z.strictObject({
1027
- format: z.literal("blocknote"),
1028
- schemaVersion: z.literal(1),
1029
- blocks: z.array(z.record(z.string(), z.unknown())),
1030
- markdown: z.string(),
1031
- });
1032
- export const ResourceGrant = z.strictObject({
1033
- id: IntelId,
1034
- resourceId: IntelId,
1035
- principal: SharePrincipal,
1036
- verb: ResourceVerb,
1037
- expiresAt: IsoDateTime.nullable(),
1038
- createdBy: IntelId,
1039
- createdAt: IsoDateTime,
1040
- });
1041
- export const ShareInput = z.strictObject({
1042
- resourceId: IntelId,
1043
- principal: SharePrincipal,
1044
- verb: ResourceVerb,
1045
- expiresAt: IsoDateTime.nullable().default(null),
1046
- idempotencyKey: z.string().min(8).max(200),
1047
- });
1048
- export const RevokeGrantInput = z.strictObject({
1049
- resourceId: IntelId,
1050
- grantId: IntelId,
1051
- idempotencyKey: z.string().min(8).max(200),
1052
- });
1053
- // What a grant does not cover, reported to whoever just made it. A flow in the shared folder may
1054
- // read a document outside it, and the run is re-authorized against the person running it — so the
1055
- // grant can be complete and the flow still stop for them (ADR-0004 §4).
1056
- //
1057
- // ⚠️ `titles` holds only the documents the sharer may see; everything else is in `hidden` as a
1058
- // number. A warning must not become a way of reading titles out of the tree.
1059
- export const UnreadableNodes = z.strictObject({
1060
- titles: z.array(z.string().min(1).max(240)),
1061
- hidden: z.number().int().nonnegative(),
1062
- });
1063
- // The grant is in the answer, so the warning cannot be mistaken for a refusal: it is written first
1064
- // and described afterwards. Blocking would force everyone who uses a central policy document to
1065
- // duplicate it, which is the opposite of what one tree is for (ADR-0004 §4).
1066
- export const ShareResult = z.strictObject({
1067
- grant: ResourceGrant,
1068
- unreadable: UnreadableNodes,
1069
- });
1070
- // `applicableVerbs` travels with the list because the answer is the business layer's, not the
1071
- // screen's: a document has nothing to execute, so `execute` is not offered on one (ADR-0004 §2).
1072
- export const ResourceGrantList = z.strictObject({
1073
- resourceId: IntelId,
1074
- applicableVerbs: z.array(ResourceVerb).min(1),
1075
- items: z.array(ResourceGrant),
1076
- });
1077
- // `scopeId` is a cut, never a grant (#126): it narrows an answer the actor is already entitled to
1078
- // and can only ever remove rows. Without it the search stays global over everything visible, which
1079
- // is why it is optional rather than nullable — an absent field and `null` would otherwise be two
1080
- // spellings of the same request.
1081
- export const SearchInput = z.strictObject({
1082
- query: z.string().trim().min(1).max(500),
1083
- limit: z.number().int().min(1).max(50).default(10),
1084
- scopeId: IntelId.optional().describe("Optional folder node id. When given, only nodes filed in that folder or beneath it are searched."),
1085
- });
1086
- export const NodeCitation = z.strictObject({
1087
- nodeId: IntelId,
1088
- versionId: IntelId,
1089
- title: z.string(),
1090
- passage: z.string(),
1091
- source: z.string(),
1092
- freshness: IsoDateTime,
1093
- score: z.number().min(0).max(1),
1094
- match: z.enum(["lexical", "semantic", "hybrid"]),
1095
- });
1096
- export const SearchResult = z.strictObject({
1097
- items: z.array(NodeCitation),
1098
- });
1099
- export const ReindexResult = z.strictObject({ queued: z.number().int().nonnegative() });
1100
- export const RevokeGrantResult = z.strictObject({ revoked: z.boolean() });
1101
- function isPrivateIpv4(hostname) {
1102
- const parts = hostname.split(".").map(Number);
1103
- if (parts.length !== 4 ||
1104
- parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
1105
- return false;
1106
- }
1107
- const [first = 0, second = 0] = parts;
1108
- return (first === 0 ||
1109
- first === 10 ||
1110
- first === 127 ||
1111
- (first === 100 && second >= 64 && second <= 127) ||
1112
- (first === 169 && second === 254) ||
1113
- (first === 172 && second >= 16 && second <= 31) ||
1114
- (first === 192 && second === 168) ||
1115
- (first === 198 && (second === 18 || second === 19)) ||
1116
- first >= 224);
1117
- }
1118
- function normalizedHostname(url) {
1119
- return url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
1120
- }
1121
- function isPublicToolHost(url) {
1122
- const hostname = normalizedHostname(url);
1123
- if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1") {
1124
- return url.protocol === "http:";
1125
- }
1126
- if (!hostname.includes(".") ||
1127
- hostname.endsWith(".local") ||
1128
- hostname.endsWith(".localhost") ||
1129
- hostname.endsWith(".internal") ||
1130
- isPrivateIpv4(hostname) ||
1131
- hostname.includes(":")) {
1132
- return false;
1133
- }
1134
- return url.protocol === "https:";
1135
- }
1136
- export const ToolSourceUrl = z.url().refine((value) => {
1137
- try {
1138
- const url = new URL(value);
1139
- return !url.username && !url.password && isPublicToolHost(url);
1140
- }
1141
- catch {
1142
- return false;
1143
- }
1144
- }, "The portal must use an approved public HTTPS host without embedded credentials");
1145
- // The portal namespaces every upstream tool, so the name alone identifies the target server. The
1146
- // portal is still the one that resolves it and attaches the credentials — Intel never holds an
1147
- // upstream credential. Since D30 Intel does read the namespace for one purpose: attributing a tool
1148
- // to a server the portal's own `portal_list_servers` already named, so a delegation can be cut to
1149
- // whole servers. That is attribution, not routing.
1150
- export const ToolName = z.string().min(1).max(240);
1151
- export const ToolAnnotations = z.strictObject({
1152
- title: z.string().max(240).optional(),
1153
- readOnlyHint: z.boolean().optional(),
1154
- destructiveHint: z.boolean().optional(),
1155
- idempotentHint: z.boolean().optional(),
1156
- openWorldHint: z.boolean().optional(),
1157
- });
1158
- export const ToolCapability = z.strictObject({
1159
- name: ToolName,
1160
- title: z.string().max(240).nullable(),
1161
- description: z.string().max(10_000).nullable(),
1162
- inputSchema: z.record(z.string(), z.unknown()),
1163
- outputSchema: z.record(z.string(), z.unknown()).nullable(),
1164
- annotations: ToolAnnotations,
1165
- fingerprint: z.string().regex(/^[a-f0-9]{64}$/),
1166
- });
1167
- // The catalog reflects one live tools/list for the requesting user. It is never stored as a
1168
- // permission mirror, so there is no per-source state and no Intel-owned connection status.
1169
- export const ToolCatalog = z.strictObject({
1170
- portalConnected: z.boolean(),
1171
- items: z.array(ToolCapability),
1172
- /**
1173
- * Which delegated servers actually contributed a tool to this catalog (#289).
1174
- *
1175
- * ⚠️ Present only where the attribution was actually made — a delegated caller whose catalog was
1176
- * read. It is absent for an ordinary user, and absent as well when the answer comes from one of
1177
- * the short paths that never reach the portal (nothing delegated, no portal sign-in, connection
1178
- * dropped). Absent therefore means "not stated", never "nothing arrived"; `[]` is the second one.
1179
- *
1180
- * That it is missing rather than empty on those paths is deliberate rather than half-finished:
1181
- * the attribution already happens for a delegation — `capabilities` has to make it to cut the
1182
- * list — so naming it costs nothing there, while computing the same thing for an ordinary user
1183
- * would mean a second portal request per call, for a question their screen does not ask.
1184
- *
1185
- * ⚠️ It is the answer to "what arrived", never to "what was granted". A server missing here has
1186
- * been switched off, revoked, or is failing right now; the delegation in the definition is
1187
- * unchanged. Reading it the other way round would turn an outage into a permission change.
1188
- */
1189
- reached: z.array(ToolServerHandle).optional(),
1190
- });
1191
- /**
1192
- * One MCP server the asking user reaches right now, as the portal itself names it (D30).
1193
- *
1194
- * ⚠️ `toolCount` is a fact about this moment and this user, not a size. It exists so a picker can
1195
- * say "9 tools" instead of showing a handle alone, and it must never be read as what an agent will
1196
- * get: the delegated run asks the portal again, with the delegator's token.
1197
- */
1198
- export const ToolServer = z.strictObject({
1199
- handle: ToolServerHandle,
1200
- name: z.string().min(1).max(240),
1201
- toolCount: z.number().int().min(0),
1202
- });
1203
- // The same live-query rule as the tool catalog, one level up. `portalConnected: false` is the state
1204
- // of somebody who has not signed into the portal yet, and it is not an error.
1205
- export const ToolServerCatalog = z.strictObject({
1206
- portalConnected: z.boolean(),
1207
- items: z.array(ToolServer),
1208
- });
1209
- /**
1210
- * Which of the named servers a tool belongs to, or `null` for none of them.
1211
- *
1212
- * ⚠️ THE TRAP: a tool name does not say where its server name ends.
1213
- *
1214
- * The portal writes `<server>_<tool>`, and both halves may contain underscores — `intel_flow_get`
1215
- * reads equally well as server `intel` with tool `flow_get` and as a server called `intel_flow`
1216
- * with tool `get`. Splitting on the first underscore is therefore a guess that is wrong the day
1217
- * somebody adds a server whose name contains one, and on the API side being wrong means an agent
1218
- * delegated server A quietly reaching server B.
1219
- *
1220
- * So the prefix is never split. It is only ever MATCHED against handles the portal itself named,
1221
- * and the longest match wins: with `intel` and `intel_flow` both declared, `intel_flow_get` belongs
1222
- * to `intel_flow`, which is the only reading in which both declarations stay true.
1223
- *
1224
- * ⚠️ This lives in the contract because HOW A NAME IS READ is a property of the wire, and both
1225
- * surfaces read the same wire: `packages/api` cuts a delegation with it, `packages/ui` groups the
1226
- * tools screen with it (#212). A second implementation in the browser would be the third answer to
1227
- * one question — the underscore rule has already been answered differently in two places once
1228
- * (#106, #107), and the copies disagreed. What deliberately stays OUT of here is everything about
1229
- * reach: which handles are declared, which are enabled, which may be delegated and which one owns
1230
- * the portal's own management tools are decisions with consequences, and they belong to
1231
- * `packages/api/src/tools/tool-servers`. This function only reads a name.
1232
- */
1233
- export function serverOf(toolName, handles) {
1234
- let best = null;
1235
- for (const handle of handles) {
1236
- if (!toolName.startsWith(`${handle}_`))
1237
- continue;
1238
- if (best === null || handle.length > best.length)
1239
- best = handle;
1240
- }
1241
- return best;
1242
- }
1243
- export const TestToolInput = z.strictObject({
1244
- name: ToolName,
1245
- arguments: z.record(z.string(), z.unknown()).default({}),
1246
- });
1247
- export const ExecuteToolInput = TestToolInput;
1248
- export const ToolTestResult = z.strictObject({
1249
- isError: z.boolean(),
1250
- content: z.array(z.unknown()),
1251
- structuredContent: z.unknown().optional(),
1252
- });
1253
- export const FlowNodeId = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/);
1254
- export const FlowPosition = z.strictObject({ x: z.number().finite(), y: z.number().finite() });
1255
- // A node has a title and nothing else to write in prose (#39). A second free text beside it was
1256
- // kept half up to date on both sides, and for an instruction the same sentence already belongs in
1257
- // the instruction itself.
1258
- const FlowNodeBase = {
1259
- id: FlowNodeId,
1260
- position: FlowPosition,
1261
- label: z.string().trim().min(1).max(160),
1262
- };
1263
- // Which version of the callee a sub-flow call takes (ADR-0004 §5). Three states rather than an
1264
- // optional identifier, because the difference between them is a decision and has to be readable:
1265
- //
1266
- // - `latest` the draft default. Nobody should have to version things while building, and
1267
- // publishing turns this into `pinned` — visibly, before the author publishes.
1268
- // - `follows` "always latest", chosen on purpose. The call rides along with the callee, so a
1269
- // change to the building block changes this flow too. Publishing leaves it alone.
1270
- // - `pinned` one immutable version, whatever is published elsewhere.
1271
- //
1272
- // ⚠️ Without the freeze a change to a building block would silently change the behavior of every
1273
- // published flow using it, which contradicts the immutable versions and pinned schema fingerprints
1274
- // Intel otherwise guarantees. That is why `latest` cannot survive publishing.
1275
- export const SubflowVersionMode = z.enum(["latest", "follows", "pinned"]);
1276
- export const SubflowVersion = z.discriminatedUnion("mode", [
1277
- z.strictObject({ mode: z.literal("latest") }),
1278
- z.strictObject({ mode: z.literal("follows") }),
1279
- z.strictObject({ mode: z.literal("pinned"), versionId: IntelId }),
1280
- ]);
1281
- // The three layers a node can belong to (D25). Which one a kind is in decides what it may carry and
1282
- // where it may sit, and both rules are enforced in `compileFlow` rather than only drawn in the
1283
- // editor — a graph arrives over MCP as readily as from the canvas.
1284
- //
1285
- // ⚠️ A LINK never stands in the chain. It hangs off a step on a `context` edge, and that is the
1286
- // distinction the graph has drawn since #37 without anyone enforcing it — which is exactly how a
1287
- // start with an attachment once began its run at the attachment (the "flow edges only" comments in
1288
- // `flows.ts`). Marker and step keep the chain; a link is what a step works with.
1289
- export const FlowNodeLayer = z.enum(["marker", "step", "link"]);
1290
- export const flowNodeLayer = {
1291
- trigger: "marker",
1292
- output: "marker",
1293
- instruction: "step",
1294
- condition: "step",
1295
- subflow: "step",
1296
- folder: "link",
1297
- document: "link",
1298
- upload: "link",
1299
- table: "link",
1300
- tool: "link",
1301
- };
1302
48
  // One reference, never a list. The old `knowledge` node carried up to a hundred, plus a retrieval
1303
49
  // mode and a query that nothing read — three things in one, and the last two describing a retrieval
1304
50
  // Intel does not perform (D24: the agent fetches, Intel does not put anything into a context).
1305
51
  // Splitting by kind is what makes "exactly one" sayable at all, and it lets the editor filter the
1306
52
  // picker by what the node is for.
1307
- const LinkConfiguration = z.strictObject({ resourceId: IntelId });
1308
- export const FlowNode = z.discriminatedUnion("kind", [
1309
- z.strictObject({
1310
- ...FlowNodeBase,
1311
- kind: z.literal("trigger"),
1312
- // ⚠️ `manual` is the only mode there is. `webhook` and `schedule` stood here and fired nothing:
1313
- // a flow is carried out by an external agent that brings its own schedule, which is what the
1314
- // code does rather than what it intends — `step()` hands back the current node, `completeStep`
1315
- // takes the result from outside, and the Cloudflare workflow waits rather than drives. #32 was
1316
- // closed on that basis, and a mode nothing triggers is a promise nobody keeps.
1317
- configuration: z.strictObject({ mode: z.literal("manual") }),
1318
- }),
1319
- z.strictObject({
1320
- ...FlowNodeBase,
1321
- kind: z.literal("instruction"),
1322
- configuration: z.strictObject({ prompt: z.string().min(1).max(50_000) }),
1323
- }),
1324
- // The four link kinds that name something in the shared tree. They are separate kinds rather than
1325
- // one with a `kind` field so the canvas, the palette and the picker can each say what they mean
1326
- // without reading into a configuration — and so a stored graph says it too.
1327
- z.strictObject({ ...FlowNodeBase, kind: z.literal("folder"), configuration: LinkConfiguration }),
1328
- z.strictObject({
1329
- ...FlowNodeBase,
1330
- kind: z.literal("document"),
1331
- configuration: LinkConfiguration,
1332
- }),
1333
- z.strictObject({ ...FlowNodeBase, kind: z.literal("upload"), configuration: LinkConfiguration }),
1334
- z.strictObject({ ...FlowNodeBase, kind: z.literal("table"), configuration: LinkConfiguration }),
1335
- z.strictObject({
1336
- ...FlowNodeBase,
1337
- kind: z.literal("tool"),
1338
- configuration: z.strictObject({
1339
- toolName: ToolName,
1340
- fingerprint: z
1341
- .string()
1342
- .regex(/^[a-f0-9]{64}$/)
1343
- .nullable()
1344
- .default(null),
1345
- arguments: z.record(z.string(), z.unknown()).default({}),
1346
- }),
1347
- }),
1348
- z.strictObject({
1349
- ...FlowNodeBase,
1350
- kind: z.literal("condition"),
1351
- configuration: z.strictObject({
1352
- mode: z.literal("semantic"),
1353
- instruction: z.string().min(1).max(10_000),
1354
- }),
1355
- }),
1356
- // ⚠️ There is no `approval` kind, and adding one back is a product decision rather than a schema
1357
- // addition (#73). It waited on a named person with a deadline, which needs a queue, a
1358
- // notification, a stand-in and an answer to "the deadline passed" — none of which exist. Without
1359
- // it, D24 holds without exception: nothing in Intel waits. A stored graph that still carries one
1360
- // is rewritten to a `condition` by migration 0007, because the question it asked is one the agent
1361
- // can answer.
1362
- //
1363
- // The seventh kind: one flow calls another (ADR-0004 §3). A schema addition, not hidden behavior
1364
- // in a generic code node — which flow is called has to be readable from the graph, or neither the
1365
- // publish-time call rule nor the sidebar could see it.
1366
- z.strictObject({
1367
- ...FlowNodeBase,
1368
- kind: z.literal("subflow"),
1369
- // What the called flow is given travels through `StartFlowRunInput.input` — the schema every run
1370
- // already uses. A second, static input here would be a promise the execution does not keep.
1371
- configuration: z.strictObject({
1372
- flowId: IntelId,
1373
- version: SubflowVersion.default({ mode: "latest" }),
1374
- }),
1375
- }),
1376
- // ⚠️ The end marks, it does not make. It used to carry a `template` that nothing ever read, and a
1377
- // node called "Result" that appeared to produce one is what everybody read it as. The result of a
1378
- // run is what the last step before it hands in (D25) — `completeStep` carries that forward.
1379
- z.strictObject({
1380
- ...FlowNodeBase,
1381
- kind: z.literal("output"),
1382
- configuration: z.strictObject({}),
1383
- }),
1384
- ]);
1385
- // The two things an edge can mean (#37). `flow` is the order of work — "and then". `context` is what
1386
- // a step works with: a document consulted in exactly this step, an approval obtained in exactly this
1387
- // step, or, at the output, the table a result is written to.
1388
- //
1389
- // ⚠️ `flow` is the default, and that is the whole of the migration: every edge stored before this
1390
- // existed parses into the meaning it already had. Nothing about saved graphs has to be rewritten.
1391
- export const FlowEdgeKind = z.enum(["flow", "context"]);
1392
- export const FlowEdge = z.strictObject({
1393
- id: FlowNodeId,
1394
- source: FlowNodeId,
1395
- target: FlowNodeId,
1396
- kind: FlowEdgeKind.default("flow"),
1397
- label: z.string().trim().min(1).max(120).nullable().default(null),
1398
- sourceHandle: z.string().trim().min(1).max(120).nullable().default(null),
1399
- });
1400
- export const FlowGraph = z.strictObject({
1401
- nodes: z.array(FlowNode).min(2).max(200),
1402
- edges: z.array(FlowEdge).min(1).max(500),
1403
- });
1404
- export const Flow = z.strictObject({
1405
- id: IntelId,
1406
- // The one thing a Flow shares with a document: its place in the shared folder tree (ADR-0004).
1407
- // Everything else stays apart — versions, R2 body and Vectorize belong to the document, the graph,
1408
- // runs and approvals to the flow. `null` is the root of that same tree.
1409
- parentId: IntelId.nullable(),
1410
- title: z.string().min(1).max(240),
1411
- description: z.string().max(2_000).nullable(),
1412
- ownerId: IntelId,
1413
- currentVersionId: IntelId.nullable(),
1414
- publishedVersionId: IntelId.nullable(),
1415
- createdAt: IsoDateTime,
1416
- updatedAt: IsoDateTime,
1417
- archivedAt: IsoDateTime.nullable(),
1418
- });
1419
- export const FlowVersion = z.strictObject({
1420
- id: IntelId,
1421
- flowId: IntelId,
1422
- sequence: z.number().int().positive(),
1423
- graph: FlowGraph,
1424
- createdBy: IntelId,
1425
- createdAt: IsoDateTime,
1426
- });
1427
- export const FlowDocument = z.strictObject({
1428
- flow: Flow,
1429
- version: FlowVersion.nullable(),
1430
- });
1431
- // One version as the history shows it: the metadata without the graph it carries. A flow's history
1432
- // is as long as its edits, and a list that shipped every graph would pay for drawings nobody asked
1433
- // for — whoever needs one asks for that one version.
1434
- export const FlowVersionSummary = z.strictObject({
1435
- id: IntelId,
1436
- flowId: IntelId,
1437
- sequence: z.number().int().positive(),
1438
- createdBy: IntelId,
1439
- createdAt: IsoDateTime,
1440
- // Whether this is the version the flow currently publishes. Derived from the flow row when the
1441
- // list is read, never stored on the version: a version is immutable and "published" is not a
1442
- // property of it — it is the flow's choice, revocable without touching the version.
1443
- published: z.boolean(),
1444
- });
1445
- export const FlowVersionList = z.strictObject({
1446
- flowId: IntelId,
1447
- items: z.array(FlowVersionSummary),
1448
- });
1449
- // Both identifiers, deliberately: a version ID alone would resolve whatever version carries it,
1450
- // whichever flow it belongs to, and the ACL is answered on the flow. The pair makes a foreign
1451
- // version a 404 rather than a read.
1452
- export const GetFlowVersionInput = z.strictObject({ flowId: IntelId, versionId: IntelId });
1453
- // The same for flows: which of them call another flow that this reader may also see (#59). An
1454
- // expand arrow on a flow whose calls are all hidden promises content that expanding it cannot
1455
- // deliver.
1456
- export const FlowList = z.strictObject({
1457
- items: z.array(Flow),
1458
- withCalls: z.array(IntelId).default([]),
1459
- });
1460
- export const ReferencedNode = z.strictObject({
1461
- id: IntelId,
1462
- title: z.string().min(1).max(240),
1463
- });
1464
- // What a flow touches: the documents its tree links name and the tools its Tool steps call,
1465
- // read straight out of the graph. Deliberately not a conflict report — there is no arithmetic here
1466
- // and nothing that can go stale, because the graph is the answer. Whether a given person may reach
1467
- // any of it is decided where it can be decided honestly: when the folder is shared, and at runtime
1468
- // (ADR-0004 §4). For tools it can only ever be the latter, because the catalog is a live query with
1469
- // the requesting user's own token (ADR-0003).
1470
- //
1471
- // ⚠️ `nodes` names only what the asking user may see. The rest is `hiddenNodes`, a count.
1472
- export const FlowRequirements = z.strictObject({
1473
- flowId: IntelId,
1474
- versionId: IntelId.nullable(),
1475
- nodes: z.array(ReferencedNode),
1476
- hiddenNodes: z.number().int().nonnegative(),
1477
- tools: z.array(ToolName),
1478
- });
1479
- // What stands between this flow and a run, asked on demand and answered for the person asking.
1480
- //
1481
- // ⚠️ A snapshot, and it says so. The tool catalog is a live query with the requesting user's own
1482
- // token (ADR-0003), so what is reachable now can be different tomorrow, and the same flow answers
1483
- // differently for two people. That is why this is a question one asks rather than a badge on the
1484
- // flow: a standing "this flow has conflicts" would be wrong for tools by construction (#72, D24).
1485
- export const FlowValidation = z.strictObject({
1486
- flowId: IntelId,
1487
- versionId: IntelId.nullable(),
1488
- // Empty means it would start now — for this person, at this moment.
1489
- problems: z.array(z.strictObject({
1490
- code: z.string().min(1).max(80),
1491
- detail: z.string().min(1).max(2_000),
1492
- })),
1493
- checkedAt: IsoDateTime,
1494
- });
1495
- export const CreateFlowInput = z.strictObject({
1496
- parentId: IntelId.nullable().default(null),
1497
- title: z.string().trim().min(1).max(240),
1498
- description: z.string().trim().max(2_000).nullable().default(null),
1499
- idempotencyKey: z.string().min(8).max(200),
1500
- });
1501
- // Renaming and moving a flow. Both are organization and nothing else: they touch no version, no
1502
- // published graph and no run, because organization has to stay free of consequence or nobody dares
1503
- // to reorganize (ADR-0004).
1504
- export const UpdateFlowInput = z
1505
- .strictObject({
1506
- flowId: IntelId,
1507
- baseUpdatedAt: IsoDateTime,
1508
- title: z.string().trim().min(1).max(240).optional(),
1509
- description: z.string().trim().max(2_000).nullable().optional(),
1510
- parentId: IntelId.nullable().optional(),
1511
- idempotencyKey: z.string().min(8).max(200),
1512
- })
1513
- .refine((input) => input.title !== undefined || input.description !== undefined || input.parentId !== undefined, { error: "At least one change is required" });
1514
- // Archiving a flow is the same shape as archiving a document, deliberately: `archived` is a boolean
1515
- // rather than a one-way verb, because an archive nothing returns from is a delete under a friendlier
1516
- // name. Restoring is the same call with `false`.
1517
- export const ArchiveFlowInput = z.strictObject({
1518
- flowId: IntelId,
1519
- baseUpdatedAt: IsoDateTime,
1520
- archived: z.boolean(),
1521
- idempotencyKey: z.string().min(8).max(200),
1522
- });
1523
- // Three answers, not two: an absent `parentId` lists every visible flow (the search dialog asks
1524
- // that), `null` lists the root of the shared tree and an ID lists one folder (the sidebar tree asks
1525
- // per level, which is what keeps the tree off the N+1 it used to load with).
1526
- export const ListFlowsInput = z.strictObject({
1527
- parentId: IntelId.nullable().optional(),
1528
- // The only way back to an archived flow, and the only place that asks for one: the bounded read
1529
- // behind the relation graph has no such flag on purpose (#30). A drawing that includes what was
1530
- // archived says the tidying up never happened.
1531
- //
1532
- // ⚠️ `.optional()` rather than `.default(false)`, unlike `ListNodesInput`. This schema is
1533
- // the argument type of `listFlows` on three layers, and a default makes the field required in the
1534
- // *parsed* type — every existing caller that lists a folder would have to spell out the answer to
1535
- // a question it is not asking. Absent means "without the archive" everywhere it is read.
1536
- includeArchived: z.boolean().optional(),
1537
- // The same question for flows, and the same override of `parentId` — see `ListNodesInput`.
1538
- archivedOnly: z.boolean().optional(),
1539
- });
1540
- export const GetFlowInput = z.strictObject({ flowId: IntelId });
1541
- export const SaveFlowVersionInput = z.strictObject({
1542
- flowId: IntelId,
1543
- baseVersionId: IntelId.nullable(),
1544
- graph: FlowGraph,
1545
- idempotencyKey: z.string().min(8).max(200),
1546
- });
1547
- export const PublishFlowInput = z.strictObject({
1548
- flowId: IntelId,
1549
- versionId: IntelId,
1550
- idempotencyKey: z.string().min(8).max(200),
1551
- });
1552
- // The way back out of a publication (#146). No versionId: what is withdrawn is whatever is
1553
- // published now, and naming one would invite a race between reading it and revoking it. Versions
1554
- // are untouched — republishing any of them is one `publish` away.
1555
- export const UnpublishFlowInput = z.strictObject({
1556
- flowId: IntelId,
1557
- idempotencyKey: z.string().min(8).max(200),
1558
- });
1559
- export const PreviewFlowPublishInput = z.strictObject({ flowId: IntelId, versionId: IntelId });
1560
- // One sub-flow call as publishing will leave it (ADR-0004 §5). `freezes` is the whole point of the
1561
- // preview: it marks the calls whose `latest` publishing turns into `versionId`, so the author reads
1562
- // the decision before making it rather than after.
1563
- //
1564
- // ⚠️ Only callees the asking actor may reach are listed at all. A call whose callee they cannot see
1565
- // is left out rather than named, because a title is the thing an unreachable flow must not hand out.
1566
- export const FlowPublishCall = z.strictObject({
1567
- nodeId: FlowNodeId,
1568
- nodeLabel: z.string().min(1).max(160),
1569
- calleeId: IntelId,
1570
- calleeTitle: z.string().min(1).max(240),
1571
- mode: SubflowVersionMode,
1572
- // The version this call will take once published. `null` when it follows the callee, which is the
1573
- // one case where the answer is only known at run time.
1574
- versionId: IntelId.nullable(),
1575
- versionSequence: z.number().int().positive().nullable(),
1576
- freezes: z.boolean(),
1577
- // The callee has a published version to be called at all. `false` is what publishing will refuse.
1578
- available: z.boolean(),
1579
- });
1580
- export const FlowPublishPreview = z.strictObject({
1581
- flowId: IntelId,
1582
- versionId: IntelId,
1583
- calls: z.array(FlowPublishCall),
1584
- });
1585
- // A flow has no share schema of its own. A grant sits on the folder a flow is filed in and inherits
1586
- // down from there (ADR-0004 §2); a narrower grant beside it would destroy the subtree guarantee
1587
- // section 3 rests on, so per-flow grants were removed rather than deprecated.
1588
- // What accesses what, for one level of the shared tree (#19). A folder answers it for its contents,
1589
- // a single flow for itself. Documents and flows are two kinds of thing that share one tree
1590
- // (ADR-0004 §1), so the graph carries both and says which of them it is.
1591
- export const RelationNodeKind = z.enum([
1592
- "folder",
1593
- "document",
1594
- "attachment",
1595
- "table",
1596
- "agent",
1597
- "board",
1598
- "flow",
1599
- ]);
1600
- export const RelationNode = z.strictObject({
1601
- id: IntelId,
1602
- kind: RelationNodeKind,
1603
- title: z.string().min(1).max(240),
1604
- // Inside the level being shown, rather than something it reaches out to. A flow reading a policy
1605
- // document from another folder pulls that document in, and the difference should be legible.
1606
- inScope: z.boolean(),
1607
- });
1608
- export const RelationEdge = z.strictObject({
1609
- id: z.string().min(1).max(400),
1610
- source: IntelId,
1611
- target: IntelId,
1612
- relation: z.enum(["reads", "calls"]),
1613
- });
1614
- export const RelationGraphScope = z.discriminatedUnion("of", [
1615
- z.strictObject({ of: z.literal("folder"), folderId: IntelId.nullable() }),
1616
- z.strictObject({ of: z.literal("flow"), flowId: IntelId }),
1617
- ]);
1618
- export const RelationGraphInput = z.strictObject({
1619
- scope: RelationGraphScope,
1620
- // How much is drawn before the answer is summarized instead. A big folder has to stay usable, and
1621
- // the cut-off is reported rather than swallowed.
1622
- limit: z.number().int().min(1).max(300).default(60),
1623
- });
1624
- // ⚠️ Only what the asking user may see is in here. A node they may not reach is absent, not greyed
1625
- // out and not counted: an edge to a placeholder would already tell them the thing exists, which is
1626
- // the leak this schema has to make impossible to write by accident. `omitted` is about the size
1627
- // limit alone, never about permissions.
1628
- export const RelationGraph = z.strictObject({
1629
- scope: RelationGraphScope,
1630
- nodes: z.array(RelationNode),
1631
- edges: z.array(RelationEdge),
1632
- omitted: z.number().int().nonnegative(),
1633
- limit: z.number().int().positive(),
1634
- });
1635
- // ⚠️ No `waiting`. It was written for the approval node and never set by anything — the two places
1636
- // that tested for it only ever saw `running` (#73). The column's CHECK constraint still allows the
1637
- // value, deliberately: rewriting it means rebuilding the table in D1 for a value nothing writes,
1638
- // and migration 0007 turns any row that somehow carries it into `failed` rather than leave a status
1639
- // the contract cannot parse.
1640
- export const FlowRunStatus = z.enum(["queued", "running", "completed", "failed", "cancelled"]);
1641
- export const FlowRun = z.strictObject({
1642
- id: IntelId,
1643
- flowId: IntelId,
1644
- versionId: IntelId,
1645
- status: FlowRunStatus,
1646
- currentNodeId: FlowNodeId.nullable(),
1647
- input: z.record(z.string(), z.unknown()),
1648
- output: z.unknown().nullable(),
1649
- error: z.string().max(2_000).nullable(),
1650
- initiatedBy: IntelId,
1651
- // The subflow node this run was called from, and the run that node belongs to. A called run is a
1652
- // run of its own: it has its own version, its own steps and its own authorization, and only these
1653
- // two fields say where its result goes back to.
1654
- parentRunId: IntelId.nullable().default(null),
1655
- parentNodeId: FlowNodeId.nullable().default(null),
1656
- createdAt: IsoDateTime,
1657
- updatedAt: IsoDateTime,
1658
- completedAt: IsoDateTime.nullable(),
1659
- });
1660
- // Which step of which flow is running, outermost caller first. Readable rather than reconstructed
1661
- // from `parentRunId` by whoever is looking (#17).
1662
- export const FlowRunTrailEntry = z.strictObject({
1663
- runId: IntelId,
1664
- flowId: IntelId,
1665
- flowTitle: z.string().min(1).max(240),
1666
- nodeId: FlowNodeId.nullable(),
1667
- nodeLabel: z.string().max(160).nullable(),
1668
- });
1669
- export const FlowRunStep = z.strictObject({
1670
- run: FlowRun,
1671
- node: FlowNode.nullable(),
1672
- trail: z.array(FlowRunTrailEntry).default([]),
1673
- });
1674
- export const StartFlowRunInput = z.strictObject({
1675
- flowId: IntelId,
1676
- input: z.record(z.string(), z.unknown()).default({}),
1677
- // Present when this run is the call a subflow node makes. It names a place, never a permission:
1678
- // the callee's `execute` is asked of the user exactly as it is for a run they start themselves,
1679
- // and the parent run must be the caller's own and standing on that very node.
1680
- parent: z.strictObject({ runId: IntelId, nodeId: FlowNodeId }).nullable().default(null),
1681
- idempotencyKey: z.string().min(8).max(200),
1682
- });
1683
- export const GetFlowRunInput = z.strictObject({ runId: IntelId });
1684
- // Ending a run on purpose (#145). Until this existed the only way off a parked manual step was
1685
- // `completeStep` with `outcome: "failed"` — which recorded a step failure that never happened.
1686
- // Cancelling records nothing into the step history: the run ends, the history stays true.
1687
- export const CancelFlowRunInput = z.strictObject({
1688
- runId: IntelId,
1689
- idempotencyKey: z.string().min(8).max(200),
1690
- });
1691
- export const CompleteFlowRunStepInput = z.strictObject({
1692
- runId: IntelId,
1693
- nodeId: FlowNodeId,
1694
- outcome: z.enum(["completed", "failed"]),
1695
- branch: z.string().min(1).max(120).nullable().default(null),
1696
- output: z.unknown().nullable().default(null),
1697
- error: z.string().max(2_000).nullable().default(null),
1698
- idempotencyKey: z.string().min(8).max(200),
1699
- });
1700
- // Why a run started. Derived when it is read and deliberately not a column: `subflow` when the run
1701
- // is the call another run made, otherwise the mode of the trigger node in the immutable version the
1702
- // run took. A stored copy would be a second answer that could disagree with the graph that ran.
1703
- //
1704
- // ⚠️ `webhook` and `schedule` stood here until #39 took them out of the trigger node. Being derived
1705
- // rather than stored is exactly what makes that safe: no run carries a trigger of its own, so once
1706
- // the 0005 migration has rewritten every stored trigger to `manual`, there is nowhere left for the
1707
- // old values to come from. Had this been a column, the enum would have had to keep reading them or
1708
- // every old run would have failed to parse the moment somebody opened the list.
1709
- export const FlowRunTrigger = z.enum(["manual", "subflow"]);
1710
- // Which step ended a run, and why, in the words the failure already used (#20).
1711
- //
1712
- // ⚠️ A call that failed carries its reason in the *called* run, and that run is a run of its own
1713
- // with its own authorization. `calledRunId` is therefore filled only when the asking user may see
1714
- // that run through the very rule every other run answer uses; otherwise the failure is named by the
1715
- // calling step alone — the caller's own label — and `detail` says no more than that it did not
1716
- // finish. Naming a callee's step or document here would be the leak #17, #19 and #20 each closed.
1717
- export const FlowRunFailure = z.strictObject({
1718
- nodeId: FlowNodeId,
1719
- nodeLabel: z.string().max(160),
1720
- detail: z.string().max(2_000),
1721
- calledRunId: IntelId.nullable(),
1722
- });
1723
- // One run as a list shows it: what it did, never what it produced.
1724
- //
1725
- // ⚠️ Neither `input` nor `output` is in here, on purpose. A run reaches its nodes and its tools
1726
- // with the rights of whoever started it, so its result is a way to content the next reader of this
1727
- // list may have no claim to. Whoever wants a result asks for the run itself, where the same rule
1728
- // decides again.
1729
- export const FlowRunSummary = z.strictObject({
1730
- id: IntelId,
1731
- flowId: IntelId,
1732
- // The version the run took. Together with the run's stored input it is what a later "run this
1733
- // again with the old data" would need; replaying is a separate ticket, this only keeps it possible.
1734
- versionId: IntelId,
1735
- status: FlowRunStatus,
1736
- trigger: FlowRunTrigger,
1737
- startedAt: IsoDateTime,
1738
- completedAt: IsoDateTime.nullable(),
1739
- durationMs: z.number().int().nonnegative().nullable(),
1740
- initiatedBy: IntelId,
1741
- parentRunId: IntelId.nullable(),
1742
- failure: FlowRunFailure.nullable(),
1743
- });
1744
- // One filter and nothing else: "only the failed ones" is the question asked in almost every case,
1745
- // and every further facet is a report rather than a search for a fault.
1746
- export const ListFlowRunsInput = z.strictObject({
1747
- flowId: IntelId,
1748
- failedOnly: z.boolean().default(false),
1749
- limit: z.number().int().min(1).max(50).default(20),
1750
- // The `nextCursor` of the previous page. Keyset rather than an offset, because runs arrive while
1751
- // someone reads and an offset would skip or repeat rows exactly when a flow is busy.
1752
- cursor: z.string().min(1).max(400).nullable().default(null),
1753
- });
1754
- export const FlowRunList = z.strictObject({
1755
- items: z.array(FlowRunSummary),
1756
- nextCursor: z.string().max(400).nullable(),
1757
- });
1758
- // One completed step of one run. `detail` is the step's own error text; an output is absent for the
1759
- // same reason it is absent from the summary.
1760
- export const FlowRunStepRecord = z.strictObject({
1761
- nodeId: FlowNodeId,
1762
- nodeLabel: z.string().max(160),
1763
- outcome: z.enum(["completed", "failed"]),
1764
- branch: z.string().max(120).nullable(),
1765
- detail: z.string().max(2_000).nullable(),
1766
- calledRunId: IntelId.nullable(),
1767
- completedAt: IsoDateTime,
1768
- });
1769
- // What one run did, step by step, oldest first, with the call chain it belongs to (#17). The trail
1770
- // is what makes a nested run readable: which step of which flow this run is.
1771
- export const FlowRunHistory = z.strictObject({
1772
- runId: IntelId,
1773
- flowId: IntelId,
1774
- status: FlowRunStatus,
1775
- steps: z.array(FlowRunStepRecord),
1776
- trail: z.array(FlowRunTrailEntry),
1777
- });
1778
- // ── Bundle export (#136) ────────────────────────────────────────────────────────────────────────
1779
- // The one name the importer looks for at the zip root. A different spelling would make a bundle a
1780
- // naked folder, so the constant lives in the contract rather than in each surface.
1781
- export const BundleManifestFilename = "manifest.json";
1782
- // What a bundle entry can be. `flow` joins the six node kinds because a flow shares the folder
1783
- // tree without being a node (ADR-0004), and the bundle mirrors the tree, not the tables.
1784
- export const BundleEntryKind = z.enum([
1785
- "folder",
1786
- "document",
1787
- "table",
1788
- "attachment",
1789
- "agent",
1790
- "board",
1791
- "flow",
1792
- ]);
1793
- // One entry of the manifest: the identity a re-import needs, next to the relative path where the
1794
- // bytes sit in the zip. A folder carries no media type — it has no bytes.
1795
- export const BundleManifestEntry = z.strictObject({
1796
- id: IntelId,
1797
- kind: BundleEntryKind,
1798
- title: z.string().min(1).max(240),
1799
- description: z.string().max(2_000).nullable(),
1800
- mediaType: z.string().min(1).max(160).nullable(),
1801
- // Relative to the zip root, forward slashes, no leading slash. Folders end with a slash so an
1802
- // empty folder still has an address.
1803
- path: z.string().min(1).max(4_000),
1804
- });
1805
- // What an export leaves out on purpose, named so a bundle says it rather than a reader guessing:
1806
- // version history, grants/shares, flow runs, and archived nodes are not in any bundle (#136).
1807
- export const BundleExclusion = z.enum(["version-history", "grants", "flow-runs", "archived-nodes"]);
1808
- export const BundleManifest = z.strictObject({
1809
- version: z.literal(1),
1810
- exportedAt: IsoDateTime,
1811
- // The node the export started at; `null` is the root of the tree — the whole installation as the
1812
- // exporting caller may read it.
1813
- rootId: IntelId.nullable(),
1814
- entries: z.array(BundleManifestEntry),
1815
- excluded: z.array(BundleExclusion),
1816
- });
1817
- // ── Bundle import (#137) ────────────────────────────────────────────────────────────────────────
1818
- // What one import made. Import always creates new nodes — no merge, no overwrite, no restored IDs
1819
- // (#137, phase 1) — so the answer is counts and the new roots, never a diff. `replayed` marks the
1820
- // idempotent second answer to the same key: nothing was created twice.
1821
- export const BundleImportResult = z.strictObject({
1822
- nodes: z.number().int().nonnegative(),
1823
- flows: z.number().int().nonnegative(),
1824
- rootNodeIds: z.array(IntelId),
1825
- replayed: z.boolean(),
1826
- });
53
+ export const LinkConfiguration = z.strictObject({ resourceId: IntelId });