Changes touching this path
- SDK: inject transport/runner seams + map binary error codes (#432, #433, #434)
Three architecture-review deepenings that make the two LootRepo adapters'
decision logic testable without a live relay or the real binary, and replace
stderr regex-scraping with the binary's coded error channel.
#432 — RelayTransport seam. A narrow dumb-pipe (`post`) + default
HttpRelayTransport, injected via `connectRelay(url, id, { transport })`. All
interpretation stays adapter-side: response classification (401→AuthError+pubkey,
non-2xx/connection-fail→TransportError) is a pure helper, and the push
visibility-resolution + GuardError enforcement is extracted to a pure
`resolvePushVisibilities`. New relay.unit.test.ts proves error classification,
path-scoping, decode, and compose/guard against a fake transport — the WASM core
exposes no bundle encoder, so decode/path-scoping replay golden `/fetch` bytes
captured from a real relay (test/fixtures, regenerate with gen-relay-fixtures.mjs
after a format bump). Relay integration trimmed to the read + write round-trip
smokes (read.behavior/write.behavior); push-errors.behavior deleted.
#433 — LootRunner seam. `run` (buffered, never throws on non-zero) + `spawn`
(streaming) + default SubprocessRunner, injected via `openRepo(path, { runner })`.
Physical error-mapping, arg composition, and pull streaming are unit-tested
against a fake runner (physical.unit.test.ts); physical integration trimmed to
the read + write round-trip smoke.
#434 — map binary error codes → LootErrorCode. physical.ts reads `error.code`
from the binary's `{"error":{"code","message"}}` under --json and maps it in one
place (demotion/mis_seal/seal_wip→guard; unsupported_format/no_repo/unknown_flag
→setup; not_found→not-found; else generic); all stderr prose regexes dropped.
`run`/`pull` append --json, so the CLI's `new`/`describe` verbs now accept --json
(not --porcelain — no consumer) to emit coded failures. loot's engine has no
conflict-family slug (it accumulates forks rather than rejecting), so conflict
stays deferred/generic — ConflictError remains exported for when a slug lands.
e75ba364 · dbf3dbe6… - the two servers share one auth preamble, and a forge handler reaches the router only through the crossing (#865)
a10e310f · dbf3dbe6…diff - the browser SDK reads a relay at the heads generation and refuses a host that cannot give it one: connectRelay list() and read() open with a fetch carrying depth one, the heads nodes and nothing older with the whole manifest on each, and the object round is bounded the same way rather than paying the history a second time, since a want is answered by address across the cut, so a read is two bounded rounds where it was one unbounded and one that repeated it, against the 53 MB per call the map measured on this repo; the host is asked once per repo, on GET /info, which the transport seam gains as an optional get so a lane that never asks stays honest, and a host whose answer does not say fetch_depth, or answers with a non-2xx, or not JSON, or through a transport with no get, is read as one that did not say and is refused as a SetupError naming the host and the version line it reported, before any bytes are asked for, unless the caller passed unboundedRead, which accepts the cost on that host and asks a capable one for the bound all the same; a probe that could not reach the host is a TransportError and is asked again next time. heads() names the set the bounded read answered with, on a RelayLootRepo the connect door now returns, and several heads fold as they did, a later head write winning a shared path; push, status and grant ride the same bounded round because the parents a change builds on and the tree it carries are the heads own, which is all they ever read. pinned on a fake transport with the golden bundles, the request bytes bounded on the metadata and the object rounds, the old host refused by name on list and on read with nothing posted, the opt-in against it and not against a capable host, the probe asked once across list and read, the three did-not-say shapes each refused and each opted into, the unreachable probe re-asked, and heads() one; and over a spawned relay with two generations, the request four bytes longer than the unbounded one ending in one, the answer one node whose parent is the base change a whole fetch shows, every round of a read one node, the head named and then two after a fork the second session pushes off a replayed metadata answer, listed as the union, and the field stripped off the real /info refused by name and paid for on opt-in. README states the cost and the option, CONTEXT and ADR 0089 say the browser SDK refuses rather than is to. red under mutation, counts read each time: the depth never sent (21 passed and 5 failed), every host assumed to cut (21 passed and 5 failed), unboundedRead ignored (22 passed and 4 failed), the probe asked on every read (18 passed and 1 failed), a failed probe remembered (17 passed and 2 failed), heads() naming nothing (23 passed and 3 failed), the refusal not naming the host (24 passed and 2 failed), a transport without get reading as capable (18 passed and 1 failed), a 404 on /info reading as capable (18 passed and 1 failed), an error on /info remembered as a refusal (18 passed and 1 failed), an explicit false read as absent (18 passed and 1 failed), a JSON non-object left unclassified (18 passed and 1 failed), the head derivation skipping the parented filter (6 passed and 1 failed), and the object round unbounded (24 passed and 2 failed). no rust, no migration, no wire or format byte moves, and no host behaviour moves, so this owes no deploy beyond the one #2123 already owes the release; the SDK suite is green in the lane (126 passed over 11 files, against release binaries and sdk/wasm built in the lane) and the workspace suite is untouched by a TypeScript change and was green at the base (4017 passed over 126 binaries, 7 ignored, at 5a007435) (#2124)
8eb8e8cd · dbf3dbe6…diff - the browser sdk transport stops waiting on a silent host indefinitely, because fetch has no timeout of its own and in a browser nothing stands in for one: HttpRelayTransport now aborts through a watchdog that waits for an answer to begin for its idle budget plus the upload at the uplink floor, then re-arms on each chunk of the body, so an answer that keeps arriving is not cut for its size and one that stops fails once the idle budget passes with no bytes, the same policy #2079 gave the native client, with TRANSFER_IDLE_MS mirroring wire::TRANSFER_IDLE and each side now naming the copy on the other. a budget that runs out throws a TransportError naming which wait it was and how long, and the adapter passes that through instead of relabelling it as an unreachable host, one rule in errors.ts asked by every catch around a transport call. TWO OF THE THREE ITEMS ARE REFUTED RATHER THAN BUILT: the forge blocking clients the ticket named both sit inside cfg(test) modules, in the tree today and at the rev the ticket was filed on, and the forge talks to its object store only through loot_s3::S3Client, which already names its own 60 second timeout; and the upload side header wait was fixed under #2079 by the byte-derived transfer budget, whose refusal already names the answer budget. the frozen-tab limit is written down rather than fixed, since a timer on the page loop measures the page clock and not peer silence, which is #2079 in a browser. red under mutation, counts read each time: the re-arm moved outside the read loop failed the steady-answer pin (6 passed and 1 failed), the answer wait ignoring the bytes failed two (5 passed and 2 failed), the watchdog verdict dropped failed all three budget pins on a bare AbortError (4 passed and 3 failed), and the adapter relabelling every throw failed the pass-through pin (0 passed and 1 failed), each restored. the sdk suite is green against release binaries built in the lane (137 passed over 13 files), no migration, no wire or format byte moves and no host behaviour moves, so this owes no deploy. the workspace suite is green (4179 passed over 133 binaries, 9 ignored) (#2070)
f6348ba0 · dbf3dbe6…diff - the browser SDK head derivation skips a superseded version: the WASM core ChangeView, the JSON WasmBundle.changesJson renders, gains predecessors as hex ids, an empty list when a change supersedes none, and snapshot() in sdk/src/repo.ts drops any id some returned change names as a predecessor before folding the heads, the engine within_depth_of_heads rule, so heads() and the parents a push builds on no longer name a superseded version under unboundedRead against a host that sends one. the golden relay bundles are regenerated with the first change pushed, amended and pushed again, so the meta bundle holds a superseded version nothing names as a parent; the amend is a loot squash rather than the loot edit the ticket named, because an edit finalize records the superseded version as a parent as well as a predecessor, measured, which the old derivation already skipped. pinned in relay.unit.test.ts: the golden holds that shape, and heads() names the amend alone while list() does not show draft.md, the path only the superseded version holds; the shared parity check_bundle pins the empty list. red first against the old golden (20 passed and 2 failed). red under mutation, counts read each time: the predecessor filter dropped from snapshot (20 passed and 2 failed), the core rendering no predecessors (19 passed and 3 failed), the field left out of the JSON (parity 0 passed and 1 failed), each restored to green. no format byte and no migration moves and no host changes, so this owes no deploy. the SDK suite is green in the lane (139 passed over 13 files), the site gate is green (678 passed and 62 skipped over 62 files, 62 surfaces) with no ceiling moved, and the workspace suite is green (4323 passed over 137 binaries, 12 ignored) (#2137)
a9fd3357 · dbf3dbe6…diff - a relay with a push allowlist now gates its reads on it too: every relay read route was unauthenticated while every bundle carries the key for every Internal object (ADR 0011), so an allowlisted relay.millerbyte.com served this private repo to a stranger key in plaintext (#2388). a route is a read when its answer comes from the store, and the store now sits behind Hold, whose only doors are a gated read and a gated write, with the router built from one match over Route that has no wildcard, so a later route cannot answer from the store ungated; the reads today are /negotiate, /offer, /fetch, /wants and /haves, /info stays the one open probe, and the grant lane is unchanged. with no list a relay stays open and unsigned, and loot serve and loot-relayd now warn at startup that anyone who can reach it can clone everything, Internal content included. /info advertises authenticated_reads and a client signs its reads iff it is advertised, so a new client reads an old relay unsigned and an old client gets a 401 naming the upgrade; loot seek records the posture and the SDK signs its reads the same way. no format constant moves. ADR 0011 and 0015 amendments, CONTEXT.md, the forge spec, sdk README and the site cli, guides and concepts pages say it. red under mutation, counts read: gate never on (the stranger clone succeeds), gate admitting any valid key, the 401 without the upgrade wording, /haves filed as open, /info not advertising, client never signs, client signing whenever a list exists, the startup warning dropped, the SDK never signing; each restored to green. workspace suite 4539 passed over 141 binaries, 13 ignored; SDK 145 of 145; site gate green (777). owes a release before loot serve users get it; the live relay stays stopped (#2389)
f6b5ecc7 · dbf3dbe6…diff
Renames are not followed. loot's tree maps a path to an address, so a rename is a delete and an add. This list is the history of the name, not of the bytes.