Skip to content

Repository files navigation

SigilGuard

In-process. OTP-supervised. Deterministic. No sidecar. Signed evidence.

Hex.pm Docs CI Coverage License

Installation · Quick Start · Agent Trust Profile · Agent Trust Gateway · Configuration · Capabilities · Architecture


SigilGuard is an embedded security runtime for MCP v2 and agent-tool boundaries, in native Elixir. MCP specifications are date-versioned; throughout this project, “MCP v2” means the final 2026-07-28 revision. SigilGuard sits between a language model and the tools it can reach, decides whether a tool call, a tool result, or a model output may cross a boundary, and produces signed, tamper-evident evidence of every decision. It runs in-process on the BEAM: no sidecar, no proxy hop, no network call on the decision path.

The problem it addresses is the one every agent deployment eventually hits: a model with access to private data, exposure to untrusted content, and the ability to act or communicate outward is one poisoned tool description or prompt-injected result away from doing real damage. Model-level guardrails help but are probabilistic. SigilGuard is the deterministic layer underneath: signed trust material, capability manifests pinned by digest, source-to-sink policy, and human-in-the-loop confirmation bound to the exact action.

Why embedded

The common answer to MCP security is a proxy, a gateway, or a cloud scanner: a separate service in the request path. That buys latency, an extra operational surface, and a trust boundary of its own. For a team already on Elixir, an in-process library is a better fit.

  • Deterministic core. Policy decisions are code, not a model call. Same inputs, same verdict, every time.
  • Signed evidence, locally. A tamper-evident HMAC and Merkle audit chain, with signed checkpoints, inclusion and consistency proofs, and portable exports, held in your app rather than a vendor's log.
  • No sidecar. OTP-supervised, sub-millisecond on the decision path, and offline by default. Trust material ships with your release.

Capabilities

Capability What it does
Sensitivity scanner Staged detection and redaction of secrets and credentials, with confidence scoring and boundary-aware enrichment.
Boundary policy kernel Deterministic source-to-sink decisions over phase, origin, sink, actor, trust zone, and sandbox identity.
MCP v2 / tool gateway Transport-agnostic guards for MCP 2026-07-28 structured requests, MRTR results, and manifests pinned by digest.
Signed attestations Canonical, DSSE-enveloped statements binding an actor, tool, action, payload, and context to a verdict.
Trust bundles Signed, local trust material — roots, keys, policies, patterns, tool manifests, and revocations — verified offline.
Confirmation tokens Short-lived human-approval grants bound to the exact action, payload, and context, never a fuzzy intent.
Streaming sanitizer Chunk-safe holdback so a secret split across output chunks is never emitted early.
Tamper-evident audit HMAC-linked event chains, Merkle checkpoints with inclusion and consistency proofs, signed exports, and external anchoring.
Agent-to-agent trust Signed agent cards and delegation-chain validation for inter-agent calls.
Vault AES-256-GCM secret storage behind a swappable behaviour (KMS, HSM, external vault).
Telemetry :telemetry events plus OpenTelemetry-style attribute mapping for every decision.

Installation

SigilGuard 1.0 requires Elixir 1.18 or later.

def deps do
  [
    {:sigil_guard, "~> 1.0"}
  ]
end

Quick Start

The core is a single decision function. Give it a payload and the boundary it is crossing; it returns a Decision.

# Block a secret leaving the runtime toward an external sink.
decision =
  SigilGuard.guard("AWS_KEY=AKIAIOSFODNN7EXAMPLE",
    phase: :tool_request,
    origin: :model,
    sink: :external,
    tool: "send_webhook",
    trust_level: :high
  )

:block = decision.action

# Redact before the same content reaches the model.
decision =
  SigilGuard.guard("token=supersecretvalue123",
    phase: :inbound_user,
    origin: :user,
    sink: :model,
    trust_level: :medium
  )

:redact = decision.action
"token=[SECRET]" = decision.sanitized_text

Scanning and policy are also available on their own:

{:ok, "safe text"} = SigilGuard.scan("safe text")
{:hit, _hits} = SigilGuard.scan("AKIAIOSFODNN7EXAMPLE")
"key=[AWS_KEY]" = SigilGuard.scan_and_redact("key=AKIAIOSFODNN7EXAMPLE")

:allowed = SigilGuard.policy_verdict("read_file", :medium)
:blocked = SigilGuard.policy_verdict("delete_database", :low)
{:confirm, _reason} = SigilGuard.policy_verdict("create_user", :low)

The gate is transport-agnostic: an MCP server, an agent loop, or a gateway calls it before tool execution, after tool results, and before outbound writes, without pulling any specific MCP adapter into the core. The MCP gateway, attestation signing, confirmation flow, trust bundles, and audit chain build on this same decision. The architecture covers the full surface.

Agent Trust Profile

