Chio/Docs
LOGIN · JOIN

Declassification

Authorize one intentional reduction in disclosure restrictions without erasing the information already retained by the agent or its lineage.

Concept chio@5f63e6e

On this page

Your pricing lead can let one reviewed summary of the fix for issue #417 reach the public pull request while the agent that wrote it stays bound by Harbor Goods’ terms, which it read along the way. The lead signs a grant for that exact request, destination, and purpose; the kernel checks every binding at the moment of use and spends the grant once.

Ordinary egress refuses the summary, because the public destination keeps none of the terms' restrictions. The pricing lab below posts it under a grant from the pricing lead's pricing-approver key, then shows the agent's retained label unchanged afterwards.

Disclosure grantChoose an attempt.
signed grantpublishunchangedverifiesrequest_hashsource_label_hashdestination_idtool_namepurposesubject · sessionexpires_atsingle usedispatch, onceτrKdℓpricing leadpricing-approverrequestsummary.txtretained: larkfield-pricing / harbor-goods-termspublic-inboxjournal: 1unchangedrequest_hashsource_label_hashdestination_idtool_namepurposesubject · sessionexpires_atsingle usedispatch, onceτrKdℓpricing leadpricing-approverrequestsummary.txtretainedpublic-inboxjournal: 1

Every binding matches, so one publication leaves

The kernel verifies the pricing lead's signature and each bound field against the host-resolved request, consumes the grant at the use boundary, and dispatches the reviewed summary to public-inbox. The retained label keeps the contract restriction. Egress.

One reviewed post: the pricing lead's grant, the request it binds, the kernel's binding checks, and the public receiver. Values follow the pricing lab; tenant and agent bindings fold into the subject row, and the durable consumption record is left out.

A grant names one request, one destination, and one purpose

Restricted information often supports a conclusion that eventually has to be communicated: a fix description on a public pull request, an extract for your partner, a quote explanation for an outside buyer. The grant makes that transition explicit and keeps it as narrow as what the reviewer judged.

crates/security/chio-security-types/src/declassification.rs11–27rust
pub struct DeclassificationGrantClaims {
    pub grant_id: GrantId,
    pub capability_id: RecordId,
    pub tenant_id: TenantId,
    pub subject_id: PrincipalId,
    pub agent_id: RecordId,
    pub session_id: SessionId,
    pub source_label_hash: Digest32,
    pub target_label: InformationLabel,
    pub destination_id: DestinationId,
    pub tool_name: RecordId,
    pub purpose: DeclassificationPurpose,
    pub request_hash: Digest32,
    pub issued_at_unix_seconds: u64,
    pub expires_at_unix_seconds: u64,
    pub authority_key_id: RecordId,
}

The request hash commits the proposed operation, so editing the summary creates another disclosure. The source-label hash identifies the restrictions being relaxed, and reading another customer's terms changes them. The target label states the permitted result. Destination, tool, and purpose stop reuse on another channel or task, and tenant, subject, agent, session, and capability tie the grant to one authenticated workflow, so another caller cannot adopt it by copying it.

Issued-at and expiry bound the use window, and the authority key ID selects the expected signer. Validation rejects an invalid time window and a Top target. The approver reviews a readable rendering of the same committed bytes the grant binds, since a short title over an unrelated payload hash gives the approver nothing to recognize. The retained reviewed request lets an operator later compare a submitted operation with the decision.

The host supplies the rules a grant is checked against

A purpose is part of the policy contract: the effective declassification purposes must permit it, and the embedding host configures which authorities it trusts to sign. An explanation a model writes, such as "this is safe to publish", selects no purpose and no signer.

crates/security/chio-flow/src/declassification.rs16–31rust
pub struct DeclassificationVerificationRequest {
    pub capability_id: RecordId,
    pub tenant_id: TenantId,
    pub subject_id: PrincipalId,
    pub agent_id: RecordId,
    pub session_id: SessionId,
    pub source_label: InformationLabel,
    pub destination_id: DestinationId,
    pub tool_name: RecordId,
    pub purpose: DeclassificationPurpose,
    pub policy_purposes: BTreeSet<DeclassificationPurpose>,
    pub manifest_purposes: BTreeSet<DeclassificationPurpose>,
    pub canonical_request: CanonicalBody,
    pub now_unix_ms: u64,
    pub trusted_authorities: BTreeMap<RecordId, PublicKey>,
}

