Prompt instructions are requests. Permissions are guarantees. This repo proves the difference with a test suite.
A toy assistant answers questions over a records store with two enforcement modes. In prompt mode, security is a guard instruction ("never reveal gov_id or case_notes") and the assistant fetches full records. In permission mode, the credential itself is scoped: restricted fields are stripped at the data layer, so nothing model-shaped ever holds them. The same five-attack suite runs against both:
prompt mode: LEAKED 4 of 5 (content injection, roleplay, "anything
sensitive", format pivot)
permission mode: 0 of 5 (the credential cannot fetch what the
prompt cannot protect)
The deceptive result is the one that passes: the plain direct ask is refused in both modes — which is exactly why prompt-layer security demos look safe and fail in production.
The "assistant" here is not an LLM — it is a deliberately naive instruction-follower built to fail the way injected models fail (it obeys directives embedded in retrieved content, and its guardrail is wording it can be argued out of). That naivety is the experimental control. The claim under test is architectural, not behavioral: prompt-layer security depends on how the reader behaves; permission-layer security does not. A real model is less naive on any given day — and the architecture still shouldn't bet on it.
CI enforces both directions: permission mode must show zero leaks, and prompt mode must still demonstrate the failure — if the naive agent ever stops leaking, the gate reports the demo as vacuous and fails the build.
Part of the -gate family: kappa-gate · roi-gate · phi-gate · trade-gate.
flowchart TB
ATK["attack suite: direct ask, content injection, roleplay, exfil-summary, format pivot"] --> AG
subgraph AG["naive assistant (worst-case instruction follower)"]
GP["guard prompt: never reveal restricted fields"]
end
subgraph ACC["access path"]
TR["trusting fetch: full record, security delegated to the prompt"]
SCP["scoped fetch: restricted fields stripped at the data layer"]
end
TR --> AG
SCP --> AG
AG --> OUT["reply text"]
OUT --> LS["leak scan vs ground-truth restricted values"]
LS --> PG{"permission mode: zero leaks?"}
PG -- "0 of 5" --> HOLD["the credential cannot fetch what the prompt cannot protect"]
LS --> PP{"prompt mode: still demonstrably fails?"}
PP -- "4 of 5 leak" --> DEMO["instructions are requests, not guarantees"]
PP -- "0 leak" --> VAC["vacuous demo: CI fails"]
subgraph EVAL["Braintrust-shaped eval: data, task, scorers"]
D["data: attacks x both modes"] --> T["task: run the assistant"] --> SC["scorers: permission_no_leak, prompt_leak_demonstrated"]
end
SC -- "regression" --> CIF["CI fails"]
SC -.-> BT["Braintrust hosted tracking (obs extra)"]
python -m permgate suite scores the thesis as numbers: permission_no_leak
must be 1.0 and prompt_leak_demonstrated must be 1.0 — the scoped credential
never leaks AND the naive agent still visibly fails, so the demo can never go
vacuous silently. The obs extra pushes the identical suite to hosted
Braintrust.
python -m venv .venv
.venv/bin/pip install -U pip
.venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest -q
.venv/bin/python -m permgate gate permission
.venv/bin/python -m permgate gate prompt
All records are fictional; the "identifiers" are labeled fakes. MIT license.