@fleetless/contracts 1.2.0 → 3.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 (68) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/artifacts/constants.json +30 -4
  3. package/artifacts/openapi.json +882 -80
  4. package/artifacts/routes.json +217 -7
  5. package/artifacts/schema/app-deletion-summary.schema.json +51 -0
  6. package/artifacts/schema/apply-error.schema.json +2 -1
  7. package/artifacts/schema/asset-list-response.schema.json +77 -12
  8. package/artifacts/schema/asset-sync-status.schema.json +35 -8
  9. package/artifacts/schema/asset.schema.json +2 -3
  10. package/artifacts/schema/assets-clear-response.schema.json +23 -0
  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/joint-state-put-request.schema.json +23 -0
  27. package/artifacts/schema/joint-state-put-response.schema.json +24 -0
  28. package/artifacts/schema/org-quota-usage-counts.schema.json +0 -5
  29. package/artifacts/schema/org-quota-usage.schema.json +1 -12
  30. package/artifacts/schema/org-quotas.schema.json +1 -7
  31. package/artifacts/schema/put-app-auth-mcp-request.schema.json +27 -0
  32. package/artifacts/schema/put-app-auth-registration-request.schema.json +36 -0
  33. package/artifacts/schema/put-app-auth-urls-request.schema.json +48 -0
  34. package/artifacts/schema/robot-config-doc.schema.json +90 -5
  35. package/artifacts/schema/robot-deletion-summary.schema.json +2 -1
  36. package/artifacts/schema/robot-detail-response.schema.json +63 -2
  37. package/artifacts/schema/robot-list-item.schema.json +15 -1
  38. package/artifacts/schema/robot-list-response.schema.json +15 -1
  39. package/artifacts/schema/robot-token-rotate-response.schema.json +15 -0
  40. package/artifacts/schema-outgoing/bridge-asset-progress.schema.json +14 -8
  41. package/artifacts/schema-outgoing/bridge-config-applied.schema.json +2 -1
  42. package/artifacts/schema-outgoing/bridge-link-mode.schema.json +37 -0
  43. package/artifacts/schema-outgoing/datapoint-frame.schema.json +4 -0
  44. package/dist/app-users.d.ts +25 -7
  45. package/dist/app-users.js +24 -6
  46. package/dist/apps.d.ts +22 -0
  47. package/dist/apps.js +33 -0
  48. package/dist/assets.d.ts +85 -50
  49. package/dist/assets.js +152 -62
  50. package/dist/audit.d.ts +1 -1
  51. package/dist/audit.js +1 -1
  52. package/dist/client-robots.d.ts +2 -0
  53. package/dist/common.d.ts +10 -0
  54. package/dist/common.js +16 -1
  55. package/dist/config.d.ts +69 -1
  56. package/dist/config.js +86 -6
  57. package/dist/errors.d.ts +1 -1
  58. package/dist/errors.js +1 -8
  59. package/dist/index.d.ts +12 -12
  60. package/dist/index.js +6 -6
  61. package/dist/protocol.d.ts +150 -71
  62. package/dist/protocol.js +144 -87
  63. package/dist/rest.d.ts +139 -35
  64. package/dist/rest.js +100 -66
  65. package/dist/routes.js +129 -21
  66. package/package.json +1 -1
  67. package/artifacts/schema/bridge-pressure.schema.json +0 -292
  68. package/artifacts/schema/put-app-auth-config-request.schema.json +0 -93
@@ -924,6 +924,93 @@
924
924
  }
925
925
  }
926
926
  }
927
+ },
928
+ "delete": {
929
+ "operationId": "delete_api_apps_id",
930
+ "summary": "Deletes an app and everything it produced.",
931
+ "tags": [
932
+ "apps"
933
+ ],
934
+ "security": [
935
+ {
936
+ "developerSession": []
937
+ }
938
+ ],
939
+ "parameters": [
940
+ {
941
+ "name": "id",
942
+ "in": "path",
943
+ "required": true,
944
+ "description": "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`.",
945
+ "schema": {
946
+ "type": "string"
947
+ }
948
+ }
949
+ ],
950
+ "responses": {
951
+ "204": {
952
+ "description": "Success."
953
+ },
954
+ "default": {
955
+ "description": "An error envelope. Codes this route is known to answer: `unauthorized`, `token_expired`, `token_revoked`, `tier_required`, `invalid_uuid`, `not_found`.",
956
+ "content": {
957
+ "application/json": {
958
+ "schema": {
959
+ "$ref": "#/components/schemas/api-error"
960
+ }
961
+ }
962
+ }
963
+ }
964
+ },
965
+ "description": "Owner tier, and the gate runs **after** the org-scoped lookup: a developer-tier admin therefore sees the same `404` a stranger would for an app outside their org, rather than a tier refusal that confirms the id exists. A full cascade — its users, roles, server keys, invitations, OIDC provider configuration and mail templates all go, recorded once as `app.deleted` carrying an `appDeletionSummary`. Its robots are untouched: they belong to the org, not to the app. \n\n**No `force` parameter** — see `GET /api/apps/:id/deletion-preview`."
966
+ }
967
+ },
968
+ "/api/apps/{id}/deletion-preview": {
969
+ "get": {
970
+ "operationId": "get_api_apps_id_deletion_preview",
971
+ "summary": "Reports what deleting the app would destroy, without destroying it.",
972
+ "tags": [
973
+ "apps"
974
+ ],
975
+ "security": [
976
+ {
977
+ "developerSession": []
978
+ }
979
+ ],
980
+ "parameters": [
981
+ {
982
+ "name": "id",
983
+ "in": "path",
984
+ "required": true,
985
+ "description": "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`.",
986
+ "schema": {
987
+ "type": "string"
988
+ }
989
+ }
990
+ ],
991
+ "responses": {
992
+ "200": {
993
+ "description": "Success.",
994
+ "content": {
995
+ "application/json": {
996
+ "schema": {
997
+ "$ref": "#/components/schemas/app-deletion-summary"
998
+ }
999
+ }
1000
+ }
1001
+ },
1002
+ "default": {
1003
+ "description": "An error envelope. Codes this route is known to answer: `unauthorized`, `token_expired`, `token_revoked`, `invalid_uuid`, `not_found`.",
1004
+ "content": {
1005
+ "application/json": {
1006
+ "schema": {
1007
+ "$ref": "#/components/schemas/api-error"
1008
+ }
1009
+ }
1010
+ }
1011
+ }
1012
+ },
1013
+ "description": "The same shape the delete's own audit event carries, computed by the same function on purpose: the confirmation dialog and the eventual receipt agree by construction, and any difference between them is real drift rather than two estimates that quietly disagree. \n\n**No `force` parameter, unlike the robot pair this is modelled on.** A robot's open live session is a single nameable state whose interruption is its own hazard, which is why that route makes the caller pass `force` explicitly. An app has no equivalent state to force past, and inventing one would be a guess wearing a guard's clothes — this preview is the guard."
927
1014
  }
928
1015
  },
