Skip to content

Commit 29e6379

Browse files
authored
Merge pull request #357 from quarto-dev/feature/hub-execution-provider
Remote code-execution provider for hub sessions (bd-sfet3264)
2 parents cd6539e + 572e698 commit 29e6379

75 files changed

Lines changed: 8863 additions & 60 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 72 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
# End-to-end test: run code from hub-client via a `q2` executor
2+
3+
A hands-on test of the remote code-execution feature (bd-sfet3264, Phases
4+
4a + 4b): a connected `q2` process executes the document's code and streams the
5+
output back into the preview — the path a real collaborator would use.
6+
7+
> **`q2 provide-hub` is consent-gated and one-shot by default (bd-9lgiulr4).**
8+
> Every execution requires you to accept an interactive prompt that shows the
9+
> *resolved document* (the post-include QMD the engine will receive). Two modes:
10+
> - **one-shot (default):** `q2 provide-hub --file <doc> …` connects, executes
11+
> that one document once (after you accept), pushes the result, and exits.
12+
> - **`--watch`:** stay online and serve the editor's **Run** button until
13+
> Ctrl-C (each request still consent-gated).
14+
>
15+
> Add `--dangerously-accept-requests` to skip the prompt (unattended — only in a
16+
> fully-trusted session). A non-interactive stdin refuses to execute (fail-safe).
17+
> The old `--allow-all` flag is gone.
18+
19+
You run hub-client locally (`npm run dev`) plus a `q2 provide-hub` executor.
20+
Both talk to an automerge **sync server**, which stores and relays the project
21+
docs (index, files, captures) and the ephemeral run requests/beacons. There are
22+
two ways to provide that sync server — pick one:
23+
24+
```
25+
┌───────────────┐ automerge sync (index+file+capture docs, ephemeral msgs)
26+
│ hub-client │◄─────────────────────┐
27+
│ (npm run dev) │ │
28+
│ Run button ──┼──── exec/request ─────┤ ┌──────────────────────┐
29+
│ shows output │◄─── capture doc ──────┤◄─►│ sync server │
30+
└───────────────┘ │ │ (A: sync.automerge.org
31+
┌──────────────────────┐│ │ B: local q2 hub) │
32+
│ q2 provide-hub ├┘ └──────────────────────┘
33+
│ (consent-gated) │ runs engines, writes captures
34+
└──────────────────────┘
35+
```
36+
37+
## Option A — public sync server (simplest; verified)
38+
39+
Uses `wss://sync.automerge.org` (hub-client's built-in default) as the sync
40+
server. **No `q2 hub` needed** — the public server stores and relays every doc
41+
any peer creates, so a project you make in hub-client is visible to the
42+
provider. Requires internet, and note that **your document text + code are
43+
pushed to a public server** (fine for throwaway test code; use Option B if you
44+
care).
45+
46+
### Prerequisites
47+
48+
- A built `q2` (the commands use `cargo run`, which builds on demand).
49+
- `npm install` from the **repo root**.
50+
- **A real engine matching the document:** `engine: jupyter` needs
51+
`python3` + `jupyter` with a `python3` kernel; `engine: knitr` needs `R`.
52+
- **Build the hub-client WASM once** (the dev server doesn't):
53+
`cd hub-client && npm run build:wasm`. Without it the preview won't render.
54+
55+
### Steps
56+
57+
1. **hub-client** (terminal 1), on its default sync server:
58+
```bash
59+
cd hub-client
60+
npm run dev # do NOT set VITE_DEFAULT_SYNC_SERVER
61+
```
62+
Open http://localhost:5173, **create a new project**, and add a document
63+
with an executable cell — front matter **must** declare an engine:
64+
```
65+
---
66+
title: demo
67+
engine: knitr # or: jupyter
68+
---
69+
70+
```{r}
71+
cat(1, 2, 3)
72+
```
73+
```
74+
> **The `engine:` key is required.** Without it the cell renders as source,
75+
> no Run button appears, and nothing executes.
76+
77+
2. Get the project's **index-document id**: click **Share** in hub-client; the
78+
id is the part after `#/share/` in the URL it shows. (Shortcut: you can pass
79+
the *whole* Share URL to the provider — it extracts the id.)
80+
81+
3. **provider** (terminal 2) — to use the editor's **Run** button you want
82+
`--watch`:
83+
```bash
84+
# from the repo root
85+
cargo run --bin q2 -- provide-hub --watch --server wss://sync.automerge.org --token dev <indexDocId>
86+
```
87+
- `--token dev` skips the interactive OAuth bridge (the public server ignores
88+
the bearer). **Local testing only.**
89+
- `--watch` stays online and serves requests. Each request pauses for you to
90+
**review the resolved document and accept** at the terminal (or add
91+
`--dangerously-accept-requests` to auto-accept). Without `--watch` the
92+
command is one-shot and needs `--file <doc>` instead.
93+
94+
You should see `Connected. N file(s)…` (N > 0) and `Watching for execution
95+
requests…`.
96+
97+
4. In the browser: open the document, switch to the **preview**, and click
98+
**Run**. Back in terminal 2, review the shown resolved-document path and
99+
press **1** to accept; within a moment the preview shows the executed output
100+
in place of the source. Edit the cell and **Re-run** to see it update; a
101+
staleness note appears when the code changed since the last run.
102+
103+
To instead push a result **without** the Run button (one-shot), run:
104+
```bash
105+
cargo run --bin q2 -- provide-hub --file <doc>.qmd --server wss://sync.automerge.org --token dev <indexDocId>
106+
```
107+
accept the prompt, and the provider executes once, pushes the output to every
108+
collaborator, and exits (the editor then shows the output with no live
109+
executor).
110+
111+
## Option B — fully local (offline / private): `q2 hub`
112+
113+
Runs a local sync server so nothing leaves your machine. The example project
114+
(`project/hello.qmd`, `engine: jupyter`) and the `start-local-hub.sh` helper
115+
are for this path.
116+
117+
**Important:** `q2 hub --project <dir>` (project mode) only serves *its own*
118+
watched project. So you must **open that project via the Share URL** — do **not**
119+
create a new project in hub-client (a fresh hub-client project isn't stored by a
120+
project-mode hub, and the provider will see `0 file(s)`).
121+
122+
```bash
123+
cd claude-notes/hub-execution-e2e
124+
./start-local-hub.sh # builds q2, starts the no-auth hub, prints the URLs
125+
```
126+
127+
It prints the project's index id (from `GET /health`) and the exact commands
128+
for the other two terminals:
129+
130+
- **hub-client**, pointed at the local hub:
131+
```bash
132+
cd hub-client
133+
VITE_DEFAULT_SYNC_SERVER=ws://127.0.0.1:3031 npm run dev
134+
```
135+
Then open the printed **Share URL** (`…/#/share/<ID>?server=ws://127.0.0.1:3031&file=hello.qmd&name=Local%20demo`) — this opens the hub-owned project, not a new one.
136+
- **provider** (watch mode, for the Run button):
137+
```bash
138+
cargo run --bin q2 -- provide-hub --watch --server ws://127.0.0.1:3031 --token dev <ID>
139+
```
140+
(accept each request at the prompt, or add `--dangerously-accept-requests`).
141+
For a one-shot push instead: `provide-hub --file hello.qmd --server
142+
ws://127.0.0.1:3031 --token dev <ID>`.
143+
144+
`q2 hub` runs with **no auth** (no `--oidc-client-id`), and hub-client runs with
145+
auth off because `VITE_GOOGLE_CLIENT_ID` is unset — so there's no login screen.
146+
147+
(If you'd rather create projects freely in hub-client while staying local, run a
148+
general relay instead of project mode: `q2 hub --no-project --port 3031`. It
149+
stores/relays arbitrary docs the way `sync.automerge.org` does.)
150+
151+
## Troubleshooting
152+
153+
- **Provider prints `0 file(s)` / `project discovery failed: … No such file`:**
154+
the provider can't see the project's files. In Option B this means you created
155+
a *new* project instead of opening the hub-owned one via the Share URL (a
156+
project-mode hub only serves its own project). Use the Share URL, or switch to
157+
Option A / `q2 hub --no-project`.
158+
- **No Run bar, only "Executor online" (or nothing):** the document has no
159+
executable cell — check the `engine:` front-matter key and that the fence is
160+
```` ```{r} ```` / ```` ```{python} ````, not ```` ```r ```` or ```` ```{.r} ````.
161+
- **No "Executor online" at all:** the provider isn't connected to the *same*
162+
sync server as hub-client, or against a different index id, **or you did not
163+
pass `--watch`** (only watch mode broadcasts the beacon that lights up
164+
"Executor online" / the Run button). Confirm the provider printed
165+
`Watching for execution requests…` and `N file(s)` with N > 0.
166+
- **Run does nothing / error in the bar:** the engine isn't installed or failed;
167+
the provider terminal logs `exec request failed …`. Sanity-check the engine
168+
standalone: `cargo run --bin q2 -- render <doc>.qmd --to html` should contain
169+
the computed output.
170+
- **Preview blank / won't render:** build the WASM — `cd hub-client && npm run build:wasm`.
171+
172+
## Notes & caveats
173+
174+
- **Single executor.** Two `provide-hub` processes on one project both execute
175+
every request (v1 limitation; see the plan's "Known limitations").
176+
- **Consent-gated by default (bd-9lgiulr4).** Every execution requires you to
177+
accept an interactive prompt showing the resolved document; `--watch` adds a
178+
"3) accept all future" option. `--dangerously-accept-requests` skips the
179+
prompt. The per-project provider-only requester gating (only your own requests
180+
run) is still a separate Phase-5 concern.
181+
- **Every run creates a fresh capture doc** (always-fresh execution); old
182+
capture docs accumulate until server-side GC exists (Phase 5/6).
183+
- Design + phase details: `claude-notes/plans/2026-06-29-remote-execution-provider.md`.
184+
185+
### Verified directly while writing this
186+
187+
- Provider dials `wss://sync.automerge.org` (TLS) and syncs; an absent doc
188+
returns a fast "not found" (no hang).
189+
- Provider against a hub-owned local project lists its files; the Jupyter/knitr
190+
engines execute on this machine (`q2 render`, same registry the provider uses).
191+
- The provider execute loop (receive `exec/request` → run engine → write capture
192+
back) is covered by `crates/quarto-hub-provider/tests/integration/execute.rs`.
193+
- The browser Run-click is the manual last mile this harness drives.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Hub project-mode data (index doc + synced file docs) is generated at runtime.
2+
.quarto/
3+
*.html
4+
*_files/
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
title: Local execution demo
3+
engine: jupyter
4+
---
5+
6+
This document has an executable Python cell. When a `q2 provide-hub` executor
7+
is connected, the preview shows a **Run** button; clicking it runs the code on
8+
the executor's machine and splices the output back into every collaborator's
9+
preview.
10+
11+
The `engine: jupyter` line in the front matter above is what makes the cell
12+
executable — without an `engine:` key the cell renders as source only. (For R,
13+
use `engine: knitr` and an `{r}` cell instead.)
14+
15+
```{python}
16+
2 + 3
17+
```
18+
19+
You can add more cells and re-run:
20+
21+
```{python}
22+
import sys
23+
f"python {sys.version_info.major}.{sys.version_info.minor} on the executor"
24+
```
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
title: R execution demo
3+
engine: knitr
4+
---
5+
6+
An R cell executed via the connected `q2` provider (engine: knitr).
7+
8+
```{r}
9+
1 + 1
10+
```
11+
12+
A second cell to confirm output splicing:
13+
14+
```{r}
15+
cat("hello from", R.version.string, "\n")
16+
sum(1:10)
17+
```

0 commit comments

Comments
 (0)