@anchrd/intel-contract 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contract/contract.d.ts +659 -159
- package/dist/contract/contract.js +375 -39
- package/package.json +1 -1
|
@@ -9,9 +9,21 @@ export const ProblemDetails = z.strictObject({
|
|
|
9
9
|
instance: z.string().optional(),
|
|
10
10
|
code: z.string().optional(),
|
|
11
11
|
});
|
|
12
|
-
|
|
12
|
+
// Who the caller is, as Gate resolved it from the bearer. Deliberately identity only: no
|
|
13
|
+
// capabilities, no token, nothing a surface could mistake for a permission.
|
|
14
|
+
export const SessionUser = z.strictObject({
|
|
15
|
+
id: IntelId,
|
|
16
|
+
email: z.email(),
|
|
17
|
+
name: z.string().min(1).max(240).nullable(),
|
|
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"]);
|
|
13
23
|
export const ContextPolicy = z.enum(["pinned", "relevant", "explicit"]);
|
|
14
|
-
|
|
24
|
+
// One verb per grant, granted independently (ADR-0004 §2). Not a ladder: seeing a process must be
|
|
25
|
+
// separable from being allowed to start it, and `execute` is meaningful only where a flow can live.
|
|
26
|
+
export const ResourceVerb = z.enum(["read", "write", "execute", "share"]);
|
|
15
27
|
export const SharePrincipal = z.discriminatedUnion("type", [
|
|
16
28
|
z.strictObject({ type: z.literal("user"), id: IntelId }),
|
|
17
29
|
z.strictObject({ type: z.literal("email"), email: z.email() }),
|
|
@@ -101,32 +113,94 @@ export const KnowledgeAttachment = z.strictObject({
|
|
|
101
113
|
version: KnowledgeVersion,
|
|
102
114
|
resourceUri: z.string().regex(/^intel:\/\/knowledge\/[^/]+\/attachment$/),
|
|
103
115
|
});
|
|
116
|
+
// A table is CSV, and CSV is the whole format: it is what is stored, what is downloaded and what a
|
|
117
|
+
// machine reads. There is no second representation to keep in step with it (#40).
|
|
118
|
+
export const TableMediaType = "text/csv";
|
|
119
|
+
// A column name is the contract between the table and everyone who appends to it, so it is trimmed,
|
|
120
|
+
// non-empty and bounded like a title. Cells are not: a cell is text, and text is what CSV carries.
|
|
121
|
+
export const TableColumn = z.string().trim().min(1).max(120);
|
|
122
|
+
export const TableCell = z.string().max(4_000);
|
|
123
|
+
export const TableRow = z.array(TableCell).min(1).max(64);
|
|
124
|
+
// Writing the header, once. The columns are the contract (#40's comment), which is why this refuses
|
|
125
|
+
// on a table that already has one: changing the header would silently reinterpret every row that
|
|
126
|
+
// was appended under the old one.
|
|
127
|
+
export const DefineKnowledgeTableInput = z.strictObject({
|
|
128
|
+
nodeId: IntelId,
|
|
129
|
+
columns: z
|
|
130
|
+
.array(TableColumn)
|
|
131
|
+
.min(1)
|
|
132
|
+
.max(64)
|
|
133
|
+
.refine((columns) => new Set(columns.map((column) => column.toLowerCase())).size === columns.length, { error: "Column names must be distinct" }),
|
|
134
|
+
idempotencyKey: z.string().min(8).max(200),
|
|
135
|
+
});
|
|
136
|
+
// ⚠️ No `baseVersionId`, and that absence is the feature. A document replaces its content and needs
|
|
137
|
+
// to know which content it replaces; an append adds to the end and cannot collide with a second
|
|
138
|
+
// append, so demanding a base version would invent a conflict that does not exist and force the
|
|
139
|
+
// caller to read the whole table first — the exact cost #40 exists to remove.
|
|
140
|
+
export const AppendKnowledgeTableRowsInput = z.strictObject({
|
|
141
|
+
nodeId: IntelId,
|
|
142
|
+
rows: z.array(TableRow).min(1).max(1_000),
|
|
143
|
+
idempotencyKey: z.string().min(8).max(200),
|
|
144
|
+
});
|
|
145
|
+
export const GetKnowledgeTableInput = z.strictObject({ nodeId: IntelId });
|
|
146
|
+
// The table as a grid rather than as text: the server owns the one CSV reader, so no surface has to
|
|
147
|
+
// grow a second one that would disagree with it about quoting.
|
|
148
|
+
export const KnowledgeTable = z.strictObject({
|
|
149
|
+
node: KnowledgeNode,
|
|
150
|
+
columns: z.array(z.string()),
|
|
151
|
+
rows: z.array(z.array(z.string())),
|
|
152
|
+
// The newest append, or `null` while the table has no header yet.
|
|
153
|
+
versionId: IntelId.nullable(),
|
|
154
|
+
});
|
|
155
|
+
export const AppendKnowledgeTableRowsResult = z.strictObject({
|
|
156
|
+
node: KnowledgeNode,
|
|
157
|
+
version: KnowledgeVersion,
|
|
158
|
+
appended: z.number().int().positive(),
|
|
159
|
+
});
|
|
160
|
+
// ⚠️ Kept for what is already stored, not for what is written. Relations were picked in a dialog
|
|
161
|
+
// until #41; a link is now made where it is meant — in the text — and every link written from now
|
|
162
|
+
// on is a `references`. Rewriting the old rows would destroy a distinction somebody chose on
|
|
163
|
+
// purpose, and dropping the column would destroy it with them, so both stay readable.
|
|
104
164
|
export const KnowledgeLinkRelation = z.enum(["references", "related", "depends_on", "implements"]);
|
|
165
|
+
// Where the link came from. `text` links are derived from a document's content and are rewritten
|
|
166
|
+
// whenever it is saved; `manual` links were made in the dialog #41 removed and are now history.
|
|
167
|
+
//
|
|
168
|
+
// ⚠️ This is provenance, never a second sort of relationship. Nothing offers the reader a choice
|
|
169
|
+
// between them, and nothing may start writing `manual` again — that would be the two ways of saying
|
|
170
|
+
// one thing that #41 exists to end. It exists so that saving a document cannot delete a link
|
|
171
|
+
// somebody made before there was another way to make one.
|
|
172
|
+
export const KnowledgeLinkOrigin = z.enum(["text", "manual"]);
|
|
105
173
|
export const KnowledgeLink = z.strictObject({
|
|
106
174
|
id: IntelId,
|
|
107
175
|
sourceNodeId: IntelId,
|
|
108
176
|
targetNodeId: IntelId,
|
|
109
177
|
relation: KnowledgeLinkRelation,
|
|
178
|
+
origin: KnowledgeLinkOrigin,
|
|
110
179
|
label: z.string().trim().min(1).max(120).nullable(),
|
|
111
180
|
createdBy: IntelId,
|
|
112
181
|
createdAt: IsoDateTime,
|
|
113
182
|
});
|
|
114
183
|
export const KnowledgeLinkList = z.strictObject({ items: z.array(KnowledgeLink) });
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
184
|
+
// The inline element a document link is, inside a BlockNote document (#41).
|
|
185
|
+
//
|
|
186
|
+
// ⚠️ The ID and nothing else. No title and no path travel with it: a stored title would go stale
|
|
187
|
+
// the moment the target is renamed, a stored path the moment it is moved — and either one would
|
|
188
|
+
// put a name the reader may not see into a document they may.
|
|
189
|
+
export const DocumentLinkInlineType = "documentLink";
|
|
190
|
+
export const ResolveKnowledgeLinksInput = z.strictObject({
|
|
191
|
+
nodeIds: z.array(IntelId).min(1).max(200),
|
|
192
|
+
});
|
|
193
|
+
export const ResolvedKnowledgeLink = z.strictObject({
|
|
194
|
+
nodeId: IntelId,
|
|
195
|
+
title: z.string().min(1).max(240),
|
|
125
196
|
});
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
197
|
+
// ⚠️ Only what the asking reader may see is in here, and an unreachable target is simply absent —
|
|
198
|
+
// never a row with an empty title, never a count, never a "restricted" marker. The list is what one
|
|
199
|
+
// side of a document link is drawn from, and an entry that says "something is here" is exactly the
|
|
200
|
+
// leak this schema has to make impossible to write by accident. Deleted and unreadable therefore
|
|
201
|
+
// look identical from the outside, which is the point.
|
|
202
|
+
export const ResolveKnowledgeLinksResult = z.strictObject({
|
|
203
|
+
items: z.array(ResolvedKnowledgeLink),
|
|
130
204
|
});
|
|
131
205
|
export const KnowledgeGraphInput = z.strictObject({
|
|
132
206
|
limit: z.number().int().min(1).max(500).default(250),
|
|
@@ -146,7 +220,7 @@ export const ResourceGrant = z.strictObject({
|
|
|
146
220
|
id: IntelId,
|
|
147
221
|
resourceId: IntelId,
|
|
148
222
|
principal: SharePrincipal,
|
|
149
|
-
|
|
223
|
+
verb: ResourceVerb,
|
|
150
224
|
expiresAt: IsoDateTime.nullable(),
|
|
151
225
|
createdBy: IntelId,
|
|
152
226
|
createdAt: IsoDateTime,
|
|
@@ -154,7 +228,7 @@ export const ResourceGrant = z.strictObject({
|
|
|
154
228
|
export const ShareKnowledgeInput = z.strictObject({
|
|
155
229
|
resourceId: IntelId,
|
|
156
230
|
principal: SharePrincipal,
|
|
157
|
-
|
|
231
|
+
verb: ResourceVerb,
|
|
158
232
|
expiresAt: IsoDateTime.nullable().default(null),
|
|
159
233
|
idempotencyKey: z.string().min(8).max(200),
|
|
160
234
|
});
|
|
@@ -163,7 +237,30 @@ export const RevokeKnowledgeGrantInput = z.strictObject({
|
|
|
163
237
|
grantId: IntelId,
|
|
164
238
|
idempotencyKey: z.string().min(8).max(200),
|
|
165
239
|
});
|
|
166
|
-
|
|
240
|
+
// What a grant does not cover, reported to whoever just made it. A flow in the shared folder may
|
|
241
|
+
// read a document outside it, and the run is re-authorized against the person running it — so the
|
|
242
|
+
// grant can be complete and the flow still stop for them (ADR-0004 §4).
|
|
243
|
+
//
|
|
244
|
+
// ⚠️ `titles` holds only the documents the sharer may see; everything else is in `hidden` as a
|
|
245
|
+
// number. A warning must not become a way of reading titles out of the tree.
|
|
246
|
+
export const UnreadableKnowledge = z.strictObject({
|
|
247
|
+
titles: z.array(z.string().min(1).max(240)),
|
|
248
|
+
hidden: z.number().int().nonnegative(),
|
|
249
|
+
});
|
|
250
|
+
// The grant is in the answer, so the warning cannot be mistaken for a refusal: it is written first
|
|
251
|
+
// and described afterwards. Blocking would force everyone who uses a central policy document to
|
|
252
|
+
// duplicate it, which is the opposite of what one tree is for (ADR-0004 §4).
|
|
253
|
+
export const ShareKnowledgeResult = z.strictObject({
|
|
254
|
+
grant: ResourceGrant,
|
|
255
|
+
unreadable: UnreadableKnowledge,
|
|
256
|
+
});
|
|
257
|
+
// `applicableVerbs` travels with the list because the answer is the business layer's, not the
|
|
258
|
+
// screen's: a document has nothing to execute, so `execute` is not offered on one (ADR-0004 §2).
|
|
259
|
+
export const ResourceGrantList = z.strictObject({
|
|
260
|
+
resourceId: IntelId,
|
|
261
|
+
applicableVerbs: z.array(ResourceVerb).min(1),
|
|
262
|
+
items: z.array(ResourceGrant),
|
|
263
|
+
});
|
|
167
264
|
export const SearchKnowledgeInput = z.strictObject({
|
|
168
265
|
query: z.string().trim().min(1).max(500),
|
|
169
266
|
limit: z.number().int().min(1).max(50).default(10),
|
|
@@ -183,7 +280,6 @@ export const SearchKnowledgeResult = z.strictObject({
|
|
|
183
280
|
});
|
|
184
281
|
export const ReindexKnowledgeResult = z.strictObject({ queued: z.number().int().nonnegative() });
|
|
185
282
|
export const RevokeGrantResult = z.strictObject({ revoked: z.boolean() });
|
|
186
|
-
export const DeleteKnowledgeLinkResult = z.strictObject({ deleted: z.boolean() });
|
|
187
283
|
function isPrivateIpv4(hostname) {
|
|
188
284
|
const parts = hostname.split(".").map(Number);
|
|
189
285
|
if (parts.length !== 4 ||
|
|
@@ -265,21 +361,42 @@ export const ToolTestResult = z.strictObject({
|
|
|
265
361
|
});
|
|
266
362
|
export const FlowNodeId = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/);
|
|
267
363
|
export const FlowPosition = z.strictObject({ x: z.number().finite(), y: z.number().finite() });
|
|
364
|
+
// A node has a title and nothing else to write in prose (#39). A second free text beside it was
|
|
365
|
+
// kept half up to date on both sides, and for an instruction the same sentence already belongs in
|
|
366
|
+
// the instruction itself.
|
|
268
367
|
const FlowNodeBase = {
|
|
269
368
|
id: FlowNodeId,
|
|
270
369
|
position: FlowPosition,
|
|
271
370
|
label: z.string().trim().min(1).max(160),
|
|
272
|
-
description: z.string().trim().max(2_000).nullable().default(null),
|
|
273
371
|
};
|
|
372
|
+
// Which version of the callee a sub-flow call takes (ADR-0004 §5). Three states rather than an
|
|
373
|
+
// optional identifier, because the difference between them is a decision and has to be readable:
|
|
374
|
+
//
|
|
375
|
+
// - `latest` the draft default. Nobody should have to version things while building, and
|
|
376
|
+
// publishing turns this into `pinned` — visibly, before the author publishes.
|
|
377
|
+
// - `follows` "always latest", chosen on purpose. The call rides along with the callee, so a
|
|
378
|
+
// change to the building block changes this flow too. Publishing leaves it alone.
|
|
379
|
+
// - `pinned` one immutable version, whatever is published elsewhere.
|
|
380
|
+
//
|
|
381
|
+
// ⚠️ Without the freeze a change to a building block would silently change the behavior of every
|
|
382
|
+
// published flow using it, which contradicts the immutable versions and pinned schema fingerprints
|
|
383
|
+
// Intel otherwise guarantees. That is why `latest` cannot survive publishing.
|
|
384
|
+
export const SubflowVersionMode = z.enum(["latest", "follows", "pinned"]);
|
|
385
|
+
export const SubflowVersion = z.discriminatedUnion("mode", [
|
|
386
|
+
z.strictObject({ mode: z.literal("latest") }),
|
|
387
|
+
z.strictObject({ mode: z.literal("follows") }),
|
|
388
|
+
z.strictObject({ mode: z.literal("pinned"), versionId: IntelId }),
|
|
389
|
+
]);
|
|
274
390
|
export const FlowNode = z.discriminatedUnion("kind", [
|
|
275
391
|
z.strictObject({
|
|
276
392
|
...FlowNodeBase,
|
|
277
393
|
kind: z.literal("trigger"),
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
394
|
+
// ⚠️ `manual` is the only mode there is. `webhook` and `schedule` stood here and fired nothing:
|
|
395
|
+
// a flow is carried out by an external agent that brings its own schedule, which is what the
|
|
396
|
+
// code does rather than what it intends — `step()` hands back the current node, `completeStep`
|
|
397
|
+
// takes the result from outside, and the Cloudflare workflow waits rather than drives. #32 was
|
|
398
|
+
// closed on that basis, and a mode nothing triggers is a promise nobody keeps.
|
|
399
|
+
configuration: z.strictObject({ mode: z.literal("manual") }),
|
|
283
400
|
}),
|
|
284
401
|
z.strictObject({
|
|
285
402
|
...FlowNodeBase,
|
|
@@ -324,16 +441,37 @@ export const FlowNode = z.discriminatedUnion("kind", [
|
|
|
324
441
|
timeout: z.string().regex(/^\d+\s+(minute|minutes|hour|hours|day|days)$/),
|
|
325
442
|
}),
|
|
326
443
|
}),
|
|
444
|
+
// The eighth kind: one flow calls another (ADR-0004 §3). A schema addition, not hidden behavior in
|
|
445
|
+
// a generic code node — which flow is called has to be readable from the graph, or neither the
|
|
446
|
+
// publish-time call rule nor the sidebar could see it.
|
|
447
|
+
z.strictObject({
|
|
448
|
+
...FlowNodeBase,
|
|
449
|
+
kind: z.literal("subflow"),
|
|
450
|
+
// What the called flow is given travels through `StartFlowRunInput.input` — the schema every run
|
|
451
|
+
// already uses. A second, static input here would be a promise the execution does not keep.
|
|
452
|
+
configuration: z.strictObject({
|
|
453
|
+
flowId: IntelId,
|
|
454
|
+
version: SubflowVersion.default({ mode: "latest" }),
|
|
455
|
+
}),
|
|
456
|
+
}),
|
|
327
457
|
z.strictObject({
|
|
328
458
|
...FlowNodeBase,
|
|
329
459
|
kind: z.literal("output"),
|
|
330
460
|
configuration: z.strictObject({ template: z.string().max(50_000).default("") }),
|
|
331
461
|
}),
|
|
332
462
|
]);
|
|
463
|
+
// The two things an edge can mean (#37). `flow` is the order of work — "and then". `context` is what
|
|
464
|
+
// a step works with: a document consulted in exactly this step, an approval obtained in exactly this
|
|
465
|
+
// step, or, at the output, the table a result is written to.
|
|
466
|
+
//
|
|
467
|
+
// ⚠️ `flow` is the default, and that is the whole of the migration: every edge stored before this
|
|
468
|
+
// existed parses into the meaning it already had. Nothing about saved graphs has to be rewritten.
|
|
469
|
+
export const FlowEdgeKind = z.enum(["flow", "context"]);
|
|
333
470
|
export const FlowEdge = z.strictObject({
|
|
334
471
|
id: FlowNodeId,
|
|
335
472
|
source: FlowNodeId,
|
|
336
473
|
target: FlowNodeId,
|
|
474
|
+
kind: FlowEdgeKind.default("flow"),
|
|
337
475
|
label: z.string().trim().min(1).max(120).nullable().default(null),
|
|
338
476
|
sourceHandle: z.string().trim().min(1).max(120).nullable().default(null),
|
|
339
477
|
});
|
|
@@ -343,6 +481,10 @@ export const FlowGraph = z.strictObject({
|
|
|
343
481
|
});
|
|
344
482
|
export const Flow = z.strictObject({
|
|
345
483
|
id: IntelId,
|
|
484
|
+
// The one thing a Flow shares with a document: its place in the Knowledge folder tree (ADR-0004).
|
|
485
|
+
// Everything else stays apart — versions, R2 body and Vectorize belong to the document, the graph,
|
|
486
|
+
// runs and approvals to the flow. `null` is the root of that same tree.
|
|
487
|
+
parentId: IntelId.nullable(),
|
|
346
488
|
title: z.string().min(1).max(240),
|
|
347
489
|
description: z.string().max(2_000).nullable(),
|
|
348
490
|
ownerId: IntelId,
|
|
@@ -365,11 +507,50 @@ export const FlowDocument = z.strictObject({
|
|
|
365
507
|
version: FlowVersion.nullable(),
|
|
366
508
|
});
|
|
367
509
|
export const FlowList = z.strictObject({ items: z.array(Flow) });
|
|
510
|
+
export const FlowKnowledgeReference = z.strictObject({
|
|
511
|
+
id: IntelId,
|
|
512
|
+
title: z.string().min(1).max(240),
|
|
513
|
+
});
|
|
514
|
+
// What a flow touches: the documents its Knowledge steps name and the tools its Tool steps call,
|
|
515
|
+
// read straight out of the graph. Deliberately not a conflict report — there is no arithmetic here
|
|
516
|
+
// and nothing that can go stale, because the graph is the answer. Whether a given person may reach
|
|
517
|
+
// any of it is decided where it can be decided honestly: when the folder is shared, and at runtime
|
|
518
|
+
// (ADR-0004 §4). For tools it can only ever be the latter, because the catalog is a live query with
|
|
519
|
+
// the requesting user's own token (ADR-0003).
|
|
520
|
+
//
|
|
521
|
+
// ⚠️ `knowledge` names only what the asking user may see. The rest is `hiddenKnowledge`, a count.
|
|
522
|
+
export const FlowRequirements = z.strictObject({
|
|
523
|
+
flowId: IntelId,
|
|
524
|
+
versionId: IntelId.nullable(),
|
|
525
|
+
knowledge: z.array(FlowKnowledgeReference),
|
|
526
|
+
hiddenKnowledge: z.number().int().nonnegative(),
|
|
527
|
+
tools: z.array(ToolName),
|
|
528
|
+
});
|
|
368
529
|
export const CreateFlowInput = z.strictObject({
|
|
530
|
+
parentId: IntelId.nullable().default(null),
|
|
369
531
|
title: z.string().trim().min(1).max(240),
|
|
370
532
|
description: z.string().trim().max(2_000).nullable().default(null),
|
|
371
533
|
idempotencyKey: z.string().min(8).max(200),
|
|
372
534
|
});
|
|
535
|
+
// Renaming and moving a flow. Both are organization and nothing else: they touch no version, no
|
|
536
|
+
// published graph and no run, because organization has to stay free of consequence or nobody dares
|
|
537
|
+
// to reorganize (ADR-0004).
|
|
538
|
+
export const UpdateFlowInput = z
|
|
539
|
+
.strictObject({
|
|
540
|
+
flowId: IntelId,
|
|
541
|
+
baseUpdatedAt: IsoDateTime,
|
|
542
|
+
title: z.string().trim().min(1).max(240).optional(),
|
|
543
|
+
description: z.string().trim().max(2_000).nullable().optional(),
|
|
544
|
+
parentId: IntelId.nullable().optional(),
|
|
545
|
+
idempotencyKey: z.string().min(8).max(200),
|
|
546
|
+
})
|
|
547
|
+
.refine((input) => input.title !== undefined || input.description !== undefined || input.parentId !== undefined, { error: "At least one change is required" });
|
|
548
|
+
// Three answers, not two: an absent `parentId` lists every visible flow (the search dialog asks
|
|
549
|
+
// that), `null` lists the root of the shared tree and an ID lists one folder (the sidebar tree asks
|
|
550
|
+
// per level, which is what keeps the tree off the N+1 it used to load with).
|
|
551
|
+
export const ListFlowsInput = z.strictObject({
|
|
552
|
+
parentId: IntelId.nullable().optional(),
|
|
553
|
+
});
|
|
373
554
|
export const GetFlowInput = z.strictObject({ flowId: IntelId });
|
|
374
555
|
export const SaveFlowVersionInput = z.strictObject({
|
|
375
556
|
flowId: IntelId,
|
|
@@ -382,18 +563,73 @@ export const PublishFlowInput = z.strictObject({
|
|
|
382
563
|
versionId: IntelId,
|
|
383
564
|
idempotencyKey: z.string().min(8).max(200),
|
|
384
565
|
});
|
|
385
|
-
export const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
566
|
+
export const PreviewFlowPublishInput = z.strictObject({ flowId: IntelId, versionId: IntelId });
|
|
567
|
+
// One sub-flow call as publishing will leave it (ADR-0004 §5). `freezes` is the whole point of the
|
|
568
|
+
// preview: it marks the calls whose `latest` publishing turns into `versionId`, so the author reads
|
|
569
|
+
// the decision before making it rather than after.
|
|
570
|
+
//
|
|
571
|
+
// ⚠️ Only callees the asking actor may reach are listed at all. A call whose callee they cannot see
|
|
572
|
+
// is left out rather than named, because a title is the thing an unreachable flow must not hand out.
|
|
573
|
+
export const FlowPublishCall = z.strictObject({
|
|
574
|
+
nodeId: FlowNodeId,
|
|
575
|
+
nodeLabel: z.string().min(1).max(160),
|
|
576
|
+
calleeId: IntelId,
|
|
577
|
+
calleeTitle: z.string().min(1).max(240),
|
|
578
|
+
mode: SubflowVersionMode,
|
|
579
|
+
// The version this call will take once published. `null` when it follows the callee, which is the
|
|
580
|
+
// one case where the answer is only known at run time.
|
|
581
|
+
versionId: IntelId.nullable(),
|
|
582
|
+
versionSequence: z.number().int().positive().nullable(),
|
|
583
|
+
freezes: z.boolean(),
|
|
584
|
+
// The callee has a published version to be called at all. `false` is what publishing will refuse.
|
|
585
|
+
available: z.boolean(),
|
|
586
|
+
});
|
|
587
|
+
export const FlowPublishPreview = z.strictObject({
|
|
588
|
+
flowId: IntelId,
|
|
589
|
+
versionId: IntelId,
|
|
590
|
+
calls: z.array(FlowPublishCall),
|
|
591
|
+
});
|
|
592
|
+
// A flow has no share schema of its own. A grant sits on the folder a flow is filed in and inherits
|
|
593
|
+
// down from there (ADR-0004 §2); a narrower grant beside it would destroy the subtree guarantee
|
|
594
|
+
// section 3 rests on, so per-flow grants were removed rather than deprecated.
|
|
595
|
+
// What accesses what, for one level of the shared tree (#19). A folder answers it for its contents,
|
|
596
|
+
// a single flow for itself. Documents and flows are two kinds of thing that share one tree
|
|
597
|
+
// (ADR-0004 §1), so the graph carries both and says which of them it is.
|
|
598
|
+
export const RelationNodeKind = z.enum(["folder", "document", "attachment", "table", "flow"]);
|
|
599
|
+
export const RelationNode = z.strictObject({
|
|
600
|
+
id: IntelId,
|
|
601
|
+
kind: RelationNodeKind,
|
|
602
|
+
title: z.string().min(1).max(240),
|
|
603
|
+
// Inside the level being shown, rather than something it reaches out to. A flow reading a policy
|
|
604
|
+
// document from another folder pulls that document in, and the difference should be legible.
|
|
605
|
+
inScope: z.boolean(),
|
|
606
|
+
});
|
|
607
|
+
export const RelationEdge = z.strictObject({
|
|
608
|
+
id: z.string().min(1).max(400),
|
|
609
|
+
source: IntelId,
|
|
610
|
+
target: IntelId,
|
|
611
|
+
relation: z.enum(["reads", "calls"]),
|
|
612
|
+
});
|
|
613
|
+
export const RelationGraphScope = z.discriminatedUnion("of", [
|
|
614
|
+
z.strictObject({ of: z.literal("folder"), folderId: IntelId.nullable() }),
|
|
615
|
+
z.strictObject({ of: z.literal("flow"), flowId: IntelId }),
|
|
616
|
+
]);
|
|
617
|
+
export const RelationGraphInput = z.strictObject({
|
|
618
|
+
scope: RelationGraphScope,
|
|
619
|
+
// How much is drawn before the answer is summarized instead. A big folder has to stay usable, and
|
|
620
|
+
// the cut-off is reported rather than swallowed.
|
|
621
|
+
limit: z.number().int().min(1).max(300).default(60),
|
|
622
|
+
});
|
|
623
|
+
// ⚠️ Only what the asking user may see is in here. A node they may not reach is absent, not greyed
|
|
624
|
+
// out and not counted: an edge to a placeholder would already tell them the thing exists, which is
|
|
625
|
+
// the leak this schema has to make impossible to write by accident. `omitted` is about the size
|
|
626
|
+
// limit alone, never about permissions.
|
|
627
|
+
export const RelationGraph = z.strictObject({
|
|
628
|
+
scope: RelationGraphScope,
|
|
629
|
+
nodes: z.array(RelationNode),
|
|
630
|
+
edges: z.array(RelationEdge),
|
|
631
|
+
omitted: z.number().int().nonnegative(),
|
|
632
|
+
limit: z.number().int().positive(),
|
|
397
633
|
});
|
|
398
634
|
export const FlowRunStatus = z.enum([
|
|
399
635
|
"queued",
|
|
@@ -413,14 +649,36 @@ export const FlowRun = z.strictObject({
|
|
|
413
649
|
output: z.unknown().nullable(),
|
|
414
650
|
error: z.string().max(2_000).nullable(),
|
|
415
651
|
initiatedBy: IntelId,
|
|
652
|
+
// The subflow node this run was called from, and the run that node belongs to. A called run is a
|
|
653
|
+
// run of its own: it has its own version, its own steps and its own authorization, and only these
|
|
654
|
+
// two fields say where its result goes back to.
|
|
655
|
+
parentRunId: IntelId.nullable().default(null),
|
|
656
|
+
parentNodeId: FlowNodeId.nullable().default(null),
|
|
416
657
|
createdAt: IsoDateTime,
|
|
417
658
|
updatedAt: IsoDateTime,
|
|
418
659
|
completedAt: IsoDateTime.nullable(),
|
|
419
660
|
});
|
|
420
|
-
|
|
661
|
+
// Which step of which flow is running, outermost caller first. Readable rather than reconstructed
|
|
662
|
+
// from `parentRunId` by whoever is looking (#17).
|
|
663
|
+
export const FlowRunTrailEntry = z.strictObject({
|
|
664
|
+
runId: IntelId,
|
|
665
|
+
flowId: IntelId,
|
|
666
|
+
flowTitle: z.string().min(1).max(240),
|
|
667
|
+
nodeId: FlowNodeId.nullable(),
|
|
668
|
+
nodeLabel: z.string().max(160).nullable(),
|
|
669
|
+
});
|
|
670
|
+
export const FlowRunStep = z.strictObject({
|
|
671
|
+
run: FlowRun,
|
|
672
|
+
node: FlowNode.nullable(),
|
|
673
|
+
trail: z.array(FlowRunTrailEntry).default([]),
|
|
674
|
+
});
|
|
421
675
|
export const StartFlowRunInput = z.strictObject({
|
|
422
676
|
flowId: IntelId,
|
|
423
677
|
input: z.record(z.string(), z.unknown()).default({}),
|
|
678
|
+
// Present when this run is the call a subflow node makes. It names a place, never a permission:
|
|
679
|
+
// the callee's `execute` is asked of the user exactly as it is for a run they start themselves,
|
|
680
|
+
// and the parent run must be the caller's own and standing on that very node.
|
|
681
|
+
parent: z.strictObject({ runId: IntelId, nodeId: FlowNodeId }).nullable().default(null),
|
|
424
682
|
idempotencyKey: z.string().min(8).max(200),
|
|
425
683
|
});
|
|
426
684
|
export const GetFlowRunInput = z.strictObject({ runId: IntelId });
|
|
@@ -433,3 +691,81 @@ export const CompleteFlowRunStepInput = z.strictObject({
|
|
|
433
691
|
error: z.string().max(2_000).nullable().default(null),
|
|
434
692
|
idempotencyKey: z.string().min(8).max(200),
|
|
435
693
|
});
|
|
694
|
+
// Why a run started. Derived when it is read and deliberately not a column: `subflow` when the run
|
|
695
|
+
// is the call another run made, otherwise the mode of the trigger node in the immutable version the
|
|
696
|
+
// run took. A stored copy would be a second answer that could disagree with the graph that ran.
|
|
697
|
+
//
|
|
698
|
+
// ⚠️ `webhook` and `schedule` stood here until #39 took them out of the trigger node. Being derived
|
|
699
|
+
// rather than stored is exactly what makes that safe: no run carries a trigger of its own, so once
|
|
700
|
+
// the 0005 migration has rewritten every stored trigger to `manual`, there is nowhere left for the
|
|
701
|
+
// old values to come from. Had this been a column, the enum would have had to keep reading them or
|
|
702
|
+
// every old run would have failed to parse the moment somebody opened the list.
|
|
703
|
+
export const FlowRunTrigger = z.enum(["manual", "subflow"]);
|
|
704
|
+
// Which step ended a run, and why, in the words the failure already used (#20).
|
|
705
|
+
//
|
|
706
|
+
// ⚠️ A call that failed carries its reason in the *called* run, and that run is a run of its own
|
|
707
|
+
// with its own authorization. `calledRunId` is therefore filled only when the asking user may see
|
|
708
|
+
// that run through the very rule every other run answer uses; otherwise the failure is named by the
|
|
709
|
+
// calling step alone — the caller's own label — and `detail` says no more than that it did not
|
|
710
|
+
// finish. Naming a callee's step or document here would be the leak #17, #19 and #20 each closed.
|
|
711
|
+
export const FlowRunFailure = z.strictObject({
|
|
712
|
+
nodeId: FlowNodeId,
|
|
713
|
+
nodeLabel: z.string().max(160),
|
|
714
|
+
detail: z.string().max(2_000),
|
|
715
|
+
calledRunId: IntelId.nullable(),
|
|
716
|
+
});
|
|
717
|
+
// One run as a list shows it: what it did, never what it produced.
|
|
718
|
+
//
|
|
719
|
+
// ⚠️ Neither `input` nor `output` is in here, on purpose. A run reaches its Knowledge and its tools
|
|
720
|
+
// with the rights of whoever started it, so its result is a way to content the next reader of this
|
|
721
|
+
// list may have no claim to. Whoever wants a result asks for the run itself, where the same rule
|
|
722
|
+
// decides again.
|
|
723
|
+
export const FlowRunSummary = z.strictObject({
|
|
724
|
+
id: IntelId,
|
|
725
|
+
flowId: IntelId,
|
|
726
|
+
// The version the run took. Together with the run's stored input it is what a later "run this
|
|
727
|
+
// again with the old data" would need; replaying is a separate ticket, this only keeps it possible.
|
|
728
|
+
versionId: IntelId,
|
|
729
|
+
status: FlowRunStatus,
|
|
730
|
+
trigger: FlowRunTrigger,
|
|
731
|
+
startedAt: IsoDateTime,
|
|
732
|
+
completedAt: IsoDateTime.nullable(),
|
|
733
|
+
durationMs: z.number().int().nonnegative().nullable(),
|
|
734
|
+
initiatedBy: IntelId,
|
|
735
|
+
parentRunId: IntelId.nullable(),
|
|
736
|
+
failure: FlowRunFailure.nullable(),
|
|
737
|
+
});
|
|
738
|
+
// One filter and nothing else: "only the failed ones" is the question asked in almost every case,
|
|
739
|
+
// and every further facet is a report rather than a search for a fault.
|
|
740
|
+
export const ListFlowRunsInput = z.strictObject({
|
|
741
|
+
flowId: IntelId,
|
|
742
|
+
failedOnly: z.boolean().default(false),
|
|
743
|
+
limit: z.number().int().min(1).max(50).default(20),
|
|
744
|
+
// The `nextCursor` of the previous page. Keyset rather than an offset, because runs arrive while
|
|
745
|
+
// someone reads and an offset would skip or repeat rows exactly when a flow is busy.
|
|
746
|
+
cursor: z.string().min(1).max(400).nullable().default(null),
|
|
747
|
+
});
|
|
748
|
+
export const FlowRunList = z.strictObject({
|
|
749
|
+
items: z.array(FlowRunSummary),
|
|
750
|
+
nextCursor: z.string().max(400).nullable(),
|
|
751
|
+
});
|
|
752
|
+
// One completed step of one run. `detail` is the step's own error text; an output is absent for the
|
|
753
|
+
// same reason it is absent from the summary.
|
|
754
|
+
export const FlowRunStepRecord = z.strictObject({
|
|
755
|
+
nodeId: FlowNodeId,
|
|
756
|
+
nodeLabel: z.string().max(160),
|
|
757
|
+
outcome: z.enum(["completed", "failed"]),
|
|
758
|
+
branch: z.string().max(120).nullable(),
|
|
759
|
+
detail: z.string().max(2_000).nullable(),
|
|
760
|
+
calledRunId: IntelId.nullable(),
|
|
761
|
+
completedAt: IsoDateTime,
|
|
762
|
+
});
|
|
763
|
+
// What one run did, step by step, oldest first, with the call chain it belongs to (#17). The trail
|
|
764
|
+
// is what makes a nested run readable: which step of which flow this run is.
|
|
765
|
+
export const FlowRunHistory = z.strictObject({
|
|
766
|
+
runId: IntelId,
|
|
767
|
+
flowId: IntelId,
|
|
768
|
+
status: FlowRunStatus,
|
|
769
|
+
steps: z.array(FlowRunStepRecord),
|
|
770
|
+
trail: z.array(FlowRunTrailEntry),
|
|
771
|
+
});
|