Changes touching this path
- Slice 1 review fixes: prove path-scoping, reusable suite, SDK docs (#423)
Addresses the code-review findings on de6e281:
- Add sdk/README.md — states plainly that loot hides CONTENT, not path
names or tree shape ("don't put secrets in path names"), documents usage
and the build:wasm step. Closes the missing-docs AC.
- Make path-scoping observable: the behavior suite now seeds TWO public
files and asserts at the wire level that a scoped /fetch returns the
requested object's bytes but NOT the sibling's — while the sibling's
address is still visible (structure is public, content is scoped).
- Make the read suite backend-parameterized: assertions live in
runReadContract(makeRepo) so #422's physical openRepo reuses them verbatim.
- Honest streaming: ReadStream now documents that a sealed object is one
AES-GCM unit yielded as a single chunk (not chunked streaming), which the
iterable shape grows into for later slices.
Left as-is: currentTree resolves heads in TS — that's the #381-sanctioned
thin store-orchestration (only crypto/codec must stay Rust), not drift.
Refs #423.
f736b628 · dbf3dbe6… - TS SDK slice 2: author & push a signed public change (#424)
The write half of the in-memory loop: a pre-registered key edits files and
pushes a signed full-tree change to the relay, read back to confirm. All
composition (change-id fold, dual signatures, bundle encode, /stow
envelope) stays in Rust/WASM (#381); TS owns only the capture-first overlay.
Prefactor:
- Move the change-id fold into loot-codec (change_id module:
compute_change_id_raw / change_signing_message / mint_change_id /
canonical_predecessors); loot-core's compute_change_id(&Change) delegates.
Behavior-preserving (loot-core 296 tests unchanged).
- loot-codec: seal_uncompressed (the wasm author path can't run zstd).
- loot-identity: public Identity::from_seed(&[u8;32]) (the flagged #383/#424
ctor, native side).
loot-wasm:
- Identity.sign + wrapEnvelope ([0x01][pubkey][sig][bundle], sig over bundle).
- ChangeBuilder: carry(unchanged) / put(edited, seals uncompressed public) /
finish() -> {envelope, changeId, versionId} — folds, signs the finalize
message, encodes the Sync frame, wraps the envelope, all in Rust.
- Golden parity (Seam 2) extended: frozen pubkey, signature, envelope bytes,
and change-id fold — green natively and under wasm-pack test --node.
sdk/:
- Capture-first overlay: edit/remove mutate an in-RAM overlay that IS the
pending change; describe names it; status/diff report it; push composes via
ChangeBuilder (carry unchanged paths, put edited, skip removed) and POSTs
the envelope to /stow, returning the durable change-id.
- Seam 1: a write behavior suite drives a real relay whose allow-list holds
the SDK key; the SDK authors the first change and reads it back through a
fresh connection (4 tests). Full SDK suite: 9 green.
Slice 2 authors PUBLIC content stored UNCOMPRESSED (valid + readable; zstd's C
won't build for wasm, and fzstd is decompress-only). Deferred: unauthorized
error mapping (slice 3), private/grant writes (slice 4).
Refs #424, #421.
2defc2f5 · dbf3dbe6…diff - SDK/WASM cross-session grant delivery: pull queues, accepting applies (#508)
e0bb183e · dbf3dbe6…diff - a stale core stops saying grantSealed and starts saying rebuild: the SDK suite stamps what it was built from (#914)
sdk/wasm/ is generated by wasm-pack from crates/loot-wasm and ignored by git AND
by loot, so it is the one input to the SDK suite that no checkout, land or lane
spawn ever updates — and nothing checked it. The failure did not look like
staleness: measured during #880, an out-of-date core produced 14 failures inside
test/grants.unit.test.ts naming an arity mismatch on grantSealed, which reads as
a defect in sealed-grant handling. It cost a diagnosis aimed at the most
security-sensitive code in the repo, for a bug that did not exist.
So the value is not preventing the stale run, it is making the stale run SAY SO
— the same shape as #843 and #847. `build:wasm` now records a stamp of what it
built from; `pretest` compares it and refuses, naming the rebuild and the #880
misdiagnosis so the next reader does not repeat it.
Content stamp, not mtime: this repo materializes whole trees routinely (every
lane spawn, adopt, op restore), so mtime would cry stale on a correct build
several times a day, and a guard that fires on a healthy tree gets switched off.
The hash covers loot-wasm path-dependency CLOSURE (walked, so a new path dep
joins on its own) plus the workspace manifest and lockfile — not all of crates/,
which would demand a 30s wasm rebuild every time an unrelated crate moved.
Proven in a lane: not-built refuses before vitest, a real wasm-pack build then
passes the check, and one appended line in loot-codec is caught. The behaviour
suites additionally need target/release binaries in the checkout — README now
says so, since a fresh lane fails them with ENOENT until cargo build --release
runs.
151884a4 · dbf3dbe6…diff - the delta reaches the TypeScript SDK as a SHAPE rather than as prose, and it is the FIRST INSTANCE of #1763 rule rather than a bespoke design: DeltaShape::of is a PROJECTION of the seam and nothing else - the mark is the #306 gutter rather than a new alphabet, the rung is the LineDelta variant, the counts are its own counts and the tally is the disclosure verbatim - so nothing here was hand-designed and the shape cannot drift from what the human rendering shows. three decisions carry the weight. added and deleted are NEVER ZERO where the count is unknown, they are absent, exactly where --stat calls a row uncounted, and ONE function now feeds both channels so the two cannot disagree. a sealed row WITHHOLDS the path, the from-path and the recipient list in both channels, because a path name is CONTENT under #306 - the shape refuses to leak through the encoding what the prose refuses to print. and the machine channel is ONE SHAPE REGARDLESS of --content and --stat, verified byte-identical, because those flags pick WORDS over a delta while this picks an ENCODING of it. the contract number is the shared VERDICT_CONTRACT and not a per-verb one, which ADR 0023 already answered for every shape and #1516 declined explicitly for porcelain, and an SDK test asserts diff and status report the SAME number so a per-verb version would go RED. a defect was caught BEFORE the freeze, which is the only time that is cheap: the first encoder used to_string_lossy, so on Windows a FROZEN contract would have shipped a backslash path beside a human line printing a forward slash - caught by the pin that compares the two RENDERINGS rather than asserting each is non-empty, and its unit pin uses an EMBEDDED backslash rather than a nested path, because a nested-path fixture is vacuous on POSIX. wire names stay snake_case deliberately, since a camelCase mirror would be a second vocabulary for one frozen contract and the only thing it could do is drift. nine mutations with counts read, and the ceiling comes DOWN 57 to 56 - the first entry on that list to PAY rather than be excepted (#1554)
ac5700af · dbf3dbe6…diff - the TS SDK gains seek(): an agent searches a repo without a clone from JS, over loot seek --json through the existing subprocess runner, typed at both ends, with the question a typed object never a string and the answer the verb own JSON parsed as it prints it, every key the binary writes and none it does not; seekRead(path) is its own call returning the recorded bytes verbatim through the streaming half of the runner, since the buffered half decodes stdout as text and the verb refuses a machine format for a read; both stand alone with a cwd and a binary, and both ride LootCheckout from openRepo with the checkout own runner. a refusal of a listing or a search arrives by class, never by prose, while a read refusal is the sentence on the generic class because the verb takes no machine format for a read, said on the doc and in the README: the verb own slugs and the CLI-shared ones map onto the SDK taxonomy in one function over the one parser of the coded stderr line, now in errors.ts beside the classes and called by the physical adapter too, bad_revision and no_such_remote as NotFoundError, no_identity, no_repo and unknown_flag as SetupError, conflicting_flags, bad_flag_value and read_is_bytes as a new InvalidQuestionError under a new invalid code, and multi_head as MultiHeadError under the same invalid code rather than conflict, which errors.ts reserves for the moved-parent family, carrying the heads parsed off the one line shape the binary prints them in, so the next call pins one with at; the shared slugs are classified once, in the table the physical adapter uses, so bad_flag_value is the invalid class for every verb. the behaviour suite reads loot seek --schema and asserts the SDK tables are the binary own: every flag the verb declares bar --read and the three the SDK spells itself is a field, every verb or shared slug bar error has a class of its own, and the example answer keys are the typed ones. seekArgs is pure and pinned flag by flag without a binary; the behaviour test drives the release binary on a listing of the three seeded paths whose target resolves to an absolute root, a search with several patterns and a count, names only, a clipped line against its whole, a bound, --name, a read of text and of binary bytes, a git target by directory with a null visibility, each refusal class including a missing binary, no identity from outside any repo, a remote name that names nothing and a window over a local target, and the multi-head heads read off the refusal that loot seek --schema renders through the verb own raiser rather than a copied string. the README gains the section and CONTEXT the sentence; the browser LootRepo is untouched, since the WASM core cannot run the binary, and its stateless read stays the map --via api follow-up. nothing in the land gate runs the SDK, so the counts here are the claim: npm run typecheck clean and npm test 114 passed over 11 files in the lane, against release binaries built in the lane; red under mutation, counts read each time: seekArgs dropping --name (3 failed, 8 passed), spelling count as -l (4 failed, 7 passed), forgetting --fresh (3 failed, 8 passed), a multi-head refusal on the generic class (2 failed, 9 passed), parseHeads reading no subject (2 failed, 9 passed), a bad revision as the invalid class (1 failed, 10 passed), seekRead decoding the bytes as text (1 failed, 10 passed), the checkout seeking from the process directory (1 failed, 10 passed), a missing binary on the generic class (1 failed, 10 passed), and a shared slug falling to the generic class (2 failed, 9 passed). no rust, no migration, no wire or format byte moves and no forge or relay byte moves, so this owes no deploy; the workspace suite is untouched by a TypeScript change and was green at the base (3999 passed over 124 binaries, 7 ignored, at b1e5791) (#2116)
7997cffe · 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 readiness waits over a spawned binary in sdk stop being budgets and start ending on the host itself, and the reason they were never seen to fire is now written where a reader meets them: every behaviour suite that stands a relay up polled a real loot serve behind Date.now() plus 20 seconds, which is a SMALLER budget than the 30 seconds that already lost in #1693 over a LARGER startup, and the 60 second beforeAll hook outside it sat inside the same measured spread, so raising the inner number alone would have left the effective bound where it was. the new sdk/test/spawned-host.ts has three endings and only the last is a clock: the probe succeeding, which is the condition these suites actually need since it proves the host ANSWERS rather than that it printed a line; the child exiting, reported with its code instead of polled at for the rest of the wait, seeded from the childs recorded state as well as its future exit event so a host already gone is not waited on; and a backstop carrying the same 600 seconds tutorial.rs carries, off the same saturated measurement and for the same reason, with the hook timeout derived strictly larger so the named refusal wins the race. the fixture generator imports that module rather than restating it, which node does by stripping the types itself. THE BEHAVIOUR SUITES CANNOT PIN ANY OF THIS AND THE CONTROL SAYS SO: with the condition deleted outright the whole sdk suite still read 11 files passed, because an idle machine has the relay up before the next await, so a dedicated pin over a spawned node child carries it instead. red under mutation, counts read each time: the exit ending deleted failed two of the three pins on vitests own timeout (2 failed and 1 passed, both at 5000ms, the wait running on to the backstop), the already-gone seed dropped failed the third alone (1 failed and 2 passed), and the probe discarded so the wait returns on the first look failed all three (3 failed and 0 passed, asked 1 against 3 and two promises resolving instead of rejecting), each restored to 3 passed and the sdk suite to 129 passed over 12 files. the gate question is NOT answered here and #2080 still owns it, but the sdk README now states what decides it and gives the grep that checks it rather than listing the workflows, and tutorial.rs stops naming budgets that no longer exist. no migration, no wire or format byte moves and no host behaviour moves, so this owes no deploy. the workspace suite is green (4178 passed over 133 binaries, 9 ignored) (#1914)
2cac70f9 · 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 tutorial interrupt red #1916 read as a removal that lied is a tree made again after the sweep: on the linux runner std remove_dir_all answers success only once the root is gone, the captured transcript prints the first prompt ahead of the handler line, and walk, still running on the main thread beside the handler, opened with a create_dir_all over alice that minted the root back before ask beat the heartbeat into it. two delays injected into that sequencing reproduced the red and its alice, heartbeat remnant on this desktop with confirm_gone in place, so that check alone would not have closed it; Sandbox::create now makes alice before the handler is armed, the same two delays pass, and a unit pin drives the sweep, the walk and the beat in that order. what stays open is said at the fix: other writers, the relay thread and the loot subprocesses among them, can still put a path back mid-step. the CI run recorded the two readings and not the sequencing, so the mechanism is established by reading and by the reproduction rather than observed there, and workflow.md stops calling the report and the look one moment. the loot-s3 client keeps its 60 s budget for want of any measured rate to its bucket, but a request that runs out now names it and what it covers, where reqwest said error sending request or error decoding response body with no word of a deadline. the sdk doc stops claiming the native policy: the formula is copied, native allows the send budget between answer chunks where the sdk allows idleMs, the rounding differs, wire.rs now points back at the copy, and the unit test reads the floor from its constant. smaller: the engine.rs verdict compares the band against the band, the measure.rs refusals state a property instead of counting callers, the fixture generator import note sits at the import, and transportFailure takes a role rather than a string. red under mutation, counts read each time: create_dir_all put back in walk (11 passed and 1 failed, holding alice and heartbeat), the s3 timeout naming removed (12 passed and 2 failed), the sdk upload rounded down (6 passed and 1 failed), each restored to 12, 14 and 7 passed. no migration and no wire or format byte moves; the forge and relay deployables word a timed out object store request differently once next deployed, and no code in the tree matches on that text, so this owes no deploy. the workspace suite is green (4192 passed over 133 binaries, 9 ignored) and the sdk suite is green (137 passed over 13 files) (#2254)
bdeb6a0c · dbf3dbe6…diff - the land now type-checks sdk/: a new sdk gate, right after the site gate in orchestrator::GATES, runs npm ci, npm run build:wasm and npm run typecheck in the landing position sdk/ (hygiene::SDK_GATE_STEPS), refusing on a failed build or a type error, riding the merged-tree re-gate, skipping under --skip-tests as sdk=SKIPPED, and degrading a provably offline npm ci to sdk=SKIPPED-OFFLINE with a block of its own, so the verdict line gains sdk= after site=; the two npm gates now share run_npm_gate and supersede_offline. the land was chosen over ci/test-main.sh because only a land refuses before main moves and GitHub Actions had not run since 2026-09-19, and over the site gate step because site= would then speak for two packages. measured in a fresh lane: npm ci 1.4 s, build:wasm 17.8 s cold but 1.7 s once the site gate has built the same crate, tsc 1.0 s, the whole gate 5.05 s. proved live through the sdk row with real npm, wasm-pack and tsc: clean, red at npm run typecheck with TS2322 for a string assigned to a number in sdk/src/hex.ts, clean again once removed. ADR 0071 gains the #2080 amendment and CONTEXT.md an SDK gate entry, and workflow.md, the land-change skill and sdk/README.md say what runs and that npm test still does not. red under mutation, counts read each time: the typecheck step dropped (7 passed, 2 failed), the sdk row running the site gate (5 and 4), the sdk= field reading the site slot (7 and 2), the supersede rule reading one fixed gate (8 and 1), an offline install refused rather than degraded (7 and 2), each restored to green. a land runs the primary release loot-first, so the gate is live once those binaries are rebuilt; no migration and no format byte moves, so this owes no deploy. the workspace suite is green (4322 passed over 137 binaries, 12 ignored) (#2080)
072abcc1 · 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.