The 1.0 release line has one wire profile: sigil_guard_agent_trust/v1. Agent Trust evidence is a DSSE envelope over a JCS-canonical in-toto-style statement, with payload and context digests bound to the boundary decision. The public metadata keys are:

Key Purpose
_agent_trust Carries an Agent Trust attestation envelope for a tool request or result.
_agent_confirmation Carries a short-lived confirmation token for a confirm-required action.
confirmation_token Compatibility-neutral token field for hosts that do not want a SigilGuard-prefixed metadata name.

SigilGuard.Attestation.strip_metadata/1 removes those keys at the payload root and inside params before digest computation, in atom and string forms. All other fields are ordinary user payload.

Agent Trust Gateway

SigilGuard.ToolGateway is the 1.0 entry point for MCP-shaped tool calls and tool results. It combines capability-manifest checks, boundary policy, confirmation tokens, and Agent Trust attestations.

Start with SigilGuard.ToolGateway when your host already owns the request and result maps and wants the full enforcement surface. Use SigilGuard.MCP.Gateway when wiring an MCP transport that wants JSON-RPC-compatible helper names and tuple responses; it is a thin permanent facade over ToolGateway.

Guard a tool request against a pinned manifest:

request = %{
  "method" => "tools/call",
  "params" => %{
    "name" => "repo_file_write",
    "arguments" => %{"path" => "README.md", "content" => "updated"},
    "_meta" => %{
      "io.modelcontextprotocol/protocolVersion" => "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities" => %{}
    }
  }
}

context = [
  actor: "spiffe://agents/editor",
  trust_level: :medium,
  sandbox_id: "sandbox-123",
  isolation_level: :container
]

decision =
  SigilGuard.ToolGateway.guard_request(request, context,
    manifests: %{"repo_file_write" => pinned_manifest},
    require_manifest: true
  )

Attach and require Agent Trust evidence with the _agent_trust metadata key:

{:ok, envelope} =
  SigilGuard.ToolGateway.attest_request(decision, context,
    payload: request,
    signer: MyApp.AgentSigner,
    keyid: "agent-ed25519-1",
    nonce: "unique-request-nonce",
    manifest: pinned_manifest
  )

trusted_request = SigilGuard.Attestation.attach(request, envelope)

verified =
  SigilGuard.ToolGateway.guard_request(trusted_request, context,
    manifests: %{"repo_file_write" => pinned_manifest},
    attestation: :required,
    trust_material: %{"agent-ed25519-1" => agent_public_key}
  )

Confirmation tokens use _agent_confirmation when a decision requires human approval:

{:ok, token} =
  SigilGuard.ToolGateway.issue_confirmation(
    request,
    context,
    decision,
    confirmation_key,
    manifest: pinned_manifest
  )

confirmed_request = SigilGuard.Attestation.attach_confirmation(request, token)

confirmed =
  SigilGuard.ToolGateway.guard_request(confirmed_request, context,
    manifests: %{"repo_file_write" => pinned_manifest},
    confirmation_key: confirmation_key
  )

MCP v2 (2026-07-28) and MCP Apps

SigilGuard does not negotiate MCP or own a transport. A host adapter selects MCP v2 response behavior with protocol_version: "2026-07-28" or supplies the standard per-request protocol-version _meta value. The adapter must also supply and validate the required client-capabilities metadata. Missing per-request fields use -32602; missing declared capabilities and unsupported revisions use MCP's -32021 and -32022 errors. Successful v2 results receive resultType: "complete" when absent and preserve every existing discriminator for host-side validation. Unknown future revisions are not treated as v2. input_required, inputRequests, inputResponses, and requestState remain part of the exact structured confirmation and attestation binding.

Capability manifests use sigil_guard_capability_manifest/v2. The v2 digest also binds optional tool title, icons, normalized MCP Apps UI metadata, and validated x-mcp-header annotations. Header annotations must be on properties-reachable boolean, integer, or string fields; header names are HTTP tokens, case-insensitively unique, and may not expose sensitive parameters. Icon sources must be HTTPS or valid image data URLs. The host adapter owns same-origin credential-free icon fetching, redirect and size limits, content sniffing and safe rendering, complete JSON Schema 2020-12 validation, JavaScript-safe integer checks at runtime, header construction, and MCP HeaderMismatch handling. App-origin tool calls use origin: :app and must name the same trusted mcp_server as an app-visible manifest.

Before a renderer receives an MCP Apps resource, verify its pinned bytes and requested browser capabilities:

{:ok, verified_resource} =
  SigilGuard.MCP.AppResource.verify(resource,
    expected_uri: "ui://repo/review",
    expected_sha256: pinned_ui_sha256,
    max_bytes: 1_048_576,
    allowed_connect_domains: ["https://api.example.com"],
    allowed_permissions: [:clipboardWrite]
  )

