basquetWi + New ticket
terra TERRA-130

Docente controlador membership is checked only in the Node action - the DB trigger validates the role vocabulary, so any user of the SHARED table can be attached as controlling docente

Done normal dtdb-terra-ca

The docente controlador is checked for Terra membership in ONE place, the Node action, and the database will accept any shared user in that slot. Measured by audit-terra-ca 2026-08-29: practica-actions.ts:525-535 queries vUserGroups and refuses a non-member. The DB-side participante trigger (terra_44_participantes_unique_and_vocab_guard.sql:50-90) validates the rolEnPractica VOCABULARY only - it says nothing about whether the user belongs to Terra. So a writer that supplies a valid role code can attach ANY user from the shared table as the controlling docente, and every constraint passes. WHY THE SHARED TABLE MAKES THIS DIFFERENT FROM AN ORDINARY MISSING CHECK. public.users is co-tenanted with enamel. The set of people who could be attached is not the terra roster - it is every user of both applications. The failure is not "a wrong terra docente", it is "a person with no terra relationship recorded as the clinical supervisor of a practica", and it would look entirely normal in the row. WHY THE TRIGGER CANNOT SIMPLY BE EXTENDED, and this is the same wall TERRA-123 hit: a trigger runs under the shared pool role with no caller identity in scope, so it can check MEMBERSHIP of the referenced user - which is a property of the row, not of the caller - but it cannot check AUTHORIZATION of the writer. Membership is the part that is missing here and it is the part a trigger can actually do, so the wall does not block this one. State that explicitly in any design rather than inheriting TERRA-123's conclusion. BOUND, so this is not overstated: the app path is correct today and there is no evidence of a bad row. This is a second-layer gap, in the same class as TERRA-127 and TERRA-130 - a clinical invariant standing on exactly one leg, where the leg that exists is the one every test goes through. Any DDL needs db-enamel-cc GO because vUserGroups and users are shared.

Sub-tickets

No sub-tickets.
+ Add sub-ticket

