@anchrd/intel-contract 0.13.0 → 0.15.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.
- package/README.md +80 -0
- package/dist/contract/bundle.d.ts +85 -0
- package/dist/contract/bundle.js +63 -0
- package/dist/contract/contract.d.ts +3 -2245
- package/dist/contract/contract.js +24 -1108
- package/dist/contract/flow-run.d.ts +346 -0
- package/dist/contract/flow-run.js +181 -0
- package/dist/contract/flow.d.ts +995 -0
- package/dist/contract/flow.js +417 -0
- package/dist/contract/node.d.ts +402 -0
- package/dist/contract/node.js +310 -0
- package/dist/contract/share.d.ts +142 -0
- package/dist/contract/share.js +67 -0
- package/dist/contract/table.d.ts +162 -0
- package/dist/contract/table.js +117 -0
- package/dist/contract/tool.d.ts +122 -0
- package/dist/contract/tool.js +172 -0
- package/package.json +29 -1
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export const IntelId = z.string().min(1).max(128);
|
|
3
3
|
export const IsoDateTime = z.iso.datetime({ offset: true });
|
|
4
|
+
// ⚠️ One described primitive rather than the same sentence on twenty-odd fields (#398). The word
|
|
5
|
+
// `idempotencyKey` means exactly the same thing everywhere it appears, and a reader who learns it
|
|
6
|
+
// once at `node_create` should not have to re-read it at `flow_publish`. Twenty copies of one
|
|
7
|
+
// sentence is also twenty places for it to drift, and `tools/list` would carry every copy.
|
|
8
|
+
//
|
|
9
|
+
// The description says what the caller has to DO, because that is the part a model gets wrong: it
|
|
10
|
+
// invents a fresh key on the retry and creates the thing twice.
|
|
11
|
+
export const IdempotencyKey = z
|
|
12
|
+
.string()
|
|
13
|
+
.min(8)
|
|
14
|
+
.max(200)
|
|
15
|
+
.describe("A caller-chosen key that makes this call safe to retry: repeating the same key returns the first result instead of doing the work twice. Reuse the SAME key when retrying one attempt, and a new one for a genuinely new call.");
|
|
4
16
|
// The languages whose UI catalog ships inside intel-ui. They live here rather than only in the UI
|
|
5
17
|
// because `intel build` has to know them: a built-in language may be chosen as `ui.defaultLanguage`
|
|
6
18
|
// without the customer listing a copy under `ui.languages` that would rot at every UI update.
|
|
@@ -17,1121 +29,25 @@ export const ProblemDetails = z.strictObject({
|
|
|
17
29
|
instance: z.string().optional(),
|
|
18
30
|
code: z.string().optional(),
|
|
19
31
|
});
|
|
20
|
-
// Who the caller is, as Gate resolved it from the bearer
|
|
21
|
-
//
|
|
32
|
+
// Who the caller is, as Gate resolved it from the bearer: no token, and no capability list.
|
|
33
|
+
//
|
|
34
|
+
// ⚠️ `isAdmin` is the one exception, and it is a DRAWING instruction rather than a permission
|
|
35
|
+
// (#416). MCP hides what a caller may not do by never registering the tool, so a model never sees a
|
|
36
|
+
// door it cannot open; the browser has no equivalent and would have to offer an admin action to
|
|
37
|
+
// everybody and answer with a 403. One boolean is what the shell needs to not do that.
|
|
38
|
+
//
|
|
39
|
+
// Two things keep it from becoming a capability list by degrees: it says what the shell may DRAW,
|
|
40
|
+
// never what the server may do — every door still asks Gate itself — and it is one field with one
|
|
41
|
+
// consumer. A second one is a second reason, written down when it exists.
|
|
22
42
|
export const SessionUser = z.strictObject({
|
|
23
43
|
id: IntelId,
|
|
24
44
|
email: z.email(),
|
|
25
45
|
name: z.string().min(1).max(240).nullable(),
|
|
46
|
+
isAdmin: z.boolean(),
|
|
26
47
|
});
|
|
27
|
-
// What this installation is equipped to do — deployment facts, never the caller's permissions
|
|
28
|
-
// (those stay behind each door, where `/session` deliberately does not carry them). `agentRuntime`
|
|
29
|
-
// says whether an agent Worker is bound at all (#190): without it the UI offers no "New agent" and
|
|
30
|
-
// an agent node explains itself instead of rendering views that could only end in a 503.
|
|
31
|
-
// The fourth kind is `table` (#40), the fifth is `agent` (#139) and the sixth is `board` (#285).
|
|
32
|
-
// Each is a kind of node, not a kind of thing: it hangs in the same folder tree, inherits the same
|
|
33
|
-
// folder grants, carries the same immutable versions and the same R2 body as a document
|
|
34
|
-
// (ADR-0004 §1, ADR-0005 §1). Only the media type and the operations below differ.
|
|
35
|
-
//
|
|
36
|
-
// ⚠️ `agent` being optional is load-bearing (ADR-0005 §1): an installation without a single agent
|
|
37
|
-
// node is complete, not unfinished, and nothing here asks anyone to classify a document as a skill
|
|
38
|
-
// or a policy in order to file it. The same holds for `board`: it is a file somebody may make, not
|
|
39
|
-
// a place the tree grows a special corner for — which is exactly why a board is one node carrying
|
|
40
|
-
// its tasks and not a folder that only tasks may live in (#285).
|
|
41
|
-
export const NodeKind = z.enum(["folder", "document", "attachment", "table"]);
|
|
42
|
-
// ⚠️ There is no `ContextPolicy`, and it is not coming back in this shape (#76). It said whether a
|
|
43
|
-
// document should be pinned into a context, be found by relevance, or be named explicitly — an
|
|
44
|
-
// instruction to a retrieval Intel does not perform. Intel hands out references and the agent
|
|
45
|
-
// fetches what it needs (D24), so nothing here could ever have read it, and nothing did.
|
|
46
|
-
//
|
|
47
|
-
// Semantic search stays: as an MCP tool the agent calls, over everything or over one area.
|
|
48
|
-
// One verb per grant, granted independently (ADR-0004 §2). Not a ladder: seeing a process must be
|
|
49
|
-
// separable from being allowed to start it, and `execute` is meaningful only where a flow can live.
|
|
50
|
-
export const ResourceVerb = z.enum(["read", "write", "execute", "share"]);
|
|
51
|
-
export const SharePrincipal = z.discriminatedUnion("type", [
|
|
52
|
-
z.strictObject({ type: z.literal("user"), id: IntelId }),
|
|
53
|
-
z.strictObject({ type: z.literal("email"), email: z.email() }),
|
|
54
|
-
z.strictObject({ type: z.literal("organization") }),
|
|
55
|
-
]);
|
|
56
|
-
export const Node = z.strictObject({
|
|
57
|
-
id: IntelId,
|
|
58
|
-
parentId: IntelId.nullable(),
|
|
59
|
-
kind: NodeKind,
|
|
60
|
-
title: z.string().min(1).max(240),
|
|
61
|
-
description: z.string().max(2_000).nullable(),
|
|
62
|
-
ownerId: IntelId,
|
|
63
|
-
currentVersionId: IntelId.nullable(),
|
|
64
|
-
createdAt: IsoDateTime,
|
|
65
|
-
updatedAt: IsoDateTime,
|
|
66
|
-
archivedAt: IsoDateTime.nullable(),
|
|
67
|
-
});
|
|
68
|
-
// What one table version carries (#135). An `append` holds only the rows one write added; a
|
|
69
|
-
// `snapshot` holds the complete table — header and every row — so reading starts at the newest
|
|
70
|
-
// snapshot and everything before it is history rather than content. Defining a table writes the
|
|
71
|
-
// first snapshot; updating, deleting, and redefining write the later ones. Documents and
|
|
72
|
-
// attachments carry `null`: each of their versions is complete by construction, and the word would
|
|
73
|
-
// say nothing about them.
|
|
74
|
-
export const NodeVersionSegment = z.enum(["append", "snapshot"]);
|
|
75
|
-
export const NodeVersion = z.strictObject({
|
|
76
|
-
id: IntelId,
|
|
77
|
-
nodeId: IntelId,
|
|
78
|
-
sequence: z.number().int().positive(),
|
|
79
|
-
contentKey: z.string().min(1),
|
|
80
|
-
mediaType: z.string().min(1).max(160),
|
|
81
|
-
contentHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
82
|
-
size: z.number().int().nonnegative(),
|
|
83
|
-
segment: NodeVersionSegment.nullable(),
|
|
84
|
-
createdBy: IntelId,
|
|
85
|
-
createdAt: IsoDateTime,
|
|
86
|
-
});
|
|
87
|
-
export const ListNodesInput = z.strictObject({
|
|
88
|
-
parentId: IntelId.nullable().default(null),
|
|
89
|
-
includeArchived: z.boolean().default(false),
|
|
90
|
-
// ⚠️ Overrides `parentId` rather than narrowing beside it: what is archived is asked for across
|
|
91
|
-
// the whole tree, because that is the only useful question. Somebody looking for what they threw
|
|
92
|
-
// away does not know which folder it was in — if they did, they would not be looking (#113).
|
|
93
|
-
// A separate flag rather than a third state on `includeArchived`, so no existing caller changes
|
|
94
|
-
// meaning — and `.optional()` rather than `.default(false)` for the same reason `ListFlowsInput`
|
|
95
|
-
// carries it that way: a default makes the field required in the PARSED type, and every existing
|
|
96
|
-
// caller would have to answer a question it is not asking.
|
|
97
|
-
archivedOnly: z.boolean().optional(),
|
|
98
|
-
});
|
|
99
|
-
export const GetNodeInput = z.strictObject({ nodeId: IntelId });
|
|
100
|
-
// One pinned version of one node (#147). Both IDs, always: a version ID alone would let anyone
|
|
101
|
-
// holding an ID read content whose node-level ACL they never passed, and a citation names both.
|
|
102
|
-
export const GetNodeVersionInput = z.strictObject({ nodeId: IntelId, versionId: IntelId });
|
|
103
|
-
export const ListGrantsInput = z.strictObject({ resourceId: IntelId });
|
|
104
|
-
export const CreateNodeInput = z.strictObject({
|
|
105
|
-
parentId: IntelId.nullable().default(null),
|
|
106
|
-
kind: NodeKind,
|
|
107
|
-
title: z.string().trim().min(1).max(240),
|
|
108
|
-
description: z.string().trim().max(2_000).nullable().default(null),
|
|
109
|
-
idempotencyKey: z.string().min(8).max(200),
|
|
110
|
-
});
|
|
111
|
-
export const SaveNodeVersionInput = z.strictObject({
|
|
112
|
-
nodeId: IntelId,
|
|
113
|
-
baseVersionId: IntelId.nullable(),
|
|
114
|
-
content: z.string().max(10_000_000),
|
|
115
|
-
mediaType: z.string().min(1).max(160).default("text/markdown"),
|
|
116
|
-
idempotencyKey: z.string().min(8).max(200),
|
|
117
|
-
});
|
|
118
|
-
export const SaveAttachmentInput = z.strictObject({
|
|
119
|
-
nodeId: IntelId,
|
|
120
|
-
baseVersionId: IntelId.nullable(),
|
|
121
|
-
contentBase64: z.string().min(1).max(20_000_000),
|
|
122
|
-
mediaType: z.string().min(1).max(160),
|
|
123
|
-
idempotencyKey: z.string().min(8).max(200),
|
|
124
|
-
});
|
|
125
|
-
export const UpdateNodeInput = z
|
|
126
|
-
.strictObject({
|
|
127
|
-
nodeId: IntelId,
|
|
128
|
-
baseUpdatedAt: IsoDateTime,
|
|
129
|
-
title: z.string().trim().min(1).max(240).optional(),
|
|
130
|
-
description: z.string().trim().max(2_000).nullable().optional(),
|
|
131
|
-
parentId: IntelId.nullable().optional(),
|
|
132
|
-
idempotencyKey: z.string().min(8).max(200),
|
|
133
|
-
})
|
|
134
|
-
.refine((input) => input.title !== undefined || input.description !== undefined || input.parentId !== undefined, { message: "At least one change is required" });
|
|
135
|
-
export const ArchiveNodeInput = z.strictObject({
|
|
136
|
-
nodeId: IntelId,
|
|
137
|
-
baseUpdatedAt: IsoDateTime,
|
|
138
|
-
archived: z.boolean(),
|
|
139
|
-
idempotencyKey: z.string().min(8).max(200),
|
|
140
|
-
});
|
|
141
|
-
// ⚠️ `withChildren` belongs to the LEVEL, not to the node (#59). Whether something has children
|
|
142
|
-
// THIS reader may see is not a property of the thing — two readers get different answers. As a
|
|
143
|
-
// field on the node, every other place that returns a node would have to compute it as well or
|
|
144
|
-
// lie; as a list beside the entries it costs only the one answer that needs it.
|
|
145
|
-
export const NodeList = z.strictObject({
|
|
146
|
-
items: z.array(Node),
|
|
147
|
-
withChildren: z.array(IntelId).default([]),
|
|
148
|
-
});
|
|
149
|
-
export const NodeVersionList = z.strictObject({ items: z.array(NodeVersion) });
|
|
150
|
-
export const NodeDocument = z.strictObject({
|
|
151
|
-
node: Node,
|
|
152
|
-
version: NodeVersion.nullable(),
|
|
153
|
-
content: z.string().nullable(),
|
|
154
|
-
});
|
|
155
|
-
export const NodeAttachment = z.strictObject({
|
|
156
|
-
node: Node,
|
|
157
|
-
version: NodeVersion,
|
|
158
|
-
resourceUri: z.string().regex(/^intel:\/\/nodes\/[^/]+\/attachment$/),
|
|
159
|
-
});
|
|
160
|
-
// A table is CSV, and CSV is the whole format: it is what is stored, what is downloaded and what a
|
|
161
|
-
// machine reads. There is no second representation to keep in step with it (#40).
|
|
162
|
-
export const TableMediaType = "text/csv";
|
|
163
|
-
// A column name is the contract between the table and everyone who appends to it, so it is trimmed,
|
|
164
|
-
// non-empty and bounded like a title. Cells are not: a cell is text, and text is what CSV carries.
|
|
165
|
-
export const TableColumn = z.string().trim().min(1).max(120);
|
|
166
|
-
export const TableCell = z.string().max(4_000);
|
|
167
|
-
export const TableRow = z.array(TableCell).min(1).max(64);
|
|
168
|
-
// Writing the header, once. The columns are the contract (#40's comment), which is why this refuses
|
|
169
|
-
// on a table that already has one: changing the header would silently reinterpret every row that
|
|
170
|
-
// was appended under the old one.
|
|
171
|
-
export const DefineTableInput = z.strictObject({
|
|
172
|
-
nodeId: IntelId,
|
|
173
|
-
columns: z
|
|
174
|
-
.array(TableColumn)
|
|
175
|
-
.min(1)
|
|
176
|
-
.max(64)
|
|
177
|
-
.refine((columns) => new Set(columns.map((column) => column.toLowerCase())).size === columns.length, { error: "Column names must be distinct" }),
|
|
178
|
-
idempotencyKey: z.string().min(8).max(200),
|
|
179
|
-
});
|
|
180
|
-
// ⚠️ No `baseVersionId`, and that absence is the feature. A document replaces its content and needs
|
|
181
|
-
// to know which content it replaces; an append adds to the end and cannot collide with a second
|
|
182
|
-
// append, so demanding a base version would invent a conflict that does not exist and force the
|
|
183
|
-
// caller to read the whole table first — the exact cost #40 exists to remove.
|
|
184
|
-
export const AppendTableRowsInput = z.strictObject({
|
|
185
|
-
nodeId: IntelId,
|
|
186
|
-
rows: z.array(TableRow).min(1).max(1_000),
|
|
187
|
-
idempotencyKey: z.string().min(8).max(200),
|
|
188
|
-
});
|
|
189
|
-
export const GetTableInput = z.strictObject({ nodeId: IntelId });
|
|
190
|
-
// The table as a grid rather than as text: the server owns the one CSV reader, so no surface has to
|
|
191
|
-
// grow a second one that would disagree with it about quoting.
|
|
192
|
-
export const NodeTable = z.strictObject({
|
|
193
|
-
node: Node,
|
|
194
|
-
columns: z.array(z.string()),
|
|
195
|
-
rows: z.array(z.array(z.string())),
|
|
196
|
-
// The newest append, or `null` while the table has no header yet.
|
|
197
|
-
versionId: IntelId.nullable(),
|
|
198
|
-
});
|
|
199
|
-
export const AppendTableRowsResult = z.strictObject({
|
|
200
|
-
node: Node,
|
|
201
|
-
version: NodeVersion,
|
|
202
|
-
appended: z.number().int().positive(),
|
|
203
|
-
});
|
|
204
|
-
// A row's address is its position among the table's current rows, counted from zero and without the
|
|
205
|
-
// header. Deliberately not an ID: rows carry no identity of their own (#135, and the same decision
|
|
206
|
-
// the grid documents), so every mutation instead pins the state its positions refer to.
|
|
207
|
-
export const TableRowPosition = z.number().int().nonnegative();
|
|
208
|
-
const distinctPositions = { error: "Row positions must be distinct" };
|
|
209
|
-
// Replacing rows in place (#135). `baseVersionId` is the version the caller read the positions
|
|
210
|
-
// from — required, never nullable, because a position into a table one has not read is a guess.
|
|
211
|
-
// A table that moved on since answers `version_conflict` rather than editing the wrong rows; that
|
|
212
|
-
// is the same optimistic concurrency the document save uses, and the deliberate opposite of
|
|
213
|
-
// `append`, which needs no base because it collides with nothing.
|
|
214
|
-
export const UpdateTableRowsInput = z.strictObject({
|
|
215
|
-
nodeId: IntelId,
|
|
216
|
-
baseVersionId: IntelId,
|
|
217
|
-
updates: z
|
|
218
|
-
.array(z.strictObject({ position: TableRowPosition, row: TableRow }))
|
|
219
|
-
.min(1)
|
|
220
|
-
.max(1_000)
|
|
221
|
-
.refine((updates) => new Set(updates.map((update) => update.position)).size === updates.length, distinctPositions),
|
|
222
|
-
idempotencyKey: z.string().min(8).max(200),
|
|
223
|
-
});
|
|
224
|
-
export const UpdateTableRowsResult = z.strictObject({
|
|
225
|
-
node: Node,
|
|
226
|
-
version: NodeVersion,
|
|
227
|
-
updated: z.number().int().positive(),
|
|
228
|
-
});
|
|
229
|
-
export const DeleteTableRowsInput = z.strictObject({
|
|
230
|
-
nodeId: IntelId,
|
|
231
|
-
baseVersionId: IntelId,
|
|
232
|
-
positions: z
|
|
233
|
-
.array(TableRowPosition)
|
|
234
|
-
.min(1)
|
|
235
|
-
.max(1_000)
|
|
236
|
-
.refine((positions) => new Set(positions).size === positions.length, distinctPositions),
|
|
237
|
-
idempotencyKey: z.string().min(8).max(200),
|
|
238
|
-
});
|
|
239
|
-
export const DeleteTableRowsResult = z.strictObject({
|
|
240
|
-
node: Node,
|
|
241
|
-
version: NodeVersion,
|
|
242
|
-
deleted: z.number().int().positive(),
|
|
243
|
-
});
|
|
244
|
-
// One entry per column the table will have afterwards, in order. `source` names the current column
|
|
245
|
-
// whose cells fill it; `null` adds an empty column, and a current column no entry names is removed
|
|
246
|
-
// together with its cells. Renaming is naming a source under a new name.
|
|
247
|
-
export const RedefineTableColumn = z.strictObject({
|
|
248
|
-
name: TableColumn,
|
|
249
|
-
source: TableColumn.nullable().default(null),
|
|
250
|
-
});
|
|
251
|
-
// Changing the header of a table that has one (#135). The mapping is explicit because it is the
|
|
252
|
-
// whole difference to the blind re-definition `defineTable` keeps refusing: without it a new header
|
|
253
|
-
// would silently reinterpret every stored row under names nobody matched to the old ones.
|
|
254
|
-
export const RedefineTableInput = z.strictObject({
|
|
255
|
-
nodeId: IntelId,
|
|
256
|
-
baseVersionId: IntelId,
|
|
257
|
-
columns: z
|
|
258
|
-
.array(RedefineTableColumn)
|
|
259
|
-
.min(1)
|
|
260
|
-
.max(64)
|
|
261
|
-
.refine((columns) => new Set(columns.map((column) => column.name.toLowerCase())).size === columns.length, { error: "Column names must be distinct" })
|
|
262
|
-
.refine((columns) => {
|
|
263
|
-
const sources = columns.map((column) => column.source).filter((source) => source !== null);
|
|
264
|
-
return new Set(sources).size === sources.length;
|
|
265
|
-
}, { error: "A current column can fill only one new column" }),
|
|
266
|
-
idempotencyKey: z.string().min(8).max(200),
|
|
267
|
-
});
|
|
268
|
-
/**
|
|
269
|
-
* One MCP server as the portal names it. The handle is what the portal puts in front of every tool
|
|
270
|
-
* that server offers (`notion_notion-search` belongs to `notion`), and it is the only identifier
|
|
271
|
-
* Intel can both store and recognise again in a live `tools/list`.
|
|
272
|
-
*
|
|
273
|
-
* ⚠️ A handle is never invented from a tool name. Which servers exist is the portal's answer
|
|
274
|
-
* (`portal_list_servers`), and the prefix is only used to attribute a tool to a server that answer
|
|
275
|
-
* already named — see `packages/api/src/tools/tool-servers` for why splitting on the underscore
|
|
276
|
-
* alone would be ambiguous.
|
|
277
|
-
*/
|
|
278
|
-
export const ToolServerHandle = z
|
|
279
|
-
.string()
|
|
280
|
-
.trim()
|
|
281
|
-
.min(1)
|
|
282
|
-
.max(120)
|
|
283
|
-
.regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "A server handle is the portal's own identifier");
|
|
284
|
-
export const NodeLinkRelation = z.enum(["references", "related", "depends_on", "implements"]);
|
|
285
|
-
// Where the link came from. `text` links are derived from a document's content and are rewritten
|
|
286
|
-
// whenever it is saved; `manual` links were made in the dialog #41 removed and are now history.
|
|
287
|
-
//
|
|
288
|
-
// ⚠️ This is provenance, never a second sort of relationship. Nothing offers the reader a choice
|
|
289
|
-
// between them, and nothing may start writing `manual` again — that would be the two ways of saying
|
|
290
|
-
// one thing that #41 exists to end. It exists so that saving a document cannot delete a link
|
|
291
|
-
// somebody made before there was another way to make one.
|
|
292
|
-
export const NodeLinkOrigin = z.enum(["text", "manual"]);
|
|
293
|
-
export const NodeLink = z.strictObject({
|
|
294
|
-
id: IntelId,
|
|
295
|
-
sourceNodeId: IntelId,
|
|
296
|
-
targetNodeId: IntelId,
|
|
297
|
-
relation: NodeLinkRelation,
|
|
298
|
-
origin: NodeLinkOrigin,
|
|
299
|
-
label: z.string().trim().min(1).max(120).nullable(),
|
|
300
|
-
createdBy: IntelId,
|
|
301
|
-
createdAt: IsoDateTime,
|
|
302
|
-
});
|
|
303
|
-
export const NodeLinkList = z.strictObject({ items: z.array(NodeLink) });
|
|
304
|
-
// The inline element a document link is, inside a BlockNote document (#41).
|
|
305
|
-
//
|
|
306
|
-
// ⚠️ The ID and nothing else. No title and no path travel with it: a stored title would go stale
|
|
307
|
-
// the moment the target is renamed, a stored path the moment it is moved — and either one would
|
|
308
|
-
// put a name the reader may not see into a document they may.
|
|
309
|
-
export const DocumentLinkInlineType = "documentLink";
|
|
310
|
-
export const ResolveNodeLinksInput = z.strictObject({
|
|
311
|
-
nodeIds: z.array(IntelId).min(1).max(200),
|
|
312
|
-
});
|
|
313
|
-
export const ResolvedNodeLink = z.strictObject({
|
|
314
|
-
nodeId: IntelId,
|
|
315
|
-
title: z.string().min(1).max(240),
|
|
316
|
-
});
|
|
317
|
-
// ⚠️ Only what the asking reader may see is in here, and an unreachable target is simply absent —
|
|
318
|
-
// never a row with an empty title, never a count, never a "restricted" marker. The list is what one
|
|
319
|
-
// side of a document link is drawn from, and an entry that says "something is here" is exactly the
|
|
320
|
-
// leak this schema has to make impossible to write by accident. Deleted and unreadable therefore
|
|
321
|
-
// look identical from the outside, which is the point.
|
|
322
|
-
export const ResolveNodeLinksResult = z.strictObject({
|
|
323
|
-
items: z.array(ResolvedNodeLink),
|
|
324
|
-
});
|
|
325
|
-
export const NodeGraphInput = z.strictObject({
|
|
326
|
-
limit: z.number().int().min(1).max(500).default(250),
|
|
327
|
-
});
|
|
328
|
-
export const NodeGraph = z.strictObject({
|
|
329
|
-
nodes: z.array(Node),
|
|
330
|
-
links: z.array(NodeLink),
|
|
331
|
-
});
|
|
332
|
-
export const BlockNoteMediaType = "application/vnd.anchrd.intel.blocknote+json";
|
|
333
|
-
export const BlockNoteDocument = z.strictObject({
|
|
334
|
-
format: z.literal("blocknote"),
|
|
335
|
-
schemaVersion: z.literal(1),
|
|
336
|
-
blocks: z.array(z.record(z.string(), z.unknown())),
|
|
337
|
-
markdown: z.string(),
|
|
338
|
-
});
|
|
339
|
-
export const ResourceGrant = z.strictObject({
|
|
340
|
-
id: IntelId,
|
|
341
|
-
resourceId: IntelId,
|
|
342
|
-
principal: SharePrincipal,
|
|
343
|
-
verb: ResourceVerb,
|
|
344
|
-
expiresAt: IsoDateTime.nullable(),
|
|
345
|
-
createdBy: IntelId,
|
|
346
|
-
createdAt: IsoDateTime,
|
|
347
|
-
});
|
|
348
|
-
export const ShareInput = z.strictObject({
|
|
349
|
-
resourceId: IntelId,
|
|
350
|
-
principal: SharePrincipal,
|
|
351
|
-
verb: ResourceVerb,
|
|
352
|
-
expiresAt: IsoDateTime.nullable().default(null),
|
|
353
|
-
idempotencyKey: z.string().min(8).max(200),
|
|
354
|
-
});
|
|
355
|
-
export const RevokeGrantInput = z.strictObject({
|
|
356
|
-
resourceId: IntelId,
|
|
357
|
-
grantId: IntelId,
|
|
358
|
-
idempotencyKey: z.string().min(8).max(200),
|
|
359
|
-
});
|
|
360
|
-
// What a grant does not cover, reported to whoever just made it. A flow in the shared folder may
|
|
361
|
-
// read a document outside it, and the run is re-authorized against the person running it — so the
|
|
362
|
-
// grant can be complete and the flow still stop for them (ADR-0004 §4).
|
|
363
|
-
//
|
|
364
|
-
// ⚠️ `titles` holds only the documents the sharer may see; everything else is in `hidden` as a
|
|
365
|
-
// number. A warning must not become a way of reading titles out of the tree.
|
|
366
|
-
export const UnreadableNodes = z.strictObject({
|
|
367
|
-
titles: z.array(z.string().min(1).max(240)),
|
|
368
|
-
hidden: z.number().int().nonnegative(),
|
|
369
|
-
});
|
|
370
|
-
// The grant is in the answer, so the warning cannot be mistaken for a refusal: it is written first
|
|
371
|
-
// and described afterwards. Blocking would force everyone who uses a central policy document to
|
|
372
|
-
// duplicate it, which is the opposite of what one tree is for (ADR-0004 §4).
|
|
373
|
-
export const ShareResult = z.strictObject({
|
|
374
|
-
grant: ResourceGrant,
|
|
375
|
-
unreadable: UnreadableNodes,
|
|
376
|
-
});
|
|
377
|
-
// `applicableVerbs` travels with the list because the answer is the business layer's, not the
|
|
378
|
-
// screen's: a document has nothing to execute, so `execute` is not offered on one (ADR-0004 §2).
|
|
379
|
-
export const ResourceGrantList = z.strictObject({
|
|
380
|
-
resourceId: IntelId,
|
|
381
|
-
applicableVerbs: z.array(ResourceVerb).min(1),
|
|
382
|
-
items: z.array(ResourceGrant),
|
|
383
|
-
});
|
|
384
|
-
// `scopeId` is a cut, never a grant (#126): it narrows an answer the actor is already entitled to
|
|
385
|
-
// and can only ever remove rows. Without it the search stays global over everything visible, which
|
|
386
|
-
// is why it is optional rather than nullable — an absent field and `null` would otherwise be two
|
|
387
|
-
// spellings of the same request.
|
|
388
|
-
export const SearchInput = z.strictObject({
|
|
389
|
-
query: z.string().trim().min(1).max(500),
|
|
390
|
-
limit: z.number().int().min(1).max(50).default(10),
|
|
391
|
-
scopeId: IntelId.optional().describe("Optional folder node id. When given, only nodes filed in that folder or beneath it are searched."),
|
|
392
|
-
});
|
|
393
|
-
export const NodeCitation = z.strictObject({
|
|
394
|
-
nodeId: IntelId,
|
|
395
|
-
versionId: IntelId,
|
|
396
|
-
title: z.string(),
|
|
397
|
-
passage: z.string(),
|
|
398
|
-
source: z.string(),
|
|
399
|
-
freshness: IsoDateTime,
|
|
400
|
-
score: z.number().min(0).max(1),
|
|
401
|
-
match: z.enum(["lexical", "semantic", "hybrid"]),
|
|
402
|
-
});
|
|
403
|
-
export const SearchResult = z.strictObject({
|
|
404
|
-
items: z.array(NodeCitation),
|
|
405
|
-
});
|
|
406
|
-
export const ReindexResult = z.strictObject({ queued: z.number().int().nonnegative() });
|
|
407
|
-
export const RevokeGrantResult = z.strictObject({ revoked: z.boolean() });
|
|
408
|
-
function isPrivateIpv4(hostname) {
|
|
409
|
-
const parts = hostname.split(".").map(Number);
|
|
410
|
-
if (parts.length !== 4 ||
|
|
411
|
-
parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
412
|
-
return false;
|
|
413
|
-
}
|
|
414
|
-
const [first = 0, second = 0] = parts;
|
|
415
|
-
return (first === 0 ||
|
|
416
|
-
first === 10 ||
|
|
417
|
-
first === 127 ||
|
|
418
|
-
(first === 100 && second >= 64 && second <= 127) ||
|
|
419
|
-
(first === 169 && second === 254) ||
|
|
420
|
-
(first === 172 && second >= 16 && second <= 31) ||
|
|
421
|
-
(first === 192 && second === 168) ||
|
|
422
|
-
(first === 198 && (second === 18 || second === 19)) ||
|
|
423
|
-
first >= 224);
|
|
424
|
-
}
|
|
425
|
-
function normalizedHostname(url) {
|
|
426
|
-
return url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
427
|
-
}
|
|
428
|
-
function isPublicToolHost(url) {
|
|
429
|
-
const hostname = normalizedHostname(url);
|
|
430
|
-
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1") {
|
|
431
|
-
return url.protocol === "http:";
|
|
432
|
-
}
|
|
433
|
-
if (!hostname.includes(".") ||
|
|
434
|
-
hostname.endsWith(".local") ||
|
|
435
|
-
hostname.endsWith(".localhost") ||
|
|
436
|
-
hostname.endsWith(".internal") ||
|
|
437
|
-
isPrivateIpv4(hostname) ||
|
|
438
|
-
hostname.includes(":")) {
|
|
439
|
-
return false;
|
|
440
|
-
}
|
|
441
|
-
return url.protocol === "https:";
|
|
442
|
-
}
|
|
443
|
-
export const ToolSourceUrl = z.url().refine((value) => {
|
|
444
|
-
try {
|
|
445
|
-
const url = new URL(value);
|
|
446
|
-
return !url.username && !url.password && isPublicToolHost(url);
|
|
447
|
-
}
|
|
448
|
-
catch {
|
|
449
|
-
return false;
|
|
450
|
-
}
|
|
451
|
-
}, "The portal must use an approved public HTTPS host without embedded credentials");
|
|
452
|
-
// The portal namespaces every upstream tool, so the name alone identifies the target server. The
|
|
453
|
-
// portal is still the one that resolves it and attaches the credentials — Intel never holds an
|
|
454
|
-
// upstream credential. Since D30 Intel does read the namespace for one purpose: attributing a tool
|
|
455
|
-
// to a server the portal's own `portal_list_servers` already named, so a delegation can be cut to
|
|
456
|
-
// whole servers. That is attribution, not routing.
|
|
457
|
-
export const ToolName = z.string().min(1).max(240);
|
|
458
|
-
export const ToolAnnotations = z.strictObject({
|
|
459
|
-
title: z.string().max(240).optional(),
|
|
460
|
-
readOnlyHint: z.boolean().optional(),
|
|
461
|
-
destructiveHint: z.boolean().optional(),
|
|
462
|
-
idempotentHint: z.boolean().optional(),
|
|
463
|
-
openWorldHint: z.boolean().optional(),
|
|
464
|
-
});
|
|
465
|
-
export const ToolCapability = z.strictObject({
|
|
466
|
-
name: ToolName,
|
|
467
|
-
title: z.string().max(240).nullable(),
|
|
468
|
-
description: z.string().max(10_000).nullable(),
|
|
469
|
-
inputSchema: z.record(z.string(), z.unknown()),
|
|
470
|
-
outputSchema: z.record(z.string(), z.unknown()).nullable(),
|
|
471
|
-
annotations: ToolAnnotations,
|
|
472
|
-
fingerprint: z.string().regex(/^[a-f0-9]{64}$/),
|
|
473
|
-
});
|
|
474
|
-
// The catalog reflects one live tools/list for the requesting user. It is never stored as a
|
|
475
|
-
// permission mirror, so there is no per-source state and no Intel-owned connection status.
|
|
476
|
-
export const ToolCatalog = z.strictObject({
|
|
477
|
-
portalConnected: z.boolean(),
|
|
478
|
-
items: z.array(ToolCapability),
|
|
479
|
-
/**
|
|
480
|
-
* Which delegated servers actually contributed a tool to this catalog (#289).
|
|
481
|
-
*
|
|
482
|
-
* ⚠️ Present only where the attribution was actually made — a delegated caller whose catalog was
|
|
483
|
-
* read. It is absent for an ordinary user, and absent as well when the answer comes from one of
|
|
484
|
-
* the short paths that never reach the portal (nothing delegated, no portal sign-in, connection
|
|
485
|
-
* dropped). Absent therefore means "not stated", never "nothing arrived"; `[]` is the second one.
|
|
486
|
-
*
|
|
487
|
-
* That it is missing rather than empty on those paths is deliberate rather than half-finished:
|
|
488
|
-
* the attribution already happens for a delegation — `capabilities` has to make it to cut the
|
|
489
|
-
* list — so naming it costs nothing there, while computing the same thing for an ordinary user
|
|
490
|
-
* would mean a second portal request per call, for a question their screen does not ask.
|
|
491
|
-
*
|
|
492
|
-
* ⚠️ It is the answer to "what arrived", never to "what was granted". A server missing here has
|
|
493
|
-
* been switched off, revoked, or is failing right now; the delegation in the definition is
|
|
494
|
-
* unchanged. Reading it the other way round would turn an outage into a permission change.
|
|
495
|
-
*/
|
|
496
|
-
reached: z.array(ToolServerHandle).optional(),
|
|
497
|
-
});
|
|
498
|
-
/**
|
|
499
|
-
* One MCP server the asking user reaches right now, as the portal itself names it (D30).
|
|
500
|
-
*
|
|
501
|
-
* ⚠️ `toolCount` is a fact about this moment and this user, not a size. It exists so a picker can
|
|
502
|
-
* say "9 tools" instead of showing a handle alone, and it must never be read as what an agent will
|
|
503
|
-
* get: the delegated run asks the portal again, with the delegator's token.
|
|
504
|
-
*/
|
|
505
|
-
export const ToolServer = z.strictObject({
|
|
506
|
-
handle: ToolServerHandle,
|
|
507
|
-
name: z.string().min(1).max(240),
|
|
508
|
-
toolCount: z.number().int().min(0),
|
|
509
|
-
});
|
|
510
|
-
// The same live-query rule as the tool catalog, one level up. `portalConnected: false` is the state
|
|
511
|
-
// of somebody who has not signed into the portal yet, and it is not an error.
|
|
512
|
-
export const ToolServerCatalog = z.strictObject({
|
|
513
|
-
portalConnected: z.boolean(),
|
|
514
|
-
items: z.array(ToolServer),
|
|
515
|
-
});
|
|
516
|
-
/**
|
|
517
|
-
* Which of the named servers a tool belongs to, or `null` for none of them.
|
|
518
|
-
*
|
|
519
|
-
* ⚠️ THE TRAP: a tool name does not say where its server name ends.
|
|
520
|
-
*
|
|
521
|
-
* The portal writes `<server>_<tool>`, and both halves may contain underscores — `intel_flow_get`
|
|
522
|
-
* reads equally well as server `intel` with tool `flow_get` and as a server called `intel_flow`
|
|
523
|
-
* with tool `get`. Splitting on the first underscore is therefore a guess that is wrong the day
|
|
524
|
-
* somebody adds a server whose name contains one, and on the API side being wrong means an agent
|
|
525
|
-
* delegated server A quietly reaching server B.
|
|
526
|
-
*
|
|
527
|
-
* So the prefix is never split. It is only ever MATCHED against handles the portal itself named,
|
|
528
|
-
* and the longest match wins: with `intel` and `intel_flow` both declared, `intel_flow_get` belongs
|
|
529
|
-
* to `intel_flow`, which is the only reading in which both declarations stay true.
|
|
530
|
-
*
|
|
531
|
-
* ⚠️ This lives in the contract because HOW A NAME IS READ is a property of the wire, and both
|
|
532
|
-
* surfaces read the same wire: `packages/api` cuts a delegation with it, `packages/ui` groups the
|
|
533
|
-
* tools screen with it (#212). A second implementation in the browser would be the third answer to
|
|
534
|
-
* one question — the underscore rule has already been answered differently in two places once
|
|
535
|
-
* (#106, #107), and the copies disagreed. What deliberately stays OUT of here is everything about
|
|
536
|
-
* reach: which handles are declared, which are enabled, which may be delegated and which one owns
|
|
537
|
-
* the portal's own management tools are decisions with consequences, and they belong to
|
|
538
|
-
* `packages/api/src/tools/tool-servers`. This function only reads a name.
|
|
539
|
-
*/
|
|
540
|
-
export function serverOf(toolName, handles) {
|
|
541
|
-
let best = null;
|
|
542
|
-
for (const handle of handles) {
|
|
543
|
-
if (!toolName.startsWith(`${handle}_`))
|
|
544
|
-
continue;
|
|
545
|
-
if (best === null || handle.length > best.length)
|
|
546
|
-
best = handle;
|
|
547
|
-
}
|
|
548
|
-
return best;
|
|
549
|
-
}
|
|
550
|
-
export const TestToolInput = z.strictObject({
|
|
551
|
-
name: ToolName,
|
|
552
|
-
arguments: z.record(z.string(), z.unknown()).default({}),
|
|
553
|
-
});
|
|
554
|
-
export const ExecuteToolInput = TestToolInput;
|
|
555
|
-
export const ToolTestResult = z.strictObject({
|
|
556
|
-
isError: z.boolean(),
|
|
557
|
-
content: z.array(z.unknown()),
|
|
558
|
-
structuredContent: z.unknown().optional(),
|
|
559
|
-
});
|
|
560
|
-
export const FlowNodeId = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/);
|
|
561
|
-
export const FlowPosition = z.strictObject({ x: z.number().finite(), y: z.number().finite() });
|
|
562
|
-
// A node has a title and nothing else to write in prose (#39). A second free text beside it was
|
|
563
|
-
// kept half up to date on both sides, and for an instruction the same sentence already belongs in
|
|
564
|
-
// the instruction itself.
|
|
565
|
-
const FlowNodeBase = {
|
|
566
|
-
id: FlowNodeId,
|
|
567
|
-
position: FlowPosition,
|
|
568
|
-
label: z.string().trim().min(1).max(160),
|
|
569
|
-
};
|
|
570
|
-
// Which version of the callee a sub-flow call takes (ADR-0004 §5). Three states rather than an
|
|
571
|
-
// optional identifier, because the difference between them is a decision and has to be readable:
|
|
572
|
-
//
|
|
573
|
-
// - `latest` the draft default. Nobody should have to version things while building, and
|
|
574
|
-
// publishing turns this into `pinned` — visibly, before the author publishes.
|
|
575
|
-
// - `follows` "always latest", chosen on purpose. The call rides along with the callee, so a
|
|
576
|
-
// change to the building block changes this flow too. Publishing leaves it alone.
|
|
577
|
-
// - `pinned` one immutable version, whatever is published elsewhere.
|
|
578
|
-
//
|
|
579
|
-
// ⚠️ Without the freeze a change to a building block would silently change the behavior of every
|
|
580
|
-
// published flow using it, which contradicts the immutable versions and pinned schema fingerprints
|
|
581
|
-
// Intel otherwise guarantees. That is why `latest` cannot survive publishing.
|
|
582
|
-
export const SubflowVersionMode = z.enum(["latest", "follows", "pinned"]);
|
|
583
|
-
export const SubflowVersion = z.discriminatedUnion("mode", [
|
|
584
|
-
z.strictObject({ mode: z.literal("latest") }),
|
|
585
|
-
z.strictObject({ mode: z.literal("follows") }),
|
|
586
|
-
z.strictObject({ mode: z.literal("pinned"), versionId: IntelId }),
|
|
587
|
-
]);
|
|
588
|
-
// The three layers a node can belong to (D25). Which one a kind is in decides what it may carry and
|
|
589
|
-
// where it may sit, and both rules are enforced in `compileFlow` rather than only drawn in the
|
|
590
|
-
// editor — a graph arrives over MCP as readily as from the canvas.
|
|
591
|
-
//
|
|
592
|
-
// ⚠️ A LINK never stands in the chain. It hangs off a step on a `context` edge, and that is the
|
|
593
|
-
// distinction the graph has drawn since #37 without anyone enforcing it — which is exactly how a
|
|
594
|
-
// start with an attachment once began its run at the attachment (the "flow edges only" comments in
|
|
595
|
-
// `flows.ts`). Marker and step keep the chain; a link is what a step works with.
|
|
596
|
-
export const FlowNodeLayer = z.enum(["marker", "step", "link"]);
|
|
597
|
-
export const flowNodeLayer = {
|
|
598
|
-
trigger: "marker",
|
|
599
|
-
output: "marker",
|
|
600
|
-
instruction: "step",
|
|
601
|
-
condition: "step",
|
|
602
|
-
subflow: "step",
|
|
603
|
-
folder: "link",
|
|
604
|
-
document: "link",
|
|
605
|
-
upload: "link",
|
|
606
|
-
table: "link",
|
|
607
|
-
tool: "link",
|
|
608
|
-
};
|
|
609
48
|
// One reference, never a list. The old `knowledge` node carried up to a hundred, plus a retrieval
|
|
610
49
|
// mode and a query that nothing read — three things in one, and the last two describing a retrieval
|
|
611
50
|
// Intel does not perform (D24: the agent fetches, Intel does not put anything into a context).
|
|
612
51
|
// Splitting by kind is what makes "exactly one" sayable at all, and it lets the editor filter the
|
|
613
52
|
// picker by what the node is for.
|
|
614
|
-
const LinkConfiguration = z.strictObject({ resourceId: IntelId });
|
|
615
|
-
export const FlowNode = z.discriminatedUnion("kind", [
|
|
616
|
-
z.strictObject({
|
|
617
|
-
...FlowNodeBase,
|
|
618
|
-
kind: z.literal("trigger"),
|
|
619
|
-
// ⚠️ `manual` is the only mode there is. `webhook` and `schedule` stood here and fired nothing:
|
|
620
|
-
// a flow is carried out by an external agent that brings its own schedule, which is what the
|
|
621
|
-
// code does rather than what it intends — `step()` hands back the current node, `completeStep`
|
|
622
|
-
// takes the result from outside, and the Cloudflare workflow waits rather than drives. #32 was
|
|
623
|
-
// closed on that basis, and a mode nothing triggers is a promise nobody keeps.
|
|
624
|
-
configuration: z.strictObject({ mode: z.literal("manual") }),
|
|
625
|
-
}),
|
|
626
|
-
z.strictObject({
|
|
627
|
-
...FlowNodeBase,
|
|
628
|
-
kind: z.literal("instruction"),
|
|
629
|
-
configuration: z.strictObject({ prompt: z.string().min(1).max(50_000) }),
|
|
630
|
-
}),
|
|
631
|
-
// The four link kinds that name something in the shared tree. They are separate kinds rather than
|
|
632
|
-
// one with a `kind` field so the canvas, the palette and the picker can each say what they mean
|
|
633
|
-
// without reading into a configuration — and so a stored graph says it too.
|
|
634
|
-
z.strictObject({ ...FlowNodeBase, kind: z.literal("folder"), configuration: LinkConfiguration }),
|
|
635
|
-
z.strictObject({
|
|
636
|
-
...FlowNodeBase,
|
|
637
|
-
kind: z.literal("document"),
|
|
638
|
-
configuration: LinkConfiguration,
|
|
639
|
-
}),
|
|
640
|
-
z.strictObject({ ...FlowNodeBase, kind: z.literal("upload"), configuration: LinkConfiguration }),
|
|
641
|
-
z.strictObject({ ...FlowNodeBase, kind: z.literal("table"), configuration: LinkConfiguration }),
|
|
642
|
-
z.strictObject({
|
|
643
|
-
...FlowNodeBase,
|
|
644
|
-
kind: z.literal("tool"),
|
|
645
|
-
configuration: z.strictObject({
|
|
646
|
-
toolName: ToolName,
|
|
647
|
-
fingerprint: z
|
|
648
|
-
.string()
|
|
649
|
-
.regex(/^[a-f0-9]{64}$/)
|
|
650
|
-
.nullable()
|
|
651
|
-
.default(null),
|
|
652
|
-
arguments: z.record(z.string(), z.unknown()).default({}),
|
|
653
|
-
}),
|
|
654
|
-
}),
|
|
655
|
-
z.strictObject({
|
|
656
|
-
...FlowNodeBase,
|
|
657
|
-
kind: z.literal("condition"),
|
|
658
|
-
configuration: z.strictObject({
|
|
659
|
-
mode: z.literal("semantic"),
|
|
660
|
-
instruction: z.string().min(1).max(10_000),
|
|
661
|
-
}),
|
|
662
|
-
}),
|
|
663
|
-
// ⚠️ There is no `approval` kind, and adding one back is a product decision rather than a schema
|
|
664
|
-
// addition (#73). It waited on a named person with a deadline, which needs a queue, a
|
|
665
|
-
// notification, a stand-in and an answer to "the deadline passed" — none of which exist. Without
|
|
666
|
-
// it, D24 holds without exception: nothing in Intel waits. A stored graph that still carries one
|
|
667
|
-
// is rewritten to a `condition` by migration 0007, because the question it asked is one the agent
|
|
668
|
-
// can answer.
|
|
669
|
-
//
|
|
670
|
-
// The seventh kind: one flow calls another (ADR-0004 §3). A schema addition, not hidden behavior
|
|
671
|
-
// in a generic code node — which flow is called has to be readable from the graph, or neither the
|
|
672
|
-
// publish-time call rule nor the sidebar could see it.
|
|
673
|
-
z.strictObject({
|
|
674
|
-
...FlowNodeBase,
|
|
675
|
-
kind: z.literal("subflow"),
|
|
676
|
-
// What the called flow is given travels through `StartFlowRunInput.input` — the schema every run
|
|
677
|
-
// already uses. A second, static input here would be a promise the execution does not keep.
|
|
678
|
-
configuration: z.strictObject({
|
|
679
|
-
flowId: IntelId,
|
|
680
|
-
version: SubflowVersion.default({ mode: "latest" }),
|
|
681
|
-
}),
|
|
682
|
-
}),
|
|
683
|
-
// ⚠️ The end marks, it does not make. It used to carry a `template` that nothing ever read, and a
|
|
684
|
-
// node called "Result" that appeared to produce one is what everybody read it as. The result of a
|
|
685
|
-
// run is what the last step before it hands in (D25) — `completeStep` carries that forward.
|
|
686
|
-
z.strictObject({
|
|
687
|
-
...FlowNodeBase,
|
|
688
|
-
kind: z.literal("output"),
|
|
689
|
-
configuration: z.strictObject({}),
|
|
690
|
-
}),
|
|
691
|
-
]);
|
|
692
|
-
// The two things an edge can mean (#37). `flow` is the order of work — "and then". `context` is what
|
|
693
|
-
// a step works with: a document consulted in exactly this step, an approval obtained in exactly this
|
|
694
|
-
// step, or, at the output, the table a result is written to.
|
|
695
|
-
//
|
|
696
|
-
// ⚠️ `flow` is the default, and that is the whole of the migration: every edge stored before this
|
|
697
|
-
// existed parses into the meaning it already had. Nothing about saved graphs has to be rewritten.
|
|
698
|
-
export const FlowEdgeKind = z.enum(["flow", "context"]);
|
|
699
|
-
export const FlowEdge = z.strictObject({
|
|
700
|
-
id: FlowNodeId,
|
|
701
|
-
source: FlowNodeId,
|
|
702
|
-
target: FlowNodeId,
|
|
703
|
-
kind: FlowEdgeKind.default("flow"),
|
|
704
|
-
label: z.string().trim().min(1).max(120).nullable().default(null),
|
|
705
|
-
sourceHandle: z.string().trim().min(1).max(120).nullable().default(null),
|
|
706
|
-
});
|
|
707
|
-
export const FlowGraph = z.strictObject({
|
|
708
|
-
nodes: z.array(FlowNode).min(2).max(200),
|
|
709
|
-
edges: z.array(FlowEdge).min(1).max(500),
|
|
710
|
-
});
|
|
711
|
-
export const Flow = z.strictObject({
|
|
712
|
-
id: IntelId,
|
|
713
|
-
// The one thing a Flow shares with a document: its place in the shared folder tree (ADR-0004).
|
|
714
|
-
// Everything else stays apart — versions, R2 body and Vectorize belong to the document, the graph,
|
|
715
|
-
// runs and approvals to the flow. `null` is the root of that same tree.
|
|
716
|
-
parentId: IntelId.nullable(),
|
|
717
|
-
title: z.string().min(1).max(240),
|
|
718
|
-
description: z.string().max(2_000).nullable(),
|
|
719
|
-
ownerId: IntelId,
|
|
720
|
-
currentVersionId: IntelId.nullable(),
|
|
721
|
-
publishedVersionId: IntelId.nullable(),
|
|
722
|
-
createdAt: IsoDateTime,
|
|
723
|
-
updatedAt: IsoDateTime,
|
|
724
|
-
archivedAt: IsoDateTime.nullable(),
|
|
725
|
-
});
|
|
726
|
-
export const FlowVersion = z.strictObject({
|
|
727
|
-
id: IntelId,
|
|
728
|
-
flowId: IntelId,
|
|
729
|
-
sequence: z.number().int().positive(),
|
|
730
|
-
graph: FlowGraph,
|
|
731
|
-
createdBy: IntelId,
|
|
732
|
-
createdAt: IsoDateTime,
|
|
733
|
-
});
|
|
734
|
-
export const FlowDocument = z.strictObject({
|
|
735
|
-
flow: Flow,
|
|
736
|
-
version: FlowVersion.nullable(),
|
|
737
|
-
});
|
|
738
|
-
// One version as the history shows it: the metadata without the graph it carries. A flow's history
|
|
739
|
-
// is as long as its edits, and a list that shipped every graph would pay for drawings nobody asked
|
|
740
|
-
// for — whoever needs one asks for that one version.
|
|
741
|
-
export const FlowVersionSummary = z.strictObject({
|
|
742
|
-
id: IntelId,
|
|
743
|
-
flowId: IntelId,
|
|
744
|
-
sequence: z.number().int().positive(),
|
|
745
|
-
createdBy: IntelId,
|
|
746
|
-
createdAt: IsoDateTime,
|
|
747
|
-
// Whether this is the version the flow currently publishes. Derived from the flow row when the
|
|
748
|
-
// list is read, never stored on the version: a version is immutable and "published" is not a
|
|
749
|
-
// property of it — it is the flow's choice, revocable without touching the version.
|
|
750
|
-
published: z.boolean(),
|
|
751
|
-
});
|
|
752
|
-
export const FlowVersionList = z.strictObject({
|
|
753
|
-
flowId: IntelId,
|
|
754
|
-
items: z.array(FlowVersionSummary),
|
|
755
|
-
});
|
|
756
|
-
// Both identifiers, deliberately: a version ID alone would resolve whatever version carries it,
|
|
757
|
-
// whichever flow it belongs to, and the ACL is answered on the flow. The pair makes a foreign
|
|
758
|
-
// version a 404 rather than a read.
|
|
759
|
-
export const GetFlowVersionInput = z.strictObject({ flowId: IntelId, versionId: IntelId });
|
|
760
|
-
// The same for flows: which of them call another flow that this reader may also see (#59). An
|
|
761
|
-
// expand arrow on a flow whose calls are all hidden promises content that expanding it cannot
|
|
762
|
-
// deliver.
|
|
763
|
-
export const FlowList = z.strictObject({
|
|
764
|
-
items: z.array(Flow),
|
|
765
|
-
withCalls: z.array(IntelId).default([]),
|
|
766
|
-
});
|
|
767
|
-
export const ReferencedNode = z.strictObject({
|
|
768
|
-
id: IntelId,
|
|
769
|
-
title: z.string().min(1).max(240),
|
|
770
|
-
});
|
|
771
|
-
// What a flow touches: the documents its tree links name and the tools its Tool steps call,
|
|
772
|
-
// read straight out of the graph. Deliberately not a conflict report — there is no arithmetic here
|
|
773
|
-
// and nothing that can go stale, because the graph is the answer. Whether a given person may reach
|
|
774
|
-
// any of it is decided where it can be decided honestly: when the folder is shared, and at runtime
|
|
775
|
-
// (ADR-0004 §4). For tools it can only ever be the latter, because the catalog is a live query with
|
|
776
|
-
// the requesting user's own token (ADR-0003).
|
|
777
|
-
//
|
|
778
|
-
// ⚠️ `nodes` names only what the asking user may see. The rest is `hiddenNodes`, a count.
|
|
779
|
-
export const FlowRequirements = z.strictObject({
|
|
780
|
-
flowId: IntelId,
|
|
781
|
-
versionId: IntelId.nullable(),
|
|
782
|
-
nodes: z.array(ReferencedNode),
|
|
783
|
-
hiddenNodes: z.number().int().nonnegative(),
|
|
784
|
-
tools: z.array(ToolName),
|
|
785
|
-
});
|
|
786
|
-
// What stands between this flow and a run, asked on demand and answered for the person asking.
|
|
787
|
-
//
|
|
788
|
-
// ⚠️ A snapshot, and it says so. The tool catalog is a live query with the requesting user's own
|
|
789
|
-
// token (ADR-0003), so what is reachable now can be different tomorrow, and the same flow answers
|
|
790
|
-
// differently for two people. That is why this is a question one asks rather than a badge on the
|
|
791
|
-
// flow: a standing "this flow has conflicts" would be wrong for tools by construction (#72, D24).
|
|
792
|
-
export const FlowValidation = z.strictObject({
|
|
793
|
-
flowId: IntelId,
|
|
794
|
-
versionId: IntelId.nullable(),
|
|
795
|
-
// Empty means it would start now — for this person, at this moment.
|
|
796
|
-
problems: z.array(z.strictObject({
|
|
797
|
-
code: z.string().min(1).max(80),
|
|
798
|
-
detail: z.string().min(1).max(2_000),
|
|
799
|
-
})),
|
|
800
|
-
checkedAt: IsoDateTime,
|
|
801
|
-
});
|
|
802
|
-
export const CreateFlowInput = z.strictObject({
|
|
803
|
-
parentId: IntelId.nullable().default(null),
|
|
804
|
-
title: z.string().trim().min(1).max(240),
|
|
805
|
-
description: z.string().trim().max(2_000).nullable().default(null),
|
|
806
|
-
idempotencyKey: z.string().min(8).max(200),
|
|
807
|
-
});
|
|
808
|
-
// Renaming and moving a flow. Both are organization and nothing else: they touch no version, no
|
|
809
|
-
// published graph and no run, because organization has to stay free of consequence or nobody dares
|
|
810
|
-
// to reorganize (ADR-0004).
|
|
811
|
-
export const UpdateFlowInput = z
|
|
812
|
-
.strictObject({
|
|
813
|
-
flowId: IntelId,
|
|
814
|
-
baseUpdatedAt: IsoDateTime,
|
|
815
|
-
title: z.string().trim().min(1).max(240).optional(),
|
|
816
|
-
description: z.string().trim().max(2_000).nullable().optional(),
|
|
817
|
-
parentId: IntelId.nullable().optional(),
|
|
818
|
-
idempotencyKey: z.string().min(8).max(200),
|
|
819
|
-
})
|
|
820
|
-
.refine((input) => input.title !== undefined || input.description !== undefined || input.parentId !== undefined, { error: "At least one change is required" });
|
|
821
|
-
// Archiving a flow is the same shape as archiving a document, deliberately: `archived` is a boolean
|
|
822
|
-
// rather than a one-way verb, because an archive nothing returns from is a delete under a friendlier
|
|
823
|
-
// name. Restoring is the same call with `false`.
|
|
824
|
-
export const ArchiveFlowInput = z.strictObject({
|
|
825
|
-
flowId: IntelId,
|
|
826
|
-
baseUpdatedAt: IsoDateTime,
|
|
827
|
-
archived: z.boolean(),
|
|
828
|
-
idempotencyKey: z.string().min(8).max(200),
|
|
829
|
-
});
|
|
830
|
-
// Three answers, not two: an absent `parentId` lists every visible flow (the search dialog asks
|
|
831
|
-
// that), `null` lists the root of the shared tree and an ID lists one folder (the sidebar tree asks
|
|
832
|
-
// per level, which is what keeps the tree off the N+1 it used to load with).
|
|
833
|
-
export const ListFlowsInput = z.strictObject({
|
|
834
|
-
parentId: IntelId.nullable().optional(),
|
|
835
|
-
// The only way back to an archived flow, and the only place that asks for one: the bounded read
|
|
836
|
-
// behind the relation graph has no such flag on purpose (#30). A drawing that includes what was
|
|
837
|
-
// archived says the tidying up never happened.
|
|
838
|
-
//
|
|
839
|
-
// ⚠️ `.optional()` rather than `.default(false)`, unlike `ListNodesInput`. This schema is
|
|
840
|
-
// the argument type of `listFlows` on three layers, and a default makes the field required in the
|
|
841
|
-
// *parsed* type — every existing caller that lists a folder would have to spell out the answer to
|
|
842
|
-
// a question it is not asking. Absent means "without the archive" everywhere it is read.
|
|
843
|
-
includeArchived: z.boolean().optional(),
|
|
844
|
-
// The same question for flows, and the same override of `parentId` — see `ListNodesInput`.
|
|
845
|
-
archivedOnly: z.boolean().optional(),
|
|
846
|
-
});
|
|
847
|
-
export const GetFlowInput = z.strictObject({ flowId: IntelId });
|
|
848
|
-
export const SaveFlowVersionInput = z.strictObject({
|
|
849
|
-
flowId: IntelId,
|
|
850
|
-
baseVersionId: IntelId.nullable(),
|
|
851
|
-
graph: FlowGraph,
|
|
852
|
-
idempotencyKey: z.string().min(8).max(200),
|
|
853
|
-
});
|
|
854
|
-
export const PublishFlowInput = z.strictObject({
|
|
855
|
-
flowId: IntelId,
|
|
856
|
-
versionId: IntelId,
|
|
857
|
-
idempotencyKey: z.string().min(8).max(200),
|
|
858
|
-
});
|
|
859
|
-
// The way back out of a publication (#146). No versionId: what is withdrawn is whatever is
|
|
860
|
-
// published now, and naming one would invite a race between reading it and revoking it. Versions
|
|
861
|
-
// are untouched — republishing any of them is one `publish` away.
|
|
862
|
-
export const UnpublishFlowInput = z.strictObject({
|
|
863
|
-
flowId: IntelId,
|
|
864
|
-
idempotencyKey: z.string().min(8).max(200),
|
|
865
|
-
});
|
|
866
|
-
export const PreviewFlowPublishInput = z.strictObject({ flowId: IntelId, versionId: IntelId });
|
|
867
|
-
// One sub-flow call as publishing will leave it (ADR-0004 §5). `freezes` is the whole point of the
|
|
868
|
-
// preview: it marks the calls whose `latest` publishing turns into `versionId`, so the author reads
|
|
869
|
-
// the decision before making it rather than after.
|
|
870
|
-
//
|
|
871
|
-
// ⚠️ Only callees the asking actor may reach are listed at all. A call whose callee they cannot see
|
|
872
|
-
// is left out rather than named, because a title is the thing an unreachable flow must not hand out.
|
|
873
|
-
export const FlowPublishCall = z.strictObject({
|
|
874
|
-
nodeId: FlowNodeId,
|
|
875
|
-
nodeLabel: z.string().min(1).max(160),
|
|
876
|
-
calleeId: IntelId,
|
|
877
|
-
calleeTitle: z.string().min(1).max(240),
|
|
878
|
-
mode: SubflowVersionMode,
|
|
879
|
-
// The version this call will take once published. `null` when it follows the callee, which is the
|
|
880
|
-
// one case where the answer is only known at run time.
|
|
881
|
-
versionId: IntelId.nullable(),
|
|
882
|
-
versionSequence: z.number().int().positive().nullable(),
|
|
883
|
-
freezes: z.boolean(),
|
|
884
|
-
// The callee has a published version to be called at all. `false` is what publishing will refuse.
|
|
885
|
-
available: z.boolean(),
|
|
886
|
-
});
|
|
887
|
-
export const FlowPublishPreview = z.strictObject({
|
|
888
|
-
flowId: IntelId,
|
|
889
|
-
versionId: IntelId,
|
|
890
|
-
calls: z.array(FlowPublishCall),
|
|
891
|
-
});
|
|
892
|
-
// A flow has no share schema of its own. A grant sits on the folder a flow is filed in and inherits
|
|
893
|
-
// down from there (ADR-0004 §2); a narrower grant beside it would destroy the subtree guarantee
|
|
894
|
-
// section 3 rests on, so per-flow grants were removed rather than deprecated.
|
|
895
|
-
// What accesses what, for one level of the shared tree (#19). A folder answers it for its contents,
|
|
896
|
-
// a single flow for itself. Documents and flows are two kinds of thing that share one tree
|
|
897
|
-
// (ADR-0004 §1), so the graph carries both and says which of them it is.
|
|
898
|
-
export const RelationNodeKind = z.enum(["folder", "document", "attachment", "table", "flow"]);
|
|
899
|
-
export const RelationNode = z.strictObject({
|
|
900
|
-
id: IntelId,
|
|
901
|
-
kind: RelationNodeKind,
|
|
902
|
-
title: z.string().min(1).max(240),
|
|
903
|
-
// Inside the level being shown, rather than something it reaches out to. A flow reading a policy
|
|
904
|
-
// document from another folder pulls that document in, and the difference should be legible.
|
|
905
|
-
inScope: z.boolean(),
|
|
906
|
-
});
|
|
907
|
-
export const RelationEdge = z.strictObject({
|
|
908
|
-
id: z.string().min(1).max(400),
|
|
909
|
-
source: IntelId,
|
|
910
|
-
target: IntelId,
|
|
911
|
-
relation: z.enum(["reads", "calls"]),
|
|
912
|
-
});
|
|
913
|
-
export const RelationGraphScope = z.discriminatedUnion("of", [
|
|
914
|
-
z.strictObject({ of: z.literal("folder"), folderId: IntelId.nullable() }),
|
|
915
|
-
z.strictObject({ of: z.literal("flow"), flowId: IntelId }),
|
|
916
|
-
]);
|
|
917
|
-
export const RelationGraphInput = z.strictObject({
|
|
918
|
-
scope: RelationGraphScope,
|
|
919
|
-
// How much is drawn before the answer is summarized instead. A big folder has to stay usable, and
|
|
920
|
-
// the cut-off is reported rather than swallowed.
|
|
921
|
-
limit: z.number().int().min(1).max(300).default(60),
|
|
922
|
-
});
|
|
923
|
-
// ⚠️ Only what the asking user may see is in here. A node they may not reach is absent, not greyed
|
|
924
|
-
// out and not counted: an edge to a placeholder would already tell them the thing exists, which is
|
|
925
|
-
// the leak this schema has to make impossible to write by accident. `omitted` is about the size
|
|
926
|
-
// limit alone, never about permissions.
|
|
927
|
-
export const RelationGraph = z.strictObject({
|
|
928
|
-
scope: RelationGraphScope,
|
|
929
|
-
nodes: z.array(RelationNode),
|
|
930
|
-
edges: z.array(RelationEdge),
|
|
931
|
-
omitted: z.number().int().nonnegative(),
|
|
932
|
-
limit: z.number().int().positive(),
|
|
933
|
-
});
|
|
934
|
-
// ⚠️ No `waiting`. It was written for the approval node and never set by anything — the two places
|
|
935
|
-
// that tested for it only ever saw `running` (#73). The column's CHECK constraint still allows the
|
|
936
|
-
// value, deliberately: rewriting it means rebuilding the table in D1 for a value nothing writes,
|
|
937
|
-
// and migration 0007 turns any row that somehow carries it into `failed` rather than leave a status
|
|
938
|
-
// the contract cannot parse.
|
|
939
|
-
export const FlowRunStatus = z.enum(["queued", "running", "completed", "failed", "cancelled"]);
|
|
940
|
-
export const FlowRun = z.strictObject({
|
|
941
|
-
id: IntelId,
|
|
942
|
-
flowId: IntelId,
|
|
943
|
-
versionId: IntelId,
|
|
944
|
-
status: FlowRunStatus,
|
|
945
|
-
currentNodeId: FlowNodeId.nullable(),
|
|
946
|
-
input: z.record(z.string(), z.unknown()),
|
|
947
|
-
output: z.unknown().nullable(),
|
|
948
|
-
error: z.string().max(2_000).nullable(),
|
|
949
|
-
initiatedBy: IntelId,
|
|
950
|
-
// The subflow node this run was called from, and the run that node belongs to. A called run is a
|
|
951
|
-
// run of its own: it has its own version, its own steps and its own authorization, and only these
|
|
952
|
-
// two fields say where its result goes back to.
|
|
953
|
-
parentRunId: IntelId.nullable().default(null),
|
|
954
|
-
parentNodeId: FlowNodeId.nullable().default(null),
|
|
955
|
-
createdAt: IsoDateTime,
|
|
956
|
-
updatedAt: IsoDateTime,
|
|
957
|
-
completedAt: IsoDateTime.nullable(),
|
|
958
|
-
});
|
|
959
|
-
// Which step of which flow is running, outermost caller first. Readable rather than reconstructed
|
|
960
|
-
// from `parentRunId` by whoever is looking (#17).
|
|
961
|
-
export const FlowRunTrailEntry = z.strictObject({
|
|
962
|
-
runId: IntelId,
|
|
963
|
-
flowId: IntelId,
|
|
964
|
-
flowTitle: z.string().min(1).max(240),
|
|
965
|
-
nodeId: FlowNodeId.nullable(),
|
|
966
|
-
nodeLabel: z.string().max(160).nullable(),
|
|
967
|
-
});
|
|
968
|
-
export const FlowRunStep = z.strictObject({
|
|
969
|
-
run: FlowRun,
|
|
970
|
-
node: FlowNode.nullable(),
|
|
971
|
-
trail: z.array(FlowRunTrailEntry).default([]),
|
|
972
|
-
});
|
|
973
|
-
export const StartFlowRunInput = z.strictObject({
|
|
974
|
-
flowId: IntelId,
|
|
975
|
-
input: z.record(z.string(), z.unknown()).default({}),
|
|
976
|
-
// Present when this run is the call a subflow node makes. It names a place, never a permission:
|
|
977
|
-
// the callee's `execute` is asked of the user exactly as it is for a run they start themselves,
|
|
978
|
-
// and the parent run must be the caller's own and standing on that very node.
|
|
979
|
-
parent: z.strictObject({ runId: IntelId, nodeId: FlowNodeId }).nullable().default(null),
|
|
980
|
-
idempotencyKey: z.string().min(8).max(200),
|
|
981
|
-
});
|
|
982
|
-
export const GetFlowRunInput = z.strictObject({ runId: IntelId });
|
|
983
|
-
// Ending a run on purpose (#145). Until this existed the only way off a parked manual step was
|
|
984
|
-
// `completeStep` with `outcome: "failed"` — which recorded a step failure that never happened.
|
|
985
|
-
// Cancelling records nothing into the step history: the run ends, the history stays true.
|
|
986
|
-
export const CancelFlowRunInput = z.strictObject({
|
|
987
|
-
runId: IntelId,
|
|
988
|
-
idempotencyKey: z.string().min(8).max(200),
|
|
989
|
-
});
|
|
990
|
-
export const CompleteFlowRunStepInput = z.strictObject({
|
|
991
|
-
runId: IntelId,
|
|
992
|
-
nodeId: FlowNodeId,
|
|
993
|
-
outcome: z.enum(["completed", "failed"]),
|
|
994
|
-
branch: z.string().min(1).max(120).nullable().default(null),
|
|
995
|
-
output: z.unknown().nullable().default(null),
|
|
996
|
-
error: z.string().max(2_000).nullable().default(null),
|
|
997
|
-
idempotencyKey: z.string().min(8).max(200),
|
|
998
|
-
});
|
|
999
|
-
// Why a run started. Derived when it is read and deliberately not a column: `subflow` when the run
|
|
1000
|
-
// is the call another run made, otherwise the mode of the trigger node in the immutable version the
|
|
1001
|
-
// run took. A stored copy would be a second answer that could disagree with the graph that ran.
|
|
1002
|
-
//
|
|
1003
|
-
// ⚠️ `webhook` and `schedule` stood here until #39 took them out of the trigger node. Being derived
|
|
1004
|
-
// rather than stored is exactly what makes that safe: no run carries a trigger of its own, so once
|
|
1005
|
-
// the 0005 migration has rewritten every stored trigger to `manual`, there is nowhere left for the
|
|
1006
|
-
// old values to come from. Had this been a column, the enum would have had to keep reading them or
|
|
1007
|
-
// every old run would have failed to parse the moment somebody opened the list.
|
|
1008
|
-
export const FlowRunTrigger = z.enum(["manual", "subflow"]);
|
|
1009
|
-
// Which step ended a run, and why, in the words the failure already used (#20).
|
|
1010
|
-
//
|
|
1011
|
-
// ⚠️ A call that failed carries its reason in the *called* run, and that run is a run of its own
|
|
1012
|
-
// with its own authorization. `calledRunId` is therefore filled only when the asking user may see
|
|
1013
|
-
// that run through the very rule every other run answer uses; otherwise the failure is named by the
|
|
1014
|
-
// calling step alone — the caller's own label — and `detail` says no more than that it did not
|
|
1015
|
-
// finish. Naming a callee's step or document here would be the leak #17, #19 and #20 each closed.
|
|
1016
|
-
export const FlowRunFailure = z.strictObject({
|
|
1017
|
-
nodeId: FlowNodeId,
|
|
1018
|
-
nodeLabel: z.string().max(160),
|
|
1019
|
-
detail: z.string().max(2_000),
|
|
1020
|
-
calledRunId: IntelId.nullable(),
|
|
1021
|
-
});
|
|
1022
|
-
// One run as a list shows it: what it did, never what it produced.
|
|
1023
|
-
//
|
|
1024
|
-
// ⚠️ Neither `input` nor `output` is in here, on purpose. A run reaches its nodes and its tools
|
|
1025
|
-
// with the rights of whoever started it, so its result is a way to content the next reader of this
|
|
1026
|
-
// list may have no claim to. Whoever wants a result asks for the run itself, where the same rule
|
|
1027
|
-
// decides again.
|
|
1028
|
-
export const FlowRunSummary = z.strictObject({
|
|
1029
|
-
id: IntelId,
|
|
1030
|
-
flowId: IntelId,
|
|
1031
|
-
// The version the run took. Together with the run's stored input it is what a later "run this
|
|
1032
|
-
// again with the old data" would need; replaying is a separate ticket, this only keeps it possible.
|
|
1033
|
-
versionId: IntelId,
|
|
1034
|
-
status: FlowRunStatus,
|
|
1035
|
-
trigger: FlowRunTrigger,
|
|
1036
|
-
startedAt: IsoDateTime,
|
|
1037
|
-
completedAt: IsoDateTime.nullable(),
|
|
1038
|
-
durationMs: z.number().int().nonnegative().nullable(),
|
|
1039
|
-
initiatedBy: IntelId,
|
|
1040
|
-
parentRunId: IntelId.nullable(),
|
|
1041
|
-
failure: FlowRunFailure.nullable(),
|
|
1042
|
-
});
|
|
1043
|
-
// One filter and nothing else: "only the failed ones" is the question asked in almost every case,
|
|
1044
|
-
// and every further facet is a report rather than a search for a fault.
|
|
1045
|
-
export const ListFlowRunsInput = z.strictObject({
|
|
1046
|
-
flowId: IntelId,
|
|
1047
|
-
failedOnly: z.boolean().default(false),
|
|
1048
|
-
limit: z.number().int().min(1).max(50).default(20),
|
|
1049
|
-
// The `nextCursor` of the previous page. Keyset rather than an offset, because runs arrive while
|
|
1050
|
-
// someone reads and an offset would skip or repeat rows exactly when a flow is busy.
|
|
1051
|
-
cursor: z.string().min(1).max(400).nullable().default(null),
|
|
1052
|
-
});
|
|
1053
|
-
export const FlowRunList = z.strictObject({
|
|
1054
|
-
items: z.array(FlowRunSummary),
|
|
1055
|
-
nextCursor: z.string().max(400).nullable(),
|
|
1056
|
-
});
|
|
1057
|
-
// One completed step of one run. `detail` is the step's own error text; an output is absent for the
|
|
1058
|
-
// same reason it is absent from the summary.
|
|
1059
|
-
export const FlowRunStepRecord = z.strictObject({
|
|
1060
|
-
nodeId: FlowNodeId,
|
|
1061
|
-
nodeLabel: z.string().max(160),
|
|
1062
|
-
outcome: z.enum(["completed", "failed"]),
|
|
1063
|
-
branch: z.string().max(120).nullable(),
|
|
1064
|
-
detail: z.string().max(2_000).nullable(),
|
|
1065
|
-
calledRunId: IntelId.nullable(),
|
|
1066
|
-
completedAt: IsoDateTime,
|
|
1067
|
-
});
|
|
1068
|
-
// What one run did, step by step, oldest first, with the call chain it belongs to (#17). The trail
|
|
1069
|
-
// is what makes a nested run readable: which step of which flow this run is.
|
|
1070
|
-
export const FlowRunHistory = z.strictObject({
|
|
1071
|
-
runId: IntelId,
|
|
1072
|
-
flowId: IntelId,
|
|
1073
|
-
status: FlowRunStatus,
|
|
1074
|
-
steps: z.array(FlowRunStepRecord),
|
|
1075
|
-
trail: z.array(FlowRunTrailEntry),
|
|
1076
|
-
});
|
|
1077
|
-
// ── Bundle export (#136) ────────────────────────────────────────────────────────────────────────
|
|
1078
|
-
// The one name the importer looks for at the zip root. A different spelling would make a bundle a
|
|
1079
|
-
// naked folder, so the constant lives in the contract rather than in each surface.
|
|
1080
|
-
export const BundleManifestFilename = "manifest.json";
|
|
1081
|
-
/**
|
|
1082
|
-
* What a bundle entry can be. `flow` joins the node kinds because a flow shares the folder tree
|
|
1083
|
-
* without being a node (ADR-0004), and the bundle mirrors the tree, not the tables.
|
|
1084
|
-
*
|
|
1085
|
-
* ⚠️ `agent` and `board` are STILL HERE, and that is the one place in this file where a value
|
|
1086
|
-
* survives its feature (#390). A bundle is somebody else's file: an export written before Agents
|
|
1087
|
-
* and Board were parked (#385) is a correct export, and it has to PARSE so the import can refuse it
|
|
1088
|
-
* by name — with the entry, the kind and the branch the code is on. Take them out and the same
|
|
1089
|
-
* bundle fails as `unexpected enum value`, which sends its holder looking for a broken file that is
|
|
1090
|
-
* not broken.
|
|
1091
|
-
*
|
|
1092
|
-
* They belong to the wire format of a file that already exists, not to the product. Nothing may
|
|
1093
|
-
* create either kind; `NodeKind` is the enum that says so.
|
|
1094
|
-
*/
|
|
1095
|
-
export const BundleEntryKind = z.enum([
|
|
1096
|
-
"folder",
|
|
1097
|
-
"document",
|
|
1098
|
-
"table",
|
|
1099
|
-
"attachment",
|
|
1100
|
-
"agent",
|
|
1101
|
-
"board",
|
|
1102
|
-
"flow",
|
|
1103
|
-
]);
|
|
1104
|
-
// One entry of the manifest: the identity a re-import needs, next to the relative path where the
|
|
1105
|
-
// bytes sit in the zip. A folder carries no media type — it has no bytes.
|
|
1106
|
-
export const BundleManifestEntry = z.strictObject({
|
|
1107
|
-
id: IntelId,
|
|
1108
|
-
kind: BundleEntryKind,
|
|
1109
|
-
title: z.string().min(1).max(240),
|
|
1110
|
-
description: z.string().max(2_000).nullable(),
|
|
1111
|
-
mediaType: z.string().min(1).max(160).nullable(),
|
|
1112
|
-
// Relative to the zip root, forward slashes, no leading slash. Folders end with a slash so an
|
|
1113
|
-
// empty folder still has an address.
|
|
1114
|
-
path: z.string().min(1).max(4_000),
|
|
1115
|
-
});
|
|
1116
|
-
// What an export leaves out on purpose, named so a bundle says it rather than a reader guessing:
|
|
1117
|
-
// version history, grants/shares, flow runs, and archived nodes are not in any bundle (#136).
|
|
1118
|
-
export const BundleExclusion = z.enum(["version-history", "grants", "flow-runs", "archived-nodes"]);
|
|
1119
|
-
export const BundleManifest = z.strictObject({
|
|
1120
|
-
version: z.literal(1),
|
|
1121
|
-
exportedAt: IsoDateTime,
|
|
1122
|
-
// The node the export started at; `null` is the root of the tree — the whole installation as the
|
|
1123
|
-
// exporting caller may read it.
|
|
1124
|
-
rootId: IntelId.nullable(),
|
|
1125
|
-
entries: z.array(BundleManifestEntry),
|
|
1126
|
-
excluded: z.array(BundleExclusion),
|
|
1127
|
-
});
|
|
1128
|
-
// ── Bundle import (#137) ────────────────────────────────────────────────────────────────────────
|
|
1129
|
-
// What one import made. Import always creates new nodes — no merge, no overwrite, no restored IDs
|
|
1130
|
-
// (#137, phase 1) — so the answer is counts and the new roots, never a diff. `replayed` marks the
|
|
1131
|
-
// idempotent second answer to the same key: nothing was created twice.
|
|
1132
|
-
export const BundleImportResult = z.strictObject({
|
|
1133
|
-
nodes: z.number().int().nonnegative(),
|
|
1134
|
-
flows: z.number().int().nonnegative(),
|
|
1135
|
-
rootNodeIds: z.array(IntelId),
|
|
1136
|
-
replayed: z.boolean(),
|
|
1137
|
-
});
|
|
53
|
+
export const LinkConfiguration = z.strictObject({ resourceId: IntelId });
|