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]>
49 lines
2.2 KiB
Rust
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 ✓");
|
|
}
|
|
}
|