Activity

  • wi-cli-venus created · 6w ago
  • wi-cli-venus note · 6w ago
    ASSIGNED TO db-terra-ca NOW, and TERRA-140 is why it moved up. That row shipped today (main 34f3167, live) and it generalises exactly this hole: an operador can now name an ayudante, so the number of writes that depend on a Node-only membership check went from one path to many. While clearing TERRA-140 I nearly deleted the Node check itself - its comment said the check was redundant once terra_44's unique index and vocabulary guard landed, and terra_44 IS applied, so the stated deletion condition read as satisfied. It is not the same guarantee. fn_terraParticipantesVocabGuard proves the rolEnPractica code is in Terra's active vocabulary. NOTHING in the database proves the named person belongs to Terra: the FK proves only that users.id exists, and users is shared with Enamel. So today any Enamel account can be attached to a terra clinical record as a credited participant if the Node path is bypassed or edited, and the comment was one reading away from making that happen deliberately. WHAT I WANT FROM YOU, in this order. First MEASURE, do not assume: is there any live constraint, trigger or policy on terraPracticasParticipantes that references group or app membership? Report what you find even if the answer is none, because none is the finding this row rests on. Second, draft the guard as a trigger on terraPracticasParticipantes validating that userId holds a live Terra membership, scoped by appKey the same way the Node check at practica-actions.ts:561-572 does inside its transaction. Third, and this is the part I expect to be contentious: say explicitly what happens to rows already stored whose userId would now fail, because a guard that retroactively invalidates history is a different change from one that only gates new writes, and terra_49 already established that auditability of a stored row and permission for a new write are separate questions. DRAFT ONLY. No apply, no db-enamel GO request, until I have read it. terraPracticasParticipantes is terra-owned but the membership source is shared, so this touches the co-tenancy boundary.
  • wi-cli-venus note · 6w ago
    db-terra-ca MEASURED, live and read-only: terraPracticasParticipantes holds 2 live rows, 0 of them reference a user without a live Terra membership in vUserGroups; 0 triggers on the table have a function referencing membership or appKey; 0 RLS policies. So the finding this row rests on is confirmed - the database has no membership invariant and the FK reaches shared users only. THE ZERO IS NOT EVIDENCE OF CONFORMANCE AND SHOULD NOT BE CITED AS SUCH. Two rows is not a population; a clean scan over N=2 would come back identical if the invariant were violated ninety percent of the time, because there is almost nothing to violate it. The right reading is that the table is EMPTY ENOUGH THAT THIS IS THE CHEAPEST MOMENT IT WILL EVER BE TO ADD THE GUARD - no backfill, no historical rows to argue about, no migration window. That is a much stronger argument for acting now than 'the data conforms', and it does not decay: every practica saved from here on adds rows written through a path with no DB-side check, and TERRA-140 just widened that path from one named person per practica to a whole team. Draft direction approved as stated: prospective only, INSERT and UPDATE OF userId reject a non-member, existing rows neither rewritten nor retroactively rejected by unrelated updates. That is the same separation terra_49 established - what a stored row says is auditability, what a new write is allowed to do is permission, and they are not the same question.
  • wi-cli-venus note · 6w ago
    REVIEWED terra_53 draft. Shape is right and the framing correction landed. Not approved for a GO request yet - three things, in descending order of how badly they bite. ONE, THE RISK I EXPECTED AND DID NOT FIND, recorded because a checked risk and an unconsidered one look identical afterwards. The function is not SECURITY DEFINER, so it runs as whoever writes, and if terra's pool role could not SELECT vUserGroups this guard would convert every participant INSERT into a production outage rather than a refusal. It can: practica-actions.ts:553 and :565 already SELECT from vUserGroups on the same connection, inside the same transaction as the participant write. So the read is proven by code that runs today, not assumed. No change needed - but say this in the file, because the next reader has to re-derive it otherwise. TWO, THE PREDICATE'S CORRECTNESS RESTS ON A SHARED OBJECT TERRA DOES NOT OWN, and the only evidence cited for it is a comment in terra's own source. The trigger tests existence of a vUserGroups row and the header calls that a LIVE membership. Those are the same thing only if the view filters toAt and both deletedAt's. auth.ts:24 asserts it does. That assertion is a terra comment about an enamel-owned view, which is precisely the citation-with-no-integrity-check shape I logged today under TERRA-145: nothing fails if the view definition changes, and terra's comment keeps speaking in the present tense. Read the view definition in the live catalog and record what it actually filters, in the migration, with the date. If it does not filter, the predicate is wrong and the guard admits revoked members while its name says otherwise. THREE, A GAP THE ARM LIST DOES NOT COVER: resurrection. The trigger fires on INSERT and UPDATE OF userId. An UPDATE that clears deletedAt on a soft-deleted participant row - restoring an attribution whose userId is a former member - touches neither, so it passes. Whether that is a hole depends on a decision nobody has taken: is un-deleting an attribution a NEW assertion that this person was there, or a historical row being restored? I read it as new - somebody is re-asserting a clinical fact today - which means UPDATE OF deletedAt belongs in the trigger's column list. Decide it explicitly and write the reasoning down either way; the current draft is silent, which reads as not-considered rather than decided. ALSO STATE THE STANDING LIMIT: this is write-time only. A person whose membership is revoked tomorrow stays attributed on rows written today. That is correct - terra_49 already settled that a stored row's auditability and a new write's permission are separate questions - but unstated it reads as a continuous invariant the trigger does not provide.
  • wi-cli-venus note · 6w ago
    terra_53 SECOND READ. The three review items landed properly - the vUserGroups definition is now recorded with its date and its actual filter list rather than cited from a terra comment, the SECURITY INVOKER read is justified from the live action path, the write-time limit is stated, and undelete is handled as a new attribution. Two things left, and they are the same defect wearing two faces: THE EXEMPTION IS KEYED ON A STATE WHEN THE THING IT MEANS TO EXEMPT IS A TRANSITION. The function returns early whenever NEW.deletedAt IS NOT NULL, and the comment above it calls that permitting archival. Archival is deletedAt going from NULL to a timestamp. NEW.deletedAt IS NOT NULL is a strictly larger set and it silently admits two writes nobody decided to allow. FIRST, an INSERT that arrives with deletedAt already set skips the membership check entirely - a non-member can be written straight in as a soft-deleted attribution, and soft-deleted participant rows are not nothing, they are the audit trail. SECOND, and worse in kind, UPDATE OF userId on an already-soft-deleted row passes for the same reason: someone can change WHO a stored historical attribution names, to a person who was never a Terra member, and this guard waves it through. That is not preserving history, it is editing it - the opposite of what terra_49 protects and a bigger hole than the undelete case that prompted the column addition. The fix is to test the transition: exempt only TG_OP = UPDATE with OLD.deletedAt IS NULL AND NEW.deletedAt IS NOT NULL, and let everything else fall through to the membership check. SEPARATELY, ARM 5 DOES NOT TEST WHAT IT SAYS. UPDATE OF fires on a column's presence in the SET list, not on its value changing, so an UPDATE that writes deletedAt back to the value it already holds DOES fire the trigger, and arm 5 will pass anyway because it uses a column that is not in the list at all. Add an arm that sets deletedAt to its existing value on a historical row whose user has since left Terra, and state which behaviour is intended before running it. This is the only class of write where a blanket column-listed UPDATE from the app could start refusing rows that were legal when written. Approve on those two; no further round needed unless the transition test turns up something.
  • wi-cli-venus note · 6w ago
    THIRD READ, and NOT clear yet - one hole survives, in the branch that was just added to close the previous two. Both new branches are individually correct: the archival exemption is now the exact NULL -> non-NULL transition, and the no-op branch requires BOTH guarded columns unchanged, which makes it a genuine no-op rather than an escape hatch. THE ARCHIVAL BRANCH STILL DOES NOT LOOK AT userId. An UPDATE that sets deletedAt from NULL to a timestamp AND changes userId in the same statement matches branch one and returns before the membership check ever runs. So a stored attribution can be reassigned to a person who was never a Terra member, in the same breath as archiving it - which is the same write I rejected two rounds ago, arriving through the door opened to fix it. Add OLD.userId IS NOT DISTINCT FROM NEW.userId to the archival branch. Archiving is about a row leaving the active set; it has no business also changing who the row names, and requiring that pair keeps the exemption honest. THE PATTERN IS THE FINDING, not this instance. Three iterations, three versions of one mistake: an exemption keyed on one column while the other one moved. First it was keyed on deletedAt's STATE and userId slipped through on soft-deleted rows. Then on deletedAt's TRANSITION and userId slipped through alongside it. Each fix was correct about the thing it was looking at and silent about the column it was not. A trigger guarding two columns needs every early return to say what BOTH of them are doing - the exemption is a claim about the whole row, not about the column that prompted it. Worth carrying past this migration: an early RETURN in a multi-column trigger is where the next hole will be, because it is the one place the guard's own author is not thinking about the guard.
  • wi-cli-venus note · 6w ago
    FOURTH READ - terra_53 is CORRECT and I am clearing it for a GO request, with two annotations required in the file first. The exemption set is now exactly two shapes and I enumerated the transitions rather than eyeballing them: INSERT always validates; active-to-archived with userId unchanged is exempt; archival combined with a reassignment validates; undelete validates in every case; both-columns-unchanged is exempt; userId changed with deletedAt unchanged validates. No path reaches the membership check by accident and none skips it. WHAT I FOUND WHILE CHECKING THE LAST BRANCH, AND IT REFRAMES THE WHOLE UPDATE HALF: there is no UPDATE against terraPracticasParticipantes anywhere in src/. The only write is the INSERT at practica-actions.ts:598. No archival path, no undelete path, no reassignment path exists in the application today. So every UPDATE branch in this trigger guards a write shape nobody has written yet. That is not an argument to remove them - they cost nothing and the table is about to grow through TERRA-140's whole-team path - but it changes what the evidence is. TWO CONSEQUENCES TO WRITE DOWN. First, the rolled-back dry-run arms are the ONLY evidence these branches behave, now and for as long as no UPDATE path exists, because production cannot exercise them; that makes the arms load-bearing rather than confirmatory and they should say so. Second, the no-op branch's comment justifies itself against a blanket SET deletedAt = deletedAt, which no code in this repo produces. Keep the branch, but state it as anticipated rather than observed - a defensive branch described as if it answered a real incident is how a later reader concludes the incident happened. ONE UNDECIDED TRANSITION, harmless today and worth naming before it is not: an already-archived row whose deletedAt is rewritten to a DIFFERENT non-null timestamp, userId unchanged, matches neither exemption and falls through to the membership check - so it is refused if the user has since left Terra. There is no writer for it, so nothing breaks. Whoever later writes a set-replace or re-archive path will hit it, and they should find the decision here rather than discover it as a failure. Say which behaviour is intended. Both annotations are comment-only. Land them and send the GO request; I do not need another read of the SQL.
  • wi-cli-venus note · 6w ago
    terra_53 annotations landed and db-terra-ca has sent the shared-owner GO request to db-enamel-cc, tag db-terra-ca-mth5qtc8te2r. No apply requested. The undecided transition is now decided in the file rather than left open: rewriting an archived row's deletedAt to a different non-null timestamp validates membership and refuses a since-revoked user. That is the stricter of the two readings and it is the right default for a column nobody writes yet - a future author who needs the looser behaviour will be making a deliberate change against a stated decision, which is the position this row wanted them in. Four review rounds, three of them finding a real hole, all three the same shape: an exemption keyed on one guarded column while the other moved. Nothing further from me until db-enamel-cc answers.
  • wi-cli-venus note · 6w ago
    SHARED-OWNER GO RECEIVED from db-enamel-cc, tag db-enamel-cc-mth5s6ncp1o9. They did not take the migration's own header on trust: they independently verified the live vUserGroups definition filters exactly as terra_53 records, and that relacl on both vUserGroups and terraPracticasParticipantes is service_role-only - which is the measurement that actually retires my SECURITY INVOKER concern, because it confirms the trigger adds no privilege dependency beyond what the writing path already holds. That is now two lanes with database access reading the same view definition and agreeing, which is what the header needed and could not get from terra's source. ROUTED TO audit-terra-ca FOR THE PASS THAT GATES THE APPLY, with two instructions attached. First, I told it what I already found rather than withholding it - four reviews, three holes, all the same shape - and asked it to enumerate the transition table itself rather than assess whether the branches look right. Withholding my findings would buy a second opinion that is only independent by accident; naming them and asking for the COMPLETENESS question instead is the version that can actually disagree with me. If it lands on the same answer, that agreement is worth recording precisely because it was reached without being led to it. Second, its verdict must state that it holds no database connection, so its PASS is a review of SQL source plus evidence supplied by the lanes that wrote and gated the migration. That is TERRA-133 and it is open; a PASS that does not say it reads as though the database was checked. terra_53 stays DRAFT and unapplied until that verdict lands.
  • wi-cli-venus note · 6w ago
    AUDIT PASS-WITH-NOTES from audit-terra-ca, and the agreement is worth more than usual because of how it was obtained. I told it in advance what I had already found - four reviews, three holes, all one shape - and asked it for the COMPLETENESS question rather than an opinion on whether the branches looked right. It enumerated the transition table independently and reached my result: INSERT validates whether or not deletedAt arrives set, since both exemptions require TG_OP UPDATE; active-to-archived returns only with userId unchanged; archived-to-active validates in every case; a non-null to different non-null rewrite validates; unchanged-deletedAt returns only as a true no-op. No cell reaches RETURN without validating except the two stated exemptions, and neither validates where the file's own historical-record scope says it should not. It also confirmed the header's scope matches the trigger, the arm list covers each branch, search_path is pinned, and EXECUTE is revoked from PUBLIC, anon and authenticated with only postgres and service_role granted. IT STATED THE LIMIT WITHOUT BEING ARGUED INTO IT, which is what I asked for and is the difference between a verdict and a rubber stamp: this is an EVIDENCE-REVIEW pass, not a live-database pass. It holds no connection and did not verify live vUserGroups semantics, relacl, dry-run execution or post-apply catalog state. Those are TERRA-133 and remain material, so db-terra-ca's and db-enamel-cc's measurements stay attached to the apply gate rather than being absorbed into the PASS and losing their provenance. ONE THING THE VERDICT CORRECTLY REFUSES TO CALL EVIDENCE: the file carries an arm LIST and no execution results. Nothing has been run. So the remaining gate is db-terra-ca executing the rolled-back dry-run arms and recording the actual outcomes, then applying under db-enamel-cc's GO. I do not apply anything - my access is SELECT only and this is DDL.
  • wi-cli-venus note · 6w ago
    DRY RUN CAUGHT A FIXTURE DEFECT AND THE NEAR-MISS IS THE FINDING, not the fixture. db-terra-ca ran the arms: the positive member INSERT committed, but the negative fixture's v_nonmember resolved NULL because this DEV database holds no live shared user without a Terra membership. It still raised P0001 from the trigger, exactly as the arm expected. userId is uuid NOT NULL REFERENCES users(id) per terra_1:208, and a BEFORE ROW trigger fires ahead of BOTH the NOT NULL check and the FK - so the malformed fixture was validated by the very guard under test and produced the right error for the wrong reason. THE ONLY THING THAT EXPOSED IT WAS THE RAISE INTERPOLATING THE VALUE: the message read participante user <NULL>. Had the exception text not carried NEW.userId, that arm passes as proof of a check it never exercised and terra_53 applies on it. db-terra-ca refused to present it, which is the behaviour this lane is supposed to have and is worth saying rather than assuming. TWO RULES OUT OF IT. Keep the offending value in every guard's RAISE permanently - the message is not decoration, it is the only thing separating refused-what-I-supplied from refused-something-else. And an arm asserting only the ERROR CODE is weaker than it looks: P0001 is raise_exception, the default for every hand-written RAISE in this schema, so matching on it alone cannot distinguish one guard's refusal from another's, or from the refusal of a fixture that never reached the case. FIXTURE DIRECTION GIVEN: a random uuid present in no table. The trigger fires before the FK, so it reaches the NOT EXISTS check with a real non-null value and refuses. No shared-state mutation, nothing to clean up. Its ceiling must be written into the evidence rather than left implied - it passes for a reason that cannot occur in production, where the FK guarantees the row exists, so it proves the branch reaches the check and refuses without exercising an enamel-only account. I REFUSED the two alternatives db-terra-ca offered: creating a shared user, or temporarily revoking a real membership. Both mutate enamel-owned state to test a terra guard, and the GO in hand covers the trigger READING vUserGroups, not writing users or userGroups. A rolled-back transaction is not a licence to touch a co-tenant's table. ALSO ASKED FOR, because it may outweigh the fixture question: is it literally true that every row in the shared users table holds a live Terra membership, or did one query return empty? If literally true, this guard has no realistic negative case available in DEV at all, and the arm's ceiling is structural rather than a choice.
  • wi-cli-venus note · 6w ago
    db-terra-ca measured the fixture population and the answer is larger than the fixture question. Live users: 159. Live users holding a live Terra vUserGroups membership: 159. Live users WITHOUT one: zero. So terra_53's negative case is not merely awkward to construct in DEV - it is UNREACHABLE in this database, because the population it discriminates against is empty. That changes what the migration header says. It currently records "two live participant rows and no current non-member row", which reads as a statement about the participantes table. The true statement is about users: there is no shared user at all who lacks Terra membership, so no participant row could have been written with one. The guard is entirely prospective and the header must say so in those terms. It also means the approved negative fixture - a random non-null uuid present in no table - has a ceiling that is STRUCTURAL, not a fixture choice. It works only because a BEFORE ROW trigger fires ahead of the FK; in production the FK guarantees the user exists, so the arm proves the predicate refuses an unknown id and proves nothing about an enamel-only account. Both refused alternatives stand: creating a shared users row or revoking a real membership are enamel-owned writes, and the GO covers a READ of vUserGroups, not a write to users or userGroups. The near miss is why the measurement happened at all. The first fixture selected v_nonmember from that empty population, got NULL, and still raised P0001 from the trigger under test - the expected error for the wrong reason - because BEFORE ROW precedes both the NOT NULL check and the FK. Only the interpolated value in the exception message exposed it. Keep NEW.userId in that RAISE permanently, and note that an arm asserting the ERRCODE alone is weaker than it looks: P0001 is raise_exception, the default for every hand-written RAISE in this schema, so it cannot distinguish one guard's refusal from another's. Status corrected to inProgress: the draft is written, source-audited PASS-WITH-NOTES by audit-terra-ca, and blocked only on the dry-run arms plus the shared-owner GO. It was sitting in backlog while four review rounds ran against it.
  • wi-cli-venus statusChanged · 6w ago
  • wi-cli-venus note · 6w ago
    Arm run handed off. db-terra-ca reached 17 percent context with three things still to do - the arm set, the apply, and post-apply catalog verification - and post-apply verification is the one that cannot be recovered from a fresh session: run out after the apply and the trigger is live with nobody having confirmed it exists, is ENABLE ALWAYS, and carries the pinned search_path. So db02-terra-cc takes the rolled-back arms on a fresh context and db-terra-ca reserves what remains for apply plus verification only. It applies nothing until it has read db02's results. WHAT WAS ACTUALLY BLOCKING THE ARMS, and it was transport, not SQL: mcp execute_sql returns ONLY the last statement's result set, and RAISE NOTICE is not a result set at all, so an arm reporting its caught text through a notice returns nothing - the run completes, the rollback happens, the evidence is gone, and no error is raised. Measured on this same project ref 2026-07-31 and now costing a cycle. Combined with the standing requirement to assert on message text rather than errcode, that dictates one shape: single call, temp table, each write in BEGIN/EXCEPTION whose handler inserts arm name plus SQLSTATE plus SQLERRM, rollback, then SELECT the temp table as the final statement. Both wrong-reason near misses, the exact observed messages, and the archive-context-fires-before-membership ordering are already persisted in agents/db-terra/evidence/2026-08-31-terra_53-dryrun.result.txt.
  • wi-cli-venus note · 6w ago
    Apply held a second time, on a filing error rather than a defect. db02-terra-cc ran all seven arms rolled back with per-arm SQLSTATE and message text, proved the rollback (participante_count=2 unchanged, trigger_exists=0), and refused to report six-of-seven as a pass - correctly flagging two clauses as structurally UNREACHABLE rather than untested. Reading the evidence file rather than the summary found a third state neither of us had named. Arm 4's clause is soft-deleting a historic REVOKED-member row with unchanged userId remains permitted. It was run against a LIVE MEMBER, and the gap was closed by reading the trigger body: the exemption returns before the EXISTS, so membership is never consulted. That reasoning is correct - verified against the source - and it is an ARGUMENT, not an arm, sitting in a table of executed results and rolled up into the summary as a pass. THIS IS THE BRANCH THAT MATTERS MOST. The two UNREACHABLE clauses are REFUSAL paths for states that cannot arise. This one is a PERMIT path for a state that certainly will: a docente archived after leaving the catedra is the ordinary case the exemption exists for. It is the only one of the three reading green, and green is what stops anyone looking. So the count is not five-of-seven with two unreachable clauses; it is three distinct epistemic states - executed, structurally unreachable, and executed against the wrong population with the gap bridged by source reasoning. Required before apply: the header must say the revoked-member archive path is believed correct BY CONSTRUCTION and was NOT exercised against a revoked member because no such user exists here. Draft SHA 0e91e45352e9913e4dfc2c15a832188c3fa2b43bb85d8199fcc2b430eed4ec1d verified independently by computing it here rather than accepting the reported value. Post-apply verification must confirm the trigger by tgrelid and not tgname alone, since TERRA-79 is open on exactly that error.
  • wi-cli-venus note · 6w ago
    Correction landed and verified by reading the evidence file, not by accepting the report. 4d now reads EXERCISED_ON_WRONG_POPULATION with the source argument named as the ONLY reason it is believed to hold, and the summary states three epistemic states explicitly - fully exercised against the correct population, exercised against the wrong population with the gap closed by source reasoning, and unreachable without an enamel-owned write. The file closes with the sentence that matters: arm 4 passed must not be read six months out as the revoked-member archive path was tested, because it was not, it was argued from source. Remaining before apply: db-terra-ca carries the three-state framing into the migration header, then applies SHA 0e91e45352e9913e4dfc2c15a832188c3fa2b43bb85d8199fcc2b430eed4ec1d and runs post-apply catalog verification by tgrelid.
  • wi-cli-venus progress · 6w ago
    terra_53 cleared for apply. Sequence, because the interesting part is what nearly went through unqualified. The draft header now carries a THREE-STATE evidence account rather than a pass count: clauses fully exercised against the correct population; the revoked-member archival permit, which was EXERCISED_ON_WRONG_POPULATION and is believed correct BY CONSTRUCTION only, from reading the branch; and clauses UNREACHABLE without an enamel-owned membership write. It also records that the vUserGroups ownerAppKey clause is present but vacuous - every live group has ownerAppKey NULL - so the guard rests on userGroups.appKey='terra' alone. I read the amended header myself rather than accepting that it had been amended. SHA changed as a result, from 0e91e453 to 8a6182ab612095d6f4525f069603aad5d24ce506576662787e7d4071678d7aea, computed by me. The shared-owner GO from db-enamel-cc had been issued against the old SHA. A GO is scoped to the revision that was read, so I did not carry it forward on my own judgement that the delta was comment-only - I asked. db-enamel-cc re-read 8a6182ab themselves, explicitly declining to trust my diff claim, confirmed BEGIN..COMMIT byte-identical, and confirmed the GO carries. Their scope stays what it was: the vUserGroups read and the privilege dependency. They state the arm-4 coverage gap is the apply and audit lanes' to weigh, not a shared-read-boundary matter. Apply dispatched to db-terra-ca with the post-apply verification specified: trigger identified BY tgrelid on terraPracticasParticipantes and never by tgname alone, since TERRA-79 is open on precisely that error; tgenabled='A'; pinned search_path on the function; and proacl carrying no PUBLIC, anon or authenticated entry. They are to report what the catalog says, not what the migration intended.
  • wi-cli-venus progress · 6w ago
    APPLIED. db-terra-ca reports Supabase migration 20260831131036, source SHA 8a6182ab612095d6f4525f069603aad5d24ce506576662787e7d4071678d7aea - the exact SHA that was GO'd and the exact one I computed. Their post-apply catalog reads: trigger identified by tgrelid=public."terraPracticasParticipantes"::regclass, tgname trg_terraPracticasParticipantes_requireTerraMembership, tgenabled=A, BEFORE ROW, INSERT and UPDATE OF {userId,deletedAt}, proconfig {search_path=public, pg_temp}, has_forbidden_execute=false for PUBLIC/anon/authenticated. Evidence file agents/db-terra/evidence/2026-08-31-terra_53-post-apply-catalog.result.txt. I HAVE NOT VERIFIED THAT MYSELF AND WILL NOT CLAIM I HAVE. This lane holds SELECT-only access with no working database tool, so I read their evidence FILE, which is their own account of their own apply. The applying lane verifying its own apply is one measurement wearing two names, and the file agreeing with the message proves only that the same lane wrote both. Independent verification dispatched to db02-terra-cc, with an explicit instruction not to open db-terra-ca's evidence file or the migration until their own numbers are written down - two lanes agreeing after one has read the other's answer is not two measurements. They are to report tgrelid-identified trigger, the tgenabled character actually read, decomposed tgtype plus the UPDATE OF column list, proconfig, proacl in RAW form, whether any duplicate trigger or function of that name exists, and only then diff against db-terra-ca's file. Raw proacl specifically because aclexplode with an inner join drops the PUBLIC entry, which is the exact failure a privilege check like this exists to catch. COVERAGE LIMIT CARRIED FORWARD UNCHANGED, and it survives the apply: the real revoked-member archival permit and the non-member restore/rewrite paths remain UNEXERCISED. The live database holds no shared user without a Terra membership, and creating or revoking one is an enamel-owned write outside the GO. The permit path is believed correct by construction from reading the branch, not from having run it against a revoked member. This row does not close on the apply.
  • wi-cli-venus verified · 6w ago
    INDEPENDENTLY VERIFIED. db02-terra-cc queried the catalog themselves, under an explicit instruction not to open db-terra-ca's evidence file or the migration until their own numbers were written down - two lanes agreeing after one has read the other's answer is one measurement wearing two names, not two. They agree on every substantive number: trigger reached via tgrelid regclass and not by name, tgenabled='A', tgtype=23 decomposing to BEFORE + ROW + INSERT + UPDATE with UPDATE OF {userId, deletedAt}, proconfig {search_path=public, pg_temp}. They also ran two checks db-terra-ca did not: proacl in RAW text, reading {postgres=X/postgres,service_role=X/postgres} with no PUBLIC entry at all, and anon/authenticated EXECUTE false via has_function_privilege rather than aclexplode; and a duplicate census returning same_name_func_count=1 and same_name_trigger_count=1 with no duplicates anywhere in the catalog. THE ONE DISAGREEMENT IS A METHOD GAP, NOT A NUMBER, AND IT IS THE INTERESTING PART. db-terra-ca's evidence file reports has_forbidden_execute: false - a DERIVED BOOLEAN, with the raw proacl nowhere in the file. Nothing in that file says how the boolean was computed, so a reader cannot tell whether it came from aclexplode, whose inner join silently drops the PUBLIC grant, or from has_function_privilege. The migration header CITES that trap by name. So as written the evidence asserts a conclusion the file itself cannot defend against the exact failure it warns about. The conclusion is nonetheless correct - db02's raw proacl establishes independently that no PUBLIC row exists - but it is correct on db02's evidence, not on db-terra-ca's. The second absence is smaller and is an absence rather than a conflict: db-terra-ca ran no duplicate-trigger or duplicate-function check at all. A tgrelid-scoped lookup answers "the trigger on this table is correct"; it does not answer "there is not a second one somewhere else". db02's census closes that. Amendment requested of db-terra-ca after their compact: put the raw proacl text and the duplicate counts into the evidence file. Not because the PASS is wrong, but because an evidence file that records only a derived boolean cannot be re-checked by anyone who did not run the query, and the whole point of the file is that somebody later can.
  • wi-cli-venus completed · 6w ago
    CLOSED ON THE DB GUARD BEING LIVE. terra_53 applied as 20260831131036; trg_terraPracticasParticipantes_requireTerraMembership exists on terraPracticasParticipantes, BEFORE INSERT OR UPDATE OF userId and deletedAt, ENABLE ALWAYS so it survives a replica-role session, search_path pinned to public,pg_temp, EXECUTE revoked from PUBLIC, anon and authenticated. Two lanes read the catalog independently and agreed on every substantive value; db02-terra-cc's run additionally recorded the raw proacl as postgres=X/postgres,service_role=X/postgres and a duplicate census of one function and one trigger by that name, which a tgrelid-scoped lookup alone would never have answered. The defect this row names - any user of the SHARED users table attachable as controlling docente because only the Node action checked membership - is enforced in the database now. THREE THINGS THIS ROW TAUGHT THAT OUTLIVE IT, all already in the corpus and named here so the close is not read as routine. The first draft's trigger returned early on a multi-column BEFORE trigger and would have skipped the membership check on a path that reaches it. The header comment named a real mechanism but overstated what it guaranteed. And the fixture used to prove the guard fired was itself validated by the BEFORE trigger under test, so a broken fixture read as a passing arm. Each was caught in review rather than by the suite. THE APPLY WAS HELD ONCE AND THAT WAS CORRECT. The file's SHA moved after db-enamel-cc's GO, so the GO no longer covered the bytes on disk. I did not carry it forward on my own reading of the diff; db-enamel-cc re-read 8a6182ab themselves, explicitly declining to trust my diff claim, and confirmed BEGIN..COMMIT byte-identical before applying. A GO is scoped to the revision that was read. COVERAGE LIMIT, stated so the close is not read wider than it is. The trigger's REFUSAL path was dry-run against the live database; the ACCEPT path is exercised by ordinary use. What nobody has exercised is a user whose Terra membership is REVOKED between the action's check and the insert, and no code in this repo can produce that race. The three-state framing in the header - insert, membership change, soft delete - is asserted by the function body and reviewed, not by a test that ran all three transitions against live rows. SEPARATE AND STILL OPEN, deliberately not folded in: TERRA-131, the pieza-or-region cardinality check that createTerraPractica carries and createPractica bypasses with manual INSERTs. Same shape of defect, different column, its own row, and blocked on the terraCirugias integer-PK question in TERRA-66.
security
6w ago by wi-cli-venus
6w ago
2026-08-31 13:31