Skip to content
Odysseus lashed to the mast of his ship under a full moon, ropes around his chest, as the Sirens sing from the rocks off the bow.

ἱστός - the mast

The model proposes.
Policy decides.

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.

Works with
  • Raw Python
  • LangChain
  • LangGraph

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]".

  • In-process
  • No proxy
  • Fail-closed in enforce mode
  • Bring your identity
  • v0.1.1 on PyPI
  • Apache-2.0
  • Python ≥ 3.12
  • Core: 0 runtime deps
  • Policy Format Draft 0.1

The problem

A tool call turns model failure into a real action.

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.

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 model may still call tools

A manipulated model can still emit a perfectly valid-looking call: the right tool name, well-formed arguments and plausible intent.

Detection is not a hard boundary

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

Put the decision outside the model.

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.

Odysseus bound to the mast with heavy rope, looking past the Sirens on the rocks. The ship's tiller is out of his reach.

Detection changes probabilities.
Enforcement changes possibilities.

Both have value. They are not the same layer.

Runtime enforcement

Block the call. Constrain the result.

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

Deny before side effects.

The proposed call is evaluated against policy and trusted runtime context before the underlying function executes.

Tool access
may this role call this tool at all?
Argument schema
are names, types, ranges, enums and patterns valid?
Trusted binding
replace model-controlled values with trusted principal attributes.
Resource authorization
does the caller actually own or have access to the target resource?
State conditions
is the resource currently in a state where this action is allowed?
Rate limits
how often may this action happen?
Budgets
how much cumulative action is permitted?
Canary / secret screening recognises
is the call carrying a planted token or a verified secret out?
Confirmation
must a human approve this exact action first?

Any failed check means the tool does not run.

Before the result gets back

Allowed execution does not imply trusted output.

The return contract is enforced before tool output — or an exception — re-enters model context.

Return schema
does the result match the declared contract?
Projection
only declared fields leave the boundary.
Sensitive-field redaction
remove fields marked as sensitive.
Secret redaction recognises
stop supported secrets from flowing back into context.
Canary redaction recognises
strip planted tokens out when they surface.
Exception redaction
errors do not become accidental exfiltration channels.

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.

Policy primitives, shipping today 31

pre-tool 17
  • RBAC
  • default deny
  • role inheritance
  • arg schema
  • regex patterns
  • numeric bounds
  • string bounds
  • enum
  • array elements
  • trusted binding
  • resource ownership
  • resource conditions
  • rate limits
  • budgets
  • confirmation
  • secret detection
  • canary in arguments
post-tool 6
  • strict returns
  • output projection
  • sensitive redaction
  • secret redaction
  • canary redaction
  • exception redaction
developer tooling 8
  • hash-chained audit
  • audit verifier
  • policy review
  • coverage as a CI gate
  • MCP / OpenAI / OpenAPI import
  • tool definition drift
  • canonical policy hash
  • async tools

Why this matters

The gate never reads the conversation

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.

UNTRUSTEDContent the agent reads documents · tool output · retrieved data · user input any of which may carry an instruction you did not write The modelmay be manipulated - assume it isproposes a tool callHISTOSdeterministic · in-process · local checks in µstrusted identitystatic policythe request itselfmay THIS principal call THIS tool, with THESE arguments, on THIS resource? nothing here is read from the conversation DENYrecorded, non-coachingthe real worldonly within the bounds

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

Models refused the obvious attacks. Policy caught the ordinary-looking one.

Seven models. Six scenarios. Four temperatures. 1,498 agent executions.

0/170

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.

83/85

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

When policy beats another if.

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.

  1. 01

    One place for the boundary

    Security rules live in one policy instead of being scattered across tools, handlers and agent code.

  2. 02

    Default-deny by design

    A new or forgotten tool stays blocked until the policy explicitly grants it.

  3. 03

    Reviewable security changes

    Changing a refund limit from 500 to 5000 becomes an obvious policy diff, not a hidden application change.

  4. 04

    Portable semantics

    Ownership, trusted binding, confirmation, limits and output controls keep the same declared meaning.

  5. 05

    Coverage and verification

    Validate policy, find uncovered tool surfaces, detect drift and fail CI before deployment.

  6. 06

    Audit without rebuilding it yourself

    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:

pull request
  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

Bring your identity. Histos enforces its limits.

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.

Who is this?
Your identity layerEntra ID, Okta, Auth0, Keycloak or workload identity - whatever your host already trusts.
What may this identity let an agent do?
HistosThis tool, with these arguments, on this resource, within this budget and approval rule.
Should the backend still verify the action?
YesThe system of record remains the final authority; Histos does not replace backend authorization.

The policy speaks your vocabulary, not your IdP's

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.

in your host, not in the policy
  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 artifact
Five ways to get this wrong - all of which compile and run - are written out in docs/identity.md.

The gate is only as strong as the identity bound to it.

The format

Put the security boundary in the diff.

The policy is the reviewable security artifact; Python is its first runtime.

Review it, diff it, validate it and version it like code.

security.policy.yaml
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: true
and in your host
from 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.

Readable by humans

A security policy should be reviewable in a pull request by someone who does not read Python.

Strict enough for machines

Strictly validated and canonicalized. The same document hashes the same everywhere, which policy pinning and approvals depend on.

Designed to outlive one runtime

Runtimes may change. The policy remains the reviewable contract.

The policy is the contract. The runtime is an implementation.

Scope

Deliberately narrow. Explicitly limited.

Histos earns trust by naming the jobs it does not do.

  • It does not detect prompt injection

    It works on capability, not on interpreting whether text is malicious.

  • It does not authenticate callers

    It consumes a principal established by your host and cannot verify that the host bound it correctly.

  • It does not replace backend authorization

    The system of record remains the final authority, especially across the check-to-execution gap.

  • It does not protect tools it never sees

    Complete mediation depends on your integration. A tool you do not wrap is a tool Histos cannot bound.

  • It does not bound a whole agent run

    Budgets and rate limits are per identity and tool inside one process. There is no run or session scope yet.

  • It does not solve human intent

    It enforces the written policy, not what someone later wishes that policy had meant.

  • It does not run an agent fleet

    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

The enforcement boundary is Apache-2.0.

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 · Apache-2.0
  • Histos Python runtime
  • The policy format
  • Deterministic policy evaluation
  • Argument and resource checks
  • Output controls and redaction
  • Local audit trail
  • CLI and developer workflow
Commercial
  • Centralized operations
  • Fleet-wide visibility
  • Enterprise workflows
  • Long-term audit and governance

Open source is the boundary. Any commercial layer begins with operating it at scale.

Status

What ships in v0.1.1

  • now

    Histos Python

    v0.1.1 is live on PyPI: the Apache-2.0 reference runtime for Python 3.12+.

  • now

    Histos Policy Format

    Draft 0.1 is implemented, documented, backed by JSON Schema and pinned by a conformance corpus.

  • now

    CLI and developer workflow

    Validation, review, coverage, tool import and definition-drift checks ship in the package and fit into CI.

  • now

    Raw Python, LangChain and LangGraph

    The framework-free core, LangChain StructuredTool adapter and LangGraph ToolNode execution path ship and are exercised in the repository demos.

  • later

    Additional runtimes and organizational tooling

    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.

Bound one real tool today.

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.