@anchrd/intel-contract 0.4.1 → 0.5.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,37 +73,49 @@ 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
- });
64
- export const GetKnowledgeNodeInput = z.strictObject({ nodeId: IntelId });
65
- export const ListKnowledgeGrantsInput = z.strictObject({ resourceId: IntelId });
66
- export const CreateKnowledgeNodeInput = z.strictObject({
83
+ // ⚠️ Overrides `parentId` rather than narrowing beside it: what is archived is asked for across
84
+ // the whole tree, because that is the only useful question. Somebody looking for what they threw
85
+ // away does not know which folder it was in — if they did, they would not be looking (#113).
86
+ // A separate flag rather than a third state on `includeArchived`, so no existing caller changes
87
+ // meaning — and `.optional()` rather than `.default(false)` for the same reason `ListFlowsInput`
88
+ // carries it that way: a default makes the field required in the PARSED type, and every existing
89
+ // caller would have to answer a question it is not asking.
90
+ archivedOnly: z.boolean().optional(),
91
+ });
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({
67
98
  parentId: IntelId.nullable().default(null),
68
- kind: KnowledgeNodeKind,
99
+ kind: NodeKind,
69
100
  title: z.string().trim().min(1).max(240),
70
101
  description: z.string().trim().max(2_000).nullable().default(null),
71
102
  idempotencyKey: z.string().min(8).max(200),
72
103
  });
