@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
@@ -0,0 +1,23 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "properties": {
5
+ "slug": {
6
+ "anyOf": [
7
+ {
8
+ "type": "string",
9
+ "minLength": 2,
10
+ "maxLength": 63,
11
+ "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$"
12
+ },
13
+ {
14
+ "type": "null"
15
+ }
16
+ ],
17
+ "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."
18
+ }
19
+ },
20
+ "required": [
21
+ "slug"
22
+ ]
23
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "properties": {
5
+ "joint_state_slug": {
6
+ "anyOf": [
7
+ {
8
+ "type": "string",
9
+ "minLength": 2,
10
+ "maxLength": 63,
11
+ "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$"
12
+ },
13
+ {
14
+ "type": "null"
15
+ }
16
+ ],
17
+ "description": "The stored mapping after the call, `null` when none is chosen. The same value `assetListResponse.joint_state_slug` carries."
18
+ }
19
+ },
20
+ "required": [
21
+ "joint_state_slug"
22
+ ],
23
+ "additionalProperties": false
24
+ }
@@ -1,47 +1,85 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "type": "object",
4
- "properties": {
5
- "grant_type": {
6
- "type": "string",
7
- "const": "authorization_code",
8
- "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."
3
+ "oneOf": [
4
+ {
5
+ "type": "object",
6
+ "properties": {
7
+ "grant_type": {
8
+ "type": "string",
9
+ "const": "authorization_code",
10
+ "description": "`authorization_code`: this request exchanges the code from the authorize redirect for an access token and a refresh token."
11
+ },
12
+ "code": {
13
+ "type": "string",
14
+ "minLength": 1,
15
+ "maxLength": 500,
16
+ "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."
17
+ },
18
+ "redirect_uri": {
19
+ "type": "string",
20
+ "minLength": 1,
21
+ "maxLength": 2000,
22
+ "description": "The same redirect URI the authorize request used. It is compared, not merely recorded."
23
+ },
24
+ "client_id": {
25
+ "type": "string",
26
+ "minLength": 1,
27
+ "maxLength": 200,
28
+ "description": "The client making the exchange, as registered."
29
+ },
30
+ "code_verifier": {
31
+ "type": "string",
32
+ "pattern": "^[A-Za-z0-9\\-._~]{43,128}$",
33
+ "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."
34
+ },
35
+ "resource": {
36
+ "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.",
37
+ "type": "string",
38
+ "format": "uri"
39
+ }
40
+ },
41
+ "required": [
42
+ "grant_type",
43
+ "code",
44
+ "redirect_uri",
45
+ "client_id",
46
+ "code_verifier"
47
+ ],
48
+ "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."
9
49
  },
10
- "code": {
11
- "type": "string",
12
- "minLength": 1,
13
- "maxLength": 500,
14
- "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."
15
- },
16
- "redirect_uri": {
17
- "type": "string",
18
- "minLength": 1,
19
- "maxLength": 2000,
20
- "description": "The same redirect URI the authorize request used. It is compared, not merely recorded."
21
- },
22
- "client_id": {
23
- "type": "string",
24
- "minLength": 1,
25
- "maxLength": 200,
26
- "description": "The client making the exchange, as registered."
27
- },
28
- "code_verifier": {
29
- "type": "string",
30
- "pattern": "^[A-Za-z0-9\\-._~]{43,128}$",
31
- "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."
32
- },
33
- "resource": {
34
- "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.",
35
- "type": "string",
36
- "format": "uri"
50
+ {
51
+ "type": "object",
52
+ "properties": {
53
+ "grant_type": {
54
+ "type": "string",
55
+ "const": "refresh_token",
56
+ "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."
57
+ },
58
+ "refresh_token": {
59
+ "type": "string",
60
+ "minLength": 1,
61
+ "maxLength": 500,
62
+ "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."
63
+ },
64
+ "client_id": {
65
+ "type": "string",
66
+ "minLength": 1,
67
+ "maxLength": 200,
68
+ "description": "The client the refresh token was issued to, as registered. A refresh token is not transferable between clients."
69
+ },
70
+ "resource": {
71
+ "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.",
72
+ "type": "string",
73
+ "format": "uri"
74
+ }
75
+ },
76
+ "required": [
77
+ "grant_type",
78
+ "refresh_token",
79
+ "client_id"
80
+ ],
81
+ "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."
37
82
  }
38
- },
39
- "required": [
40
- "grant_type",
41
- "code",
42
- "redirect_uri",
43
- "client_id",
44
- "code_verifier"
45
83
  ],
46
- "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."
84
+ "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."
47
85
  }
@@ -19,7 +19,7 @@
19
19
  "description": "How long the access token is valid, in **seconds**, per RFC 6749 §5.1. Not a timestamp, and not milliseconds."
20
20
  },
21
21
  "refresh_token": {
22
- "description": "The refresh token, when one was issued. It rotates on every use.",
22
+ "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.",
23
23
  "type": "string",
24
24
  "minLength": 1
25
25
  },
@@ -22,11 +22,6 @@
22
22
  "minimum": 0,
23
23
  "maximum": 9007199254740991
24
24
  },
