kiosk-server 0.5.20

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 (127) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +549 -0
  3. data/LICENSE.txt +202 -0
  4. data/README.md +635 -0
  5. data/app/channels/kiosk_events.rb +138 -0
  6. data/app/controllers/kiosk/server/application_controller.rb +17 -0
  7. data/app/controllers/kiosk/server/assistants_controller.rb +96 -0
  8. data/app/controllers/kiosk/server/auth_controller.rb +187 -0
  9. data/app/controllers/kiosk/server/device_verify_controller.rb +101 -0
  10. data/app/controllers/kiosk/server/discovery_controller.rb +66 -0
  11. data/app/controllers/kiosk/server/jwks_controller.rb +15 -0
  12. data/app/controllers/kiosk/server/kyc_attestation_controller.rb +69 -0
  13. data/app/controllers/kiosk/server/kyc_callback_controller.rb +15 -0
  14. data/app/controllers/kiosk/server/oauth_device_authorization_controller.rb +73 -0
  15. data/app/controllers/kiosk/server/oauth_token_controller.rb +59 -0
  16. data/app/controllers/kiosk/server/open_api_controller.rb +20 -0
  17. data/app/controllers/kiosk/server/payment_setup_controller.rb +31 -0
  18. data/app/controllers/kiosk/server/verb_controller.rb +61 -0
  19. data/app/controllers/kiosk/server/wire_controller.rb +43 -0
  20. data/app/models/kiosk/cart_mandate.rb +21 -0
  21. data/app/models/kiosk/server/application_record.rb +10 -0
  22. data/app/models/kiosk/settlement.rb +12 -0
  23. data/app/models/kiosk/visit.rb +13 -0
  24. data/app/views/kiosk/server/assistants/show.html.erb +94 -0
  25. data/app/views/kiosk/server/device_verify/decided.html.erb +14 -0
  26. data/app/views/kiosk/server/device_verify/show.html.erb +53 -0
  27. data/lib/generators/kiosk/install/install_generator.rb +125 -0
  28. data/lib/generators/kiosk/install/templates/create_kiosk_auth_challenges.rb.tt +16 -0
  29. data/lib/generators/kiosk/install/templates/create_kiosk_device_authorizations.rb.tt +17 -0
  30. data/lib/generators/kiosk/install/templates/create_kiosk_events.rb.tt +22 -0
  31. data/lib/generators/kiosk/install/templates/create_kiosk_identity_tables.rb.tt +16 -0
  32. data/lib/generators/kiosk/install/templates/create_kiosk_kyc_attributes.rb.tt +20 -0
  33. data/lib/generators/kiosk/install/templates/create_kiosk_mandates.rb.tt +21 -0
  34. data/lib/generators/kiosk/install/templates/create_kiosk_pow_spent.rb.tt +16 -0
  35. data/lib/generators/kiosk/install/templates/create_kiosk_refunds.rb.tt +16 -0
  36. data/lib/generators/kiosk/install/templates/create_kiosk_revocations.rb.tt +16 -0
  37. data/lib/generators/kiosk/install/templates/create_kiosk_schema.rb.tt +17 -0
  38. data/lib/generators/kiosk/install/templates/create_kiosk_visits.rb.tt +16 -0
  39. data/lib/generators/kiosk/install/templates/initializer.rb.tt +53 -0
  40. data/lib/kiosk/handler.rb +43 -0
  41. data/lib/kiosk/owned.rb +17 -0
  42. data/lib/kiosk/payable.rb +90 -0
  43. data/lib/kiosk/server/account_binding.rb +188 -0
  44. data/lib/kiosk/server/account_holder_gate.rb +58 -0
  45. data/lib/kiosk/server/action_event.rb +60 -0
  46. data/lib/kiosk/server/actions.rb +14 -0
  47. data/lib/kiosk/server/agent_identity_providers/default_agent_idp.rb +89 -0
  48. data/lib/kiosk/server/agent_login.rb +49 -0
  49. data/lib/kiosk/server/agent_registration.rb +88 -0
  50. data/lib/kiosk/server/argument_decoder.rb +260 -0
  51. data/lib/kiosk/server/audit_sink.rb +42 -0
  52. data/lib/kiosk/server/auth_challenge.rb +33 -0
  53. data/lib/kiosk/server/auth_challenge_stores.rb +74 -0
  54. data/lib/kiosk/server/auth_toll.rb +44 -0
  55. data/lib/kiosk/server/binding_module_gate.rb +25 -0
  56. data/lib/kiosk/server/caller_timezone.rb +50 -0
  57. data/lib/kiosk/server/column_spending_cap.rb +26 -0
  58. data/lib/kiosk/server/configuration_extension.rb +328 -0
  59. data/lib/kiosk/server/controller_verb.rb +63 -0
  60. data/lib/kiosk/server/current_request.rb +31 -0
  61. data/lib/kiosk/server/cursor.rb +33 -0
  62. data/lib/kiosk/server/device_authorization.rb +136 -0
  63. data/lib/kiosk/server/device_authorization_stores.rb +184 -0
  64. data/lib/kiosk/server/device_code_grant.rb +145 -0
  65. data/lib/kiosk/server/device_verification.rb +46 -0
  66. data/lib/kiosk/server/engine.rb +203 -0
  67. data/lib/kiosk/server/errors.rb +339 -0
  68. data/lib/kiosk/server/event_stores.rb +124 -0
  69. data/lib/kiosk/server/events.rb +86 -0
  70. data/lib/kiosk/server/events_cable.rb +102 -0
  71. data/lib/kiosk/server/events_connection.rb +170 -0
  72. data/lib/kiosk/server/executor/repeat.rb +42 -0
  73. data/lib/kiosk/server/executor/spending_cap.rb +102 -0
  74. data/lib/kiosk/server/executor.rb +397 -0
  75. data/lib/kiosk/server/failure_log.rb +28 -0
  76. data/lib/kiosk/server/handler_mixin.rb +418 -0
  77. data/lib/kiosk/server/handler_registrations.rb +57 -0
  78. data/lib/kiosk/server/handler_serving.rb +197 -0
  79. data/lib/kiosk/server/headers.rb +82 -0
  80. data/lib/kiosk/server/headers_middleware.rb +31 -0
  81. data/lib/kiosk/server/identity_resolution.rb +20 -0
  82. data/lib/kiosk/server/issuer_middleware.rb +22 -0
  83. data/lib/kiosk/server/jwks.rb +14 -0
  84. data/lib/kiosk/server/jwt_issuer.rb +98 -0
  85. data/lib/kiosk/server/kyc.rb +261 -0
  86. data/lib/kiosk/server/kyc_verifier.rb +106 -0
  87. data/lib/kiosk/server/link_code.rb +90 -0
  88. data/lib/kiosk/server/linked_assistants.rb +116 -0
  89. data/lib/kiosk/server/mandate_verifier.rb +221 -0
  90. data/lib/kiosk/server/open_api.rb +543 -0
  91. data/lib/kiosk/server/page.rb +19 -0
  92. data/lib/kiosk/server/payment_claim.rb +293 -0
  93. data/lib/kiosk/server/payment_setup.rb +102 -0
  94. data/lib/kiosk/server/pop_verifier.rb +87 -0
  95. data/lib/kiosk/server/pow_gate.rb +121 -0
  96. data/lib/kiosk/server/pow_spent_stores.rb +96 -0
  97. data/lib/kiosk/server/pow_toll.rb +136 -0
  98. data/lib/kiosk/server/queries.rb +14 -0
  99. data/lib/kiosk/server/reputation_factors.rb +38 -0
  100. data/lib/kiosk/server/request_validation.rb +171 -0
  101. data/lib/kiosk/server/response_validation.rb +45 -0
  102. data/lib/kiosk/server/result.rb +25 -0
  103. data/lib/kiosk/server/revocation_stores.rb +57 -0
  104. data/lib/kiosk/server/routing.rb +76 -0
  105. data/lib/kiosk/server/schema_definitions.rb +351 -0
  106. data/lib/kiosk/server/schema_document.rb +116 -0
  107. data/lib/kiosk/server/schema_slots.rb +119 -0
  108. data/lib/kiosk/server/schemas/auth.schema.json +135 -0
  109. data/lib/kiosk/server/schemas/binding.schema.json +145 -0
  110. data/lib/kiosk/server/schemas/kyc.schema.json +114 -0
  111. data/lib/kiosk/server/schemas/mandates.schema.json +75 -0
  112. data/lib/kiosk/server/schemas/pow.schema.json +94 -0
  113. data/lib/kiosk/server/session_context.rb +88 -0
  114. data/lib/kiosk/server/settled_spend.rb +21 -0
  115. data/lib/kiosk/server/signing_key.rb +82 -0
  116. data/lib/kiosk/server/verb_registry.rb +77 -0
  117. data/lib/kiosk/server/verb_vocabulary.rb +163 -0
  118. data/lib/kiosk/server/verb_wire.rb +85 -0
  119. data/lib/kiosk/server/version.rb +7 -0
  120. data/lib/kiosk/server/well_known.rb +416 -0
  121. data/lib/kiosk/server/wire_rendering.rb +116 -0
  122. data/lib/kiosk/server/wrong_door.rb +42 -0
  123. data/lib/kiosk/server.rb +42 -0
  124. data/lib/kiosk/skill_link.rb +16 -0
  125. data/listen.py +278 -0
  126. data/requirements.txt +1 -0
  127. metadata +349 -0
