openui-rails 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -2,470 +2,80 @@
2
2
 
3
3
  [![CI](https://github.com/yorzi/openui-rails/actions/workflows/ci.yml/badge.svg)](https://github.com/yorzi/openui-rails/actions/workflows/ci.yml)
4
4
 
5
- [Documentation](https://openui-rails.wangyaodi.com/)
5
+ Render streaming, interactive OpenUI Lang interfaces inside an existing Rails
6
+ application. Use Rails views and Stimulus to mount the official browser runtime;
7
+ keep authentication, authorization, and persistence in Rails.
6
8
 
7
- Render model-generated, streaming, interactive **OpenUI Lang** UI inside an
8
- existing Rails application — without migrating the whole frontend to a
9
- React/Next.js SPA and without hand-writing the stream, state and action glue
10
- for every screen.
9
+ **Unofficial integration:** this project is not affiliated with or maintained by
10
+ Thesys. It uses the OpenUI JavaScript runtime and does not implement a Ruby parser
11
+ or renderer.
11
12
 
12
- > **Unofficial integration.** `openui-rails` is not affiliated with, endorsed
13
- > by, or maintained by Thesys. It reuses the official, version-pinned OpenUI
14
- > JavaScript runtime (loaded in the browser) and keeps Rails authoritative for
15
- > authentication, authorization and persistence. It is **not** a Ruby
16
- > reimplementation of OpenUI Lang — there is no Ruby parser or renderer in this
17
- > Gem.
13
+ [Documentation](https://openui-rails.wangyaodi.com/) ·
14
+ [RubyGems](https://rubygems.org/gems/openui-rails) ·
15
+ [Roadmap](ROADMAP.md)
18
16
 
19
- ## What it does
17
+ ## What you get
20
18
 
21
- - **Rails view helper + Stimulus bridge** that mounts the official OpenUI
22
- renderer against a server-owned surface record.
23
- - **Versioned library/prompt contract** so the model prompt and the browser
24
- component library are generated from the *same* library revision, with a
25
- SHA-256 fingerprint persisted alongside the response.
26
- - **Ordered streaming** envelopes with snapshot recovery when chunks arrive out
27
- of order or a client reconnects. Transport-agnostic: broadcast over Turbo
28
- Streams or Action Cable.
29
- - **A single, authorized action loop**: component `onAction` and browser
30
- `toolProvider` `Query()`/`Mutation()` calls both route through one allowlisted
31
- endpoint with CSRF, tenancy, staleness and idempotency controls. It fails
32
- closed.
33
- - **Optional RubyLLM adapter** that owns RubyLLM-specific event normalization.
19
+ - A view helper and Stimulus bridge for rendering server-owned surfaces.
20
+ - A shared library fingerprint for the prompt, browser runtime, and stored UI.
21
+ - Ordered stream envelopes with gap detection over Turbo Streams or Action Cable.
22
+ - One allowlisted action endpoint with authorization, validation, and replay protection.
23
+ - An optional RubyLLM streaming adapter.
34
24
 
35
- RubyLLM is an optional integration, never a core dependency. Rails remains the
36
- source of truth for users, tenants and business objects.
25
+ Your app supplies scoped record lookup, stream subscriptions, snapshot recovery,
26
+ and shared idempotency storage when running multiple processes.
37
27
 
38
- ## Requirements
28
+ ## Get started
39
29
 
40
- | Component | Supported |
41
- | --- | --- |
42
- | Ruby | `>= 3.2` |
43
- | Rails | `>= 8.0` (`< 9.0`) |
44
- | OpenUI browser runtime | pinned JS runtime, `@openuidev/browser-bundle` **0.2.1** |
45
- | OpenUI CLI (prompt generation) | pinned `@openuidev/cli` **0.6.0** |
46
- | RubyLLM | optional; **2.1.1** verified through its real OpenAI chat-completions SSE parser with mocked HTTP |
47
-
48
- Unknown provider/version combinations are *unsupported*, not implicitly
49
- compatible. The JavaScript runtime is always referenced by an exact version;
50
- there is no `@latest` or floating CDN range.
51
-
52
- > **Rails 8.0 note.** On the Rails 8.0.x line, use `json < 3`. Rails 8.0's
53
- > ActiveSupport and integration-test helpers pass a `quirks_mode:` keyword to
54
- > `JSON.generate` that `json` 3.1 removed; Rails 8.1 no longer does. CI covers
55
- > both lines (`RAILS_VERSION` in the `Gemfile` pins the matrix).
56
-
57
- ## Installation
30
+ Requires Ruby 3.2+ and Rails 8.0 or 8.1. On Rails 8.0, use `json < 3`.
31
+ The runtime-fetch task also needs npm. RubyLLM is optional.
58
32
 
59
33
  ```ruby
60
34
  # Gemfile
61
- gem "openui-rails", "~> 0.1.0"
35
+ gem "openui-rails", "~> 0.2.0"
62
36
  ```
63
37
 
64
- ```bash
38
+ ```sh
65
39
  bundle install
66
40
  bin/rails generate openui:install
41
+ bin/rails openui:runtime:fetch
42
+ bin/rails openui:library:prepare
43
+ bin/rails openui:doctor
67
44
  ```
68
45
 
69
- The generator writes `config/initializers/openui.rb`, creates `config/openui/`
70
- and mounts the engine:
71
-
72
- ```ruby
73
- # config/routes.rb
74
- mount OpenUI::Rails::Engine => "/openui-rails"
75
- ```
76
-
77
- It intentionally does **not** download the runtime or generate the library
78
- prompt — those are explicit, reproducible steps (below).
79
-
80
- ## Quickstart
81
-
82
- ### 1. Configure
83
-
84
- ```ruby
85
- # config/initializers/openui.rb
86
- OpenUI::Rails.configure do |config|
87
- config.transport = :turbo # :turbo, :action_cable or :none
88
- config.library_spec_path = Rails.root.join("config/openui/library.spec.json")
89
- config.prompt_path = Rails.root.join("config/openui/library.prompt.txt")
90
- config.runtime_assets = :self_hosted # :self_hosted or :external
91
-
92
- # Authz is required and fails closed: with no authorizer an action is denied.
93
- config.authorize_action = ->(invocation) { invocation.actor.present? }
94
-
95
- # Resolve a surface id to a record scoped to the actor/tenant. Returning nil
96
- # is treated as an unknown/inaccessible surface and denies the action.
97
- config.surface_resolver = ->(surface_id, actor:, **) {
98
- actor&.surfaces&.find_by(id: surface_id)
99
- }
100
-
101
- # The engine inherits your ApplicationController, including authentication
102
- # filters. It reads current_user by default; configure this for other auth.
103
- config.actor_resolver = ->(controller) { controller.current_user }
104
- end
105
- ```
106
-
107
- ### 2. Generate the library prompt and spec
108
-
109
- The default browser bundle contains **only `openuiChatLibrary`**. Its prompt
110
- and spec must describe that exact library, not an unrelated custom library.
111
- The loaded bundle exposes `window.__OpenUI.openuiChatLibrary.prompt()` and
112
- `.toSpec()`. Save those outputs as the configured prompt text and JSON spec.
113
- The compatibility suite verifies this exact workflow against the pinned bundle.
114
-
115
- For a host-built custom library, use the pinned CLI and a matching host-built
116
- runtime that exposes the same `window.__OpenUI` contract. The CLI alone does
117
- not add components to the default browser bundle:
118
-
119
- ```bash
120
- npx --yes @openuidev/cli@0.6.0 generate ./src/library.ts \
121
- --out config/openui/library.prompt.txt --no-telemetry
122
- # rename the emitted library.prompt.txt.spec.json to library.spec.json
123
- # (or point library_spec_path at the generated name) and commit both.
124
- ```
125
-
126
- Set `DO_NOT_TRACK=1` in CI as well as `--no-telemetry`.
127
-
128
- ### 3. Self-host the pinned runtime
129
-
130
- ```bash
131
- bin/rails openui:runtime:fetch # vendors the bundle into public/openui (needs npm)
132
- ```
133
-
134
- The task checks the archive and exact JS/CSS/license files against committed
135
- checksums. It disables npm lifecycle scripts and never extracts archive paths
136
- to disk. The default runtime and stylesheet use pinned SRI hashes.
137
-
138
- Include the stylesheet once in your layout's `<head>`:
139
-
140
- ```erb
141
- <%= openui_stylesheet_tag %>
142
- ```
143
-
144
- For external assets, configure both `runtime_url` and `runtime_stylesheet_url`
145
- with exact-version URLs. Supply their integrity hashes too.
146
-
147
- ### 4. Register the Stimulus controllers
148
-
149
- The engine adds its `app/javascript` directory to the asset paths, so the
150
- controllers are available to the asset pipeline. For importmap, add the pins:
151
-
152
- ```ruby
153
- # config/importmap.rb
154
- pin_all_from OpenUI::Rails::Engine.root.join("app/javascript/openui"), under: "openui"
155
- ```
156
-
157
- ```js
158
- // app/javascript/controllers/index.js
159
- import { application } from "controllers/application"
160
- import OpenUIController from "openui/rails/openui_controller"
161
- import OpenUIEnvelopeController from "openui/rails/openui_envelope_controller"
162
-
163
- application.register("openui", OpenUIController)
164
- application.register("openui-envelope", OpenUIEnvelopeController)
165
- ```
166
-
167
- The `openui` controller mounts the renderer; `openui-envelope` is only needed
168
- for the Turbo Streams transport (below).
169
-
170
- ### 5. Render a surface
46
+ Follow [Getting Started](https://openui-rails.wangyaodi.com/getting-started/) to
47
+ register the Stimulus controllers and include the stylesheet. After setup,
48
+ render a surface from an ERB view:
171
49
 
172
50
  ```erb
173
- <%# @surface is any object exposing #id (and optionally #version,
174
- #library_version, #response, #context, #message_id) %>
175
- <%= openui_surface(surface: @surface, stream: true) %>
176
- ```
177
-
178
- Pass `label: "Order details"` to expose the mount as a named `region` landmark.
179
- A streaming surface carries `aria-busy="true"` until it completes, so assistive
180
- technology waits for the region to settle instead of announcing each delta.
181
-
182
- ### 6. Register actions
183
-
184
- ```ruby
185
- # config/initializers/openui.rb (or an initializer of your own)
186
- OpenUI::Rails.actions.register "orders.refresh",
187
- description: "Re-fetch an order",
188
- schema: { "orderId" => String },
189
- authorize: ->(invocation) { OrderPolicy.new(invocation.actor, invocation.args["orderId"]).refresh? },
190
- handler: ->(invocation) { OrderSerializer.new(Order.find(invocation.args["orderId"])).as_json }
191
- ```
192
-
193
- ## Rendering surfaces
194
-
195
- ```ruby
196
- openui_surface(surface = nil, stream: false, dom_id: nil, initial_state: nil, label: nil, **options)
51
+ <%= openui_surface(
52
+ surface: { id: "support-42", response: 'root = TextContent("How can I help?")' },
53
+ label: "Support"
54
+ ) %>
197
55
  ```
198
56
 
199
- `Surface.coerce` accepts a `Surface`, a plain `Hash`, or a host record. It
200
- emits a flat `data-openui-*` mount element; the Stimulus controller reads it and
201
- never receives a credential or a server secret. Recognised record methods:
202
- `id`, `version`, `library_version`, `response`, `context`, `message_id`.
57
+ Actions are denied until you configure authorization.
203
58
 
204
- When a manifest is configured, the helper rejects an incompatible persisted
205
- `library_version`. Before mounting, the browser hashes the runtime library's
206
- `toSpec()` output and compares it with that manifest. A mismatch emits
207
- `openui:error` with `library_version_mismatch` and does not mount the renderer.
208
- Without a configured manifest, no library identity check is possible.
59
+ ## Documentation and development
209
60
 
210
- ## Streaming
211
-
212
- The stream bridge normalizes model output into ordered envelopes and coalesces
213
- small chunks up to `pending_byte_limit` (default 32 KiB), enforcing
214
- `stream_byte_limit` (default 1 MiB).
215
-
216
- Envelope types: `openui.delta`, `openui.snapshot`, `openui.completed`,
217
- `openui.error`. Every envelope carries `message_id`, `sequence` (from `0`) and
218
- an optional `library_version`.
219
-
220
- ```ruby
221
- bridge = OpenUI::Rails::Stream::Bridge.new(
222
- message_id: surface.id, library_version: config.library_fingerprint
223
- )
224
-
225
- # Pull source (an Enumerable of chunks):
226
- bridge.envelopes(chunks).each { |envelope| broadcast(envelope) }
227
-
228
- # Push source (a streaming provider callback):
229
- writer = bridge.writer
230
- writer.push(chunk).each { |envelope| broadcast(envelope) }
231
- writer.finish.each { |envelope| broadcast(envelope) }
232
- ```
233
-
234
- The browser controller applies envelopes in order, ignores duplicates, and
235
- requests a canonical snapshot (`openui:snapshot-required` event) when it detects
236
- a sequence gap. Deltas pause until a current snapshot arrives. Stale snapshots,
237
- duplicate terminal frames, malformed frames, and post-completion deltas cannot
238
- rewind or extend a completed response. The browser also enforces the byte limit.
239
-
240
- The controller **receives** `openui:envelope` and emits `openui:action`,
241
- `openui:state`, `openui:error`, and `openui:snapshot-required`. Your host must
242
- fetch and dispatch the authoritative snapshot on a gap or reconnect. The gem
243
- does not provide a persistence layer or snapshot HTTP endpoint.
244
-
245
- ### Transports
246
-
247
- `Stream::Broadcaster` binds the bridge to a transport and broadcasts each
248
- envelope as it is produced. Delivery is best effort — neither Action Cable nor
249
- Turbo Streams guarantee order — which is exactly why the client applies
250
- sequence numbers and recovers from a snapshot on a gap.
251
-
252
- ```ruby
253
- transport = OpenUI::Rails::Stream::Transports.build(:action_cable, stream_name: "openui:surface:#{surface.id}")
254
- # or: Transports.build(:turbo, stream: surface, target: "openui-surface-#{surface.id}")
255
- # or: Transports.build(:memory) # tests/examples
256
-
257
- broadcaster = OpenUI::Rails::Stream::Broadcaster.new(
258
- transport: transport, message_id: surface.id, library_version: config.library_fingerprint
259
- )
260
- chat.complete { |chunk| broadcaster.push(chunk.content) }
261
- broadcaster.finish
262
- ```
263
-
264
- - **Action Cable** (`:action_cable`) broadcasts the envelope hash. Subscribe in
265
- the browser and re-emit it as an event; authorization for the subscription
266
- stays in your channel:
267
-
268
- ```js
269
- import { createConsumer } from "@rails/actioncable"
270
- const consumer = createConsumer()
271
- consumer.subscriptions.create(
272
- { channel: "YourStreamChannel", stream: "openui:surface:42" },
273
- { received: (data) =>
274
- document.dispatchEvent(new CustomEvent("openui:envelope", { bubbles: true, detail: data })) }
275
- )
276
- ```
277
-
278
- - **Turbo Streams** (`:turbo`) appends an `openui-envelope` element to a target
279
- already in the DOM. The `openui-envelope` controller re-emits the envelope as
280
- a bubbling `openui:envelope` event and removes itself — no inline script, no
281
- CSP relaxation. This transport requires `turbo-rails` in the host; it is
282
- looked up lazily and never a hard dependency of this Gem.
283
-
284
- Subscribe with `<%= turbo_stream_from stream %>` and provide a dedicated
285
- inbox, such as `<div id="openui-inbox" hidden></div>`. Pass that inbox's ID
286
- as `target:`. Keep the inbox outside the React-owned subtree. Restrict stream
287
- subscription and signed-stream access to authorized users in your host.
288
-
289
- - **Memory** (`:memory`) keeps every envelope in memory for tests and examples.
290
-
291
- The surface controller listens for `openui:envelope` on `document`, so any
292
- transport that produces a bubbling event (or dispatches on the element) works,
293
- and envelopes for other `message_id`s are ignored.
294
-
295
- ## The action loop
296
-
297
- Component `onAction` and browser `toolProvider` `Query()`/`Mutation()` calls
298
- post the same body to the single endpoint (`/openui-rails/actions` by default):
299
-
300
- ```json
301
- { "action": "orders.refresh", "args": {}, "surface_id": "…", "version": 3,
302
- "idempotency_key": "…", "confirmed": false }
303
- ```
304
-
305
- The official component event uses `{ type, params }`; the bridge maps it to
306
- `action` and `args`. A tool receives the handler's `value`, not the endpoint's
307
- result wrapper. Unknown tool names still fail the server allowlist.
308
-
309
- `confirmed: true` is a host-supplied acknowledgement, not cryptographic proof
310
- of human consent. It is never inferred from `params` or form state. Collect
311
- confirmation in trusted host UI. For high-risk operations, verify a signed,
312
- server-issued approval in the authorizer as well.
313
-
314
- The `Actions::Dispatcher` applies every gate **before any side effect**, in
315
- order:
316
-
317
- 1. submission parse/validate
318
- 2. action is registered (allowlist — no reflection from a name to a method)
319
- 3. args match the schema (required/optional types, extra keys rejected)
320
- 4. explicit `confirmed: true` when the action is destructive
321
- 5. surface revision is current (stale protection)
322
- 6. authorizer allows it (required; fail closed)
323
- 7. idempotency claim (duplicates never run twice, replay with different args is rejected)
324
-
325
- Idempotency keys are scoped to the server actor and tenant. The request digest
326
- also binds the action, surface, version, arguments, and confirmation flag.
327
- See [configuration](CONFIGURATION.md) for the shared-store contract and TTL
328
- limits. This is not an exactly-once guarantee across expiry or process restarts.
329
-
330
- Structured errors (JSON `{ error: { code, message } }`):
331
-
332
- | Code | Status | Meaning |
333
- | --- | --- | --- |
334
- | `unknown_action` | 404 | not in the registry |
335
- | `unauthorized` | 403 | authorizer denied, no authorizer, or unresolvable surface |
336
- | `stale_surface` | 409 | submitted against a superseded revision |
337
- | `replay` | 409 | idempotency key reused |
338
- | `invalid_action` | 422 | malformed payload/args or missing confirmation |
339
- | `csrf_failed` | 422 | invalid authenticity token |
340
-
341
- ## Message protocol
342
-
343
- `OpenUI::Rails::Message` implements the upstream `buildMessage`/`parseMessage`
344
- framing (marker lines beginning with `]]>openui:`). Use it to store a response
345
- so the browser `Renderer` can read both content and context, and to strip the
346
- markers before a stored message is sent back to a model:
347
-
348
- ```ruby
349
- stored = OpenUI::Rails::Message.build(content: lang, context: { form: form_state }, finished: true)
350
- OpenUI::Rails::Message.strip(stored) # => model-safe content, no markers
351
- OpenUI::Rails::Message.parse(stored).context
352
- ```
353
-
354
- This is *message framing*, not an OpenUI Lang parser. Client-supplied context is
355
- untrusted input.
356
-
357
- ## RubyLLM adapter
358
-
359
- Optional and never required. It owns RubyLLM event normalization and delegates
360
- sequencing/limits to the stream bridge, so a host keeps RubyLLM's own Rails
361
- streaming pattern:
362
-
363
- ```ruby
364
- OpenUI::Rails::Adapters::RubyLLM.stream(
365
- chat: chat, question: question, surface_id: surface.id
366
- ) { |envelope| broadcast(envelope) }
367
- ```
368
-
369
- `ruby_llm` is isolated under `OpenUI::Rails::Adapters`; failures are classified
370
- by class *name*, so no `RubyLLM` constant is referenced at load time.
371
-
372
- ## Assets and CSP
373
-
374
- - The runtime is version-pinned and served from your app (or an exact-version
375
- URL you configure). Self-hosting is the default.
376
- - You may pin subresource integrity via `config.runtime_integrity`; the fetch
377
- task prints the digest to use.
378
- - To use a strict Content Security Policy, add a nonce/host for the runtime
379
- script you serve. This bridge adds no inline scripts. The upstream renderer
380
- does use inline styles and injects a loading `<style>` element. Strict
381
- `style-src` policies need separate validation; script SRI is not proof of
382
- strict-CSP compatibility.
383
-
384
- ## Accessibility
385
-
386
- - A streaming surface is marked `aria-busy="true"` and flips to `"false"` when
387
- it completes (or errors), so a screen reader waits for the region to settle.
388
- - Pass `label:` to `openui_surface` to expose the mount as a named `region`
389
- landmark; without a label no role is added, so unnamed containers are not
390
- invented.
391
- - The mount is not `aria-hidden`. Automated checks cover named regions,
392
- streaming busy states, and real keyboard activation. A human screen-reader
393
- audit of generated content has not been performed.
394
-
395
- ## Security model
396
-
397
- Model output is untrusted UI DSL. The controls this Gem is responsible for:
398
-
399
- - **Allowlist only** — the browser can resolve only components and actions the
400
- pinned library declares, and only actions registered in the Rails registry.
401
- - **One action path** — every write passes all seven gates above; no
402
- model-provided name maps to Ruby reflection.
403
- - **Fail closed** — no authorizer means denied.
404
- - **Rails-owned identity** — CSRF protection, session auth and per-surface
405
- version all come from Rails; surface ids are resolved scoped to actor/tenant.
406
- - **Untrusted context** — client form state is never treated as a write
407
- confirmation, and `]]>openui:` markers are stripped before model history.
408
- - **Pinned supply chain** — exact runtime/CLI versions, optional integrity,
409
- no production mutable CDN.
410
-
411
- Report vulnerabilities privately per [`SECURITY.md`](SECURITY.md).
412
-
413
- ## Versioning and rollback
414
-
415
- - `0.x` releases allow documented breaking changes with migration notes; the
416
- Ruby API is not frozen. See [`CHANGELOG.md`](CHANGELOG.md).
417
- - The JS runtime is version-locked, and a response is stored with its
418
- prompt/library fingerprint, so a mismatched fingerprint is a **visible
419
- failure** rather than rendering old source against a new schema.
420
- - Downgrading the Gem is the supported rollback; persisted snapshots are
421
- readable by previous versions where the schema is compatible, otherwise they
422
- degrade to a safe read-only/fallback with a diagnostic.
423
-
424
- ## Status
425
-
426
- Early development. The public API may change between `0.x` releases.
427
-
428
- The audit fixed defects that the original stub-only tests missed. See
429
- [AUDIT.md](AUDIT.md) for findings, verification evidence, and publication gates.
430
- CI runs unit/integration tests, deterministic browser tests, the real pinned
431
- OpenUI bundle, real Turbo/Action Cable delivery, a RubyLLM 2.1.1 SSE probe,
432
- dependency advisory checks, and a fresh-host test of the built gem.
433
-
434
- A live-provider smoke completed through OpenRouter on 2026-10-18; see
435
- [`AUDIT.md`](AUDIT.md) for its scope and evidence. Releases use the verified
436
- artifact, RubyGems Trusted Publishing, and the protected GitHub `release`
437
- environment. See [RubyGems](https://rubygems.org/gems/openui-rails) and
438
- [GitHub Releases](https://github.com/yorzi/openui-rails/releases) for published
439
- versions. Hosts that redistribute the fetched browser bundle must review its
440
- complete transitive notices as described in [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md).
441
-
442
- ## Development
443
-
444
- ```bash
445
- bundle install
446
- bundle exec bin/rails db:test:prepare
447
- bundle exec bin/rails test # unit + integration
448
- bundle exec bin/rails test:system # browser (needs Chrome)
449
- bundle exec rubocop # rubocop-rails-omakase
450
- bundle exec gem build openui-rails.gemspec
451
- ```
61
+ The documentation site contains the full guides and API reference:
452
62
 
453
- See [`CONTRIBUTING.md`](CONTRIBUTING.md).
63
+ - [Configuration](https://openui-rails.wangyaodi.com/configuration/)
64
+ - [Streaming](https://openui-rails.wangyaodi.com/streaming/) and [server actions](https://openui-rails.wangyaodi.com/actions/)
65
+ - [RubyLLM](https://openui-rails.wangyaodi.com/ruby-llm/) and [production setup](https://openui-rails.wangyaodi.com/production/)
66
+ - [Upgrading](https://openui-rails.wangyaodi.com/upgrading/) and [API reference](https://openui-rails.wangyaodi.com/api-reference/)
454
67
 
455
- ## Documentation
68
+ Version `0.2.0` adds default-library preparation and setup diagnostics.
69
+ The API may change during `0.x`; see the
70
+ [changelog](CHANGELOG.md) and the proposed [path to 1.0](ROADMAP.md).
71
+ The site's stable channel follows the latest release; [/next/](https://openui-rails.wangyaodi.com/next/)
72
+ follows `main`.
456
73
 
457
- - [`CHANGELOG.md`](CHANGELOG.md)
458
- - [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md)
459
- - [`CONTRIBUTING.md`](CONTRIBUTING.md)
460
- - [`CONFIGURATION.md`](CONFIGURATION.md)
461
- - [`UPGRADING.md`](UPGRADING.md)
462
- - [`AUDIT.md`](AUDIT.md)
463
- - [`RELEASING.md`](RELEASING.md)
464
- - [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md)
465
- - [`SECURITY.md`](SECURITY.md)
74
+ For repository work, use [Contributing](CONTRIBUTING.md),
75
+ [Releasing](RELEASING.md), and the dated [0.1.0 audit](AUDIT.md).
76
+ Report vulnerabilities through the [security policy](SECURITY.md).
466
77
 
467
78
  ## License
468
79
 
469
- Released under the [MIT License](LICENSE). OpenUI and the `@openuidev/*`
470
- packages are MIT-licensed by Thesys ("Copyright (c) 2011-2024 Thesys Inc.");
471
- their notices ship in [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md).
80
+ [MIT](LICENSE). See [third-party notices](THIRD_PARTY_NOTICES.md) before
81
+ redistributing the OpenUI browser bundle.
data/RELEASING.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Releasing openui-rails
2
2
 
3
- The owner authorized the initial `0.1.0` release on 2026-10-18. This is an
4
- unofficial integration, not a Thesys product. Publish only a tag whose complete
3
+ This is an unofficial integration, not a Thesys product. Obtain owner
4
+ authorization to push and publish each release. Publish only a tag whose complete
5
5
  CI suite and release metadata checks pass.
6
6
 
7
7
  ## Publishing configuration
@@ -12,9 +12,15 @@ administrator bypass. Self-review is available for the single maintainer.
12
12
  RubyGems trusts workflow `release.yml` and environment `release` through GitHub
13
13
  OIDC. No long-lived RubyGems API key is stored in GitHub.
14
14
 
15
- The initial pending publisher was registered on 2026-10-18. Pending publishers
16
- expire if unused; check the RubyGems settings before a delayed first publish.
17
- The first successful publication creates the gem and activates its publisher.
15
+ The initial pending publisher was registered and successfully used for `0.1.0`
16
+ on 2026-10-18. Future releases use the gem's Trusted Publisher. Update RubyGems
17
+ trust before changing the repository, workflow filename, or environment.
18
+
19
+ The documentation uses GitHub Pages with the GitHub Actions source. Its
20
+ `github-pages` environment permits branch `main` and tags matching `v*`.
21
+ Both rules are needed: a published release triggers the docs workflow with
22
+ the release tag, even though the workflow checks out the default branch to
23
+ build the stable and next documentation channels.
18
24
 
19
25
  Before each release, review [AUDIT.md](AUDIT.md), the dated changelog section,
20
26
  and the complete CI result for the exact commit. Preserve third-party notices.
@@ -40,7 +46,8 @@ and a 16-token output cap. See [AUDIT.md](AUDIT.md) for the result and limits.
40
46
  RELEASE_TAG=vx.y.z ruby bin/check-release
41
47
  ```
42
48
 
43
- 5. Commit the version and notes.
49
+ 5. Commit and push the version and notes to `main`. Wait for the complete CI
50
+ suite to pass for that exact commit before tagging.
44
51
  6. Create the release tag:
45
52
 
46
53
  ```bash
@@ -73,8 +80,11 @@ write permission then creates a **draft** GitHub release.
73
80
  The draft includes the verified gem, its SHA-256 checksum, and the dated
74
81
  changelog section. Verify the RubyGems version and review that draft before
75
82
  making it public.
76
- `CHANGELOG.md` now contains the dated `0.1.0` notes, and
77
- `RELEASE_TAG=v0.1.0 ruby bin/check-release` accepts the release metadata.
83
+ Publishing the GitHub release triggers the versioned documentation deployment.
84
+ Verify the stable site names the released version, `/next/` follows `main`,
85
+ and both channels include the matching setup and upgrading guides. Record
86
+ the workflow, published gem checksum, and hosted verification in a dated
87
+ release record. Keep earlier local validation evidence unchanged.
78
88
 
79
89
  ## Recovery
80
90
 
@@ -24,6 +24,16 @@ or referenced at an exact pinned version):
24
24
 
25
25
  The OpenUI repository is licensed MIT: "Copyright (c) 2011-2024 Thesys Inc."
26
26
 
27
+ ## Default-library exports
28
+
29
+ The gem includes the `openuiChatLibrary.prompt()` and `toSpec()` exports from
30
+ the exact pinned browser bundle under `lib/openui/rails/default_library/`.
31
+ `provenance.json` records the runtime integrity pin and export checksums. The
32
+ upstream MIT license accompanies them as `openui.LICENSE.txt` and is copied
33
+ alongside application artifacts by `openui:library:prepare`. Preserve it when
34
+ redistributing these exports. The exports contain library definitions and
35
+ prompt instructions, not the browser renderer implementation.
36
+
27
37
  ## Transitive browser dependencies
28
38
 
29
39
  The official browser bundle inlines dependencies, including React 19. They do
data/UPGRADING.md CHANGED
@@ -1,26 +1,9 @@
1
- # Pre-release audit changes
1
+ # Upgrading
2
2
 
3
- These changes apply before the first published release.
3
+ Read the [upgrade guide](https://openui-rails.wangyaodi.com/upgrading/)
4
+ for migration steps and the [changelog](CHANGELOG.md) for release changes.
4
5
 
5
- - Custom idempotency stores must distinguish `:pending` from completed
6
- `:replay`, return a claim token, and accept `record(..., claim_token:)`.
7
- See [CONFIGURATION.md](CONFIGURATION.md).
8
- - Action arguments are canonical JSON copies with string keys. Submissions,
9
- surfaces, envelopes, and manifests no longer share mutable caller input.
10
- - The engine controller inherits the host's `ApplicationController` and
11
- preserves its authentication filters. Full-stack Rails controllers are
12
- required. API-only controllers need a host-owned HTML/session/CSRF boundary;
13
- that integration is not covered by this release.
14
- - The helper rejects a persisted surface whose library differs from the
15
- configured manifest. The browser verifies the actual runtime `toSpec()`.
16
- Custom prompt/spec files require a matching custom runtime, not the default
17
- chat bundle.
18
- - Add `<%= openui_stylesheet_tag %>` once in your layout. The runtime fetch task
19
- now installs checksum-verified JS, CSS, and the upstream license.
20
- - The RubyLLM adapter no longer passes a question argument to `Chat#complete`.
21
- It adds the user message first. Both `:complete` and `:ask` work with 2.1.1.
22
- - Completion and error frames participate in sequence validation. After a gap,
23
- all deltas pause until the host sends a current snapshot. Duplicate finishes
24
- and writes after a terminal writer state produce no additional frames.
25
- - Tag pushes verify and build a release artifact. Publication now requires an
26
- explicit manual workflow dispatch after the full CI suite succeeds.
6
+ The site documents the latest release. For `main`, use
7
+ [/next/upgrading/](https://openui-rails.wangyaodi.com/next/upgrading/).
8
+ To read offline, open the [guide source](docs/_reference/upgrading.md)
9
+ at the tag matching your installed gem.
@@ -39,13 +39,14 @@ module OpenUI
39
39
  say "openui-rails installed.", :green
40
40
  say ""
41
41
  say "Next steps:"
42
- say " 1. Generate the library prompt and spec with the pinned CLI:"
43
- say " npx --yes @openuidev/cli@0.6.0 generate <entry> --out config/openui/library.prompt.txt --no-telemetry"
42
+ say " 1. Prepare the default chat library (no browser, Node, or provider required):"
43
+ say " bin/rails openui:library:prepare"
44
44
  say " 2. Self-host the pinned runtime:"
45
45
  say " bin/rails openui:runtime:fetch"
46
- say " 3. Register actions and an authorizer (see config/initializers/openui.rb)."
47
- say " 4. Pin/register the Stimulus controllers and include openui_stylesheet_tag in your layout."
48
- say " The default bundle requires the exact openuiChatLibrary prompt/spec. See the gem README."
46
+ say " 3. Pin/register the Stimulus controllers and include openui_stylesheet_tag in your layout."
47
+ say " 4. Run bin/rails openui:doctor, then verify rendering in a browser."
48
+ say " 5. Configure scoped surfaces and authorization before enabling actions."
49
+ say " Custom libraries require a matching custom runtime; see config/openui/README.md."
49
50
  say ""
50
51
  end
51
52
 
@@ -4,8 +4,8 @@ OpenUI::Rails.configure do |config|
4
4
  # Transport for incremental updates: :turbo, :action_cable or :none.
5
5
  config.transport = :turbo
6
6
 
7
- # The prompt and spec generated from the same, pinned library revision by the
8
- # OpenUI CLI. See config/openui/README.md for the exact command.
7
+ # Run bin/rails openui:library:prepare for the pinned default chat library.
8
+ # Custom prompt/spec files need a matching custom runtime.
9
9
  config.library_spec_path = Rails.root.join("config/openui/library.spec.json")
10
10
  config.prompt_path = Rails.root.join("config/openui/library.prompt.txt")
11
11