Execution security
Carry a permitted request to the component that performs it, and keep the state that recovers its result.
On this page
An operator can hand an agent's request to the component that performs it and still know, after any interruption, who authorized it, which operation it belongs to, and what may come back.
The kernel admits the request and retains its commitments. The host connects that decision to the tool. The resource or provider performs the effect, and output guards decide what returns. One program may play several of these roles; the boundaries between them stay distinct. The reviewer's read of src/pricing.py in the pricing project follows one call across all of them.
Four boundaries each settle one fact about a call
A call crosses admission, capture, launch, and release in that order. Each boundary establishes its own fact and leaves the next one to a separate observation.
Admission settles the exact request
The host authenticates the connection and selects the caller context. The kernel evaluates the exact request against current authority, policy, information-flow restrictions, and accounting, and signs the decision.
Still observed separately: that the intended tool launched and its resource path was constrained.
- Admission. The host authenticates the connection and selects the caller context. The kernel evaluates the exact request against current authority, policy, information-flow restrictions, and accounting. Whether the intended tool launched, with its resource path constrained, is observed at launch.
- Durable capture. Before dispatch, the operation commits its participating authority: the budget, invocation, approval, nonce, and security participants, bound to the original request and its executor. Completion of the external effect is the executor's fact to report.
- Enforced native launch. For a separate native tool, the parent observes the required restrictions and the target's execution transition before reporting
FullyEnforced. That record covers the executable and its resources; the application result of one call is observed at release. - Guarded release. The raw result returns to the host, passes the output guards and, when required, the original operation's release authority, and only then becomes visible to the caller. Where a flow authority is installed, response classification joins retained restrictions before release, so a completed external effect can coexist with a withheld response. Receipt by the caller is a delivery question, recorded separately.
The retained request binds the arguments and the selected execution path. Rebuilding arguments from mutable application state after admission breaks that binding. A replacement host keeps the original identity and reconciles its retained state under the new serving fence; an old receipt or a successful state lookup grants no permission to dispatch again.
The worker supplies a path and a key; the host supplies the identity
The execution lab contains a trusted Rust host, an authenticated worker client, and a workspace connector that runs inside the host over a copy of the pricing project. The reviewer asks to read src/pricing.py. It holds a connection credential; the host keeps the signing keys and the grant, and reconstructs the tool request itself.
let runtime = ProcessRuntime::open(state.join("process.db"), Arc::new(kernel))?
.with_security_profile(ProcessSecurityProfile {
tenant_id: "larkfield".into(),
isolation_epoch_id: "review-epoch-1".into(),
generation: 1,
})?;An agent process supplies the durable caller identity. The host fixes the tenant and isolation generation, and the runtime derives the principal and lineage from the retained process and capability after authenticating the worker. A principal field added to the tool arguments changes nothing.
The process client carries the logical operation key separately from the request's correlation ID, and the host freezes the request associated with that key. A repeated key with different arguments conflicts; a repeated original request resolves the same retained call. The worker cannot replace the issuer, select another process, or reopen the task under a fresh grant, so after a restart the same key resolves the original read even if the file has changed.
Read more in Native secure invocation, which walks through the host constructor and the worker's invocation.
A restart resumes at the last boundary the operation crossed
On restart the host reopens its issuer and subject keys, the original capability, the receipt store, revocations, accounting, and the admission store. It registers the connector and reconciles durable admissions before serving another worker, and a second host cannot take ownership of the same state directory.
no outcome retained, not an eligible read: unknown effectful outcome. Keep the original identity; reconcile with the effect owner or operator. The kernel refuses to dispatch it again. A fresh key would be a new intent; it cannot resolve this one.
admission_operation.rs:167pub enum AdmissionOperationState {
// …
DispatchCommitted,
Finalizing,
Completed,
CompensatedBeforeDispatch,
NotAcceptedAfterDispatchCommit,
OutcomeUnknownAfterDispatch,
// …
}
Before capture, recovery reconciles the reservations the original operation owns, and a request saved but never dispatched is submitted again under its original key. A refused request is fixed and sent as a new intent under a new key. An operation with dispatch committed and no retained outcome keeps its accounting and is resolved through the selected executor's attempt by its original key: invoke_known_only keeps an unknown outcome from becoming a fresh attempt, and only a read-only tool without request-bound, constrained, or budget state may take a bounded next attempt. A retained result is repaired through its guarded return and replayed; a valid release checkpoint permits that exact guarded response and records nothing about delivery.
First client: process response was truncated; the operation may have committed
Original-key replay: OutcomeUnknownAfterDispatch
Resource journal: 2 reads total; no third read during recoveryIn this capture one read had completed. A second reached the connector's read journal, then the host was killed before the response returned. Startup reconciled that operation, and replay returned a signed refusal naming its retained unknown state, which stops further dispatch under that request while the interrupted read stays in the retained history. Both operations ended with dispatch state terminal and different operation states, completed and outcome_unknown_after_dispatch, so read the operation state and retained outcome together. Report progress from that state: an unknown provider outcome is neither a policy denial nor evidence that nothing happened.
Read more in Dispatch commitments, which maps each state to what an application tells its user.
A native tool reports confinement only after its launch is observed
When the tool runs as a separate native process, admission connects to a verified launch. The live manifest registry binds the publisher's signed manifest, the server and tool identity, the runtime topology, and the operator's launch policy, which sets the resource ceiling.
The receipt reads confined because the launch record is bound at dispatch
The publisher's signed registry and the operator's resource ceiling meet at admission, and the host retains the exact helper, target, and resource descriptors. The parent observes the target exec under the compiled restrictions and records a FullyEnforced launch record. At dispatch the kernel binds that record into the receipt as native_launch; a verifier checks its digest and signer against pinned kernel keys. Verified tool launch.
Preparing a launch retains the helper, the exact target, and its descriptors. On the Linux cage, the parent observes the required restrictions and the target's exec before reporting FullyEnforced, and the kernel binds that native_launch record into the receipt at dispatch, verified against pinned kernel keys. A call with no launch reference reads "not confined by Chio" with its tool_origin reason. A record short of FullyEnforced, or a target that exits before dispatch, fails dispatch closed: the host reaps the child and never relaunches the tool unrestricted.
On the installed Linux reference runtime, the edge unit runs enforcement preflight before it starts. Its findings name four probes:
| Probe | Checks | On failure |
|---|---|---|
security.platform | The host can enforce the cage mechanisms | Move to a supported host |
security.bearer_roles | Authentication roles are present and distinct | Repair credential delivery and role assignment |
security.native_launch | Signed inputs and pinned signers match this server and command | Check manifest, launch policy, stage, target, and arguments together |
security.durable_stores | Retained stores meet their requirements | Diagnose the named store; keep authority history |
Parsing a policy cannot supply missing enforcement, and a platform-only report says nothing about this launch. A Shadow or Disabled launch that passes without --require-enforcement establishes no containment. The complete preflight invocation uses the edge's credential environment, independent manifest and cage-policy signer pins, session store, and exact wrapped command.
Inspect the service result, or run the complete preflight on its own
sudo systemctl start chio-mcp-edge.service
systemctl show chio-mcp-edge.service --property=ActiveState,SubState,Result
journalctl -u chio-mcp-edge.service -b --no-pagerTo run the preflight independently of service startup, set CHIO_MANIFEST_PUBLIC_KEY and CHIO_CAGE_POLICY_SIGNER from the independently reviewed provisioning record. The systemd credentials supply the same four distinct bearer roles as the edge, and the command preserves the exact reader target and the pricing project root.
sudo systemd-run --wait --pipe --collect \
-p LoadCredential=session-token:/etc/chio/credentials/edge-session-token \
-p LoadCredential=admin-token:/etc/chio/credentials/edge-admin-token \
-p LoadCredential=control-token:/etc/chio/credentials/edge-control-token \
-p LoadCredential=workload-token:/etc/chio/credentials/edge-workload-token \
-p User=chio-edge \
/usr/local/bin/chio security supervise --exec \
--credential-env CHIO_AUTH_TOKEN=session-token \
--credential-env CHIO_ADMIN_TOKEN=admin-token \
--credential-env CHIO_CONTROL_TOKEN=control-token \
--credential-env CHIO_REMOTE_AUTHORITY_WORKLOAD_TOKEN=workload-token \
-- /usr/local/bin/chio --session-db /var/lib/chio-mcp-edge/sessions.sqlite3 \
security preflight --require-enforcement \
--signed-manifest /etc/chio/mcp-edge/launch/signed-manifest.json \
--manifest-public-key "$CHIO_MANIFEST_PUBLIC_KEY" \
--cage-policy /etc/chio/mcp-edge/launch/cage-launch-policy.json \
--cage-policy-signer "$CHIO_CAGE_POLICY_SIGNER" \
--server-id workspace \
-- /usr/local/libexec/chio/chio-tool-repo-reader --root /srv/larkfield/pricingThe reviewer reads src/pricing.py once, even across a restart
Pull the execution lab. It builds against the published Chio crates with Rust 1.94.1 or newer, and the chio commands come from the installed CLI. The project holds the complete host, the worker client, the lockfile, and a copy of the pricing project under repository/.
Prepare the example commands
Build the lab once, then put its binaries on the command path. Repeat the export in each new terminal opened in security-execution/.
chio new security-execution --example security-execution
cd security-execution
cargo build --locked --bins
export PATH="$PWD/target/debug:$PATH"Start the host in the first prepared terminal. It retains its state in .chio/ and prints readiness after publishing the worker connection.
security-execution-host .chio repositoryreview read-pricing-v1 src/pricing.py read-pricing.receipt.json \
< .chio/connection.json
chio receipt verify --input read-pricing.receipt.json \
--trusted-kernel-pubkey .chio/kernel.pub{
"bytes": 97,
"path": "src/pricing.py",
"sha256": "effbf40757e3cd23d8e72b86b8b515cbbb3e975344cc985c68308445b67ac421",
"text": "TIER_PERCENT = 10\n\n\ndef discount(price):\n return round(price * (100 - TIER_PERCENT) / 100, 2)\n"
}You should see discount() with its 10 percent tier, the file's byte length, and its SHA-256 digest. The worker saves the original receipt bytes before interpreting that value, and the verifier checks the signature against the host's separately retained public key; displaying or explaining a receipt checks nothing.
chio --receipt-db .chio/receipts.db receipt list \
--admin-all --tool-server workspace --limit 5
jq '
.metadata.admission_operation | {
projected_state, projected_dispatch_state, projected_operation_version,
retained_dispatch_commit
}
' read-pricing.receipt.json{
"projected_state": "completed",
"projected_dispatch_state": "terminal",
"projected_operation_version": 8,
"retained_dispatch_commit": {
"committed_version": 6,
"provider_attempt": {
"transport_id": "kernel-tool-server:workspace"
}
}
}The dispatch committed at version 6 and the completed operation was projected at version 8, under the executor binding kernel-tool-server:workspace. Both version numbers are observations from one run. The excerpt selects fields from the signed receipt; verify the complete receipt, which also retains the operation ID, request binding, provider-attempt identity, and serving fence. --admin-all reads this host's whole store; a tenant-scoped deployment selects its authenticated read boundary instead.
Stop the host with Ctrl-C and start it again with the same command; it publishes a new connection credential for the retained process. Then run the original call again.
review read-pricing-v1 src/pricing.py read-pricing.receipt.json \
< .chio/connection.json
cat .chio/resource-reads.jsonlYou should see the same receipt, byte for byte, and no second read in the journal. A new revision of src/pricing.py leaves the result for read-pricing-v1 unchanged; choose a new key only for new work.
The archive's interruption instructions add an operator-controlled pause after the source read and before the connector returns; preserve the state if it leaves an unknown outcome. A read shows a retained result surviving host replacement; deduplicating a mutating provider operation is the provider's contract. For an embedded integration, observe_dispatch.rs reads dispatch_state() and the commit's committed_version around a call as advisory snapshots: an unavailable observation stays an observation error, and neither read acquires mutation or dispatch authority.
A path outside src/ stops before the connector opens it
The host grants the reviewer only workspace/read_file under src/, so the project's README.md one level up is out of scope. The connector opens path components relative to a directory it already holds, refuses symlinks and parent traversal, and bounds the returned file to 64 KiB.
for (index, component) in components.iter().enumerate() {
let Component::Normal(name) = component else {
anyhow::bail!("relative normal path components required")
};
let name = CString::new(name.as_encoded_bytes())?;
let final_component = index + 1 == components.len();
let flags = libc::O_RDONLY
| libc::O_CLOEXEC
| libc::O_NOFOLLOW
| libc::O_NONBLOCK
| if final_component {
0
} else {
libc::O_DIRECTORY
};
// Each component is opened relative to an already held directory.
// O_NOFOLLOW excludes symlink substitution at every step.
let fd = unsafe { libc::openat(current.as_raw_fd(), name.as_ptr(), flags) };
ensure!(
fd >= 0,
"open repository component: {}",
std::io::Error::last_os_error()
);
current = unsafe { File::from_raw_fd(fd) };
}Each openat starts from the held directory descriptor, and O_NOFOLLOW refuses a substituted symlink at every component, so a permitted-looking src/link.rs cannot redirect the read. The connector also checks the file type and response size. These checks complement the grant; isolating arbitrary worker code from the rest of the Unix account is the host's job, through confinement.
review read-parent-v1 ../README.md refused.receipt.json \
< .chio/connection.json
cat .chio/resource-reads.jsonlYou should see a signed denial and an unchanged journal. The connector records an entry only after reading a file, which gives an observation independent of the verdict: a denial accompanied by another read would expose a broken integration. The journal is a diagnostic record, separate from signed execution evidence.
The complete scope selected by this host
fn scope() -> ChioScope {
ChioScope {
grants: vec![ToolGrant {
server_id: "workspace".into(),
tool_name: "read_file".into(),
operations: vec![Operation::Invoke],
constraints: vec![Constraint::PathPrefix("src/".into())],
max_invocations: None,
max_cost_per_invocation: None,
max_total_cost: None,
dpop_required: None,
}],
..Default::default()
}
}Every route to the resource passes through one entry point
A resource is governed only when every path from the agent to it crosses the kernel. A writable mount, a separate native tool, or an inherited credential each opens a route that skips it.
Every route from the agent to the repository crosses the kernel, so admission is mandatory.
In Rust · lib.rs:25913 lines
/// Invoke a bridged tool. A dispatcher is required so the kernel cannot
/// sign successful receipts for simulated side effects.
pub fn invoke_tool(&self, tool_name: &str, arguments: Value) -> Result<Value, BridgeError> {
// …
if let Some(dispatcher) = &self.dispatcher {
// … egress contract checks
let response = dispatcher(&binding.method, &url, &arguments)?;
// …
} else {
Err(BridgeError::Kernel(
"OpenAPI bridge requires a dispatcher for live tool invocation".to_string(),
))
}
A credential broker closes the third route. The tool presents a reference and a constrained signed request; the broker resolves the secret, contacts the provider, and retains the bounded outcome. Local launch evidence identifies the executable and its resource contract, while a remote provider owns its final behavior: a lost provider response may need the provider's authenticated status or idempotency contract, and neither a successful socket write nor a copied response proves remote completion. Choose the entry point that owns your effect:
- Native secure invocation when you embed the kernel and implement a connector.
- Caller delivery when a pinned external executor performs the effect.
- Protected native hosts and control surfaces when a coding host runs against protected resources.
- Tool launch, confinement, and resource limits when you launch a separate native tool.
- Dispatch commitments, unknown outcomes, and recovery when you reconcile interrupted work.
Read more in Bypass resistance before adding a shell, an alternate SDK, a credential mount, or a delegated executor.