ruby_llm-ag_ui 0.1.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 +7 -0
- data/CHANGELOG.md +48 -0
- data/CONTRIBUTING.md +56 -0
- data/LICENSE.txt +21 -0
- data/README.md +146 -0
- data/SECURITY.md +30 -0
- data/docs/_advanced/rails-integration.md +199 -0
- data/docs/_advanced/security-and-deployment.md +204 -0
- data/docs/_core_features/approval-and-resume.md +62 -0
- data/docs/_core_features/input-validation.md +64 -0
- data/docs/_core_features/reconnects.md +42 -0
- data/docs/_core_features/streaming-runs.md +49 -0
- data/docs/_getting_started/configuration.md +57 -0
- data/docs/_getting_started/getting-started.md +52 -0
- data/docs/_getting_started/overview.md +189 -0
- data/docs/_getting_started/whats-new-in-0-1.md +41 -0
- data/docs/_reference/api-reference.md +26 -0
- data/docs/_reference/compatibility.md +130 -0
- data/docs/_reference/protocol-coverage.md +175 -0
- data/docs/_reference/release-history.md +22 -0
- data/docs/_reference/releasing.md +54 -0
- data/docs/_reference/third-party-notices.md +82 -0
- data/docs/_reference/upgrading.md +42 -0
- data/docs/index.md +53 -0
- data/lib/generators/ruby_llm/ag_ui/install_generator.rb +37 -0
- data/lib/generators/ruby_llm/ag_ui/templates/create_ruby_llm_ag_ui_runs.rb.tt +24 -0
- data/lib/generators/ruby_llm/ag_ui/templates/initializer.rb.tt +24 -0
- data/lib/ruby_llm/ag_ui/canonical.rb +40 -0
- data/lib/ruby_llm/ag_ui/configuration.rb +113 -0
- data/lib/ruby_llm/ag_ui/coverage.rb +163 -0
- data/lib/ruby_llm/ag_ui/emitter.rb +119 -0
- data/lib/ruby_llm/ag_ui/error.rb +69 -0
- data/lib/ruby_llm/ag_ui/error_sanitizer.rb +69 -0
- data/lib/ruby_llm/ag_ui/input.rb +299 -0
- data/lib/ruby_llm/ag_ui/interrupt.rb +207 -0
- data/lib/ruby_llm/ag_ui/mapper.rb +333 -0
- data/lib/ruby_llm/ag_ui/message_parser.rb +127 -0
- data/lib/ruby_llm/ag_ui/protocol.rb +130 -0
- data/lib/ruby_llm/ag_ui/rails/active_record_store.rb +119 -0
- data/lib/ruby_llm/ag_ui/rails/railtie.rb +29 -0
- data/lib/ruby_llm/ag_ui/rails/record.rb +51 -0
- data/lib/ruby_llm/ag_ui/rails/streaming.rb +166 -0
- data/lib/ruby_llm/ag_ui/reconnect.rb +86 -0
- data/lib/ruby_llm/ag_ui/redaction.rb +58 -0
- data/lib/ruby_llm/ag_ui/redemption.rb +62 -0
- data/lib/ruby_llm/ag_ui/reservation.rb +56 -0
- data/lib/ruby_llm/ag_ui/resume.rb +108 -0
- data/lib/ruby_llm/ag_ui/resume_entry.rb +49 -0
- data/lib/ruby_llm/ag_ui/run.rb +177 -0
- data/lib/ruby_llm/ag_ui/run_record.rb +229 -0
- data/lib/ruby_llm/ag_ui/sse_writer.rb +119 -0
- data/lib/ruby_llm/ag_ui/store/base.rb +123 -0
- data/lib/ruby_llm/ag_ui/store/memory_store.rb +123 -0
- data/lib/ruby_llm/ag_ui/validation.rb +63 -0
- data/lib/ruby_llm/ag_ui/version.rb +8 -0
- data/lib/ruby_llm/ag_ui.rb +65 -0
- metadata +223 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: "Security and Deployment"
|
|
4
|
+
nav_order: 2
|
|
5
|
+
description: Keep identity, tool permissions, and sensitive output under the host application's control.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# {{ page.title }}
|
|
9
|
+
|
|
10
|
+
{{ page.description }}
|
|
11
|
+
{: .fs-6 .fw-300 }
|
|
12
|
+
|
|
13
|
+
After reading this guide, you will know:
|
|
14
|
+
|
|
15
|
+
* Which values the gem treats as untrusted.
|
|
16
|
+
* How redaction, error sanitization, and interrupt redemption work.
|
|
17
|
+
* Which security decisions the host application must make.
|
|
18
|
+
|
|
19
|
+
This gem is a **producer**, not an authorization layer. It decides what a run may
|
|
20
|
+
report on the wire and refuses what it cannot report truthfully. It does not
|
|
21
|
+
decide who may run what: authentication, authorization, which tools and model an
|
|
22
|
+
actor may use, and whether a tool may execute are the host's decisions.
|
|
23
|
+
|
|
24
|
+
The model below is the one the test suite enforces, and the negative cases are
|
|
25
|
+
fixtures, not prose. If a claim here has no test, it is marked as host
|
|
26
|
+
responsibility.
|
|
27
|
+
|
|
28
|
+
## Trust boundaries
|
|
29
|
+
|
|
30
|
+
| Input | Trusted? | Why |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `messages`, `state`, `tools`, `context`, `forwardedProps` | **No** | Arbitrary JSON from the client. Accepted as data only. |
|
|
33
|
+
| `threadId`, `runId`, `parentRunId`, `resume` | **No** | They name things; they never select a record or grant a right. |
|
|
34
|
+
| `protocolVersion` | **No** | Reported and compared; an unknown version is refused, not honored. |
|
|
35
|
+
| The `scope` string | **Yes, because the host built it** | It must come from authenticated identity. The gem rejects a non-String or empty scope. |
|
|
36
|
+
| The `chat` object | **Yes** | The host constructed it, with the host's tools and model. |
|
|
37
|
+
| Tool output | **No** | A tool reads databases, APIs and files. Its payload can hold secrets and personal data. |
|
|
38
|
+
| Provider exceptions | **No** | They can embed request bodies, URLs, headers and keys. |
|
|
39
|
+
| The run store | **Yes** | Host infrastructure. |
|
|
40
|
+
|
|
41
|
+
## What a client cannot do
|
|
42
|
+
|
|
43
|
+
- **Choose the model, tools or system prompt.** `input.tools` is informational:
|
|
44
|
+
`Input#tool_names` and `Input#client_tools?` report it, and nothing in the gem
|
|
45
|
+
registers a client-supplied tool on the chat. `input.messages` is parsed and
|
|
46
|
+
echoed, never appended to the chat as trusted turns; `config.allow_client_history`
|
|
47
|
+
is off and documented as off.
|
|
48
|
+
- **Reach another actor's run.** Every lookup is bounded by the scope the host
|
|
49
|
+
supplies. The same `(threadId, runId)` under a different scope is a different
|
|
50
|
+
run (`spec/ruby_llm/ag_ui/run_spec.rb`: "does not let one scope attach to
|
|
51
|
+
another tenant run").
|
|
52
|
+
- **Redeclare a run id.** The same identity with a *different* input is refused
|
|
53
|
+
as `RunConflictError` (409 in the Rails adapter) rather than executed
|
|
54
|
+
("refuses the same identity with a changed input as a conflict").
|
|
55
|
+
- **Run the same tool twice.** A resume is recorded once; a second answer to the
|
|
56
|
+
same interrupt is refused without executing the tool ("refuses a decision
|
|
57
|
+
another resume already recorded, without executing the tool").
|
|
58
|
+
- **Widen what an approval covers.** An interrupt pins the tool call and the
|
|
59
|
+
digest of the arguments RubyLLM asked to run. A resume whose call arguments
|
|
60
|
+
changed, whose call is no longer pending, or which names an interrupt that
|
|
61
|
+
expired or does not exist is refused before anything is reserved or written
|
|
62
|
+
("refuses a resume whose tool call arguments changed since the interrupt",
|
|
63
|
+
"refuses an expired interrupt", "refuses an unknown interrupt before the run is
|
|
64
|
+
reserved or opened").
|
|
65
|
+
- **Inject SSE framing.** Every frame is one JSON document on one `data:` line;
|
|
66
|
+
a payload containing newlines stays inside the JSON string. There is no path
|
|
67
|
+
that writes a raw payload into a frame.
|
|
68
|
+
- **Exhaust the process with one body.** The body is bounded before it is parsed
|
|
69
|
+
(`max_input_bytes`, 256 KB default), and so are message, tool and context
|
|
70
|
+
counts.
|
|
71
|
+
|
|
72
|
+
## Redaction is the default
|
|
73
|
+
|
|
74
|
+
`config.redact` defaults to `RubyLLM::AgUI::Redaction.method(:default)`: the
|
|
75
|
+
payload keeps its **shape** and loses its **content** (`[redacted]`), because the
|
|
76
|
+
gem cannot know which part of a tool result is a secret. The mapper warns once per
|
|
77
|
+
run that the host has not chosen a policy
|
|
78
|
+
(`spec/ruby_llm/ag_ui/mapper_spec.rb`: "keeps the shape and withholds the
|
|
79
|
+
content", "warns once that the host has not chosen a policy").
|
|
80
|
+
|
|
81
|
+
Opting out is explicit:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
RubyLLM::AgUI.configure do |config|
|
|
85
|
+
config.redact = RubyLLM::AgUI::Redaction.method(:none) # publish as-is
|
|
86
|
+
# or
|
|
87
|
+
config.redact = ->(kind, payload) { MyPolicy.call(kind, payload) }
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
A `nil` policy is not an opt-out: `Configuration#redact` falls back to the
|
|
92
|
+
default, so a mistyped configuration cannot publish raw payloads. What is in
|
|
93
|
+
force is reported by `RubyLLM::AgUI.config.to_h[:redact]` as `:default`, `:none`
|
|
94
|
+
or `:custom`.
|
|
95
|
+
|
|
96
|
+
Redaction applies to what the gem publishes. It does not scrub the host's logs,
|
|
97
|
+
the chat's own history, the provider's copy of the conversation, or any payload
|
|
98
|
+
the host writes to the wire by other means.
|
|
99
|
+
|
|
100
|
+
## Errors never carry exception text
|
|
101
|
+
|
|
102
|
+
`RUN_ERROR` is built from `ErrorSanitizer`: a fixed message and code per error
|
|
103
|
+
class, and a fixed `internal_error` fallback for anything unrecognized. The raw
|
|
104
|
+
exception goes to the host logger, if one is configured, and nowhere else
|
|
105
|
+
(`spec/ruby_llm/ag_ui/run_spec.rb`: "never echoes the exception text";
|
|
106
|
+
`spec/ruby_llm/ag_ui/error_sanitizer_spec.rb`).
|
|
107
|
+
|
|
108
|
+
This matters because upstream Rails examples put `e.message` straight into the
|
|
109
|
+
event. A provider error message can contain the prompt, the tool arguments, a URL
|
|
110
|
+
with a key in it, or an internal path.
|
|
111
|
+
|
|
112
|
+
## Interrupts
|
|
113
|
+
|
|
114
|
+
An interrupt is a pause, not a permission. It carries the tool call id, a reason,
|
|
115
|
+
a human-readable question, and a host-owned expiry. It does **not** carry the
|
|
116
|
+
arguments digest that the resume check uses ("does not let the arguments digest
|
|
117
|
+
reach the client"), so the client cannot forge a matching approval.
|
|
118
|
+
|
|
119
|
+
- TTL is `config.interrupt_ttl` (900 s default) or per-run `expires_in:`.
|
|
120
|
+
- An expired interrupt is not answerable; a resume for it is refused.
|
|
121
|
+
- A resume with `status: "cancelled"` records a denial. The chat's next
|
|
122
|
+
`complete` reports the denial as RubyLLM normally would; no tool runs.
|
|
123
|
+
- Approval does not skip the host's re-authorization: the host re-checks before
|
|
124
|
+
a tool executes, and the gem cannot do it for them.
|
|
125
|
+
|
|
126
|
+
The store's redemption step is the concurrency guard: the first decision wins and
|
|
127
|
+
is recorded, the loser is told it was already recorded. That is why an approved
|
|
128
|
+
tool executes once even when two resumes arrive together.
|
|
129
|
+
|
|
130
|
+
## Reconnect
|
|
131
|
+
|
|
132
|
+
A run that already ended is served from its record: `RUN_STARTED` (with the
|
|
133
|
+
protocol version and the input echo), `STATE_SNAPSHOT` when one was recorded, and
|
|
134
|
+
the recorded terminal event. Deltas are never replayed, and a run that has not
|
|
135
|
+
ended is refused rather than attached ("refuses to attach a second stream to a run
|
|
136
|
+
that has not ended"). Both rules exist to prevent a duplicated side effect, not
|
|
137
|
+
just duplicated text.
|
|
138
|
+
|
|
139
|
+
A recorded failure is reported with the sanitized message and code it was stored
|
|
140
|
+
with. A record with no recorded failure reports `internal_error` rather than
|
|
141
|
+
guessing.
|
|
142
|
+
|
|
143
|
+
## Transport
|
|
144
|
+
|
|
145
|
+
The gem writes to whatever object the host gives it and sets no security policy
|
|
146
|
+
of its own beyond the framing. The Rails adapter adds:
|
|
147
|
+
|
|
148
|
+
- `Accept` negotiation: a client that cannot read `text/event-stream` gets 406
|
|
149
|
+
before any event.
|
|
150
|
+
- Input refusal before the response commits: 400, 404, 409, 413, never a
|
|
151
|
+
half-open stream.
|
|
152
|
+
- `Content-Type: text/event-stream`, `Cache-Control: no-cache`,
|
|
153
|
+
`Connection: keep-alive`, `X-Accel-Buffering: no`.
|
|
154
|
+
- A client disconnect is swallowed: it is not a run outcome.
|
|
155
|
+
|
|
156
|
+
The gem does not terminate TLS, authenticate, rate-limit, validate Origin/CSRF
|
|
157
|
+
for browser clients, or configure CORS. Those are the host's, and getting them
|
|
158
|
+
wrong is outside what this gem can detect.
|
|
159
|
+
|
|
160
|
+
## Deployment checklist
|
|
161
|
+
|
|
162
|
+
1. Authenticate before parsing the body; build `scope` from the authenticated
|
|
163
|
+
actor, never from anything in the body.
|
|
164
|
+
2. Authorize the `threadId → chat` lookup inside the actor's own association, so
|
|
165
|
+
a client-chosen id cannot select somebody else's conversation.
|
|
166
|
+
3. Re-authorize before an approved tool runs. An approval answers an interrupt;
|
|
167
|
+
it does not grant permission.
|
|
168
|
+
4. Use a durable store with a uniqueness constraint on
|
|
169
|
+
`(scope, thread_id, run_id)` and `(scope, thread_id, interrupt_id)`. The Rails
|
|
170
|
+
store is built for that; `MemoryStore` is single-process only.
|
|
171
|
+
5. Keep redaction on until you have classified which payloads may be published.
|
|
172
|
+
Scrub raw bodies, prompts and tool payloads out of your logs.
|
|
173
|
+
6. Treat `messages`, `state` and `forwardedProps` as untrusted text in any UI you
|
|
174
|
+
build. Escape it where it is rendered; a producer cannot prevent XSS.
|
|
175
|
+
7. Bound the resource: request size (`max_input_bytes`), provider timeouts, rate
|
|
176
|
+
limits, and the number of concurrent streams (an `ActionController::Live`
|
|
177
|
+
response holds a thread for the run's lifetime).
|
|
178
|
+
8. Serve over TLS, and set CORS and cookie policy deliberately if the stream is
|
|
179
|
+
consumed from a browser.
|
|
180
|
+
9. Do not enable client-supplied tools for a multi-tenant deployment in this
|
|
181
|
+
release; they are never used, and that is the intended behavior.
|
|
182
|
+
10. Do not log the request body or the events at info level.
|
|
183
|
+
|
|
184
|
+
## What is out of scope
|
|
185
|
+
|
|
186
|
+
- **Prompt injection.** Content that arrives in `messages`, tool results or
|
|
187
|
+
documents can steer a model. This gem maps the model's behavior faithfully; it
|
|
188
|
+
cannot make the model trustworthy.
|
|
189
|
+
- **Host authentication and authorization**, including whether a given actor
|
|
190
|
+
should be allowed to run a given tool.
|
|
191
|
+
- **Provider-side data handling**, retention and training.
|
|
192
|
+
- **Anything the host writes outside the gem's events**, such as its own log lines,
|
|
193
|
+
its own extra SSE frames, its own UI.
|
|
194
|
+
- **Denial of service by an authenticated actor** who opens many legitimate
|
|
195
|
+
streams.
|
|
196
|
+
|
|
197
|
+
## Reporting
|
|
198
|
+
|
|
199
|
+
See the [repository security policy](https://github.com/yorzi/ruby_llm-ag-ui/blob/main/SECURITY.md) for the reporting process. Reports about a silent duplicate execution, an approval that widens what it covers, a secret in a `RUN_ERROR`, or a leak into the log are treated as vulnerabilities.
|
|
200
|
+
|
|
201
|
+
## Next Steps
|
|
202
|
+
|
|
203
|
+
* [Configuration]({% link _getting_started/configuration.md %}) - set safe redaction and storage defaults.
|
|
204
|
+
* [Rails Integration]({% link _advanced/rails-integration.md %}) - apply these boundaries in a controller.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: "Approval and Resume"
|
|
4
|
+
nav_order: 3
|
|
5
|
+
description: "Pause a run for host-authorized tool approval and answer the interrupt on a new run."
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# {{ page.title }}
|
|
9
|
+
|
|
10
|
+
{{ page.description }}
|
|
11
|
+
{: .fs-6 .fw-300 }
|
|
12
|
+
|
|
13
|
+
After reading this guide, you will know:
|
|
14
|
+
|
|
15
|
+
* What an approval interrupt contains.
|
|
16
|
+
* How the client answers it on a new run.
|
|
17
|
+
* What the host must authorize again before a tool executes.
|
|
18
|
+
|
|
19
|
+
## Return an Interrupt
|
|
20
|
+
|
|
21
|
+
When RubyLLM pauses on `requires_approval`, the run ends with an interrupt outcome. The interrupt identifies the pending tool call and includes a host-owned expiry. The id and expiry below are from a sample run; each run receives new values.
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"type": "RUN_FINISHED",
|
|
26
|
+
"threadId": "thread-1",
|
|
27
|
+
"runId": "run-1",
|
|
28
|
+
"outcome": {
|
|
29
|
+
"type": "interrupt",
|
|
30
|
+
"interrupts": [{
|
|
31
|
+
"id": "817cc7a4-c2b4-4393-a25b-09819cf7198a",
|
|
32
|
+
"reason": "tool_call_confirmation",
|
|
33
|
+
"message": "Approve the cancel_order tool call?",
|
|
34
|
+
"toolCallId": "call_cancel_42",
|
|
35
|
+
"expiresAt": "2026-10-18T02:19:38Z"
|
|
36
|
+
}]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The interrupt does not expose the tool arguments digest used by the server. A client cannot use the response as a permission token.
|
|
42
|
+
|
|
43
|
+
## Answer on a New Run
|
|
44
|
+
|
|
45
|
+
The client sends a new `runId` for the same thread and includes a resume entry. `resolved` approves the pending call; `cancelled` records a denial.
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"threadId": "thread-1",
|
|
50
|
+
"runId": "run-2",
|
|
51
|
+
"messages": [{ "id": "m1", "role": "user", "content": "Cancel order 42." }],
|
|
52
|
+
"resume": [{ "interruptId": "817cc7a4-c2b4-4393-a25b-09819cf7198a", "status": "resolved" }]
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The interrupt must still be pending and unexpired. The tool call id and argument digest must match the call RubyLLM is holding. The host must re-authorize the action before the tool executes. The store records only one decision, so a replay cannot execute the tool twice.
|
|
57
|
+
|
|
58
|
+
## Next Steps
|
|
59
|
+
|
|
60
|
+
* [Streaming Runs]({% link _core_features/streaming-runs.md %}) - follow the event and outcome sequence.
|
|
61
|
+
* [Reconnects and Run Identity]({% link _core_features/reconnects.md %}) - understand stored run identity.
|
|
62
|
+
* [Security and Deployment]({% link _advanced/security-and-deployment.md %}) - keep approval separate from authorization.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: "Validating Input"
|
|
4
|
+
nav_order: 1
|
|
5
|
+
description: "Validate a bounded AG-UI request and turn it into immutable values."
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# {{ page.title }}
|
|
9
|
+
|
|
10
|
+
{{ page.description }}
|
|
11
|
+
{: .fs-6 .fw-300 }
|
|
12
|
+
|
|
13
|
+
After reading this guide, you will know:
|
|
14
|
+
|
|
15
|
+
* How to parse a request body and inspect its typed values.
|
|
16
|
+
* How unknown members, size limits, and validation errors work.
|
|
17
|
+
* Which request values remain untrusted after parsing.
|
|
18
|
+
|
|
19
|
+
## Parse a Request
|
|
20
|
+
|
|
21
|
+
`RunAgentInput` arrives as JSON. Parse it before choosing a chat so malformed or oversized input is refused before provider work begins.
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
require 'ruby_llm/ag_ui'
|
|
25
|
+
|
|
26
|
+
input = RubyLLM::AgUI::Input.parse({
|
|
27
|
+
'threadId' => 'thread-1',
|
|
28
|
+
'runId' => 'run-1',
|
|
29
|
+
'messages' => [{ 'id' => 'm1', 'role' => 'user', 'content' => 'Hello' }]
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
puts [input.thread_id, input.run_id].join('/')
|
|
33
|
+
# => thread-1/run-1
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`Input.parse` accepts a JSON string, a Hash, or a readable stream. The result is immutable. It exposes the accepted values, a canonical digest, and any non-fatal warnings.
|
|
37
|
+
|
|
38
|
+
## Unknown Members and Limits
|
|
39
|
+
|
|
40
|
+
By default, top-level members outside the pinned schema are stripped and recorded in `warnings`. Set `strict: true` to reject them. The parser also bounds the request body, nesting depth, and counts for messages, tools, and context.
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
input = RubyLLM::AgUI::Input.parse({
|
|
44
|
+
'threadId' => 'thread-1',
|
|
45
|
+
'runId' => 'run-1',
|
|
46
|
+
'messages' => [],
|
|
47
|
+
'clientOnly' => true
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
input.warnings
|
|
51
|
+
# => ["stripped unknown RunAgentInput member \"clientOnly\""]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Pass `max_bytes:` to set a per-parse body limit. Configure `max_messages`, `max_tools`, and `max_context` on `RubyLLM::AgUI.config`. Invalid JSON, unsupported protocol versions, missing identifiers, and invalid message shapes raise an `InputError` subclass before a run starts.
|
|
55
|
+
|
|
56
|
+
## Parsed Data Does Not Grant Trust
|
|
57
|
+
|
|
58
|
+
The parser validates shape, not authority. Client-supplied tools are reported by `tool_names` and `client_tools?`, but they are never registered on the chat. The host chooses the model, tools, system prompt, chat history, and authenticated scope. Do not use `thread_id` or `run_id` as authorization by themselves.
|
|
59
|
+
|
|
60
|
+
## Next Steps
|
|
61
|
+
|
|
62
|
+
* [Getting Started]({% link _getting_started/getting-started.md %}) - install and parse a request.
|
|
63
|
+
* [Streaming Runs]({% link _core_features/streaming-runs.md %}) - pass a validated input to a RubyLLM chat.
|
|
64
|
+
* [Security and Deployment]({% link _advanced/security-and-deployment.md %}) - define the host trust boundary.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: "Reconnects and Run Identity"
|
|
4
|
+
nav_order: 4
|
|
5
|
+
description: "Reserve each run once and report its recorded outcome after a reconnect."
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# {{ page.title }}
|
|
9
|
+
|
|
10
|
+
{{ page.description }}
|
|
11
|
+
{: .fs-6 .fw-300 }
|
|
12
|
+
|
|
13
|
+
After reading this guide, you will know:
|
|
14
|
+
|
|
15
|
+
* How the gem reserves `(scope, threadId, runId)` for one input digest.
|
|
16
|
+
* How retries and reconnects differ from duplicate execution.
|
|
17
|
+
* Which store to use for one process or several workers.
|
|
18
|
+
|
|
19
|
+
## Reuse a Run Identity
|
|
20
|
+
|
|
21
|
+
The store reserves a run before any provider work. It keys the record by authenticated scope, thread id, and run id, then compares the canonical digest of the accepted input.
|
|
22
|
+
|
|
23
|
+
| Request | Result |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| New identity | Reserve and execute the run. |
|
|
26
|
+
| Same identity and same input while running | Refuse a second live stream. |
|
|
27
|
+
| Same identity and same input after completion | Return the recorded start, optional state snapshot, and terminal event. |
|
|
28
|
+
| Same identity with different input | Raise a run conflict. |
|
|
29
|
+
|
|
30
|
+
Reconnect does not replay text or tool deltas. Replaying deltas could display text twice or repeat a side effect. The host can publish a state snapshot for a finished run, but the gem does not reconstruct conversation history.
|
|
31
|
+
|
|
32
|
+
## Choose a Store
|
|
33
|
+
|
|
34
|
+
`MemoryStore` is suitable for tests and a single process. Its records disappear on restart and are not shared between workers. The Rails adapter's `ActiveRecordStore` persists records and redeems interrupts under a database lock.
|
|
35
|
+
|
|
36
|
+
The store boundary is `RubyLLM::AgUI::Store::Base`. Configure a compatible store globally or pass it to `Run.new`. For multi-process deployments, the database must enforce unique run identity and serialize interrupt redemption.
|
|
37
|
+
|
|
38
|
+
## Next Steps
|
|
39
|
+
|
|
40
|
+
* [Configuration]({% link _getting_started/configuration.md %}) - set the default store.
|
|
41
|
+
* [Rails Integration]({% link _advanced/rails-integration.md %}) - install the durable ActiveRecord store.
|
|
42
|
+
* [Protocol Coverage]({% link _reference/protocol-coverage.md %}) - inspect reconnect events.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: "Streaming Runs"
|
|
4
|
+
nav_order: 2
|
|
5
|
+
description: "Run one RubyLLM chat turn and write ordered AG-UI events as server-sent events."
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# {{ page.title }}
|
|
9
|
+
|
|
10
|
+
{{ page.description }}
|
|
11
|
+
{: .fs-6 .fw-300 }
|
|
12
|
+
|
|
13
|
+
After reading this guide, you will know:
|
|
14
|
+
|
|
15
|
+
* How to connect a host-owned chat, parsed input, scope, and store.
|
|
16
|
+
* How a RubyLLM turn becomes ordered AG-UI events.
|
|
17
|
+
* What each terminal outcome means.
|
|
18
|
+
|
|
19
|
+
## Run a Complete Example
|
|
20
|
+
|
|
21
|
+
The repository includes a scripted chat that emits an approval pause, resumes the tool call, and prints the server-sent events. Run it from the repository root; it does not need a provider key.
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
bundle exec rake example
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
In an application, parse the request first, then pass the host-selected chat, parsed input, authenticated scope, and run store to `RubyLLM::AgUI::Run`. Call `stream(to:)` with an IO-like stream. The host builds the chat and authorizes access to the thread; the gem reserves the run identity before provider work.
|
|
28
|
+
|
|
29
|
+
## Event Order
|
|
30
|
+
|
|
31
|
+
The mapper turns RubyLLM callbacks into AG-UI events. The emitter assigns each event a sequence, persists run state, closes open text or tool events, and writes one JSON event per `data:` line through the SSE writer.
|
|
32
|
+
|
|
33
|
+
An assistant message opens on its first non-empty chunk and closes before a tool call, phase change, or terminal event. A tool call starts when its id and name are known, emits the arguments as received, and ends before the call runs. One run ends with one terminal result.
|
|
34
|
+
|
|
35
|
+
## Outcomes
|
|
36
|
+
|
|
37
|
+
* A completed chat ends with `RUN_FINISHED` and a `success` outcome.
|
|
38
|
+
* A `requires_approval` pause ends with `RUN_FINISHED` and an `interrupt` outcome.
|
|
39
|
+
* `Chat#cancel` ends with `RUN_FINISHED` and a `cancelled` outcome.
|
|
40
|
+
* A failure ends with `RUN_ERROR` and a sanitized message and code.
|
|
41
|
+
* A pause the protocol cannot express is an error, never a success.
|
|
42
|
+
|
|
43
|
+
The run record reaches a terminal state before the terminal event is written. If the connection closes in the middle, the record remains available for a later reconnect.
|
|
44
|
+
|
|
45
|
+
## Next Steps
|
|
46
|
+
|
|
47
|
+
* [Validating Input]({% link _core_features/input-validation.md %}) - parse untrusted request data.
|
|
48
|
+
* [Approval and Resume]({% link _core_features/approval-and-resume.md %}) - answer an interrupt on a new run.
|
|
49
|
+
* [Reconnects and Run Identity]({% link _core_features/reconnects.md %}) - serve a completed run again.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Configuration
|
|
4
|
+
nav_order: 3
|
|
5
|
+
description: Set run storage, input limits, logging, and redaction defaults for the application.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# {{ page.title }}
|
|
9
|
+
|
|
10
|
+
{{ page.description }}
|
|
11
|
+
{: .fs-6 .fw-300 }
|
|
12
|
+
|
|
13
|
+
After reading this guide, you will know:
|
|
14
|
+
|
|
15
|
+
* How to configure defaults once for the application.
|
|
16
|
+
* Which settings are available and what each one controls.
|
|
17
|
+
|
|
18
|
+
## Configuring RubyLLM AG-UI
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
require 'ruby_llm/ag_ui'
|
|
22
|
+
|
|
23
|
+
RubyLLM::AgUI.configure do |config|
|
|
24
|
+
config.run_store = RubyLLM::AgUI::Store::MemoryStore.new
|
|
25
|
+
config.max_input_bytes = 256_000
|
|
26
|
+
config.redact = RubyLLM::AgUI::Redaction.method(:default)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
p RubyLLM::AgUI.config.to_h[:redact]
|
|
30
|
+
# => :default
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Settings
|
|
34
|
+
|
|
35
|
+
The global configuration supplies defaults. `Run.new` can override the store, logger, redaction policy, clock, and interrupt expiry for one run. `MemoryStore` is process-local, so use a durable store when runs can move between workers.
|
|
36
|
+
|
|
37
|
+
| Setting | Default | Purpose |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `run_store` | `nil` | Store run records and interrupts. A run needs a store, supplied here or to `Run.new`. |
|
|
40
|
+
| `redact` | `Redaction.default` | Transform tool results before they reach the stream. |
|
|
41
|
+
| `logger` | `nil` | Receive host-side diagnostics without prompts or raw tool payloads. |
|
|
42
|
+
| `protocol_version` | `"1.0"` | Value reported by `RUN_STARTED`. |
|
|
43
|
+
| `max_input_bytes` | `256_000` | Maximum request body size. |
|
|
44
|
+
| `max_messages` | `500` | Maximum messages in one request. |
|
|
45
|
+
| `max_tools` | `64` | Maximum tool definitions in one request. |
|
|
46
|
+
| `max_context` | `64` | Maximum context entries in one request. |
|
|
47
|
+
| `interrupt_ttl` | `900` seconds | How long an approval interrupt can be answered. |
|
|
48
|
+
| `clock` | `Time.now.utc` | Clock used to set interrupt expiry. |
|
|
49
|
+
| `allow_client_history` | `false` | Reserved for future history behavior; client history is not trusted in this release. |
|
|
50
|
+
|
|
51
|
+
Provider keys and model selection are configured through RubyLLM and the host app. This gem does not read credentials when it loads.
|
|
52
|
+
|
|
53
|
+
## Next Steps
|
|
54
|
+
|
|
55
|
+
* [Getting Started]({% link _getting_started/getting-started.md %}) - install and parse a request.
|
|
56
|
+
* [How RubyLLM AG-UI Works]({% link _getting_started/overview.md %}) - the parts of one run.
|
|
57
|
+
* [Security and Deployment]({% link _advanced/security-and-deployment.md %}) - host responsibilities and safe defaults.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Getting Started
|
|
4
|
+
nav_order: 1
|
|
5
|
+
description: Install ruby_llm-ag_ui and validate an AG-UI request before connecting a chat.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# {{ page.title }}
|
|
9
|
+
|
|
10
|
+
{{ page.description }}
|
|
11
|
+
{: .fs-6 .fw-300 }
|
|
12
|
+
|
|
13
|
+
After reading this guide, you will know:
|
|
14
|
+
|
|
15
|
+
* How to install the release or the current development branch.
|
|
16
|
+
* How to parse a `RunAgentInput` into validated Ruby values.
|
|
17
|
+
* How to run the included offline end-to-end example.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
{% if site.docs_unreleased %}bundle add ruby_llm-ag_ui --git https://github.com/yorzi/ruby_llm-ag-ui --branch main{% else %}bundle add ruby_llm-ag_ui --version {{ site.version }}{% endif %}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The gem requires Ruby 3.2 or newer, RubyLLM 2.1 or newer, and `ag-ui-protocol` 0.2.x. The Rails adapter is optional. Provider credentials belong to your RubyLLM application; the parser example below needs none.
|
|
26
|
+
|
|
27
|
+
The runtime dependencies select patched `cgi`, `resolv`, `uri`, and `zlib` gems. An older Ruby installation's default library versions do not override those dependency requirements.
|
|
28
|
+
|
|
29
|
+
## Parse a Request
|
|
30
|
+
|
|
31
|
+
Before you start a chat, validate the request body. The parser bounds the input, checks required fields, and returns an immutable value.
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
require 'ruby_llm/ag_ui'
|
|
35
|
+
|
|
36
|
+
input = RubyLLM::AgUI::Input.parse({
|
|
37
|
+
'threadId' => 'thread-1',
|
|
38
|
+
'runId' => 'run-1',
|
|
39
|
+
'messages' => [{ 'id' => 'm1', 'role' => 'user', 'content' => 'Hello' }]
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
puts "#{input.thread_id}/#{input.run_id}"
|
|
43
|
+
# => thread-1/run-1
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The host still chooses the chat, model, tools, and authenticated scope. `Input` treats client fields as data and does not grant access. To see a complete run with an approval pause and resume, run `bundle exec rake example` from the repository; it uses a scripted chat and needs no provider key.
|
|
47
|
+
|
|
48
|
+
## Next Steps
|
|
49
|
+
|
|
50
|
+
* [How RubyLLM AG-UI Works]({% link _getting_started/overview.md %}) - the parts of one run.
|
|
51
|
+
* [Configuration]({% link _getting_started/configuration.md %}) - defaults and run stores.
|
|
52
|
+
* [Validating Input]({% link _core_features/input-validation.md %}) - request limits and strict parsing.
|