@fleetless/contracts 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/artifacts/constants.json +30 -4
  3. package/artifacts/openapi.json +667 -98
  4. package/artifacts/routes.json +97 -6
  5. package/artifacts/schema/apply-error.schema.json +2 -1
  6. package/artifacts/schema/asset-list-response.schema.json +77 -12
  7. package/artifacts/schema/asset-sync-status.schema.json +35 -8
  8. package/artifacts/schema/asset.schema.json +2 -3
  9. package/artifacts/schema/assets-clear-response.schema.json +23 -0
  10. package/artifacts/schema/authorization-server-metadata.schema.json +1 -1
  11. package/artifacts/schema/bridge-asset-progress.schema.json +14 -8
  12. package/artifacts/schema/bridge-config-applied.schema.json +2 -1
  13. package/artifacts/schema/bridge-link-mode.schema.json +36 -0
  14. package/artifacts/schema/bridge-state.schema.json +6 -1
  15. package/artifacts/schema/client-robot-list-item.schema.json +6 -1
  16. package/artifacts/schema/client-robot-list-response.schema.json +6 -1
  17. package/artifacts/schema/cloud-config.schema.json +90 -5
  18. package/artifacts/schema/cloud-hello-ok.schema.json +45 -0
  19. package/artifacts/schema/cloud-ping.schema.json +27 -1
  20. package/artifacts/schema/config-draft-response.schema.json +90 -5
  21. package/artifacts/schema/config-state.schema.json +2 -1
  22. package/artifacts/schema/config-version-response.schema.json +90 -5
  23. package/artifacts/schema/datapoint-config.schema.json +5 -0
  24. package/artifacts/schema/datapoint-frame.schema.json +4 -0
  25. package/artifacts/schema/datapoint-list-response.schema.json +2 -2
  26. package/artifacts/schema/dynamic-client-registration-request.schema.json +1 -1
  27. package/artifacts/schema/dynamic-client-registration-response.schema.json +1 -1
  28. package/artifacts/schema/joint-state-put-request.schema.json +23 -0
  29. package/artifacts/schema/joint-state-put-response.schema.json +24 -0
  30. package/artifacts/schema/oauth-token-request.schema.json +79 -41
  31. package/artifacts/schema/oauth-token-response.schema.json +1 -1
  32. package/artifacts/schema/org-quota-usage-counts.schema.json +0 -5
  33. package/artifacts/schema/org-quota-usage.schema.json +1 -12
  34. package/artifacts/schema/org-quotas.schema.json +1 -7
  35. package/artifacts/schema/robot-config-doc.schema.json +90 -5
  36. package/artifacts/schema/robot-deletion-summary.schema.json +2 -1
  37. package/artifacts/schema/robot-detail-response.schema.json +63 -2
  38. package/artifacts/schema/robot-list-item.schema.json +15 -1
  39. package/artifacts/schema/robot-list-response.schema.json +15 -1
  40. package/artifacts/schema/robot-token-rotate-response.schema.json +15 -0
  41. package/artifacts/schema-outgoing/bridge-asset-progress.schema.json +14 -8
  42. package/artifacts/schema-outgoing/bridge-config-applied.schema.json +2 -1
  43. package/artifacts/schema-outgoing/bridge-link-mode.schema.json +37 -0
  44. package/artifacts/schema-outgoing/datapoint-frame.schema.json +4 -0
  45. package/dist/assets.d.ts +85 -50
  46. package/dist/assets.js +152 -62
  47. package/dist/audit.d.ts +1 -1
  48. package/dist/audit.js +1 -1
  49. package/dist/client-robots.d.ts +2 -0
  50. package/dist/common.d.ts +10 -0
  51. package/dist/common.js +16 -1
  52. package/dist/config.d.ts +69 -1
  53. package/dist/config.js +86 -6
  54. package/dist/errors.d.ts +1 -1
  55. package/dist/errors.js +1 -8
  56. package/dist/index.d.ts +10 -10
  57. package/dist/index.js +5 -5
  58. package/dist/oauth.d.ts +34 -19
  59. package/dist/oauth.js +39 -24
  60. package/dist/protocol.d.ts +150 -71
  61. package/dist/protocol.js +144 -87
  62. package/dist/rest.d.ts +137 -35
  63. package/dist/rest.js +98 -66
  64. package/dist/routes.js +68 -19
  65. package/package.json +1 -1
  66. package/artifacts/schema/bridge-pressure.schema.json +0 -292
@@ -3382,7 +3382,7 @@
3382
3382
  }
3383
3383
  }
3384
3384
  },
3385
- "description": "RFC 7591. **The request schema is what this endpoint accepts, not what it parses**: the handler reads the body field by field, because §3.2.2 distinguishes `invalid_redirect_uri` from `invalid_client_metadata` and one `safeParse` failure cannot say which of the two a caller earned. The shape is deliberately **not** strict, which is the schema agreeing with §3.1 rather than a gap in it — a conforming client sends `client_uri`, `logo_uri` and `software_id`, and both the schema and the server ignore them. `client_name` and `redirect_uris` are the two fields read; `grant_types`, `response_types` and `scope` are accepted and ignored. What comes back is what was actually granted, which §3.2.1 allows a server to substitute — this authorization server issues `authorization_code` only, so a client that asked for `refresh_token` is registered and told plainly that it did not get one. The registration carries a TTL. Refusals are `oauthError`; the rate limiter answers `apiError`.",
3385
+ "description": "RFC 7591. **The request schema is what this endpoint accepts, not what it parses**: the handler reads the body field by field, because §3.2.2 distinguishes `invalid_redirect_uri` from `invalid_client_metadata` and one `safeParse` failure cannot say which of the two a caller earned. The shape is deliberately **not** strict, which is the schema agreeing with §3.1 rather than a gap in it — a conforming client sends `client_uri`, `logo_uri` and `software_id`, and both the schema and the server ignore them. `client_name` and `redirect_uris` are the two fields read; `grant_types`, `response_types` and `scope` are accepted and ignored. What comes back is what was actually granted, which §3.2.1 allows a server to substitute — this authorization server grants `authorization_code` and `refresh_token` to every registration. The registration carries a TTL. Refusals are `oauthError`; the rate limiter answers `apiError`.",
3386
3386
  "requestBody": {
3387
3387
  "required": true,
3388
3388
  "content": {
@@ -3522,7 +3522,7 @@
3522
3522
  }
3523
3523
  }
3524
3524
  },
3525
- "description": "Only `authorization_code` is supported — there is no refresh grant here, so a session ends when its token expires and the client signs in again. Refusals are RFC 6749 §5.2's `oauthError`, so this route emits none of the codes in this reference. The response carries no `refresh_token`; the shape is the same `oauthTokenResponse` the app flow answers, whose refresh field is optional. The code is single-use, PKCE-verified, and its `resource` must match the audience it was authorized for.",
3525
+ "description": "`authorization_code` mints an `mcp_session` access token bound to the central resource and a refresh token; `refresh_token` rotates that pair, and the presented refresh token is consumed — a second presentation revokes the session, as on `/api/auth/refresh`. The refresh token lives ninety days from its last use and is bound to the `client_id` it was issued to. A refresh re-reads the Fleetless user, so a removed account cannot refresh. Refusals are RFC 6749 §5.2's `oauthError`, so this route emits none of the codes in this reference. The code is single-use, PKCE-verified, and its `resource` must match the audience it was authorized for; a `resource` on a refresh must match the session's audience, and is checked before the token is consumed.",
3526
3526
  "requestBody": {
3527
3527
  "required": true,
3528
3528
  "content": {
@@ -3814,7 +3814,7 @@
3814
3814
  }
3815
3815
  }
3816
3816
  },
