IronScope
June 23, 2026 ยท View on GitHub
IronScope helps you understand and control what an AI agent's tools are doing on the machine where they run.
If your app uses LangChain or LangGraph, the agent may call tools that read files, start subprocesses, connect to services, or perform other local actions. In normal logs, it can be hard to tell which exact tool caused which system action, especially when agents use async code, background workers, retries, or subprocesses.
IronScope watches supported Python tool execution at runtime and connects local system activity back to the active tool call. That means you can see events like:
read_fileopened/tmp/report.txtshelltried to execute/usr/bin/idbackground_popenstarted a child Python processasyncio_to_thread_readeraccessed a protected file from a worker thread
You can run IronScope in monitor mode to audit what tools are doing, or enforce mode to block specific file, process, or network actions per tool.
The core use case is giving agentic applications a runtime safety layer around tool execution. Instead of only trusting application logs or network traces, IronScope observes local behavior at the OS boundary and labels it with the tool context that caused it.
In the current V0.1 scope, IronScope targets LangChain/LangGraph-style tools running on validated CPython 3.12 Linux environments. It is useful for developers who want to inspect, debug, or restrict real tool behavior without rewriting their agent framework.
For the deeper design rationale and current limitations, see
docs/architecture/runtime-tool-context.md.
For a first-user walkthrough, see docs/quickstart/langchain-shell-tool.md. For broader async, worker-thread, subprocess, and nested-tool coverage, see examples/langgraph_tool_execution_daemon.py with examples/policies/langgraph-tool-execution-enforce.yaml.
License
IronScope is licensed under the Apache License, Version 2.0. See LICENSE.
V0.1 Support Scope
Validated in V0.1:
- CPython 3.12.3 on aarch64 Linux with a matching validated contract profile.
langchain-core==1.4.7.- Tool execution through
BaseTool.invoke,BaseTool.ainvoke,BaseTool.run, orBaseTool.arun. - Tool shape coverage: privileged E2E tests exercise subclassed
BaseTool; resolver tests cover@tool,StructuredTool.from_function, and subclassedBaseToolname resolution. - Asyncio task lifecycle tracking, nested same-task tool calls, parallel async tools, cancellation/exception cleanup,
ThreadPoolExecutor,asyncio.to_thread,threading.Thread, and subprocesses created inside tool context. - Kernel-visible file open, exec, and socket connect monitoring/enforcement through LSM hooks.
Not claimed in V0.1:
- CPython 3.10/3.11/3.13, PyPy, no-GIL/free-threaded Python, x86_64, or embedded Python. These need separate validated contracts and are not part of the packaged V0.1 support claim.
- Raw function calls that bypass LangChain
BaseToolboundaries. - CPython instrumented opcode paths enabled by tracing, coverage, debuggers, profilers, or
sys.monitoring. - Arbitrary async runtimes that do not use supported CPython
_asynciotask boundaries. - Full per-tool policy for the first execution of a previously unseen tool object when the object has not yet been resolved. That execution is still inside a tool context, but it follows
unknown_tool_policy; later executions of the same resolved live object use per-tool policy. - HTTP payload parsing or LLM provider wire-format parsing. Tool identity comes from Python runtime state, not network inspection.
How Runtime Tool Identity Works
- eBPF probes CPython frame execution and returns early for unprotected processes.
- Supported LangChain
BaseToolboundary frames create a tool context. - Known tool objects use cached tool ids and per-tool policy.
- Unknown tool objects use
unknown_tool_policyand emit a resolver candidate. - Userspace reads CPython runtime memory, resolves the LangChain tool name, and updates the BPF cache.
- Later executions of that live tool object use per-tool policy.
The V0.1 LangChain path does not require application-level tool registration. Tool identity is resolved from CPython runtime state after IronScope observes a supported LangChain tool boundary.
Policy Controls
V0.1 policy fields live under ironscope:. unknown_tool_policy and resolver_error_policy currently accept allow or deny.
ironscope:
agents:
- pid: 12345
unknown_tool_policy: allow
resolver_error_policy: deny
agent_child_scope: protect_only_tool_children
default_tool_policy: allow
tools:
- name: read_file
fs:
deny:
- /etc/passwd
- name: shell
exec:
deny:
- /usr/bin/id
mode: monitor
Policy control fields:
unknown_tool_policy: decision for a supported tool execution before IronScope has resolved the runtime tool object to a known tool name. V0.1 may hit this on the first execution of a newly seen tool object; useallowfor a less disruptive first run ordenyto fail closed for unresolved tools.resolver_error_policy: decision when IronScope detects a possible tool boundary but userspace cannot safely resolve the runtime identity. Usedenywhen unresolved identity should fail closed.agent_child_scope: controls child-process protection.protect_only_tool_childrenmeans only children spawned inside an active tool context inherit tool protection; idle children outside tool context are not treated as tool work.default_tool_policy: fallback for known-tool resource accesses that do not match an explicit allow or deny rule. Useallowfor deny-list style policies anddenyfor allow-list style policies.
These fields exist because runtime identity can be temporarily unknown, resolver failures must have explicit behavior, child processes can outlive the Python frame that spawned them, and known tools need a clear fallback when no resource-specific rule matches.
V0.1 does not expose a non-tool enforcement policy. Activity outside an active tool context is emitted as unattributed audit data and is not denied by IronScope.
Audit Output
Structured JSON output uses schema ironscope.audit.v1 and includes:
ctx_id: unique context for a tool execution.tool_nameandtool_id: resolved tool identity when known;unknown,resolver_error, orunattributedotherwise.identity_state:known_tool,unknown_tool,resolver_error, orunattributed.policy_source:tool,unknown_tool,resolver_error,unattributed,default_allow, ormonitor.py_events: tool lifecycle, async task, worker, and cleanup events.resolver_events: CPython object/frame/code candidates used by the userspace resolver.guard_events: file open, exec, and socket connect decisions with allow/deny action.bracket_checkandfinal_state: lifecycle attribution and cleanup evidence.
In monitor mode, action: "deny" means the policy decision was logged as a would-deny decision. Actual syscall blocking requires --mode enforce and active BPF LSM.
CPython Contracts
IronScope does not guess CPython frame, object, or _asyncio offsets at runtime. The CPython runtime path detects the target process Python executable, Python build-id or SHA256 fallback, _asyncio module build-id, and architecture, then loads a matching contract from --contract-dir or IRONSCOPE_CONTRACT_DIR. If no validated contract matches, IronScope fails closed before attaching tool-context probes.
A contract describes where IronScope can safely observe CPython execution lifecycle state for one specific Python build. This is necessary because CPython internals are not a stable public ABI: frame layout, coroutine/task layout, and interpreter instruction offsets can change across Python versions, build flags, distributions, and CPU architectures.
The development tree currently ships a validated CPython 3.12.3 aarch64 contract under tools/python-contracts/. That is the validated V0.1 contract path. Production packages should install validated contracts under /usr/share/ironscope/python-contracts.
IronScope V0.1 does not provide general-purpose contract discovery for arbitrary CPython versions. Contract generation for additional CPython versions and architectures is experimental development work and is not part of the V0.1 support claim. Reliable generation requires proving version- and architecture-specific interpreter lifecycle points such as frame activation, coroutine resume, normal return cleanup, exception cleanup, and _asyncio task transitions.
Supported profile workflow:
# Inspect the target process runtime identity.
ironscope profile detect --pid 12345
# Validate that a packaged contract matches the target process identity
# and contains the required probe offsets.
ironscope profile validate --pid 12345 --contract tools/python-contracts/cpython-3.12.3-aarch64-*.json
V0.1 does not expose runtime contract generation. IronScope does not fetch debug symbols, download profiles, or synthesize missing contracts at enforcement startup. Missing profiles are release/support decisions, not runtime guesses.
Build And Run
Prerequisites:
- Linux kernel with BPF LSM enabled and
bpflisted in/sys/kernel/security/lsm. - Root privileges for loading BPF programs and attaching LSM hooks.
- Rust toolchain for building the userspace binary and BPF object.
- A supported CPython 3.12.3 aarch64 target process.
cargo build --release
sudo ./target/release/ironscope \
--config examples/policies/langchain-monitor.yaml \
--contract-dir tools/python-contracts \
--mode monitor \
--duration 60 \
--output /tmp/ironscope-audit.json
Use --mode enforce only after setting explicit unknown_tool_policy, resolver_error_policy, agent_child_scope, default_tool_policy, and per-tool rules for the workload.
Release images install runtime assets at the default paths IronScope searches automatically:
/usr/bin/ironscope
/usr/share/ironscope/python-contracts/index.json
/usr/share/ironscope/python-contracts/*.json
/usr/share/ironscope/rules/framework_rules.yaml
/usr/share/ironscope/examples/policies/*.yaml
Build the release image with the verified Rust/Aya build path:
docker build -f docker/Dockerfile.release -t ironscope:v0.1.0-alpha .
The image is a packaging convenience, not a sandbox. Runtime monitoring and enforcement still need host privileges, host PID namespace access, BPF LSM, BTF, and a target process that matches a packaged CPython contract. A typical host attach run looks like:
docker run --rm \
--privileged \
--pid=host \
-v /proc:/proc \
-v /sys:/sys \
-v /tmp/ironscope:/tmp/ironscope \
ironscope:v0.1.0-alpha \
--config /tmp/ironscope/policy.yaml \
--contract-dir /usr/share/ironscope/python-contracts \
--mode monitor \
--duration 60 \
--output /tmp/ironscope/audit.json
Verification
Core deterministic gates for the V0.1 runtime path:
cargo fmt --all --check
cargo check
cargo test
python3 tools/verify_offsets.py --contract tools/python-contracts/cpython-3.12.3-aarch64-f44e82bd43207357844bfae18cf0460f0825d1ae.json
PYTHON=.venv-e2e-v1/bin/python bash tests/e2e_langchain_tool_policy.sh
PYTHON=.venv-e2e-v1/bin/python bash tests/e2e_langchain_first_call_unknown_policy.sh
PYTHON=.venv-e2e-v1/bin/python bash tests/e2e_langgraph_tool_execution_daemon.sh
IRONSCOPE_ASYNC_CASE=nested PYTHON=.venv-e2e-v1/bin/python bash tests/e2e_async_lifecycle_case.sh
IRONSCOPE_WORKER_CASE=threadpool PYTHON=.venv-e2e-v1/bin/python bash tests/e2e_worker_lifecycle_case.sh
PYTHON=.venv-e2e-v1/bin/python bash tests/e2e_resource_policy_case.sh fs enforce
Historical traffic/provider and phase-prototype test assets are not part of the V0.1 support claim and are not kept in the release-facing test tree.