929
1016
  "/api/apps/{id}/roles": {
@@ -2376,11 +2463,129 @@
2376
2463
  }
2377
2464
  }
2378
2465
  },
2379
- "description": "One row per app, created with the app and never absent — an app that has configured nothing reads back the defaults rather than a `404`. `oidc_callback_url` is in the answer and not in the request: it is minted by the cloud from its own public base URL, is the same for every app and every provider, and is the value a developer registers at their identity provider."
2380
- },
2466
+ "description": "One row per app, created with the app and never absent — an app that has configured nothing reads back the defaults rather than a `404`. `oidc_callback_url` is in the answer and not in the request: it is minted by the cloud from its own public base URL, is the same for every app and every provider, and is the value a developer registers at their identity provider. It stays read-only on every slice write below for a second reason: a writable callback URL would let a caller point the return leg of an OIDC sign-in, which carries an authorization code, at a host they own. `updated_at` is read-only for a duller one: the server stamps it on every write, and a client-supplied value would be a lie about when the row last changed."
2467
+ }
2468
+ },
2469
+ "/api/apps/{id}/auth-config/registration": {
2470
+ "put": {
2471
+ "operationId": "put_api_apps_id_auth_config_registration",
2472
+ "summary": "Replaces who may self-register, and from where.",
2473
+ "tags": [
2474
+ "apps"
2475
+ ],
2476
+ "security": [
2477
+ {
2478
+ "developerSession": []
2479
+ }
2480
+ ],
2481
+ "parameters": [
2482
+ {
2483
+ "name": "id",
2484
+ "in": "path",
2485
+ "required": true,
2486
+ "description": "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`.",
2487
+ "schema": {
2488
+ "type": "string"
2489
+ }
2490
+ }
2491
+ ],
2492
+ "responses": {
2493
+ "200": {
2494
+ "description": "Success.",
2495
+ "content": {
2496
+ "application/json": {
2497
+ "schema": {
2498
+ "$ref": "#/components/schemas/app-auth-config"
2499
+ }
2500
+ }
2501
+ }
2502
+ },
2503
+ "default": {
2504
+ "description": "An error envelope. Codes this route is known to answer: `unauthorized`, `token_expired`, `token_revoked`, `invalid_uuid`, `validation_error`, `not_found`.",
2505
+ "content": {
2506
+ "application/json": {
2507
+ "schema": {
2508
+ "$ref": "#/components/schemas/api-error"
2509
+ }
2510
+ }
2511
+ }
2512
+ }
2513
+ },
2514
+ "description": "**A replace, not a merge, and `.strict()`**: `self_registration`, `allowed_domains` and `allowed_origins` all arrive or the write is refused, so a client built against an older shape cannot silently clear a setting it does not know about. `oidc_callback_url` and `updated_at` are the server's, refused in this body as in every slice's — see `GET`'s notes for why. \n\n`400 validation_error` is where the two field rules land: an entry in `allowed_domains` must be lowercase, since a capitalised one can never match a lowercased address, and an entry in `allowed_origins` must be a bare scheme-host-port with no path, since a browser sends nothing longer in its `Origin` header. Each refuses at configuration time rather than failing silently later. \n\nThe merge is server-side against the stored row, so this write never disturbs the urls or mcp slice.",
2515
+ "requestBody": {
2516
+ "required": true,
2517
+ "content": {
2518
+ "application/json": {
2519
+ "schema": {
2520
+ "$ref": "#/components/schemas/put-app-auth-registration-request"
2521
+ }
2522
+ }
2523
+ }
2524
+ }
2525
+ }
2526
+ },
2527
+ "/api/apps/{id}/auth-config/urls": {
2528
+ "put": {
2529
+ "operationId": "put_api_apps_id_auth_config_urls",
2530
+ "summary": "Replaces the three pages Fleetless's mails point at.",
2531
+ "tags": [
2532
+ "apps"
2533
+ ],
2534
+ "security": [
2535
+ {
2536
+ "developerSession": []
2537
+ }
2538
+ ],
2539
+ "parameters": [
2540
+ {
2541
+ "name": "id",
2542
+ "in": "path",
2543
+ "required": true,
2544
+ "description": "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`.",
2545
+ "schema": {
2546
+ "type": "string"
2547
+ }
2548
+ }
2549
+ ],
2550
+ "responses": {
2551
+ "200": {
2552
+ "description": "Success.",
2553
+ "content": {
2554
+ "application/json": {
2555
+ "schema": {
2556
+ "$ref": "#/components/schemas/app-auth-config"
2557
+ }
2558
+ }
2559
+ }
2560
+ },
2561
+ "default": {
2562
+ "description": "An error envelope. Codes this route is known to answer: `unauthorized`, `token_expired`, `token_revoked`, `invalid_uuid`, `validation_error`, `not_found`.",
2563
+ "content": {
2564
+ "application/json": {
2565
+ "schema": {
2566
+ "$ref": "#/components/schemas/api-error"
2567
+ }
2568
+ }
2569
+ }
2570
+ }
2571
+ },
2572
+ "description": "**A replace, not a merge, and `.strict()`**: `invite_url`, `verify_url` and `reset_url` all arrive or the write is refused, so a client built against an older shape cannot silently clear a setting it does not know about. `oidc_callback_url` and `updated_at` are the server's, refused in this body as in every slice's — see `GET`'s notes for why. \n\n`400 validation_error` is where the field rule lands: a URL template must be https (or `http` on `localhost`) and carry its placeholder exactly once — a second occurrence leaves one literal in a mailed link, refused here rather than failing silently once the mail is sent. \n\nThe merge is server-side against the stored row, so this write never disturbs the registration or mcp slice.",
2573
+ "requestBody": {
2574
+ "required": true,
2575
+ "content": {
2576
+ "application/json": {
2577
+ "schema": {
2578
+ "$ref": "#/components/schemas/put-app-auth-urls-request"
2579
+ }
2580
+ }
2581
+ }
2582
+ }
2583
+ }
2584
+ },
2585
+ "/api/apps/{id}/auth-config/mcp": {
2381
2586
  "put": {
2382
- "operationId": "put_api_apps_id_auth_config",
2383
- "summary": "Replaces the app's auth settings in one write.",
2587
+ "operationId": "put_api_apps_id_auth_config_mcp",
2588
+ "summary": "Replaces the MCP switch and its login URL together.",
2384
2589
  "tags": [
2385
2590
  "apps"
2386
2591
  ],
@@ -2422,13 +2627,13 @@
2422
2627
  }
2423
2628
  }