3817
- "description": "RFC 7591, the same wire and the same handler as `POST /mcp/oauth/register` — one implementation, because a second answer to \"is this redirect URI acceptable\" would agree with the first only by luck. The request schema is what the endpoint accepts rather than what it parses, for the reason that row gives: §3.2.2 needs two distinguishable refusals and one `safeParse` failure offers one. `client_name` and `redirect_uris` are read; `grant_types`, `response_types` and `scope` are accepted and ignored, and what comes back is what was actually granted, which §3.2.1 allows — `authorization_code` only, so a client that asked for `refresh_token` is registered and told plainly that it did not get one. The registration carries a TTL. \n\n**The registration is scoped to this app.** A `client_id` minted here authorizes at this app's endpoint and nowhere else, so a client registered against one app cannot walk into another's authorize with it, and a developer who switches MCP off is not left with strangers' registrations valid somewhere adjacent. \n\nRefusals are `oauthError`; the rate limiter and `404 not_found` answer `apiError`. That `404` covers an unknown identifier **and** an app with the switch off, mirroring the two metadata documents this endpoint is discovered from — a client that could not read those has no business registering here, and giving it a third distinct answer would only tell it something the documents deliberately do not.",
3817
+ "description": "RFC 7591, the same wire and the same handler as `POST /mcp/oauth/register` — one implementation, because a second answer to \"is this redirect URI acceptable\" would agree with the first only by luck. The request schema is what the endpoint accepts rather than what it parses, for the reason that row gives: §3.2.2 needs two distinguishable refusals and one `safeParse` failure offers one. `client_name` and `redirect_uris` are read; `grant_types`, `response_types` and `scope` are accepted and ignored, and what comes back is what was actually granted, which §3.2.1 allows — this authorization server grants `authorization_code` and `refresh_token` to every registration. The registration carries a TTL. \n\n**The registration is scoped to this app.** A `client_id` minted here authorizes at this app's endpoint and nowhere else, so a client registered against one app cannot walk into another's authorize with it, and a developer who switches MCP off is not left with strangers' registrations valid somewhere adjacent. \n\nRefusals are `oauthError`; the rate limiter and `404 not_found` answer `apiError`. That `404` covers an unknown identifier **and** an app with the switch off, mirroring the two metadata documents this endpoint is discovered from — a client that could not read those has no business registering here, and giving it a third distinct answer would only tell it something the documents deliberately do not.",
3818
3818
  "requestBody": {
3819
3819
  "required": true,
3820
3820
  "content": {
@@ -3973,7 +3973,7 @@
3973
3973
  }
3974
3974
  }
3975
3975
  },
3976
- "description": "Only `authorization_code`, PKCE-verified and single-use. There is no refresh grant here either, so a session ends when its token expires and the client signs in again; the shape is the same `oauthTokenResponse` the central endpoint answers, whose refresh field is optional and stays empty. **The `aud` is this app's endpoint URL on the canonical public base**, and the code's `resource` must match it — that is the whole of what stops a token minted for one app being spent at another's endpoint. \n\n**Every refusal is RFC 6749 §5.2's `oauthError`, so this route emits none of the codes in this reference — including the ones about the app.** An unknown identifier and a switched-off app are `invalid_client` here, not the `404` and `403` the authorize route beside it answers. The difference is who reads the answer: authorize is walked by a browser and its refusal is read by a person, while this endpoint is called by a client's own code in the middle of a flow, and handing that code an envelope its OAuth library cannot parse turns a clean refusal into an unexplained crash.",
3976
+ "description": "`authorization_code`, PKCE-verified and single-use, and `refresh_token`, which rotates the pair the exchange minted; the refresh token lives ninety days from its last use, is bound to its client and to this app, and a refresh re-reads the app user's status and their standing consent to the client, so a block or a withdrawn consent ends the session at its next refresh at the latest. **The `aud` is this app's endpoint URL on the canonical public base**, and the code's `resource` must match it — that is the whole of what stops a token minted for one app being spent at another's endpoint. \n\n**Every refusal is RFC 6749 §5.2's `oauthError`, so this route emits none of the codes in this reference — including the ones about the app.** An unknown identifier and a switched-off app are `invalid_client` here, not the `404` and `403` the authorize route beside it answers. The difference is who reads the answer: authorize is walked by a browser and its refusal is read by a person, while this endpoint is called by a client's own code in the middle of a flow, and handing that code an envelope its OAuth library cannot parse turns a clean refusal into an unexplained crash.",
3977
3977
  "requestBody": {
3978
3978
  "required": true,
3979
3979
  "content": {
@@ -5022,6 +5022,112 @@
5022
5022
  }
5023
5023
  }
5024
5024
  },
5025
+ "/api/robots/{id}/token/rotate": {
5026
+ "post": {
5027
+ "operationId": "post_api_robots_id_token_rotate",
5028
+ "summary": "Mints a new bridge token for the robot and invalidates the old one.",
5029
+ "tags": [
5030
+ "robots"
5031
+ ],
5032
+ "security": [
5033
+ {
5034
+ "developerSession": []
5035
+ }
5036
+ ],
5037
+ "parameters": [
5038
+ {
5039
+ "name": "id",
5040
+ "in": "path",
5041
+ "required": true,
5042
+ "description": "The robot's uuid, as returned by `POST /api/robots` or listed by `GET /api/robots`.",
5043
+ "schema": {
5044
+ "type": "string"
5045
+ }
5046
+ }
5047
+ ],
5048
+ "responses": {
5049
+ "201": {
5050
+ "description": "Success.",
5051
+ "content": {
5052
+ "application/json": {
5053
+ "schema": {
5054
+ "$ref": "#/components/schemas/robot-token-rotate-response"
5055
+ }
5056
+ }
5057
+ }
5058
+ },
5059
+ "default": {
5060
+ "description": "An error envelope. Codes this route is known to answer: `unauthorized`, `token_expired`, `token_revoked`, `tier_required`, `invalid_uuid`, `not_found`.",
5061
+ "content": {
5062
+ "application/json": {
5063
+ "schema": {
5064
+ "$ref": "#/components/schemas/api-error"
5065
+ }
5066
+ }
5067
+ }
5068
+ }
5069
+ },
5070
+ "description": "Owner tier, behind the org-scoped lookup, so a developer-tier admin sees the `404` a stranger would for a robot outside their org rather than a tier refusal that confirms the id exists. `token` is the only moment the new secret exists outside the caller's hands — the cloud stores a hash — so a caller who loses it rotates again. Audited as `robot.token_rotated`, with no `details`: the one interesting value here is the token. \n\n**It stops the bridge that is connected right now.** The old secret is gone the instant the hash is replaced, so the cloud closes that socket with `CLOSE_TOKEN_ROTATED` rather than leaving a bridge speaking on a credential nothing would accept again. A bridge that does not know the code reconnects and is refused at hello as `invalid_token`, which is the honest answer and ends the same way. **The robot is offline until somebody puts the new token on it** — this is a deliberate interruption, not a background rekey, and a fleet cannot be rotated without a visit to each robot."
5071
+ }
5072
+ },
5073
+ "/api/robots/{id}/urdf/joint-state": {
5074
+ "put": {
5075
+ "operationId": "put_api_robots_id_urdf_joint_state",
5076
+ "summary": "Chooses the datapoint whose joint positions move the robot's URDF, or clears it.",
5077
+ "tags": [
5078
+ "robots"
5079
+ ],
5080
+ "security": [
5081
+ {
5082
+ "developerSession": []
5083
+ }
5084
+ ],
5085
+ "parameters": [
5086
+ {
5087
+ "name": "id",
5088
+ "in": "path",
5089
+ "required": true,
5090
+ "description": "The robot's uuid, as returned by `POST /api/robots` or listed by `GET /api/robots`.",
5091
+ "schema": {
5092
+ "type": "string"
5093
+ }
5094
+ }
5095
+ ],
5096
+ "responses": {
5097
+ "200": {
5098
+ "description": "Success.",
5099
+ "content": {
5100
+ "application/json": {
5101
+ "schema": {
5102
+ "$ref": "#/components/schemas/joint-state-put-response"
5103
+ }
5104
+ }
5105
+ }
5106
+ },
5107
+ "default": {
5108
+ "description": "An error envelope. Codes this route is known to answer: `unauthorized`, `token_expired`, `token_revoked`, `invalid_uuid`, `not_found`, `validation_error`.",
5109
+ "content": {
5110
+ "application/json": {
5111
+ "schema": {
5112
+ "$ref": "#/components/schemas/api-error"
5113
+ }
5114
+ }
5115
+ }
5116
+ }
5117
+ },
5118
+ "description": "**What qualifies**: a datapoint of the **published** configuration whose ROS type is `sensor_msgs/msg/JointState` and which carries no `field` — the whole message, because positions and names arrive together and a single extracted field is half of a pose. Anything else is a `validation_error` naming that rule rather than a stored mapping that renders a battery reading as a robot. `{ \"slug\": null }` clears it, which is why the field is required and nullable rather than optional. \n\n**The mapping cannot outlive what it points at.** Every successful publish re-checks it against the new document and clears it when it no longer qualifies, recording `robot.joint_state_cleared` with the version that did it; a slug rename rewrites it like every other reference the editor already rewrites; deleting the robot takes it along. Every write through this route — a slug or `null` — is on the record too, as `robot.joint_state_set` with the actor and the slug, so a clear a person made is never mistaken for one a publish made. The stored value reads back on `GET /api/robots/:id/assets` as `joint_state_slug`, so a renderer fetches the URDF, the meshes and the mapping from one place.",
5119
+ "requestBody": {
5120
+ "required": true,
5121
+ "content": {
5122
+ "application/json": {
5123
+ "schema": {
5124
+ "$ref": "#/components/schemas/joint-state-put-request"
5125
+ }
5126
+ }
5127
+ }
5128
+ }
5129
+ }
5130
+ },
5025
5131
  "/api/robots/{id}": {
5026
5132
  "get": {
5027
5133
  "operationId": "get_api_robots_id",
@@ -7282,6 +7388,58 @@
7282
7388
  }
7283
7389
  },
