Untrusted content reaches the model
Documents, retrieved passages, tool output and user input share one context. Instruction hierarchy can reduce bad behaviour; it cannot authorize an action.

ἱστός - the mast
Deterministic policy enforcement for Python agent tool calls.
Bound the tool, arguments, resource and returned data before model output can touch the real world — or flow back into context.
Odysseus did not silence the Sirens. He bound himself before they started singing. Histos makes the same move: decide the agent's capabilities before it reads untrusted content.
pip install histos Installs histos 0.1.1 from PyPI and requires Python 3.12+. The core has zero runtime dependencies. For YAML policies, install the optional parser: pip install "histos[yaml]".
The problem
A chatbot can say something wrong. An agent can do something wrong.
Once a model can modify records or trigger external systems, prompt injection becomes an authorization problem at the tool boundary.
Documents, retrieved passages, tool output and user input share one context. Instruction hierarchy can reduce bad behaviour; it cannot authorize an action.
A manipulated model can still emit a perfectly valid-looking call: the right tool name, well-formed arguments and plausible intent.
Detection can lower risk. It cannot make a deterministic decision about which capability this principal may exercise on this resource.
Assume the model can be manipulated. Put the boundary somewhere it cannot negotiate with.
The answer
Policy enforcement is a commitment made before the model encounters the world.
Histos does not interpret intent. It evaluates each proposed call against static policy and trusted runtime context, then allows or denies it before the tool executes.
Histos is not an agent platform, proxy, identity provider or sandbox. It is the narrow enforcement layer inside your Python process: authorize the action, run only what policy permits, constrain what returns.

