Changes touching this path
- a long-lived container holds no credential: the forge and the site seed their environment from a file the container env only points to - ADR 0059, the loot half of (#1034)
27df4dc9 · dbf3dbe6… - the browser opens what the forge cannot: own-private read ships an owner read tier, a once-per-device unlock, and a grant mailbox that never acks (#1043)
c8dd4da1 · dbf3dbe6…diff - 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…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
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.