@anchrd/intel-contract 0.4.2 → 0.7.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.
@@ -16,10 +16,22 @@ export const SessionUser = z.strictObject({
16
16
  email: z.email(),
17
17
  name: z.string().min(1).max(240).nullable(),
18
18
  });
19
- // The fourth kind is `table` (#40). It is a kind of node, not a kind of thing: it hangs in the same
20
- // folder tree, inherits the same folder grants, carries the same immutable versions and the same
21
- // R2 body as a document (ADR-0004 §1). Only its media type and the one operation below differ.
22
- export const KnowledgeNodeKind = z.enum(["folder", "document", "attachment", "table"]);
19
+ // What this installation is equipped to do deployment facts, never the caller's permissions
20
+ // (those stay behind each door, where `/session` deliberately does not carry them). `agentRuntime`
21
+ // says whether an agent Worker is bound at all (#190): without it the UI offers no "New agent" and
22
+ // an agent node explains itself instead of rendering views that could only end in a 503.
23
+ export const IntelCapabilities = z.strictObject({
24
+ agentRuntime: z.boolean(),
25
+ });
26
+ // The fourth kind is `table` (#40) and the fifth is `agent` (#139). Each is a kind of node, not a
27
+ // kind of thing: it hangs in the same folder tree, inherits the same folder grants, carries the
28
+ // same immutable versions and the same R2 body as a document (ADR-0004 §1, ADR-0005 §1). Only the
29
+ // media type and the operations below differ.
30
+ //
31
+ // ⚠️ `agent` being optional is load-bearing (ADR-0005 §1): an installation without a single agent
32
+ // node is complete, not unfinished, and nothing here asks anyone to classify a document as a skill
33
+ // or a policy in order to file it.
34
+ export const NodeKind = z.enum(["folder", "document", "attachment", "table", "agent"]);
23
35
  // ⚠️ There is no `ContextPolicy`, and it is not coming back in this shape (#76). It said whether a
24
36
  // document should be pinned into a context, be found by relevance, or be named explicitly — an
25
37
  // instruction to a retrieval Intel does not perform. Intel hands out references and the agent
@@ -34,10 +46,10 @@ export const SharePrincipal = z.discriminatedUnion("type", [
34
46
  z.strictObject({ type: z.literal("email"), email: z.email() }),
35
47
  z.strictObject({ type: z.literal("organization") }),
36
48
  ]);
