@fleetless/contracts 1.0.0 → 1.0.3

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 (49) hide show
  1. package/CHANGELOG.md +97 -2
  2. package/CODE_OF_CONDUCT.md +83 -0
  3. package/CONTRIBUTING.md +136 -0
  4. package/README.md +49 -13
  5. package/SECURITY.md +55 -0
  6. package/artifacts/openapi.json +11 -11
  7. package/artifacts/routes.json +12 -12
  8. package/artifacts/schema/create-app-oidc-provider-request.schema.json +1 -1
  9. package/dist/alerts.d.ts +23 -28
  10. package/dist/alerts.js +23 -29
  11. package/dist/app-users.d.ts +18 -19
  12. package/dist/app-users.js +18 -20
  13. package/dist/apps.d.ts +21 -25
  14. package/dist/apps.js +42 -52
  15. package/dist/assets.d.ts +70 -132
  16. package/dist/assets.js +130 -223
  17. package/dist/audit.d.ts +14 -15
  18. package/dist/audit.js +28 -55
  19. package/dist/client-auth.d.ts +9 -9
  20. package/dist/client-auth.js +8 -9
  21. package/dist/common.d.ts +29 -37
  22. package/dist/common.js +28 -37
  23. package/dist/config-issues.d.ts +23 -25
  24. package/dist/config-issues.js +17 -17
  25. package/dist/config.d.ts +37 -44
  26. package/dist/config.js +145 -187
  27. package/dist/errors.d.ts +4 -3
  28. package/dist/errors.js +83 -116
  29. package/dist/identity.d.ts +24 -27
  30. package/dist/identity.js +23 -27
  31. package/dist/index.d.ts +4 -4
  32. package/dist/index.js +14 -15
  33. package/dist/introspection.d.ts +7 -6
  34. package/dist/introspection.js +6 -6
  35. package/dist/jobs.d.ts +16 -16
  36. package/dist/jobs.js +24 -29
  37. package/dist/mcp.d.ts +14 -15
  38. package/dist/mcp.js +12 -14
  39. package/dist/oauth.d.ts +21 -27
  40. package/dist/oauth.js +33 -43
  41. package/dist/protocol.d.ts +51 -62
  42. package/dist/protocol.js +107 -139
  43. package/dist/realtime.d.ts +53 -68
  44. package/dist/realtime.js +78 -104
  45. package/dist/rest.d.ts +183 -244
  46. package/dist/rest.js +305 -399
  47. package/dist/routes.d.ts +4 -3
  48. package/dist/routes.js +33 -32
  49. package/package.json +12 -7
package/dist/app-users.js CHANGED
@@ -2,8 +2,7 @@
2
2
  import { z } from 'zod';
3
3
  import { idpIssuer, mailStatus, password } from './identity.js';
4
4
  /**
5
- * **App users: the per-app identity space** (spec `2026-09-05-app-user-auth`,
6
- * D1–D7).
5
+ * **App users: the per-app identity space.**
7
6
  *
8
7
  * The 2026-08-29 model put developers and end users into one pool per org,
9
8
  * tied apps to groups, and let an org admin enter an app only by
@@ -22,7 +21,7 @@ import { idpIssuer, mailStatus, password } from './identity.js';
22
21
  * as unrelated accounts, and a Fleetless user who wants to use an app
23
22
  * registers or is invited like anybody else.
24
23
  *
25
- * **Fleetless shows an app user no page** (D2). The developer's own UI owns
24
+ * **Fleetless shows an app user no page**. The developer's own UI owns
26
25
  * every screen and calls the JSON client-auth API (`client-auth.ts`). The one
27
26
  * Fleetless-rendered surface an app user can reach is the problem page for an
28
27
  * OIDC callback whose state no longer resolves to a redirect URI — every other
@@ -52,12 +51,12 @@ export const providerSlug = z
52
51
  /**
53
52
  * **The three states an app user can be in, and the order is the lifecycle.**
54
53
  *
55
- * - `pending_verification` — self-registered, mail sent, cannot log in yet
56
- * (D6). Without this state the domain whitelist would prove nothing: anybody
57
- * could claim any address at an allowed domain.
54
+ * - `pending_verification` — self-registered, mail sent, cannot log in yet.
55
+ * Without this state a domain allow-list would prove nothing: anybody could
56
+ * claim any address at an allowed domain.
58
57
  * - `active` — may log in.
59
58
  * - `blocked` — may not, and every refusal is the same `invalid_credentials`
60
- * a wrong password gets (§4). A block that announced itself would be an
59
+ * a wrong password gets. A block that announced itself would be an
61
60
  * account-enumeration oracle with an extra step.
62
61
  *
63
62
  * `pending_verification` is reached exactly once and left only by spending the
@@ -68,7 +67,7 @@ export const appUserStatus = z.enum(['pending_verification', 'active', 'blocked'
68
67
  * **A user of one app.** Not a user of the org: `app_id` is the whole scope,
69
68
  * and the uniqueness constraint the cloud enforces is `(app_id, lower(email))`
70
69
  * rather than a global one. The same person at two apps of one org is two
71
- * unrelated rows, by design (D1).
70
+ * unrelated rows, by design.
72
71
  */
