Chio/Docs

External Guards

External guards call out to third-party services (cloud content-safety APIs, threat-intel feeds) to inform the kernel's allow or deny decision. The kernel's guard pipeline is synchronous and fail-closed, so every provider call is wrapped in circuit-breaker, cache, rate-limit, and retry machinery. This page is the operator and policy contract for that surface: which Chio types you configure, which failure modes translate to which kernel verdicts, and what evidence lands on receipts.

Stability

The external-guard crates (chio-external-guards, chio-guards::external) are workspace version 0.1.0, unpublished, and evolving alongside the HushSpec canonical pipeline. Treat the policy-compiler tests in crates/chio-cli/src/policy.rs as the executable spec for the YAML shape. This page does not freeze the schema in prose.

Provider Catalog

Six providers ship today. Each implements the async ExternalGuard trait and composes with the same AsyncGuardAdapter infrastructure.

ProviderChio guard typeClassEvidence struct
AWS Bedrock GuardrailsBedrockGuardrailGuardContent safetyBedrockDecisionDetails
Azure AI Content SafetyAzureContentSafetyGuardContent safetyAzureDecisionDetails
Google Vertex AI SafetyVertexSafetyGuardContent safetyVertexDecisionDetails
Google Safe BrowsingSafeBrowsingGuardURL threat intelSafeBrowsingEvidence
VirusTotalVirusTotalGuardURL threat intelVirusTotalEvidence
SnykSnykGuardVulnerabilitySnykEvidence

All six are fail-closed by default: a downstream error produces Verdict::Deny. Advisory mode (return Allow on degraded states) is opt-in per adapter and must be enabled explicitly.


Adapter Architecture

The adapter wrapping an ExternalGuard composes four pieces in a fixed order. The order matters: it determines which failure mode returns which verdict.

text
evaluate(ctx):
    1. CircuitBreaker.allow_call()      -> CircuitOpenVerdict on deny
    2. TtlCache.get(cache_key)          -> cached verdict on hit
    3. TokenBucket.try_acquire()        -> RateLimitedVerdict on empty
    4. retry_with_jitter(inner.eval)    -> Verdict::Deny on permanent failure
                                         -> Verdict on success (also cached)

Key invariants (see crates/chio-guards/src/external/mod.rs):

  • Cache is checked before the token bucket. Cache hits do not spend rate-limit budget.
  • Rate-limited calls do not count as circuit-breaker failures. Only real attempts at the external service do.
  • Permanent errors short-circuit the retry loop. Only Timeout and Transient errors retry and count against the breaker. 4xx and malformed-request errors do not.
  • The final fallback is deny. Any uncaught error path returns Verdict::Deny with a tracing::warn! record.

Fail-Mode Matrix

This is the contract operators should reason about. Each row is one failure mode; the verdict is what the kernel sees.

ConditionDefault verdictConfigurable?
Circuit breaker openDenyCircuitOpenVerdict::Allow opt-in
Rate limiter emptyDenyRateLimitedVerdict::Allow opt-in
Cache hitcached verdictTTL and capacity
Transient error (5xx, reset)retriesRetryConfig backoff and attempts
TimeoutretriesRetryConfig backoff and attempts
Retries exhausted (still retryable)Denyraise max_retries to trade latency
Permanent error (4xx, malformed)Denynot configurable
Provider returns AllowAllow (cached)cache TTL
Provider returns DenyDeny (cached)cache TTL
Tool name does not match scope patternsAllowScopedAsyncGuard tool patterns

Do not enable advisory Allow for last-line-of-defense guards

The advisory Allow modes (CircuitOpenVerdict::Allow, RateLimitedVerdict::Allow) exist for guards whose outputs inform a human-review queue rather than gate an action. If this guard is the last line of defense on a capability, leave the defaults (Deny) in place.

Policy Wiring

External guards are instantiated through the policy-compiler path in crates/chio-cli/src/policy.rs. Authoring lives on the HushSpec-canonical pipeline. The shape is the same for every provider:

  • Provider-specific config block (credentials, endpoint, thresholds).
  • adapter tuning (cache, rate limit, circuit breaker, retry).
  • tool_patterns: wildcard tool-name patterns the guard applies to.

Providers land under two top-level guards groups: cloud_guardrails for content-safety providers and threat_intel for threat-intel feeds. The minimal HushSpec below compiles through build_pipeline_from_external_guard_policy and is tested in crates/chio-cli/src/policy.rs:

policy.yaml
hushspec: "0.1.0"
guards:
  cloud_guardrails:
    azure_content_safety:
      enabled: true
      endpoint: "https://<region>.cognitiveservices.azure.com"
      api_key: "azure-key"
      severity_threshold: 4
      tool_patterns:
        - "slack_*"
      adapter:
        cache_ttl_seconds: 60
        rate_per_second: 20
        rate_burst: 20
        circuit_failure_threshold: 5
        retry_max_retries: 3
  threat_intel:
    safe_browsing:
      enabled: true
      api_key: "sb-key"
      base_url: "https://safebrowsing.googleapis.com/v4"
      tool_patterns:
        - "fetch_url"

