Chio/Docs
LOGIN · JOIN

Response authorization

Authorize one specific containment plan from reviewed policy and artifact snapshots, with independent service identity and bounded execution.

Concept chio@5f63e6e

On this page

Approval of a containment response covers the exact change its plan names, and a later edit to that plan needs its own approval. Automatic and governed responses follow the same rule.

A response plan fixes the finding, the target membership, each contribution, the observed base state, and the duration. Chio binds that complete commitment to the capability and approval needed to apply it, so changing a target, extending expiry, or replacing a contribution makes a different plan even when its display ID stays the same.

Plan commitmentChange one input.
findingaffected setcontributionbase versiondurationexecutor capabilityhplan_hashboundτapprovalKadmissiondadapterfindingaffected setcontributionbase versiondurationexecutor capabilityhplan_hashτapprovalKadmissiondadapter

One hash binds the whole five-minute throttle

The plan binds the finding, session review-1, the contribution and its digest, the observed base version, ttl_ms 300000, and the executor capability. The governed approval and the reservation prepared under it are bound to this plan, and the kernel's active-response admission checks the plan before the adapter applies the effect. Threshold approvals.

One response plan's commitment: six bound inputs, the plan hash, the approval bound to it, the kernel's active-response admission, and the adapter that applies the effect. Policy, reason, submitter, and approval-quorum details are left out.

A plan turns a finding into one bounded, reviewable change

An investigator may weigh several explanations for an event. A responder carries a narrower instruction: change these resources, in this way, for this long, under this authority. A finding converted straight into a broad administrative credential would let the response component choose consequences beyond the reviewed decision.

crates/security/chio-security-types/src/response.rs239–257rust
pub struct ResponsePlanAuthorizationBody {
    pub action_id: ActionId,
    pub trigger_finding_id: RecordId,
    pub trigger_finding_hash: Digest32,
    pub trigger_finding_receipt_id: OpaqueReceiptRef,
    pub tenant_id: TenantId,
    pub policy_version: RecordId,
    pub policy_hash: Digest32,
    pub affected_ids: RecordIdSet,
    pub affected_set_hash: Digest32,
    pub effects: ResponsePlanAuthorizationEffects,
    pub ttl_ms: u64,
    pub created_at_unix_ms: u64,
    pub expires_at_unix_ms: u64,
    pub operator_capability: OperatorCapabilityBinding,
    pub approval_requirement: ResponseApprovalRequirement,
    pub submitter: RecordId,
    pub reason_hash: Digest32,
}

The plan identifies the triggering finding and its receipt, the tenant, the affected set, the policy, the effects, the duration, the operator capability, the approval requirement, the submitter, and a reason commitment. It binds the affected set by hash, so a plan approved for one group of identities cannot expand during execution. Automatic policy can admit a known, bounded plan, and a human reviewer inspects the same commitment before approving it.

Causal scope must be complete for the chosen action. A lineage-scoped effect never binds a guessed set: the state machine refuses an issuance fence whose approved blast-radius result is anything other than an Exact set matching the plan. Truncated lineage stays in investigation, or in a narrower response with its own authority.

Each effect names a typed target, established before review

A session target names a session, a lineage target a lineage, a capability-set target an already resolved affected set by its hash, and a tenant target a tenant. Each effect kind accepts only its own target type, and a tenant target must be the plan's own tenant.

crates/security/chio-security-types/src/response.rs129–134rust
pub enum ResponseTarget {
    Tenant { tenant_id: TenantId },
    Session { session_id: SessionId },
    Lineage { lineage_id: LineageId },
    CapabilitySet { affected_set_hash: Digest32 },
}

For an incident in the fixer's session, start from the identities the verified finding established. A session suspension binds that session and tenant into the plan. Suspending descendants requires resolving the lineage and its affected set under the trusted host context first, through Blast radius, since a list supplied by the requesting agent is no authoritative account of its descendants. The scopes differ, so one target is never swapped for another after review.

Each effect carries its exact contribution and the base version it observed

An effect specification carries the canonical contribution, its digest, and the observed base-version commitment. build_response_plan refuses a contribution body that is not canonical or does not hash to its recorded digest, and the state machine repeats that check on every plan it validates.

crates/security/chio-security-types/src/response.rs154–160rust
pub struct ResponseEffectSpec {
    pub kind: ResponseEffectKind,
    pub target: ResponseTarget,
    pub canonical_contribution: CanonicalBody,
    pub contribution_hash: Digest32,
    pub observed_base_version_hash: Digest32,
}