2424
2629
  },
2425
- "description": "**A replace, not a merge, and `.strict()`**: every field arrives or the write is refused, so a client built against an older shape cannot silently clear a setting it does not know about. `oidc_callback_url` and `updated_at` are refused in the body — a writable callback URL would let a caller point the return leg of an OIDC sign-in, which carries an authorization code, at a host they own. \n\n`400 validation_error` is where the three field rules land: a URL template must be https (or `http` on `localhost`) and carry its placeholder exactly once, an origin must be a bare scheme-host-port with no path, and a domain must be lowercase. Each refuses at configuration time because each would otherwise fail silently later — a second placeholder leaves one occurrence literal in a mailed link, an origin with a path can never equal a browser's `Origin` header, and a capitalised domain can never match a lowercased address.",
2630
+ "description": "**A replace, not a merge, and `.strict()`**: `mcp_enabled` and `mcp_login_url` both arrive or the write is refused, so a client built against an older shape cannot silently clear a setting it does not know about. `oidc_callback_url` and `updated_at` are the server's, refused in this body as in every slice's — see `GET`'s notes for why. \n\n`mcp_login_url` answers to the same rule as the `urls` slice's three templates — https (or `http` on `localhost`), its placeholder exactly once — refused as `400 validation_error` rather than left to fail mid-OAuth, in a client's browser where no console screen is watching. \n\nThe merge is server-side against the stored row, so this write never disturbs the registration or urls slice.",
2426
2631
  "requestBody": {
2427
2632
  "required": true,
2428
2633
  "content": {
2429
2634
  "application/json": {
2430
2635
  "schema": {
2431
- "$ref": "#/components/schemas/put-app-auth-config-request"
2636
+ "$ref": "#/components/schemas/put-app-auth-mcp-request"
2432
2637
  }
2433
2638
  }
2434
2639
  }
@@ -5022,6 +5227,112 @@
5022
5227
  }
5023
5228
  }
5024
5229
  },
5230
+ "/api/robots/{id}/token/rotate": {
5231
+ "post": {
5232
+ "operationId": "post_api_robots_id_token_rotate",
5233
+ "summary": "Mints a new bridge token for the robot and invalidates the old one.",
5234
+ "tags": [
5235
+ "robots"
5236
+ ],
5237
+ "security": [
5238
+ {
5239
+ "developerSession": []
5240
+ }
5241
+ ],
5242
+ "parameters": [
5243
+ {
5244
+ "name": "id",
5245
+ "in": "path",
5246
+ "required": true,
5247
+ "description": "The robot's uuid, as returned by `POST /api/robots` or listed by `GET /api/robots`.",
5248
+ "schema": {
5249
+ "type": "string"
5250
+ }
5251
+ }
5252
+ ],
5253
+ "responses": {
5254
+ "201": {
5255
+ "description": "Success.",
5256
+ "content": {
5257
+ "application/json": {
5258
+ "schema": {
5259
+ "$ref": "#/components/schemas/robot-token-rotate-response"
5260
+ }
5261
+ }
5262
+ }
5263
+ },
5264
+ "default": {
5265
+ "description": "An error envelope. Codes this route is known to answer: `unauthorized`, `token_expired`, `token_revoked`, `tier_required`, `invalid_uuid`, `not_found`.",
5266
+ "content": {
5267
+ "application/json": {
5268
+ "schema": {
5269
+ "$ref": "#/components/schemas/api-error"
5270
+ }
5271
+ }
5272
+ }
5273
+ }
5274
+ },
5275
+ "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."
5276
+ }
5277
+ },
5278
+ "/api/robots/{id}/urdf/joint-state": {
5279
+ "put": {
5280
+ "operationId": "put_api_robots_id_urdf_joint_state",
5281
+ "summary": "Chooses the datapoint whose joint positions move the robot's URDF, or clears it.",
5282
+ "tags": [
5283
+ "robots"
5284
+ ],
5285
+ "security": [
5286
+ {
5287
+ "developerSession": []
5288
+ }
5289
+ ],
5290
+ "parameters": [
5291
+ {
5292
+ "name": "id",
5293
+ "in": "path",
5294
+ "required": true,
5295
+ "description": "The robot's uuid, as returned by `POST /api/robots` or listed by `GET /api/robots`.",
5296
+ "schema": {
5297
+ "type": "string"
5298
+ }
5299
+ }
5300
+ ],
5301
+ "responses": {
5302
+ "200": {
5303
+ "description": "Success.",
5304
+ "content": {
5305
+ "application/json": {
5306
+ "schema": {
5307
+ "$ref": "#/components/schemas/joint-state-put-response"
5308
+ }
5309
+ }
5310
+ }
5311
+ },
5312
+ "default": {
5313
+ "description": "An error envelope. Codes this route is known to answer: `unauthorized`, `token_expired`, `token_revoked`, `invalid_uuid`, `not_found`, `validation_error`.",
5314
+ "content": {
5315
+ "application/json": {
5316
+ "schema": {
5317
+ "$ref": "#/components/schemas/api-error"
5318
+ }
5319
+ }
5320
+ }
5321
+ }
5322
+ },
5323
+ "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.",
5324
+ "requestBody": {
5325
+ "required": true,
5326
+ "content": {
5327
+ "application/json": {
5328
+ "schema": {
5329
+ "$ref": "#/components/schemas/joint-state-put-request"
5330
+ }
5331
+ }
5332
+ }
5333
+ }
5334
+ }
5335
+ },
5025
5336
  "/api/robots/{id}": {
5026
5337
  "get": {
5027
5338
  "operationId": "get_api_robots_id",
@@ -7282,6 +7593,58 @@
7282
7593
  }
7283
7594
  },
7284
7595
  "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."
