Skip to content

Latest commit

 

History

History
106 lines (87 loc) · 4.33 KB

File metadata and controls

106 lines (87 loc) · 4.33 KB

Architecture

The seam

A Langflow Agent normally talks to an MCP server directly. APERION Shield inserts itself as a Streamable HTTP MCP server in front of the real one:

flowchart LR
    subgraph Langflow
      Agent["Agent"]
      Comp["APERION Shield component\n(signed principal + readout)"]
      Comp -->|"guarded tools"| Agent
    end
    Agent -->|"tools/call (Streamable HTTP)"| Shield["aperion-shield\n--http-listen 0.0.0.0:8848"]
    Shield -->|"allow → forward (stdio)"| PG["sandbox Postgres MCP\n(execute_sql)"]
    Shield -->|"block → JSON-RPC error -32099"| Agent
    Shield -. "shield_eval JSONL" .-> Stderr[("stderr / docker logs")]
    Comp -. "signed governed-call JSONL" .-> Gov[("~/.aperion-shield-langflow/\ngoverned-calls.jsonl")]
Loading

Why a sidecar (not embedded)

Shield already ships an HTTP MCP server (--http-listen) and an upstream front (--upstream-url for remote, -- <cmd> for stdio). Reusing it means:

  • Zero Rust changes for launch.
  • Language-agnostic — any MCP client, not just Langflow, can point at the sidecar. That keeps the "neutral guardrail" framing credible.
  • The Python component stays thin: target the sidecar URL, sign the principal, render the readout.

An in-process pyo3 embedding is the documented v1.1 fast-follow (cleaner single-process readout) but adds wheel-build and version-coupling risk, so it is deferred until after launch.

Request lifecycle

  1. Catalog. On first contact Shield hashes each tool's (name, description, input schema) and pins it (trust-on-first-use). A later change to a pinned tool is a rug pull and is blocked. Hidden instructions in a description (tool poisoning) are blocked at pin time.
  2. tools/call. The component scope-checks the call against the bound principal, signs a canonical descriptor (Ed25519), and relays the call to the sidecar.
  3. Evaluate. Shield's engine evaluates the real payload against the ruleset, with adaptive scoring (composite weak signals, workspace prod probe, burst detection).
  4. Decide.
    • allow → forwarded to the upstream; the result is returned.
    • block (Critical) → the sidecar answers the JSON-RPC request with error -32099 / shield_blocked carrying rule_id, severity, reason, safer_alternative. The component turns that into a refusal the agent can read and reason about.
    • warn (Medium) → allowed, audited, bannered.
    • approval (High) → in the engine this waits on a local inbox file; the demo shieldset promotes the DROP TABLE rule to Critical so the launch demo is a clean hard block rather than an on-canvas approval prompt. Approval-on-canvas is a post-launch story.
  5. Audit. Shield emits one {"kind":"shield_eval", ...} JSONL line per evaluation to stderr. The component independently appends one Ed25519-signed line per governed call, bound to the principal.

Wire formats (for integrators)

Block response (top-level JSON-RPC error):

{
  "jsonrpc": "2.0",
  "id": 2,
  "error": {
    "code": -32099,
    "message": "shield_blocked",
    "data": {
      "rule_id": "sql.drop_table_or_schema",
      "severity": "Critical",
      "reason": "DROP TABLE / TRUNCATE TABLE ... is never auto-allowed.",
      "safer_alternative": "Rename to *_to_drop_YYYYMMDD first ...",
      "tool": "execute_sql"
    }
  }
}

Shield eval-audit line (stderr):

{"ts":"...","kind":"shield_eval","tool":"execute_sql","primary_rule_id":"sql.drop_table_or_schema","decision":"block","final_severity":"Critical","matched_rules":["sql.drop_table_or_schema"]}

Component governed-call line (signed):

{"decision":"block","rule_id":"sql.drop_table_or_schema","reason":"...","principal":"langflow-governed-agent","tool":"execute_sql","args_sha256":"...","nonce":"...","ts":"...","public_key":"<base64 ed25519>","signature":"<base64>"}

Verify any line with the embedded public key (RFC 8032 Ed25519 over the canonical {principal,tool,args_sha256,nonce,ts} JSON).

Production vs demo ruleset

The demo loads sidecar/shieldset.demo.yaml — a small, readable subset so a reviewer can see exactly which rule fires, with sql.drop_table_or_schema set to Critical for a deterministic block. Production users omit --rules to get Shield's full bundled set: 45+ rules across 8 destructive surfaces.