The reviewer and semantic-tier principals were fixed demo keys ([0xAE], [0x5e]). They are now persistent joule-code-identity keyfiles: the structural reviewer loads/creates .joule/reviewer.key (or JOULE_REVIEWER_IDENTITY), the semantic tier .joule/semantic-reviewer.key (or JOULE_REVIEW_SEMANTIC_IDENTITY). Each is a distinct, traced agent principal that stays the same across runs — the same reviewer signs across sessions, attributable by its did:key, rather than an anonymous per-run seed. Separation of duties is unchanged and now identity-backed: author, structural reviewer, and semantic reviewer are three distinct persistent principals; an op authored by the reviewer's own identity is still skipped (self-review). The self-review integration test now exercises the real path — write a reviewer keyfile, author an op as that principal, confirm the binary skips it. (joule-code-identity needs ed25519-dalek's rand_core feature for key generation; enabled on that crate.) 17 tests; clippy clean. Co-Authored-By: Claude Opus 4.8 <[email protected]>
190 lines
7.9 KiB
Rust
190 lines
7.9 KiB
Rust
//! The cross-process loop: an author exports a signed spec + ops into
|
|
//! `.joule/`, the `joule-review` binary runs as an independent reviewer
|
|
//! process, and the reviews it writes back satisfy (or fail) the gate.
|
|
|
|
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
|
|
use ed25519_dalek::SigningKey;
|
|
use joule_code_cargo::{Anchor, CodingOp, Edit, SignedOp};
|
|
use joule_code_review::{approval, SignedReview};
|
|
use joule_code_schema::{ContentId, Tier};
|
|
use joule_code_spec::{Clause, ClauseKind, SignedSpec, Spec, ALG_ED25519 as SALG};
|
|
|
|
fn sk(s: u8) -> SigningKey {
|
|
SigningKey::from_bytes(&[s; 32])
|
|
}
|
|
|
|
fn op(author: &SigningKey, intent: &str, body: &str) -> SignedOp {
|
|
CodingOp {
|
|
alg: 0x01,
|
|
prior: ContentId::of_bytes(body.as_bytes()),
|
|
edits: vec![Edit {
|
|
path: "src/store.rs".into(),
|
|
anchor: Anchor::ByteRange { start: 0, end: 0 },
|
|
replacement: body.as_bytes().to_vec(),
|
|
}],
|
|
author: author.verifying_key().to_bytes(),
|
|
intent: intent.into(),
|
|
tier: Tier::L3,
|
|
}
|
|
.sign(author)
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn an_independent_reviewer_process_closes_the_loop_over_files() {
|
|
let author = sk(1);
|
|
let work = std::env::temp_dir().join(format!("joule-review-it-{}", std::process::id()));
|
|
let joule = work.join(".joule");
|
|
std::fs::create_dir_all(&joule).unwrap();
|
|
|
|
// author exports the adopted spec + ops
|
|
let spec: SignedSpec = Spec {
|
|
alg: SALG,
|
|
intent: "a persistent todo list".into(),
|
|
clauses: vec![
|
|
Clause { id: "C1".into(), text: "todos persist".into(), kind: ClauseKind::Requirement },
|
|
Clause { id: "NG1".into(), text: "no multiplayer".into(), kind: ClauseKind::NonGoal },
|
|
],
|
|
parent: None,
|
|
author: author.verifying_key().to_bytes(),
|
|
}
|
|
.sign(&author)
|
|
.unwrap();
|
|
std::fs::write(joule.join("spec.adopted.json"), serde_json::to_string(&spec).unwrap()).unwrap();
|
|
|
|
let good = op(&author, "C1: persist todos", "fn save() { write_to_disk(); }");
|
|
let bad = op(&author, "C1: persist todos", "fn save() { todo!() }");
|
|
let scope = op(&author, "NG1: multiplayer", "fn mp() {}");
|
|
let ops = vec![good.clone(), bad.clone(), scope.clone()];
|
|
std::fs::write(joule.join("ops.json"), serde_json::to_string(&ops).unwrap()).unwrap();
|
|
|
|
// run the independent reviewer process
|
|
let status = Command::new(env!("CARGO_BIN_EXE_joule-review"))
|
|
.arg(&work)
|
|
.status()
|
|
.expect("joule-review runs");
|
|
assert!(status.success());
|
|
|
|
// ingest the reviews it wrote
|
|
let text = std::fs::read_to_string(joule.join("reviews.json")).unwrap();
|
|
let reviews: Vec<SignedReview> = serde_json::from_str(&text).unwrap();
|
|
|
|
// the reviewer is a DIFFERENT principal than the author → reviews count
|
|
let for_op = |o: &SignedOp| -> Vec<SignedReview> {
|
|
let id = o.op.op_id().unwrap();
|
|
reviews.iter().filter(|r| r.review.op_id == id).cloned().collect()
|
|
};
|
|
assert!(approval(&good, &for_op(&good)).is_approved(), "the finished op is approved");
|
|
assert!(!approval(&bad, &for_op(&bad)).is_approved(), "the todo!() op is sent back");
|
|
assert!(!approval(&scope, &for_op(&scope)).is_approved(), "the non-goal op is sent back");
|
|
|
|
let _ = std::fs::remove_dir_all(&work);
|
|
let _: PathBuf = work; // silence unused on early-return paths
|
|
}
|
|
|
|
#[test]
|
|
fn the_reviewer_skips_ops_it_authored_itself() {
|
|
// give the binary a known persistent identity (its reviewer keyfile), then
|
|
// author an op with THAT principal: self-review, skipped, no reviews.
|
|
use joule_code_identity::{ActorKind, Identity};
|
|
let work = std::env::temp_dir().join(format!("joule-review-self-{}", std::process::id()));
|
|
let joule = work.join(".joule");
|
|
std::fs::create_dir_all(&joule).unwrap();
|
|
|
|
// the reviewer principal the binary will load from .joule/reviewer.key
|
|
let reviewer = Identity::generate("structural-reviewer", ActorKind::Agent);
|
|
reviewer.save(&joule.join("reviewer.key")).unwrap();
|
|
|
|
let spec: SignedSpec = Spec {
|
|
alg: SALG,
|
|
intent: "x".into(),
|
|
clauses: vec![Clause { id: "C1".into(), text: "a".into(), kind: ClauseKind::Requirement }],
|
|
parent: None,
|
|
author: sk(1).verifying_key().to_bytes(),
|
|
}
|
|
.sign(&sk(1))
|
|
.unwrap();
|
|
std::fs::write(joule.join("spec.adopted.json"), serde_json::to_string(&spec).unwrap()).unwrap();
|
|
|
|
let own = op(reviewer.signing_key(), "C1: x", "fn f() { ok(); }");
|
|
std::fs::write(joule.join("ops.json"), serde_json::to_string(&vec![own]).unwrap()).unwrap();
|
|
|
|
let status = Command::new(env!("CARGO_BIN_EXE_joule-review"))
|
|
.arg(&work)
|
|
.status()
|
|
.unwrap();
|
|
assert!(status.success());
|
|
|
|
let reviews: Vec<SignedReview> =
|
|
serde_json::from_str(&std::fs::read_to_string(joule.join("reviews.json")).unwrap()).unwrap();
|
|
assert!(reviews.is_empty(), "self-authored op yields no reviews");
|
|
|
|
let _ = std::fs::remove_dir_all(&work);
|
|
}
|
|
|
|
/// The full amortization loop, through the real binary and a real subprocess
|
|
/// model: run 1 escalates a clean-but-unsafe op to the model, which rejects it
|
|
/// and teaches a skill that persists to `.joule/review-skills.json`; run 2,
|
|
/// with NO model configured, catches a *recurrence* of the same defect at the
|
|
/// learned tier — the model's judgment, paid once, applied for free.
|
|
#[test]
|
|
fn the_model_teaches_a_skill_that_persists_and_catches_recurrences_model_free() {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
let author = sk(1);
|
|
let work = std::env::temp_dir().join(format!("joule-review-learn-{}", std::process::id()));
|
|
let joule = work.join(".joule");
|
|
std::fs::create_dir_all(&joule).unwrap();
|
|
|
|
let spec: SignedSpec = Spec {
|
|
alg: SALG,
|
|
intent: "x".into(),
|
|
clauses: vec![Clause { id: "C1".into(), text: "run input".into(), kind: ClauseKind::Requirement }],
|
|
parent: None,
|
|
author: author.verifying_key().to_bytes(),
|
|
}
|
|
.sign(&author)
|
|
.unwrap();
|
|
std::fs::write(joule.join("spec.adopted.json"), serde_json::to_string(&spec).unwrap()).unwrap();
|
|
|
|
// a subprocess "model" that rejects eval( and names it as the inducible cause
|
|
let model = work.join("model.sh");
|
|
std::fs::write(
|
|
&model,
|
|
"#!/bin/sh\ncat >/dev/null\nprintf 'VERDICT: REQUEST_CHANGES\\nFINDINGS: eval on untrusted input\\nINDUCE: eval(\\n'\n",
|
|
)
|
|
.unwrap();
|
|
std::fs::set_permissions(&model, std::fs::Permissions::from_mode(0o755)).unwrap();
|
|
|
|
// run 1: a clean-but-unsafe op (passes structural, escalates to the model)
|
|
let unsafe1 = op(&author, "C1: run", "fn run() { eval(input); }");
|
|
std::fs::write(joule.join("ops.json"), serde_json::to_string(&vec![unsafe1.clone()]).unwrap()).unwrap();
|
|
let status = Command::new(env!("CARGO_BIN_EXE_joule-review"))
|
|
.arg(&work)
|
|
.env("JOULE_REVIEW_MODEL_CMD", &model)
|
|
.status()
|
|
.unwrap();
|
|
assert!(status.success());
|
|
let r1: Vec<SignedReview> =
|
|
serde_json::from_str(&std::fs::read_to_string(joule.join("reviews.json")).unwrap()).unwrap();
|
|
assert!(!approval(&unsafe1, &r1).is_approved()); // the model rejected it
|
|
// the skill was learned and persisted
|
|
let skills = std::fs::read_to_string(joule.join("review-skills.json")).unwrap();
|
|
assert!(skills.contains("eval("), "the eval( skill persisted: {skills}");
|
|
|
|
// run 2: a DIFFERENT op with the same defect, NO model configured at all
|
|
let unsafe2 = op(&author, "C1: run", "fn other() { eval(x); }");
|
|
std::fs::write(joule.join("ops.json"), serde_json::to_string(&vec![unsafe2.clone()]).unwrap()).unwrap();
|
|
let status = Command::new(env!("CARGO_BIN_EXE_joule-review"))
|
|
.arg(&work) // no JOULE_REVIEW_MODEL_CMD
|
|
.status()
|
|
.unwrap();
|
|
assert!(status.success());
|
|
let r2: Vec<SignedReview> =
|
|
serde_json::from_str(&std::fs::read_to_string(joule.join("reviews.json")).unwrap()).unwrap();
|
|
assert!(!approval(&unsafe2, &r2).is_approved()); // caught by the LEARNED tier, no model
|
|
|
|
let _ = std::fs::remove_dir_all(&work);
|
|
}
|