7596
+ },
7597
+ "delete": {
7598
+ "operationId": "delete_api_robots_id_assets",
7599
+ "summary": "Empties a robot's asset store: every URDF, mesh and texture, gone at once.",
7600
+ "tags": [
7601
+ "assets"
7602
+ ],
7603
+ "security": [
7604
+ {
7605
+ "developerSession": []
7606
+ },
7607
+ {
7608
+ "clientToken": []
7609
+ },
7610
+ {
7611
+ "serverKey": []
7612
+ }
7613
+ ],
7614
+ "parameters": [
7615
+ {
7616
+ "name": "id",
7617
+ "in": "path",
7618
+ "required": true,
7619
+ "description": "The robot's uuid, as returned by `POST /api/robots` or listed by `GET /api/robots`.",
7620
+ "schema": {
7621
+ "type": "string"
7622
+ }
7623
+ }
7624
+ ],
7625
+ "responses": {
7626
+ "200": {
7627
+ "description": "Success.",
7628
+ "content": {
7629
+ "application/json": {
7630
+ "schema": {
7631
+ "$ref": "#/components/schemas/assets-clear-response"
7632
+ }
7633
+ }
7634
+ }
7635
+ },
7636
+ "default": {
7637
+ "description": "An error envelope. Codes this route is known to answer: `unauthorized`, `token_expired`, `token_revoked`, `forbidden`, `tier_required`, `invalid_uuid`, `not_found`, `busy`.",
7638
+ "content": {
7639
+ "application/json": {
7640
+ "schema": {
7641
+ "$ref": "#/components/schemas/api-error"
7642
+ }
7643
+ }
7644
+ }
7645
+ }
7646
+ },
7647
+ "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
7648
  }
7286
7649
  },
7287
7650
  "/api/robots/{id}/assets/{assetId}": {
@@ -8516,6 +8879,56 @@
8516
8879
  ],
8517
8880
  "additionalProperties": false
8518
8881
  },
