env | grep -i supabase | sed 's/=.*/=<set>/' && echo '---' && echo "URL: $SUPABASE_URL" && echo "REF: $SUPABASE_PROJECT_REF"
SUPABASE_DB_PASSWORD=<set> SUPABASE_DB_URL=<set> SUPABASE_URL=<set> SUPABASE_SERVICE_ROLE_KEY=<set> SUPABASE_PROJECT_REF=<set> SUPABASE_ANON_KEY=<set> --- URL: https://<ref>.supabase.co REF: <ref>
which psql || (apt-get install -y postgresql-client 2>/dev/null | tail -1); echo "---"; echo "DB_URL: ${SUPABASE_DB_URL}" | sed 's/:[^:@]*@/:<hidden>@/'/usr/bin/psql --- DB_URL: postgresql://postgres.<ref>:<hidden>@aws-0-us-east-1.pooler.supabase.com:5432/postgres
export PGPASSWORD="$SUPABASE_DB_PASSWORD"
psql "$SUPABASE_DB_URL" -X -A -c "
SELECT schemaname, tablename, rowsecurity FROM pg_tables WHERE schemaname='public' AND tablename IN ('orgs','org_members','tickets');
" -c "
SELECT polname, polpermissive, polcmd, polroles, qual, with_check FROM pg_policies WHERE schemaname='public' ORDER BY tablename, polname;
" -c "
SELECT n.nspname, p.proname, pg_get_function_arguments(p.oid) FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace WHERE n.nspname IN ('public','auth') AND p.proname ILIKE '%member%';
"schemaname|tablename|rowsecurity public|org_members|f public|orgs|f public|tickets|f (3 rows) nspname|proname|pg_get_function_arguments (0 rows)
ERROR: column "polname" does not exist
LINE 2: SELECT polname, polpermissive, polcmd, polroles, qual, with_...
^
HINT: Perhaps you meant to reference the column "pg_policies.policyname".
export PGPASSWORD="$SUPABASE_DB_PASSWORD"
psql "$SUPABASE_DB_URL" -X -A -c "
SELECT policyname, permissive, cmd, roles, qual, with_check FROM pg_policies WHERE schemaname='public';
" -c "
SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema='public' AND table_name IN ('orgs','org_members','tickets') ORDER BY table_name, ordinal_position;
" -c "
SELECT o.name AS org, m.role, u.email FROM public.org_members m JOIN public.orgs o ON o.id=m.org_id JOIN auth.users u ON u.id=m.user_id ORDER BY o.name, m.role;
" -c "
SELECT o.name, count(t.id) AS tickets FROM public.orgs o LEFT JOIN public.tickets t ON t.org_id=o.id GROUP BY o.name ORDER BY o.name;
" -c "SELECT grantee, table_name, privilege_type FROM information_schema.role_table_grants WHERE table_schema='public' AND table_name IN ('orgs','org_members','tickets') AND grantee IN ('anon','authenticated') ORDER BY table_name, grantee, privilege_type;"policyname|permissive|cmd|roles|qual|with_check (0 rows) column_name|data_type|is_nullable org_id|uuid|NO user_id|uuid|NO role|text|NO id|uuid|NO name|text|NO id|bigint|NO org_id|uuid|NO title|text|NO status|text|NO (9 rows) org|role|email Acme|admin|uma-admin@cari.dev Acme|member|vic-member@cari.dev Globex|member|wen-outsider@cari.dev (3 rows) name|tickets Acme|3 Globex|2 (2 rows) grantee|table_name|privilege_type anon|org_members|DELETE anon|org_members|INSERT anon|org_members|REFERENCES anon|org_members|SELECT anon|org_members|TRIGGER anon|org_members|TRUNCATE anon|org_members|UPDATE authenticated|org_members|DELETE authenticated|org_members|INSERT authenticated|org_members|REFERENCES authenticated|org_members|SELECT authenticated|org_members|TRIGGER authenticated|org_members|TRUNCATE authenticated|org_members|UPDATE anon|orgs|DELETE anon|orgs|INSERT anon|orgs|REFERENCES anon|orgs|SELECT anon|orgs|TRIGGER anon|orgs|TRUNCATE anon|orgs|UPDATE authenticated|orgs|DELETE authenticated|orgs|INSERT authenticated|orgs|REFERENCES authenticated|orgs|SELECT authenticated|orgs|TRIGGER authenticated|orgs|TRUNCATE authenticated|orgs|UPDATE anon|tickets|DELETE anon|tickets|INSERT anon|tickets|REFERENCES anon|tickets|SELECT anon|tickets|TRIGGER anon|tickets|TRUNCATE anon|tickets|UPDATE authenticated|tickets|DELETE authenticated|tickets|INSERT authenticated|tickets|REFERENCES authenticated|tickets|SELECT authenticated|tickets|TRIGGER authenticated|tickets|TRUNCATE authenticated|tickets|UPDATE (42 rows)
mkdir -p /opt/rls && cat > /opt/rls/migration.sql <<'SQL'
-- ============================================================================
-- Multi-tenant tenant isolation via Row-Level Security
-- Tables: public.orgs, public.org_members, public.tickets
-- Rules:
-- * org members (any role) : READ tickets of their org
-- * org admins only : INSERT/UPDATE/DELETE tickets of their org
-- * non-members / anonymous : see no tickets
-- * org_members rows : visible only to members of the same org;
-- anonymous sees none
-- * orgs : stays readable (unchanged behaviour)
-- ============================================================================
BEGIN;
-- ----------------------------------------------------------------------------
-- 1. SECURITY DEFINER helpers (owned by postgres -> bypass RLS on org_members,
-- so policies on org_members can self-reference without infinite recursion).
-- Not granted to API roles; callable only from within policies.
-- ----------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION public.is_org_member(p_org_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
SELECT EXISTS (
SELECT 1
FROM public.org_members m
WHERE m.org_id = p_org_id
AND m.user_id = auth.uid()
);
$$;
CREATE OR REPLACE FUNCTION public.is_org_admin(p_org_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
SELECT EXISTS (
SELECT 1
FROM public.org_members m
WHERE m.org_id = p_org_id
AND m.user_id = auth.uid()
AND m.role = 'admin'
);
$$;
REVOKE ALL ON FUNCTION public.is_org_member(uuid) FROM PUBLIC, anon, authenticated;
REVOKE ALL ON FUNCTION public.is_org_admin(uuid) FROM PUBLIC, anon, authenticated;
-- ----------------------------------------------------------------------------
-- 2. Enable RLS on all three tables.
-- ----------------------------------------------------------------------------
ALTER TABLE public.tickets ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.org_members ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.orgs ENABLE ROW LEVEL SECURITY;
-- ----------------------------------------------------------------------------
-- 3. tickets policies
-- ----------------------------------------------------------------------------
-- READ: any member (admin or member role) of the ticket's org.
CREATE POLICY tickets_select_member
ON public.tickets FOR SELECT
TO authenticated
USING (public.is_org_member(org_id));
-- INSERT: only admins of the org; new row must belong to an org they admin.
CREATE POLICY tickets_insert_admin
ON public.tickets FOR INSERT
TO authenticated
WITH CHECK (public.is_org_admin(org_id));
-- UPDATE: only admins; row must be in their org before AND after the change
-- (USING gates the existing row, WITH CHECK gates the new values).
CREATE POLICY tickets_update_admin
ON public.tickets FOR UPDATE
TO authenticated
USING (public.is_org_admin(org_id))
WITH CHECK (public.is_org_admin(org_id));
-- DELETE: only admins of the org.
CREATE POLICY tickets_delete_admin
ON public.tickets FOR DELETE
TO authenticated
USING (public.is_org_admin(org_id));
-- ----------------------------------------------------------------------------
-- 4. org_members policies
-- ----------------------------------------------------------------------------
-- READ: a user sees only rows of orgs they themselves belong to.
-- (helper function is SECURITY DEFINER -> no policy recursion)
CREATE POLICY org_members_select_own_org
ON public.org_members FOR SELECT
TO authenticated
USING (public.is_org_member(org_id));
-- No INSERT/UPDATE/DELETE policies for API roles on org_members:
-- membership management stays server-side (service_role bypasses RLS).
-- ----------------------------------------------------------------------------
-- 5. orgs policies -- "migration written 114 /opt/rls/migration.sql
export PGPASSWORD="$SUPABASE_DB_PASSWORD"
psql "$SUPABASE_DB_URL" -X -v ON_ERROR_STOP=1 -f /opt/rls/migration.sql && echo "=== APPLIED ===" && psql "$SUPABASE_DB_URL" -X -A -c "
SELECT tablename, rowsecurity FROM pg_tables WHERE schemaname='public' AND tablename IN ('orgs','org_members','tickets') ORDER BY tablename;
" -c "
SELECT tablename, policyname, cmd, roles FROM pg_policies WHERE schemaname='public' ORDER BY tablename, policyname;
"BEGIN
CREATE FUNCTION
CREATE FUNCTION
REVOKE
REVOKE
ALTER TABLE
ALTER TABLE
ALTER TABLE
CREATE POLICY
CREATE POLICY
CREATE POLICY
CREATE POLICY
CREATE POLICY
CREATE POLICY
COMMIT
=== APPLIED ===
tablename|rowsecurity
org_members|t
orgs|t
tickets|t
(3 rows)
tablename|policyname|cmd|roles
org_members|org_members_select_own_org|SELECT|{authenticated}
orgs|orgs_select_all|SELECT|{anon,authenticated}
tickets|tickets_delete_admin|DELETE|{authenticated}
tickets|tickets_insert_admin|INSERT|{authenticated}
tickets|tickets_select_member|SELECT|{authenticated}
tickets|tickets_update_admin|UPDATE|{authenticated}
(6 rows)
cat > /opt/rls/verify.sh <<'SH'
#!/usr/bin/env bash
set -u
URL="$SUPABASE_URL"; ANON="$SUPABASE_ANON_KEY"
PASS=0; FAIL=0
ok() { if [ "$1" = "$2" ]; then PASS=$((PASS+1)); echo "PASS: $3 [got=$1]"; else FAIL=$((FAIL+1)); echo "FAIL: $3 [want=$2 got=$1]"; fi; }
login() {
curl -sS -X POST "$URL/auth/v1/token?grant_type=password" \
-H "apikey: $ANON" -H "Content-Type: application/json" \
-d "{\"email\":\"$1\",\"password\":\"$2\"}" | jq -r '.access_token // empty'
}
UMA=$(login uma-admin@cari.dev 'Passw0rd-uma-11') # Acme admin
VIC=$(login vic-member@cari.dev 'Passw0rd-vic-22') # Acme member
WEN=$(login wen-outsider@cari.dev 'Passw0rd-wen-33') # Globex member
[ -n "$UMA" ] && [ -n "$VIC" ] && [ -n "$WEN" ] && echo "All 3 logins OK" || { echo "LOGIN FAILURE"; exit 1; }
ACME=$(curl -sS "$URL/rest/v1/orgs?select=id,name&name=eq.Acme" -H "apikey: $ANON" | jq -r '.[0].id')
GLOBEX=$(curl -sS "$URL/rest/v1/orgs?select=id,name&name=eq.Globex" -H "apikey: $ANON" | jq -r '.[0].id')
echo "Acme=$ACME Globex=$GLOBEX"
t_count() { curl -sS "$URL/rest/v1/tickets?select=id,org_id,title" -H "apikey: $ANON" ${2:+-H "Authorization: Bearer $2"} | jq "if type==\"array\" then [.[]|select(.org_id==\"$1\")]|length else -1 end"; }
t_titles() { curl -sS "$URL/rest/v1/tickets?select=org_id,title" -H "apikey: $ANON" -H "Authorization: Bearer $2" | jq -r "[.[]|select(.org_id==\"$1\")|.title]|join(\",\")"; }
m_count() { curl -sS "$URL/rest/v1/org_members?select=org_id,role" -H "apikey: $ANON" ${2:+-H "Authorization: Bearer $2"} | jq "if type==\"array\" then [.[]|select(.org_id==\"$1\")]|length else -1 end"; }
echo; echo "--- 1. TICKET READS ---"
ok "$(t_count "$ACME" "$UMA")" 3 "uma(admin,Acme) reads Acme tickets: $(t_titles "$ACME" "$UMA")"
ok "$(t_count "$ACME" "$VIC")" 3 "vic(member,Acme) reads Acme tickets"
ok "$(t_count "$GLOBEX" "$WEN")" 2 "wen(member,Globex) reads Globex tickets"
ok "$(t_count "$GLOBEX" "$UMA")" 0 "uma sees ZERO Globex tickets"
ok "$(t_count "$GLOBEX" "$VIC")" 0 "vic sees ZERO Globex tickets"
ok "$(t_count "$ACME" "$WEN")" 0 "wen sees ZERO Acme tickets"
ok "$(t_count "$ACME")" 0 "anonymous sees ZERO Acme tickets"
ok "$(t_count "$GLOBEX")" 0 "anonymous sees ZERO Globex tickets"
echo; echo "--- 2. TICKET WRITES ---"
# helper: attempt write, return HTTP status
ins() { curl -sS -o /dev/null -w '%{http_code}' -X POST "$URL/rest/v1/tickets" -H "apikey: $ANON" -H "Authorization: Bearer $1" -H "Content-Type: application/json" -H "Prefer: return=minimal" -d "{\"org_id\":\"$2\",\"title\":\"__rls_probe__\",\"status\":\"open\"}"; }
# admin inserts own-org ticket -> 201, then delete it
ST=$(ins "$UMA" "$ACME"); ok "$ST" 201 "uma(admin) INSERT Acme ticket"
NEWID=$(curl -sS "$URL/rest/v1/tickets?select=id&title=eq.__rls_probe__&org_id=eq.$ACME" -H "apikey: $ANON" -H "Authorization: Bearer $UMA" | jq -r '.[0].id // empty')
# member insert -> denied (403/401/42501)
ST=$(ins "$VIC" "$ACME"); case "$ST" in 401|403) ok "$ST" "$ST" "vic(member) INSERT Acme denied";; *) ok "$ST" 403 "vic(member) INSERT Acme denied";; esac
# admin insert into OTHER org -> denied
ST=$(ins "$UMA" "$GLOBEX"); case "$ST" in 401|403) ok "$ST" "$ST" "uma(admin) INSERT Globex denied (not her org)";; *) ok "$ST" 403 "uma INSERT Globex denied";; esac
# outsider insert own org as member -> denied
ST=$(ins "$WEN" "$GLOBEX"); case "$ST" in 401|403) ok "$ST" "$ST" "wen(member) INSERT Globex denied";; *) ok "$ST" 403 "wen INSERT Globex denied";; esac
# UPDATE: admin updates own ticket title, then restores
DELCODE() { curl -sS -o /dev/null -w '%{http_code}' -X PATCH "$URL/rest/v1/tickets?id=eq.$2" -H "apikey: $ANON" -H "Authorization: Bearer $1" -H "Content-Type: application/json" -H "Prefer: return=representation" -d "{\"title\":\"$3\"}"; }
REAL=$(curl -sS "$URL/rest/v1/tickets?select=id,title&org_id=eq.$ACME&id=neq.$NEWID&limit=1" -H "apikey: $ANON" -H "Authorization: Bearer $UMA" | jq -r '.[0].id')
REAL_T=$(curl -sS "$URL/rest/v1/tickets?select=tAll 3 logins OK Acme=f112a917-8dc5-4c87-baa3-b60922d2c6b2 Globex=0ca3dfe5-cc5d-4343-aee8-320e6ae03f46 --- 1. TICKET READS --- FAIL: uma(admin,Acme) reads Acme tickets: [want=3 got=-1] FAIL: vic(member,Acme) reads Acme tickets [want=3 got=-1] FAIL: wen(member,Globex) reads Globex tickets [want=2 got=-1] FAIL: uma sees ZERO Globex tickets [want=0 got=-1] FAIL: vic sees ZERO Globex tickets [want=0 got=-1] FAIL: wen sees ZERO Acme tickets [want=0 got=-1] PASS: anonymous sees ZERO Acme tickets [got=0] PASS: anonymous sees ZERO Globex tickets [got=0] --- 2. TICKET WRITES --- FAIL: uma(admin) INSERT Acme ticket [want=201 got=403] PASS: vic(member) INSERT Acme denied [got=403] PASS: uma(admin) INSERT Globex denied (not her org) [got=403] PASS: wen(member) INSERT Globex denied [got=403] FAIL: uma(admin) UPDATE own-org ticket [want=1 got=4] FAIL: vic(member) UPDATE Acme ticket -> 0 rows (denied) [want=0 got=4] FAIL: uma(admin) UPDATE Globex ticket -> 0 rows (not her org) [want=0 got=4] FAIL: vic(member) DELETE Acme ticket -> 0 rows (denied) [want=0 got=4] FAIL: uma(admin) DELETE own-org ticket (probe cleaned up) [want=1 got=4] --- 3. ORG_MEMBERS VISIBILITY --- FAIL: uma sees Acme member list (2 rows) [want=2 got=-1] FAIL: vic sees Acme member list (2 rows) [want=2 got=-1] FAIL: uma sees ZERO Globex members [want=0 got=-1] FAIL: wen sees ZERO Acme members [want=0 got=-1] FAIL: wen sees her own Globex membership (1 row) [want=1 got=-1] PASS: anonymous sees ZERO org_members [got=0] FAIL: wen cannot DELETE her own membership row (still present) [want=1 got=4] --- 4. ORGS READABLE (unchanged) --- PASS: anonymous can still READ orgs (2) [got=2] PASS: uma can still READ orgs (2) [got=2] --- 5. DATA INTEGRITY CHECK (unchanged) --- FAIL: Acme still has exactly 3 tickets [want=3 got=4] FAIL: Globex still has exactly 2 tickets [want=2 got=4] FAIL: no probe rows left behind [want=0 got=4] =================================== RESULT: PASS=8 FAIL=21
jq: error (at <stdin>:0): Cannot index string with string "org_id" jq: error (at <stdin>:0): Cannot index object with number jq: error (at <stdin>:0): Cannot index object with number jq: error (at <stdin>:0): Cannot index object with number jq: error (at <stdin>:0): Cannot index object with number
ANON="$SUPABASE_ANON_KEY"; URL="$SUPABASE_URL"
UMA=$(curl -sS -X POST "$URL/auth/v1/token?grant_type=password" -H "apikey: $ANON" -H "Content-Type: application/json" -d '{"email":"uma-admin@cari.dev","password":"Passw0rd-uma-11"}' | jq -r '.access_token // empty')
echo "token len: ${#UMA}"
echo "--- raw tickets response as uma:"
curl -sS "$URL/rest/v1/tickets?select=id,org_id,title" -H "apikey: $ANON" -H "Authorization: Bearer $UMA"
echo; echo "--- raw org_members response as uma:"
curl -sS "$URL/rest/v1/org_members?select=org_id,role" -H "apikey: $ANON" -H "Authorization: Bearer $UMA"token len: 796
--- raw tickets response as uma:
{"code":"42501","details":null,"hint":null,"message":"permission denied for function is_org_member"}
--- raw org_members response as uma:
{"code":"42501","details":null,"hint":null,"message":"permission denied for function is_org_member"}export PGPASSWORD="$SUPABASE_DB_PASSWORD"
psql "$SUPABASE_DB_URL" -X -v ON_ERROR_STOP=1 -c "GRANT EXECUTE ON FUNCTION public.is_org_member(uuid) TO authenticated;" -c "GRANT EXECUTE ON FUNCTION public.is_org_admin(uuid) TO authenticated;" && echo OK && psql "$SUPABASE_DB_URL" -X -A -c "
SELECT p.proname, p.proacl, r.rolname AS owner, p.prosecdef
FROM pg_proc p JOIN pg_roles r ON r.oid=p.proowner
JOIN pg_namespace n ON n.oid=p.pronamespace
WHERE n.nspname='public' AND p.proname IN ('is_org_member','is_org_admin');"GRANT
GRANT
OK
proname|proacl|owner|prosecdef
is_org_member|{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}|postgres|t
is_org_admin|{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}|postgres|t
(2 rows)
/opt/rls/verify.sh 2>/tmp/jqerr.log; echo "--- jq stderr (should be empty):"; cat /tmp/jqerr.log
All 3 logins OK Acme=f112a917-8dc5-4c87-baa3-b60922d2c6b2 Globex=0ca3dfe5-cc5d-4343-aee8-320e6ae03f46 --- 1. TICKET READS --- PASS: uma(admin,Acme) reads Acme tickets: Fix login,Update billing,Rotate keys [got=3] PASS: vic(member,Acme) reads Acme tickets [got=3] PASS: wen(member,Globex) reads Globex tickets [got=2] PASS: uma sees ZERO Globex tickets [got=0] PASS: vic sees ZERO Globex tickets [got=0] PASS: wen sees ZERO Acme tickets [got=0] PASS: anonymous sees ZERO Acme tickets [got=0] PASS: anonymous sees ZERO Globex tickets [got=0] --- 2. TICKET WRITES --- PASS: uma(admin) INSERT Acme ticket [got=201] PASS: vic(member) INSERT Acme denied [got=403] PASS: uma(admin) INSERT Globex denied (not her org) [got=403] PASS: wen(member) INSERT Globex denied [got=403] PASS: uma(admin) UPDATE own-org ticket [got=1] PASS: vic(member) UPDATE Acme ticket -> 0 rows (denied) [got=0] PASS: uma(admin) UPDATE Globex ticket -> 0 rows (not her org) [got=0] PASS: vic(member) DELETE Acme ticket -> 0 rows (denied) [got=0] PASS: uma(admin) DELETE own-org ticket (probe cleaned up) [got=1] --- 3. ORG_MEMBERS VISIBILITY --- PASS: uma sees Acme member list (2 rows) [got=2] PASS: vic sees Acme member list (2 rows) [got=2] PASS: uma sees ZERO Globex members [got=0] PASS: wen sees ZERO Acme members [got=0] PASS: wen sees her own Globex membership (1 row) [got=1] PASS: anonymous sees ZERO org_members [got=0] PASS: wen cannot DELETE her own membership row (still present) [got=1] --- 4. ORGS READABLE (unchanged) --- PASS: anonymous can still READ orgs (2) [got=2] PASS: uma can still READ orgs (2) [got=2] --- 5. DATA INTEGRITY CHECK (unchanged) --- PASS: Acme still has exactly 3 tickets [got=3] PASS: Globex still has exactly 2 tickets [got=2] PASS: no probe rows left behind [got=0] =================================== RESULT: PASS=29 FAIL=0 --- jq stderr (should be empty):
export PGPASSWORD="$SUPABASE_DB_PASSWORD"
psql "$SUPABASE_DB_URL" -X -A -c "
SELECT o.name, count(t.id) AS tickets FROM public.orgs o LEFT JOIN public.tickets t ON t.org_id=o.id GROUP BY o.name ORDER BY o.name;
" -c "
SELECT count(*) AS probe_rows_left FROM public.tickets WHERE title LIKE '\_\_%probe%\_\_' OR title='\_\_rls_probe\_\_';
" -c "
SELECT tablename, policyname, cmd, roles FROM pg_policies WHERE schemaname='public' ORDER BY tablename, policyname;
"
echo "--- anon calling helper function directly (must be denied/return false):"
ANON="$SUPABASE_ANON_KEY"; URL="$SUPABASE_URL"
curl -sS -X POST "$URL/rest/v1/rpc/is_org_member" -H "apikey: $ANON" -H "Content-Type: application/json" -d "{\"p_org_id\":\"f112a917-8dc5-4c87-baa3-b60922d2c6b2\"}"
echo
echo "--- original Acme ticket titles intact:"
UMA=$(curl -sS -X POST "$URL/auth/v1/token?grant_type=password" -H "apikey: $ANON" -H "Content-Type: application/json" -d '{"email":"uma-admin@cari.dev","password":"Passw0rd-uma-11"}' | jq -r '.access_token')
curl -sS "$URL/rest/v1/tickets?select=title,status&order=id" -H "apikey: $ANON" -H "Authorization: Bearer $UMA" | jq -c '.'name|tickets
Acme|3
Globex|2
(2 rows)
probe_rows_left
0
(1 row)
tablename|policyname|cmd|roles
org_members|org_members_select_own_org|SELECT|{authenticated}
orgs|orgs_select_all|SELECT|{anon,authenticated}
tickets|tickets_delete_admin|DELETE|{authenticated}
tickets|tickets_insert_admin|INSERT|{authenticated}
tickets|tickets_select_member|SELECT|{authenticated}
tickets|tickets_update_admin|UPDATE|{authenticated}
(6 rows)
--- anon calling helper function directly (must be denied/return false):
{"code":"42501","details":null,"hint":null,"message":"permission denied for function is_org_member"}
--- original Acme ticket titles intact:
[{"title":"Fix login","status":"open"},{"title":"Update billing","status":"open"},{"title":"Rotate keys","status":"open"}]
5 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