7284
7390
  "description": "Needs the `assets` capability, refused as `403 capability_required` rather than a bare `forbidden`: the code says a capability is missing and the message says which, so a developer who switched the wrong toggle on is told what to switch. The capability is checked before existence, so a denied robot and an absent one read alike to a caller with no right to tell them apart. `urdf` reports whether a URDF is present and which of its mesh references have no stored asset."
7391
+ },
7392
+ "delete": {
7393
+ "operationId": "delete_api_robots_id_assets",
7394
+ "summary": "Empties a robot's asset store: every URDF, mesh and texture, gone at once.",
7395
+ "tags": [
7396
+ "assets"
7397
+ ],
7398
+ "security": [
7399
+ {
7400
+ "developerSession": []
7401
+ },
7402
+ {
7403
+ "clientToken": []
7404
+ },
7405
+ {
7406
+ "serverKey": []
7407
+ }
7408
+ ],
7409
+ "parameters": [
7410
+ {
7411
+ "name": "id",
7412
+ "in": "path",
7413
+ "required": true,
7414
+ "description": "The robot's uuid, as returned by `POST /api/robots` or listed by `GET /api/robots`.",
7415
+ "schema": {
7416
+ "type": "string"
7417
+ }
7418
+ }
7419
+ ],
7420
+ "responses": {
7421
+ "200": {
7422
+ "description": "Success.",
7423
+ "content": {
7424
+ "application/json": {
7425
+ "schema": {
7426
+ "$ref": "#/components/schemas/assets-clear-response"
7427
+ }
7428
+ }
7429
+ }
7430
+ },
7431
+ "default": {
7432
+ "description": "An error envelope. Codes this route is known to answer: `unauthorized`, `token_expired`, `token_revoked`, `forbidden`, `tier_required`, `invalid_uuid`, `not_found`, `busy`.",
7433
+ "content": {
7434
+ "application/json": {
7435
+ "schema": {
7436
+ "$ref": "#/components/schemas/api-error"
7437
+ }
7438
+ }
7439
+ }
7440
+ }
7441
+ },
7442
+ "description": "The store's escape hatch: a full store is never a dead end, and this is the blunt third of the three answers to it — the URDF upload is exempt from the gate, reconcile after a sync already frees what the new URDF stopped referencing, and this route lets an Owner clear the robot outright. Owner tier, unconditionally, like starting a sync. Removes every asset of the robot and resets its store to `0`; the next sync fills it again. It does not touch the bridge's availability report — `urdf_available` still answers from the connected robot, unrelated to what this cloud happens to have stored. A clear while a sync is running is `409 busy` naming that sync's details, the same refusal starting a second sync gets, because deleting under a running upload would leave the store counter wrong."
7285
7443
  }
7286
7444
  },
7287
7445
  "/api/robots/{id}/assets/{assetId}": {
@@ -9244,10 +9402,9 @@
9244
9402
  "enum": [
9245
9403
  "urdf",
9246
9404
  "mesh",
9247
- "texture",
9248
- "other"
9405
+ "texture"
9249
9406
  ],
9250
- "description": "What the file is: the `urdf` itself, a `mesh` it references, a `texture` a mesh or the URDF paints with, or `other`. A renderer decides from this alone, before fetching anything, what to pre-fetch."
9407
+ "description": "What the file is: the `urdf` itself, a `mesh` it references, or a `texture` a mesh or the URDF paints with. A renderer decides from this alone, before fetching anything, what to pre-fetch."
9251
9408
  },
9252
9409
  "name": {
9253
9410
  "type": "string",
@@ -9348,32 +9505,38 @@
9348
9505
  "enum": [
9349
9506
  "unresolvable",
9350
9507
  "upload_failed",
9351
- "refused",
9352
- "too_large"
9508
+ "refused"
9353
9509
  ],
9354
- "description": "Why it failed. `unresolvable` means the reference names nothing the producer can find or may read, and is **permanent** — the only kind reconciliation may treat as gone. `upload_failed` means the bytes exist and the transfer did not succeed, `refused` means it was never attempted because a producer-side ceiling was hit, and `too_large` means it exceeds the upload limit and carries both numbers in `details`."
9510
+ "description": "Why it failed. `unresolvable` means the reference names nothing the producer can find or may read, and is **permanent** — the only kind reconciliation may treat as gone. `upload_failed` means the bytes exist and the transfer did not succeed, and `refused` means it was never attempted, either because the robot's asset store had no room — then `details` carries the three numbers — or because a producer-side ceiling was hit."
9355
9511
  },
9356
9512
  "details": {
9357
- "description": "The two numbers behind a `too_large` failure, and absent for every other kind — a forced `null` on every `unresolvable` entry buys nothing. The pairing is enforced, not merely described.",
9513
+ "description": "The three numbers behind a `refused` entry the robot's store had no room for, and absent for every other kind — a forced `null` on every `unresolvable` entry buys nothing. A `refused` entry may also carry no details: the producer's own ceiling is the other half of that kind, and no store number describes it.",
9358
9514
  "anyOf": [
9359
9515
  {
9360
9516
  "type": "object",
9361
9517
  "properties": {
9362
- "limit_bytes": {
9518
+ "store_bytes": {
9363
9519
  "type": "integer",
9364
9520
  "exclusiveMinimum": 0,
9365
9521
  "maximum": 9007199254740991,
9366
- "description": "The upload ceiling, in bytes."
9522
+ "description": "The robot's store, in bytes."
9523
+ },
9524
+ "used_bytes": {
9525
+ "type": "integer",
9526
+ "minimum": 0,
9527
+ "maximum": 9007199254740991,
9528
+ "description": "Bytes the robot's assets occupy before this upload."
9367
9529
  },
9368
9530
  "size_bytes": {
9369
9531
  "type": "integer",
9370
9532
  "exclusiveMinimum": 0,
9371
9533
  "maximum": 9007199254740991,
9372
- "description": "How large the refused file is, in bytes. With `limit_bytes` beside it a developer can tell whether to shrink the mesh or raise the limit; \"too large\" alone answers neither."
9534
+ "description": "The refused upload, in bytes."
9373
9535
  }
9374
9536
  },
9375
9537
  "required": [
9376
- "limit_bytes",
9538
+ "store_bytes",
9539
+ "used_bytes",
9377
9540
  "size_bytes"
9378
9541
  ],
9379
9542
  "additionalProperties": false
@@ -9404,6 +9567,25 @@
9404
9567
  ],
9405
9568
  "description": "Why the sync ended as it did, when that is not a per-reference fact. `null` when `failed` already says everything there is to say."
9406
9569
  },
9570
+ "stored": {
9571
+ "anyOf": [
9572
+ {
9573
+ "type": "integer",
9574
+ "minimum": 0,
9575
+ "maximum": 9007199254740991
9576
+ },
9577
+ {
9578
+ "type": "null"
9579
+ }
9580
+ ],
9581
+ "description": "How many of the announced files the cloud's store actually holds. Counted once, after the robot reports the sync done, and `null` until then — nobody has looked yet. Read it against `announced`: `state` is what the robot reported, this is what arrived."
9582
+ },
9583
+ "announced": {
9584
+ "type": "integer",
9585
+ "minimum": 0,
9586
+ "maximum": 9007199254740991,
9587
+ "description": "How many files the robot announced for this sync — the URDF, if it has one, plus every mesh URI its description references. `0` when the robot announced nothing, and also `0` until it has answered at all: read it beside `stored`, which stays `null` until the terminal frame."
9588
+ },
9407
9589
  "started_at": {
9408
9590
  "type": "string",
9409
9591
  "format": "date-time",
@@ -9425,6 +9607,8 @@
9425
9607
  "total",
9426
9608
  "failed",
9427
9609
  "reason",
9610
+ "stored",
9611
+ "announced",
9428
9612
  "started_at",
9429
9613
  "updated_at"
9430
9614
  ],
@@ -9496,13 +9680,52 @@
9496
9680
  }
9497
9681
  ],
