feat(asset-bridge): the shard's own files stop needing a shared filesystem (Phase 7) #34
Reference in New Issue
Block a user
No description provided.
Delete Branch "feat/asset-bridge-p7"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 7 of the Asset Bridge —
docs/link/v8.md§10. Pairs with Module-uo#40 and docs#242.linkneeded nothing, for the third phase running.What this closes
SPAWN_ATLAS.mdrequired the website to read the ServUO tree — "same host, a bind mount, or a shared volume". That was the one place the platform's own rule (only the sidecar bridges the shard) was broken, and it was broken by the component that faces the internet. The shard now serves those five labelled groups over the loopback link.The measurement changed the phase, again
§10 said the shard would serve
tree/<label>→ bytes. It cannot.Spawns/trammel.xmlis 4.03 MB, the sidecar discards any inbound line over 1 MiB, and that file as one base64 row is 5.4 MiB — dropped, timed out, re-requested forever, with no error in it anywhere. Two files on a stock tree are in that state.The winner is not the smallest. The chunk is the bound and the compression is only the saving: nothing guarantees an operator's files compress, so the ceiling has to hold when they do not — a 512 KiB chunk that refuses to compress is ~683 KiB of base64, inside the wire cap that
AssetBatchBytes' deliberate factor of two leaves room for. Whole-file gzip works on every tree anyone would test and fails on the first one nobody did.The key gains a depth segment —
tree/Spawns/trammel.xml/c0— which is §5's scheme doing the same job it does forbody/400/a0/f0, and needing no protocol change to do it.A family, not two new commands
§14 planned
tree.manifest/tree.fetchwith/tree/*REST. Phase 5 had already learned that the command is the transport and the family is a property of the key, and §5 had already writtentree/Spawns/Trammel.xmlas a key. So this registers atreefamily and generalisesassets.manifestthe same way — reusing the single slot, the byte budget, the paging envelope, the key ceiling, thecatalogguard and the 425 backoff, none of them written twice.It gets its own consent, though:
Bridge.TreeEnabled.AssetsEnabledis an operator agreeing the website may read their EA-licensed UO client. This is the shard's own configuration, which they wrote, and which the public bestiary is built from. One switch could not express both, and the casualty for an operator who declined the first would have been their spawn atlas. So the consent check moved out of the front ofassets.fetchand into the family lookup, andassets.sourcesnow answers whenever either plane is on, withfamiliesfiltered to what is actually enabled.Verified live, all four combinations:
Two defects, both fixed plane-wide, and which harness found which
Both are the same kind of thing, and it is the kind a stock install finds: a value meaning "nothing here" has two spellings, and you handled one of them.
1. An empty
catalogis not an absent one.expected != nulltreats""as a real fingerprint, so a caller that serialises a missing value as an empty string has every fetch refused — with a sentence naming no catalog at all (catalog is now 8159778b), which reads as a shard fault rather than a caller one. Found by an offline probe that passed one by accident.It was inline in three places by then — the body catalogue (phase 3), statics and land (phase 5), and the tree. All three now go through one
BridgeAssets.CatalogMismatch; three copies of a comparison are three chances for the next family to get it wrong in a way only a differently-written client would reveal.BridgeLeaseskeeps its ownexpected != nulland is deliberately untouched: there the value is a world property, where an empty string is a legitimate thing to expect.2.
GZipStreamwrites NOTHING for zero bytes of input. The gzip header is emitted lazily on the first write, so a stream opened and closed without one yields a zero-length buffer, not the 20-byte empty member. Stock ServUO 57.4 ships two empty decoration files (Felucca/ambitious solen queen quest.cfg,Tokuno/terrible hatchlings quest.cfg), so this broke every import off an untouched tree. The offline probe reassembled all 141 files and reported success — .NET's own decompressor reads an empty stream as empty data, and the chunk's declared length (0) and hash (of nothing) both agreed with it. Only the live walk, through a reader on a different runtime, disagreed.The whole plane, against a stock install
Every family, each asked three ways — with a real catalog, with the field absent, and with an empty string:
The label set is this shard's, never the caller's
This is the only family whose keys look like paths, and the caller is the internet-facing component. A fetch resolves its label against the set the shard itself enumerated — refused before any file is opened, whatever it spells — the five groups and their extensions are fixed in code, and the resolved path is still checked to be under the tree root. Live:
Measured end to end
Live ServUO shard → real sidecar → module-uo's own reader:
dotnet build Scripts.csproj -c Release→ 0 warnings, 0 errors.Protocol
Stays 8, and
EXTRACTOR_VERSIONstays 3. This family derives nothing — it forwards an operator's own file unchanged — so the number that tracks our derivation has nothing to say about it.overlay.tomlis untouched.🤖 Generated with Claude Code
https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
The spawn atlas was the one place the platform's rule -- only the sidecar bridges the shard -- was broken, and it was broken by the component that faces the internet: SPAWN_ATLAS.md required the website to read the ServUO tree off a bind mount or a shared volume. This serves those files over the loopback link instead (docs/link/v8.md 10). The measurement came first and changed the shape. 10 said the shard would serve `tree/<label>` -> bytes; against a stock 57.4 tree it cannot. Spawns/trammel.xml is 4.03 MB, the sidecar discards any inbound line over 1 MiB, and that file as one base64 row is 5.4 MiB -- it would be dropped, time out, and be re-requested forever with no error anywhere. Two files on a STOCK tree are in that state. So a file crosses as 512 KiB chunks, each gzipped: tree/Spawns/trammel.xml/c0 and so on, which is 5's depth scheme doing the same job it does for body/400/a0/f0 and needing no protocol change to do it. The chunk is the bound and the compression is only the saving -- nothing guarantees an operator's files compress, so the ceiling has to hold when they do not, and a 512 KiB chunk that refuses to compress is still ~683 KiB of base64, inside the wire cap that AssetBatchBytes' deliberate factor of two leaves room for. It is a `tree` FAMILY on assets.fetch rather than 14's separate tree.* commands: phase 5 had already learned that the command is the transport and the family is a property of the key, and assets.manifest is generalised here the same way. That reuses the single slot, the paging envelope, the key ceiling and the mid-import guard -- and leaves `link` with nothing to do for the third phase running. But it gets its OWN consent, Bridge.TreeEnabled. AssetsEnabled is an operator agreeing the website may read their EA-licensed UO client; this is the shard's own configuration, which they wrote, and which the public bestiary is built from. One switch could not express both, and the thing that would silently disappear for an operator who declined the first is their spawn atlas. So the consent check moved into the family lookup, and assets.sources answers whenever either plane is on, reporting `families` filtered to what is actually enabled -- which is how a tree-only shard's website discovers there is anything to ask for. Two defects found, and which harness found which is the part worth keeping: - An empty `catalog` is not an absent one. `expected != null` refused every fetch from a caller that sent "", with a sentence naming no catalog at all. Found by an offline probe that passed one by accident. - GZipStream writes NOTHING for zero bytes of input -- the header is emitted lazily, so a stream opened and closed without a write yields a zero-length buffer rather than the 20-byte empty member. Stock ServUO ships two empty decoration files, so this broke every import off an untouched tree. The offline probe reassembled all 141 files and reported success, because .NET's own decompressor reads an empty stream as empty data and the chunk's declared length (0) and hash (of nothing) both agreed. Only the live walk, through a reader on another runtime, disagreed. Measured end to end against a live shard, the real sidecar and the website's own reader: 141 files, 11,895,427 bytes, 158 chunks, 3 pages, 1.33 MB on the wire, 512 ms; every file byte-identical to disk; the atlas built over the bridge identical to the one built off it. A drift check is the manifest alone -- 32 KB, ~70 ms, no file bytes. The label set is this shard's, never the caller's: a fetch resolves against the set the shard itself enumerated, and tree/../../Scripts/..., Config/Bridge.cfg and Saves/Accounts/accounts.xml are all answered `absent` before a path is built out of them. Protocol stays 8 and EXTRACTOR_VERSION stays 3 -- this family derives nothing, it forwards an operator's own file unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4Phase 7 found this on the tree family and fixed it there. It was inline in THREE places: the body catalogue (phase 3), statics and land (phase 5), and the tree. `expected != null` treats "" as a real fingerprint, so a caller that serialises a missing value as an empty string has EVERY fetch refused -- with a sentence that names no catalog at all ("catalog is now 8159778b"), which reads as a shard fault rather than a caller one. All three now go through one BridgeAssets.CatalogMismatch. Three copies of a comparison are three chances for the next family to get it wrong in a way only a differently-written client would ever reveal. BridgeLeases keeps its own `expected != null` and is deliberately untouched: there the value is a world property, where an empty string is a legitimate thing to expect. Verified against a live shard on a stock ServUO install, every family asked three ways -- with a real catalog, with the field absent, and with an empty string: cliloc.table walk 67,496 rows, 12 pages body manifest / fetch 1,095 rows; ok all three ways static + land fetch ok all three ways static/land carry their OWN catalog art 66a112c1 vs body 323f284f a cross-family catalog refused 422 tree manifest / fetch 141 files incl. BOTH empty ones, all three ways empty files carry a VALID gzip member 2 rows gunzip to 0 bytes a STALE catalog still refused on body, static and tree Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4