-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathopenapi.yaml
More file actions
798 lines (756 loc) · 27.3 KB
/
Copy pathopenapi.yaml
File metadata and controls
798 lines (756 loc) · 27.3 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
openapi: 3.1.0
info:
title: Pagent API
version: 0.0.1
description: |
Hosted UI rendering for terminal-bound AI agents. The agent emits an
A2UI surface to this service, prints a short URL, and reads the user's
interactions back via this API.
Spec contents are opaque to the service. The `format` field on `POST /new`
selects how the spec is interpreted:
- `a2ui` (default) — `spec` is an A2UI v0.9 message array. Pages take user
input and produce a result.
- `html` — `spec` is a UTF-8 HTML string (up to 1,000,000 bytes).
Pages are view-only: they never transition past `open`, and
`POST /{id}/result` returns 400 `invalid_for_format`.
See https://github.com/blockful/pagent for the source.
## Page lifecycle
A page walks a 3-state machine: `open → submitted → received`.
- **open** — created, waiting for a browser submission.
- **submitted** — the browser has called `POST /{id}/result`. The agent
has not yet read the result.
- **received** — the agent called `GET /{id}/result` at least once after
submission. The state flip is idempotent; subsequent reads keep returning
`received`.
contact:
name: Pagent maintainers
url: https://github.com/blockful/pagent
license:
name: MIT
url: https://github.com/blockful/pagent/blob/main/LICENSE
servers:
- url: https://api.pagent.link
description: Production
- url: http://localhost:8787
description: Local development
paths:
/health:
get:
operationId: getHealth
tags: [Operations]
summary: Liveness probe with DB connectivity check
description: |
Returns 200 when the service and its Postgres connection are healthy.
Returns 503 when Postgres is unreachable. Railway polls this endpoint
and restarts the container on 503.
This endpoint is **not** part of the versioned API contract — it is
an ops endpoint and may change without notice.
responses:
'200':
description: Service and database are healthy.
headers:
X-Request-ID:
$ref: '#/components/headers/XRequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/HealthOk'
example:
ok: true
db: ok
'503':
description: |
Postgres is unreachable. Check the database connection string and
the Postgres service status before retrying.
headers:
X-Request-ID:
$ref: '#/components/headers/XRequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/HealthError'
example:
ok: false
db: error
message: Database connection failed
/openapi.json:
get:
operationId: getOpenapiJson
tags: [Operations]
summary: OpenAPI 3.1 spec (JSON — canonical machine-readable form)
description: |
Returns the canonical OpenAPI 3.1 document as `application/json`.
Parsed once at boot from `docs/openapi.yaml` and cached in memory.
This is the URL that tooling (Postman, Insomnia, OpenAPI Generator,
Stoplight) expects to fetch. Returns 503 if the source file was
missing at boot.
responses:
'200':
description: The OpenAPI document as JSON.
headers:
X-Request-ID:
$ref: '#/components/headers/XRequestId'
content:
application/json:
schema:
type: object
'503':
description: |
`docs/openapi.yaml` was not found at boot. Ensure the file is
present in the repository and deployed alongside the API.
headers:
X-Request-ID:
$ref: '#/components/headers/XRequestId'
content:
application/json:
schema:
type: object
properties:
error:
type: string
enum: [openapi_unavailable]
/openapi.yaml:
get:
operationId: getOpenapiYaml
tags: [Operations]
summary: OpenAPI 3.1 spec (YAML — human-readable courtesy copy)
description: |
Returns the same OpenAPI 3.1 document as `application/yaml`.
Served raw from memory (no re-serialisation). Useful for humans
who prefer YAML. Tooling should prefer `/openapi.json`.
Returns 503 if the source file was missing at boot.
responses:
'200':
description: The OpenAPI document as YAML.
headers:
X-Request-ID:
$ref: '#/components/headers/XRequestId'
content:
application/yaml:
schema:
type: string
'503':
description: |
`docs/openapi.yaml` was not found at boot. Ensure the file is
present in the repository and deployed alongside the API.
headers:
X-Request-ID:
$ref: '#/components/headers/XRequestId'
content:
application/json:
schema:
type: object
properties:
error:
type: string
enum: [openapi_unavailable]
/docs:
get:
operationId: getApiDocs
tags: [Operations]
summary: Interactive Scalar API Reference
description: |
Returns an HTML page rendering the Pagent API Reference via Scalar.
Scalar fetches `/openapi.json` client-side and renders an interactive
explorer — no build step required. Opens directly in a browser.
responses:
'200':
description: The Scalar API Reference HTML page.
headers:
X-Request-ID:
$ref: '#/components/headers/XRequestId'
content:
text/html:
schema:
type: string
/new:
post:
operationId: createPage
tags: [Pages]
summary: Create a new page
description: |
Creates a new page from a spec and returns a short URL to share
with the user. The page starts in state `open`.
The `format` field (default `a2ui`) selects the spec shape:
- `a2ui` — `spec` is an A2UI v0.9 message array. The service stores
and serves it back verbatim. Pages collect user input and produce a
result.
- `html` — `spec` is a UTF-8 HTML string (up to 1,000,000 bytes). The
service sanitises it with DOMPurify before storage. Pages are
view-only: they never transition past `open`, and
`POST /{id}/result` returns 400 `invalid_for_format`.
**What to do next:** print the returned `url` in the terminal (or chat)
so the user can open it in a browser. For A2UI pages, poll
`GET /{id}/result` until `state` is no longer `open`. For HTML pages,
there is nothing to poll for — once the user opens the URL, you are
done.
Rate-limited to 30 requests per minute per source IP.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NewPageRequest'
example:
spec:
- createSurface:
surfaceId: main
catalogId: https://a2ui.org/specification/v0_9/basic_catalog.json
- updateComponents:
surfaceId: main
components: []
responses:
'201':
description: Page created. Open the returned URL in a browser.
headers:
X-Request-ID:
$ref: '#/components/headers/XRequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/NewPageResponse'
'400':
description: |
Request body is malformed or missing the `spec` field.
Fix the request before retrying.
headers:
X-Request-ID:
$ref: '#/components/headers/XRequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'413':
description: |
Request body exceeds the service limit. A2UI specs cap at
256 000 bytes (post-parse); HTML payloads cap at 1 000 000 bytes
(wire body limit). The response's `format` field tells you which
cap fired so you can size the next submission appropriately.
headers:
X-Request-ID:
$ref: '#/components/headers/XRequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/Error413'
example:
error: payload_too_large
format: a2ui
max_bytes: 256000
message: A2UI spec exceeds the 256 KB limit; use format "html" for larger payloads only when appropriate
'429':
description: |
Per-IP rate limit exceeded. Wait `retry_after_seconds` seconds
before retrying. The `Retry-After` response header carries the
same value in seconds (RFC 7231).
headers:
X-Request-ID:
$ref: '#/components/headers/XRequestId'
Retry-After:
$ref: '#/components/headers/RetryAfter'
content:
application/json:
schema:
$ref: '#/components/schemas/Error429'
example:
error: rate_limited
retry_after_seconds: 60
'500':
description: |
Unexpected server error. Quote the `request_id` from the response
body (or the `X-Request-ID` header — they are identical) in any
bug report. Do not retry in a tight loop; this is unlikely to
self-heal without a deployment.
headers:
X-Request-ID:
$ref: '#/components/headers/XRequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
/{id}:
parameters:
- $ref: '#/components/parameters/PageId'
get:
operationId: getPage
tags: [Pages]
summary: Get page spec and current state
description: |
Returns the full page record including the original `spec`, the
current `state`, the `result` (null until submitted), and when the
page expires.
**What to do next:**
- If `state` is `open`, the user has not yet submitted. Show them the
URL or wait.
- If `state` is `submitted`, the result is ready — read it via
`GET /{id}/result`.
- If `state` is `received`, you have already consumed the result.
- If `expires_at` is in the past, the page has been deleted and you
will get 404.
responses:
'200':
description: Page found.
headers:
X-Request-ID:
$ref: '#/components/headers/XRequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/PageResponse'
'404':
description: |
No active page with this ID exists. It may have expired (default
TTL 30 minutes) or never been created. Create a new page if you
need another.
headers:
X-Request-ID:
$ref: '#/components/headers/XRequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
'500':
description: Unexpected server error. See 500 on `POST /new` for guidance.
headers:
X-Request-ID:
$ref: '#/components/headers/XRequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
/{id}/result:
parameters:
- $ref: '#/components/parameters/PageId'
post:
operationId: submitResult
tags: [Pages]
summary: Submit a user action for a page (browser → service)
description: |
Called by the renderer (browser) when the user submits a form. The
body is an A2UI action object that the agent reads back via
`GET /{id}/result`.
Requires the page to be in state `open`. Calling this twice on the
same page returns 409.
**Note:** this endpoint is typically called by the Pagent renderer,
not directly by the agent. Agents should use `GET /{id}/result`
to read back the submitted action.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ResultRequest'
example:
name: submitted
surfaceId: main
sourceComponentId: submit-btn
context:
color: blue
timestamp: '2024-01-15T12:34:56.789Z'
responses:
'200':
description: Result recorded. The page is now in state `submitted`.
headers:
X-Request-ID:
$ref: '#/components/headers/XRequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/SubmitOk'
'400':
description: |
Request body is malformed or missing required fields (`name`,
`surfaceId`); error `bad_request`. Or the target page is HTML
(view-only and cannot accept a result); error `invalid_for_format`.
Fix the request — or, for `invalid_for_format`, stop trying to
submit to this page — before retrying.
headers:
X-Request-ID:
$ref: '#/components/headers/XRequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'404':
description: |
No active page with this ID. The page may have expired. Create
a new page.
headers:
X-Request-ID:
$ref: '#/components/headers/XRequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
'409':
description: |
The page was already submitted. Each page is single-shot — only
one result can be recorded per page. Create a new page if you
need another submission.
headers:
X-Request-ID:
$ref: '#/components/headers/XRequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/Error409'
'500':
description: Unexpected server error. See 500 on `POST /new` for guidance.
headers:
X-Request-ID:
$ref: '#/components/headers/XRequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
get:
operationId: getResult
tags: [Pages]
summary: Poll for a submitted result (agent → service)
description: |
Returns the current `state` and `result` for the page. Agents use
this to poll until the user has submitted the form.
**State transitions on read:**
- If `state` is `open`, the user has not yet submitted. Poll again
after a short back-off (e.g. 1 s, doubling to 30 s max).
- The **first** call after submission returns `state: "submitted"` and
the full result, then atomically flips the page to `received`. Use
the returned result immediately — this is your one chance to observe
the `submitted` state.
- Subsequent calls return `state: "received"` with the same result.
**What to do next:** parse the `result` object and continue the agent
conversation. `result` is null until the user submits.
responses:
'200':
description: State and result returned.
headers:
X-Request-ID:
$ref: '#/components/headers/XRequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/ResultResponse'
'404':
description: |
No active page with this ID. The page may have expired. Create
a new page.
headers:
X-Request-ID:
$ref: '#/components/headers/XRequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
'500':
description: Unexpected server error. See 500 on `POST /new` for guidance.
headers:
X-Request-ID:
$ref: '#/components/headers/XRequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
components:
parameters:
PageId:
name: id
in: path
required: true
description: 32-character lowercase hex page identifier.
schema:
$ref: '#/components/schemas/PageId'
headers:
XRequestId:
description: >
A unique identifier for this request (32-character lowercase hex or a
client-supplied value up to 128 characters). Quote it in bug reports —
the same ID appears on every log line for that request.
schema:
type: string
pattern: '^[A-Za-z0-9_-]{1,128}$'
RetryAfter:
description: Seconds to wait before the next attempt (RFC 7231).
schema:
type: integer
minimum: 1
schemas:
PageId:
type: string
pattern: '^[a-f0-9]{32}$'
description: 32-character lowercase hex identifier generated at page creation.
example: c0f2ec161aac8b1a8d26222f45ca812d
PageState:
type: string
enum: [open, submitted, received]
description: |
Lifecycle state of a page:
- `open` — awaiting user submission.
- `submitted` — user submitted; agent has not yet read the result.
- `received` — agent has consumed the result via `GET /{id}/result`.
Spec:
description: >
Page contents. Shape depends on the request's `format` field:
`a2ui` (default) — an array of A2UI v0.9 messages; the service stores
and returns this verbatim without parsing the structure.
`html` — a UTF-8 HTML string up to 1,000,000 bytes; the service runs
DOMPurify on the string at create time and stores the sanitised
result.
oneOf:
- type: array
description: A2UI v0.9 message array (when format is `a2ui`).
items:
type: object
additionalProperties: true
- type: string
description: HTML body, up to 1,000,000 bytes (when format is `html`).
example:
- createSurface:
surfaceId: main
catalogId: https://a2ui.org/specification/v0_9/basic_catalog.json
- updateComponents:
surfaceId: main
components: []
PageFormat:
type: string
enum: [a2ui, html]
description: |
Spec format. `a2ui` (default) — `spec` is an A2UI v0.9 message array.
`html` — `spec` is a UTF-8 HTML string up to 1,000,000 bytes; the
page becomes view-only and never produces a result.
NewPageRequest:
type: object
required: [spec]
properties:
spec:
$ref: '#/components/schemas/Spec'
format:
type: string
enum: [a2ui, html]
default: a2ui
description: |
Spec format. `a2ui` (default) — `spec` is an A2UI v0.9 message array.
`html` — `spec` is a UTF-8 HTML string up to 1,000,000 bytes; the
page becomes view-only and never produces a result.
NewPageResponse:
type: object
required: [id, url, expires_at]
properties:
id:
$ref: '#/components/schemas/PageId'
url:
type: string
format: uri
description: >
Short URL for the user to open in a browser. Points to the
Pagent renderer with this page's ID.
example: https://pagent.link/c0f2ec161aac8b1a8d26222f45ca812d
expires_at:
type: integer
description: Unix epoch milliseconds when this page will be deleted.
example: 1709122000000
PageResponse:
type: object
required: [spec, state, result, expires_at]
properties:
spec:
$ref: '#/components/schemas/Spec'
format:
$ref: '#/components/schemas/PageFormat'
state:
$ref: '#/components/schemas/PageState'
result:
type: object
nullable: true
description: >
The submitted A2UI action, or null if the page is still open or
if the page is an HTML page (HTML pages never produce a result).
Shape mirrors `ResultRequest`.
expires_at:
type: integer
description: Unix epoch milliseconds when this page will be deleted.
example: 1709122000000
ResultRequest:
type: object
required: [name, surfaceId]
additionalProperties: true
description: >
An A2UI action object. Extra fields beyond those listed here are
preserved verbatim (`.passthrough()` behaviour).
properties:
name:
type: string
minLength: 1
description: Event name emitted by the A2UI component.
example: submitted
surfaceId:
type: string
minLength: 1
description: ID of the surface that emitted the action.
example: main
sourceComponentId:
type: string
description: ID of the component that triggered the action.
example: submit-btn
context:
type: object
additionalProperties: true
description: Arbitrary key/value pairs captured from the form state.
example:
color: blue
timestamp:
type: string
format: date-time
description: ISO 8601 timestamp of when the action was triggered.
example: '2024-01-15T12:34:56.789Z'
ResultResponse:
type: object
required: [state, result]
properties:
state:
$ref: '#/components/schemas/PageState'
format:
$ref: '#/components/schemas/PageFormat'
result:
type: object
nullable: true
description: >
The submitted A2UI action, or null if the page is still `open`.
HTML pages always return null here — they never produce a result.
SubmitOk:
type: object
required: [ok]
properties:
ok:
type: boolean
enum: [true]
HealthOk:
type: object
required: [ok, db]
properties:
ok:
type: boolean
enum: [true]
db:
type: string
enum: [ok]
HealthError:
type: object
required: [ok, db, message]
properties:
ok:
type: boolean
enum: [false]
db:
type: string
enum: [error]
message:
type: string
description: Human-readable description of the failure.
example: Database connection failed
Error400:
type: object
required: [error, message]
properties:
error:
type: string
enum: [bad_request, invalid_for_format, sanitized_empty]
description: |
`bad_request` — body is malformed or fails schema validation.
`invalid_for_format` — operation is not supported for this page's
format (e.g. POSTing a result to an HTML page).
`sanitized_empty` — `POST /new` with `format: "html"` whose payload
was reduced to empty by the sanitizer (only forbidden tags like
`<script>` or `<iframe>` were supplied). Re-author the HTML with
safe markup and inline styles.
format:
type: string
enum: [a2ui, html]
description: |
Present on `invalid_for_format` and `sanitized_empty` errors —
identifies which format triggered the rejection.
message:
type: string
description: Human-readable description of why the request was rejected.
example: Request body did not match the expected schema
issues:
type: array
description: Zod validation issues describing what failed.
items:
type: object
additionalProperties: true
Error404:
type: object
required: [error, message]
properties:
error:
type: string
enum: [not_found]
message:
type: string
description: Human-readable description of what was not found.
example: Page not found or expired
Error409:
type: object
required: [error, message]
properties:
error:
type: string
enum: [conflict]
message:
type: string
description: Human-readable description of the conflict.
example: Page was already submitted; create a new page if you need another submission
Error413:
type: object
required: [error, max_bytes, message]
properties:
error:
type: string
enum: [payload_too_large]
format:
type: string
enum: [a2ui, html]
description: |
The format whose size cap was exceeded. `a2ui` caps at 256 000 bytes
(enforced post-parse). `html` caps at 1 000 000 bytes (enforced by
the body-limit middleware before parse).
max_bytes:
type: integer
description: Maximum allowed body size in bytes for this format.
example: 256000
message:
type: string
description: Human-readable description of the size limit.
example: A2UI spec exceeds the 256 KB limit; use format "html" for larger payloads only when appropriate
Error429:
type: object
required: [error, retry_after_seconds, message]
properties:
error:
type: string
enum: [rate_limited]
retry_after_seconds:
type: integer
description: Seconds to wait before retrying.
example: 60
message:
type: string
description: Human-readable rate limit message including the retry delay.
example: Too many requests; retry after 60 seconds
Error500:
type: object
required: [error, request_id, message]
properties:
error:
type: string
enum: [internal_error]
request_id:
type: string
description: >
The `X-Request-ID` for this request. Quote it in bug reports so
maintainers can locate the matching log lines and traces.
example: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
message:
type: string
description: Human-readable description of the error. Always safe to show to users.
example: An unexpected error occurred; quote the request_id when reporting this