9498
9682
  "description": "What the connected bridge says it *could* transfer — deliberately separate from what has been transferred. `null` when no bridge is connected, distinct from `false`: \"no robot is online to ask\" and \"the robot has no URDF\" send a developer to different places. After a publisher is killed rather than shut down this can read `true` for some seconds, on the underlying DDS liveliness timeout rather than on any check made here."
9683
+ },
9684
+ "store": {
9685
+ "type": "object",
9686
+ "properties": {
9687
+ "bytes": {
9688
+ "type": "integer",
9689
+ "exclusiveMinimum": 0,
9690
+ "maximum": 9007199254740991,
9691
+ "description": "The robot's asset store, `ROBOT_ASSET_STORE_BYTES`."
9692
+ },
9693
+ "used_bytes": {
9694
+ "type": "integer",
9695
+ "minimum": 0,
9696
+ "maximum": 9007199254740991,
9697
+ "description": "Bytes its assets occupy."
9698
+ }
9699
+ },
9700
+ "required": [
9701
+ "bytes",
9702
+ "used_bytes"
9703
+ ],
9704
+ "additionalProperties": false,
9705
+ "description": "How full this robot's store is."
9706
+ },
9707
+ "joint_state_slug": {
9708
+ "anyOf": [
9709
+ {
9710
+ "type": "string",
9711
+ "minLength": 2,
9712
+ "maxLength": 63,
9713
+ "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$"
9714
+ },
9715
+ {
9716
+ "type": "null"
9717
+ }
9718
+ ],
9719
+ "description": "The whole-message `sensor_msgs/msg/JointState` datapoint that drives the console's URDF viewer; null when none is chosen or a publish removed it. Set through `PUT /api/robots/:id/urdf/joint-state`."
9499
9720
  }
9500
9721
  },
9501
9722
  "required": [
9502
9723
  "assets",
9503
9724
  "active_sync",
9504
9725
  "urdf",
9505
- "urdf_available"
9726
+ "urdf_available",
9727
+ "store",
9728
+ "joint_state_slug"
9506
9729
  ],
9507
9730
  "additionalProperties": false
9508
9731
  },
@@ -9590,32 +9813,38 @@
9590
9813
  "enum": [
9591
9814
  "unresolvable",
9592
9815
  "upload_failed",
9593
- "refused",
9594
- "too_large"
9816
+ "refused"
9595
9817
  ],
9596
- "description": "Why it failed. `unresolvable` means the reference names nothing the producer can find or may read, and is **permanent** — the only kind reconciliation may treat as gone. `upload_failed` means the bytes exist and the transfer did not succeed, `refused` means it was never attempted because a producer-side ceiling was hit, and `too_large` means it exceeds the upload limit and carries both numbers in `details`."
9818
+ "description": "Why it failed. `unresolvable` means the reference names nothing the producer can find or may read, and is **permanent** — the only kind reconciliation may treat as gone. `upload_failed` means the bytes exist and the transfer did not succeed, and `refused` means it was never attempted, either because the robot's asset store had no room — then `details` carries the three numbers — or because a producer-side ceiling was hit."
9597
9819
  },
9598
9820
  "details": {
9599
- "description": "The two numbers behind a `too_large` failure, and absent for every other kind — a forced `null` on every `unresolvable` entry buys nothing. The pairing is enforced, not merely described.",
9821
+ "description": "The three numbers behind a `refused` entry the robot's store had no room for, and absent for every other kind — a forced `null` on every `unresolvable` entry buys nothing. A `refused` entry may also carry no details: the producer's own ceiling is the other half of that kind, and no store number describes it.",
9600
9822
  "anyOf": [
9601
9823
  {
9602
9824
  "type": "object",
9603
9825
  "properties": {
9604
- "limit_bytes": {
9826
+ "store_bytes": {
9605
9827
  "type": "integer",
9606
9828
  "exclusiveMinimum": 0,
9607
9829
  "maximum": 9007199254740991,
9608
- "description": "The upload ceiling, in bytes."
9830
+ "description": "The robot's store, in bytes."
9831
+ },
9832
+ "used_bytes": {
9833
+ "type": "integer",
9834
+ "minimum": 0,
9835
+ "maximum": 9007199254740991,
9836
+ "description": "Bytes the robot's assets occupy before this upload."
9609
9837
  },
9610
9838
  "size_bytes": {
9611
9839
  "type": "integer",
9612
9840
  "exclusiveMinimum": 0,
9613
9841
  "maximum": 9007199254740991,
9614
- "description": "How large the refused file is, in bytes. With `limit_bytes` beside it a developer can tell whether to shrink the mesh or raise the limit; \"too large\" alone answers neither."
9842
+ "description": "The refused upload, in bytes."
9615
9843
  }
9616
9844
  },
9617
9845
  "required": [
9618
- "limit_bytes",
9846
+ "store_bytes",
9847
+ "used_bytes",
9619
9848
  "size_bytes"
9620
9849
  ],
9621
9850
  "additionalProperties": false
@@ -9646,6 +9875,25 @@
9646
9875
  ],
9647
9876
  "description": "Why the sync ended as it did, when that is not a per-reference fact. `null` when `failed` already says everything there is to say."
9648
9877
  },
9878
+ "stored": {
9879
+ "anyOf": [
9880
+ {
9881
+ "type": "integer",
9882
+ "minimum": 0,
9883
+ "maximum": 9007199254740991
9884
+ },
9885
+ {
9886
+ "type": "null"
9887
+ }
9888
+ ],
9889
+ "description": "How many of the announced files the cloud's store actually holds. Counted once, after the robot reports the sync done, and `null` until then — nobody has looked yet. Read it against `announced`: `state` is what the robot reported, this is what arrived."
9890
+ },
9891
+ "announced": {
9892
+ "type": "integer",
9893
+ "minimum": 0,
9894
+ "maximum": 9007199254740991,
9895
+ "description": "How many files the robot announced for this sync — the URDF, if it has one, plus every mesh URI its description references. `0` when the robot announced nothing, and also `0` until it has answered at all: read it beside `stored`, which stays `null` until the terminal frame."
9896
+ },
9649
9897
  "started_at": {
9650
9898
  "type": "string",
9651
9899
  "format": "date-time",
@@ -9667,11 +9915,35 @@
9667
9915
  "total",
9668
9916
  "failed",
9669
9917
  "reason",
9918
+ "stored",
9919
+ "announced",
9670
9920
  "started_at",
9671
9921
  "updated_at"
9672
9922
  ],
9673
9923
  "additionalProperties": false
9674
9924
  },
9925
+ "assets-clear-response": {
9926
+ "type": "object",
9927
+ "properties": {
9928
+ "deleted": {
9929
+ "type": "integer",
9930
+ "minimum": 0,
9931
+ "maximum": 9007199254740991,
9932
+ "description": "How many assets — URDF, meshes and textures together — were removed."
9933
+ },
9934
+ "bytes_freed": {
9935
+ "type": "integer",
9936
+ "minimum": 0,
9937
+ "maximum": 9007199254740991,
9938
+ "description": "The bytes the robot's store got back."
9939
+ }
9940
+ },
9941
+ "required": [
9942
+ "deleted",
9943
+ "bytes_freed"
9944
+ ],
9945
+ "additionalProperties": false
9946
+ },
9675
9947
  "audit-list-response": {
9676
9948
  "type": "object",
9677
9949
  "properties": {
@@ -9953,7 +10225,7 @@
9953
10225
  "refresh_token"
9954
10226
  ]
9955
10227
  },
9956
- "description": "The grants this server offers. OAuth 2.1 removes the implicit and password grants, so neither appears here."
10228
+ "description": "The grants this server offers: `authorization_code` and `refresh_token`. OAuth 2.1 removes the implicit and password grants, so neither appears here."
9957
10229
  },
