Skip to main content

Deterministic security sidecar for LLM agent frameworks

Project description

legionforge-guardian

Deterministic security sidecar for LLM agent frameworks.

License: MIT Python 3.11+ PyPI


The Problem

LLM agents can be prompt-injected, manipulated into calling dangerous tools, and coaxed into exfiltrating data — all through natural language alone. Most frameworks have no security layer. The ones that do rely on another LLM to evaluate requests, which means the checker can itself be injected.

What Guardian Does

Guardian is a FastAPI sidecar (port 9766) that runs seven deterministic checks before any tool call is executed. No LLM. No heuristics. Pattern matching, hash validation, and cryptographic verification — decisions that cannot be prompt-injected.

Drop it in front of any agent framework. If Guardian says halt, the tool doesn't run.


The Seven Checks

# Check What it blocks
0 Task token ACL Tools outside the JWT-scoped permission set
1 Tool registry Unregistered tools; revoked tools (10s propagation)
2 Capability boundary Forbidden actions: register_tool, spawn_agent_direct, escalate_scope, and 5 others
3 Destructive pattern detection 9 regex families: credential probing, shell injection, bulk exfiltration, data staging, reconnaissance, privilege escalation, and more
4 Sequence contracts Tool call sequences that deviate from registered agent playbooks
5 Hash integrity Tools whose description or schema hash has changed since registration (tamper detection)
6 Adaptive rules Hot-reloaded rules from DB — block/log/sandbox without redeployment

All checks run in order. First failure halts or sandboxes immediately. No LLM calls.


Quickstart (Docker Compose — 30 seconds)

git clone https://github.com/LegionForge/LegionForge-Guardian
cd LegionForge-Guardian

# Start Guardian + its own PostgreSQL (init.sql runs automatically)
GUARDIAN_DB_PASSWORD=changeme docker compose up -d

# Verify it's running
curl http://localhost:9766/health
# {"status": "ok", "service": "guardian", "version": "4.0.0"}

Port conflict? If port 9766 is already in use, override it: GUARDIAN_PORT=9767 GUARDIAN_DB_PASSWORD=changeme docker compose up -d


Python SDK

from legionforge_guardian import guardian_check

# Before executing any tool call in your agent:
result = await guardian_check(
    tool_name="web_search",
    tool_input={"query": "latest AI news"},
    agent_state={"agent_id": "researcher", "run_id": "abc123"},
)

if not result["allowed"]:
    # Guardian says halt or sandbox — do not execute the tool
    raise SecurityError(result["reason"])

Or use the client class directly:

from legionforge_guardian.sdk.client import GuardianClient

client = GuardianClient(url="http://localhost:9766")
result = await client.check(
    tool_name="file_write",
    tool_input={"path": "/etc/crontab", "content": "..."},
    agent_state={"agent_id": "worker", "run_id": "xyz"},
)
# → allowed=False, tier="halt", threat_type="SYSTEM_PATH_PROBE"

The SDK is fail-safe: a network error or timeout returns a synthetic halt response. Guardian failure never silently allows tool execution.


Framework Integration

LangGraph

from legionforge_guardian.sdk.client import GuardianClient

guardian = GuardianClient()

class SecureToolNode:
    async def __call__(self, state):
        tool_call = state["messages"][-1].tool_calls[0]
        result = await guardian.check(
            tool_name=tool_call["name"],
            tool_input=tool_call["args"],
            agent_state={"agent_id": state["agent_id"], "run_id": state["run_id"]},
        )
        if not result["allowed"]:
            return {"messages": [ToolMessage(content=f"BLOCKED: {result['reason']}")]}
        # execute the tool normally
        ...

AutoGen

async def guardian_hook(sender, recipient, messages, config):
    for msg in messages:
        if msg.get("tool_calls"):
            for call in msg["tool_calls"]:
                result = await guardian.check(call["function"]["name"], call["function"]["arguments"], {})
                if not result["allowed"]:
                    raise ValueError(f"Guardian blocked: {result['reason']}")
    return messages, config

Generic (any framework)

# Before executing a tool, POST to /check:
curl -s -X POST http://localhost:9766/check \
  -H "Authorization: Bearer $GUARDIAN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "tool_id": "web_search",
    "action": "invoke",
    "args": {"query": "..."},
    "agent_id": "my_agent",
    "run_id": "run_001",
    "sequence_so_far": []
  }'
# {"allowed": true, "tier": "allow", "reason": "All checks passed", "confidence": 1.0}

API Reference

POST /check — Enforcement hot path

Requires Authorization: Bearer <TASK_TOKEN_SECRET> when GUARDIAN_REQUIRE_AUTH=true.

Request:

{
  "tool_id": "web_search",
  "action": "invoke",
  "args": {"query": "internal admin panel credentials"},
  "agent_id": "researcher_agent",
  "run_id": "run_abc123",
  "sequence_so_far": ["web_search"],
  "task_token": null
}