8882
+ "app-deletion-summary": {
8883
+ "type": "object",
8884
+ "properties": {
8885
+ "user_count": {
8886
+ "type": "integer",
8887
+ "minimum": 0,
8888
+ "maximum": 9007199254740991,
8889
+ "description": "App users deleted with the app. They are the developer's own customers, not Fleetless users, and exist in no other app."
8890
+ },
8891
+ "role_count": {
8892
+ "type": "integer",
8893
+ "minimum": 0,
8894
+ "maximum": 9007199254740991,
8895
+ "description": "Roles deleted with the app, each with its per-robot slug grants."
8896
+ },
8897
+ "server_key_count": {
8898
+ "type": "integer",
8899
+ "minimum": 0,
8900
+ "maximum": 9007199254740991,
8901
+ "description": "Server keys deleted with the app. A client still holding one is refused at its next request."
8902
+ },
8903
+ "invitation_count": {
8904
+ "type": "integer",
8905
+ "minimum": 0,
8906
+ "maximum": 9007199254740991,
8907
+ "description": "Outstanding invitations — unspent and unexpired — that will never be accepted."
8908
+ },
8909
+ "oidc_provider_count": {
8910
+ "type": "integer",
8911
+ "minimum": 0,
8912
+ "maximum": 9007199254740991,
8913
+ "description": "Identity providers configured for this app. The providers themselves are somebody else's; only this app's configuration of them goes."
8914
+ },
8915
+ "mail_template_count": {
8916
+ "type": "integer",
8917
+ "minimum": 0,
8918
+ "maximum": 9007199254740991,
8919
+ "description": "Custom mail templates, of at most three. A kind using the Fleetless default text is not counted — there is no row to lose."
8920
+ }
8921
+ },
8922
+ "required": [
8923
+ "user_count",
8924
+ "role_count",
8925
+ "server_key_count",
8926
+ "invitation_count",
8927
+ "oidc_provider_count",
8928
+ "mail_template_count"
8929
+ ],
8930
+ "additionalProperties": false
8931
+ },
8519
8932
  "app-invitation": {
8520
8933
  "type": "object",
8521
8934
  "properties": {
@@ -9244,10 +9657,9 @@
9244
9657
  "enum": [
9245
9658
  "urdf",
9246
9659
  "mesh",
9247
- "texture",
9248
- "other"
9660
+ "texture"
9249
9661
  ],
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."
9662
+ "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
9663
  },
9252
9664
  "name": {
9253
9665
  "type": "string",
@@ -9348,32 +9760,38 @@
9348
9760
  "enum": [
9349
9761
  "unresolvable",
9350
9762
  "upload_failed",
9351
- "refused",
9352
- "too_large"
9763
+ "refused"
9353
9764
  ],
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`."
9765
+ "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
9766
  },
9356
9767
  "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.",
9768
+ "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
9769
  "anyOf": [
9359
9770
  {
9360
9771
  "type": "object",
9361
9772
  "properties": {
9362
- "limit_bytes": {
9773
+ "store_bytes": {
9363
9774
  "type": "integer",
9364
9775
  "exclusiveMinimum": 0,
9365
9776
  "maximum": 9007199254740991,
9366
- "description": "The upload ceiling, in bytes."
9777
+ "description": "The robot's store, in bytes."
9778
+ },
9779
+ "used_bytes": {
9780
+ "type": "integer",
9781
+ "minimum": 0,
9782
+ "maximum": 9007199254740991,
9783
+ "description": "Bytes the robot's assets occupy before this upload."
9367
9784
  },
9368
9785
  "size_bytes": {
9369
9786
  "type": "integer",
9370
9787
  "exclusiveMinimum": 0,
9371
9788
  "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."
9789
+ "description": "The refused upload, in bytes."
9373
9790
  }
9374
9791
  },
9375
9792
  "required": [
9376
- "limit_bytes",
9793
+ "store_bytes",
9794
+ "used_bytes",
9377
9795
  "size_bytes"
9378
9796
  ],
9379
9797
  "additionalProperties": false
@@ -9404,6 +9822,25 @@
9404
9822
  ],
9405
9823
  "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
9824
  },
9825
+ "stored": {
9826
+ "anyOf": [
9827
+ {
9828
+ "type": "integer",
9829
+ "minimum": 0,
9830
+ "maximum": 9007199254740991
9831
+ },
9832
+ {
9833
+ "type": "null"
9834
+ }
9835
+ ],
9836
+ "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."
9837
+ },
9838
+ "announced": {
9839
+ "type": "integer",
9840
+ "minimum": 0,
9841
+ "maximum": 9007199254740991,
9842
+ "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."
9843
+ },
9407
9844
  "started_at": {
9408
9845
  "type": "string",
9409
9846
  "format": "date-time",
@@ -9425,6 +9862,8 @@
9425
9862
  "total",
9426
9863
  "failed",
9427
9864
  "reason",
9865
+ "stored",
9866
+ "announced",
9428
9867
  "started_at",
9429
9868
  "updated_at"
9430
9869
  ],
@@ -9496,13 +9935,52 @@
9496
9935
  }
9497
9936
  ],
9498
9937
  "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."
9938
+ },
9939
+ "store": {
9940
+ "type": "object",
9941
+ "properties": {
9942
+ "bytes": {
9943
+ "type": "integer",
9944
+ "exclusiveMinimum": 0,
9945
+ "maximum": 9007199254740991,
9946
+ "description": "The robot's asset store, `ROBOT_ASSET_STORE_BYTES`."
9947
+ },
9948
+ "used_bytes": {
9949
+ "type": "integer",
9950
+ "minimum": 0,
9951
+ "maximum": 9007199254740991,
9952
+ "description": "Bytes its assets occupy."
9953
+ }
9954
+ },
9955
+ "required": [
9956
+ "bytes",
9957
+ "used_bytes"
9958
+ ],
9959
+ "additionalProperties": false,
9960
+ "description": "How full this robot's store is."
9961
+ },
9962
+ "joint_state_slug": {
9963
+ "anyOf": [
9964
+ {
9965
+ "type": "string",
9966
+ "minLength": 2,
9967
+ "maxLength": 63,
9968
+ "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$"
9969
+ },
9970
+ {
9971
+ "type": "null"
9972
+ }
9973
+ ],
9974
+ "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
9975
  }
9500
9976
  },
9501
9977
  "required": [
9502
9978
  "assets",
9503
9979
  "active_sync",
9504
9980
  "urdf",
9505
- "urdf_available"
9981
+ "urdf_available",
9982
+ "store",
9983
+ "joint_state_slug"
9506
9984
  ],
9507
9985
  "additionalProperties": false
9508
9986
  },
@@ -9590,32 +10068,38 @@
9590
10068
  "enum": [
9591
10069
  "unresolvable",
9592
10070
  "upload_failed",
9593
- "refused",
9594
- "too_large"
10071
+ "refused"
9595
10072
  ],
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`."
10073
+ "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
10074
  },
9598
10075
  "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.",
10076
+ "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
10077
  "anyOf": [
9601
10078
  {
9602
10079
  "type": "object",
9603
10080
  "properties": {
9604
- "limit_bytes": {
10081
+ "store_bytes": {
10082
+ "type": "integer",
10083
+ "exclusiveMinimum": 0,
10084
+ "maximum": 9007199254740991,
10085
+ "description": "The robot's store, in bytes."
10086
+ },
10087
+ "used_bytes": {
9605
10088
  "type": "integer",
9606
- "exclusiveMinimum": 0,
10089
+ "minimum": 0,
9607
10090
  "maximum": 9007199254740991,
9608
- "description": "The upload ceiling, in bytes."
10091
+ "description": "Bytes the robot's assets occupy before this upload."
9609
10092
  },
9610
10093
  "size_bytes": {
9611
10094
  "type": "integer",
9612
10095
  "exclusiveMinimum": 0,
9613
10096
  "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."
10097
+ "description": "The refused upload, in bytes."
9615
10098
  }
9616
10099
  },
9617
10100
  "required": [
9618
- "limit_bytes",
10101
+ "store_bytes",
10102
+ "used_bytes",
9619
10103
  "size_bytes"
9620
10104
  ],
9621
10105
  "additionalProperties": false
@@ -9646,6 +10130,25 @@
9646
10130
  ],
9647
10131
  "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
10132
  },
10133
+ "stored": {
10134
+ "anyOf": [
10135
+ {
10136
+ "type": "integer",
10137
+ "minimum": 0,
10138
+ "maximum": 9007199254740991
10139
+ },
10140
+ {
10141
+ "type": "null"
10142
+ }
10143
+ ],
10144
+ "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."
10145
+ },
10146
+ "announced": {
10147
+ "type": "integer",
10148
+ "minimum": 0,
10149
+ "maximum": 9007199254740991,
10150
+ "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."
10151
+ },
9649
10152
  "started_at": {
9650
10153
  "type": "string",
9651
10154
  "format": "date-time",
@@ -9667,11 +10170,35 @@
9667
10170
  "total",
9668
10171
  "failed",
9669
10172
  "reason",
10173
+ "stored",
10174
+ "announced",
9670
10175
  "started_at",
9671
10176
  "updated_at"
9672
10177
  ],
9673
10178
  "additionalProperties": false
9674
10179
  },
10180
+ "assets-clear-response": {
10181
+ "type": "object",
10182
+ "properties": {
10183
+ "deleted": {
10184
+ "type": "integer",
10185
+ "minimum": 0,
10186
+ "maximum": 9007199254740991,
10187
+ "description": "How many assets — URDF, meshes and textures together — were removed."
10188
+ },
10189
+ "bytes_freed": {
10190
+ "type": "integer",
10191
+ "minimum": 0,
10192
+ "maximum": 9007199254740991,
10193
+ "description": "The bytes the robot's store got back."
10194
+ }
10195
+ },
10196
+ "required": [
10197
+ "deleted",
10198
+ "bytes_freed"
10199
+ ],
10200
+ "additionalProperties": false
10201
+ },
9675
10202
  "audit-list-response": {
9676
10203
  "type": "object",
9677
10204
  "properties": {
@@ -10535,11 +11062,16 @@
10535
11062
  "type": "null"
10536
11063
  }
10537
11064
  ]
11065
+ },
11066
+ "low_bandwidth": {
11067
+ "type": "boolean",
11068
+ "description": "Whether the bridge is in its low-bandwidth mode: datapoints capped, cameras reduced or stopped. Bridge-reported."
10538
11069
  }
10539
11070
  },
10540
11071
  "required": [
10541
11072
  "online",
10542
- "latency_ms"
11073
+ "latency_ms",
11074
+ "low_bandwidth"
10543
11075
  ],
10544
11076
  "additionalProperties": false,
10545
11077
  "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 +11197,11 @@
10665
11197
  0.5
10666
11198
  ]
10667
11199
  },