9958
10230
  "code_challenge_methods_supported": {
9959
10231
  "type": "array",
@@ -10535,11 +10807,16 @@
10535
10807
  "type": "null"
10536
10808
  }
10537
10809
  ]
10810
+ },
10811
+ "low_bandwidth": {
10812
+ "type": "boolean",
10813
+ "description": "Whether the bridge is in its low-bandwidth mode: datapoints capped, cameras reduced or stopped. Bridge-reported."
10538
10814
  }
10539
10815
  },
10540
10816
  "required": [
10541
10817
  "online",
10542
- "latency_ms"
10818
+ "latency_ms",
10819
+ "low_bandwidth"
10543
10820
  ],
10544
10821
  "additionalProperties": false,
10545
10822
  "description": "The built-in `bridge_state` datapoint as the cloud observes it right now: whether the bridge is connected, and its latency when it is."
@@ -10665,6 +10942,11 @@
10665
10942
  0.5
10666
10943
  ]
10667
10944
  },
10945
+ "low_bandwidth": {
10946
+ "description": "`keep` exempts this datapoint from the low-bandwidth rate cap; its backfill still pauses.",
10947
+ "type": "string",
10948
+ "const": "keep"
10949
+ },
10668
10950
  "description": {
10669
10951
  "description": "Prose about what this value is, for whoever meets it in the console. It changes nothing the robot does, so a publish that touches only it pushes no configuration at all — but it is carried verbatim into `robot_describe`, where a model that has never seen this robot reads it. The datapoint is offered whenever the role grants it; without one it is offered with `description: null` and the model has less to go on, as for actions, services, publishers and cameras. Omission is the only way to say nothing; an empty string is refused, here and on all five.",
10670
10952
  "examples": [
@@ -10862,7 +11144,7 @@
10862
11144
  ],
10863
11145
  "additionalProperties": false
10864
11146
  },
10865
- "description": "Values the robot publishes, each one field of one topic or a whole topic, and **never several topics**. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all four are refused when the document is validated."
11147
+ "description": "Values the robot publishes, each one field of one topic or a whole topic, and **never several topics**. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all three are refused when the document is validated."
10866
11148
  },
10867
11149
  "actions": {
10868
11150
  "type": "object",
@@ -11012,7 +11294,7 @@
11012
11294
  ],
11013
11295
  "additionalProperties": false
11014
11296
  },
11015
- "description": "Things the robot does on request that take time, each reported as a job with progress. **At most one job runs per action slug**: a second call is refused `busy`, and every observer of that slug watches the same job. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all four are refused when the document is validated."
11297
+ "description": "Things the robot does on request that take time, each reported as a job with progress. **At most one job runs per action slug**: a second call is refused `busy`, and every observer of that slug watches the same job. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all three are refused when the document is validated."
11016
11298
  },
11017
11299
  "services": {
11018
11300
  "type": "object",
@@ -11162,7 +11444,7 @@
11162
11444
  ],
11163
11445
  "additionalProperties": false
11164
11446
  },
11165
- "description": "ROS service calls the robot answers — one request, one reply. Unlike an action a service reports **no progress** and the call returns with its result already on the job, so there is nothing left to observe; a second concurrent call is still refused `busy`, exactly as for an action. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all four are refused when the document is validated."
11447
+ "description": "ROS service calls the robot answers — one request, one reply. Unlike an action a service reports **no progress** and the call returns with its result already on the job, so there is nothing left to observe; a second concurrent call is still refused `busy`, exactly as for an action. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all three are refused when the document is validated."
11166
11448
  },
11167
11449
  "publishers": {
11168
11450
  "type": "object",
@@ -11348,7 +11630,7 @@
11348
11630
  ],
11349
11631
  "additionalProperties": false
11350
11632
  },
11351
- "description": "Topics clients may send to, and where the format's whole safety story lives. The `message` template fixes every value a caller cannot change, and **`failsafe` is required**: once a client falls silent the bridge sends the failsafe message itself, so an operator whose window closed does not leave a robot driving. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all four are refused when the document is validated."
11633
+ "description": "Topics clients may send to, and where the format's whole safety story lives. The `message` template fixes every value a caller cannot change, and **`failsafe` is required**: once a client falls silent the bridge sends the failsafe message itself, so an operator whose window closed does not leave a robot driving. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all three are refused when the document is validated."
11352
11634
  },
11353
11635
  "cameras": {
11354
11636
  "type": "object",
@@ -11596,7 +11878,67 @@
11596
11878
  ],
11597
11879
  "additionalProperties": false
11598
11880
  },
11599
- "description": "Video the robot streams, and the still frames the cloud serves from it. `width`, `height`, `fps` and `bitrate_kbps` are what **the bridge produces before sending**, not what the camera captures — they live in the configuration rather than in a viewer's request precisely so that no viewer can make a robot send more. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all four are refused when the document is validated."
11881
+ "description": "Video the robot streams, and the still frames the cloud serves from it. `width`, `height`, `fps` and `bitrate_kbps` are what **the bridge produces before sending**, not what the camera captures — they live in the configuration rather than in a viewer's request precisely so that no viewer can make a robot send more. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all three are refused when the document is validated."
11882
+ },
11883
+ "low_bandwidth": {
11884
+ "description": "Overrides for the bridge's low-bandwidth mode; see the section schema.",
11885
+ "type": "object",
11886
+ "properties": {
11887
+ "mode": {
11888
+ "description": "`auto` decides from the measured lag; `on` and `off` force the mode, for tests and for an operator who knows the link.",
11889
+ "type": "string",
11890
+ "enum": [
11891
+ "auto",
11892
+ "on",
11893
+ "off"
11894
+ ]
11895
+ },
11896
+ "enter_lag_ms": {
11897
+ "description": "Lag or queue dwell above this enters the mode. Checked against exit_lag_ms only when both are in this document; a lone key composes with the bridge's parameter or the default on the robot, and a crossed pair is refused there when the configuration is applied, so name both when you change either.",
11898
+ "type": "integer",
11899
+ "minimum": 100,
11900
+ "maximum": 9007199254740991
11901
+ },
11902
+ "enter_after_s": {
11903
+ "description": "The entry condition must hold this long.",
11904
+ "type": "integer",
11905
+ "minimum": 1,
11906
+ "maximum": 9007199254740991
11907
+ },
11908
+ "exit_lag_ms": {
11909
+ "description": "Lag and dwell both at or below this leave the mode. Must be at or below enter_lag_ms: a crossed pair is a mode that leaves as it arrives. Checked here only when both keys are present; a lone key is checked on the robot against the parameter or default it composes with.",
11910
+ "type": "integer",
11911
+ "minimum": 0,
11912
+ "maximum": 9007199254740991
11913
+ },
11914
+ "exit_after_s": {
11915
+ "description": "The exit condition must hold this long.",
11916
+ "type": "integer",
11917
+ "minimum": 1,
11918
+ "maximum": 9007199254740991
11919
+ },
11920
+ "datapoint_max_hz": {
11921
+ "description": "The long-run rate for every datapoint in the mode, unless the datapoint says `low_bandwidth: keep`. It is an average, not a minimum gap: after a quiet spell two samples may go out close together, and over any longer window the rate holds.",
11922
+ "type": "number",
11923
+ "exclusiveMinimum": 0,
11924
+ "maximum": 20
11925
+ },
11926
+ "camera": {
11927
+ "description": "What happens to a running stream in the mode. New streams are refused either way.",
11928
+ "type": "string",
11929
+ "enum": [
11930
+ "reduce",
11931
+ "stop"
11932
+ ]
11933
+ },
11934
+ "camera_bitrate_kbps": {
11935
+ "description": "Bitrate applied to running streams under `reduce`.",
11936
+ "type": "integer",
11937
+ "minimum": 50,
11938
+ "maximum": 20000
11939
+ }
11940
+ },
11941
+ "additionalProperties": false
11600
11942
  }
11601
11943
  },
11602
11944
  "required": [
@@ -11762,6 +12104,11 @@
11762
12104
  0.5
11763
12105
  ]
11764
12106
  },
