Chio/Docs
LOGIN · JOIN

Control surfaces

Give agents tool access to a sidecar or remote MCP edge while only operators hold the credentials that change enforcement.

Concept chio@5f63e6e

On this page

Agents can call tools through a sidecar or a remote MCP edge without holding the power to resolve their own approvals, reconcile their own holds, or reissue the authority that grants them work.

Each of those powers sits behind its own bearer credential. A compromised worker that holds the agent credential reaches none of the controls meant to contain it, and Chio refuses to launch, or to answer a request, when two roles share one value. Loopback is a network location, never an administrative identity: other local processes can reach a listening service, so protected routes still demand their role's credential.

Bearer rolesChoose a credential.
session tokenadmin tokencontrol tokenworkload tokenaκσKagentoperatorremote MCP edgechio mcp serve-httptrust servicesession tokenadmin tokencontrol tokenworkload tokenaκσKagentoperatorremote edgetrust service

The operator holds a separate admin token

A bearer-authenticated edge launches only with an explicit --admin-token. Every admin route compares the presented bearer with it in constant time, on every request.

One remote MCP edge with a trust service, and the four bearer credentials that cross between them. The sidecar's control token, session resume binding, and the pinned authority keys are left out.

Each role carries its own value, and launch refuses any overlap

The remote edge accepts session and admin credentials for different operations. When it connects to a trust service, it presents separate control and workload credentials.

RolePresented toOpens
SessionRemote MCP edgeThe agent-facing session
AdminRemote MCP edgeProtected administration routes
ControlTrust serviceControl-plane storage
WorkloadTrust serviceCapability issuance

Every edge needs distinct session and admin credentials, and --control-url adds the control and workload credentials. Provision each role on its own, and keep the distinction through environment files, service units, and remote clients. Service roles maps the four to their operating accounts and preflight checks; the Trust Control Plane applies the same rule with a principal per route.

The sidecar's control routes answer only the control token, even on loopback

The sidecar keeps sidecar_control_token for operator and trusted tool-server work, configured apart from agent credentials. A missing or blank token closes the protected control routes even to loopback; public liveness and the separately authorized /v1/evaluate path stay open.

require_sidecar_control_middleware gates every route the router assembles under it: the approvals routes an operator uses to review and resolve holds, the caller-delivery start, report, and reconcile routes a trusted tool server calls, and the metrics route. The capability mint, release, validate, and attenuate routes and the receipt submission route check the same credential inside their handlers, and sidecar_control_bearer_token_matches compares the presented value in constant time. The tests unconfigured_control_denies_ipv4_loopback_on_every_route and blank_control_configuration_denies_even_loopback pin the loopback case.

The CLI reads the token from CHIO_SIDECAR_CONTROL_TOKEN, falling back to CHIO_API_PROTECT_CONTROL_TOKEN.