The adapter block is shared by every provider and maps to ExternalAdapterPolicyConfig. Fields that are absent fall back to the adapter defaults listed under Operator Tuning.


Endpoint Security (SSRF Posture)

Every external-guard adapter that accepts a configurable endpoint routes it through chio_external_guards::validate_external_guard_url before any HTTP call is issued. The rules:

  • Scheme must be https. The one exception is http://localhost or http://127.0.0.1, permitted to support test doubles.
  • Host must not resolve to loopback, link-local, or RFC1918 private ranges. DNS resolution runs at config-load time by default. A no-DNS variant (validate_external_guard_url_without_dns) exists for environments where DNS is unavailable at config time; it still enforces the scheme and literal-host checks.
  • Validation errors produce ExternalGuardError::Permanent. These surface as adapter-construction failures rather than runtime Deny verdicts. Misconfiguration is caught at policy load, not at traffic time.

Do not reimplement URL validation

Operators should not layer their own URL guard on top. Every provider already calls validate_external_guard_url before issuing a request. Reimplementing it higher up introduces a second source of truth that will drift.

Kernel Bridge

The kernel's Guard trait is synchronous. The bridge is chio_external_guards::ScopedAsyncGuard<E>, which:

  • Wraps an AsyncGuardAdapter<E> in a sync Guard impl.
  • Scopes the guard to a set of wildcard tool-name patterns. Empty patterns mean the guard applies to every tool. A non-matching tool returns Verdict::Allow without calling the external service, and without consuming rate-limit or cache budget.
  • Bridges async to sync by detecting the current Tokio runtime flavor:
    • MultiThread runtime: uses tokio::task::block_in_place.
    • CurrentThread runtime: spawns a fresh current-thread runtime on a scoped thread to avoid deadlocking the caller's executor.
    • No runtime in scope: builds a transient current-thread runtime.

The practical consequence: external guards are safe to use from any of Chio's edges (stdio, Streamable HTTP, Envoy ext-authz) without guessing at the caller's runtime topology. If a runtime flavor is unsupported (for instance, a custom flavor added in a future Tokio release), the guard returns KernelError::GuardDenied with a diagnostic name rather than panicking.


Receipt Evidence

Every provider guard exposes an evidence_from_decision(...) method that returns a structured evidence record. The adapter records this onto the receipt's guard-evidence block on both allow and deny outcomes. The evidence struct is the authoritative surface, and its format is versioned by the provider crate. Do not invent a side-channel for external-guard provenance.

The evidence block preserves at minimum:

  • Guard name and provider identity.
  • Provider decision label (Azure severity, Vertex probability, VirusTotal detection count, and so on).
  • Upstream request correlation identity where the provider returns one.
  • The cache-or-live flag, so auditors can tell a cached decision from a fresh call.

The receipt does not embed provider API response bodies verbatim. Operators who need raw provider telemetry should route that through SIEM export, not the receipt log.

Cache TTL bounds evidence freshness

A cached verdict attests to a decision made within the configured TTL, not a live call. If your compliance posture requires a re-check per call, set cache_ttl_seconds to 0, or override cache_key to return None.

Operator Tuning

Defaults come from AsyncGuardAdapterConfig::default():

KnobDefaultSource
Cache capacity1024 entriesAsyncGuardAdapterConfig::default
Cache TTL60scache_ttl_seconds
Rate limit20 calls/sec, burst 20rate_per_second, rate_burst
Circuit failure threshold5 failures per 60s windowCircuitBreakerConfig::default
Circuit reset timeout30sreset_timeout
Retry max attempts3retry_max_retries
Retry base delay100ms (exponential, jittered)RetryConfig::default

Guidance:

  • Start at defaults. Only widen rate limit or shorten cache TTL if production traffic shows real budget pressure or staleness.
  • Do not tune the circuit-breaker threshold below 3. A noisy network will trip a 1- or 2-failure breaker, and the deny-on-open default will translate into blanket outage behavior.
  • Cache TTL bounds evidence freshness. Set it low, or disable caching, when you need per-call freshness.

Claim Boundary

External guards participate in Chio's fail-closed pipeline but do not upgrade Chio's outward claims about the external service. Per docs/standards/CHIO_BOUNDED_OPERATIONAL_PROFILE.md:

  • An external-guard Allow verdict attests that the provider did not flag the request, not that the provider's judgment is correct.
  • A cached verdict attests to a prior provider decision within the cache TTL, not a live one.
  • A circuit-open Deny attests that the provider was unreachable under the configured breaker policy, not that the request was semantically unsafe.
  • Receipts record the distinction; reports and exports must preserve it.

Do not collapse claim classes

Do not add release-facing language ("content-safety verified", "threat-intel screened") that folds these classes together. A cached Allow, a live Allow, and a circuit-open Deny are distinct attestations. Any stronger claim requires its own qualification lane, not a documentation change.

Next Steps

External Guards · Chio Docs