12107
+ "low_bandwidth": {
12108
+ "description": "`keep` exempts this datapoint from the low-bandwidth rate cap; its backfill still pauses.",
12109
+ "type": "string",
12110
+ "const": "keep"
12111
+ },
11765
12112
  "description": {
11766
12113
  "description": "Prose about what this value is, for whoever meets it in the console. It changes nothing the robot does, so a publish that touches only it pushes no configuration at all — but it is carried verbatim into `robot_describe`, where a model that has never seen this robot reads it. The datapoint is offered whenever the role grants it; without one it is offered with `description: null` and the model has less to go on, as for actions, services, publishers and cameras. Omission is the only way to say nothing; an empty string is refused, here and on all five.",
11767
12114
  "examples": [
@@ -11959,7 +12306,7 @@
11959
12306
  ],
11960
12307
  "additionalProperties": false
11961
12308
  },
11962
- "description": "Values the robot publishes, each one field of one topic or a whole topic, and **never several topics**. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all four are refused when the document is validated."
12309
+ "description": "Values the robot publishes, each one field of one topic or a whole topic, and **never several topics**. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all three are refused when the document is validated."
11963
12310
  },
11964
12311
  "actions": {
11965
12312
  "type": "object",
@@ -12109,7 +12456,7 @@
12109
12456
  ],
12110
12457
  "additionalProperties": false
12111
12458
  },
12112
- "description": "Things the robot does on request that take time, each reported as a job with progress. **At most one job runs per action slug**: a second call is refused `busy`, and every observer of that slug watches the same job. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all four are refused when the document is validated."
12459
+ "description": "Things the robot does on request that take time, each reported as a job with progress. **At most one job runs per action slug**: a second call is refused `busy`, and every observer of that slug watches the same job. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all three are refused when the document is validated."
12113
12460
  },
12114
12461
  "services": {
12115
12462
  "type": "object",
@@ -12259,7 +12606,7 @@
12259
12606
  ],
12260
12607
  "additionalProperties": false
12261
12608
  },
12262
- "description": "ROS service calls the robot answers — one request, one reply. Unlike an action a service reports **no progress** and the call returns with its result already on the job, so there is nothing left to observe; a second concurrent call is still refused `busy`, exactly as for an action. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all four are refused when the document is validated."
12609
+ "description": "ROS service calls the robot answers — one request, one reply. Unlike an action a service reports **no progress** and the call returns with its result already on the job, so there is nothing left to observe; a second concurrent call is still refused `busy`, exactly as for an action. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all three are refused when the document is validated."
12263
12610
  },
12264
12611
  "publishers": {
12265
12612
  "type": "object",
@@ -12445,7 +12792,7 @@
12445
12792
  ],
12446
12793
  "additionalProperties": false
12447
12794
  },
12448
- "description": "Topics clients may send to, and where the format's whole safety story lives. The `message` template fixes every value a caller cannot change, and **`failsafe` is required**: once a client falls silent the bridge sends the failsafe message itself, so an operator whose window closed does not leave a robot driving. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all four are refused when the document is validated."
12795
+ "description": "Topics clients may send to, and where the format's whole safety story lives. The `message` template fixes every value a caller cannot change, and **`failsafe` is required**: once a client falls silent the bridge sends the failsafe message itself, so an operator whose window closed does not leave a robot driving. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all three are refused when the document is validated."
12449
12796
  },
12450
12797
  "cameras": {
12451
12798
  "type": "object",
@@ -12693,7 +13040,67 @@
12693
13040
  ],
12694
13041
  "additionalProperties": false
12695
13042
  },
12696
- "description": "Video the robot streams, and the still frames the cloud serves from it. `width`, `height`, `fps` and `bitrate_kbps` are what **the bridge produces before sending**, not what the camera captures — they live in the configuration rather than in a viewer's request precisely so that no viewer can make a robot send more. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all four are refused when the document is validated."
13043
+ "description": "Video the robot streams, and the still frames the cloud serves from it. `width`, `height`, `fps` and `bitrate_kbps` are what **the bridge produces before sending**, not what the camera captures — they live in the configuration rather than in a viewer's request precisely so that no viewer can make a robot send more. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all three are refused when the document is validated."
13044
+ },
13045
+ "low_bandwidth": {
13046
+ "description": "Overrides for the bridge's low-bandwidth mode; see the section schema.",
13047
+ "type": "object",
13048
+ "properties": {
13049
+ "mode": {
13050
+ "description": "`auto` decides from the measured lag; `on` and `off` force the mode, for tests and for an operator who knows the link.",
13051
+ "type": "string",
13052
+ "enum": [
13053
+ "auto",
13054
+ "on",
13055
+ "off"
13056
+ ]
13057
+ },
13058
+ "enter_lag_ms": {
13059
+ "description": "Lag or queue dwell above this enters the mode. Checked against exit_lag_ms only when both are in this document; a lone key composes with the bridge's parameter or the default on the robot, and a crossed pair is refused there when the configuration is applied, so name both when you change either.",
13060
+ "type": "integer",
13061
+ "minimum": 100,
13062
+ "maximum": 9007199254740991
13063
+ },
13064
+ "enter_after_s": {
13065
+ "description": "The entry condition must hold this long.",
13066
+ "type": "integer",
13067
+ "minimum": 1,
13068
+ "maximum": 9007199254740991
13069
+ },
13070
+ "exit_lag_ms": {
13071
+ "description": "Lag and dwell both at or below this leave the mode. Must be at or below enter_lag_ms: a crossed pair is a mode that leaves as it arrives. Checked here only when both keys are present; a lone key is checked on the robot against the parameter or default it composes with.",
13072
+ "type": "integer",
13073
+ "minimum": 0,
13074
+ "maximum": 9007199254740991
13075
+ },
13076
+ "exit_after_s": {
13077
+ "description": "The exit condition must hold this long.",
13078
+ "type": "integer",
13079
+ "minimum": 1,
13080
+ "maximum": 9007199254740991
13081
+ },
13082
+ "datapoint_max_hz": {
13083
+ "description": "The long-run rate for every datapoint in the mode, unless the datapoint says `low_bandwidth: keep`. It is an average, not a minimum gap: after a quiet spell two samples may go out close together, and over any longer window the rate holds.",
13084
+ "type": "number",
13085
+ "exclusiveMinimum": 0,
13086
+ "maximum": 20
13087
+ },
13088
+ "camera": {
13089
+ "description": "What happens to a running stream in the mode. New streams are refused either way.",
13090
+ "type": "string",
13091
+ "enum": [
13092
+ "reduce",
13093
+ "stop"
13094
+ ]
13095
+ },
13096
+ "camera_bitrate_kbps": {
13097
+ "description": "Bitrate applied to running streams under `reduce`.",
13098
+ "type": "integer",
13099
+ "minimum": 50,
13100
+ "maximum": 20000
13101
+ }
13102
+ },
13103
+ "additionalProperties": false
12697
13104
  }
12698
13105
  },
12699
13106
  "required": [
@@ -13102,7 +13509,7 @@
13102
13509
  },
13103
13510
  "builtin": {
13104
13511
  "type": "boolean",
13105
- "description": "`true` for the datapoints every robot has — `bridge_state`, `robot_details` and `bridge_pressure` — and `false` for everything the published configuration adds."
13512
+ "description": "`true` for the datapoints every robot has — `bridge_state` and `robot_details` — and `false` for everything the published configuration adds."
13106
13513
  },
13107
13514
  "unit": {
13108
13515
  "anyOf": [
@@ -13137,7 +13544,7 @@
13137
13544
  ],
13138
13545
  "additionalProperties": false
13139
13546
  },
13140
- "description": "Everything a client may read on this robot: the three built-ins, plus every datapoint the published configuration exposes and the caller's role grants."
13547
+ "description": "Everything a client may read on this robot: the two built-ins, plus every datapoint the published configuration exposes and the caller's role grants."
13141
13548
  }
13142
13549
  },
13143
13550
  "required": [
@@ -13200,7 +13607,7 @@
13200
13607
  ]
13201
13608
  },
