open-standards/jcp/jcp-rs/crates/jcp-prompt/examples/registry.rs
Transaction Science 2bb8a5f33d jcp-prompt: a signed, content-addressed prompt registry
The serving ecosystem manages prompts as immutable versions of a templated
prompt with mutable aliases (dev / staging / production) resolved by a
prompts:/<name>@<alias> URI. The shape is right, but the store is mutable —
"which prompt did the agent actually run under" is a row someone with write
access can edit after the fact.

jcp-prompt keeps the shape and adds the family's trust layer:
- PromptVersion: immutable, content-addressed (the id IS the name+version+
  template, so swapping the text changes the id); render() substitutes
  {{ variable }} placeholders and reports missing ones; variables() lists them.
- PromptRegistry: register (auto-incrementing versions) + set_alias (promote)
  + resolve(prompts:/name@alias | prompts:/name/version). Each register and
  each promotion seals a hash-chained prompt/1 JCR-1 receipt, so the exact
  instructions and which version was live when are tamper-evident artifacts.
- parse_ref / verify round out the URI scheme and receipt verification.

Completes the MLflow methodology trio alongside jcp-trace's content-addressed
gate + judge alignment. Reuses jcp-schema + jcp-receipt jcr1 — no new crypto.
7 tests + example green, clippy clean; workspace test + clippy --all-targets
still clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-27 19:57:46 -04:00

49 lines
2.2 KiB
Rust

//! A signed prompt registry: version a template, promote an alias, render it,
//! and show that every change sealed a verifiable receipt.
//!
//! ```text
//! cargo run -p jcp-prompt --example registry
//! ```
use std::collections::BTreeMap;
use ed25519_dalek::SigningKey;
use jcp_prompt::{verify, PromptRegistry};
fn main() {
let mut reg = PromptRegistry::new(SigningKey::from_bytes(&[9u8; 32]), "did:example:prompt-steward");
// Two versions of a support prompt; v2 tightens the instruction.
let v1 = reg.register("support", "Answer {{ user }} about {{ topic }}.");
let v2 = reg.register("support", "Answer {{ user }} about {{ topic }} in one sentence.");
println!("registered support v{} ({}…)", v1.version, &v1.content_id().to_hex()[..12]);
println!("registered support v{} ({}…)", v2.version, &v2.content_id().to_hex()[..12]);
println!("template variables: {:?}", v2.variables());
// Promote production to v1 first, then to v2 — an auditable move.
reg.set_alias("support", "production", 1).unwrap();
reg.set_alias("support", "production", 2).unwrap();
// Resolve the production alias and render it.
let live = reg.resolve("prompts:/support@production").unwrap();
let mut vars = BTreeMap::new();
vars.insert("user".to_string(), "Alice".to_string());
vars.insert("topic".to_string(), "a refund".to_string());
println!("\nprompts:/support@production → v{}", live.version);
println!("rendered: {}", live.render(&vars).unwrap());
// A pinned older version still resolves byte-for-byte.
println!("prompts:/support/1 → {:?}", reg.resolve("prompts:/support/1").unwrap().template);
// Every register + promote sealed a hash-chained, verifiable receipt.
let steward = reg.steward().to_string();
let pk = reg.steward_pubkey();
println!("\nsealed prompt/1 receipts:");
for (i, r) in reg.receipts().iter().enumerate() {
let pr = verify(r, &steward, &pk).unwrap();
let what = match pr.alias {
Some(a) => format!("{} {}@{} → v{}", pr.kind, pr.name, a, pr.version),
None => format!("{} {} v{}", pr.kind, pr.name, pr.version),
};
println!(" [{i}] {what:38} verifies ✓");
}
}