In Rust: the full sidecar configuration declaration
crates/products/chio-api-protect/src/proxy/config.rs11–70rust
pub struct ProtectConfig {
    /// Upstream URL to proxy to.
    pub upstream: String,
    /// Optional in-memory OpenAPI spec content (YAML or JSON).
    pub spec_content: Option<String>,
    /// Optional OpenAPI spec path. When omitted, the proxy auto-discovers the spec.
    pub spec_path: Option<String>,
    /// Address to listen on (e.g., "127.0.0.1:9090").
    pub listen_addr: String,
    /// Optional SQLite path for receipt persistence.
    pub receipt_db: Option<String>,
    /// Explicit opt-in to run without a durable receipt store. A durable audit
    /// log is the product's core promise, so a `None` `receipt_db` is refused at
    /// startup unless this is set, mirroring the CLI boot gate. When set, the
    /// proxy runs with in-memory receipts and revocations that are lost on every
    /// restart. Leave it `false` for durable-by-default embedding.
    pub allow_ephemeral_receipts: bool,
    /// Bearer token required for all sidecar control requests, including loopback.
    /// Missing or blank configuration disables control routes without disabling
    /// public liveness or the independently authorized proxy path. Keep this
    /// operator/tool-server credential separate from agent credentials.
    /// Nonempty values must use the RFC 6750 bearer-token alphabet and be at most
    /// 512 bytes after trimming; invalid configuration rejects before startup I/O.
    /// Proxy routes reject this token's bytes in any header value before egress.
    pub sidecar_control_token: Option<String>,
    /// Optional seed used to keep the sidecar signer stable across restarts.
    pub signer_seed_hex: Option<String>,
    /// Explicit capability issuers trusted by the HTTP authority.
    pub trusted_capability_issuers: Vec<PublicKey>,
    /// Control-plane URL. When set, budget holds go through a `RemoteBudgetStore`.
    pub control_url: Option<String>,
    /// Bearer token for the control-plane budget endpoints.
    pub control_token: Option<String>,
    /// Local SQLite budget-store path used when no `control_url` is configured.
    pub budget_db: Option<String>,
    /// Optional durable SQLite revocation-store path. When set, the sidecar
    /// loads its revoked capability ids at startup so operator revocations
    /// recorded through `chio trust revoke --revocation-db <path>` are enforced
    /// on `/v1/evaluate` and every other path that consults the revoked set.
    /// Opening or reading a configured store that fails is fatal (fail-closed):
    /// the sidecar refuses to start rather than run without the revocations it
    /// was told to enforce.
    pub revocation_db: Option<String>,
    /// Retained for API compatibility. The kernel-mediated `/v1/evaluate`
    /// route is a pre-execution authorization gate and always runs the
    /// mediation kernel in execution-nonce strict mode, so this flag no
    /// longer changes mediation behavior.
    pub require_nonce: bool,
    /// When true, the `/v1/evaluate/advisory` route is active.
    /// Defaults to false; production deployments should leave this off to
    /// prevent agents from receiving advisory receipts and bypassing the
    /// kernel-mediated route.
    pub allow_advisory: bool,
    /// Wall-clock ceiling on a single upstream proxy hop, including reading the
    /// full response body. Raise it for upstreams with legitimately slow calls or
    /// large bounded responses; the serve site widens its drain window to match so
    /// an in-flight hop is still receipted before shutdown force-closes it.
    /// Defaults to [`DEFAULT_UPSTREAM_REQUEST_TIMEOUT`].
    pub upstream_request_timeout: Duration,
}

A remote edge launches only with an admin token of its own

A bearer-authenticated remote MCP edge needs an explicit --admin-token, or CHIO_ADMIN_TOKEN, before admin_bearer_token lets it launch, and it refuses a value that repeats --auth-token, the session credential clients present. Each admin request then passes validate_admin_auth, which compares the presented token in constant time.

Session authority stays pinned to the operator's keys on every refresh

When a remote edge configures a trust service, build_pinned_remote_capability_authority builds the authority each session issues capabilities against, pinned to the current and trusted signing keys the operator sets with --control-authority-public-key and --control-authority-trusted-public-keys.

The trust service's reported current key and trusted set must equal those pins when the authority is built and on every refresh. A mismatch refuses to build the session, and a refresh that reports one is never installed. The edge authenticates with the workload token, --remote-authority-workload-token or CHIO_REMOTE_AUTHORITY_WORKLOAD_TOKEN, kept apart from the control token used for storage and from the session and admin tokens.

A resumed session must match the transport, origin, and identity it began with

Resuming a session authenticates it again. SessionAuthContext records the transport, method, and origin a session was authenticated under, and validate_session_auth_context compares it with the current request: transport and origin first, then the authenticated identity the method carries, such as its principal and credential fingerprint.

The persisted session record folds the same context into its resume-integrity tag, an HMAC over the full record that is checked before the record is read back. Neither the transport binding nor any other field can change between persistence and resumption without failing that check.

The operator reads the approval queue, and the agent's token gets 403

On a configured sidecar with its distinct CHIO_SIDECAR_CONTROL_TOKEN, this read-only client sends the same queue request under each role and keeps both response bodies. It never prints a credential or resolves an approval.

