codecraftai-restclient-pychestrate 1.2.0__py3-none-any.whl

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 (84) hide show
  1. ccai/pychestrate/py.typed +0 -0
  2. ccai/pychestrate/rest/client/__init__.py +0 -0
  3. ccai/pychestrate/rest/client/generation/__init__.py +0 -0
  4. ccai/pychestrate/rest/client/generation/pychestraterestclientgenerator.py +98 -0
  5. ccai/pychestrate/rest/client/generation/specs/pychestrate.yaml +3757 -0
  6. ccai/pychestrate/rest/client/groups/__init__.py +0 -0
  7. ccai/pychestrate/rest/client/groups/agents.py +171 -0
  8. ccai/pychestrate/rest/client/groups/artifacts.py +103 -0
  9. ccai/pychestrate/rest/client/groups/audit.py +99 -0
  10. ccai/pychestrate/rest/client/groups/auth.py +55 -0
  11. ccai/pychestrate/rest/client/groups/clients.py +287 -0
  12. ccai/pychestrate/rest/client/groups/dispatch.py +118 -0
  13. ccai/pychestrate/rest/client/groups/enrollment.py +212 -0
  14. ccai/pychestrate/rest/client/groups/flags.py +332 -0
  15. ccai/pychestrate/rest/client/groups/me.py +55 -0
  16. ccai/pychestrate/rest/client/groups/notifications.py +128 -0
  17. ccai/pychestrate/rest/client/groups/pluginapi.py +128 -0
  18. ccai/pychestrate/rest/client/groups/pods.py +558 -0
  19. ccai/pychestrate/rest/client/groups/progress.py +64 -0
  20. ccai/pychestrate/rest/client/groups/projects.py +420 -0
  21. ccai/pychestrate/rest/client/groups/projecttemplates.py +91 -0
  22. ccai/pychestrate/rest/client/groups/rbac.py +243 -0
  23. ccai/pychestrate/rest/client/groups/results.py +193 -0
  24. ccai/pychestrate/rest/client/groups/retention.py +169 -0
  25. ccai/pychestrate/rest/client/groups/runs.py +152 -0
  26. ccai/pychestrate/rest/client/groups/secrets.py +91 -0
  27. ccai/pychestrate/rest/client/groups/tagging.py +216 -0
  28. ccai/pychestrate/rest/client/groups/testcatalog.py +152 -0
  29. ccai/pychestrate/rest/client/groups/testruns.py +58 -0
  30. ccai/pychestrate/rest/client/groups/users.py +91 -0
  31. ccai/pychestrate/rest/client/groups/variables.py +228 -0
  32. ccai/pychestrate/rest/client/models/__init__.py +0 -0
  33. ccai/pychestrate/rest/client/models/agents.py +34 -0
  34. ccai/pychestrate/rest/client/models/artifactlocaluploadresult.py +18 -0
  35. ccai/pychestrate/rest/client/models/artifacts.py +24 -0
  36. ccai/pychestrate/rest/client/models/artifactuploadtarget.py +20 -0
  37. ccai/pychestrate/rest/client/models/audit.py +18 -0
  38. ccai/pychestrate/rest/client/models/auth.py +15 -0
  39. ccai/pychestrate/rest/client/models/capabilityentry.py +15 -0
  40. ccai/pychestrate/rest/client/models/client.py +19 -0
  41. ccai/pychestrate/rest/client/models/clients.py +40 -0
  42. ccai/pychestrate/rest/client/models/dispatch.py +26 -0
  43. ccai/pychestrate/rest/client/models/enrollment.py +39 -0
  44. ccai/pychestrate/rest/client/models/flags.py +55 -0
  45. ccai/pychestrate/rest/client/models/jobdeadline.py +11 -0
  46. ccai/pychestrate/rest/client/models/jobenvelope.py +38 -0
  47. ccai/pychestrate/rest/client/models/jobforwardinginfo.py +12 -0
  48. ccai/pychestrate/rest/client/models/jobpipelineref.py +11 -0
  49. ccai/pychestrate/rest/client/models/jobrefs.py +14 -0
  50. ccai/pychestrate/rest/client/models/jobresultslocator.py +11 -0
  51. ccai/pychestrate/rest/client/models/me.py +14 -0
  52. ccai/pychestrate/rest/client/models/notifications.py +24 -0
  53. ccai/pychestrate/rest/client/models/pagemeta.py +18 -0
  54. ccai/pychestrate/rest/client/models/pluginapi.py +21 -0
  55. ccai/pychestrate/rest/client/models/podmember.py +14 -0
  56. ccai/pychestrate/rest/client/models/pods.py +75 -0
  57. ccai/pychestrate/rest/client/models/podtopologylink.py +15 -0
  58. ccai/pychestrate/rest/client/models/problem.py +19 -0
  59. ccai/pychestrate/rest/client/models/progress.py +18 -0
  60. ccai/pychestrate/rest/client/models/progressrecord.py +26 -0
  61. ccai/pychestrate/rest/client/models/progresssummary.py +29 -0
  62. ccai/pychestrate/rest/client/models/projects.py +49 -0
  63. ccai/pychestrate/rest/client/models/projecttemplates.py +14 -0
  64. ccai/pychestrate/rest/client/models/rbac.py +46 -0
  65. ccai/pychestrate/rest/client/models/results.py +56 -0
  66. ccai/pychestrate/rest/client/models/retention.py +26 -0
  67. ccai/pychestrate/rest/client/models/runevent.py +25 -0
  68. ccai/pychestrate/rest/client/models/runs.py +32 -0
  69. ccai/pychestrate/rest/client/models/scopedsecret.py +13 -0
  70. ccai/pychestrate/rest/client/models/secrets.py +16 -0
  71. ccai/pychestrate/rest/client/models/signingkey.py +19 -0
  72. ccai/pychestrate/rest/client/models/tagging.py +24 -0
  73. ccai/pychestrate/rest/client/models/testcatalog.py +50 -0
  74. ccai/pychestrate/rest/client/models/testcatalogimportsummary.py +16 -0
  75. ccai/pychestrate/rest/client/models/testruns.py +25 -0
  76. ccai/pychestrate/rest/client/models/users.py +23 -0
  77. ccai/pychestrate/rest/client/models/variableeffectiveentry.py +13 -0
  78. ccai/pychestrate/rest/client/models/variables.py +30 -0
  79. ccai/pychestrate/rest/client/pychestraterestclient.py +166 -0
  80. ccai/pychestrate/rest/client/pychestraterestclientbase.py +147 -0
  81. codecraftai_restclient_pychestrate-1.2.0.dist-info/METADATA +96 -0
  82. codecraftai_restclient_pychestrate-1.2.0.dist-info/RECORD +84 -0
  83. codecraftai_restclient_pychestrate-1.2.0.dist-info/WHEEL +4 -0
  84. codecraftai_restclient_pychestrate-1.2.0.dist-info/licenses/LICENSE.md +204 -0
