Protected native hosts
Route a coding host's file tools through the kernel, then scope its connection with an expiring session credential.
On this page
Every file call a coding host sends through its MCP filesystem tool is admitted by the kernel before the wrapped server touches the workspace. The remote MCP edge sits between the host and a caged filesystem server, and an expiring session credential bounds what the host's connection can call.
The edge authenticates the caller, the kernel admits the request, and the wrapped server performs the file operation under the admitted policy. The operator keeps the edge's administrative credential and hands the host only a narrower, revocable one. Govern a coding agent provides the worked filesystem setup.
StartκcapabilityσscopegguardsBbudget
Every check passes, so the kernel invokes the server
Rate limit, origin, bearer, session, and protocol-version gates run at /mcp before the edge accepts the call. The edge resolves the tool against the session’s grant and hands the request to the kernel, which invokes the tool server and signs the receipt before the result returns. The Mediated Call.
Rate limit, origin, bearer, session, and protocol-version gates run at /mcp before the edge accepts a call. The edge resolves the tool against the session's grant and hands the request to the kernel, which checks capability, scope, guards, and budget, invokes the tool server, and signs the receipt. A refused call returns an error result with a signed deny receipt, and the tool server is never called. tools/list answers with the tools the grant covers, and a listed tool carries no authority of its own.
A coding host can reach the same workspace through an MCP filesystem tool, a built-in file editor, a shell, or a browser upload. Routing one of those paths through the edge governs the calls that use it; the rest stay in the deployment's authority inventory. Decide which component owns each effect before configuring the session, and keep the host from answering a refused call by trying the same change through a built-in tool with broader access.
1. Start the edge in front of the filesystem server
chio mcp serve-http wraps a stdio MCP filesystem server behind a kernel-mediated edge. Its signed cage policy pins the wrapped command's absolute path and exact arguments; the edge resolves the command on PATH, canonicalizes it, and refuses to start unless both match.
# Replace every quoted <placeholder>. Both signers are bare 64-character hex Ed25519 public keys.
export CHIO_AUTH_TOKEN='<session-bearer>'
export CHIO_ADMIN_TOKEN='<distinct-admin-bearer>'
chio --session-db /srv/larkfield/edge/sessions.sqlite \
--receipt-db /srv/larkfield/edge/receipts.sqlite \
--authority-db /srv/larkfield/edge/authority.sqlite \
mcp serve-http \
--policy /srv/larkfield/edge/filesystem-policy.yaml \
--server-id workspace \
--signed-manifest /srv/larkfield/edge/workspace-manifest.signed.json \
--manifest-public-key '<manifest-signer-64-hex>' \
--cage-policy /srv/larkfield/edge/workspace-cage-policy.json \
--cage-policy-signer '<cage-policy-signer-64-hex>' \
--resume-hmac-keyring /srv/larkfield/edge/resume-hmac-keyring.json \
--shared-hosted-owner \
--listen 127.0.0.1:8931 \
-- /opt/mcp/bin/mcp-server-filesystem /srv/larkfield/pricing
# The signed cage policy pins this canonical command path and these exact arguments.The edge serves on 127.0.0.1:8931. Once the policy's migration stage is enforced, the kernel launches the server inside a cage that confines it to what the policy grants, whatever the host asks for; stages before enforcement launch it without the cage, as shadow migration describes. The cage runs on Linux, and so does the edge's durable session storage. McpCommands::ServeHttp parses only with the first four inputs below, and the edge starts only with an authentication mode and a signed manifest:
| Input | Supplies | Required |
|---|---|---|
--policy | The tool policy | To parse |
--server-id | The wrapped server's name inside Chio | To parse |
--cage-policy, --cage-policy-signer | Native-launch policy and its pinned signer | To parse |
-- command args | The wrapped MCP server, given last | To parse |
CHIO_AUTH_TOKEN or --auth-token | A static session bearer, one authentication mode | To start |
--signed-manifest, --manifest-public-key | Publisher-signed manifest and its registered key | To start |
--resume-hmac-keyring | A dedicated keyring for durable sessions | With --session-db |
CHIO_ADMIN_TOKEN, --shared-hosted-owner | A distinct admin bearer and one shared wrapped server | For the credential exchange |
2. Exchange the session for an expiring credential
With a session established, the operator exchanges it for a session credential under the wire schema chio.mcp.session-credential.v1, so the host stops presenting its own client credential on every call. The exchange takes an explicit ttlSeconds and a bounded allowedTools list.
pub(super) struct SessionCredential {
schema: String,
token_hash: String,
session_id: String,
subject_key: String,
capability_ids: Vec<String>,
server_id: String,
endpoint_path: String,
auth_mode_fingerprint: String,
policy_fingerprint: String,
allowed_tools: Vec<String>,
issued_at: u64,
expires_at: u64,
}Each listed tool must be covered by an unexpired capability issued to the session's subject for an Invoke operation and must appear in the wrapped server's tool inventory. ttlSeconds runs from 1 to 3600 (MAX_TTL_SECONDS), otherwise HTTP 400, and the credential expires no later than the session's earliest issued capability. Without a static session bearer, a distinct admin bearer, --shared-hosted-owner, durable session storage, and durable admission, the exchange returns HTTP 409.
The returned bearer token is opaque. The operator-owned session database keeps only its hash and the signed record, which pins the session id, subject key, capability ids, server id, endpoint path, and the session's authentication-mode and policy fingerprints; the response carries Cache-Control: no-store. When the token is presented, the edge reloads the record, verifies the kernel's signature, and compares every field and the request's Origin against the caller's current session before accepting the call.
3. Read once, acknowledge the result, and withdraw the credential
Continue an initialized, retained MCP session on the edge from step 1, whose signed grant and discovered inventory cover read_file. The client exchanges a 120-second read-only credential, submits one immutable read, acknowledges the delivered result, withdraws the credential, and checks that redelivery is refused.
Complete session client, including durable evidence and SSE handling
import http.client
import json
from urllib.parse import urlsplit, quote
def review_with_session(base_url, session_id, admin_token, read_arguments,
original_request_id, persist_evidence, origin=None):
"""Use one already initialized, retained session on the configured fs host.
read_arguments is the original admitted read_file argument object.
persist_evidence must durably store each private observation before returning.
Credentials are never printed or included in that evidence.
"""
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 host origin")
def request(method, path, token, body=None, session=False):
cls = http.client.HTTPSConnection if base.scheme == "https" else http.client.HTTPConnection
conn = cls(base.hostname, base.port, timeout=20)
headers = {"Authorization": "Bearer " + token, "Accept": "application/json, text/event-stream"}
if session:
headers["Mcp-Session-Id"] = session_id
if origin is not None:
headers["Origin"] = origin
if body is not None:
headers["Content-Type"] = "application/json"
try:
conn.request(method, path, None if body is None else json.dumps(body), headers)
response = conn.getresponse()
# http.client does not follow redirects to another credential recipient.
if 300 <= response.status < 400:
raise RuntimeError("trusted host redirected the request")
content_type = response.getheader("Content-Type", "").split(";", 1)[0].strip()
if content_type == "text/event-stream":
data = []
expected_id = body.get("id") if isinstance(body, dict) else None
while True:
line = response.readline()
if not line:
raise RuntimeError("stream ended before the original terminal response")
line = line.decode("utf-8").rstrip("\r\n")
if line.startswith("data:"):
data.append(line[5:].lstrip(" "))
elif not line and data:
payload = "\n".join(data); data = []
if not payload.strip():
continue # MCP may send an initial empty SSE event.
event = json.loads(payload)
if event.get("id") == expected_id and ("result" in event or "error" in event):
return response.status, event
raw = response.read()
try:
body = json.loads(raw)
except json.JSONDecodeError:
body = raw.decode("utf-8", errors="replace")
return response.status, body
finally:
conn.close()
admin_path = "/admin/sessions/" + quote(session_id, safe="") + "/credential"
status, issued = request("POST", admin_path, admin_token,
{"ttlSeconds": 120, "allowedTools": ["read_file"]})
if status != 200:
raise RuntimeError("session credential exchange refused: " + str(status))
token = issued["bearerToken"]
original = {"jsonrpc": "2.0", "id": original_request_id, "method": "tools/call",
"params": {"name": "read_file", "arguments": read_arguments,
"_meta": {"chioRequestId": original_request_id}}}
evidence = {"binding": {k:v for k,v in issued.items() if k != "bearerToken"},
"request": original}
status, result = request("POST", "/mcp", token, original, session=True)
evidence["original_status"] = status; evidence["original"] = result
persist_evidence(evidence) # Must return only after private durable storage.
if status != 200 or not isinstance(result, dict) or "error" in result:
raise RuntimeError("original read needs retained-session diagnosis")
# Keep the original result before acknowledging delivery. Acknowledge an
# authenticated terminal tool error too; acknowledgement does not mean success.
acknowledgement = result["result"]["_meta"]["chioDelivery"]
status, acknowledged = request("POST", "/mcp", token,
{"jsonrpc": "2.0", "id": "ack-" + original_request_id, "method": "chio/acknowledge",
"params": acknowledgement}, session=True)
evidence["ack_status"] = status; evidence["acknowledged"] = acknowledged
persist_evidence(evidence)
if status != 200 or not isinstance(acknowledged, dict) or not acknowledged.get("result", {}).get("acknowledged"):
raise RuntimeError("retain original result and acknowledgement for recovery")
status, revoked = request("POST", admin_path + "/revoke", admin_token)
evidence["revoke_status"] = status; evidence["revoked"] = revoked
persist_evidence(evidence)
if status != 200:
raise RuntimeError("revocation not established")
status, rejected = request("POST", "/mcp", token, original, session=True)
evidence["refused_status"] = status; evidence["refusal"] = rejected
persist_evidence(evidence)
assert status == 401, "revoked session credential reached the call path"
return evidenceCall review_with_session with the trusted host origin, the retained session ID, the operator credential, the original read arguments, their retained request ID, and a private durable evidence writer. persist_evidence must finish durable storage before returning, so a failure stops the client before it acknowledges anything. The stable operation key travels in params._meta.chioRequestId, apart from the JSON-RPC correlation ID, and an optional Origin must match the session's original origin. Download the complete client. On a configured host, the protocol returns:
- Exchange. HTTP 200 with a bounded binding and an opaque bearer for one retained session; it mints no capability and grants no admin power.
- Read. The original tool response with a delivery acknowledgement token, returned as a terminal JSON-RPC event over SSE. Tool success or error is read separately from authenticated delivery.
- Acknowledge.
acknowledged: true; the delivery latch advances and no new effect occurs. An authenticated terminal tool error is acknowledged too. - Withdraw and redeliver. HTTP 401, often as plain text; the revoked credential cannot reach the tool again.
You should see each response recorded before the next transition, so a later error still leaves the original result and acknowledgement for recovery. Admin, acknowledgement, and cached responses use JSON. The evidence excludes the bearer token, and the one-use acknowledgement belongs in private caller storage. Authenticate receipt signatures against your independent kernel pin before trusting their claims. If a response is uncertain or lacks a verified acknowledgement, inspect the original credential's status and latch: a new credential cannot reset pending custody.
4. Revoke the credential before it expires
Revocation is an explicit operator action. POST /admin/sessions/{session_id}/credential issues the credential; POST /admin/sessions/{session_id}/credential/revoke and GET /admin/sessions/{session_id}/credential/status are suffixes of that same path. All three require the configured admin bearer before the session id in the path is resolved.
Revoke deletes the session's stored credential row, invalidating any bearer issued for that session regardless of its remaining TTL. Status reports whichever call is latched against the credential, with a fixed recovery instruction to inspect and revoke instead of waiting for automatic unfencing.
5. Launch Hermes in restricted mode
chio_hermes.restricted.prepare() composes three constraints before the Hermes process starts, and refuses to run anywhere but macOS with sandbox-exec installed. It disables Hermes's Python plugin hook and points Hermes at exactly one MCP server, an explicit gateway whose tool allowlist comes from the operator-prepared gateway configuration and must match the session credential's allowedTools, so a tool outside that list is never offered.
It generates a fresh Hermes profile holding only the launcher's local gateway bearer: the model relay's token appears there only as an environment-variable reference, and the kernel session bearer not at all. It checks that the gateway configuration carrying the session credential, the gateway's journal directory, and any Codex auth file sit outside every path the profile may read. Finally it writes the sandbox-exec profile that confines the host and its children.
Acceptance cases every host integration passes
Cases I01 through I08 cover Claude Code, the Codex plugin, Cursor, Hermes, Pi Agent, and OpenClaw. I01 installs documented artifacts into an isolated profile and records the host, kernel, plugin, SDK, OS, and configuration identities. I02 completes a representative useful workflow in a live host session, so a blanket denial fails. I03 attempts a forbidden action through an alternate tool, shell indirection, a descendant process, delegation, or a change to the enforcement configuration, and requires no forbidden effect. I04 requires that an absent, killed, unreachable, or malformed-response kernel, and a failed, crashed, timed-out, or silently skipped plugin hook, still prevent new protected effects.
I05 requires that an expired or revoked capability, the wrong principal, session, or resource, scope escalation, an exhausted budget, or a pending or rejected approval authorizes no effect. I06 binds a decision and any claimed execution result to the caller and request, rejecting a substituted request or result, the wrong signer, or forged evidence. I07 requires that retry, cancellation, resume, parallel calls, and a kernel or host restart preserve the operation's identity and never silently redispatch an unknown outcome. I08 requires that an operator can install, configure, upgrade, recover, and remove the integration from its shipped instructions, with no hidden skips and the tested version combination published.
Claude Code, the Codex plugin, Cursor, and OpenClaw each have a plugin repository: chio-claude-code-plugin, chio-codex-plugin, chio-cursor-plugin, and chio-open-claw-plugin, a hosted Chio edge in Slack, Discord, and Telegram. Hermes's integration lives in the chio-hermes SDK.
A refused session call returns its exact HTTP status and string
Session credential refusals are plain HTTP status codes carrying the string the caller sees; no typed error variant crosses this edge.
| Response | Condition |
|---|---|
HTTP 401 "invalid, expired, or revoked session credential" | Unknown, revoked, or expired token; not a POST or DELETE to the MCP endpoint; no valid session header; or a stored field or Origin differs from the current session |
HTTP 403 "method or tool outside session credential authority" | A method or tool the credential does not allow |
HTTP 400 "restricted calls require a stable chioRequestId" or "invalid restricted JSON-RPC request" | A tool call without params., or a malformed JSON-RPC request |
HTTP 409 "session credential has a pending or uncertain call; operator reconciliation is required" | A previous call not completed and acknowledged, or a retry that changed the request under the same chioRequestId |
HTTP 409 or 410 with the session's lifecycle, for example "MCP session is still initializing" or "MCP session expired and must be re-initialized" | Initializing or draining (409); deleted, expired, or shut down (410) |
HTTP 409 naming a missing prerequisite, for example "session credentials require durable admission" | The edge lacks an exchange prerequisite from step 2 |
HTTP 503 "session credential storage unavailable" | The credential store cannot be opened, read, or written |
For a pending or uncertain call, keep the stable chioRequestId and the original request, and inspect the retained session and operation before retrying; reinitializing a session leaves its prior effect unreconciled. For an expired or revoked credential, use the host's legitimate credential lifecycle and keep the same least-privilege method and tool limits.