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

  1. 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).
  2. The untrusted-author profile: no_extern on 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).
  3. 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 granted decorative) 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).
  4. 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).
  5. 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

Claimpath:line or testStatusNote
Argument from confinement: declared access, emission bound, enumerable crossings; install is admissiondocs/prompt-injection-resistance.md:44-109shipped (argument)Doc says "Everything above is an argument from the shape of the language"
Emission fixed point closes first-class-value dodgesdocs/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 swapdocs/prompt-injection-resistance.md:160-186 (_enforce_sandbox in src/revl/mcp/session.py)shippedRead via the doc; the doc names the function
Untrusted-author profile: fields and factorysrc/revl/admit_profile.py:101-245shippedIncludes no_declassify, taint_strict, no_realm_placement, realm_bindings
Extern smuggling refused; admits without profiletests/test_admit_profile.py:78,88shipped
Transitive host reach refused through nonroot wrappers and stdlibtests/test_admit_profile.py:263,286,304,324,388; CHANGELOG.md:353-355shipped
Allowlist: ungranted reach refused; empty set refuses any reachtests/test_admit_profile.py:117,126shipped
Provision-key exemption fails closed; decoy refused, cannot reach admit decidertests/test_r2_allowlist_key_binding.py:152,164,174,190,241,284; src/revl/admit_profile.py:763-830shippedDocstring states the name-keyed version "was a full bypass"
Admit crossing: refusal is a verdict, commit persists, abort reverts residue-freetests/test_admit_crossing.py:128,143,165,179,191,210,237,258shipped
MCP: transport-borne source compiles under the profile; inline @py refused; default closedtests/test_mcp_authoring_trust.py:77,90,97,113,125,140,173,491,508; CHANGELOG.md:356-362shipped--author-trust trusted restores the old behaviour and stamps tickets
Library doors confine use paths under an untrusted profiledocs/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 auditdocs/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 escapedbench/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 codedocs/design/329-untrusted-author-profile.md:119-160designed (deferred)The doc lists three TODOs

Numbers that exist

Paper outline

  1. Introduction: injection as ambient authority; admission as a different place to stand.
  2. Model and threat: the attacker controls source and data, not the checker, runtime or policy.
  3. Confinement: declared access, emission bound with its fixed point, enumerable crossings; install is admission.
  4. The escape hatch: why host code defeats it; the untrusted-author profile and its fail-closed binding rule; the name-keyed bypass.
  5. Doors: every path that admits model-authored source (transport, library, in-language crossing); the "approve one, run another" exploit.
  6. Per-turn admission loop and refusal as repair signal.
  7. Measurement: compliance versus containment; error feedback as carrier; results and their limits.
  8. Residual risks, related work, conclusion.

Threats to validity and limits

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.

← All research drafts