11200
+ "low_bandwidth": {
11201
+ "description": "`keep` exempts this datapoint from the low-bandwidth rate cap; its backfill still pauses.",
11202
+ "type": "string",
11203
+ "const": "keep"
11204
+ },
10668
11205
  "description": {
10669
11206
  "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
11207
  "examples": [
@@ -10862,7 +11399,7 @@
10862
11399
  ],
10863
11400
  "additionalProperties": false
10864
11401
  },
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."
11402
+ "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
11403
  },
10867
11404
  "actions": {
10868
11405
  "type": "object",
@@ -11012,7 +11549,7 @@
11012
11549
  ],
11013
11550
  "additionalProperties": false
11014
11551
  },
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."
11552
+ "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
11553
  },
11017
11554
  "services": {
11018
11555
  "type": "object",
@@ -11162,7 +11699,7 @@
11162
11699
  ],
11163
11700
  "additionalProperties": false
11164
11701
  },
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."
11702
+ "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
11703
  },
11167
11704
  "publishers": {
11168
11705
  "type": "object",
@@ -11348,7 +11885,7 @@
11348
11885
  ],
11349
11886
  "additionalProperties": false
11350
11887
  },
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."
11888
+ "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
11889
  },
11353
11890
  "cameras": {
11354
11891
  "type": "object",
@@ -11596,7 +12133,67 @@
11596
12133
  ],
11597
12134
  "additionalProperties": false
11598
12135
  },
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."
12136
+ "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."
12137
+ },
12138
+ "low_bandwidth": {
12139
+ "description": "Overrides for the bridge's low-bandwidth mode; see the section schema.",
12140
+ "type": "object",
12141
+ "properties": {
12142
+ "mode": {
12143
+ "description": "`auto` decides from the measured lag; `on` and `off` force the mode, for tests and for an operator who knows the link.",
12144
+ "type": "string",
12145
+ "enum": [
12146
+ "auto",
12147
+ "on",
12148
+ "off"
12149
+ ]
12150
+ },
12151
+ "enter_lag_ms": {
12152
+ "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.",
12153
+ "type": "integer",
12154
+ "minimum": 100,
12155
+ "maximum": 9007199254740991
12156
+ },
12157
+ "enter_after_s": {
12158
+ "description": "The entry condition must hold this long.",
12159
+ "type": "integer",
12160
+ "minimum": 1,
12161
+ "maximum": 9007199254740991
12162
+ },
12163
+ "exit_lag_ms": {
12164
+ "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.",
12165
+ "type": "integer",
12166
+ "minimum": 0,
12167
+ "maximum": 9007199254740991
12168
+ },
12169
+ "exit_after_s": {
12170
+ "description": "The exit condition must hold this long.",
12171
+ "type": "integer",
12172
+ "minimum": 1,
12173
+ "maximum": 9007199254740991
12174
+ },
12175
+ "datapoint_max_hz": {
12176
+ "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.",
12177
+ "type": "number",
12178
+ "exclusiveMinimum": 0,
12179
+ "maximum": 20
12180
+ },
12181
+ "camera": {
12182
+ "description": "What happens to a running stream in the mode. New streams are refused either way.",
12183
+ "type": "string",
12184
+ "enum": [
12185
+ "reduce",
12186
+ "stop"
12187
+ ]
12188
+ },
12189
+ "camera_bitrate_kbps": {
12190
+ "description": "Bitrate applied to running streams under `reduce`.",
12191
+ "type": "integer",
12192
+ "minimum": 50,
12193
+ "maximum": 20000
12194
+ }
12195
+ },
12196
+ "additionalProperties": false
11600
12197
  }
11601
12198
  },
11602
12199
  "required": [
@@ -11762,6 +12359,11 @@
11762
12359
  0.5
11763
12360
  ]
11764
12361
  },
12362
+ "low_bandwidth": {
12363
+ "description": "`keep` exempts this datapoint from the low-bandwidth rate cap; its backfill still pauses.",
12364
+ "type": "string",
12365
+ "const": "keep"
12366
+ },
11765
12367
  "description": {
11766
12368
  "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
12369
  "examples": [
@@ -11959,7 +12561,7 @@
11959
12561
  ],
11960
12562
  "additionalProperties": false
11961
12563
  },
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."
12564
+ "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
12565
  },
11964
12566
  "actions": {
11965
12567
  "type": "object",
@@ -12109,7 +12711,7 @@
12109
12711
  ],
12110
12712
  "additionalProperties": false
12111
12713
  },
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."
12714
+ "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
12715
  },
12114
12716
  "services": {
12115
12717
  "type": "object",
@@ -12259,7 +12861,7 @@
12259
12861
  ],
12260
12862
  "additionalProperties": false
12261
12863
  },
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."
12864
+ "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
12865
  },
12264
12866
  "publishers": {
12265
12867
  "type": "object",
@@ -12445,7 +13047,7 @@
12445
13047
  ],
12446
13048
  "additionalProperties": false
12447
13049
  },
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."
13050
+ "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
13051
  },
12450
13052
  "cameras": {
12451
13053
  "type": "object",
@@ -12693,7 +13295,67 @@
12693
13295
  ],
12694
13296
  "additionalProperties": false
12695
13297
  },
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."
13298
+ "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."
13299
+ },
13300
+ "low_bandwidth": {
13301
+ "description": "Overrides for the bridge's low-bandwidth mode; see the section schema.",
13302
+ "type": "object",
13303
+ "properties": {
13304
+ "mode": {
13305
+ "description": "`auto` decides from the measured lag; `on` and `off` force the mode, for tests and for an operator who knows the link.",
13306
+ "type": "string",
13307
+ "enum": [
13308
+ "auto",
13309
+ "on",
13310
+ "off"
13311
+ ]
13312
+ },
13313
+ "enter_lag_ms": {
13314
+ "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.",
13315
+ "type": "integer",
13316
+ "minimum": 100,
13317
+ "maximum": 9007199254740991
13318
+ },
13319
+ "enter_after_s": {
13320
+ "description": "The entry condition must hold this long.",
13321
+ "type": "integer",
13322
+ "minimum": 1,
13323
+ "maximum": 9007199254740991
13324
+ },
13325
+ "exit_lag_ms": {
13326
+ "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.",
13327
+ "type": "integer",
13328
+ "minimum": 0,
13329
+ "maximum": 9007199254740991
13330
+ },
13331
+ "exit_after_s": {
13332
+ "description": "The exit condition must hold this long.",
13333
+ "type": "integer",
13334
+ "minimum": 1,
13335
+ "maximum": 9007199254740991
13336
+ },
13337
+ "datapoint_max_hz": {
13338
+ "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.",
13339
+ "type": "number",
13340
+ "exclusiveMinimum": 0,
13341
+ "maximum": 20
13342
+ },
13343
+ "camera": {
13344
+ "description": "What happens to a running stream in the mode. New streams are refused either way.",
13345
+ "type": "string",
13346
+ "enum": [
13347
+ "reduce",
13348
+ "stop"
13349
+ ]
13350
+ },
13351
+ "camera_bitrate_kbps": {
13352
+ "description": "Bitrate applied to running streams under `reduce`.",
13353
+ "type": "integer",
13354
+ "minimum": 50,
13355
+ "maximum": 20000
13356
+ }
13357
+ },
13358
+ "additionalProperties": false
12697
13359
  }
