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 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 - the live-database checks test-main.yml runs can run locally, from the same script CI runs, because the account is out of Actions minutes and a land never ran them: the provisioning, the database keys and the suites move out of the workflow into ci/test-main.sh, which builds every connection URL from a host and a port it requires rather than defaulting to the one a real cluster listens on, and the workflow keeps only what prepares a fresh runner and calls it. ci/local.sh starts a throwaway Postgres on its own port, refuses if anything already answers there, runs that script against it and deletes the cluster on exit, so it runs beside a real cluster and from a lane before a land. run in a lane against a portable Postgres 18.6 it applied forge migrations 0001 to 0017 and the three site migrations, and 122 cargo test results and all seven site live files passed with LOOT_FORGE_TEST_REQUIRE_DATABASE set, so none skipped, before and after the review fixes. with the returning alias put back into read.ts it failed at the site step on syntax error at or near returning, and pointed at the port a real cluster owns it refused with exit 2. the CI-shape pin reads through the script and its STEPS list, because the job is still named cargo test --workspace and a check on the workflow text alone would pass on the name after the command had gone; it went red when the workflow stopped calling the script and when the script dropped --locked. the comments and living docs that said the workflow sets the keys now say the script does, the dated ADR and evidence entries are left as history, and AGENTS.md, docs/agents/workflow.md and the afk-loop security hunt point at the script. the Actions wiring itself is unexercised until minutes return (#2061)
8ff6117c · 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.