Repository navigation
Expand file tree
/
Copy pathopenapi.yaml
More file actions
1296 lines (1196 loc) · 52.9 KB
/
Copy pathopenapi.yaml
File metadata and controls
1296 lines (1196 loc) · 52.9 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.0
info:
title: Bindu Gateway API
version: "1.0.0"
summary: External HTTP surface of the Bindu Gateway — a task-first orchestrator that plans over a caller-supplied catalog of A2A agents.
description: |
# Bindu Gateway API
The **Bindu Gateway** sits between an external system (your app, a custom
frontend, another service) and one or more **Bindu A2A agents**. It takes
a user question + an agent catalog and returns a streaming plan: the
gateway's planner LLM decomposes the request, invokes A2A agents via the
polling protocol, and emits Server-Sent Events in real time.
Distinct from the per-agent **Bindu Agent API** (see the repo-root
`openapi.yaml`), which describes what a single `bindufy()`-built agent
exposes. This spec documents the **gateway** — the orchestrator sitting
one layer up.
---
## Mental model: one endpoint, many turns
Every orchestration goes through `POST /plan`. Inside, the planner LLM
runs an agentic loop — it calls A2A agents as tools, the results feed
back into the LLM, and the loop continues up to `max_steps` or until the
plan resolves.
Two auxiliary endpoints support health probing and DID-based peer
authentication:
| Path | Purpose |
|---|---|
| `POST /plan` | Open a new plan or resume an existing session. Streams SSE. |
| `GET /health` | Liveness + cheap config probe. |
| `GET /.well-known/did.json` | The gateway's own DID document (only when a DID identity is configured via env). |
---
## Request shape
A `/plan` request carries three things:
1. **`question`** — the user's natural-language input.
2. **`agents[]`** — the catalog of A2A peers the planner may call, each
with an endpoint, authentication descriptor, and list of skills.
The gateway does **not** host agents; the caller is always the
source of truth for "what can we reach."
3. **`preferences`** and **`session_id`** (both optional) — caps and
continuation handles.
The shape is stable and additive; unknown top-level keys are accepted
(forward-compatible `.passthrough()`), but `preferences` keys are strict
snake_case. Clients sending camelCase preferences will have them
silently dropped — match the schema below.
---
## Response shape — Server-Sent Events
The happy path returns `200 OK` with `Content-Type: text/event-stream`.
Errors surface in three ways depending on when they occur:
- **Before streaming starts** (auth failure, invalid JSON, malformed
request, session creation failure): `401`/`400`/`500` with a JSON
`{ error, detail? }` body.
- **During streaming** (planner or tool failure): a single
`event: error` SSE frame, followed by `event: done`.
- **Never silent** — every successful plan closes with `event: done`
(empty payload). Consumers should treat the absence of `done` as
an incomplete stream.
SSE events emitted during a plan, in typical order:
| Event | When | Purpose |
|---|---|---|
| `session` | Once, before the plan starts | Carries session identifiers so clients can correlate. |
| `plan` | Once, when the planner starts its first turn | Announces plan_id. |
| `text.delta` | Many (streaming planner output) | Incremental text chunks for the final assistant message. |
| `task.started` | Per A2A tool call | The planner decided to call a peer agent. |
| `task.artifact` | Per A2A tool call | The peer returned an artifact, wrapped in a `<remote_content>` envelope. |
| `task.finished` | Per A2A tool call | Terminal state of the peer call. |
| `compaction-summary` | Zero or one per call (mid-stream) | New compaction summary produced when history overflowed the context window. Client must persist and ship back as `prior_summary` on the next `/plan` call. |
| `final` | Once, at the end | Stop reason + usage counters. |
| `error` | Only on failure during streaming | Human-readable message. |
| `done` | Always last | Empty marker so clients can close cleanly. |
---
## Recipes (internal)
The gateway supports **progressive-disclosure recipes** — markdown
playbooks the planner lazy-loads when a task matches (e.g.,
"multi-agent research", "payment-required flow"). Recipes are operator-
authored and not part of this HTTP API surface: they live in
`gateway/recipes/` and are injected automatically into the planner's
system prompt as metadata, with the body fetched on demand via an
internal `load_recipe` tool.
You cannot upload, list, or invoke recipes via the HTTP API; they
influence the planner's behavior transparently. See the gateway README
§Recipes for authoring details.
---
## A2A protocol pass-through
The gateway speaks A2A (JSON-RPC 2.0 over HTTP) to every peer in
`agents[]` — `message/send` + `tasks/get` polling, with DID signature
verification when configured. A2A task states (`submitted`, `working`,
`input-required`, `auth-required`, `payment-required`, `completed`,
`failed`, `canceled`) flow through to the planner; terminal states
become `task.finished` events, non-terminal states can surface as
planner text or trigger recipe-based handling (e.g., surfacing a
`payment-required` URL to the user).
See the Bindu Agent API spec (`openapi.yaml` at the repo root) for the
full A2A protocol surface.
contact:
name: Bindu Team
url: https://docs.getbindu.com/
license:
name: Apache-2.0
servers:
- url: http://localhost:3774
description: Local development (default port)
- url: https://gateway.example.com
description: Production deployment (replace with your host)
tags:
- name: Plan
description: |
Open a new plan or resume an existing session. Server-Sent Events
stream back the planner's turn-by-turn output, tool calls, and
final answer.
- name: Health
description: Liveness and basic configuration probes.
- name: Identity
description: |
The gateway's self-published DID document, for A2A peers that
need to verify `did_signed` outbound calls. Only exposed when
the gateway has a DID identity configured via env.
paths:
/plan:
post:
tags: [Plan]
operationId: postPlan
summary: Open a plan; stream SSE of the orchestration.
description: |
Accepts a user question + agent catalog, starts (or resumes) a
session, and streams Server-Sent Events as the planner runs.
### Session continuation (stateless model — Path A)
The gateway no longer owns durability. Each `/plan` call is its
own ephemeral session: pass prior turns in `history` and the
latest compaction summary (if you have one) in `prior_summary`.
The client is the canonical record; the gateway is pure compute.
`session_id` is now just a correlation tag echoed back on the
first SSE frame — it doesn't index a server-side store. The
durable conversation lives in your application (in the Bindu
reference frontend, that's comms's SQLite events log).
### Compaction-summary sidechannel
When the planner compacts overflowing history, it emits an
`event: compaction-summary` SSE frame mid-stream. Clients
should persist the `summary` field locally and ship it back as
`prior_summary` on the next call so the planner keeps the
compacted context across requests.
### Catalog immutability per request
The `agents` catalog applies to a single `/plan` call. Each
request is independent — there's no first-plan / subsequent-
plan distinction in stateless mode.
### Streaming & abort
Closing the HTTP connection aborts the plan — in-flight A2A calls
receive an `AbortSignal` and the planner loop terminates. Clients
that want a partial result should buffer `text.delta` frames
client-side rather than relying on `final`.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/PlanRequest"
examples:
minimal:
summary: Simplest possible plan (no agents)
value:
question: "What's the capital of France?"
singleAgent:
summary: One agent with one skill, no auth
value:
question: "Find 3 recent papers on LLM evaluation."
agents:
- name: "research"
endpoint: "http://localhost:3773"
auth: { type: "none" }
skills:
- id: "search"
description: "Web search."
multiAgentDIDSigned:
summary: Two agents, DID-signed auth, session continuation
value:
session_id: "client-session-42"
question: "Compare AWS and GCP pricing for a 5-node Kubernetes cluster; then summarize for a non-technical audience."
agents:
- name: "pricing"
endpoint: "https://pricing.example.com"
auth: { type: "did_signed" }
trust:
verifyDID: true
pinnedDID: "did:bindu:pricing-agent-key-1"
skills:
- id: "compare"
description: "Compare cloud pricing."
inputSchema:
type: object
properties:
provider_a: { type: "string" }
provider_b: { type: "string" }
workload: { type: "string" }
required: [provider_a, provider_b, workload]
- name: "summarizer"
endpoint: "https://summarize.example.com"
auth:
type: "bearer_env"
envVar: "SUMMARIZER_TOKEN"
skills:
- id: "summarize"
description: "Summarize text for a target audience."
preferences:
max_steps: 8
timeout_ms: 60000
responses:
"200":
description: |
SSE stream of the plan. Each event is one of the types
documented under `SSEEvent` below. The stream closes after
`event: done`.
content:
text/event-stream:
schema:
$ref: "#/components/schemas/SSEStream"
examples:
happyPath:
summary: Plan with one tool call and a final answer
value: |
event: session
data: {"session_id":"s_01H...","external_session_id":"client-session-42","created":true}
event: plan
data: {"plan_id":"m_01H...","session_id":"s_01H..."}
event: task.started
data: {"task_id":"call_01H...","agent":"research","agent_did":null,"skill":"search","input":{"input":"Find 3 recent papers on LLM evaluation."}}
event: task.artifact
data: {"task_id":"call_01H...","agent":"research","agent_did":null,"content":"<remote_content agent=\"research\" verified=\"unknown\">Paper A ...\nPaper B ...\nPaper C ...</remote_content>","title":"@research/search"}
event: task.finished
data: {"task_id":"call_01H...","agent":"research","agent_did":null,"state":"completed"}
event: text.delta
data: {"session_id":"s_01H...","part_id":"p_01H...","delta":"Here are three recent papers on LLM evaluation:\n\n"}
event: final
data: {"session_id":"s_01H...","stop_reason":"stop","usage":{"inputTokens":1820,"outputTokens":312,"totalTokens":2132,"cachedInputTokens":0}}
event: done
data: {}
"400":
description: |
Malformed JSON, missing required fields, schema validation
failure, or a catalog that would produce colliding tool ids
(two entries whose `<agent>_<skill>` combination normalizes
to the same value — silently swallowed before this guard,
which let one peer mask another).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
examples:
missingField:
summary: Schema validation failure
value:
error: "invalid_request"
detail: "question: Required; question must be a non-empty string"
collidingToolIds:
summary: Two catalog entries produce the same normalized tool id
value:
error: "invalid_request"
detail: 'agents catalog has colliding tool ids — toolId "call_research_search" produced by: research/search, research/search'
"401":
description: Missing or invalid bearer token.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
error: "unauthorized"
"500":
description: |
Internal error before the SSE stream opens — e.g. planner
misconfiguration or an exception during request handling.
Once streaming starts, errors surface as `event: error` on
the stream instead. The `session_failed` shape from the
stateful era is gone; the stateless gateway has no
session-creation step that can fail at the DB layer.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
error: "internal_error"
detail: "planner: model unavailable"
/health:
get:
tags: [Health]
operationId: getHealth
summary: Liveness and basic configuration probe.
description: |
Unauthenticated, cheap, returns immediately. Does NOT verify
downstream connectivity (Supabase, OpenRouter, Hydra) — it only
reports whether the gateway process has booted with the expected
config. Use this for container liveness checks; for readiness
probes that include downstream health, build a higher-level
check.
security: []
responses:
"200":
description: |
Gateway is up. Response body describes the process — version,
identity, configured planner model, recipe count, uptime. The
200 status is informational, not a health gate: read `status`
and `ready` in the body to distinguish healthy from degraded.
content:
application/json:
schema:
$ref: "#/components/schemas/HealthResponse"
example:
version: "0.1.0"
health: "healthy"
runtime:
storage_backend: "stateless"
bus_backend: "EffectPubSub"
planner:
model: "openrouter/anthropic/claude-sonnet-4.6"
provider: "openrouter"
model_id: "anthropic/claude-sonnet-4.6"
temperature: 0.3
top_p: null
max_steps: 10
recipe_count: 2
did_signing_enabled: true
hydra_integrated: true
application:
name: "@bindu/gateway"
session_mode: "stateless"
gateway_did: "did:bindu:ops_at_example_com:gateway:f72ba681-f873-324c-6012-23c4d5b72451"
gateway_id: "f72ba681-f873-324c-6012-23c4d5b72451"
author: "ops_at_example_com"
system:
node_version: "v22.22.1"
platform: "darwin"
architecture: "arm64"
environment: "development"
status: "ok"
ready: true
uptime_seconds: 2.4
/.well-known/did.json:
get:
tags: [Identity]
operationId: getDidDocument
summary: The gateway's self-published DID document.
description: |
Returns a W3C DID Core v1-compatible document with the gateway's
Ed25519 public key under `authentication[]`. A2A peers that
accept `did_signed` requests fetch this to verify the gateway's
outbound signatures.
**Availability:** only registered when the gateway has a DID
identity configured via env — `BINDU_GATEWAY_DID_SEED`,
`BINDU_GATEWAY_AUTHOR`, and `BINDU_GATEWAY_NAME` all set. When no
identity is loaded this endpoint returns 404.
**Caching:** the gateway's DID is stable across process lifetime
(env-driven); responses carry `Cache-Control: public, max-age=300`
as a defense against bad caches that would otherwise hold the key
indefinitely.
**Content-Type:** `application/did+json` per W3C DID Core, not
plain `application/json`. Some DID resolvers enforce the media
type.
**Auth:** none. Well-known endpoints are public by spec — the
whole point is that any peer can resolve the DID without
credentials.
security: []
responses:
"200":
description: DID document for the configured gateway identity.
headers:
Cache-Control:
schema:
type: string
example: "public, max-age=300"
content:
application/did+json:
schema:
$ref: "#/components/schemas/GatewayDidDocument"
example:
"@context":
- "https://www.w3.org/ns/did/v1"
- "https://getbindu.com/ns/v1"
id: "did:bindu:gateway-prod-key-1"
authentication:
- id: "did:bindu:gateway-prod-key-1#key-1"
type: "Ed25519VerificationKey2020"
controller: "did:bindu:gateway-prod-key-1"
publicKeyBase58: "6MkjQ2r..."
"404":
description: No DID identity configured on this gateway instance.
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: opaque
description: |
Shared-secret bearer token(s) configured via `config.gateway.auth.tokens`.
Validated in constant time against a SHA-256 hash of each configured
token, so neither timing nor length leaks which token matched. Set
`gateway.auth.mode: "none"` in config to disable bearer auth
(not recommended outside of localhost).
schemas:
# -----------------------------------------------------------------
# /plan request
# -----------------------------------------------------------------
PlanRequest:
type: object
additionalProperties: true
required: [question]
properties:
question:
type: string
minLength: 1
description: |
The user's natural-language question. Non-empty — an empty
string is rejected upstream because some LLM providers
(Anthropic) reject empty user messages with a 400 mid-stream,
surfacing as a vague "Provider returned error". Validating
here gives a clean 400 with `invalid_request` instead.
example: "Summarize the latest quarterly results for Apple."
agents:
type: array
default: []
description: |
Catalog of A2A peers the planner may call. Empty array =
planner runs with no tools (useful for questions the
configured planner LLM can answer on its own, e.g.,
general knowledge).
items:
$ref: "#/components/schemas/AgentRequest"
preferences:
$ref: "#/components/schemas/PlanPreferences"
session_id:
type: string
description: |
Opaque correlation tag the caller chooses. Echoed back on
the first SSE `session` frame as `external_session_id`.
In the stateless gateway this is NOT a resumption key —
the gateway has no persistent session store. Pass prior
turns explicitly via `history` (and optionally a
`prior_summary`) to give the planner context across
calls.
example: "client-session-42"
history:
type: array
description: |
Prior conversation for this session. The client (canonical
record) sends the most recent turns on every call so the
planner has context. Older turns that didn't fit into the
cap are preserved as the `prior_summary` field instead.
Order: oldest → newest. Omit (or send `[]`) for a brand-
new session.
Reference frontend (comms) caps this at 30 turns; bigger
payloads work but waste tokens.
items:
$ref: "#/components/schemas/HistoryTurn"
prior_summary:
type: string
description: |
Compaction summary the gateway emitted on a prior call,
persisted by the client. The planner sees it as a
synthetic user turn at the head of history: "[Prior
session context, compacted]\n\n...". Omit on first call.
example: "[Prior session context, compacted]\n\nUser asked about Apple Q3 earnings; planner ran research_agent.search then summarizer.summarize…"
HistoryTurn:
type: object
required: [role, parts]
description: |
One user/assistant turn from prior conversation. Minimal
shape — no metadata, no ids — because the gateway doesn't
persist these anymore; they're only used to populate the
planner's prompt for the current call.
properties:
role:
type: string
enum: [user, assistant]
parts:
type: array
minItems: 1
items:
type: object
required: [type, text]
properties:
type:
type: string
enum: [text]
text:
type: string
AgentRequest:
type: object
required: [name, endpoint]
properties:
name:
type: string
description: |
Display name of the peer. Used to derive the tool id exposed
to the planner LLM (`call_<name>_<skillId>`) and to correlate
SSE events back to the catalog entry. Operator-chosen and
potentially collision-prone — use `trust.pinnedDID` for a
cryptographically stable identifier.
example: "research"
endpoint:
type: string
format: uri
description: |
Absolute HTTP(S) URL where the peer's A2A endpoint is
reachable. The gateway POSTs JSON-RPC envelopes here for
`message/send` and `tasks/get`.
example: "http://localhost:3773"
auth:
$ref: "#/components/schemas/PeerAuth"
trust:
$ref: "#/components/schemas/PeerTrust"
skills:
type: array
default: []
description: |
Peer capabilities the planner may invoke. Each becomes one
dynamic tool scoped to this request. The gateway does NOT
discover skills from the peer's `AgentCard` — the caller
declares them, ensuring the planner sees only capabilities
the caller vouches for.
items:
$ref: "#/components/schemas/SkillRequest"
SkillRequest:
type: object
required: [id]
properties:
id:
type: string
description: |
The skill id the A2A peer recognizes. Passed back to the
peer inside `message/send` so it can route to the right
internal handler.
example: "search"
description:
type: string
description: |
Human-readable description. The planner LLM relies heavily
on this to decide whether to invoke the skill — write 3–4
sentences covering intent, inputs, outputs, and when to use
it. Descriptions under 120 chars are auto-padded server-side
with agent/skill context so the LLM still gets enough
signal.
example: "Search the open web and return a ranked list of passages."
inputSchema:
description: |
Optional JSON Schema for structured inputs. When present,
the planner LLM emits a JSON object matching this shape
and the gateway forwards it as the message text (serialized).
When omitted, the planner sends a plain-text `input` string.
type: object
additionalProperties: true
outputModes:
type: array
items:
type: string
description: |
Advisory list of output MIME-like hints the peer may return
(e.g., `text/plain`, `application/json`). Surfaced in the
tool description so the planner knows what to expect back.
example: ["text/plain", "application/json"]
tags:
type: array
items:
type: string
description: |
Free-form tags — helps the planner disambiguate when
multiple peers expose similarly-named skills.
example: ["research", "web"]
PeerAuth:
description: |
How the gateway authenticates its outbound calls to this peer.
Discriminated on `type`:
- `none` — anonymous; peer must accept unauthenticated calls.
- `bearer` — static token passed literally in `Authorization`.
Caller includes the secret in the request, so only use over TLS.
- `bearer_env` — gateway reads the token from the named env var.
Keeps secrets out of the wire; rotation = restart.
- `did_signed` — gateway signs the request body with its
configured Ed25519 identity and attaches an OAuth2 token. By
default uses the gateway's own auto-acquired Hydra token;
pass `tokenEnvVar` to use a per-peer federated token.
oneOf:
- $ref: "#/components/schemas/PeerAuth_None"
- $ref: "#/components/schemas/PeerAuth_Bearer"
- $ref: "#/components/schemas/PeerAuth_BearerEnv"
- $ref: "#/components/schemas/PeerAuth_DidSigned"
discriminator:
propertyName: type
mapping:
none: "#/components/schemas/PeerAuth_None"
bearer: "#/components/schemas/PeerAuth_Bearer"
bearer_env: "#/components/schemas/PeerAuth_BearerEnv"
did_signed: "#/components/schemas/PeerAuth_DidSigned"
PeerAuth_None:
type: object
required: [type]
properties:
type:
type: string
enum: [none]
PeerAuth_Bearer:
type: object
required: [type, token]
properties:
type:
type: string
enum: [bearer]
token:
type: string
description: "Literal bearer token to include in `Authorization: Bearer <token>`."
PeerAuth_BearerEnv:
type: object
required: [type, envVar]
properties:
type:
type: string
enum: [bearer_env]
envVar:
type: string
description: Name of the env var on the gateway process whose value is the bearer token.
example: "PEER_A_TOKEN"
PeerAuth_DidSigned:
type: object
required: [type]
properties:
type:
type: string
enum: [did_signed]
tokenEnvVar:
type: string
description: |
Optional. Env var name for a pre-acquired OAuth2 token to pair
with the DID signature. Omit to use the gateway's own Hydra
auto-acquired token (requires `BINDU_GATEWAY_HYDRA_*` env).
PeerTrust:
type: object
description: |
Per-peer trust policy. Both fields are optional; omitting both
means "trust the peer's identity at face value — don't verify."
properties:
verifyDID:
type: boolean
description: |
When true, the gateway verifies every Ed25519 signature on
artifacts returned by this peer. Mismatched signatures fail
the task. Requires a resolvable DID on the peer.
pinnedDID:
type: string
description: |
DID the peer is expected to present. Used both for
correlation (SSE `agent_did`) and, when `verifyDID` is true,
to reject responses signed by a different key.
example: "did:bindu:research-agent-key-1"
PlanPreferences:
type: object
additionalProperties: true
description: |
Caps and shaping hints. All keys are **snake_case**; an earlier
draft declared them camelCase, which caused docs-compliant clients
to silently lose the caps — the schema is now strict on casing
and unknown keys pass through via `additionalProperties: true`
for forward compatibility.
properties:
response_format:
type: string
description: |
Advisory hint for the planner's final-message format
(`"markdown"`, `"plain"`, `"json"`, etc.). Not enforced by
the gateway; the planner may honor or ignore it.
max_hops:
type: integer
minimum: 1
description: |
Maximum number of A2A hops (recursive peer-to-peer calls)
the gateway allows. Phase 2+ enforced; currently informational.
timeout_ms:
type: integer
minimum: 1000
maximum: 21600000
description: |
Overall wall-clock budget for the `/plan` call, in
milliseconds. Applies to the entire planner loop including
LLM calls, compaction, and every downstream peer call
combined. When the budget expires, in-flight peer polls
are aborted and a best-effort `tasks/cancel` is dispatched
to each peer; the gateway then returns
`BinduError(-32040, AbortedByCaller)` with
`data.reason = "deadline"`.
Default when unset: **1,800,000** ms (30 minutes).
Minimum: 1,000 ms. Maximum: 21,600,000 ms (6 hours).
Requests above the ceiling are rejected at the API
boundary as `invalid_request` — callers with genuine
multi-hour workloads set it explicitly.
example: 1800000
max_steps:
type: integer
minimum: 1
description: |
Maximum agentic loop steps. Overrides the planner agent's
default (`agent.steps`). A "step" is one LLM call — tool
calls inside a step don't count.
example: 8
# -----------------------------------------------------------------
# Responses
# -----------------------------------------------------------------
HealthResponse:
type: object
required: [version, health, runtime, application, system, status, ready, uptime_seconds]
description: |
Detailed gateway health payload. Shape aligned with the per-agent
Bindu health (the one a `bindufy()`-built agent returns), adapted
for the coordinator role: `gateway_id`/`gateway_did` replace the
agent-side `penguin_id`/`agent_did`, and `runtime` reports
gateway-specific knobs (planner model, recipe count, DID-signing
status) instead of the agent's task-manager fields.
properties:
version:
type: string
description: Gateway package version, from gateway/package.json.
example: "0.1.0"
health:
type: string
enum: [healthy, degraded, unhealthy]
description: |
Overall classification.
- `healthy`: every boot invariant satisfied, planner model resolves.
- `degraded`: non-critical subsystem missing (reserved — no current signals trigger this).
- `unhealthy`: a required invariant is broken (e.g. no planner model configured).
runtime:
$ref: "#/components/schemas/HealthRuntime"
application:
$ref: "#/components/schemas/HealthApplication"
system:
$ref: "#/components/schemas/HealthSystem"
status:
type: string
enum: [ok, error]
description: Two-state mirror of `health` — `ok` when healthy, `error` when unhealthy. Provided for operators that prefer binary.
ready:
type: boolean
description: Liveness gate. True when every boot invariant is satisfied. Use this for k8s readiness probes via a `jq` post-processor.
uptime_seconds:
type: number
description: Seconds since gateway process boot (float, 2 decimal places).
example: 23.3
HealthRuntime:
type: object
required: [storage_backend, bus_backend, planner, recipe_count, did_signing_enabled, hydra_integrated]
properties:
storage_backend:
type: string
enum: [stateless]
description: |
The gateway's persistence model. Always `stateless` since
the Path A migration — session state lives in memory for
the lifetime of each `/plan` call only; the calling client
owns durable history.
bus_backend:
type: string
description: Event bus driver. Today always `EffectPubSub` (in-process).
planner:
$ref: "#/components/schemas/HealthPlanner"
recipe_count:
type: integer
description: Number of recipes discovered at boot (union across all scanned directories, after permission filtering for the default agent).
example: 2
did_signing_enabled:
type: boolean
description: True when a gateway DID identity is loaded (env vars `BINDU_GATEWAY_DID_SEED` + friends all set). `did_signed` peers require this.
hydra_integrated:
type: boolean
description: True when a Hydra token provider was successfully wired at boot. `did_signed` peers without `tokenEnvVar` need this to auto-acquire tokens.
HealthPlanner:
type: object
required: [model, provider, model_id, temperature, top_p, max_steps]
description: |
The planner LLM configuration — what model drives the agentic loop
inside every `/plan` call. Sourced from `gateway/agents/planner.md`
frontmatter (or config.agent.planner overrides).
properties:
model:
type: [string, "null"]
description: Full provider-prefixed model id as configured. Null when no planner agent is configured.
example: "openrouter/anthropic/claude-sonnet-4.6"
provider:
type: [string, "null"]
description: Provider segment (bit before the first `/`). Today always `openrouter`.
example: "openrouter"
model_id:
type: [string, "null"]
description: Upstream model id the provider understands. For OpenRouter-proxied Anthropic this is `anthropic/claude-sonnet-4.6`.
example: "anthropic/claude-sonnet-4.6"
temperature:
type: [number, "null"]
description: Sampling temperature configured on the planner agent.
top_p:
type: [number, "null"]
description: Nucleus sampling top_p.
max_steps:
type: [integer, "null"]
description: Cap on agentic loop steps per plan. Null when no cap is set (the planner will run until natural completion or context overflow).
HealthApplication:
type: object
required: [name, session_mode, gateway_did, gateway_id, author]
properties:
name:
type: string
const: "@bindu/gateway"
session_mode:
type: string
enum: [stateless]
description: |
Session persistence model. Always `stateless` — the
`stateful` value (Supabase-backed) was removed in the
Path A migration. Clients pass prior turns via
`history` on each /plan call.
gateway_did:
type: [string, "null"]
description: The gateway's full DID, null when no identity is configured.
example: "did:bindu:ops_at_example_com:gateway:f72ba681-f873-324c-6012-23c4d5b72451"
gateway_id:
type: [string, "null"]
description: Short identifier — last segment of the DID (UUID-ish hash of the public key for `did:bindu`).
example: "f72ba681-f873-324c-6012-23c4d5b72451"
author:
type: [string, "null"]
description: Author segment from the DID. Null for non-Bindu DIDs or when no identity is configured.
example: "ops_at_example_com"
HealthSystem:
type: object
required: [node_version, platform, architecture, environment]
properties:
node_version:
type: string
description: Node.js runtime version.
example: "v22.22.1"
platform:
type: string
description: Underlying OS kernel identifier from `process.platform`.
example: "darwin"
architecture:
type: string
description: CPU architecture from `process.arch`.
example: "arm64"
environment:
type: string
description: Value of `NODE_ENV`, or `"development"` when unset.
example: "development"
GatewayDidDocument:
type: object
required: ["@context", id, authentication]
description: |
W3C DID Core v1 document describing the gateway's identity.
Deliberately omits `created` — the gateway's identity is env-
driven and stateless, so there's no persisted "first published"
moment to report (W3C DID Core has `created` as optional).
properties:
"@context":
type: array
items:
type: string
example:
- "https://www.w3.org/ns/did/v1"
- "https://getbindu.com/ns/v1"
id:
type: string
example: "did:bindu:gateway-prod-key-1"
authentication:
type: array
items:
$ref: "#/components/schemas/GatewayVerificationMethod"
GatewayVerificationMethod:
type: object
required: [id, type, controller, publicKeyBase58]
properties:
id:
type: string
example: "did:bindu:gateway-prod-key-1#key-1"
type:
type: string
enum: [Ed25519VerificationKey2020]
controller:
type: string
example: "did:bindu:gateway-prod-key-1"
publicKeyBase58:
type: string
description: Ed25519 public key, base58-encoded.
example: "6MkjQ2r..."
ErrorResponse:
type: object
required: [error]
properties:
error:
type: string
enum: [unauthorized, invalid_request, internal_error]
description: Machine-readable error code.
detail:
type: string
description: Human-readable explanation. Absent for `unauthorized` (don't leak whether a token matched any configured value).
# -----------------------------------------------------------------
# SSE stream — descriptive schemas
# -----------------------------------------------------------------
SSEStream:
type: string
description: |
The `text/event-stream` body is a sequence of `event:` / `data:`
pairs. Each `data:` value is a JSON object matching one of the
`SSEEvent_*` schemas below. OpenAPI doesn't model SSE natively;
`$ref` the per-event schemas to generate typed consumers.
SSEEvent_Session:
type: object
description: |
Emitted first, before the plan starts. Carries session identifiers
so clients can cache them for resume.
required: [session_id, external_session_id, created]
properties:
session_id:
type: string
description: Server-assigned internal session id. Stable across resumes.
example: "s_01H..."
external_session_id:
type: [string, "null"]
description: Echo of `session_id` from the request body, if provided.