VAULT
Encrypt and decrypt files with a password, entirely in your browser. Nothing is uploaded.
CaMeL-style control/data-flow separation that reduces injection-to-effect risk under stated assumptions. Topology, not a safety certificate.
Translation pending
SecurityPython · JustGitHub @gianlucamazzaOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Python reference for a CaMeL-like Reasoning Kernel. Control/data-flow separation reduces prompt-injection-to-effect risk when planner input is trusted, the capability policy is correct, and tools enforce it. Topology, not a proof that injection cannot cause an unauthorized effect.
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
The problem. An LLM agent that reads untrusted data — an email, a web page, a tool result — can
be hijacked by instructions hidden in that data and then act on them: leak your contacts, send mail,
call tools on your behalf.Read the full README ↗Live site · Quick start · Embedding · Conformance · Operations · Working paper
The problem. An LLM agent that reads untrusted data — an email, a web page, a tool result — can be hijacked by instructions hidden in that data and then act on them: leak your contacts, send mail, call tools on your behalf.
The approach. This repository is a small, framework-agnostic Python reference implementation of the Reasoning Kernel pattern in its strong, CaMeL-like form (Debenedetti et al., 2025). Every LLM is treated as untrusted compute. CaMeL-style control/data-flow separation reduces the injection-to-effect risk when the assumptions below hold (trusted planner input, a correct capability policy, tools that enforce it, no side channels). It is not a proof that prompt injection is unable to cause an unauthorized effect, and it is not a substitute for a correct host policy.
A Reasoning Kernel is an architecture in which probabilistic reasoning is treated as an untrusted computational resource, mediated by context on input and verification on output.
Who this is for. If you're building an LLM agent that takes actions on untrusted input, this is a tested reference implementation and spec: read it to understand the pattern, fork it, or conform your own system to it. It is not a turn-key security product, a formal non-interference proof, or an independent security audit.
The topology, threat model, and limits are written up as a working paper (technical note): Reasoning Kernel: A Capability-Mediated Reference Architecture for Untrusted Tool Data in LLM Agents (PDF). MIT; no peer-review or safety-certificate claim. No Zenodo DOI is assigned — deposit is on hold. Do not conflate this note with the separate emotional-memory Zenodo records. Cite CaMeL as arXiv:2503.18813.
context/).kernel/gate.py).The pattern fixes a topology, not a safety property: it places mediation and verification at those two boundaries. Conformance is a necessary, not a sufficient, condition. Concretely: an injected message cannot fire a registered tool without passing the Gate you configured. The root planner is isolated from tool results; delegated sub-planners deliberately see untrusted data under reduced grants. Whether that Gate's policy is correct, and whether an allowed tool can still exfiltrate data, is on you. See Threat model & limits.
Following CaMeL (Debenedetti et al., 2025), the kernel contains no trusted reasoner. It has two reasoners at differentiated privilege, both untrusted (section references like §5.4 below point to that paper):
reasoner/roles.py:PLLM) — privileged planner; sees only the controlled query + tool
catalog; emits a typed Plan, never prose or code.reasoner/roles.py:QLLM) — quarantined parser; turns untrusted content into typed
values; has no tool capability.The trusted, deterministic kernel is the interpreter + capability/provenance gate, never a model.
| Role (paper) | Module | Reason to change |
|---|---|---|
| Context | context/assembler.py | input assembly / Invariant A |
| Reasoner(s) | reasoner/ (multi-provider) | provider or interface |
| Conductor | kernel/interpreter.py | execution loop |
| Verifier | kernel/gate.py, effects.py | verification policy |
| Tool catalog | tools/registry.py | sole holder of tool callables |
| Memory / Trace | memory/ | durability / audit format |
Reasoner providers: Anthropic, OpenAI, DeepSeek (OpenAI-compatible, reusing the openai SDK via a
base_url — no separate dependency), plus a deterministic FakeProvider for key-free tests — all
behind one interface (reasoner/base.py). Configured providers can be exercised through the same
live contract (just test-live). Releases require both DeepSeek and OpenAI qualification.
Version 0.6.4 (same defaults as 0.6.3) defaults to OpenAI gpt-6.1-sol, Anthropic claude-sonnet-5-5,
and DeepSeek deepseek-flash; see configuration and qualification.
The published 0.6.2 wheel additionally passed OpenAI gpt-5.5 qualification after release, using
SDK 3.19.2; evidence is attached to the 0.6.2 release.
Anthropic remains contract-tested without live qualification. These checks do not qualify every
model variant or replace host-adapter acceptance.
These are constructor and dispatch facts for this implementation's registered tools — the no-bypass wiring, not a proof that no unauthorized effect can occur:
ToolRegistry, handed only to EffectDispatcher; the interpreter
does not hold one.EffectDispatcher cannot be constructed without a Gate, and dispatch authorizes the call
before the callable runs.ToolCallStep is the only step kind that invokes a tool callable, and its only handler routes
through the dispatcher. The other step kinds (const, q_parse, subkernel, merge) produce
values; a sub-kernel may invoke tools through its reduced Gate and the same dispatcher path.This wiring does not cover side channels, a host-owned callable kept outside the registry, or a
tool that exceeds its declared ToolSpec. Tests for the wiring are cited under
Threat model & limits.
"Summarize my latest email and send it to me" becomes a typed, four-step plan: read_inbox →
q_parse (summarize the body) → const (my own address) → send_email. Under the demo
policy (RecipientIsUserPolicy) and a scripted FakeProvider, two fixture attacks stay inert:
send. That is not a claim that an arbitrary compromised planner cannot cause
harm — any effect the policy already allows can still fire, including exfiltration via an
allowed tool.Run it with just demo (the trace shows each gate decision and why).
uv sync --extra dev # key-free: demo + the full default test suite
just demo # demo fixture: legit send commits; injection inert under that policy
just test # coverage, conformance, and the policy-fixture suite (see Threat model)
just docs-check # links, anchors, release metadata and claim-drift guard
just lint && just typecheck
# Focused deterministic demos
just demo-subkernel # delegate untrusted content under a reduced grant (§5.4)
just demo-limits # abort before exceeding RunLimits
just demo-reasoner-error # reject a failing reasoner without a later effect
just demo-merge # combine values; provenance flows through the join
# Real providers (requires keys in .env)
uv sync --all-extras
just demo-live
just test-live # RK_LIVE_PROVIDERS makes a configured provider set mandatory
See docs/DEVELOPMENT.md for the quality bar (coverage gate, strict typing,
pre-commit) and how to configure provider keys. Release notes are in
CHANGELOG.md; vulnerability reporting and scope in SECURITY.md.
Install the package from PyPI; it imports as reasoning_kernel because reasoning-kernel was
already taken by an unrelated project:
pip install capability-reasoning-kernel
Pin the exact release when producing conformance evidence:
pip install capability-reasoning-kernel==0.6.4
For operational embedding, use RunSession with a persistent sink and bounded defaults; see
operations and migration and the conformance checklist.
It isolates each run, records partial effects and refuses automatic replay. Package publication and
consumer compatibility tests are not evidence that a host's live adapters are correctly configured.
Version 0.6 adds fixed gate-v1 and operational-v1 profiles for turning host tests into
sanitized, repeatable evidence. gate-v1 covers hosts that use the verifier as a pre-pipeline
checkpoint; operational-v1 covers complete RunSession integrations. Run the key-free
operational reference:
reasoning-kernel-conformance \
reasoning_kernel.conformance.reference:reference_suite \
--output conformance.json
An application supplies a trusted zero-argument factory returning ConformanceSuite for one
profile. Each required ScenarioKind runs in isolation and returns a ConformanceObservation
containing the decision or kernel result and counts observed in the external test world.
Expectations are fixed by the profile: applications cannot redefine a denial as success.
CLI exit codes:
0 — every case passed.1 — at least one case failed or was inconclusive.2 — invalid suite/runner, factory-load failure, execution error, serialization error or output
write failure.Reports contain only version metadata, scenario/check identifiers and outcomes—never prompts, payloads, paths or raw provider errors.
See the conformance guide for the required cases and host factory contract.
Explicit low-level wiring remains available. The package root re-exports the building blocks. This
is a sketch; see demo/email_exfil.py for a complete,
runnable version:
from pydantic import BaseModel
from reasoning_kernel import (
Capability,
CapabilitySet,
EffectDispatcher,
EffectLevel,
FakeProvider,
Gate,
Interpreter,
PLLM,
QLLM,
RunContext,
RunId,
ToolRegistry,
ToolSpec,
TraceWriter,
TrustedQuery,
VerifierVerdict,
)
# 1. Tools: the callable lives ONLY in the registry, never reachable by the interpreter.
class SendIn(BaseModel):
to: str
body: str
class SendOut(BaseModel):
ok: bool
def send(inp: BaseModel) -> BaseModel: ... # your real side effect
registry = ToolRegistry()
registry.register(
ToolSpec(
name="send",
input_schema=SendIn,
output_schema=SendOut,
required_caps=frozenset({Capability(name="mail.send")}),
effect_level=EffectLevel.WRITE,
),
send,
)
# 2. Your deterministic declassification policy — the one place trust is relaxed.
class Policy:
def may_declassify(self, tool, named_args, ctx) -> VerifierVerdict:
return VerifierVerdict(allowed=False, reason="deny tainted writes by default")
grant = CapabilitySet(granted=frozenset({Capability(name="mail.send")}))
ctx = RunContext(
run_id=RunId("run-1"),
user="[email protected]",
query=TrustedQuery(text="…your task…"),
)
trace = TraceWriter(ctx.run_id)
dispatcher = EffectDispatcher(registry, Gate(grant, Policy()), trace, ctx)
provider = FakeProvider({}) # swap for get_llm_provider() with a key in .env
kernel = Interpreter(
planner=PLLM(provider, grant=grant),
quarantine=QLLM(provider),
dispatcher=dispatcher,
trace=trace,
q_schemas={},
)
result = kernel.run(ctx) # committed is None if the run failed closed
Project status: pre-1.0 — the public API may change between minor versions until 1.0. Released on PyPI as
capability-reasoning-kernel(imports asreasoning_kernel), and on TestPyPI.
These are mechanism properties of the reference implementation. They are not a claim that prompt injection is unable to cause an unauthorized effect.
ProvenanceLabel carries origin (sources), where it
may flow (readers), and whose data it is (subjects). Third-party data is not
auto-released into a WRITE — even to the requesting user — and a Q-LLM parse does not strip any
of these dimensions. A tainted value whose flow was never scoped (readers=None is reserved for
purely trusted data) is likewise not auto-permitted into a WRITE: it is routed to the
declassifier like any other tainted flow.TrustedQuery (text + label);
const/inline literals DERIVE their label from it, so the trust assumption is explicit rather
than by convention.RunLimits bounds steps / effects / q-parses (and an optional per-call timeout);
a run exceeding a bound aborts closed (RunAborted), committing nothing further. The timeout
abort is prompt — it does not block waiting on the hung call
(kernel/interpreter.py:_call_reasoner).ReasonerError (reasoner/base.py), and a provider call that fails in
transport (rate limit exhausted, 5xx, network fault) raises its TransportError subclass;
either way the Conductor records a terminal trace event and stops subsequent work. Earlier
effects remain real
and are reported in RunResult.effects; committed=None means no final value, not rollback.CapabilitySet; the kernel
rejects a reasoner whose grant exceeds the dispatcher's — a child cannot widen authority. A
SubKernelStep delegates untrusted content to an inner kernel at a clamped, reduced grant: an
injection in that content is confined to what the delegated grant permits, even capabilities the
outer kernel holds but did not delegate (see just demo-subkernel). RunLimits.max_depth bounds
nesting.Plan is a forward-only DAG of five step kinds
(const, tool, q_parse, subkernel, merge), executed linearly by
kernel/interpreter.py; a
QuarantineParseStep's target schema is fixed at plan time
(schema_ref), never chosen on the quarantined value. There are no runtime branches or loops.
Delegated sub-planners can choose a child plan based on untrusted input; its authority and literal
provenance are reduced accordingly. This is not a claim of data-independent planning across
delegation.The public claim is scoped: CaMeL-style control/data-flow separation reduces the injection-to-effect risk under the assumptions below. The kernel fixes where mediation and verification live. It does not prove an impossibility, and it does not make an incorrect policy safe.
TrustedQuery and its label are host-supplied and not
attacker-controlled. If the attacker writes the query, or the host marks attacker text as
trusted, Invariant A does not apply.DeclassPolicy, tool catalog, and Q-LLM schemas
match the host's intended authority. A pass-through declassifier conforms and protects nothing.ToolSpec declares: no
extra I/O, no undeclared writes. The kernel authorizes the declared call; it does not sandbox
the Python callable.DeclassPolicy is a Protocol the Gate calls
blindly; nothing in the types forbids consulting a model. Determinism is required of the
declassifier, not enforced on it.The default suite (just test) uses FakeProvider. It does not run live-model injection
unless you opt into just test-live, and even then those checks are provider-contract tests, not
an AgentDojo-style attack evaluation.
Mechanism / wiring (policy-independent facts about this tree):
| Claim | Tests |
|---|---|
Denied capability or provenance does not run the callable; a committed effect has a preceding allowed GateDecision for the same invocation | tests/test_no_bypass_conformance.py |
| Assembled planner context, and an end-to-end spy provider, exclude the injected email body from the P-LLM; the Q-LLM sees it | tests/test_invariant_a.py |
| Missing capability denies regardless of provenance; bad args fail the schema stage | tests/test_gate_capability.py |
Tainted WRITE is denied unless declassified or readers explicitly cover the tool; tainted readers=None is not an auto-pass | tests/test_gate_provenance_declassify.py, tests/test_gate_unrestricted_readers.py |
Q-LLM parse and merge join labels conservatively (no laundering in these fixtures) | tests/test_provenance_propagation.py, tests/test_subject_provenance.py, tests/test_merge.py |
| A planner grant cannot exceed the dispatcher; a sub-kernel grant is clamped; a scripted injection is confined to the delegated grant | tests/test_composition.py, tests/test_subkernel.py (test_injection_confined_by_reduced_grant) |
One demo policy (RecipientIsUserPolicy in tools/demo_mail.py) — an illustrative fixture,
not a general safety theorem:
| Claim | Tests |
|---|---|
| Each documented rejection branch of that policy | tests/test_declass_policy.py |
| Legit send to the user; injection inert under a scripted honest plan; a scripted malicious plan is denied; third-party contacts are not mailed even to the user | tests/test_demo_email_exfil.py |
Packaged operational-v1 injected_control / injected_egress fixtures with a trusted observer | tests/test_conformance.py, reasoning_kernel.conformance.reference |
Those conformance cases do not independently discover unauthorized effects on the network; they
trust the host observer. See docs/CONFORMANCE.md.
TrustedQuery,
schemas that drop fields the Gate should see.RunLimits, and
tamper-proof audit storage.MergeStep labels its result with the
join of its inputs (over-approximation). Field-level labels stay deferred.The declassifier remains the residual risk surface: every may_declassify=True is a deliberate,
traced trust decision. Conformance is not safety. An "if the email says X, do Y" must be lifted
into a typed value the Gate can inspect, not a runtime branch on quarantined text.
Plan)
and the quarantined parser (turns untrusted content into typed data, with no tool access).ProvenanceLabel recording where it came from
(sources), where it may flow (readers), and whose data it is (subjects).CapabilitySet (its grant), and a sub-kernel's grant can only ever shrink.DeclassPolicy) — the single deterministic seam that may let tainted data into
a WRITE; the one place trust is deliberately relaxed.CaMeL — Debenedetti et al., Defeating Prompt Injections by Design, 2025 (arXiv:2503.18813). Section references (e.g. §5.4, §6.2) point to it.
Until a Zenodo DOI is assigned for the working paper (deposit is on hold pending Owner instructions), cite the software by URL and version:
Mazza, G. (2026). reasoning-kernel (Version 0.6.4) [Computer software]. https://github.com/gianlucamazza/reasoning-kernel
See the working paper for the technical note and related-work citations, including CaMeL (arXiv:2503.18813). Do not cite emotional-memory Zenodo records as Reasoning Kernel identifiers.
MIT — see LICENSE.