Native secure invocation
Register your connector with the kernel so one governed lifecycle admits, invokes, and guards each tool call.
On this page
When the embedding host registers its connector with the kernel, admission, invocation, and output guarding become one lifecycle. The kernel admits the exact request, commits the operation, invokes the connector, and guards what comes back; the application consumes one completed result.
The application supplies the connector and authenticates the caller. Keys, grants, accounting, and operation state outlive the serving process; worker connections belong to it. Keeping the two lifetimes apart lets a replacement host authenticate a new connection and still resolve the original work.
The kernel owns the call from admission to the returned value
Admission and invocation are parts of one governed lifecycle. An allowed verdict is never an instruction for the application to perform the call a second time.
That ownership removes handoffs the host would otherwise reconcile, and it makes connector registration a trusted integration step. The connector implements the declared operation, uses the selected resource boundary, and returns the result it observed. Calling the same underlying tool elsewhere in the application opens another effect path that needs its own review.
The connector here runs in the host's process. A separate program needs its own launch contract, and untrusted tool code pairs this lifecycle with tool launch and confinement.
The host builds durable authority before it accepts a connection
The repository application from Execution is a complete embedding: a Rust host, an authenticated worker, and an in-process file connector. Download its source, or read kernel.rs beside the excerpts. The root grant permits workspace/read_file under src/.
kernel.set_receipt_store(Box::new(SqliteReceiptStore::open(
state.join("receipts.db"),
)?))?;
kernel.set_revocation_store(Box::new(authority.revocation_store()));
kernel.set_budget_store(Box::new(authority.budget_store()));
kernel.set_durable_admission_store(
Arc::new(authority.admission_operation_store()),
Arc::new(authority.tool_outcome_store()),
authority.mutation_fence(),
)?;
kernel.configure_durable_admission(DurableAdmissionMode::All, false)?;Revocation, budgets, admission operations, and tool outcomes come from one serving authority, and admission and outcome mutations use its fence. The receipt log is persistent: with ephemeral stores disabled, missing configuration is an integration error that loses no history. DurableAdmissionMode::All puts every tool invocation in this application through durable admission.
Before this, the host reopens its retained issuer, which signs receipts and the root capability, and the subject that identifies the worker's grant. open_serving obtains the authority store's serving handle while the application holds an exclusive lock on the state directory, so a second owner or a different repository binding is refused. A restart keeps the original grant and its expiry; opening state never issues a new one.
Reopen the authority and register the connector
let issuer = key(&state.join("kernel.key"))?;
let subject = key(&state.join("worker.key"))?;
let authority_path = state.join("authority.db");
if !authority_path.exists() {
SqliteAuthorityStore::provision(&authority_path, &locks)?;
}
let authority = SqliteAuthorityStore::open_serving(&authority_path, &locks)?;kernel.register_tool_server(Box::new(Repository::open(repository, state)?));
kernel.reconcile_durable_admission_startup()?;Registration hands the kernel the connector that reads files. Startup reconciliation runs before the host publishes a worker connection; if construction or reconciliation fails, the application returns the error and never reports readiness. Only this configured invocation path reaches the connector, whatever a policy evaluation elsewhere in the application returns.
Authentication selects the process; the worker selects a path and a key
The host chooses the security profile and attaches its retained capability to process reviewer. The host fixes the principal, lineage, tenant, and isolation generation; the worker supplies only a path and an operation key.
fn worker_service(
state: &Path,
kernel: ChioKernel,
capability: &CapabilityToken,
) -> Result<WorkerService> {
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,
})?;
runtime.create_root(
"reviewer",
capability,
ProcessLimits {
max_processes: 1,
max_depth: 0,
max_calls: 16,
state: Default::default(),
},
)?;
let service = WorkerService::new(runtime);
service.revoke_credentials("reviewer")?;
Ok(service)
}Afterward, main.rs checks the original grant's expiry, issues a fresh credential for reviewer, and publishes a private connection descriptor; credentials from the previous run are revoked when the host reopens. A deployment with native information-flow enforcement also installs its native security hook and authority bindings before serving, which adds the capture stages below.
A completed value needs a terminal state and guarded output
The worker reads the connection on standard input and submits its operation key through the process client. The runtime retains the request behind that key: a changed path under it is a conflict, and resolving it after a lost response keeps the same request.
let boot = Bootstrap::<Value>::read(std::io::stdin().lock())?;
let client = ProcessClient::new(boot.connection, Duration::from_secs(30))?;
let response = client
.invoke_known_only(&operation, "workspace", "read_file", json!({"path": path}))
.await?;Before interpreting the result, the worker saves the original signed receipt, comparing bytes if the file already exists. It then separates a completed result from a refusal, a pending approval, or an error.
match &response.verdict {
Verdict::Allow => {
let result = response.completed_value().context(
"allowed request has no completed guarded result; retain the original operation",
)?;
println!("{}", serde_json::to_string_pretty(result)?);
}
Verdict::Deny => {
println!(
"{}",
json!({"status":"denied", "reason":response.reason, "receipt":receipt})
);
std::process::exit(2);
}
Verdict::PendingApproval => {
println!(
"{}",
json!({"status":"awaiting_approval", "receipt":receipt})
);
std::process::exit(3);
}
}completed_value() requires both a completed terminal state and guarded output; Allow on its own fails that contract. A transport or incomplete-result error propagates with the original operation still awaiting resolution. The complete worker adds receipt persistence and command-line handling.
Direct kernel invocation in a synchronous embedding
An embedding that owns authentication itself can call evaluate_tool_call_blocking_with_security_context, or await evaluate_tool_call_with_security_context, with the original ToolCallRequest and a SecurityInvocationContext derived by the host. The native_review.rs adapter preserves the response or error in each branch; it expects an assembled kernel and leaves store installation and caller authentication to the host.
Its Returned variant names an allowed response whose terminal state and guarded output still need inspection, never an instruction to call the connector again. Preserve unresolved errors and inspect the original operation.
With native security installed, capture binds live authority to one transaction
The native security lifecycle commits custody in stages, each a separate durable write, so a later failure leaves the records before it intact. The kernel and the configured adapter retain these records around physical capture.
- Credentials. A credential reservation belongs to one operation and stays subject to current authority. Immediately before capture the kernel checks the live reservation against the immutable request, and the store validates its physical claims, authority, and time window inside the capture transaction. A previously verified record cannot replace the live reservation or authorize a second dispatch.
- Egress. When flow admission carries an egress fence plan,
NativeSecurityEgressAcquisitionV1records the fence the kernel acquired and the digest of that acquisition event, which is neither a lease nor permission to dispatch; the fence may since have expired.NativeSecurityEgressCommitmentV1then commits the fence to the operation's dispatch commitment id, tied to the acquisition digest and any one-shot declassification consumption it joined. A commitment records custody; it is separate from proof of capture or execution. - Return context. Immediately before capture the kernel freezes a
DurableToolReturnContext, one component of the complete caller snapshot and never an execution permit or custody certificate by itself. Its request-material digest excludes transient credentials, so the frozen record cannot stand in for them, and the kernel refuses to freeze one for an operation no longer pending capture. At handoff to dispatch it is revalidated against the current authority selection, revocation status, and emergency-stop state, and the capture must be unexpired and unchanged; output release checks revocation and the stop again before a result reaches the caller. - Capture.
NativeSecurityDispatchCaptureAuthority::captureis a kernel-owned boundary that borrows the live evaluation's budget mutation and credential reservation, and no public constructor accepts a stored record in their place. It marks the reservation for retention if the commit is uncertain, then binds the reservation and policy to one physical transaction. A successful result records that capture happened; it is never a tool permit or authority to reacquire resources. - Dispatch commitment. Once the operation transitions to committed,
AdmissionDispatchCommitBindingV1fixes the committed version, the coordinator's lease id and epoch, the store fence, and any provider attempt, and the dispatch state becomesAdmissionDispatchState::Committed. That binding proves the operation reached a durable dispatch commitment whether or not the host survives to read the response.
The credential reservation and egress commitment in Rust
pub struct VerifiedNativeDispatchCredentials<'a> {
reservation: &'a DispatchCredentialReservation<'a>,
request: &'a ToolCallRequest,
operation: &'a AdmissionOperationV1,
original: &'a RetainedToolAdmissionRequestV1,
grant_index: usize,
runtime: Option<(RuntimeParticipantClaimHistoryV1, RuntimeDispatchValidity)>,
execution_nonce: Option<&'a AdmissionExecutionNonceReservationV1>,
valid_until_unix_ms: u64,
}pub struct NativeSecurityEgressCommitmentV1 {
pub commitment: CommittedEgressFence,
pub event_digest: AdmissionDigest,
pub acquisition_digest: AdmissionDigest,
/// Exact retained consumption, not a fresh one-shot permit or reusable grant.
pub declassification:
Option<chio_security_types::ports::DeclassificationConsumptionEvidenceCommit>,
}Read more in Dispatch commitments, which covers the full state machine and what recovery does with it.
Five requests against the prepared host leave one read in the journal
Complete the Execution command setup once: the security-execution project from chio new security-execution --example security-execution and its lockfile. Start the host, and wait for its readiness record before running the worker in a second terminal.
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
cat .chio/resource-reads.jsonl
review read-parent-v1 ../README.md refused.receipt.json < .chio/connection.json
chio receipt verify --input refused.receipt.json --trusted-kernel-pubkey .chio/kernel.pub
cat .chio/resource-reads.jsonlYou should see the 34-byte source file with its text and SHA-256 digest, a verified original receipt, and one read in the connector's resource journal; the out-of-scope request under a different key adds a signed denial and no read. The captured exercise recorded these outcomes:
| Request | Observed result | Resource journal |
|---|---|---|
Original src/pricing.py read | Exit 0; completed guarded value and original receipt | One file read |
Parent path outside src/ | Exit 2; signed denial | No additional read |
| In-scope path through a symlink | Exit 2; connector refusal | No additional read |
| Different path under the original key | Exit 1; request conflict | No additional read |
| Same request after host restart | Exit 0; byte-identical original receipt | No additional read, even after the source changes |
The connector refuses symlinks through descriptor-relative opens with O_NOFOLLOW and bounds file reads to 64 KiB, because a valid scope must reach the correct physical resource and a string-prefix check alone cannot show that. The chio receipt verify commands use the installed CLI.
Read more in Recover an operation, which reopens this host and separates a replayable result from an interrupted call.
Refusals before and during capture arrive as signed denials with fixed reasons
A refusal reaches the embedding host as a signed, denied ToolCallResponse whose reason is a string. An output classification failure after the tool returns is the one case that arrives as an error value.
| Reason | Condition | Raised by |
|---|---|---|
"guard denied the request: <adapter reason>", for example "guard denied the request: classifier evaluation failed" | At admission, the input cannot be classified, or the authority binding or declassification grant is refused | Native flow adapter ( NativeFlowError in Kernel) |
"durable admission failed: native lifecycle hook is absent", "durable admission failed: native lifecycle security context is absent", or "durable admission failed: native lifecycle callback did not capture original custody" | No hook, no security invocation context, or a hook that returned without capturing; nothing captured | Kernel ( Durable) |
"security dispatch outcome requires reconciliation: security native dispatch capture callback failed; authoritative recovery required" | An adapter check failed before capture began; reversible credentials rolled back | Native flow adapter |
That reason, or "security dispatch outcome requires reconciliation: security native lifecycle handoff callback failed; authoritative recovery required" | Capture failed after it began, or handoff revalidation failed; holds and credentials retained | Kernel ( Durable) |
Err(Kernel, "security dispatch outcome requires reconciliation: security native output preparation callback failed; authoritative recovery required" | The output cannot be classified after the tool returned | Native flow adapter |
The checks that gate capture belong to the control plane's native flow adapter, outside the kernel. Two matter most: the operation's authority binding must match the one the adapter was configured with, and the input taint recorded at admission must agree with the adapter's policy view. The kernel wraps each adapter call in a callback boundary that discards what the adapter returned and substitutes one fixed reason, so an authority mismatch, an unrecorded input taint, an untrusted declassification issuer, changed flow state, a policy denial, a clock check, and a failed egress custody step all reach the host as the same text, with any rollback failure appended. Only a missing security hook or context is named.
The frozen return context and DurableDispatchCommitError are private to the kernel crate, so neither reaches the host as a value. A rejection before capture rolls back the attempt's reversible credentials; once capture has begun, the kernel keeps the original holds and credentials for authoritative recovery. For an admission refusal, inspect the input classification and authority binding. For the fixed capture-callback reason, inspect the retained operation and capture evidence, since that message covers several physical states, and follow unknown outcomes before deciding whether a retry is safe.