The verifier compares the signed grant with a complete request the host resolves. policy_purposes come from current policy, manifest_purposes from the verified tool declaration, and trusted_authorities from the host's authority configuration; copying any of them from the submitted grant would let the grant define the rules for accepting itself. The source label is the one the host accumulated, because a rewritten summary still derives from that state.

A host enables declassification by building its flow resolver with the authorities it trusts and carrying the signed grant on the tool-call request in declassification_grant. A resolver that trusts no authority leaves every restricted disclosure refused. The pricing lab's host trusts pricing-approver, the pricing lead's key; a deployed application connects its own authenticated review procedure to that signing step.

Verify a reviewed summary and change the request

Verify the reviewed summary and change the request

The pricing lead reviews one summary for the public pull request while the reviewer still knows Harbor Goods' terms. The program verifies that one exception, then refuses a changed destination, a changed purpose, a summary that adds Harbor Goods' price floor, and use at expiry. Its disposable key and fixed clock make the exercise reproducible; an embedding resolves the signer, purposes, subject, canonical request, and source label from their independent owners.

security-host/disclosure_cases.rsrust
use chio_core::crypto::Keypair;
use chio_core_types::SignedDeclassificationGrant;
use chio_flow::{authorize_egress, canonical_request_hash, information_label_hash, verify_declassification, DeclassificationError, DeclassificationVerificationRequest, EgressDenial};
use chio_security_types::{Compartment, DeclassificationGrantBody, DeclassificationGrantClaims, InformationLabel, PrincipalId};
use chio_security_types::flow::DeclassificationPurpose;
use chio_security_types::ports::{CanonicalBody, DestinationId, GrantId, RecordId, SessionId, TenantId};
use std::collections::{BTreeMap, BTreeSet};