@@ -0,0 +1,3757 @@
1
+ openapi: 3.1.0
2
+ info:
3
+ title: Pychestrate Service API
4
+ version: 0.0.7-preview
5
+ description: |
6
+ Authoritative REST contract for the **Pychestrate** continuous integration
7
+ and orchestration service.
8
+
9
+ This document is the single source of truth for the `/api/v1/*` HTTP
10
+ surface. It is consumed by:
11
+
12
+ * `codecraftai-restclient-base`'s `RestClientGenerator`, which produces the
13
+ typed Python client `codecraftai-restclient-pychestrate`.
14
+ * Server-side route implementations in the `pychestrate-service` repo
15
+ (route handlers are expected to conform to the schemas declared here).
16
+ * External integrators and tooling authors.
17
+
18
+ ## Scope
19
+
20
+ * Every endpoint currently implemented under
21
+ `source/site/ccai/pychestrate/webservice/**/restapi/v1/*_routes.py`
22
+ is described.
23
+ * Endpoints planned for the Phase 3 minimum-viable test-run slice
24
+ (runs, dispatch ack, progress, results, test catalog, artifacts,
25
+ SSE events) are also described — these are flagged with the
26
+ `x-pychestrate-status: planned` extension on the operation.
27
+
28
+ The HTML admin / consumer UI surfaces under `/admin/*`, `/setup/*`,
29
+ `/login`, and the projects / resources blueprints are **not** part of
30
+ this contract; they are server-rendered HTML.
31
+
32
+ ## Conventions
33
+
34
+ * All paths are versioned under `/api/v1/`.
35
+ * Path / query / request / response field names are `snake_case`.
36
+ * Resource identifiers in paths use snake_case (`run_id`,
37
+ `project_id`, `pod_id`, ...).
38
+ * Non-CRUD verbs on a resource use the colon-action form
39
+ (`/projects/{project_id}/test-catalog:import`).
40
+ * All timestamps are ISO-8601 / RFC 3339 UTC strings
41
+ (`2026-05-17T19:42:00Z`).
42
+ * Every 4xx / 5xx response is a RFC 7807 Problem Document
43
+ (`application/problem+json`) — see the `Problem` schema.
44
+ * Paginated list endpoints return a `Page<T>` envelope (see the
45
+ `Page` schema for the shape) so the codegen surfaces them through
46
+ `ccai.restclient.base.pagedlist.PagedList`.
47
+ contact:
48
+ name: Code Craft AI LLC
49
+ url: https://github.com/automationmojo/pychestrate-service
50
+ license:
51
+ name: Apache-2.0
52
+ url: https://www.apache.org/licenses/LICENSE-2.0.html
53
+
54
+ servers:
55
+ - url: https://tc-app-server.tailb75ce6.ts.net/api/v1
56
+ description: Author's live alpha deployment (Tailscale-fronted).
57
+ - url: https://{host}/api/v1
58
+ description: Generic templated server; supply the deployment host.
59
+ variables:
60
+ host:
61
+ default: pychestrate.example.com
62
+ description: Hostname of the Pychestrate service.
63
+
64
+ tags:
65
+ - name: auth
66
+ description: Local-credential authentication and session lifecycle.
67
+ - name: me
68
+ description: Permissions and identity of the currently-authenticated actor.
69
+ - name: users
70
+ description: User account administration.
71
+ - name: rbac
72
+ description: Roles, permissions, role bindings, and permission checks.
73
+ - name: audit
74
+ description: Append-only audit log access.
75
+ - name: secrets
76
+ description: Encrypted-secrets read API (writes are out-of-band today).
77
+ - name: variables
78
+ description: Hierarchical typed configuration variables.
79
+ - name: flags
80
+ description: Feature flags (runtime toggles).
81
+ - name: tagging
82
+ description: Shared tag model across entities.
83
+ - name: retention
84
+ description: Per-data-class retention policies.
85
+ - name: notifications
86
+ description: Notification channels (email, Slack, webhook, ...).
87
+ - name: project_templates
88
+ description: Scaffolded new-project presets.
89
+ - name: projects
90
+ description: Project workspaces — the top-level entity that owns runs,
91
+ pods, members, repositories, builds, and tests.
92
+ - name: clients
93
+ description: Registered pod client agents.
94
+ - name: enrollment
95
+ description: Client-enrollment bootstrap tokens (mTLS issuance).
96
+ - name: agents
97
+ description: Agent rollouts and per-client capability inventory.
98
+ - name: pods
99
+ description: Pod definitions, registrations, topology, and templates.
100
+ - name: plugin_api
101
+ description: Plugin-API catalog metadata for installed extensions.
102
+ - name: runs
103
+ description: Run-state reads (`GET /runs/...`).
104
+ - name: test_runs
105
+ description: Trigger a new test run on a project + pod.
106
+ - name: test_catalog
107
+ description: Per-project test-catalog entries.
108
+ - name: dispatch
109
+ description: Pod-client → server dispatch acknowledgements.
110
+ - name: progress
111
+ description: Pod-client → server live progress ingestion.
112
+ - name: results
113
+ description: Pod-client → server final-result manifests.
114
+ - name: artifacts
115
+ description: Artifact listings and browsing for a run.
116
+ - name: metrics
117
+ description: UI-facing metrics endpoints (planned).
118
+
119
+ security:
120
+ - pychestrate_session: []
121
+ - pychestrate_api_key: []
122
+
123
+ paths:
124
+
125
+ # =====================================================================
126
+ # auth
127
+ # =====================================================================
128
+
129
+ /auth/logout:
130
+ post:
131
+ tags: [auth]
132
+ summary: Close the current session.
133
+ operationId: auth_logout
134
+ description: |
135
+ Close the session named in the request body. Returns
136
+ `{session_id, status: "closed"}` on success.
137
+ requestBody:
138
+ required: true
139
+ content:
140
+ application/json:
141
+ schema:
142
+ $ref: '#/components/schemas/LogoutRequest'
143
+ responses:
144
+ '200':
145
+ description: Session closed.
146
+ content:
147
+ application/json:
148
+ schema:
149
+ $ref: '#/components/schemas/LogoutResponse'
150
+ '400': { $ref: '#/components/responses/BadRequest' }
151
+ '503': { $ref: '#/components/responses/ServiceUnavailable' }
152
+
153
+ # =====================================================================
154
+ # me
155
+ # =====================================================================
156
+
157
+ /me/permissions:
158
+ get:
159
+ tags: [me]
160
+ summary: List the current actor's effective permissions.
161
+ operationId: me_get_permissions
162
+ description: |
163
+ Returns the set of permission keys granted to the calling actor by
164
+ the RBAC evaluator, plus the actor's user_id and a list of role
165
+ bindings.
166
+ responses:
167
+ '200':
168
+ description: Effective permission snapshot.
169
+ content:
170
+ application/json:
171
+ schema:
172
+ $ref: '#/components/schemas/MePermissions'
173
+ '401': { $ref: '#/components/responses/Unauthorized' }
174
+
175
+ # =====================================================================
176
+ # users
177
+ # =====================================================================
178
+
179
+ /identity/users:
180
+ post:
181
+ tags: [users]
182
+ summary: Create a user account.
183
+ operationId: users_create
184
+ requestBody:
185
+ required: true
186
+ content:
187
+ application/json:
188
+ schema:
189
+ $ref: '#/components/schemas/UserCreateRequest'
190
+ responses:
191
+ '201':
192
+ description: User created.
193
+ content:
194
+ application/json:
195
+ schema:
196
+ $ref: '#/components/schemas/User'
197
+ '400': { $ref: '#/components/responses/BadRequest' }
198
+ '401': { $ref: '#/components/responses/Unauthorized' }
199
+ '403': { $ref: '#/components/responses/Forbidden' }
200
+ '409': { $ref: '#/components/responses/Conflict' }
201
+
202
+ /identity/users/{user_id}:
203
+ parameters:
204
+ - $ref: '#/components/parameters/UserIdParam'
205
+ get:
206
+ tags: [users]
207
+ summary: Read a user account.
208
+ operationId: users_get_by_id
209
+ responses:
210
+ '200':
211
+ description: User account.
212
+ content:
213
+ application/json:
214
+ schema:
215
+ $ref: '#/components/schemas/User'
216
+ '404': { $ref: '#/components/responses/NotFound' }
217
+
218
+ # =====================================================================
219
+ # rbac
220
+ # =====================================================================
221
+
222
+ /rbac/permissions:
223
+ get:
224
+ tags: [rbac]
225
+ summary: List declared RBAC permission keys.
226
+ operationId: rbac_list_permissions
227
+ responses:
228
+ '200':
229
+ description: All declared permissions.
230
+ content:
231
+ application/json:
232
+ schema:
233
+ type: object
234
+ required: [items]
235
+ properties:
236
+ items:
237
+ type: array
238
+ items: { $ref: '#/components/schemas/RbacPermission' }
239
+ /rbac/roles:
240
+ get:
241
+ tags: [rbac]
242
+ summary: List declared roles.
243
+ operationId: rbac_list_roles
244
+ responses:
245
+ '200':
246
+ description: All declared roles.
247
+ content:
248
+ application/json:
249
+ schema:
250
+ type: object
251
+ required: [items]
252
+ properties:
253
+ items:
254
+ type: array
255
+ items: { $ref: '#/components/schemas/RbacRole' }
256
+ /rbac/bindings:
257
+ get:
258
+ tags: [rbac]
259
+ summary: List role bindings.
260
+ operationId: rbac_list_bindings
261
+ parameters:
262
+ - in: query
263
+ name: user_id
264
+ schema: { type: string, format: uuid }
265
+ - in: query
266
+ name: project_id
267
+ schema: { type: string, format: uuid }
268
+ responses:
269
+ '200':
270
+ description: Matching role bindings.
271
+ content:
272
+ application/json:
273
+ schema:
274
+ type: object
275
+ required: [items]
276
+ properties:
277
+ items:
278
+ type: array
279
+ items: { $ref: '#/components/schemas/RbacRoleBinding' }
280
+ post:
281
+ tags: [rbac]
282
+ summary: Create a role binding.
283
+ operationId: rbac_create_binding
284
+ requestBody:
285
+ required: true
286
+ content:
287
+ application/json:
288
+ schema:
289
+ $ref: '#/components/schemas/RbacRoleBindingCreateRequest'
290
+ responses:
291
+ '201':
292
+ description: Role binding created.
293
+ content:
294
+ application/json:
295
+ schema: { $ref: '#/components/schemas/RbacRoleBinding' }
296
+ '400': { $ref: '#/components/responses/BadRequest' }
297
+ '409': { $ref: '#/components/responses/Conflict' }
298
+ /rbac/bindings/{binding_id}:
299
+ parameters:
300
+ - in: path
301
+ name: binding_id
302
+ required: true
303
+ schema: { type: string, format: uuid }
304
+ delete:
305
+ tags: [rbac]
306
+ summary: Delete a role binding.
307
+ operationId: rbac_delete_binding
308
+ responses:
309
+ '204':
310
+ description: Binding deleted.
311
+ '404': { $ref: '#/components/responses/NotFound' }
312
+ /rbac/check:
313
+ post:
314
+ tags: [rbac]
315
+ summary: Check whether the actor has a permission in a scope.
316
+ operationId: rbac_check_permission
317
+ requestBody:
318
+ required: true
319
+ content:
320
+ application/json:
321
+ schema:
322
+ $ref: '#/components/schemas/RbacCheckRequest'
323
+ responses:
324
+ '200':
325
+ description: Permission decision.
326
+ content:
327
+ application/json:
328
+ schema: { $ref: '#/components/schemas/RbacCheckResponse' }
329
+ '400': { $ref: '#/components/responses/BadRequest' }
330
+
331
+ # =====================================================================
332
+ # audit
333
+ # =====================================================================
334
+
335
+ /audit:
336
+ get:
337
+ tags: [audit]
338
+ summary: List audit-log records.
339
+ operationId: audit_list
340
+ parameters:
341
+ - in: query
342
+ name: limit
343
+ schema: { type: integer, minimum: 1, maximum: 1000, default: 100 }
344
+ - in: query
345
+ name: after_sequence
346
+ description: Only return records whose sequence_number is greater than this value.
347
+ schema: { type: integer, minimum: 0 }
348
+ - in: query
349
+ name: actor_id
350
+ schema: { type: string, format: uuid }
351
+ - in: query
352
+ name: action
353
+ schema: { type: string }
354
+ responses:
355
+ '200':
356
+ description: Audit records (ordered by sequence_number ascending).
357
+ content:
358
+ application/json:
359
+ schema:
360
+ type: object
361
+ required: [items]
362
+ properties:
363
+ items:
364
+ type: array
365
+ items: { $ref: '#/components/schemas/AuditRecord' }
366
+ /audit/{sequence_number}:
367
+ parameters:
368
+ - in: path
369
+ name: sequence_number
370
+ required: true
371
+ schema: { type: integer, minimum: 0 }
372
+ get:
373
+ tags: [audit]
374
+ summary: Read a single audit record by sequence number.
375
+ operationId: audit_get_by_sequence
376
+ responses:
377
+ '200':
378
+ description: Audit record.
379
+ content:
380
+ application/json:
381
+ schema: { $ref: '#/components/schemas/AuditRecord' }
382
+ '404': { $ref: '#/components/responses/NotFound' }
383
+
384
+ # =====================================================================
385
+ # secrets
386
+ # =====================================================================
387
+
388
+ /secrets:
389
+ get:
390
+ tags: [secrets]
391
+ summary: List secret references (metadata only — no plaintext).
392
+ operationId: secrets_list
393
+ responses:
394
+ '200':
395
+ description: Secret references.
396
+ content:
397
+ application/json:
398
+ schema:
399
+ type: object
400
+ required: [items]
401
+ properties:
402
+ items:
403
+ type: array
404
+ items: { $ref: '#/components/schemas/SecretRef' }
405
+ /secrets/{secret_ref}:
406
+ parameters:
407
+ - in: path
408
+ name: secret_ref
409
+ description: Slash-delimited secret reference path (e.g. `vault/myteam/api-token`).
410
+ required: true
411
+ schema: { type: string }
412
+ get:
413
+ tags: [secrets]
414
+ summary: Read a single secret reference (metadata only).
415
+ operationId: secrets_get_by_ref
416
+ responses:
417
+ '200':
418
+ description: Secret reference.
419
+ content:
420
+ application/json:
421
+ schema: { $ref: '#/components/schemas/SecretRef' }
422
+ '404': { $ref: '#/components/responses/NotFound' }
423
+
424
+ # =====================================================================
425
+ # variables
426
+ # =====================================================================
427
+
428
+ /variables/effective:
429
+ get:
430
+ tags: [variables]
431
+ summary: Resolve effective variable values for a project / pod / run scope.
432
+ operationId: variables_get_effective
433
+ parameters:
434
+ - in: query
435
+ name: project_id
436
+ schema: { type: string, format: uuid }
437
+ - in: query
438
+ name: pod_id
439
+ schema: { type: string, format: uuid }
440
+ - in: query
441
+ name: run_id
442
+ schema: { type: string, format: uuid }
443
+ responses:
444
+ '200':
445
+ description: Effective variable map with inheritance trace.
446
+ content:
447
+ application/json:
448
+ schema: { $ref: '#/components/schemas/VariableEffectiveSet' }
449
+
450
+ /variables/{scope}/{scope_id}:
451
+ parameters:
452
+ - $ref: '#/components/parameters/VariableScopeParam'
453
+ - $ref: '#/components/parameters/VariableScopeIdParam'
454
+ get:
455
+ tags: [variables]
456
+ summary: List variables declared at a specific scope.
457
+ operationId: variables_list_scope
458
+ responses:
459
+ '200':
460
+ description: Variables declared at the scope.
461
+ content:
462
+ application/json:
463
+ schema:
464
+ type: object
465
+ required: [items]
466
+ properties:
467
+ items:
468
+ type: array
469
+ items: { $ref: '#/components/schemas/Variable' }
470
+
471
+ /variables/{scope}/{scope_id}/{name}:
472
+ parameters:
473
+ - $ref: '#/components/parameters/VariableScopeParam'
474
+ - $ref: '#/components/parameters/VariableScopeIdParam'
475
+ - in: path
476
+ name: name
477
+ required: true
478
+ schema: { type: string }
479
+ get:
480
+ tags: [variables]
481
+ summary: Read one variable at a specific scope.
482
+ operationId: variables_get_one
483
+ responses:
484
+ '200':
485
+ description: Variable definition at the scope.
486
+ content:
487
+ application/json:
488
+ schema: { $ref: '#/components/schemas/Variable' }
489
+ '404': { $ref: '#/components/responses/NotFound' }
490
+ put:
491
+ tags: [variables]
492
+ summary: Upsert one variable at a specific scope.
493
+ operationId: variables_put_one
494
+ requestBody:
495
+ required: true
496
+ content:
497
+ application/json:
498
+ schema: { $ref: '#/components/schemas/VariableUpsertRequest' }
499
+ responses:
500
+ '200':
501
+ description: Variable upserted.
502
+ content:
503
+ application/json:
504
+ schema: { $ref: '#/components/schemas/Variable' }
505
+ '400': { $ref: '#/components/responses/BadRequest' }
506
+ delete:
507
+ tags: [variables]
508
+ summary: Delete one variable at a specific scope.
509
+ operationId: variables_delete_one
510
+ responses:
511
+ '204':
512
+ description: Variable deleted.
513
+ '404': { $ref: '#/components/responses/NotFound' }
514
+
515
+ # =====================================================================
516
+ # flags
517
+ # =====================================================================
518
+
519
+ /flags:
520
+ get:
521
+ tags: [flags]
522
+ summary: List feature flags.
523
+ operationId: flags_list
524
+ responses:
525
+ '200':
526
+ description: Declared flags.
527
+ content:
528
+ application/json:
529
+ schema:
530
+ type: object
531
+ required: [items]
532
+ properties:
533
+ items:
534
+ type: array
535
+ items: { $ref: '#/components/schemas/FeatureFlag' }
536
+ post:
537
+ tags: [flags]
538
+ summary: Create a new feature flag.
539
+ operationId: flags_create
540
+ requestBody:
541
+ required: true
542
+ content:
543
+ application/json:
544
+ schema: { $ref: '#/components/schemas/FeatureFlagCreateRequest' }
545
+ responses:
546
+ '201':
547
+ description: Flag created.
548
+ content:
549
+ application/json:
550
+ schema: { $ref: '#/components/schemas/FeatureFlag' }
551
+ '400': { $ref: '#/components/responses/BadRequest' }
552
+ '409': { $ref: '#/components/responses/Conflict' }
553
+ /flags/backends:
554
+ get:
555
+ tags: [flags]
556
+ summary: List installed feature-flag backends.
557
+ operationId: flags_list_backends
558
+ responses:
559
+ '200':
560
+ description: Registered backends.
561
+ content:
562
+ application/json:
563
+ schema:
564
+ type: object
565
+ required: [items]
566
+ properties:
567
+ items:
568
+ type: array
569
+ items: { $ref: '#/components/schemas/FeatureFlagBackend' }
570
+ /flags/{flag_key}:
571
+ parameters:
572
+ - $ref: '#/components/parameters/FlagKeyParam'
573
+ get:
574
+ tags: [flags]
575
+ summary: Read a feature flag.
576
+ operationId: flags_get_by_key
577
+ responses:
578
+ '200':
579
+ description: Flag.
580
+ content:
581
+ application/json:
582
+ schema: { $ref: '#/components/schemas/FeatureFlag' }
583
+ '404': { $ref: '#/components/responses/NotFound' }
584
+ patch:
585
+ tags: [flags]
586
+ summary: Update a feature flag.
587
+ operationId: flags_update
588
+ requestBody:
589
+ required: true
590
+ content:
591
+ application/json:
592
+ schema: { $ref: '#/components/schemas/FeatureFlagUpdateRequest' }
593
+ responses:
594
+ '200':
595
+ description: Flag updated.
596
+ content:
597
+ application/json:
598
+ schema: { $ref: '#/components/schemas/FeatureFlag' }
599
+ '400': { $ref: '#/components/responses/BadRequest' }
600
+ '404': { $ref: '#/components/responses/NotFound' }
601
+ /flags/{flag_key}/retire:
602
+ parameters:
603
+ - $ref: '#/components/parameters/FlagKeyParam'
604
+ post:
605
+ tags: [flags]
606
+ summary: Retire a feature flag.
607
+ operationId: flags_retire
608
+ responses:
609
+ '200':
610
+ description: Flag retired.
611
+ content:
612
+ application/json:
613
+ schema: { $ref: '#/components/schemas/FeatureFlag' }
614
+ '404': { $ref: '#/components/responses/NotFound' }
615
+ /flags/{flag_key}/history:
616
+ parameters:
617
+ - $ref: '#/components/parameters/FlagKeyParam'
618
+ get:
619
+ tags: [flags]
620
+ summary: Read change history for a feature flag.
621
+ operationId: flags_get_history
622
+ responses:
623
+ '200':
624
+ description: History entries.
625
+ content:
626
+ application/json:
627
+ schema:
628
+ type: object
629
+ required: [items]
630
+ properties:
631
+ items:
632
+ type: array
633
+ items: { $ref: '#/components/schemas/FeatureFlagHistoryEntry' }
634
+ /flags/{flag_key}/evaluate:
635
+ parameters:
636
+ - $ref: '#/components/parameters/FlagKeyParam'
637
+ post:
638
+ tags: [flags]
639
+ summary: Evaluate a feature flag against an actor / context.
640
+ operationId: flags_evaluate
641
+ requestBody:
642
+ required: true
643
+ content:
644
+ application/json:
645
+ schema: { $ref: '#/components/schemas/FeatureFlagEvaluateRequest' }
646
+ responses:
647
+ '200':
648
+ description: Evaluation result.
649
+ content:
650
+ application/json:
651
+ schema: { $ref: '#/components/schemas/FeatureFlagEvaluateResponse' }
652
+
653
+ # =====================================================================
654
+ # tagging
655
+ # =====================================================================
656
+
657
+ /tags/search:
658
+ get:
659
+ tags: [tagging]
660
+ summary: Search tag definitions.
661
+ operationId: tagging_search
662
+ parameters:
663
+ - in: query
664
+ name: q
665
+ schema: { type: string }
666
+ - in: query
667
+ name: taxonomy
668
+ schema: { type: string }
669
+ responses:
670
+ '200':
671
+ description: Matching tags.
672
+ content:
673
+ application/json:
674
+ schema:
675
+ type: object
676
+ required: [items]
677
+ properties:
678
+ items:
679
+ type: array
680
+ items: { $ref: '#/components/schemas/Tag' }
681
+ /tags/taxonomies:
682
+ get:
683
+ tags: [tagging]
684
+ summary: List declared tag taxonomies.
685
+ operationId: tagging_list_taxonomies
686
+ responses:
687
+ '200':
688
+ description: Taxonomies.
689
+ content:
690
+ application/json:
691
+ schema:
692
+ type: object
693
+ required: [items]
694
+ properties:
695
+ items:
696
+ type: array
697
+ items: { $ref: '#/components/schemas/TagTaxonomy' }
698
+ /tags/{entity_kind}/{entity_id}:
699
+ parameters:
700
+ - $ref: '#/components/parameters/EntityKindParam'
701
+ - $ref: '#/components/parameters/EntityIdParam'
702
+ get:
703
+ tags: [tagging]
704
+ summary: List tags attached to an entity.
705
+ operationId: tagging_list_for_entity
706
+ responses:
707
+ '200':
708
+ description: Tag attachments.
709
+ content:
710
+ application/json:
711
+ schema:
712
+ type: object
713
+ required: [items]
714
+ properties:
715
+ items:
716
+ type: array
717
+ items: { $ref: '#/components/schemas/Tag' }
718
+ post:
719
+ tags: [tagging]
720
+ summary: Attach a tag to an entity.
721
+ operationId: tagging_attach
722
+ requestBody:
723
+ required: true
724
+ content:
725
+ application/json:
726
+ schema: { $ref: '#/components/schemas/TagAttachRequest' }
727
+ responses:
728
+ '201':
729
+ description: Tag attached.
730
+ content:
731
+ application/json:
732
+ schema: { $ref: '#/components/schemas/Tag' }
733
+ /tags/{entity_kind}/{entity_id}/{tag}:
734
+ parameters:
735
+ - $ref: '#/components/parameters/EntityKindParam'
736
+ - $ref: '#/components/parameters/EntityIdParam'
737
+ - in: path
738
+ name: tag
739
+ required: true
740
+ schema: { type: string }
741
+ delete:
742
+ tags: [tagging]
743
+ summary: Detach a tag from an entity.
744
+ operationId: tagging_detach
745
+ responses:
746
+ '204':
747
+ description: Tag detached.
748
+ '404': { $ref: '#/components/responses/NotFound' }
749
+
750
+ # =====================================================================
751
+ # retention
752
+ # =====================================================================
753
+
754
+ /retention/policies:
755
+ get:
756
+ tags: [retention]
757
+ summary: List retention policies.
758
+ operationId: retention_list
759
+ responses:
760
+ '200':
761
+ description: Retention policies.
762
+ content:
763
+ application/json:
764
+ schema:
765
+ type: object
766
+ required: [items]
767
+ properties:
768
+ items:
769
+ type: array
770
+ items: { $ref: '#/components/schemas/RetentionPolicy' }
771
+ post:
772
+ tags: [retention]
773
+ summary: Create a retention policy.
774
+ operationId: retention_create
775
+ requestBody:
776
+ required: true
777
+ content:
778
+ application/json:
779
+ schema: { $ref: '#/components/schemas/RetentionPolicyCreateRequest' }
780
+ responses:
781
+ '201':
782
+ description: Policy created.
783
+ content:
784
+ application/json:
785
+ schema: { $ref: '#/components/schemas/RetentionPolicy' }
786
+ '400': { $ref: '#/components/responses/BadRequest' }
787
+ /retention/policies/{policy_id}:
788
+ parameters:
789
+ - in: path
790
+ name: policy_id
791
+ required: true
792
+ schema: { type: string, format: uuid }
793
+ get:
794
+ tags: [retention]
795
+ summary: Read a retention policy.
796
+ operationId: retention_get_by_id
797
+ responses:
798
+ '200':
799
+ description: Policy.
800
+ content:
801
+ application/json:
802
+ schema: { $ref: '#/components/schemas/RetentionPolicy' }
803
+ '404': { $ref: '#/components/responses/NotFound' }
804
+ /retention/policies/{policy_id}/publish:
805
+ parameters:
806
+ - in: path
807
+ name: policy_id
808
+ required: true
809
+ schema: { type: string, format: uuid }
810
+ post:
811
+ tags: [retention]
812
+ summary: Publish (activate) a retention policy.
813
+ operationId: retention_publish
814
+ responses:
815
+ '200':
816
+ description: Policy published.
817
+ content:
818
+ application/json:
819
+ schema: { $ref: '#/components/schemas/RetentionPolicy' }
820
+
821
+ # =====================================================================
822
+ # notifications
823
+ # =====================================================================
824
+
825
+ /notifications/channels:
826
+ get:
827
+ tags: [notifications]
828
+ summary: List notification channels.
829
+ operationId: notifications_list_channels
830
+ responses:
831
+ '200':
832
+ description: Channels.
833
+ content:
834
+ application/json:
835
+ schema:
836
+ type: object
837
+ required: [items]
838
+ properties:
839
+ items:
840
+ type: array
841
+ items: { $ref: '#/components/schemas/NotificationChannel' }
842
+ post:
843
+ tags: [notifications]
844
+ summary: Create a notification channel.
845
+ operationId: notifications_create_channel
846
+ requestBody:
847
+ required: true
848
+ content:
849
+ application/json:
850
+ schema: { $ref: '#/components/schemas/NotificationChannelCreateRequest' }
851
+ responses:
852
+ '201':
853
+ description: Channel created.
854
+ content:
855
+ application/json:
856
+ schema: { $ref: '#/components/schemas/NotificationChannel' }
857
+ '400': { $ref: '#/components/responses/BadRequest' }
858
+ /notifications/channels/{channel_id}:
859
+ parameters:
860
+ - in: path
861
+ name: channel_id
862
+ required: true
863
+ schema: { type: string, format: uuid }
864
+ get:
865
+ tags: [notifications]
866
+ summary: Read a notification channel.
867
+ operationId: notifications_get_channel
868
+ responses:
869
+ '200':
870
+ description: Channel.
871
+ content:
872
+ application/json:
873
+ schema: { $ref: '#/components/schemas/NotificationChannel' }
874
+ '404': { $ref: '#/components/responses/NotFound' }
875
+
876
+ # =====================================================================
877
+ # project_templates
878
+ # =====================================================================
879
+
880
+ /project-templates:
881
+ get:
882
+ tags: [project_templates]
883
+ summary: List project templates.
884
+ operationId: project_templates_list
885
+ responses:
886
+ '200':
887
+ description: Templates.
888
+ content:
889
+ application/json:
890
+ schema:
891
+ type: object
892
+ required: [items]
893
+ properties:
894
+ items:
895
+ type: array
896
+ items: { $ref: '#/components/schemas/ProjectTemplate' }
897
+ /project-templates/{template_id}:
898
+ parameters:
899
+ - in: path
900
+ name: template_id
901
+ required: true
902
+ schema: { type: string }
903
+ get:
904
+ tags: [project_templates]
905
+ summary: Read a project template.
906
+ operationId: project_templates_get_by_id
907
+ responses:
908
+ '200':
909
+ description: Template.
910
+ content:
911
+ application/json:
912
+ schema: { $ref: '#/components/schemas/ProjectTemplate' }
913
+ '404': { $ref: '#/components/responses/NotFound' }
914
+
915
+ # =====================================================================
916
+ # projects
917
+ # =====================================================================
918
+
919
+ /projects:
920
+ post:
921
+ tags: [projects]
922
+ summary: Create a project.
923
+ operationId: projects_create
924
+ requestBody:
925
+ required: true
926
+ content:
927
+ application/json:
928
+ schema: { $ref: '#/components/schemas/ProjectCreateRequest' }
929
+ responses:
930
+ '201':
931
+ description: Project created.
932
+ content:
933
+ application/json:
934
+ schema: { $ref: '#/components/schemas/Project' }
935
+ '400': { $ref: '#/components/responses/BadRequest' }
936
+ '409': { $ref: '#/components/responses/Conflict' }
937
+ /projects/{slug}:
938
+ parameters:
939
+ - $ref: '#/components/parameters/ProjectSlugParam'
940
+ get:
941
+ tags: [projects]
942
+ summary: Read a project by slug.
943
+ operationId: projects_get_by_slug
944
+ responses:
945
+ '200':
946
+ description: Project.
947
+ content:
948
+ application/json:
949
+ schema: { $ref: '#/components/schemas/Project' }
950
+ '404': { $ref: '#/components/responses/NotFound' }
951
+ patch:
952
+ tags: [projects]
953
+ summary: Patch a project.
954
+ operationId: projects_patch
955
+ requestBody:
956
+ required: true
957
+ content:
958
+ application/json:
959
+ schema: { $ref: '#/components/schemas/ProjectPatchRequest' }
960
+ responses:
961
+ '200':
962
+ description: Project patched.
963
+ content:
964
+ application/json:
965
+ schema: { $ref: '#/components/schemas/Project' }
966
+ '400': { $ref: '#/components/responses/BadRequest' }
967
+ '404': { $ref: '#/components/responses/NotFound' }
968
+ delete:
969
+ tags: [projects]
970
+ summary: Delete a project.
971
+ operationId: projects_delete
972
+ responses:
973
+ '204':
974
+ description: Project deleted.
975
+ '404': { $ref: '#/components/responses/NotFound' }
976
+ '409': { $ref: '#/components/responses/Conflict' }
977
+ /projects/{slug}/archive:
978
+ parameters:
979
+ - $ref: '#/components/parameters/ProjectSlugParam'
980
+ post:
981
+ tags: [projects]
982
+ summary: Archive a project.
983
+ operationId: projects_archive
984
+ responses:
985
+ '200':
986
+ description: Project archived.
987
+ content:
988
+ application/json:
989
+ schema: { $ref: '#/components/schemas/Project' }
990
+ /projects/{slug}/unarchive:
991
+ parameters:
992
+ - $ref: '#/components/parameters/ProjectSlugParam'
993
+ post:
994
+ tags: [projects]
995
+ summary: Un-archive a project.
996
+ operationId: projects_unarchive
997
+ responses:
998
+ '200':
999
+ description: Project un-archived.
1000
+ content:
1001
+ application/json:
1002
+ schema: { $ref: '#/components/schemas/Project' }
1003
+ /projects/{slug}/members:
1004
+ parameters:
1005
+ - $ref: '#/components/parameters/ProjectSlugParam'
1006
+ get:
1007
+ tags: [projects]
1008
+ summary: List project members.
1009
+ operationId: projects_list_members
1010
+ responses:
1011
+ '200':
1012
+ description: Members.
1013
+ content:
1014
+ application/json:
1015
+ schema:
1016
+ type: object
1017
+ required: [items]
1018
+ properties:
1019
+ items:
1020
+ type: array
1021
+ items: { $ref: '#/components/schemas/ProjectMember' }
1022
+ post:
1023
+ tags: [projects]
1024
+ summary: Add a member to a project.
1025
+ operationId: projects_add_member
1026
+ requestBody:
1027
+ required: true
1028
+ content:
1029
+ application/json:
1030
+ schema: { $ref: '#/components/schemas/ProjectMemberAddRequest' }
1031
+ responses:
1032
+ '201':
1033
+ description: Member added.
1034
+ content:
1035
+ application/json:
1036
+ schema: { $ref: '#/components/schemas/ProjectMember' }
1037
+ '400': { $ref: '#/components/responses/BadRequest' }
1038
+ '409': { $ref: '#/components/responses/Conflict' }
1039
+ /projects/{slug}/members/{user_id}:
1040
+ parameters:
1041
+ - $ref: '#/components/parameters/ProjectSlugParam'
1042
+ - $ref: '#/components/parameters/UserIdParam'
1043
+ delete:
1044
+ tags: [projects]
1045
+ summary: Remove a member's owner binding from the project.
1046
+ operationId: projects_remove_member
1047
+ responses:
1048
+ '204':
1049
+ description: Owner binding removed.
1050
+ '404': { $ref: '#/components/responses/NotFound' }
1051
+ /projects/{slug}/members/{user_id}/{role_name}:
1052
+ parameters:
1053
+ - $ref: '#/components/parameters/ProjectSlugParam'
1054
+ - $ref: '#/components/parameters/UserIdParam'
1055
+ - in: path
1056
+ name: role_name
1057
+ required: true
1058
+ schema: { type: string }
1059
+ delete:
1060
+ tags: [projects]
1061
+ summary: Remove a specific role binding from a member of the project.
1062
+ operationId: projects_remove_member_role
1063
+ responses:
1064
+ '204':
1065
+ description: Role binding removed.
1066
+ '404': { $ref: '#/components/responses/NotFound' }
1067
+
1068
+ # =====================================================================
1069
+ # clients
1070
+ # =====================================================================
1071
+
1072
+ /clients:
1073
+ get:
1074
+ tags: [clients]
1075
+ summary: List enrolled clients.
1076
+ operationId: clients_list
1077
+ parameters:
1078
+ - $ref: '#/components/parameters/PageParam'
1079
+ - $ref: '#/components/parameters/PerPageParam'
1080
+ responses:
1081
+ '200':
1082
+ description: Clients.
1083
+ content:
1084
+ application/json:
1085
+ schema:
1086
+ type: object
1087
+ required: [items]
1088
+ properties:
1089
+ items:
1090
+ type: array
1091
+ items: { $ref: '#/components/schemas/Client' }
1092
+ /clients/{client_id}:
1093
+ parameters:
1094
+ - $ref: '#/components/parameters/ClientIdParam'
1095
+ get:
1096
+ tags: [clients]
1097
+ summary: Read a client.
1098
+ operationId: clients_get_by_id
1099
+ responses:
1100
+ '200':
1101
+ description: Client.
1102
+ content:
1103
+ application/json:
1104
+ schema: { $ref: '#/components/schemas/Client' }
1105
+ '404': { $ref: '#/components/responses/NotFound' }
1106
+ /clients/{client_id}/revoke:
1107
+ parameters:
1108
+ - $ref: '#/components/parameters/ClientIdParam'
1109
+ post:
1110
+ tags: [clients]
1111
+ summary: Revoke a client's enrollment.
1112
+ operationId: clients_revoke
1113
+ requestBody:
1114
+ required: false
1115
+ content:
1116
+ application/json:
1117
+ schema: { $ref: '#/components/schemas/ClientRevokeRequest' }
1118
+ responses:
1119
+ '200':
1120
+ description: Client revoked.
1121
+ content:
1122
+ application/json:
1123
+ schema: { $ref: '#/components/schemas/Client' }
1124
+ '404': { $ref: '#/components/responses/NotFound' }
1125
+ /clients/{client_id}/attributes:
1126
+ parameters:
1127
+ - $ref: '#/components/parameters/ClientIdParam'
1128
+ put:
1129
+ tags: [clients]
1130
+ summary: Replace a client's attributes.
1131
+ operationId: clients_update_attributes
1132
+ requestBody:
1133
+ required: true
1134
+ content:
1135
+ application/json:
1136
+ schema: { $ref: '#/components/schemas/ClientAttributesUpdateRequest' }
1137
+ responses:
1138
+ '200':
1139
+ description: Attributes updated.
1140
+ content:
1141
+ application/json:
1142
+ schema: { $ref: '#/components/schemas/Client' }
1143
+ '400': { $ref: '#/components/responses/BadRequest' }
1144
+ '404': { $ref: '#/components/responses/NotFound' }
1145
+
1146
+ # =====================================================================
1147
+ # enrollment
1148
+ # =====================================================================
1149
+
1150
+ /enrollment-tokens:
1151
+ get:
1152
+ tags: [enrollment]
1153
+ summary: List enrollment tokens.
1154
+ operationId: enrollment_list_tokens
1155
+ responses:
1156
+ '200':
1157
+ description: Tokens.
1158
+ content:
1159
+ application/json:
1160
+ schema:
1161
+ type: object
1162
+ required: [items]
1163
+ properties:
1164
+ items:
1165
+ type: array
1166
+ items: { $ref: '#/components/schemas/EnrollmentToken' }
1167
+ post:
1168
+ tags: [enrollment]
1169
+ summary: Issue a new enrollment token.
1170
+ operationId: enrollment_create_token
1171
+ requestBody:
1172
+ required: true
1173
+ content:
1174
+ application/json:
1175
+ schema: { $ref: '#/components/schemas/EnrollmentTokenCreateRequest' }
1176
+ responses:
1177
+ '201':
1178
+ description: Token issued.
1179
+ content:
1180
+ application/json:
1181
+ schema: { $ref: '#/components/schemas/EnrollmentTokenWithSecret' }
1182
+ '400': { $ref: '#/components/responses/BadRequest' }
1183
+ /enrollment-tokens/redeem:
1184
+ post:
1185
+ tags: [enrollment]
1186
+ summary: Redeem an enrollment token for an mTLS identity.
1187
+ operationId: enrollment_redeem
1188
+ description: |
1189
+ The client agent calls this endpoint with the bootstrap token it
1190
+ received out-of-band and a CSR; the response carries the signed
1191
+ client certificate plus the issued client_id.
1192
+ requestBody:
1193
+ required: true
1194
+ content:
1195
+ application/json:
1196
+ schema: { $ref: '#/components/schemas/EnrollmentRedeemRequest' }
1197
+ responses:
1198
+ '200':
1199
+ description: mTLS identity issued.
1200
+ content:
1201
+ application/json:
1202
+ schema: { $ref: '#/components/schemas/EnrollmentRedeemResponse' }
1203
+ '400': { $ref: '#/components/responses/BadRequest' }
1204
+ '401': { $ref: '#/components/responses/Unauthorized' }
1205
+ /enrollment-tokens/{token_id}:
1206
+ parameters:
1207
+ - in: path
1208
+ name: token_id
1209
+ required: true
1210
+ schema: { type: string, format: uuid }
1211
+ delete:
1212
+ tags: [enrollment]
1213
+ summary: Revoke an enrollment token.
1214
+ operationId: enrollment_delete_token
1215
+ responses:
1216
+ '204':
1217
+ description: Token revoked.
1218
+ '404': { $ref: '#/components/responses/NotFound' }
1219
+ /enrollment-tokens/clients/{client_id}/revoke:
1220
+ parameters:
1221
+ - $ref: '#/components/parameters/ClientIdParam'
1222
+ post:
1223
+ tags: [enrollment]
1224
+ summary: Revoke an enrolled client's mTLS identity.
1225
+ operationId: enrollment_revoke_client
1226
+ responses:
1227
+ '200':
1228
+ description: Client revoked.
1229
+ content:
1230
+ application/json:
1231
+ schema: { $ref: '#/components/schemas/Client' }
1232
+ '404': { $ref: '#/components/responses/NotFound' }
1233
+
1234
+ # =====================================================================
1235
+ # agents
1236
+ # =====================================================================
1237
+
1238
+ /clients:inventory:
1239
+ get:
1240
+ tags: [agents]
1241
+ summary: Cross-fleet inventory of clients.
1242
+ operationId: agents_get_client_inventory
1243
+ responses:
1244
+ '200':
1245
+ description: Inventory snapshot.
1246
+ content:
1247
+ application/json:
1248
+ schema: { $ref: '#/components/schemas/ClientInventory' }
1249
+ /clients/{client_id}/capabilities:
1250
+ parameters:
1251
+ - $ref: '#/components/parameters/ClientIdParam'
1252
+ get:
1253
+ tags: [agents]
1254
+ summary: Read advertised capabilities for a client.
1255
+ operationId: agents_get_client_capabilities
1256
+ responses:
1257
+ '200':
1258
+ description: Capabilities.
1259
+ content:
1260
+ application/json:
1261
+ schema: { $ref: '#/components/schemas/ClientCapabilities' }
1262
+ '404': { $ref: '#/components/responses/NotFound' }
1263
+ /agent-rollouts:
1264
+ post:
1265
+ tags: [agents]
1266
+ summary: Create an agent rollout plan.
1267
+ operationId: agents_create_rollout
1268
+ requestBody:
1269
+ required: true
1270
+ content:
1271
+ application/json:
1272
+ schema: { $ref: '#/components/schemas/AgentRolloutCreateRequest' }
1273
+ responses:
1274
+ '201':
1275
+ description: Rollout created.
1276
+ content:
1277
+ application/json:
1278
+ schema: { $ref: '#/components/schemas/AgentRollout' }
1279
+ /agent-rollouts/{rollout_id}:start:
1280
+ parameters:
1281
+ - in: path
1282
+ name: rollout_id
1283
+ required: true
1284
+ schema: { type: string, format: uuid }
1285
+ post:
1286
+ tags: [agents]
1287
+ summary: Start an agent rollout.
1288
+ operationId: agents_start_rollout
1289
+ responses:
1290
+ '200':
1291
+ description: Rollout started.
1292
+ content:
1293
+ application/json:
1294
+ schema: { $ref: '#/components/schemas/AgentRollout' }
1295
+ '404': { $ref: '#/components/responses/NotFound' }
1296
+
1297
+ # ---------------------------------------------------------------------
1298
+ # Client pull surface — PR p19g (renamed to /clients/ in p19h)
1299
+ # ---------------------------------------------------------------------
1300
+
1301
+ /clients/{client_id}/dispatches:next:
1302
+ parameters:
1303
+ - $ref: '#/components/parameters/ClientIdParam'
1304
+ get:
1305
+ tags: [clients]
1306
+ summary: Long-poll for the next signed dispatch envelope.
1307
+ operationId: clients_dispatches_next
1308
+ x-pychestrate-status: planned
1309
+ description: |
1310
+ Long-polling pull endpoint the pod-client calls to fetch
1311
+ the next signed envelope queued for it by the server
1312
+ dispatcher.
1313
+
1314
+ The server blocks the request for up to `timeout_seconds`
1315
+ (default 30, hard cap 60). On a queued dispatch the server
1316
+ returns 200 with a `PendingDispatch` body and the record
1317
+ is dequeued — the client MUST then POST
1318
+ `/runs/{run_id}/dispatch-ack` before the embedded
1319
+ `ack_deadline_at`, otherwise the dispatch reconciler will
1320
+ roll the attempt back. On a timeout the server returns 204
1321
+ and the client immediately re-polls.
1322
+
1323
+ The cert common-name nginx forwards must match the path
1324
+ `client_id` — a mismatch returns 403.
1325
+ security:
1326
+ - pychestrate_mtls: []
1327
+ parameters:
1328
+ - in: query
1329
+ name: timeout_seconds
1330
+ description: How long the server may block before returning 204.
1331
+ schema:
1332
+ type: integer
1333
+ minimum: 0
1334
+ maximum: 60
1335
+ default: 30
1336
+ responses:
1337
+ '200':
1338
+ description: Dispatch dequeued for this client.
1339
+ content:
1340
+ application/json:
1341
+ schema: { $ref: '#/components/schemas/PendingDispatch' }
1342
+ '204':
1343
+ description: No dispatch became available within `timeout_seconds`.
1344
+ '401': { $ref: '#/components/responses/Unauthorized' }
1345
+ '403': { $ref: '#/components/responses/Forbidden' }
1346
+ '422': { $ref: '#/components/responses/Unprocessable' }
1347
+ '503': { $ref: '#/components/responses/ServiceUnavailable' }
1348
+
1349
+ /clients/{client_id}/heartbeat:
1350
+ parameters:
1351
+ - $ref: '#/components/parameters/ClientIdParam'
1352
+ get:
1353
+ tags: [clients]
1354
+ summary: Pod-client liveness ping.
1355
+ operationId: clients_heartbeat
1356
+ x-pychestrate-status: planned
1357
+ description: |
1358
+ Lightweight ping the pod-client calls between long-poll
1359
+ cycles to keep its registry row `last_seen` current.
1360
+ Returns 204 unconditionally on a successful mTLS
1361
+ handshake; rows missing from the registry are logged
1362
+ server-side and still answered 204.
1363
+ security:
1364
+ - pychestrate_mtls: []
1365
+ responses:
1366
+ '204':
1367
+ description: Heartbeat recorded.
1368
+ '401': { $ref: '#/components/responses/Unauthorized' }
1369
+ '403': { $ref: '#/components/responses/Forbidden' }
1370
+ '422': { $ref: '#/components/responses/Unprocessable' }
1371
+
1372
+ # =====================================================================
1373
+ # pods (nested under /projects/{project_id})
1374
+ # =====================================================================
1375
+
1376
+ /projects/{project_id}/pod-definitions:
1377
+ parameters:
1378
+ - $ref: '#/components/parameters/ProjectIdParam'
1379
+ get:
1380
+ tags: [pods]
1381
+ summary: List pod definitions for a project.
1382
+ operationId: pods_list_definitions
1383
+ responses:
1384
+ '200':
1385
+ description: Pod definitions.
1386
+ content:
1387
+ application/json:
1388
+ schema:
1389
+ type: object
1390
+ required: [items]
1391
+ properties:
1392
+ items:
1393
+ type: array
1394
+ items: { $ref: '#/components/schemas/PodDefinition' }
1395
+ post:
1396
+ tags: [pods]
1397
+ summary: Create a pod definition.
1398
+ operationId: pods_create_definition
1399
+ requestBody:
1400
+ required: true
1401
+ content:
1402
+ application/json:
1403
+ schema: { $ref: '#/components/schemas/PodDefinitionCreateRequest' }
1404
+ responses:
1405
+ '201':
1406
+ description: Pod definition created.
1407
+ content:
1408
+ application/json:
1409
+ schema: { $ref: '#/components/schemas/PodDefinition' }
1410
+ '400': { $ref: '#/components/responses/BadRequest' }
1411
+ /projects/{project_id}/pod-definitions/{pod_id}:
1412
+ parameters:
1413
+ - $ref: '#/components/parameters/ProjectIdParam'
1414
+ - $ref: '#/components/parameters/PodIdParam'
1415
+ get:
1416
+ tags: [pods]
1417
+ summary: Read a pod definition.
1418
+ operationId: pods_get_definition
1419
+ responses:
1420
+ '200':
1421
+ description: Pod definition.
1422
+ content:
1423
+ application/json:
1424
+ schema: { $ref: '#/components/schemas/PodDefinition' }
1425
+ '404': { $ref: '#/components/responses/NotFound' }
1426
+ /projects/{project_id}/pods/{pod_id}/state:
1427
+ parameters:
1428
+ - $ref: '#/components/parameters/ProjectIdParam'
1429
+ - $ref: '#/components/parameters/PodIdParam'
1430
+ get:
1431
+ tags: [pods]
1432
+ summary: Read a pod's current lifecycle state.
1433
+ operationId: pods_get_state
1434
+ responses:
1435
+ '200':
1436
+ description: Pod state.
1437
+ content:
1438
+ application/json:
1439
+ schema: { $ref: '#/components/schemas/PodState' }
1440
+ /projects/{project_id}/pods/{pod_id}/state/history:
1441
+ parameters:
1442
+ - $ref: '#/components/parameters/ProjectIdParam'
1443
+ - $ref: '#/components/parameters/PodIdParam'
1444
+ get:
1445
+ tags: [pods]
1446
+ summary: Read a pod's lifecycle state history.
1447
+ operationId: pods_get_state_history
1448
+ responses:
1449
+ '200':
1450
+ description: State history.
1451
+ content:
1452
+ application/json:
1453
+ schema:
1454
+ type: object
1455
+ required: [items]
1456
+ properties:
1457
+ items:
1458
+ type: array
1459
+ items: { $ref: '#/components/schemas/PodStateTransition' }
1460
+ /projects/{project_id}/pods/{pod_id}/registration:
1461
+ parameters:
1462
+ - $ref: '#/components/parameters/ProjectIdParam'
1463
+ - $ref: '#/components/parameters/PodIdParam'
1464
+ get:
1465
+ tags: [pods]
1466
+ summary: Read the latest pod registration.
1467
+ operationId: pods_get_registration
1468
+ responses:
1469
+ '200':
1470
+ description: Registration.
1471
+ content:
1472
+ application/json:
1473
+ schema: { $ref: '#/components/schemas/PodRegistration' }
1474
+ /projects/{project_id}/pods/{pod_id}/registrations:
1475
+ parameters:
1476
+ - $ref: '#/components/parameters/ProjectIdParam'
1477
+ - $ref: '#/components/parameters/PodIdParam'
1478
+ get:
1479
+ tags: [pods]
1480
+ summary: List all historical pod registrations.
1481
+ operationId: pods_list_registrations
1482
+ responses:
1483
+ '200':
1484
+ description: Registrations.
1485
+ content:
1486
+ application/json:
1487
+ schema:
1488
+ type: object
1489
+ required: [items]
1490
+ properties:
1491
+ items:
1492
+ type: array
1493
+ items: { $ref: '#/components/schemas/PodRegistration' }
1494
+ /projects/{project_id}/pods/{pod_id}/topology:
1495
+ parameters:
1496
+ - $ref: '#/components/parameters/ProjectIdParam'
1497
+ - $ref: '#/components/parameters/PodIdParam'
1498
+ get:
1499
+ tags: [pods]
1500
+ summary: Read a pod's topology graph.
1501
+ operationId: pods_get_topology
1502
+ responses:
1503
+ '200':
1504
+ description: Topology.
1505
+ content:
1506
+ application/json:
1507
+ schema: { $ref: '#/components/schemas/PodTopology' }
1508
+ /projects/{project_id}/pod-templates:
1509
+ parameters:
1510
+ - $ref: '#/components/parameters/ProjectIdParam'
1511
+ get:
1512
+ tags: [pods]
1513
+ summary: List pod templates.
1514
+ operationId: pods_list_templates
1515
+ responses:
1516
+ '200':
1517
+ description: Templates.
1518
+ content:
1519
+ application/json:
1520
+ schema:
1521
+ type: object
1522
+ required: [items]
1523
+ properties:
1524
+ items:
1525
+ type: array
1526
+ items: { $ref: '#/components/schemas/PodTemplate' }
1527
+ post:
1528
+ tags: [pods]
1529
+ summary: Create a pod template.
1530
+ operationId: pods_create_template
1531
+ requestBody:
1532
+ required: true
1533
+ content:
1534
+ application/json:
1535
+ schema: { $ref: '#/components/schemas/PodTemplateCreateRequest' }
1536
+ responses:
1537
+ '201':
1538
+ description: Template created.
1539
+ content:
1540
+ application/json:
1541
+ schema: { $ref: '#/components/schemas/PodTemplate' }
1542
+ /projects/{project_id}/pod-templates/{template_id}:materialize:
1543
+ parameters:
1544
+ - $ref: '#/components/parameters/ProjectIdParam'
1545
+ - in: path
1546
+ name: template_id
1547
+ required: true
1548
+ schema: { type: string }
1549
+ post:
1550
+ tags: [pods]
1551
+ summary: Materialize a pod from a template.
1552
+ operationId: pods_materialize_template
1553
+ requestBody:
1554
+ required: true
1555
+ content:
1556
+ application/json:
1557
+ schema: { $ref: '#/components/schemas/PodTemplateMaterializeRequest' }
1558
+ responses:
1559
+ '202':
1560
+ description: Materialization accepted (async).
1561
+ content:
1562
+ application/json:
1563
+ schema: { $ref: '#/components/schemas/PodDefinition' }
1564
+ /projects/{project_id}/pod-templates/{template_id}/parameters:
1565
+ parameters:
1566
+ - $ref: '#/components/parameters/ProjectIdParam'
1567
+ - in: path
1568
+ name: template_id
1569
+ required: true
1570
+ schema: { type: string }
1571
+ put:
1572
+ tags: [pods]
1573
+ summary: Update template parameters.
1574
+ operationId: pods_update_template_parameters
1575
+ requestBody:
1576
+ required: true
1577
+ content:
1578
+ application/json:
1579
+ schema: { type: object }
1580
+ responses:
1581
+ '200':
1582
+ description: Parameters updated.
1583
+ content:
1584
+ application/json:
1585
+ schema: { $ref: '#/components/schemas/PodTemplate' }
1586
+ delete:
1587
+ tags: [pods]
1588
+ summary: Delete a pod template.
1589
+ operationId: pods_delete_template
1590
+ responses:
1591
+ '204':
1592
+ description: Template deleted.
1593
+
1594
+ # =====================================================================
1595
+ # plugin_api
1596
+ # =====================================================================
1597
+
1598
+ /plugin-api/_meta/catalog:
1599
+ get:
1600
+ tags: [plugin_api]
1601
+ summary: List the plugin-API protocol catalog.
1602
+ operationId: plugin_api_list_catalog
1603
+ responses:
1604
+ '200':
1605
+ description: Catalog.
1606
+ content:
1607
+ application/json:
1608
+ schema:
1609
+ type: object
1610
+ required: [items]
1611
+ properties:
1612
+ items:
1613
+ type: array
1614
+ items: { $ref: '#/components/schemas/PluginApiProtocol' }
1615
+ /plugin-api/_meta/catalog/{protocol_name}:
1616
+ parameters:
1617
+ - in: path
1618
+ name: protocol_name
1619
+ required: true
1620
+ schema: { type: string }
1621
+ get:
1622
+ tags: [plugin_api]
1623
+ summary: Read one plugin-API protocol's metadata.
1624
+ operationId: plugin_api_get_protocol
1625
+ responses:
1626
+ '200':
1627
+ description: Protocol metadata.
1628
+ content:
1629
+ application/json:
1630
+ schema: { $ref: '#/components/schemas/PluginApiProtocol' }
1631
+ '404': { $ref: '#/components/responses/NotFound' }
1632
+ /plugin-api/_meta/mounts:
1633
+ get:
1634
+ tags: [plugin_api]
1635
+ summary: List extension mounts.
1636
+ operationId: plugin_api_list_mounts
1637
+ responses:
1638
+ '200':
1639
+ description: Mounts.
1640
+ content:
1641
+ application/json:
1642
+ schema:
1643
+ type: object
1644
+ required: [items]
1645
+ properties:
1646
+ items:
1647
+ type: array
1648
+ items: { $ref: '#/components/schemas/PluginApiMount' }
1649
+
1650
+ # =====================================================================
1651
+ # PLANNED — runs / test_runs / test_catalog / progress / dispatch /
1652
+ # results / artifacts
1653
+ #
1654
+ # These endpoints are NOT yet implemented in the codebase. They are
1655
+ # declared here so that:
1656
+ # 1. The generated client surfaces them as soon as the spec lands.
1657
+ # 2. Parallel implementation PRs target a stable, agreed-upon shape.
1658
+ #
1659
+ # Each operation carries `x-pychestrate-status: planned`.
1660
+ # =====================================================================
1661
+
1662
+ /runs:
1663
+ get:
1664
+ tags: [runs]
1665
+ summary: List runs (with filters).
1666
+ operationId: runs_list
1667
+ x-pychestrate-status: planned
1668
+ parameters:
1669
+ - in: query
1670
+ name: project_id
1671
+ schema: { type: string, format: uuid }
1672
+ - in: query
1673
+ name: pod_id
1674
+ schema: { type: string, format: uuid }
1675
+ - in: query
1676
+ name: state
1677
+ schema: { $ref: '#/components/schemas/RunState' }
1678
+ - $ref: '#/components/parameters/PageParam'
1679
+ - $ref: '#/components/parameters/PerPageParam'
1680
+ responses:
1681
+ '200':
1682
+ description: Page of runs.
1683
+ content:
1684
+ application/json:
1685
+ schema: { $ref: '#/components/schemas/RunPage' }
1686
+ /runs/{run_id}:
1687
+ parameters:
1688
+ - $ref: '#/components/parameters/RunIdParam'
1689
+ get:
1690
+ tags: [runs]
1691
+ summary: Read a run.
1692
+ operationId: runs_get_by_id
1693
+ x-pychestrate-status: planned
1694
+ responses:
1695
+ '200':
1696
+ description: Run.
1697
+ content:
1698
+ application/json:
1699
+ schema: { $ref: '#/components/schemas/Run' }
1700
+ '404': { $ref: '#/components/responses/NotFound' }
1701
+ /runs/{run_id}/events:
1702
+ parameters:
1703
+ - $ref: '#/components/parameters/RunIdParam'
1704
+ get:
1705
+ tags: [runs]
1706
+ summary: Subscribe to run events via Server-Sent Events.
1707
+ operationId: runs_subscribe_events
1708
+ x-pychestrate-status: planned
1709
+ description: |
1710
+ Long-lived response stream encoded as `text/event-stream`
1711
+ (Server-Sent Events). Each SSE frame's `data:` payload is a
1712
+ JSON document conforming to the `RunEvent` schema; the SSE
1713
+ `event:` line carries the event type
1714
+ (`state_changed` | `progress` | `result_manifest` | `heartbeat`).
1715
+
1716
+ Clients should send `Accept: text/event-stream` and reconnect
1717
+ with `Last-Event-ID` to resume from a known sequence number.
1718
+ responses:
1719
+ '200':
1720
+ description: SSE stream of run events.
1721
+ content:
1722
+ text/event-stream:
1723
+ schema: { $ref: '#/components/schemas/RunEvent' }
1724
+ '404': { $ref: '#/components/responses/NotFound' }
1725
+
1726
+ /runs/{run_id}/dispatch-ack:
1727
+ parameters:
1728
+ - $ref: '#/components/parameters/RunIdParam'
1729
+ post:
1730
+ tags: [dispatch]
1731
+ summary: Client agent acknowledges receipt of a dispatched envelope.
1732
+ operationId: dispatch_ack_run
1733
+ x-pychestrate-status: planned
1734
+ description: |
1735
+ Posted by the pod client agent over mTLS after it has accepted
1736
+ a dispatch and minted a local run identifier. Signals that the
1737
+ envelope was received and is being prepared for execution; the
1738
+ server transitions the run's state from `DISPATCHING` to
1739
+ `DISPATCHED` on success.
1740
+ security:
1741
+ - pychestrate_mtls: []
1742
+ requestBody:
1743
+ required: true
1744
+ content:
1745
+ application/json:
1746
+ schema: { $ref: '#/components/schemas/DispatchAck' }
1747
+ responses:
1748
+ '204':
1749
+ description: Ack recorded.
1750
+ '400': { $ref: '#/components/responses/BadRequest' }
1751
+ '401': { $ref: '#/components/responses/Unauthorized' }
1752
+ '404': { $ref: '#/components/responses/NotFound' }
1753
+ '409': { $ref: '#/components/responses/Conflict' }
1754
+
1755
+ /runs/{run_id}/progress:
1756
+ parameters:
1757
+ - $ref: '#/components/parameters/RunIdParam'
1758
+ post:
1759
+ tags: [progress]
1760
+ summary: Ingest a live-progress frame for a run.
1761
+ operationId: progress_post_frame
1762
+ x-pychestrate-status: planned
1763
+ description: |
1764
+ Posted by the pod client agent over mTLS at intervals during the
1765
+ run. Payload shape matches the `codecraftai-testbase`
1766
+ `ProgressConcentratorServer` / `ForwardingInfo` contract so an
1767
+ unmodified testbase client can post directly without
1768
+ translation.
1769
+
1770
+ Returns 202 on accept; the ingester is passive and never blocks
1771
+ the caller. Errors in downstream parsing are surfaced via the
1772
+ `RunEvent` SSE stream rather than the HTTP response.
1773
+ security:
1774
+ - pychestrate_mtls: []
1775
+ requestBody:
1776
+ required: true
1777
+ content:
1778
+ application/json:
1779
+ schema: { $ref: '#/components/schemas/ProgressFrame' }
1780
+ responses:
1781
+ '202':
1782
+ description: Progress frame accepted.
1783
+ '400': { $ref: '#/components/responses/BadRequest' }
1784
+ '401': { $ref: '#/components/responses/Unauthorized' }
1785
+ '404': { $ref: '#/components/responses/NotFound' }
1786
+
1787
+ /runs/{run_id}/results/complete:
1788
+ parameters:
1789
+ - $ref: '#/components/parameters/RunIdParam'
1790
+ post:
1791
+ tags: [results]
1792
+ summary: Signal that the result tree for a run is fully uploaded.
1793
+ operationId: results_post_complete
1794
+ x-pychestrate-status: planned
1795
+ description: |
1796
+ Posted by the pod client agent over mTLS once it has finished
1797
+ writing the testbase result tree (`testrun_summary.json`,
1798
+ `testrun_results_stream.jsos`, `testrun_results.json`,
1799
+ `testsummary.html`, plus the `artifacts/`, `taskings/`,
1800
+ `tc-by-products/`, `diagnostics/` subtrees) to the shared
1801
+ results backend.
1802
+
1803
+ The server transitions the run to a terminal state
1804
+ (`COMPLETED` / `FAILED`) based on the manifest's
1805
+ `terminal_outcome` field and kicks off the
1806
+ `result_tree_parser` extension chain.
1807
+ security:
1808
+ - pychestrate_mtls: []
1809
+ requestBody:
1810
+ required: true
1811
+ content:
1812
+ application/json:
1813
+ schema: { $ref: '#/components/schemas/ResultManifest' }
1814
+ responses:
1815
+ '202':
1816
+ description: Manifest accepted; result-tree parsing scheduled.
1817
+ '400': { $ref: '#/components/responses/BadRequest' }
1818
+ '401': { $ref: '#/components/responses/Unauthorized' }
1819
+ '404': { $ref: '#/components/responses/NotFound' }
1820
+ '409': { $ref: '#/components/responses/Conflict' }
1821
+
1822
+ /envelope-signing-keys:
1823
+ get:
1824
+ tags: [dispatch]
1825
+ summary: List the Ed25519 public keys used to sign job envelopes.
1826
+ operationId: envelope_signing_keys_list
1827
+ x-pychestrate-status: planned
1828
+ description: |
1829
+ A JWKS-style public-key distribution endpoint. The pod-client
1830
+ agent calls this to obtain the Ed25519 public key(s) it must
1831
+ verify signed job envelopes against — without the operator
1832
+ installing the key out-of-band.
1833
+
1834
+ Auth: a signing *public* key is not a secret, so the endpoint
1835
+ accepts **any authenticated principal** — a cookie session, an
1836
+ API key, OR a verified mTLS client certificate. A
1837
+ freshly-enrolled pod client already holds its mTLS identity, so
1838
+ the mTLS path covers the client's use case. Anonymous requests
1839
+ get a 401. The endpoint returns 503 when no signing key has
1840
+ been provisioned.
1841
+
1842
+ The response carries one or more keys to support rotation: at
1843
+ most one key is `active` and any others are `retiring`
1844
+ (grace-window verify keys). The same key set is also bundled in
1845
+ the `POST /enrollment-tokens/redeem` response so the happy path
1846
+ needs zero extra calls.
1847
+ responses:
1848
+ '200':
1849
+ description: The active envelope-signing public-key set.
1850
+ content:
1851
+ application/json:
1852
+ schema: { $ref: '#/components/schemas/SigningKeySet' }
1853
+ '401': { $ref: '#/components/responses/Unauthorized' }
1854
+ '403': { $ref: '#/components/responses/Forbidden' }
1855
+ '503': { $ref: '#/components/responses/ServiceUnavailable' }
1856
+
1857
+ /runs/{run_id}/artifacts:
1858
+ parameters:
1859
+ - $ref: '#/components/parameters/RunIdParam'
1860
+ get:
1861
+ tags: [artifacts]
1862
+ summary: List artifacts for a run.
1863
+ operationId: artifacts_list_for_run
1864
+ x-pychestrate-status: planned
1865
+ parameters:
1866
+ - in: query
1867
+ name: prefix
1868
+ description: Limit results to artifacts whose path starts with this prefix.
1869
+ schema: { type: string }
1870
+ - $ref: '#/components/parameters/PageParam'
1871
+ - $ref: '#/components/parameters/PerPageParam'
1872
+ responses:
1873
+ '200':
1874
+ description: Page of artifact entries.
1875
+ content:
1876
+ application/json:
1877
+ schema: { $ref: '#/components/schemas/ArtifactEntryPage' }
1878
+ '404': { $ref: '#/components/responses/NotFound' }
1879
+ /runs/{run_id}/artifacts/{artifact_path}:
1880
+ parameters:
1881
+ - $ref: '#/components/parameters/RunIdParam'
1882
+ - in: path
1883
+ name: artifact_path
1884
+ required: true
1885
+ description: Slash-delimited artifact path relative to the run's results root.
1886
+ schema: { type: string }
1887
+ get:
1888
+ tags: [artifacts]
1889
+ summary: Read metadata for one artifact entry.
1890
+ operationId: artifacts_get_entry
1891
+ x-pychestrate-status: planned
1892
+ responses:
1893
+ '200':
1894
+ description: Artifact entry.
1895
+ content:
1896
+ application/json:
1897
+ schema: { $ref: '#/components/schemas/ArtifactEntry' }
1898
+ '404': { $ref: '#/components/responses/NotFound' }
1899
+
1900
+ /runs/{run_id}/artifacts:presign:
1901
+ parameters:
1902
+ - $ref: '#/components/parameters/RunIdParam'
1903
+ post:
1904
+ tags: [results]
1905
+ summary: Mint presigned PUT URLs for a run's result-tree upload.
1906
+ operationId: artifacts_presign_upload
1907
+ x-pychestrate-status: planned
1908
+ description: |
1909
+ Posted by the pod client agent over mTLS to obtain
1910
+ short-lived presigned PUT URLs for the relative artifact
1911
+ paths it discovered while walking its testbase result tree.
1912
+
1913
+ The agent PUTs each artifact's bytes directly to the
1914
+ returned `put_url`, then calls
1915
+ `POST /runs/{run_id}/results/complete`. Object-store
1916
+ credentials never leave the server -- the credentialed
1917
+ transfer is encoded entirely in the presigned URL.
1918
+
1919
+ On the dev / alpha deploy there is no S3-compatible object
1920
+ store, so the server mints **local-filesystem fallback**
1921
+ URLs that point back at
1922
+ `PUT /runs/{run_id}/artifacts/_local-upload`. The URL
1923
+ carries an HMAC signature over `run_id` + path + expiry, so
1924
+ the fallback needs no object-store credential while staying
1925
+ tamper-proof. The agent's code path is identical either way.
1926
+
1927
+ Auth: the verified mTLS client common-name must match the
1928
+ `client_id` that owns the run's current dispatch receipt --
1929
+ only the client running the job may upload its results. A
1930
+ run that has already reached a terminal state no longer
1931
+ accepts uploads (409).
1932
+ security:
1933
+ - pychestrate_mtls: []
1934
+ requestBody:
1935
+ required: true
1936
+ content:
1937
+ application/json:
1938
+ schema: { $ref: '#/components/schemas/ArtifactPresignRequest' }
1939
+ responses:
1940
+ '200':
1941
+ description: Presigned upload targets minted, one per requested path.
1942
+ content:
1943
+ application/json:
1944
+ schema: { $ref: '#/components/schemas/ArtifactPresignResponse' }
1945
+ '400': { $ref: '#/components/responses/BadRequest' }
1946
+ '401': { $ref: '#/components/responses/Unauthorized' }
1947
+ '403': { $ref: '#/components/responses/Forbidden' }
1948
+ '404': { $ref: '#/components/responses/NotFound' }
1949
+ '409': { $ref: '#/components/responses/Conflict' }
1950
+ '422': { $ref: '#/components/responses/Unprocessable' }
1951
+ '503': { $ref: '#/components/responses/ServiceUnavailable' }
1952
+
1953
+ /runs/{run_id}/artifacts/_local-upload:
1954
+ parameters:
1955
+ - $ref: '#/components/parameters/RunIdParam'
1956
+ - in: query
1957
+ name: path
1958
+ required: true
1959
+ description: Canonicalized relative artifact path the bytes are stored under.
1960
+ schema: { type: string }
1961
+ - in: query
1962
+ name: expires
1963
+ required: true
1964
+ description: Expiry of the presigned URL as a Unix epoch second.
1965
+ schema: { type: integer }
1966
+ - in: query
1967
+ name: signature
1968
+ required: true
1969
+ description: HMAC-SHA256 signature over run_id + path + expiry.
1970
+ schema: { type: string }
1971
+ put:
1972
+ tags: [results]
1973
+ summary: Upload sink for the local-filesystem presign fallback.
1974
+ operationId: artifacts_local_upload
1975
+ x-pychestrate-status: planned
1976
+ description: |
1977
+ The upload sink the `:presign` operation's local-filesystem
1978
+ fallback URLs point at. Used only when the deploy has no
1979
+ S3-compatible object store.
1980
+
1981
+ The request body is the raw artifact bytes. The handler
1982
+ re-verifies the `signature` query parameter against the
1983
+ server-held HMAC secret before writing the bytes under the
1984
+ run's allocated `backend_key_prefix`. A forged or expired
1985
+ signature is rejected with 403; the signature -- not a
1986
+ permission check -- is this endpoint's auth contract, so no
1987
+ object-store credential is involved.
1988
+ security:
1989
+ - pychestrate_mtls: []
1990
+ requestBody:
1991
+ required: true
1992
+ content:
1993
+ application/octet-stream:
1994
+ schema:
1995
+ type: string
1996
+ format: binary
1997
+ responses:
1998
+ '201':
1999
+ description: Artifact bytes written to the result tree.
2000
+ content:
2001
+ application/json:
2002
+ schema: { $ref: '#/components/schemas/ArtifactLocalUploadResult' }
2003
+ '400': { $ref: '#/components/responses/BadRequest' }
2004
+ '401': { $ref: '#/components/responses/Unauthorized' }
2005
+ '403': { $ref: '#/components/responses/Forbidden' }
2006
+ '404': { $ref: '#/components/responses/NotFound' }
2007
+ '409': { $ref: '#/components/responses/Conflict' }
2008
+ '413': { $ref: '#/components/responses/PayloadTooLarge' }
2009
+ '503': { $ref: '#/components/responses/ServiceUnavailable' }
2010
+
2011
+ /projects/{project_id}/test-runs:
2012
+ parameters:
2013
+ - $ref: '#/components/parameters/ProjectIdParam'
2014
+ post:
2015
+ tags: [test_runs]
2016
+ summary: Trigger a new test run on a project.
2017
+ operationId: test_runs_trigger
2018
+ x-pychestrate-status: planned
2019
+ description: |
2020
+ Creates a new run from a catalog entry and a pod selector.
2021
+ Returns the freshly-created run in `PENDING` state; the
2022
+ dispatcher transitions it to `DISPATCHING` asynchronously.
2023
+ requestBody:
2024
+ required: true
2025
+ content:
2026
+ application/json:
2027
+ schema: { $ref: '#/components/schemas/TestRunCreateRequest' }
2028
+ responses:
2029
+ '202':
2030
+ description: Run accepted (queued for dispatch).
2031
+ content:
2032
+ application/json:
2033
+ schema: { $ref: '#/components/schemas/Run' }
2034
+ '400': { $ref: '#/components/responses/BadRequest' }
2035
+ '401': { $ref: '#/components/responses/Unauthorized' }
2036
+ '403': { $ref: '#/components/responses/Forbidden' }
2037
+ '404': { $ref: '#/components/responses/NotFound' }
2038
+ '409': { $ref: '#/components/responses/Conflict' }
2039
+
2040
+ /projects/{project_id}/test-catalog:
2041
+ parameters:
2042
+ - $ref: '#/components/parameters/ProjectIdParam'
2043
+ get:
2044
+ tags: [test_catalog]
2045
+ summary: List test-catalog entries for a project.
2046
+ operationId: test_catalog_list
2047
+ x-pychestrate-status: planned
2048
+ parameters:
2049
+ - in: query
2050
+ name: importer_tag
2051
+ schema: { type: string }
2052
+ - in: query
2053
+ name: include_retired
2054
+ schema: { type: boolean, default: false }
2055
+ - $ref: '#/components/parameters/PageParam'
2056
+ - $ref: '#/components/parameters/PerPageParam'
2057
+ responses:
2058
+ '200':
2059
+ description: Page of catalog entries.
2060
+ content:
2061
+ application/json:
2062
+ schema: { $ref: '#/components/schemas/TestCatalogEntryPage' }
2063
+ post:
2064
+ tags: [test_catalog]
2065
+ summary: Upsert a test-catalog entry.
2066
+ operationId: test_catalog_upsert
2067
+ x-pychestrate-status: planned
2068
+ requestBody:
2069
+ required: true
2070
+ content:
2071
+ application/json:
2072
+ schema: { $ref: '#/components/schemas/TestCatalogEntryUpsertRequest' }
2073
+ responses:
2074
+ '200':
2075
+ description: Entry upserted.
2076
+ content:
2077
+ application/json:
2078
+ schema: { $ref: '#/components/schemas/TestCatalogEntry' }
2079
+ '400': { $ref: '#/components/responses/BadRequest' }
2080
+ /projects/{project_id}/test-catalog:import:
2081
+ parameters:
2082
+ - $ref: '#/components/parameters/ProjectIdParam'
2083
+ post:
2084
+ tags: [test_catalog]
2085
+ summary: Run a test-catalog importer (e.g., testbase project discovery).
2086
+ operationId: test_catalog_import
2087
+ x-pychestrate-status: planned
2088
+ description: |
2089
+ Invokes the named importer (resolved via the
2090
+ `test_catalog_importer` factory protocol) against the project.
2091
+ Each import overwrites entries with the same
2092
+ `(project_id, importer_tag, entry_key)` key in place.
2093
+ requestBody:
2094
+ required: true
2095
+ content:
2096
+ application/json:
2097
+ schema: { $ref: '#/components/schemas/TestCatalogImportRequest' }
2098
+ responses:
2099
+ '202':
2100
+ description: Import accepted (runs asynchronously).
2101
+ content:
2102
+ application/json:
2103
+ schema: { $ref: '#/components/schemas/TestCatalogImportSummary' }
2104
+ '400': { $ref: '#/components/responses/BadRequest' }
2105
+ '404': { $ref: '#/components/responses/NotFound' }
2106
+
2107
+ components:
2108
+
2109
+ securitySchemes:
2110
+ pychestrate_session:
2111
+ type: apiKey
2112
+ in: cookie
2113
+ name: pych_session
2114
+ description: |
2115
+ HMAC-signed session cookie issued by the local-login flow at
2116
+ `/login` (HTML form, not part of the REST surface). The
2117
+ webworks `PychestrateSessionMiddleware` resolves this cookie
2118
+ to a request-scoped actor on every request.
2119
+ pychestrate_api_key:
2120
+ type: apiKey
2121
+ in: header
2122
+ name: X-Pychestrate-Api-Key
2123
+ description: |
2124
+ Long-lived API key for tooling and CI integrations. Issued
2125
+ out-of-band through the admin UI.
2126
+ pychestrate_mtls:
2127
+ type: mutualTLS
2128
+ description: |
2129
+ Mutual TLS using the client certificate issued by
2130
+ `POST /enrollment-tokens/redeem`. Required on every endpoint
2131
+ the pod client agent calls (`dispatch-ack`, `progress`,
2132
+ `results/complete`, plus any future client-pull endpoints).
2133
+
2134
+ parameters:
2135
+
2136
+ PageParam:
2137
+ in: query
2138
+ name: page
2139
+ description: One-based page index for paginated list endpoints.
2140
+ schema: { type: integer, minimum: 1, default: 1 }
2141
+ PerPageParam:
2142
+ in: query
2143
+ name: per_page
2144
+ description: Page size for paginated list endpoints.
2145
+ schema: { type: integer, minimum: 1, maximum: 500, default: 50 }
2146
+ UserIdParam:
2147
+ in: path
2148
+ name: user_id
2149
+ required: true
2150
+ schema: { type: string, format: uuid }
2151
+ ProjectIdParam:
2152
+ in: path
2153
+ name: project_id
2154
+ required: true
2155
+ schema: { type: string, format: uuid }
2156
+ ProjectSlugParam:
2157
+ in: path
2158
+ name: slug
2159
+ required: true
2160
+ description: Project slug — kebab-case, globally unique.
2161
+ schema: { type: string, pattern: '^[a-z0-9][a-z0-9-]*$' }
2162
+ ClientIdParam:
2163
+ in: path
2164
+ name: client_id
2165
+ required: true
2166
+ schema: { type: string, format: uuid }
2167
+ PodIdParam:
2168
+ in: path
2169
+ name: pod_id
2170
+ required: true
2171
+ schema: { type: string, format: uuid }
2172
+ RunIdParam:
2173
+ in: path
2174
+ name: run_id
2175
+ required: true
2176
+ schema: { type: string, format: uuid }
2177
+ FlagKeyParam:
2178
+ in: path
2179
+ name: flag_key
2180
+ required: true
2181
+ schema: { type: string }
2182
+ VariableScopeParam:
2183
+ in: path
2184
+ name: scope
2185
+ required: true
2186
+ description: Variable scope kind.
2187
+ schema:
2188
+ type: string
2189
+ enum: [global, project, pod, run]
2190
+ VariableScopeIdParam:
2191
+ in: path
2192
+ name: scope_id
2193
+ required: true
2194
+ schema: { type: string }
2195
+ EntityKindParam:
2196
+ in: path
2197
+ name: entity_kind
2198
+ required: true
2199
+ description: Type of entity the tag is attached to.
2200
+ schema:
2201
+ type: string
2202
+ enum: [project, pod, client, run, resource, artifact, run_artifact]
2203
+ EntityIdParam:
2204
+ in: path
2205
+ name: entity_id
2206
+ required: true
2207
+ schema: { type: string }
2208
+
2209
+ responses:
2210
+ BadRequest:
2211
+ description: Request was malformed or failed validation.
2212
+ content:
2213
+ application/problem+json:
2214
+ schema: { $ref: '#/components/schemas/Problem' }
2215
+ Unauthorized:
2216
+ description: Caller is not authenticated.
2217
+ content:
2218
+ application/problem+json:
2219
+ schema: { $ref: '#/components/schemas/Problem' }
2220
+ Forbidden:
2221
+ description: Caller is authenticated but lacks the required permission.
2222
+ content:
2223
+ application/problem+json:
2224
+ schema: { $ref: '#/components/schemas/Problem' }
2225
+ NotFound:
2226
+ description: Target resource was not found.
2227
+ content:
2228
+ application/problem+json:
2229
+ schema: { $ref: '#/components/schemas/Problem' }
2230
+ Conflict:
2231
+ description: Request conflicts with current resource state.
2232
+ content:
2233
+ application/problem+json:
2234
+ schema: { $ref: '#/components/schemas/Problem' }
2235
+ Unprocessable:
2236
+ description: Request was well-formed but semantically invalid.
2237
+ content:
2238
+ application/problem+json:
2239
+ schema: { $ref: '#/components/schemas/Problem' }
2240
+ PayloadTooLarge:
2241
+ description: Request body exceeds the server-side size cap.
2242
+ content:
2243
+ application/problem+json:
2244
+ schema: { $ref: '#/components/schemas/Problem' }
2245
+ ServiceUnavailable:
2246
+ description: Server-side dependency unavailable.
2247
+ content:
2248
+ application/problem+json:
2249
+ schema: { $ref: '#/components/schemas/Problem' }
2250
+
2251
+ schemas:
2252
+
2253
+ # ----------------------------------------------------------------
2254
+ # Error / pagination envelopes
2255
+ # ----------------------------------------------------------------
2256
+
2257
+ Problem:
2258
+ type: object
2259
+ description: |
2260
+ RFC 7807 Problem Document. Every 4xx / 5xx response in this API
2261
+ is served as `application/problem+json` with this shape.
2262
+ required: [type, title, status, detail]
2263
+ properties:
2264
+ type:
2265
+ type: string
2266
+ format: uri
2267
+ description: URI identifying the problem class (`about:blank` for generic statuses).
2268
+ default: about:blank
2269
+ title:
2270
+ type: string
2271
+ description: Short, human-readable summary of the problem type.
2272
+ status:
2273
+ type: integer
2274
+ description: HTTP status code generated by the origin server.
2275
+ detail:
2276
+ type: string
2277
+ description: Occurrence-specific explanation.
2278
+ instance:
2279
+ type: string
2280
+ format: uri-reference
2281
+ description: URI reference identifying this specific occurrence.
2282
+
2283
+ PageMeta:
2284
+ type: object
2285
+ description: Pagination metadata returned alongside every paged `items` array.
2286
+ required: [page, per_page, total]
2287
+ properties:
2288
+ page: { type: integer, minimum: 1 }
2289
+ per_page: { type: integer, minimum: 1 }
2290
+ total:
2291
+ type: integer
2292
+ minimum: 0
2293
+ description: Total number of items across all pages (when known).
2294
+ next_cursor:
2295
+ type: string
2296
+ nullable: true
2297
+ description: Opaque cursor for the next page (when cursor-based).
2298
+
2299
+ # ----------------------------------------------------------------
2300
+ # auth / me
2301
+ # ----------------------------------------------------------------
2302
+
2303
+ LogoutRequest:
2304
+ type: object
2305
+ required: [session_id]
2306
+ properties:
2307
+ session_id: { type: string }
2308
+ LogoutResponse:
2309
+ type: object
2310
+ required: [session_id, status]
2311
+ properties:
2312
+ session_id: { type: string }
2313
+ status:
2314
+ type: string
2315
+ enum: [closed]
2316
+
2317
+ MePermissions:
2318
+ type: object
2319
+ required: [user_id, permissions]
2320
+ properties:
2321
+ user_id: { type: string, format: uuid }
2322
+ permissions:
2323
+ type: array
2324
+ items: { type: string }
2325
+ role_bindings:
2326
+ type: array
2327
+ items: { $ref: '#/components/schemas/RbacRoleBinding' }
2328
+
2329
+ # ----------------------------------------------------------------
2330
+ # users / rbac
2331
+ # ----------------------------------------------------------------
2332
+
2333
+ User:
2334
+ type: object
2335
+ required: [user_id, username, status, created_at]
2336
+ properties:
2337
+ user_id: { type: string, format: uuid }
2338
+ username: { type: string }
2339
+ display_name: { type: string }
2340
+ email: { type: string, format: email }
2341
+ status:
2342
+ type: string
2343
+ enum: [active, suspended, deleted]
2344
+ created_at: { type: string, format: date-time }
2345
+ updated_at: { type: string, format: date-time }
2346
+ UserCreateRequest:
2347
+ type: object
2348
+ required: [username]
2349
+ properties:
2350
+ username: { type: string }
2351
+ display_name: { type: string }
2352
+ email: { type: string, format: email }
2353
+ initial_password:
2354
+ type: string
2355
+ description: Optional initial password for local-auth users.
2356
+
2357
+ RbacPermission:
2358
+ type: object
2359
+ required: [name]
2360
+ properties:
2361
+ name: { type: string }
2362
+ description: { type: string }
2363
+ RbacRole:
2364
+ type: object
2365
+ required: [name]
2366
+ properties:
2367
+ name: { type: string }
2368
+ description: { type: string }
2369
+ permissions:
2370
+ type: array
2371
+ items: { type: string }
2372
+ RbacRoleBinding:
2373
+ type: object
2374
+ required: [binding_id, user_id, role_name]
2375
+ properties:
2376
+ binding_id: { type: string, format: uuid }
2377
+ user_id: { type: string, format: uuid }
2378
+ role_name: { type: string }
2379
+ project_id:
2380
+ type: string
2381
+ format: uuid
2382
+ nullable: true
2383
+ description: Project scope of the binding; `null` for site-wide bindings.
2384
+ created_at: { type: string, format: date-time }
2385
+ RbacRoleBindingCreateRequest:
2386
+ type: object
2387
+ required: [user_id, role_name]
2388
+ properties:
2389
+ user_id: { type: string, format: uuid }
2390
+ role_name: { type: string }
2391
+ project_id:
2392
+ type: string
2393
+ format: uuid
2394
+ nullable: true
2395
+ RbacCheckRequest:
2396
+ type: object
2397
+ required: [permission]
2398
+ properties:
2399
+ permission: { type: string }
2400
+ project_id:
2401
+ type: string
2402
+ format: uuid
2403
+ nullable: true
2404
+ user_id:
2405
+ type: string
2406
+ format: uuid
2407
+ description: When omitted, the check runs against the calling actor.
2408
+ RbacCheckResponse:
2409
+ type: object
2410
+ required: [allowed, permission]
2411
+ properties:
2412
+ allowed: { type: boolean }
2413
+ permission: { type: string }
2414
+ project_id:
2415
+ type: string
2416
+ format: uuid
2417
+ nullable: true
2418
+ reason: { type: string }
2419
+
2420
+ # ----------------------------------------------------------------
2421
+ # audit
2422
+ # ----------------------------------------------------------------
2423
+
2424
+ AuditRecord:
2425
+ type: object
2426
+ required: [sequence_number, action, recorded_at]
2427
+ properties:
2428
+ sequence_number: { type: integer, minimum: 0 }
2429
+ action: { type: string, description: 'Action key (e.g. `project.created`).' }
2430
+ actor_id:
2431
+ type: string
2432
+ format: uuid
2433
+ nullable: true
2434
+ target_kind: { type: string }
2435
+ target_id: { type: string }
2436
+ recorded_at: { type: string, format: date-time }
2437
+ payload:
2438
+ type: object
2439
+ additionalProperties: true
2440
+ description: Action-specific payload bag.
2441
+
2442
+ # ----------------------------------------------------------------
2443
+ # secrets
2444
+ # ----------------------------------------------------------------
2445
+
2446
+ SecretRef:
2447
+ type: object
2448
+ required: [secret_ref, backend, created_at]
2449
+ properties:
2450
+ secret_ref:
2451
+ type: string
2452
+ description: Backend-qualified secret path (e.g. `vault/myteam/api-token`).
2453
+ backend: { type: string }
2454
+ description: { type: string }
2455
+ last_rotated_at:
2456
+ type: string
2457
+ format: date-time
2458
+ nullable: true
2459
+ created_at: { type: string, format: date-time }
2460
+
2461
+ # ----------------------------------------------------------------
2462
+ # variables
2463
+ # ----------------------------------------------------------------
2464
+
2465
+ Variable:
2466
+ type: object
2467
+ required: [name, scope, scope_id, value_type, value]
2468
+ properties:
2469
+ name: { type: string }
2470
+ scope:
2471
+ type: string
2472
+ enum: [global, project, pod, run]
2473
+ scope_id: { type: string }
2474
+ value_type:
2475
+ type: string
2476
+ enum: [string, integer, float, boolean, json, secret_ref]
2477
+ value: {}
2478
+ description: { type: string }
2479
+ sensitive: { type: boolean, default: false }
2480
+ updated_at: { type: string, format: date-time }
2481
+ VariableUpsertRequest:
2482
+ type: object
2483
+ required: [value_type, value]
2484
+ properties:
2485
+ value_type:
2486
+ type: string
2487
+ enum: [string, integer, float, boolean, json, secret_ref]
2488
+ value: {}
2489
+ description: { type: string }
2490
+ sensitive: { type: boolean, default: false }
2491
+ VariableEffectiveSet:
2492
+ type: object
2493
+ required: [items]
2494
+ properties:
2495
+ items:
2496
+ type: array
2497
+ items: { $ref: '#/components/schemas/VariableEffectiveEntry' }
2498
+ VariableEffectiveEntry:
2499
+ type: object
2500
+ required: [name, value_type, value, resolved_from]
2501
+ properties:
2502
+ name: { type: string }
2503
+ value_type:
2504
+ type: string
2505
+ enum: [string, integer, float, boolean, json, secret_ref]
2506
+ value: {}
2507
+ resolved_from:
2508
+ type: string
2509
+ enum: [global, project, pod, run]
2510
+ description: Scope that won inheritance for this variable.
2511
+
2512
+ # ----------------------------------------------------------------
2513
+ # flags
2514
+ # ----------------------------------------------------------------
2515
+
2516
+ FeatureFlag:
2517
+ type: object
2518
+ required: [flag_key, enabled, created_at]
2519
+ properties:
2520
+ flag_key: { type: string }
2521
+ description: { type: string }
2522
+ enabled: { type: boolean }
2523
+ rollout_percent:
2524
+ type: integer
2525
+ minimum: 0
2526
+ maximum: 100
2527
+ retired: { type: boolean, default: false }
2528
+ created_at: { type: string, format: date-time }
2529
+ updated_at: { type: string, format: date-time }
2530
+ FeatureFlagCreateRequest:
2531
+ type: object
2532
+ required: [flag_key]
2533
+ properties:
2534
+ flag_key: { type: string }
2535
+ description: { type: string }
2536
+ enabled: { type: boolean, default: false }
2537
+ rollout_percent: { type: integer, minimum: 0, maximum: 100 }
2538
+ FeatureFlagUpdateRequest:
2539
+ type: object
2540
+ properties:
2541
+ description: { type: string }
2542
+ enabled: { type: boolean }
2543
+ rollout_percent: { type: integer, minimum: 0, maximum: 100 }
2544
+ FeatureFlagBackend:
2545
+ type: object
2546
+ required: [name]
2547
+ properties:
2548
+ name: { type: string }
2549
+ description: { type: string }
2550
+ active: { type: boolean }
2551
+ FeatureFlagHistoryEntry:
2552
+ type: object
2553
+ required: [recorded_at, action]
2554
+ properties:
2555
+ recorded_at: { type: string, format: date-time }
2556
+ actor_id:
2557
+ type: string
2558
+ format: uuid
2559
+ nullable: true
2560
+ action: { type: string }
2561
+ before:
2562
+ type: object
2563
+ additionalProperties: true
2564
+ after:
2565
+ type: object
2566
+ additionalProperties: true
2567
+ FeatureFlagEvaluateRequest:
2568
+ type: object
2569
+ properties:
2570
+ context:
2571
+ type: object
2572
+ additionalProperties: true
2573
+ description: Evaluation context bag (user_id, project_id, attrs).
2574
+ FeatureFlagEvaluateResponse:
2575
+ type: object
2576
+ required: [flag_key, value]
2577
+ properties:
2578
+ flag_key: { type: string }
2579
+ value: { type: boolean }
2580
+ reason: { type: string }
2581
+
2582
+ # ----------------------------------------------------------------
2583
+ # tagging
2584
+ # ----------------------------------------------------------------
2585
+
2586
+ Tag:
2587
+ type: object
2588
+ required: [tag, entity_kind, entity_id]
2589
+ properties:
2590
+ tag: { type: string }
2591
+ entity_kind: { type: string }
2592
+ entity_id: { type: string }
2593
+ taxonomy: { type: string }
2594
+ attached_at: { type: string, format: date-time }
2595
+ TagAttachRequest:
2596
+ type: object
2597
+ required: [tag]
2598
+ properties:
2599
+ tag: { type: string }
2600
+ taxonomy: { type: string }
2601
+ TagTaxonomy:
2602
+ type: object
2603
+ required: [name]
2604
+ properties:
2605
+ name: { type: string }
2606
+ description: { type: string }
2607
+
2608
+ # ----------------------------------------------------------------
2609
+ # retention
2610
+ # ----------------------------------------------------------------
2611
+
2612
+ RetentionPolicy:
2613
+ type: object
2614
+ required: [policy_id, name, data_class, action, status, created_at]
2615
+ properties:
2616
+ policy_id: { type: string, format: uuid }
2617
+ name: { type: string }
2618
+ description: { type: string }
2619
+ data_class: { type: string }
2620
+ action:
2621
+ type: string
2622
+ enum: [archive, cold_tier, delete, custom]
2623
+ retention_period_days: { type: integer, minimum: 0 }
2624
+ status:
2625
+ type: string
2626
+ enum: [draft, published, archived]
2627
+ created_at: { type: string, format: date-time }
2628
+ updated_at: { type: string, format: date-time }
2629
+ RetentionPolicyCreateRequest:
2630
+ type: object
2631
+ required: [name, data_class, action]
2632
+ properties:
2633
+ name: { type: string }
2634
+ description: { type: string }
2635
+ data_class: { type: string }
2636
+ action:
2637
+ type: string
2638
+ enum: [archive, cold_tier, delete, custom]
2639
+ retention_period_days: { type: integer, minimum: 0 }
2640
+
2641
+ # ----------------------------------------------------------------
2642
+ # notifications
2643
+ # ----------------------------------------------------------------
2644
+
2645
+ NotificationChannel:
2646
+ type: object
2647
+ required: [channel_id, name, kind, status, created_at]
2648
+ properties:
2649
+ channel_id: { type: string, format: uuid }
2650
+ name: { type: string }
2651
+ kind:
2652
+ type: string
2653
+ enum: [email, slack, webhook, pagerduty]
2654
+ config:
2655
+ type: object
2656
+ additionalProperties: true
2657
+ status:
2658
+ type: string
2659
+ enum: [active, disabled, error]
2660
+ created_at: { type: string, format: date-time }
2661
+ updated_at: { type: string, format: date-time }
2662
+ NotificationChannelCreateRequest:
2663
+ type: object
2664
+ required: [name, kind, config]
2665
+ properties:
2666
+ name: { type: string }
2667
+ kind:
2668
+ type: string
2669
+ enum: [email, slack, webhook, pagerduty]
2670
+ config:
2671
+ type: object
2672
+ additionalProperties: true
2673
+
2674
+ # ----------------------------------------------------------------
2675
+ # project_templates
2676
+ # ----------------------------------------------------------------
2677
+
2678
+ ProjectTemplate:
2679
+ type: object
2680
+ required: [template_id, name]
2681
+ properties:
2682
+ template_id: { type: string }
2683
+ name: { type: string }
2684
+ description: { type: string }
2685
+ version: { type: string }
2686
+ provided_by: { type: string }
2687
+
2688
+ # ----------------------------------------------------------------
2689
+ # projects
2690
+ # ----------------------------------------------------------------
2691
+
2692
+ Project:
2693
+ type: object
2694
+ required: [project_id, slug, display_name, status, created_at]
2695
+ properties:
2696
+ project_id: { type: string, format: uuid }
2697
+ slug: { type: string }
2698
+ display_name: { type: string }
2699
+ description: { type: string }
2700
+ owning_group_id:
2701
+ type: string
2702
+ format: uuid
2703
+ nullable: true
2704
+ template_id:
2705
+ type: string
2706
+ nullable: true
2707
+ template_version:
2708
+ type: string
2709
+ nullable: true
2710
+ status:
2711
+ type: string
2712
+ enum: [active, archived, deleted]
2713
+ extension_fields:
2714
+ type: object
2715
+ additionalProperties: true
2716
+ created_at: { type: string, format: date-time }
2717
+ updated_at: { type: string, format: date-time }
2718
+ ProjectCreateRequest:
2719
+ type: object
2720
+ required: [slug, display_name]
2721
+ properties:
2722
+ slug: { type: string, pattern: '^[a-z0-9][a-z0-9-]*$' }
2723
+ display_name: { type: string }
2724
+ description: { type: string }
2725
+ owning_group_id:
2726
+ type: string
2727
+ format: uuid
2728
+ nullable: true
2729
+ template_id:
2730
+ type: string
2731
+ nullable: true
2732
+ template_version:
2733
+ type: string
2734
+ nullable: true
2735
+ ProjectPatchRequest:
2736
+ type: object
2737
+ properties:
2738
+ display_name: { type: string }
2739
+ description: { type: string }
2740
+ extension_fields:
2741
+ type: object
2742
+ additionalProperties: true
2743
+ ProjectMember:
2744
+ type: object
2745
+ required: [user_id, role_bindings]
2746
+ properties:
2747
+ user_id: { type: string, format: uuid }
2748
+ username: { type: string }
2749
+ display_name: { type: string }
2750
+ role_bindings:
2751
+ type: array
2752
+ items: { $ref: '#/components/schemas/RbacRoleBinding' }
2753
+ ProjectMemberAddRequest:
2754
+ type: object
2755
+ required: [user_id, role_name]
2756
+ properties:
2757
+ user_id: { type: string, format: uuid }
2758
+ role_name: { type: string }
2759
+
2760
+ # ----------------------------------------------------------------
2761
+ # clients / enrollment / agents
2762
+ # ----------------------------------------------------------------
2763
+
2764
+ Client:
2765
+ type: object
2766
+ required: [client_id, status, created_at]
2767
+ properties:
2768
+ client_id: { type: string, format: uuid }
2769
+ display_name: { type: string }
2770
+ status:
2771
+ type: string
2772
+ enum: [pending, active, revoked]
2773
+ attributes:
2774
+ type: object
2775
+ additionalProperties: true
2776
+ last_seen_at:
2777
+ type: string
2778
+ format: date-time
2779
+ nullable: true
2780
+ agent_version: { type: string }
2781
+ created_at: { type: string, format: date-time }
2782
+ updated_at: { type: string, format: date-time }
2783
+ ClientRevokeRequest:
2784
+ type: object
2785
+ properties:
2786
+ reason: { type: string }
2787
+ ClientAttributesUpdateRequest:
2788
+ type: object
2789
+ required: [attributes]
2790
+ properties:
2791
+ attributes:
2792
+ type: object
2793
+ additionalProperties: true
2794
+ ClientInventory:
2795
+ type: object
2796
+ required: [items, generated_at]
2797
+ properties:
2798
+ items:
2799
+ type: array
2800
+ items: { $ref: '#/components/schemas/Client' }
2801
+ generated_at: { type: string, format: date-time }
2802
+ ClientCapabilities:
2803
+ type: object
2804
+ required: [client_id, capabilities, advertised_at]
2805
+ properties:
2806
+ client_id: { type: string, format: uuid }
2807
+ capabilities:
2808
+ type: array
2809
+ items: { $ref: '#/components/schemas/CapabilityEntry' }
2810
+ advertised_at: { type: string, format: date-time }
2811
+ CapabilityEntry:
2812
+ type: object
2813
+ required: [kind, name]
2814
+ properties:
2815
+ kind:
2816
+ type: string
2817
+ description: Capability kind name (resolves a `CapabilityKindExtension`).
2818
+ name: { type: string }
2819
+ version: { type: string }
2820
+ attributes:
2821
+ type: object
2822
+ additionalProperties: true
2823
+
2824
+ EnrollmentToken:
2825
+ type: object
2826
+ required: [token_id, created_at]
2827
+ properties:
2828
+ token_id: { type: string, format: uuid }
2829
+ description: { type: string }
2830
+ expires_at:
2831
+ type: string
2832
+ format: date-time
2833
+ nullable: true
2834
+ max_redemptions: { type: integer, minimum: 1 }
2835
+ redeemed_count: { type: integer, minimum: 0 }
2836
+ created_at: { type: string, format: date-time }
2837
+ revoked_at:
2838
+ type: string
2839
+ format: date-time
2840
+ nullable: true
2841
+ EnrollmentTokenCreateRequest:
2842
+ type: object
2843
+ properties:
2844
+ description: { type: string }
2845
+ ttl_seconds: { type: integer, minimum: 60 }
2846
+ max_redemptions: { type: integer, minimum: 1, default: 1 }
2847
+ EnrollmentTokenWithSecret:
2848
+ allOf:
2849
+ - $ref: '#/components/schemas/EnrollmentToken'
2850
+ - type: object
2851
+ required: [bootstrap_secret]
2852
+ properties:
2853
+ bootstrap_secret:
2854
+ type: string
2855
+ description: One-time secret returned only at issuance.
2856
+ EnrollmentRedeemRequest:
2857
+ type: object
2858
+ required: [bootstrap_secret, csr_pem]
2859
+ properties:
2860
+ bootstrap_secret: { type: string }
2861
+ csr_pem:
2862
+ type: string
2863
+ description: PEM-encoded PKCS#10 certificate signing request.
2864
+ display_name: { type: string }
2865
+ attributes:
2866
+ type: object
2867
+ additionalProperties: true
2868
+ EnrollmentRedeemResponse:
2869
+ type: object
2870
+ required: [client_id, client_certificate_pem, ca_chain_pem]
2871
+ properties:
2872
+ client_id: { type: string, format: uuid }
2873
+ client_certificate_pem: { type: string }
2874
+ ca_chain_pem: { type: string }
2875
+ not_after: { type: string, format: date-time }
2876
+ envelope_signing_keys:
2877
+ description: |
2878
+ The JWKS-style Ed25519 public-key set the service signs job
2879
+ envelopes with, bundled here so a freshly-enrolled client
2880
+ can verify signed envelopes without an extra call to
2881
+ `GET /envelope-signing-keys`. Empty when no signing key is
2882
+ provisioned; the client should refresh via the dedicated
2883
+ endpoint in that case.
2884
+ type: array
2885
+ items: { $ref: '#/components/schemas/SigningKey' }
2886
+
2887
+ SigningKey:
2888
+ type: object
2889
+ description: |
2890
+ A single Ed25519 public key in the envelope-signing key set.
2891
+ Only the public half is ever serialized — the private signing
2892
+ key never leaves the service process.
2893
+ required: [key_id, algorithm, public_key_pem, status]
2894
+ properties:
2895
+ key_id:
2896
+ type: string
2897
+ description: JWKS-style identifier referenced by each signed envelope.
2898
+ algorithm:
2899
+ type: string
2900
+ enum: [Ed25519]
2901
+ public_key_pem:
2902
+ type: string
2903
+ description: PEM-encoded SubjectPublicKeyInfo public key.
2904
+ status:
2905
+ type: string
2906
+ description: |
2907
+ `active` — the key currently signing envelopes.
2908
+ `retiring` — a previous-generation key kept verifiable
2909
+ through the rotation grace window.
2910
+ enum: [active, retiring]
2911
+
2912
+ SigningKeySet:
2913
+ type: object
2914
+ required: [keys]
2915
+ properties:
2916
+ keys:
2917
+ type: array
2918
+ items: { $ref: '#/components/schemas/SigningKey' }
2919
+
2920
+ AgentRollout:
2921
+ type: object
2922
+ required: [rollout_id, status, created_at]
2923
+ properties:
2924
+ rollout_id: { type: string, format: uuid }
2925
+ agent_version: { type: string }
2926
+ targeting:
2927
+ type: object
2928
+ additionalProperties: true
2929
+ status:
2930
+ type: string
2931
+ enum: [draft, scheduled, running, completed, halted]
2932
+ created_at: { type: string, format: date-time }
2933
+ started_at:
2934
+ type: string
2935
+ format: date-time
2936
+ nullable: true
2937
+ completed_at:
2938
+ type: string
2939
+ format: date-time
2940
+ nullable: true
2941
+ AgentRolloutCreateRequest:
2942
+ type: object
2943
+ required: [agent_version]
2944
+ properties:
2945
+ agent_version: { type: string }
2946
+ targeting:
2947
+ type: object
2948
+ additionalProperties: true
2949
+
2950
+ # ----------------------------------------------------------------
2951
+ # pods
2952
+ # ----------------------------------------------------------------
2953
+
2954
+ PodDefinition:
2955
+ type: object
2956
+ required: [pod_id, project_id, name, created_at]
2957
+ properties:
2958
+ pod_id: { type: string, format: uuid }
2959
+ project_id: { type: string, format: uuid }
2960
+ name: { type: string }
2961
+ description: { type: string }
2962
+ members:
2963
+ type: array
2964
+ items: { $ref: '#/components/schemas/PodMember' }
2965
+ attributes:
2966
+ type: object
2967
+ additionalProperties: true
2968
+ created_at: { type: string, format: date-time }
2969
+ updated_at: { type: string, format: date-time }
2970
+ PodDefinitionCreateRequest:
2971
+ type: object
2972
+ required: [name]
2973
+ properties:
2974
+ name: { type: string }
2975
+ description: { type: string }
2976
+ members:
2977
+ type: array
2978
+ items: { $ref: '#/components/schemas/PodMember' }
2979
+ attributes:
2980
+ type: object
2981
+ additionalProperties: true
2982
+ PodMember:
2983
+ type: object
2984
+ required: [name, kind]
2985
+ properties:
2986
+ name: { type: string }
2987
+ kind:
2988
+ type: string
2989
+ description: Pod-member kind name (resolves a `PodMemberKindExtension`).
2990
+ attributes:
2991
+ type: object
2992
+ additionalProperties: true
2993
+ PodState:
2994
+ type: object
2995
+ required: [pod_id, state, updated_at]
2996
+ properties:
2997
+ pod_id: { type: string, format: uuid }
2998
+ state:
2999
+ type: string
3000
+ enum:
3001
+ - declared
3002
+ - provisioning
3003
+ - registering
3004
+ - ready
3005
+ - busy
3006
+ - draining
3007
+ - offline
3008
+ - error
3009
+ - retired
3010
+ updated_at: { type: string, format: date-time }
3011
+ last_message: { type: string }
3012
+ PodStateTransition:
3013
+ type: object
3014
+ required: [pod_id, from_state, to_state, occurred_at]
3015
+ properties:
3016
+ pod_id: { type: string, format: uuid }
3017
+ from_state: { type: string }
3018
+ to_state: { type: string }
3019
+ occurred_at: { type: string, format: date-time }
3020
+ actor_id:
3021
+ type: string
3022
+ format: uuid
3023
+ nullable: true
3024
+ reason: { type: string }
3025
+ PodRegistration:
3026
+ type: object
3027
+ required: [pod_id, client_id, registered_at]
3028
+ properties:
3029
+ pod_id: { type: string, format: uuid }
3030
+ client_id: { type: string, format: uuid }
3031
+ registered_at: { type: string, format: date-time }
3032
+ attributes:
3033
+ type: object
3034
+ additionalProperties: true
3035
+ PodTopology:
3036
+ type: object
3037
+ required: [pod_id, members, links]
3038
+ properties:
3039
+ pod_id: { type: string, format: uuid }
3040
+ members:
3041
+ type: array
3042
+ items: { $ref: '#/components/schemas/PodMember' }
3043
+ links:
3044
+ type: array
3045
+ items: { $ref: '#/components/schemas/PodTopologyLink' }
3046
+ PodTopologyLink:
3047
+ type: object
3048
+ required: [from_member, to_member, kind]
3049
+ properties:
3050
+ from_member: { type: string }
3051
+ to_member: { type: string }
3052
+ kind: { type: string }
3053
+ attributes:
3054
+ type: object
3055
+ additionalProperties: true
3056
+ PodTemplate:
3057
+ type: object
3058
+ required: [template_id, project_id, name]
3059
+ properties:
3060
+ template_id: { type: string }
3061
+ project_id: { type: string, format: uuid }
3062
+ name: { type: string }
3063
+ description: { type: string }
3064
+ parameters:
3065
+ type: object
3066
+ additionalProperties: true
3067
+ PodTemplateCreateRequest:
3068
+ type: object
3069
+ required: [template_id, name]
3070
+ properties:
3071
+ template_id: { type: string }
3072
+ name: { type: string }
3073
+ description: { type: string }
3074
+ parameters:
3075
+ type: object
3076
+ additionalProperties: true
3077
+ PodTemplateMaterializeRequest:
3078
+ type: object
3079
+ properties:
3080
+ name:
3081
+ type: string
3082
+ description: Optional name override for the materialized pod.
3083
+ parameters:
3084
+ type: object
3085
+ additionalProperties: true
3086
+
3087
+ # ----------------------------------------------------------------
3088
+ # plugin_api
3089
+ # ----------------------------------------------------------------
3090
+
3091
+ PluginApiProtocol:
3092
+ type: object
3093
+ required: [protocol_name]
3094
+ properties:
3095
+ protocol_name: { type: string }
3096
+ description: { type: string }
3097
+ schema_version: { type: string }
3098
+ implementations:
3099
+ type: array
3100
+ items:
3101
+ type: object
3102
+ properties:
3103
+ name: { type: string }
3104
+ precedence: { type: integer }
3105
+ package: { type: string }
3106
+ PluginApiMount:
3107
+ type: object
3108
+ required: [mount_path]
3109
+ properties:
3110
+ mount_path: { type: string }
3111
+ extension_name: { type: string }
3112
+ capabilities:
3113
+ type: array
3114
+ items: { type: string }
3115
+
3116
+ # ----------------------------------------------------------------
3117
+ # JobEnvelope (planned — surfaced as a referenced schema, not a top-
3118
+ # level operation body, because it travels as the payload of a
3119
+ # signed dispatch and is not POST'd as JSON by clients of the
3120
+ # public REST surface.)
3121
+ # ----------------------------------------------------------------
3122
+
3123
+ JobEnvelope:
3124
+ type: object
3125
+ description: |
3126
+ Canonical dispatch envelope. Matches the dataclass in
3127
+ `source/site/ccai/pychestrate/webservice/jobs/envelope/schema.py`.
3128
+ Serialized with RFC 8785 (JCS) canonical JSON; consumers MUST
3129
+ verify against the declared `schema_version`
3130
+ (current: `2026-04-24`).
3131
+ required:
3132
+ - schema_version
3133
+ - envelope_id
3134
+ - issued_at
3135
+ - issuer
3136
+ - project_id
3137
+ - job_id
3138
+ - job_name
3139
+ - job_type
3140
+ - run_id
3141
+ - client_id
3142
+ - pod_id
3143
+ - refs
3144
+ - params
3145
+ - scoped_secrets
3146
+ - results
3147
+ - forwarding
3148
+ - deadline
3149
+ - extension_claims
3150
+ properties:
3151
+ schema_version: { type: string }
3152
+ envelope_id: { type: string }
3153
+ issued_at: { type: string, format: date-time }
3154
+ issuer: { type: string }
3155
+ project_id: { type: string, format: uuid }
3156
+ job_id: { type: string, format: uuid }
3157
+ job_name: { type: string }
3158
+ job_type: { type: string }
3159
+ pipeline:
3160
+ nullable: true
3161
+ $ref: '#/components/schemas/JobPipelineRef'
3162
+ run_id: { type: string, format: uuid }
3163
+ client_id: { type: string, format: uuid }
3164
+ pod_id: { type: string, format: uuid }
3165
+ refs: { $ref: '#/components/schemas/JobRefs' }
3166
+ params:
3167
+ type: object
3168
+ additionalProperties: true
3169
+ scoped_secrets:
3170
+ type: array
3171
+ items: { $ref: '#/components/schemas/ScopedSecret' }
3172
+ results: { $ref: '#/components/schemas/JobResultsLocator' }
3173
+ forwarding: { $ref: '#/components/schemas/JobForwardingInfo' }
3174
+ deadline: { $ref: '#/components/schemas/JobDeadline' }
3175
+ extension_claims:
3176
+ type: object
3177
+ additionalProperties: true
3178
+ JobRefs:
3179
+ type: object
3180
+ required: [branch, build, flavor, release, commit_sha]
3181
+ properties:
3182
+ branch: { type: string }
3183
+ build: { type: string }
3184
+ flavor: { type: string }
3185
+ release: { type: string }
3186
+ commit_sha: { type: string }
3187
+ JobPipelineRef:
3188
+ type: object
3189
+ required: [pipeline_id, instance_id]
3190
+ properties:
3191
+ pipeline_id: { type: string }
3192
+ instance_id: { type: string }
3193
+ JobResultsLocator:
3194
+ type: object
3195
+ required: [backend_key_prefix, backend_driver]
3196
+ properties:
3197
+ backend_key_prefix: { type: string }
3198
+ backend_driver: { type: string }
3199
+ JobForwardingInfo:
3200
+ type: object
3201
+ required: [progress_url, summary_url, auth_method]
3202
+ properties:
3203
+ progress_url: { type: string, format: uri }
3204
+ summary_url: { type: string, format: uri }
3205
+ auth_method:
3206
+ type: string
3207
+ enum: [bearer, mtls]
3208
+ JobDeadline:
3209
+ type: object
3210
+ required: [soft_deadline_at, hard_deadline_at]
3211
+ properties:
3212
+ soft_deadline_at: { type: string, format: date-time }
3213
+ hard_deadline_at: { type: string, format: date-time }
3214
+
3215
+ PendingDispatch:
3216
+ type: object
3217
+ description: |
3218
+ Server response to `GET /api/v1/clients/{client_id}/dispatches:next`
3219
+ when a queued dispatch is available. Carries the signed
3220
+ `JobEnvelope` plus its detached Ed25519 signature and the
3221
+ ack-deadline the agent must beat. The signature is hex-encoded;
3222
+ agents re-verify against the canonical JSON serialization of the
3223
+ embedded `envelope` field.
3224
+ required:
3225
+ - dispatch_attempt_id
3226
+ - run_id
3227
+ - client_id
3228
+ - pod_id
3229
+ - envelope_id
3230
+ - enqueued_at
3231
+ - ack_deadline_at
3232
+ - envelope
3233
+ - signature
3234
+ - signature_key_id
3235
+ - signed_at
3236
+ - transport_name
3237
+ properties:
3238
+ dispatch_attempt_id: { type: string, format: uuid }
3239
+ run_id: { type: string, format: uuid }
3240
+ client_id: { type: string, format: uuid }
3241
+ pod_id: { type: string, format: uuid }
3242
+ envelope_id: { type: string, format: uuid }
3243
+ enqueued_at: { type: string, format: date-time }
3244
+ ack_deadline_at: { type: string, format: date-time }
3245
+ envelope: { $ref: '#/components/schemas/JobEnvelope' }
3246
+ signature:
3247
+ type: string
3248
+ description: Hex-encoded Ed25519 detached signature.
3249
+ signature_key_id:
3250
+ type: string
3251
+ description: JWKS key id for the signing key.
3252
+ signed_at: { type: string, format: date-time }
3253
+ transport_name:
3254
+ type: string
3255
+ description: Stable transport identifier (`http-long-poll`).
3256
+ ScopedSecret:
3257
+ type: object
3258
+ required: [name, value, expires_at, token_type]
3259
+ properties:
3260
+ name: { type: string }
3261
+ value: { type: string }
3262
+ expires_at: { type: string, format: date-time }
3263
+ token_type: { type: string }
3264
+
3265
+ # ----------------------------------------------------------------
3266
+ # RunState / Run
3267
+ # ----------------------------------------------------------------
3268
+
3269
+ RunState:
3270
+ type: string
3271
+ description: |
3272
+ Run lifecycle state. Mirrors
3273
+ `jobs.run.state.enums.RunState`.
3274
+ enum:
3275
+ - pending
3276
+ - dispatching
3277
+ - dispatched
3278
+ - dispatch_unconfirmed
3279
+ - dispatch_lost
3280
+ - dispatch_failed
3281
+ - running
3282
+ - progress_stalled
3283
+ - cancel_requested
3284
+ - cancelling
3285
+ - cancelled
3286
+ - completing
3287
+ - completed
3288
+ - failed
3289
+ - needs_investigation
3290
+
3291
+ TerminalOutcome:
3292
+ type: string
3293
+ enum:
3294
+ - success
3295
+ - failure
3296
+ - cancelled
3297
+ - dispatch_failed
3298
+ - dispatch_lost
3299
+
3300
+ Run:
3301
+ type: object
3302
+ required: [run_id, project_id, pod_id, job_type, state, created_at]
3303
+ properties:
3304
+ run_id: { type: string, format: uuid }
3305
+ project_id: { type: string, format: uuid }
3306
+ pod_id: { type: string, format: uuid }
3307
+ client_id:
3308
+ type: string
3309
+ format: uuid
3310
+ nullable: true
3311
+ job_id: { type: string, format: uuid }
3312
+ job_name: { type: string }
3313
+ job_type: { type: string }
3314
+ state: { $ref: '#/components/schemas/RunState' }
3315
+ terminal_outcome:
3316
+ nullable: true
3317
+ $ref: '#/components/schemas/TerminalOutcome'
3318
+ current_dispatch_attempt_id:
3319
+ type: string
3320
+ format: uuid
3321
+ nullable: true
3322
+ progress_summary:
3323
+ nullable: true
3324
+ $ref: '#/components/schemas/ProgressSummary'
3325
+ created_at: { type: string, format: date-time }
3326
+ dispatched_at:
3327
+ type: string
3328
+ format: date-time
3329
+ nullable: true
3330
+ started_at:
3331
+ type: string
3332
+ format: date-time
3333
+ nullable: true
3334
+ completed_at:
3335
+ type: string
3336
+ format: date-time
3337
+ nullable: true
3338
+ labels:
3339
+ type: object
3340
+ additionalProperties: { type: string }
3341
+ RunPage:
3342
+ type: object
3343
+ required: [items, meta]
3344
+ properties:
3345
+ items:
3346
+ type: array
3347
+ items: { $ref: '#/components/schemas/Run' }
3348
+ meta: { $ref: '#/components/schemas/PageMeta' }
3349
+
3350
+ # ----------------------------------------------------------------
3351
+ # SSE events
3352
+ # ----------------------------------------------------------------
3353
+
3354
+ RunEvent:
3355
+ type: object
3356
+ description: |
3357
+ One event emitted on the `/runs/{run_id}/events` SSE stream.
3358
+ The SSE `event:` field carries the event type tag; the SSE
3359
+ `data:` field carries this JSON object.
3360
+ required: [run_id, event_type, sequence, occurred_at]
3361
+ properties:
3362
+ run_id: { type: string, format: uuid }
3363
+ event_type:
3364
+ type: string
3365
+ enum: [state_changed, progress, result_manifest, heartbeat]
3366
+ sequence: { type: integer, minimum: 0 }
3367
+ occurred_at: { type: string, format: date-time }
3368
+ state: { $ref: '#/components/schemas/RunState' }
3369
+ terminal_outcome:
3370
+ nullable: true
3371
+ $ref: '#/components/schemas/TerminalOutcome'
3372
+ progress:
3373
+ nullable: true
3374
+ $ref: '#/components/schemas/ProgressSummary'
3375
+ manifest:
3376
+ nullable: true
3377
+ $ref: '#/components/schemas/ResultManifest'
3378
+
3379
+ # ----------------------------------------------------------------
3380
+ # dispatch / progress / results
3381
+ # ----------------------------------------------------------------
3382
+
3383
+ DispatchAck:
3384
+ type: object
3385
+ description: |
3386
+ Posted by the pod client agent to `POST /runs/{run_id}/dispatch-ack`
3387
+ to confirm receipt of a dispatched envelope.
3388
+ required: [envelope_id, dispatch_attempt_id, client_id, local_run_id, accepted_at]
3389
+ properties:
3390
+ envelope_id: { type: string }
3391
+ dispatch_attempt_id: { type: string, format: uuid }
3392
+ client_id: { type: string, format: uuid }
3393
+ local_run_id:
3394
+ type: string
3395
+ description: Local identifier minted by the client agent for reconciliation.
3396
+ accepted_at: { type: string, format: date-time }
3397
+ notes: { type: string }
3398
+
3399
+ ProgressSummary:
3400
+ type: object
3401
+ description: |
3402
+ Roll-up the client agent emits at intervals. Matches the
3403
+ testbase `ForwardingInfo` shape; see
3404
+ `ingest.progress.schema.ProgressSummary`.
3405
+ required: [run_id, client_id, format_tag, tests_passed, tests_failed, tests_skipped, tests_errored, recorded_at]
3406
+ properties:
3407
+ run_id: { type: string, format: uuid }
3408
+ client_id: { type: string, format: uuid }
3409
+ format_tag:
3410
+ type: string
3411
+ description: Parser tag (`testbase` for the in-tree default).
3412
+ tests_total:
3413
+ type: integer
3414
+ nullable: true
3415
+ tests_passed: { type: integer, minimum: 0 }
3416
+ tests_failed: { type: integer, minimum: 0 }
3417
+ tests_skipped: { type: integer, minimum: 0 }
3418
+ tests_errored: { type: integer, minimum: 0 }
3419
+ percent_complete:
3420
+ type: number
3421
+ minimum: 0
3422
+ maximum: 100
3423
+ nullable: true
3424
+ notes:
3425
+ type: string
3426
+ nullable: true
3427
+ recorded_at: { type: string, format: date-time }
3428
+ raw:
3429
+ type: object
3430
+ additionalProperties: true
3431
+
3432
+ ProgressRecord:
3433
+ type: object
3434
+ description: |
3435
+ One progress record from the testbase
3436
+ `testrun_results_stream.jsos` stream — represents one test
3437
+ scope completion (or preview).
3438
+ required: [run_id, client_id, record_index, test_name, result]
3439
+ properties:
3440
+ run_id: { type: string, format: uuid }
3441
+ client_id: { type: string, format: uuid }
3442
+ record_index: { type: integer, minimum: 0 }
3443
+ test_name: { type: string }
3444
+ result:
3445
+ type: string
3446
+ enum: [PASSED, FAILED, SKIPPED, ERRORED, UNSET]
3447
+ started_at:
3448
+ type: string
3449
+ format: date-time
3450
+ nullable: true
3451
+ finished_at:
3452
+ type: string
3453
+ format: date-time
3454
+ nullable: true
3455
+ duration_seconds:
3456
+ type: number
3457
+ nullable: true
3458
+ raw:
3459
+ type: object
3460
+ additionalProperties: true
3461
+
3462
+ ProgressFrame:
3463
+ type: object
3464
+ description: |
3465
+ Body of `POST /runs/{run_id}/progress`. One frame carries either
3466
+ a summary roll-up, or a batch of per-test records, or both.
3467
+ properties:
3468
+ summary:
3469
+ nullable: true
3470
+ $ref: '#/components/schemas/ProgressSummary'
3471
+ records:
3472
+ type: array
3473
+ items: { $ref: '#/components/schemas/ProgressRecord' }
3474
+
3475
+ ResultManifest:
3476
+ type: object
3477
+ description: |
3478
+ Body of `POST /runs/{run_id}/results/complete`. Signals that
3479
+ the agent has finished writing the full testbase result tree
3480
+ to the shared results backend and tells the server where the
3481
+ tree lives + how it should be classified.
3482
+ required:
3483
+ - run_id
3484
+ - client_id
3485
+ - format_tag
3486
+ - backend_key_prefix
3487
+ - completed_at
3488
+ - terminal_outcome
3489
+ properties:
3490
+ run_id: { type: string, format: uuid }
3491
+ client_id: { type: string, format: uuid }
3492
+ format_tag:
3493
+ type: string
3494
+ description: Parser tag (`testbase` for testbase result trees).
3495
+ backend_key_prefix:
3496
+ type: string
3497
+ description: Per-run prefix the agent wrote the tree under
3498
+ (echoes `JobResultsLocator.backend_key_prefix`).
3499
+ backend_driver:
3500
+ type: string
3501
+ description: Object-store driver name the agent used.
3502
+ completed_at: { type: string, format: date-time }
3503
+ terminal_outcome: { $ref: '#/components/schemas/TerminalOutcome' }
3504
+ summary_artifact_path:
3505
+ type: string
3506
+ description: Path (relative to `backend_key_prefix`) of
3507
+ `testrun_summary.json`.
3508
+ default: testrun_summary.json
3509
+ results_stream_artifact_path:
3510
+ type: string
3511
+ description: Path of `testrun_results_stream.jsos`.
3512
+ default: testrun_results_stream.jsos
3513
+ results_artifact_path:
3514
+ type: string
3515
+ description: Path of the consolidated `testrun_results.json`.
3516
+ default: testrun_results.json
3517
+ html_summary_artifact_path:
3518
+ type: string
3519
+ description: Path of `testsummary.html`.
3520
+ default: testsummary.html
3521
+ tests_passed: { type: integer, minimum: 0 }
3522
+ tests_failed: { type: integer, minimum: 0 }
3523
+ tests_skipped: { type: integer, minimum: 0 }
3524
+ tests_errored: { type: integer, minimum: 0 }
3525
+ notes: { type: string }
3526
+ extras:
3527
+ type: object
3528
+ additionalProperties: true
3529
+
3530
+ # ----------------------------------------------------------------
3531
+ # artifacts
3532
+ # ----------------------------------------------------------------
3533
+
3534
+ ArtifactEntry:
3535
+ type: object
3536
+ required: [path, size_bytes, content_type]
3537
+ properties:
3538
+ path:
3539
+ type: string
3540
+ description: Slash-delimited path relative to the run's results root.
3541
+ size_bytes: { type: integer, minimum: 0 }
3542
+ content_type: { type: string }
3543
+ artifact_type:
3544
+ type: string
3545
+ description: Logical category assigned by an `ArtifactClassifierExtension`.
3546
+ download_url:
3547
+ type: string
3548
+ format: uri
3549
+ description: Signed URL the caller can use to download the bytes.
3550
+ download_url_expires_at:
3551
+ type: string
3552
+ format: date-time
3553
+ sha256: { type: string }
3554
+ recorded_at: { type: string, format: date-time }
3555
+ ArtifactEntryPage:
3556
+ type: object
3557
+ required: [items, meta]
3558
+ properties:
3559
+ items:
3560
+ type: array
3561
+ items: { $ref: '#/components/schemas/ArtifactEntry' }
3562
+ meta: { $ref: '#/components/schemas/PageMeta' }
3563
+
3564
+ ArtifactPresignRequest:
3565
+ type: object
3566
+ description: |
3567
+ Body of `POST /runs/{run_id}/artifacts:presign`. Carries the
3568
+ relative artifact paths the agent discovered while walking
3569
+ its result tree and wants presigned upload URLs for.
3570
+ required: [paths]
3571
+ properties:
3572
+ paths:
3573
+ type: array
3574
+ description: |
3575
+ Relative artifact paths (relative to the run's result
3576
+ root). Must be non-empty; the server caps the count and
3577
+ the summed path length and rejects an over-cap batch
3578
+ with 422.
3579
+ minItems: 1
3580
+ maxItems: 512
3581
+ items:
3582
+ type: string
3583
+ description: A single slash-delimited relative path.
3584
+ maxLength: 1024
3585
+
3586
+ ArtifactUploadTarget:
3587
+ type: object
3588
+ description: |
3589
+ One minted presigned upload target. The agent PUTs the
3590
+ artifact's bytes to `put_url`, sending `headers` verbatim.
3591
+ required: [path, put_url, expires_at, headers]
3592
+ properties:
3593
+ path:
3594
+ type: string
3595
+ description: The relative artifact path this target covers.
3596
+ put_url:
3597
+ type: string
3598
+ description: |
3599
+ URL the agent PUTs the artifact bytes to. For the
3600
+ local-filesystem fallback this is a path-absolute URL on
3601
+ this service carrying the upload signature in its query
3602
+ string; for an S3-backed deploy it is an absolute
3603
+ object-store URL.
3604
+ expires_at:
3605
+ type: string
3606
+ format: date-time
3607
+ description: UTC timestamp after which the URL is rejected.
3608
+ headers:
3609
+ type: object
3610
+ additionalProperties: { type: string }
3611
+ description: |
3612
+ Headers the agent must send verbatim on the PUT. Empty
3613
+ for the local-filesystem fallback (the signature travels
3614
+ in the URL query string).
3615
+
3616
+ ArtifactPresignResponse:
3617
+ type: object
3618
+ description: |
3619
+ Body of a successful `POST /runs/{run_id}/artifacts:presign`.
3620
+ Carries one upload target per requested path, in request
3621
+ order.
3622
+ required: [run_id, uploads]
3623
+ properties:
3624
+ run_id: { type: string, format: uuid }
3625
+ uploads:
3626
+ type: array
3627
+ items: { $ref: '#/components/schemas/ArtifactUploadTarget' }
3628
+
3629
+ ArtifactLocalUploadResult:
3630
+ type: object
3631
+ description: |
3632
+ Body of a successful
3633
+ `PUT /runs/{run_id}/artifacts/_local-upload`.
3634
+ required: [run_id, path, bytes_written, stored_at]
3635
+ properties:
3636
+ run_id: { type: string, format: uuid }
3637
+ path:
3638
+ type: string
3639
+ description: The relative artifact path the bytes were stored under.
3640
+ bytes_written: { type: integer, minimum: 0 }
3641
+ stored_at:
3642
+ type: string
3643
+ description: Server-side absolute path the bytes landed at.
3644
+
3645
+ # ----------------------------------------------------------------
3646
+ # test_runs
3647
+ # ----------------------------------------------------------------
3648
+
3649
+ TestRunCreateRequest:
3650
+ type: object
3651
+ description: |
3652
+ Body of `POST /projects/{project_id}/test-runs`. Either
3653
+ `catalog_entry_id` or `(importer_tag, entry_key)` MUST be
3654
+ provided so the server can resolve the catalog row.
3655
+ required: [pod_id]
3656
+ properties:
3657
+ catalog_entry_id:
3658
+ type: string
3659
+ format: uuid
3660
+ importer_tag: { type: string }
3661
+ entry_key: { type: string }
3662
+ pod_id: { type: string, format: uuid }
3663
+ params:
3664
+ type: object
3665
+ additionalProperties: true
3666
+ labels:
3667
+ type: object
3668
+ additionalProperties: { type: string }
3669
+ soft_deadline_seconds: { type: integer, minimum: 1 }
3670
+ hard_deadline_seconds: { type: integer, minimum: 1 }
3671
+
3672
+ # ----------------------------------------------------------------
3673
+ # test_catalog
3674
+ # ----------------------------------------------------------------
3675
+
3676
+ TestCatalogEntry:
3677
+ type: object
3678
+ description: |
3679
+ Matches the SQLAlchemy model in
3680
+ `orchestration.test.catalog.models.TestCatalogEntry`.
3681
+ required:
3682
+ - entry_id
3683
+ - project_id
3684
+ - importer_tag
3685
+ - entry_key
3686
+ - display_name
3687
+ - canonical_digest
3688
+ - retired
3689
+ - created_at
3690
+ properties:
3691
+ entry_id: { type: string, format: uuid }
3692
+ project_id: { type: string, format: uuid }
3693
+ importer_tag: { type: string }
3694
+ entry_key: { type: string }
3695
+ display_name: { type: string }
3696
+ description: { type: string }
3697
+ required_capability_tags:
3698
+ type: array
3699
+ items: { type: string }
3700
+ param_schema:
3701
+ type: object
3702
+ additionalProperties: true
3703
+ raw_metadata:
3704
+ type: object
3705
+ additionalProperties: true
3706
+ canonical_digest: { type: string }
3707
+ retired: { type: boolean }
3708
+ created_at: { type: string, format: date-time }
3709
+ updated_at: { type: string, format: date-time }
3710
+ TestCatalogEntryPage:
3711
+ type: object
3712
+ required: [items, meta]
3713
+ properties:
3714
+ items:
3715
+ type: array
3716
+ items: { $ref: '#/components/schemas/TestCatalogEntry' }
3717
+ meta: { $ref: '#/components/schemas/PageMeta' }
3718
+ TestCatalogEntryUpsertRequest:
3719
+ type: object
3720
+ required: [importer_tag, entry_key, display_name]
3721
+ properties:
3722
+ importer_tag: { type: string }
3723
+ entry_key: { type: string }
3724
+ display_name: { type: string }
3725
+ description: { type: string }
3726
+ required_capability_tags:
3727
+ type: array
3728
+ items: { type: string }
3729
+ param_schema:
3730
+ type: object
3731
+ additionalProperties: true
3732
+ raw_metadata:
3733
+ type: object
3734
+ additionalProperties: true
3735
+ retired: { type: boolean, default: false }
3736
+ TestCatalogImportRequest:
3737
+ type: object
3738
+ required: [importer_tag]
3739
+ properties:
3740
+ importer_tag:
3741
+ type: string
3742
+ description: Importer factory tag (e.g. `testbase-project`).
3743
+ source:
3744
+ type: object
3745
+ additionalProperties: true
3746
+ description: Importer-specific source descriptor (repo URL, branch, path).
3747
+ TestCatalogImportSummary:
3748
+ type: object
3749
+ required: [importer_tag, started_at]
3750
+ properties:
3751
+ importer_tag: { type: string }
3752
+ started_at: { type: string, format: date-time }
3753
+ entries_imported: { type: integer, minimum: 0 }
3754
+ entries_retired: { type: integer, minimum: 0 }
3755
+ errors:
3756
+ type: array
3757
+ items: { type: string }