13202
13609
  "grant_types": {
13203
- "description": "Accepted for conformance with RFC 7591 and then **ignored**. What comes back is what was actually granted, which §3.2.1 permits a server to substitute: `authorization_code` and nothing else, so a client that asks for `refresh_token` is registered and told plainly that it did not get one.",
13610
+ "description": "Accepted for conformance with RFC 7591 and then **ignored**: both MCP authorization servers grant `authorization_code` and `refresh_token` to every registration, and the answer states what was granted (§3.2.1) rather than what was asked.",
13204
13611
  "type": "array",
13205
13612
  "items": {
13206
13613
  "type": "string",
@@ -13260,7 +13667,7 @@
13260
13667
  "items": {
13261
13668
  "type": "string"
13262
13669
  },
13263
- "description": "The grants this client may use. Always exactly `[\"authorization_code\"]` — a client that asked for `refresh_token` is registered and told here that it did not get one, which is the substitution RFC 7591 §3.2.1 permits."
13670
+ "description": "The grants this client may use. Always exactly `[\"authorization_code\", \"refresh_token\"]` — an exchange mints a refresh token and the token endpoint rotates it."
13264
13671
  },
13265
13672
  "response_types": {
13266
13673
  "type": "array",
@@ -14452,6 +14859,51 @@
14452
14859
  ],
14453
14860
  "additionalProperties": false
14454
14861
  },
14862
+ "joint-state-put-request": {
14863
+ "type": "object",
14864
+ "properties": {
14865
+ "slug": {
14866
+ "anyOf": [
14867
+ {
14868
+ "type": "string",
14869
+ "minLength": 2,
14870
+ "maxLength": 63,
14871
+ "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$"
14872
+ },
14873
+ {
14874
+ "type": "null"
14875
+ }
14876
+ ],
14877
+ "description": "The datapoint to read joint positions from, or `null` to choose none. It must name a whole-message `sensor_msgs/msg/JointState` datapoint of the published configuration; anything else is a `validation_error` naming the rule."
14878
+ }
14879
+ },
14880
+ "required": [
14881
+ "slug"
14882
+ ]
14883
+ },
14884
+ "joint-state-put-response": {
14885
+ "type": "object",
14886
+ "properties": {
14887
+ "joint_state_slug": {
14888
+ "anyOf": [
14889
+ {
14890
+ "type": "string",
14891
+ "minLength": 2,
14892
+ "maxLength": 63,
14893
+ "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$"
14894
+ },
14895
+ {
14896
+ "type": "null"
14897
+ }
14898
+ ],
14899
+ "description": "The stored mapping after the call, `null` when none is chosen. The same value `assetListResponse.joint_state_slug` carries."
14900
+ }
14901
+ },
14902
+ "required": [
14903
+ "joint_state_slug"
14904
+ ],
14905
+ "additionalProperties": false
14906
+ },
14455
14907
  "live-session-response": {
14456
14908
  "type": "object",
14457
14909
  "properties": {
@@ -14874,50 +15326,88 @@
14874
15326
  "additionalProperties": false
14875
15327
  },
14876
15328
  "oauth-token-request": {
14877
- "type": "object",
14878
- "properties": {
14879
- "grant_type": {
14880
- "type": "string",
14881
- "const": "authorization_code",
14882
- "description": "Always `authorization_code`: this request exchanges the code from the authorize redirect for tokens. Any other value — `refresh_token` included — is `unsupported_grant_type`, refused before the code is looked up."
14883
- },
14884
- "code": {
14885
- "type": "string",
14886
- "minLength": 1,
14887
- "maxLength": 500,
14888
- "description": "The authorization code from the redirect. It may be exchanged once; a second presentation is `invalid_grant`, the same answer a fabricated code gets."
14889
- },
14890
- "redirect_uri": {
14891
- "type": "string",
14892
- "minLength": 1,
14893
- "maxLength": 2000,
14894
- "description": "The same redirect URI the authorize request used. It is compared, not merely recorded."
14895
- },
14896
- "client_id": {
14897
- "type": "string",
14898
- "minLength": 1,
14899
- "maxLength": 200,
14900
- "description": "The client making the exchange, as registered."
14901
- },
14902
- "code_verifier": {
14903
- "type": "string",
14904
- "pattern": "^[A-Za-z0-9\\-._~]{43,128}$",
14905
- "description": "The PKCE verifier whose `S256` hash was sent as the challenge at the authorize step. Between `43` and `128` unreserved characters, per RFC 7636 §4.1 — it is compared rather than parsed, so a length nobody checks is a length an attacker chooses. PKCE is mandatory for every client under OAuth 2.1."
15329
+ "oneOf": [
15330
+ {
15331
+ "type": "object",
15332
+ "properties": {
15333
+ "grant_type": {
15334
+ "type": "string",
15335
+ "const": "authorization_code",
15336
+ "description": "`authorization_code`: this request exchanges the code from the authorize redirect for an access token and a refresh token."
15337
+ },
15338
+ "code": {
15339
+ "type": "string",
15340
+ "minLength": 1,
15341
+ "maxLength": 500,
15342
+ "description": "The authorization code from the redirect. It may be exchanged once; a second presentation is `invalid_grant`, the same answer a fabricated code gets."
15343
+ },
15344
+ "redirect_uri": {
15345
+ "type": "string",
15346
+ "minLength": 1,
15347
+ "maxLength": 2000,
15348
+ "description": "The same redirect URI the authorize request used. It is compared, not merely recorded."
15349
+ },
15350
+ "client_id": {
15351
+ "type": "string",
15352
+ "minLength": 1,
15353
+ "maxLength": 200,
15354
+ "description": "The client making the exchange, as registered."
15355
+ },
15356
+ "code_verifier": {
15357
+ "type": "string",
15358
+ "pattern": "^[A-Za-z0-9\\-._~]{43,128}$",
15359
+ "description": "The PKCE verifier whose `S256` hash was sent as the challenge at the authorize step. Between `43` and `128` unreserved characters, per RFC 7636 §4.1 — it is compared rather than parsed, so a length nobody checks is a length an attacker chooses. PKCE is mandatory for every client under OAuth 2.1."
15360
+ },
15361
+ "resource": {
15362
+ "description": "The resource the token is requested for, per RFC 8707. It must match the audience the code was authorized for, or the answer is `invalid_target`; omitted, the code's own audience stands. It becomes the token's `aud`, and a resource refuses a token whose audience names something else — which is what keeps a token minted for one app out of another app's endpoint.",
15363
+ "type": "string",
15364
+ "format": "uri"
15365
+ }
15366
+ },
15367
+ "required": [
15368
+ "grant_type",
15369
+ "code",
15370
+ "redirect_uri",
15371
+ "client_id",
15372
+ "code_verifier"
15373
+ ],
15374
+ "description": "RFC 6749 §4.1.3's authorization-code exchange with PKCE, as either MCP authorization server reads it. Sent as `application/x-www-form-urlencoded`, per §4.1.3, though the server accepts a JSON body too."
14906
15375
  },
14907
- "resource": {
14908
- "description": "The resource the token is requested for, per RFC 8707. It must match the audience the code was authorized for, or the answer is `invalid_target`; omitted, the code's own audience stands. It becomes the token's `aud`, and a resource refuses a token whose audience names something else — which is what keeps a token minted for one app out of another app's endpoint.",
14909
- "type": "string",
14910
- "format": "uri"
15376
+ {
15377
+ "type": "object",
15378
+ "properties": {
15379
+ "grant_type": {
15380
+ "type": "string",
15381
+ "const": "refresh_token",
15382
+ "description": "`refresh_token`: this request rotates a refresh token into a new access token and a new refresh token. The presented token is consumed; presenting it again revokes the whole session."
15383
+ },
15384
+ "refresh_token": {
15385
+ "type": "string",
15386
+ "minLength": 1,
15387
+ "maxLength": 500,
15388
+ "description": "The refresh token from the last token response. Bound to the client that received it and to one identity space: presented by another client, or at the other MCP server, it is `invalid_grant` and stays unconsumed."
15389
+ },
15390
+ "client_id": {
15391
+ "type": "string",
15392
+ "minLength": 1,
15393
+ "maxLength": 200,
15394
+ "description": "The client the refresh token was issued to, as registered. A refresh token is not transferable between clients."
15395
+ },
15396
+ "resource": {
15397
+ "description": "The resource the new token is for, per RFC 8707. Optional; when named it must be the audience the session was issued for, or the answer is `invalid_target` and the refresh token is left untouched. The successor carries the same audience either way.",
15398
+ "type": "string",
15399
+ "format": "uri"
15400
+ }
15401
+ },
15402
+ "required": [
15403
+ "grant_type",
15404
+ "refresh_token",
15405
+ "client_id"
15406
+ ],
15407
+ "description": "RFC 6749 §6's refresh, as either MCP authorization server reads it. Every use rotates: the answer carries a new refresh token and the presented one is dead."
14911
15408
  }
14912
- },
14913
- "required": [
14914
- "grant_type",
14915
- "code",
14916
- "redirect_uri",
14917
- "client_id",
14918
- "code_verifier"
14919
15409
  ],
14920
- "description": "RFC 6749 §4.1.3's authorization-code exchange with PKCE, as either MCP authorization server reads it. Sent as `application/x-www-form-urlencoded`, per §4.1.3, though the server accepts a JSON body too."
15410
+ "description": "What an MCP token endpoint accepts: the authorization-code exchange, or a refresh. Any other `grant_type` is `unsupported_grant_type`, refused before a lookup happens."
14921
15411
  },
14922
15412
  "oauth-token-response": {
14923
15413
  "type": "object",
@@ -14939,7 +15429,7 @@
14939
15429
  "description": "How long the access token is valid, in **seconds**, per RFC 6749 §5.1. Not a timestamp, and not milliseconds."
14940
15430
  },
14941
15431
  "refresh_token": {
14942
- "description": "The refresh token, when one was issued. It rotates on every use.",
15432
+ "description": "The refresh token. Both MCP token endpoints issue one on every exchange and every refresh; it rotates on every use, lives ninety days from its last use, and dies with the account's sessions — a block, a password change, a withdrawn consent. The console's own OAuth portal issues none.",
14943
15433
  "type": "string",
14944
15434
  "minLength": 1
14945
15435
  },
@@ -15291,11 +15781,6 @@
15291
15781
  "type": "integer",
15292
15782
  "exclusiveMinimum": 0,
15293
15783
  "maximum": 9007199254740991
15294
- },
15295
- "max_asset_storage_bytes": {
15296
- "type": "integer",
15297
- "minimum": 0,
15298
- "maximum": 9007199254740991
15299
15784
  }
15300
15785
  },
15301
15786
  "required": [
@@ -15304,8 +15789,7 @@
15304
15789
  "max_end_users",
15305
15790
  "max_retention_bytes",
15306
15791
  "max_retention_writes_per_minute",
15307
- "max_realtime_connections",
15308
- "max_asset_storage_bytes"
15792
+ "max_realtime_connections"
15309
15793
  ],
15310
15794
  "additionalProperties": false
15311
15795
  },
@@ -15332,11 +15816,6 @@
15332
15816
  "minimum": 0,
15333
15817
  "maximum": 9007199254740991
15334
15818
  },
