Skip to content

feat: eval the Row Level Security guide in supabase.com/docs - #168

Open
czenko wants to merge 14 commits into
mainfrom
quickstart-rls
Open

feat: eval the Row Level Security guide in supabase.com/docs#168
czenko wants to merge 14 commits into
mainfrom
quickstart-rls

Conversation

@czenko

@czenko czenko commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Closes DOCS-1259

Problem

We point agents at the Row Level Security guide. Nobody has measured whether reading it is enough to produce secure policies.

The previous version of this eval could not answer that. It copied the guide's AI prompt panel into PROMPT.md, so the eleven rules that spell out the answer were the prompt itself.

Solution

The guide is now the subject under test. The prompt is a normal feature request plus the guide's url. The eleven rules became the grading criteria.

The prompt is written as a vibe coder. It never says RLS, policy, security, role, tenant or test. An agent that only writes secure policies when the prompt says "security" has not been served by the guide.

The seed is two separate apps, so one access pattern cannot be applied to everything.

  • Owner-private. todos.
  • Shared through membership. lists, list_members, list_items.
  • Public read with no client writes. weather_stations, weather_readings.

using (true) is correct on the weather feed and catastrophic on todos. list_members forces a security definer helper, because a policy that joins it is a join inside a policy, and a policy that queries itself raises 42P17.

Nothing in the migration says who may read or write what.

The prompt does not ask for tests. Whether the agent arrives at pgTAP is part of the measurement.

Rename

Both docs-prompt evals move into a series named for the documentation under test.

From To
build-cli-004-quickstart-nextjs build-docs-001-homepage-quickstart
build-cli-005-quickstart-rls build-docs-002-rls-guide

Renaming breaks the gh-pages history series for 004, which is keyed by eval id.

Testing

pnpm typecheck and pnpm format:check are clean. pnpm eval:dry confirms discovery and frontmatter for both ids. Results are refreshing in CI.

The scorer has not been run end to end. Local runs were blocked by another project's stack holding the Supabase ports.

@czenko
czenko requested a review from a team August 4, 2026 22:24
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
evals Ready Ready Preview Aug 7, 2026 11:50pm

Request Review

@barryroodt barryroodt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is a useful eval and the core shape looks good. pnpm format:check and pnpm typecheck pass. I found several false-pass paths that need closing before merge, and I’ve left the exact cases inline.

The PR also needs refreshed eval results. The handwritten solution proves one passing path. Real agent results show whether the eval discriminates.

pnpm check failed in one results-export test; attribution is unresolved.

Comment thread evals/build-cli-005-quickstart-rls/EVAL.ts Outdated
Comment thread evals/build-cli-005-quickstart-rls/EVAL.ts Outdated
Comment thread evals/build-cli-005-quickstart-rls/EVAL.ts Outdated
Comment thread evals/build-cli-005-quickstart-rls/EVAL.ts Outdated
Comment thread evals/build-cli-005-quickstart-rls/EVAL.ts Outdated
Comment thread evals/build-cli-005-quickstart-rls/PROMPT.md Outdated
Comment thread evals/build-cli-005-quickstart-rls/EVAL.ts Outdated
@czenko
czenko marked this pull request as draft August 7, 2026 19:00
@czenko czenko changed the title Write Eval for RLS quickstart prompt feat: eval the Row Level Security guide, add a build-docs series Aug 7, 2026
czenko added a commit that referenced this pull request Aug 7, 2026
The RLS eval previously copied the guide's "AI Prompt" panel verbatim into
PROMPT.md, so the eleven rules that spell out the answer were the prompt and
the scorer checked the agent against them. That measures spec-following, not
the guide.

Rewrite it so the guide is the subject under test. The prompt is now a vibe
coder's request naming two separate apps plus the guide's url, with RLS,
policy, security, role, tenant and test stripped from the vocabulary. The
eleven rules became the grading criteria. The seed carries the app domain and
says nothing about who may read or write what.

Seed covers the three access shapes most apps need: owner-private todos,
membership-shared lists, and a public weather feed no client writes to.
list_members forces a security definer helper, since a policy that joins it is
the join to avoid and a policy that queries itself raises 42P17.

Move both docs-prompt evals into a build-docs series, named for the
documentation under test rather than the interface:

  build-cli-004-quickstart-nextjs -> build-docs-001-homepage-quickstart
  build-cli-005-quickstart-rls    -> build-docs-002-rls-guide

Address the review on #168:

- Allowed writes now prove state changed, including delete, and denied writes
  prove the row survived.
- The helper check resolves the exact function the list_items policies call and
  requires security definer plus search_path pinned empty.
- pgTAP coverage is bound to the seeded tables before the judge runs.
- Every rule the seed implies has a live probe, plus a 42P17 probe.
- The index check requires btree and rejects partial indexes.
- The scorer is split into EVAL.ts, catalog.ts, access.ts and tests.ts.

