kiosk-server 0.5.20 → 0.6.1

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -0
  3. data/README.md +63 -41
  4. data/app/controllers/kiosk/server/auth_controller.rb +18 -62
  5. data/app/controllers/kiosk/server/discovery_controller.rb +25 -3
  6. data/app/controllers/kiosk/server/jwks_controller.rb +2 -2
  7. data/app/controllers/kiosk/server/kyc_attestation_controller.rb +1 -4
  8. data/app/controllers/kiosk/server/oauth_device_authorization_controller.rb +11 -29
  9. data/app/controllers/kiosk/server/oauth_registration_controller.rb +34 -0
  10. data/app/controllers/kiosk/server/oauth_token_controller.rb +33 -32
  11. data/app/models/kiosk/checkout_mandate.rb +22 -0
  12. data/app/models/kiosk/settlement.rb +1 -1
  13. data/lib/generators/kiosk/install/install_generator.rb +0 -5
  14. data/lib/generators/kiosk/install/templates/create_kiosk_mandates.rb.tt +4 -5
  15. data/lib/kiosk/payable.rb +19 -25
  16. data/lib/kiosk/server/account_binding.rb +19 -58
  17. data/lib/kiosk/server/agent_identity_providers/default_agent_idp.rb +18 -16
  18. data/lib/kiosk/server/agent_rotation.rb +62 -0
  19. data/lib/kiosk/server/assistant_key.rb +86 -0
  20. data/lib/kiosk/server/auth_toll.rb +1 -1
  21. data/lib/kiosk/server/binding_module_gate.rb +1 -1
  22. data/lib/kiosk/server/checkout.rb +57 -0
  23. data/lib/kiosk/server/client.rb +61 -0
  24. data/lib/kiosk/server/client_assertion.rb +153 -0
  25. data/lib/kiosk/server/client_registration.rb +94 -0
  26. data/lib/kiosk/server/configuration_extension.rb +56 -44
  27. data/lib/kiosk/server/device_code_grant.rb +38 -69
  28. data/lib/kiosk/server/dpop.rb +121 -0
  29. data/lib/kiosk/server/eddsa.rb +38 -0
  30. data/lib/kiosk/server/engine.rb +14 -11
  31. data/lib/kiosk/server/errors.rb +7 -0
  32. data/lib/kiosk/server/events_connection.rb +15 -5
  33. data/lib/kiosk/server/executor/repeat.rb +13 -12
  34. data/lib/kiosk/server/executor/spending_cap.rb +25 -26
  35. data/lib/kiosk/server/executor.rb +82 -101
  36. data/lib/kiosk/server/identity_resolution.rb +12 -3
  37. data/lib/kiosk/server/jwk.rb +83 -0
  38. data/lib/kiosk/server/jwks.rb +6 -0
  39. data/lib/kiosk/server/jwt_issuer.rb +7 -13
  40. data/lib/kiosk/server/kyc_verifier.rb +2 -2
  41. data/lib/kiosk/server/link_code.rb +9 -18
  42. data/lib/kiosk/server/linked_assistants.rb +1 -1
  43. data/lib/kiosk/server/mandate_verifier.rb +97 -139
  44. data/lib/kiosk/server/oauth_answers.rb +30 -0
  45. data/lib/kiosk/server/oauth_error.rb +30 -0
  46. data/lib/kiosk/server/open_api.rb +22 -20
  47. data/lib/kiosk/server/payment_claim.rb +44 -75
  48. data/lib/kiosk/server/receipts.rb +29 -0
  49. data/lib/kiosk/server/request_validation.rb +1 -2
  50. data/lib/kiosk/server/revocation_stores.rb +8 -0
  51. data/lib/kiosk/server/schema_definitions.rb +33 -64
  52. data/lib/kiosk/server/schemas/auth.schema.json +368 -57
  53. data/lib/kiosk/server/schemas/binding.schema.json +23 -58
  54. data/lib/kiosk/server/schemas/kyc.schema.json +19 -76
  55. data/lib/kiosk/server/schemas/mandates.schema.json +122 -38
  56. data/lib/kiosk/server/sd_jwt.rb +104 -0
  57. data/lib/kiosk/server/signing_key.rb +18 -52
  58. data/lib/kiosk/server/verb_wire.rb +1 -1
  59. data/lib/kiosk/server/version.rb +1 -1
  60. data/lib/kiosk/server/well_known.rb +268 -204
  61. data/lib/kiosk/server/wire_rendering.rb +1 -1
  62. data/lib/kiosk/server.rb +2 -0
  63. data/lib/kiosk/skill_link.rb +6 -2
  64. data/listen.py +38 -8
  65. metadata +26 -17
  66. data/app/models/kiosk/cart_mandate.rb +0 -21
  67. data/lib/generators/kiosk/install/templates/create_kiosk_auth_challenges.rb.tt +0 -16
  68. data/lib/kiosk/server/agent_login.rb +0 -49
  69. data/lib/kiosk/server/agent_registration.rb +0 -88
  70. data/lib/kiosk/server/auth_challenge.rb +0 -33
  71. data/lib/kiosk/server/auth_challenge_stores.rb +0 -74
  72. data/lib/kiosk/server/pop_verifier.rb +0 -87