12698
13360
  },
12699
13361
  "required": [
@@ -13102,7 +13764,7 @@
13102
13764
  },
13103
13765
  "builtin": {
13104
13766
  "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."
13767
+ "description": "`true` for the datapoints every robot has — `bridge_state` and `robot_details` — and `false` for everything the published configuration adds."
13106
13768
  },
13107
13769
  "unit": {
13108
13770
  "anyOf": [
@@ -13137,7 +13799,7 @@
13137
13799
  ],
13138
13800
  "additionalProperties": false
13139
13801
  },
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."
13802
+ "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
13803
  }
13142
13804
  },
13143
13805
  "required": [
@@ -14452,6 +15114,51 @@
14452
15114
  ],
14453
15115
  "additionalProperties": false
14454
15116
  },
15117
+ "joint-state-put-request": {
15118
+ "type": "object",
15119
+ "properties": {
15120
+ "slug": {
15121
+ "anyOf": [
15122
+ {
15123
+ "type": "string",
15124
+ "minLength": 2,
15125
+ "maxLength": 63,
15126
+ "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$"
15127
+ },
15128
+ {
15129
+ "type": "null"
15130
+ }
15131
+ ],
15132
+ "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."
15133
+ }
15134
+ },
15135
+ "required": [
15136
+ "slug"
15137
+ ]
15138
+ },
15139
+ "joint-state-put-response": {
15140
+ "type": "object",
15141
+ "properties": {
15142
+ "joint_state_slug": {
15143
+ "anyOf": [
15144
+ {
15145
+ "type": "string",
15146
+ "minLength": 2,
15147
+ "maxLength": 63,
15148
+ "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$"
15149
+ },
15150
+ {
15151
+ "type": "null"
15152
+ }
15153
+ ],
15154
+ "description": "The stored mapping after the call, `null` when none is chosen. The same value `assetListResponse.joint_state_slug` carries."
15155
+ }
15156
+ },
15157
+ "required": [
15158
+ "joint_state_slug"
15159
+ ],
15160
+ "additionalProperties": false
15161
+ },
14455
15162
  "live-session-response": {
14456
15163
  "type": "object",
14457
15164
  "properties": {
@@ -15329,11 +16036,6 @@
15329
16036
  "type": "integer",
15330
16037
  "exclusiveMinimum": 0,
15331
16038
  "maximum": 9007199254740991
15332
- },
15333
- "max_asset_storage_bytes": {
15334
- "type": "integer",
15335
- "minimum": 0,
15336
- "maximum": 9007199254740991
15337
16039
  }
15338
16040
  },
15339
16041
  "required": [
@@ -15342,8 +16044,7 @@
15342
16044
  "max_end_users",
15343
16045
  "max_retention_bytes",
15344
16046
  "max_retention_writes_per_minute",
15345
- "max_realtime_connections",
15346
- "max_asset_storage_bytes"
16047
+ "max_realtime_connections"
15347
16048
  ],
15348
16049
  "additionalProperties": false
15349
16050
  },
@@ -15370,11 +16071,6 @@
15370
16071
  "minimum": 0,
15371
16072
  "maximum": 9007199254740991
15372
16073
  },
15373
- "max_asset_storage_bytes": {
15374
- "type": "integer",
15375
- "minimum": 0,
15376
- "maximum": 9007199254740991
15377
- },
15378
16074
  "max_retention_writes_per_minute": {
15379
16075
  "type": "integer",
15380
16076
  "minimum": 0,
@@ -15877,7 +16573,33 @@
15877
16573
  "message"
15878
16574
  ]
15879
16575
  },
15880
- "put-app-auth-config-request": {
16576
+ "put-app-auth-mcp-request": {
16577
+ "type": "object",
16578
+ "properties": {
16579
+ "mcp_enabled": {
16580
+ "type": "boolean",
16581
+ "description": "Whether this app serves an MCP endpoint at `/mcp/<identifier>`. Off refuses the whole OAuth surface for the app, not merely the tool calls, and is re-read on every request rather than cached off a token."
16582
+ },
16583
+ "mcp_login_url": {
16584
+ "anyOf": [
16585
+ {
16586
+ "type": "string",
16587
+ "maxLength": 500
16588
+ },
16589
+ {
16590
+ "type": "null"
16591
+ }
16592
+ ],
16593
+ "description": "The page an MCP authorization redirects to, with `{interaction}` where the interaction id goes. Not a token: the id names a pending request the server already holds, and the app authenticates the user itself before approving it."
16594
+ }
16595
+ },
16596
+ "required": [
16597
+ "mcp_enabled",
16598
+ "mcp_login_url"
16599
+ ],
16600
+ "additionalProperties": false
16601
+ },
16602
+ "put-app-auth-registration-request": {
15881
16603
  "type": "object",
15882
16604
  "properties": {
15883
16605
  "self_registration": {
@@ -15903,11 +16625,18 @@
15903
16625
  "maxLength": 200
15904
16626
  },
15905
16627
  "description": "The origins the client auth API answers CORS for, and the only origins an OIDC `redirect_uri` may name. Bare origins: scheme, host and port, with no path — a browser sends nothing longer, so an entry carrying one could never match."
15906
- },
15907
- "mcp_enabled": {
15908
- "type": "boolean",
15909
- "description": "Whether this app serves an MCP endpoint at `/mcp/<identifier>`. Off refuses the whole OAuth surface for the app, not merely the tool calls, and is re-read on every request rather than cached off a token."
15910
- },
16628
+ }
16629
+ },
16630
+ "required": [
16631
+ "self_registration",
16632
+ "allowed_domains",
16633
+ "allowed_origins"
16634
+ ],
16635
+ "additionalProperties": false
16636
+ },
16637
+ "put-app-auth-urls-request": {
16638
+ "type": "object",
16639
+ "properties": {
15911
16640
  "invite_url": {
15912
16641
  "anyOf": [
15913
16642
  {
@@ -15943,29 +16672,12 @@
15943
16672
  }
15944
16673
  ],
15945
16674
  "description": "The page that takes a new password, with `{token}` where the token goes."
15946
- },
15947
- "mcp_login_url": {
15948
- "anyOf": [
15949
- {
15950
- "type": "string",
15951
- "maxLength": 500
15952
- },
15953
- {
15954
- "type": "null"
15955
- }
15956
- ],
15957
- "description": "The page an MCP authorization redirects to, with `{interaction}` where the interaction id goes. Not a token: the id names a pending request the server already holds, and the app authenticates the user itself before approving it."
15958
16675
  }
15959
16676
  },
15960
16677
  "required": [
15961
- "self_registration",
15962
- "allowed_domains",
15963
- "allowed_origins",
15964
- "mcp_enabled",
15965
16678
  "invite_url",
15966
16679
  "verify_url",
15967
- "reset_url",
15968
- "mcp_login_url"
16680
+ "reset_url"
15969
16681
  ],
15970
16682
  "additionalProperties": false
15971
16683
  },