Build the contribution with the selected adapter's contract, compute its digest, and retain the version observed during planning; a reason or severity value stands in for none of these fields. A conflicting version is reported under the original operation, because rewriting the contribution would apply a plan the authority never reviewed. Each of these changes after authorization needs a fresh decision:

  • Another session or capability. The affected scope expands.
  • A longer duration. The control stays in force longer than reviewed.
  • A suspension in place of an egress restriction. The effect and its operational consequence change.
  • A different policy snapshot. The authority's reviewed basis changes.
  • Reordered or replaced admission artifacts. The canonical artifact bundle retained for the operation changes.

Two plans with the same explanation can request different powers, so compare canonical plan and artifact commitments to decide whether a submission is an exact retry.

A separate authority approves plans from a reviewed snapshot

Plans are approved by a separate daemon that accepts requests only from pinned processes and only under one validated deployment digest. It selects policy and admission artifacts from a reviewed, immutable snapshot, and its decision binds the exact finding and plan to the deployment and store digests the operator approved. Policy, capabilities, approvals, and governance artifacts change only through a new reviewed snapshot and deployment configuration; the authority mints none of them online.

Policy selection returns an opaque admission-artifact reference, and loading it returns the complete signed bundle. The broker retains a write-once canonical bundle digest before kernel preparation, so a changed proof, token order, payload, authority, or reference is equivocation instead of a harmless retry.

Read more in Response authority service, which covers the snapshot, process pinning, configuration, and start-up.

A five-minute throttle binds once, and every change builds a new plan

The fixer's session has a reviewed credential-then-egress finding: a decoy read in /secrets, then egress toward collect.example.net. A proposed five-minute session throttle must bind that finding, the affected session, the contribution bytes, the observed base version, and the executor capability.

Call compare_response_plans() in a Rust host that uses chio-quarantine, chio-core-types, and chio-security-types/std. Its named digests and posture_rank contribution form a structural fixture, as in the builder's contract tests; a live planner obtains the signed finding, the executor grant, and an adapter-ready contribution from their owners.

Rust hostrust
response_plan_cases::compare_response_plans()?;
Output
BUILD bound five-minute session plan: original affected set, contribution and base
NEW BINDING for changed set, duration, contribution or base; REFUSE stale content digest

You should see the original plan build, then a new binding for each change. One more millisecond of duration changes the plan commitment, and so do another affected session, a new observed base version, or a new contribution with its new digest. Changed bytes under the old digest are structurally invalid and build nothing. Each new well-formed plan still needs authority for its own commitment.

The complete response_plan_cases module
security-host/response_plan_cases.rsrust
use chio_core_types::sha256;
use chio_quarantine::{build_response_plan, ApprovalCoordinatorError, ResponseApprovalCoordinator};
use chio_security_types::ports::{
    ActionId, ApprovalVerifierPort, CanonicalBody, Digest32, GovernedApprovalRequest,
    GovernedApprovalReservation, OpaqueReceiptRef, RecordId, SessionId, TenantId,
};
use chio_security_types::{
    OperatorCapabilityBinding, ResponseApprovalRequirement, ResponseEffectKind, ResponseEffectSpec,
    ResponsePlan, ResponsePlanInput, ResponseTarget,
};

fn digest(label: &[u8]) -> Digest32 {
    Digest32::new(*sha256(label).as_bytes())
}

