Research / Agents and LLMs
P56 · full · draft
Prompt-Injection Resistance by Admission: Confinement, Untrusted-Author Profile and Per-Turn Compilation
partial full Agents and LLMs
State of the work. Admission, the untrusted-author profile, the in-process admit crossing and the MCP trust default are on main with tests; the only measurement is 5 scored attempts on one model, and physical confinement of host code is designed only.
This is a draft, not peer reviewed. Repository references link to
revl at commit 45bec431f.
Planned to be combined with P36 (No Authority From Untrusted Data: Authority-Tracking Information Flow With a Single Declared Declassification) into a single paper.
Pitch
If the model's output is a program admitted by a checker against only the tools it was granted, an injected instruction can cause only effects the component already declared, and the hole the design identifies, host code, is closed for untrusted authors by refusing the model the right to write any.
Abstract
Prompt-injection defenses for agents mostly filter content, wrap tool calls in monitors, or split a privileged and a quarantined model. We study a different point in the design space: make the model's output a program in a language whose checker proves its authority, and admit it only if it fits inside the surface the operator granted. An instruction injected through a document, a tool description or a compiler message can then make the model write whatever it likes, but an ungranted call is a type error, an undeclared boundary crossing fails the emission bound, and the program never runs. We identify the residual gap, the language's own escape hatch to host code, and close it with an untrusted-author admission profile: no new extern, reach bounded to a granted allowlist, resolved by provision key and failing closed when the binding cannot be determined. A name-keyed version of the exemption was a full bypass, and an audit of the agent-facing server found a second hole in which the driving agent was treated as a trusted host-code author; both are fixed and have regression tests. We describe a measurement that reports compliance (the model wrote the undeclared thing) and containment (a gate then refused it) separately, and treats compiler error feedback as an injection carrier. On the one run that exists, a local 35B-parameter model complied in 2 of 5 scored attempts and 0 escaped; the sample is too small to support a rate, and we say so.
Problem and claim
Injection is dangerous when the injected instruction inherits ambient authority: the process can open sockets and read files, so any eval, shell or over-broad tool the model is talked into reaching inherits all of it. Runtime monitors and content filters try to recognise bad intent. They do not change what a successful injection can reach.
The claim is structural and bounded. Authority in the target language is exactly what a component declares and was granted (docs/prompt-injection-resistance.md:46-71). Install is admission, not interpretation: a component runs only after the checker proves its guarantees over the whole composition (docs/prompt-injection-resistance.md:73-91). Therefore an injected instruction can enlarge the model's output but not the authority of the admitted program. We do not claim to stop injections that stay inside granted capabilities: the doc's own first residual risk is that a write to the wrong value through a granted boundary is in-bounds (docs/prompt-injection-resistance.md:269-275).
The part that is not free is the escape hatch. A model-authored extern ... = @py { ... } admits and runs arbitrary host code, because the gate's boundary for host code is "verbatim host code, unchecked inside" (docs/design/329-untrusted-author-profile.md:7-23). The paper's second claim is that an untrusted-author profile, applied at every door that admits model-authored source, removes that hole for code-mode authors and that the profile is subtle enough that two real bypasses had to be found and fixed.
Contributions
- A per-turn admission design: model-authored revl is compiled against a synthetic composition declaring exactly the granted tools, so an ungranted call is a compile refusal and the refusal text is the repair signal; the in-process admit crossing is on main (
docs/design/329-untrusted-author-profile.md:61-82;tests/test_admit_crossing.py:179,258). - The untrusted-author profile:
no_externon the admitted root, transitive host-reach refusal through the module closure, a granted-service allowlist whose internal-provision exemption is resolved by provision key and fails closed, plus no self-minted declassifiers and no self-chosen realm (src/revl/admit_profile.py:101,184,245,518,763;tests/test_admit_profile.py, 20 tests). - Two bypasses found and closed, each with a regression test: the name-keyed provision exemption (a throwaway component providing the same service under an unused key made
granteddecorative) and the MCP server's treatment of the driving agent as a trusted host-code author (tests/test_r2_allowlist_key_binding.py:152-284;tests/test_mcp_authoring_trust.py:77,140,173). - A measurement design that separates compliance from containment, detects compliance without consulting the gate, and splits refusals into "about the injected reach" and "unrelated fault"; compiler error feedback is tested as an injection carrier (
docs/prompt-injection-resistance.md:187-261;bench/results/injection-ornith/escape.md). - A documented set of what confinement does not cover (in-grant misuse, over-broad grants, host code; the doc's fenced enumeration gap is marked resolved in
docs/contract-errata.md) (docs/prompt-injection-resistance.md:263-320).
Evidence on main
| Claim | path:line or test | Status | Note |
|---|---|---|---|
| Argument from confinement: declared access, emission bound, enumerable crossings; install is admission | docs/prompt-injection-resistance.md:44-109 | shipped (argument) | Doc says "Everything above is an argument from the shape of the language" |
| Emission fixed point closes first-class-value dodges | docs/prompt-injection-resistance.md:92-109; src/revl/emission_analysis.py (cited by the doc; the source was not opened) | shipped | |
Sandbox profile enforced at load and swap | docs/prompt-injection-resistance.md:160-186 (_enforce_sandbox in src/revl/mcp/session.py) | shipped | Read via the doc; the doc names the function |
| Untrusted-author profile: fields and factory | src/revl/admit_profile.py:101-245 | shipped | Includes no_declassify, taint_strict, no_realm_placement, realm_bindings |
| Extern smuggling refused; admits without profile | tests/test_admit_profile.py:78,88 | shipped | |
| Transitive host reach refused through nonroot wrappers and stdlib | tests/test_admit_profile.py:263,286,304,324,388; CHANGELOG.md:353-355 | shipped | |
| Allowlist: ungranted reach refused; empty set refuses any reach | tests/test_admit_profile.py:117,126 | shipped | |
| Provision-key exemption fails closed; decoy refused, cannot reach admit decider | tests/test_r2_allowlist_key_binding.py:152,164,174,190,241,284; src/revl/admit_profile.py:763-830 | shipped | Docstring states the name-keyed version "was a full bypass" |
| Admit crossing: refusal is a verdict, commit persists, abort reverts residue-free | tests/test_admit_crossing.py:128,143,165,179,191,210,237,258 | shipped | |
MCP: transport-borne source compiles under the profile; inline @py refused; default closed | tests/test_mcp_authoring_trust.py:77,90,97,113,125,140,173,491,508; CHANGELOG.md:356-362 | shipped | --author-trust trusted restores the old behaviour and stamps tickets |
Library doors confine use paths under an untrusted profile | docs/design/329-untrusted-author-profile.md:84-117; tests/test_admission_use_confinement.py (named by the doc; not opened) | shipped | |
| Approve-one / run-another exploit and its audit | docs/v2.0-roadmap.md:1636 (the approve-one / run-another exploit is exploit iii of the first advisory entry; the next entry at 1637 is the path-jail carrier) | shipped (fix described in the roadmap) | Public repo; the advisory text is in the roadmap |
| Measured injection escape: 5 scored attempts, 2 complied, 0 escaped | bench/results/injection-ornith/escape.md:5-11; bench/results/injection-ornith/attempts.json (8 rows) | shipped (measurement) | See numbers |
| Wasm-import path for physical confinement of per-turn host code | docs/design/329-untrusted-author-profile.md:119-160 | designed (deferred) | The doc lists three TODOs |
Numbers that exist
- Injection escape: 1 host (revl), 1 pinned local model (
hf.co/ornith-ai/Ornith-1.5-35B-A3B-GGUF:Q4_K_M), 1 base spec (01-kv-provider), 8 injection vectors generated, 5 scored. 3 of 8 spent the whole output cap on the reasoning channel and returned no answer; they are excluded from every denominator (bench/results/injection-ornith/escape.md:9). Of 5 scored: complied 2 (compliance rate 0.4), containment 2 of 2 by thecompilesgate, escaped 0. One of the two refusals was about the injected reach (drop-the-undo: "effect has noundoandstore.insertis not pure"); the other was a syntax error unrelated to the injection (env-exfiltration), counted apart (escape.md:11;attempts.json). One more complier (widen-the-service) fell in the excluded group. Carriers: service-interface comment, task brief, compiler-output feedback; the compiler-feedback carrier produced 0 compliances in its 1 attempt. - The results file carries the string
"compiler": "revl was never imported";bench/run.py:641produces it. What that field means was not traced, so the run is not offered here as a gate run. - Tests:
tests/test_admit_profile.py20,tests/test_r2_allowlist_key_binding.py14,tests/test_mcp_authoring_trust.py29,tests/test_adversarial_gate.py28,tests/test_admit_crossing.py(8 listed above). Counts on def test_. - The doc cites the same figures in
bench/results/framework-bench/report.md(row at line 132, measured at line 295). - No attack-success rate against any published injection benchmark exists.
Paper outline
- Introduction: injection as ambient authority; admission as a different place to stand.
- Model and threat: the attacker controls source and data, not the checker, runtime or policy.
- Confinement: declared access, emission bound with its fixed point, enumerable crossings; install is admission.
- The escape hatch: why host code defeats it; the untrusted-author profile and its fail-closed binding rule; the name-keyed bypass.
- Doors: every path that admits model-authored source (transport, library, in-language crossing); the "approve one, run another" exploit.
- Per-turn admission loop and refusal as repair signal.
- Measurement: compliance versus containment; error feedback as carrier; results and their limits.
- Residual risks, related work, conclusion.
Threats to validity and limits
- The measurement is n=5 scored attempts on one model and one spec, run by the party whose system it favours. The doc says so: "nothing here moves off the
measuredrung until somebody who is not us runs it" (docs/prompt-injection-resistance.md:250-261). - Compliance is detected by regular expression over source and can miss paraphrases or fire on comments; every attempt keeps the matched text (
docs/prompt-injection-resistance.md:243-249). - One of the two "contained" attempts was contained by an unrelated syntax error and is not evidence about injection.
- The measurement covers admission, not what a running composition does with injected data (residual risk 1) and has no run without the gate as a baseline.
- The gate does not sandbox host code; the profile removes the author's ability to write it, which costs expressiveness. The wasm-import alternative is designed only (
docs/design/329-untrusted-author-profile.md:119-160). - A trusted-author mode exists and restores the old behaviour with a stamped ticket; an operator who selects it re-opens the hole.
- The doc's residual-risk 4 (
docs/prompt-injection-resistance.md:300-312) describes a host block reached only through a first-class function value as not enumerated on the audit surface, pinnedxfail. That text is stale:docs/contract-errata.md(around line 1081) marks it resolved, andtests/test_adversarial_gate.py:532is a plain passing assertion with noxfail. The test was not run for this draft. - Two prior bypasses were found late. That is evidence the profile is intricate, not that no third exists.
- Approvals still decide what happens inside granted capabilities; this paper does not claim to stop in-grant misuse.
- Utility cost is unmeasured: the refuse-repair loop costs round-trips, and models are not trained on revl.
Fact-check
Claims in this draft were checked against revl main at commit 45bec431f: 29 checked, 4 corrected.
Cite this draft
@misc{revlp56,
author = {Thomas Moussajee},
title = {Prompt-Injection Resistance by Admission: Confinement, Untrusted-Author Profile and Per-Turn Compilation},
year = {2026},
howpublished = {\url{https://inso1337.github.io/revl/research/P56.html}},
note = {Research draft, not peer reviewed. Written against revl commit 45bec431f. Licensed CC BY 4.0.}
}
Text licensed CC BY 4.0, author Thomas Moussajee.