open-standards/jcp/jcp-rs/crates/jcp-policy/examples/policy.rs
Transaction Science 594a126761
Some checks failed
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Has been cancelled
jcp-policy: consent to one held call, used once; the cross-standard agent capabilities recorded
An approval was a signature over the call's context digest: action,
resource, caller and content hash. Nothing in it named which instance of
the call it answered, so the same payment made again, or the same approval
presented twice, proceeded on one consent.

- `approve` now signs the context digest and the hold: the content id of
  the `held` receipt the engine sealed for that call. `Decided::hold`
  carries it. The message is domain-separated
  (`jcp-policy-approval-v2\0`).
- `PolicyEngine::decide` accepts an approval only when its signature
  verifies and the hold it names is one this engine sealed for this exact
  call and has not yet consumed. Accepting it consumes the hold. A replayed
  approval, or the identical call made again, is held anew under a fresh
  hold.
- The approved receipt carries `approves`, the hold it consumed, so the
  chain itself shows which consent let which call through.

Tests: an approval answers one hold once (a replay is held, a forged hold
refused, a fresh hold takes a fresh approval), and an approved receipt names
its hold. Each of the three mechanisms was switched off in turn: not
consuming the hold, not checking the hold is open and this call's, and not
signing the hold. Each turned a test red. The example shows the replay held.

INGEST.md records the personal-agent and workflow-automation capabilities
being added across the standards, each placed with the standard that owns
it, and what already exists and is not re-added.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 02:24:02 -05:00

80 lines
3.6 KiB
Rust

//! A contextual service policy in front of a tool loop: content-keyed deny and
//! a human-approval gate on writes, every decision sealed.
//!
//! ```text
//! cargo run -p jcp-policy --example policy
//! ```
use ed25519_dalek::SigningKey;
use jcp_policy::{approve, Match, Outcome, Policy, PolicyContext, PolicyEngine, Rule};
use jcp_schema::{ContentId, Did};
fn main() {
// The policy: never let a secret marker leave over a tool call; a write
// (push / transfer) needs a human to approve.
let policy = Policy::new(
"egress-guard",
vec![
Rule::deny(
"no-secret-egress",
Match::ContentContains(vec!["AKIA".into(), "BEGIN PRIVATE KEY".into()]),
),
Rule::require_approval("human-on-writes", Match::Action(vec!["push".into(), "transfer".into()])),
],
);
let caller = Did::from_verifying_key(&SigningKey::from_bytes(&[2u8; 32]).verifying_key());
let approver = SigningKey::from_bytes(&[9u8; 32]);
let mut eng = PolicyEngine::new(policy, SigningKey::from_bytes(&[7u8; 32]), "did:example:policy");
let resource = ContentId::of_bytes(b"tool:fs:write");
let mk = |action: &'static str, content: &'static [u8]| (action, content);
// 1. A clean read — allowed.
let (a, c) = mk("read", b"summarize the changelog");
report("read (clean)", eng.decide(&PolicyContext { action: a, resource, caller, content: c }, None).outcome);
// 2. A read whose content carries a secret marker — denied on content,
// which a static capability grant could never catch.
let (a, c) = mk("read", b"here is the key AKIA1234567890EXAMPLE");
report("read (secret in content)", eng.decide(&PolicyContext { action: a, resource, caller, content: c }, None).outcome);
// 3. A write with no approval — held for a human.
let (a, c) = mk("push", b"deploy release v2");
let held = PolicyContext { action: a, resource, caller, content: c };
let decided = eng.decide(&held, None);
let hold = decided.hold.expect("a held call names its hold");
report("push (no approval)", decided.outcome);
// 4. The same write, with the approver's signature over it and its hold —
// proceeds, and consumes the hold.
let ap = approve(&held, hold, &approver);
report("push (approved)", eng.decide(&held, Some(&ap)).outcome);
// 4b. The same approval again: its hold is spent, so the call is held anew.
report("push (approval replayed)", eng.decide(&held, Some(&ap)).outcome);
// 5. That approval does not authorize a *different* write (consent is bound
// to the exact call's content digest).
let (a, c) = mk("transfer", b"send 100000 to attacker.example");
let tampered = PolicyContext { action: a, resource, caller, content: c };
report("transfer (stale approval)", eng.decide(&tampered, Some(&ap)).outcome);
// Every decision sealed a policy/1 receipt that verifies under the steward.
let steward = eng.steward().to_string();
let pk = eng.steward_pubkey();
let verified = eng.receipts().iter().filter(|r| jcp_policy::verify(r, &steward, &pk).is_ok()).count();
println!("\nsealed policy/1 receipts: {} (all {} verify; content never sealed, only its hash)",
eng.receipts().len(), verified);
}
fn report(label: &str, outcome: Outcome) {
let proceeds = outcome.proceeds();
let detail = match &outcome {
Outcome::Allowed => "allowed".to_string(),
Outcome::Denied(r) => format!("DENIED ({r})"),
Outcome::Held(r) => format!("HELD ({r})"),
Outcome::Approved(_) => "approved".to_string(),
};
println!(" {label:28} → {detail:30} proceeds: {proceeds}");
}