73
- export const SaveKnowledgeVersionInput = z.strictObject({
104
+ export const SaveNodeVersionInput = z.strictObject({
74
105
  nodeId: IntelId,
75
106
  baseVersionId: IntelId.nullable(),
76
107
  content: z.string().max(10_000_000),
77
108
  mediaType: z.string().min(1).max(160).default("text/markdown"),
78
109
  idempotencyKey: z.string().min(8).max(200),
79
110
  });
80
- export const SaveKnowledgeAttachmentInput = z.strictObject({
111
+ export const SaveAttachmentInput = z.strictObject({
81
112
  nodeId: IntelId,
82
113
  baseVersionId: IntelId.nullable(),
83
114
  contentBase64: z.string().min(1).max(20_000_000),
84
115
  mediaType: z.string().min(1).max(160),
85
116
  idempotencyKey: z.string().min(8).max(200),
86
117
  });
87
- export const UpdateKnowledgeNodeInput = z
118
+ export const UpdateNodeInput = z
88
119
  .strictObject({
89
120
  nodeId: IntelId,
90
121
  baseUpdatedAt: IsoDateTime,
@@ -94,30 +125,30 @@ export const UpdateKnowledgeNodeInput = z
94
125
  idempotencyKey: z.string().min(8).max(200),
95
126
  })
96
127
  .refine((input) => input.title !== undefined || input.description !== undefined || input.parentId !== undefined, { message: "At least one change is required" });
97
- export const ArchiveKnowledgeNodeInput = z.strictObject({
128
+ export const ArchiveNodeInput = z.strictObject({
98
129
  nodeId: IntelId,
99
130
  baseUpdatedAt: IsoDateTime,
100
131
  archived: z.boolean(),
101
132
  idempotencyKey: z.string().min(8).max(200),
102
133
  });
103
- // ⚠️ `withChildren` gehört zur EBENE, nicht zum Knoten (#59). Ob etwas Kinder hat, die DIESER
104
- // Leser sehen darf, ist keine Eigenschaft der Sachezwei Leser bekommen verschiedene Antworten.
105
- // Als Feld am Knoten müsste jede andere Stelle, die einen Knoten zurückgibt, es mitberechnen oder
106
- // lügen; als Liste neben den Einträgen kostet es nur die eine Antwort, die es braucht.
107
- export const KnowledgeNodeList = z.strictObject({
108
- 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),
109
140
  withChildren: z.array(IntelId).default([]),
110
141
  });
111
- export const KnowledgeVersionList = z.strictObject({ items: z.array(KnowledgeVersion) });
112
- export const KnowledgeDocument = z.strictObject({
113
- node: KnowledgeNode,
114
- 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(),
115
146
  content: z.string().nullable(),
116
147
  });
117
- export const KnowledgeAttachment = z.strictObject({
118
- node: KnowledgeNode,
119
- version: KnowledgeVersion,
120
- 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$/),
121
152
  });
122
153
  // A table is CSV, and CSV is the whole format: it is what is stored, what is downloaded and what a
123
154
  // machine reads. There is no second representation to keep in step with it (#40).
@@ -130,7 +161,7 @@ export const TableRow = z.array(TableCell).min(1).max(64);
130
161
  // Writing the header, once. The columns are the contract (#40's comment), which is why this refuses
131
162
  // on a table that already has one: changing the header would silently reinterpret every row that
132
163
  // was appended under the old one.
133
- export const DefineKnowledgeTableInput = z.strictObject({
164
+ export const DefineTableInput = z.strictObject({
134
165
  nodeId: IntelId,
135
166
  columns: z
136
167
  .array(TableColumn)
@@ -143,31 +174,233 @@ export const DefineKnowledgeTableInput = z.strictObject({
143
174
  // to know which content it replaces; an append adds to the end and cannot collide with a second
144
175
  // append, so demanding a base version would invent a conflict that does not exist and force the
145
176
  // caller to read the whole table first — the exact cost #40 exists to remove.
146
- export const AppendKnowledgeTableRowsInput = z.strictObject({
177
+ export const AppendTableRowsInput = z.strictObject({
147
178
  nodeId: IntelId,
148
179
  rows: z.array(TableRow).min(1).max(1_000),
149
180
  idempotencyKey: z.string().min(8).max(200),
150
181
  });
151
- export const GetKnowledgeTableInput = z.strictObject({ nodeId: IntelId });
182
+ export const GetTableInput = z.strictObject({ nodeId: IntelId });
152
183
  // The table as a grid rather than as text: the server owns the one CSV reader, so no surface has to
153
184
  // grow a second one that would disagree with it about quoting.
154
- export const KnowledgeTable = z.strictObject({
155
- node: KnowledgeNode,
185
+ export const NodeTable = z.strictObject({
186
+ node: Node,
156
187
  columns: z.array(z.string()),
157
188
  rows: z.array(z.array(z.string())),
158
189
  // The newest append, or `null` while the table has no header yet.
159
190
  versionId: IntelId.nullable(),
160
191
  });
161
- export const AppendKnowledgeTableRowsResult = z.strictObject({
162
- node: KnowledgeNode,
163
- version: KnowledgeVersion,
192
+ export const AppendTableRowsResult = z.strictObject({
193
+ node: Node,
194
+ version: NodeVersion,
164
195
  appended: z.number().int().positive(),
165
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
+ * ⚠️ No accounts, no secrets, no channels, and no tools — and the reason is mechanical rather than
296
+ * tidy (ADR-0005 §4): this body is read, shared, exported and put into model context, so a secret
297
+ * in it is a secret in a citation. Identity is Gate's, accounts are the portal's, channels are
298
+ * runtime configuration, and the tool catalog is a live `tools/list` that is never mirrored.
299
+ *
300
+ * ⚠️ Strict on purpose, and deliberately stricter than the runtime's own reader
301
+ * (`packages/agent/src/definition/definition.ts`, which is `z.object`). Intel is the writer: an
302
+ * unknown field here is a caller's mistake and is refused at the boundary. The runtime is the
303
+ * reader and released separately, so it must keep starting agents when Intel adds a field
304
+ * tomorrow. The asymmetry is the point, not an oversight.
305
+ */
306
+ export const AgentDefinition = z.strictObject({
307
+ references: z.array(AgentReference).max(200).default([]),
308
+ schedules: z.array(AgentSchedule).max(50).default([]),
309
+ model: AgentModel,
310
+ });
311
+ export const SaveAgentDefinitionInput = z.strictObject({
312
+ nodeId: IntelId,
313
+ baseVersionId: IntelId.nullable(),
314
+ definition: AgentDefinition,
315
+ idempotencyKey: z.string().min(8).max(200),
316
+ });
317
+ export const GetAgentInput = z.strictObject({ nodeId: IntelId });
318
+ // Switching an agent off and on again, and starting one run by hand. All three name only the agent
319
+ // and — for a run — which of the targets it already schedules.
320
+ //
321
+ // ⚠️ Intel holds none of this. Whether an agent is paused is state of its Durable Object, not a
322
+ // field of the definition: a definition is versioned, shared and read into model context (ADR-0005
323
+ // §4), so every pause would otherwise be a new version and would tell the agent it is switched off.
324
+ // These inputs are what Intel accepts and passes on, nothing that Intel stores.
325
+ export const PauseAgentInput = z.strictObject({ nodeId: IntelId });
326
+ export const RunAgentNowInput = z.strictObject({
327
+ nodeId: IntelId,
328
+ target: AgentScheduleTarget,
329
+ });
330
+ // ⚠️ Three states, not two, and the same three the flow list makes: omitted is the whole tree,
331
+ // `null` is the root level, an ID is that folder. "Which agents may I use" is a question about the
332
+ // tree rather than about one folder, so the useful answer has to be reachable without knowing where
333
+ // somebody filed them.
334
+ export const ListAgentsInput = z.strictObject({
335
+ parentId: IntelId.nullable().optional(),
336
+ includeArchived: z.boolean().default(false),
337
+ });
338
+ export const CreateAgentInput = z.strictObject({
339
+ parentId: IntelId.nullable().default(null),
340
+ title: z.string().trim().min(1).max(240),
341
+ description: z.string().trim().max(2_000).nullable().default(null),
342
+ definition: AgentDefinition,
343
+ idempotencyKey: z.string().min(8).max(200),
344
+ });
345
+ // The ID of the Gate Application an agent runs as. Deliberately NOT an `IntelId`: it is Better
346
+ // Auth's user ID, minted in Gate and only ever handed back to Gate, so validating it against
347
+ // Intel's own ID shape would be Intel inventing a rule about somebody else's identifier.
348
+ export const GateApplicationId = z.string().min(1).max(255);
349
+ // The definition is `null` exactly while the node exists and no version has been written yet — the
350
+ // same window in which a document's content is `null`.
351
+ //
352
+ // ⚠️ `applicationId` names the machine principal, it does not authenticate it (#182, D27). That is
353
+ // why the ID may be stored, listed and drawn while the key may not: one is a name, the other is the
354
+ // credential, and Gate hands the credential out exactly once and keeps only its hash. `null` means
355
+ // this agent has no Application — an agent node written before #182, restored from a bundle, or
356
+ // imported from another installation. Such an agent is not switched with its node, and giving it a
357
+ // principal is an operator's act in Gate.
358
+ export const NodeAgent = z.strictObject({
359
+ node: Node,
360
+ version: NodeVersion.nullable(),
361
+ definition: AgentDefinition.nullable(),
362
+ applicationId: GateApplicationId.nullable(),
363
+ });
364
+ /**
365
+ * The single value in Intel's wire formats that IS a credential (#182, D27).
366
+ *
367
+ * ⚠️ It exists for exactly one response — the one that created the agent — and for no other. Gate
368
+ * returns an Application key once in plain text and stores only its hash, so this is not a value
369
+ * Intel could fetch again later even if it wanted to; nothing in Intel writes it to D1, to R2, to
370
+ * an audit event or to a log, and no read surface has a field it could travel in. What the caller
371
+ * does with it is named in `notice` rather than left to them: it belongs in the agent runtime's
372
+ * `AGENT_APPLICATION_KEYS` secret, keyed by `agentId`, and nowhere else.
373
+ *
374
+ * ⚠️ `agentId` is the Intel node ID and not the Application ID, because that is the key the runtime
375
+ * looks an entry up by. Writing the Application ID into the runtime's map would produce a
376
+ * deployment that parses, starts, and then cannot find a single agent.
377
+ */
378
+ export const AgentApplicationKey = z.strictObject({
379
+ agentId: IntelId,
380
+ applicationId: GateApplicationId,
381
+ key: z.string().min(1),
382
+ notice: z.string().min(1),
383
+ });
384
+ /**
385
+ * What `POST /nodes/agents` and `agent_create` answer, and the only shape carrying a key. Every
386
+ * other agent read answers with the plain `NodeAgent` above.
387
+ *
388
+ * ⚠️ `applicationKey` is `null` on a REPLAY, and that is the honest answer rather than a gap. An
389
+ * idempotency key repeated after the first response was lost still returns the agent that exists —
390
+ * the promise every create in Intel makes — but the key belonged to the one response that created
391
+ * it and is gone from Gate. Minting a second principal to fill this field would leave the
392
+ * installation with two machine accounts for one agent, one of which nobody would ever switch off.
393
+ * `application_rotate_key` in Gate is the way to a new key, and it is a deliberate act.
394
+ */
395
+ export const CreatedAgent = NodeAgent.extend({
396
+ applicationKey: AgentApplicationKey.nullable(),
397
+ });
398
+ export const AgentList = z.strictObject({ items: z.array(Node) });
166
399
  // ⚠️ Kept for what is already stored, not for what is written. Relations were picked in a dialog
167
400
  // until #41; a link is now made where it is meant — in the text — and every link written from now
168
401
  // on is a `references`. Rewriting the old rows would destroy a distinction somebody chose on
169
402
  // purpose, and dropping the column would destroy it with them, so both stay readable.
170
- export const KnowledgeLinkRelation = z.enum(["references", "related", "depends_on", "implements"]);
403
+ export const NodeLinkRelation = z.enum(["references", "related", "depends_on", "implements"]);
171
404
  // Where the link came from. `text` links are derived from a document's content and are rewritten
172
405
  // whenever it is saved; `manual` links were made in the dialog #41 removed and are now history.
173
406
  //
@@ -175,28 +408,28 @@ export const KnowledgeLinkRelation = z.enum(["references", "related", "depends_o
175
408
  // between them, and nothing may start writing `manual` again — that would be the two ways of saying
176
409
  // one thing that #41 exists to end. It exists so that saving a document cannot delete a link
177
410
  // somebody made before there was another way to make one.
178
- export const KnowledgeLinkOrigin = z.enum(["text", "manual"]);
179
- export const KnowledgeLink = z.strictObject({
411
+ export const NodeLinkOrigin = z.enum(["text", "manual"]);
412
+ export const NodeLink = z.strictObject({
180
413
  id: IntelId,
181
414
  sourceNodeId: IntelId,
182
415
  targetNodeId: IntelId,
183
- relation: KnowledgeLinkRelation,
184
- origin: KnowledgeLinkOrigin,
416
+ relation: NodeLinkRelation,
417
+ origin: NodeLinkOrigin,
185
418
  label: z.string().trim().min(1).max(120).nullable(),
186
419
  createdBy: IntelId,
187
420
  createdAt: IsoDateTime,
188
421
  });
189
- export const KnowledgeLinkList = z.strictObject({ items: z.array(KnowledgeLink) });
422
+ export const NodeLinkList = z.strictObject({ items: z.array(NodeLink) });
190
423
  // The inline element a document link is, inside a BlockNote document (#41).
191
424
  //
192
425
  // ⚠️ The ID and nothing else. No title and no path travel with it: a stored title would go stale
193
426
  // the moment the target is renamed, a stored path the moment it is moved — and either one would
194
427
  // put a name the reader may not see into a document they may.
195
428
  export const DocumentLinkInlineType = "documentLink";
196
- export const ResolveKnowledgeLinksInput = z.strictObject({
429
+ export const ResolveNodeLinksInput = z.strictObject({
197
430
  nodeIds: z.array(IntelId).min(1).max(200),
198
431
  });
199
- export const ResolvedKnowledgeLink = z.strictObject({
432
+ export const ResolvedNodeLink = z.strictObject({
200
433
  nodeId: IntelId,
201
434
  title: z.string().min(1).max(240),
202
435
  });
@@ -205,15 +438,15 @@ export const ResolvedKnowledgeLink = z.strictObject({
205
438
  // side of a document link is drawn from, and an entry that says "something is here" is exactly the
206
439
  // leak this schema has to make impossible to write by accident. Deleted and unreadable therefore
207
440
  // look identical from the outside, which is the point.
208
- export const ResolveKnowledgeLinksResult = z.strictObject({
209
- items: z.array(ResolvedKnowledgeLink),
441
+ export const ResolveNodeLinksResult = z.strictObject({
442
+ items: z.array(ResolvedNodeLink),
210
443
  });
211
- export const KnowledgeGraphInput = z.strictObject({
444
+ export const NodeGraphInput = z.strictObject({
212
445
  limit: z.number().int().min(1).max(500).default(250),
213
446
  });
214
- export const KnowledgeGraph = z.strictObject({
215
- nodes: z.array(KnowledgeNode),
216
- links: z.array(KnowledgeLink),
447
+ export const NodeGraph = z.strictObject({
448
+ nodes: z.array(Node),
449
+ links: z.array(NodeLink),
217
450
  });
218
451
  export const BlockNoteMediaType = "application/vnd.anchrd.intel.blocknote+json";
219
452
  export const BlockNoteDocument = z.strictObject({
@@ -231,14 +464,14 @@ export const ResourceGrant = z.strictObject({
231
464
  createdBy: IntelId,
232
465
  createdAt: IsoDateTime,
233
466
  });
234
- export const ShareKnowledgeInput = z.strictObject({
467
+ export const ShareInput = z.strictObject({
235
468
  resourceId: IntelId,
236
469
  principal: SharePrincipal,
237
470
  verb: ResourceVerb,
238
471
  expiresAt: IsoDateTime.nullable().default(null),
239
472
  idempotencyKey: z.string().min(8).max(200),
240
473
  });
241
- export const RevokeKnowledgeGrantInput = z.strictObject({
474
+ export const RevokeGrantInput = z.strictObject({
242
475
  resourceId: IntelId,
243
476
  grantId: IntelId,
244
477
  idempotencyKey: z.string().min(8).max(200),
@@ -249,16 +482,16 @@ export const RevokeKnowledgeGrantInput = z.strictObject({
249
482
  //
250
483
  // ⚠️ `titles` holds only the documents the sharer may see; everything else is in `hidden` as a
251
484
  // number. A warning must not become a way of reading titles out of the tree.
252
- export const UnreadableKnowledge = z.strictObject({
485
+ export const UnreadableNodes = z.strictObject({
253
486
  titles: z.array(z.string().min(1).max(240)),
254
487
  hidden: z.number().int().nonnegative(),
255
488
  });
256
489
  // The grant is in the answer, so the warning cannot be mistaken for a refusal: it is written first
257
490
  // and described afterwards. Blocking would force everyone who uses a central policy document to
258
491
  // duplicate it, which is the opposite of what one tree is for (ADR-0004 §4).
259
- export const ShareKnowledgeResult = z.strictObject({
492
+ export const ShareResult = z.strictObject({
260
493
  grant: ResourceGrant,
261
- unreadable: UnreadableKnowledge,
494
+ unreadable: UnreadableNodes,
262
495
  });
263
496
  // `applicableVerbs` travels with the list because the answer is the business layer's, not the
264
497
  // screen's: a document has nothing to execute, so `execute` is not offered on one (ADR-0004 §2).
@@ -267,11 +500,16 @@ export const ResourceGrantList = z.strictObject({
267
500
  applicableVerbs: z.array(ResourceVerb).min(1),
268
501
  items: z.array(ResourceGrant),
269
502
  });
270
- export const SearchKnowledgeInput = z.strictObject({
503
+ // `scopeId` is a cut, never a grant (#126): it narrows an answer the actor is already entitled to
504
+ // and can only ever remove rows. Without it the search stays global over everything visible, which
505
+ // is why it is optional rather than nullable — an absent field and `null` would otherwise be two
506
+ // spellings of the same request.
507
+ export const SearchInput = z.strictObject({
271
508
  query: z.string().trim().min(1).max(500),
272
509
  limit: z.number().int().min(1).max(50).default(10),
510
+ scopeId: IntelId.optional().describe("Optional folder node id. When given, only nodes filed in that folder or beneath it are searched."),
273
511
  });
274
- export const KnowledgeCitation = z.strictObject({
512
+ export const NodeCitation = z.strictObject({
275
513
  nodeId: IntelId,
276
514
  versionId: IntelId,
277
515
  title: z.string(),
@@ -281,10 +519,10 @@ export const KnowledgeCitation = z.strictObject({
281
519
  score: z.number().min(0).max(1),
282
520
  match: z.enum(["lexical", "semantic", "hybrid"]),
283
521
  });
284
- export const SearchKnowledgeResult = z.strictObject({
285
- items: z.array(KnowledgeCitation),
522
+ export const SearchResult = z.strictObject({
523
+ items: z.array(NodeCitation),
286
524
  });
287
- export const ReindexKnowledgeResult = z.strictObject({ queued: z.number().int().nonnegative() });
525
+ export const ReindexResult = z.strictObject({ queued: z.number().int().nonnegative() });
288
526
  export const RevokeGrantResult = z.strictObject({ revoked: z.boolean() });
289
527
  function isPrivateIpv4(hostname) {
290
528
  const parts = hostname.split(".").map(Number);
@@ -518,7 +756,7 @@ export const FlowGraph = z.strictObject({
518
756
  });
519
757
  export const Flow = z.strictObject({
520
758
  id: IntelId,
521
- // The one thing a Flow shares with a document: its place in the Knowledge folder tree (ADR-0004).
759
+ // The one thing a Flow shares with a document: its place in the shared folder tree (ADR-0004).
522
760
  // Everything else stays apart — versions, R2 body and Vectorize belong to the document, the graph,
523
761
  // runs and approvals to the flow. `null` is the root of that same tree.
524
762
  parentId: IntelId.nullable(),
@@ -543,30 +781,52 @@ export const FlowDocument = z.strictObject({
543
781
  flow: Flow,
544
782
  version: FlowVersion.nullable(),
545
783
  });
546
- // Dasselbe für Flows: welche von ihnen einen anderen Flow rufen, den dieser Leser auch sehen darf
547
- // (#59). Ein Aufklapp-Pfeil an einem Flow, dessen Aufrufe alle verborgen sind, verspricht Inhalt,
548
- // den das Aufklappen nicht liefern kann.
784
+ // One version as the history shows it: the metadata without the graph it carries. A flow's history
785
+ // is as long as its edits, and a list that shipped every graph would pay for drawings nobody asked
786
+ // for whoever needs one asks for that one version.
787
+ export const FlowVersionSummary = z.strictObject({
788
+ id: IntelId,
789
+ flowId: IntelId,
790
+ sequence: z.number().int().positive(),
791
+ createdBy: IntelId,
792
+ createdAt: IsoDateTime,
793
+ // Whether this is the version the flow currently publishes. Derived from the flow row when the
794
+ // list is read, never stored on the version: a version is immutable and "published" is not a
795
+ // property of it — it is the flow's choice, revocable without touching the version.
796
+ published: z.boolean(),
797
+ });
798
+ export const FlowVersionList = z.strictObject({
799
+ flowId: IntelId,
800
+ items: z.array(FlowVersionSummary),
801
+ });
802
+ // Both identifiers, deliberately: a version ID alone would resolve whatever version carries it,
803
+ // whichever flow it belongs to, and the ACL is answered on the flow. The pair makes a foreign
804
+ // version a 404 rather than a read.
805
+ export const GetFlowVersionInput = z.strictObject({ flowId: IntelId, versionId: IntelId });
806
+ // The same for flows: which of them call another flow that this reader may also see (#59). An
807
+ // expand arrow on a flow whose calls are all hidden promises content that expanding it cannot
808
+ // deliver.
549
809
  export const FlowList = z.strictObject({
550
810
  items: z.array(Flow),
551
811
  withCalls: z.array(IntelId).default([]),
552
812
  });
553
- export const FlowKnowledgeReference = z.strictObject({
813
+ export const ReferencedNode = z.strictObject({
554
814
  id: IntelId,
555
815
  title: z.string().min(1).max(240),
556
816
  });
557
- // What a flow touches: the documents its Knowledge steps name and the tools its Tool steps call,
817
+ // What a flow touches: the documents its tree links name and the tools its Tool steps call,
558
818
  // read straight out of the graph. Deliberately not a conflict report — there is no arithmetic here
559
819
  // and nothing that can go stale, because the graph is the answer. Whether a given person may reach
560
820
  // any of it is decided where it can be decided honestly: when the folder is shared, and at runtime
561
821
  // (ADR-0004 §4). For tools it can only ever be the latter, because the catalog is a live query with
562
822
  // the requesting user's own token (ADR-0003).
563
823
  //
564
- // ⚠️ `knowledge` names only what the asking user may see. The rest is `hiddenKnowledge`, a count.
824
+ // ⚠️ `nodes` names only what the asking user may see. The rest is `hiddenNodes`, a count.
565
825
  export const FlowRequirements = z.strictObject({
566
826
  flowId: IntelId,
567
827
  versionId: IntelId.nullable(),
568
- knowledge: z.array(FlowKnowledgeReference),
569
- hiddenKnowledge: z.number().int().nonnegative(),
828
+ nodes: z.array(ReferencedNode),
829
+ hiddenNodes: z.number().int().nonnegative(),
570
830
  tools: z.array(ToolName),
571
831
  });
572
832
  // What stands between this flow and a run, asked on demand and answered for the person asking.
@@ -622,11 +882,13 @@ export const ListFlowsInput = z.strictObject({
622
882
  // behind the relation graph has no such flag on purpose (#30). A drawing that includes what was
623
883
  // archived says the tidying up never happened.
624
884
  //
625
- // ⚠️ `.optional()` rather than `.default(false)`, unlike `ListKnowledgeNodesInput`. This schema is
885
+ // ⚠️ `.optional()` rather than `.default(false)`, unlike `ListNodesInput`. This schema is
626
886
  // the argument type of `listFlows` on three layers, and a default makes the field required in the
627
887
  // *parsed* type — every existing caller that lists a folder would have to spell out the answer to
628
888
  // a question it is not asking. Absent means "without the archive" everywhere it is read.
629
889
  includeArchived: z.boolean().optional(),
890
+ // The same question for flows, and the same override of `parentId` — see `ListNodesInput`.
891
+ archivedOnly: z.boolean().optional(),
630
892
  });
631
893
  export const GetFlowInput = z.strictObject({ flowId: IntelId });
632
894
  export const SaveFlowVersionInput = z.strictObject({
@@ -640,6 +902,13 @@ export const PublishFlowInput = z.strictObject({
640
902
  versionId: IntelId,
641
903
  idempotencyKey: z.string().min(8).max(200),
642
904
  });
905
+ // The way back out of a publication (#146). No versionId: what is withdrawn is whatever is
906
+ // published now, and naming one would invite a race between reading it and revoking it. Versions
907
+ // are untouched — republishing any of them is one `publish` away.
908
+ export const UnpublishFlowInput = z.strictObject({
909
+ flowId: IntelId,
910
+ idempotencyKey: z.string().min(8).max(200),
911
+ });
643
912
  export const PreviewFlowPublishInput = z.strictObject({ flowId: IntelId, versionId: IntelId });
644
913
  // One sub-flow call as publishing will leave it (ADR-0004 §5). `freezes` is the whole point of the
645
914
  // preview: it marks the calls whose `latest` publishing turns into `versionId`, so the author reads
@@ -672,7 +941,14 @@ export const FlowPublishPreview = z.strictObject({
672
941
  // What accesses what, for one level of the shared tree (#19). A folder answers it for its contents,
673
942
  // a single flow for itself. Documents and flows are two kinds of thing that share one tree
674
943
  // (ADR-0004 §1), so the graph carries both and says which of them it is.
675
- export const RelationNodeKind = z.enum(["folder", "document", "attachment", "table", "flow"]);
944
+ export const RelationNodeKind = z.enum([
945
+ "folder",
946
+ "document",
947
+ "attachment",
948
+ "table",
949
+ "agent",
950
+ "flow",
951
+ ]);
676
952
  export const RelationNode = z.strictObject({
677
953
  id: IntelId,
678
954
  kind: RelationNodeKind,
@@ -757,6 +1033,13 @@ export const StartFlowRunInput = z.strictObject({
757
1033
  idempotencyKey: z.string().min(8).max(200),
758
1034
  });
759
1035
  export const GetFlowRunInput = z.strictObject({ runId: IntelId });
1036
+ // Ending a run on purpose (#145). Until this existed the only way off a parked manual step was
1037
+ // `completeStep` with `outcome: "failed"` — which recorded a step failure that never happened.
1038
+ // Cancelling records nothing into the step history: the run ends, the history stays true.
1039
+ export const CancelFlowRunInput = z.strictObject({
1040
+ runId: IntelId,
1041
+ idempotencyKey: z.string().min(8).max(200),
1042
+ });
760
1043
  export const CompleteFlowRunStepInput = z.strictObject({
761
1044
  runId: IntelId,
762
1045
  nodeId: FlowNodeId,
@@ -791,7 +1074,7 @@ export const FlowRunFailure = z.strictObject({
791
1074
  });
792
1075
  // One run as a list shows it: what it did, never what it produced.
793
1076
  //
794
- // ⚠️ Neither `input` nor `output` is in here, on purpose. A run reaches its Knowledge and its tools
1077
+ // ⚠️ Neither `input` nor `output` is in here, on purpose. A run reaches its nodes and its tools
795
1078
  // with the rights of whoever started it, so its result is a way to content the next reader of this
796
1079
  // list may have no claim to. Whoever wants a result asks for the run itself, where the same rule
797
1080
  // decides again.
@@ -844,3 +1127,51 @@ export const FlowRunHistory = z.strictObject({
844
1127
  steps: z.array(FlowRunStepRecord),
845
1128
  trail: z.array(FlowRunTrailEntry),
846
1129
  });
1130
+ // ── Bundle export (#136) ────────────────────────────────────────────────────────────────────────
1131
+ // The one name the importer looks for at the zip root. A different spelling would make a bundle a
1132
+ // naked folder, so the constant lives in the contract rather than in each surface.
1133
+ export const BundleManifestFilename = "manifest.json";
1134
+ // What a bundle entry can be. `flow` joins the five node kinds because a flow shares the folder
1135
+ // tree without being a node (ADR-0004), and the bundle mirrors the tree, not the tables.
1136
+ export const BundleEntryKind = z.enum([
1137
+ "folder",
1138
+ "document",
1139
+ "table",
1140
+ "attachment",
1141
+ "agent",
1142
+ "flow",
1143
+ ]);
1144
+ // One entry of the manifest: the identity a re-import needs, next to the relative path where the
1145
+ // bytes sit in the zip. A folder carries no media type — it has no bytes.
1146
+ export const BundleManifestEntry = z.strictObject({
1147
+ id: IntelId,
1148
+ kind: BundleEntryKind,
1149
+ title: z.string().min(1).max(240),
1150
+ description: z.string().max(2_000).nullable(),
1151
+ mediaType: z.string().min(1).max(160).nullable(),
1152
+ // Relative to the zip root, forward slashes, no leading slash. Folders end with a slash so an
1153
+ // empty folder still has an address.
1154
+ path: z.string().min(1).max(4_000),
1155
+ });
1156
+ // What an export leaves out on purpose, named so a bundle says it rather than a reader guessing:
1157
+ // version history, grants/shares, flow runs, and archived nodes are not in any bundle (#136).
1158
+ export const BundleExclusion = z.enum(["version-history", "grants", "flow-runs", "archived-nodes"]);
1159
+ export const BundleManifest = z.strictObject({
1160
+ version: z.literal(1),
1161
+ exportedAt: IsoDateTime,
1162
+ // The node the export started at; `null` is the root of the tree — the whole installation as the
1163
+ // exporting caller may read it.
1164
+ rootId: IntelId.nullable(),
1165
+ entries: z.array(BundleManifestEntry),
1166
+ excluded: z.array(BundleExclusion),
1167
+ });
1168
+ // ── Bundle import (#137) ────────────────────────────────────────────────────────────────────────
1169
+ // What one import made. Import always creates new nodes — no merge, no overwrite, no restored IDs
1170
+ // (#137, phase 1) — so the answer is counts and the new roots, never a diff. `replayed` marks the
1171
+ // idempotent second answer to the same key: nothing was created twice.
1172
+ export const BundleImportResult = z.strictObject({
1173
+ nodes: z.number().int().nonnegative(),
1174
+ flows: z.number().int().nonnegative(),
1175
+ rootNodeIds: z.array(IntelId),
1176
+ replayed: z.boolean(),
1177
+ });