security-host/control_roles.pypython
import http.client
import json
from urllib.parse import urlsplit


def compare_control_roles(base_url, control_token, agent_token):
    """Read an existing sidecar approval queue using independently configured roles.
    No approval is issued or resolved. Never print either bearer credential.
    """
    if not control_token or not agent_token or control_token == agent_token:
        raise ValueError("operator and agent credentials must be distinct")
    base = urlsplit(base_url)
    if base.scheme not in ("http", "https") or base.path not in ("", "/") or base.query or base.fragment:
        raise ValueError("supply the exact trusted sidecar origin")

    def queue(token):
        cls = http.client.HTTPSConnection if base.scheme == "https" else http.client.HTTPConnection
        conn = cls(base.hostname, base.port, timeout=20)
        try:
            conn.request("GET", "/approvals/pending", headers={
                "Authorization": "Bearer " + token, "Accept": "application/json"})
            response = conn.getresponse()
            return response.status, json.loads(response.read())
        finally:
            conn.close()

    allowed_status, queue_result = queue(control_token)
    assert allowed_status == 200, "configured control client could not read the queue"
    denied_status, refusal = queue(agent_token)
    assert denied_status == 403 and refusal["error"] == "chio_control_forbidden"
    return {"operator_status": allowed_status, "queue": queue_result,
            "agent_status": denied_status, "refusal": refusal}

Download the client and call it with your exact sidecar origin and two independently provisioned credentials. You should see HTTP 200 with the queue for the operator and HTTP 403 chio_control_forbidden for the agent. The production-router tests cover configured, missing, malformed, and duplicate bearer values before request parsing; duplicate Authorization values and a wrong token get the same 403 before any handler or store is touched.

Role collisions stop at launch, and wrong bearers stop at the route

A configuration the remote edge refuses prints the standard four-line error to stderr, with a help line naming the flag to change, and exits 1 before the edge serves a request. A request the sidecar or edge refuses gets the status and body below.

RefusalCause
HTTP 403 chio_control_forbidden
Sidecar control token missing, wrong, or not configured
error[CHIO-CLI-AUTH-REQUIRED]: a bearer-authenticated remote MCP edge needs --admin-token; the admin routes never run on the session credential
Edge launched without an admin token
error[CHIO-CLI-FLAG-CONFLICT]: --auth-token and --admin-token must be distinct bearer credentials
Session and admin tokens share a value
error[CHIO-CLI-FLAG-CONFLICT]: --control-token and --auth-token must be distinct bearer credentials, or the same line naming --admin-token
Control token repeats the session or admin token
error[CHIO-CLI-FLAG-CONFLICT]: the remote authority workload token must be distinct from the service, session, and admin tokens
Workload token repeats another role
error[CHIO-CLI-AUTH-REQUIRED]: remote MCP control-plane storage needs --control-token, or error[CHIO-CLI-AUTH-REQUIRED]: remote MCP capability issuance needs --remote-authority-workload-token
--control-url set without the control or workload token
HTTP 500 on initialize, body {"error":"urn:chio:error:trust:pin-mismatch [trust:error]: the remote capability authority's current key does not match the operator pin (pin the authority's current key with chio trust pin issuer <KEY>, or restore the pinned key on the trust service)"}, or the same body for a trusted key set that does not match the operator pins
Trust service keys differ from the operator pins; restoring such a session refuses launch with the matching stderr line
HTTP 401 "missing or invalid admin bearer token"
Admin route given a non-admin bearer; no bearer at all gets "missing or invalid bearer token"
HTTP 403 "authenticated transport does not match session"
Session presented over a different transport
HTTP 403 "authenticated origin does not match session"
Session presented from a different origin
HTTP 403 "authenticated authorization context does not match session"
Session resumed under another principal or credential

Fix the role configuration at the service that refused: give session, admin, control, and workload roles distinct values, and confirm the independently retained authority key before changing a pin. A session credential never stands in for a missing operator credential. The MCP edge and node admin show where each credential is set on a running node.

Next: map each credential to its operating account