Changes touching this path

  • RLS arrives on the trigger #702 named rather than on a schedule, because #1725 made it fire the same night: forge_member_writer holds SELECT, INSERT and DELETE on repo_member and SELECT on account_key and repo, which are base tables and not views, and its owner-scoping lived entirely in three SQL statements the site writes. It also held a bare unpredicated SELECT on repo, so it could enumerate every namespace on the forge including the metadata-private ones. Migration 0014 enables RLS on eight tables - the schema header's own repo-scoped group plus proposal, which 0011 added to that group after this ticket was written - and the verdict on the forge role is that it is UNCONSTRAINED, said out loud rather than decorated: all eight of its policies are USING (true) WITH CHECK (true), named forge_ingest_is_unconstrained so the catalogue itself prints the admission, because it writes every tenant from one process with no request-scoped identity and a predicate would be either inert or an outage. What ENABLE does buy is exactly this ticket's condition 2, the accident it was deferred against: a role no policy names now reads zero rows, so an ALTER DEFAULT PRIVILEGES that hands a reader every future table is answered by the tables themselves. The per-transaction ingest binding that would make the forge role genuinely constrainable is filed as #1732. FORCE ROW LEVEL SECURITY was proved rather than assumed, and is deliberately NOT set: on a real cluster a postgres-owned table with RLS enabled AND forced and zero policies still reads 2 of 2 as postgres, where a NOBYPASSRLS role reads 1 of 2 at the same moment, and a non-superuser-owned table reads 2 without FORCE and 0 with it - so the mechanism works, it is inert against a superuser owner, and setting it would be precisely the decorative control this ticket forbids. The red proofs are the output that matters and one of them corrected the method: a policy must be proved by REPLACING its predicate with true, never by dropping the policy, because enabled-with-no-policy is deny-all and returns zero, which proves nothing about the predicate - the first draft did exactly that and it is recorded so the next reader does not repeat it. With the predicate widened instead, the cross-owner INSERT lands and owner B's roster gains a collaborator its owner never named. One thing only a real run could find: membership.pg.test.ts's foreign-key case went red because the policy now stands in FRONT of the constraint, so an unscoped statement never reaches the foreign key the test is about, and left alone it would have quietly become an RLS test named for the wrong mechanism - so it is fixed and the RLS half split into its own case. The site half ships in the same change because it must: membership/db.ts gains withOwner, owner/db.ts's shape minus READ ONLY, and without it 0014 empties the membership surface (#702) c562196e · dbf3dbe6…
  • the sweep's fix-up, and the finding that mattered most is one no single-lane review could see: docs/sql/forge-schema.sql still read that repo_member and tenant_blob remain producer-less, false since #1725, and #702 had EDITED that very file and left the line standing - which matters because account_tier.rs scans this file and its failure message sends a reader here first, while CONTEXT.md had already been corrected to say tenant_blob is the only table in the tier without one, so the two documents contradicted each other outright. Migration 0014 derived seven tables and enabled eight, with repo_member appearing nowhere in the derivation, so the set is NAMED now rather than derived at three sites and the schema header's own bullet says not to derive 0014's set from it. The account tier had THREE mutually inconsistent lists and the tie is broken by a fourth the ticket did not know about: account_tier.rs carries the list as CODE and matches CONTEXT.md's five exactly, so store.rs gains the account_seed its own next sentence already named, and 0014 stops calling the tier untouched while carrying two policies on repo_member - the resolution being that repo_member is in BOTH tiers, account by tenant_id and repo-scoped by repo_id, and it is that second column that gives a policy something to key on, which is exactly why it is the one tier table 0014 can constrain. rls.rs asserted a COUNT in set language, and the red proof is the contrast rather than the failure: two separate breaks that each keep the cardinality at eight - enabling tenant while disabling proposal, and moving the ingest policy from proposal to tenant - make the new set equality print both lists and fire, while the old count assertions were left standing beside them and reported both passing under the break at 8 and 8. The claim that nothing in CI runs rls.rs is true, and the ticket's own scope was one test too wide: there are eight tests, seven need the cluster, and the eighth reads 0014's text and runs bare, so every site now says seven of eight and names what the eighth holds, in the register ADR 0077 already used for the view invariant, pointing at #1735 for the build. The honest-revoke copy was ADR 0038's failure with the sign flipped, promising that removal refuses reads when membership grants none yet, so it leads with refuses nothing new today and keeps the refusal as the future half that #1726 and #1728 turn on, pinned by a case that also refuses the old opening. Two scope-drift sentences are narrowed to the statements they are actually true of, and CONTEXT.md's copy of one is deliberately left alone because it says every WRITE, which is true of both (#1734) 750a3b64 · dbf3dbe6…diff
  • the collaborator can read someone else's namespace: migration 0015 adds the fourth role forge_member and eleven forge_member views, one per relation 0009 serves, so the shared prefix-parameterised TreeReader reads either family unchanged - and the gate is strictly stronger than 0009's, because the app names only the account the Clerk session resolved to while the membership join sits INSIDE the barrier view, so the application cannot name a repo it is not a member of even by mistake. metadata_public is absent as both column and predicate, and repo_member is named exactly once, by the gate. Two premises did not survive, and the first is a defect in the decision itself: ADR 0077's and the ticket's literal current_setting of loot.reader_tenant cast to bigint DOES NOT fail closed. It fails closed for an UNSET parameter and not for a CLEARED one, because set_config with NULL leaves the empty string behind and casting that to bigint RAISES - which 0009 never met because decoding an empty string as hex does not. Proved by removing the fold from the shipped migration and reading invalid input syntax for type bigint, so the gate ships as nullif and then the cast, malformed values still raise deliberately, and withMember validates a positive decimal with the pin on both sides. The second is a defect in #1727, which landed earlier tonight: its exact-set spelling, that the views naming repo_member equal the forge_member family, is satisfiable ONLY by a family of one, because the ten chained views name the GATE view and never the base table - so a second member view gaining the roster, which is precisely the disclosure ADR 0077 forbids, read as CORRECTNESS under the old spelling. It is re-spelled to exactly forge_member_repo whenever the family exists, its vacuity guard extended to require the member family non-empty, and its positive control now plants four breaks where it planted two. The row-level-security interaction is proved rather than assumed, as three readings on one database in one moment: the seven enabled tables this family reads really carry relrowsecurity, read as a set; the role handed a direct SELECT on repo reads ZERO rows, which is 0014's own named accident and shows the policy system live for that role; and the same role on the same connection at the same moment reads its repo through the gate view and gets one. The mechanism is then named from the catalogue and red-proved by setting security_invoker, which takes the read to insufficient_privilege, and the failure text names the non-superuser deployment case so a future deploy diagnoses itself. The isolation break is ADR 0077's own named careless shape rather than an invented one - the gate keeps its repo_member join and loses only the correlation to repo, which passes every text guard in migrate.rs - and seven of eight tests fire on it. Rows are planted from tests and from the site, outside account_tier.rs's scan of src, the way rls.rs already recorded, so the tripwire is untouched and green. forge_member ships NOLOGIN and provisioning is a different repo, so the class answers 404 rather than 500 and is not an existence oracle (#1726) e76ca863 · dbf3dbe6…diff
  • the forge role is bound to one repo per transaction: migration 0017 replaces the permissive forge_ingest_is_unconstrained policies with forge_is_bound_to_one_repo, keyed on loot.repo_owner and loot.repo_name, which Pg::with_repo sets before any other statement, so an unbound statement reads no row of those tables and has every write to them refused. every MetadataStore method that reaches a row-secured table now runs in with_repo, held in source by a census test and in behaviour by the conformance suite run as forge, and the ref CAS is still the first statement that locks ref. tests/rls.rs proves the binding refuses cross-repo reads and writes and goes red when any one table predicate is replaced with true, and the recorded-decision test that pinned the unconstrained role is removed. pg::tests now starts its sessions as forge. ADR 0078 decision 3 is amended with the census, the deploy order and what the binding does not constrain, and CONTEXT.md, the schema header, ADR 0077 and the evidence doc follow (#1732) af1c95ee · dbf3dbe6…diff
  • the forge live-database suites now run in CI: test-main.yml stands up a postgres:18 service, provisions it the way setup-forge.js does including its default privileges, migrates it with the commit own loot-forge --migrate, sets every LOOT_FORGE_TEST database url the Rust and site suites read, and runs cargo test and then the site pg tests against it. LOOT_FORGE_TEST_REQUIRE_DATABASE turns a database test skip into a failure through pg::live_opt_in and the site skippingLive helper, and with it set and no url 91 Rust tests and all six site pg files fail, while two half-suites that used to skip silently now say so. The default-privilege tripwires run in CI by decision, and fail on a cluster without provisioning. A new migration that neutralises the roster policy passed the bare suite and failed rls.rs and membership.pg.test.ts against the cluster. It is a canary on landed main and not a pre-land gate, and the prose that said no workflow sets these urls now says that instead (#1735) c0931f78 · dbf3dbe6…diff
  • migration 0017 and ADR 0078 now state the deploy window setup-forge.js leaves by migrating before it swaps the container: until the new binary is up the old one reads empty repos and has pushes refused, nothing is corrupted, and the order is kept because migrating first is what the deploy script relies on for every release (#573); the checksum pin stays green and went red on a one-token statement edit, and 0017 says statements rather than bytes. CONTEXT.md and ADR 0078 stop saying no CI runs the live suites, the rls.rs header names the key the workflow sets and says the cluster tests are the ones that return early, and the 2026-09-08 evidence entry gets a dated follow-up rather than a rewrite. binding_census now walks every file under src/pg instead of a hand-kept pair, reads fn items of any visibility, follows SQL held in a module-level const and drops cfg(test) items and files, and Pg::with_conn, with_txn and with_repo are private to pg so the compiler keeps other modules from issuing statements through them. a planted pub(crate) method in a new pg/planted.rs reading such a const went red under the new census (1 passed, 1 failed) and stayed green under the old one, and removing each widened shape turned the census red. the source_walk message on flags.rs drops its stale six-lines count, and a new assertion holds that flags.rs calls into the module on more lines than it has consumers, which went red when that premise was broken. the table counts in CONTEXT.md and pg/mod.rs point at the constants that hold those sets rather than restating a number, pg/mod.rs points at the path include instead of listing its includers, the Admitted doc says within and within_leaf read through the leaf spec they are handed, the test-main.yml provisioning comment names the grant repair it drops as a no-op on a fresh cluster, and the rewrap leftovers are fixed. the bare workspace suite is green, and the loot-forge suite is green against a throwaway cluster with the CI env block (#1953) f2cbbb4c · dbf3dbe6…diff
  • withdraw and its close can no longer be separated by another writer: close_proposal takes the row its caller read and closes only if that is still the row on file, compared as a whole row, answering false otherwise, and withdraw reads again and decides again when it does, as admit has since 1747, so an author whose tip a carrier moved in between meets the author check against the moved row. the reference store compares under its lock and the postgres driver reads the whole row with FOR UPDATE in the transaction that already read its state; no migration. a conformance case holds a carrier amendment between the read and the close, and it went red on the old code on both stores (0 passed, 2 failed) with the withdrawal closing a row whose tip another key wrote; a second pins the compare on both stores, and a postgres case holds the row from another session inside the call and went red with FOR UPDATE removed. ignoring the answer in withdraw, or removing either store compare, also turned cases red. binding_census now opens a fn item only outside another fn body, found by counting braces outside the string and char literals and comments it recognises, and fails a file whose count does not close; a planted pg/planted.rs method holding a nested fn before unbound SQL went red under the new census (2 passed, 1 failed) and stayed green under the old one (1 passed, 0 failed), and a fixture case with unbalanced braces in literals went red on the old census and on the new one with its string or char handling removed. the visibility fixture now covers pub(super), pub(self), pub(in path) and private methods, the stale upsert prose and the statement count in pg/meta.rs are fixed, the misplaced memory.rs comment moves to its arm, CONTEXT.md points at the matrix doc instead of a column count and adds the 1955 caveat, the evidence entry calls the 1956 mechanism suspected, the rewrap leftovers in model.rs, ADR 0078 and conformance.rs are fixed, and ADR 0075 and CONTEXT.md record the withdraw follow-up. the loot-forge suite is green against a throwaway cluster with the CI env block and bare, and the rest of the workspace is green bare (#1959) e3d4cd50 · 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.