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")]
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.
- 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. tools/call. The component scope-checks the call against the bound principal, signs a canonical descriptor (Ed25519), and relays the call to the sidecar.- Evaluate. Shield's engine evaluates the real payload against the ruleset, with adaptive scoring (composite weak signals, workspace prod probe, burst detection).
- Decide.
allow→ forwarded to the upstream; the result is returned.block(Critical) → the sidecar answers the JSON-RPC request with error-32099/shield_blockedcarryingrule_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.
- 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.
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).
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.