@anchrd/intel-contract 0.11.0 → 0.13.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.
@@ -28,9 +28,6 @@ export const SessionUser = z.strictObject({
28
28
  // (those stay behind each door, where `/session` deliberately does not carry them). `agentRuntime`
29
29
  // says whether an agent Worker is bound at all (#190): without it the UI offers no "New agent" and
30
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
31
  // The fourth kind is `table` (#40), the fifth is `agent` (#139) and the sixth is `board` (#285).
35
32
  // Each is a kind of node, not a kind of thing: it hangs in the same folder tree, inherits the same
36
33
  // folder grants, carries the same immutable versions and the same R2 body as a document
@@ -41,7 +38,7 @@ export const IntelCapabilities = z.strictObject({
41
38
  // or a policy in order to file it. The same holds for `board`: it is a file somebody may make, not
42
39
  // a place the tree grows a special corner for — which is exactly why a board is one node carrying
43
40
  // 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"]);
41
+ export const NodeKind = z.enum(["folder", "document", "attachment", "table"]);
45
42
  // ⚠️ There is no `ContextPolicy`, and it is not coming back in this shape (#76). It said whether a
46
43
  // document should be pinned into a context, be found by relevance, or be named explicitly — an
47
44
  // instruction to a retrieval Intel does not perform. Intel hands out references and the agent
@@ -268,412 +265,6 @@ export const RedefineTableInput = z.strictObject({
268
265
  }, { error: "A current column can fill only one new column" }),
269
266
  idempotencyKey: z.string().min(8).max(200),
270
267
  });
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
- export const BoardTaskOrder = z.string().regex(/^[0-9A-Za-z]{1,64}$/);
341
- export const BoardTaskId = IntelId;
342
- export const BoardTaskLabel = z.string().trim().min(1).max(60);
343
- // A day, not an instant. A task is due on a date; giving it a time zone would make the same task
344
- // due on two different days depending on who is looking at it.
345
- export const BoardTaskDate = z.iso.date();
346
- // Markdown, and capped: a task's description is a card, and what needs more than this is a document
347
- // the task can point at through `references`.
348
- export const BoardTaskDescription = z.string().max(20_000);
349
- /**
350
- * The tasks one task waits for (#285), each of them at most once (anchrd/intel#318).
351
- *
352
- * ⚠️ Board-internal only, enforced on the write path: a dependency on a task in another board would
353
- * hang this node on a file that can change without anyone here noticing. Across boards the link is
354
- * `references`, which points at the board NODE and lands in the link graph.
355
- *
356
- * ⚠️ Refused rather than folded together, the same shape as the status ids in `ConfigureBoardInput`.
357
- * A repeat carries no information — but that is a fact about the value, not about the answer: a
358
- * caller handed back a shorter list than it sent is told nothing, and composes the same one again.
359
- * The refusal names the mistake once, and the caller is holding the list it has to fix. (The one
360
- * place a repeat is folded instead is `upgradeStoredBoard`, where there is no caller to tell.)
361
- *
362
- * ⚠️ It sits on the STORED task as well as on the two inputs, so a consumer may rely on it rather
363
- * than defend against it — `createBoardGraph` mints one edge key per pair, and a second one threw
364
- * the whole graph view off the screen for everybody looking at that board.
365
- */
366
- export const BoardTaskDependsOn = z
367
- .array(BoardTaskId)
368
- .max(64)
369
- .refine((ids) => new Set(ids).size === ids.length, {
370
- error: "A task can be named only once in dependsOn",
371
- });
372
- /**
373
- * A task's labels, each of them at most once (anchrd/intel#318).
374
- *
375
- * ⚠️ The same rule the detail panel has always applied to what a person types — it refuses to add a
376
- * label the task already carries — stated where every surface meets it, because the MCP write path
377
- * did not. A repeated label draws the same chip twice on the card, with two remove buttons of which
378
- * either takes both away, and weights that word higher in the search text (`indexing.ts`).
379
- */
380
- export const BoardTaskLabels = z
381
- .array(BoardTaskLabel)
382
- .max(32)
383
- .refine((labels) => new Set(labels).size === labels.length, {
384
- error: "A label can be named only once",
385
- });
386
- /**
387
- * The Intel nodes a task points at, each of them at most once (anchrd/intel#318).
388
- *
389
- * They land in the link graph as `text` links, the same way a document's inline links do, so what a
390
- * board points at is visible from the other side too.
391
- *
392
- * ⚠️ Distinct for the same reason as `labels`: the picker in the detail panel already refuses one
393
- * the task holds, and the link graph counts a repeat once anyway (`ON CONFLICT DO NOTHING`), so a
394
- * duplicate is a second row in the panel and nothing else — which is exactly the kind of value that
395
- * has no reading and should not be storable.
396
- */
397
- export const BoardTaskReferences = z
398
- .array(IntelId)
399
- .max(64)
400
- .refine((ids) => new Set(ids).size === ids.length, {
401
- error: "A node can be referenced only once",
402
- });
403
- export const BoardTask = z.strictObject({
404
- id: BoardTaskId,
405
- title: z.string().trim().min(1).max(240),
406
- status: BoardStatusId,
407
- assignee: BoardAssignee.nullable(),
408
- labels: BoardTaskLabels,
409
- startDate: BoardTaskDate.nullable(),
410
- dueDate: BoardTaskDate.nullable(),
411
- // ⚠️ The whole hierarchy in one field, deliberately: epic, task and subtask are a DEPTH and not a
412
- // type (#285). A `kind` beside it would allow a subtask under nothing and an epic under an epic,
413
- // and every surface would then need its own opinion about which combinations mean anything.
414
- parentId: BoardTaskId.nullable(),
415
- dependsOn: BoardTaskDependsOn,
416
- order: BoardTaskOrder,
417
- description: BoardTaskDescription,
418
- references: BoardTaskReferences,
419
- });
420
- // The whole board, as it is stored and as it is read. There is no second representation to keep in
421
- // step with it — this document is the file.
422
- export const BoardDocument = z.strictObject({
423
- statuses: z.array(BoardStatus).min(1).max(32),
424
- tasks: z.array(BoardTask).max(5_000),
425
- });
426
- // How deep `parentId` may nest. Five is epic → task → subtask with room left over; without a bound
427
- // a chain of a thousand tasks would be a valid board that no view can draw and no walk can afford.
428
- export const BoardMaxTaskDepth = 5;
429
- export const GetBoardInput = z.strictObject({ nodeId: IntelId });
430
- // A board as it is read. `versionId` is `null` while nothing has been written yet — the same answer
431
- // a table gives before its header exists — and the document is then the defaults.
432
- export const NodeBoard = z.strictObject({
433
- node: Node,
434
- board: BoardDocument,
435
- versionId: IntelId.nullable(),
436
- });
437
- export const BoardStatusInput = z.strictObject({
438
- id: BoardStatusId,
439
- label: z.string().trim().min(1).max(60),
440
- /**
441
- * Whether this column means finished (anchrd/intel#311).
442
- *
443
- * ⚠️ Optional, and the absence is not the same as `false`. A caller who says nothing gets the
444
- * server's answer — `false` for an ordinary column, `true` for the shelf, which cannot be
445
- * anything else. Making it a required boolean would force every caller that only wanted to rename
446
- * a column to restate the whole board's notion of done, and getting one entry wrong there is a
447
- * silent change to what counts as blocked.
448
- */
449
- terminal: z.boolean().optional(),
450
- });
451
- // The status list, written whole and in the order it should be drawn — never a patch. Adding,
452
- // renaming and reordering are all this one call, and `archived` has to be in what it is given.
453
- export const ConfigureBoardInput = z.strictObject({
454
- nodeId: IntelId,
455
- statuses: z
456
- .array(BoardStatusInput)
457
- .min(1)
458
- .max(32)
459
- .refine((statuses) => new Set(statuses.map((status) => status.id)).size === statuses.length, {
460
- error: "Status ids must be distinct",
461
- })
462
- .refine((statuses) => statuses.some((status) => status.id === ArchivedBoardStatusId), {
463
- error: `The "${ArchivedBoardStatusId}" status cannot be removed`,
464
- })
465
- // ⚠️ Refused rather than corrected, the same way removing the shelf is refused. A task swept
466
- // onto `archived` is finished with, and a board that could declare the shelf non-terminal would
467
- // hold every archived task open as a blocker forever. Only an EXPLICIT `false` is refused —
468
- // saying nothing is fine and means the server's `true` (anchrd/intel#311).
469
- .refine((statuses) => statuses.find((status) => status.id === ArchivedBoardStatusId)?.terminal !== false, { error: `The "${ArchivedBoardStatusId}" status is always terminal` }),
470
- idempotencyKey: z.string().min(8).max(200),
471
- });
472
- /**
473
- * A new task (#285).
474
- *
475
- * ⚠️ No `baseVersionId`, on this and on every other task operation, and that absence is the
476
- * feature. A board is addressed by stable task id and never by position, so two agents touching two
477
- * different tasks cannot collide — demanding a base version would invent the `version_conflict`
478
- * that #285 exists to remove, and force every caller to read the whole board first.
479
- */
480
- export const AddBoardTaskInput = z.strictObject({
481
- nodeId: IntelId,
482
- title: z.string().trim().min(1).max(240),
483
- // Omitted means the first status that is not `archived`: a new task belongs on the board, not on
484
- // the shelf.
485
- status: BoardStatusId.optional(),
486
- assignee: BoardAssignee.nullable().default(null),
487
- labels: BoardTaskLabels.default([]),
488
- startDate: BoardTaskDate.nullable().default(null),
489
- dueDate: BoardTaskDate.nullable().default(null),
490
- parentId: BoardTaskId.nullable().default(null),
491
- dependsOn: BoardTaskDependsOn.default([]),
492
- description: BoardTaskDescription.default(""),
493
- references: BoardTaskReferences.default([]),
494
- // Where among its neighbours it goes. Both absent puts it last in its column.
495
- afterTaskId: BoardTaskId.nullable().default(null),
496
- beforeTaskId: BoardTaskId.nullable().default(null),
497
- idempotencyKey: z.string().min(8).max(200),
498
- });
499
- /**
500
- * What a task says about itself.
501
- *
502
- * ⚠️ Deliberately no `status`, no `parentId` and no `order`: where a task SITS is a move, and a
503
- * move is the operation that has to mint an order key and re-check the two cycle rules. Folding
504
- * both into one call would mean every field edit pays for those checks and every move could quietly
505
- * rewrite a description.
506
- */
507
- export const UpdateBoardTaskInput = z
508
- .strictObject({
509
- nodeId: IntelId,
510
- taskId: BoardTaskId,
511
- title: z.string().trim().min(1).max(240).optional(),
512
- assignee: BoardAssignee.nullable().optional(),
513
- labels: BoardTaskLabels.optional(),
514
- startDate: BoardTaskDate.nullable().optional(),
515
- dueDate: BoardTaskDate.nullable().optional(),
516
- dependsOn: BoardTaskDependsOn.optional(),
517
- description: BoardTaskDescription.optional(),
518
- references: BoardTaskReferences.optional(),
519
- idempotencyKey: z.string().min(8).max(200),
520
- })
521
- .refine((input) => input.title !== undefined ||
522
- input.assignee !== undefined ||
523
- input.labels !== undefined ||
524
- input.startDate !== undefined ||
525
- input.dueDate !== undefined ||
526
- input.dependsOn !== undefined ||
527
- input.description !== undefined ||
528
- input.references !== undefined, { error: "At least one change is required" });
529
- // Where a task sits: its column, its parent, its place among its neighbours. Archiving is this call
530
- // with `status: "archived"` — there is no separate verb, because it is not a separate act.
531
- export const MoveBoardTaskInput = z
532
- .strictObject({
533
- nodeId: IntelId,
534
- taskId: BoardTaskId,
535
- status: BoardStatusId.optional(),
536
- parentId: BoardTaskId.nullable().optional(),
537
- afterTaskId: BoardTaskId.nullable().default(null),
538
- beforeTaskId: BoardTaskId.nullable().default(null),
539
- idempotencyKey: z.string().min(8).max(200),
540
- })
541
- .refine((input) => input.status !== undefined ||
542
- input.parentId !== undefined ||
543
- input.afterTaskId !== null ||
544
- input.beforeTaskId !== null, { error: "A move needs a status, a parent or a neighbour" });
545
- // ⚠️ Deleting cascades to every descendant, and the count comes back so a surface can warn BEFORE
546
- // asking. See `DeleteBoardTaskResult`.
547
- export const DeleteBoardTaskInput = z.strictObject({
548
- nodeId: IntelId,
549
- taskId: BoardTaskId,
550
- idempotencyKey: z.string().min(8).max(200),
551
- });
552
- // The one task that was written, not the whole board: a board can hold thousands of tasks, and
553
- // answering a one-card edit with all of them would make every write pay for the read.
554
- export const BoardTaskResult = z.strictObject({
555
- node: Node,
556
- version: NodeVersion,
557
- task: BoardTask,
558
- });
559
- // `deleted` counts the task AND every descendant that went with it, so a caller can say what
560
- // happened rather than "done".
561
- export const DeleteBoardTaskResult = z.strictObject({
562
- node: Node,
563
- version: NodeVersion,
564
- deleted: z.number().int().positive(),
565
- });
566
- export const ConfigureBoardResult = z.strictObject({
567
- node: Node,
568
- version: NodeVersion,
569
- statuses: z.array(BoardStatus),
570
- });
571
- // ── The agent definition (#139, ADR-0005 §4) ─────────────────────────────────────────────────────
572
- //
573
- // An agent's body is a definition, stored as an immutable version in R2 exactly like a document's.
574
- // Its own media type exists so a reader can tell a definition from prose without parsing it.
575
- export const AgentMediaType = "application/vnd.anchrd.agent+json";
576
- // ⚠️ The role lives on the AGENT, never on the node it names, and that is the whole difference to
577
- // the removed `context_policy` (ADR-0005 §2, #76). The same folder can be the system message for
578
- // one agent and nothing but search space for another; a node has no opinion about how it is used.
579
- // Any future field on a node saying how it should be loaded is `context_policy` under a new name.
580
- //
581
- // system-message prepended verbatim by the runtime
582
- // semantic-context search space; the agent searches it when it decides to
583
- // memory write target — ordinary Knowledge, versioned and readable like everything else
584
- export const AgentReferenceRole = z.enum(["system-message", "semantic-context", "memory"]);
585
- export const AgentReference = z.strictObject({ nodeId: IntelId, role: AgentReferenceRole });
586
- /**
587
- * Which kinds of node each role can actually be given (#255).
588
- *
589
- * ⚠️ Not every role takes every kind, and the reasons are about what the runtime DOES with a
590
- * reference rather than about tidiness:
591
- *
592
- * `memory` is a folder because the agent WRITES there — `agent_remember` creates a note inside
593
- * it. A single document as memory would mean the agent overwrites the document it was given.
594
- *
595
- * `semantic-context` is a folder because it is a search SPACE, searched per folder by
596
- * `loop/scoped-search`. A single document is not a narrower search space; reading it whole is a
597
- * different behaviour, and one that gets named before it is introduced, not slipped in.
598
- *
599
- * `system-message` reads single nodes already and takes a document or a table as well as a
600
- * folder. A document is the natural case — a skill somebody wrote as ordinary text — and a table
601
- * is the same read: the runtime asks intel for the node and prepends its content, which for a
602
- * table is its CSV.
603
- *
604
- * ⚠️ `folder` stays on `system-message` although a folder carries no content of its own. Every
605
- * definition written before #255 could only name folders, and taking the combination away here
606
- * would refuse the next save of an agent that has been working for months — "existing definitions
607
- * stay valid" is not only about reading them.
608
- *
609
- * ⚠️ This is the ONE place the rule lives. The screen offers what it says and the write path
610
- * refuses what it forbids; a surface that made its own list would eventually disagree with the
611
- * other, and the one that matters is whichever runs last.
612
- */
613
- export const AgentReferenceKinds = {
614
- "system-message": ["folder", "document", "table"],
615
- "semantic-context": ["folder"],
616
- memory: ["folder"],
617
- };
618
- export function agentReferenceAccepts(role, kind) {
619
- return AgentReferenceKinds[role].includes(kind);
620
- }
621
- /** The roles a node of this kind may be given — the same rule, read from the other side. */
622
- export function agentReferenceRolesFor(kind) {
623
- return AgentReferenceRole.options.filter((role) => agentReferenceAccepts(role, kind));
624
- }
625
- // A `document` target means the content of that document is the instruction — a "skill" somebody
626
- // wrote as ordinary text; a `flow` target means a run is started through Intel MCP and worked step
627
- // by step. Both are references, so nothing in here goes stale (ADR-0005 §4).
628
- //
629
- // ⚠️ Intel stores a schedule as a declared fact and never fires it. The alarm lives in the runtime
630
- // (ADR-0005 §3); Intel gains no scheduler, which is D24 confirmed rather than bent.
631
- export const AgentScheduleTarget = z.strictObject({
632
- kind: z.enum(["document", "flow"]),
633
- id: IntelId,
634
- });
635
- /**
636
- * ⚠️ `timezone` is what the cron expression is READ IN, and it belongs to the schedule rather than
637
- * to whoever is looking at it (#228). "Every morning at eight" means eight o'clock where the person
638
- * who wrote it sits — in Berlin that is 06:00 UTC in summer and 07:00 in winter, and a field that
639
- * does not carry the zone cannot express that difference. A UTC cron is an hour wrong twice a year
640
- * and nobody sees why.
641
- *
642
- * The UI suggests the reader's own zone when a schedule is created, but it is not a per-user
643
- * setting: an agent's schedule would otherwise move whenever its owner travelled, and it would mean
644
- * different times to two people reading the same definition. What is stored is the answer.
645
- *
646
- * ⚠️ The default is `"UTC"`, and it is load-bearing rather than tidy: every definition written
647
- * before this field parses to it and therefore keeps firing exactly when it did. A default of
648
- * "whatever the writer's browser says" would silently move every existing schedule at the next save.
649
- *
650
- * The name is validated against this runtime's own tz database rather than a pattern. A regular
651
- * expression would accept `Mars/Olympus`, and the failure would surface inside a Durable Object
652
- * alarm — the place where nobody is watching.
653
- */
654
- const IanaTimezone = z
655
- .string()
656
- .trim()
657
- .min(1)
658
- .max(64)
659
- .refine((zone) => {
660
- try {
661
- new Intl.DateTimeFormat("en-US", { timeZone: zone });
662
- return true;
663
- }
664
- catch {
665
- return false;
666
- }
667
- }, { message: "must be an IANA timezone name this runtime knows, for example Europe/Berlin" });
668
- export const AgentSchedule = z.strictObject({
669
- cron: z.string().trim().min(1).max(120),
670
- timezone: IanaTimezone.default("UTC"),
671
- target: AgentScheduleTarget,
672
- });
673
- export const AgentModel = z.strictObject({
674
- provider: z.enum(["workers-ai", "anthropic"]),
675
- model: z.string().trim().min(1).max(120),
676
- });
677
268
  /**
678
269
  * One MCP server as the portal names it. The handle is what the portal puts in front of every tool
679
270
  * that server offers (`notion_notion-search` belongs to `notion`), and it is the only identifier
@@ -690,233 +281,6 @@ export const ToolServerHandle = z
690
281
  .min(1)
691
282
  .max(120)
692
283
  .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "A server handle is the portal's own identifier");
693
- const ToolServerHandles = z.array(ToolServerHandle).max(32).default([]);
694
- /**
695
- * What a caller may ASK for: whole MCP servers, and nothing about who delegates them (D30).
696
- *
697
- * ⚠️ The absence of `delegatedBy` is the point, and it is why the write shape differs from the read
698
- * shape at all. Intel writes that field from the session it authorized; a caller who could name
699
- * somebody else would be handing an agent a portal connection they do not have, and the agent would
700
- * act on it unattended. Leaving the field out of the input makes that structural instead of a
701
- * runtime overwrite: a body carrying it is a parse error at the boundary, on every surface, and no
702
- * screen ever has to invent a value it has no business knowing.
703
- */
704
- export const AgentToolSelection = z.strictObject({ servers: ToolServerHandles });
705
- /**
706
- * What is STORED and read back: the selection plus whose portal connection it came from (D30).
707
- *
708
- * ⚠️ This is a selection, not a permission. Nothing here grants anything: whether a server is
709
- * reachable is still decided by one live `tools/list` with `delegatedBy`'s own portal token, so a
710
- * delegator who loses the server or the connection takes it away from the agent at the next run
711
- * with no edit to this document.
712
- *
713
- * ⚠️ Read shape only. It appears in `AgentDefinition` and never in an input — see
714
- * `AgentToolSelection` for why the two are deliberately different documents rather than one schema
715
- * with an optional field.
716
- */
717
- export const AgentToolDelegation = z.strictObject({
718
- delegatedBy: IntelId,
719
- servers: ToolServerHandles,
720
- });
721
- /**
722
- * ⚠️ No accounts, no secrets and no channels — and the reason is mechanical rather than tidy
723
- * (ADR-0005 §4): this body is read, shared, exported and put into model context, so a secret in it
724
- * is a secret in a citation. Identity is Gate's, accounts are the portal's, channels are runtime
725
- * configuration.
726
- *
727
- * ⚠️ `tools` is the one correction to that list (D30), and it is narrower than it looks. What is
728
- * stored is a **selection of whole servers plus who delegated them**, never a mirrored permission
729
- * and never a catalog: the catalog stays a live `tools/list` made with the delegator's token at the
730
- * moment the agent runs. ADR-0005 §4's "no tools in the definition" forbade the mirror, and the
731
- * mirror is still forbidden — a tool name, a schema or an account in here would be the thing that
732
- * line was written against.
733
- *
734
- * ⚠️ Strict on purpose, and deliberately stricter than the runtime's own reader
735
- * (`packages/agent/src/definition/definition.ts`, which is `z.object`). Intel is the writer: an
736
- * unknown field here is a caller's mistake and is refused at the boundary. The runtime is the
737
- * reader and released separately, so it must keep starting agents when Intel adds a field
738
- * tomorrow. The asymmetry is the point, not an oversight.
739
- */
740
- const AgentBody = {
741
- references: z.array(AgentReference).max(200).default([]),
742
- schedules: z.array(AgentSchedule).max(50).default([]),
743
- model: AgentModel,
744
- };
745
- export const AgentDefinition = z.strictObject({
746
- ...AgentBody,
747
- // `null` is "this agent has no tools", and it is also what every definition written before D30
748
- // parses to. An empty `servers` list means the same thing and is kept as its own state so
749
- // removing the last server does not have to erase who was delegating.
750
- tools: AgentToolDelegation.nullable().default(null),
751
- });
752
- /**
753
- * The same document as `AgentDefinition`, minus the one field a caller may not write.
754
- *
755
- * ⚠️ Two schemas rather than one, and the split is load-bearing (#208, D30). Everything an agent IS
756
- * comes from whoever edits it; **whose portal connection it acts on** does not, because that is an
757
- * authority the editor would be granting to themselves. So the write shape simply has no place to
758
- * put it: `{ tools: { servers: [...] } }` is what a screen or an MCP client sends, Intel adds
759
- * `delegatedBy` from the session, and a body that tries to name one is refused by the strict object
760
- * before any of it is read. The reading shape keeps the field because a reader must be able to see
761
- * whose connection an agent runs on.
762
- */
763
- export const AgentDefinitionInput = z.strictObject({
764
- ...AgentBody,
765
- tools: AgentToolSelection.nullable().default(null),
766
- });
767
- export const SaveAgentDefinitionInput = z.strictObject({
768
- nodeId: IntelId,
769
- baseVersionId: IntelId.nullable(),
770
- definition: AgentDefinitionInput,
771
- idempotencyKey: z.string().min(8).max(200),
772
- });
773
- export const GetAgentInput = z.strictObject({ nodeId: IntelId });
774
- // Switching an agent off and on again, and starting one run by hand. All three name only the agent
775
- // and — for a run — which of the targets it already schedules.
776
- //
777
- // ⚠️ Intel holds none of this. Whether an agent is paused is state of its Durable Object, not a
778
- // field of the definition: a definition is versioned, shared and read into model context (ADR-0005
779
- // §4), so every pause would otherwise be a new version and would tell the agent it is switched off.
780
- // These inputs are what Intel accepts and passes on, nothing that Intel stores.
781
- export const PauseAgentInput = z.strictObject({ nodeId: IntelId });
782
- export const RunAgentNowInput = z.strictObject({
783
- nodeId: IntelId,
784
- target: AgentScheduleTarget,
785
- });
786
- /**
787
- * What one agent has actually cost, read out of Cloudflare's AI Gateway log (#251).
788
- *
789
- * ⚠️ Intel computes none of this from tokens and a price table. The gateway publishes the billed
790
- * figure per call, and that figure is the debit from the Cloudflare balance 1:1 — Cloudflare takes
791
- * its 5 % when the balance is loaded and passes inference through unchanged (measured 2026-08-07).
792
- * A second, self-maintained answer beside it would be wrong on the day the two disagreed, and the
793
- * wrong one would be the one on screen.
794
- *
795
- * ⚠️ `status` travels with the numbers and may never be dropped. `runs: []` means "cost nothing"
796
- * only when `status` is `read`; under `not_configured` or `unreadable` it means "not known", and a
797
- * screen that renders the two alike reports an outage as a saving.
798
- */
799
- export const AgentCostStatus = z.enum(["read", "not_configured", "unreadable"]);
800
- export const AgentRunCost = z.strictObject({
801
- runId: z.string(),
802
- cost: z.number(),
803
- calls: z.number(),
804
- });
805
- export const AgentCostWindow = z.strictObject({
806
- days: z.number(),
807
- cost: z.number(),
808
- calls: z.number(),
809
- // Which models produced this figure. It is here so the model select can say the number is about
810
- // the PAST (#257) — a reader who switched model would otherwise take it for a forecast.
811
- models: z.array(z.string()),
812
- });
813
- export const AgentCosts = z.strictObject({
814
- status: AgentCostStatus,
815
- currency: z.literal("USD"),
816
- runs: z.array(AgentRunCost),
817
- windows: z.array(AgentCostWindow),
818
- // The read hit its page limit, so every total above is a floor rather than a total.
819
- partial: z.boolean(),
820
- });
821
- /**
822
- * What the models on offer cost and how much they hold (#257).
823
- *
824
- * ⚠️ `source` is per ENTRY and not per response, and that is not over-engineering. Cloudflare
825
- * publishes figures for the models it serves itself and none at all for the Anthropic models it
826
- * resells through Unified Billing — so a perfectly healthy read still leaves half the list on a
827
- * written-out table, and one flag for the whole answer would call either the read stale or the
828
- * table live.
829
- */
830
- export const ModelPrice = z.strictObject({
831
- inputPerMillion: z.number(),
832
- outputPerMillion: z.number(),
833
- });
834
- export const ModelCatalogEntry = z.strictObject({
835
- provider: z.enum(["workers-ai", "anthropic"]),
836
- model: z.string(),
837
- name: z.string(),
838
- contextTokens: z.number().nullable(),
839
- // `null` where this installation has no figure. Never zero and never a guess — an invented number
840
- // is a false statement about money.
841
- price: ModelPrice.nullable(),
842
- source: z.enum(["cloudflare", "builtin"]),
843
- });
844
- export const ModelCatalog = z.strictObject({
845
- entries: z.array(ModelCatalogEntry),
846
- liveStatus: z.enum(["read", "not_configured", "unreadable"]),
847
- });
848
- // ⚠️ Three states, not two, and the same three the flow list makes: omitted is the whole tree,
849
- // `null` is the root level, an ID is that folder. "Which agents may I use" is a question about the
850
- // tree rather than about one folder, so the useful answer has to be reachable without knowing where
851
- // somebody filed them.
852
- export const ListAgentsInput = z.strictObject({
853
- parentId: IntelId.nullable().optional(),
854
- includeArchived: z.boolean().default(false),
855
- });
856
- export const CreateAgentInput = z.strictObject({
857
- parentId: IntelId.nullable().default(null),
858
- title: z.string().trim().min(1).max(240),
859
- description: z.string().trim().max(2_000).nullable().default(null),
860
- definition: AgentDefinitionInput,
861
- idempotencyKey: z.string().min(8).max(200),
862
- });
863
- // The ID of the Gate Application an agent runs as. Deliberately NOT an `IntelId`: it is Better
864
- // Auth's user ID, minted in Gate and only ever handed back to Gate, so validating it against
865
- // Intel's own ID shape would be Intel inventing a rule about somebody else's identifier.
866
- export const GateApplicationId = z.string().min(1).max(255);
867
- // The definition is `null` exactly while the node exists and no version has been written yet — the
868
- // same window in which a document's content is `null`.
869
- //
870
- // ⚠️ `applicationId` names the machine principal, it does not authenticate it (#182, D27). That is
871
- // why the ID may be stored, listed and drawn while the key may not: one is a name, the other is the
872
- // credential, and Gate hands the credential out exactly once and keeps only its hash. `null` means
873
- // this agent has no Application — an agent node written before #182, restored from a bundle, or
874
- // imported from another installation. Such an agent is not switched with its node, and giving it a
875
- // principal is an operator's act in Gate.
876
- export const NodeAgent = z.strictObject({
877
- node: Node,
878
- version: NodeVersion.nullable(),
879
- definition: AgentDefinition.nullable(),
880
- applicationId: GateApplicationId.nullable(),
881
- });
882
- /**
883
- * ⚠️ There is NO key field in this file, and adding one back would be the regression (D29, #207).
884
- *
885
- * Until #207 the create answer carried the Application key in plain text, once, and a person had to
886
- * carry it into a Worker secret by hand — which is why an agent created through the screen could
887
- * never run (#200). The key now goes from Gate straight into the agent runtime over Intel's service
888
- * binding and is encrypted into that agent's Durable Object; it reaches no browser, no MCP tool
889
- * result and no response body at all. `NodeAgent` is a `z.strictObject`, so a field named `key`
890
- * added anywhere in this file is a parse error at the boundary rather than a leak somebody has to
891
- * spot in review.
892
- *
893
- * What `POST /nodes/agents` and `agent_create` answer is therefore exactly what every read answers:
894
- * the node, its first definition, and the `applicationId` that NAMES the principal without
895
- * authenticating it.
896
- */
897
- export const CreatedAgent = NodeAgent;
898
- // Which agent's key is being replaced. `nodeId` and not the Application ID: this addresses an agent
899
- // in Intel's tree, and the Application behind it is Intel's to look up — a caller naming the
900
- // principal directly would be rotating a key for an agent nobody checked they may edit.
901
- export const RotateAgentKeyInput = z.strictObject({ nodeId: IntelId });
902
- /**
903
- * What replacing an agent's key answers.
904
- *
905
- * ⚠️ No key, and that is the whole shape of D29: Intel asks Gate for a new one, hands it to the
906
- * runtime over the service binding, and forgets it inside the same call. What the caller gets is
907
- * the fact that it happened, so a screen can say so — `applicationId` names the principal whose key
908
- * was replaced, which is a name and not a credential.
909
- */
910
- export const AgentKeyRotated = z.strictObject({
911
- nodeId: IntelId,
912
- applicationId: GateApplicationId,
913
- rotatedAt: IsoDateTime,
914
- });
915
- export const AgentList = z.strictObject({ items: z.array(Node) });
916
- // ⚠️ Kept for what is already stored, not for what is written. Relations were picked in a dialog
917
- // until #41; a link is now made where it is meant — in the text — and every link written from now
918
- // on is a `references`. Rewriting the old rows would destroy a distinction somebody chose on
919
- // purpose, and dropping the column would destroy it with them, so both stay readable.
920
284
  export const NodeLinkRelation = z.enum(["references", "related", "depends_on", "implements"]);
921
285
  // Where the link came from. `text` links are derived from a document's content and are rewritten
922
286
  // whenever it is saved; `manual` links were made in the dialog #41 removed and are now history.
@@ -1531,15 +895,7 @@ export const FlowPublishPreview = z.strictObject({
1531
895
  // What accesses what, for one level of the shared tree (#19). A folder answers it for its contents,
1532
896
  // a single flow for itself. Documents and flows are two kinds of thing that share one tree
1533
897
  // (ADR-0004 §1), so the graph carries both and says which of them it is.
1534
- export const RelationNodeKind = z.enum([
1535
- "folder",
1536
- "document",
1537
- "attachment",
1538
- "table",
1539
- "agent",
1540
- "board",
1541
- "flow",
1542
- ]);
898
+ export const RelationNodeKind = z.enum(["folder", "document", "attachment", "table", "flow"]);
1543
899
  export const RelationNode = z.strictObject({
1544
900
  id: IntelId,
1545
901
  kind: RelationNodeKind,
@@ -1722,8 +1078,20 @@ export const FlowRunHistory = z.strictObject({
1722
1078
  // The one name the importer looks for at the zip root. A different spelling would make a bundle a
1723
1079
  // naked folder, so the constant lives in the contract rather than in each surface.
1724
1080
  export const BundleManifestFilename = "manifest.json";
1725
- // What a bundle entry can be. `flow` joins the six node kinds because a flow shares the folder
1726
- // tree without being a node (ADR-0004), and the bundle mirrors the tree, not the tables.
1081
+ /**
1082
+ * What a bundle entry can be. `flow` joins the node kinds because a flow shares the folder tree
1083
+ * without being a node (ADR-0004), and the bundle mirrors the tree, not the tables.
1084
+ *
1085
+ * ⚠️ `agent` and `board` are STILL HERE, and that is the one place in this file where a value
1086
+ * survives its feature (#390). A bundle is somebody else's file: an export written before Agents
1087
+ * and Board were parked (#385) is a correct export, and it has to PARSE so the import can refuse it
1088
+ * by name — with the entry, the kind and the branch the code is on. Take them out and the same
1089
+ * bundle fails as `unexpected enum value`, which sends its holder looking for a broken file that is
1090
+ * not broken.
1091
+ *
1092
+ * They belong to the wire format of a file that already exists, not to the product. Nothing may
1093
+ * create either kind; `NodeKind` is the enum that says so.
1094
+ */
1727
1095
  export const BundleEntryKind = z.enum([
1728
1096
  "folder",
1729
1097
  "document",