@tai42/api-client 0.1.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.
Files changed (48) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +28 -0
  3. package/dist/agents.d.ts +265 -0
  4. package/dist/agents.d.ts.map +1 -0
  5. package/dist/agents.js +200 -0
  6. package/dist/agents.js.map +1 -0
  7. package/dist/client.d.ts +1316 -0
  8. package/dist/client.d.ts.map +1 -0
  9. package/dist/client.js +468 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/errors.d.ts +43 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +63 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/fleet-report.d.ts +61 -0
  16. package/dist/fleet-report.d.ts.map +1 -0
  17. package/dist/fleet-report.js +42 -0
  18. package/dist/fleet-report.js.map +1 -0
  19. package/dist/http.d.ts +69 -0
  20. package/dist/http.d.ts.map +1 -0
  21. package/dist/http.js +152 -0
  22. package/dist/http.js.map +1 -0
  23. package/dist/index.d.ts +23 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +16 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/schemas.d.ts +5469 -0
  28. package/dist/schemas.d.ts.map +1 -0
  29. package/dist/schemas.js +1103 -0
  30. package/dist/schemas.js.map +1 -0
  31. package/dist/sse.d.ts +27 -0
  32. package/dist/sse.d.ts.map +1 -0
  33. package/dist/sse.js +84 -0
  34. package/dist/sse.js.map +1 -0
  35. package/dist/tool-runs.d.ts +112 -0
  36. package/dist/tool-runs.d.ts.map +1 -0
  37. package/dist/tool-runs.js +70 -0
  38. package/dist/tool-runs.js.map +1 -0
  39. package/package.json +51 -0
  40. package/src/agents.ts +248 -0
  41. package/src/client.ts +837 -0
  42. package/src/errors.ts +67 -0
  43. package/src/fleet-report.ts +94 -0
  44. package/src/http.ts +198 -0
  45. package/src/index.ts +142 -0
  46. package/src/schemas.ts +1341 -0
  47. package/src/sse.ts +90 -0
  48. package/src/tool-runs.ts +103 -0
package/src/schemas.ts ADDED
@@ -0,0 +1,1341 @@
1
+ /**
2
+ * One zod schema per consumed endpoint. These mirror the skeleton's router
3
+ * response shapes exactly; a drift throws (ApiSchemaError) and the UI shows the
4
+ * error state. JSON-schema-shaped fields are kept as permissive records — the
5
+ * auto-form renderer interprets them at runtime.
6
+ */
7
+ import { z } from 'zod';
8
+
9
+ /** An arbitrary JSON value (tool results, rendered payloads). */
10
+ export const jsonValue: z.ZodType<unknown> = z.unknown();
11
+
12
+ /** A JSON Schema object (Pydantic-emitted). Permissive; parsed by the auto-form. */
13
+ export const jsonSchema = z.record(z.string(), z.unknown());
14
+
15
+ // -- tools -------------------------------------------------------------------
16
+
17
+ export const toolNames = z.array(z.string());
18
+
19
+ export const toolSchema = z.object({
20
+ input: jsonSchema,
21
+ output: jsonSchema.nullable(),
22
+ description: z.string().nullable(),
23
+ });
24
+ export type ToolSchema = z.infer<typeof toolSchema>;
25
+
26
+ export const allToolSchemas = z.record(z.string(), toolSchema);
27
+
28
+ export const runToolResult = jsonValue;
29
+
30
+ /**
31
+ * The MEDIA variant a direct tool run (`/api/run-tool`) can return:
32
+ * fastmcp serializes a returned `Image`/`Audio` MediaBlock to the MCP content
33
+ * object `{ type: 'image' | 'audio', data: <base64>, mimeType: <mime> }`. The
34
+ * `ResultViewer` detects this shape and renders an `<img>`/`<audio>` from a
35
+ * `data:` URI. A tool result is still arbitrary JSON (`runToolResult`); this is
36
+ * one recognised shape it can take, kept as its own schema so the viewer's
37
+ * detection is a schema parse, not an ad-hoc shape check.
38
+ *
39
+ * SECURITY: the refinement pins `mimeType` to its media kind — an `image` block
40
+ * MUST carry an `image/*` type, an `audio` block an `audio/*` type. A drifting or
41
+ * arbitrary mimeType (e.g. a script type) fails the parse, so the viewer never
42
+ * turns an unexpected mime into a `data:` URI — it falls back to the JSON/text
43
+ * view instead. A `File` MediaBlock serializes to a distinct `EmbeddedResource`
44
+ * shape (no top-level `data`/`mimeType`), so it also fails here and is not
45
+ * media-rendered.
46
+ */
47
+ export const toolMediaResult = z
48
+ .object({
49
+ type: z.enum(['image', 'audio']),
50
+ data: z.string(),
51
+ mimeType: z.string(),
52
+ })
53
+ .refine((media) => media.mimeType.startsWith(`${media.type}/`), {
54
+ message: 'media mimeType must match its media kind (image/* for image, audio/* for audio)',
55
+ path: ['mimeType'],
56
+ });
57
+ export type ToolMediaResult = z.infer<typeof toolMediaResult>;
58
+
59
+ /**
60
+ * The ADDITIVE per-tool native-tags map (`GET /api/tools/tags`). Distinct from the
61
+ * flat `toolNames` contract (`GET /api/tools`, unchanged): this surfaces each
62
+ * tool's FastMCP `tags` — including the ones preset bodies project — so the shared
63
+ * `ToolPicker` can group/filter its options by tag.
64
+ */
65
+ export const toolTagEntry = z.object({ name: z.string(), tags: z.array(z.string()) });
66
+ export const toolTags = z.array(toolTagEntry);
67
+ export type ToolTagEntry = z.infer<typeof toolTagEntry>;
68
+
69
+ // -- extensions --------------------------------------------------------------
70
+
71
+ export const extension = z.object({ name: z.string(), kind: z.string() });
72
+ export const extensions = z.array(extension);
73
+ export type Extension = z.infer<typeof extension>;
74
+
75
+ /**
76
+ * One element of an extension combo: a bare extension NAME, or a
77
+ * `{ name, config }` mapping binding author config (e.g. an `output_schema` JSON
78
+ * Schema, or an `ask_external` verifier). Both wire forms are admitted so a
79
+ * config-bearing combo parses without dropping server data. A drift throws
80
+ * `ApiSchemaError`.
81
+ */
82
+ export const presetExtensionElement = z.union([
83
+ z.string(),
84
+ z.object({ name: z.string(), config: z.record(z.string(), z.unknown()) }),
85
+ ]);
86
+ export type PresetExtensionElement = z.infer<typeof presetExtensionElement>;
87
+
88
+ /**
89
+ * `GET /api/tools/{name}/extensions` — a tool's FULL lossless list-of-combos plus
90
+ * the extension catalog. `combos` is a list of combos, each an ordered list of
91
+ * extension ELEMENTS (a bare name or a config-bearing `{ name, config }` mapping —
92
+ * `output_schema` carries its schema this way); `[]` when the tool has no map entry.
93
+ * It carries EVERY combo mapped to the base tool (the union across every `tools`/`mcp`
94
+ * config), never a single flattened one. `available` is the same flat catalog
95
+ * `GET /api/extensions` returns. A drift throws `ApiSchemaError`.
96
+ */
97
+ export const toolExtensions = z.object({
98
+ combos: z.array(z.array(presetExtensionElement)),
99
+ available: extensions,
100
+ });
101
+ export type ToolExtensions = z.infer<typeof toolExtensions>;
102
+
103
+ // -- presets -----------------------------------------------------------------
104
+
105
+ /** A list of extension COMBOS — each combo an ordered list of elements. */
106
+ export const presetExtensions = z.array(z.array(presetExtensionElement));
107
+
108
+ /**
109
+ * One preset as `GET /api/presets` returns it. Every preset is versioned, so
110
+ * `active_version` is always an int; `conflicted` marks a QUARANTINED row (its
111
+ * name collided with a foreign tool at boot — delete-only). `extensions` are the
112
+ * ACTIVE version's extension combos, `tags` the preset's categorization labels
113
+ * (distinct from a version's generic label list), and `output_schema` the
114
+ * optional author-set output JSON Schema. A drift throws `ApiSchemaError`;
115
+ * nothing is coerced.
116
+ */
117
+ export const presetRecord = z.object({
118
+ name: z.string(),
119
+ base_tool: z.string(),
120
+ description: z.string(),
121
+ active_version: z.number(),
122
+ tags: z.array(z.string()),
123
+ extensions: presetExtensions,
124
+ output_schema: z.record(z.string(), z.unknown()).nullable(),
125
+ conflicted: z.boolean(),
126
+ // The human-readable cause a row is quarantined (its name collided with a
127
+ // foreign tool at boot), or `null` when the row is not conflicted. Rendered
128
+ // verbatim; nothing is coerced.
129
+ conflicted_reason: z.string().nullable(),
130
+ });
131
+ export type PresetRecord = z.infer<typeof presetRecord>;
132
+
133
+ export const presetList = z.array(presetRecord);
134
+
135
+ /** `GET /api/presets/{name}` — the record plus the active version's baked kwargs. */
136
+ export const presetDetail = presetRecord.extend({
137
+ fixed_kwargs: z.record(z.string(), z.unknown()),
138
+ });
139
+ export type PresetDetail = z.infer<typeof presetDetail>;
140
+
141
+ /** The full version body a preset stores under `kind="preset"`. */
142
+ export const presetBody = z.object({
143
+ base_tool: z.string(),
144
+ description: z.string(),
145
+ fixed_kwargs: z.record(z.string(), z.unknown()),
146
+ extensions: presetExtensions,
147
+ tags: z.array(z.string()),
148
+ output_schema: z.record(z.string(), z.unknown()).nullable(),
149
+ });
150
+ export type PresetBody = z.infer<typeof presetBody>;
151
+
152
+ /**
153
+ * One immutable version row (`GET /api/presets/{name}/versions[/{version}]`).
154
+ * `is_current` flags the active version; `tags` here is the GENERIC per-version
155
+ * label list (kind-agnostic), distinct from the categorization `body.tags`.
156
+ */
157
+ export const presetVersion = z.object({
158
+ version: z.number(),
159
+ body: presetBody,
160
+ tags: z.array(z.string()),
161
+ created_at: z.string(),
162
+ is_current: z.boolean(),
163
+ });
164
+ export type PresetVersion = z.infer<typeof presetVersion>;
165
+
166
+ export const presetVersionList = z.array(presetVersion);
167
+
168
+ /** `POST /api/presets/{name}/rollback` — the re-pointed active version. */
169
+ export const presetRollback = z.object({ name: z.string(), active_version: z.number() });
170
+
171
+ /** `DELETE /api/presets/{name}` — the removed preset. */
172
+ export const presetDeleted = z.object({ name: z.string(), deleted: z.literal(true) });
173
+
174
+ /** `POST /api/presets/{name}/rename` — the re-keyed record identity. */
175
+ export const presetRenamed = z.object({
176
+ name: z.string(),
177
+ renamed_from: z.string(),
178
+ active_version: z.number(),
179
+ });
180
+ export type PresetRenamed = z.infer<typeof presetRenamed>;
181
+
182
+ /**
183
+ * `GET /api/presets/{name}/referees` — every OTHER preset whose active authored
184
+ * composition names this one as a tool (the referees a rename would strand). An
185
+ * empty list is the clean case; a non-empty list means a rename is blocked until
186
+ * those presets are updated. A drift throws `ApiSchemaError`.
187
+ */
188
+ export const presetReferees = z.object({
189
+ name: z.string(),
190
+ referees: z.array(z.string()),
191
+ });
192
+ export type PresetReferees = z.infer<typeof presetReferees>;
193
+
194
+ /**
195
+ * `POST /api/presets/validate` — the dry-run verdict for a preset draft. `valid`
196
+ * is the absence of an `error`; an invalid draft is a SUCCESSFUL validation (HTTP
197
+ * 200), so `error` carries the server's verbatim message. A drift throws
198
+ * `ApiSchemaError`.
199
+ */
200
+ export const presetValidation = z.object({
201
+ valid: z.boolean(),
202
+ error: z.string().nullable(),
203
+ });
204
+ export type PresetValidation = z.infer<typeof presetValidation>;
205
+
206
+ /**
207
+ * `PUT /api/presets/{name}/versions/{version}/tags` — the re-annotated version.
208
+ * Version tags are labels on an immutable body, so this rebinds nothing; the
209
+ * response echoes the identity plus the new tag list. A drift throws
210
+ * `ApiSchemaError`.
211
+ */
212
+ export const presetVersionTags = z.object({
213
+ name: z.string(),
214
+ version: z.number(),
215
+ tags: z.array(z.string()),
216
+ });
217
+ export type PresetVersionTags = z.infer<typeof presetVersionTags>;
218
+
219
+ // -- templates ---------------------------------------------------------------
220
+
221
+ export const templateNames = z.array(z.string());
222
+ export const templateDetail = z.object({ template: z.string(), schema: jsonSchema });
223
+ export const templateUploaded = z.object({ path: z.string(), uploaded: z.literal(true) });
224
+ export const templateDeleted = z.object({ path: z.string(), deleted: z.literal(true) });
225
+ export const templateRendered = z.object({ rendered: z.string() });
226
+ export const templateCacheCleared = z.object({ cleared: z.literal(true) });
227
+
228
+ // -- storage -----------------------------------------------------------------
229
+
230
+ /** `GET /api/storage` — the storage-provider identity, or the absent sentinel
231
+ * (`present: false`, a 200) when no storage-provider plugin is installed. */
232
+ export const storageInfo = z.object({
233
+ present: z.boolean(),
234
+ provider: z.string().nullable(),
235
+ module: z.string().nullable(),
236
+ });
237
+ export type StorageInfo = z.infer<typeof storageInfo>;
238
+
239
+ /** `GET /api/storage/resources` — the full sorted resource-id list. */
240
+ export const storageResourceList = z.object({ resources: z.array(z.string()) });
241
+
242
+ /** `GET /api/storage/resources/{id}/stat` — an object's inferred content type;
243
+ * `null` when the provider cannot infer one (e.g. an unknown suffix). */
244
+ export const storageResourceStat = z.object({
245
+ id: z.string(),
246
+ content_type: z.string().nullable(),
247
+ });
248
+ export type StorageResourceStat = z.infer<typeof storageResourceStat>;
249
+
250
+ /** `POST /api/storage/resources` — the stored object's id. */
251
+ export const storageResourceStored = z.object({ id: z.string(), stored: z.literal(true) });
252
+
253
+ /** `DELETE /api/storage/resources/{id}` — the removed object's id. */
254
+ export const storageResourceDeleted = z.object({ id: z.string(), deleted: z.literal(true) });
255
+
256
+ /** `DELETE /api/storage/dirs/{path}` — the removed directory subtree. */
257
+ export const storageDirDeleted = z.object({ dir: z.string(), deleted: z.literal(true) });
258
+
259
+ // -- backend identity + fleet (worker bus) -----------------------------------
260
+
261
+ /** `GET /api/backend` — the execution-backend identity, or the absent sentinel
262
+ * (`present: false`, a 200) when no backend plugin is registered. Backend identity
263
+ * is kept distinct from the fleet: the fleet doors ride the app's worker bus and
264
+ * need no backend at all. */
265
+ export const backendInfo = z.object({
266
+ present: z.boolean(),
267
+ backend: z.string().nullable(),
268
+ module: z.string().nullable(),
269
+ });
270
+ export type BackendInfo = z.infer<typeof backendInfo>;
271
+
272
+ /** The two worker kinds that join the bus: an ASGI `serve` worker or a
273
+ * `backend` runtime process. A drift throws `ApiSchemaError`. */
274
+ export const originKind = z.enum(['serve', 'backend']);
275
+ export type OriginKind = z.infer<typeof originKind>;
276
+
277
+ /**
278
+ * One live worker on the bus — a presence-census entry. `origin` is the
279
+ * `{kind}-{uuid}` identity used as a reload target; `kind` tells the two worker
280
+ * kinds apart; `pid` is the process id the presence key carries.
281
+ */
282
+ export const fleetOrigin = z.object({
283
+ origin: z.string(),
284
+ kind: originKind,
285
+ pid: z.number(),
286
+ });
287
+ export type FleetOrigin = z.infer<typeof fleetOrigin>;
288
+
289
+ /**
290
+ * `GET /api/fleet/workers` — the live worker fleet from the bus presence census.
291
+ * The census lists ALL subscribed origins (ASGI `serve` workers and backend-runtime
292
+ * processes alike), not only backend workers. A census read that fails surfaces as a
293
+ * loud 500, never a fabricated empty fleet.
294
+ */
295
+ export const fleetWorkers = z.object({ workers: z.array(fleetOrigin) });
296
+ export type FleetWorkers = z.infer<typeof fleetWorkers>;
297
+
298
+ /**
299
+ * One origin's outcome within a fleet broadcast. `applied`/`failed` are terminal
300
+ * wire replies from a subscriber; `missing`/`departed`/`timed_out` are
301
+ * publisher-computed from the presence census (a silent origin never sends them).
302
+ */
303
+ export const fleetOutcome = z.enum(['applied', 'failed', 'missing', 'departed', 'timed_out']);
304
+ export type FleetOutcome = z.infer<typeof fleetOutcome>;
305
+
306
+ /**
307
+ * One origin's result within a {@link fleetResult}. `payload` carries query-op data
308
+ * (opaque to the UI); `error` carries a failed apply's message; `detail` carries the
309
+ * publisher's report text for a computed `missing`/`departed`/`timed_out`. A drift
310
+ * throws `ApiSchemaError`.
311
+ */
312
+ export const fleetOriginResult = z.object({
313
+ origin: z.string(),
314
+ outcome: fleetOutcome,
315
+ payload: jsonValue,
316
+ error: z.string().nullable(),
317
+ detail: z.string().nullable(),
318
+ });
319
+ export type FleetOriginResult = z.infer<typeof fleetOriginResult>;
320
+
321
+ /**
322
+ * The awaited per-origin report of one fleet broadcast. Two honest shapes ride the
323
+ * same schema. Reachable (`reachable: true`): `results` holds one entry per expected
324
+ * origin (the serving worker's own entry included), so an unconfirmed sibling is a
325
+ * visible `failed`/`missing`/`departed`/`timed_out` entry, never a silent stale
326
+ * worker. Bus-unreachable (`reachable: false`): the transport failed before any
327
+ * origin could reply, so `results` is empty and only `error` is carried. This is the
328
+ * bare body of `POST /api/fleet/reload-config` and the single-MCP reload; it is also
329
+ * the body the mode-wrapped {@link fleetReportFanout} carries under its `fleet` /
330
+ * `unreachable` modes.
331
+ */
332
+ export const fleetResult = z.object({
333
+ op: z.string(),
334
+ reachable: z.boolean(),
335
+ local_only: z.boolean(),
336
+ results: z.array(fleetOriginResult),
337
+ error: z.string().nullable(),
338
+ });
339
+ export type FleetResult = z.infer<typeof fleetResult>;
340
+
341
+ /**
342
+ * `POST /api/fleet/reload-config` — soft-restart the fleet and report per-origin. The
343
+ * serving worker applies its own reload first, then broadcasts; the returned report
344
+ * names every origin's outcome. A failed local apply does not abort the broadcast, so
345
+ * the report is embedded even on the convergence-failure path.
346
+ */
347
+ export const fleetReloadResult = fleetResult;
348
+ export type FleetReloadResult = z.infer<typeof fleetReloadResult>;
349
+
350
+ /**
351
+ * The mode-wrapped fan-out summary a mutation embeds under its `fanout` field. Three
352
+ * honest modes, discriminated on `mode`:
353
+ * - `local-only` — the broadcast reached no sibling (a lone worker / no bus), a
354
+ * benign single-process note.
355
+ * - `fleet` — a reachable multi-worker broadcast; carries the full {@link
356
+ * fleetResult} so an unconfirmed sibling is visible.
357
+ * - `unreachable` — the bus itself could not be reached; carries the
358
+ * bus-unreachable {@link fleetResult} (no origin list, only `error`).
359
+ *
360
+ * Every mutation whose response embeds this parses it EXPLICITLY: zod's default
361
+ * unknown-key stripping would otherwise silently drop the field, hiding a failed
362
+ * propagation from the shared fleet-report handler. A drift throws `ApiSchemaError`.
363
+ */
364
+ export const fleetReportFanout = z.discriminatedUnion('mode', [
365
+ z.object({ mode: z.literal('local-only'), note: z.string() }),
366
+ fleetResult.extend({ mode: z.literal('fleet') }),
367
+ fleetResult.extend({ mode: z.literal('unreachable') }),
368
+ ]);
369
+ export type FleetReportFanout = z.infer<typeof fleetReportFanout>;
370
+
371
+ // -- manifest / mcp ----------------------------------------------------------
372
+
373
+ export const manifestView = z.object({
374
+ mcp: z.array(z.record(z.string(), z.unknown())),
375
+ user_tools: z.array(z.string()),
376
+ });
377
+ /**
378
+ * The apply result every config-writer mutation returns (`POST /api/mcp-config`,
379
+ * `POST /api/config/env`): the serving worker's local reload result (`status` +
380
+ * `env_keys`) plus the mode-wrapped fleet fan-out of the reload it broadcast. The
381
+ * `fanout` is parsed EXPLICITLY so the shared fleet-report handler can surface a
382
+ * failed propagation — zod's unknown-key stripping would otherwise hide it. A drift
383
+ * throws `ApiSchemaError`.
384
+ */
385
+ export const reloadConfigResult = z.object({
386
+ status: z.string(),
387
+ env_keys: z.number(),
388
+ fanout: fleetReportFanout,
389
+ });
390
+ export type ReloadConfigResult = z.infer<typeof reloadConfigResult>;
391
+
392
+ // -- system kinds ------------------------------------------------------------
393
+ // `GET /api/system/kinds` — one row per pluggable kind (identity, accounts,
394
+ // monitoring, storage, backend, channels, webhook_verifiers, config,
395
+ // studio_plugins). `state` is the kind's active/default/off status: `active` = a
396
+ // real plugin serves it, `default` = a built-in fallback serves it, `off` =
397
+ // nothing is registered (a legal state, reported not "fixed"). `plugin` is the
398
+ // provider/module name when known, else null. Plain (non-strict) `z.object` so a
399
+ // server that adds a field to the row still parses.
400
+
401
+ export const kindStatus = z.object({
402
+ kind: z.string(),
403
+ state: z.enum(['active', 'default', 'off']),
404
+ plugin: z.string().nullable(),
405
+ detail: z.string(),
406
+ });
407
+ export const kindStatusList = z.array(kindStatus);
408
+ export type KindStatus = z.infer<typeof kindStatus>;
409
+
410
+ /**
411
+ * `POST /api/tools/{name}/extensions` — the apply result. The write persists the
412
+ * manifest, reloads locally, then broadcasts the reload to the whole fleet. The
413
+ * response is the one apply-result shape every config-writer returns: the local
414
+ * reload result (`status` + `env_keys`) plus the mode-wrapped fleet `fanout`. The
415
+ * fan-out rides EVERY deployment (a lone worker reports `local-only`), so this flat
416
+ * shape parses every save and the shared fleet-report handler renders any failed
417
+ * propagation from `fanout`.
418
+ */
419
+ export const toolExtensionsApplyResult = reloadConfigResult;
420
+ export type ToolExtensionsApplyResult = z.infer<typeof toolExtensionsApplyResult>;
421
+ export const mcpStatus = z.object({
422
+ bound: z.record(z.string(), z.array(z.string())),
423
+ failed: z.array(z.object({ title: z.string(), status: z.string() })),
424
+ });
425
+
426
+ /**
427
+ * `POST /api/mcp-status/{title}/reload` — re-probe a single MCP server and broadcast
428
+ * the reload to the fleet. The response is the bare per-origin {@link fleetResult}
429
+ * (the serving worker's re-probe rides its own `results` entry as the payload); there
430
+ * is no local/workers split. A drift throws `ApiSchemaError`.
431
+ */
432
+ export const mcpReloadResult = fleetResult;
433
+ export type McpReloadResult = z.infer<typeof mcpReloadResult>;
434
+
435
+ // -- sub-mcp -----------------------------------------------------------------
436
+
437
+ export const subMcpList = z.record(z.string(), z.record(z.string(), z.unknown()));
438
+ export const subMcpCreated = z.object({ slug: z.string(), tools: z.array(z.string()) });
439
+ export const subMcpRemoved = z.object({ slug: z.string(), removed: z.literal(true) });
440
+
441
+ // -- config ------------------------------------------------------------------
442
+
443
+ export const envConfig = z.object({
444
+ env: z.record(z.string(), z.string()),
445
+ secret_keys: z.array(z.string()),
446
+ });
447
+ /**
448
+ * `GET /api/config/mode` — the active config backend mode (`file` / `k8s`). The
449
+ * skeleton emits ONLY `config_mode`; `read_only` is a UI-side gate that no config
450
+ * backend currently drives (both the `file` and `k8s` providers implement writes),
451
+ * so it is tolerated-absent and defaults to `false` (writable). A deployment whose
452
+ * config is genuinely read-only can still opt in by emitting `read_only: true`,
453
+ * which the settings surface honors — but its absence is the normal case, not a
454
+ * drift, so requiring it here would make every real `getConfigMode()` throw.
455
+ */
456
+ export const configMode = z.object({
457
+ config_mode: z.string(),
458
+ read_only: z.boolean().default(false),
459
+ });
460
+
461
+ // -- connectors --------------------------------------------------------------
462
+
463
+ export const subServiceView = z.object({
464
+ id: z.string(),
465
+ display_name: z.string(),
466
+ description: z.string(),
467
+ scopes: z.array(z.string()),
468
+ });
469
+
470
+ export const configFieldView = z.object({
471
+ key: z.string(),
472
+ label: z.string(),
473
+ target: z.enum(['env', 'header']),
474
+ required: z.boolean(),
475
+ secret: z.boolean(),
476
+ });
477
+
478
+ export const providerView = z.object({
479
+ id: z.string(),
480
+ display_name: z.string(),
481
+ description: z.string(),
482
+ icon_url: z.string(),
483
+ kind: z.enum(['oauth', 'none']),
484
+ origin: z.enum(['system', 'community']),
485
+ category: z.string(),
486
+ sub_services: z.array(subServiceView),
487
+ config_fields: z.array(configFieldView),
488
+ });
489
+ export const providers = z.array(providerView);
490
+ export type ProviderView = z.infer<typeof providerView>;
491
+
492
+ export const connectionView = z.object({
493
+ connection_id: z.string(),
494
+ provider_id: z.string(),
495
+ alias: z.string(),
496
+ kind: z.enum(['oauth', 'none']),
497
+ account_identity: z.string().nullable(),
498
+ enabled_sub_services: z.array(z.string()),
499
+ granted_scopes: z.array(z.string()),
500
+ auth_health_state: z.enum(['healthy', 'reconnect_required', 'refresh_failing']),
501
+ created_at: z.string(),
502
+ });
503
+ export type ConnectionView = z.infer<typeof connectionView>;
504
+
505
+ export const connectionsList = z.object({ items: z.array(connectionView), total: z.number() });
506
+
507
+ // A connector mutation that writes the manifest embeds the mode-wrapped fleet
508
+ // `fanout` of the reload it broadcast; it is `null` when the response path mutated no
509
+ // manifest (an OAuth START opens a flow before any write). Parsed EXPLICITLY so the
510
+ // shared fleet-report handler surfaces a failed propagation on every connector flow —
511
+ // zod's unknown-key stripping would otherwise drop it silently.
512
+ export const startConnectResult = z.union([
513
+ z.object({ flow_id: z.string(), authorize_url: z.string() }),
514
+ z.object({
515
+ connection_id: z.string(),
516
+ added_manifest_entries: z.array(z.string()),
517
+ fanout: fleetReportFanout.nullable(),
518
+ }),
519
+ ]);
520
+
521
+ // A reconnect START opens an OAuth flow and writes no manifest, so it carries no
522
+ // fan-out (the manifest write lands when the flow completes).
523
+ export const reconnectResult = z.object({ flow_id: z.string(), authorize_url: z.string() });
524
+
525
+ export const disconnectResult = z.object({
526
+ connection_id: z.string(),
527
+ upstream_revoke_outcome: z.enum(['success', 'failed', 'skipped']),
528
+ upstream_revoke_status: z.number().nullable(),
529
+ removed_manifest_entries: z.array(z.string()),
530
+ fanout: fleetReportFanout.nullable(),
531
+ });
532
+
533
+ export const patchSubServicesResult = z.object({
534
+ connection_id: z.string(),
535
+ enabled_sub_services: z.array(z.string()),
536
+ consent_required: z.boolean(),
537
+ flow_id: z.string().nullable(),
538
+ authorize_url: z.string().nullable(),
539
+ added_manifest_entries: z.array(z.string()),
540
+ removed_manifest_entries: z.array(z.string()),
541
+ // `null` on a consent-only toggle that forks an OAuth flow without a manifest write.
542
+ fanout: fleetReportFanout.nullable(),
543
+ });
544
+
545
+ export const oauthCompleteResult = z.discriminatedUnion('kind', [
546
+ z.object({
547
+ kind: z.literal('success'),
548
+ connection_id: z.string(),
549
+ return_url: z.string(),
550
+ // `null` when completing the flow mutated no manifest (a reconcile that changed
551
+ // nothing).
552
+ fanout: fleetReportFanout.nullable(),
553
+ }),
554
+ z.object({ kind: z.literal('failed'), reason: z.string() }),
555
+ z.object({ kind: z.literal('cancelled'), message: z.string() }),
556
+ ]);
557
+ export type OAuthCompleteResult = z.infer<typeof oauthCompleteResult>;
558
+
559
+ // -- studio plugin registry --------------------------------------------------
560
+
561
+ export const studioPluginManifest = z.object({
562
+ name: z.string(),
563
+ version: z.string(),
564
+ api_version: z.number(),
565
+ entry: z.string(),
566
+ integrity: z.record(z.string(), z.string()),
567
+ contributions: z.object({
568
+ tool_panels: z.record(z.string(), z.string()).default({}),
569
+ pages: z.array(z.string()).default([]),
570
+ settings_tabs: z.array(z.string()).default([]),
571
+ // Optional with a default so a manifest without the field still parses; the
572
+ // non-strict object would otherwise SILENTLY STRIP an undeclared field, so a
573
+ // server that serves `nav_entries` would never reach the UI.
574
+ nav_entries: z.array(z.string()).default([]),
575
+ }),
576
+ });
577
+ export const studioPluginRegistry = z.array(studioPluginManifest);
578
+ export type StudioPluginManifest = z.infer<typeof studioPluginManifest>;
579
+
580
+ // -- interactions ------------------------------------------------------------
581
+ // Shapes match the skeleton's interactions SSE contract: the SSE
582
+ // `interaction.add` frame carries the question and its answer format; the answer
583
+ // door returns { interaction_id, status }. `answered` is not a wire field — it
584
+ // is a client-side flag the stream hook flips on an `interaction.answered` event.
585
+
586
+ export const answerFormat = z.enum(['text', 'confirm', 'select', 'form', 'external']);
587
+ export type AnswerFormat = z.infer<typeof answerFormat>;
588
+
589
+ /** The kind of a question's display-only media item: a remote/inline image, or a link. */
590
+ export const mediaKind = z.enum(['image', 'link']);
591
+ export type MediaKind = z.infer<typeof mediaKind>;
592
+
593
+ /**
594
+ * One item of a question's display-only media. Applied PER ITEM by the renderer
595
+ * (never as a strict array on the frame): the frame's `media` field is a loose
596
+ * `z.array(z.unknown())`, and the renderer `safeParse`s each item with this schema
597
+ * so one malformed/hostile item is a loud per-item notice, not a whole-frame parse
598
+ * failure that would silently vanish an answerable question.
599
+ *
600
+ * `caption` is `.nullish()` (absent OR `null` both parse to a caption-less item):
601
+ * a caption-less item may arrive with the key omitted OR with an explicit `null`, so
602
+ * `.optional()` here would false-fail the explicit `null` and render a good image as
603
+ * malformed. `url` and `kind` are the only load-bearing fields; the image
604
+ * src scheme gate and the link neutralization are enforced by the renderer.
605
+ */
606
+ export const interactionMediaItem = z.object({
607
+ kind: mediaKind,
608
+ url: z.string(),
609
+ caption: z.string().nullish(),
610
+ });
611
+ export type InteractionMediaItem = z.infer<typeof interactionMediaItem>;
612
+
613
+ export const interaction = z.object({
614
+ interaction_id: z.string(),
615
+ group_id: z.string(),
616
+ question: z.string().default(''),
617
+ answer_format: answerFormat,
618
+ // Format-specific payload: select options, form JSON schema, external url. The
619
+ // wire sends `null` for formats that carry none (text/confirm); it is
620
+ // normalized to `{}` so consumers always see an object.
621
+ format_payload: z
622
+ .record(z.string(), z.unknown())
623
+ .nullish()
624
+ .transform((value) => value ?? {}),
625
+ created_at: z.string(),
626
+ timeout_at: z.string(),
627
+ // Whether the answer body is sensitive and never persisted server-side. Rides
628
+ // every add frame; an absent value (older frame) normalizes to false.
629
+ sensitive: z.boolean().default(false),
630
+ // True only for an external question answered by a signed server-to-server
631
+ // callback (never by a human); the frame never carries the verifier object.
632
+ // Absent/undefined for every other question.
633
+ server_verified: z.boolean().optional(),
634
+ // Name of the channel plugin the question was also delivered through (e.g.
635
+ // "telegram"); absent when the question lives only in this inbox.
636
+ channel: z.string().optional(),
637
+ // Display-only media shown WITH the question (images and/or links). Absent when
638
+ // the question carries none. Deliberately LOOSE (`z.array(z.unknown())`): each
639
+ // item is `safeParse`d per item by the renderer against `interactionMediaItem`,
640
+ // so one malformed/hostile item can never fail the whole frame parse and hide an
641
+ // answerable question. A present non-array value is still malformed.
642
+ media: z.array(z.unknown()).optional(),
643
+ });
644
+ export type Interaction = z.infer<typeof interaction>;
645
+
646
+ /** The answer door's response — the blocked caller was woken. */
647
+ export const interactionAnswered = z.object({
648
+ interaction_id: z.string(),
649
+ status: z.string(),
650
+ });
651
+ export type InteractionAnswered = z.infer<typeof interactionAnswered>;
652
+
653
+ // -- channels ----------------------------------------------------------------
654
+ // The installed channel-plugin catalog: names registered via the manifest's
655
+ // channel modules. A deployment with none bound answers questions only in this
656
+ // inbox (the Studio default).
657
+
658
+ export const channels = z.object({
659
+ channels: z.array(z.string()),
660
+ });
661
+ export type Channels = z.infer<typeof channels>;
662
+
663
+ // -- notifications -----------------------------------------------------------
664
+ // The internal notifications sink: the messages the `notify_user` operation records
665
+ // with no channel, so they land only in the Studio inbox. Each record carries the
666
+ // server-minted id + timestamp; `recipient` is null when the message names none.
667
+
668
+ export const notification = z.object({
669
+ id: z.string(),
670
+ message: z.string(),
671
+ recipient: z.string().nullable(),
672
+ created_at: z.string(),
673
+ });
674
+ export type Notification = z.infer<typeof notification>;
675
+
676
+ export const notifications = z.object({
677
+ notifications: z.array(notification),
678
+ });
679
+ export type Notifications = z.infer<typeof notifications>;
680
+
681
+ // -- hooks -------------------------------------------------------------------
682
+ // Shapes match tai_contract.hooks.HookParams: a hook fires a `tool` on a `topic`,
683
+ // optionally gated by a `condition` and shaped by an `expr` (each either an inline
684
+ // spec or a registered id, with its own kwargs).
685
+
686
+ export const hookParams = z.object({
687
+ name: z.string(),
688
+ topic: z.string(),
689
+ tool: z.string(),
690
+ tool_kwargs: z.record(z.string(), z.unknown()).default({}),
691
+ condition: z.string().nullable().default(null),
692
+ condition_id: z.string().nullable().default(null),
693
+ // The `*_kwargs` pair is non-optional in the contract (defaults to {}, never
694
+ // null) — unlike the sibling condition/expr string fields, which are nullable.
695
+ condition_kwargs: z.record(z.string(), z.unknown()).default({}),
696
+ expr: z.string().nullable().default(null),
697
+ expr_id: z.string().nullable().default(null),
698
+ expr_kwargs: z.record(z.string(), z.unknown()).default({}),
699
+ });
700
+ export type HookParams = z.infer<typeof hookParams>;
701
+
702
+ export const hookList = z.object({
703
+ items: z.array(hookParams),
704
+ total: z.number(),
705
+ // Per-topic verifier bindings: the topic name maps to the verifier bound to it
706
+ // and that verifier's config (which carries a `secret_env` name, never a secret
707
+ // value). Tolerant of an older/empty response, which omits the key entirely.
708
+ topic_verifiers: z
709
+ .record(
710
+ z.string(),
711
+ z.object({ verifier: z.string(), config: z.record(z.string(), z.unknown()) }),
712
+ )
713
+ .default({}),
714
+ });
715
+
716
+ export const hookRegistered = z.object({
717
+ registered: z.boolean(),
718
+ name: z.string(),
719
+ });
720
+
721
+ export const hookRemoved = z.object({
722
+ removed: z.boolean(),
723
+ name: z.string(),
724
+ });
725
+
726
+ // The registered webhook-verifier catalog (`GET /api/hooks/verifiers`) — the
727
+ // sorted verifier NAMES only, never any verifier's config. An empty registry is a
728
+ // valid empty array. This is the ONLY source the topic-verifier bind picker reads.
729
+ export const hookVerifiers = z.array(z.string());
730
+
731
+ /** `PUT /api/hooks/topics/{topic}/verifier` — the bound topic + verifier name. */
732
+ export const topicVerifierSet = z.object({ topic: z.string(), verifier: z.string() });
733
+
734
+ /** `DELETE /api/hooks/topics/{topic}/verifier` — the unbound topic. */
735
+ export const topicVerifierRemoved = z.object({ removed: z.boolean(), topic: z.string() });
736
+
737
+ // -- settings schema ---------------------------------------------------------
738
+ // The settings form is auto-rendered from a declarative group/field description
739
+ // the skeleton emits from its typed settings models.
740
+
741
+ export const settingsSchema = z.object({
742
+ groups: z.array(
743
+ z.object({
744
+ name: z.string(),
745
+ module: z.string(),
746
+ qualname: z.string(),
747
+ fields: z.array(
748
+ z.object({
749
+ name: z.string(),
750
+ env_var: z.string(),
751
+ type: z.string(),
752
+ default: jsonValue.nullable(),
753
+ required: z.boolean(),
754
+ secret: z.boolean(),
755
+ description: z.string().nullable(),
756
+ nested_group: z.string().nullable(),
757
+ value: jsonValue.nullable(),
758
+ }),
759
+ ),
760
+ }),
761
+ ),
762
+ });
763
+ export type SettingsSchema = z.infer<typeof settingsSchema>;
764
+
765
+ // -- auth: scopes + api keys -------------------------------------------------
766
+
767
+ /** `{ url: scope_id }` — every URL currently mapped to a scope. */
768
+ export const authScopes = z.record(z.string(), z.string());
769
+
770
+ export const addUrlToScopeResult = z.object({ scope_id: z.string(), url: z.string() });
771
+ export const removeUrlFromScopeResult = z.object({ url: z.string() });
772
+ export const removeScopeResult = z.object({ scope_id: z.string(), deleted_keys: z.number() });
773
+
774
+ /**
775
+ * One row of the route catalog (`GET /api/auth/routes`) — a plain HTTP route with
776
+ * its current mapping. `mapped` is the scope id, the public marker, or `null` when
777
+ * the path is unmapped (the mapper's "unassigned" bucket source). `methods` is the
778
+ * route's method set with `HEAD` removed.
779
+ */
780
+ export const authRoute = z.object({
781
+ path: z.string(),
782
+ methods: z.array(z.string()),
783
+ mapped: z.string().nullable(),
784
+ });
785
+ export type AuthRoute = z.infer<typeof authRoute>;
786
+ export const authRoutes = z.array(authRoute);
787
+
788
+ /** `GET /api/auth/public-routes` — the sorted urls pinned to the public marker. */
789
+ export const publicRoutes = z.array(z.string());
790
+
791
+ /** `POST /api/auth/public-routes` — the pinned url. */
792
+ export const pinPublicResult = z.object({ url: z.string() });
793
+ /** `DELETE /api/auth/public-routes` — the unpinned url (404 when not pinned). */
794
+ export const unpinPublicResult = z.object({ url: z.string() });
795
+
796
+ export const tokensPayload = z.array(
797
+ z.object({
798
+ user_id: z.string(),
799
+ description: z.string(),
800
+ scopes: z.array(z.string()),
801
+ policy_data: jsonValue,
802
+ condition: z.string().nullable().optional(),
803
+ condition_id: z.string().nullable().optional(),
804
+ condition_kwargs: jsonValue.nullable().optional(),
805
+ }),
806
+ );
807
+ export type TokensPayload = z.infer<typeof tokensPayload>;
808
+
809
+ /** The raw `sk-…` key string, returned once at creation time and never again. */
810
+ export const createdApiKey = z.string();
811
+ export const editApiKeyResult = z.object({ user_id: z.string(), updated: z.boolean() });
812
+ export const revokeApiKeyResult = z.object({ user_id: z.string(), revoked: z.boolean() });
813
+
814
+ // -- login methods (the public sign-in aggregator) ---------------------------
815
+
816
+ /**
817
+ * One field of a `form` login method. `secret` fields render as password
818
+ * inputs; `autocomplete` passes through to the input (e.g. "email",
819
+ * "current-password", "new-password").
820
+ */
821
+ export const loginFormField = z.object({
822
+ name: z.string().min(1),
823
+ label: z.string().min(1),
824
+ secret: z.boolean(),
825
+ autocomplete: z.string().optional(),
826
+ });
827
+
828
+ /**
829
+ * The two — and only two — login-method shapes. Discriminated on `shape`; an
830
+ * unknown shape fails validation loudly (contract drift, not a soft skip).
831
+ * Optional fields are `.optional()` (absent), never `.nullable()`: the server
832
+ * serializes with `exclude_none`, so an absent field is OMITTED, never `null`.
833
+ */
834
+ export const loginMethod = z.discriminatedUnion('shape', [
835
+ z.object({
836
+ shape: z.literal('form'),
837
+ id: z.string().min(1),
838
+ title: z.string().min(1),
839
+ purpose: z.enum(['login', 'bootstrap', 'invite']).default('login'),
840
+ fields: z.array(loginFormField).min(1),
841
+ submit_path: z.string().min(1),
842
+ }),
843
+ z.object({
844
+ shape: z.literal('button'),
845
+ id: z.string().min(1),
846
+ label: z.string().min(1),
847
+ icon: z.string().optional(),
848
+ href: z.string().min(1),
849
+ }),
850
+ ]);
851
+
852
+ /**
853
+ * `GET /api/login/methods` (PUBLIC). `bootstrap: true` ⇒ the deployment has zero
854
+ * accounts and `methods` contains the owner-creation form. Non-strict — the
855
+ * skeleton may grow additive fields.
856
+ */
857
+ export const loginMethods = z.object({
858
+ methods: z.array(loginMethod),
859
+ bootstrap: z.boolean(),
860
+ });
861
+
862
+ /**
863
+ * A successful login/exchange: the minted session token (opaque, `tai-sess-`
864
+ * prefixed — the client treats it as an opaque string) + its user.
865
+ */
866
+ export const loginResult = z.object({
867
+ token: z.string().min(1),
868
+ user_id: z.string().min(1),
869
+ });
870
+
871
+ export type LoginFormField = z.infer<typeof loginFormField>;
872
+ export type LoginMethod = z.infer<typeof loginMethod>;
873
+ export type LoginMethods = z.infer<typeof loginMethods>;
874
+ export type LoginResult = z.infer<typeof loginResult>;
875
+
876
+ // -- auth: the caller's capability projection (GET /api/auth/me) --------------
877
+
878
+ /** A concrete route the caller can reach, with the methods that pass its jq fence. */
879
+ export const routeEntry = z.object({
880
+ path: z.string(),
881
+ methods: z.array(z.string()),
882
+ });
883
+ export type RouteEntry = z.infer<typeof routeEntry>;
884
+
885
+ /**
886
+ * A dynamic route pattern the caller can reach — a mount/pattern surface not
887
+ * enumerable into concrete paths, projected only when its scope AND jq admit it.
888
+ */
889
+ export const patternEntry = z.object({
890
+ pattern: z.string(),
891
+ scope_id: z.string(),
892
+ });
893
+ export type PatternEntry = z.infer<typeof patternEntry>;
894
+
895
+ /** A sub-MCP mount the caller can reach, with the tools it exposes. */
896
+ export const subMcpEntry = z.object({
897
+ slug: z.string(),
898
+ tools: z.array(z.string()),
899
+ transport: z.string(),
900
+ });
901
+ export type SubMcpEntry = z.infer<typeof subMcpEntry>;
902
+
903
+ /**
904
+ * `GET /api/auth/me` — the caller's derived capability projection: the concrete
905
+ * (path, method) surface, dynamic patterns, sub-MCP mounts, tools, and agents it
906
+ * can reach RIGHT NOW (derived server-side, never stored). `admin` is the
907
+ * condition-free ownerless `"*"` discriminator (a TOTAL projection); a scoped
908
+ * session carries `admin: false` and a jq-exact `routes` list. `owner_user_id` is
909
+ * the key's owner claim, `null` for an ownerless key. The invariant is
910
+ * projection ⊆ gate: every projected surface is one the server would admit, so
911
+ * the UI can filter on it without ever advertising a door the gate denies.
912
+ */
913
+ export const meProjection = z.object({
914
+ user_id: z.string(),
915
+ owner_user_id: z.string().nullable(),
916
+ admin: z.boolean(),
917
+ scopes: z.array(z.string()),
918
+ routes: z.array(routeEntry),
919
+ route_patterns: z.array(patternEntry),
920
+ sub_mcp: z.array(subMcpEntry),
921
+ tools: z.array(z.string()),
922
+ agents: z.array(z.string()),
923
+ mintable: z.boolean(),
924
+ });
925
+ export type MeProjection = z.infer<typeof meProjection>;
926
+
927
+ /**
928
+ * `POST /api/auth/claim-links` — a one-time claim link minted in the key-create
929
+ * flow. `claim_path` is a fragment-carrier path (`/login#claim=<token>`, never an
930
+ * absolute URL — the Studio composes the origin); `token` is the raw claim token
931
+ * returned exactly ONCE; `expires_at` is an ISO-8601 instant.
932
+ */
933
+ export const claimLinkCreated = z.object({
934
+ claim_path: z.string(),
935
+ token: z.string(),
936
+ expires_at: z.string(),
937
+ });
938
+ export type ClaimLinkCreated = z.infer<typeof claimLinkCreated>;
939
+
940
+ /**
941
+ * `POST /api/auth/logout` — the single authed logout dispatcher's result.
942
+ * `revoked` is `true` when a live accounts session was revoked; a plain `sk-` key
943
+ * has nothing to revoke and answers `404` (a loud absent-session signal), never a
944
+ * `revoked: false` body. Non-strict, so a SUCCESSFUL logout never throws
945
+ * `ApiSchemaError` on an additive field.
946
+ */
947
+ export const logoutResult = z.object({ revoked: z.boolean() });
948
+ export type LogoutResult = z.infer<typeof logoutResult>;
949
+
950
+ /**
951
+ * `GET /api/auth/capabilities` — whether this deployment can MINT API keys
952
+ * locally. `mintable: false` means every configured identity provider is
953
+ * validator-only (keys are issued at an external issuer), so the key-creation
954
+ * UI disables up front instead of surfacing a raw mint failure. Non-strict.
955
+ */
956
+ export const authCapabilities = z.object({
957
+ mintable: z.boolean(),
958
+ providers: z.array(z.object({ name: z.string(), mintable: z.boolean() })),
959
+ });
960
+ export type AuthCapabilities = z.infer<typeof authCapabilities>;
961
+
962
+ // -- access-control policy (authoring + versioning) --------------------------
963
+
964
+ /**
965
+ * `POST /api/auth/validate-condition` — the fail-closed jq guard. `ok` is the
966
+ * compile/render result; `result` is the boolean of a sample evaluation, or
967
+ * `null` when no `sample_context` was supplied (compile-only). A broken condition
968
+ * is a `400 {error}` surfaced by the transport, NEVER a false `ok` — so a
969
+ * syntactically-broken condition can be caught before it locks a key out. A drift
970
+ * throws `ApiSchemaError`.
971
+ */
972
+ export const validateConditionResult = z.object({
973
+ ok: z.boolean(),
974
+ result: z.boolean().nullable(),
975
+ });
976
+ export type ValidateConditionResult = z.infer<typeof validateConditionResult>;
977
+
978
+ /**
979
+ * The enforced policy shape a version body carries. SECRET-ADJACENT — `condition`
980
+ * can reveal auth logic, so its fixtures are hand-authored, never captured.
981
+ * `condition`/`condition_id` are mutually exclusive at author time; both are
982
+ * nullable here because whichever mode is unused is stored as `null`.
983
+ */
984
+ export const policyBody = z.object({
985
+ scopes: z.array(z.string()),
986
+ policy_data: jsonValue,
987
+ condition: z.string().nullable(),
988
+ condition_id: z.string().nullable(),
989
+ condition_kwargs: jsonValue.nullable(),
990
+ });
991
+ export type PolicyBody = z.infer<typeof policyBody>;
992
+
993
+ /**
994
+ * One immutable policy version row
995
+ * (`GET /api/auth/api-keys/{user_id}/policy/versions`). `is_current` flags the
996
+ * active pointer; `tags` is the generic per-version label list (kind-agnostic),
997
+ * mirroring the preset version shape.
998
+ */
999
+ export const policyVersion = z.object({
1000
+ version: z.number(),
1001
+ body: policyBody,
1002
+ tags: z.array(z.string()),
1003
+ created_at: z.string(),
1004
+ is_current: z.boolean(),
1005
+ });
1006
+ export type PolicyVersion = z.infer<typeof policyVersion>;
1007
+
1008
+ export const policyVersionList = z.array(policyVersion);
1009
+
1010
+ /** `POST /api/auth/api-keys/{user_id}/policy/rollback` — the re-pointed version. */
1011
+ export const policyRollback = z.object({ user_id: z.string(), active_version: z.number() });
1012
+
1013
+ // -- backup ------------------------------------------------------------------
1014
+
1015
+ export const backupSections = z.array(z.object({ name: z.string(), secret: z.boolean() }));
1016
+
1017
+ /** The exported backup document — validated when the UI reads a picked file. */
1018
+ export const backupDocument = z.object({
1019
+ version: z.literal(1),
1020
+ created_at: z.string(),
1021
+ sections: z.record(z.string(), z.unknown()),
1022
+ errors: z.record(z.string(), z.string()).optional(),
1023
+ });
1024
+ export type BackupDocument = z.infer<typeof backupDocument>;
1025
+
1026
+ export const backupImportReport = z.object({
1027
+ ok: z.boolean(),
1028
+ sections: z.record(
1029
+ z.string(),
1030
+ z.object({
1031
+ created: z.number(),
1032
+ updated: z.number(),
1033
+ skipped: z.number(),
1034
+ errors: z.array(z.string()),
1035
+ new_api_keys: z
1036
+ .array(
1037
+ z.object({
1038
+ user_id: z.string(),
1039
+ description: z.string(),
1040
+ api_key: z.string(),
1041
+ }),
1042
+ )
1043
+ .optional(),
1044
+ // The `manifest` and `env` sections apply through the config pipeline, so their
1045
+ // report carries the mode-wrapped fleet `fanout` of the restore's reload; other
1046
+ // sections omit it. Parsed EXPLICITLY so the shared fleet-report handler can
1047
+ // surface a failed restore propagation — otherwise zod would strip it silently.
1048
+ fanout: fleetReportFanout.optional(),
1049
+ }),
1050
+ ),
1051
+ });
1052
+ export type BackupImportReport = z.infer<typeof backupImportReport>;
1053
+
1054
+ // -- schedules ---------------------------------------------------------------
1055
+ // The skeleton returns opaque backend-tool results; the table reads a few known
1056
+ // fields and passes the rest through untouched.
1057
+
1058
+ export const scheduleItem = z
1059
+ .object({
1060
+ name: z.string(),
1061
+ enabled: z.boolean().optional(),
1062
+ schedule: jsonValue.optional(),
1063
+ target: jsonValue.optional(),
1064
+ args: z.array(jsonValue).optional(),
1065
+ kwargs: z.record(z.string(), jsonValue).optional(),
1066
+ })
1067
+ .passthrough();
1068
+ export const scheduleList = z.array(scheduleItem);
1069
+ export type ScheduleItem = z.infer<typeof scheduleItem>;
1070
+
1071
+ export const serverDateTime = z
1072
+ .object({
1073
+ utc: jsonValue,
1074
+ local: jsonValue.optional(),
1075
+ system: jsonValue.optional(),
1076
+ })
1077
+ .passthrough();
1078
+
1079
+ // -- observability -----------------------------------------------------------
1080
+
1081
+ export const dashboardMetrics = z.object({
1082
+ summary: z.object({
1083
+ totalRuns: z.number(),
1084
+ totalCost: z.number(),
1085
+ totalTokens: z.number(),
1086
+ averageLatencyMs: z.number(),
1087
+ avgCostPerRun: z.number(),
1088
+ avgTokensPerRun: z.number(),
1089
+ timeToFirstTokenMs: z.number().nullable(),
1090
+ }),
1091
+ timeSeries: z.array(
1092
+ z.object({
1093
+ bucket: z.string().nullable(),
1094
+ runs: z.number(),
1095
+ cost: z.number(),
1096
+ avgLatencyMs: z.number(),
1097
+ totalTokens: z.number(),
1098
+ }),
1099
+ ),
1100
+ byModel: z.array(
1101
+ z.object({
1102
+ model: z.string(),
1103
+ calls: z.number(),
1104
+ cost: z.number(),
1105
+ totalTokens: z.number(),
1106
+ avgLatencyMs: z.number(),
1107
+ }),
1108
+ ),
1109
+ granularity: z.enum(['hour', 'day', 'week']),
1110
+ });
1111
+ export type DashboardMetrics = z.infer<typeof dashboardMetrics>;
1112
+
1113
+ export const run = z.object({
1114
+ id: z.string(),
1115
+ traceId: z.string(),
1116
+ createdAt: z.string().nullable(),
1117
+ tags: z.array(z.string()),
1118
+ status: z.enum(['error', 'success']),
1119
+ cost: z.number().nullable(),
1120
+ latencyMs: z.number().nullable(),
1121
+ totalTokens: z.number().nullable(),
1122
+ model: z.string().nullable(),
1123
+ inputPreview: jsonValue,
1124
+ outputPreview: jsonValue,
1125
+ fetchError: z.string().nullable(),
1126
+ });
1127
+ export type Run = z.infer<typeof run>;
1128
+
1129
+ export const runsPage = z.object({
1130
+ items: z.array(run),
1131
+ page: z.number(),
1132
+ nextPage: z.number().nullable(),
1133
+ });
1134
+
1135
+ export const runSpan = z.object({
1136
+ id: z.string(),
1137
+ parentId: z.string().nullable(),
1138
+ traceId: z.string().nullable(),
1139
+ name: z.string().nullable(),
1140
+ type: z.string().nullable(),
1141
+ level: z.string().nullable(),
1142
+ statusMessage: z.string().nullable(),
1143
+ start: z.string().nullable(),
1144
+ end: z.string().nullable(),
1145
+ model: z.string().nullable(),
1146
+ usage: jsonValue.nullable(),
1147
+ metadata: jsonValue.nullable(),
1148
+ input: jsonValue,
1149
+ output: jsonValue,
1150
+ nodeId: z.string().nullable(),
1151
+ });
1152
+ export type RunSpan = z.infer<typeof runSpan>;
1153
+
1154
+ export const runTrace = z.object({
1155
+ traceId: z.string(),
1156
+ timestamp: z.string().nullable(),
1157
+ tags: z.array(z.string()),
1158
+ totalCost: z.number().nullable(),
1159
+ input: jsonValue,
1160
+ output: jsonValue,
1161
+ metadata: jsonValue.nullable(),
1162
+ availability: z.enum(['unavailable', 'full', 'partial']),
1163
+ fetchError: z.string().nullable(),
1164
+ spans: z.array(runSpan),
1165
+ });
1166
+ export type RunTrace = z.infer<typeof runTrace>;
1167
+
1168
+ // -- marketplace -------------------------------------------------------------
1169
+
1170
+ /**
1171
+ * One contained item of a marketplace plugin (a searchable row): its kind
1172
+ * (tool / agent / extension / …), name, description, and free-form tags.
1173
+ */
1174
+ export const marketplaceItem = z.object({
1175
+ kind: z.string(),
1176
+ name: z.string(),
1177
+ description: z.string(),
1178
+ tags: z.array(z.string()),
1179
+ });
1180
+ export type MarketplaceItem = z.infer<typeof marketplaceItem>;
1181
+
1182
+ /**
1183
+ * One item-level search row (`GET /api/marketplace/search`): the contained item
1184
+ * plus the listing it belongs to. `ref` is the "namespace/name" install target
1185
+ * (install targets the LISTING), so every row carries enough to group under and
1186
+ * link to its plugin. `latest_version` is nullable defensively; the search
1187
+ * relation only emits listings with a published version.
1188
+ */
1189
+ export const marketplaceSearchRow = z.object({
1190
+ item: marketplaceItem,
1191
+ ref: z.string(),
1192
+ namespace: z.string(),
1193
+ name: z.string(),
1194
+ display_name: z.string().nullable(),
1195
+ icon_url: z.string().nullable(),
1196
+ package: z.string(),
1197
+ description: z.string(),
1198
+ categories: z.array(z.string()),
1199
+ tags: z.array(z.string()),
1200
+ trust_tier: z.string(),
1201
+ pricing: z.string(),
1202
+ latest_version: z.string().nullable(),
1203
+ downloads: z.number(),
1204
+ updated_at: z.string(),
1205
+ });
1206
+ export type MarketplaceSearchRow = z.infer<typeof marketplaceSearchRow>;
1207
+
1208
+ /**
1209
+ * One page of search rows. The wire carries no next-page field — has-next is
1210
+ * COMPUTED: `page * page_size < total`.
1211
+ */
1212
+ export const marketplaceSearchPage = z.object({
1213
+ items: z.array(marketplaceSearchRow),
1214
+ total: z.number(),
1215
+ page: z.number(),
1216
+ page_size: z.number(),
1217
+ });
1218
+ export type MarketplaceSearchPage = z.infer<typeof marketplaceSearchPage>;
1219
+
1220
+ /**
1221
+ * One version of a listing as the versions list carries it (`version`, its
1222
+ * publication `status`, the `contract_range` it was validated against, and when
1223
+ * it was `published_at`). `contract_range` and `published_at` are null until a
1224
+ * version reaches the published state, so both are nullable.
1225
+ */
1226
+ export const marketplaceVersion = z.object({
1227
+ version: z.string(),
1228
+ contract_range: z.string().nullable(),
1229
+ status: z.string(),
1230
+ published_at: z.string().nullable(),
1231
+ });
1232
+ export type MarketplaceVersion = z.infer<typeof marketplaceVersion>;
1233
+
1234
+ /**
1235
+ * The listing's latest published version as embedded in its detail: the version
1236
+ * plus the items that version contains.
1237
+ */
1238
+ export const marketplaceLatestVersion = marketplaceVersion.extend({
1239
+ items: z.array(marketplaceItem),
1240
+ });
1241
+ export type MarketplaceLatestVersion = z.infer<typeof marketplaceLatestVersion>;
1242
+
1243
+ /** One advisory row; `withdrawn_at` non-null means it no longer applies. */
1244
+ export const marketplaceAdvisory = z.object({
1245
+ id: z.number(),
1246
+ // The "namespace/name" listing ref exactly as the registry serves it — the
1247
+ // wire field consumers match installed plugins against.
1248
+ listing: z.string(),
1249
+ affected_versions: z.string(),
1250
+ severity: z.string(),
1251
+ summary: z.string(),
1252
+ created_at: z.string(),
1253
+ withdrawn_at: z.string().nullable(),
1254
+ });
1255
+ export type MarketplaceAdvisory = z.infer<typeof marketplaceAdvisory>;
1256
+
1257
+ /**
1258
+ * A plugin's full detail (`GET /api/marketplace/plugins/{ns}/{name}`): the
1259
+ * listing, its latest published version (`null` when nothing is published yet,
1260
+ * carrying the contained items), and the known version history.
1261
+ */
1262
+ export const marketplacePluginDetail = z.object({
1263
+ namespace: z.string(),
1264
+ name: z.string(),
1265
+ display_name: z.string().nullable(),
1266
+ icon_url: z.string().nullable(),
1267
+ package: z.string(),
1268
+ description: z.string(),
1269
+ readme_md: z.string().nullable(),
1270
+ license: z.string().nullable(),
1271
+ homepage_url: z.string().nullable(),
1272
+ repository_url: z.string().nullable(),
1273
+ categories: z.array(z.string()),
1274
+ tags: z.array(z.string()),
1275
+ trust_tier: z.string(),
1276
+ pricing: z.string(),
1277
+ downloads: z.number(),
1278
+ latest: marketplaceLatestVersion.nullable(),
1279
+ versions: z.array(marketplaceVersion),
1280
+ });
1281
+ export type MarketplacePluginDetail = z.infer<typeof marketplacePluginDetail>;
1282
+
1283
+ /**
1284
+ * One installed marketplace plugin as the local attribution store records it.
1285
+ * `latest` is the newest registry version (`null` when unknown); update
1286
+ * availability comes from the wire field `update_available` (computed
1287
+ * server-side as a PEP 440 comparison). `missing_upstream` flags a plugin the
1288
+ * registry no longer lists.
1289
+ */
1290
+ export const marketplaceInstalledPlugin = z.object({
1291
+ ref: z.string(),
1292
+ version: z.string(),
1293
+ source: z.string(),
1294
+ installed_at: z.string(),
1295
+ latest: z.string().nullable(),
1296
+ update_available: z.boolean(),
1297
+ missing_upstream: z.boolean(),
1298
+ });
1299
+ export const marketplaceInstalled = z.array(marketplaceInstalledPlugin);
1300
+ export type MarketplaceInstalledPlugin = z.infer<typeof marketplaceInstalledPlugin>;
1301
+
1302
+ /** The advisory state for installed plugins, with when it was last fetched. */
1303
+ export const marketplaceAdvisories = z.object({
1304
+ advisories: z.array(marketplaceAdvisory),
1305
+ fetched_at: z.string(),
1306
+ });
1307
+
1308
+ /** The registry's controlled category list, proxied through the skeleton. */
1309
+ export const marketplaceCategories = z.array(z.string());
1310
+
1311
+ /**
1312
+ * The receipt of an install / update: what landed and at which version.
1313
+ * `notes` (env-selected items naming their activating env var) and any
1314
+ * non-critical `advisories` ride BOTH the install and update receipts. The wire
1315
+ * also carries `package`, `reload`, and `pip_output`, which the receipt does not
1316
+ * model — zod's default strip drops them; the UI renders the receipt, not the
1317
+ * raw install log.
1318
+ */
1319
+ export const marketplaceInstallResult = z.object({
1320
+ ref: z.string(),
1321
+ version: z.string(),
1322
+ notes: z.array(z.string()),
1323
+ advisories: z.array(marketplaceAdvisory),
1324
+ });
1325
+ export type MarketplaceInstallResult = z.infer<typeof marketplaceInstallResult>;
1326
+
1327
+ /**
1328
+ * The receipt of an uninstall. `notes` carries operator warnings (e.g. a config
1329
+ * provider still selected by its env var); render them like the install
1330
+ * receipt's notes.
1331
+ */
1332
+ export const marketplaceUninstallResult = z.object({
1333
+ ref: z.string(),
1334
+ uninstalled: z.literal(true),
1335
+ notes: z.array(z.string()),
1336
+ });
1337
+ export type MarketplaceUninstallResult = z.infer<typeof marketplaceUninstallResult>;
1338
+
1339
+ // Background tool-run schemas live in their own module; re-exported here so the
1340
+ // shared fixture-validation contract resolves them by name alongside the rest.
1341
+ export { toolRunSubmitResult, toolRunRecord, toolRunList } from './tool-runs';