15335
- "max_asset_storage_bytes": {
15336
- "type": "integer",
15337
- "minimum": 0,
15338
- "maximum": 9007199254740991
15339
- },
15340
15819
  "max_retention_writes_per_minute": {
15341
15820
  "type": "integer",
15342
15821
  "minimum": 0,
@@ -16230,7 +16709,8 @@
16230
16709
  "asset_bytes_freed": {
16231
16710
  "type": "integer",
16232
16711
  "minimum": 0,
16233
- "maximum": 9007199254740991
16712
+ "maximum": 9007199254740991,
16713
+ "description": "What the robot's store gives back: every distinct mesh or texture blob it holds, counted once, URDF excluded; a blob another robot also references stays in the object store but is still credited here, because each robot's counter carries it."
16234
16714
  },
16235
16715
  "job_run_count": {
16236
16716
  "type": "integer",
@@ -16294,11 +16774,16 @@
16294
16774
  "type": "null"
16295
16775
  }
16296
16776
  ]
16777
+ },
16778
+ "low_bandwidth": {
16779
+ "type": "boolean",
16780
+ "description": "Whether the bridge is in its low-bandwidth mode: datapoints capped, cameras reduced or stopped. Bridge-reported."
16297
16781
  }
16298
16782
  },
16299
16783
  "required": [
16300
16784
  "online",
16301
- "latency_ms"
16785
+ "latency_ms",
16786
+ "low_bandwidth"
16302
16787
  ],
16303
16788
  "additionalProperties": false
16304
16789
  },
@@ -16340,6 +16825,15 @@
16340
16825
  ],
16341
16826
  "additionalProperties": false
16342
16827
  },
16828
+ "protocol_status": {
16829
+ "description": "Where this robot's bridge stands against the protocol window: `current`, `deprecated` (still served, sunset date on the detail), or `refused` (its last hello was refused for its version; offline until upgraded). Absent from a cloud older than 0.21.0; read absence as `current`.",
16830
+ "type": "string",
16831
+ "enum": [
16832
+ "current",
16833
+ "deprecated",
16834
+ "refused"
16835
+ ]
16836
+ },
16343
16837
  "bridge_version": {
16344
16838
  "anyOf": [
16345
16839
  {
@@ -16351,6 +16845,52 @@
16351
16845
  }
16352
16846
  ]
16353
16847
  },
16848
+ "protocol_version": {
16849
+ "description": "The protocol version the bridge announced in its last accepted hello; `null` before the first. Absent from a cloud older than 0.21.0.",
16850
+ "anyOf": [
16851
+ {
16852
+ "type": "integer",
16853
+ "exclusiveMinimum": 0,
16854
+ "maximum": 9007199254740991
16855
+ },
16856
+ {
16857
+ "type": "null"
16858
+ }
16859
+ ]
16860
+ },
16861
+ "protocol": {
16862
+ "description": "The window verdict for `protocol_version`.",
16863
+ "type": "object",
16864
+ "properties": {
16865
+ "status": {
16866
+ "type": "string",
16867
+ "enum": [
16868
+ "current",
16869
+ "deprecated",
16870
+ "refused"
16871
+ ],
16872
+ "description": "Same values as `protocol_status`."
16873
+ },
16874
+ "sunset_at": {
16875
+ "anyOf": [
16876
+ {
16877
+ "type": "string",
16878
+ "format": "date",
16879
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$"
16880
+ },
16881
+ {
16882
+ "type": "null"
16883
+ }
16884
+ ],
16885
+ "description": "ISO date the announced version stops being served; `null` when current or unknown."
16886
+ }
16887
+ },
16888
+ "required": [
16889
+ "status",
16890
+ "sunset_at"
16891
+ ],
16892
+ "additionalProperties": false
16893
+ },
16354
16894
  "last_hello_error": {
16355
16895
  "anyOf": [
16356
16896
  {
@@ -16460,7 +17000,8 @@
16460
17000
  "action",
16461
17001
  "service",
16462
17002
  "publisher",
16463
- "camera"
17003
+ "camera",
17004
+ "low_bandwidth"
16464
17005
  ]
16465
17006
  },
16466
17007
  "code": {
@@ -16678,11 +17219,16 @@
16678
17219
  "type": "null"
16679
17220
  }
16680
17221
  ]
17222
+ },
17223
+ "low_bandwidth": {
17224
+ "type": "boolean",
17225
+ "description": "Whether the bridge is in its low-bandwidth mode: datapoints capped, cameras reduced or stopped. Bridge-reported."
16681
17226
  }
16682
17227
  },
16683
17228
  "required": [
16684
17229
  "online",
16685
- "latency_ms"
17230
+ "latency_ms",
17231
+ "low_bandwidth"
16686
17232
  ],
16687
17233
  "additionalProperties": false
16688
17234
  },
@@ -16723,6 +17269,15 @@
16723
17269
  "cameras"
16724
17270
  ],
16725
17271
  "additionalProperties": false
17272
+ },
17273
+ "protocol_status": {
17274
+ "description": "Where this robot's bridge stands against the protocol window: `current`, `deprecated` (still served, sunset date on the detail), or `refused` (its last hello was refused for its version; offline until upgraded). Absent from a cloud older than 0.21.0; read absence as `current`.",
17275
+ "type": "string",
17276
+ "enum": [
17277
+ "current",
17278
+ "deprecated",
17279
+ "refused"
17280
+ ]
16726
17281
  }
16727
17282
  },
16728
17283
  "required": [
@@ -16741,6 +17296,20 @@
16741
17296
  ],
16742
17297
  "additionalProperties": false
16743
17298
  },
17299
+ "robot-token-rotate-response": {
17300
+ "type": "object",
17301
+ "properties": {
17302
+ "token": {
17303
+ "type": "string",
17304
+ "pattern": "^frt_[0-9a-f]{32}$",
17305
+ "description": "The robot's new bridge token. Returned exactly once; the previous token stops working at the bridge's next hello."
17306
+ }
17307
+ },
17308
+ "required": [
17309
+ "token"
17310
+ ],
17311
+ "additionalProperties": false
17312
+ },
16744
17313
  "role": {
16745
17314
  "type": "object",
16746
17315
  "properties": {