/// Disposable signing key and fixed time exercise exact verification bindings.
/// The configured host separately consumes a grant at its durable use boundary.
pub fn compare_disclosures() -> Result<(), Box<dyn std::error::Error>> {
    let signer = Keypair::from_seed(&[31; 32]);
    let owner = PrincipalId::new("larkfield-pricing")?;
    let source = InformationLabel::try_known(
        BTreeMap::from([(owner.clone(), BTreeSet::from([owner]))]),
        BTreeSet::from([Compartment::new("harbor-goods-terms")?]),
    )?;
    let purpose = DeclassificationPurpose::new("reviewed-summary")?;
    let request = DeclassificationVerificationRequest {
        capability_id: RecordId::new("publication-capability")?,
        tenant_id: TenantId::new("larkfield")?,
        subject_id: PrincipalId::new("reviewer")?,
        agent_id: RecordId::new("review-agent")?,
        session_id: SessionId::new("pr-421-review")?,
        source_label: source.clone(),
        destination_id: DestinationId::new("public-pull-request")?,
        tool_name: RecordId::new("publish")?,
        purpose: purpose.clone(),
        policy_purposes: BTreeSet::from([purpose.clone()]),
        manifest_purposes: BTreeSet::from([purpose.clone()]),
        canonical_request: CanonicalBody::new(br#"{"destination":"public-pull-request","summary":"quote() applies the tier discount for sku-204"}"#.to_vec())?,
        now_unix_ms: 150_000,
        trusted_authorities: BTreeMap::from([(RecordId::new("pricing-approver")?, signer.public_key())]),
    };
    let body = DeclassificationGrantBody::new(DeclassificationGrantClaims {
        grant_id: GrantId::new("reviewed-summary-417")?,
        capability_id: request.capability_id.clone(), tenant_id: request.tenant_id.clone(),
        subject_id: request.subject_id.clone(), agent_id: request.agent_id.clone(),
        session_id: request.session_id.clone(), source_label_hash: information_label_hash(&source)?,
        target_label: InformationLabel::bottom(), destination_id: request.destination_id.clone(),
        tool_name: request.tool_name.clone(), purpose,
        request_hash: canonical_request_hash(&request.canonical_request)?,
        issued_at_unix_seconds: 100, expires_at_unix_seconds: 200,
        authority_key_id: RecordId::new("pricing-approver")?,
    })?;
    let grant = SignedDeclassificationGrant::sign(body, &signer)?;
    let verified = verify_declassification(&grant, &request)?;
    assert!(verified.target_label().is_bottom());
    assert_eq!(request.source_label, source);
    assert_eq!(authorize_egress(&source, Some(&InformationLabel::bottom())), Err(EgressDenial::FlowViolation));
    let mut changed = request.clone();
    changed.destination_id = DestinationId::new("other-receiver")?;
    assert_eq!(verify_declassification(&grant, &changed), Err(DeclassificationError::BindingMismatch));
    changed = request.clone(); changed.purpose = DeclassificationPurpose::new("unreviewed-export")?;
    assert_eq!(verify_declassification(&grant, &changed), Err(DeclassificationError::BindingMismatch));
    changed = request.clone();
    changed.canonical_request = CanonicalBody::new(br#"{"destination":"public-pull-request","summary":"Harbor Goods keeps a USD 41.00 floor"}"#.to_vec())?;
    assert_eq!(verify_declassification(&grant, &changed), Err(DeclassificationError::BindingMismatch));
    changed = request.clone(); changed.now_unix_ms = 200_000;
    assert_eq!(verify_declassification(&grant, &changed), Err(DeclassificationError::Expired));
    println!("VERIFY reviewed summary: exact pull-request release binding");
    println!("RETAIN source: ordinary public egress still refuses");
    println!("REFUSE changed destination, purpose and summary bytes");
    println!("REFUSE expiry at use: earlier verification does not freeze time");
    Ok(())
}
Output
VERIFY reviewed summary: exact pull-request release binding
RETAIN source: ordinary public egress still refuses
REFUSE changed destination, purpose and summary bytes
REFUSE expiry at use: earlier verification does not freeze time

After verification, ordinary public egress still refuses the unchanged source. Reaching the expiry boundary refuses a fresh verification even though preparation succeeded before it, and a successful verification still needs durable consumption at the operation's use boundary.

The grant is spent once, at the use boundary, against a fresh clock

Preparation verifies a proposed grant. The operation-owned path consumes it at the use boundary, with a second time check, so a grant that expired while waiting for approval or capacity stays expired. Single-use state is durable, and two concurrent attempts cannot both spend it.

Change after authorizationResult from verify_declassificationWhat to do
Different request bytes
DeclassificationError::BindingMismatch
Review and authorize the new request
Different destination or tool
DeclassificationError::BindingMismatch
Obtain authority for that disclosure path
Different session or subject
DeclassificationError::BindingMismatch
Reestablish the intended authenticated workflow
Time passes beyond expiry
DeclassificationError::Expired at use
Issue a fresh grant only after reconciling any original attempt
Original attempt has unknown effect
The consumed use is recorded as OutcomeUnknown
Reconcile the original operation without blind reuse

A changed draft is a new authorization question: stop before the effect, keep any original pending state, and request authority for the changed bytes, without normalizing a meaningful change away to match the old hash. A lost reply is a recovery question. The original operation may already have spent its grant and sent the data, so the consumption stays attached to that operation, a timeout leaves the grant spent, and the approver issues no equivalent grant to cover the unresolved attempt.

Replaying a retained guarded response under the release contract is distinct from sending the publication again. When the provider's effect stays unknown, keep that uncertainty and use the provider's authenticated reconciliation interface; a verified receipt shows what the kernel decided, and the code host's records show whether the comment was posted.

The pricing lab posts one reviewed summary and keeps the contract label

Continue the delegated fix with its receiver running, its retained .research state, and the reviewed summary.txt. The approve command signs with the pricing lead's separate approver key, binding the exact file contents, the reviewer capability, the session, the destination, the tool, the purpose, and the current source-label hash, with a ten-minute expiry:

Terminalbash
cat summary.txt
research approve summary.txt --as reviewer --operation public-reviewed-v1 --json > grant.json
research publish summary.txt \
  --as reviewer --operation public-reviewed-v1 \
  --to public --grant .research/operations/public-reviewed-v1/disclosure.json --json > published.json
jq '{verdict, output, retained: .flow.snapshot.session_label}' published.json
Output
quote() applies the tier discount for sku-204 across both price lists; a new test covers the split order.
{
  "verdict": "Allow",
  "output": {
    "destination": "public",
    "received": true
  },
  "retained": {
    "compartments": [
      "harbor-goods-terms"
    ],
    "kind": "known",
    "owners": {
      "larkfield-pricing": [
        "larkfield-pricing"
      ]
    }
  }
}

You should see an Allow, a public receiver that received the summary, and a retained label that still names the larkfield-pricing owner and the harbor-goods-terms compartment. The review command only signs and retains the grant; the publication carries it through native dispatch, where the operation consumes the use and records its outcome. The grant body names public-inbox, the publish tool, the purpose reviewed-summary, and the authority pricing-approver, with an expiry 600 seconds after issue. Verify the receipt against the retained kernel key:

Terminalbash
chio receipt verify \
  --input .research/operations/public-reviewed-v1/completed.receipt.json \
  --trusted-kernel-pubkey .research/kernel.pub
Output
Verified 1 receipt signatures, signer pins and action hashes.

This single-user program keeps every operator key, the approver key in editor.key included, in one private directory so the example runs end to end. In a deployed application the approver key sits under separate review custody.

A retry replays the release, and a second send is refused

The same operation key and arguments ask the original operation for its result. A new operation key with the same grant is a new request to publish, and the spent grant refuses it:

Terminalbash
research publish summary.txt \
  --as reviewer --operation public-reviewed-v1 \
  --to public --grant .research/operations/public-reviewed-v1/disclosure.json --json > replayed.json
cmp <(jq -S .receipt published.json) <(jq -S .receipt replayed.json)
wc -l .receiver/public.jsonl
if research publish summary.txt \
  --as reviewer --operation public-reuse-v1 \
  --to public --grant .research/operations/public-reviewed-v1/disclosure.json --json > reused.json; then
  echo 'Unexpected second disclosure' >&2
  exit 1
else
  test "$?" -eq 2
fi
wc -l .receiver/public.jsonl
Output
       1 .receiver/public.jsonl
       1 .receiver/public.jsonl

You should see matching receipt bytes and one public entry after both attempts. Keep enough invocation allowance to run the comparison, since a refusal caused only by exhausted quota would test a different boundary. A grant can also be validly signed and still not apply to the bytes being sent. Here the pricing lead reviews one summary and the call submits a changed file under a new operation key, one that adds Harbor Goods' price floor:

Terminalbash
research approve summary.txt --as reviewer --operation revised-review-v1 --json > revised-grant.json
cp summary.txt changed-summary.txt
printf '%s\n' 'Harbor Goods keeps its USD 41.00 floor on sku-204.' >> changed-summary.txt
if research publish changed-summary.txt \
  --as reviewer --operation changed-draft-v1 \
  --to public --grant .research/operations/revised-review-v1/disclosure.json --json > changed.json; then
  echo 'Unexpected changed disclosure' >&2
  exit 1
else
  test "$?" -eq 2
fi
research publish summary.txt \
  --as reviewer --operation revised-review-v1 \
  --to public --grant .research/operations/revised-review-v1/disclosure.json --json > unchanged.json

The altered request is refused before sending, and the unchanged reviewed request then succeeds, because a binding refusal leaves the grant unspent. The receiver gains one public summary across the two attempts. For a deliberate revision, review the new bytes and issue new authority, leaving the original retained request as it was.

The release leaves the agent's knowledge restricted

The grant permits one egress label change and rewrites no retained state. The agent still knows Harbor Goods' terms, so its next external request meets ordinary flow or presents another applicable grant. Revocation, containment, response classification, and the ownership required to deliver a result all still apply to the release itself.

The boundary is testable from outside. After an authorized publication, an ordinary public disclosure from the same retained context meets the original restrictions. With the grant held fixed, a changed destination, purpose, request body, source-label commitment, or authenticated subject each refuses on its own, a grant that expires between preparation and use refuses at use, and concurrent or restarted attempts spend one grant once. The grant's consumption identity, the operation ledger, and an independent receiver log answer three questions: which authorization was spent, which operation owns the attempt, and which disclosure reached the destination.

Retain the grant commitment and consumption evidence without placing private input in public receipt metadata. When a release needs independent reviewers, threshold approvals add an approval set with its own bindings and replay state, and meeting the threshold leaves the grant's destination and single use exactly as signed.

Next: require more than one reviewer