37
- export const KnowledgeNode = z.strictObject({
49
+ export const Node = z.strictObject({
38
50
  id: IntelId,
39
51
  parentId: IntelId.nullable(),
40
- kind: KnowledgeNodeKind,
52
+ kind: NodeKind,
41
53
  title: z.string().min(1).max(240),
42
54
  description: z.string().max(2_000).nullable(),
43
55
  ownerId: IntelId,
@@ -46,7 +58,14 @@ export const KnowledgeNode = z.strictObject({
46
58
  updatedAt: IsoDateTime,
47
59
  archivedAt: IsoDateTime.nullable(),
48
60
  });
49
- export const KnowledgeVersion = z.strictObject({
61
+ // What one table version carries (#135). An `append` holds only the rows one write added; a
62
+ // `snapshot` holds the complete table — header and every row — so reading starts at the newest
63
+ // snapshot and everything before it is history rather than content. Defining a table writes the
64
+ // first snapshot; updating, deleting, and redefining write the later ones. Documents and
65
+ // attachments carry `null`: each of their versions is complete by construction, and the word would
66
+ // say nothing about them.
67
+ export const NodeVersionSegment = z.enum(["append", "snapshot"]);
68
+ export const NodeVersion = z.strictObject({
50
69
  id: IntelId,
51
70
  nodeId: IntelId,
52
71
  sequence: z.number().int().positive(),
@@ -54,10 +73,11 @@ export const KnowledgeVersion = z.strictObject({
54
73
  mediaType: z.string().min(1).max(160),
55
74
  contentHash: z.string().regex(/^[a-f0-9]{64}$/),
56
75
  size: z.number().int().nonnegative(),
76
+ segment: NodeVersionSegment.nullable(),
57
77
  createdBy: IntelId,
58
78
  createdAt: IsoDateTime,
59
79
  });
60
- export const ListKnowledgeNodesInput = z.strictObject({
80
+ export const ListNodesInput = z.strictObject({
61
81
  parentId: IntelId.nullable().default(null),
62
82
  includeArchived: z.boolean().default(false),
63
83
  // ⚠️ Overrides `parentId` rather than narrowing beside it: what is archived is asked for across
@@ -69,30 +89,33 @@ export const ListKnowledgeNodesInput = z.strictObject({
69
89
  // caller would have to answer a question it is not asking.
70
90
  archivedOnly: z.boolean().optional(),
71
91
  });
72
- export const GetKnowledgeNodeInput = z.strictObject({ nodeId: IntelId });
73
- export const ListKnowledgeGrantsInput = z.strictObject({ resourceId: IntelId });
74
- export const CreateKnowledgeNodeInput = z.strictObject({
92
+ export const GetNodeInput = z.strictObject({ nodeId: IntelId });
93
+ // One pinned version of one node (#147). Both IDs, always: a version ID alone would let anyone
94
+ // holding an ID read content whose node-level ACL they never passed, and a citation names both.
95
+ export const GetNodeVersionInput = z.strictObject({ nodeId: IntelId, versionId: IntelId });
96
+ export const ListGrantsInput = z.strictObject({ resourceId: IntelId });
97
+ export const CreateNodeInput = z.strictObject({
75
98
  parentId: IntelId.nullable().default(null),
76
- kind: KnowledgeNodeKind,
99
+ kind: NodeKind,
77
100
  title: z.string().trim().min(1).max(240),
78
101
  description: z.string().trim().max(2_000).nullable().default(null),
79
102
  idempotencyKey: z.string().min(8).max(200),
80
103
  });
81
- export const SaveKnowledgeVersionInput = z.strictObject({
104
+ export const SaveNodeVersionInput = z.strictObject({
82
105
  nodeId: IntelId,
83
106
  baseVersionId: IntelId.nullable(),
84
107
  content: z.string().max(10_000_000),
85
108
  mediaType: z.string().min(1).max(160).default("text/markdown"),
86
109
  idempotencyKey: z.string().min(8).max(200),
87
110
  });
88
- export const SaveKnowledgeAttachmentInput = z.strictObject({
111
+ export const SaveAttachmentInput = z.strictObject({
89
112
  nodeId: IntelId,
90
113
  baseVersionId: IntelId.nullable(),
91
114
  contentBase64: z.string().min(1).max(20_000_000),
92
115
  mediaType: z.string().min(1).max(160),
93
116
  idempotencyKey: z.string().min(8).max(200),
94
117
  });
95
- export const UpdateKnowledgeNodeInput = z
118
+ export const UpdateNodeInput = z
96
119
  .strictObject({
97
120
  nodeId: IntelId,
98
121
  baseUpdatedAt: IsoDateTime,
@@ -102,30 +125,30 @@ export const UpdateKnowledgeNodeInput = z
102
125
  idempotencyKey: z.string().min(8).max(200),
103
126
  })
104
127
  .refine((input) => input.title !== undefined || input.description !== undefined || input.parentId !== undefined, { message: "At least one change is required" });
105
- export const ArchiveKnowledgeNodeInput = z.strictObject({
128
+ export const ArchiveNodeInput = z.strictObject({
106
129
  nodeId: IntelId,
107
130
  baseUpdatedAt: IsoDateTime,
108
131
  archived: z.boolean(),
109
132
  idempotencyKey: z.string().min(8).max(200),
110
133
  });
111
- // ⚠️ `withChildren` gehört zur EBENE, nicht zum Knoten (#59). Ob etwas Kinder hat, die DIESER
112
- // Leser sehen darf, ist keine Eigenschaft der Sachezwei Leser bekommen verschiedene Antworten.
113
- // Als Feld am Knoten müsste jede andere Stelle, die einen Knoten zurückgibt, es mitberechnen oder
114
- // lügen; als Liste neben den Einträgen kostet es nur die eine Antwort, die es braucht.
115
- export const KnowledgeNodeList = z.strictObject({
116
- items: z.array(KnowledgeNode),
134
+ // ⚠️ `withChildren` belongs to the LEVEL, not to the node (#59). Whether something has children
135
+ // THIS reader may see is not a property of the thing two readers get different answers. As a
136
+ // field on the node, every other place that returns a node would have to compute it as well or
137
+ // lie; as a list beside the entries it costs only the one answer that needs it.
138
+ export const NodeList = z.strictObject({
139
+ items: z.array(Node),
117
140
  withChildren: z.array(IntelId).default([]),
118
141
  });
119
- export const KnowledgeVersionList = z.strictObject({ items: z.array(KnowledgeVersion) });
120
- export const KnowledgeDocument = z.strictObject({
121
- node: KnowledgeNode,
122
- version: KnowledgeVersion.nullable(),
142
+ export const NodeVersionList = z.strictObject({ items: z.array(NodeVersion) });
143
+ export const NodeDocument = z.strictObject({
144
+ node: Node,
145
+ version: NodeVersion.nullable(),
123
146
  content: z.string().nullable(),
124
147
  });
125
- export const KnowledgeAttachment = z.strictObject({
126
- node: KnowledgeNode,
127
- version: KnowledgeVersion,
128
- resourceUri: z.string().regex(/^intel:\/\/knowledge\/[^/]+\/attachment$/),
148
+ export const NodeAttachment = z.strictObject({
149
+ node: Node,
150
+ version: NodeVersion,
151
+ resourceUri: z.string().regex(/^intel:\/\/nodes\/[^/]+\/attachment$/),
129
152
  });
130
153
  // A table is CSV, and CSV is the whole format: it is what is stored, what is downloaded and what a
131
154
  // machine reads. There is no second representation to keep in step with it (#40).
@@ -138,7 +161,7 @@ export const TableRow = z.array(TableCell).min(1).max(64);
138
161
  // Writing the header, once. The columns are the contract (#40's comment), which is why this refuses
139
162
  // on a table that already has one: changing the header would silently reinterpret every row that
140
163
  // was appended under the old one.
141
- export const DefineKnowledgeTableInput = z.strictObject({
164
+ export const DefineTableInput = z.strictObject({
142
165
  nodeId: IntelId,
143
166
  columns: z
144
167
  .array(TableColumn)
@@ -151,31 +174,305 @@ export const DefineKnowledgeTableInput = z.strictObject({
151
174
  // to know which content it replaces; an append adds to the end and cannot collide with a second
152
175
  // append, so demanding a base version would invent a conflict that does not exist and force the
153
176
  // caller to read the whole table first — the exact cost #40 exists to remove.
154
- export const AppendKnowledgeTableRowsInput = z.strictObject({
177
+ export const AppendTableRowsInput = z.strictObject({
155
178
  nodeId: IntelId,
156
179
  rows: z.array(TableRow).min(1).max(1_000),
157
180
  idempotencyKey: z.string().min(8).max(200),
158
181
  });
159
- export const GetKnowledgeTableInput = z.strictObject({ nodeId: IntelId });
182
+ export const GetTableInput = z.strictObject({ nodeId: IntelId });
160
183
  // The table as a grid rather than as text: the server owns the one CSV reader, so no surface has to
161
184
  // grow a second one that would disagree with it about quoting.
162
- export const KnowledgeTable = z.strictObject({
163
- node: KnowledgeNode,
185
+ export const NodeTable = z.strictObject({
186
+ node: Node,
164
187
  columns: z.array(z.string()),
165
188
  rows: z.array(z.array(z.string())),
166
189
  // The newest append, or `null` while the table has no header yet.
167
190
  versionId: IntelId.nullable(),
168
191
  });
169
- export const AppendKnowledgeTableRowsResult = z.strictObject({
170
- node: KnowledgeNode,
171
- version: KnowledgeVersion,
192
+ export const AppendTableRowsResult = z.strictObject({
193
+ node: Node,
194
+ version: NodeVersion,
172
195
  appended: z.number().int().positive(),
173
196
  });
197
+ // A row's address is its position among the table's current rows, counted from zero and without the
198
+ // header. Deliberately not an ID: rows carry no identity of their own (#135, and the same decision
199
+ // the grid documents), so every mutation instead pins the state its positions refer to.
200
+ export const TableRowPosition = z.number().int().nonnegative();
201
+ const distinctPositions = { error: "Row positions must be distinct" };
202
+ // Replacing rows in place (#135). `baseVersionId` is the version the caller read the positions
203
+ // from — required, never nullable, because a position into a table one has not read is a guess.
204
+ // A table that moved on since answers `version_conflict` rather than editing the wrong rows; that
205
+ // is the same optimistic concurrency the document save uses, and the deliberate opposite of
206
+ // `append`, which needs no base because it collides with nothing.
207
+ export const UpdateTableRowsInput = z.strictObject({
208
+ nodeId: IntelId,
209
+ baseVersionId: IntelId,
210
+ updates: z
211
+ .array(z.strictObject({ position: TableRowPosition, row: TableRow }))
212
+ .min(1)
213
+ .max(1_000)
214
+ .refine((updates) => new Set(updates.map((update) => update.position)).size === updates.length, distinctPositions),
215
+ idempotencyKey: z.string().min(8).max(200),
216
+ });
217
+ export const UpdateTableRowsResult = z.strictObject({
218
+ node: Node,
219
+ version: NodeVersion,
220
+ updated: z.number().int().positive(),
221
+ });
222
+ export const DeleteTableRowsInput = z.strictObject({
223
+ nodeId: IntelId,
224
+ baseVersionId: IntelId,
225
+ positions: z
226
+ .array(TableRowPosition)
227
+ .min(1)
228
+ .max(1_000)
229
+ .refine((positions) => new Set(positions).size === positions.length, distinctPositions),
230
+ idempotencyKey: z.string().min(8).max(200),
231
+ });
232
+ export const DeleteTableRowsResult = z.strictObject({
233
+ node: Node,
234
+ version: NodeVersion,
235
+ deleted: z.number().int().positive(),
236
+ });
237
+ // One entry per column the table will have afterwards, in order. `source` names the current column
238
+ // whose cells fill it; `null` adds an empty column, and a current column no entry names is removed
239
+ // together with its cells. Renaming is naming a source under a new name.
240
+ export const RedefineTableColumn = z.strictObject({
241
+ name: TableColumn,
242
+ source: TableColumn.nullable().default(null),
243
+ });
244
+ // Changing the header of a table that has one (#135). The mapping is explicit because it is the
245
+ // whole difference to the blind re-definition `defineTable` keeps refusing: without it a new header
246
+ // would silently reinterpret every stored row under names nobody matched to the old ones.
247
+ export const RedefineTableInput = z.strictObject({
248
+ nodeId: IntelId,
249
+ baseVersionId: IntelId,
250
+ columns: z
251
+ .array(RedefineTableColumn)
252
+ .min(1)
253
+ .max(64)
254
+ .refine((columns) => new Set(columns.map((column) => column.name.toLowerCase())).size === columns.length, { error: "Column names must be distinct" })
255
+ .refine((columns) => {
256
+ const sources = columns.map((column) => column.source).filter((source) => source !== null);
257
+ return new Set(sources).size === sources.length;
258
+ }, { error: "A current column can fill only one new column" }),
259
+ idempotencyKey: z.string().min(8).max(200),
260
+ });
261
+ // ── The agent definition (#139, ADR-0005 §4) ─────────────────────────────────────────────────────
262
+ //
263
+ // An agent's body is a definition, stored as an immutable version in R2 exactly like a document's.
264
+ // Its own media type exists so a reader can tell a definition from prose without parsing it.
265
+ export const AgentMediaType = "application/vnd.anchrd.agent+json";
266
+ // ⚠️ The role lives on the AGENT, never on the node it names, and that is the whole difference to
267
+ // the removed `context_policy` (ADR-0005 §2, #76). The same folder can be the system message for
268
+ // one agent and nothing but search space for another; a node has no opinion about how it is used.
269
+ // Any future field on a node saying how it should be loaded is `context_policy` under a new name.
270
+ //
271
+ // system-message prepended verbatim by the runtime
272
+ // semantic-context search space; the agent searches it when it decides to
273
+ // memory write target — ordinary Knowledge, versioned and readable like everything else
274
+ export const AgentReferenceRole = z.enum(["system-message", "semantic-context", "memory"]);
275
+ export const AgentReference = z.strictObject({ nodeId: IntelId, role: AgentReferenceRole });
276
+ // A `document` target means the content of that document is the instruction — a "skill" somebody
277
+ // wrote as ordinary text; a `flow` target means a run is started through Intel MCP and worked step
278
+ // by step. Both are references, so nothing in here goes stale (ADR-0005 §4).
279
+ //
280
+ // ⚠️ Intel stores a schedule as a declared fact and never fires it. The alarm lives in the runtime
281
+ // (ADR-0005 §3); Intel gains no scheduler, which is D24 confirmed rather than bent.
282
+ export const AgentScheduleTarget = z.strictObject({
283
+ kind: z.enum(["document", "flow"]),
284
+ id: IntelId,
285
+ });
286
+ export const AgentSchedule = z.strictObject({
287
+ cron: z.string().trim().min(1).max(120),
288
+ target: AgentScheduleTarget,
289
+ });
290
+ export const AgentModel = z.strictObject({
291
+ provider: z.enum(["workers-ai", "anthropic"]),
292
+ model: z.string().trim().min(1).max(120),
293
+ });
294
+ /**
295
+ * One MCP server as the portal names it. The handle is what the portal puts in front of every tool
296
+ * that server offers (`notion_notion-search` belongs to `notion`), and it is the only identifier
297
+ * Intel can both store and recognise again in a live `tools/list`.
298
+ *
299
+ * ⚠️ A handle is never invented from a tool name. Which servers exist is the portal's answer
300
+ * (`portal_list_servers`), and the prefix is only used to attribute a tool to a server that answer
301
+ * already named — see `packages/api/src/tools/tool-servers` for why splitting on the underscore
302
+ * alone would be ambiguous.
303
+ */
304
+ export const ToolServerHandle = z
305
+ .string()
306
+ .trim()
307
+ .min(1)
308
+ .max(120)
309
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "A server handle is the portal's own identifier");
310
+ const ToolServerHandles = z.array(ToolServerHandle).max(32).default([]);
311
+ /**
312
+ * What a caller may ASK for: whole MCP servers, and nothing about who delegates them (D30).
313
+ *
314
+ * ⚠️ The absence of `delegatedBy` is the point, and it is why the write shape differs from the read
315
+ * shape at all. Intel writes that field from the session it authorized; a caller who could name
316
+ * somebody else would be handing an agent a portal connection they do not have, and the agent would
317
+ * act on it unattended. Leaving the field out of the input makes that structural instead of a
318
+ * runtime overwrite: a body carrying it is a parse error at the boundary, on every surface, and no
319
+ * screen ever has to invent a value it has no business knowing.
320
+ */
321
+ export const AgentToolSelection = z.strictObject({ servers: ToolServerHandles });
322
+ /**
323
+ * What is STORED and read back: the selection plus whose portal connection it came from (D30).
324
+ *
325
+ * ⚠️ This is a selection, not a permission. Nothing here grants anything: whether a server is
326
+ * reachable is still decided by one live `tools/list` with `delegatedBy`'s own portal token, so a
327
+ * delegator who loses the server or the connection takes it away from the agent at the next run
328
+ * with no edit to this document.
329
+ *
330
+ * ⚠️ Read shape only. It appears in `AgentDefinition` and never in an input — see
331
+ * `AgentToolSelection` for why the two are deliberately different documents rather than one schema
332
+ * with an optional field.
333
+ */
334
+ export const AgentToolDelegation = z.strictObject({
335
+ delegatedBy: IntelId,
336
+ servers: ToolServerHandles,
337
+ });
338
+ /**
339
+ * ⚠️ No accounts, no secrets and no channels — and the reason is mechanical rather than tidy
340
+ * (ADR-0005 §4): this body is read, shared, exported and put into model context, so a secret in it
341
+ * is a secret in a citation. Identity is Gate's, accounts are the portal's, channels are runtime
342
+ * configuration.
343
+ *
344
+ * ⚠️ `tools` is the one correction to that list (D30), and it is narrower than it looks. What is
345
+ * stored is a **selection of whole servers plus who delegated them**, never a mirrored permission
346
+ * and never a catalog: the catalog stays a live `tools/list` made with the delegator's token at the
347
+ * moment the agent runs. ADR-0005 §4's "no tools in the definition" forbade the mirror, and the
348
+ * mirror is still forbidden — a tool name, a schema or an account in here would be the thing that
349
+ * line was written against.
350
+ *
351
+ * ⚠️ Strict on purpose, and deliberately stricter than the runtime's own reader
352
+ * (`packages/agent/src/definition/definition.ts`, which is `z.object`). Intel is the writer: an
353
+ * unknown field here is a caller's mistake and is refused at the boundary. The runtime is the
354
+ * reader and released separately, so it must keep starting agents when Intel adds a field
355
+ * tomorrow. The asymmetry is the point, not an oversight.
356
+ */
357
+ const AgentBody = {
358
+ references: z.array(AgentReference).max(200).default([]),
359
+ schedules: z.array(AgentSchedule).max(50).default([]),
360
+ model: AgentModel,
361
+ };
362
+ export const AgentDefinition = z.strictObject({
363
+ ...AgentBody,
364
+ // `null` is "this agent has no tools", and it is also what every definition written before D30
365
+ // parses to. An empty `servers` list means the same thing and is kept as its own state so
366
+ // removing the last server does not have to erase who was delegating.
367
+ tools: AgentToolDelegation.nullable().default(null),
368
+ });
369
+ /**
370
+ * The same document as `AgentDefinition`, minus the one field a caller may not write.
371
+ *
372
+ * ⚠️ Two schemas rather than one, and the split is load-bearing (#208, D30). Everything an agent IS
373
+ * comes from whoever edits it; **whose portal connection it acts on** does not, because that is an
374
+ * authority the editor would be granting to themselves. So the write shape simply has no place to
375
+ * put it: `{ tools: { servers: [...] } }` is what a screen or an MCP client sends, Intel adds
376
+ * `delegatedBy` from the session, and a body that tries to name one is refused by the strict object
377
+ * before any of it is read. The reading shape keeps the field because a reader must be able to see
378
+ * whose connection an agent runs on.
379
+ */
380
+ export const AgentDefinitionInput = z.strictObject({
381
+ ...AgentBody,
382
+ tools: AgentToolSelection.nullable().default(null),
383
+ });
384
+ export const SaveAgentDefinitionInput = z.strictObject({
385
+ nodeId: IntelId,
386
+ baseVersionId: IntelId.nullable(),
387
+ definition: AgentDefinitionInput,
388
+ idempotencyKey: z.string().min(8).max(200),
389
+ });
390
+ export const GetAgentInput = z.strictObject({ nodeId: IntelId });
391
+ // Switching an agent off and on again, and starting one run by hand. All three name only the agent
392
+ // and — for a run — which of the targets it already schedules.
393
+ //
394
+ // ⚠️ Intel holds none of this. Whether an agent is paused is state of its Durable Object, not a
395
+ // field of the definition: a definition is versioned, shared and read into model context (ADR-0005
396
+ // §4), so every pause would otherwise be a new version and would tell the agent it is switched off.
397
+ // These inputs are what Intel accepts and passes on, nothing that Intel stores.
398
+ export const PauseAgentInput = z.strictObject({ nodeId: IntelId });
399
+ export const RunAgentNowInput = z.strictObject({
400
+ nodeId: IntelId,
401
+ target: AgentScheduleTarget,
402
+ });
403
+ // ⚠️ Three states, not two, and the same three the flow list makes: omitted is the whole tree,
404
+ // `null` is the root level, an ID is that folder. "Which agents may I use" is a question about the
405
+ // tree rather than about one folder, so the useful answer has to be reachable without knowing where
406
+ // somebody filed them.
407
+ export const ListAgentsInput = z.strictObject({
408
+ parentId: IntelId.nullable().optional(),
409
+ includeArchived: z.boolean().default(false),
410
+ });
411
+ export const CreateAgentInput = z.strictObject({
412
+ parentId: IntelId.nullable().default(null),
413
+ title: z.string().trim().min(1).max(240),
414
+ description: z.string().trim().max(2_000).nullable().default(null),
415
+ definition: AgentDefinitionInput,
416
+ idempotencyKey: z.string().min(8).max(200),
417
+ });
418
+ // The ID of the Gate Application an agent runs as. Deliberately NOT an `IntelId`: it is Better
419
+ // Auth's user ID, minted in Gate and only ever handed back to Gate, so validating it against
420
+ // Intel's own ID shape would be Intel inventing a rule about somebody else's identifier.
421
+ export const GateApplicationId = z.string().min(1).max(255);
422
+ // The definition is `null` exactly while the node exists and no version has been written yet — the
423
+ // same window in which a document's content is `null`.
424
+ //
425
+ // ⚠️ `applicationId` names the machine principal, it does not authenticate it (#182, D27). That is
426
+ // why the ID may be stored, listed and drawn while the key may not: one is a name, the other is the
427
+ // credential, and Gate hands the credential out exactly once and keeps only its hash. `null` means
428
+ // this agent has no Application — an agent node written before #182, restored from a bundle, or
429
+ // imported from another installation. Such an agent is not switched with its node, and giving it a
430
+ // principal is an operator's act in Gate.
431
+ export const NodeAgent = z.strictObject({
432
+ node: Node,
433
+ version: NodeVersion.nullable(),
434
+ definition: AgentDefinition.nullable(),
435
+ applicationId: GateApplicationId.nullable(),
436
+ });
437
+ /**
438
+ * ⚠️ There is NO key field in this file, and adding one back would be the regression (D29, #207).
439
+ *
440
+ * Until #207 the create answer carried the Application key in plain text, once, and a person had to
441
+ * carry it into a Worker secret by hand — which is why an agent created through the screen could
442
+ * never run (#200). The key now goes from Gate straight into the agent runtime over Intel's service
443
+ * binding and is encrypted into that agent's Durable Object; it reaches no browser, no MCP tool
444
+ * result and no response body at all. `NodeAgent` is a `z.strictObject`, so a field named `key`
445
+ * added anywhere in this file is a parse error at the boundary rather than a leak somebody has to
446
+ * spot in review.
447
+ *
448
+ * What `POST /nodes/agents` and `agent_create` answer is therefore exactly what every read answers:
449
+ * the node, its first definition, and the `applicationId` that NAMES the principal without
450
+ * authenticating it.
451
+ */
452
+ export const CreatedAgent = NodeAgent;
453
+ // Which agent's key is being replaced. `nodeId` and not the Application ID: this addresses an agent
454
+ // in Intel's tree, and the Application behind it is Intel's to look up — a caller naming the
455
+ // principal directly would be rotating a key for an agent nobody checked they may edit.
456
+ export const RotateAgentKeyInput = z.strictObject({ nodeId: IntelId });
457
+ /**
458
+ * What replacing an agent's key answers.
459
+ *
460
+ * ⚠️ No key, and that is the whole shape of D29: Intel asks Gate for a new one, hands it to the
461
+ * runtime over the service binding, and forgets it inside the same call. What the caller gets is
462
+ * the fact that it happened, so a screen can say so — `applicationId` names the principal whose key
463
+ * was replaced, which is a name and not a credential.
464
+ */
465
+ export const AgentKeyRotated = z.strictObject({
466
+ nodeId: IntelId,
467
+ applicationId: GateApplicationId,
468
+ rotatedAt: IsoDateTime,
469
+ });
470
+ export const AgentList = z.strictObject({ items: z.array(Node) });
174
471
  // ⚠️ Kept for what is already stored, not for what is written. Relations were picked in a dialog
175
472
  // until #41; a link is now made where it is meant — in the text — and every link written from now
176
473
  // on is a `references`. Rewriting the old rows would destroy a distinction somebody chose on
177
474
  // purpose, and dropping the column would destroy it with them, so both stay readable.
178
- export const KnowledgeLinkRelation = z.enum(["references", "related", "depends_on", "implements"]);
475
+ export const NodeLinkRelation = z.enum(["references", "related", "depends_on", "implements"]);
179
476
  // Where the link came from. `text` links are derived from a document's content and are rewritten
180
477
  // whenever it is saved; `manual` links were made in the dialog #41 removed and are now history.
181
478
  //
@@ -183,28 +480,28 @@ export const KnowledgeLinkRelation = z.enum(["references", "related", "depends_o
183
480
  // between them, and nothing may start writing `manual` again — that would be the two ways of saying
184
481
  // one thing that #41 exists to end. It exists so that saving a document cannot delete a link
185
482
  // somebody made before there was another way to make one.
186
- export const KnowledgeLinkOrigin = z.enum(["text", "manual"]);
187
- export const KnowledgeLink = z.strictObject({
483
+ export const NodeLinkOrigin = z.enum(["text", "manual"]);
484
+ export const NodeLink = z.strictObject({
188
485
  id: IntelId,
189
486
  sourceNodeId: IntelId,
190
487
  targetNodeId: IntelId,
191
- relation: KnowledgeLinkRelation,
192
- origin: KnowledgeLinkOrigin,
488
+ relation: NodeLinkRelation,
489
+ origin: NodeLinkOrigin,
193
490
  label: z.string().trim().min(1).max(120).nullable(),
194
491
  createdBy: IntelId,
195
492
  createdAt: IsoDateTime,
196
493
  });
197
- export const KnowledgeLinkList = z.strictObject({ items: z.array(KnowledgeLink) });
494
+ export const NodeLinkList = z.strictObject({ items: z.array(NodeLink) });
198
495
  // The inline element a document link is, inside a BlockNote document (#41).
199
496
  //
200
497
  // ⚠️ The ID and nothing else. No title and no path travel with it: a stored title would go stale
201
498
  // the moment the target is renamed, a stored path the moment it is moved — and either one would
202
499
  // put a name the reader may not see into a document they may.
203
500
  export const DocumentLinkInlineType = "documentLink";
204
- export const ResolveKnowledgeLinksInput = z.strictObject({
501
+ export const ResolveNodeLinksInput = z.strictObject({
205
502
  nodeIds: z.array(IntelId).min(1).max(200),
206
503
  });
207
- export const ResolvedKnowledgeLink = z.strictObject({
504
+ export const ResolvedNodeLink = z.strictObject({
208
505
  nodeId: IntelId,
209
506
  title: z.string().min(1).max(240),
210
507
  });
@@ -213,15 +510,15 @@ export const ResolvedKnowledgeLink = z.strictObject({
213
510
  // side of a document link is drawn from, and an entry that says "something is here" is exactly the
214
511
  // leak this schema has to make impossible to write by accident. Deleted and unreadable therefore
215
512
  // look identical from the outside, which is the point.
216
- export const ResolveKnowledgeLinksResult = z.strictObject({
217
- items: z.array(ResolvedKnowledgeLink),
513
+ export const ResolveNodeLinksResult = z.strictObject({
514
+ items: z.array(ResolvedNodeLink),
218
515
  });
219
- export const KnowledgeGraphInput = z.strictObject({
516
+ export const NodeGraphInput = z.strictObject({
220
517
  limit: z.number().int().min(1).max(500).default(250),
221
518
  });
222
- export const KnowledgeGraph = z.strictObject({
223
- nodes: z.array(KnowledgeNode),
224
- links: z.array(KnowledgeLink),
519
+ export const NodeGraph = z.strictObject({
520
+ nodes: z.array(Node),
521
+ links: z.array(NodeLink),
225
522
  });
226
523
  export const BlockNoteMediaType = "application/vnd.anchrd.intel.blocknote+json";
227
524
  export const BlockNoteDocument = z.strictObject({
@@ -239,14 +536,14 @@ export const ResourceGrant = z.strictObject({
239
536
  createdBy: IntelId,
240
537
  createdAt: IsoDateTime,
241
538
  });
242
- export const ShareKnowledgeInput = z.strictObject({
539
+ export const ShareInput = z.strictObject({
243
540
  resourceId: IntelId,
244
541
  principal: SharePrincipal,
245
542
  verb: ResourceVerb,
246
543
  expiresAt: IsoDateTime.nullable().default(null),
247
544
  idempotencyKey: z.string().min(8).max(200),
248
545
  });
249
- export const RevokeKnowledgeGrantInput = z.strictObject({
546
+ export const RevokeGrantInput = z.strictObject({
250
547
  resourceId: IntelId,
251
548
  grantId: IntelId,
252
549
  idempotencyKey: z.string().min(8).max(200),
@@ -257,16 +554,16 @@ export const RevokeKnowledgeGrantInput = z.strictObject({
257
554
  //
258
555
  // ⚠️ `titles` holds only the documents the sharer may see; everything else is in `hidden` as a
259
556
  // number. A warning must not become a way of reading titles out of the tree.
260
- export const UnreadableKnowledge = z.strictObject({
557
+ export const UnreadableNodes = z.strictObject({
261
558
  titles: z.array(z.string().min(1).max(240)),
262
559
  hidden: z.number().int().nonnegative(),
263
560
  });
264
561
  // The grant is in the answer, so the warning cannot be mistaken for a refusal: it is written first
265
562
  // and described afterwards. Blocking would force everyone who uses a central policy document to
266
563
  // duplicate it, which is the opposite of what one tree is for (ADR-0004 §4).
267
- export const ShareKnowledgeResult = z.strictObject({
564
+ export const ShareResult = z.strictObject({
268
565
  grant: ResourceGrant,
269
- unreadable: UnreadableKnowledge,
566
+ unreadable: UnreadableNodes,
270
567
  });
271
568
  // `applicableVerbs` travels with the list because the answer is the business layer's, not the
272
569
  // screen's: a document has nothing to execute, so `execute` is not offered on one (ADR-0004 §2).
@@ -275,11 +572,16 @@ export const ResourceGrantList = z.strictObject({
275
572
  applicableVerbs: z.array(ResourceVerb).min(1),
276
573
  items: z.array(ResourceGrant),
277
574
  });
278
- export const SearchKnowledgeInput = z.strictObject({
575
+ // `scopeId` is a cut, never a grant (#126): it narrows an answer the actor is already entitled to
576
+ // and can only ever remove rows. Without it the search stays global over everything visible, which
577
+ // is why it is optional rather than nullable — an absent field and `null` would otherwise be two
578
+ // spellings of the same request.
579
+ export const SearchInput = z.strictObject({
279
580
  query: z.string().trim().min(1).max(500),
280
581
  limit: z.number().int().min(1).max(50).default(10),
582
+ scopeId: IntelId.optional().describe("Optional folder node id. When given, only nodes filed in that folder or beneath it are searched."),
281
583
  });
282
- export const KnowledgeCitation = z.strictObject({
584
+ export const NodeCitation = z.strictObject({
283
585
  nodeId: IntelId,
284
586
  versionId: IntelId,
285
587
  title: z.string(),
@@ -289,10 +591,10 @@ export const KnowledgeCitation = z.strictObject({
289
591
  score: z.number().min(0).max(1),
290
592
  match: z.enum(["lexical", "semantic", "hybrid"]),
291
593
  });
292
- export const SearchKnowledgeResult = z.strictObject({
293
- items: z.array(KnowledgeCitation),
594
+ export const SearchResult = z.strictObject({
595
+ items: z.array(NodeCitation),
294
596
  });
295
- export const ReindexKnowledgeResult = z.strictObject({ queued: z.number().int().nonnegative() });
597
+ export const ReindexResult = z.strictObject({ queued: z.number().int().nonnegative() });
296
598
  export const RevokeGrantResult = z.strictObject({ revoked: z.boolean() });
297
599
  function isPrivateIpv4(hostname) {
298
600
  const parts = hostname.split(".").map(Number);
@@ -338,8 +640,11 @@ export const ToolSourceUrl = z.url().refine((value) => {
338
640
  return false;
339
641
  }
340
642
  }, "The portal must use an approved public HTTPS host without embedded credentials");
341
- // The portal namespaces every upstream tool, so the name alone identifies the target server. Intel
342
- // never learns which server that is the portal resolves it and attaches the credentials.
643
+ // The portal namespaces every upstream tool, so the name alone identifies the target server. The
644
+ // portal is still the one that resolves it and attaches the credentials — Intel never holds an
645
+ // upstream credential. Since D30 Intel does read the namespace for one purpose: attributing a tool
646
+ // to a server the portal's own `portal_list_servers` already named, so a delegation can be cut to
647
+ // whole servers. That is attribution, not routing.
343
648
  export const ToolName = z.string().min(1).max(240);
344
649
  export const ToolAnnotations = z.strictObject({
345
650
  title: z.string().max(240).optional(),
@@ -363,6 +668,58 @@ export const ToolCatalog = z.strictObject({
363
668
  portalConnected: z.boolean(),
364
669
  items: z.array(ToolCapability),
365
670
  });
671
+ /**
672
+ * One MCP server the asking user reaches right now, as the portal itself names it (D30).
673
+ *
674
+ * ⚠️ `toolCount` is a fact about this moment and this user, not a size. It exists so a picker can
675
+ * say "9 tools" instead of showing a handle alone, and it must never be read as what an agent will
676
+ * get: the delegated run asks the portal again, with the delegator's token.
677
+ */
678
+ export const ToolServer = z.strictObject({
679
+ handle: ToolServerHandle,
680
+ name: z.string().min(1).max(240),
681
+ toolCount: z.number().int().min(0),
682
+ });
683
+ // The same live-query rule as the tool catalog, one level up. `portalConnected: false` is the state
684
+ // of somebody who has not signed into the portal yet, and it is not an error.
685
+ export const ToolServerCatalog = z.strictObject({
686
+ portalConnected: z.boolean(),
687
+ items: z.array(ToolServer),
688
+ });
689
+ /**
690
+ * Which of the named servers a tool belongs to, or `null` for none of them.
691
+ *
692
+ * ⚠️ THE TRAP: a tool name does not say where its server name ends.
693
+ *
694
+ * The portal writes `<server>_<tool>`, and both halves may contain underscores — `intel_flow_get`
695
+ * reads equally well as server `intel` with tool `flow_get` and as a server called `intel_flow`
696
+ * with tool `get`. Splitting on the first underscore is therefore a guess that is wrong the day
697
+ * somebody adds a server whose name contains one, and on the API side being wrong means an agent
698
+ * delegated server A quietly reaching server B.
699
+ *
700
+ * So the prefix is never split. It is only ever MATCHED against handles the portal itself named,
701
+ * and the longest match wins: with `intel` and `intel_flow` both declared, `intel_flow_get` belongs
702
+ * to `intel_flow`, which is the only reading in which both declarations stay true.
703
+ *
704
+ * ⚠️ This lives in the contract because HOW A NAME IS READ is a property of the wire, and both
705
+ * surfaces read the same wire: `packages/api` cuts a delegation with it, `packages/ui` groups the
706
+ * tools screen with it (#212). A second implementation in the browser would be the third answer to
707
+ * one question — the underscore rule has already been answered differently in two places once
708
+ * (#106, #107), and the copies disagreed. What deliberately stays OUT of here is everything about
709
+ * reach: which handles are declared, which are enabled, which may be delegated and which one owns
710
+ * the portal's own management tools are decisions with consequences, and they belong to
711
+ * `packages/api/src/tools/tool-servers`. This function only reads a name.
712
+ */
713
+ export function serverOf(toolName, handles) {
714
+ let best = null;
715
+ for (const handle of handles) {
716
+ if (!toolName.startsWith(`${handle}_`))
717
+ continue;
718
+ if (best === null || handle.length > best.length)
719
+ best = handle;
720
+ }
721
+ return best;
722
+ }
366
723
  export const TestToolInput = z.strictObject({
367
724
  name: ToolName,
368
725
  arguments: z.record(z.string(), z.unknown()).default({}),
@@ -526,7 +883,7 @@ export const FlowGraph = z.strictObject({
526
883
  });
527
884
  export const Flow = z.strictObject({
528
885
  id: IntelId,
529
- // The one thing a Flow shares with a document: its place in the Knowledge folder tree (ADR-0004).
886
+ // The one thing a Flow shares with a document: its place in the shared folder tree (ADR-0004).
530
887
  // Everything else stays apart — versions, R2 body and Vectorize belong to the document, the graph,
531
888
  // runs and approvals to the flow. `null` is the root of that same tree.
532
889
  parentId: IntelId.nullable(),
@@ -551,30 +908,52 @@ export const FlowDocument = z.strictObject({
551
908
  flow: Flow,
552
909
  version: FlowVersion.nullable(),
553
910
  });
554
- // Dasselbe für Flows: welche von ihnen einen anderen Flow rufen, den dieser Leser auch sehen darf
555
- // (#59). Ein Aufklapp-Pfeil an einem Flow, dessen Aufrufe alle verborgen sind, verspricht Inhalt,
556
- // den das Aufklappen nicht liefern kann.
911
+ // One version as the history shows it: the metadata without the graph it carries. A flow's history
912
+ // is as long as its edits, and a list that shipped every graph would pay for drawings nobody asked
913
+ // for whoever needs one asks for that one version.
914
+ export const FlowVersionSummary = z.strictObject({
915
+ id: IntelId,
916
+ flowId: IntelId,
917
+ sequence: z.number().int().positive(),
918
+ createdBy: IntelId,
919
+ createdAt: IsoDateTime,
920
+ // Whether this is the version the flow currently publishes. Derived from the flow row when the
921
+ // list is read, never stored on the version: a version is immutable and "published" is not a
922
+ // property of it — it is the flow's choice, revocable without touching the version.
923
+ published: z.boolean(),
924
+ });
925
+ export const FlowVersionList = z.strictObject({
926
+ flowId: IntelId,
927
+ items: z.array(FlowVersionSummary),
928
+ });
929
+ // Both identifiers, deliberately: a version ID alone would resolve whatever version carries it,
930
+ // whichever flow it belongs to, and the ACL is answered on the flow. The pair makes a foreign
931
+ // version a 404 rather than a read.
932
+ export const GetFlowVersionInput = z.strictObject({ flowId: IntelId, versionId: IntelId });
933
+ // The same for flows: which of them call another flow that this reader may also see (#59). An
934
+ // expand arrow on a flow whose calls are all hidden promises content that expanding it cannot
935
+ // deliver.
557
936
  export const FlowList = z.strictObject({
558
937
  items: z.array(Flow),
559
938
  withCalls: z.array(IntelId).default([]),
560
939
  });
561
- export const FlowKnowledgeReference = z.strictObject({
940
+ export const ReferencedNode = z.strictObject({
562
941
  id: IntelId,
563
942
  title: z.string().min(1).max(240),
564
943
  });
565
- // What a flow touches: the documents its Knowledge steps name and the tools its Tool steps call,
944
+ // What a flow touches: the documents its tree links name and the tools its Tool steps call,
566
945
  // read straight out of the graph. Deliberately not a conflict report — there is no arithmetic here
567
946
  // and nothing that can go stale, because the graph is the answer. Whether a given person may reach
568
947
  // any of it is decided where it can be decided honestly: when the folder is shared, and at runtime
569
948
  // (ADR-0004 §4). For tools it can only ever be the latter, because the catalog is a live query with
570
949
  // the requesting user's own token (ADR-0003).
571
950
  //
572
- // ⚠️ `knowledge` names only what the asking user may see. The rest is `hiddenKnowledge`, a count.
951
+ // ⚠️ `nodes` names only what the asking user may see. The rest is `hiddenNodes`, a count.
573
952
  export const FlowRequirements = z.strictObject({
574
953
  flowId: IntelId,
575
954
  versionId: IntelId.nullable(),
576
- knowledge: z.array(FlowKnowledgeReference),
577
- hiddenKnowledge: z.number().int().nonnegative(),
955
+ nodes: z.array(ReferencedNode),
956
+ hiddenNodes: z.number().int().nonnegative(),
578
957
  tools: z.array(ToolName),
579
958
  });
580
959
  // What stands between this flow and a run, asked on demand and answered for the person asking.
@@ -630,12 +1009,12 @@ export const ListFlowsInput = z.strictObject({
630
1009
  // behind the relation graph has no such flag on purpose (#30). A drawing that includes what was
631
1010
  // archived says the tidying up never happened.
632
1011
  //
633
- // ⚠️ `.optional()` rather than `.default(false)`, unlike `ListKnowledgeNodesInput`. This schema is
1012
+ // ⚠️ `.optional()` rather than `.default(false)`, unlike `ListNodesInput`. This schema is
634
1013
  // the argument type of `listFlows` on three layers, and a default makes the field required in the
635
1014
  // *parsed* type — every existing caller that lists a folder would have to spell out the answer to
636
1015
  // a question it is not asking. Absent means "without the archive" everywhere it is read.
637
1016
  includeArchived: z.boolean().optional(),
638
- // The same question for flows, and the same override of `parentId` — see `ListKnowledgeNodesInput`.
1017
+ // The same question for flows, and the same override of `parentId` — see `ListNodesInput`.
639
1018
  archivedOnly: z.boolean().optional(),
640
1019
  });
641
1020
  export const GetFlowInput = z.strictObject({ flowId: IntelId });
@@ -650,6 +1029,13 @@ export const PublishFlowInput = z.strictObject({
650
1029
  versionId: IntelId,
651
1030
  idempotencyKey: z.string().min(8).max(200),
652
1031
  });
1032
+ // The way back out of a publication (#146). No versionId: what is withdrawn is whatever is
1033
+ // published now, and naming one would invite a race between reading it and revoking it. Versions
1034
+ // are untouched — republishing any of them is one `publish` away.
1035
+ export const UnpublishFlowInput = z.strictObject({
1036
+ flowId: IntelId,
1037
+ idempotencyKey: z.string().min(8).max(200),
1038
+ });
653
1039
  export const PreviewFlowPublishInput = z.strictObject({ flowId: IntelId, versionId: IntelId });
654
1040
  // One sub-flow call as publishing will leave it (ADR-0004 §5). `freezes` is the whole point of the
655
1041
  // preview: it marks the calls whose `latest` publishing turns into `versionId`, so the author reads
@@ -682,7 +1068,14 @@ export const FlowPublishPreview = z.strictObject({
682
1068
  // What accesses what, for one level of the shared tree (#19). A folder answers it for its contents,
683
1069
  // a single flow for itself. Documents and flows are two kinds of thing that share one tree
684
1070
  // (ADR-0004 §1), so the graph carries both and says which of them it is.
685
- export const RelationNodeKind = z.enum(["folder", "document", "attachment", "table", "flow"]);
1071
+ export const RelationNodeKind = z.enum([
1072
+ "folder",
1073
+ "document",
1074
+ "attachment",
1075
+ "table",
1076
+ "agent",
1077
+ "flow",
1078
+ ]);
686
1079
  export const RelationNode = z.strictObject({
687
1080
  id: IntelId,
688
1081
  kind: RelationNodeKind,
@@ -767,6 +1160,13 @@ export const StartFlowRunInput = z.strictObject({
767
1160
  idempotencyKey: z.string().min(8).max(200),
768
1161
  });
769
1162
  export const GetFlowRunInput = z.strictObject({ runId: IntelId });
1163
+ // Ending a run on purpose (#145). Until this existed the only way off a parked manual step was
1164
+ // `completeStep` with `outcome: "failed"` — which recorded a step failure that never happened.
1165
+ // Cancelling records nothing into the step history: the run ends, the history stays true.
1166
+ export const CancelFlowRunInput = z.strictObject({
1167
+ runId: IntelId,
1168
+ idempotencyKey: z.string().min(8).max(200),
1169
+ });
770
1170
  export const CompleteFlowRunStepInput = z.strictObject({
771
1171
  runId: IntelId,
772
1172
  nodeId: FlowNodeId,
@@ -801,7 +1201,7 @@ export const FlowRunFailure = z.strictObject({
801
1201
  });
802
1202
  // One run as a list shows it: what it did, never what it produced.
803
1203
  //
804
- // ⚠️ Neither `input` nor `output` is in here, on purpose. A run reaches its Knowledge and its tools
1204
+ // ⚠️ Neither `input` nor `output` is in here, on purpose. A run reaches its nodes and its tools
805
1205
  // with the rights of whoever started it, so its result is a way to content the next reader of this
806
1206
  // list may have no claim to. Whoever wants a result asks for the run itself, where the same rule
807
1207
  // decides again.
@@ -854,3 +1254,51 @@ export const FlowRunHistory = z.strictObject({
854
1254
  steps: z.array(FlowRunStepRecord),
855
1255
  trail: z.array(FlowRunTrailEntry),
856
1256
  });
1257
+ // ── Bundle export (#136) ────────────────────────────────────────────────────────────────────────
1258
+ // The one name the importer looks for at the zip root. A different spelling would make a bundle a
1259
+ // naked folder, so the constant lives in the contract rather than in each surface.
1260
+ export const BundleManifestFilename = "manifest.json";
1261
+ // What a bundle entry can be. `flow` joins the five node kinds because a flow shares the folder
1262
+ // tree without being a node (ADR-0004), and the bundle mirrors the tree, not the tables.
1263
+ export const BundleEntryKind = z.enum([
1264
+ "folder",
1265
+ "document",
1266
+ "table",
1267
+ "attachment",
1268
+ "agent",
1269
+ "flow",
1270
+ ]);
1271
+ // One entry of the manifest: the identity a re-import needs, next to the relative path where the
1272
+ // bytes sit in the zip. A folder carries no media type — it has no bytes.
1273
+ export const BundleManifestEntry = z.strictObject({
1274
+ id: IntelId,
1275
+ kind: BundleEntryKind,
1276
+ title: z.string().min(1).max(240),
1277
+ description: z.string().max(2_000).nullable(),
1278
+ mediaType: z.string().min(1).max(160).nullable(),
1279
+ // Relative to the zip root, forward slashes, no leading slash. Folders end with a slash so an
1280
+ // empty folder still has an address.
1281
+ path: z.string().min(1).max(4_000),
1282
+ });
1283
+ // What an export leaves out on purpose, named so a bundle says it rather than a reader guessing:
1284
+ // version history, grants/shares, flow runs, and archived nodes are not in any bundle (#136).
1285
+ export const BundleExclusion = z.enum(["version-history", "grants", "flow-runs", "archived-nodes"]);
1286
+ export const BundleManifest = z.strictObject({
1287
+ version: z.literal(1),
1288
+ exportedAt: IsoDateTime,
1289
+ // The node the export started at; `null` is the root of the tree — the whole installation as the
1290
+ // exporting caller may read it.
1291
+ rootId: IntelId.nullable(),
1292
+ entries: z.array(BundleManifestEntry),
1293
+ excluded: z.array(BundleExclusion),
1294
+ });
1295
+ // ── Bundle import (#137) ────────────────────────────────────────────────────────────────────────
1296
+ // What one import made. Import always creates new nodes — no merge, no overwrite, no restored IDs
1297
+ // (#137, phase 1) — so the answer is counts and the new roots, never a diff. `replayed` marks the
1298
+ // idempotent second answer to the same key: nothing was created twice.
1299
+ export const BundleImportResult = z.strictObject({
1300
+ nodes: z.number().int().nonnegative(),
1301
+ flows: z.number().int().nonnegative(),
1302
+ rootNodeIds: z.array(IntelId),
1303
+ replayed: z.boolean(),
1304
+ });