Peter D. BethkeSiteShell.netSpring Hollow Publishing, Inc.

Where the boundary lives

Who may write, what they may write, and the place I put the boundary that an agent walks straight past.

Thirty years of building multi-user systems has left me with one question I ask of every new platform: can it build an RBAC?

It's a good test, because role-based access control exercises everything at once. A data model that expresses many-to-many honestly. Joins that behave predictably when a foreign key is null. Somewhere to hold identity, and a way to resolve it into effective permissions. Writes, and a story about who may make them. And a UI that reflects authority without being the authority.

So I built one on Sigma, and published it.

Open it and act as Priya. She sees three stores. Switch to Kwame and the same page, running the same formula, shows one. There is no filter on screen to explain the difference. Nothing was hidden from him. The rows were never in his result set.

WHO MAY WRITE

The part I'd point out to anyone building this kind of thing is smaller than that, and easier to get wrong. The store dropdown on the entry form offers only the stores you are allowed to write to. Not validated on submit, gated at the point of offer, so an illegal choice is never representable.

That distinction matters more than it looks. A dropdown that lists three stores to someone who may only touch one has already disclosed the other two. Most authorization work I've reviewed guards the result set and leaves the option lists wide open.

What makes this an application-level model rather than a platform feature: Sigma knows nothing about it. My roles, my scopes, my grant semantics. The same tables run unchanged in SQLite, and that SQL model computes the expected answer for every count in the app, independently.

Sigma passed, and the exercise left me confident in the platform.

WHAT THEY MAY WRITE

Every validation you hardcode is a deploy waiting to happen.

The entry form in the RBAC demo takes a serial number. Serial formats vary by brand, and some product lines override their brand's format. That is the kind of requirement that usually ends up as a regex in application code, which means every new brand is a pull request, a review and a release.

So I put the patterns in the data instead. Brands carry a serial pattern. Product lines carry one too, and when both exist the more specific wins. Resolution is a single COALESCE: line first, then brand. Adding a rule for a new brand is inserting a row.

Two details turned out to matter more than the pattern matching.

The first is that an empty pattern is not a missing rule. It is the absence of a constraint, and I had to decide that deliberately rather than let a null decide it for me. Products exist that no one has written a format for yet, and the right behavior is to accept anything, not to block the user out of a rule that was never authored.

The second is the one I'd keep. The hint shown under the field lives in the same row as the pattern that enforces it. Change the rule and the help text changes with it, because they are the same fact. Documentation cannot drift from enforcement when there is only one of them.

Worth naming what stacks up by the time someone hits submit. Whether you may post at all is one question, answered by your grant. Aaliyah reads every row of her store and can post nothing. Which store you may post to is a second, answered by a dropdown that offers only your own, so an illegal choice is never representable. Kwame stands in the same store as Aaliyah, reads the same 31,500 rows, and differs from her in one boolean, and even he can only choose Abilene. Whether the serial is well formed is the third. Three independent questions, none of them answered by hiding a button.

A SQL model checks every stored serial against its resolved pattern independently. That check has been made to fail on purpose, so an empty result means it works rather than that it was never exercised.

WHERE THE BOUNDARY LIVES

Prompt injection is an attack on instruction-following. That's why it keeps working.

If your boundary is an instruction — a system prompt listing which users the model may discuss — then injection is a conversation with the boundary, and conversations can be won. If the boundary is what's in context at all, there is nothing to talk to. You cannot talk your way into data that was never in the query.

An agent grounded on a scoped view doesn't decline questions about other people's data. It has no idea the rows exist. Nothing is refused, because nothing is there.

As of July 2026, Sigma has announced agents that run outside the workbook, over an API and MCP, so they work from Claude or Codex. For this pattern that's genuinely good news: a governed model stops being somewhere you visit and becomes something your tools can reach.

It also handed me a piece of homework, and it's the useful kind.

Every boundary in this demo is a workbook element — a join I authored in the presentation layer. That was the right way to build a demonstration and the wrong place to leave it. An agent that reaches data without opening the page doesn't pass through those joins. Not because anything is broken, but because they were never on its route. That's a fact about where I put the boundary, not about the platform.

The fix is the one the architecture was already pointing at. Express the same rule as a governed view in the warehouse, and every consumer inherits it: the workbook, the API, an agent in someone's editor, whatever ships next year. My repo already carries that rule as SQL, which is the proof it ports. Authorization in the presentation layer was always a convenience, and agents are simply what makes the bill arrive.

The next thing I want to test is identity. If an external agent carries the calling user through, per-user scoping works at the data layer and the whole pattern extends cleanly. That's the interesting question, and I'd rather ask it than assume an answer.

When I get to it, the test is one number: ask for inventory as a store manager and see whether 31,500 comes back.

It's a demo, not a product. The build log, the SQL oracle and a Flask-to-Sigma translation guide are all in the repo.

Workbook: https://public.sigmacomputing.com/view-workbook?workbook=2PKJk4ZoUqLEksgSHHicKN

Code: github.com/pdbethke/sigma-rbac-lab