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