feat(sidecar): protocol 8 — the asset plane (Asset Bridge cutover, 1 of 5) #44

Merged
whitlocktech merged 6 commits from edge into main 2026-09-14 23:09:15 +00:00
Member

Step 1 of 5 of the Asset Bridge edge → main cutover (docs/link/v8.md §16 row 9b).

Land this together with servuo-plugins#36

installer's bundle.yml Gate 1 refuses a sidecar/overlay pair that disagrees on protocol.
This PR moves the sidecar to 8; servuo-plugins#36 moves overlay.toml to 8. Whichever lands
first leaves a compose that cannot run until the other does — exactly what happened at the Event
System cutover (compose run 102 failed, 103 composed once both were out). Merge them in the same
window.

What comes over

Three phases of the Asset Bridge, all already merged to edge and reviewed there:

Phase PR What it added here
1 #41 PROTOCOL_VERSION 7 → 8, and MAX_INBOUND_LINE_BYTES — the shard → sidecar direction had no line cap at all (shard.rs read_line), while sidecar → shard has capped at 1 MiB since the beginning. Protocol 8 is the first version that sends bulk data up that direction, so it is the first that needed the bound
2 #42 GET /cliloc — the cliloc table forwarded verbatim, and the fix to stop reading a refusal as meaning
3 #43 GET /assets/sources, GET /assets/manifest, POST /assets/fetch, POST /assets/bodies — the manifest, the pixels and the body pass

sidecar/src/main.rs +35, shard.rs +268, web.rs +405.

The sidecar stays a dumb forwarder: it validates shape and forwards; every decision about what
an asset is, who may see it and whether to keep it is the website's. Phases 5, 6 and 7 of the
Asset Bridge needed no change here at all — item/land art, the action ceiling and the spawn-tree
transport all ride assets.fetch's existing family shape, which is the design working.

After the merge

Release fires on push to main; expect a minor sidecar release (conventional commits), and
bundle.yml dispatched from its final step to recompose the bundle at protocol 8. edge stays
standing.

Verification

Run on edge at baa04e1:

  • cargo fmt --check — clean
  • cargo clippy --all-targets -- -D warnings — clean
  • cargo test64 passed, 0 failed

Plus phase 9a's acceptance walk against a real ServUO shard and this sidecar: 1,095 portraits in
3.18 s, 67,496 cliloc names in 1.42 s, 313 item pictures in 1.38 s, the spawn atlas over the bridge
in ~2 s.

AI disclosure

Authored with Claude Code (Claude Opus 5). Commits carry the Co-Authored-By trailer.

🤖 Generated with Claude Code

https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4

Step **1 of 5** of the Asset Bridge `edge → main` cutover (`docs/link/v8.md` §16 row 9b). ## Land this together with `servuo-plugins#36` `installer`'s `bundle.yml` **Gate 1 refuses a sidecar/overlay pair that disagrees on protocol.** This PR moves the sidecar to **8**; `servuo-plugins#36` moves `overlay.toml` to 8. Whichever lands first leaves a compose that cannot run until the other does — exactly what happened at the Event System cutover (compose run 102 failed, 103 composed once both were out). Merge them in the same window. ## What comes over Three phases of the Asset Bridge, all already merged to `edge` and reviewed there: | Phase | PR | What it added here | |---|---|---| | 1 | #41 | `PROTOCOL_VERSION` **7 → 8**, and `MAX_INBOUND_LINE_BYTES` — the shard → sidecar direction had **no** line cap at all (`shard.rs` `read_line`), while sidecar → shard has capped at 1 MiB since the beginning. Protocol 8 is the first version that sends bulk data up that direction, so it is the first that needed the bound | | 2 | #42 | `GET /cliloc` — the cliloc table forwarded verbatim, and the fix to stop reading a refusal as meaning | | 3 | #43 | `GET /assets/sources`, `GET /assets/manifest`, `POST /assets/fetch`, `POST /assets/bodies` — the manifest, the pixels and the body pass | `sidecar/src/main.rs` +35, `shard.rs` +268, `web.rs` +405. The sidecar stays a **dumb forwarder**: it validates shape and forwards; every decision about what an asset is, who may see it and whether to keep it is the website's. Phases **5, 6 and 7** of the Asset Bridge needed **no change here at all** — item/land art, the action ceiling and the spawn-tree transport all ride `assets.fetch`'s existing family shape, which is the design working. ## After the merge Release fires on push to `main`; expect a **minor** sidecar release (conventional commits), and `bundle.yml` dispatched from its final step to recompose the bundle at protocol 8. `edge` stays standing. ## Verification Run on `edge` at `baa04e1`: - `cargo fmt --check` — clean - `cargo clippy --all-targets -- -D warnings` — clean - `cargo test` — **64 passed, 0 failed** Plus phase 9a's acceptance walk against a real ServUO shard and this sidecar: 1,095 portraits in 3.18 s, 67,496 cliloc names in 1.42 s, 313 item pictures in 1.38 s, the spawn atlas over the bridge in ~2 s. ## AI disclosure Authored with Claude Code (Claude Opus 5). Commits carry the `Co-Authored-By` trailer. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 6 commits 2026-09-14 22:36:38 +00:00
feat(sidecar): protocol 8 — the asset plane, and a bound on what the shard can send
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 2m39s
a8f1804de9
Asset Bridge phase 1, sidecar half (docs/link/v8.md §3.3, §14).
Shard half: RunicGateway/servuo-plugins#28. Docs half: RunicGateway/docs#236.

Three things, one of which is not additive.

## The inbound line cap (§3.3) — the one that matters