/// Complete structural plan fixture. Named hash inputs are illustrative reviewed
/// bindings, not an attested finding, live operator capability or effect capture.
pub fn compare_response_plans() -> Result<(), Box<dyn std::error::Error>> {
    let contribution = CanonicalBody::new(br#"{"posture_rank":2}"#.to_vec())?;
    let input = ResponsePlanInput {
        action_id: ActionId::new("review-response-1")?,
        trigger_finding_id: RecordId::new("credential-egress-finding")?,
        trigger_finding_hash: digest(b"original finding fixture"),
        trigger_finding_receipt_id: OpaqueReceiptRef::new("original-finding-receipt")?,
        tenant_id: TenantId::new("larkfield")?,
        policy_version: RecordId::new("pricing-v1")?,
        policy_hash: digest(b"reviewed response policy"),
        affected_ids: vec![RecordId::new("review-1")?],
        effects: vec![ResponseEffectSpec {
            kind: ResponseEffectKind::ThrottleSession,
            target: ResponseTarget::Session {
                session_id: SessionId::new("review-1")?,
            },
            contribution_hash: digest(contribution.as_bytes()),
            canonical_contribution: contribution,
            observed_base_version_hash: digest(b"observed throttle base version"),
        }],
        ttl_ms: 300_000,
        created_at_unix_ms: 1_800_000_000_000,
        operator_capability: OperatorCapabilityBinding {
            capability_id: RecordId::new("response-executor-grant")?,
            capability_digest: digest(b"operator capability fixture"),
            expires_at_unix_ms: 1_800_000_600_000,
            executor_subject: RecordId::new("response-executor")?,
        },
        approval_requirement: ResponseApprovalRequirement::Governed {
            policy_id: RecordId::new("reviewed-containment-policy")?,
        },
        submitter: RecordId::new("incident-reviewer")?,
        reason_hash: digest(b"reviewed finding reason"),
    };
    let original = build_response_plan(input.clone())?;
    assert_eq!(
        original.expires_at_unix_ms,
        input.created_at_unix_ms + 300_000
    );
    let mut neighbors = vec![];
    let mut duration = input.clone();
    duration.ttl_ms += 1;
    neighbors.push(duration);
    let mut scope = input.clone();
    scope.affected_ids.push(RecordId::new("another-session")?);
    scope.affected_ids.sort(); // canonical sets require strictly sorted, unique IDs
    neighbors.push(scope);
    let mut base = input.clone();
    base.effects[0].observed_base_version_hash = digest(b"new base");
    neighbors.push(base);
    let mut content = input.clone();
    content.effects[0].canonical_contribution =
        CanonicalBody::new(br#"{"posture_rank":3}"#.to_vec())?;
    // Altered bytes under the old contribution digest cannot build a valid plan.
    assert!(build_response_plan(content.clone()).is_err());
    content.effects[0].contribution_hash =
        digest(content.effects[0].canonical_contribution.as_bytes());
    neighbors.push(content);
    for changed in neighbors {
        assert_ne!(build_response_plan(changed)?.plan_hash, original.plan_hash);
    }
    println!("BUILD bound five-minute session plan: original affected set, contribution and base");
    println!(
        "NEW BINDING for changed set, duration, contribution or base; REFUSE stale content digest"
    );
    Ok(())
}

/// The configured native authority owns signature, replay and capability checks.
/// A returned reservation is prepared authority, not dispatch or an applied control.
pub fn prepare_original<V: ApprovalVerifierPort>(
    coordinator: &ResponseApprovalCoordinator<V>,
    original: &ResponsePlan,
    reviewed_request: &GovernedApprovalRequest,
    now_unix_ms: u64,
) -> Result<GovernedApprovalReservation, ApprovalCoordinatorError> {
    coordinator.prepare(original, reviewed_request, now_unix_ms)
}

pub fn reconstruct_original<V: ApprovalVerifierPort>(
    coordinator: &ResponseApprovalCoordinator<V>,
    original: &ResponsePlan,
    reviewed_request: &GovernedApprovalRequest,
    retained: &GovernedApprovalReservation,
    now_unix_ms: u64,
) -> Result<Option<GovernedApprovalReservation>, ApprovalCoordinatorError> {
    coordinator.reconstruct(original, reviewed_request, retained, now_unix_ms)
}

A changed or expired plan applies no control, and recovery reuses the original

Each stage hands the next one a narrower, retained result:

StageResultStill to establish
Plan builderValidated canonical plan, effect IDs, and hashFinding, capability, and approval verification
Governed prepareThe original bound reservationDispatch, durable commit, and application
ReconstructThe original preparation, or absence or refusalRetained admission and dispatch state
Per-effect resultApplied, failed, or restoration progressExternal target evidence and complete recovery

ResponseApprovalCoordinator checks the plan, request, and reservation bindings at its dependency boundary, and the configured native verifier owns signatures, threshold approval, freshness, replay, and capability authority. Reconstruction compares the original retained preparation and never rebinds it to a changed plan whose body looks similar. Recovery loads the original reservation and signed authorization.

Automatic policy skips governed threshold collection and still requires the response capability, policy and finding admission, and the durable effect path. If the worker stops after application, continue the same per-effect recovery instead of building a new plan to remove an old contribution. Test an altered plan, a changed affected set, and an expired authorization, and confirm from the adapter's retained control state that none applied a control; then confirm an exact retry and a restart recover the original artifact and effect identities.

Read more in Rollout, which covers choosing the response classes to enable.

Next: apply and recover each effect