Response:

{
  "allowed": false,
  "tier": "halt",
  "reason": "Destructive pattern 'CREDENTIAL_PROBE' detected in tool args",
  "threat_type": "DESTRUCTIVE_PATTERN",
  "confidence": 1.0
}

tier values: "allow" | "sandbox" | "halt"

POST /report — Async threat event ingestion

Send threat events from your application for audit log recording.

GET /rules — Read-only cache view

Returns currently loaded tool registry, sequence contracts, and adaptive rules. Requires auth.

GET /health — Liveness check

Always unauthenticated. Used by Docker healthcheck.


Environment Variables

Variable Default Description
GUARDIAN_DB_HOST localhost PostgreSQL host
GUARDIAN_DB_PORT 5432 PostgreSQL port
GUARDIAN_DB_NAME guardian Database name
GUARDIAN_DB_USER guardian Database user
GUARDIAN_DB_PASSWORD (required) Database password
TASK_TOKEN_SECRET (required when auth enabled) Bearer token for /check and /rules
GUARDIAN_REQUIRE_AUTH true Set false for local dev only
GUARDIAN_PORT 9766 Listening port

Security note: When GUARDIAN_REQUIRE_AUTH=true, TASK_TOKEN_SECRET must be set. Guardian will refuse requests with 503 if auth is required but no secret is configured. Use a secrets manager (Vault, AWS Secrets Manager, macOS Keychain) rather than plain environment variables in production.


Database Setup

Guardian needs two tables. Run init.sql against any PostgreSQL 14+ instance:

psql $GUARDIAN_DB_URL -f init.sql

If you're deploying alongside LegionForge, Guardian uses LegionForge's existing tables. Running init.sql against a LegionForge database is safe — all statements use CREATE TABLE IF NOT EXISTS.


Architecture

Your Agent Framework
        │
        ▼  (before every tool call)
┌───────────────────┐
│   Guardian /check  │  ← FastAPI, localhost:9766
│                   │
│  Check 0: Token   │
│  Check 1: Registry│
│  Check 2: Caps    │  ← In-memory caches, refreshed every 10s
│  Check 3: Patterns│
│  Check 4: Sequence│
│  Check 5: Hash    │
│  Check 6: Rules   │  ← Hot-reloaded from DB
│                   │
└───────────────────┘
        │
        ▼  (if allowed)
   Tool executes

Design principles:

  • No LLM in the hot path. Every decision is deterministic and auditable.
  • Fail-safe. Network error or timeout → synthetic halt. Guardian failure never silently permits tool execution.
  • Fail-closed. Unknown tools are rejected. Novel sequences are sandboxed. Auth misconfiguration returns 503, not 200.
  • In-memory hot path. Registry + rules cached at startup, refreshed every 10s. DB latency is not on the critical path.
  • Tamper-evident audit log. SHA-256 hash chain on every check result. Verifiable offline.

Why not an LLM-based checker?

An LLM evaluating another LLM's output can itself be prompt-injected. If the agent under evaluation has been compromised, its output can contain instructions that manipulate the checker. Deterministic pattern matching cannot be prompt-injected. The security boundary must be outside the LLM's influence.


Security

See SECURITY.md for the threat model, what Guardian does not protect against, and vulnerability reporting instructions.


Tested Against

Guardian was validated against 24 adversarial attack functions covering: prompt injection, credential probing, SSRF, path traversal, shell injection, privilege escalation, data exfiltration, tool tampering, and sequence contract violations. Result: 0 bypasses on a clean LegionForge deployment.

Reproduce: make test-pentest in the LegionForge monorepo.


License

MIT — see LICENSE.

Part of the LegionForge project. Built by Jp Cruz.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

legionforge_guardian-0.1.0.tar.gz (29.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

legionforge_guardian-0.1.0-py3-none-any.whl (22.7 kB view details)

Uploaded Python 3

File details

Details for the file legionforge_guardian-0.1.0.tar.gz.

File metadata

  • Download URL: legionforge_guardian-0.1.0.tar.gz
  • Upload date:
  • Size: 29.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for legionforge_guardian-0.1.0.tar.gz
Algorithm Hash digest
SHA256 a2da5f34915a56ad1f727a8be93483c22b12996a9aa11349677dd73ffe1becde
MD5 60fbf0d7145c805d83ce5775239c3bc1
BLAKE2b-256 7a0f5d26642b3ad1a2dd54708b4b3c59d3f5151628eaad4080c7621f8010d9c4

See more details on using hashes here.

File details

Details for the file legionforge_guardian-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for legionforge_guardian-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3ec8821c8c26a1b530f1e243f6b12531232efb71a6b6b86b2b2886a2eef301e7
MD5 bdd5dcec8b044bbae8d5e6dd87f1bb5d
BLAKE2b-256 b5ec4735833844206d7d1a118c61a49d920cbd8a2dd4d8524729d1280d881bf2

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page