Detection changes probabilities.
Enforcement changes possibilities.Both have value. They are not the same layer.
Runtime enforcement
Two deterministic checks at the tool boundary.
Histos mediates both directions of the tool boundary.
Before execution, it decides whether the action may happen. After execution, it decides what may return to the model.
Use the framework-free core with ordinary Python callables, or protect the tool objects handed to LangChain and LangGraph.
Before the tool runs
The proposed call is evaluated against policy and trusted runtime context before the underlying function executes.
Any failed check means the tool does not run.
Before the result gets back
The return contract is enforced before tool output — or an exception — re-enters model context.
The boundary protects both what the agent can do and what the model gets to see next.
Two different strengths, and the difference matters more than the count. Most of these decide from a declared fact — a role holds a grant or it does not, an argument matches a schema or it does not, a caller owns the resource or does not. There is no recognition step, so no class of input gets through by looking unfamiliar. The three marked recognises have to identify something inside a value: a secret, a planted token. They are worth having and they are not guarantees — what they have not seen, they do not catch. Read those three as defence in depth, and the rest as the boundary.
Why this matters
Untrusted content reaches the model; the model proposes a call. Everything the gate uses to decide - a trusted identity, your static policy, the request itself - comes from outside that channel.
In enforce mode every step is fail-closed: an error inside a check is a denial. observe is the explicit calibration mode - it records the decision, then runs the original call unchanged and protects nothing. A denial answers two audiences at once: the developer gets the field, the bound and the fix; the agent gets a code that teaches it nothing about how to succeed on the next attempt.
Hijacked.
Still bounded.
Not harmless, and the difference is the point. A €400 refund to the right tenant, on an allowed order, with valid arguments goes through - because that is what your policy says. What a manipulated model cannot do is cross the bounds: wrong tenant, wrong tool, wrong resource, over budget, over limit, without the approval you required, carrying a value you marked.
The point is not to make the model perfect. The point is to keep a manipulated model bounded - whether or not anyone noticed the attack.
Measured
Seven models. Six scenarios. Four temperatures. 1,498 agent executions.
Runs in which gpt-4.1, gpt-4o-mini, o4-mini, gemma4:26b and gemma4:e4b fell for an invoice fraud or an injected ops runbook. No policy involved — they refused unaided.
Runs in which those same five models sent a patient's appointment details to a stranger's phone, because a sentence in the patient's own intake note asked them to.
The difference is not capability and not vendor — a 26B open-weight model reproduces gpt-4.1 run for run. It is whether the attack looks like one. A bank account that disagrees with the supplier master does. A runbook ordering production to zero does. “I do not use my own phone for messages” does not, because it is an ordinary service request. There is nothing there to detect, and a reasoning model has nothing to reason about.
Behind a policy: 0 harmful outcomes across the 375 gated attack cases — the 374 controls are counted separately, because pooling them would report benign runs as prevented attacks. That column is not a discovery either: a deterministic gate refuses the call it was written to refuse, and on two of the three scenarios it did nothing at all because the model had already declined. What the runs are for is the other question: where a boundary is worth its cost, and what that cost is. On accounts payable it is zero. On the clinic it is an entire legitimate feature.
Why a policy
For one agent with three tools, write the checks in code.
Histos starts paying off when the same boundary must stay consistent across more tools, agents and teams.
Security rules live in one policy instead of being scattered across tools, handlers and agent code.
A new or forgotten tool stays blocked until the policy explicitly grants it.
Changing a refund limit from 500 to 5000 becomes an obvious policy diff, not a hidden application change.
Ownership, trusted binding, confirmation, limits and output controls keep the same declared meaning.
Validate policy, find uncovered tool surfaces, detect drift and fail CI before deployment.
Tie each decision to the tool, principal, policy and reason without inventing another logging convention.
What “an obvious policy diff” means, since the whole claim is that you can see it:
tools:
make_refund:
args:
- amount: { type: integer, minimum: 1, maximum: 500 }
+ amount: { type: integer, minimum: 1, maximum: 5000 } The value isn't avoiding the if.
It's making the boundary explicit, portable and verifiable.
Three tools? Write the ifs.
Thirty? Write the policy.
Identity
Authentication belongs to your existing system, never to the model.
Your host establishes who is calling. Histos receives that trusted principal and enforces what the agent may do on its behalf.
A directory GUID in a roles block ties the policy to one tenant of one provider, and makes the file unreviewable - a security lead can tell you whether refund_officer should hold make_refund; nobody can tell you that about a9481de2.
So the mapping lives in your host, and the same policy survives a change of identity provider, of runtime, and of customer.
The gate is exactly as strong as the principal you bind to it - and the library cannot check that binding. It says so, rather than implying otherwise with an API that looks safe.
Entra app role Histos role
finance-refund-operator → refund_officer
support-tier2 → support_agent
Okta group Histos role
eng-oncall → incident_responder
── and never the other way round ──────────────────
roles:
"a9481de2-f123-4c77-9e21-…": ✕ one directory, one tenant
refund_officer: ✓ a portable artifactThe gate is only as strong as the identity bound to it.
The format
The policy is the reviewable security artifact; Python is its first runtime.
Review it, diff it, validate it and version it like code.
schema_version: histos.policy/0.1
policy_id: refund-approval
version: "1"
roles:
refund_officer:
allow: [make_refund]
tools:
make_refund:
args:
amount: { type: integer, minimum: 1, maximum: 50000 }
confirmation:
required: truefrom histos import protect
guarded = protect(my_tools, policy=class="token-string">"security.policy.yaml")
agent.tools = list(guarded) # the same tools, now bounded Your editor can validate it too: $schema points at the schema served here. Start with the policy-writing guide.
A security policy should be reviewable in a pull request by someone who does not read Python.
Strictly validated and canonicalized. The same document hashes the same everywhere, which policy pinning and approvals depend on.
Runtimes may change. The policy remains the reviewable contract.
The policy is the contract. The runtime is an implementation.
Scope
Histos earns trust by naming the jobs it does not do.
It works on capability, not on interpreting whether text is malicious.
It consumes a principal established by your host and cannot verify that the host bound it correctly.
The system of record remains the final authority, especially across the check-to-execution gap.
Complete mediation depends on your integration. A tool you do not wrap is a tool Histos cannot bound.
Budgets and rate limits are per identity and tool inside one process. There is no run or session scope yet.
It enforces the written policy, not what someone later wishes that policy had meant.
There is no agent registry, identity provider, sandbox or hosted control plane. Histos is the local enforcement layer inside a Python host.
Histos answers can. Other layers may help answer should.
Open source
You can inspect, test and own the code that decides.
Histos Python, the policy format and deterministic enforcement are Apache-2.0. A future commercial layer would operate that same boundary across teams and services — not hide stronger enforcement.
Open source is the boundary. Any commercial layer begins with operating it at scale.
Status
v0.1.1 is live on PyPI: the Apache-2.0 reference runtime for Python 3.12+.
Draft 0.1 is implemented, documented, backed by JSON Schema and pinned by a conformance corpus.
Validation, review, coverage, tool import and definition-drift checks ship in the package and fit into CI.
The framework-free core, LangChain StructuredTool adapter and LangGraph ToolNode execution path ship and are exercised in the repository demos.
Not shipped. Additional runtimes and fleet operations wait for evidence from real adoption.
Histos stays narrow until real deployments prove which next layer is worth adding.
Install Histos, wrap a real tool and make its limits explicit before the model reads untrusted content.
histos 0.1.1 and Policy Format Draft 0.1 are released under Apache-2.0.