feat(sidecar): protocol 8 — the asset plane, and a bound on what the shard can send #41

Merged
whitlocktech merged 1 commits from feat/asset-bridge-p1 into edge 2026-09-10 15:04:26 +00:00
Member

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 keptBridgeLink.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.busy425 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.

  • AI-assisted — Claude Code (Opus 5)

🤖 Generated with Claude Code

https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4

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) 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 1 commit 2026-09-10 13:34:37 +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
whitlocktech merged commit 6d83df0a2c into edge 2026-09-10 15:04:26 +00:00
whitlocktech deleted branch feat/asset-bridge-p1 2026-09-10 15:04:27 +00:00
Sign in to join this conversation.
No description provided.