25
- "max_asset_storage_bytes": {
26
- "type": "integer",
27
- "minimum": 0,
28
- "maximum": 9007199254740991
29
- },
30
25
  "max_retention_writes_per_minute": {
31
26
  "type": "integer",
32
27
  "minimum": 0,
@@ -34,11 +34,6 @@
34
34
  "type": "integer",
35
35
  "exclusiveMinimum": 0,
36
36
  "maximum": 9007199254740991
37
- },
38
- "max_asset_storage_bytes": {
39
- "type": "integer",
40
- "minimum": 0,
41
- "maximum": 9007199254740991
42
37
  }
43
38
  },
44
39
  "required": [
@@ -47,8 +42,7 @@
47
42
  "max_end_users",
48
43
  "max_retention_bytes",
49
44
  "max_retention_writes_per_minute",
50
- "max_realtime_connections",
51
- "max_asset_storage_bytes"
45
+ "max_realtime_connections"
52
46
  ],
53
47
  "additionalProperties": false
54
48
  },
@@ -75,11 +69,6 @@
75
69
  "minimum": 0,
76
70
  "maximum": 9007199254740991
77
71
  },
78
- "max_asset_storage_bytes": {
79
- "type": "integer",
80
- "minimum": 0,
81
- "maximum": 9007199254740991
82
- },
83
72
  "max_retention_writes_per_minute": {
84
73
  "type": "integer",
85
74
  "minimum": 0,
@@ -31,11 +31,6 @@
31
31
  "type": "integer",
32
32
  "exclusiveMinimum": 0,
33
33
  "maximum": 9007199254740991
34
- },
35
- "max_asset_storage_bytes": {
36
- "type": "integer",
37
- "minimum": 0,
38
- "maximum": 9007199254740991
39
34
  }
40
35
  },
41
36
  "required": [
@@ -44,8 +39,7 @@
44
39
  "max_end_users",
45
40
  "max_retention_bytes",
46
41
  "max_retention_writes_per_minute",
47
- "max_realtime_connections",
48
- "max_asset_storage_bytes"
42
+ "max_realtime_connections"
49
43
  ],
50
44
  "additionalProperties": false
51
45
  }
@@ -105,6 +105,11 @@
105
105
  0.5
106
106
  ]
107
107
  },
108
+ "low_bandwidth": {
109
+ "description": "`keep` exempts this datapoint from the low-bandwidth rate cap; its backfill still pauses.",
110
+ "type": "string",
111
+ "const": "keep"
112
+ },
108
113
  "description": {
109
114
  "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.",
110
115
  "examples": [
@@ -425,7 +430,7 @@
425
430
  }
426
431
  ]
