-
Notifications
You must be signed in to change notification settings - Fork 175
Expand file tree
/
Copy pathbox.openapi.yaml
More file actions
2528 lines (2388 loc) · 85.1 KB
/
Copy pathbox.openapi.yaml
File metadata and controls
2528 lines (2388 loc) · 85.1 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
# Copyright 2024-2026 BoxLite Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
---
openapi: 3.1.1
info:
title: BoxLite Box API
version: 0.1.0
description: |
HTTP API for BoxLite boxes — hardware-isolated VMs for secure code
execution. Exposes box lifecycle management, command execution, file
transfer, service tunnels, and image management as a multi-tenant
service.
## Design Principles
This specification follows industry-standard HTTP API patterns:
- Versioned URL prefix (`/v1/`) for graceful API evolution
- Multi-tenancy via an opaque `{prefix}` URL segment
- Capability discovery via `GET /v1/config`
- Optional idempotency keys for safe mutation retries
- Opaque page-token pagination
- Standard error model with typed error codes
## Vendor-agnostic routing — the `{prefix}` slot
Box-scoped endpoints include a `{prefix}` URL path segment whose
semantics are **deployment-defined**. This contract is
intentionally silent on what the slot means — a vendor may use it
for an organization id, a workspace, a warehouse / catalog, a
region+team pair, or any other routing scope. Clients **MUST**
treat the value as an opaque substring and substitute it verbatim
into the URL, including any internal `/` characters (multi-segment
values are supported).
Clients discover the value to use via `Principal.path_prefix`
from `GET /v1/me`. A `null` (or empty) value means the deployment
does **not** use a routing slot; clients then build URLs without
the segment (`/v1/boxes/…` instead of `/v1/<prefix>/boxes/…`).
This is the canonical shape for single-tenant deployments such as
the `boxlite serve` local server.
Identity / discovery endpoints (`GET /v1/me`, `GET /v1/config`)
sit at the version root and **never** include the `{prefix}`
segment, by spec.
## Authentication
All endpoints (except `GET /v1/config`) require an opaque token in the
`Authorization: Bearer` header (RFC 6750). The server is **bearer-
format-agnostic** — any token its validation pipeline can verify is
accepted. See the `BearerAuth` security scheme for the supported token
sources, which include BoxLite-issued API keys, plus (configurable per
deployment) federated SSO JWTs, third-party OIDC tokens, and customer-
issued gateway tokens.
Token acquisition is out of scope for this contract: dashboard API
keys are issued by the BoxLite dashboard; other token sources are
configured at the customer's gateway or IdP layer.
Validate a freshly-acquired token via `GET /v1/me`.
## Execution Model
Command execution is two-step:
1. `POST /exec` creates the execution and returns an `execution_id`.
2. `GET /executions/{exec_id}/attach` upgrades to a bidirectional
WebSocket carrying stdin, stdout, stderr, and in-band control
(resize, signal, exit). One attach at a time per exec; the exec
continues running across WebSocket close so a client can reattach.
Out-of-band control endpoints exist for clients that prefer a
request/response style: `GET /executions/{exec_id}` (status),
`POST .../resize`, `POST .../signal`, `DELETE /executions/{exec_id}`
(kill).
license:
name: Apache-2.0
url: https://www.apache.org/licenses/LICENSE-2.0
contact:
name: BoxLite
url: https://github.com/boxlite-ai/boxlite
servers:
- url: "{scheme}://{host}{basePath}/v1"
description: BoxLite Box API v1
variables:
scheme:
default: https
enum: [https, http]
host:
default: api.boxlite.ai
basePath:
description: |
Deployment-defined hostpath prefix. Empty for local servers
(`boxlite serve` at `http://localhost:8100/v1/…`); typically
`/api` for cloud deployments (`https://api.boxlite.ai/api/v1/…`).
default: ""
# ---------------------------------------------------------------------------
# Security
# ---------------------------------------------------------------------------
security:
- BearerAuth: []
tags:
- name: Configuration
description: Server capability discovery
- name: Authentication
description: Credential validation and identity lookup
- name: Boxes
description: Box lifecycle management
- name: Execution
description: Command execution and streaming
- name: Files
description: File upload and download
- name: Network
description: Box networking and service access
- name: Metrics
description: Runtime and per-box metrics
- name: Images
description: Container image management
- name: Volumes
description: Managed persistent volumes
# ===========================================================================
# Paths
# ===========================================================================
paths:
# -------------------------------------------------------------------------
# Configuration & Discovery
# -------------------------------------------------------------------------
/config:
get:
operationId: getConfig
summary: Get server configuration and capabilities
description: |
Returns server defaults, enforced limits, and supported features.
Clients should call this on startup to adapt behavior.
No authentication required.
tags: [Configuration]
security: []
responses:
"200":
description: Server configuration
content:
application/json:
schema:
$ref: "#/components/schemas/ServerConfig"
# -------------------------------------------------------------------------
# Authentication
# -------------------------------------------------------------------------
/me:
get:
operationId: getCurrentPrincipal
summary: Identity and scopes for the calling credential
description: |
Returns the principal (user or service account) and granted scopes
for the Bearer credential in the request. Typical uses: validating
a freshly-issued credential, and rendering an identity / scopes
banner for the current session.
tags: [Authentication]
responses:
"200":
description: Authenticated principal
content:
application/json:
schema:
$ref: "#/components/schemas/Principal"
"401":
$ref: "#/components/responses/UnauthorizedError"
# -------------------------------------------------------------------------
# Volumes
# -------------------------------------------------------------------------
/{prefix}/volumes:
parameters:
- $ref: "#/components/parameters/prefix"
post:
operationId: createVolume
summary: Create a managed persistent volume
description: Creates a volume and waits until its backing object storage is ready.
tags: [Volumes]
requestBody:
required: false
content:
application/json:
schema:
$ref: "#/components/schemas/CreateVolumeRequest"
responses:
"201":
description: Volume created and ready for use
content:
application/json:
schema:
$ref: "#/components/schemas/Volume"
"400":
$ref: "#/components/responses/BadRequestError"
"408":
$ref: "#/components/responses/RequestTimeoutError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
get:
operationId: listVolumes
summary: List managed persistent volumes
tags: [Volumes]
responses:
"200":
description: List of volumes
content:
application/json:
schema:
$ref: "#/components/schemas/ListVolumesResponse"
/{prefix}/volumes/{volume_id}:
parameters:
- $ref: "#/components/parameters/prefix"
- $ref: "#/components/parameters/volumeId"
get:
operationId: getVolume
summary: Get managed persistent volume metadata
tags: [Volumes]
responses:
"200":
description: Volume metadata
content:
application/json:
schema:
$ref: "#/components/schemas/Volume"
"404":
$ref: "#/components/responses/NotFoundError"
delete:
operationId: removeVolume
summary: Remove a managed persistent volume
tags: [Volumes]
parameters:
- name: force
in: query
description: Treat an already deleting or missing volume as successfully removed
schema:
type: boolean
default: false
responses:
"204":
description: Volume removal accepted
"400":
$ref: "#/components/responses/BadRequestError"
"404":
$ref: "#/components/responses/NotFoundError"
# -------------------------------------------------------------------------
# Boxes
# -------------------------------------------------------------------------
/{prefix}/boxes:
parameters:
- $ref: "#/components/parameters/prefix"
post:
operationId: createBox
summary: Create a new box
description: |
Creates a new box with the specified configuration.
The box starts in `configured` status. Call `POST /start` to
initialize the VM, or it will start lazily on first `exec`.
tags: [Boxes]
parameters:
- $ref: "#/components/parameters/idempotencyKey"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateBoxRequest"
responses:
"201":
description: Box created
headers:
Location:
description: URL of the created box
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/Box"
"400":
$ref: "#/components/responses/BadRequestError"
"409":
$ref: "#/components/responses/ConflictError"
"422":
$ref: "#/components/responses/UnprocessableEntityError"
"429":
$ref: "#/components/responses/ResourceExhaustedError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
get:
operationId: listBoxes
summary: List all boxes
description: |
Returns a paginated list of boxes in the workspace.
Optionally filter by status or name.
tags: [Boxes]
parameters:
- $ref: "#/components/parameters/pageSize"
- $ref: "#/components/parameters/pageToken"
- name: status
in: query
description: Filter by box status
schema:
$ref: "#/components/schemas/BoxStatus"
responses:
"200":
description: List of boxes
content:
application/json:
schema:
$ref: "#/components/schemas/ListBoxesResponse"
/{prefix}/boxes/{box_id}:
parameters:
- $ref: "#/components/parameters/prefix"
- $ref: "#/components/parameters/boxId"
get:
operationId: getBox
summary: Get box details
description: Returns full metadata for a box. Accepts box ID (ULID) or name.
tags: [Boxes]
responses:
"200":
description: Box details
content:
application/json:
schema:
$ref: "#/components/schemas/Box"
"404":
$ref: "#/components/responses/NotFoundError"
head:
operationId: boxExists
summary: Check if a box exists
description: Returns 204 if the box exists, 404 if not. No response body.
tags: [Boxes]
responses:
"204":
description: Box exists
"404":
description: Box not found
delete:
operationId: removeBox
summary: Remove a box
description: |
Removes a box and all its associated data (disk, config, state).
Use `?force=true` to force-remove a running box.
tags: [Boxes]
parameters:
- $ref: "#/components/parameters/idempotencyKey"
- name: force
in: query
description: Force remove even if the box is running
schema:
type: boolean
default: false
responses:
"204":
description: Box removed
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
# -------------------------------------------------------------------------
# Box Lifecycle
# -------------------------------------------------------------------------
/{prefix}/boxes/{box_id}/start:
parameters:
- $ref: "#/components/parameters/prefix"
- $ref: "#/components/parameters/boxId"
post:
operationId: startBox
summary: Start a box (initialize VM)
description: |
Initializes the VM for a box. Idempotent: if already running, returns
current state. Transitions: configured → running, stopped → running.
tags: [Boxes]
parameters:
- $ref: "#/components/parameters/idempotencyKey"
responses:
"200":
description: Box started (or already running)
content:
application/json:
schema:
$ref: "#/components/schemas/Box"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
/{prefix}/boxes/{box_id}/stop:
parameters:
- $ref: "#/components/parameters/prefix"
- $ref: "#/components/parameters/boxId"
post:
operationId: stopBox
summary: Stop a box (terminate VM)
description: |
Gracefully stops the VM. The box transitions to `stopped` status.
AutoDelete may later delete the stopped box according to its configured interval.
tags: [Boxes]
parameters:
- $ref: "#/components/parameters/idempotencyKey"
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/StopBoxRequest"
responses:
"200":
description: Box stopped
content:
application/json:
schema:
$ref: "#/components/schemas/Box"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
# -------------------------------------------------------------------------
# Snapshot / Portability
# -------------------------------------------------------------------------
/{prefix}/boxes/{box_id}/snapshots:
parameters:
- $ref: "#/components/parameters/prefix"
- $ref: "#/components/parameters/boxId"
post:
operationId: createSnapshot
summary: Create a snapshot
description: |
Creates a named snapshot from the box's current stopped disk state.
The box must be stopped.
tags: [Boxes]
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateSnapshotRequest"
responses:
"201":
description: Snapshot created
content:
application/json:
schema:
$ref: "#/components/schemas/Snapshot"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
get:
operationId: listSnapshots
summary: List box snapshots
tags: [Boxes]
responses:
"200":
description: Snapshot list
content:
application/json:
schema:
$ref: "#/components/schemas/ListSnapshotsResponse"
"404":
$ref: "#/components/responses/NotFoundError"
/{prefix}/boxes/{box_id}/snapshots/{snapshot_name}:
parameters:
- $ref: "#/components/parameters/prefix"
- $ref: "#/components/parameters/boxId"
- name: snapshot_name
in: path
required: true
schema:
type: string
get:
operationId: getSnapshot
summary: Get snapshot details
tags: [Boxes]
responses:
"200":
description: Snapshot details
content:
application/json:
schema:
$ref: "#/components/schemas/Snapshot"
"404":
$ref: "#/components/responses/NotFoundError"
delete:
operationId: removeSnapshot
summary: Remove a snapshot
tags: [Boxes]
responses:
"204":
description: Snapshot removed
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
/{prefix}/boxes/{box_id}/snapshots/{snapshot_name}/restore:
parameters:
- $ref: "#/components/parameters/prefix"
- $ref: "#/components/parameters/boxId"
- name: snapshot_name
in: path
required: true
schema:
type: string
post:
operationId: restoreSnapshot
summary: Restore box from snapshot
description: Restores the box's stopped disks from a named snapshot.
tags: [Boxes]
responses:
"204":
description: Snapshot restored
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
/{prefix}/boxes/{box_id}/clone:
parameters:
- $ref: "#/components/parameters/prefix"
- $ref: "#/components/parameters/boxId"
post:
operationId: cloneBox
summary: Clone a box
description: |
Creates a new box cloned from the source box's current stopped disk state.
The source box must be stopped.
tags: [Boxes]
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CloneBoxRequest"
responses:
"201":
description: Cloned box created
content:
application/json:
schema:
$ref: "#/components/schemas/Box"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
/{prefix}/boxes/{box_id}/export:
parameters:
- $ref: "#/components/parameters/prefix"
- $ref: "#/components/parameters/boxId"
post:
operationId: exportBox
summary: Export a box archive
description: Exports a stopped box as a portable archive payload.
tags: [Boxes]
requestBody:
required: false
content:
application/json:
schema:
$ref: "#/components/schemas/ExportBoxRequest"
responses:
"200":
description: Archive payload
content:
application/octet-stream:
schema:
type: string
format: binary
/{prefix}/boxes/import:
parameters:
- $ref: "#/components/parameters/prefix"
post:
operationId: importBox
summary: Import a box from an archive
description: |
Imports a `.boxlite` archive (produced by the export endpoint) to create
a new box. The archive is uploaded as a binary payload and treated as
untrusted: archived security settings are replaced by the server's
defaults. Archives selecting server host paths through a local rootfs or
volume mount are rejected, as are archives requiring host-only
experimental features such as custom kernels or nested virtualization.
tags: [Boxes]
parameters:
- name: name
in: query
description: Name for the imported box (auto-generated if omitted)
schema:
type: string
requestBody:
required: true
content:
application/octet-stream:
schema:
type: string
format: binary
responses:
"201":
description: Imported box created
content:
application/json:
schema:
$ref: "#/components/schemas/Box"
"400":
$ref: "#/components/responses/BadRequestError"
# -------------------------------------------------------------------------
# Execution
# -------------------------------------------------------------------------
/{prefix}/boxes/{box_id}/exec:
parameters:
- $ref: "#/components/parameters/prefix"
- $ref: "#/components/parameters/boxId"
post:
operationId: startExecution
summary: Start an asynchronous command execution
description: |
Starts a command in the box and returns an execution ID immediately.
The box is automatically started if not already running.
Use the returned `execution_id` to:
- Attach stdio: `GET /executions/{exec_id}/attach` (WebSocket)
- Check status: `GET /executions/{exec_id}`
- Send signal: `POST /executions/{exec_id}/signal`
- Resize PTY: `POST /executions/{exec_id}/resize`
- Kill: `DELETE /executions/{exec_id}`
tags: [Execution]
parameters:
- $ref: "#/components/parameters/idempotencyKey"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ExecRequest"
responses:
"201":
description: Execution started
headers:
Location:
description: URL of the execution resource
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/ExecResponse"
"400":
$ref: "#/components/responses/BadRequestError"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
/{prefix}/boxes/{box_id}/executions/{exec_id}:
parameters:
- $ref: "#/components/parameters/prefix"
- $ref: "#/components/parameters/boxId"
- $ref: "#/components/parameters/execId"
get:
operationId: getExecution
summary: Get execution status
description: Returns the current status and result of an execution.
tags: [Execution]
responses:
"200":
description: Execution status
content:
application/json:
schema:
$ref: "#/components/schemas/ExecutionInfo"
"404":
$ref: "#/components/responses/NotFoundError"
delete:
operationId: killExecution
summary: Kill execution (SIGKILL + evict)
description: |
Terminates the execution with SIGKILL and removes it from the
server's session map atomically. Use this rather than
`POST /signal` with `signal=9` — the signal endpoint rejects
SIGKILL because it cannot perform the eviction half of the
operation.
On success, any attached WebSocket on `/attach` is closed; the
client observes an `{"type":"exit","exit_code":...}` text frame
followed by a normal close.
tags: [Execution]
responses:
"204":
description: Execution killed and evicted.
"404":
$ref: "#/components/responses/NotFoundError"
/{prefix}/boxes/{box_id}/attach:
parameters:
- $ref: "#/components/parameters/prefix"
- $ref: "#/components/parameters/boxId"
get:
operationId: attachBox
summary: Attach to the box's main command session (WebSocket)
description: |
Opens a WebSocket on the box's **main command session** — the container
init, which is what `run IMAGE COMMAND` starts. Docker's
`POST /containers/{id}/attach`, as distinct from its exec-attach.
The session is opened lazily on the first attach. Its execution id rides
back on the upgrade response in the `x-boxlite-execution-id` header,
after which the client can address it through the ordinary
`/executions/{exec_id}/…` routes.
The frame protocol, single-attach rule and keepalive are identical to
[`attachExecution`](#operation/attachExecution).
tags: [Execution]
parameters:
- name: stdin
in: query
required: false
schema:
type: string
enum: ["0", "1", "false", "true"]
default: "1"
description: |
Identical to `attachExecution`'s `stdin`: `0` or `false` opens a
**read-only attach** — docker's `--no-stdin` — and a value outside
this enum is rejected with `400`, never treated as the writable
default.
responses:
"101":
description: WebSocket upgrade successful; bidirectional protocol begins.
headers:
x-boxlite-execution-id:
description: |
The main command session's execution id, assigned when the
session is opened. Address it through the ordinary
`/executions/{exec_id}/…` routes.
schema:
type: string
"400":
$ref: "#/components/responses/BadRequestError"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
description: Another client is already attached to this box's main session.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/{prefix}/boxes/{box_id}/executions/{exec_id}/attach:
parameters:
- $ref: "#/components/parameters/prefix"
- $ref: "#/components/parameters/boxId"
- $ref: "#/components/parameters/execId"
get:
operationId: attachExecution
summary: Attach to an execution (WebSocket)
description: |
Opens a bidirectional WebSocket carrying stdin, stdout, stderr, and
in-band control for a previously-created execution. This is the
canonical streaming path for an execution.
**Single-attach.** Only one client may be attached at a time. A
second `GET /attach` to the same `exec_id` while one is connected
returns `409 Conflict` (no upgrade). The exec keeps running across
WebSocket close — a client can reconnect to the same `exec_id` and
resume streaming. Orphaned sessions are reaped per the runner's
configured policy (default: SIGHUP after 5 min idle).
**Frame protocol.**
Client → Server:
- **Binary frame** — raw bytes appended to the execution's stdin pipe.
- **Text frame** (JSON) — control messages:
- `{"type":"resize","cols":120,"rows":40}` — terminal resize.
- `{"type":"signal","sig":15}` — send a Unix signal. Whitelist
matches `POST /signal` (HUP/INT/QUIT/ABRT/USR1/USR2/TERM/WINCH);
non-whitelisted signals respond with a `{"type":"error",...}`
text frame and the connection stays open.
- `{"type":"stdin_eof"}` — close stdin (EOF to the PTY).
Server → Client:
- **Binary frame** — `[channel:u8][payload:bytes]`. Channel is
`0x01` for stdout, `0x02` for stderr. TTY-mode executions only
emit `0x01` since the kernel merges stdout/stderr into the PTY.
- **Text frame** (JSON) — control messages:
- `{"type":"exit","exit_code":N}` on process exit. Followed by a
normal WebSocket close (1000).
- `{"type":"error","message":"..."}` on non-fatal internal errors
(e.g., rejected signal value). Connection stays open.
**Keepalive.** The server emits a WebSocket `Ping` control frame
every 15 seconds. Clients are expected to respond with `Pong`
(most libraries do this automatically). Either side may treat the
absence of frames for >45s as a dead connection.
tags: [Execution]
parameters:
- name: stdin
in: query
required: false
schema:
type: string
enum: ["0", "1", "false", "true"]
default: "1"
description: |
`0` or `false` opens a **read-only attach** — docker's
`--no-stdin`. The server then refuses stdin data frames,
`stdin_eof`, `signal` and `resize` on this socket, answering the
first of each kind with an
`{"type":"error","code":"read_only_attach",...}` text frame and
ignoring repeats of a kind already refused; a stdin data frame
additionally closes the connection with WebSocket status 1008.
Read-only is a property of the individual WebSocket, not of the
session: other clients attaching to the same session are
unaffected.
A value outside this enum is rejected with `400`, never treated as
the writable default.
responses:
"101":
description: WebSocket upgrade successful; bidirectional protocol begins.
"400":
$ref: "#/components/responses/BadRequestError"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
description: Another client is already attached to this execution.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/{prefix}/boxes/{box_id}/executions/{exec_id}/signal:
parameters:
- $ref: "#/components/parameters/prefix"
- $ref: "#/components/parameters/boxId"
- $ref: "#/components/parameters/execId"
post:
operationId: signalExecution
summary: Send a signal to an execution
description: |
Sends a POSIX signal to the running execution.
Common signals: 9 (SIGKILL), 15 (SIGTERM), 2 (SIGINT).
tags: [Execution]
parameters:
- $ref: "#/components/parameters/idempotencyKey"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/SignalRequest"
responses:
"204":
description: Signal sent
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
/{prefix}/boxes/{box_id}/executions/{exec_id}/resize:
parameters:
- $ref: "#/components/parameters/prefix"
- $ref: "#/components/parameters/boxId"
- $ref: "#/components/parameters/execId"
post:
operationId: resizeExecution
summary: Resize execution TTY
description: |
Resizes the pseudo-terminal for a TTY-enabled execution.
Only applicable to executions started with `tty: true`.
tags: [Execution]
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ResizeRequest"
responses:
"204":
description: TTY resized
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
# -------------------------------------------------------------------------
# Files
# -------------------------------------------------------------------------
/{prefix}/boxes/{box_id}/files:
parameters:
- $ref: "#/components/parameters/prefix"
- $ref: "#/components/parameters/boxId"
put:
operationId: uploadFiles
summary: Upload files into the box
description: |
Uploads a tar archive and extracts it at the specified path
inside the container. The box must be running.
Transfer operates on the container's root filesystem. A destination
at or under a mount inside the container, or an archive entry that
would land on one, is rejected with `400` — such a write would not be
visible to processes in the box. Extracted files are owned by the
container's configured user.
tags: [Files]
parameters:
- name: path
in: query
required: true
description: Destination path inside the container
schema:
type: string
example: /app
- name: overwrite
in: query
description: Overwrite existing files at destination
schema:
type: boolean
default: true
requestBody:
required: true
content:
application/x-tar:
schema:
type: string
format: binary
description: Tar archive to extract at destination
responses:
"204":
description: Upload successful
"400":
$ref: "#/components/responses/BadRequestError"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
get:
operationId: downloadFiles
summary: Download files from the box
description: |
Downloads files from the specified path inside the container
as a tar archive. The box must be running.
Transfer operates on the container's root filesystem. A source at or
under a mount inside the container, or a directory containing one, is
rejected with `400` — the archive would carry the underlying files
rather than the ones processes in the box see.
tags: [Files]
parameters:
- name: path
in: query
required: true
description: Source path inside the container
schema:
type: string
example: /app/output
- name: follow_symlinks
in: query
description: Follow symlinks when archiving
schema:
type: boolean
default: false
responses:
"200":
description: Tar archive of requested path