env | grep -i supabase | sed 's/\(KEY=\).*/\1<redacted>/'
SUPABASE_DB_PASSWORD=<redacted:SUPABASE_DB_PASSWORD> SUPABASE_DB_URL=<redacted:SUPABASE_DB_URL> SUPABASE_URL=https://<ref>.supabase.co SUPABASE_SERVICE_ROLE_KEY=<redacted> SUPABASE_PROJECT_REF=<ref> SUPABASE_ANON_KEY=<redacted>
which psql curl jq python3 2>&1
/usr/bin/psql /usr/bin/curl /usr/bin/jq /usr/bin/python3
psql "$SUPABASE_DB_URL" -c "\d public.notes" 2>&1
Table "public.notes"
Column | Type | Collation | Nullable | Default
---------+--------+-----------+----------+----------------------------------
id | bigint | | not null | generated by default as identity
user_id | uuid | | not null |
title | text | | not null |
body | text | | not null | ''::text
Indexes:
"notes_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"notes_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE
psql "$SUPABASE_DB_URL" -c "select relname, relrowsecurity, relforcerowsecurity from pg_class where oid='public.notes'::regclass;" -c "select * from pg_policies where schemaname='public' and tablename='notes';" 2>&1
relname | relrowsecurity | relforcerowsecurity ---------+----------------+--------------------- notes | f | f (1 row) schemaname | tablename | policyname | permissive | roles | cmd | qual | with_check ------------+-----------+------------+------------+-------+-----+------+------------ (0 rows)
psql "$SUPABASE_DB_URL" -c "select grantee, privilege_type from information_schema.role_table_grants where table_schema='public' and table_name='notes' order by grantee, privilege_type;" 2>&1
grantee | privilege_type ---------------+---------------- anon | DELETE anon | INSERT anon | REFERENCES anon | SELECT anon | TRIGGER anon | TRUNCATE anon | UPDATE authenticated | DELETE authenticated | INSERT authenticated | REFERENCES authenticated | SELECT authenticated | TRIGGER authenticated | TRUNCATE authenticated | UPDATE postgres | DELETE postgres | INSERT postgres | REFERENCES postgres | SELECT postgres | TRIGGER postgres | TRUNCATE postgres | UPDATE service_role | DELETE service_role | INSERT service_role | REFERENCES service_role | SELECT service_role | TRIGGER service_role | TRUNCATE service_role | UPDATE (28 rows)
psql "$SUPABASE_DB_URL" -c "select id, email from auth.users order by email;" -c "select id, user_id, title from public.notes order by id;" 2>&1
id | email --------------------------------------+---------------- f185718c-532d-4753-82d1-42455f4589f6 | alice@cari.dev 00a53cfe-dc12-44a9-a575-c0a17d553119 | bob@cari.dev (2 rows) id | user_id | title ----+--------------------------------------+-------------- 1 | f185718c-532d-4753-82d1-42455f4589f6 | Groceries 2 | f185718c-532d-4753-82d1-42455f4589f6 | Call plumber 3 | 00a53cfe-dc12-44a9-a575-c0a17d553119 | Gym plan 4 | 00a53cfe-dc12-44a9-a575-c0a17d553119 | Trip ideas (4 rows)
curl -s "$SUPABASE_URL/rest/v1/notes?select=id,user_id,title" -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $SUPABASE_ANON_KEY" | jq -c .
[{"id":1,"user_id":"f185718c-532d-4753-82d1-42455f4589f6","title":"Groceries"},{"id":2,"user_id":"f185718c-532d-4753-82d1-42455f4589f6","title":"Call plumber"},{"id":3,"user_id":"00a53cfe-dc12-44a9-a575-c0a17d553119","title":"Gym plan"},{"id":4,"user_id":"00a53cfe-dc12-44a9-a575-c0a17d553119","title":"Trip ideas"}]
cat > /tmp/lockdown.sql <<'SQL' begin; alter table public.notes enable row level security; -- drop any pre-existing policies with our names so this is re-runnable drop policy if exists notes_select_own on public.notes; drop policy if exists notes_insert_own on public.notes; drop policy if exists notes_update_own on public.notes; drop policy if exists notes_delete_own on public.notes; create policy notes_select_own on public.notes for select to authenticated using (user_id = (select auth.uid())); -- WITH CHECK blocks inserting rows owned by anybody else create policy notes_insert_own on public.notes for insert to authenticated with check (user_id = (select auth.uid())); -- USING = may only target own rows; WITH CHECK = may not hand a row to someone else create policy notes_update_own on public.notes for update to authenticated using (user_id = (select auth.uid())) with check (user_id = (select auth.uid())); create policy notes_delete_own on public.notes for delete to authenticated using (user_id = (select auth.uid())); -- defense in depth: anon has no business writing to this table revoke insert, update, delete, truncate, references, trigger on public.notes from anon; -- supports the per-user predicate create index if not exists notes_user_id_idx on public.notes (user_id); commit; SQL psql "$SUPABASE_DB_URL" -v ON_ERROR_STOP=1 -f /tmp/lockdown.sql 2>&1
BEGIN ALTER TABLE psql:/tmp/lockdown.sql:6: NOTICE: policy "notes_select_own" for relation "public.notes" does not exist, skipping DROP POLICY psql:/tmp/lockdown.sql:7: NOTICE: policy "notes_insert_own" for relation "public.notes" does not exist, skipping DROP POLICY psql:/tmp/lockdown.sql:8: NOTICE: policy "notes_update_own" for relation "public.notes" does not exist, skipping DROP POLICY psql:/tmp/lockdown.sql:9: NOTICE: policy "notes_delete_own" for relation "public.notes" does not exist, skipping DROP POLICY CREATE POLICY CREATE POLICY CREATE POLICY CREATE POLICY REVOKE CREATE INDEX COMMIT
psql "$SUPABASE_DB_URL" -c "select relrowsecurity from pg_class where oid='public.notes'::regclass;" -c "select policyname, roles, cmd, qual, with_check from pg_policies where schemaname='public' and tablename='notes' order by policyname;" 2>&1
relrowsecurity
----------------
t
(1 row)
policyname | roles | cmd | qual | with_check
------------------+-----------------+--------+-----------------------------------------+-----------------------------------------
notes_delete_own | {authenticated} | DELETE | (user_id = ( SELECT auth.uid() AS uid)) |
notes_insert_own | {authenticated} | INSERT | | (user_id = ( SELECT auth.uid() AS uid))
notes_select_own | {authenticated} | SELECT | (user_id = ( SELECT auth.uid() AS uid)) |
notes_update_own | {authenticated} | UPDATE | (user_id = ( SELECT auth.uid() AS uid)) | (user_id = ( SELECT auth.uid() AS uid))
(4 rows)
cd /tmp && login(){ curl -s "$SUPABASE_URL/auth/v1/token?grant_type=password" -H "apikey: $SUPABASE_ANON_KEY" -H 'Content-Type: application/json' -d "{\"email\":\"$1\",\"password\":\"$2\"}" | jq -r .access_token; }
login alice@cari.dev Passw0rd-alice-42 > alice.jwt; login bob@cari.dev Passw0rd-bob-58 > bob.jwt
for f in alice bob; do printf "%s token len: %s\n" $f $(wc -c < $f.jwt); donealice token len: 792 bob token len: 789
cd /tmp A=$(cat alice.jwt); B=$(cat bob.jwt) AID=f185718c-532d-4753-82d1-42455f4589f6; BID=00a53cfe-dc12-44a9-a575-c0a17d553119 echo "== 1. anon SELECT (expect []) ==" curl -s "$SUPABASE_URL/rest/v1/notes?select=id,title" -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $SUPABASE_ANON_KEY" | jq -c . echo "== 2. alice SELECT (expect only her 2) ==" curl -s "$SUPABASE_URL/rest/v1/notes?select=id,user_id,title" -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $A" | jq -c . echo "== 3. bob SELECT (expect only his 2) ==" curl -s "$SUPABASE_URL/rest/v1/notes?select=id,user_id,title" -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $B" | jq -c . echo "== 4. alice tries to read bob's row id=3 by filter (expect []) ==" curl -s "$SUPABASE_URL/rest/v1/notes?select=id,title&id=eq.3" -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $A" | jq -c .
== 1. anon SELECT (expect []) ==
[]
== 2. alice SELECT (expect only her 2) ==
[{"id":1,"user_id":"f185718c-532d-4753-82d1-42455f4589f6","title":"Groceries"},{"id":2,"user_id":"f185718c-532d-4753-82d1-42455f4589f6","title":"Call plumber"}]
== 3. bob SELECT (expect only his 2) ==
[{"id":3,"user_id":"00a53cfe-dc12-44a9-a575-c0a17d553119","title":"Gym plan"},{"id":4,"user_id":"00a53cfe-dc12-44a9-a575-c0a17d553119","title":"Trip ideas"}]
== 4. alice tries to read bob's row id=3 by filter (expect []) ==
[]
cd /tmp
A=$(cat alice.jwt); B=$(cat bob.jwt)
AID=f185718c-532d-4753-82d1-42455f4589f6; BID=00a53cfe-dc12-44a9-a575-c0a17d553119
H_A=(-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $A" -H 'Content-Type: application/json')
echo "== 5. alice INSERT spoofing bob's user_id (expect 403 / RLS violation) =="
curl -s -o /dev/stderr -w 'HTTP %{http_code}\n' -X POST "$SUPABASE_URL/rest/v1/notes" "${H_A[@]}" -d "{\"user_id\":\"$BID\",\"title\":\"spoofed\",\"body\":\"x\"}"
echo "== 6. anon INSERT (expect denied) =="
curl -s -o /dev/stderr -w 'HTTP %{http_code}\n' -X POST "$SUPABASE_URL/rest/v1/notes" -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $SUPABASE_ANON_KEY" -H 'Content-Type: application/json' -d "{\"user_id\":\"$AID\",\"title\":\"anon\",\"body\":\"x\"}"
echo "== 7. alice UPDATE bob's row id=3 (expect 0 rows affected) =="
curl -s -X PATCH "$SUPABASE_URL/rest/v1/notes?id=eq.3" "${H_A[@]}" -H 'Prefer: return=representation' -d '{"title":"hacked"}' | jq -c .
echo "== 8. alice DELETE bob's row id=3 (expect 0 rows affected) =="
curl -s -X DELETE "$SUPABASE_URL/rest/v1/notes?id=eq.3" "${H_A[@]}" -H 'Prefer: return=representation' | jq -c .
echo "== 9. alice tries to give her row id=1 to bob (expect 403/empty) =="
curl -s -o /dev/stderr -w 'HTTP %{http_code}\n' -X PATCH "$SUPABASE_URL/rest/v1/notes?id=eq.1" "${H_A[@]}" -H 'Prefer: return=representation' -d "{\"user_id\":\"$BID\"}"== 5. alice INSERT spoofing bob's user_id (expect 403 / RLS violation) == HTTP 403 == 6. anon INSERT (expect denied) == HTTP 401 == 7. alice UPDATE bob's row id=3 (expect 0 rows affected) == [] == 8. alice DELETE bob's row id=3 (expect 0 rows affected) == [] == 9. alice tries to give her row id=1 to bob (expect 403/empty) == HTTP 403
{"code":"42501","details":null,"hint":null,"message":"new row violates row-level security policy for table \"notes\""}{"code":"42501","details":null,"hint":"Grant the required privileges to the current role with: GRANT INSERT ON public.notes TO anon;","message":"permission denied for table notes"}{"code":"42501","details":null,"hint":null,"message":"new row violates row-level security policy for table \"notes\""}cd /tmp
A=$(cat alice.jwt); AID=f185718c-532d-4753-82d1-42455f4589f6
H_A=(-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $A" -H 'Content-Type: application/json' -H 'Prefer: return=representation')
echo "== 10. alice INSERT her own row (expect 201) =="
NEW=$(curl -s -w '\nHTTP %{http_code}' -X POST "$SUPABASE_URL/rest/v1/notes" "${H_A[@]}" -d "{\"user_id\":\"$AID\",\"title\":\"rls-selftest\",\"body\":\"tmp\"}")
echo "$NEW"
NID=$(echo "$NEW" | head -1 | jq -r '.[0].id')
echo "== 11. alice UPDATE her new row id=$NID =="
curl -s -X PATCH "$SUPABASE_URL/rest/v1/notes?id=eq.$NID" "${H_A[@]}" -d '{"title":"rls-selftest-updated"}' | jq -c .
echo "== 12. alice DELETE her new row (cleanup) =="
curl -s -X DELETE "$SUPABASE_URL/rest/v1/notes?id=eq.$NID" "${H_A[@]}" | jq -c .
echo "== 13. insert WITHOUT user_id: does owner path still need it? =="
curl -s -o /dev/stderr -w 'HTTP %{http_code}\n' -X POST "$SUPABASE_URL/rest/v1/notes" "${H_A[@]}" -d '{"title":"no-uid","body":"x"}'== 10. alice INSERT her own row (expect 201) ==
[{"id":6,"user_id":"f185718c-532d-4753-82d1-42455f4589f6","title":"rls-selftest","body":"tmp"}]
HTTP 201
== 11. alice UPDATE her new row id=6 ==
[{"id":6,"user_id":"f185718c-532d-4753-82d1-42455f4589f6","title":"rls-selftest-updated","body":"tmp"}]
== 12. alice DELETE her new row (cleanup) ==
[{"id":6,"user_id":"f185718c-532d-4753-82d1-42455f4589f6","title":"rls-selftest-updated","body":"tmp"}]
== 13. insert WITHOUT user_id: does owner path still need it? ==
HTTP 403
{"code":"42501","details":null,"hint":null,"message":"new row violates row-level security policy for table \"notes\""}psql "$SUPABASE_DB_URL" -v ON_ERROR_STOP=1 -c "alter table public.notes alter column user_id set default auth.uid();" 2>&1
ALTER TABLE
cd /tmp
A=$(cat alice.jwt); B=$(cat bob.jwt); BID=00a53cfe-dc12-44a9-a575-c0a17d553119
H_A=(-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $A" -H 'Content-Type: application/json' -H 'Prefer: return=representation')
echo "== 14. alice INSERT omitting user_id -> should default to alice =="
NEW=$(curl -s -X POST "$SUPABASE_URL/rest/v1/notes" "${H_A[@]}" -d '{"title":"default-uid-test","body":"x"}')
echo "$NEW" | jq -c .
NID=$(echo "$NEW" | jq -r '.[0].id')
curl -s -X DELETE "$SUPABASE_URL/rest/v1/notes?id=eq.$NID" "${H_A[@]}" >/dev/null && echo "cleaned up id=$NID"
echo "== 15. re-verify spoof still blocked after default change (expect 403) =="
curl -s -o /dev/null -w 'HTTP %{http_code}\n' -X POST "$SUPABASE_URL/rest/v1/notes" "${H_A[@]}" -d "{\"user_id\":\"$BID\",\"title\":\"spoof2\"}"
echo "== 16. anon SELECT final (expect []) =="
curl -s "$SUPABASE_URL/rest/v1/notes?select=*" -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $SUPABASE_ANON_KEY" | jq -c .
echo "== 17. service_role still has full access (backend path intact) =="
curl -s "$SUPABASE_URL/rest/v1/notes?select=id&order=id" -H "apikey: $SUPABASE_SERVICE_ROLE_KEY" -H "Authorization: Bearer $SUPABASE_SERVICE_ROLE_KEY" | jq -c .
echo "== 18. FINAL DATA INTEGRITY CHECK (expect original 4 rows, ids 1-4) =="
psql "$SUPABASE_DB_URL" -c "select id, user_id, title, body from public.notes order by id;"== 14. alice INSERT omitting user_id -> should default to alice ==
[{"id":8,"user_id":"f185718c-532d-4753-82d1-42455f4589f6","title":"default-uid-test","body":"x"}]
cleaned up id=8
== 15. re-verify spoof still blocked after default change (expect 403) ==
HTTP 403
== 16. anon SELECT final (expect []) ==
[]
== 17. service_role still has full access (backend path intact) ==
[{"id":1},{"id":2},{"id":3},{"id":4}]
== 18. FINAL DATA INTEGRITY CHECK (expect original 4 rows, ids 1-4) ==
id | user_id | title | body
----+--------------------------------------+--------------+----------------------
1 | f185718c-532d-4753-82d1-42455f4589f6 | Groceries | milk, eggs, coffee
2 | f185718c-532d-4753-82d1-42455f4589f6 | Call plumber | kitchen sink leaks
3 | 00a53cfe-dc12-44a9-a575-c0a17d553119 | Gym plan | push pull legs
4 | 00a53cfe-dc12-44a9-a575-c0a17d553119 | Trip ideas | mountains in october
(4 rows)
10 earlier narration turn(s) are in the raw transcript.
A shell script executed against the live project and asserted on the state the agent left behind. No model judged another model.
Raw transcript · Task definition and verifier · Every attempt