@@ -16268,7 +16980,8 @@
16268
16980
  "asset_bytes_freed": {
16269
16981
  "type": "integer",
16270
16982
  "minimum": 0,
16271
- "maximum": 9007199254740991
16983
+ "maximum": 9007199254740991,
16984
+ "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."
16272
16985
  },
16273
16986
  "job_run_count": {
16274
16987
  "type": "integer",
@@ -16332,11 +17045,16 @@
16332
17045
  "type": "null"
16333
17046
  }
16334
17047
  ]
17048
+ },
17049
+ "low_bandwidth": {
17050
+ "type": "boolean",
17051
+ "description": "Whether the bridge is in its low-bandwidth mode: datapoints capped, cameras reduced or stopped. Bridge-reported."
16335
17052
  }
16336
17053
  },
16337
17054
  "required": [
16338
17055
  "online",
16339
- "latency_ms"
17056
+ "latency_ms",
17057
+ "low_bandwidth"
16340
17058
  ],
16341
17059
  "additionalProperties": false
16342
17060
  },
@@ -16378,6 +17096,15 @@
16378
17096
  ],
16379
17097
  "additionalProperties": false
16380
17098
  },
17099
+ "protocol_status": {
17100
+ "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`.",
17101
+ "type": "string",
17102
+ "enum": [
17103
+ "current",
17104
+ "deprecated",
17105
+ "refused"
17106
+ ]
17107
+ },
16381
17108
  "bridge_version": {
16382
17109
  "anyOf": [
16383
17110
  {
@@ -16389,6 +17116,52 @@
16389
17116
  }
16390
17117
  ]
16391
17118
  },
17119
+ "protocol_version": {
17120
+ "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.",
17121
+ "anyOf": [
17122
+ {
17123
+ "type": "integer",
17124
+ "exclusiveMinimum": 0,
17125
+ "maximum": 9007199254740991
17126
+ },
17127
+ {
17128
+ "type": "null"
17129
+ }
17130
+ ]
17131
+ },
17132
+ "protocol": {
17133
+ "description": "The window verdict for `protocol_version`.",
17134
+ "type": "object",
17135
+ "properties": {
17136
+ "status": {
17137
+ "type": "string",
17138
+ "enum": [
17139
+ "current",
17140
+ "deprecated",
17141
+ "refused"
17142
+ ],
17143
+ "description": "Same values as `protocol_status`."
17144
+ },
17145
+ "sunset_at": {
17146
+ "anyOf": [
17147
+ {
17148
+ "type": "string",
17149
+ "format": "date",
17150
+ "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])))$"
17151
+ },
17152
+ {
17153
+ "type": "null"
17154
+ }
17155
+ ],
17156
+ "description": "ISO date the announced version stops being served; `null` when current or unknown."
17157
+ }
17158
+ },
17159
+ "required": [
17160
+ "status",
17161
+ "sunset_at"
17162
+ ],
17163
+ "additionalProperties": false
17164
+ },
16392
17165
  "last_hello_error": {
16393
17166
  "anyOf": [
16394
17167
  {
@@ -16498,7 +17271,8 @@
16498
17271
  "action",
16499
17272
  "service",
16500
17273
  "publisher",
16501
- "camera"
17274
+ "camera",
17275
+ "low_bandwidth"
16502
17276
  ]
16503
17277
  },
16504
17278
  "code": {
@@ -16716,11 +17490,16 @@
16716
17490
  "type": "null"
16717
17491
  }
16718
17492
  ]
17493
+ },
17494
+ "low_bandwidth": {
17495
+ "type": "boolean",
17496
+ "description": "Whether the bridge is in its low-bandwidth mode: datapoints capped, cameras reduced or stopped. Bridge-reported."
16719
17497
  }
16720
17498
  },
16721
17499
  "required": [
16722
17500
  "online",
16723
- "latency_ms"
17501
+ "latency_ms",
17502
+ "low_bandwidth"
16724
17503
  ],
16725
17504
  "additionalProperties": false
16726
17505
  },
@@ -16761,6 +17540,15 @@
16761
17540
  "cameras"
16762
17541
  ],
16763
17542
  "additionalProperties": false
17543
+ },
17544
+ "protocol_status": {
17545
+ "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`.",
17546
+ "type": "string",
17547
+ "enum": [
17548
+ "current",
17549
+ "deprecated",
17550
+ "refused"
17551
+ ]
16764
17552
  }
16765
17553
  },
16766
17554
  "required": [
@@ -16779,6 +17567,20 @@
16779
17567
  ],
16780
17568
  "additionalProperties": false
16781
17569
  },
17570
+ "robot-token-rotate-response": {
17571
+ "type": "object",
17572
+ "properties": {
17573
+ "token": {
17574
+ "type": "string",
17575
+ "pattern": "^frt_[0-9a-f]{32}$",
17576
+ "description": "The robot's new bridge token. Returned exactly once; the previous token stops working at the bridge's next hello."
17577
+ }
17578
+ },
17579
+ "required": [
17580
+ "token"
17581
+ ],
17582
+ "additionalProperties": false
17583
+ },
16782
17584
  "role": {
16783
17585
  "type": "object",
16784
17586
  "properties": {