73
72
  export const appUser = z.object({
74
73
  id: z.uuid().meta({
@@ -148,8 +147,8 @@ export const createAppUserRequest = z
148
147
  * offering it is a refusal rather than a silently dropped field.
149
148
  *
150
149
  * **`status` admits only `active` and `blocked`.** `pending_verification` is
151
- * reached once, by self-registration, and left by spending the mailed token
152
- * (D6). A developer able to set it back could void a verified address without
150
+ * reached once, by self-registration, and left by spending the mailed token.
151
+ * A developer able to set it back could void a verified address without
153
152
  * the user ever seeing a mail, and there is no route out of that state that
154
153
  * does not require a token nobody re-sent. So the narrower enum is the rule,
155
154
  * stated in the schema rather than left to a handler to remember.
@@ -196,7 +195,7 @@ export const createAppInvitationRequest = z
196
195
  * An app that has configured none has nowhere for it to point, so there is no
197
196
  * link to hand back — `null` says that outright, where an absent key would be
198
197
  * indistinguishable from a mapper that dropped the field and a fabricated
199
- * Fleetless-hosted URL would name a page this product does not serve (D2).
198
+ * Fleetless-hosted URL would name a page this product does not serve.
200
199
  */
201
200
  export const appInvitation = z.object({
202
201
  id: z.uuid().meta({ description: 'The invitation, as listed and revoked by the developer.' }),
@@ -231,7 +230,7 @@ export const appInvitationListResponse = z.object({
231
230
  }),
232
231
  });
233
232
  /**
234
- * **An app's OIDC provider, as read back** (D4). Any number per app, unlike
233
+ * **An app's OIDC provider, as read back**. Any number per app, unlike
235
234
  * the group provider this replaces — a developer serving two customers needs
236
235
  * two, and the old at-most-one rule was a property of groups rather than of
237
236
  * identity.
@@ -305,7 +304,7 @@ export const createAppOidcProviderRequest = z
305
304
  description: 'The client secret, **write-only**: it is stored encrypted and comes back through nothing — not the read, not this route\'s own answer, not an audit detail. Required on create, since a provider with no secret cannot exchange a code; the minimum length refuses a value that is a misconfiguration rather than a secret.',
306
305
  }),
307
306
  scopes: z.array(z.string().min(1).max(60)).min(1).max(20).default(['openid', 'email', 'profile']).meta({
308
- description: 'The scopes to request. Defaults to `openid email profile`, which is what the linking rules in this design actually read: the subject, the address and its verified flag, and a name.',
307
+ description: 'The scopes to request. Defaults to `openid email profile`, which is what account linking actually reads: the subject, the address and its verified flag, and a name.',
309
308
  }),
310
309
  link_verified_emails: z.boolean().default(false).meta({
311
310
  description: 'Whether a federated login may join an existing app user by verified address. **Defaults to off**, because relaxing later is additive and admitting duplicates now and tightening afterwards is not.',
@@ -438,16 +437,15 @@ export const emailDomain = z
438
437
  .max(253)
439
438
  .regex(/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/, 'must be a lowercase domain name with at least two labels');
440
439
  /**
441
- * **The app's auth settings: one row per app, configured by a Fleetless user**
442
- * (D3).
440
+ * **The app's auth settings: one row per app, configured by a Fleetless user.**
443
441
  *
444
442
  * `self_registration` and `allowed_domains` are **one policy for one
445
443
  * decision** — they govern registration by password and registration through
446
- * an identity provider alike (D4). An invitation always bypasses both, because
444
+ * an identity provider alike. An invitation always bypasses both, because
447
445
  * a developer inviting somebody by hand has already made the decision the
448
446
  * whitelist automates.
449
447
  *
450
- * The four URLs are what makes D2 work: Fleetless mails a link, and the link
448
+ * The four URLs are what makes that work: Fleetless mails a link, and the link
451
449
  * points into the developer's app. An app that has configured none of them
452
450
  * still works for password login — it simply cannot send a mail that leads
453
451
  * anywhere, and `send_mail` is refused rather than silently sending a dead
@@ -494,7 +492,7 @@ export const putAppAuthConfigRequest = appAuthConfig
494
492
  .omit({ oidc_callback_url: true, updated_at: true })
495
493
  .strict();
496
494
  /**
497
- * The three mails a developer may replace with their own template (D5).
495
+ * The three mails a developer may replace with their own template.
498
496
  * Mails to *Fleetless* users — a team invitation, a console password reset —
499
497
  * stay Fleetless default and are deliberately not customisable: they are
500
498
  * about this platform, not about the developer's product.
@@ -519,7 +517,7 @@ export const MAIL_TEMPLATE_VARIABLES = [
519
517
  'expires_in_hours',
520
518
  ];
521
519
  /**
522
- * **The Fleetless default text for the three app mails** (spec D5, §6).
520
+ * **The Fleetless default text for the three app mails.**
523
521
  *
524
522
  * It lives here rather than in the cloud because two products send the same
525
523
  * words: the cloud renders these when an app has no template of its own, and
@@ -553,7 +551,7 @@ export const MAIL_TEMPLATE_VARIABLES = [
553
551
  * that one is written by an authenticated developer about somebody they
554
552
  * invited.
555
553
  *
556
- * **`expires_in_hours` is the only lifetime variable the spec offers**, and
554
+ * **`expires_in_hours` is the only lifetime variable a template gets**, and
557
555
  * the three values are 1, 24 and 168. "The next 168 hours" is not how a person
558
556
  * says a week, so each default converts: 48 and up reads in days, exactly one
559
557
  * reads "1 hour", everything else reads in hours. The conversion is in the
package/dist/apps.d.ts CHANGED
@@ -1,6 +1,7 @@
1
+ // SPDX-License-Identifier: Apache-2.0
1
2
  import { z } from 'zod';
2
3
  /**
3
- * Apps, roles and rights (spec §3.2, §3.3, §12.2).
4
+ * Apps, roles and rights.
4
5
  *
5
6
  * The rule that shapes all of this: **roles are the only filter**. A robot
6
7
  * assigned to an app exposes every one of its services to that app; what a
@@ -42,12 +43,11 @@ export declare const appListResponse: z.ZodObject<{
42
43
  }, z.core.$strip>;
43
44
  export type AppListResponse = z.infer<typeof appListResponse>;
44
45
  /**
45
- * **`robot_ids` is accepted here, and `.strict()` catches everything else
46
- * (W7a).** Through W7 this shape carried `name` and `identifier` only, robots
47
- * attached through `updateAppRequest`, and zod stripped the extra key — so a
48
- * caller creating an app *with* robots got a `201` and an app with none.
49
- * **Two people fell into it independently on the same day**, which is the
50
- * definition of a shape that reads as though it does something it does not.
46
+ * **`robot_ids` is accepted here, and `.strict()` catches everything else.**
47
+ * A create shape carrying `name` and `identifier` only would let zod strip an
48
+ * offered `robot_ids`, so a caller creating an app *with* robots gets a `201`
49
+ * and an app with none — a shape that reads as though it does something it does
50
+ * not.
51
51
  *
52
52
  * Both halves matter and neither alone is enough. Accepting `robot_ids` is
53
53
  * right because attaching robots at creation is the obvious operation and the
@@ -69,21 +69,17 @@ export type CreateAppRequest = z.infer<typeof createAppRequest>;
69
69
  /**
70
70
  * **`.strict()` is what makes an absent field mean something here.**
71
71
  *
72
- * This shape was not strict until 2026-08-29, which meant an offered field the
73
- * route does not implement was *dropped* — the caller got a `200`, nothing
74
- * changed, and nothing anywhere said so. That is the exact silence
75
- * `createAppRequest` above already learned about in W7a (*"a create shape that
76
- * silently drops a field cost two people a day each"*), and the lesson had not
77
- * been carried one shape over. A caller who sends a field this route does not
78
- * do is asking for something, and the honest answer is `400`, not a success
79
- * that means less than it looks.
72
+ * Without it, an offered field the route does not implement is *dropped* — the
73
+ * caller gets a `200`, nothing changes, and nothing anywhere says so. It is the
74
+ * same silence `createAppRequest` above describes. A caller who sends a field
75
+ * this route does not do is asking for something, and the honest answer is
76
+ * `400`, not a success that means less than it looks.
80
77
  *
81
- * Two fields left with the two-space cut and are worth naming, because both
82
- * were on this shape and neither has a successor here.
78
+ * Two fields are absent and worth naming, because neither has a successor here.
83
79
  * `accepts_dynamic_clients` gated app-level OAuth dynamic client registration,
84
- * which is deleted: apps use the JSON client-auth API and OAuth 2.1 remains
85
- * only for MCP. `group_id` named the group that owned the app, and groups are
86
- * gone; who may log into an app is now the app's own user list.
80
+ * which no longer exists: apps use the JSON client-auth API, and OAuth 2.1
81
+ * remains only for MCP. `group_id` named a group that owned the app; who may
82
+ * log into an app is the app's own user list.
87
83
  *
88
84
  * The route keeps its own check as belt-and-braces; a schema and a handler
89
85
  * agreeing is not two policies, it is one policy stated where each half can
@@ -96,9 +92,9 @@ export declare const updateAppRequest: z.ZodObject<{
96
92
  }, z.core.$strict>;
97
93
  export type UpdateAppRequest = z.infer<typeof updateAppRequest>;
98
94
  /**
99
- * A server key carries full app rights for server-side code (spec §3.4) —
100
- * never for clients. Same handling as the robot token from W1: the value is
101
- * returned exactly once and only its hash is stored.
95
+ * A server key carries full app rights for server-side code — never for
96
+ * clients. Same handling as the robot token: the value is returned exactly once
97
+ * and only its hash is stored.
102
98
  */
103
99
  export declare const serverKeyToken: z.ZodString;
104
100
  export declare const serverKey: z.ZodObject<{
@@ -133,7 +129,7 @@ export declare const createServerKeyResponse: z.ZodObject<{
133
129
  export type CreateServerKeyResponse = z.infer<typeof createServerKeyResponse>;
134
130
  /**
135
131
  * Every app starts with `observe` and `operate`; custom roles are allowed
136
- * from v1 (§3.3). `builtin` marks the two starting roles — they may be
132
+ * too. `builtin` marks the two starting roles — they may be
137
133
  * edited like any other, the flag exists so the console can explain where
138
134
  * they came from.
139
135
  */
@@ -156,7 +152,7 @@ export declare const roleListResponse: z.ZodObject<{
156
152
  export type RoleListResponse = z.infer<typeof roleListResponse>;
157
153
  /**
158
154
  * The rights matrix of one role: which slugs of which robot it may use, plus
159
- * the capabilities roles also govern (§3.3). `capabilities`' own doc comment
155
+ * the capabilities roles also govern. `capabilities`' own doc comment
160
156
  * below says which of them are enforced today and which is still a switch
161
157
  * that changes nothing.
162
158
  */
package/dist/apps.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { z } from 'zod';
3
3
  import { slug } from './common.js';
4
4
  /**
5
- * Apps, roles and rights (spec §3.2, §3.3, §12.2).
5
+ * Apps, roles and rights.
6
6
  *
7
7
  * The rule that shapes all of this: **roles are the only filter**. A robot
8
8
  * assigned to an app exposes every one of its services to that app; what a
@@ -33,7 +33,7 @@ export const app = z.object({
33
33
  identifier: appIdentifier.meta({
34
34
  description: 'The stable handle a client sends at login, lowercase and underscore-separated. **Globally unique, not per organisation** — `clientLoginRequest` carries no org context to disambiguate with, so a collision is refused with `identifier_taken`.',
35
35
  }),
36
- /** Robots are referenced individually; tags never grant rights (§12.2). */
36
+ /** Robots are referenced individually; tags never grant rights. */
37
37
  robot_ids: z.array(z.uuid()).meta({
38
38
  description: 'The robots this app may reach, each referenced individually. Tags never grant rights, and a robot absent from this list is invisible to the app whatever a role grants.',
39
39
  }),
@@ -79,12 +79,11 @@ export const appListResponse = z.object({
79
79
  }),
80
80
  });
81
81
  /**
82
- * **`robot_ids` is accepted here, and `.strict()` catches everything else
83
- * (W7a).** Through W7 this shape carried `name` and `identifier` only, robots
84
- * attached through `updateAppRequest`, and zod stripped the extra key — so a
85
- * caller creating an app *with* robots got a `201` and an app with none.
86
- * **Two people fell into it independently on the same day**, which is the
87
- * definition of a shape that reads as though it does something it does not.
82
+ * **`robot_ids` is accepted here, and `.strict()` catches everything else.**
83
+ * A create shape carrying `name` and `identifier` only would let zod strip an
84
+ * offered `robot_ids`, so a caller creating an app *with* robots gets a `201`
85
+ * and an app with none — a shape that reads as though it does something it does
86
+ * not.
88
87
  *
89
88
  * Both halves matter and neither alone is enough. Accepting `robot_ids` is
90
89
  * right because attaching robots at creation is the obvious operation and the
@@ -105,21 +104,17 @@ export const createAppRequest = z.object({
105
104
  /**
106
105
  * **`.strict()` is what makes an absent field mean something here.**
107
106
  *
108
- * This shape was not strict until 2026-08-29, which meant an offered field the
109
- * route does not implement was *dropped* — the caller got a `200`, nothing
110
- * changed, and nothing anywhere said so. That is the exact silence
111
- * `createAppRequest` above already learned about in W7a (*"a create shape that
112
- * silently drops a field cost two people a day each"*), and the lesson had not
113
- * been carried one shape over. A caller who sends a field this route does not
114
- * do is asking for something, and the honest answer is `400`, not a success
115
- * that means less than it looks.
107
+ * Without it, an offered field the route does not implement is *dropped* — the
108
+ * caller gets a `200`, nothing changes, and nothing anywhere says so. It is the
109
+ * same silence `createAppRequest` above describes. A caller who sends a field
110
+ * this route does not do is asking for something, and the honest answer is
111
+ * `400`, not a success that means less than it looks.
116
112
  *
117
- * Two fields left with the two-space cut and are worth naming, because both
118
- * were on this shape and neither has a successor here.
113
+ * Two fields are absent and worth naming, because neither has a successor here.
119
114
  * `accepts_dynamic_clients` gated app-level OAuth dynamic client registration,
120
- * which is deleted: apps use the JSON client-auth API and OAuth 2.1 remains
121
- * only for MCP. `group_id` named the group that owned the app, and groups are
122
- * gone; who may log into an app is now the app's own user list.
115
+ * which no longer exists: apps use the JSON client-auth API, and OAuth 2.1
116
+ * remains only for MCP. `group_id` named a group that owned the app; who may
117
+ * log into an app is the app's own user list.
123
118
  *
124
119
  * The route keeps its own check as belt-and-braces; a schema and a handler
125
120
  * agreeing is not two policies, it is one policy stated where each half can
@@ -129,7 +124,8 @@ export const updateAppRequest = z.object({
129
124
  name: z.string().min(1).max(120).optional(),
130
125
  robot_ids: z.array(z.uuid()).optional(),
131
126
  /**
132
- * `app.default_role_id`'s write half — an app *setting*, which is where D1
127
+ * `app.default_role_id`'s write half — an app *setting*, which is where the
128
+ * two-identity-space model
133
129
  * put the default role, so it belongs on the app's own PATCH and not on a
134
130
  * route of its own.
135
131
  *
@@ -142,9 +138,9 @@ export const updateAppRequest = z.object({
142
138
  default_role_id: z.uuid().nullable().optional(),
143
139
  }).strict();
144
140
  /**
145
- * A server key carries full app rights for server-side code (spec §3.4) —
146
- * never for clients. Same handling as the robot token from W1: the value is
147
- * returned exactly once and only its hash is stored.
141
+ * A server key carries full app rights for server-side code — never for
142
+ * clients. Same handling as the robot token: the value is returned exactly once
143
+ * and only its hash is stored.
148
144
  */
149
145
  export const serverKeyToken = z.string().regex(/^flk_[0-9a-f]{32}$/);
150
146
  export const serverKey = z.object({
@@ -177,7 +173,7 @@ export const createServerKeyResponse = z.object({
177
173
  });
178
174
  /**
179
175
  * Every app starts with `observe` and `operate`; custom roles are allowed
180
- * from v1 (§3.3). `builtin` marks the two starting roles — they may be
176
+ * too. `builtin` marks the two starting roles — they may be
181
177
  * edited like any other, the flag exists so the console can explain where
182
178
  * they came from.
183
179
  */
@@ -203,23 +199,20 @@ export const roleListResponse = z.object({
203
199
  });
204
200
  /**
205
201
  * The rights matrix of one role: which slugs of which robot it may use, plus
206
- * the capabilities roles also govern (§3.3). `capabilities`' own doc comment
202
+ * the capabilities roles also govern. `capabilities`' own doc comment
207
203
  * below says which of them are enforced today and which is still a switch
208
204
  * that changes nothing.
209
205
  */
210
206
  export const rolePermissions = z.object({
211
207
  role_id: z.uuid(),
212
208
  /**
213
- * **A slug is unique per robot across ALL service kinds** (spec §4.1:
214
- * "Jeder Dienst erhält einen Slug" — one namespace, not one per kind), and
215
- * the cloud's config validation enforces that with a kind-agnostic
216
- * collection pass. That is why this list carries slugs and not
217
- * (kind, slug) pairs: when W4 adds actions, services and publishers, a
218
- * grant keeps meaning exactly what it means today, and this shape does not
219
- * change. What W4 does need is an endpoint that lists every *grantable*
220
- * slug of a robot with its kind, so the console's matrix can offer them —
221
- * today it enumerates datapoints only, which is the seam that would
222
- * otherwise force a rebuild.
209
+ * **A slug is unique per robot across ALL exposure kinds** — one namespace,
210
+ * not one per kind — and the cloud's configuration validation enforces that
211
+ * with a kind-agnostic collection pass. That is why this list carries slugs
212
+ * and not (kind, slug) pairs: a grant means the same thing whichever kind the
213
+ * slug turns out to name. `GET /api/robots/:id/exposures` is the companion
214
+ * read that lists every grantable slug of a robot **with** its kind, so a
215
+ * rights matrix can offer them.
223
216
  */
224
217
  grants: z.array(z.object({
225
218
  robot_id: z.uuid(),
@@ -228,25 +221,22 @@ export const rolePermissions = z.object({
228
221
  /**
229
222
  * App-wide abilities a role grants, as opposed to per-slug grants above.
230
223
  *
231
- * **A capability here is a promise, and one of them is still not kept.**
232
- * `action_history` and `presence` were both gated by this object from W4
233
- * and implemented nowhere — no route, no SDK method, no realtime frame
234
- * (register row 8). A console could therefore switch them on and nothing
235
- * changed, which is worse than their absence: the developer believes they
236
- * granted something. **This paragraph stays** whatever the current tally
237
- * is: it is the only place that says a switch in the console may change
238
- * nothing, and it is how the next unkept capability gets caught.
224
+ * **A capability here is a promise, and one of them is still not kept.** A
225
+ * capability with no route, no SDK method and no realtime frame behind it can
226
+ * be switched on while nothing changes, which is worse than its absence: the
227
+ * developer believes they granted something. **This paragraph stays**
228
+ * whatever the current tally is: it is the only place that says a switch may
229
+ * change nothing, and it is how the next unkept capability gets caught.
239
230
  *
240
- * `assets` (W7) was the first one redeemed. It gates §4.6's asset store,
241
- * which is not covered by `grants` because **assets are not slugs** — and it
242
- * is its own decision rather than a side effect of reaching the robot,
243
- * because a mesh set gives away the machine's build.
231
+ * `assets` gates the asset store, which is not covered by `grants` because
232
+ * **assets are not slugs** — and it is its own decision rather than a side
233
+ * effect of reaching the robot, because a mesh set gives away the machine's
234
+ * build.
244
235
  *
245
- * **`action_history` is kept as of the run-history delta.** It gates
236
+ * **`action_history` is kept.** It gates
246
237
  * `GET /api/robots/:id/jobs/history` — an end user whose role lacks it is
247
238
  * refused `403 capability_required`, naming the capability so the developer
248
- * knows which switch is off. It was unkeepable while nothing durable
249
- * recorded what had run; `jobRun` and `job_runs` are that record.
239
+ * knows which switch is off.
250
240
  *
251
241
  * **What granting it discloses.** A `jobRun` names the actor who invoked
252
242
  * it, and `jobActor.label` is an email — so an end user holding this