Conversation
…314) Every one of these has cost time, and the shape is always identical: a change is made, something reports success, and nothing happens. A table of every configuration surface — GitHub variables, the two env files, the host env file, config.alloy, the Caddyfile, the Grafana JSON — saying when a change reaches the running system and whether a deploy overwrites it. Then the three that are genuinely surprising. docker restart does not reload env_file. Docker reads it when it CREATES a container, not when it starts one. Editing /opt/swarm_connect.env and restarting changes nothing, and the container reports healthy on the old configuration. That cost an incident: the pool target was lowered, the container restarted, and it kept the old target of five while holding two — reporting low_reserve_warning true and preparing to buy three replacements, the exact opposite of the change being made. A bind-mounted single FILE pins its inode. git pull replaces the file rather than editing it, so the container keeps reading the old one, and compose correctly does nothing because the service definition is unchanged. Changes to config.alloy were silently dead from 2026-08-25 until #301. Not everything in the repo is deployed. deploy/Caddyfile and the Grafana JSON are version controlled for review and history; nothing applies them. They drift, and overwriting the live Caddyfile from the repo without diffing first once reintroduced tls internal and broke TLS for two minutes. The rule it lands on: changing a setting takes BOTH a gh variable set, so the next deploy keeps it, AND an env file edit plus force-recreate, so it applies now. Only the second and the next deploy reverts it; only the first and nothing changes until someone deploys. Docs only. Full suite: 1075 passed, 25 skipped.
* Stop strangers storing data on batches the gateway paid for check_access() allowed any caller to use a batch absent from the ownership registry, commented "backward compatibility" for batches predating it. The set that default actually covered was not legacy callers. Every path by which a caller obtains a batch already registers it — /pool/acquire, POST /stamps/, /stamps/for-owner. What it covered was the POOL'S OWN INVENTORY: ownership was recorded when a caller ACQUIRED a batch, never when the pool bought one, so everything sitting in the pool was untracked and therefore writable by anyone who knew its id. Batch ids are not secret; GET /stamps/ lists them. A production pool batch is 50% utilised having never been acquired. That is capacity the gateway funded, consumed by someone who never asked for it, on batches it pays roughly 0.018-0.14 BZZ a day each to keep alive. Two changes, in the order the fix requires: Pool inventory is registered as POOL_OWNER at purchase (add_stamp_to_pool) and adopted on sync, so batches bought before this change are protected too rather than staying open for the rest of their lives. Registration failure never fails a purchase — the batch exists and was paid for — but logs that it is unprotected until registered. check_access then refuses POOL_OWNER outright, and refuses untracked batches instead of allowing them. Acquiring re-registers the batch to the caller, which is the only way to get one. STAMP_OWNERSHIP_ALLOW_UNTRACKED restores the old behaviour, for one situation: STAMP_OWNERSHIP_FILE is lost, every batch becomes untracked at once, and legitimate owners would be locked out of batches they paid for. A test pins that permissive mode does NOT reach pool inventory — recovering from a lost registry must not reopen the hole it is recovering from. tests/test_stamp_ownership.py::test_untracked_stamp_allowed asserted the old behaviour and now asserts the new one. Three tests added: permissive mode, pool inventory refused to paid/free/anonymous callers alike, and permissive mode not unlocking pool inventory. Full suite: 1078 passed, 25 skipped. Closes #312. * Test that the pool claims what it buys, not just that the check works The enforcement tests construct registry state by hand, so they pass whether or not any code registers anything. Nothing asserted that add_stamp_to_pool or sync_from_bee_node call register_stamp at all — meaning the lock was tested and the door was not, which is the same asymmetry that let #312 stay open. Three tests: a purchased batch is registered to POOL_OWNER with source pool_purchase a registration failure does not lose a batch already paid for sync adopts pre-existing inventory, so batches bought before this change are protected rather than staying open for the rest of their lives The second matters because the batch exists on chain regardless: raising there would drop it from the pool while the money is spent, which is worse than an unprotected batch. It is logged instead, and the log says it is unprotected. The sync test initially failed with synced == 0 — the fixture omitted `local` and `usable`, which sync filters on. Fixed by matching the record shape the other sync tests use. TEST_STRATEGY.md records why both halves are needed. Full suite: 1081 passed, 25 skipped. * Report gateway inventory as 'pool', not as free for anyone to use Found reviewing this PR. accessMode is computed as: "owned" if ownership_info.get("mode") == "paid" else "shared" Pool inventory registers with mode="pool", which is not "paid", so it fell through to "shared" — the value that tells a client the batch is free for anyone to use, about exactly the batches check_access now refuses. A client reading the listing would believe it and be denied on upload, which is worse than the original defect in one respect: the API would be actively misleading rather than merely permissive. Neither existing value was honest. "shared" invites use; null means "unknown to the registry", which it is not. So the Literal gains "pool" and the field description says how to obtain one — acquiring re-registers it to the caller. Two tests: the owner-to-accessMode mapping for all three cases, and that the Literal permits "pool" (it is validated at serialisation, so an unlisted value would fail at runtime rather than at import). provenance-smasher asserts accessMode membership against a fixed list and would have failed on every pool batch. Covered by provenance-smasher#31, which must merge before this deploys. Full suite: 1083 passed, 25 skipped.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #312 on production, where the hole is still open.
Merge with a merge commit, not a squash — the merge base is currently correct (
f26f695) and this PR shows only the 10 files that actually changed. Squashing would break that again.Why this matters now
Production's batches are all untracked, which means writable by anyone who reads a batch id out of
GET /api/v1/stamps/:The gateway pays roughly 0.018–0.14 BZZ a day per batch to keep these alive.
What lands
Ownership.
check_accessrefused nothing when a batch was absent from the registry, commented "backward compatibility". The set that covered was not legacy callers — every path a caller obtains a batch through already registers it — it was the pool's own inventory, because ownership was recorded on acquire, never on purchase. The pool now claims what it buys, and adopts what it already holds on sync, so batches bought before this are protected rather than staying open for the rest of their lives.accessModereportspool. Found reviewing the above: the mapping is"owned" if mode == "paid" else "shared", and pool inventory registers withmode="pool"— so it would have been reported asshared, the value that tells a client a batch is free to use, about exactly the batches now refused. Permissive-and-honest would have become actively misleading.STAMP_OWNERSHIP_ALLOW_UNTRACKEDrestores the old behaviour for one case: the registry file is lost, every batch becomes untracked at once, and legitimate owners are locked out of batches they paid for. A test pins that it does not reach pool inventory — recovering from a lost registry must not reopen the hole.Config propagation documented in CLAUDE.md: what takes effect when, why
docker restartdoes not reloadenv_file, why a bind-mounted file pins its inode, and which repo files are never deployed.Verified on staging after deploy
Three distinct states, each honest.
Ordering
provenance-smasher#31 is already merged. It had to be:
ACCESS_MODESdid not include"pool"and would have failed on every pool batch.Effect on production at deploy
Existing inventory is adopted on the first sync, so the currently-open batches are claimed without manual intervention. The 50%-utilised batch keeps its data — this stops further writing, it evicts nothing. Downloads are unaffected and remain free.
Pre-flight
0.233.c1131fc, bee healthy, 134 peers{20: 1}, no deficit