427
432
  },
428
- "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.",
433
+ "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.",
429
434
  "defaultSnippets": [
430
435
  {
431
436
  "label": "a datapoint",
@@ -673,7 +678,7 @@
673
678
  }
674
679
  ]
675
680
  },
676
- "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.",
681
+ "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.",
677
682
  "defaultSnippets": [
678
683
  {
679
684
  "label": "an action",
@@ -894,7 +899,7 @@
894
899
  }
895
900
  ]
896
901
  },
897
- "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.",
902
+ "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.",
898
903
  "defaultSnippets": [
899
904
  {
900
905
  "label": "a service",
@@ -1202,7 +1207,7 @@
1202
1207
  }
1203
1208
  ]
1204
1209
  },
1205
- "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.",
1210
+ "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.",
1206
1211
  "defaultSnippets": [
1207
1212
  {
1208
1213
  "label": "a publisher, with its parameters and its failsafe",
@@ -1586,7 +1591,7 @@
1586
1591
  }
1587
1592
  ]
1588
1593
  },
1589
- "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.",
1594
+ "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.",
1590
1595
  "defaultSnippets": [
1591
1596
  {
1592
1597
  "label": "a camera",
@@ -1607,6 +1612,86 @@
1607
1612
  }
1608
1613
  }
1609
1614
  ]
1615
+ },
1616
+ "low_bandwidth": {
1617
+ "description": "Overrides for the bridge's low-bandwidth mode; see the section schema.",
1618
+ "defaultSnippets": [
1619
+ {
1620
+ "label": "low-bandwidth mode, tuned",
1621
+ "description": "Enters after ten seconds above two seconds of lag, caps every datapoint to 1 Hz and lets running streams continue at a reduced bitrate.",
1622
+ "body": {
1623
+ "enter_lag_ms": 2000,
1624
+ "datapoint_max_hz": 1,
1625
+ "camera": "reduce"
1626
+ }
1627
+ }
1628
+ ],
1629
+ "type": "object",
1630
+ "properties": {
1631
+ "mode": {
1632
+ "description": "`auto` decides from the measured lag; `on` and `off` force the mode, for tests and for an operator who knows the link.",
1633
+ "enumDescriptions": [
1634
+ "The bridge enters and leaves the mode on its own, from the lag the cloud reports and the dwell it measures in its own send queue. The setting to leave alone.",
1635
+ "The mode is held on, whatever the link is doing. For a robot on a link known to be poor, and for a test that would otherwise have to wait for a real one.",
1636
+ "The mode never engages, whatever the link is doing. The robot then sends at its configured rates over a link that cannot carry them, which is a choice and not a default."
1637
+ ],
1638
+ "type": "string",
1639
+ "enum": [
1640
+ "auto",
1641
+ "on",
1642
+ "off"
1643
+ ]
1644
+ },
1645
+ "enter_lag_ms": {
1646
+ "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.",
1647
+ "type": "integer",
1648
+ "minimum": 100,
1649
+ "maximum": 9007199254740991
1650
+ },
1651
+ "enter_after_s": {
1652
+ "description": "The entry condition must hold this long.",
1653
+ "type": "integer",
1654
+ "minimum": 1,
1655
+ "maximum": 9007199254740991
1656
+ },
1657
+ "exit_lag_ms": {
1658
+ "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.",
1659
+ "type": "integer",
1660
+ "minimum": 0,
1661
+ "maximum": 9007199254740991
1662
+ },
1663
+ "exit_after_s": {
1664
+ "description": "The exit condition must hold this long.",
1665
+ "type": "integer",
1666
+ "minimum": 1,
1667
+ "maximum": 9007199254740991
1668
+ },
1669
+ "datapoint_max_hz": {
1670
+ "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.",
1671
+ "type": "number",
1672
+ "exclusiveMinimum": 0,
1673
+ "maximum": 20
1674
+ },
1675
+ "camera": {
1676
+ "description": "What happens to a running stream in the mode. New streams are refused either way.",
1677
+ "enumDescriptions": [
1678
+ "A running stream is re-encoded at `camera_bitrate_kbps` and keeps running. A viewer sees a worse picture rather than none.",
1679
+ "A running stream ends and the viewer is told why. The uplink is then free for datapoints, which is the right trade where video is the nice-to-have."
1680
+ ],
1681
+ "type": "string",
1682
+ "enum": [
1683
+ "reduce",
1684
+ "stop"
1685
+ ]
1686
+ },
1687
+ "camera_bitrate_kbps": {
1688
+ "description": "Bitrate applied to running streams under `reduce`.",
1689
+ "type": "integer",
1690
+ "minimum": 50,
1691
+ "maximum": 20000
1692
+ }
1693
+ },
1694
+ "additionalProperties": false
1610
1695
  }