`read_line` had **no bound at all**. That was survivable only because the shard had
never had a reason to send a large line. Protocol 8 gives it one deliberately, and an
unbounded read facing a component that now sends megabytes is a memory-exhaustion
shape we would be inventing ourselves.

`MAX_INBOUND_LINE_BYTES` is **1 MiB** — symmetric with the cap `BridgeLink.cs` has
always applied to its own inbound lines, so both directions of this link now read the
same. The shard's batch budget is 512 KiB, and the factor of two is load-bearing: a
page always admits its first item even when that item alone exceeds the budget (the
alternative is an oversized item skipped for the budget on every page forever), so the
wire needs room for one overshoot.

An over-long line is **discarded and the connection kept** — `BridgeLink.cs`'s own
disposition in the other direction. Tearing the link down would take the live event
feed with it over one malformed frame, and the lost reply just times out and is
re-requested; everything on this plane is idempotent.

**`LineReader` holds its state in a struct rather than in locals, and that is the
subtle part.** This is polled inside a `tokio::select!`, so the future is dropped
whenever a command wins the race. A `discarding` flag in a local would be lost with
it — and losing it turns the tail of an over-long line into a line of its own, silently.
There is a test for exactly that, and another for an over-long line whose terminator
lands in the very chunk that crosses the cap.

## `GET /assets/sources`

Stage 1 of the import gate, forwarded verbatim like everything else. `respond_assets`
maps `bridge.busy` → **425** and a disabled plane → **403**.

425 deserves a note: on this plane it is not an idempotency collision, it is flow
control, and it is the **ordinary** answer mid-import rather than a rare one. The shard
serves one asset request at a time because its outbound queue is bounded in lines, not
bytes. A caller treating it as an error would abandon a healthy transfer.

403 for the same reason the event plane's gate is a 403: `Bridge.AssetsEnabled` off is
an operator declining to let the website read their client files, not a malformed
request, and 400 would send an administrator hunting a bug in a correct call.

## `PROTOCOL_VERSION` 7 → 8

Paired with `servuo-plugins/overlay.toml` in the linked PR — the installer refuses to
compose a bundle whose halves disagree, so a split bump fails silently at the next
release.

## Also

`docs/link/INTEGRATION.md` still advertised `X-UOLink-Version: 6`; it was already two
versions stale before this change. Fixed in the docs PR.

61 tests pass, `cargo fmt --check` and `cargo clippy -- -D warnings` clean. Verified
against the real shard: `/health` reports protocol 8, `/assets/sources` returns 200 with
`X-UOLink-Version: 8`, and live events kept flowing through the new reader with no
warnings logged.

- [x] AI-assisted — Claude Code (Opus 5)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Reviewed-on: #41
feat(sidecar): forward the cliloc table, and stop reading refusals for meaning (Phase 2)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 3m2s
b92393d224
`GET /cliloc` — the first protocol-8 family that carries content rather than a
manifest. The shard decompresses its own client's table and cuts it into pages;
this forwards them and keeps none of it, which matters more here than usual: the
payload is five megabytes of EA's strings out of the operator's own client, and
the one copy that should exist is the one the website imports.

Paging is the caller's, deliberately. `?cursor=` echoes back the previous reply's
cursor until one says `more: false`; a sidecar that helpfully assembled the pages
would be holding the whole table in memory to do it. `?lang=` selects the file
and defaults on the shard.

`asset_error_status` replaces phase 1's substring test. That test chose 403 or
400 by looking for the word "disabled" in an operator-facing sentence, so
rewording the message would silently turn a refusal into a bad request. The
overlay now sends a `code`: DISABLED 403, NOT_FOUND 404 (a client without the
file — an operator fact, not a bug), UNREADABLE 422 (a file it has and cannot
decode, where repeating the request cannot help), UNAVAILABLE 503, BAD_REQUEST
400. The substring check survives as a fallback, with a test, because an overlay
and a sidecar are deployed separately and a phase-1 shard must keep its 403.

No `PROTOCOL_VERSION` change: 8 already covers this family (v8.md §14).

Verified against a live shard: 12 pages, 67,496 rows, every page inside the
512 KiB budget (max 524,086 of 524,288) and well under the 1 MiB line cap, the
whole table in 1.4 s. Concurrent callers get 425 while one is served, which is
the flow control working rather than an error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Reviewed-on: #42
feat(sidecar): forward the asset manifest, the pixels and the body pass (Phase 3)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 2m24s
143f424867
Three routes, forwarded verbatim like everything else on this link:

  GET  /assets/manifest?family=&cursor=
  POST /assets/fetch
  POST /assets/bodies

**The two POSTs are reads.** The method is the request body, not a side
effect -- a few hundred asset keys do not belong in a query string, and these
are the only reads on this link that take one. `assets_call` is `event_call`'s
shape with one difference that matters: it responds through `respond_assets`,
so `bridge.busy` is a 425 rather than an idempotency collision. On this plane
busy is the ORDINARY answer during an import, and a caller that read it as an
error would abandon a healthy transfer.

422 gains a second meaning here alongside "the shard cannot decode that file":
the mid-import guard. A manifest reply carries a `catalog` id the shard derives
from its own client files, and passing it back on a fetch makes the shard refuse
if those files moved in between -- without which an operator patching their
client halfway through an import gets one asset set stitched out of two, with no
error anywhere.

v8.md §16 listed phase 3 as servuo-plugins + module-uo. That was wrong: web.rs
routes every command explicitly and has no generic /assets/* forwarder, so this
repo is in the phase. The doc now says so.

64 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Reviewed-on: #43
whitlocktech approved these changes 2026-09-14 23:09:05 +00:00
whitlocktech merged commit 82872ffba7 into main 2026-09-14 23:09:15 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RunicGateway/link#44
No description provided.