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, runnableexamples/chained_escalation.py— the chain scenario, all four variantsexamples/stale_authorization.py— why a human approval doesn't become a standing permissionexamples/external_agent_demo.py— a plain, non-framework tool-calling agent using onlyguard_tools(), proving the agent never receives the unwrapped toolexamples/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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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.