Python Sidecar Security
Overview
Section titled “Overview”The Python sidecar (apps/python-sidecar/) is a long-running Python process that executes dspy_module skill artifacts on behalf of the Inklings agent harness. It communicates with the Rust host process (dspy_daemon.rs) via stdin/stdout JSON-RPC.
The sidecar uses a template-only execution model — only shipped templates run, eliminating arbitrary code execution. A four-layer defense-in-depth strategy protects against the remaining attack surface.
Scope: Security boundaries, threat model, and mitigations for the Python sidecar process
Dependencies: Agent Core System, Skill System, LLM System
Architecture
Section titled “Architecture”Trust Boundaries
Section titled “Trust Boundaries”+----------------------------------+ +----------------------------------+| Rust Process | | Python Daemon || | | || +----------------------------+ | | +----------------------------+ || | OS Keychain (API Keys) | | | | No API Keys | || | Rig ProviderRegistry | | | | No Auth Tokens | || | Supabase Auth Tokens | | | | Restricted PATH | || | Workspace SQLite DBs | | | | No Workspace Access | || +----------------------------+ | | +----------------------------+ || | | || +----------------------------+ | | +----------------------------+ || | Rust AST Validator | | | | Python AST Validator | || | (ruff_python_parser) |--+-+ | | (ast module) | || | Validates BEFORE IPC | | | | | Validates BEFORE import | || +----------------------------+ | | | +----------------------------+ || | | | || +----------------------------+ | | | +----------------------------+ || | LLM Broker | | | | | InklingsLM Adapter | || | (Receives llm_request, |<-+-+--+--| (DSPy LM subclass, | || | calls Rig, returns |--+-+--+->| routes via IPC) | || | llm_response) | | | | +----------------------------+ || +----------------------------+ | | | || | | | +----------------------------+ || | +--+->| dspy_module loading | || | | | (importlib, sandboxed) | || | | +----------------------------+ |+----------------------------------+ +----------------------------------+ stdin/stdout IPC (JSON-RPC, one object per line)Four-Layer Defense Model
Section titled “Four-Layer Defense Model”| Layer | Mechanism | Location |
|---|---|---|
| 1. Import allowlist | Deny-by-default module allowlist | Rust + Python AST validators |
| 2. Dual AST validation | ruff_python_parser (Rust, pre-dispatch) + ast module (Python, pre-import) | Both sides of IPC boundary |
| 3. Environment sanitization | .env_clear() with explicit PATH/HOME/TMPDIR only | dspy_daemon.rs |
| 4. LLM broker pattern | Credentials never reach daemon; all LLM calls via IPC to Rust ProviderRegistry | InklingsLM adapter |
Threat Model
Section titled “Threat Model”T1: Arbitrary Code via Dangerous Imports
Section titled “T1: Arbitrary Code via Dangerous Imports”Threat: A malicious dspy_module imports os, subprocess, socket, or ctypes to run arbitrary commands, open network connections, or call native functions.
Mitigations:
| Layer | Mechanism | Coverage |
|---|---|---|
| Rust AST validator | ruff_python_parser walks Import/ImportFrom nodes against allowlist | Catches import os, from subprocess import run, etc. before IPC |
| Python AST validator | ast.parse() + ast.walk() with same allowlist | Catches at daemon level before importlib |
| Builtins detection | AST inspection for __import__(), compile() calls | Catches dynamic import bypass |
Residual risk: Pure-Python computation that doesn’t import blocked modules can still run. Addressed by T5 (resource exhaustion) mitigations.
T2: Credential Exfiltration
Section titled “T2: Credential Exfiltration”Threat: A module reads environment variables containing API keys and exfiltrates them via network or file output.
Mitigations:
| Layer | Mechanism | Coverage |
|---|---|---|
| Environment sanitization | .env_clear() + explicit allowlist in dspy_daemon.rs | Zero credentials in daemon env |
| LLM broker | InklingsLM adapter routes completions via IPC; no direct API access | Credentials never leave Rust |
| AST validation | Blocks import os (needed for os.environ) | Defense-in-depth against env access |
T3: Supply Chain Compromise
Section titled “T3: Supply Chain Compromise”Threat: A compromised skill artifact contains obfuscated imports or side effects.
Mitigations:
| Layer | Mechanism | Coverage |
|---|---|---|
| Rust AST validator | Validates at publish-time AND at run-time | Double gate |
| Python AST validator | Final validation before importlib | Catches anything missed by Rust |
| Allowlist (deny-default) | Unknown modules rejected regardless of obfuscation | Blocks novel attack vectors |
| Builtins detection | Catches __import__('os'.encode().decode()) patterns | Obfuscation resistance |
T4: IPC Injection
Section titled “T4: IPC Injection”Threat: A module writes crafted JSON to stdout to manipulate the IPC protocol — injecting fake responses or hijacking control flow.
Mitigations:
| Layer | Mechanism | Coverage |
|---|---|---|
| Correlation IDs | Each llm_request/llm_response pair uses a unique ID | Prevents response injection |
| Message type discrimination | Rust parser expects specific type field values | Rejects malformed messages |
| Sequential protocol | Rust controls the read loop; only one run at a time | No interleaving attacks |
Residual risk: AST validator blocks import sys, preventing direct sys.stdout.write(). DSPy’s internal print() calls go to stderr, not stdout.
T5: Resource Exhaustion
Section titled “T5: Resource Exhaustion”Threat: A module consumes unbounded CPU, memory, or disk.
Mitigations:
| Layer | Mechanism | Coverage |
|---|---|---|
| Response timeout | RESPONSE_TIMEOUT (60s) in dspy_daemon.rs | Bounds wall-clock time per request |
| Idle timeout | DEFAULT_IDLE_TIMEOUT (5 min) auto-shuts down daemon | Prevents persistent resource hold |
| kill_on_drop | Command::new().kill_on_drop(true) | Cleans up on Rust process exit |
Not mitigated (requires OS sandbox): Memory limits, CPU limits, disk write limits, fork bombs. This is the primary driver for OS-level sandboxing.
T6: Privilege Escalation
Section titled “T6: Privilege Escalation”Threat: A module accesses the host filesystem or network to escalate beyond the sidecar context.
Mitigations:
| Layer | Mechanism | Coverage |
|---|---|---|
| AST validation | Blocks os, subprocess, socket, http, urllib, pathlib imports | Prevents filesystem/network access via stdlib |
| Environment sanitization | Restricted PATH limits discoverable programs | Reduces subprocess attack surface |
| Module-level restriction | Blocks bare function calls outside class definitions | Prevents side effects at import time |
Known gap: The open() builtin is available without any import. A module could read files with json.load(open('/path/to/file')) using only allowed imports. OS-level sandboxing (filesystem restrictions via Landlock/seatbelt) closes this gap.
Residual Risks
Section titled “Residual Risks”| Risk | Severity | Status |
|---|---|---|
open() builtin filesystem access | Medium | Unmitigated — requires OS sandbox (Landlock/seatbelt) |
| Memory exhaustion | Medium | Partially mitigated (timeout kills process, but damage may occur) |
| CPU exhaustion | Low | Mitigated by response timeout (60s bound) |
| Python interpreter vulnerabilities | Low | Out of scope — keep Python updated |
| C extension loading in allowed packages | Low | pydantic and dspy may load C extensions; accepted risk |
Compiled .pyc injection | Very Low | PYTHONDONTWRITEBYTECODE=1 prevents creation |
Key Design Decisions
Section titled “Key Design Decisions”- Template-only execution: Only shipped templates run in the sidecar.
dspy_moduleartifacts storetemplate_id+state_blob(serialized DSPy JSON state), never source code. This eliminates arbitrary code execution as a threat vector. - Dual AST validation: Both Rust (pre-IPC) and Python (pre-import) validate the same allowlist. This prevents any single validation layer from being the sole trust boundary.
- LLM broker pattern: Credentials never cross the IPC boundary. The Python
InklingsLMadapter is a DSPy LM subclass that routes completion requests back to Rust via JSON-RPC, where theProviderRegistryhandles actual API calls. - Environment sanitization via
env_clear(): The daemon process starts with a completely empty environment, then adds onlyPATH,HOME,TMPDIR, andPYTHONDONTWRITEBYTECODE. Zero API keys or auth tokens are present.
Implementation Details
Section titled “Implementation Details”Key Files
Section titled “Key Files”| File | Role |
|---|---|
crates/infrastructure/agent-harness/src/dspy_daemon.rs | Rust-side daemon management, IPC, AST validation |
apps/python-sidecar/inklings_py/daemon.py | Python-side daemon loop, module loading |
apps/python-sidecar/inklings_py/inklings_lm.py | InklingsLM DSPy adapter (LLM broker client) |
apps/python-sidecar/inklings_py/ast_validator.py | Python AST validation (allowlist enforcement) |
Import Allowlist
Section titled “Import Allowlist”The following modules are allowed (deny-by-default):
dspy,typing,dataclasses,json,re,math,collections,functools,itertools,enum,abc,copy,datetime,pydantic
All other imports are rejected by both AST validators.
Related
Section titled “Related”- Skill System — Skill artifact kinds and execution model
- Agent Core System — Agent process model and harness integration
- LLM System — Provider registry and credential management
Was this page helpful?
Thanks for your feedback!