1611
1696
  },
1612
1697
  "required": [
@@ -34,7 +34,8 @@
34
34
  "asset_bytes_freed": {
35
35
  "type": "integer",
36
36
  "minimum": 0,
37
- "maximum": 9007199254740991
37
+ "maximum": 9007199254740991,
38
+ "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."
38
39
  },
39
40
  "job_run_count": {
40
41
  "type": "integer",
@@ -36,11 +36,16 @@
36
36
  "type": "null"
37
37
  }
38
38
  ]
39
+ },
40
+ "low_bandwidth": {
41
+ "type": "boolean",
42
+ "description": "Whether the bridge is in its low-bandwidth mode: datapoints capped, cameras reduced or stopped. Bridge-reported."
39
43
  }
40
44
  },
41
45
  "required": [
42
46
  "online",
43
- "latency_ms"
47
+ "latency_ms",
48
+ "low_bandwidth"
44
49
  ],
45
50
  "additionalProperties": false
46
51
  },
@@ -82,6 +87,15 @@
82
87
  ],
83
88
  "additionalProperties": false
84
89
  },
90
+ "protocol_status": {
91
+ "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`.",
92
+ "type": "string",
93
+ "enum": [
94
+ "current",
95
+ "deprecated",
96
+ "refused"
97
+ ]
98
+ },
85
99
  "bridge_version": {
86
100
  "anyOf": [
87
101
  {
@@ -93,6 +107,52 @@
93
107
  }
94
108
  ]
95
109
  },
110
+ "protocol_version": {
111
+ "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.",
112
+ "anyOf": [
113
+ {
114
+ "type": "integer",
115
+ "exclusiveMinimum": 0,
116
+ "maximum": 9007199254740991
117
+ },
118
+ {
119
+ "type": "null"
120
+ }
121
+ ]
122
+ },
123
+ "protocol": {
124
+ "description": "The window verdict for `protocol_version`.",
125
+ "type": "object",
126
+ "properties": {
127
+ "status": {
128
+ "type": "string",
129
+ "enum": [
130
+ "current",
131
+ "deprecated",
132
+ "refused"
133
+ ],
134
+ "description": "Same values as `protocol_status`."
135
+ },
136
+ "sunset_at": {
137
+ "anyOf": [
138
+ {
139
+ "type": "string",
140
+ "format": "date",
141
+ "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])))$"
142
+ },
143
+ {
144
+ "type": "null"
145
+ }
146
+ ],
147
+ "description": "ISO date the announced version stops being served; `null` when current or unknown."
148
+ }
149
+ },
150
+ "required": [
151
+ "status",
152
+ "sunset_at"
153
+ ],
154
+ "additionalProperties": false
155
+ },
96
156
  "last_hello_error": {
97
157
  "anyOf": [
98
158
  {
@@ -202,7 +262,8 @@
202
262
  "action",
203
263
  "service",
204
264
  "publisher",
205
- "camera"
265
+ "camera",
266
+ "low_bandwidth"
206
267
  ]
207
268
  },
208
269
  "code": {
@@ -36,11 +36,16 @@
36
36
  "type": "null"
37
37
  }
38
38
  ]
