Delegate Sensitive Research
Delegate a review of work that read a customer's contract terms, keep the result useful to your team, and keep the terms off the public pull request.
On this page
The fixer needs Harbor Goods’ negotiated terms to fix issue #417, where quote() drops the tier discount for sku-204. Delegate the review to reviewer, return the review to your pricing team, and post exactly one summary that your pricing lead approved to the public pull request. The terms’ restrictions stay on everything the workflow learned.
Starts from chio new security-research --example security-research, a host for the pricing project with Harbor Goods’ terms and issue #417 in its documents.
The host in the security-research project runs the kernel’s native flow lifecycle against durable SQLite state and two independently journaled HTTP receivers. It is a trusted local operator program: it holds the fixer and reviewer identities, signed tool manifests, a classifier, and the pricing lead’s disclosure key, performs file reads and loopback sends, and runs no language model or confined worker. To expose the same operations to an agent, connect an authenticated process host.
One disclosure grant admits one exact send
The pricing lead’s grant binds this summary and the public destination for ten minutes. Dispatch consumes it under public-reviewed-v1, and the retained label still carries the contract restrictions. Step 6.
The workflow separates three decisions. Reading the terms gives the fixer a customer’s confidential pricing, delegating gives the reviewer narrower permission to work with it, and posting to the pull request changes the audience to anyone who can read a public repository, so it needs its own disclosure decision.
1. Build the host and start its receiver
Before you begin
Use macOS or Linux, Rust 1.94.1 or newer, Python 3.11 or newer, jq, and the installed chio CLI. The host/ subdirectory has its own manifest and lockfile; the portable label programs at the archive root run on their own.
chio new security-research --example security-research
cd security-research/host
cargo build --locked --bin researchThe host depends on the published chio-kernel, chio-flow, chio-process, and chio-store-sqlite crates; Cargo checks their versions against the lockfile. The result is a separate research binary beside the installed chio. If the build fails, check rustc --version and the project’s lockfile before changing source.
python3 receiver.py .receiverThe receiver binds 127.0.0.1:19041 and logs /internal, the pricing team’s channel, and /public, the public pull request, to separate JSONL files: two destination policies in one local service. Give it a fresh directory and a free port; the receiver counts in later steps assume empty journals. Open a second terminal in security-research/host/ and put the built command on its path, repeating this in any new terminal:
export PATH="$PWD/target/debug:$PATH"2. Read Harbor Goods’ terms as the fixer
research init
research read terms \
--as fixer --operation terms-v1Allow: terms-v1 Caller: fixer Document: terms CONTRACT-HARBOR-GOODS-17 Harbor Goods pays 18 percent below list on sku-204 through June, with a floor of USD 41.00. Do not quote these terms outside Larkfield.
You should see the terms: 18 percent below list on sku-204, with a USD 41.00 floor. Initialization issued the fixer capability and a signed, attenuated reviewer capability, retained the keys and manifests, and initialized the native flow authority. Before releasing the text, the kernel recorded the larkfield-pricing owner and the harbor-goods-terms compartment in principal, lineage, and session state.
3. Continue the work as the narrower reviewer
research read issue \
--as reviewer --operation review-source-v1Allow: review-source-v1 Caller: reviewer Document: issue Issue #417: quote() drops the tier discount for sku-204 when one order spans two price lists.
You should see issue #417 returned to the reviewer. Its delegation depth is 1: the grant removes permission to delegate, halves each per-tool invocation limit from 64 to 32, and expires after 30 minutes instead of the fixer’s hour. The host registers the reviewer’s fixed budget share when it reopens. The public issue adds context, and the shared lineage keeps the terms’ restrictions.
Each command reopens the same stores in a new host process. Actor names select retained grants in this operator interface; an untrusted worker never receives these keys and never chooses its own principal, lineage, or isolation epoch.
4. Return the review to the pricing team
Save the reviewer’s summary as summary.txt, then send it to the internal destination:
quote() applies the tier discount for sku-204 across both price lists; a new test covers the split order.research publish summary.txt \
--as reviewer --to internal \
--operation internal-review-v1Allow: internal-review-v1 Caller: reviewer Destination: internal
You should see the internal destination acknowledged, and the internal receiver logs the exact summary with its digest. Two conditions admitted the send: the grant permits invoking the publisher, and the publisher’s clearance preserves the contract label. Tool permission alone never decides where previously learned information may go.
5. Post the same summary to the pull request and see it refused
research publish summary.txt \
--as reviewer --to public \
--operation public-unreviewed-v1Deny: public-unreviewed-v1 Caller: reviewer
You should see a deny for public-unreviewed-v1, and the command exits 2. The native flow boundary refused before the public connector sent anything, so no public journal exists; that receiver observation is separate from the signed decision. The summary names no customer, and the refusal stands anyway: it was written by a session that read the terms. Rewriting it without the contract marker, or reading more public material, leaves the accumulated label in place.
6. Approve one summary and publish it
Your pricing lead reads the final contents of summary.txt and approves exactly those bytes for the pull request. The host signs the grant with the lead’s approver key and prints the path where it retains the grant.
research approve summary.txt \
--as reviewer --operation public-reviewed-v1Approved exact summary: public-reviewed-v1 Grant: .research/operations/public-reviewed-v1/disclosure.json
research publish summary.txt \
--as reviewer --to public \
--operation public-reviewed-v1 \
--grant .research/operations/public-reviewed-v1/disclosure.jsonAllow: public-reviewed-v1 Caller: reviewer Destination: public
You should see the public destination acknowledged. The pricing lead’s grant binds this summary and this destination for ten minutes, and native dispatch consumes the use: the public receiver observes one summary while retained state stays restricted. A later post needs its own compatible clearance or grant. Declassification, the embedder API behind this grant, continues with exact replay, changed drafts, and refused reuse.
7. Verify the receipt and the receiver journals
chio receipt verify \
--input .research/operations/public-reviewed-v1/completed.receipt.json \
--trusted-kernel-pubkey .research/kernel.pubVerified 1 receipt signatures, signer pins and action hashes.
You should see one receipt verified. The verifier checks the receipt’s signature, signer pin, and action hash against the separately retained kernel public key, which establishes what the kernel signed. The receiver journal separately establishes which summary reached the pull request.
Inspect the release, the labels, the delegation, and the journals
These commands read the original read and the completed post; reopening the operation sends no second summary. JSON mode retains the complete application record, and jq selects the fields discussed here.
research release public-reviewed-v1 --json > release.json
jq '{state, raw_recorded, evaluated, schema: .release.schema}' release.json
research read issue --as reviewer --operation review-source-v1 --json > child.json
jq '{depth: .receipt.metadata.attribution.delegation_depth,
label: .flow.snapshot.lineage_label}' child.json
research inspect --as reviewer --json > flow.json
jq .snapshot.session_label flow.json
cat .receiver/internal.jsonl
cat .receiver/public.jsonlA completed result carries the raw invocation record, the post-return evaluation, and the original release checkpoint. Output release explains their order and compares a lost reply with an interrupted effect; Persistent taint follows new sessions and reopened state; Classification changes the detector input without lowering the source floor.
Stop the receiver with Ctrl-C. Keep the state directory while you examine receipts or replay the original operation. Its retained grants expire; after expiry, start a fresh exercise instead of replacing keys or clearing stores inside an old operation.
Your own host passes when it reproduces five observations
A deployed host resolves labels from authenticated context and durable state, never from a label the model supplies or a fresh empty label per call. The reviewer gets its own authenticated principal and an attenuated capability, carries the fixer’s lineage restrictions when it receives the material, and returns to an internal receiver whose clearance retains the resulting obligations.
A shared invocation limit bounds the family’s total activity. The host carries the signed flow declaration and the complete authenticated context through its adapter, and durably joins the classified review output before releasing it.
| Exercise | Observe at the host | Observe outside the host |
|---|---|---|
| Internal review succeeds | Bound child request and permitted release | The internal result arrives |
| Public post is refused | Retained label and a signed deny | No public request arrives |
| A public input is read later | The join keeps prior obligations | Public post stays refused |
| Host restarts after the terms read | Principal and lineage state reopens | The next disclosure still obeys it |
| Output release is interrupted | Original operation and release checkpoint | No duplicate tool effect on replay |
The deny’s flow reason sits in receipt guard evidence on an embedder kernel and in the host’s log on the native process host. Keep three observations independent: the host’s admission and guarded-return decisions, the destination’s record of what arrived, and the restrictions a reopened state authority returns. A correct in-memory label establishes none of them. When a row fails, change one boundary at a time:
- The internal result is refused. Inspect reader sets and compartments before adding tool authority.
- A public request reaches its receiver despite a deny. Inspect alternate network and host-tool paths.
- A restart loses the restriction. Repair persistence and release ordering before extending the workflow.
Portable label programs
Search for owner, join, NDA, or Top.
The project root holds the unchanged portable Chio label and lattice modules. Its programs isolate the policy calculations behind the steps above; they authenticate no reviewer, persist no native state, and contact no receiver. If your own label fails construction, inspect owner self-membership, canonical identifiers, and collection bounds; if egress fails, compare owners, readers, and compartments in the label reference.
Download and run the lab
Use the Rust version declared in the project’s Cargo.toml. The project includes a lockfile, the example, and two unmodified Chio source modules with their provenance and license; its local manifests select the portable label and lattice APIs, with no second implementation of the security decision. Pull the project, run the program, and run its tests:
chio new security-research --example security-research
cd security-research
cargo run --locked --quiet
cargo test --lockedALLOW pricing-team destination: clearance retains the contract restrictions DENY public destination: source label does not flow to destination clearance RETAIN later public input: session restrictions unchanged DENY missing clearance: egress clearance is missing DENY Top clearance: top is not an operational egress clearance NARROW shared owner: readers intersect to larkfield-pricing and fixer DENY Larkfield-only destination: Harbor Goods obligations also apply RETAIN delegated input: Larkfield and Harbor Goods obligations remain
Each line prints after an assertion checks its decision; the run checks nine assertions. Six tests cover prior-read retention, unknown sources, independent owners, shared-owner intersection, compartment loss, and order-independent joins, including two counterexamples: an unknown source cannot egress even toward Top, and your pricing team’s terms joined with Harbor Goods’ own NDA obligation cannot reach a destination that accepts only your team’s. Run cargo test --locked after changing a reader set or compartment to see which property your change affects.
Execute the parent, child, and return joins
One inherited restriction from your pricing team (the larkfield-pricing owner), the reviewer’s read of a document Harbor Goods owns, and the result returning to the fixer. Two independent owners make a dropped obligation fail visibly.
use chio_flow::{authorize_egress, EgressDenial};
use chio_security_types::{InformationLabel, PrincipalId};
use std::collections::{BTreeMap, BTreeSet};
fn owned(name: &str) -> InformationLabel {
let owner = PrincipalId::new(name).unwrap();
InformationLabel::try_known(
BTreeMap::from([(owner.clone(), BTreeSet::from([owner]))]),
BTreeSet::new(),
)
.unwrap()
}
fn main() {
let parent_knowledge = owned("larkfield-pricing");
let child_read = owned("harbor-goods");
let child_knowledge = parent_knowledge.join_restrictions(&child_read).unwrap();
let returned_knowledge = parent_knowledge
.join_restrictions(&child_knowledge)
.unwrap();
assert_eq!(returned_knowledge, child_knowledge);
assert_eq!(
authorize_egress(&returned_knowledge, Some(&parent_knowledge)),
Err(EgressDenial::FlowViolation),
);
assert_eq!(
authorize_egress(&returned_knowledge, Some(&child_knowledge)),
Ok(())
);
println!("Child input and returned result retain both Larkfield and Harbor Goods owners");
}cargo run --locked --quiet --example delegated_restrictionsChild input and returned result retain both Larkfield and Harbor Goods owners
The fixer’s original destination, which accepts only your team’s obligation, no longer satisfies the combined result, and a destination retaining both owners does. The program models the label calculations at delegation and return; the host’s delegation, persistence, and guarded return produce the same expected labels with an authenticated reviewer.
Give the terms an owner
fn contract_label() -> InformationLabel {
let owner = PrincipalId::new("larkfield-pricing").expect("canonical owner");
let fixer = PrincipalId::new("fixer").expect("canonical reader");
InformationLabel::try_known(
BTreeMap::from([(owner.clone(), BTreeSet::from([owner, fixer]))]),
BTreeSet::from([Compartment::new("harbor-goods-terms").expect("canonical compartment")]),
)
.expect("bounded label")
}The larkfield-pricing owner permits itself and the fixer, and the harbor-goods-terms compartment keeps one customer’s terms apart from every other customer’s. The owner appears in its own reader set because a known label requires it.
Join the read into the session
let public = InformationLabel::bottom();
let internal = contract_label();
let session = public.join_restrictions(&internal).expect("valid join");
assert_eq!(authorize_egress(&session, Some(&internal)), Ok(()));
assert_eq!(
authorize_egress(&session, Some(&public)),
Err(EgressDenial::FlowViolation),
);The internal destination accepts every restriction; the public destination drops them and is refused. Joining another public input into session leaves the label equal to the restricted session, because the latest message changes nothing the agent already learned.
Combine a second owner without losing the first
let customer = label_for("harbor-goods", &["harbor-goods", "fixer"], &["harbor-goods-nda"]);
let combined = session
.join_restrictions(&customer)
.expect("independent owner join");
assert_eq!(
authorize_egress(&combined, Some(&internal)),
Err(EgressDenial::FlowViolation)
);
assert_eq!(authorize_egress(&combined, Some(&combined)), Ok(()));Harbor Goods owns its NDA, permits itself and the fixer, and requires the harbor-goods-nda compartment. The join keeps both owners and both compartments, so a destination that accepts only your team’s obligation can no longer receive the result. label_for, in the example’s src/main.rs, builds canonical principals and compartments and calls InformationLabel::try_known; selecting and authenticating the destination stays with the host.
The executable also joins two policies from the same owner, your pricing team, one permitting the reviewer and one not, and their intersection removes the reviewer. Shared owners narrow their reader sets; independent owners keep separate obligations.