Resource verification is capped at 1 MiB by default. Dedicated app domains use the host-defined format and must match :allowed_app_domains exactly. The host remains responsible for server/discover, subscriptions/listen, OAuth, transport headers, HTML5 validation, iframe creation, sandbox attributes, and CSP enforcement. The stable MCP Apps extension predates MCP v2, so the host maps its negotiated UI capability into v2 per-request metadata and discovery; SigilGuard only enforces the resulting boundary metadata.

See Migrating to MCP v2.

Configuration

Stateful features are initialized automatically by SigilGuard.Runtime, which provides stable ownership for replay, rate-limit, and trust-bundle ETS state. The default setup uses application environment:

config :sigil_guard,
  trust_bundle: {:priv, :my_app, "sigil/trust_bundle.json"},
  scanner_patterns: :bundle,
  attestation_ttl_ms: 300_000,
  max_skew_ms: 60_000,
  replay_ttl_ms: 300_000

Hosts that need control over failure placement or want to avoid global configuration can disable automatic startup and supervise the runtime directly:

config :sigil_guard, runtime: false

children = [
  {SigilGuard.Runtime,
   config: [
     runtime: false,
     trust_bundle: {:priv, :my_app, "sigil/trust_bundle.json"},
     scanner_patterns: :bundle,
     attestation_ttl_ms: 300_000,
     max_skew_ms: 60_000,
     replay_ttl_ms: 300_000
   ]}
]

Supervisor.start_link(children, strategy: :one_for_one)

Unknown keys and removed legacy keys fail runtime startup with SigilGuard.ConfigError and a pointer to MIGRATING-1.0.md. Omitting the runtime child's :config option makes it read the :sigil_guard application environment.

Key Default Purpose
:runtime true Start the singleton runtime automatically; set false for caller-owned supervision.
:trust_bundle :none Local trust-bundle source: :none, {:file, path}, {:priv, app, path}, {:map, map}, or {:binary, bytes}.
:scanner_patterns :built_in Pattern source, either :built_in or :bundle; :bundle requires :trust_bundle.
:http_client nil Host-provided module implementing SigilGuard.HTTPClient for audit anchor HTTP stores.
:attestation_ttl_ms 300_000 Attestation lifetime in milliseconds.
:max_skew_ms 60_000 Maximum accepted clock skew in milliseconds.
:replay_ttl_ms 300_000 Replay cache lifetime in milliseconds.
:vault_master_key nil Optional base64-encoded key for SigilGuard.Vault.InMemory.
:trust_mappings [] Ordered {pattern, trust_level} actor mappings; patterns are exact strings or one trailing *.

Configured trust bundles are loaded and verified when SigilGuard.Runtime starts. Verified snapshots are cached for the current BEAM boot in the :sigil_guard_trust_bundle ETS table, along with rollback floors, root pins, rotation digests, and revoked key ids. The signed bundle remains the durable source of truth across boots; remote distribution, if needed, belongs to the host application before bytes are passed to SigilGuard.TrustBundle.load/2. Trust-bundle roles always carry their declared threshold. For compatibility, the 1.0 release line uses an effective threshold of 1 unless verification is called with enforce_declared_threshold: true; root rotation documents always enforce the full declared old-root and new-root thresholds.

Extension Points

Host applications own their transports, auth, storage, and deployment. SigilGuard plugs into them through behaviours:

Behaviour Purpose Typical implementation
SigilGuard.Signer Cryptographic signing HSM, KMS, cloud key management
SigilGuard.Vault Encrypted storage HashiCorp Vault, AWS KMS, database
SigilGuard.Audit.Logger Audit persistence Database, file, external service
SigilGuard.Identity Trust and identity context Your auth system
SigilGuard.HTTPClient Outbound HTTP for anchor stores Req or your own client

Telemetry

SigilGuard emits :telemetry events for scanning, gate decisions, MCP requests, policy verdicts, trust-bundle verification, and audit logging. Each decision event carries the sanitized boundary metadata — phase, actor, origin, sink, tool, trust zone, verdict, and digests — with raw payloads kept out by default. Use SigilGuard.Telemetry.otel_attributes/3 to map them into OpenTelemetry-style attributes under the sigilguard.* namespace.

Development

mix setup            # install dependencies
mix test             # run tests
mix lint             # format + Credo + Dialyzer
mix check            # full quality gate
mix docs             # generate documentation
mix bench            # run benchmarks
mix sigil_guard.sbom --output dist/sigil_guard.spdx.json

Coverage is held at or above 95%, and security modules carry negative, tamper, replay, expiration, and malformed-input tests.

Architecture

The architecture overview maps the component layers, the module topology, and the runtime, MCP, and audit-evidence flows, with diagrams.

References

Contributing

Contributions are welcome. See CONTRIBUTING.md for guidelines.

License

SigilGuard is released under the MIT License. See LICENSE.

About

Native Elixir security runtime for MCP and agent-tool boundaries: policy gating, credential scanning, attestations, trust bundles, and audit trails.

Topics

Resources

Contributing

Security policy

Stars

Watchers

Forks

Releases

Used by

Contributors

Languages