Narrow apps/framework/tsconfig.json from evals/**/EVAL.ts to evals/*/*.ts so
sibling scorer modules are typechecked, while still excluding local/, remote/
and tests/ fixtures.

Drop two grant checks that could only fail on over-revoke, since Supabase grants
all four privileges to anon and authenticated by default and the live probes
cover the outcome. The two remaining grant checks measure narrowing those
defaults.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@czenko czenko added the run-evals-changed Add to a PR to refresh only the benchmark evals that have had changes label Aug 7, 2026
@czenko czenko changed the title feat: eval the Row Level Security guide, add a build-docs series feat: eval the Row Level Security guide Aug 7, 2026
czenko and others added 4 commits August 7, 2026 15:15
The RLS eval previously copied the guide's "AI Prompt" panel verbatim into
PROMPT.md, so the eleven rules that spell out the answer were the prompt and
the scorer checked the agent against them. That measures spec-following, not
the guide.

Rewrite it so the guide is the subject under test. The prompt is now a vibe
coder's request naming two separate apps plus the guide's url, with RLS,
policy, security, role, tenant and test stripped from the vocabulary. The
eleven rules became the grading criteria. The seed carries the app domain and
says nothing about who may read or write what.

Seed covers the three access shapes most apps need: owner-private todos,
membership-shared lists, and a public weather feed no client writes to.
list_members forces a security definer helper, since a policy that joins it is
the join to avoid and a policy that queries itself raises 42P17.

Move both docs-prompt evals into a build-docs series, named for the
documentation under test rather than the interface:

  build-cli-004-quickstart-nextjs -> build-docs-001-homepage-quickstart
  build-cli-005-quickstart-rls    -> build-docs-002-rls-guide

Address the review on #168:

- Allowed writes now prove state changed, including delete, and denied writes
  prove the row survived.
- The helper check resolves the exact function the list_items policies call and
  requires security definer plus search_path pinned empty.
- pgTAP coverage is bound to the seeded tables before the judge runs.
- Every rule the seed implies has a live probe, plus a 42P17 probe.
- The index check requires btree and rejects partial indexes.
- The scorer is split into EVAL.ts, catalog.ts, access.ts and tests.ts.

Narrow apps/framework/tsconfig.json from evals/**/EVAL.ts to evals/*/*.ts so
sibling scorer modules are typechecked, while still excluding local/, remote/
and tests/ fixtures.

Drop two grant checks that could only fail on over-revoke, since Supabase grants
all four privileges to anon and authenticated by default and the live probes
cover the outcome. The two remaining grant checks measure narrowing those
defaults.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Narrowing the include to evals/*/*.ts was unnecessary. TypeScript typechecks
transitively imported files, so EVAL.ts importing ./catalog.js, ./access.js and
./tests.js already pulls the sibling scorer modules into the program.

Verified by planting a type error in access.ts and running tsc with the original
evals/**/EVAL.ts include. The error is reported.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The check scanned tool call requests for the guide url, which misses a
search_docs hit. There the url comes back in the result, not the request, so an
agent that reached the guide through the docs MCP server was scored as never
having read it.

Seen in CI: one run web_fetched the url and passed, the next used search_docs,
pulled 72,200 characters with the guide as the top hit, and failed.

Reuse buildDocsResult, which the harness already uses to resolve pages across
search_docs, web_fetch, web_search and shell_fetch, and require the call to have
actually retrieved page content.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@czenko czenko changed the title feat: eval the Row Level Security guide feat: eval the Row Level Security guide in supabase.com/docs Aug 7, 2026
@czenko
czenko marked this pull request as ready for review August 7, 2026 22:43
czenko and others added 2 commits August 7, 2026 16:16
persona is not in evalFrontmatterSchema. The preprocessor builds a fixed object
from known keys, so it was silently dropped and surfaced nowhere. It also had no
precedent: across 44 evals it appeared once, in this file. motivation is not in
the schema either, but 29 evals use it, so that one is convention.

Fold the pointer into motivation. The persona itself is already documented in the
eval README, which is where it belongs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
czenko and others added 5 commits August 7, 2026 16:28
… for a helper

The check required a security definer helper in a private schema and failed
anything else. Membership has more than one safe implementation, and the guide
itself teaches one that needs no function at all, keeping membership in the
JWT. An agent following that example wrote secure policies, passed every access
probe, and still failed this check. That is checking process, not end state.

Invert it. Whether membership actually holds is already proven by the access
probes. This now fails only the shapes that are genuinely dangerous:

- a security definer function in public or graphql_public, which PostgREST
  exposes as an RPC callable by anon
- a security definer function anywhere that does not pin search_path

No such function is a pass, because the agent solved it another way and the
probes covered the outcome.

Widen the function loader to keep the exposed schemas in scope. It previously
filtered public out as pre-existing, which is exactly where the dangerous case
lands. A stock stack has no security definer functions in either exposed schema,
so anything found there is the agent's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Audited every check against the guide's actual text. Two tested beyond it.

Remove the force-row-level-security check. The guide never mentions forcing
RLS. Its Bypassing section covers service keys and the bypassrls role privilege
only, so an agent had no way to learn this from the guide.

Remove the search_path condition on security definer functions. The guide
cautions only that they must not live in an exposed schema, and its own
private.has_good_role() example does not pin search_path. Pinning is good
practice, but testing it measures the model's background knowledge rather than
the guide, so it belongs in a doc recommendation instead.

What remains traces to the guide: enabling RLS, specifying roles, per-operation
policies, using plus with check on updates, granting only what a role needs,
indexing policy columns, wrapping auth.uid() in a select, minimizing joins, and
keeping security definer functions out of exposed schemas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… gap

The scoping rule is whether something is the guide's job, not whether the guide
currently says it. A gap is a failure, because the question is whether the
document does its job.

Teaching a pattern includes teaching it safely. The guide tells a reader to
create a security definer function and gives a copy-pasteable example that
leaves search_path unpinned, which is exploitable: anyone able to create objects
can shadow an unqualified name and change what the elevated function reads. An
agent following the guide faithfully inherits that.

Same shape as the pgTAP gap, so it is scored the same way.

The force-row-level-security check stays removed. Protecting a table from its
own owner is not what this guide is for, and the access probes already catch the
breakage it would cause.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-evals-changed Add to a PR to refresh only the benchmark evals that have had changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants