Repository navigation
Expand file tree
/
Copy pathopenapi.yaml
More file actions
7550 lines (7199 loc) · 247 KB
/
Copy pathopenapi.yaml
File metadata and controls
7550 lines (7199 loc) · 247 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# OpenAPI 3.1 description of the af-hub REST API.
#
# Derived from the hub source tree and from apikit (github.com/txsvc/apikit,
# resolved through the `replace` directive in go.mod), cross-checked against
# docs/api.md and docs/permissions.md. Endpoints served by apikit rather than
# by hub code are marked `x-provider: apikit`. Where the prose docs disagree
# with the handlers, the handlers win; the divergences are recorded in
# docs/errata/openapi_description.md.
openapi: 3.1.0
info:
title: agent-fox hub REST API
version: "0.19.0"
summary: Headless harness for spec-driven, multi-agent software development.
description: |
The `hub` binary is the API server for agent-fox. It owns user identity,
OAuth, workspaces, git hosting, the merge queue, carry-patch workflows,
secrets, variables, agent sessions, and audit storage.
## Base paths
| Prefix | Contents |
|--------|----------|
| `/api/v1` | Everything under the API mount point: workspaces, patches, rebuilds, merges, secrets, variables, sessions, audit, and the apikit identity and administration endpoints |
| `/git` | Built-in git smart HTTP server (`/git/<org>/<slug>.git`) |
| *(root)* | `/healthz`, `/readyz`, `/version`, and `/metrics` |
The `/api/v1` prefix comes from `server.mount_point` in `config.toml` and
defaults to `/api/v1`. Everything apikit mounts -- `/user`, `/users`,
`/orgs`, `/auth` -- goes on that same group, so those paths are
`/api/v1/user`, `/api/v1/users`, and so on. `docs/api.md` and
`docs/permissions.md` both write them without the prefix; see
`docs/errata/openapi_description.md`.
## Authentication
Every `/api/v1` endpoint requires a bearer credential, except the two
OAuth routes (`GET /api/v1/auth/providers` and
`POST /api/v1/auth/callback`), which are registered before the auth
middleware is attached to the group. Three credential types exist:
- **Admin token** (`af_admin_<64-hex>`) -- implicit full access, bypasses
permission and ownership checks. Cannot create workspaces (no user
identity to own them).
- **API key** (`af_<key_id>_<secret>`) -- implicit full access to the
owning user's resources. Created only through the OAuth login flow.
- **Personal access token / PAT** (`af_pat_<token_id>_<secret>`) --
explicit scope-based access, restricted to the scopes granted at
creation. Never treated as admin, even for admin-role users.
Scope checks constrain PATs only. Each operation records the scopes it
accepts in the non-standard `x-required-scopes` field; an empty list
means no PAT scope check is performed.
The git smart HTTP endpoints use HTTP Basic authentication instead: the
username is ignored and the password must be any valid hub credential.
## Anti-enumeration policy
Workspace CRUD endpoints (list, get, update, archive, reactivate, delete)
return `404` -- not `403` -- when a PAT lacks the required scope or the
workspace is not owned by the caller, so that workspace slugs cannot be
enumerated. The git server likewise answers `404` for non-owner access.
Workspace *creation* returns `403`, because there is no existing resource
to enumerate. Other endpoint families return `403` for a missing scope.
## Ownership enforcement
Ownership is enforced on every workspace-scoped endpoint: sync, reclone,
patches, merges, rebuilds, rerere, patch-status, sessions and audit
reads look the workspace up by slug *and* owner and answer `404` for a
workspace the caller does not own (admin tokens bypass). The unified
audit query and SSE stream are restricted to the caller's workspaces.
## Workspace lock
Operations that modify a workspace clone are serialised per workspace.
Handlers that cannot take the lock immediately answer `409` with
`error_type: workspace_busy`.
A carry-patch rebuild applies patches in its own detached worktree and
holds the lock only for its fetch phase (upstream fetch and base
resolution) and its final phase (integration-ref update, patch
bookkeeping, optional push to `origin`). Sync, rollback, rerere forget and
batch rebase are rejected only while a rebuild is in one of those two
phases. Archive and reclone delete the directory the worktree lives in, so
they answer `409 workspace_busy` for the whole rebuild.
## Error envelope
All errors use the apikit envelope: `{"error": {"code": N, "message":
"..."}}`. Some handlers add a machine-readable `error_type` field.
license:
name: Apache-2.0
identifier: Apache-2.0
contact:
name: agent-fox hub
url: https://github.com/agent-fox-dev/hub
servers:
- url: http://localhost:8080
description: Local development server (default `server.port` = 8080)
- url: "{external_url}"
description: Deployment configured through `server.external_url` in config.toml
variables:
external_url:
default: http://localhost:8080
externalDocs:
description: Hand-written API reference
url: https://github.com/agent-fox-dev/hub/blob/main/docs/api.md
tags:
- name: Health
description: Liveness and readiness probes. Unauthenticated.
- name: Metrics
description: Prometheus exposition endpoint. Unauthenticated.
- name: Workspaces
description: Workspace lifecycle -- create, list, update, archive, sync, reclone, delete.
- name: Patches
description: Ordered patch-branch list for carry-patch workspaces.
- name: Rerere
description: Recorded git rerere conflict resolutions.
- name: Rebuilds
description: Integration-branch rebuild jobs for carry-patch workspaces.
- name: Merges
description: Rebase-then-fast-forward merge queue and batch rebase.
- name: Secrets
description: Write-only secret storage scoped to a user, org, or workspace.
- name: Variables
description: Readable variable storage scoped to a user, org, or workspace.
- name: Sessions
description: Agent session lifecycle, token usage, and cost aggregation.
- name: Audit Ingestion
description: Per-run telemetry ingestion and query (DuckDB backed).
- name: Audit Query
description: Unified audit query, transcripts, and the SSE event stream.
- name: Git
description: Built-in git smart HTTP server.
- name: Identity
description: >-
OAuth login, the caller's own profile, API keys, and personal access
tokens. Provided by apikit and mounted under the API prefix.
- name: Administration
description: >-
Cross-tenant user and organization administration. Provided by apikit;
requires an admin token or an admin-role API key.
security:
- bearerAuth: []
paths:
# ---------------------------------------------------------------------------
# Health and metrics
# ---------------------------------------------------------------------------
/healthz:
get:
tags: [Health]
operationId: getHealthz
summary: Liveness probe
description: Reports that the process is up. Mounted outside the API auth group.
security: []
x-provider: apikit
x-required-scopes: []
responses:
"200":
description: The server is alive.
content:
application/json:
schema:
type: object
properties:
status:
const: ok
example: { status: ok }
/readyz:
get:
tags: [Health]
operationId: getReadyz
summary: Readiness probe
description: |
Runs the registered health checker, which pings the primary SQLite
database. Mounted outside the API auth group.
security: []
x-provider: apikit
x-required-scopes: []
responses:
"200":
description: The server is ready to serve traffic.
content:
application/json:
schema:
type: object
properties:
status:
const: ready
example: { status: ready }
"503":
description: A dependency (the database) is unreachable.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/metrics:
get:
tags: [Metrics]
operationId: getMetrics
summary: Prometheus metrics
description: |
Scrapes all `afhub_*` metrics from the custom Prometheus registry in
the text exposition format. Mounted outside the API auth middleware,
so no `Authorization` header is required.
Exposed series: `afhub_http_requests_total`,
`afhub_http_request_duration_seconds`, `afhub_agent_sessions_active`,
`afhub_agent_tokens_total`, `afhub_audit_events_total`,
`afhub_audit_table_rows`, `afhub_retention_errors_total`,
`afhub_retention_last_run_timestamp_seconds`, `afhub_sse_connections`,
and `afhub_jobqueue_depth`.
The `path` label carries the Echo route template (for example
`/api/v1/sessions/:id`) rather than the resolved URI, to bound
cardinality.
security: []
x-required-scopes: []
responses:
"200":
description: Metrics in Prometheus text exposition format.
content:
text/plain:
schema:
type: string
# ---------------------------------------------------------------------------
# Workspaces
# ---------------------------------------------------------------------------
/api/v1/workspaces:
post:
tags: [Workspaces]
operationId: createWorkspace
summary: Create a workspace
description: |
Creates a workspace and enqueues an asynchronous clone job. The
response is returned immediately with `clone_status` set to
`pending`; poll `GET /api/v1/workspaces/{slug}` to observe the
transition to `cloning` and then `ready` or `failed`.
When `org_id` is omitted or empty the server assigns the caller's
personal organization; if the caller has none, the request fails with
`400`.
Git credentials supplied in the body are validated with `ls-remote`,
stored as workspace secrets, and zeroed out of the request struct
immediately after binding so they never reach logs.
Admin tokens are rejected with `403`: a workspace requires a real
user as owner.
x-required-scopes: ["workspaces:create"]
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateWorkspaceRequest"
examples:
standard:
summary: Standard workspace
value:
slug: my-project
git_url: https://github.com/user/repo.git
branch: main
display_name: My Project
description: A description of the workspace
carryPatch:
summary: Carry-patch workspace with a private upstream
value:
slug: vendor-fork
git_url: https://github.com/acme/vendor-fork.git
workspace_mode: carry_patch
upstream_url: https://github.com/upstream/repo.git
integration_branch: deploy
git_pat: ghp_xxxxxxxxxxxx
responses:
"201":
description: Workspace created; the clone job is queued.
content:
application/json:
schema:
$ref: "#/components/schemas/Workspace"
"400":
description: |
Missing `slug` or `git_url`; `display_name` over 128 characters;
`description` over 1024 characters; invalid `sync_mode` or
`workspace_mode`; `upstream_url` missing for a carry-patch
workspace or supplied for a standard one; git credentials that are
empty, mutually exclusive, incompletely paired, used with a
non-HTTPS `git_url`, or that fail the `ls-remote` check; or the
caller has no personal organization while `org_id` was omitted.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
description: >-
An admin token attempted to create a workspace, or the PAT lacks
the `workspaces:create` scope.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409":
description: A workspace with this slug already exists.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
$ref: "#/components/responses/InternalServerError"
get:
tags: [Workspaces]
operationId: listWorkspaces
summary: List workspaces
description: |
Non-admin callers see only workspaces they own. Admin tokens see every
workspace on the server.
x-required-scopes: ["workspaces:read", "workspaces:create", "workspaces:write"]
parameters:
- name: include_archived
in: query
required: false
description: >-
Include archived workspaces in the listing. Only the exact string
`true` enables this.
schema:
type: boolean
default: false
responses:
"200":
description: The accessible workspaces.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Workspace"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
description: The PAT lacks read access (anti-enumeration).
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
$ref: "#/components/responses/InternalServerError"
/api/v1/workspaces/{slug}:
parameters:
- $ref: "#/components/parameters/WorkspaceSlug"
get:
tags: [Workspaces]
operationId: getWorkspace
summary: Get a workspace
x-required-scopes: ["workspaces:read", "workspaces:create", "workspaces:write"]
responses:
"200":
description: The workspace.
content:
application/json:
schema:
$ref: "#/components/schemas/Workspace"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/WorkspaceNotFoundOrForbidden"
"500":
$ref: "#/components/responses/InternalServerError"
patch:
tags: [Workspaces]
operationId: updateWorkspace
summary: Update mutable workspace properties
description: |
Partial update: only the fields present in the body are modified.
At least one field must be supplied.
`slug`, `git_url`, `branch`, `owner_id`, `workspace_mode`,
`upstream_url`, and `integration_branch` are immutable -- including any
of them in the body is rejected with `400`. `updated_at` is advanced on
every successful update.
x-required-scopes: ["workspaces:write"]
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateWorkspaceRequest"
responses:
"200":
description: The updated workspace.
content:
application/json:
schema:
$ref: "#/components/schemas/Workspace"
"400":
description: >-
Empty body; workspace is archived (reactivate first);
`display_name` over 128 characters; `description` over 1024
characters; an immutable field was included; or `sync_mode` is
invalid or null.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
description: The workspace owner is not a member of the organization in `org_id`.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"404":
$ref: "#/components/responses/WorkspaceNotFoundOrForbidden"
"500":
$ref: "#/components/responses/InternalServerError"
delete:
tags: [Workspaces]
operationId: deleteWorkspace
summary: Permanently delete an archived workspace
description: |
Only archived workspaces can be deleted. `workspaces:delete` does not
imply `workspaces:read`.
x-required-scopes: ["workspaces:delete"]
responses:
"204":
description: Workspace deleted.
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/WorkspaceNotFoundOrForbidden"
"409":
description: The workspace is not archived.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
$ref: "#/components/responses/InternalServerError"
/api/v1/workspaces/{slug}/archive:
parameters:
- $ref: "#/components/parameters/WorkspaceSlug"
post:
tags: [Workspaces]
operationId: archiveWorkspace
summary: Archive a workspace
description: |
Archived workspaces are read-only; all state is preserved and the
workspace can be reactivated later. Any active agent sessions for the
workspace are force-closed and an audit event is emitted.
x-required-scopes: ["workspaces:write"]
responses:
"200":
description: The archived workspace.
content:
application/json:
schema:
$ref: "#/components/schemas/Workspace"
"400":
description: The workspace is already archived.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/WorkspaceNotFoundOrForbidden"
"409":
description: >-
A clone is in progress (archiving is rejected until it settles), or
another operation holds the workspace lock (`error_type:
workspace_busy`). A running carry-patch rebuild makes archive answer
`workspace_busy` for the whole rebuild.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
$ref: "#/components/responses/InternalServerError"
/api/v1/workspaces/{slug}/reactivate:
parameters:
- $ref: "#/components/parameters/WorkspaceSlug"
post:
tags: [Workspaces]
operationId: reactivateWorkspace
summary: Reactivate an archived workspace
description: |
Restores `status` to `active`, resets `clone_status` to `pending`, and
enqueues a reclone job.
x-required-scopes: ["workspaces:write"]
responses:
"200":
description: The reactivated workspace.
content:
application/json:
schema:
$ref: "#/components/schemas/Workspace"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/WorkspaceNotFoundOrForbidden"
"409":
description: The workspace is not archived.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
$ref: "#/components/responses/InternalServerError"
/api/v1/workspaces/{slug}/sync:
parameters:
- $ref: "#/components/parameters/WorkspaceSlug"
post:
tags: [Workspaces]
operationId: syncWorkspace
summary: Sync a workspace with its upstream
description: |
Fetches from the remote and fast-forwards the local integration branch
when possible. Ownership is **not** enforced: any authenticated caller
holding the scope may sync any workspace.
Preconditions: `status` is `active`, `clone_status` is `ready`,
`sync_mode` is not `disabled`, and `sync_status` is not `syncing`.
`sync_status` follows a strict state machine
(`idle -> syncing -> idle|error`, `error -> syncing -> idle`); a
deferred cleanup guarantees it never remains stuck at `syncing`.
### Response shape
The response is always the updated workspace object. For a
`carry_patch` workspace the request is delegated to the carry-patch
sync extension -- which fetches the `upstream` remote, detects patches
merged upstream, flags upstream force-pushes, and may enqueue a
rebuild -- and its four fields are merged into the workspace JSON as
additional top-level keys, per 16-REQ-5.1. The workspace row is
re-read after the extension runs, so `upstream_head_sha` and
`last_sync_at` reflect its writes.
A `standard` workspace takes the standard sync path and carries none
of the carry-patch fields (16-REQ-5.E4).
Merge detection for carry-patch workspaces is governed by the
`SQUASH_MERGE_DETECTION` workspace variable (`ancestry_only`,
`content_based`, or `both`, the default). A rebuild is enqueued when
the upstream ref advanced unless `AUTO_REBUILD_AFTER_SYNC` is
`"false"`; a duplicate enqueue is ignored silently and leaves
`rebuild_triggered` false.
x-required-scopes: ["workspaces:sync", "workspaces:write"]
x-scope-note: >-
The standard sync path requires `workspaces:sync` exclusively. The
carry-patch path also accepts `workspaces:write`.
parameters:
- name: reset_to_upstream
in: query
required: false
description: >-
Force-reset the local integration branch to the upstream HEAD,
ignoring ancestry. Used to recover from a diverged (force-pushed)
upstream. Standard workspaces only.
schema:
type: boolean
default: false
responses:
"200":
description: |
Sync completed. The body is the updated workspace; a carry-patch
workspace additionally carries `patches_merged`,
`rebuild_triggered`, `force_push_detected`, and -- when a rebuild
was enqueued -- `rebuild_job_id`.
content:
application/json:
schema:
oneOf:
- $ref: "#/components/schemas/Workspace"
- $ref: "#/components/schemas/CarryPatchSyncResponse"
examples:
standard:
summary: Standard workspace, fast-forwarded
value:
slug: my-project
status: active
clone_status: ready
sync_status: idle
workspace_mode: standard
head_sha: fedcba9876543210fedcba9876543210fedcba98
upstream_head_sha: fedcba9876543210fedcba9876543210fedcba98
last_sync_at: "2026-09-01T12:00:00Z"
carryPatch:
summary: Carry-patch workspace, rebuild enqueued
value:
slug: vendor-fork
status: active
clone_status: ready
sync_status: idle
workspace_mode: carry_patch
upstream_url: https://github.com/upstream/repo.git
integration_branch: deploy
upstream_head_sha: abc123def456abc123def456abc123def456abcd
last_sync_at: "2026-09-01T12:00:00Z"
patches_merged: ["feature/already-merged"]
rebuild_triggered: true
rebuild_job_id: d3b07384-d113-4ec5-8a4e-a12345678901
force_push_detected: false
"400":
description: The workspace is not active, the clone is not ready, or sync is disabled.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
description: The PAT lacks the `workspaces:sync` scope.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"404":
$ref: "#/components/responses/WorkspaceNotFound"
"409":
description: >-
A sync is already in progress, or the upstream history has diverged
(force-push detected) on a standard workspace, or another operation
holds the workspace lock (`error_type: workspace_busy`). A rebuild
holds the lock only during its fetch phase and final phase, not
while it applies patches.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
$ref: "#/components/responses/InternalServerError"
"502":
description: >-
Upstream fetch failed (network, authentication, or repository
error), or credential resolution failed. In origin mode, also
returned when the origin fetch fails (`error_type:
origin_fetch_failed`) or origin credentials cannot be resolved
(`failed to resolve origin credentials`).
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"504":
description: The request context was cancelled mid-sync.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/api/v1/workspaces/{slug}/reclone:
parameters:
- $ref: "#/components/parameters/WorkspaceSlug"
post:
tags: [Workspaces]
operationId: recloneWorkspace
summary: Nuclear recovery -- delete and re-clone the local repository
description: |
Runs the archive flow (attempting to push local commits upstream first;
a failed push is logged and does not abort), deletes
`<WORKSPACE_ROOT>/<slug>/`, resets `clone_status` to `pending`, clears
`sync_error` and `upstream_head_sha`, and enqueues a clone job. The
workspace `status` stays `active` throughout.
Ownership is not enforced.
x-required-scopes: ["workspaces:sync"]
responses:
"200":
description: Reclone accepted; the clone job is queued.
content:
application/json:
schema:
$ref: "#/components/schemas/Workspace"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
description: The PAT lacks the `workspaces:sync` scope.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"404":
$ref: "#/components/responses/WorkspaceNotFound"
"409":
description: >-
A clone is already in progress (`clone_status` is `pending` or
`cloning`), or another operation holds the workspace lock, or a
carry-patch rebuild is running (`error_type: workspace_busy`;
reclone is rejected for the whole rebuild).
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
description: Failed to delete the workspace directory, or the database update failed.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
# ---------------------------------------------------------------------------
# Patches
# ---------------------------------------------------------------------------
/api/v1/workspaces/{slug}/patches:
parameters:
- $ref: "#/components/parameters/WorkspaceSlug"
post:
tags: [Patches]
operationId: addPatches
summary: Add one or more patches
description: |
Accepts either a single patch object or a JSON array of them. A batch
is validated and inserted inside one transaction: if any entry fails,
nothing is inserted.
When `position` is omitted the patch is appended at `max(position) + 1`.
When it is supplied, existing patches at that position and beyond are
shifted down by one. Positions greater than `max + 1` are clamped to an
append.
Unless `skip_branch_check` is `true`, the server resolves the branch
in this order: (1) `local` — `refs/heads/<name>` exists in the trunk;
(2) `origin_tracking` — `refs/remotes/origin/<name>` exists and a
local branch is created at that commit; (3) `origin_fetch` — only
when `PATCH_BRANCH_SOURCE` is `origin`, the branch is fetched from
the fork and the local branch is created. Tags, bare SHAs and
remote-qualified names are not accepted. `PATCH_BRANCH_SOURCE` is read
once per request, so every element of a batch resolves in the same
mode.
A batch validates the syntax of every element first (name,
integration branch, position) and starts resolving branches only
after all elements pass.
Ownership is not enforced.
x-required-scopes: ["patches:write"]
requestBody:
required: true
content:
application/json:
schema:
oneOf:
- $ref: "#/components/schemas/AddPatchRequest"
- type: array
items:
$ref: "#/components/schemas/AddPatchRequest"
examples:
single:
summary: Single patch
value:
branch_name: feature/auth-headers
position: 2
upstream_pr_url: https://github.com/upstream/repo/pull/42
description: Add authentication headers to API
batch:
summary: Batch insert
value:
- { branch_name: feature/a, position: 1 }
- { branch_name: feature/b }
- { branch_name: feature/c, upstream_pr_url: "https://github.com/upstream/repo/pull/99" }
responses:
"200":
description: >-
`if_not_exists` was true and the branch is already registered; the
existing patch is returned.
content:
application/json:
schema:
$ref: "#/components/schemas/Patch"
"201":
description: The created patch, or the array of created patches for a batch request.
content:
application/json:
schema:
oneOf:
- $ref: "#/components/schemas/Patch"
- type: array
items:
$ref: "#/components/schemas/Patch"
"400":
description: >-
`branch_name` is absent or empty; `branch_name` equals the
workspace `integration_branch`; the workspace is missing, inactive,
or in `standard` mode; `position` is below 1; or the branch does
not exist in the repository or on origin and `skip_branch_check`
is false. In a batch the message is prefixed `patch[i]: `.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/MissingScope"
"409":
description: >-
The branch is already registered for this workspace (and
`if_not_exists` is false), or the batch contains a duplicate
`branch_name`, or another operation holds the workspace lock
(`error_type: workspace_busy`).
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
$ref: "#/components/responses/InternalServerError"
"502":
description: >-
Origin fetch failed (`error_type: origin_fetch_failed`). Returned
when `PATCH_BRANCH_SOURCE` is `origin` and the fetch from the fork
fails due to a network or authentication error, or when origin
credentials cannot be resolved.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
get:
tags: [Patches]
operationId: listPatches
summary: List patches
description: |
Returns every non-deleted patch ordered by ascending position.
Soft-deleted patches (status `deleted`) are excluded. Standard
(non-carry-patch) workspaces return an empty array rather than an
error. Ownership is not enforced.
x-required-scopes: ["patches:read", "patches:write"]
responses:
"200":
description: The patch list in position order.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Patch"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/MissingScope"
"404":
$ref: "#/components/responses/WorkspaceNotFound"
"500":
$ref: "#/components/responses/InternalServerError"
/api/v1/workspaces/{slug}/patches/{id}:
parameters:
- $ref: "#/components/parameters/WorkspaceSlug"
- $ref: "#/components/parameters/PatchId"
get:
tags: [Patches]
operationId: getPatch
summary: Get a single patch
description: |
Returns the patch record. When `refs/hub/replaced/<branch>` resolves
to a commit, the response also carries `replaced_sha` (the full
40-character SHA). The field is omitted when the ref does not exist,
the trunk is not on disk, no recovery hook is registered, or the
lookup fails. A soft-deleted patch is returned with its `deleted`
status.
x-required-scopes: ["patches:read", "patches:write"]
responses:
"200":
description: The patch.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/Patch"
- type: object
properties:
replaced_sha:
type: string
description: >
Full 40-character SHA of the backup ref
`refs/hub/replaced/<branch>`. Omitted when the ref
does not exist.
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/MissingScope"
"404":
$ref: "#/components/responses/PatchNotFound"
patch:
tags: [Patches]
operationId: updatePatch
summary: Update a patch
description: |
Only the supplied fields change. `branch_name` is immutable and is
silently ignored if present. Changing `position` reshuffles the other
patches so positions stay a contiguous 1-based sequence.
x-required-scopes: ["patches:write"]
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdatePatchRequest"
responses:
"200":
description: The updated patch.
content:
application/json:
schema:
$ref: "#/components/schemas/Patch"
"400":
description: >-
`status` is not one of the valid values, or `position` is below 1 or
above the total patch count.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/MissingScope"
"404":
$ref: "#/components/responses/PatchNotFound"
"500":
$ref: "#/components/responses/InternalServerError"
delete:
tags: [Patches]
operationId: removePatch
summary: Remove a patch
description: |
Deletes the patch row and compacts the remaining positions to a
contiguous 1-based sequence. Both steps run in a single transaction.
x-required-scopes: ["patches:write"]
responses:
"204":
description: Patch removed and positions compacted.
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/MissingScope"
"404":
$ref: "#/components/responses/PatchNotFound"
"500":
description: The delete-and-compact transaction failed; no partial state was persisted.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/api/v1/workspaces/{slug}/patches/{id}/restore:
parameters:
- $ref: "#/components/parameters/WorkspaceSlug"
- $ref: "#/components/parameters/PatchId"
post:
tags: [Patches]
operationId: restorePatch
summary: Restore a soft-deleted patch
description: |
Moves the patch from `deleted` back to `active`, clears `deleted_at`,
and appends it at `max(position) + 1`.
x-required-scopes: ["patches:write"]
responses:
"200":
description: The restored patch.
content:
application/json:
schema:
$ref: "#/components/schemas/Patch"
"400":
description: >-
The workspace is missing, inactive, or not in `carry_patch` mode, or
the patch is not in `deleted` status.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/MissingScope"
"404":
$ref: "#/components/responses/PatchNotFound"
"500":