@@ -2,101 +2,297 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://kiosk.tech/spec/schemas/auth.schema.json",
4
4
  "$comment": "VENDORED copy of the normative kiosk.tech schema at spec/schemas/auth.schema.json. It is here so RequestValidation can hold a reserved-plane request body to it at runtime, and so e2e/schema_conformance.rb can validate this origin's LIVE wire bytes against the PUBLISHED schema -- both with no second checkout and no network fetch. Identical to the upstream copy as PARSED JSON once this $comment is removed.",
5
- "title": "Kiosk registration and login (kiosk-pop)",
6
- "description": "The wire objects of Section 5: kiosk-pop, a proof-of-possession challenge-response. The caller proves that it holds the private key of the public key it presents, and the operator answers with a short-lived access token. The roots are open: they state what MUST be present and the shape of each member.",
5
+ "title": "Kiosk client registration and tokens (OAuth)",
6
+ "description": "The wire objects of Section 5: RFC 7591 open client registration of the AI assistant's public key, RFC 7523 client assertions signed with that key (the private_key_jwt method), and DPoP-bound access tokens from the client_credentials grant (RFC 9449). The roots are open: they state what MUST be present and the shape of each member.",
7
7
  "$defs": {
8
- "challenge": {
8
+ "publicJwk": {
9
9
  "type": "object",
10
- "description": "Section 5.1 -- the answer to GET <endpoint>/auth/challenge?public_key=<url-encoded PEM>. Single-use and short-lived; the nonce is worthless to anyone without the matching private key, which is why the endpoint is unauthenticated.",
11
- "required": ["challenge", "exp"],
10
+ "description": "Section 5.1 -- the AI assistant's public key as an RFC 7517 JWK: an Ed25519 key (RFC 8037), or a P-256 key where the operator accepts ES256. A member that only a private key has is refused. Other members (kid, use, alg) are allowed and ignored.",
11
+ "required": [
12
+ "kty",
13
+ "crv",
14
+ "x"
15
+ ],
12
16
  "properties": {
13
- "challenge": {
17
+ "kty": {
18
+ "enum": [
19
+ "OKP",
20
+ "EC"
21
+ ],
22
+ "description": "The key type: OKP for an Ed25519 key, which signs EdDSA; EC for a P-256 key, which signs ES256."
23
+ },
24
+ "crv": {
25
+ "enum": [
26
+ "Ed25519",
27
+ "P-256"
28
+ ],
29
+ "description": "The curve: Ed25519 for OKP, P-256 for EC."
30
+ },
31
+ "x": {
14
32
  "type": "string",
15
- "description": "The server-issued nonce to sign. It becomes the `nonce` claim of the possession proof.",
16
- "minLength": 1
33
+ "minLength": 1,
34
+ "description": "The base64url public key of an OKP key, or the base64url x coordinate of an EC key."
17
35
  },
18
- "exp": {
19
- "type": "integer",
20
- "description": "Unix expiry of the challenge."
36
+ "y": {
37
+ "type": "string",
38
+ "minLength": 1,
39
+ "description": "The base64url y coordinate of an EC key."
40
+ }
41
+ },
42
+ "if": {
43
+ "properties": {
44
+ "kty": {
45
+ "const": "EC"
46
+ }
47
+ }
48
+ },
49
+ "then": {
50
+ "required": [
51
+ "y"
52
+ ],
53
+ "properties": {
54
+ "crv": {
55
+ "const": "P-256"
56
+ }
21
57
  }
58
+ },
59
+ "else": {
60
+ "properties": {
61
+ "crv": {
62
+ "const": "Ed25519"
63
+ }
64
+ }
65
+ },
66
+ "not": {
67
+ "required": [
68
+ "d"
69
+ ]
22
70
  }
23
71
  },
24
- "possessionProof": {
72
+ "jwks": {
25
73
  "type": "object",
26
- "description": "Section 5.2 -- the decoded payload of `signed`, a compact RS256 JWS. This is the object the register, login, claim and device-grant token calls all carry. It is a PAYLOAD, not a request body: the wire carries the compact serialisation, and this schema describes what a verifier reads out of it.",
27
- "required": ["aud", "nonce", "jti"],
74
+ "description": "Section 5.1 -- an RFC 7517 JWK Set that holds exactly one key, the AI assistant's public key.",
75
+ "required": [
76
+ "keys"
77
+ ],
28
78
  "properties": {
29
- "aud": {
30
- "type": "string",
31
- "description": "ORIGIN-BINDING, and the reason a relayed proof is worthless. MUST be the origin the AI assistant dialed (Section 15.1); the operator rejects any other `aud`."
32
- },
33
- "nonce": {
34
- "type": "string",
35
- "description": "The `challenge` from Section 5.1. Single-use and server-TTL-bounded."
79
+ "keys": {
80
+ "type": "array",
81
+ "minItems": 1,
82
+ "maxItems": 1,
83
+ "items": {
84
+ "$ref": "#/$defs/publicJwk"
85
+ }
86
+ }
87
+ }
88
+ },
89
+ "registrationRequest": {
90
+ "type": "object",
91
+ "description": "Section 5.1 -- the body of POST <endpoint>/oauth/register, an RFC 7591 open registration: no initial access token and no human. The operator ignores every member it does not know (RFC 7591 Section 2). An optional proof-of-work proof rides in the `Kiosk-PoW` request header, never in this body.",
92
+ "required": [
93
+ "jwks"
94
+ ],
95
+ "properties": {
96
+ "jwks": {
97
+ "$ref": "#/$defs/jwks"
36
98
  },
37
- "jti": {
38
- "type": "string",
39
- "description": "A unique id."
99
+ "token_endpoint_auth_method": {
100
+ "const": "private_key_jwt",
101
+ "description": "The one client authentication method: a client assertion signed with the registered key (RFC 7523)."
40
102
  },
41
- "pub": {
103
+ "grant_types": {
104
+ "type": "array",
105
+ "items": {
106
+ "enum": [
107
+ "client_credentials",
108
+ "urn:ietf:params:oauth:grant-type:device_code"
109
+ ]
110
+ },
111
+ "description": "A subset of the grant types this origin serves. The operator registers the client for every grant type it serves and answers that list."
112
+ }
113
+ }
114
+ },
115
+ "clientInformation": {
116
+ "type": "object",
117
+ "description": "Section 5.1 -- the 201 answer to POST <endpoint>/oauth/register (RFC 7591 Section 3.2.1), and the 200 answer to POST <endpoint>/auth/rotate (Section 5.7). Registering a key that is registered already answers the same client again.",
118
+ "required": [
119
+ "client_id",
120
+ "jwks",
121
+ "token_endpoint_auth_method",
122
+ "grant_types",
123
+ "dpop_bound_access_tokens"
124
+ ],
125
+ "properties": {
126
+ "client_id": {
42
127
  "type": "string",
43
- "description": "RFC 7638 thumbprint of the public key; verified only when present."
128
+ "minLength": 1,
129
+ "description": "The client identifier. It is also the `agent_id` of the assistant account, stable across a rotation and a rebind."
44
130
  },
45
- "iat": {
131
+ "client_id_issued_at": {
46
132
  "type": "integer",
47
- "description": "Informational only -- the server-issued `nonce` is the authoritative freshness bound, so an operator MUST NOT rely on this claim for freshness."
133
+ "description": "When the client was registered, in Unix seconds."
134
+ },
135
+ "jwks": {
136
+ "$ref": "#/$defs/jwks"
137
+ },
138
+ "token_endpoint_auth_method": {
139
+ "const": "private_key_jwt"
140
+ },
141
+ "grant_types": {
142
+ "type": "array",
143
+ "minItems": 1,
144
+ "items": {
145
+ "enum": [
146
+ "client_credentials",
147
+ "urn:ietf:params:oauth:grant-type:device_code"
148
+ ]
149
+ },
150
+ "description": "`client_credentials`, and the device-code grant where the origin serves account binding."
151
+ },
152
+ "dpop_bound_access_tokens": {
153
+ "const": true,
154
+ "description": "RFC 9449 Section 5.2: every token request carries a DPoP proof."
48
155
  }
49
156
  }
50
157
  },
51
- "credentialRequest": {
158
+ "registrationError": {
52
159
  "type": "object",
53
- "description": "Section 5.3 -- the body of POST /auth/register and POST /auth/login. Register also accepts an optional proof-of-work proof, which rides in the `Kiosk-PoW` request HEADER (Sections 5.5 and 10.1) and never in this body, so that the signed bytes are identical on the retry.",
54
- "required": ["public_key", "signed"],
160
+ "description": "Section 5.1 -- the 400 answer of POST <endpoint>/oauth/register (RFC 7591 Section 3.2.2). The toll on registration answers with problem documents instead (Section 10).",
161
+ "required": [
162
+ "error"
163
+ ],
55
164
  "properties": {
56
- "public_key": {
57
- "type": "string",
58
- "description": "The AI assistant's public key, PEM-encoded. An AI assistant SHOULD generate a fresh keypair per operator origin (Section 15.3)."
165
+ "error": {
166
+ "const": "invalid_client_metadata",
167
+ "description": "The one registration error code: a member of the request is missing or not valid."
59
168
  },
60
- "signed": {
169
+ "error_description": {
61
170
  "type": "string",
62
- "description": "The possession proof of Section 5.2, as a compact RS256 JWS. Its decoded payload is `#/$defs/possessionProof`."
171
+ "description": "Human-readable detail. An AI assistant branches on `error` and never on this string."
63
172
  }
64
173
  }
65
174
  },
66
- "registration": {
175
+ "clientAssertion": {
67
176
  "type": "object",
68
- "description": "Section 5.3 -- the 201 answer to POST /auth/register. Registering an already-known key answers 409 conflict instead (use login). This is also the 201 answer to POST /auth/claim (Section 6.2), which is why `#/$defs/claimResponse` in binding.schema.json refers here rather than restating it.",
69
- "required": ["agent_id", "user_id", "access_token"],
177
+ "description": "Section 5.2 -- the decoded payload of a client assertion (RFC 7523 Section 3), a compact JWS that the registered key signs. It is sent as `client_assertion`, with `client_assertion_type` = `urn:ietf:params:oauth:client-assertion-type:jwt-bearer`. It is a PAYLOAD, not a request body.",
178
+ "required": [
179
+ "iss",
180
+ "sub",
181
+ "aud",
182
+ "exp",
183
+ "jti"
184
+ ],
70
185
  "properties": {
71
- "agent_id": {
186
+ "iss": {
72
187
  "type": "string",
73
- "description": "The acting agent id. Stable across a rebind (Section 6.3)."
188
+ "minLength": 1,
189
+ "description": "The client_id."
74
190
  },
75
- "user_id": {
191
+ "sub": {
76
192
  "type": "string",
77
- "description": "The identity the key maps to. An operator MUST map a known key to the same `user_id` so a saved payment card survives across sessions."
193
+ "minLength": 1,
194
+ "description": "The client_id, the same value as `iss`."
78
195
  },
79
- "access_token": {
196
+ "aud": {
197
+ "description": "ORIGIN-BINDING. The issuer of the origin the AI assistant dialed, as its only value (Section 15.1). A string, or an array with that one string.",
198
+ "oneOf": [
199
+ {
200
+ "type": "string"
201
+ },
202
+ {
203
+ "type": "array",
204
+ "items": {
205
+ "type": "string"
206
+ },
207
+ "minItems": 1,
208
+ "maxItems": 1
209
+ }
210
+ ]
211
+ },
212
+ "exp": {
213
+ "type": "integer",
214
+ "description": "Expiry. The operator refuses an assertion that has expired or that expires more than 300 seconds after its clock."
215
+ },
216
+ "jti": {
80
217
  "type": "string",
81
- "description": "The access token, a compact RS256 JWT. Its claims are `#/$defs/accessTokenClaims`."
218
+ "minLength": 1,
219
+ "description": "A unique id. The operator accepts each id of a client one time."
220
+ },
221
+ "iat": {
222
+ "type": "integer",
223
+ "description": "Optional: the time of signing."
82
224
  }
83
225
  }
84
226
  },
85
- "token": {
227
+ "tokenResponse": {
86
228
  "type": "object",
87
- "description": "Section 5.3: the 200 answer to POST /auth/login; an unknown key answers 404 not_found. It is ALSO the 200 answer to POST /auth/revoke (Section 5.5), with a token issued after the new watermark.",
88
- "required": ["access_token"],
229
+ "description": "Section 5.3 -- the 200 answer of POST <endpoint>/oauth/token (RFC 6749 Section 5.1), for the client_credentials grant and for the device-code grant (Section 6.1). The principal (`user_id`, `agent_id`) is in the access token's claims.",
230
+ "required": [
231
+ "access_token",
232
+ "token_type",
233
+ "expires_in"
234
+ ],
89
235
  "properties": {
90
236
  "access_token": {
91
237
  "type": "string",
92
- "description": "The access token, a compact RS256 JWT. Its claims are `#/$defs/accessTokenClaims`."
238
+ "description": "The access token, a compact EdDSA JWT. Its claims are `#/$defs/accessTokenClaims`."
239
+ },
240
+ "token_type": {
241
+ "const": "DPoP",
242
+ "description": "Always \"DPoP\": the token is bound to the registered key (Section 5.6)."
243
+ },
244
+ "expires_in": {
245
+ "type": "integer",
246
+ "description": "Seconds until the access token expires."
247
+ },
248
+ "scope": {
249
+ "type": "string",
250
+ "description": "The role of the assistant account, as the scope that RFC 6749 Section 5.1 grants. It is present only when the account has a role, and never null. An AI assistant MUST NOT treat its absence as an error."
251
+ }
252
+ }
253
+ },
254
+ "oauthError": {
255
+ "type": "object",
256
+ "description": "Sections 5.3 and 6.1 -- the error answer of POST <endpoint>/oauth/token and POST <endpoint>/oauth/device_authorization (RFC 6749 Section 5.2). The status is 400 for every code except `invalid_client`, which is 401. The toll answers with problem documents instead (Section 6.4).",
257
+ "required": [
258
+ "error"
259
+ ],
260
+ "properties": {
261
+ "error": {
262
+ "enum": [
263
+ "invalid_request",
264
+ "invalid_client",
265
+ "invalid_grant",
266
+ "unsupported_grant_type",
267
+ "invalid_dpop_proof",
268
+ "authorization_pending",
269
+ "slow_down",
270
+ "expired_token",
271
+ "access_denied"
272
+ ],
273
+ "description": "The OAuth error code: four of RFC 6749 Section 5.2, `invalid_dpop_proof` of RFC 9449, and the four device-grant codes of RFC 8628 Section 3.5. A closed vocabulary: an operator MUST NOT emit a code outside it."
274
+ },
275
+ "error_description": {
276
+ "type": "string",
277
+ "description": "Human-readable detail. An AI assistant branches on `error` and never on this string."
93
278
  }
94
279
  }
95
280
  },
96
281
  "accessTokenClaims": {
97
282
  "type": "object",
98
- "description": "Section 5.4 -- the decoded claim set of `access_token`. A 3-part RS256 JWT signed by the operator and verifiable statelessly against the JWKS of Section 4.4, presented as `Authorization: Bearer`.",
99
- "required": ["sub", "agent_id", "actor", "iss", "aud", "iat", "nbf", "exp", "jti"],
283
+ "description": "Section 5.4 -- the decoded claim set of `access_token`. A 3-part EdDSA JWT signed by the operator and verifiable statelessly against the JWKS of Section 4.4, bound to the identity's key and presented as `Authorization: DPoP` with a DPoP proof (Section 5.6).",
284
+ "required": [
285
+ "sub",
286
+ "agent_id",
287
+ "actor",
288
+ "iss",
289
+ "aud",
290
+ "iat",
291
+ "nbf",
292
+ "exp",
293
+ "jti",
294
+ "cnf"
295
+ ],
100
296
  "properties": {
101
297
  "sub": {
102
298
  "type": "string",
@@ -121,14 +317,129 @@
121
317
  "aud": {
122
318
  "description": "The operator issuer. A string, or the array spelling RFC 7519 permits.",
123
319
  "oneOf": [
124
- { "type": "string" },
125
- { "type": "array", "items": { "type": "string" }, "minItems": 1 }
320
+ {
321
+ "type": "string"
322
+ },
323
+ {
324
+ "type": "array",
325
+ "items": {
326
+ "type": "string"
327
+ },
328
+ "minItems": 1
329
+ }
126
330
  ]
127
331
  },
128
- "iat": { "type": "integer", "description": "Issued-at. Also what the revoked-before watermark of Section 15.4 is compared against." },
129
- "nbf": { "type": "integer", "description": "Not-before." },
130
- "exp": { "type": "integer", "description": "Expiry. Default lifetime is 1 hour; the durable credential is the private key, not the token." },
131
- "jti": { "type": "string", "description": "Unique token id." }
332
+ "iat": {
333
+ "type": "integer",
334
+ "description": "Issued-at. Also what the revoked-before watermark of Section 15.4 is compared against."
335
+ },
336
+ "nbf": {
337
+ "type": "integer",
338
+ "description": "Not-before."
339
+ },
340
+ "exp": {
341
+ "type": "integer",
342
+ "description": "Expiry. Default lifetime is 1 hour; the durable credential is the private key, not the token."
343
+ },
344
+ "jti": {
345
+ "type": "string",
346
+ "description": "Unique token id."
347
+ },
348
+ "cnf": {
349
+ "type": "object",
350
+ "description": "RFC 9449 Section 6.1 -- the key the token is bound to. Only a DPoP proof that this key signs makes the token usable.",
351
+ "required": [
352
+ "jkt"
353
+ ],
354
+ "properties": {
355
+ "jkt": {
356
+ "type": "string",
357
+ "minLength": 1,
358
+ "description": "The RFC 7638 thumbprint of the registered key."
359
+ }
360
+ }
361
+ }
362
+ }
363
+ },
364
+ "dpopProof": {
365
+ "type": "object",
366
+ "description": "Section 5.6 -- the decoded payload of the `DPoP` request header, a compact JWS whose header carries `typ: \"dpop+jwt\"`, the `alg` of the registered key (EdDSA, or ES256) and its public `jwk`. The AI assistant signs a new one for each token request and for each request that carries an access token.",
367
+ "required": [
368
+ "jti",
369
+ "htm",
370
+ "htu",
371
+ "iat"
372
+ ],
373
+ "properties": {
374
+ "jti": {
375
+ "type": "string",
376
+ "minLength": 1,
377
+ "description": "A unique id for this proof."
378
+ },
379
+ "htm": {
380
+ "type": "string",
381
+ "minLength": 1,
382
+ "description": "The HTTP method of the request."
383
+ },
384
+ "htu": {
385
+ "type": "string",
386
+ "minLength": 1,
387
+ "description": "The URL of the request, with no query and no fragment. On the event stream it is `events_url` with `wss` written as `https`."
388
+ },
389
+ "iat": {
390
+ "type": "integer",
391
+ "description": "The time of signing. The operator refuses a proof more than 60 seconds from its clock."
392
+ },
393
+ "ath": {
394
+ "type": "string",
395
+ "minLength": 1,
396
+ "description": "The base64url SHA-256 of the ASCII access token, with no padding. REQUIRED on a request that carries an access token; absent on a token request."
397
+ }
398
+ }
399
+ },
400
+ "rotationAssertion": {
401
+ "type": "object",
402
+ "description": "Section 5.7 -- the decoded payload of `client_assertion` in a rotation: a client assertion by the registered key that also names the new key.",
403
+ "allOf": [
404
+ {
405
+ "$ref": "#/$defs/clientAssertion"
406
+ }
407
+ ],
408
+ "required": [
409
+ "new_key"
410
+ ],
411
+ "properties": {
412
+ "new_key": {
413
+ "type": "string",
414
+ "minLength": 1,
415
+ "description": "The RFC 7638 thumbprint of the new key in `jwks`."
416
+ }
417
+ }
418
+ },
419
+ "rotationRequest": {
420
+ "type": "object",
421
+ "description": "Section 5.7 -- the body of POST <endpoint>/auth/rotate, a Kiosk extension. The answer is `#/$defs/clientInformation` with the new key; the AI assistant then asks the token endpoint for a token with the new key.",
422
+ "required": [
423
+ "client_assertion_type",
424
+ "client_assertion",
425
+ "new_client_assertion",
426
+ "jwks"
427
+ ],
428
+ "properties": {
429
+ "client_assertion_type": {
430
+ "const": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
431
+ },
432
+ "client_assertion": {
433
+ "type": "string",
434
+ "description": "A client assertion by the registered key. Its decoded payload is `#/$defs/rotationAssertion`."
435
+ },
436
+ "new_client_assertion": {
437
+ "type": "string",
438
+ "description": "A client assertion by the new key, for the same client_id. Its decoded payload is `#/$defs/clientAssertion`."
439
+ },
440
+ "jwks": {
441
+ "$ref": "#/$defs/jwks"
442
+ }
132
443
  }
133
444
  }
134
445
  }
@@ -3,7 +3,7 @@
3
3
  "$id": "https://kiosk.tech/spec/schemas/binding.schema.json",
4
4
  "$comment": "VENDORED copy of the normative kiosk.tech schema at spec/schemas/binding.schema.json. It is here so RequestValidation can hold a reserved-plane request body to it at runtime, and so e2e/schema_conformance.rb can validate this origin's LIVE wire bytes against the PUBLISHED schema -- both with no second checkout and no network fetch. Identical to the upstream copy as PARSED JSON once this $comment is removed.",
5
5
  "title": "Kiosk account binding -- claim and link",
6
- "description": "The wire objects of Section 6, the one-time ceremonies that bind an AI assistant to an existing operator account: claim, an RFC 8628 device grant, and link. Both need the human's approval and a valid possession proof. The two /oauth/* requests are form-encoded and have no schema here; their error answer has one.",
6
+ "description": "The wire objects of Section 6, the one-time ceremonies that bind a registered AI assistant to an existing operator account: claim, an RFC 8628 device grant, and link. Both need the human's approval and a client assertion by the registered key. The two /oauth/* requests are form-encoded and have no schema here; their answers are in auth.schema.json.",
7
7
  "$defs": {
8
8
  "deviceAuthorization": {
9
9
  "type": "object",
@@ -46,56 +46,20 @@
46
46
  }
47
47
  },
48
48
  "deviceTokenResponse": {
49
- "type": "object",
50
- "description": "Section 6.1 step 3: the success answer to POST <endpoint>/oauth/token. The bound principal (`user_id`, `agent_id`) is in the access token's claims.",
51
- "required": ["access_token", "token_type", "expires_in"],
52
- "properties": {
53
- "access_token": {
54
- "type": "string",
55
- "description": "The access token. Its claims are auth.schema.json#/$defs/accessTokenClaims."
56
- },
57
- "token_type": {
58
- "const": "Bearer",
59
- "description": "Always \"Bearer\"."
60
- },
61
- "expires_in": {
62
- "type": "integer",
63
- "description": "Seconds until the access token expires."
64
- },
65
- "scope": {
66
- "type": "string",
67
- "description": "The approving human's role, as the scope that RFC 6749 Section 5.1 grants. It is present only when the binding carries a role, and never null. An AI assistant MUST NOT treat its absence as an error."
68
- }
69
- }
49
+ "$ref": "https://kiosk.tech/spec/schemas/auth.schema.json#/$defs/tokenResponse",
50
+ "description": "Section 6.1 step 3: the success answer to the device-code poll of POST <endpoint>/oauth/token. `scope` is the approving human's role."
70
51
  },
71
52
  "oauthError": {
72
- "type": "object",
73
- "description": "Section 6.1: the error answer of both /oauth/* endpoints, the one error on this wire that is not an RFC 9457 problem document. The status is 400 for every code except `invalid_client`, which is 401.",
74
- "required": ["error"],
75
- "properties": {
76
- "error": {
77
- "enum": [
78
- "authorization_pending",
79
- "slow_down",
80
- "expired_token",
81
- "access_denied",
82
- "invalid_grant",
83
- "invalid_client",
84
- "invalid_request",
85
- "unsupported_grant_type"
86
- ],
87
- "description": "The OAuth error code. A closed vocabulary: an operator MUST NOT emit anything outside it, or an AI assistant polling this ceremony has a branch it cannot take."
88
- },
89
- "error_description": {
90
- "type": "string",
91
- "description": "Human-readable detail. An AI assistant branches on `error` and never on this string."
92
- }
93
- }
53
+ "$ref": "https://kiosk.tech/spec/schemas/auth.schema.json#/$defs/oauthError",
54
+ "description": "Section 6.1: the error answer of both /oauth/* endpoints, the one error shape on this wire that is not an RFC 9457 problem document."
94
55
  },
95
56
  "linkCode": {
96
57
  "type": "object",
97
58
  "description": "Section 6.2 -- the 201 answer to POST <endpoint>/auth/link, called on the operator's own session by the signed-in human. 201 rather than 200 because the call MINTS a redeemable single-use credential.",
98
- "required": ["link_code", "expires_in"],
59
+ "required": [
60
+ "link_code",
61
+ "expires_in"
62
+ ],
99
63
  "properties": {
100
64
  "link_code": {
101
65
  "type": "string",
@@ -109,35 +73,36 @@
109
73
  },
110
74
  "claimRequest": {
111
75
  "type": "object",
112
- "description": "Section 6.2 -- the body of POST <endpoint>/auth/claim, which redeems a link code. The possession proof is REQUIRED, so a leaked code alone can never bind a key its holder does not control.",
113
- "required": ["code", "public_key", "signed"],
76
+ "description": "Section 6.2 -- the body of POST <endpoint>/auth/claim, which redeems a link code. The client assertion is REQUIRED, so a leaked code alone can never bind a key its holder does not control. The answer is 204 No Content; the AI assistant then asks the token endpoint for a token under the human's account.",
77
+ "required": [
78
+ "code",
79
+ "client_assertion_type",
80
+ "client_assertion"
81
+ ],
114
82
  "properties": {
115
83
  "code": {
116
84
  "type": "string",
117
85
  "description": "The `link_code` the human handed over."
118
86
  },
119
- "public_key": {
120
- "type": "string",
121
- "description": "The AI assistant's public key, PEM-encoded."
87
+ "client_assertion_type": {
88
+ "const": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
122
89
  },
123
- "signed": {
90
+ "client_assertion": {
124
91
  "type": "string",
125
- "description": "The possession proof of Section 5.2, as a compact RS256 JWS."
92
+ "description": "A client assertion by the registered key (auth.schema.json#/$defs/clientAssertion)."
126
93
  }
127
94
  }
128
95
  },
129
- "claimResponse": {
130
- "$ref": "https://kiosk.tech/spec/schemas/auth.schema.json#/$defs/registration",
131
- "description": "Section 6.2: the 201 answer to POST <endpoint>/auth/claim, in the shape of the register answer. A new key, a rebound key and a repeated bind give answers that cannot be told apart (Section 6.3)."
132
- },
133
96
  "unlinkRequest": {
134
97
  "type": "object",
135
98
  "description": "Section 6.3 -- the body of POST <endpoint>/auth/unlink, called on the operator's own session. The response is 204 No Content and carries no body at all, which is why no response object appears here.",
136
- "required": ["agent_id"],
99
+ "required": [
100
+ "agent_id"
101
+ ],
137
102
  "properties": {
138
103
  "agent_id": {
139
104
  "type": "string",
140
- "description": "The bound AI assistant to deactivate. Its tokens stop verifying and /auth/login answers 404 (Section 15.4)."
105
+ "description": "The bound AI assistant (its client_id) to deactivate. Its tokens stop verifying and the token endpoint answers it `invalid_client` (Section 15.4)."
141
106
  }
142
107
  }
143
108
  }