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.
- checksums.yaml +4 -4
- data/AUDIT.md +24 -4
- data/CHANGELOG.md +28 -1
- data/CONFIGURATION.md +9 -77
- data/CONTRIBUTING.md +29 -1
- data/DEVELOPMENT.md +68 -0
- data/README.md +49 -439
- data/RELEASING.md +18 -8
- data/THIRD_PARTY_NOTICES.md +10 -0
- data/UPGRADING.md +7 -24
- data/lib/generators/openui/install/install_generator.rb +6 -5
- data/lib/generators/openui/install/templates/config/initializers/openui.rb.tt +2 -2
- data/lib/generators/openui/install/templates/config/openui/README.md +19 -4
- data/lib/openui/rails/default_library/library.prompt.txt +198 -0
- data/lib/openui/rails/default_library/library.spec.json +4390 -0
- data/lib/openui/rails/default_library/openui.LICENSE.txt +21 -0
- data/lib/openui/rails/default_library/provenance.json +15 -0
- data/lib/openui/rails/default_library.rb +85 -0
- data/lib/openui/rails/doctor.rb +143 -0
- data/lib/openui/rails/library_manifest.rb +2 -1
- data/lib/openui/rails/version.rb +1 -1
- data/lib/openui/rails.rb +2 -0
- data/lib/tasks/openui.rake +19 -0
- metadata +8 -1
data/README.md
CHANGED
|
@@ -2,470 +2,80 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/yorzi/openui-rails/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
17
|
+
## What you get
|
|
20
18
|
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
36
|
-
|
|
25
|
+
Your app supplies scoped record lookup, stream subscriptions, snapshot recovery,
|
|
26
|
+
and shared idempotency storage when running multiple processes.
|
|
37
27
|
|
|
38
|
-
##
|
|
28
|
+
## Get started
|
|
39
29
|
|
|
40
|
-
|
|
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.
|
|
35
|
+
gem "openui-rails", "~> 0.2.0"
|
|
62
36
|
```
|
|
63
37
|
|
|
64
|
-
```
|
|
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
|
-
|
|
70
|
-
and
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
470
|
-
|
|
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
|
-
|
|
4
|
-
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
|
data/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -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
|
-
#
|
|
1
|
+
# Upgrading
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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.
|
|
43
|
-
say "
|
|
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.
|
|
47
|
-
say " 4.
|
|
48
|
-
say "
|
|
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
|
-
#
|
|
8
|
-
#
|
|
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
|
|