The joule ceiling is JCP's flagship enforcement, and the spec's contract is that every decision is sealed, allow and deny alike. dispatch() did not honour that. route() pre-filtered out any harness whose remaining budget could not cover the task's quote, so an over-budget harness never reached runtime.decide() -- the ceiling fired, the loop failed over, and no receipt recorded that it had. The enforcement happened; the audit of it did not. Replace route() with candidates(), which filters on capability only. dispatch() now offers the task to each granted harness in turn, letting the runtime make and seal the decision -- so a harness that cannot pay is denied on the record before the loop fails over to one that can. Denied dispatches are still not debited (Runtime::decide only debits on allow), so per-harness joule totals are unchanged. One refusal deliberately seals nothing: a task whose kind no registered harness holds. There is no grant to decide against, and JCP seals decisions about a grant, not the absence of one. It is recorded denied in the RunReport instead. Module doc and MAPPING.md now state both cases precisely rather than claiming a blanket allow-and-deny seal. Two regression tests: budget_denials_are_sealed_not_silently_skipped (the deny lands in the ledger, verifies under the steward key, and is not debited) and ungranted_kind_seals_nothing. All six pre-existing tests pass unchanged. The background_agents example now shows 7 sealed decisions, up from 6 -- the seventh being the budget denial that used to disappear. Co-Authored-By: Claude Opus 4.8 <[email protected]>
133 lines
5.5 KiB
Rust
133 lines
5.5 KiB
Rust
//! A background-agent orchestrator, gated: run a batch of coding tasks across a
|
|
//! heterogeneous fleet of coding-agent backends — each in its own isolated
|
|
//! sandbox, on its own branch — where every dispatch is capability-gated,
|
|
//! joule-budgeted, and sealed **before** the agent runs.
|
|
//!
|
|
//! This is the shape the cloud background-agent products converged on (tracker →
|
|
//! branch → dispatch → PR). The orchestrator is blind to which backend runs a
|
|
//! task; it only knows each backend quotes a joule cost and reports an outcome.
|
|
//! What it adds over a bare meta-harness is the one artifact none of them
|
|
//! produce: an ordered, third-party-verifiable ledger of every decision — who was
|
|
//! authorised to run what, for how many joules, and how it turned out.
|
|
//!
|
|
//! ```text
|
|
//! cargo run -p jcp-orchestrate --example background_agents
|
|
//! ```
|
|
|
|
use std::cell::Cell;
|
|
|
|
use ed25519_dalek::SigningKey;
|
|
use jcp_orchestrate::{Harness, HarnessOutcome, Orchestrator, Task};
|
|
|
|
/// A coding-agent backend that completes at a fixed quoted cost. Stands in for
|
|
/// any agent CLI/framework running in its own sandbox container — the
|
|
/// orchestrator neither knows nor cares which.
|
|
struct Backend {
|
|
id: String,
|
|
kinds: Vec<String>,
|
|
cost_uj: u64,
|
|
}
|
|
impl Harness for Backend {
|
|
fn id(&self) -> &str {
|
|
&self.id
|
|
}
|
|
fn handles(&self, kind: &str) -> bool {
|
|
self.kinds.iter().any(|k| k == kind)
|
|
}
|
|
fn estimate(&self, _task: &Task) -> u64 {
|
|
self.cost_uj
|
|
}
|
|
fn run(&self, _task: &Task) -> HarnessOutcome {
|
|
HarnessOutcome::done(self.cost_uj)
|
|
}
|
|
}
|
|
|
|
/// A backend whose sandbox flakes `fails` times (transient container/network
|
|
/// failure) before the task lands — the bounded-retry path.
|
|
struct Flaky {
|
|
id: String,
|
|
kind: String,
|
|
cost_uj: u64,
|
|
fails: Cell<u32>,
|
|
}
|
|
impl Harness for Flaky {
|
|
fn id(&self) -> &str {
|
|
&self.id
|
|
}
|
|
fn handles(&self, kind: &str) -> bool {
|
|
kind == self.kind
|
|
}
|
|
fn estimate(&self, _task: &Task) -> u64 {
|
|
self.cost_uj
|
|
}
|
|
fn run(&self, _task: &Task) -> HarnessOutcome {
|
|
let left = self.fails.get();
|
|
if left > 0 {
|
|
self.fails.set(left - 1);
|
|
HarnessOutcome::retry(self.cost_uj, "sandbox cold-start timeout")
|
|
} else {
|
|
HarnessOutcome::done(self.cost_uj)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
// The principal that authorises the fleet, and the steward that seals every
|
|
// decision. A third party trusts the steward key, not the orchestrator.
|
|
let mut fleet = Orchestrator::new(
|
|
SigningKey::from_bytes(&[1u8; 32]),
|
|
SigningKey::from_bytes(&[7u8; 32]),
|
|
"did:example:background-agents",
|
|
3, // max attempts per item
|
|
);
|
|
|
|
// Two backends can `implement`; the first has budget for two tasks, then the
|
|
// fleet fails over to the second. A `review` backend flakes twice before it
|
|
// lands. Nothing is granted `deploy` — a capability no backend holds.
|
|
fleet
|
|
.register(Box::new(Backend { id: "coder-alpha".into(), kinds: vec!["implement".into()], cost_uj: 120 }), &["implement"], 300)
|
|
.unwrap();
|
|
fleet
|
|
.register(Box::new(Backend { id: "coder-beta".into(), kinds: vec!["implement".into()], cost_uj: 120 }), &["implement"], 1_000)
|
|
.unwrap();
|
|
fleet
|
|
.register(Box::new(Flaky { id: "reviewer".into(), kind: "review".into(), cost_uj: 90, fails: Cell::new(2) }), &["review"], 1_000)
|
|
.unwrap();
|
|
|
|
// A batch of work items — each is a branch a backend will take up.
|
|
fleet.submit(Task::new("feat/parser-tokenize", "implement", b"add tokenizer to the parser".to_vec()));
|
|
fleet.submit(Task::new("feat/parser-ast", "implement", b"build the AST layer".to_vec()));
|
|
fleet.submit(Task::new("feat/parser-errors", "implement", b"structured parse errors".to_vec()));
|
|
fleet.submit(Task::new("review/pr-482", "review", b"review the tokenizer PR".to_vec()));
|
|
fleet.submit(Task::new("ops/ship-prod", "deploy", b"push to production".to_vec())); // ungranted capability
|
|
|
|
let report = fleet.run();
|
|
|
|
println!("── run report ──");
|
|
println!(" completed : {}", report.done);
|
|
println!(" denied : {} (no backend holds that capability)", report.denied);
|
|
println!(" failed : {}", report.failed);
|
|
println!(" authorised: {} µJ across the fleet", report.total_uj);
|
|
println!(" sealed : {} decision receipts", report.receipts);
|
|
|
|
println!("\n── per branch ──");
|
|
for id in ["feat/parser-tokenize", "feat/parser-ast", "feat/parser-errors", "review/pr-482", "ops/ship-prod"] {
|
|
println!(" {id:22} → {:?}", fleet.state_of(id).unwrap());
|
|
}
|
|
|
|
println!("\n── joules per backend ──");
|
|
for b in ["coder-alpha", "coder-beta", "reviewer"] {
|
|
println!(" {b:12} {} µJ", fleet.spent_uj(b));
|
|
}
|
|
|
|
// The whole run is one ordered, verifiable ledger — the audit a bare
|
|
// background-agent orchestrator has no way to produce. Every allow was sealed
|
|
// before its backend ran, and so was every deny: `coder-alpha` refusing the
|
|
// third branch for want of joules is on the record, not silently skipped.
|
|
// Only `ops/ship-prod` seals nothing — no backend holds `deploy`, so there was
|
|
// no grant to decide against.
|
|
let steward = fleet.steward().to_string();
|
|
let pk = fleet.steward_pubkey();
|
|
let verified = fleet.receipts().iter().filter(|r| jcp_receipt::extract(r, &steward, &pk).is_ok()).count();
|
|
println!("\naudit: {verified}/{} sealed decisions verify under the steward key", fleet.receipts().len());
|
|
}
|