Skip to main content

Mediation Layer

A runtime authorization layer that sits between an AI agent and the tools it's allowed to use. Every proposed tool call is intercepted and evaluated — ALLOW, DENY, or ESCALATE — before it ever reaches the real tool. The agent proposes an action; it can never authorize itself.

AI Agent → Mediation Layer → Tool
             │
             ├── Directive & parameter provenance (who/what told the
             │   agent to do this — user, prior tool output, or a
             │   document/webpage it read?)
             ├── Declared scope + session context
             ├── Cumulative session risk
             └── Chained-privilege-escalation detection: individually
                 in-scope steps whose COMBINED effect was never
                 authorized (e.g. read-sensitive-data → send-externally)

Distributed as mediation-layer; the Python package you import is toolgate (that's the project's internal/working name — see Mediation_Layer.md for the full design rationale behind the rule set).

Install

pip install mediation-layer

(Pre-release / from source: pip install . from the repo root, or pip install dist/mediation_layer-*.whl after building.)

from toolgate import TaskSession, guarded_tool, untrusted, ToolDenied, ToolEscalated

Quickstart

Protect your own tool functions with @guarded_tool. The function body is whatever you already had — toolgate only decides whether it runs:

from toolgate import TaskSession, guarded_tool, untrusted, ToolDenied

@guarded_tool(category="file_ops", operation="read")
def read_file(path: str) -> str:
    return open(path).read()

@guarded_tool(category="messaging", operation="send")
def send_message(to: str, body: str) -> dict:
    ...  # your real send logic
    return {"sent": True, "to": to}

with TaskSession(intent="Read the invoice and summarize it",
                 scope={"read:file_ops"}) as session:

    content = read_file(path="inbox/invoice.txt")   # in scope -> ALLOW

    # A value that came from somewhere untrusted is labelled, so the
    # policy engine knows its real origin instead of trusting the agent:
    try:
        send_message(to=untrusted("evil@example.com", origin="invoice.txt"),
                     body="...")
    except ToolDenied as e:
        print(e.result.rule, e.result.reason)

Already have plain functions (or LangChain tools) you don't want to rewrite as decorators? Wrap them instead — same enforcement, no changes to the functions themselves:

from toolgate import guard_tools

def delete_file(path: str) -> bool:
    ...  # your real delete logic, completely unaware of toolgate

# overrides is only needed when the function name doesn't hint its
# category/operation clearly enough for auto-inference to guess right
protected = {t.__name__: t for t in guard_tools(
    [delete_file],
    overrides={"delete_file": ("file_ops", "delete")},
)}

# hand `protected` to your agent's tool registry — never the raw function.
# See examples/external_agent_demo.py for this exact pattern end to end,
# including proof the agent only ever receives the wrapped version.
from toolgate.integrations.langchain import guard_langchain_tools

safe_tools = guard_langchain_tools(my_tools)      # list[BaseTool] in, list[BaseTool] out
agent = create_react_agent(llm, safe_tools)

The three decisions

Decision Meaning What happens
ALLOW Sufficiently authorized, consistent with the task The real function runs
DENY Outside permitted scope, or an untrusted source trying to direct a sensitive action Raises ToolDenied. Final — a denied action is not recoverable; only a fresh proposal is evaluated
ESCALATE Ambiguous, or a chain of otherwise-fine steps whose combined effect wasn't authorized Raises ToolEscalated. The real function does not run until a human approves

Resolving an escalation:

try:
    delete_file(path="reports/old_draft.txt")
except ToolEscalated as e:
    print(e.result.reason)
    result = session.confirm(e)   # records the human override, runs the
                                   # original call exactly once — never
                                   # re-argued by the content that
                                   # triggered the escalation

session.confirm(e, reusable=True) lets an identical later call skip re-asking; anything different (destination, resource, phase, scope, directive source, or risk state) is evaluated fresh, never covered by an old approval.

Chained privilege escalation

Each step below is individually within the declared scope. The mediator still catches the combined effect:

with TaskSession(intent="Read the summary and notify the auditor",
                 scope={"read:file_ops", "query:db_access", "send:messaging"}):
    read_file(path="reports/q3_summary.txt")              # ALLOW
    query_db(table="employees", columns="name,salary")    # ALLOW (tags read_sensitive)
    send_message(to="auditor@vendor-x.com", body=...)      # ESCALATE:
                                                            # "data-exfiltration" chain

See examples/chained_escalation.py for the full scenario, including the same chain driven by an injected document (denied outright) and the same chain pre-authorized by an explicit scope tag (allowed straight through).

Dashboard

import toolgate
toolgate.serve_dashboard()            # background thread, http://127.0.0.1:8000

Reads the same in-process store every TaskSession writes to — decisions from @guarded_tool, guard_tools(), and framework adapters all show up with no extra wiring. GET /audit/recent is the aggregated feed the dashboard itself polls, if you want the raw JSON.

Examples

  • examples/quickstart.py — the pattern above, runnable
  • examples/chained_escalation.py — the chain scenario, all four variants
  • examples/stale_authorization.py — why a human approval doesn't become a standing permission
  • examples/external_agent_demo.py — a plain, non-framework tool-calling agent using only guard_tools(), proving the agent never receives the unwrapped tool
  • examples/langchain_agent.py + examples/agent_server.py — a real LangChain agent behind a chat UI with the dashboard embedded

Optional extras

pip install "mediation-layer[llm]"        # live LLM advisor (Groq or Anthropic) for ambiguous-scope
                                           # and injection checks; falls back to a deterministic
                                           # heuristic with no API key set, so it always runs offline
pip install "mediation-layer[langchain]"  # guard_langchain_tools
pip install "mediation-layer[dev]"        # pytest, httpx (for the test suite)

The LLM advisor is consulted only for two narrow, confidence-scored questions (ambiguous scope match, suspected injected instruction) and never returns an authorization decision itself — the rule-based policy engine is always the final word.

Testing

pip install -e ".[dev,langchain]"
pytest tests/ -v

License

MIT

Metadata

Release files for mediation-layer 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mediation-layer 0.1.0
File Size Uploaded Provenance Actions
mediation_layer-0.1.0.tar.gz 42.8 kB Without provenance Details for mediation_layer-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for mediation-layer 0.1.0
File Interpreter ABI Platform Actions
mediation_layer-0.1.0-py3-none-any.whl Python 3 none any Details for mediation_layer-0.1.0-py3-none-any.whl

Total release size: 87.4 kB

Release files / mediation_layer-0.1.0.tar.gz

Download URL mediation_layer-0.1.0.tar.gz
Size 42.8 kB
Tags Source
SHA-256 checksum
How to use checksums
1e4ae048a7b139e7bc0cc799b1f5f7125d0addf2a647e433c9b74664a6b60c20
BLAKE2b-256 checksum
How to use checksums
689ae32bbdaa018174ef6620267e6f5a6ad6993eadfd2901f6443c3b0f64cad1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.4

Release files / mediation_layer-0.1.0-py3-none-any.whl

Download URL mediation_layer-0.1.0-py3-none-any.whl
Size 44.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b0a631ceed458e7121071ad8337ce3a749e5a9bb296def8d5675c942e4f30280
BLAKE2b-256 checksum
How to use checksums
840e242b7ee0e952a29e99603f761013076356bb8dd97fea05b2333212426c16
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.4

Security for mediation-layer 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page