Skip to content

About

A system prompt refusing a request doesn't prove the underlying data access is blocked. This repo builds that boundary as an actual permission check, then proves with tests that it holds even when the agent tries to break out of its role. Companion code for a writeup at zetesis.dev.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

3 Commits

Folders and files

Repository files navigation

rag-credential-boundary

A system prompt cannot substitute for an authorization boundary. If a service inherits credentials scoped to a resource it should never touch, no amount of prompt engineering closes that gap, because the check that matters happens below the model, at the point where a query hits the data store. This repo makes that checkable two ways: watch the leak happen when the credential is wrong, and watch the fix hold while the agent's behaviour stays deliberately adversarial.

Where the permission check runs

Run it

Three commands. You need Python 3.12 or newer and nothing else: no accounts, no API keys, no paid services.

git clone https://github.com/Zetesis-dev/rag-credential-boundary
cd rag-credential-boundary
python run.py

run.py builds a local environment, installs what it needs, starts the demo and opens it in your browser at http://127.0.0.1:8001. Press Ctrl+C to stop it. On macOS and Linux, type python3 instead of python.

Pick a credential on the page, press Run, and watch the request travel to the permission check and branch. Every result shown comes from a real call into the code in src/.

The rest of it

run.py puts everything in a .venv folder. Activate it to get at the terminal demo and the tests:

.venv\Scripts\activate           # Windows
source .venv/bin/activate        # macOS and Linux

python demo/before.py    # credential copied wholesale from internal-assistant  ->  LEAK
python demo/after.py     # credential scoped to public_docs                     ->  BLOCKED
pytest                   # the proof
python web/serve.py      # the interactive page again, without the setup step

demo/before.py and demo/after.py are the same file apart from one line. Diff them:

$ diff demo/before.py demo/after.py
15c15
<     "public-assistant", grant=INTERNAL_ASSISTANT.allowed_collections
---
>     "public-assistant", grant=[PUBLIC_DOCS]

Same agent, same system prompt, same adversarial turn, same store. Only the credential changed.

What this proves

  • tests/test_permission_boundary.py: the check stands on its own. It calls the store directly with a credential scoped to public_docs, asks for internal_docs, and gets a PermissionError. No agent is imported anywhere in the file, because a boundary that needs a model in the loop is not a boundary.

  • tests/test_agent_behavior_is_irrelevant.py: the fix is not "the agent got smarter". The same scripted agent, given the same turn, plans the same out-of-role request in both runs. With the inherited credential it leaks; with the correctly scoped one the store refuses. The behaviour is identical and the outcome is not, because enforcement moved somewhere the agent cannot reach.

  • tests/test_no_implicit_inheritance.py: the regression test for the root cause. Building one service's credential from another's config is a reasonable thing to do; carrying its grants along silently is what shipped the bug. Credential.derive() returns a credential with no collections until each one is named, and this test holds it to that.

The agent is a scripted stand-in rather than a real model call, on purpose. A real model might refuse the adversarial turn, and a demonstration that depends on it refusing proves nothing. This one always takes the widest collection it can name, so whatever holds here holds against a model that has been talked into anything.

Synthetic reconstruction

This models a pattern from a real engagement. Every service name, credential, collection and document in this repo is invented for the reconstruction. None of it is the original code, and nothing here identifies the client or the system it came from.

Writeup

The engineering writeup this accompanies is at zetesis.dev.

License

MIT. See LICENSE.

About

A system prompt refusing a request doesn't prove the underlying data access is blocked. This repo builds that boundary as an actual permission check, then proves with tests that it holds even when the agent tries to break out of its role. Companion code for a writeup at zetesis.dev.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Contributors

Languages