Build an agent cooperative
Let independent operators authorize shared computation, including a registered 2-of-3 threshold settlement with retained local-credit accounting.
On this page
A cooperative lets an effect happen only when its members agree. This application gives independent members their own policies and asks them to authorize one statistical computation; the bilateral path gathers two signatures over iroh, and the threshold path creates a 2-of-3 FROST group that executes a registered settlement, transferring local work credits and computing the result in one retained transaction.
Run the cooperativeTwo of three members authorize one computation and its settlement
Two operators authorize shared research compute
Independent member policies decide an exact computation. Both signatures travel over Iroh before the compute owner accepts the work.
Change the numeric samples or the member budget. In bilateral mode, either required operator can refuse. In threshold mode, three member processes keep separate signing custody and two eligible members can authorize the registered settlement. Inspect the computation and balances, then the changed-parameter and repeat-spend refusals. Credits are local to this application.
Members sign and the compute owner verifies and executes
Each member’s policy sits beside its key share. The compute owner accepts two of three signatures over the exact action, then debits and computes in one transaction.
Two of three members authorize one action
Each member keeps its policy beside its signing key, and the threshold ceremony gives each member only the private packages addressed to it. The compute owner verifies the registered action against the current roster and epoch; a signature over changed samples or parameters is refused before the ledger. FROST threshold signatures.
Each member’s policy sits beside its signing key
The bilateral receiver evaluates the exact proposed work before it returns a signature. Its policy limits participation, parameters, lifetime, and local resource allowance. The proposer can request agreement and never replaces a member’s decision, and iroh carries the request between independent processes.
pub fn allow(
policy: &Policy,
action: &Action,
local: &str,
other: &str,
gate: &DirectoryGate,
) -> Result<()> {
anyhow::ensure!(
policy.accept,
"This member declined the proposal under its local policy"
);
anyhow::ensure!(
action.schema == "chio.cooperative.computation.v1",
"Unsupported joint action"
);
uuid::Uuid::parse_str(&action.nonce)?;
let now = now_ms();
anyhow::ensure!(
action.issued_at_ms <= now
&& now < action.expires_at_ms
&& action.expires_at_ms - action.issued_at_ms <= 300_000,
"Proposal expired or has an excessive validity window"
);
anyhow::ensure!(
action.participants.len() == 2
&& action.participants.contains(&local.to_string())
&& action.participants.contains(&other.to_string())
&& local != other,
"Proposal does not bind these two independent members"
);
anyhow::ensure!(
action.resource_id == "shared-research-compute"
&& !action.values.is_empty()
&& action.values.len() <= policy.max_values
&& action.values.iter().all(|v| v.is_finite())
&& action.work_units == action.values.len() as u64
&& action.work_units <= policy.max_work_units,
"Computation exceeds this member's resource policy"
);
let directory = gate.directory();
anyhow::ensure!(
directory.expires_at_unix_ms() > now
&& action
.participants
.iter()
.all(|id| directory.resolve_passport_key(id).is_some()),
"A proposal member is no longer admitted"
);
Ok(())
}In the verified run, events 02 and 03 record each member’s independent execution with its original receipts.
Threshold custody is created at each member
The threshold ceremony uses distributed key generation, and each member receives only the private round-two packages addressed to it. The coordinator transports public receipts, never the full set of private packages. Completion checks the public transcript and the member’s own packages before it retains the key share.
async fn complete(
State(node): State<Arc<Node>>,
h: HeaderMap,
Json(receipts): Json<Vec<FrostDkgPackageReceipt>>,
) -> Api {
admin(&node, &h).map_err(error)?;
let mut guard = node.ceremony.lock().map_err(error)?;
let state = guard.as_mut().ok_or_else(|| error("No ceremony"))?;
let complete = complete_frost_ceremony_scoped(
&state.config,
state
.secret
.take()
.ok_or_else(|| error("Round two already consumed"))?,
&state.round1,
&state.inbound,
&receipts,
)
.map_err(error)?;
private_bytes(
&node.config.root.join("key-package.custody"),
complete.key_package.custody_bytes(),
)
.map_err(error)?;
let value = json!({
"group_public_key":complete.group_public_key,
"verification_shares":complete.verification_shares,
"transcript_digest":complete.transcript_digest,
"received_private_packages":state.inbound.len()
});
state.key = Some(complete.key_package);
state.public = Some(value.clone());
state.inbound.clear();
Ok(Json(value))
}The effect owner verifies the registered action
The threshold action binds the exact samples, units, payer, payee, operation ID, and execution fence. The compute owner verifies the registered settlement against the current roster, epoch, completed signing slot, and selected trust keys before it touches its ledger, so a valid signature over other bytes authorizes nothing here.
fn execute(&self, input: Value) -> Result<Value> {
let settlement: Settlement = serde_json::from_value(input["settlement"].clone())?;
settlement.validate()?;
let proof: FrostAuthorizationV1 = serde_json::from_value(input["proof"].clone())?;
let action = action(&settlement)?;
let digest = action.action_digest()?;
let registration = frost_action_registration(FrostAuthorizationDomain::SettleCommitment)
.context("Settlement is not registered")?;
let contract = registration.ladder_contract_digest()?;
let expected = ExpectedFrostAuthorization {
domain: FrostAuthorizationDomain::SettleCommitment,
ladder_action_class: registration.ladder_action_class,
ladder_contract_digest: &contract,
scope_id: super::SCOPE,
resource_id: &settlement.operation_id,
resource_version: 1,
resource_fence: 1,
action_digest: &digest,
};
verify_for_execution(
&proof,
&expected,
&self.0.active()?,
&*self.0,
&*self.0,
&self.0.trust()?,
now(),
)?;
// The local credit transfer and its useful deterministic computation commit together.
let mut db = self
.0
.db
.lock()
.map_err(|_| anyhow::anyhow!("Settlement store lock failed"))?;
let tx = db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
let count: i64 = tx.query_row(
"SELECT COUNT(*) FROM settlements WHERE id=?1",
[&settlement.operation_id],
|r| r.get(0),
)?;
anyhow::ensure!(
count == 0,
"This operation already settled; inspect the retained result instead of spending again"
);
let raw: String = tx.query_row("SELECT body FROM records WHERE id='credits'", [], |r| {
r.get(0)
})?;
let mut credits: Value = serde_json::from_str(&raw)?;
let available = credits["research"]
.as_u64()
.context("Invalid payer balance")?;
anyhow::ensure!(
available >= settlement.units,
"The research account has insufficient local credits"
);
let mean = settlement.values.iter().sum::<f64>() / (settlement.values.len() as f64);
let output = json!({
"mean":mean,
"samples":settlement.values.len(),
"operation_id":settlement.operation_id,
"local_credits_transferred":settlement.units,
"action_digest":digest,
"authorization_id":proof.body.authorization_id
});
credits["research"] = json!(available - settlement.units);
credits["compute"] = json!(
credits["compute"]
.as_u64()
.context("Invalid recipient balance")?
+ settlement.units
);
tx.execute(
"UPDATE records SET body=?1 WHERE id='credits'",
[serde_json::to_string(&credits)?],
)?;
tx.execute(
"INSERT INTO settlements(id,action_digest,result) VALUES(?1,?2,?3)",
rusqlite::params![
settlement.operation_id,
digest,
serde_json::to_string(&output)?
],
)?;
tx.commit()?;
Ok(output)
}
}Event 04 completes the jointly authorized computation, a mean of 24.0 over four samples, with receipt c812a54b…31af6a.
Authority and accounting are retained together
The compute owner retains its current anchors and the operation ledger. The test kills and restarts that process and resubmits the approved action, and the unique operation debits the account once. The example has one compute operator with distinct authority keys, whose database and keys form one backup unit.
async fn execute(State(node): State<Arc<Node>>, h: HeaderMap, Json(input): Json<Value>) -> Api {
admin(&node, &h).map_err(error)?;
let result = node
.host
.call(
&node.run,
"research-operator",
&node.cap,
"cooperative",
"settle_and_compute",
input,
)
.await
.map_err(error)?;
Ok(Json(json!({
"allowed":result.allowed,
"output":result.output,
"reason":result.reason,
"receipt_id":result.receipt_id,
"run":node.run.snapshot().map_err(error)?
})))
}Too few approvals, changed parameters, and a replayed settlement move no credits
The verified run is the bilateral path: one proposal, two independent members, one computation.
./run --run input.json01 cooperative.proposed research-lab
Request a bounded research computation
02 cooperative.member member-host
Independent member execution and original receipts
03 cooperative.member member-host
Independent member execution and original receipts
04 cooperative.completed compute-owner
Jointly authorized computation completed
receipt c812a54b…31af6a- Collect too few approvals: no computation or credit transfer occurs.
- Change samples or settlement parameters after signing: verification refuses the action.
- Submit a proof without the selected current anchors: the effect owner refuses it.
- Restart and replay a completed settlement: balances and the effect count stay unchanged.
The downloadable tests assert each effect or its absence: run cargo test --locked -p chio-cooperative from the extracted project.
Run the cooperative on your machine
Keep the run directory to inspect the joint authorization, the computation, and the local-credit settlement.
cd chio-cooperative
./runThe archive holds the application, the shared host code, tests, sample input, pinned dependencies, and a README. The launcher builds and runs the Rust source included in this archive. Install Rust 1.94.1 and the README’s native prerequisites; filesystem isolation and operator workloads require the listed Linux host profile. Cargo resolves immutable public dependencies, so a sibling checkout is unnecessary.
For model workers, set OPENROUTER_API_KEY on the local host and select model mode; credentials stay on that host and never enter the docs form. The qualified model is openai/gpt-4.1-mini. Analysis, retrieval, and deterministic worker modes run without a model key. Ctrl-C stops the local web process and its active child work and keeps the run directory.
Download identity and source revision
Download complete project · View public source
1826c9c7de1944e6c4496527af5c2781fdfc573b1414bec22b74c48ff717d042Register another action the owner can check atomically
Define another registered action whose effect its owner can check atomically. Keep member policy local, bind every effectful parameter into the signed action, and retain the operation fence with the effect. An externally settled asset plugs in as a settlement adapter, an optional module beside the local work credits.