Changes touching this path

  • repo_member gets the producer it has never had and it is the site's: migration 0013 creates forge_member_writer holding SELECT, INSERT and DELETE on repo_member plus SELECT on account_key and repo and nothing else, named writer rather than forge_member because ADR 0077 reserves that name for #1726's read role - and a test enforces the reservation. The load-bearing half is that membership is addressed by pubkey and STORED BY ACCOUNT: the owner names a collaborator pubkey, the server resolves it through account_key to a tenant_id, and storing the key instead would silently un-member anyone who rotated, which ADR 0016 makes an account fact and #927 already taught the push path. That resolution deliberately ignores retired_at, proved by adding the filter and watching a rotation test refuse a member who had merely rotated. The site module is a SIBLING of identity, read and owner rather than a widening of any of them, and the route reuses withOwnedRepo for authorization while every membership statement ALSO joins repo on owner_pubkey, so the database refuses a stranger independently of the route - proved by dropping that clause and watching a stranger's repo really take the row. Thirteen guards were each broken and read before being restored, and the one worth naming is the column-ACL half: a live GRANT UPDATE (tenant_id) ON repo_member PASSES the set equality and is caught only by the column check, because has_table_privilege cannot see a column-scoped grant, and its positive control revokes 0007's own column grant to prove the query can see one at all. The accountless refusal is its own sentence rather than a foreign-key error because a keypair is free and infinitely mintable, so an accountless member can be neither quota'd nor banned; and the honest-revoke copy interpolates MAX_PRESIGN_SECONDS from read/blob.ts so it cannot drift from the sixty-second window it admits is small and not zero. account_tier.rs's assertion is untouched and passes unamended, which is what ADR 0077 predicted - what needed repointing was #1639's own replacement header, true when written and false on landing. ADR 0059's count of the site's Postgres URLs was a live count, is now four, and is named rather than counted. Membership is write-only until #1726 and #1728 give it a reader, and provisioning is a different repo, so the route throws until scripts gives the role LOGIN (#1730) (#1725) 4fac0c99 · dbf3dbe6…
  • 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…diff
  • 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 second sweep's fix-up, and the guard at its centre could never have failed: it asserted that a view statement contains the member family prefix, while the statement BEGINS with create or replace view forge_member and then the view's own name - so the needle matched the name it was reading - and the two assertions beside it were absences that an UNGATED view also passes. The replacement computes the TRANSITIVE set of views reaching the gate, matching whole identifiers against each view's body rather than its header, and the contrast is committed rather than transcribed: a synthetic family per gate carrying an ungated base-table view, a shadow-prefix trap, and a one-hop and a two-hop view, asserting the OLD needle green on the first two while the new check refuses them, with the chained views as positive controls - a spelling that demands a direct FROM the gate fails that control, which is exactly why it is not the fix. The same shape had left the owner family unguarded since #1043, and both were red-proved on one planted break, deleting the join from a chained view and reading back that it reaches its gate by no chain at all. Beyond the ticket, migration 0003 had NO reach check whatsoever, only the outer-join tripwire, so the ANONYMOUS family was the least guarded of the three rather than the most; it gains the same check, red-proved the same way, and all three migration files are restored byte-identical with md5 verified. The night's dominant defect was counting: two different roles were each called the fourth role, ADR 0077 called the same thing a third family and a fourth family 284 lines apart, and ADR 0059's heading said FOUR and they are named rather than counted from here on, directly above a sentence listing five. Roughly twenty sites now list NAMES instead, because a corrected ordinal drifts again on the next role while a list of names cannot, and the arithmetic is settled against a live cluster rather than by reading - pg_views reads fifteen, eleven and eleven, so thirty-seven views in THREE families, which makes ADR 0077's own amendment the half that was wrong. The sentence that had been wrong three separate times is made CODE-DERIVED and stops being prose anyone must remember: removal now asks whether the member read tier is configured, the same question the pool itself asks, so the web half follows the deployment - while the sync half stays prose and is NAMED as the one line left to remember, because nothing in that process can observe the access class. The tenant validator is fixed in the CODE rather than in its claim, since the claim is what later readers rely on and narrowing the regex to eighteen digits would reject legal ids: set_config was measured accepting a nineteen-digit value with the first member query raising out of range, so a range check joins the pattern and the maximum itself is asserted as the positive control (#1739) 74afeacc · dbf3dbe6…diff
  • the sync surface learns membership, which is ADR 0077's other half: AccessClass::of stops being a byte compare and becomes owner-or-member, with the owner compare staying FIRST because a repo can have an owner with no account at all - repo carries no tenant_id and a push creates no tenant - and the membership arm resolving through a LIVE account_key row, since a rotation retires a key precisely so a leaked one stops working and reading a collaborator's sealed metadata is what a leaked key would be used for. The read is a new trait method with no writing sibling, and liveness stays in the CALLER so the two stores cannot disagree about #927's rule. Two breaks prove it and the interesting one is the small break: resolving membership before the owner compare moves exactly ONE cell of twenty-one, which is the point rather than a weakness, because a matrix without an accountless owner in it would not have moved at all. The dangerous shape is the other one - resolving from account_key alone with no repo_member join, which makes every account a member of every repo - and it turns eight of twenty-one green including two whole columns; the same break is red in SQL against a live cluster, and the unjoined query is run BESIDE the shipped one inside the test and asserted to admit the repo it should not. The write rules are pinned by #1650's method rather than described: the whole matrix of five signers across both door positions, run with membership rows planted for every account and again with none, demanding identical rendered answers including the message text, with a control that those same rows really do move the read class - and folding membership into require_pusher flips the contributor cell from a 403 that names another account to ok, which IS a stranger moving the owner's ref. The may_ship_key pin found something that strengthens ADR 0077's sixth decision: widening the class ships no key that was not already world-readable, and a member gets the Internal object's BYTES with the key set asserted as exactly the published oid rather than as an absence - but making may_ship_key answer true left the case GREEN, because verify_publication independently refuses a non-world object, so the lane is closed THREE times and not twice and the pin cannot be broken by widening Member in any spelling. A gap the ticket and the ADR both missed is pinned rather than fixed: a rotated owner's OTHER live key reads Anon on their own repo while require_pusher admits it, which this change did not cause since a byte compare answered the same way, but the new matrix makes it visible - the remedy needs no new predicate, only a repo_member row for the owner's own account. The claim that there is exactly one read method here was a live count in nine places and staled the moment a second arrived, so all nine become names and no number is written anywhere (#1728) 1fc1f6a2 · 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.