39
+ },
40
+ "low_bandwidth": {
41
+ "type": "boolean",
42
+ "description": "Whether the bridge is in its low-bandwidth mode: datapoints capped, cameras reduced or stopped. Bridge-reported."
39
43
  }
40
44
  },
41
45
  "required": [
42
46
  "online",
43
- "latency_ms"
47
+ "latency_ms",
48
+ "low_bandwidth"
44
49
  ],
45
50
  "additionalProperties": false
46
51
  },
@@ -81,6 +86,15 @@
81
86
  "cameras"
82
87
  ],
83
88
  "additionalProperties": false
89
+ },
90
+ "protocol_status": {
91
+ "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`.",
92
+ "type": "string",
93
+ "enum": [
94
+ "current",
95
+ "deprecated",
96
+ "refused"
97
+ ]
84
98
  }
85
99
  },
86
100
  "required": [
@@ -41,11 +41,16 @@
41
41
  "type": "null"
42
42
  }
43
43
  ]
44
+ },
45
+ "low_bandwidth": {
46
+ "type": "boolean",
47
+ "description": "Whether the bridge is in its low-bandwidth mode: datapoints capped, cameras reduced or stopped. Bridge-reported."
44
48
  }
45
49
  },
46
50
  "required": [
47
51
  "online",
48
- "latency_ms"
52
+ "latency_ms",
53
+ "low_bandwidth"
49
54
  ],
50
55
  "additionalProperties": false
51
56
  },
@@ -86,6 +91,15 @@
86
91
  "cameras"
87
92
  ],
88
93
  "additionalProperties": false
94
+ },
95
+ "protocol_status": {
96
+ "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`.",
97
+ "type": "string",
98
+ "enum": [
99
+ "current",
100
+ "deprecated",
101
+ "refused"
102
+ ]
89
103
  }
90
104
  },
91
105
  "required": [
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "properties": {
5
+ "token": {
6
+ "type": "string",
7
+ "pattern": "^frt_[0-9a-f]{32}$",
8
+ "description": "The robot's new bridge token. Returned exactly once; the previous token stops working at the bridge's next hello."
9
+ }
10
+ },
11
+ "required": [
12
+ "token"
13
+ ],
14
+ "additionalProperties": false
15
+ }
@@ -38,32 +38,38 @@
38
38
  "enum": [
39
39
  "unresolvable",
40
40
  "upload_failed",
41
- "refused",
42
- "too_large"
41
+ "refused"
43
42
  ],
44
- "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`."
43
+ "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."
45
44
  },
46
45
  "details": {
47
- "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.",
46
+ "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.",
48
47
  "anyOf": [
49
48
  {
50
49
  "type": "object",
51
50
  "properties": {
52
- "limit_bytes": {
51
+ "store_bytes": {
53
52
  "type": "integer",
54
53
  "exclusiveMinimum": 0,
55
54
  "maximum": 9007199254740991,
56
- "description": "The upload ceiling, in bytes."
55
+ "description": "The robot's store, in bytes."
56
+ },
57
+ "used_bytes": {
58
+ "type": "integer",
59
+ "minimum": 0,
60
+ "maximum": 9007199254740991,
61
+ "description": "Bytes the robot's assets occupy before this upload."
57
62
  },
58
63
  "size_bytes": {
59
64
  "type": "integer",
60
65
  "exclusiveMinimum": 0,
61
66
  "maximum": 9007199254740991,
62
- "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."
67
+ "description": "The refused upload, in bytes."
63
68
  }
64
69
  },
65
70
  "required": [
66
- "limit_bytes",
71
+ "store_bytes",
72
+ "used_bytes",
67
73
  "size_bytes"
68
74
  ],
69
75
  "additionalProperties": false