Chio/Docs
LOGIN · JOIN

Native secure invocation

Register your connector with the kernel so one governed lifecycle admits, invokes, and guards each tool call.

Concept chio@5f63e6e

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.

Embedded connectorChoose an outcome.
embedding hostpath, keynew connectionvalue, receiptoriginal receiptretainsinvokeopenatsigned denialO_NOFOLLOWconflictaKρdfworkerkernelretained operationconnectorsrc/exit 0 · +1 readembedding hostpath, keynew connectionvalue,receiptoriginal receiptretainsinvokeopenatsigned denialO_NOFOLLOWconflictaKρdfworkerkernelretainedoperationconnectorsrc/exit 0 · +1 read

The kernel invokes the registered connector once

The host derives the caller from its retained process, the kernel admits the read under src/, captures the operation, and invokes the connector it registered at startup. The worker receives the completed, guarded value with the original signed receipt.

read-lib-v1 · src/lib.rs · exit 0 · +1 read

One worker, one embedding host, and the repository connector it registers, with where each observed outcome stops. Paths and keys follow the downloadable Execution example; approvals, budgets, and output classification are left out.

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/.

security-execution/src/kernel.rsrust
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
security-execution/src/kernel.rsrust
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)?;
security-execution/src/kernel.rsrust
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.

security-execution/src/main.rsrust
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.

src/review.rsrust
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.

src/review.rsrust
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.

  1. 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.
  2. Egress. When flow admission carries an egress fence plan, NativeSecurityEgressAcquisitionV1 records 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. NativeSecurityEgressCommitmentV1 then 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.
  3. 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.
  4. Capture. NativeSecurityDispatchCaptureAuthority::capture is 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.
  5. Dispatch commitment. Once the operation transitions to committed, AdmissionDispatchCommitBindingV1 fixes the committed version, the coordinator's lease id and epoch, the store fence, and any provider attempt, and the dispatch state becomes AdmissionDispatchState::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
crates/kernel/chio-kernel/src/kernel/credential_reservation/native_dispatch.rs15–24rust
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,
}
crates/kernel/chio-kernel/src/admission_operation/native_egress.rs58–65rust
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.

Host terminalbash
security-execution-host .chio repository
Terminal 2bash
review 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.jsonl

You 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:

RequestObserved resultResource journal
Original src/pricing.py readExit 0; completed guarded value and original receiptOne file read
Parent path outside src/Exit 2; signed denialNo additional read
In-scope path through a symlinkExit 2; connector refusalNo additional read
Different path under the original keyExit 1; request conflictNo additional read
Same request after host restartExit 0; byte-identical original receiptNo 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.

ReasonConditionRaised 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 KernelError::GuardDenied)
"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 (DurableDispatchCommitError::RejectedBeforeCommit)
"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 (DurableDispatchCommitError::CommitUnconfirmed)
Err(KernelError::SecurityDispatchOutcomeRecoveryRequired), "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.

Next: hand the effect to a separate executor