data/CHANGELOG.md ADDED
@@ -0,0 +1,549 @@
1
+ # Changelog
2
+
3
+ All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic Versioning](https://semver.org/).
4
+
5
+ How to write an entry, and what a release section means: `CHANGELOG-RULE.md` at
6
+ the root of this repository —
7
+ <https://github.com/kiosk-hq/kiosk/blob/main/CHANGELOG-RULE.md>. In short: under
8
+ 200 characters, one or two sentences, the essence rather than the content; write it
9
+ under `## [Unreleased]`; a cut renames that heading to
10
+ `## [MAJOR.MINOR.PATCH] — <date>` and opens a fresh empty one above it; nothing
11
+ already written is edited.
12
+
13
+ ## [Unreleased]
14
+
15
+ - `Kiosk.clock_gap_note(name, zone, caller_zone)` answers one sentence on how far a place's clock is from the caller's now, else `nil`.
16
+
17
+ ## [0.5.20] — 2026-10-20
18
+
19
+ - 2026-10-20: The link-code claim and the first token poll of each device code pay the registration toll before any lookup. `RegistrationPow` is now `AuthToll`.
20
+ - 2026-10-20: A verb route reaches the controller action that declares the verb, and `Kiosk::Handler` runs the wire's checks around it. `HandlerDispatch` is removed.
21
+ - 2026-10-20: kiosk-server validates an action's body against its `input_schema` with no exemption. Only a query accepts an undeclared `limit` or `cursor`.
22
+
23
+ ## [0.5.19] — 2026-10-20
24
+
25
+ - 2026-10-20: `listen.py`'s usage line keeps the token in the origin folder `~/.kiosk/https_shop.example_443/`, as the skill says.
26
+ - 2026-10-20: A spending cap has a currency. kiosk-server refuses a `pay` in another currency with `403 spending_cap_exceeded`; `config.spending_cap` returns `{ amount_cents:, currency: }`.
27
+ - 2026-10-20: The `min_client` setting is removed from kiosk-server. `Kiosk-Min-Client` and `kiosk.min_client` carry kiosk-core's `MIN_CLIENT`.
28
+ - 2026-10-20: kiosk-server refuses at class load a verb that declares `cursor`, or a `limit` that is not an integer. A declared `limit` sets the query's page size.
29
+ - 2026-10-20: The `on_bad_proof` setting is removed from kiosk-server.
30
+ - 2026-10-20: The mandate verifier refuses a currency with surrounding spaces, as §11.1 says. Upper case is still folded to lower case.
31
+ - 2026-10-20: The README says a refund gives its amount back to the spending cap.
32
+ - 2026-10-20: The spend the manage-assistants page shows is the settled part of what the spending cap counts. The cap also counts the carts in flight.
33
+ - 2026-10-20: The derived OpenAPI document closes the verb and topic descriptors, as the spec does: a member the spec does not name fails validation.
34
+ - 2026-10-19: The manage-assistants page shows the spend of each assistant per currency, less refunds and never below zero, as the spending cap counts it.
35
+ - 2026-10-19: `Kyc.require!` takes the attribute names an action needs and an optional refusal `detail`; with none it requires every one of `kyc_claims`.
36
+ - 2026-10-19: The `capabilities` configuration setting is removed. Discovery always derives the list from what is registered.
37
+ - 2026-10-19: The spending cap subtracts the `kiosk.refunds` rows of each settlement, never below zero. It needs the `create_kiosk_refunds` migration.
38
+ - 2026-10-19: The derived OpenAPI document writes the verb and topic name `pattern` as `^[a-z][a-z0-9_]*$`, a valid ECMA-262 pattern, as JSON Schema requires.
39
+ - 2026-10-19: An error a handler renders keeps the `type`, `title`, `status` and `detail` of its code; the engine drops its `instance`, and its `challenges` unless the code is `pow_required`.
40
+ - 2026-10-19: The derived OpenAPI document describes the `events` array of the `schema` catalog, so it accepts a catalog that serves topics.
41
+ - 2026-10-19: A `Page` refuses an empty `next_cursor`: the last page passes nil, and the wire sends no `next` link that leads back to page one.
42
+ - 2026-10-19: The engine refuses a query answer that is not a JSON array with `action_failed`, paginated or not.
43
+ - 2026-10-19: The README says the engine keeps its own state in the `kiosk` schema and writes no audit record there; the audit sink is off by default.
44
+
45
+ ## [0.5.18] — 2026-10-18
46
+
47
+ - 2026-10-18: Version 0.5.18, the tree cut that matches skill 0.5.18.
48
+
49
+ ## [0.5.17] — 2026-10-18
50
+
51
+ - 2026-10-18: The `kyc_required` hint names the attributes it asks for, as the specification says.
52
+ - 2026-10-18: `refund` takes `key:`, which names the refund among the refunds of its charge; kiosk-pay-stripe keys the Stripe request by the charge and that key.
53
+ - 2026-10-18: A `kyc_provider` with empty `kyc_claims` raises `ConfigurationError` at boot: no verification could grant a gated action.
54
+ - 2026-10-18: `PaymentClaim` takes no `status_column:` or `unpaid:` (the column is `payment_status`), and `ColumnSpendingCap.new` takes no arguments.
55
+ - 2026-10-18: The kiosk-server README tells the operator to set the status column to `refunded` after a full refund, before the announcement.
56
+ - 2026-10-18: A `reputation_policy` toll with string keys is a `ConfigurationError`; the toll reads `alg`, `params` and `count` as symbols, as the policy contract states.
57
+ - 2026-10-18: The `on_proof_verified` policy hook of `PowGate` is removed.
58
+ - 2026-10-18: `KycVerifier.verify` asks the configured `kyc_provider#accepts?`, so `POST /kiosk/agents/kyc` refuses with 403 what the provider does not accept.
59
+ - 2026-10-18: `PaymentClaim#refund` records the refund in the new `kiosk.refunds` table, and recent spend leaves the refunded cents out. Run the `create_kiosk_refunds` migration.
60
+
61
+ ## [0.5.16] — 2026-10-18
62
+
63
+ - 2026-10-18: `Kiosk::Server::ActionEvent#ok?` and `#error?` are removed; read `status`.
64
+ - 2026-10-18: `include Kiosk::SkillLink` in a page controller sends `Link: <skill_url>; rel="kiosk"` on every answer.
65
+ - 2026-10-18: A public key over 4096 bits is refused with `400 bad_request`; `pay` resolves the caller before it reads the body, so a bad token is 401 first.
66
+ - 2026-10-18: kiosk-server no longer depends on `solid_cable`; the host bundles the Action Cable adapter its `config/cable.yml` names.
67
+ - 2026-10-18: Boot, declaration and currency refusal messages state the fault and the fix in one or two sentences, with no specification section numbers.
68
+ - 2026-10-18: `Kiosk.reader_zone(default = nil)` answers the zone the caller declared in `Kiosk-Timezone`, else `default`.
69
+ - 2026-10-18: `c.after_payment` runs after the engine records the settlement, through `PaymentClaim#settled`, so the hook reads the row's `settlement`.
70
+
71
+ ## [0.5.15] — 2026-10-18
72
+
73
+ - 2026-10-18: `Kiosk::Payable` gives a payable model `payment_state`, `with_settlement`, `cart` and `settlement`; `PaymentClaim` pushes the `payment` topic when a capture returns.
74
+
75
+ - 2026-10-18: A device authorization store answers `lookup_by_device_code_hash` and `lookup_by_user_code_hash`; a custom store renames its two `find_by_*` methods.
76
+
77
+ ## [0.5.14] — 2026-10-18
78
+
79
+ - 2026-10-18: Version 0.5.14, the tree cut that matches skill 0.5.14.
80
+
81
+ - 2026-10-18: The proof-of-work toll is paid once per access token (`access_token_ttl`, 3600 s). Settlements and recorded visits count as recent spend; `reputation_factors` is gone.
82
+
83
+ - 2026-10-18: A repeat of an unfinished `pay` chain charges again under the same key and records the missing settlement. A declined charge stops counting against the spending cap at once.
84
+
85
+ - 2026-10-17: `kiosk:install` writes each migration's DDL into the file, so a generated migration no longer depends on the installed kiosk-server version.
86
+
87
+ - 2026-10-17: Registration proof-of-work always uses kiosk-pow-equihash's default params (n=168 k=7); the `registration_pow_params` setting is removed.
88
+
89
+ - 2026-10-17: `schema` and `guc_namespace` are kiosk-server settings; the `registration_difficulty=` stub is gone. `kiosk:install` writes no line that repeats a default.
90
+
91
+ - 2026-10-17: A production origin that serves events and has no Action Cable adapter in `config/cable.yml` does not boot; outside production it falls back to `async`.
92
+
93
+ - 2026-10-17: Auth challenges and events are stored in the database by default; run `kiosk:install`'s new `kiosk.auth_challenges` migration. The in-process stores are gone.
94
+
95
+ - 2026-10-17: `PaymentClaim` takes the payable model by name (`model: "Order"`) instead of a table, so ids, owner and timestamps follow the model. `Kiosk::Owned` adds `owned_by`.
96
+
97
+ - 2026-10-17: Loads through Zeitwerk; controllers, the KioskEvents channel and views live under app/. TestExecutor and ConformanceOrigin moved to kiosk-test-support.
98
+
99
+ - 2026-10-17: `kiosk:install` no longer creates `kiosk.reservations`, and `SchemaDefinitions.reservations_sql` is gone; nothing used the table.
100
+
101
+ - 2026-10-17: The spending cap counts carts whose capture may still run and serialises one assistant's pays, so two concurrent pays cannot pass the cap together.
102
+ - 2026-10-17: `PaymentClaim` runs `after_payment` only for the call that moves the row to paid, so a hook never runs twice for one payment.
103
+ - 2026-10-17: The proof-of-work toll checks a proof's signature, expiry and parameters before it records the challenge as spent, and reads no more proofs than it asked for.
104
+ - 2026-10-17: Token revocation is stored in the new `kiosk.revocations` table by default, so a revoke or an unlink holds in every process and after a restart; run `kiosk:install`'s new migration.
105
+ - 2026-10-17: The wire and the event stream accept only a bearer token, never the operator's session cookie; `auth/link` and `auth/unlink` refuse a request that is not `application/json`.
106
+ - 2026-10-17: `Kiosk::Server.listener_path`, `Page#truncated?` and `EventStore#prune_before` are removed; the in-process event store never reports a truncated cursor.
107
+ - 2026-10-17: A handler may answer through its JSON template (Jbuilder, say), rendered whatever the caller's `Accept`; `render_kiosk_page` without rows pages what the template renders.
108
+ - 2026-10-17: A topic may declare `example` payloads, checked against its `payload_schema` at declaration and published as the topic's `examples`.
109
+
110
+ ## [0.5.13] — 2026-10-11
111
+
112
+ - 2026-10-11: Version 0.5.13, the tree cut that matches skill 0.5.13; the engine's default `skill_url` pins `skill-v0.5.13.md`.
113
+
114
+ - 2026-10-11: **Operators route with a `kiosk do … end` block in `config/routes.rb`**: it mounts the engine, then draws `queries` as GET and `actions` as POST; `config/routes/kiosk.rb` is gone.
115
+
116
+ - 2026-10-10: **A failed model validation answers 400 `bad_request` naming what is wrong**: `RecordInvalid` and `ActiveModel::ValidationError` carry their full messages to the wire.
117
+
118
+ - 2026-10-09: **kiosk-server depends on `solid_cable`**, the event stream's pubsub across workers; an operator no longer adds it.
119
+
120
+ - 2026-10-09: **`assistant_claimed` receives `from:` and `to:` accounts, and `assistant_unlinked` an `account:`**, as `user_model` records instead of ids.
121
+
122
+ - 2026-10-09: **Spent proof-of-work ids live in the database by default**, so a deploy or a second worker no longer makes a spent proof spendable again; `kiosk:install` lays down `kiosk.pow_spent`.
123
+
124
+ - 2026-10-09: **`Kiosk::OperationResult` and `render_kiosk_result` are removed**; operations raise `Kiosk::Server::Errors`. `PaymentClaim` pays only the owner's row.
125
+
126
+ - 2026-10-09: **`Kiosk.current_role`** beside `Kiosk.current_user_id`; `SessionContext.user_id` becomes `SessionContext.identity`.
127
+
128
+ - 2026-10-09: **The `schema_major()` boot check is removed**, and `kiosk:install` sets `dump_schemas` so `schema.rb` carries the kiosk tables.
129
+
130
+ - 2026-10-09: **`c.signing_key` is required and no longer read from `KIOSK_SIGNING_KEY_*`; `c.pow_secret=` refuses a secret under 32 bytes.**
131
+
132
+ - 2026-10-09: **`Kiosk::Owned` and `Kiosk.current_user_id`**; `Kiosk::Settlement.own` replaces `of_current_principal`, and `SessionContext.require_open!` is gone.
133
+
134
+ - 2026-10-09: **`c.handlers` is removed**: classes including `Kiosk::Handler` register themselves, and the engine loads `app/controllers/kiosk/` on every reload.
135
+
136
+ ## [0.5.12] — 2026-10-08
137
+
138
+ - 2026-10-08: Version 0.5.12, the tree cut that matches skill 0.5.12; this gem's surface is unchanged.
139
+
140
+ ## [0.5.11] — 2026-10-08
141
+
142
+ - 2026-10-08: Version 0.5.11, the tree cut that matches skill 0.5.11; this gem's surface is unchanged.
143
+
144
+ ## [0.5.10] — 2026-10-08
145
+
146
+ - 2026-10-08: Version 0.5.10, the tree cut that matches skill 0.5.10 and carries the entries below.
147
+
148
+ - 2026-10-08: `SchemaDefinitions` drops `agents_issuer_sql`, `kyc_on_person_sql`, `kyc_user_fk_sql` and the agents column repairs; `kiosk:install` emits only creates.
149
+ - 2026-10-08: **`PaymentClaim` checks the cart itself**; the operator configures only `c.cart_price_checker`, its price from its own catalog, and an optional `c.after_payment`.
150
+
151
+ ## [0.5.9] — 2026-10-08
152
+
153
+ - 2026-10-08: Version 0.5.9, the tree cut that matches skill 0.5.9 and carries the entries below.
154
+
155
+ - 2026-10-07: **`Kiosk::Server::PaymentClaim` keeps the operator half of §11.6**: one capture per payable row, and a paid state that rests on the capture (T-176).
156
+
157
+ - 2026-10-07: **`Kiosk::Settlement` and `Kiosk::CartMandate` read the receipts `pay` records**, so an operator answers "is this paid" without modelling the engine's tables (K-1788).
158
+
159
+ - 2026-10-07: **`render_kiosk_result` renders a `Kiosk::OperationResult` inside a handler**: the value on success, the coded refusal otherwise (K-1788).
160
+
161
+ ## [0.5.8] — 2026-10-07
162
+
163
+ - 2026-10-07: Version 0.5.8, the tree cut that matches skill 0.5.8; this gem's surface is unchanged.
164
+
165
+ ## [0.5.7] — 2026-10-07
166
+
167
+ - 2026-10-07: **An unreadable `Kiosk-Timezone` is refused `400` before the proof-of-work toll**, with the arguments, rather than after a paid proof (K-2016).
168
+
169
+ - 2026-10-07: **The binding pages' `401` names the operator's sign-in URL and the page's own**, so a non-browser caller can hand the human a working path (K-2015).
170
+
171
+ ## [0.5.6] — 2026-10-07
172
+
173
+ - 2026-10-07: **A malformed date or timestamp in an action body is refused with a hint naming `YYYY-MM-DD` or ISO 8601**, as the query channel already did (K-2007).
174
+ - 2026-10-07: **The device-authorization call refuses an empty `role` or `scope` with `400 invalid_request`**, as it refuses any other value (K-2005).
175
+
176
+ ## [0.5.5] — 2026-10-07
177
+
178
+ - 2026-10-07: **The default `skill_url` names `skill-v0.5.5.md`, and `listen.py` reads the bearer token from `--token-file`**, never from its command line.
179
+
180
+ - 2026-10-07: **An `action_failed` detail names the verb and no longer the exception class** the handler raised; the operator's log still carries both.
181
+ - 2026-10-07: **With neither a `kyc_provider` nor a `kyc_public_key`, the `kyc_required` hint names no KYC path** and says verification is not available at this origin.
182
+ - 2026-10-07: **`kiosk.kyc_attributes` and `kiosk.kyc_requests` reference the host's user table `ON DELETE CASCADE`**; `kyc_user_fk_sql` adds the key to existing tables.
183
+
184
+ - 2026-10-07: **The `payment_setup` return page says the assistant can pay only when the provider confirms it**; otherwise it says the assistant will check.
185
+
186
+ - 2026-10-07: **The `kyc_required` hint names `request_kyc` only where a `kyc_provider` serves it**, and a wrong-subject refusal names the principal each KYC path compares.
187
+
188
+ ## [0.5.4] — 2026-10-07
189
+
190
+ - 2026-10-07: **The default `skill_url` names `skill-v0.5.4.md`**, the cut that says the KYC cap and attributes are the human's and `payment_setup` is a fixed path.
191
+
192
+ - 2026-10-07: **kiosk-server serves `request_kyc`, the provider callback and `kyc_verification` against a `kyc_provider`**, recording attributes against the person (T-220).
193
+
194
+ - 2026-10-07: **The engine serves `payment_setup` and the provider's return page** wherever a `payment_provider` is configured, so an operator no longer writes either (T-219).
195
+
196
+ - 2026-10-06: **The production boot refusal for an in-process event store now says why: a restart loses events inside the 24-hour retention window.**
197
+
198
+ ## [0.5.3] — 2026-10-06
199
+
200
+ - 2026-10-06: **The default `skill_url` names `skill-v0.5.3.md`**, and `listen.py` tells an assistant to run it tracked in the background and tell its human.
201
+
202
+ ## [0.5.2] — 2026-10-06
203
+
204
+ - 2026-10-06: **`/auth.md` names the event stream among the modules `501 module_not_served` can mean**, beside binding, payment and KYC.
205
+
206
+ - 2026-10-06: **`listen.py` leaves on `501 module_not_served` with exit 4** rather than reporting a dead token or re-minting for ever, and sends no `Origin` header.
207
+
208
+ - 2026-10-06: **The default `skill_url` names `skill-v0.5.2.md`.**
209
+
210
+ - 2026-10-05: **A subscription that names no `subject` is accepted on every topic** and carries what was addressed to the subscriber; the subject rule answers for a named one.
211
+
212
+ - 2026-10-05: **Security: a revoked or expired token closes every events socket, subscribed or not**, and can no longer open a subscription on one held from before.
213
+
214
+ - 2026-10-05: **The package ships `requirements.txt` beside `listen.py`**, declaring the event-stream listener's `websockets>=12` in a manifest.
215
+
216
+ - 2026-10-05: **The boot warning reads every served origin**, `c.additional_origins` included, and names the loopback one it found.
217
+
218
+ - 2026-10-05: **An assistant account belongs to one origin.** Register, login, bind, unlink and the manage page look a key up on the origin being served only.
219
+
220
+ - 2026-10-05: **`kiosk.agents` gains `issuer`**, and a live key is unique per origin. `SchemaDefinitions.agents_issuer_sql` backfills an existing table with `c.issuer`.
221
+
222
+ - 2026-10-05: **Each served origin is its own issuer.** Discovery, the possession proof, tokens, mandates, the realm and the KYC audience read `Kiosk.current_issuer`, set per request.
223
+
224
+
225
+ - 2026-10-05: **`KioskEvents` refuses a `since` that is not a cursor and a `subject` that is not a string.** A cursor is a non-negative integer up to 2^53 - 1, in the frame or the URL.
226
+
227
+ - 2026-10-04: **`Engine.issuer_warning` names an `issuer` that cannot be right at boot.** Unset in any environment, loopback outside development and test; it warns rather than refusing.
228
+
229
+ - 2026-10-04: **`SchemaDocument`'s own comments name the three members it builds.** They said `{queries, actions}` directly above the line that adds `events`.
230
+
231
+ - 2026-10-04: **`EventsConnection` answers the frames Action Cable swallowed.** An `unsubscribe` it cannot resolve and a non-cursor `since` are refused; a duplicate `subscribe` is re-confirmed.
232
+
233
+ - 2026-10-04: **`EventsConnection` answers an unusable frame.** Action Cable logged and returned, leaving a subscriber on a socket that would never speak; now a `reject_subscription` or a typed close.
234
+
235
+ - 2026-10-04: **`<endpoint>/events` answers `501 module_not_served` where no topic is declared.** The events module declines like `pay` and KYC, before the credential is read.
236
+
237
+ - 2026-10-04: **The replay filter narrows by topic as well as subject.** It reads the identity's whole tail, so a subscription used to be handed events of topics it never named.
238
+
239
+ - 2026-10-04: **The revoked and expired disconnects are each published once.** `Connection::Base#close` carries the reason and the flag; a frame transmitted before it published them twice.
240
+
241
+ - 2026-09-29: **Security: the handler dispatch inherits the host's parameter filter.** A filtered field was masked on the wire request's log line and written in full on the handler's.
242
+
243
+ - 2026-09-29: **Security: the engine keeps the wire's own credentials out of the request log.** The possession proof, the link and device codes, the KYC attestation and the three mandates are filtered.
244
+
245
+ - 2026-09-29: **An operator's own `subject_reachable` rule that RAISES is now named in the engine's log.** The refusal is unchanged; it simply stopped being indistinguishable from the rule saying no.
246
+
247
+ - 2026-09-29: **`topic … do description nil end` is accepted**, on the same terms as a verb's null description. The macro must be called; its value may be null, as the schema types it.
248
+
249
+ - 2026-09-29: **`EventStore#truncated?` agrees with the durable store at the floor.** A cursor one below it holds a complete range, so it no longer costs a subscriber an ordinary re-read.
250
+
251
+ - 2026-09-29: **The stream tells an expired credential from a revoked one**: `token_expired` with `reconnect: true` where the identity chain refused a token that had simply reached its `exp`.
252
+
253
+ - 2026-09-29: **Security: the event stream's re-authorisation now runs.** A withdrawn reach gets `unsubscribed` and a credential that stops resolving gets `disconnect` and a closed socket.
254
+
255
+ - 2026-09-29: **The manage-assistants listing only swallows a MISSING settlements table.** Any other statement error now raises instead of rendering a 200 with settled 0.
256
+
257
+ - 2026-09-29: **The genesis records `<schema>.schema_major()` and the engine reads it at boot**, refusing a gem two or more majors ahead of the schema and naming the major to install next.
258
+
259
+ - 2026-09-28: **`kiosk:install` writes `config/routes/kiosk.rb` with the engine mounted, and draws it from `config/routes.rb`.** An install that stopped at the initializer served nothing.
260
+
261
+ - `validate_requests` now also holds register, login, claim, unlink, the KYC attestation and `pay` bodies to their published schemas; a wrong-typed member is a 400 naming it.
262
+
263
+ - 2026-09-28: **`c.serve_account_binding = false` declines the binding module: every binding path answers `501 module_not_served`.** Default true, and the six published auth URLs do not move.
264
+ - 2026-09-28: **The engine's Action Cable server stops running the browser forgery check.** It refused every upgrade without an `Origin` header; the `Authorization` header is the authorisation.
265
+ - 2026-09-28: **A signed-out human landing on the claim ceremony’s verify page is sent to the operator’s sign-in, as on the manage-assistants page.** `sign_in_path` now governs both pages.
266
+
267
+ ## [0.5.0] — 2026-09-26
268
+
269
+ - 2026-09-25: **`listen.py` gains `--until-event`: it exits 0 on the first event and 5 at the deadline**, so an assistant holds a wait in one foreground call instead of tailing a log.
270
+ - 2026-09-25: **An origin that declares an event topic and leaves the in-process store in place is refused at production boot (K-1804).** One declaring no topic is untouched.
271
+ ### Changed
272
+
273
+ - **BREAKING (small): an origin that declares `c.roles` must configure `c.registration_role`, or it does not boot (ADR-0036).** An origin that declares no roles is untouched.
274
+ - **BREAKING (small): a role-less rebind no longer keeps the assistant's existing role.** It binds at `registration_role`, or the empty role set, exactly as a fresh key does (K-1791).
275
+ - **The install template stops calling the external agent-IdP seam «planned».** It names the one subclass the engine actually ships (K-1673).
276
+ - **The gemspec named `ActiveRecord::Base.connection`; the engine uses `lease_connection` everywhere.** The install template stops naming the deleted query/run endpoints.
277
+ - **SUPERSEDES the 0.2.0 note that the `user_code` alphabet is «8-char Crockford (no 0/O/1/I/L/U)».** It is 31 chars and KEEPS U; the brute-force space is 31^8.
278
+ - **The PoW gate and spent store described a caller, a spec count and a memory bound they do not have.** A new example covers the sweep that actually runs.
279
+ - **A second declaration of one verb name on a handler class is refused at class-body load.** It used to replace the first, taking that verb off the wire with nothing saying so.
280
+ - **Three more engine comments named a wrong code count, a stale demo tally, or a version that would carry unbuilt work.** Each now describes the code beside it.
281
+ - **Engine comments that named a wrong count, caller or cause now say what the code does.** A new spec holds `server.rb`'s two controller lists to the directory they describe.
282
+ - **Mounting the engine is how the protocol plane is drawn, not one of two ways.** The README, the gemspec and the controller headers stop offering a hand-drawn copy as an escape hatch.
283
+ - **A configuration store slot could hand two concurrent first-touchers two different stores, so one PoW proof was accepted twice.** The four store defaults now build under a lock.
284
+ - **`/auth.md` hands a cold assistant the wire skill and the published solver.** `## Discover` carries `skill_url` and its digest; `## Register` names the solver the 403 hint names.
285
+ - README: the actionpack message quoted as an example of a sentence the wire does not publish is described in the present tense. It said `params.require` «used to» put that string on a 400; actionpack still does, so the past tense claimed a change that has not happened. The point it illustrates — a library's wording moves when the dependency moves, so the CODE travels and the sentence does not — is unchanged.
286
+ - **`Kiosk-Timezone` is read, validated and handed to handlers.** A caller may declare its own clock as an IANA `Area/Location` name (or `UTC`) on any verb request; the engine parses it once at the wire seam, refuses a value it cannot read — a UTC offset included, since one cannot carry a DST transition — with a `400 bad_request` naming the header, and exposes the resolved zone as `Kiosk::Server::CurrentRequest.timezone`. A caller that declares nothing gets `nil`, which is not an error. `Vary` on every verb response grows to `Authorization, Kiosk-PoW, Kiosk-Timezone`. The engine learns nothing about which zone an ANSWER is rendered in: that belongs to the resource being served, which is operator data.
287
+ - **`Kiosk::Server::VerbRefusalController` and the tail route that reached it are removed.** A mounted engine draws the protocol plane and nothing else; the operator draws one explicit route per verb. A path under the mount that no line draws — an unknown name, or a verb dialed with the method its kind does not take — matches nothing, so it is the ordinary 404 your web framework answers at any unrouted path, with no `code` and no `hint`. The mount-path middleware still stamps the version headers on it. Draw a catch-all action of your own at the end of your routes file if you want the wire's own `404 verb_not_found` there; nothing requires one, because an assistant reads `GET <mount>/schema` before it dials.
288
+ - **Three surfaces still described the refusal route this release removed.** `GET <endpoint>/openapi.json` told every generated client that calling a verb with the other method answers `405` with `Allow`; it now says what this engine does — one route per verb with the method its kind requires, so the other method matches nothing and answers an ordinary 404. The README's component list said the engine «appends the wire's own 404/405 refusal below your routes», 280 lines above its own account of what it actually draws. And the api-catalog comment called a `GET` at an action's path «a 405». No behaviour changed; three descriptions of it did.
289
+ - **The account-binding keys in `/.well-known/kiosk.json` state the property instead of a version.** They were annotated «additive 0.1.x-compatible», which named a wire this engine refuses by construction — it stamps `Kiosk-Min-Client: 0.4.0` on every mount-path response. What an implementer needs is the additive part: a consumer that does not know `device_authorization_url` and `claim_url` still finds a complete register/login path in the same block, so binding is an option a client may ignore rather than a second dialect it has to speak.
290
+ - README: every `gem` line in the install section now carries `github: "kiosk-hq/kiosk"`, so a copied line resolves before publication; the banner above them states what the block does rather than asking the reader to add it.
291
+ - A payment-free or KYC-free origin answers `POST /kiosk/pay` and
292
+ `POST /kiosk/agents/kyc` with `module_not_served` (501). The behaviour is
293
+ unchanged; the route table's own comments had promised 403, and now say 501
294
+ and why: whether an origin serves a module at all is a fact about the origin,
295
+ not about the caller's identity.
296
+ - `c.validate_requests` is documented as what it is — ON by default, with the
297
+ one line that turns it off — where two comments had still called it opt-in.
298
+ - Source comments are in English throughout: quoted design conversation and
299
+ maintainer attributions are restated as statements about the code.
300
+ - Source comments, the README, the gemspec and the emitted identity-table SQL no
301
+ longer cite this project's private tracker or its ADRs. Every sentence that
302
+ explained the code is kept, restated in the present tense where it had been
303
+ written as history; the afterlife of 0.3's `POST /kiosk/query` and
304
+ `POST /kiosk/run` is gone from the engine, the controllers and the README.
305
+ - **BREAKING: the engine no longer routes your verbs — you draw one explicit route per verb (T-183).** `mount Kiosk::Server::Engine` now draws the PROTOCOL PLANE only (`schema`, `pay`, `openapi.json`, JWKS, the `auth/*` ceremonies, the device-grant pair, `agents/kyc`, the binding pages, root discovery). The `get`/`post "<mount>/:kiosk_verb"` pair it used to draw LAST is deleted. Draw the mount first, then `get "/kiosk/<query>", to: "kiosk/server/verb#show", defaults: { kiosk_verb: "<query>" }` and `post "/kiosk/<action>", to: "kiosk/server/verb#create", …` — GET for a query, POST for an action. See the README's «Draw the routes».
306
+
307
+ - **The vendored PoW schema types a proof's `nonce` relative to `challenge.alg` (K-1410).** It required the Equihash solution object of every proof, so with `validate_requests` on a valid proof from any other backend was a 400 before the gate saw it — while the protocol keeps `alg` open on purpose. Equihash keeps its exact shape; any other `alg` is left to its own backend. The 400's hint follows.
308
+
309
+ - **`Cursor` publishes the offset as a decimal integer and refuses a cursor it did not issue (K-1403).** BREAKING for a handler that stored the old base64 token: `encode_offset(40)` is now `"40"`, and `decode_offset` raises `Errors::BadRequest` on a present-but-malformed cursor instead of returning the default. An absent or empty cursor is still the first page. A cursor authorizes nothing, so it is not signed — scope your page by the principal, not by the token.
310
+
311
+ - **`c.validate_requests` now defaults to `true` (K-1399).** With it off, a malformed `Kiosk-PoW` yields no parseable proofs and the gate re-issues a fresh 402 forever with no diagnostic; with it on, the caller gets a typed 400 naming the shape. Every shipped origin and the install generator already turned it on, so `false` was a default nobody wanted. `c.validate_responses` stays `false`: a response-shape refusal is a 500 to a caller who did nothing wrong, and that reason does not transfer to a request-shape 400. An explicit `c.validate_requests = false` is honoured.
312
+
313
+ - **A policy that branches on `:action` is now refused at configuration time (K-1395).** `Kiosk.configuration.reputation_policy=` and `reputation_factors=` raise `Kiosk::Server::Errors::ConfigurationError` when the hook compares its `verb:` argument against `:action` — the DECLARATION vocabulary's word, which the gate never hands over. A handler declared `kind :action` arrives at these hooks as `:run` (protocol §13), and the wrong spelling used to match nothing and decline to toll every write in silence. A `:action` symbol that is not a comparison — a hash key, a string, a comment — is untouched, and a hook whose source cannot be read raises nothing.
314
+
315
+ - **`rails g kiosk:install` now writes both validation flags into the initializer it generates (K-1336, recorded here by K-1390).** The template previously carried no `validate` line at all, so a freshly generated app inherited `validate_requests = false` and `validate_responses = false` from the engine defaults and had to opt in before either assertion could fire. It now writes `c.validate_requests = true` and `c.validate_responses = !Rails.env.production?`, so a generated app validates request shapes from its first boot and validates its own responses everywhere except production, where doing so would turn a descriptor typo into a 500 for a caller. Nothing changes for an existing app: the defaults are untouched and only the generated file moved. This is a change to what the packaged gem PRODUCES, which is why it belongs here and not only in the repository journal — it was recorded only there when it shipped.
316
+
317
+ - **`AccountBinding.bind!` warns when role resolution is not total (T-165, Phil's decision on K-1134).** An operator that declares roles owes one to EVERY human who can approve a binding; a role for staff and nothing for customers is a misconfiguration of the operator's identity system, and it is the only way the retain-the-existing-role branch becomes reachable. A ceremony carrying no role at an origin declaring more than one is exactly that mixture and nothing else, so it now emits one loud line naming the cause and the repair. It does NOT raise and the ceremony is unchanged. There is no boot-time check because there cannot be one: totality is a property of the host's `#kiosk_role` over its users table, which no configuration file holds.
318
+
319
+ - **BREAKING (small): `DeviceVerification.approve` now REQUIRES `role:` (K-1127).** The module's own header invites a provider to drive a bespoke consent page from these helpers, and the keyword defaulted to `nil` — so a host that took up the invitation and simply did not know about `role:` produced role-less bindings silently, indistinguishably from an origin that deliberately reports no role. Naming it is mandatory now; passing `nil` is still legal and still means the second of those two, so `registration_role`/absent binding is unchanged (ADR-0011's no-regression clause). Nothing in this repo relied on the default: the engine's own `DeviceVerifyController` has always passed the approving identity's role, and the other role-less `approve` — the one `LinkCode.mint` calls, legitimately, because a link row is minted BY the human and already carries theirs — is `DeviceAuthorization#approve`, a different method that this module never reaches for a `:link` row at all.
320
+
321
+ - **BREAKING: the claim ceremony no longer accepts a client-chosen `role`/`scope`, and takes the role from the approving human instead (K-072 — this entry cited K-1109 when written, which is an unrelated row; corrected in place 2026-08-27, K-1130).** `POST <mount>/oauth/device_authorization` is unauthenticated; it read the role from that request, checked only that `config.roles` contained it, and wrote it into the row and then into the issued JWT — a privilege-escalation primitive on any origin declaring more than one role, reachable by a stranger and rubber-stamped by whichever account holder approved. The parameter is now `400 invalid_request` (refused, not ignored: a silent drop leaves the caller believing it got what it asked for), `DeviceCodeGrant.start` no longer takes `requested_role:` so a claim row cannot be born with one, and `DeviceVerification.approve` gained `role:` — the approving human's `Identity#role`, which `DeviceVerifyController` passes and the durable store now persists on UPDATE. The rebind branch is covered by the same source, so the escalation is not available one ceremony later. Side effect worth naming: the claim direction now INHERITS the human's role at all, which roles-from-IdP always specified and only the link direction implemented — an owner claiming used to bind at `registration_role`. Hosts calling `DeviceVerification.approve` from their own consent page should pass `role:`.
322
+
323
+ - **The engine's binding consent page names the access it is handing over (K-072 — this entry cited K-1109 when written, which is an unrelated row; corrected in place 2026-08-27, K-1130).** The verify page showed the key's RFC 7638 fingerprint and when it asked, and nothing about what the assistant would be able to do — so an approval was informed about identity and blind about privilege. It now states that the assistant will act as the approver, and names the approver's role where the provider defines one. Every host inherits it; a host shipping its own `app/views/kiosk/server/device_verify/show.html.erb` override should add the same line.
324
+
325
+ - **The wire-dispatch guard answers an RFC 9457 problem document, not the retired 0.3 envelope (K-1092).** `Kiosk::Handler`'s `kiosk_require_wire_dispatch!` 404s a route an operator drew straight at a handler controller — and it renders that 404 to a CLIENT, since the guard returns early under sub-dispatch and nothing re-wraps it. It was the last shipped body emitting `{ok:false, error:{…}}`, the shape deleted with the endpoints that served it; it now emits the flat document the wire emits, `application/problem+json` and all, built by `Errors::NotFound` so the two cannot drift. The nested shape inside `kiosk_rescue_to_wire` is untouched: that one is the internal handler↔`HandlerDispatch` protocol, which the Executor decodes.
326
+
327
+ - **`SchemaDefinitions` emits DDL that can replay onto a database that already has the kiosk schema (K-1083).** The 2026-08-20 rebuild renumbered every kiosk-owned migration, so on any pre-existing database all six re-emitted files read as pending and replayed onto tables that were already there — measured, `db:migrate` aborts one step in with `PG::DuplicateTable` and strands every later migration, including any corrective one. Every `CREATE` is now guarded, and migration 002 additionally brings a pre-fold `agents` table up to shape (`ADD COLUMN IF NOT EXISTS` for the three columns K-646 folded into the create) rather than stepping over it. A guarded create accepts a drifted table where an unguarded one shouted, so `bin/check-migration-replay` diffs the replayed catalogue against `db/structure.sql` on every push: the loud failure moved earlier, it did not go away.
328
+
329
+ - **The manage-assistants page says so when a spending cap binds nothing (K-1014).** The cap editor is rendered on every origin, but the number it saves is consulted only where an operator wires a `config.spending_cap` seam, so a human could set a policy nothing enforced and the page said nothing about it. It now states that where the control is rendered; the field still submits, because setting the policy on an origin that may not be the one that charges is deliberate.
330
+
331
+ - **BREAKING: `Kiosk::Query` and `Kiosk::Action` are replaced by one `Kiosk::Handler` mixin (K-921).** A handler controller had a kind, so queries and actions could never share a class. They can now: `include Kiosk::Handler` and mark each declaration `kind :query` or `kind :action` — the kind is a property of the declaration, and a declaration without one is refused at class-body load rather than defaulted onto an HTTP method. A name declared as both kinds on one class is refused there too, the same rule `HandlerRegistrations` already applied across classes. Migration is mechanical: swap the include, add one `kind` line per verb.
332
+
333
+ - **The PoW gates refuse to mint a challenge at a difficulty no proof could satisfy (K-843).** `Challenge.issue` is algorithm-agnostic and signs whatever params it is handed, so a degenerate `registration_pow_params` — or a policy computing one — produced challenges that every honest solve failed, answered with `403 invalid proof of work`. `PowGate.enforce` now asks the registered backend first (`Kiosk::Reputation::Backends.valid_params?`) and raises `Errors::ConfigurationError` naming the setting instead. A NEW RAISE PATH: a host whose PoW parameters were already unsatisfiable now fails loudly at the gate rather than quietly on the client. Backends that do not implement `.valid_params?` are unconstrained and unaffected.
334
+
335
+ - **A host with no `payment_provider` answers `pay` with the 403 it always promised, before it asks for mandates (K-800).** `pay` is drawn unconditionally by the engine's routes drawer, but `Executor#verb_pay` validated the three mandate arguments ahead of the provider lookup, so an empty POST at a payment-free origin got `400 args.intent_mandate_jws required` — the least informative answer, telling an assistant to sign an AP2 chain that origin could never settle. The provider check now runs first, matching what the route comment and the discovery document have said all along.
336
+
337
+ - **The u64 index bound is stated inclusively so a double-precision validator cannot refuse a legal proof (K-845).** Written as `exclusiveMaximum: 2**64`, the bound rejected the largest legal index in any reader that parses JSON numbers as IEEE-754 doubles, which rounds `18446744073709551615` onto the excluded `2**64` — a third party validating against the published schema would have refused work the reference accepts. The accepted set is unchanged for an exact-integer reader.
338
+
339
+ - **A PoW proof whose `nonce.indices` carries an out-of-range index is refused by the shape check with a typed 400 (K-839).** The vendored `pow.schema.json` left `indices.items` unbounded though its description said u64; it now carries `minimum: 0, maximum: 2**64 - 1`, matching what `Kiosk::Pow::Equihash.verify` has enforced since K-540. Before, such a proof cleared validation, consumed its challenge id and came back `403 invalid proof of work` with `on_bad_proof` charged; now it is a `400 bad_request` naming the proof shape, and no challenge is spent.
340
+
341
+ - **The canonical migration set is rebuilt from scratch: six `create` migrations, renumbered 001-006 (K-646).** `kiosk:install` used to emit ten ordinals of which one was retired, one created a `device_authorizations` table the next-but-two DROPPED and rebuilt, and three added columns to a table created two files earlier — so a fresh adopter ran a migration HISTORY to arrive at a schema the templates can state outright. Every amendment is folded into the `create` it amended: `kyc_verified_at`, `spending_cap_cents` and `human_label` are columns of 002's `agents` table, and 004 creates `device_authorizations` in the account-binding shape directly. `SchemaDefinitions.kyc_verified_at_sql`, `.agent_governance_columns_sql` and `.rebuild_device_authorizations_sql` are GONE rather than deprecated — a shim would be a second way to build the same schema, which is the divergence this rebuild exists to end. Renumbering was measured before it was done: eight live references to an old ordinal existed outside the artifacts the rewrite touches anyway, two of them already stale, so they were swept. Phil: «Поменяй шаблон генерации миграций и сделай миграции начисто в демо. прод базы можно дропнуть.»
342
+
343
+ - **KYC attributes become a TABLE, and the grant becomes a row's existence (K-656/T-061).** The named anonymized booleans an attestation grants no longer live in a `kiosk.agents.kyc_attributes` jsonb map; canonical migration 009 now creates `kiosk.kyc_attributes` — one row per `(agent_id, name)`, no value column at all — and `DefaultAgentIdp#kyc_attributes` / `#kyc_has_attributes?` read it unchanged at the public API. A map had to carry a value and a value has spellings, so every reader had to decide which spellings of `true` count; with presence as the grant a gate is an `EXISTS` that cannot answer NULL and cannot be fooled by a truthy-but-not-`true` value, and the one place a spelling is still judged is the WRITE — `POST /kiosk/agents/kyc` inserts names via `jsonb_each(...) WHERE value = 'true'::jsonb`, in Postgres, once for every operator. The write is one transaction whose stamp gates the grants (a revoked agent is granted nothing) and it REPLACES the set, so a later bare attestation takes earlier grants away.
344
+
345
+ - **A handler may relax its own `200`'s cache policy, and the wire refuses one that would leak (K-823).** The spec has always let an operator serve an identity-independent payload `private, max-age=N` — that is how an assistant's own cache saves a toll — but the dispatch seam discarded a handler's response headers, so the permission was unreachable by the only code an operator writes; it now carries the handler's `Cache-Control` through, and the render seam refuses (rather than quietly edits) any policy that would let a shared cache hand one identity's rows to another.
346
+
347
+ - **The `/auth.md` every operator serves stopped teaching an error contract the wire retired (T-068 slice 7).** Its Errors section described the 0.3 `{ ok: false, error: { code, message, hint } }` envelope; since 0.4 every refusal — the auth plane included — is an RFC 9457 problem document with the code as a flat member. An assistant that believed the document was branching on a field that no longer exists, so the section now shows the document it will actually receive, and a spec pins the shape rather than the prose.
348
+
349
+ - **`GET <endpoint>/openapi.json` is PUBLIC too, and `POLICY_VERBS` is gone with the toll it existed for (K-804).** The derived OpenAPI document renders the same registry `GET <endpoint>/schema` renders, so gating one while the other is open withheld nothing; it now takes the identical treatment — no Bearer, no toll, `public` with a strong `ETag`, a `304`, and a year at `?v=<version>` — from one shared render seam, so the two cannot drift apart again. `Executor::POLICY_VERBS` had become a byte-identical copy of `VERBS` under a second name and was deleted rather than kept as documentation.
350
+
351
+ - **The public documents expire in a minute and carry no `Vary` at all.** `Headers::SHORT_MAX_AGE` drops from 300 to 60 — the number is how long a deploy takes to become visible to a client holding the previous pointer, not a cache-efficiency knob, since the traffic is absorbed by the immutable `?v=` url — and `DiscoveryController` drops the `Vary: Accept` Rails stamps on any negotiated render, which would otherwise split a shared cache by `Accept` string for a variance none of these six documents has. `/.well-known/api-catalog` also links both service descriptions at their versioned url rather than at the bare path.
352
+
353
+ - **`GET <endpoint>/schema` is PUBLIC, cacheable, and no longer tolled (T-094).** The verb catalogue holds no per-agent value and no secret, so gating it while `/.well-known/*` stood open bought nothing and cost an explanation; it is now served from a document derived once at boot by the engine, under `public` caching with a strong `ETag`, and the discovery documents link it at `?v=<digest>` so a deploy invalidates every cache by changing the URL rather than by hoping a TTL expires. `/kiosk/openapi.json` is deliberately left gated.
354
+
355
+ - **`/.well-known/api-catalog` hyperlinks every verb the origin serves, unauthenticated (T-093).** The RFC 9727 linkset used to name one endpoint per module because the verb roster was treated as something to withhold; it is not a secret, and a document composed from in-process state caches behind a CDN, so the catalog now lists the real per-verb endpoints with the method each one answers, alongside the two service descriptions it already carried.
356
+
357
+ - **The `schema` descriptor drops `verbs` and the module set has ONE home (T-095).** `verbs` rendered the very call `/.well-known/kiosk.json` renders as `capabilities`, so it was one value published twice under two names rather than two facts; `capabilities` is the single spelling, and a client reads it from the discovery document it already fetches first.
358
+
359
+ - **Discovery advertises MODULES, and the catalog's `verbs` IS `capabilities`
360
+ (T-068 slice 5; T-075 = A, K-740, ADR-0025).** `Kiosk.configuration
361
+ .capabilities` — and therefore `/.well-known/kiosk.json` — now computes
362
+ `["schema", "queries", "actions", "pay"]` from the same three questions of
363
+ the same registry it always asked; only the names it emits moved from verbs
364
+ to modules. `Executor#verb_schema` stops emitting the `VERBS` constant and
365
+ answers with that same array, so an origin with no payment provider no
366
+ longer advertises `pay` in one self-description and omits it from the other.
367
+ `agents.json`'s `x-kiosk` block stops echoing the capability list: it is now
368
+ `{schema, api_catalog, mount_path, api_version}` — pointers, not a copy of
369
+ the contract — and `min_client` went with the echo (`kiosk.json` is
370
+ canonical for it). `/.well-known/api-catalog` maps one link per module.
371
+ Intent: none of these documents requires a token, and the verb list is
372
+ deliberately behind one — `GET <endpoint>/schema` and
373
+ `GET <endpoint>/openapi.json` both demand Bearer, and the per-verb wire
374
+ answers `401` before `404` so an anonymous prober cannot enumerate names.
375
+ BREAKING for anything that read the old spellings: the member values change,
376
+ the field names do not.
377
+
378
+ - **The error taxonomy is the wire contract, not a parallel class hierarchy.**
379
+ The `error.code` vocabulary now lives in one table, and handlers express
380
+ errors in Rails' own idiom: a rendered status becomes its wire code, an
381
+ exception registered in `config.action_dispatch.rescue_responses` is mapped
382
+ by a single `rescue_from` the mixin installs (the operator's own handlers
383
+ win over it), and an explicitly rendered vocabulary code — including a
384
+ specific 402 — travels verbatim. Intent: an operator should never need a
385
+ Kiosk exception class to say what a bare `render json:, status:` already
386
+ says; the classes that restated HTTP statuses are deprecated for handler
387
+ code, and the unused `QuotaExceeded` class is gone (the code stays
388
+ reserved in the vocabulary).
389
+
390
+ - **kiosk-server declares Rails.** New runtime dependencies: `railties`,
391
+ `actionpack`, `activerecord` and `activesupport`, all `~> 8.1` — the four
392
+ components the gem references. Deliberately not the `rails` meta-gem: nothing
393
+ here touches Action Mailer, Action Cable, Active Job, Active Storage, Action
394
+ Text or Action Mailbox. The engine and the nine controllers no longer hide
395
+ behind `if defined?(::ActionController::API)` / `if defined?(::Rails::Engine)`
396
+ — `require "kiosk/server"` defines all ten unconditionally, so a missing
397
+ framework is a LoadError at require time rather than a NameError at request
398
+ time. Consequences: `Kiosk.configuration.device_authorization_store` always
399
+ lazy-defaults to the durable ActiveRecord adapter (it already did in every
400
+ Rails host), and `TestExecutor` no longer offers an "ActiveRecord absent"
401
+ error path. Rails < 8.1 is untested and therefore not claimed (K-495, T-052).
402
+ - **PoW proof moves to the `Kiosk-PoW` request header.** `WireController` and
403
+ `AuthController#register` now read the proof(s) from the `Kiosk-PoW` request
404
+ header as raw JSON (`HTTP_KIOSK_POW`) instead of a `pow` body field (ADR-0022,
405
+ K-483). The parser dual-accepts a single proof, a JSON array, repeated header
406
+ lines (Rack `\n`-joined), or a proxy comma-combined value, flattening all into
407
+ one proofs list — so the tolled `schema` GET can carry its proof (a GET has no
408
+ body). The body is now purely verb args (the challenge fingerprint is over the
409
+ plain body, unchanged on retry); the old body-pow path is removed (no
410
+ back-compat). A malformed header is a `bad_request` (400) with a hint;
411
+ `RequestValidation` validates each parsed proof; the 402 keeps its
412
+ `WWW-Authenticate: Kiosk-PoW` header (completes T-022).
413
+ - **Install-generator honesty.** The `kiosk:install` initializer template no longer advertises a nonexistent `MyCustomAgentIdp`; it now states plainly that the bundled kiosk-pop agent-IdP is the default (zero config), that fronting an external agent-identity issuer means subclassing `Kiosk::AgentIdentityProviders::Base` (a planned seam, none shipped), and that `c.user_idp` binds a human account with `kiosk-user-idp-devise` as the worked example. Template text only — no generated behavior change.
414
+ - **RLS is now opt-in.** kiosk-server no longer depends on or requires
415
+ `kiosk-rls`. Hosts that want DB-level row enforcement add
416
+ `gem "kiosk-rls"` themselves (see the kiosk-rls README). Config moves:
417
+ `schema` and `app_role` now live in kiosk-core's `Kiosk::Configuration`;
418
+ `enforce_db_role` in kiosk-server's extension. No wire-surface change.
419
+
420
+ ### Added
421
+
422
+ - `SessionContext.require_open!`, `.current` and `.open?` — the guard an identity-scoped scope calls, so a call with no session raises `401 unauthenticated` rather than answering an empty relation.
423
+ - `Kiosk::Server::ConformanceOrigin` — the origin the `kiosk-test-support` conformance checks run against in a real Rails app. It reads verbs from the `Queries`/`Actions` registries, routes from `Rails.application.routes`, dispatches a verb inside a GUC-scoped `SessionContext`, and reports schema failures through the engine's own validators — `RequestValidation` for arguments (so the reserved `limit`/`cursor` names stay exempt exactly as on the wire) and `ResponseValidation` for answers — so a conformance test and a running server cannot disagree. Not auto-loaded — require it from your test helper, like `test_executor.rb` beside it.
424
+ - `Kiosk::Server::TestExecutor#run_query(name, args)` — invokes a Query verb by name inside the active identity scope, answering a paginated query with its rows. `query` takes SQL and `run_action` resolves against the Action registry, so a declared `kind :query` verb was previously reachable through neither.
425
+
426
+ - **`Kiosk::Server::VerbRefusalController` and the tail route that reaches it (T-183).** A mounted engine appends a single-segment refusal pair BELOW the host's own routes, so a path under the mount that names no verb you drew still answers the spec's `404 verb_not_found` (with the registry's hint) and a verb called with the other method still answers `405` + `Allow`. It refuses; it never serves — a name registered as the kind asked for raises, naming the route line you did not write.
427
+
428
+ - **The audit sink — Kiosk offers the trail and stores none of it (K-828).** `c.audit_sink` is a callable an operator sets; it receives one `Kiosk::Server::ActionEvent` per action invocation, success and failure alike, carrying the action name, the `{user_id, agent_id}` pair, the role, the actor, the outcome, the error class and message, the timestamp — and the ARGUMENTS IN FULL, deliberately unredacted, because the retention policy for an operator's customers' data is not Kiosk's to assume. Default is no sink: nothing is emitted and nothing is written anywhere. Redaction is one call (`ActionEvent#with_arg_types`, `#without_args`), a sink that raises is logged and never fails the action, and emission sits outside the action's transaction so a sink cannot hold one open. Replaces the `kiosk.action_log` writer this release briefly shipped.
429
+
430
+ - **`Kiosk::OperationResult` — the answer-or-refusal a write returns, so an operator writes the decision and not the plumbing (K-792).** An Operation must be able to REFUSE without knowing whether the answer will be rendered as JSON, redirected with a flash, or read from a console, and the value object that makes that possible had been hand-copied into every demo. It ships once now: subclass it, declare the `error.code` → HTTP status map for the refusals your app actually makes, and nothing else. The map stays yours on purpose — the wire vocabulary is not injective (`kyc_required` and `forbidden` are both 403), so no shared table could infer it, and an unmapped code raises rather than being guessed at.
431
+
432
+ - **The install generator now emits the `c.handlers` slot.** `rails generate kiosk:install` previously produced an initializer that never mentioned `c.handlers`, so a fresh operator following the generator — rather than the onboarding page — landed in exactly the dead-origin state that hole causes (empty schema catalog, 404 on `query`/`run`, `"capabilities": []`). The template now emits an active `c.handlers = []` (empty because no handler controllers exist yet at generation time — naming one would fail boot) with a comment stating the consequence of leaving it empty and the fill-in syntax for when handlers exist.
433
+
434
+ - **The engine owns handler registration.** An operator names their handler controllers (`c.handlers = %w[Kiosk::CatalogController]`) and the engine registers them — at boot in every environment, and again after every code reload — so a mixin-declared verb is on the wire in development, where nothing eager-loads `app/` and nothing else ever references a handler controller. Without it such an origin served no verbs at all: an empty schema catalog, a 404 on every `query`/`run`, and discovery advertising `"capabilities": []`. Each pass rebuilds rather than re-registers, so a verb deleted from a controller also leaves the wire; the rebuild can be total because `c.handlers` is the only path into the registry — the `Queries.register` initializer API it once had to spare is removed in this same release (T-081, below).
435
+
436
+ - **The engine owns the routes.** `mount Kiosk::Server::Engine => Kiosk.configuration.mount_path` is now the whole routing story: the engine's drawer carries the wire verbs, the kiosk-pop auth plane, JWKS, KYC attestation and the account-binding ceremony (including the previously missing `auth/assistants/update` the page's own form posts to), and a mount-gated `routes.append` initializer installs the root-relative discovery documents (`/agents.txt`, `/agents.json`, `/auth.md`, `/.well-known/*`) into the host app. Merely bundling the gem stays inert — no mount, no routes — and hand-drawn routes keep winning over the engine's (Rails dispatches the first match), so hand-mounted hosts are unchanged and hand-drawing remains the documented escape hatch.
437
+
438
+ - **Opt-in request-shape validation.** A new `c.validate_requests` flag (default off; byte-identical old behaviour when off) makes `WireController` validate a PRESENT `pow` field against the vendored normative PoW JSON Schema at the wire choke point — BEFORE the gate consumes it — and reject a malformed shape with a `bad_request` (400) carrying a hint naming the expected `pow.proofs[]` form. This closes the K-479 silent-re-challenge loop (a `pow: {solutions:[…]}` whose proofs the gate couldn't parse yielded a fresh 402 on every retry with no diagnostic). It is a shape check IN FRONT of the gate, not a replacement: an absent pow still gets the normal 402, a well-formed pow still faces the real cryptographic verification. `json_schemer` is a RUNTIME dependency of this gem since 0.4, not an optional extra tied to this flag: §8.1 item 5 makes coerce-then-validate an operator obligation, so `VerbController` validates every per-verb call against its declared `input_schema` behind no flag at all and an origin that cannot load a validator cannot serve a conformant wire (K-1333 — this bullet said «an OPTIONAL dependency, lazily required only when the flag is on», which was true when the flag shipped and stopped being true at 0.4). It is still required LAZILY, so a vendored checkout without it gets a clear ConfigurationError naming the gem rather than a LoadError at boot. The schema is a vendored self-contained copy of `kiosk.tech/spec/schemas/pow.schema.json` (sync-check tracked as T-045). Slice-1 of the broader uniform-validation layer (T-045).
439
+ - **KYC named anonymized attributes.** A KYC attestation MAY now carry an `attributes` object of `{name: true}` booleans (e.g. `age_over_18`, `licence_a`) beside the binary `level`; the verifier records only the granted booleans — never the underlying documents — in the new nullable `agents.kyc_attributes` jsonb column (migration 009, `SchemaDefinitions.kyc_attributes_sql`, install-generator template). `DefaultAgentIdp` gains `kyc_attributes` / `kyc_has_attributes?` so an Action can gate on required attributes, rejecting with the new `kyc_required` (403) error. Additive and backward-compatible: a bare `level: "verified"` attestation still verifies with an empty attribute set.
440
+ - **Account binding.** The dormant device-grant machinery is revived as the key-bound claim/link ceremonies: `POST /oauth/device_authorization` requires the agent's `public_key`, the engine-drawn verify page authenticates via `user_idp`, and the token poll demands a possession proof (`signed`, BIND-POP) before any binding — fresh keys register as linked assistant accounts, known keys rebind with reputation carried; link codes mint/redeem via `POST /auth/link` / `POST /auth/claim`; `POST /auth/unlink` deactivates a binding and fires `assistant_unlinked`. Codes are stored hashed in the new durable `DeviceAuthorizationStores::ActiveRecord` default (migration 008); discovery gains `/auth.md` and additive binding keys across the other five surfaces. Tokens remain kiosk-pop-only.
441
+ - **Per-assistant spending cap.** A provider MAY cap what each bound assistant may settle, via the `config.spending_cap` pay-hook seam (`(agent_id:) -> cents | nil`, with an optional `spending_cap_window_days` rolling window). The pay path enforces it before the irreversible capture — a cap of 0 disables the assistant — and rejects with the new `spending_cap_exceeded` (403) error. Batteries-included `ColumnSpendingCap` reads the cap from the new nullable `agents.spending_cap_cents` column (plus `human_label`), added idempotently by `SchemaDefinitions.agent_governance_columns_sql`. Opt-in: default off, no behaviour change until wired. Powers per-assistant governance on the manage-assistants page.
442
+ - **Manage-assistants governance editing.** The manage-assistants page (`AssistantsController`) gains a `POST …/update` action letting an account holder set each bound assistant's `human_label` and `spending_cap_cents` (blank clears the cap to unlimited; non-integer → 400), ownership-scoped so a holder edits only their own live rows. The listing now also surfaces per-assistant label, settled spend (summed from `settlements`, honouring `spending_cap_window_days`; falls back to zero where no settlements table exists), and the current cap.
443
+ - Initial skeleton.
444
+ - `Kiosk::Server::ConfigurationExtension` — adds `mount_path`, `capabilities`, `owner`, `min_client` to `Kiosk::Configuration` with lazy defaults.
445
+ - `Kiosk::Server::WellKnown` — pure-Ruby builder for `/.well-known/kiosk.json` per spec §3.4. Returns Hash or JSON string. Validates `issuer` is set.
446
+ - `/.well-known/api-catalog` discovery document (RFC 9727 standards alignment): a `application/linkset+json` catalog of the live wire endpoints (schema tagged `service-desc`) plus the agents.json companion, so standards-aware agents can discover the Kiosk API surface via the conventional api-catalog URL. Advertised from agents.json under the `x-kiosk` extension.
447
+ - `Kiosk::Server::Headers` — composes the three Kiosk response headers (`Kiosk-Server-Version`, `Kiosk-API-Version`, `Kiosk-Min-Client`).
448
+ - `Kiosk::Server::HeadersMiddleware` — Rack middleware that injects the headers on responses whose path starts with the configured mount path.
449
+ - `Kiosk::Server::SchemaDefinitions` — pure SQL generators for canonical migrations 001-004 (kiosk schema + current_*() helpers, agents/agent_tokens/agent_mappings, actions/action_log, reservations); typed against the configured user-id type (`:uuid`, `:bigint`, `:integer`, `:text`).
450
+ - `Kiosk::Server::Engine` — Rails engine declaration (conditionally loaded when `Rails::Engine` is defined). Auto-mounts `HeadersMiddleware` in the host app's stack.
451
+
452
+ - **Executor and Errors layer** (follow-up addition within Unreleased):
453
+ - `Kiosk::Server::Errors` hierarchy — `Base`, `BadRequest`, `Unauthenticated`, `Forbidden`, `RLSDenied`, `NotFound`, `QuotaExceeded`, `ActionFailed` — each carrying `CODE` and `HTTP_STATUS` per spec §5.2; `Base#to_envelope` serialises `ok:false` body with `code/message/hint` (nil fields dropped).
454
+ - `Kiosk::Server::Result` Data class — `:rows`/`:value` envelope shapes; `to_envelope` for JSON serialisation.
455
+ - `Kiosk::Server::SessionContext` — opens a transaction on any connection responding to `#transaction`/`#execute`, emits `SET LOCAL` for the four canonical GUCs per spec §6.3; skips `agent_id` GUC for non-agent actors; respects configured `guc_namespace`.
456
+ - `Kiosk::Server::Actions` — minimal process-wide registry (`register(name, &block)`, `fetch(name)`, `known`, `reset!`); raises `Errors::NotFound` on unknown.
457
+ - `Kiosk::Server::Executor` — four-verb dispatch (`query`, `run`, `pay`, `schema`), all fully working against any conforming connection.
458
+ - `Kiosk::Server::WireController` — Rails controller (conditionally defined when `ActionController::API` is loaded); resolves identity via configured `agent_idp` then `user_idp`, parses JSON body, calls `Executor`, serialises envelope with HTTP status + Kiosk headers.
459
+
460
+ - **Install generator** (follow-up addition within Unreleased):
461
+ - `Kiosk::Generators::InstallGenerator` — `bin/rails g kiosk:install` produces `config/initializers/kiosk.rb` and the four canonical migrations (`create_kiosk_schema`, `create_kiosk_identity_tables`, `create_kiosk_actions_log`, `create_kiosk_reservations`); each migration is a thin wrapper that calls into `Kiosk::Server::SchemaDefinitions` so SQL regenerates against the current `Kiosk.configuration` at `db:migrate` time. Class options: `--user-table`, `--user-id-type`, `--schema`, `--guc-namespace`. Adds `railties ~> 8.1` as a development dependency.
462
+
463
+ - **Fixes uncovered by the first end-to-end run** (within Unreleased):
464
+ - `SessionContext` now quotes each dot-segment of the GUC name in `SET LOCAL`. Required because `current_role` is a PostgreSQL reserved keyword; unquoted `SET LOCAL app.current_role = '…'` is a syntax error. Quoting is safe across all GUC names.
465
+ - `WireController#parse_body!` now reads `request.raw_post` instead of `request.body.read`. The latter returns empty when a prior middleware (Rails' `ParamsWrapper` with `--api`-style controllers) has already consumed the body stream; `raw_post` is Rails-safe.
466
+
467
+ - **JWKS foundation** (within Unreleased; first piece of the §6.7 OAuth surface):
468
+ - `Kiosk::Server::SigningKey` — RSA keypair value object. `.generate` for fresh keys, `.from_pem` for loaded keys, `#kid` as the RFC 7638 thumbprint, `#to_jwk` for JWKS-publish shape. Enforces 2048-bit minimum; rejects non-RSA inputs; never leaks private parameters via `#to_jwk`.
469
+ - `Kiosk::Server::Jwks.build(keys: [...])` — pure-Ruby JWKS document builder per RFC 7517. Multi-key shape supports key rotation overlap windows.
470
+ - `Kiosk::Server::ConfigurationExtension#signing_key` — lazy default resolves in this order: explicit setter value, `KIOSK_SIGNING_KEY_PEM` env var, `KIOSK_SIGNING_KEY_B64` env var; raises when none is set (no silent fresh-key generation — a deployment must provide its key). Setter accepts a `SigningKey` instance or a PEM string. `Kiosk.reset!` drops the configured key.
471
+
472
+ - **JWT issue + verify** (within Unreleased; second piece of the §6.7 OAuth surface):
473
+ - `Kiosk::Server::JwtIssuer.issue(claims:, audience:, ...)` — RS256-signed token with `iat`/`nbf`/`exp`/`iss`/`aud`/`jti` set automatically. JWS header carries `kid` so verifiers can pick the right key during rotation overlap. Default lifetime one hour; caller can override.
474
+ - `Kiosk::Server::JwtIssuer.verify(token:, jwks:, audience:, ...)` — validates signature + lifetime + audience (+ optional issuer) against a JWKS. Returns symbol-keyed claims on success. Raises typed errors: `ExpiredError`, `AudienceError`, `SignatureError`, `InvalidError`.
475
+ - `jwks:` argument accepts a Hash (`{ keys: [...] }`), an Array of `SigningKey`, or a single `SigningKey` — verifier-side ergonomics for the common cases (self-issued, multi-key rotation, single foreign key).
476
+ - Adds `jwt ~> 2.8` dependency (MIT, no transitive deps).
477
+
478
+ - **JWKS endpoint controller** (within Unreleased; third piece of the §6.7 OAuth surface):
479
+ - `Kiosk::Server::JwksController` — `GET <mount>/.well-known/jwks.json`. Serves `Kiosk::Server::Jwks.build(keys: [Kiosk.configuration.signing_key])`. Conditionally defined (only when `ActionController::API` is loaded). Adds Kiosk response headers via `Headers.add_to`.
480
+
481
+ - **M3 — Install generator extended for migration 005** (within Unreleased):
482
+ - `bin/rails g kiosk:install` now produces five migrations (was four). Added template `create_kiosk_device_authorizations.rb.tt` calling `SchemaDefinitions.device_authorizations_sql` so the host app gets the §6.7 OAuth state-machine table out of the box.
483
+ - Generator spec extended to assert 5-migration count + new `005 create_kiosk_device_authorizations` context (SQL invocation + #down DROP).
484
+
485
+ - **M3 — `Kiosk::Server::TestExecutor`** (within Unreleased; closes the journey-test infrastructure stream):
486
+ - Pure-Ruby executor satisfying the `Kiosk::TestHelpers::Journey` contract (defined in `kiosk-test-support`). Bridges the rspec/minitest matchers to a real ActiveRecord+Postgres connection so providers can write journey tests against actual RLS policies.
487
+ - `with_identity(identity)` opens an AR transaction, sets the four canonical GUCs via `SessionContext`, yields, **ROLLS BACK unconditionally** — tests stay hermetic across runs. Block return value preserved; exceptions re-raised after rollback. Custom `RollbackMarker` works with both AR (which catches it as a transaction-abort signal) and connection doubles.
488
+ - `query(sql)` enforces default-deny (raises `NoScopeError` outside any `with_identity`), returns rows with symbolised keys, translates Postgres RLS errors (`row-level security`, `violates row-level`, `permission denied for table`) to `Kiosk::TestHelpers::Errors::RLSDenied`.
489
+ - `run_action(name, args)` looks up via `Kiosk::Server::Actions`, calls the registered block with current identity.
490
+ - `pay_action(name, args)` raises `NotImplementedError` (lands with `kiosk-pay-*` in M4).
491
+ - `seed(table, attrs, count:)` bulk-inserts through `system_connection` (RLS-bypass connection the host configures separately); quotes column/table identifiers and values defensively.
492
+ - **Not autoloaded by `require "kiosk/server"`** — test-time infrastructure. Users explicitly `require "kiosk/server/test_executor"` in their `spec_helper.rb` / `test_helper.rb`, then wire `Kiosk::TestHelpers.executor = Kiosk::Server::TestExecutor.new`.
493
+ - Adds `kiosk-test-support` as a development dependency (for error class definitions). Host apps using TestExecutor will have it loaded transitively via `kiosk-rls-rspec` / `kiosk-rls-minitest`.
494
+ - 27 new test examples cover scope enforcement, identity propagation, hermetic rollback semantics, RLS-error translation, action invocation, seed quoting + value rendering. Full kiosk-server suite: 280 examples, 0 failures (was 251 — added 29).
495
+
496
+ - **Device Authorization Grant — Sub-slices 4+5 of 5: CLI login + e2e wire** (within Unreleased; later unwound):
497
+ - The `kiosk-cli` RFC 8628 login client and the e2e device-login assertions built in this sub-slice were subsequently REMOVED: Kiosk auth is the proof-of-possession challenge-response handshake, not OAuth. `kiosk-cli` is not part of the release, and e2e intentionally does not exercise the device-grant endpoints.
498
+ - The device-grant surface itself (`/oauth/device_authorization`, `/oauth/token`, state machine, stores) stays in kiosk-server as dormant code — retained for a possible future human-in-the-loop consent flow, covered by its unit specs.
499
+
500
+ - **Device Authorization Grant — Sub-slice 3 of 5: DeviceVerification helper** (within Unreleased; sixth piece of the §6.7 OAuth surface):
501
+ - `Kiosk::Server::DeviceVerification` — pure-Ruby state-machine helpers for the user-facing half of the Device Grant flow. `.find_pending(user_code:)`, `.approve(user_code:, user_id:)`, `.deny(user_code:)`. Strips visual XXXX-XXXX dash + whitespace, upcases (auto-cap keyboards). Raises `CodeNotFoundError` when user_code doesn't resolve to a pending row (distinct from `DeviceAuthorization::StateError` — that one signals a logic error in calling code).
502
+ - **Scope decision:** Kiosk owns the state-machine half (identical across providers). The consent-screen HTML/branding/host-app-login integration is provider responsibility — host's Rails controller calls these helpers from its own actions and renders its own UI. This is consistent with the satellite-mode-neutral principle (Kiosk ships primitives; provider owns presentation).
503
+ - (The e2e `_test/device_authorization` fixture endpoints that exercised this helper were later removed along with the rest of the e2e device-grant wiring — the surface is dormant; coverage lives in the unit specs.)
504
+ - Test coverage: 17 new examples (normalize_user_code 4 / find_pending 6 / approve 5 / deny 2). Full kiosk-server suite: 251 examples, 0 failures (+17).
505
+
506
+ - **Device Authorization Grant — Sub-slice 2 of 5: OAuth endpoints + e2e wire** (within Unreleased; fifth piece of the §6.7 OAuth surface):
507
+ - `Kiosk::Server::DeviceCodeGrant` — pure-Ruby service module. `.start` and `.exchange` implement the §6.5 + RFC 8628 §3.5 state machine; controllers are thin shims (same pattern as `Executor` ↔ `WireController`). `.exchange` produces a uniform `{ok:, ...}` Hash mapping RFC 8628 error codes (`authorization_pending`, `access_denied`, `expired_token`, `invalid_grant`, `invalid_request`) to outcomes. Lazy expiry: a row past `expires_at` is bumped to `:expired` on first poll, then errors uniformly.
508
+ - `Kiosk::Server::OauthDeviceAuthorizationController` — POST `<mount>/oauth/device_authorization`. Returns the full RFC 8628 §3.2 response (`device_code`, `user_code`, `verification_uri`, `verification_uri_complete`, `expires_in`, `interval`). Composes `verification_uri` from `request.base_url + mount_path` so it works regardless of where the engine is mounted.
509
+ - `Kiosk::Server::OauthTokenController` — POST `<mount>/oauth/token`. Multi-grant entry point; currently dispatches `urn:ietf:params:oauth:grant-type:device_code` to `DeviceCodeGrant.exchange`. Other grants (`authorization_code` with PKCE, `refresh_token`) return `unsupported_grant_type` with a descriptive note pointing at the follow-up sub-slice.
510
+ - (The e2e device-grant assertions added here were later removed — e2e intentionally does not exercise the dormant surface. The JWT-aware composite IdP (`JwtOrStubIdp`) remains in the e2e fixtures.)
511
+ - Test coverage: 12 new `DeviceCodeGrant` examples covering state-machine outcomes, JWT verification round-trip, expires_in/scope omission. Full kiosk-server suite remains at 234 examples, 0 failures (+12).
512
+
513
+ - **Device Authorization Grant foundation (RFC 8628) — Sub-slice 1 of 5** (within Unreleased; fourth piece of the §6.7 OAuth surface; covers spec §6.5 `kiosk login` flow):
514
+ - `SchemaDefinitions.device_authorizations_sql` — canonical migration 005. Table `kiosk.device_authorizations` with `device_code_hash bytea`, `user_code text`, `client_id`, `requested_role`, `status` (5-value CHECK constraint), `user_id`, `expires_at`, `consumed_at`, `created_at`. Unique index on `device_code_hash`; partial unique index on `user_code WHERE status='pending'`; expiry-scan index on `expires_at`.
515
+ - `Kiosk::Server::DeviceAuthorization` — `Data`-based value object with the full state machine (`pending → approved | denied → consumed | expired`). Non-destructive transitions via `Data#with`. `.generate(client_id:, requested_role:, expires_in:)` returns `[plain_device_code, da]`; plain code is the only opportunity to read it (persistent form holds only the SHA-256 hash). `user_code` uses 8-char Crockford alphabet (no 0/O/1/I/L/U) for human-friendly typing; `#display_user_code` returns the `XXXX-XXXX` form. `StateError` raised on illegal transitions.
516
+ - `Kiosk::Server::DeviceAuthorizationStores::Base` + `::InMemory` — pluggable storage adapter. InMemory variant is thread-safe via Mutex; suitable for dev / integration tests / small single-process deployments. ActiveRecord adapter follows in a later release.
517
+ - `Kiosk.configuration.device_authorization_store` — lazy-default `InMemory`; setter accepts any subclass of `DeviceAuthorizationStores::Base`. `Kiosk.reset!` drops any configured store.
518
+ - Test coverage: 47 new examples (33 on `DeviceAuthorization` + 14 on stores + config integration). Full kiosk-server suite remains green at 222 examples, 0 failures.
519
+
520
+ ### Removed
521
+
522
+ - **The connect ticket for the event stream is gone (ADR-0034).** `POST <endpoint>/events/ticket` and `?ticket=` are removed; the upgrade takes the `Authorization` header.
523
+
524
+ - **`Kiosk::Server::Result#ok?` is gone.** A constant-true predicate on a type this gem calls internal, with no caller anywhere; `#http_status` is the whole of what a Result answers with.
525
+
526
+ - **`kiosk.actions` and `kiosk.action_log` leave the canonical migration set (K-828).** Canonical migration 003 is retired and the install generator emits nine migrations instead of ten (004-010 keep their ordinals). With the audit trail now the operator's, a shipped migration that creates two audit tables nothing ever fills is exactly the dead schema K-791 objected to. `SchemaDefinitions.actions_log_sql`, the `create_kiosk_actions_log` template and `Kiosk::Server::ActionLog` (with `c.audit_log` / `c.audit_log_args`) are gone rather than deprecated: there are no adopters to carry a shim for, and an existing installation drops the two tables by hand.
527
+
528
+ - **`Kiosk::Server::Queries.register` / `Kiosk::Server::Actions.register` are gone (T-081).** A shipped public API of the 0.3 series is removed with no deprecation shim: an initializer that calls one now raises NoMethodError. A verb is declared one way — a controller that `include`s `Kiosk::Query` / `Kiosk::Action`, named in `c.handlers` — because the second shape could not be reloaded, could not be reached by the host's filters, `rescue_from` or strong parameters, and taught, in the file an adopter copies first, that Rails does not apply to the surface they expose to assistants. The registries' read surface (`fetch`, `describe`, `catalog`, `known`) is unchanged and the wire is byte-identical, including the ADR-0023-retired `params` descriptor slot, which no macro can set and which keeps publishing null.
529
+
530
+ ### Fixed
531
+
532
+ - **A routing 404 or an unhandled 500 under the mount now carries the version headers the spec makes mandatory (K-824).** The headers middleware was appended to the host stack, so it sat inside Rails' exception renderers and never saw a response Rails composed for itself — leaving exactly the answers a mis-versioned client is most likely to receive with no handshake on them. It is installed outside those renderers now, and reads the request path on the way in, because Rails rewrites it before rendering an error. An operator's own routes outside the mount are still untouched.
533
+
534
+ - **A provider that configures no `registration_role` can register assistants again (K-788).** Roles are optional by decision (ADR-0011: "registration MUST NOT fail when it is unset"), but the register door and the fresh-key binding branch wrote a literal `NULL` into `agents.allowed_roles`, which the shipped migration declares `NOT NULL` — so both 500'd for exactly the single-role operator the decision protects, while every shipped demo configures a role and never saw it. "No role" is now the empty role set.
535
+
536
+ ### Security
537
+
538
+ - **The identity GUCs are set through bind parameters (K-789).** `SessionContext` built `SET LOCAL <name> = '<value>'` with a hand-rolled quote-doubler — the last value escaped by hand anywhere in the gem, and the one value every predicate and every RLS policy trusts. Postgres takes no binds in `SET`, so the statement is now `SELECT set_config($1, $2, true)`, whose third argument IS `LOCAL`; equivalence (same value in the transaction, gone after COMMIT and after ROLLBACK) is asserted against a real database rather than argued. `SessionContext`'s connection contract moves from `#execute` to `#exec_query`, and `#guc_statements` returns `[sql, binds]` pairs.
539
+
540
+ - **The pay path writes through bind parameters, not assembled SQL (K-654).** `Executor`'s four `persist_*` helpers and its spending-cap tally were heredocs with every value spliced in through a private `connection.quote` wrapper; they now pass `$1…$N` binds to `exec_query`, so a value can no longer be read as SQL and there is no quote call left to omit. Nothing on the wire changes — this closes the gap between what the engine ships and what it asks operators to do, since the same idiom was removed from all seven demos first. `WireController` also stops calling the Rails-8.1 soft-deprecated `ActiveRecord::Base.connection`, which raises outright under `permanent_connection_checkout = :disallowed`.
541
+
542
+ ### Out of scope for first release
543
+
544
+ - OAuth 2.1 surface (authorize/introspect/PKCE/DCR) — dropped as the auth mechanism: Kiosk auth is the proof-of-possession challenge-response handshake; the device-grant code ships dormant.
545
+ - Full `Kiosk::Action` DSL (`description`, `accepts`, `requires_payment`, `escalate_to :system`) — current `Actions` registry is a minimal callable map.
546
+ - `help` and `events` verbs — removed from the wire surface entirely (`help` dropped by decision, `events` removed too), not deferred.
547
+ - `rake kiosk:doctor` — follow-up.
548
+ - Full Rails-engine integration tests (requires booting a host app) — follow-up.
549
+ - Satellite-mode connection-pool plumbing per spec §7.7 (currently `WireController#connection_for` uses `ActiveRecord::Base.connection`).