ruby_llm-browser 0.1.0 → 0.3.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/CHANGELOG.md +34 -0
- data/CONTRIBUTING.md +8 -1
- data/README.md +25 -96
- data/docs/API.md +5 -172
- data/docs/RAILS.md +5 -162
- data/docs/README.md +23 -12
- data/docs/RELEASE_ACTION_PLAN.md +28 -13
- data/docs/RELEASE_AUDIT.md +19 -0
- data/docs/RELEASING.md +24 -12
- data/docs/SECURITY_MODEL.md +1 -1
- data/docs/WORK.md +22 -0
- data/docs/_advanced/development-and-testing.md +107 -0
- data/docs/_advanced/live-backend-qualification.md +5 -0
- data/docs/_advanced/rails-integration.md +147 -29
- data/docs/_advanced/result-delivery.md +119 -0
- data/docs/_config.yml +8 -4
- data/docs/_core_features/diagnostics.md +68 -0
- data/docs/_core_features/inspect-extract.md +18 -2
- data/docs/_data/navigation.yml +2 -0
- data/docs/_getting_started/getting-started.md +11 -1
- data/docs/_getting_started/overview.md +3 -0
- data/docs/_getting_started/whats-new-in-0-3.md +48 -0
- data/docs/_plugins/versioned_docs.rb +3 -3
- data/docs/_reference/api-and-configuration.md +203 -0
- data/docs/_reference/api-reference.md +3 -2
- data/docs/_reference/release-history.md +3 -1
- data/docs/_reference/roadmap.md +171 -0
- data/docs/_reference/upgrading.md +48 -14
- data/docs/_reference/whats-new-in-0-2.md +46 -0
- data/docs/index.md +2 -3
- data/examples/plain_ruby.rb +7 -4
- data/examples/rails_results.rb +141 -0
- data/lib/ruby_llm/browser/doctor.rb +25 -10
- data/lib/ruby_llm/browser/rails/operation_job.rb +16 -4
- data/lib/ruby_llm/browser/rails/result_delivery_error.rb +22 -0
- data/lib/ruby_llm/browser/rails.rb +16 -0
- data/lib/ruby_llm/browser/result.rb +3 -3
- data/lib/ruby_llm/browser/version.rb +1 -1
- data/lib/ruby_llm/browser.rb +11 -0
- metadata +11 -2
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Roadmap to 1.0
|
|
4
|
+
nav_order: 8
|
|
5
|
+
description: Plan small releases toward a reliable read-only browser integration for RubyLLM.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# {{ page.title }}
|
|
9
|
+
|
|
10
|
+
{{ page.description }}
|
|
11
|
+
|
|
12
|
+
After reading this guide, you will know:
|
|
13
|
+
|
|
14
|
+
- What works today and what still blocks practical live use.
|
|
15
|
+
- Which small features belong in the next release.
|
|
16
|
+
- What must pass before the project releases 1.0.0.
|
|
17
|
+
|
|
18
|
+
## Start with a Narrow 1.0
|
|
19
|
+
|
|
20
|
+
The proposed 1.0 supports one complete workflow: your RubyLLM application inspects an
|
|
21
|
+
approved public page or extracts schema-checked data, receives a bounded result with
|
|
22
|
+
provenance, and can account for the browser session through cleanup. It works in plain
|
|
23
|
+
Ruby and through the optional Rails adapter.
|
|
24
|
+
|
|
25
|
+
Version 0.1.0 introduced the offline foundation: Inspect, Extract, URL policy, bounded
|
|
26
|
+
results, operation bookkeeping, Rails jobs, and Act simulation. It does not provide a
|
|
27
|
+
supported live backend. The current Stagehand adapter refuses live operations. The
|
|
28
|
+
0.3.0 release adds opt-in Rails result delivery to host-owned storage;
|
|
29
|
+
it does not add a gem-managed results database or guaranteed delivery.
|
|
30
|
+
|
|
31
|
+
**This is a proposed development sequence, not a list of available features or release
|
|
32
|
+
dates.** Future API names below are proposals. Keep each release small; use patch
|
|
33
|
+
releases for fixes and split a milestone if implementation reveals a larger change.
|
|
34
|
+
|
|
35
|
+
## Release Sequence
|
|
36
|
+
|
|
37
|
+
Build convenience on the current offline API first. Complete the Rails result path
|
|
38
|
+
next. Qualify one live deployment before advertising browser support, then use pilot
|
|
39
|
+
feedback to settle the public API.
|
|
40
|
+
|
|
41
|
+
| Version | Outcome | Release condition |
|
|
42
|
+
| --- | --- | --- |
|
|
43
|
+
| 0.2.0 | Easier tool setup and clearer offline diagnostics | Two additive features, tested with no provider requests |
|
|
44
|
+
| 0.3.0 | Usable Rails result delivery | A host can retrieve a job's result after the worker exits |
|
|
45
|
+
| 0.4.0 | One supported live read-only deployment | Egress, final URL, session expiry and cleanup qualified on that deployment |
|
|
46
|
+
| 0.5.0 | Pilot feedback and recovery hardening | Plain-Ruby and Rails workflows exercised outside the test suite |
|
|
47
|
+
| 1.0.0-rc.1 | Stable API candidate | Feature freeze, complete upgrade instructions, candidate package verified |
|
|
48
|
+
| 1.0.0 | Supported read-only integration | All acceptance criteria below pass on the release candidate |
|
|
49
|
+
|
|
50
|
+
### 0.2.0: Two Small Features
|
|
51
|
+
|
|
52
|
+
Version `0.2.0` adds both features below. Milestones after 0.3 remain proposed scope. See
|
|
53
|
+
[What's New]({% link _reference/whats-new-in-0-2.md %}) for usage and
|
|
54
|
+
[development and testing]({% link _advanced/development-and-testing.md %}) for validation.
|
|
55
|
+
|
|
56
|
+
1. Add the read-only tool factory `RubyLLM::Browser.tools(context:, schema:)`.
|
|
57
|
+
Return Inspect and Extract instances suitable for `chat.with_tools(*tools)`. Keep
|
|
58
|
+
the context and extraction schema explicit; never include Act. Reuse the existing
|
|
59
|
+
constructors and validation instead of creating a second configuration API.
|
|
60
|
+
2. Make Doctor distinguish offline and live-backend checks. FakeBackend diagnostics
|
|
61
|
+
should not warn that provider credentials are missing. Live-backend reports must
|
|
62
|
+
keep the unsupported Stagehand reason visible. Preserve the structured report and
|
|
63
|
+
make clear that a passing local check does not qualify a provider.
|
|
64
|
+
|
|
65
|
+
Acceptance: exercise the factory through RubyLLM's public tool-registration API on
|
|
66
|
+
both supported RubyLLM versions; reject an invalid schema; verify no Act registration,
|
|
67
|
+
provider request, credential discovery, or Rails load. Test Doctor with fake and
|
|
68
|
+
unsupported live backends. Update the relevant guides and changelog with the features.
|
|
69
|
+
|
|
70
|
+
Do not add a CLI, a context builder, new database tables, or live support in this slice.
|
|
71
|
+
The factory and Doctor are implemented in separate commits. Local and remote
|
|
72
|
+
validation precede the release tag and publication.
|
|
73
|
+
|
|
74
|
+
### 0.3.0: Complete the Rails Result Path
|
|
75
|
+
|
|
76
|
+
Version `0.3.0` implements `Rails.result_delivery`, typed
|
|
77
|
+
host-delivery failures, and a runnable SQLite host example with separate workers.
|
|
78
|
+
Read [What's New in 0.3]({% link _getting_started/whats-new-in-0-3.md %}) and
|
|
79
|
+
[Rails Result Delivery]({% link _advanced/result-delivery.md %}) for the current API.
|
|
80
|
+
The source repository's `docs/evidence/V0_3_VALIDATION.md` records local candidate
|
|
81
|
+
checks. `docs/evidence/V0_3_RELEASE.md` records remote verification and publication.
|
|
82
|
+
|
|
83
|
+
The result-delivery hook lets the host persist the sanitized `Result`
|
|
84
|
+
against its operation ID. Its runnable host example stores and retrieves
|
|
85
|
+
the result after an asynchronous job finishes. The host owns access checks, retention,
|
|
86
|
+
and storage; the gem does not need an admin interface or a general results database.
|
|
87
|
+
|
|
88
|
+
Acceptance: retrieve a schema-checked extraction after worker exit; refuse cross-actor
|
|
89
|
+
reads in the host example; handle success, truncation, failure, and pending cleanup.
|
|
90
|
+
A delivery failure must be visible and must never cause the browser operation to run
|
|
91
|
+
again. Document delivery semantics, duplicate handling, and recovery separately from
|
|
92
|
+
the operation's session state. Verify generator upgrades preserve existing records.
|
|
93
|
+
|
|
94
|
+
### 0.4.0: Qualify One Live Read-only Deployment
|
|
95
|
+
|
|
96
|
+
Begin with a feasibility check of the pinned SDK and a named deployment. Determine
|
|
97
|
+
whether the deployment can enforce every required browser request boundary and report
|
|
98
|
+
verified final URLs and terminal session state. Implement only the missing adapter
|
|
99
|
+
integration needed for that deployment. Keep live Act disabled.
|
|
100
|
+
|
|
101
|
+
Acceptance requires deployment-specific evidence for redirects, subresources, frames,
|
|
102
|
+
popups, WebSockets, connection-time DNS/IP restrictions, final URLs, session expiry,
|
|
103
|
+
and confirmed cleanup. Exercise timeouts, worker death, and lost start responses with
|
|
104
|
+
controlled fixtures. Unknown sessions retain quota and have an explicit host review
|
|
105
|
+
path; an SDK acknowledgement alone cannot release them.
|
|
106
|
+
|
|
107
|
+
Prepare the fixtures and opt-in qualification runner offline first. Live runs require
|
|
108
|
+
separate owner authorization for the deployment, data, and spending. Keep default CI
|
|
109
|
+
offline. Report provider billing controls separately from the advisory cost field.
|
|
110
|
+
|
|
111
|
+
If the deployment cannot meet the required controls, stop this milestone and record
|
|
112
|
+
the gap. Keep live support disabled. Do not replace qualification with a boolean flag
|
|
113
|
+
or promote an offline-only package to 1.0 under this plan.
|
|
114
|
+
|
|
115
|
+
### 0.5.0: Exercise Real Application Workflows
|
|
116
|
+
|
|
117
|
+
Run two authorized pilots: a plain-Ruby extraction and a Rails background extraction
|
|
118
|
+
with durable host result retrieval. Record the exact dependency/deployment versions,
|
|
119
|
+
fixture expectations, observed results, failures, and cleanup outcomes. A controlled
|
|
120
|
+
host application is sufficient; customer adoption is not a release prerequisite.
|
|
121
|
+
|
|
122
|
+
Use pilot findings to improve errors, delivery, cancellation, and recovery. Confirm
|
|
123
|
+
that duplicate jobs do not repeat provider starts, unknown outcomes remain visible,
|
|
124
|
+
and result limits and redaction behave as documented. Resolve public API changes now
|
|
125
|
+
and provide migrations before freezing the API.
|
|
126
|
+
|
|
127
|
+
### 1.0.0-rc.1: Freeze and Verify
|
|
128
|
+
|
|
129
|
+
Freeze tool constructors, factory arguments, result fields, error codes, backend/store
|
|
130
|
+
contracts, and Rails schema/delivery hooks. Publish migration instructions for every
|
|
131
|
+
breaking pre-1.0 change. Run the declared Ruby/RubyLLM/Rails matrix, build the gem,
|
|
132
|
+
install the actual artifact in a clean environment, and verify its shipped examples,
|
|
133
|
+
generator, guides, and dependency audit.
|
|
134
|
+
|
|
135
|
+
Repeat the live qualification against the candidate's exact artifact and deployment.
|
|
136
|
+
Fix defects through further release candidates. Feature requests return to a later
|
|
137
|
+
minor release. A candidate with an unresolved blocker does not advance to 1.0.0.
|
|
138
|
+
|
|
139
|
+
## 1.0.0 Acceptance Criteria
|
|
140
|
+
|
|
141
|
+
- **Useful:** both pilot workflows complete, and one named live read-only deployment
|
|
142
|
+
is supported with reproducible evidence and documented limits.
|
|
143
|
+
- **Controlled:** URL policy, schema validation, result bounds, and redaction pass
|
|
144
|
+
positive and denial cases. Deployment qualification covers all browser request paths.
|
|
145
|
+
- **Recoverable:** cancellation, duplicate jobs, worker death, unknown starts, and
|
|
146
|
+
pending cleanup preserve correct state and quota. Host recovery steps are tested.
|
|
147
|
+
- **Retrievable:** a Rails host can persist and authorize result retrieval after a job
|
|
148
|
+
exits. Delivery failure cannot trigger a repeated browser action.
|
|
149
|
+
- **Stable:** the public API and database upgrade path are documented and covered by
|
|
150
|
+
compatibility checks. Unsupported databases and backends are identified explicitly.
|
|
151
|
+
- **Distributable:** the release artifact passes the compatibility matrix, clean
|
|
152
|
+
installation, examples, documentation build, and dependency checks. Version, tag,
|
|
153
|
+
changelog, and stable documentation agree. The owner authorizes publication.
|
|
154
|
+
|
|
155
|
+
Live Act, multiple providers, PostgreSQL qualification, authenticated browsing,
|
|
156
|
+
screenshots, downloads, arbitrary JavaScript, a hosted UI, and a cost ledger are not
|
|
157
|
+
required for this 1.0. Keep unsupported capabilities explicit. Consider live Act only
|
|
158
|
+
as a separate feature with same-session approval and target identity guarantees.
|
|
159
|
+
|
|
160
|
+
## Keep Planning and Documentation Separate
|
|
161
|
+
|
|
162
|
+
The README introduces the project. Site guides teach supported behavior and provide
|
|
163
|
+
the API reference. This page owns future scope; the changelog records shipped changes.
|
|
164
|
+
Historical release audits remain evidence for their exact versions, not a current
|
|
165
|
+
task queue. Plan changes do not rewrite the approved specification baseline.
|
|
166
|
+
|
|
167
|
+
Read [API and configuration]({% link _reference/api-and-configuration.md %}),
|
|
168
|
+
[Rails jobs and recovery]({% link _advanced/rails-integration.md %}), and
|
|
169
|
+
[live-backend qualification]({% link _advanced/live-backend-qualification.md %}) for
|
|
170
|
+
the current contracts. Follow [the release checklist]({% link RELEASING.md %}) when a
|
|
171
|
+
milestone is ready to ship.
|
|
@@ -2,32 +2,66 @@
|
|
|
2
2
|
layout: default
|
|
3
3
|
title: Upgrading
|
|
4
4
|
nav_order: 2
|
|
5
|
-
description:
|
|
5
|
+
description: Upgrade from 0.2.0 while preserving operation data and configuring optional Rails result delivery.
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# {{ page.title }}
|
|
9
9
|
|
|
10
10
|
{{ page.description }}
|
|
11
|
-
{: .fs-6 .fw-300 }
|
|
12
11
|
|
|
13
12
|
After reading this guide, you will know:
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
- How to update an application from 0.2.0.
|
|
15
|
+
- How to preserve existing data and enable result delivery.
|
|
16
|
+
- Which result and worker behaviors change.
|
|
18
17
|
|
|
19
|
-
##
|
|
18
|
+
## Upgrade from 0.2.0
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
to migrate from. Install it with `bundle add ruby_llm-browser --version "~> {{ site.version }}"`.
|
|
20
|
+
Update your Gemfile and bundle:
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
```ruby
|
|
23
|
+
gem "ruby_llm-browser", "~> {{ site.version }}"
|
|
24
|
+
```
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
```sh
|
|
27
|
+
bundle update ruby_llm-browser
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This release adds no gem-managed database migration or runtime dependency. Preserve all
|
|
31
|
+
operation, approval, and actor-lock rows. Do not rerun the install generator on an
|
|
32
|
+
existing installation. Result storage belongs to your application.
|
|
33
|
+
|
|
34
|
+
## Enable Result Delivery When Needed
|
|
35
|
+
|
|
36
|
+
`RubyLLM::Browser::Rails.result_delivery` defaults to `nil`. Existing job return values
|
|
37
|
+
remain supported. To retain results after a worker exits, follow
|
|
38
|
+
[Rails Result Delivery]({% link _advanced/result-delivery.md %}). Configure persistence,
|
|
39
|
+
authorized reads, idempotent writes, and recovery before enabling the callback.
|
|
40
|
+
|
|
41
|
+
A callback failure raises `Rails::ResultDeliveryError`. Queue retries do not redeliver
|
|
42
|
+
a result. The error retains the result only in process; it is not durable storage.
|
|
43
|
+
Recover delivery separately from browser execution. A crash before host persistence
|
|
44
|
+
can still lose the result, so do not treat this hook as guaranteed delivery.
|
|
45
|
+
|
|
46
|
+
## Account for Immutable Results and Fenced Workers
|
|
47
|
+
|
|
48
|
+
`Result` now also copies and freezes operation ID, error code and error message strings,
|
|
49
|
+
consistent with its other fields. Mutating these attributes raises `FrozenError`;
|
|
50
|
+
duplicate a string before changing it in host code. Caller-owned inputs stay mutable.
|
|
51
|
+
|
|
52
|
+
A worker fenced during browser execution now returns `:fenced_out` and cannot deliver
|
|
53
|
+
its stale result. Cancelled work never invokes the delivery hook, including while
|
|
54
|
+
cleanup remains pending. Delivery failures do not change the session state or free
|
|
55
|
+
quota. Stagehand and live Act remain unsupported.
|
|
56
|
+
|
|
57
|
+
## Earlier Versions
|
|
58
|
+
|
|
59
|
+
For upgrades from 0.1.0 and earlier development snapshots, first follow the
|
|
60
|
+
[0.2.0 upgrading guide](https://github.com/yorzi/ruby_llm-browser/blob/v0.2.0/docs/_reference/upgrading.md).
|
|
61
|
+
Preserve operation data and use additive migrations where required.
|
|
29
62
|
|
|
30
63
|
## Next Steps
|
|
31
64
|
|
|
32
|
-
|
|
33
|
-
|
|
65
|
+
- Read [What's New in 0.3]({% link _getting_started/whats-new-in-0-3.md %}).
|
|
66
|
+
- Review [release history]({% link _reference/release-history.md %}) and
|
|
67
|
+
[live-backend qualification]({% link _advanced/live-backend-qualification.md %}).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: What's New in 0.2
|
|
4
|
+
parent: Release History
|
|
5
|
+
nav_order: 1
|
|
6
|
+
description: Use the read-only tool factory and backend-aware diagnostics in this release.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# {{ page.title }}
|
|
10
|
+
|
|
11
|
+
{{ page.description }}
|
|
12
|
+
|
|
13
|
+
After reading this guide, you will know:
|
|
14
|
+
|
|
15
|
+
- How to register the read-only tool pair.
|
|
16
|
+
- How to distinguish offline diagnostics from live-backend checks.
|
|
17
|
+
- Where to verify changes locally before upgrading.
|
|
18
|
+
|
|
19
|
+
## Read-only Tool Factory
|
|
20
|
+
|
|
21
|
+
You can register the read-only tools in one call:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
chat.with_tools(*RubyLLM::Browser.tools(context: context, schema: schema))
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The host still supplies context and schema. The factory returns Inspect and Extract
|
|
28
|
+
instances and never registers Act. Individual constructors remain supported. Read
|
|
29
|
+
[Inspecting and extracting]({% link _core_features/inspect-extract.md %}) for a complete
|
|
30
|
+
offline example and the validation behavior.
|
|
31
|
+
|
|
32
|
+
## Backend-aware Diagnostics
|
|
33
|
+
|
|
34
|
+
Doctor adds a `mode` field while preserving `ok`, `summary`, and `checks`. FakeBackend
|
|
35
|
+
reports `offline` mode and skips provider environment and SDK checks. Stagehand
|
|
36
|
+
reports `live` mode and remains blocked. Custom backends no longer receive unrelated
|
|
37
|
+
Stagehand warnings. Read [Checking Configuration]({% link _core_features/diagnostics.md %}).
|
|
38
|
+
|
|
39
|
+
## Development Verification
|
|
40
|
+
|
|
41
|
+
Use `bin/check` for offline contracts, lint, alternate bundles, and examples. Use
|
|
42
|
+
`bin/check-browser` for desktop/mobile workflows and fixture screenshots. The
|
|
43
|
+
[development guide]({% link _advanced/development-and-testing.md %}) explains setup,
|
|
44
|
+
package verification, and the limits of this evidence.
|
|
45
|
+
|
|
46
|
+
Stagehand and live Act remain unsupported. This release remains offline-only.
|
data/docs/index.md
CHANGED
|
@@ -35,7 +35,7 @@ features:
|
|
|
35
35
|
|
|
36
36
|
## Before You Install
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
Version `{{ site.version }}` supports only the offline `FakeBackend`. The built-in
|
|
39
39
|
Stagehand adapter is unsupported until provider egress and confirmed cleanup are
|
|
40
40
|
qualified. The full live Act protocol is deferred to a later version.
|
|
41
41
|
|
|
@@ -70,8 +70,7 @@ schema = {
|
|
|
70
70
|
"required" => [ "title" ], "additionalProperties" => false
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
inspect_tool = RubyLLM::Browser
|
|
74
|
-
extract_tool = RubyLLM::Browser::Extract.new(context: context, schema: schema)
|
|
73
|
+
inspect_tool, extract_tool = RubyLLM::Browser.tools(context: context, schema: schema)
|
|
75
74
|
puts inspect_tool.execute(url: "https://docs.example.com/")[:status] # => ok
|
|
76
75
|
puts extract_tool.execute(url: "https://docs.example.com/")[:data] # => {"title"=>"Example"}
|
|
77
76
|
```
|
data/examples/plain_ruby.rb
CHANGED
|
@@ -23,12 +23,15 @@ context = RubyLLM::Browser::Context.new(
|
|
|
23
23
|
budget: RubyLLM::Browser::Budget.new(max_seconds: 30, max_steps: 6)
|
|
24
24
|
)
|
|
25
25
|
|
|
26
|
-
inspect = RubyLLM::Browser::Inspect.new(context: context)
|
|
27
|
-
puts "Inspect: #{inspect.execute(url: "https://docs.example.com/")}"
|
|
28
|
-
|
|
29
26
|
schema = { "type" => "object", "properties" => { "title" => { "type" => "string" } } }
|
|
30
|
-
extract = RubyLLM::Browser
|
|
27
|
+
inspect, extract = RubyLLM::Browser.tools(context: context, schema: schema)
|
|
28
|
+
puts "Inspect: #{inspect.execute(url: "https://docs.example.com/")}"
|
|
31
29
|
puts "Extract: #{extract.execute(url: "https://docs.example.com/", instruction: "the title")}"
|
|
32
30
|
|
|
33
31
|
blocked = inspect.execute(url: "http://169.254.169.254/latest/meta-data")
|
|
34
32
|
puts "Blocked: #{blocked[:error][:code]}"
|
|
33
|
+
|
|
34
|
+
report = RubyLLM::Browser::Doctor.new(backend: context.backend).report
|
|
35
|
+
raise "offline diagnostic failed" unless report[:ok] && report[:mode] == :offline
|
|
36
|
+
raise "provider checks ran offline" if report[:checks].any? { |check| check[:name] == :credentials }
|
|
37
|
+
puts "Doctor: #{report[:mode]}"
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A host-owned result table and a single-job file queue, using only FakeBackend.
|
|
4
|
+
# This disposable demonstration is not a production queue, controller or migration.
|
|
5
|
+
require "ruby_llm/browser/rails"
|
|
6
|
+
require "logger"
|
|
7
|
+
require "open3"
|
|
8
|
+
require "tmpdir"
|
|
9
|
+
|
|
10
|
+
module BrowserResultsExample
|
|
11
|
+
SCHEMA = { "type" => "object", "properties" => { "title" => { "type" => "string" } },
|
|
12
|
+
"required" => [ "title" ], "additionalProperties" => false }.freeze
|
|
13
|
+
RESOLVER = Object.new
|
|
14
|
+
def RESOLVER.getaddresses(_host) = [ "93.184.216.34" ]
|
|
15
|
+
|
|
16
|
+
class ResultRecord < ActiveRecord::Base
|
|
17
|
+
self.table_name = "example_browser_results"
|
|
18
|
+
self.primary_key = "operation_id"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class FileQueue
|
|
22
|
+
def initialize(path) = @path = path
|
|
23
|
+
def enqueue(job) = File.write(@path, JSON.generate(job.serialize))
|
|
24
|
+
def enqueue_at(*) = raise(ArgumentError, "demo queue supports immediate jobs only")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
def connect(database)
|
|
30
|
+
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: database, timeout: 5000)
|
|
31
|
+
ActiveRecord::Base.logger = Logger.new(IO::NULL)
|
|
32
|
+
ActiveJob::Base.logger = Logger.new(IO::NULL)
|
|
33
|
+
RubyLLM::Browser::Rails::Schema.create!
|
|
34
|
+
connection = ActiveRecord::Base.connection
|
|
35
|
+
return if connection.table_exists?(ResultRecord.table_name)
|
|
36
|
+
|
|
37
|
+
connection.create_table(ResultRecord.table_name, id: false) do |table|
|
|
38
|
+
table.string :operation_id, primary_key: true, null: false
|
|
39
|
+
table.string :actor_id, null: false
|
|
40
|
+
table.string :run_id, null: false
|
|
41
|
+
table.text :body, null: false
|
|
42
|
+
table.datetime :created_at, null: false
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def configure(backend: RubyLLM::Browser::FakeBackend.new)
|
|
47
|
+
RubyLLM::Browser::Rails.context_factory = lambda do |actor_id:, run_id:|
|
|
48
|
+
RubyLLM::Browser::Context.new(
|
|
49
|
+
actor_id: actor_id, run_id: run_id, backend: backend,
|
|
50
|
+
source_policy: RubyLLM::Browser::Policy.new(allowed_hosts: [ "docs.example.com" ], resolver: RESOLVER),
|
|
51
|
+
operation_store: RubyLLM::Browser::Rails.operation_store
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
RubyLLM::Browser::Rails.result_delivery = lambda do |operation:, result:|
|
|
55
|
+
store_result(operation: operation, payload: result.to_h)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Trusted server API. The worker supplies identity, never a model or request body.
|
|
60
|
+
def store_result(operation:, payload:)
|
|
61
|
+
body = RubyLLM::Browser::Redactor.canonical_json(payload)
|
|
62
|
+
ResultRecord.transaction do
|
|
63
|
+
record = ResultRecord.create_or_find_by!(operation_id: operation.id) do |row|
|
|
64
|
+
row.actor_id = operation.actor_id
|
|
65
|
+
row.run_id = operation.run_id
|
|
66
|
+
row.body = body
|
|
67
|
+
end
|
|
68
|
+
unless [ record.actor_id, record.run_id, record.body ] == [ operation.actor_id, operation.run_id, body ]
|
|
69
|
+
raise RubyLLM::Browser::InvalidInput, "conflicting result delivery"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# In an HTTP host, derive actor/run from authenticated records and recheck access.
|
|
75
|
+
# Use the same response for a missing result and an inaccessible result.
|
|
76
|
+
def read_result(operation_id:, actor_id:, run_id:)
|
|
77
|
+
record = ResultRecord.find_by(operation_id: operation_id, actor_id: actor_id, run_id: run_id)
|
|
78
|
+
raise RubyLLM::Browser::NotAuthorized, "result unavailable" unless record
|
|
79
|
+
|
|
80
|
+
JSON.parse(record.body)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def worker(database, queue_file)
|
|
84
|
+
connect(database)
|
|
85
|
+
backend = RubyLLM::Browser::FakeBackend.new
|
|
86
|
+
configure(backend: backend)
|
|
87
|
+
ActiveJob::Base.execute(JSON.parse(File.read(queue_file)))
|
|
88
|
+
puts JSON.generate(starts: backend.call_count(:start), closes: backend.call_count(:end_session))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def run_demo
|
|
92
|
+
Dir.mktmpdir("browser-results-") do |directory|
|
|
93
|
+
database = File.join(directory, "host.sqlite3")
|
|
94
|
+
queue_file = File.join(directory, "job.json")
|
|
95
|
+
connect(database)
|
|
96
|
+
ActiveJob::Base.queue_adapter = FileQueue.new(queue_file)
|
|
97
|
+
operation = RubyLLM::Browser::Rails::OperationJob.enqueue(
|
|
98
|
+
actor_id: "demo-actor", run_id: "demo-run", kind: :extract,
|
|
99
|
+
url: "https://docs.example.com/", schema: SCHEMA,
|
|
100
|
+
budget: RubyLLM::Browser::Budget.new, idempotency_key: "demo-result"
|
|
101
|
+
)
|
|
102
|
+
# capture3 waits for exit. Execute the serialized job twice in separate workers.
|
|
103
|
+
[ 1, 0 ].each do |expected_starts|
|
|
104
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, "-I", File.expand_path("../lib", __dir__),
|
|
105
|
+
__FILE__, "worker", database, queue_file)
|
|
106
|
+
raise "worker failed: #{stderr}" unless status.success?
|
|
107
|
+
unless JSON.parse(stdout) == { "starts" => expected_starts, "closes" => expected_starts }
|
|
108
|
+
raise "unexpected browser dispatch"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
payload = read_result(operation_id: operation.id, actor_id: "demo-actor", run_id: "demo-run")
|
|
113
|
+
raise "lost extraction" unless payload.fetch("data") == { "title" => "Example" } && payload.fetch("status") == "ok"
|
|
114
|
+
raise "lost provenance" unless payload.fetch("locator").fetch("schema_digest") == RubyLLM::Browser::Redactor.digest(SCHEMA)
|
|
115
|
+
puts "RESULT_AFTER_WORKER_EXIT_PASS"
|
|
116
|
+
|
|
117
|
+
[ [ "other-actor", "demo-run" ], [ "demo-actor", "other-run" ] ].each do |actor, run|
|
|
118
|
+
begin
|
|
119
|
+
read_result(operation_id: operation.id, actor_id: actor, run_id: run)
|
|
120
|
+
raise "cross-scope result leak"
|
|
121
|
+
rescue RubyLLM::Browser::NotAuthorized
|
|
122
|
+
# Expected refusal, including after the worker has exited.
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
puts "SCOPED_READS_PASS"
|
|
126
|
+
2.times { store_result(operation: operation, payload: payload) }
|
|
127
|
+
raise "duplicate result rows" unless ResultRecord.count == 1
|
|
128
|
+
puts "IDEMPOTENT_WRITE_PASS"
|
|
129
|
+
ensure
|
|
130
|
+
ActiveRecord::Base.connection_pool.disconnect!
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
if $PROGRAM_NAME == __FILE__
|
|
136
|
+
if ARGV.first == "worker"
|
|
137
|
+
BrowserResultsExample.worker(*ARGV.drop(1))
|
|
138
|
+
else
|
|
139
|
+
BrowserResultsExample.run_demo
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module RubyLLM
|
|
4
4
|
module Browser
|
|
5
|
-
# Read-only
|
|
5
|
+
# Read-only configuration diagnostics for an offline or live backend.
|
|
6
6
|
#
|
|
7
7
|
# The Doctor never makes a network call, never starts a session and never prints a
|
|
8
|
-
# credential.
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
# credential. Common checks report RubyLLM compatibility and backend capabilities.
|
|
9
|
+
# Only Stagehand backends inspect Stagehand's SDK and environment. FakeBackend
|
|
10
|
+
# checks do not load the SDK or inspect provider credentials. A local pass never
|
|
11
|
+
# qualifies a provider's actual guarantees. A `:fail` check blocks use.
|
|
11
12
|
#
|
|
12
13
|
# report = RubyLLM::Browser::Doctor.new(backend: backend).report
|
|
13
14
|
# raise report[:summary] unless report[:ok]
|
|
@@ -23,13 +24,15 @@ module RubyLLM
|
|
|
23
24
|
@env = env
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
# @return [Hash] `{ ok:, summary:, checks: [...] }`, safe to log
|
|
27
|
+
# @return [Hash] `{ mode:, ok:, summary:, checks: [...] }`, safe to log.
|
|
28
|
+
# Mode is `:offline` for FakeBackend in fake mode, `:unconfigured` without
|
|
29
|
+
# a backend, and `:live` otherwise. Existing report keys remain available.
|
|
27
30
|
def report
|
|
28
|
-
checks = [
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
].compact
|
|
31
|
+
checks = [ ruby_llm_check, egress_check, local_binary_check, custom_headers_check ]
|
|
32
|
+
checks.concat([ stagehand_check, base_url_check, credentials_check ]) if backend.is_a?(StagehandBackend)
|
|
33
|
+
checks.compact!
|
|
32
34
|
{
|
|
35
|
+
mode: mode,
|
|
33
36
|
ok: checks.none? { |check| check[:status] == :fail },
|
|
34
37
|
summary: summary_for(checks),
|
|
35
38
|
checks: checks
|
|
@@ -40,6 +43,13 @@ module RubyLLM
|
|
|
40
43
|
|
|
41
44
|
private
|
|
42
45
|
|
|
46
|
+
def mode
|
|
47
|
+
return :unconfigured unless backend
|
|
48
|
+
return :offline if backend.is_a?(FakeBackend) && backend.capabilities.server_mode == :fake
|
|
49
|
+
|
|
50
|
+
:live
|
|
51
|
+
end
|
|
52
|
+
|
|
43
53
|
def ruby_llm_check
|
|
44
54
|
version = Gem::Version.new(RubyLLM::VERSION)
|
|
45
55
|
required = Gem::Version.new("2.0.0")
|
|
@@ -72,7 +82,12 @@ module RubyLLM
|
|
|
72
82
|
"Stagehand backend is unsupported until provider egress and cleanup are qualified")
|
|
73
83
|
end
|
|
74
84
|
if capabilities.enforced_egress?
|
|
75
|
-
|
|
85
|
+
detail = if mode == :offline
|
|
86
|
+
"FakeBackend performs no network I/O; live egress is not verified"
|
|
87
|
+
else
|
|
88
|
+
"the backend asserts enforced, browser-side egress isolation"
|
|
89
|
+
end
|
|
90
|
+
check(:egress_isolation, :pass, detail)
|
|
76
91
|
else
|
|
77
92
|
check(:egress_isolation, :fail,
|
|
78
93
|
"the backend does not assert enforced egress isolation; live sessions are refused")
|
|
@@ -91,8 +91,14 @@ module RubyLLM
|
|
|
91
91
|
max_bytes: max_bytes)
|
|
92
92
|
|
|
93
93
|
# Keep unknown action/start results, even when cleanup later closes a session.
|
|
94
|
-
|
|
94
|
+
completed = store.fetch!(operation_id)
|
|
95
|
+
final = settled(completed)
|
|
95
96
|
return :cancelled if final == :cancelled
|
|
97
|
+
# Cancellation can still await cleanup. A reaper or newer worker can also
|
|
98
|
+
# fence this job while the browser call is in flight.
|
|
99
|
+
return :fenced_out unless completed.cancel_requested? || completed.fence == leased.fence
|
|
100
|
+
|
|
101
|
+
deliver(completed, result) unless completed.cancel_requested?
|
|
96
102
|
|
|
97
103
|
result.ok? ? (final || result.to_h) : result.to_h
|
|
98
104
|
rescue InvalidInput => e
|
|
@@ -106,15 +112,21 @@ module RubyLLM
|
|
|
106
112
|
# Reports the durable state if the host cancelled or another worker fenced this
|
|
107
113
|
# one while the operation was running; otherwise returns nil so the engine's own
|
|
108
114
|
# result is used.
|
|
109
|
-
def settled(
|
|
110
|
-
|
|
111
|
-
case final&.state
|
|
115
|
+
def settled(operation)
|
|
116
|
+
case operation.state
|
|
112
117
|
when :cancelled then :cancelled
|
|
113
118
|
when :cleanup_pending then :cleanup_pending
|
|
114
119
|
when :unknown then :unknown
|
|
115
120
|
end
|
|
116
121
|
end
|
|
117
122
|
|
|
123
|
+
def deliver(operation, result)
|
|
124
|
+
RubyLLM::Browser::Rails.result_delivery&.call(operation: operation, result: result)
|
|
125
|
+
rescue StandardError
|
|
126
|
+
# Queue logs may print chained exceptions containing host credentials.
|
|
127
|
+
raise ResultDeliveryError.new(operation: operation, result: result), cause: nil
|
|
128
|
+
end
|
|
129
|
+
|
|
118
130
|
def run(kind, operation, url:, instruction:, schema:, max_bytes:)
|
|
119
131
|
context = build_context(operation)
|
|
120
132
|
unless context.is_a?(Context) && context.operation_store.equal?(store)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Browser
|
|
5
|
+
module Rails
|
|
6
|
+
# The host callback failed after browser execution and cleanup were attempted.
|
|
7
|
+
# Retain these values in host storage to retry delivery without rerunning a job.
|
|
8
|
+
# These are in-process objects, not a durable queue or automatic retry policy.
|
|
9
|
+
class ResultDeliveryError < Error
|
|
10
|
+
CODE = "RESULT_DELIVERY_FAILED"
|
|
11
|
+
|
|
12
|
+
attr_reader :operation, :result
|
|
13
|
+
|
|
14
|
+
def initialize(operation:, result:)
|
|
15
|
+
@operation = operation
|
|
16
|
+
@result = result
|
|
17
|
+
super("host result delivery failed; retry delivery without repeating browser work")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -27,6 +27,7 @@ require_relative "rails/operation_record"
|
|
|
27
27
|
require_relative "rails/approval_record"
|
|
28
28
|
require_relative "rails/active_record_operation_store"
|
|
29
29
|
require_relative "rails/active_record_approval_store"
|
|
30
|
+
require_relative "rails/result_delivery_error"
|
|
30
31
|
require_relative "rails/operation_job"
|
|
31
32
|
require_relative "rails/reaper"
|
|
32
33
|
|
|
@@ -41,6 +42,20 @@ module RubyLLM
|
|
|
41
42
|
# ->(actor_id:, run_id:) { RubyLLM::Browser::Context }, supplied by the host.
|
|
42
43
|
attr_accessor :context_factory
|
|
43
44
|
|
|
45
|
+
# Optional host callback: ->(operation:, result:) { persist_result }.
|
|
46
|
+
# Runs synchronously after execution and cleanup, before the job returns.
|
|
47
|
+
# Raise to report failure; return values are ignored. Duplicate jobs do not
|
|
48
|
+
# redeliver. The host owns persistence, idempotency, access and recovery.
|
|
49
|
+
attr_reader :result_delivery
|
|
50
|
+
|
|
51
|
+
def result_delivery=(callback)
|
|
52
|
+
unless callback.nil? || callback.respond_to?(:call)
|
|
53
|
+
raise InvalidInput, "result_delivery must be callable or nil"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
@result_delivery = callback
|
|
57
|
+
end
|
|
58
|
+
|
|
44
59
|
# Returns the current UTC time used by the adapter.
|
|
45
60
|
def clock
|
|
46
61
|
@clock ||= -> { Time.now.utc }
|
|
@@ -68,6 +83,7 @@ module RubyLLM
|
|
|
68
83
|
# Restores process defaults. Primarily a test helper.
|
|
69
84
|
def reset!
|
|
70
85
|
@context_factory = nil
|
|
86
|
+
@result_delivery = nil
|
|
71
87
|
@clock = nil
|
|
72
88
|
@job_queue = nil
|
|
73
89
|
@operation_store = nil
|