Skip to content
Documentation GitHub
Platform

Python Sidecar Security

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

+----------------------------------+ +----------------------------------+
| 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)
LayerMechanismLocation
1. Import allowlistDeny-by-default module allowlistRust + Python AST validators
2. Dual AST validationruff_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 onlydspy_daemon.rs
4. LLM broker patternCredentials never reach daemon; all LLM calls via IPC to Rust ProviderRegistryInklingsLM adapter

Threat: A malicious dspy_module imports os, subprocess, socket, or ctypes to run arbitrary commands, open network connections, or call native functions.

Mitigations:

LayerMechanismCoverage
Rust AST validatorruff_python_parser walks Import/ImportFrom nodes against allowlistCatches import os, from subprocess import run, etc. before IPC
Python AST validatorast.parse() + ast.walk() with same allowlistCatches at daemon level before importlib
Builtins detectionAST inspection for __import__(), compile() callsCatches dynamic import bypass

Residual risk: Pure-Python computation that doesn’t import blocked modules can still run. Addressed by T5 (resource exhaustion) mitigations.

Threat: A module reads environment variables containing API keys and exfiltrates them via network or file output.

Mitigations:

LayerMechanismCoverage
Environment sanitization.env_clear() + explicit allowlist in dspy_daemon.rsZero credentials in daemon env
LLM brokerInklingsLM adapter routes completions via IPC; no direct API accessCredentials never leave Rust
AST validationBlocks import os (needed for os.environ)Defense-in-depth against env access

Threat: A compromised skill artifact contains obfuscated imports or side effects.

Mitigations:

LayerMechanismCoverage
Rust AST validatorValidates at publish-time AND at run-timeDouble gate
Python AST validatorFinal validation before importlibCatches anything missed by Rust
Allowlist (deny-default)Unknown modules rejected regardless of obfuscationBlocks novel attack vectors
Builtins detectionCatches __import__('os'.encode().decode()) patternsObfuscation resistance

Threat: A module writes crafted JSON to stdout to manipulate the IPC protocol — injecting fake responses or hijacking control flow.

Mitigations:

LayerMechanismCoverage
Correlation IDsEach llm_request/llm_response pair uses a unique IDPrevents response injection
Message type discriminationRust parser expects specific type field valuesRejects malformed messages
Sequential protocolRust controls the read loop; only one run at a timeNo interleaving attacks

Residual risk: AST validator blocks import sys, preventing direct sys.stdout.write(). DSPy’s internal print() calls go to stderr, not stdout.

Threat: A module consumes unbounded CPU, memory, or disk.

Mitigations:

LayerMechanismCoverage
Response timeoutRESPONSE_TIMEOUT (60s) in dspy_daemon.rsBounds wall-clock time per request
Idle timeoutDEFAULT_IDLE_TIMEOUT (5 min) auto-shuts down daemonPrevents persistent resource hold
kill_on_dropCommand::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.

Threat: A module accesses the host filesystem or network to escalate beyond the sidecar context.

Mitigations:

LayerMechanismCoverage
AST validationBlocks os, subprocess, socket, http, urllib, pathlib importsPrevents filesystem/network access via stdlib
Environment sanitizationRestricted PATH limits discoverable programsReduces subprocess attack surface
Module-level restrictionBlocks bare function calls outside class definitionsPrevents 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.

RiskSeverityStatus
open() builtin filesystem accessMediumUnmitigated — requires OS sandbox (Landlock/seatbelt)
Memory exhaustionMediumPartially mitigated (timeout kills process, but damage may occur)
CPU exhaustionLowMitigated by response timeout (60s bound)
Python interpreter vulnerabilitiesLowOut of scope — keep Python updated
C extension loading in allowed packagesLowpydantic and dspy may load C extensions; accepted risk
Compiled .pyc injectionVery LowPYTHONDONTWRITEBYTECODE=1 prevents creation
  • Template-only execution: Only shipped templates run in the sidecar. dspy_module artifacts store template_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 InklingsLM adapter is a DSPy LM subclass that routes completion requests back to Rust via JSON-RPC, where the ProviderRegistry handles actual API calls.
  • Environment sanitization via env_clear(): The daemon process starts with a completely empty environment, then adds only PATH, HOME, TMPDIR, and PYTHONDONTWRITEBYTECODE. Zero API keys or auth tokens are present.
FileRole
crates/infrastructure/agent-harness/src/dspy_daemon.rsRust-side daemon management, IPC, AST validation
apps/python-sidecar/inklings_py/daemon.pyPython-side daemon loop, module loading
apps/python-sidecar/inklings_py/inklings_lm.pyInklingsLM DSPy adapter (LLM broker client)
apps/python-sidecar/inklings_py/ast_validator.pyPython AST validation (allowlist enforcement)

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.

Was this page helpful?