diff --git a/epiphany-core/src/bin/epiphany-repo-close-mind-adoption-guard-smoke.rs b/epiphany-core/src/bin/epiphany-repo-close-mind-adoption-guard-smoke.rs deleted file mode 100644 index b37e326d..00000000 --- a/epiphany-core/src/bin/epiphany-repo-close-mind-adoption-guard-smoke.rs +++ /dev/null @@ -1,1065 +0,0 @@ -use anyhow::Context; -use anyhow::Result; -use anyhow::anyhow; -use chrono::Utc; -use serde::Deserialize; -use serde_json::Value; -use serde_json::json; -use std::env; -use std::fs; -use std::path::Path; -use std::path::PathBuf; -use std::process::Command; - -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct RepoWorkMapStore { - schema_version: String, - entries: Vec, - private_state_exposed: bool, -} - -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct RepoWorkMapEntry { - schema_version: String, - item: String, - changed_paths: Vec, - commit_sha: String, - safe_action_family: String, - modeling_summary: String, - soul_verdict_receipt_id: String, - mind_gateway_review_id: String, - mind_state_commit_receipt_id: String, - durable_state_admitted: bool, - private_state_exposed: bool, -} - -fn main() -> Result<()> { - let args = Args::parse()?; - let result = run_smoke(args)?; - println!("{}", serde_json::to_string_pretty(&result)?); - Ok(()) -} - -#[derive(Clone, Debug)] -struct Args { - root: PathBuf, -} - -impl Args { - fn parse() -> Result { - let mut root = env::current_dir().context("failed to resolve current directory")?; - let mut args = env::args().skip(1).peekable(); - while let Some(arg) = args.next() { - match arg.as_str() { - "--root" => root = take_path(&mut args, "--root")?, - other => return Err(anyhow!("unexpected argument {other:?}")), - } - } - Ok(Self { root }) - } -} - -fn run_smoke(args: Args) -> Result { - let root = args - .root - .canonicalize() - .with_context(|| format!("failed to resolve {}", args.root.display()))?; - let smoke_root = root.join(".epiphany-smoke"); - let manifest = root.join("epiphany-core").join("Cargo.toml"); - if !manifest.exists() { - return Err(anyhow!( - "could not find epiphany-core manifest at {}", - manifest.display() - )); - } - fs::create_dir_all(&smoke_root) - .with_context(|| format!("failed to create {}", smoke_root.display()))?; - let stamp = Utc::now().format("%Y%m%d-%H%M%S").to_string(); - let smoke_dir = smoke_root.join(format!("repo-close-mind-adoption-guard-{stamp}")); - fs::create_dir(&smoke_dir) - .with_context(|| format!("failed to claim fresh smoke dir {}", smoke_dir.display()))?; - - let repo = smoke_dir.join("repo-body"); - fs::create_dir_all(&repo).with_context(|| format!("failed to create {}", repo.display()))?; - git(["init"], &repo)?; - git( - ["config", "user.email", "epiphany-smoke@example.invalid"], - &repo, - )?; - git(["config", "user.name", "Epiphany Smoke"], &repo)?; - fs::write( - repo.join("README.md"), - "# Repo Close Mind Adoption Guard Smoke\n\nThis repository proves Soul refuses closure when the Mind adoption proof is tampered.\n", - ) - .with_context(|| format!("failed to seed {}", repo.join("README.md").display()))?; - git(["add", "README.md"], &repo)?; - git( - [ - "commit", - "-m", - "Seed repo close mind adoption guard smoke body", - ], - &repo, - )?; - git( - ["switch", "-c", "epiphany/repo-close-mind-adoption-guard"], - &repo, - )?; - - let item = "repo-close-mind-adoption-guard"; - let target_path = "notes/epiphany-work/repo-close-mind-adoption-guard.md"; - let local_verse = repo.join(".epiphany").join("local-verse.ccmp"); - cargo_json( - &manifest, - "epiphany-repo", - &[ - "init", - "--workspace", - path_str(&repo)?, - "--epiphany-root", - path_str(&root)?, - "--swarm-id", - "repo-close-mind-adoption-guard-smoke", - "--topic", - "repo-close-mind-adoption-guard", - ], - &root, - )?; - cargo_json( - &manifest, - "epiphany-swarm", - &[ - "online", - "--workspace", - path_str(&repo)?, - "--epiphany-root", - path_str(&root)?, - "--runtime-id", - "repo-close-mind-adoption-guard-smoke", - "--local-verse-store", - path_str(&local_verse)?, - ], - &root, - )?; - cargo_json( - &manifest, - "epiphany-work", - &[ - "accept", - "--workspace", - path_str(&repo)?, - "--from", - "persona", - "--item", - item, - "--summary", - "Ask Soul to reject counterfeit Mind adoption proof before closure.", - "--candidate-action-ref", - "candidate-action://repo-close-mind-adoption-guard/tampered-adoption", - "--public-discussion-ref", - "epiphany-global/persona-collaboration/repo-close-mind-adoption-guard", - ], - &root, - )?; - let plan = cargo_json( - &manifest, - "epiphany-work", - &[ - "derive-plan", - "--workspace", - path_str(&repo)?, - "--item", - item, - "--action-family", - "planning-note", - "--model-ref", - "repo-close-mind-adoption-guard-smoke-imagination-v1", - "--model-authored", - ], - &root, - )?; - let run = cargo_json( - &manifest, - "epiphany-work", - &[ - "run", - "--workspace", - path_str(&repo)?, - "--epiphany-root", - path_str(&root)?, - "--item", - item, - "--path", - target_path, - ], - &root, - )?; - let plan_path = value_at_path(&plan, &["receiptPath"]) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("plan output had no receiptPath"))?; - let run_path = value_at_path(&run, &["receiptPath"]) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("run output had no receiptPath"))?; - let adopt = cargo_json( - &manifest, - "epiphany-work", - &[ - "adopt", - "--workspace", - path_str(&repo)?, - "--epiphany-root", - path_str(&root)?, - "--item", - item, - "--run-receipt", - run_path, - "--from-plan", - plan_path, - ], - &root, - )?; - let adopt_path = value_at_path(&adopt, &["receiptPath"]) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("adopt output had no receiptPath"))?; - let execute = cargo_json( - &manifest, - "epiphany-work", - &[ - "execute", - "--workspace", - path_str(&repo)?, - "--epiphany-root", - path_str(&root)?, - "--item", - item, - "--adopt-receipt", - adopt_path, - "--from-plan", - plan_path, - ], - &root, - )?; - let execute_path = value_at_path(&execute, &["receiptPath"]) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("execute output had no receiptPath"))?; - - let mut tampered_adopt = read_json(Path::new(adopt_path))?; - tampered_adopt["mindAdoptionDecision"]["status"] = json!("tampered-after-adoption"); - tampered_adopt["mindAdoptionDecision"]["interpretation"]["classification"]["actionItemAccepted"] = - json!(false); - let tampered_adopt_path = smoke_dir.join("tampered-adopt.json"); - write_json(&tampered_adopt_path, &tampered_adopt)?; - - let mut tampered_execute = read_json(Path::new(execute_path))?; - tampered_execute["adoptReceiptPath"] = json!(path_str(&tampered_adopt_path)?); - let tampered_execute_path = smoke_dir.join("tampered-execute.json"); - write_json(&tampered_execute_path, &tampered_execute)?; - - let tampered_close = cargo_json( - &manifest, - "epiphany-work", - &[ - "close", - "--workspace", - path_str(&repo)?, - "--item", - item, - "--execute-receipt", - path_str(&tampered_execute_path)?, - "--verification-command", - "git status --short", - ], - &root, - )?; - require_eq(&tampered_close, &["status"], "verification-failed")?; - require_eq(&tampered_close, &["soul", "verdict"], "failed")?; - require_eq( - &tampered_close, - &["closureReview", "mindAdoptionReview", "status"], - "failed", - )?; - require_bool( - &tampered_close, - &["closureReview", "sourceGrounding", "mindAdoptionPassed"], - false, - )?; - require_bool( - &tampered_close, - &["closureReview", "mindAdoptionReview", "privateStateExposed"], - false, - )?; - require_assertion( - &tampered_close, - &["closureReview", "mindAdoptionReview", "assertions"], - "decision-status-adopted", - false, - )?; - require_assertion( - &tampered_close, - &["closureReview", "mindAdoptionReview", "assertions"], - "action-item-accepted", - false, - )?; - let repo_map_path = repo - .join(".epiphany") - .join("state") - .join("repo-work-map.msgpack"); - if repo_map_path.exists() { - return Err(anyhow!( - "failed closure wrote repo map store before Mind/Soul passed: {}", - repo_map_path.display() - )); - } - - let close = cargo_json( - &manifest, - "epiphany-work", - &[ - "close", - "--workspace", - path_str(&repo)?, - "--item", - item, - "--execute-receipt", - execute_path, - "--verification-command", - "git status --short", - ], - &root, - )?; - require_eq(&close, &["status"], "closed")?; - require_eq(&close, &["soul", "verdict"], "passed")?; - require_eq( - &close, - &["closureReview", "mindAdoptionReview", "status"], - "passed", - )?; - require_bool( - &close, - &["closureReview", "sourceGrounding", "mindAdoptionPassed"], - true, - )?; - require_bool(&close, &["privateStateExposed"], false)?; - require_eq( - &close, - &["mind", "repoMapEntry", "schemaVersion"], - "epiphany.repo_work_map_entry.v0", - )?; - require_eq( - &close, - &["mind", "repoMapEntry", "safeActionFamily"], - "repo.markdown_planning_note", - )?; - require_bool( - &close, - &["mind", "repoMapEntry", "durableStateAdmitted"], - true, - )?; - require_bool( - &close, - &["mind", "repoMapEntry", "privateStateExposed"], - false, - )?; - require_bool( - &close, - &["mind", "repoMapLocalVerseProjection", "projected"], - true, - )?; - require_eq( - &close, - &["mind", "repoMapLocalVerseProjection", "documentType"], - "epiphany.cultmesh.repo_work_map_entry", - )?; - require_eq( - &close, - &["mind", "repoMapLocalVerseProjection", "latestKey"], - "gamecult-local/repo-work-map/latest", - )?; - require_bool( - &close, - &["mind", "repoMapLocalVerseProjection", "privateStateExposed"], - false, - )?; - let map_store = read_repo_work_map_store(&repo_map_path)?; - if map_store.schema_version != "epiphany.repo_work_map_store.v0" { - return Err(anyhow!( - "unexpected repo map schema {}", - map_store.schema_version - )); - } - if map_store.private_state_exposed { - return Err(anyhow!("repo map store exposed private state")); - } - let map_entry = map_store - .entries - .iter() - .find(|entry| entry.item == item) - .ok_or_else(|| anyhow!("repo map store has no entry for {item}"))?; - if map_entry.schema_version != "epiphany.repo_work_map_entry.v0" { - return Err(anyhow!( - "unexpected repo map entry schema {}", - map_entry.schema_version - )); - } - if map_entry.changed_paths != vec![target_path.to_string()] { - return Err(anyhow!( - "unexpected repo map changed paths {:?}", - map_entry.changed_paths - )); - } - require_struct_eq( - &map_entry.commit_sha, - value_at_path(&close, &["handsReceipts", "commitSha"]) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("close output had no handsReceipts.commitSha"))?, - "repo map commit sha", - )?; - require_struct_eq( - &map_entry.soul_verdict_receipt_id, - value_at_path(&close, &["soul", "verdictReceiptId"]) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("close output had no soul.verdictReceiptId"))?, - "repo map Soul verdict id", - )?; - require_struct_eq( - &map_entry.mind_gateway_review_id, - value_at_path(&close, &["mind", "gatewayReviewId"]) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("close output had no mind.gatewayReviewId"))?, - "repo map Mind gateway id", - )?; - require_struct_eq( - &map_entry.mind_state_commit_receipt_id, - value_at_path(&close, &["mind", "stateCommitReceiptId"]) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("close output had no mind.stateCommitReceiptId"))?, - "repo map Mind commit id", - )?; - require_struct_eq( - &map_entry.safe_action_family, - "repo.markdown_planning_note", - "repo map safe family", - )?; - if !map_entry.durable_state_admitted || map_entry.private_state_exposed { - return Err(anyhow!( - "repo map authority flags were wrong: durable={}, private={}", - map_entry.durable_state_admitted, - map_entry.private_state_exposed - )); - } - let swarm_overview = cargo_json( - &manifest, - "epiphany-verse-query", - &[ - "swarm-overview", - "--store", - path_str(&local_verse)?, - "--runtime-id", - "repo-close-mind-adoption-guard-smoke", - ], - &root, - )?; - require_eq( - &swarm_overview, - &["latestRepoWorkMapEntry"], - "repo-work-map-repo-close-mind-adoption-guard", - )?; - require_bool(&swarm_overview, &["privateStateExposed"], false)?; - let map_rows = value_at_path(&swarm_overview, &["repoWorkMapRows"]) - .and_then(Value::as_array) - .ok_or_else(|| anyhow!("Swarm overview output had no repoWorkMapRows array"))?; - let swarm_overview_map_row = map_rows - .iter() - .find(|row| row.get("item").and_then(Value::as_str) == Some(item)) - .ok_or_else(|| anyhow!("Swarm overview output had no repo work map row for {item}"))?; - require_eq_value( - swarm_overview_map_row, - &["safeActionFamily"], - "repo.markdown_planning_note", - )?; - require_eq_value( - swarm_overview_map_row, - &["mindStateCommitReceiptId"], - &map_entry.mind_state_commit_receipt_id, - )?; - require_bool_value(swarm_overview_map_row, &["privateStateExposed"], false)?; - let map_tui_rows = value_at_path(&swarm_overview, &["repoWorkMapTuiRows"]) - .and_then(Value::as_array) - .ok_or_else(|| anyhow!("Swarm overview output had no repoWorkMapTuiRows array"))?; - if !map_tui_rows.iter().any(|row| { - row.as_str() - .is_some_and(|row| row.contains("REPO-WORK-MAP") && row.contains(item)) - }) { - return Err(anyhow!( - "Swarm overview TUI rows did not expose compact repo map sight" - )); - } - require_u64(&swarm_overview, &["repoWorkMapSemanticCount"], 1)?; - let semantic_rows = value_at_path(&swarm_overview, &["repoWorkMapSemanticRows"]) - .and_then(Value::as_array) - .ok_or_else(|| anyhow!("Swarm overview output had no repoWorkMapSemanticRows array"))?; - let semantic_row = semantic_rows - .iter() - .find(|row| row.get("item").and_then(Value::as_str) == Some(item)) - .ok_or_else(|| anyhow!("Swarm overview output had no repo map semantic row for {item}"))?; - require_eq_value(semantic_row, &["stage"], "imagination-planning")?; - require_eq_value(semantic_row, &["stageOwner"], "Imagination")?; - require_eq_value(semantic_row, &["publicationGate"], "Bifrost")?; - require_eq_value(semantic_row, &["gateOwner"], "Bifrost")?; - require_eq_value( - semantic_row, - &["modelingSummary"], - &map_entry.modeling_summary, - )?; - require_bool_value(semantic_row, &["sightOnly"], true)?; - require_bool_value(semantic_row, &["privateStateExposed"], false)?; - let semantic_tui_rows = value_at_path(&swarm_overview, &["repoWorkMapSemanticTuiRows"]) - .and_then(Value::as_array) - .ok_or_else(|| anyhow!("Swarm overview output had no repoWorkMapSemanticTuiRows array"))?; - if !semantic_tui_rows.iter().any(|row| { - row.as_str().is_some_and(|row| { - row.contains("REPO-WORK-MAP-SEMANTIC") - && row.contains("stage=imagination-planning") - && row.contains("stageOwner=Imagination") - && row.contains("gateOwner=Bifrost") - && row.contains("sightOnly=true") - }) - }) { - return Err(anyhow!( - "Swarm overview TUI rows did not expose repo map semantic sight" - )); - } - require_u64(&swarm_overview, &["repoWorkMapFamilyLensCount"], 1)?; - let family_lens_rows = value_at_path(&swarm_overview, &["repoWorkMapFamilyLensRows"]) - .and_then(Value::as_array) - .ok_or_else(|| anyhow!("Swarm overview output had no repoWorkMapFamilyLensRows array"))?; - let family_lens_row = family_lens_rows - .iter() - .find(|row| { - row.get("safeActionFamily").and_then(Value::as_str) - == Some("repo.markdown_planning_note") - }) - .ok_or_else(|| anyhow!("Swarm overview output had no repo map family lens row"))?; - require_eq_value(family_lens_row, &["latestItem"], item)?; - require_eq_value( - family_lens_row, - &["latestMindStateCommitReceiptId"], - &map_entry.mind_state_commit_receipt_id, - )?; - require_bool_value(family_lens_row, &["privateStateExposed"], false)?; - let family_lens_tui_rows = value_at_path(&swarm_overview, &["repoWorkMapFamilyLensTuiRows"]) - .and_then(Value::as_array) - .ok_or_else(|| { - anyhow!("Swarm overview output had no repoWorkMapFamilyLensTuiRows array") - })?; - if !family_lens_tui_rows.iter().any(|row| { - row.as_str().is_some_and(|row| { - row.contains("REPO-WORK-MAP-LENS") && row.contains("repo.markdown_planning_note") - }) - }) { - return Err(anyhow!( - "Swarm overview TUI rows did not expose repo map family lens sight" - )); - } - require_u64(&swarm_overview, &["repoWorkMapPathLensCount"], 1)?; - let path_lens_rows = value_at_path(&swarm_overview, &["repoWorkMapPathLensRows"]) - .and_then(Value::as_array) - .ok_or_else(|| anyhow!("Swarm overview output had no repoWorkMapPathLensRows array"))?; - let path_lens_row = path_lens_rows - .iter() - .find(|row| row.get("path").and_then(Value::as_str) == Some(target_path)) - .ok_or_else(|| anyhow!("Swarm overview output had no repo map path lens row"))?; - require_eq_value(path_lens_row, &["latestItem"], item)?; - require_eq_value( - path_lens_row, - &["latestMindStateCommitReceiptId"], - &map_entry.mind_state_commit_receipt_id, - )?; - require_bool_value(path_lens_row, &["privateStateExposed"], false)?; - let path_lens_tui_rows = value_at_path(&swarm_overview, &["repoWorkMapPathLensTuiRows"]) - .and_then(Value::as_array) - .ok_or_else(|| anyhow!("Swarm overview output had no repoWorkMapPathLensTuiRows array"))?; - if !path_lens_tui_rows.iter().any(|row| { - row.as_str() - .is_some_and(|row| row.contains("REPO-WORK-MAP-PATH") && row.contains(target_path)) - }) { - return Err(anyhow!( - "Swarm overview TUI rows did not expose repo map path lens sight" - )); - } - require_u64(&swarm_overview, &["repoWorkMapBranchLensCount"], 1)?; - let branch_lens_rows = value_at_path(&swarm_overview, &["repoWorkMapBranchLensRows"]) - .and_then(Value::as_array) - .ok_or_else(|| anyhow!("Swarm overview output had no repoWorkMapBranchLensRows array"))?; - let branch_lens_row = branch_lens_rows - .iter() - .find(|row| { - row.get("branch").and_then(Value::as_str) - == Some("epiphany/repo-close-mind-adoption-guard") - }) - .ok_or_else(|| anyhow!("Swarm overview output had no repo map branch lens row"))?; - require_eq_value(branch_lens_row, &["latestItem"], item)?; - require_eq_value( - branch_lens_row, - &["latestMindStateCommitReceiptId"], - &map_entry.mind_state_commit_receipt_id, - )?; - require_bool_value(branch_lens_row, &["privateStateExposed"], false)?; - let branch_lens_tui_rows = value_at_path(&swarm_overview, &["repoWorkMapBranchLensTuiRows"]) - .and_then(Value::as_array) - .ok_or_else(|| { - anyhow!("Swarm overview output had no repoWorkMapBranchLensTuiRows array") - })?; - if !branch_lens_tui_rows.iter().any(|row| { - row.as_str().is_some_and(|row| { - row.contains("REPO-WORK-MAP-BRANCH") - && row.contains("epiphany/repo-close-mind-adoption-guard") - }) - }) { - return Err(anyhow!( - "Swarm overview TUI rows did not expose repo map branch lens sight" - )); - } - require_u64(&swarm_overview, &["repoWorkMapStageLensCount"], 1)?; - let stage_lens_rows = value_at_path(&swarm_overview, &["repoWorkMapStageLensRows"]) - .and_then(Value::as_array) - .ok_or_else(|| anyhow!("Swarm overview output had no repoWorkMapStageLensRows array"))?; - let stage_lens_row = stage_lens_rows - .iter() - .find(|row| row.get("stage").and_then(Value::as_str) == Some("imagination-planning")) - .ok_or_else(|| anyhow!("Swarm overview output had no repo map stage lens row"))?; - require_eq_value(stage_lens_row, &["owner"], "Imagination")?; - require_eq_value(stage_lens_row, &["latestItem"], item)?; - require_eq_value( - stage_lens_row, - &["latestMindStateCommitReceiptId"], - &map_entry.mind_state_commit_receipt_id, - )?; - require_bool_value(stage_lens_row, &["privateStateExposed"], false)?; - let stage_lens_tui_rows = value_at_path(&swarm_overview, &["repoWorkMapStageLensTuiRows"]) - .and_then(Value::as_array) - .ok_or_else(|| anyhow!("Swarm overview output had no repoWorkMapStageLensTuiRows array"))?; - if !stage_lens_tui_rows.iter().any(|row| { - row.as_str().is_some_and(|row| { - row.contains("REPO-WORK-MAP-STAGE") - && row.contains("stage=imagination-planning") - && row.contains("owner=Imagination") - }) - }) { - return Err(anyhow!( - "Swarm overview TUI rows did not expose repo map stage lens sight" - )); - } - require_u64(&swarm_overview, &["repoWorkMapGateLensCount"], 1)?; - let gate_lens_rows = value_at_path(&swarm_overview, &["repoWorkMapGateLensRows"]) - .and_then(Value::as_array) - .ok_or_else(|| anyhow!("Swarm overview output had no repoWorkMapGateLensRows array"))?; - let gate_lens_row = gate_lens_rows - .iter() - .find(|row| row.get("publicationGate").and_then(Value::as_str) == Some("Bifrost")) - .ok_or_else(|| anyhow!("Swarm overview output had no repo map gate lens row"))?; - require_eq_value(gate_lens_row, &["owner"], "Bifrost")?; - require_eq_value(gate_lens_row, &["latestItem"], item)?; - require_eq_value( - gate_lens_row, - &["latestMindStateCommitReceiptId"], - &map_entry.mind_state_commit_receipt_id, - )?; - require_bool_value(gate_lens_row, &["sightOnly"], true)?; - require_bool_value(gate_lens_row, &["privateStateExposed"], false)?; - let gate_lens_tui_rows = value_at_path(&swarm_overview, &["repoWorkMapGateLensTuiRows"]) - .and_then(Value::as_array) - .ok_or_else(|| anyhow!("Swarm overview output had no repoWorkMapGateLensTuiRows array"))?; - if !gate_lens_tui_rows.iter().any(|row| { - row.as_str().is_some_and(|row| { - row.contains("REPO-WORK-MAP-GATE") - && row.contains("gate=Bifrost") - && row.contains("owner=Bifrost") - && row.contains("sightOnly=true") - }) - }) { - return Err(anyhow!( - "Swarm overview TUI rows did not expose repo map gate lens sight" - )); - } - require_u64(&swarm_overview, &["repoWorkMapClosureCount"], 1)?; - let closure_rows = value_at_path(&swarm_overview, &["repoWorkMapClosureRows"]) - .and_then(Value::as_array) - .ok_or_else(|| anyhow!("Swarm overview output had no repoWorkMapClosureRows array"))?; - let closure_row = closure_rows - .iter() - .find(|row| row.get("item").and_then(Value::as_str) == Some(item)) - .ok_or_else(|| anyhow!("Swarm overview output had no repo map closure row"))?; - require_eq_value( - closure_row, - &["safeActionFamily"], - "repo.markdown_planning_note", - )?; - require_eq_value( - closure_row, - &["soulVerdictReceiptId"], - close["soul"]["verdictReceiptId"] - .as_str() - .unwrap_or(""), - )?; - require_eq_value( - closure_row, - &["mindGatewayReviewId"], - &map_entry.mind_gateway_review_id, - )?; - require_eq_value( - closure_row, - &["mindStateCommitReceiptId"], - &map_entry.mind_state_commit_receipt_id, - )?; - require_bool_value(closure_row, &["durableStateAdmitted"], true)?; - require_eq_value(closure_row, &["publicationGate"], "Bifrost")?; - require_bool_value(closure_row, &["publicationAuthorized"], false)?; - require_bool_value(closure_row, &["mergeAuthorized"], false)?; - require_bool_value(closure_row, &["serviceLifecycleAuthorized"], false)?; - require_bool_value(closure_row, &["deploymentExecutionAuthorized"], false)?; - require_bool_value(closure_row, &["crossRepoMutationAuthorized"], false)?; - require_bool_value(closure_row, &["sightOnly"], true)?; - require_bool_value(closure_row, &["privateStateExposed"], false)?; - let closure_tui_rows = value_at_path(&swarm_overview, &["repoWorkMapClosureTuiRows"]) - .and_then(Value::as_array) - .ok_or_else(|| anyhow!("Swarm overview output had no repoWorkMapClosureTuiRows array"))?; - if !closure_tui_rows.iter().any(|row| { - row.as_str().is_some_and(|row| { - row.contains("REPO-WORK-MAP-CLOSURE") - && row.contains("publicationGate=Bifrost") - && row.contains("publicationAuth=false") - && row.contains("mergeAuth=false") - && row.contains("serviceAuth=false") - && row.contains("deploymentAuth=false") - && row.contains("crossRepoAuth=false") - && row.contains("sightOnly=true") - }) - }) { - return Err(anyhow!( - "Swarm overview TUI rows did not expose repo map closure sight" - )); - } - - require_u64(&swarm_overview, &["repoWorkMapAcceptanceCount"], 1)?; - let acceptance_rows = value_at_path(&swarm_overview, &["repoWorkMapAcceptanceRows"]) - .and_then(Value::as_array) - .ok_or_else(|| anyhow!("Swarm overview output had no repoWorkMapAcceptanceRows array"))?; - let acceptance_row = acceptance_rows - .iter() - .find(|row| { - value_at_path(row, &["item"]) - .and_then(Value::as_str) - .is_some_and(|value| value == item) - }) - .ok_or_else(|| anyhow!("Swarm overview output had no acceptance row for {item}"))?; - require_eq_value( - acceptance_row, - &["safeActionFamily"], - "repo.markdown_planning_note", - )?; - require_eq_value( - acceptance_row, - &["acceptanceStatus"], - "accepted-awaiting-publication-gate", - )?; - require_bool_value(acceptance_row, &["soulClosed"], true)?; - require_bool_value(acceptance_row, &["modelingClosed"], true)?; - require_bool_value(acceptance_row, &["mindCommitted"], true)?; - require_bool_value(acceptance_row, &["durableStateAdmitted"], true)?; - require_bool_value(acceptance_row, &["bifrostGateNamed"], true)?; - require_eq_value( - acceptance_row, - &["soulVerdictReceiptId"], - close["soul"]["verdictReceiptId"] - .as_str() - .unwrap_or(""), - )?; - require_eq_value( - acceptance_row, - &["mindGatewayReviewId"], - &map_entry.mind_gateway_review_id, - )?; - require_eq_value( - acceptance_row, - &["mindStateCommitReceiptId"], - &map_entry.mind_state_commit_receipt_id, - )?; - require_eq_value(acceptance_row, &["publicationGate"], "Bifrost")?; - require_bool_value(acceptance_row, &["sightOnly"], true)?; - require_bool_value(acceptance_row, &["privateStateExposed"], false)?; - let acceptance_tui_rows = value_at_path(&swarm_overview, &["repoWorkMapAcceptanceTuiRows"]) - .and_then(Value::as_array) - .ok_or_else(|| { - anyhow!("Swarm overview output had no repoWorkMapAcceptanceTuiRows array") - })?; - if !acceptance_tui_rows.iter().any(|row| { - row.as_str().is_some_and(|row| { - row.contains("REPO-WORK-MAP-ACCEPTANCE") - && row.contains("status=accepted-awaiting-publication-gate") - && row.contains("soulClosed=true") - && row.contains("modelingClosed=true") - && row.contains("mindCommitted=true") - && row.contains("durableAdmitted=true") - && row.contains("publicationGate=Bifrost") - && row.contains("bifrostGateNamed=true") - && row.contains("sightOnly=true") - }) - }) { - return Err(anyhow!( - "Swarm overview TUI rows did not expose repo map acceptance sight" - )); - } - - let summary = json!({ - "schemaVersion": "epiphany.repo_close_mind_adoption_guard_smoke.v0", - "status": "ok", - "smokeDir": smoke_dir, - "repo": repo, - "branch": git_output(["branch", "--show-current"], &repo)?, - "item": item, - "targetPath": target_path, - "tamperedCloseStatus": tampered_close["status"], - "tamperedSoulVerdict": tampered_close["soul"]["verdict"], - "tamperedMindAdoptionReviewStatus": tampered_close["closureReview"]["mindAdoptionReview"]["status"], - "closeStatus": close["status"], - "soulVerdict": close["soul"]["verdict"], - "mindAdoptionReviewStatus": close["closureReview"]["mindAdoptionReview"]["status"], - "repoMapStorePath": repo_map_path, - "repoMapEntrySchema": map_entry.schema_version, - "repoMapDurableStateAdmitted": map_entry.durable_state_admitted, - "repoMapLocalVerseProjected": true, - "swarmOverviewLatestRepoWorkMapEntry": swarm_overview["latestRepoWorkMapEntry"], - "swarmOverviewRepoWorkMapSemanticCount": swarm_overview["repoWorkMapSemanticCount"], - "swarmOverviewRepoWorkMapFamilyLensCount": swarm_overview["repoWorkMapFamilyLensCount"], - "swarmOverviewRepoWorkMapPathLensCount": swarm_overview["repoWorkMapPathLensCount"], - "swarmOverviewRepoWorkMapBranchLensCount": swarm_overview["repoWorkMapBranchLensCount"], - "swarmOverviewRepoWorkMapStageLensCount": swarm_overview["repoWorkMapStageLensCount"], - "swarmOverviewRepoWorkMapGateLensCount": swarm_overview["repoWorkMapGateLensCount"], - "swarmOverviewRepoWorkMapClosureCount": swarm_overview["repoWorkMapClosureCount"], - "swarmOverviewRepoWorkMapAcceptanceCount": swarm_overview["repoWorkMapAcceptanceCount"], - "swarmOverviewRepoWorkMapAcceptanceStatus": acceptance_row["acceptanceStatus"], - "publicationAuthorized": false, - "privateStateExposed": false - }); - write_json(&smoke_dir.join("summary.json"), &summary)?; - Ok(summary) -} - -fn cargo_json(manifest: &Path, bin: &str, args: &[&str], cwd: &Path) -> Result { - let output = Command::new("cargo") - .arg("run") - .arg("--quiet") - .arg("--manifest-path") - .arg(manifest) - .arg("--bin") - .arg(bin) - .arg("--") - .args(args) - .current_dir(cwd) - .output() - .with_context(|| format!("failed to run cargo bin {bin}"))?; - if !output.status.success() { - return Err(anyhow!( - "{bin} failed with status {:?}\nstdout:\n{}\nstderr:\n{}", - output.status.code(), - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )); - } - serde_json::from_slice(&output.stdout).with_context(|| { - format!( - "{bin} did not emit JSON on stdout:\n{}", - String::from_utf8_lossy(&output.stdout) - ) - }) -} - -fn git(args: [&str; N], cwd: &Path) -> Result<()> { - let output = Command::new("git") - .args(args) - .current_dir(cwd) - .output() - .with_context(|| format!("failed to run git in {}", cwd.display()))?; - if output.status.success() { - Ok(()) - } else { - Err(anyhow!( - "git failed in {} with status {:?}\nstdout:\n{}\nstderr:\n{}", - cwd.display(), - output.status.code(), - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )) - } -} - -fn git_output(args: [&str; N], cwd: &Path) -> Result { - let output = Command::new("git") - .args(args) - .current_dir(cwd) - .output() - .with_context(|| format!("failed to run git in {}", cwd.display()))?; - if !output.status.success() { - return Err(anyhow!( - "git failed in {} with status {:?}\nstdout:\n{}\nstderr:\n{}", - cwd.display(), - output.status.code(), - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )); - } - Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) -} - -fn read_json(path: &Path) -> Result { - let raw = - fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?; - serde_json::from_str(&raw).with_context(|| format!("failed to decode {}", path.display())) -} - -fn write_json(path: &Path, value: &Value) -> Result<()> { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("failed to create {}", parent.display()))?; - } - fs::write(path, serde_json::to_vec_pretty(value)?) - .with_context(|| format!("failed to write {}", path.display())) -} - -fn read_repo_work_map_store(path: &Path) -> Result { - let bytes = fs::read(path).with_context(|| format!("failed to read {}", path.display()))?; - rmp_serde::from_slice(&bytes).with_context(|| format!("failed to decode {}", path.display())) -} - -fn require_struct_eq(actual: &str, expected: &str, label: &str) -> Result<()> { - if actual == expected { - Ok(()) - } else { - Err(anyhow!( - "expected {label} to be {:?}, got {:?}", - expected, - actual - )) - } -} - -fn require_eq(value: &Value, path: &[&str], expected: &str) -> Result<()> { - let actual = value_at_path(value, path) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("missing string at {}", path.join(".")))?; - if actual == expected { - Ok(()) - } else { - Err(anyhow!( - "expected {} to be {:?}, got {:?}", - path.join("."), - expected, - actual - )) - } -} - -fn require_bool(value: &Value, path: &[&str], expected: bool) -> Result<()> { - let actual = value_at_path(value, path) - .and_then(Value::as_bool) - .ok_or_else(|| anyhow!("missing bool at {}", path.join(".")))?; - if actual == expected { - Ok(()) - } else { - Err(anyhow!( - "expected {} to be {}, got {}", - path.join("."), - expected, - actual - )) - } -} - -fn require_u64(value: &Value, path: &[&str], expected: u64) -> Result<()> { - let actual = value_at_path(value, path) - .and_then(Value::as_u64) - .ok_or_else(|| anyhow!("missing unsigned integer at {}", path.join(".")))?; - if actual == expected { - Ok(()) - } else { - Err(anyhow!( - "expected {} to be {}, got {}", - path.join("."), - expected, - actual - )) - } -} - -fn require_eq_value(value: &Value, path: &[&str], expected: &str) -> Result<()> { - let actual = value_at_path(value, path) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("missing string at {}", path.join(".")))?; - if actual == expected { - Ok(()) - } else { - Err(anyhow!( - "expected {} to be {:?}, got {:?}", - path.join("."), - expected, - actual - )) - } -} - -fn require_bool_value(value: &Value, path: &[&str], expected: bool) -> Result<()> { - let actual = value_at_path(value, path) - .and_then(Value::as_bool) - .ok_or_else(|| anyhow!("missing bool at {}", path.join(".")))?; - if actual == expected { - Ok(()) - } else { - Err(anyhow!( - "expected {} to be {}, got {}", - path.join("."), - expected, - actual - )) - } -} - -fn require_assertion( - value: &Value, - assertions_path: &[&str], - assertion_id: &str, - expected: bool, -) -> Result<()> { - let assertions = value_at_path(value, assertions_path) - .and_then(Value::as_array) - .ok_or_else(|| anyhow!("missing assertions at {}", assertions_path.join(".")))?; - let assertion = assertions - .iter() - .find(|entry| entry.get("assertionId").and_then(Value::as_str) == Some(assertion_id)) - .ok_or_else(|| anyhow!("missing assertion {assertion_id}"))?; - require_bool(assertion, &["passed"], expected) -} - -fn value_at_path<'a>(value: &'a Value, path: &[&str]) -> Option<&'a Value> { - let mut cursor = value; - for segment in path { - cursor = cursor.get(*segment)?; - } - Some(cursor) -} - -fn take_path(args: &mut std::iter::Peekable, name: &str) -> Result -where - I: Iterator, -{ - args.next() - .map(PathBuf::from) - .ok_or_else(|| anyhow!("missing value for {name}")) -} - -fn path_str(path: &Path) -> Result<&str> { - path.to_str() - .ok_or_else(|| anyhow!("path is not valid UTF-8: {}", path.display())) -} diff --git a/epiphany-core/src/bin/epiphany-repo-doctrine-review-family-smoke.rs b/epiphany-core/src/bin/epiphany-repo-doctrine-review-family-smoke.rs deleted file mode 100644 index cad4c0b0..00000000 --- a/epiphany-core/src/bin/epiphany-repo-doctrine-review-family-smoke.rs +++ /dev/null @@ -1,454 +0,0 @@ -use anyhow::Context; -use anyhow::Result; -use anyhow::anyhow; -use chrono::Utc; -use serde_json::Value; -use serde_json::json; -use std::env; -use std::fs; -use std::path::Path; -use std::path::PathBuf; -use std::process::Command; - -fn main() -> Result<()> { - let args = Args::parse()?; - let result = run_smoke(args)?; - println!("{}", serde_json::to_string_pretty(&result)?); - Ok(()) -} - -#[derive(Clone, Debug)] -struct Args { - root: PathBuf, -} - -impl Args { - fn parse() -> Result { - let mut root = env::current_dir().context("failed to resolve current directory")?; - let mut args = env::args().skip(1).peekable(); - while let Some(arg) = args.next() { - match arg.as_str() { - "--root" => root = take_path(&mut args, "--root")?, - other => return Err(anyhow!("unexpected argument {other:?}")), - } - } - Ok(Self { root }) - } -} - -fn run_smoke(args: Args) -> Result { - let root = args - .root - .canonicalize() - .with_context(|| format!("failed to resolve {}", args.root.display()))?; - let smoke_root = root.join(".epiphany-smoke"); - let manifest = root.join("epiphany-core").join("Cargo.toml"); - if !manifest.exists() { - return Err(anyhow!( - "could not find epiphany-core manifest at {}", - manifest.display() - )); - } - fs::create_dir_all(&smoke_root) - .with_context(|| format!("failed to create {}", smoke_root.display()))?; - let stamp = Utc::now().format("%Y%m%d-%H%M%S").to_string(); - let smoke_dir = smoke_root.join(format!("repo-doctrine-update-request-family-{stamp}")); - fs::create_dir(&smoke_dir) - .with_context(|| format!("failed to claim fresh smoke dir {}", smoke_dir.display()))?; - - let repo = smoke_dir.join("repo-body"); - fs::create_dir_all(&repo).with_context(|| format!("failed to create {}", repo.display()))?; - git(["init"], &repo)?; - git( - ["config", "user.email", "epiphany-smoke@example.invalid"], - &repo, - )?; - git(["config", "user.name", "Epiphany Smoke"], &repo)?; - fs::write( - repo.join("README.md"), - "# Repo Doctrine Update Request Family Smoke\n\nThis repository proves branch-local doctrine update request cargo without direct doctrine mutation authority.\n", - ) - .with_context(|| format!("failed to seed {}", repo.join("README.md").display()))?; - fs::write( - repo.join("AGENTS.md"), - "# Repo Agent Doctrine\n\nExisting doctrine stays untouched by the request artifact.\n", - ) - .with_context(|| format!("failed to seed {}", repo.join("AGENTS.md").display()))?; - git(["add", "README.md", "AGENTS.md"], &repo)?; - git( - [ - "commit", - "-m", - "Seed repo doctrine update request smoke body", - ], - &repo, - )?; - git( - [ - "switch", - "-c", - "epiphany/repo-doctrine-update-request-family", - ], - &repo, - )?; - - let item = "repo-doctrine-update-request-family"; - let target_path = ".epiphany/doctrine-update-requests/repo-doctrine-update-request-family.toml"; - let local_verse = repo.join(".epiphany").join("local-verse.ccmp"); - cargo_json( - &manifest, - "epiphany-repo", - &[ - "init", - "--workspace", - path_str(&repo)?, - "--epiphany-root", - path_str(&root)?, - "--swarm-id", - "repo-doctrine-update-request-family-smoke", - "--topic", - "repo-doctrine-update-request-family", - ], - &root, - )?; - cargo_json( - &manifest, - "epiphany-swarm", - &[ - "online", - "--workspace", - path_str(&repo)?, - "--epiphany-root", - path_str(&root)?, - "--runtime-id", - "repo-doctrine-update-request-family-smoke", - "--local-verse-store", - path_str(&local_verse)?, - ], - &root, - )?; - cargo_json( - &manifest, - "epiphany-work", - &[ - "accept", - "--workspace", - path_str(&repo)?, - "--from", - "persona", - "--item", - item, - "--summary", - "Ask for a reviewed AGENTS.md doctrine update while preserving Mind, Soul, and maintainer gates.", - "--candidate-action-ref", - "candidate-action://repo-doctrine-update-request-family/agents-update", - "--public-discussion-ref", - "epiphany-global/persona-collaboration/repo-doctrine-update-request-family", - ], - &root, - )?; - let plan = cargo_json( - &manifest, - "epiphany-work", - &[ - "derive-plan", - "--workspace", - path_str(&repo)?, - "--item", - item, - "--action-family", - "repo-doctrine-update-request", - "--model-ref", - "repo-doctrine-update-request-family-smoke-imagination-v1", - "--model-authored", - ], - &root, - )?; - for _ in 0..4 { - cargo_json( - &manifest, - "epiphany-work", - &[ - "tick", - "--workspace", - path_str(&repo)?, - "--epiphany-root", - path_str(&root)?, - "--item", - item, - "--cooldown-seconds", - "0", - ], - &root, - )?; - } - - let close = read_json( - &repo - .join(".epiphany") - .join("work") - .join(format!("work-close-{item}.json")), - )?; - let request_text = fs::read_to_string(repo.join(target_path)) - .with_context(|| format!("failed to read {}", repo.join(target_path).display()))?; - let agents_text = fs::read_to_string(repo.join("AGENTS.md")) - .with_context(|| format!("failed to read {}", repo.join("AGENTS.md").display()))?; - - require_eq( - &plan, - &["derivation", "safeActionFamily"], - "repo.doctrine_update_request", - )?; - require_eq(&close, &["status"], "closed")?; - require_eq(&close, &["soul", "verdict"], "passed")?; - require_eq( - &close, - &["closureReview", "familyAssertions", "status"], - "passed", - )?; - require_bool( - &close, - &["closureReview", "sourceGrounding", "pathScopeMatched"], - true, - )?; - require_bool(&close, &["privateStateExposed"], false)?; - require_text( - &request_text, - "schema_version = \"epiphany.repo_doctrine_update_request.v0\"", - )?; - require_text( - &request_text, - "safe_action_family = \"repo.doctrine_update_request\"", - )?; - require_text(&request_text, "[request]")?; - require_text(&request_text, "status = \"awaiting-doctrine-review\"")?; - require_text(&request_text, "routing_owner = \"Self\"")?; - require_text( - &request_text, - "required_reviewers = [\"Maintainer\", \"Mind\", \"Soul\"]", - )?; - require_text(&request_text, "doctrine_admission_owner = \"Mind\"")?; - require_text(&request_text, "mutation_owner = \"Hands\"")?; - require_text( - &request_text, - "requested_effect = \"review-repo-agent-doctrine-update\"", - )?; - require_text(&request_text, "doctrine_target = \"AGENTS.md\"")?; - require_text(&request_text, "requires_source_grounding = true")?; - require_text(&request_text, "maintainer_review_required = true")?; - require_text(&request_text, "[antecedents]")?; - require_text(&request_text, "persona_or_human_feedback_required = true")?; - require_text(&request_text, "imagination_plan_required = true")?; - require_text(&request_text, "mind_adoption_required = true")?; - require_text(&request_text, "soul_review_required = true")?; - require_text(&request_text, "maintainer_review_required = true")?; - require_text(&request_text, "[required_receipts]")?; - require_text( - &request_text, - "imagination_plan = \"epiphany.repo_work_imagination_action_items_receipt.v0\"", - )?; - require_text( - &request_text, - "mind_adoption = \"epiphany.repo_work_mind_adoption_decision.v0\"", - )?; - require_text( - &request_text, - "soul_review = \"epiphany.repo_work_closure_review.v0\"", - )?; - require_text( - &request_text, - "maintainer_review = \"gamecult.maintainer.review_receipt.v0\"", - )?; - require_text( - &request_text, - "hands_commit = \"epiphany.hands.commit_receipt\"", - )?; - require_text(&request_text, "[doctrine_packet]")?; - require_text(&request_text, "requires_current_doctrine_ref = true")?; - require_text(&request_text, "requires_proposed_change_summary = true")?; - require_text(&request_text, "requires_invariant_impact = true")?; - require_text(&request_text, "requires_rehydration_impact = true")?; - require_text(&request_text, "requires_rollback_plan = true")?; - require_text( - &request_text, - "requires_private_state_redaction_check = true", - )?; - require_text(&request_text, "[authority]")?; - require_text(&request_text, "direct_doctrine_mutation_authority = false")?; - require_text(&request_text, "direct_hands_authority = false")?; - require_text(&request_text, "direct_mind_state_commit = false")?; - require_text(&request_text, "publication_authorized = false")?; - require_text(&request_text, "merge_authorized = false")?; - require_text(&request_text, "service_lifecycle_authority = false")?; - require_text(&request_text, "cross_body_mutation_authorized = false")?; - require_text(&request_text, "private_verse_rummaging = false")?; - require_text(&request_text, "maintainer_review_required = true")?; - require_text(&request_text, "mind_admission_required = true")?; - require_text(&request_text, "hands_receipts_required = true")?; - require_text(&request_text, "private_state_exposed = false")?; - require_text( - &agents_text, - "Existing doctrine stays untouched by the request artifact.", - )?; - - let summary = json!({ - "schemaVersion": "epiphany.repo_doctrine_update_request_family_smoke.v0", - "status": "ok", - "smokeDir": smoke_dir, - "repo": repo, - "branch": git_output(["branch", "--show-current"], &repo)?, - "item": item, - "safeActionFamily": plan["derivation"]["safeActionFamily"], - "targetPath": target_path, - "closeStatus": close["status"], - "soulVerdict": close["soul"]["verdict"], - "familyAssertionsStatus": close["closureReview"]["familyAssertions"]["status"], - "pathScopeMatched": close["closureReview"]["sourceGrounding"]["pathScopeMatched"], - "awaitingDoctrineReview": true, - "doctrineMutationAuthorized": false, - "handsAuthorityGranted": false, - "publicationAuthorized": false, - "mergeAuthorized": false, - "serviceLifecycleAuthority": false, - "privateStateExposed": false, - }); - write_json(&smoke_dir.join("summary.json"), &summary)?; - Ok(summary) -} - -fn cargo_json(manifest: &Path, bin: &str, args: &[&str], cwd: &Path) -> Result { - let output = Command::new("cargo") - .arg("run") - .arg("--quiet") - .arg("--manifest-path") - .arg(manifest) - .arg("--bin") - .arg(bin) - .arg("--") - .args(args) - .current_dir(cwd) - .output() - .with_context(|| format!("failed to run cargo bin {bin}"))?; - if !output.status.success() { - return Err(anyhow!( - "{bin} failed with status {:?}\nstdout:\n{}\nstderr:\n{}", - output.status.code(), - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )); - } - serde_json::from_slice(&output.stdout).with_context(|| { - format!( - "{bin} did not emit JSON on stdout:\n{}", - String::from_utf8_lossy(&output.stdout) - ) - }) -} - -fn git(args: [&str; N], cwd: &Path) -> Result<()> { - let output = Command::new("git") - .args(args) - .current_dir(cwd) - .output() - .with_context(|| format!("failed to run git in {}", cwd.display()))?; - if output.status.success() { - Ok(()) - } else { - Err(anyhow!( - "git failed in {} with status {:?}\nstdout:\n{}\nstderr:\n{}", - cwd.display(), - output.status.code(), - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )) - } -} - -fn git_output(args: [&str; N], cwd: &Path) -> Result { - let output = Command::new("git") - .args(args) - .current_dir(cwd) - .output() - .with_context(|| format!("failed to run git in {}", cwd.display()))?; - if output.status.success() { - Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) - } else { - Err(anyhow!( - "git failed in {} with status {:?}\nstdout:\n{}\nstderr:\n{}", - cwd.display(), - output.status.code(), - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )) - } -} - -fn read_json(path: &Path) -> Result { - let bytes = fs::read(path).with_context(|| format!("failed to read {}", path.display()))?; - serde_json::from_slice(&bytes).with_context(|| format!("failed to parse {}", path.display())) -} - -fn write_json(path: &Path, value: &Value) -> Result<()> { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("failed to create {}", parent.display()))?; - } - fs::write(path, serde_json::to_vec_pretty(value)?) - .with_context(|| format!("failed to write {}", path.display())) -} - -fn require_eq(value: &Value, path: &[&str], expected: &str) -> Result<()> { - let actual = path - .iter() - .try_fold(value, |current, key| current.get(*key)) - .and_then(Value::as_str) - .unwrap_or(""); - if actual == expected { - Ok(()) - } else { - Err(anyhow!( - "expected {} to be {expected:?}, got {actual:?}", - path.join(".") - )) - } -} - -fn require_bool(value: &Value, path: &[&str], expected: bool) -> Result<()> { - let actual = path - .iter() - .try_fold(value, |current, key| current.get(*key)) - .and_then(Value::as_bool); - if actual == Some(expected) { - Ok(()) - } else { - Err(anyhow!( - "expected {} to be {expected}, got {:?}", - path.join("."), - actual - )) - } -} - -fn require_text(haystack: &str, needle: &str) -> Result<()> { - if haystack.contains(needle) { - Ok(()) - } else { - Err(anyhow!( - "expected doctrine update request text to contain {needle:?}" - )) - } -} - -fn take_path( - args: &mut std::iter::Peekable>, - flag: &str, -) -> Result { - args.next() - .map(PathBuf::from) - .with_context(|| format!("missing value for {flag}")) -} - -fn path_str(path: &Path) -> Result<&str> { - path.to_str() - .ok_or_else(|| anyhow!("path is not valid UTF-8: {}", path.display())) -} diff --git a/epiphany-core/src/bin/epiphany-repo-mind-evidence-guard-smoke.rs b/epiphany-core/src/bin/epiphany-repo-mind-evidence-guard-smoke.rs deleted file mode 100644 index a840d218..00000000 --- a/epiphany-core/src/bin/epiphany-repo-mind-evidence-guard-smoke.rs +++ /dev/null @@ -1,468 +0,0 @@ -use anyhow::Context; -use anyhow::Result; -use anyhow::anyhow; -use chrono::Utc; -use serde_json::Value; -use serde_json::json; -use std::env; -use std::fs; -use std::path::Path; -use std::path::PathBuf; -use std::process::Command; - -fn main() -> Result<()> { - let args = Args::parse()?; - let result = run_smoke(args)?; - println!("{}", serde_json::to_string_pretty(&result)?); - Ok(()) -} - -#[derive(Clone, Debug)] -struct Args { - root: PathBuf, -} - -impl Args { - fn parse() -> Result { - let mut root = env::current_dir().context("failed to resolve current directory")?; - let mut args = env::args().skip(1); - while let Some(arg) = args.next() { - match arg.as_str() { - "--root" => root = take_path(&mut args, "--root")?, - other => return Err(anyhow!("unexpected argument {other:?}")), - } - } - Ok(Self { root }) - } -} - -fn run_smoke(args: Args) -> Result { - let root = args - .root - .canonicalize() - .with_context(|| format!("failed to resolve {}", args.root.display()))?; - let smoke_root = root.join(".epiphany-smoke"); - let manifest = root.join("epiphany-core").join("Cargo.toml"); - if !manifest.exists() { - return Err(anyhow!( - "could not find epiphany-core manifest at {}", - manifest.display() - )); - } - fs::create_dir_all(&smoke_root) - .with_context(|| format!("failed to create {}", smoke_root.display()))?; - let stamp = Utc::now().format("%Y%m%d-%H%M%S").to_string(); - let smoke_dir = smoke_root.join(format!("repo-mind-evidence-guard-{stamp}")); - fs::create_dir(&smoke_dir) - .with_context(|| format!("failed to claim fresh smoke dir {}", smoke_dir.display()))?; - - let repo = smoke_dir.join("repo-body"); - fs::create_dir_all(&repo).with_context(|| format!("failed to create {}", repo.display()))?; - git(["init"], &repo)?; - git( - ["config", "user.email", "epiphany-smoke@example.invalid"], - &repo, - )?; - git(["config", "user.name", "Epiphany Smoke"], &repo)?; - fs::write( - repo.join("README.md"), - "# Repo Mind Evidence Guard Smoke\n\nThis repository proves Mind refuses proofless action items before Hands authority.\n", - ) - .with_context(|| format!("failed to seed {}", repo.join("README.md").display()))?; - git(["add", "README.md"], &repo)?; - git( - ["commit", "-m", "Seed repo mind evidence guard smoke body"], - &repo, - )?; - git(["switch", "-c", "epiphany/repo-mind-evidence-guard"], &repo)?; - - let item = "repo-mind-evidence-guard"; - let planned_path = "notes/epiphany-work/repo-mind-evidence-guard.md"; - let local_verse = repo.join(".epiphany").join("local-verse.ccmp"); - cargo_json( - &manifest, - "epiphany-repo", - &[ - "init", - "--workspace", - path_str(&repo)?, - "--epiphany-root", - path_str(&root)?, - "--swarm-id", - "repo-mind-evidence-guard-smoke", - "--topic", - "repo-mind-evidence-guard", - ], - &root, - )?; - cargo_json( - &manifest, - "epiphany-swarm", - &[ - "online", - "--workspace", - path_str(&repo)?, - "--epiphany-root", - path_str(&root)?, - "--runtime-id", - "repo-mind-evidence-guard-smoke", - "--local-verse-store", - path_str(&local_verse)?, - ], - &root, - )?; - cargo_json( - &manifest, - "epiphany-work", - &[ - "accept", - "--workspace", - path_str(&repo)?, - "--from", - "persona", - "--item", - item, - "--summary", - "Ask Mind to reject a proofless action item before Hands receives authority.", - "--candidate-action-ref", - "candidate-action://repo-mind-evidence-guard/proofless-action", - "--public-discussion-ref", - "epiphany-global/persona-collaboration/repo-mind-evidence-guard", - ], - &root, - )?; - let plan = cargo_json( - &manifest, - "epiphany-work", - &[ - "derive-plan", - "--workspace", - path_str(&repo)?, - "--item", - item, - "--action-family", - "planning-note", - "--model-ref", - "repo-mind-evidence-guard-smoke-imagination-v1", - "--model-authored", - ], - &root, - )?; - let run = cargo_json( - &manifest, - "epiphany-work", - &[ - "run", - "--workspace", - path_str(&repo)?, - "--epiphany-root", - path_str(&root)?, - "--item", - item, - "--path", - planned_path, - ], - &root, - )?; - - let plan_path = value_at_path(&plan, &["receiptPath"]) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("plan output had no receiptPath"))?; - let mut tampered_plan = read_json(Path::new(plan_path))?; - tampered_plan["derivation"]["actionItemReceipt"]["verificationAsks"] = json!([]); - tampered_plan["derivation"]["actionItemReceipt"]["planningFacets"]["evidenceNeeds"] = json!([]); - let tampered_path = smoke_dir.join("tampered-plan.json"); - write_json(&tampered_path, &tampered_plan)?; - - let adopt_failure = cargo_json_expect_failure( - &manifest, - "epiphany-work", - &[ - "adopt", - "--workspace", - path_str(&repo)?, - "--epiphany-root", - path_str(&root)?, - "--item", - item, - "--run-receipt", - value_at_path(&run, &["receiptPath"]) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("run output had no receiptPath"))?, - "--from-plan", - path_str(&tampered_path)?, - ], - &root, - )?; - if !adopt_failure.contains("lacks verification or evidence needs") { - return Err(anyhow!( - "adopt failure did not name missing evidence needs: {adopt_failure}" - )); - } - - let refusal_path = repo - .join(".epiphany") - .join("work") - .join("work-mind-adopt-repo-mind-evidence-guard.json"); - let refusal = read_json(&refusal_path)?; - require_eq( - &refusal, - &["schemaVersion"], - "epiphany.repo_work_mind_adoption_decision.v0", - )?; - require_eq(&refusal, &["status"], "refused-missing-evidence-needs")?; - require_eq( - &refusal, - &["interpretation", "inputSummary", "safeActionFamily"], - "repo.markdown_planning_note", - )?; - require_eq( - &refusal, - &["interpretation", "inputSummary", "planChangedPaths", "0"], - planned_path, - )?; - require_bool( - &refusal, - &["interpretation", "classification", "actionItemAccepted"], - false, - )?; - require_bool( - &refusal, - &["interpretation", "classification", "safeFamilyRecognized"], - true, - )?; - require_bool( - &refusal, - &["interpretation", "classification", "requestedPathsDeclared"], - true, - )?; - require_bool( - &refusal, - &[ - "interpretation", - "classification", - "requestedPathsMatchPlan", - ], - true, - )?; - require_bool( - &refusal, - &[ - "interpretation", - "classification", - "verificationAsksPresent", - ], - false, - )?; - require_bool( - &refusal, - &["interpretation", "classification", "evidenceNeedsPresent"], - false, - )?; - require_text_field( - &refusal, - &["interpretation", "refusalReasons", "0"], - "lacks verification or evidence needs", - )?; - require_bool(&refusal, &["gates", "safeFamilyRecognized"], true)?; - require_bool(&refusal, &["gates", "requestedPathsMatchPlan"], true)?; - require_bool(&refusal, &["gates", "verificationAsksPresent"], false)?; - require_bool(&refusal, &["gates", "evidenceNeedsPresent"], false)?; - require_bool(&refusal, &["gates", "branchLocalOnly"], false)?; - require_bool(&refusal, &["authority", "handsAuthorityGranted"], false)?; - require_bool(&refusal, &["authority", "durableStateAdmitted"], false)?; - require_bool(&refusal, &["authority", "serviceLifecycleAuthority"], false)?; - require_bool(&refusal, &["authority", "privateStateExposed"], false)?; - require_eq( - &refusal, - &["authority", "nextGate"], - "imagination.replan_with_explicit_soul_evidence_needs", - )?; - require_bool(&refusal, &["privateStateExposed"], false)?; - let adoption_receipt_path = repo - .join(".epiphany") - .join("work") - .join("work-adopt-repo-mind-evidence-guard.json"); - if adoption_receipt_path.exists() { - return Err(anyhow!( - "adoption receipt was written despite missing-evidence refusal: {}", - adoption_receipt_path.display() - )); - } - - Ok(json!({ - "schemaVersion": "epiphany.repo_work_mind_evidence_guard_smoke.v0", - "status": "ok", - "smokeDir": smoke_dir, - "repo": repo, - "item": item, - "planChangedPath": planned_path, - "mindDecisionStatus": refusal["status"], - "actionItemAccepted": false, - "safeFamilyRecognized": true, - "requestedPathsMatchPlan": true, - "verificationAsksPresent": false, - "evidenceNeedsPresent": false, - "handsAuthorityGranted": false, - "durableStateAdmitted": false, - "serviceLifecycleAuthority": false, - "adoptionReceiptWritten": false, - "privateStateExposed": false - })) -} - -fn cargo_json(manifest_path: &Path, bin_name: &str, args: &[&str], cwd: &Path) -> Result { - let output = Command::new("cargo") - .arg("run") - .arg("--quiet") - .arg("--manifest-path") - .arg(manifest_path) - .arg("--bin") - .arg(bin_name) - .arg("--") - .args(args) - .current_dir(cwd) - .output() - .with_context(|| format!("failed to spawn cargo run --bin {bin_name}"))?; - if !output.status.success() { - return Err(anyhow!( - "cargo run --bin {bin_name} failed:\nstdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )); - } - serde_json::from_slice(&output.stdout) - .with_context(|| format!("{bin_name} returned invalid JSON")) -} - -fn cargo_json_expect_failure( - manifest_path: &Path, - bin_name: &str, - args: &[&str], - cwd: &Path, -) -> Result { - let output = Command::new("cargo") - .arg("run") - .arg("--quiet") - .arg("--manifest-path") - .arg(manifest_path) - .arg("--bin") - .arg(bin_name) - .arg("--") - .args(args) - .current_dir(cwd) - .output() - .with_context(|| format!("failed to spawn cargo run --bin {bin_name}"))?; - if output.status.success() { - return Err(anyhow!("{bin_name} unexpectedly succeeded")); - } - Ok(format!( - "{}\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )) -} - -fn git(args: [&str; N], cwd: &Path) -> Result<()> { - let output = Command::new("git") - .args(args) - .current_dir(cwd) - .output() - .with_context(|| format!("failed to spawn git in {}", cwd.display()))?; - if output.status.success() { - Ok(()) - } else { - Err(anyhow!( - "git failed in {}:\n{}", - cwd.display(), - String::from_utf8_lossy(&output.stderr) - )) - } -} - -fn read_json(path: &Path) -> Result { - let raw = - fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?; - serde_json::from_str(&raw).with_context(|| format!("failed to decode {}", path.display())) -} - -fn write_json(path: &Path, value: &Value) -> Result<()> { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("failed to create {}", parent.display()))?; - } - fs::write(path, serde_json::to_vec_pretty(value)?) - .with_context(|| format!("failed to write {}", path.display())) -} - -fn require_eq(value: &Value, path: &[&str], expected: &str) -> Result<()> { - let actual = value_at_path(value, path) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("missing string at {}", path.join(".")))?; - if actual == expected { - Ok(()) - } else { - Err(anyhow!( - "expected {} to be {:?}, got {:?}", - path.join("."), - expected, - actual - )) - } -} - -fn require_bool(value: &Value, path: &[&str], expected: bool) -> Result<()> { - let actual = value_at_path(value, path) - .and_then(Value::as_bool) - .ok_or_else(|| anyhow!("missing bool at {}", path.join(".")))?; - if actual == expected { - Ok(()) - } else { - Err(anyhow!( - "expected {} to be {}, got {}", - path.join("."), - expected, - actual - )) - } -} - -fn require_text_field(value: &Value, path: &[&str], needle: &str) -> Result<()> { - let actual = value_at_path(value, path) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("missing string at {}", path.join(".")))?; - if actual.contains(needle) { - Ok(()) - } else { - Err(anyhow!( - "expected {} to contain {:?}, got {:?}", - path.join("."), - needle, - actual - )) - } -} - -fn value_at_path<'a>(value: &'a Value, path: &[&str]) -> Option<&'a Value> { - let mut cursor = value; - for segment in path { - if let Ok(index) = segment.parse::() { - cursor = cursor.as_array()?.get(index)?; - } else { - cursor = cursor.get(*segment)?; - } - } - Some(cursor) -} - -fn path_str(path: &Path) -> Result<&str> { - path.to_str() - .ok_or_else(|| anyhow!("path is not valid UTF-8: {}", path.display())) -} - -fn take_path(args: &mut impl Iterator, name: &str) -> Result { - Ok(PathBuf::from( - args.next() - .ok_or_else(|| anyhow!("missing value for {name}"))?, - )) -} diff --git a/epiphany-core/src/bin/epiphany-repo-mind-path-guard-smoke.rs b/epiphany-core/src/bin/epiphany-repo-mind-path-guard-smoke.rs deleted file mode 100644 index c6d9e1f0..00000000 --- a/epiphany-core/src/bin/epiphany-repo-mind-path-guard-smoke.rs +++ /dev/null @@ -1,457 +0,0 @@ -use anyhow::Context; -use anyhow::Result; -use anyhow::anyhow; -use chrono::Utc; -use serde_json::Value; -use serde_json::json; -use std::env; -use std::fs; -use std::path::Path; -use std::path::PathBuf; -use std::process::Command; - -fn main() -> Result<()> { - let args = Args::parse()?; - let result = run_smoke(args)?; - println!("{}", serde_json::to_string_pretty(&result)?); - Ok(()) -} - -#[derive(Clone, Debug)] -struct Args { - root: PathBuf, -} - -impl Args { - fn parse() -> Result { - let mut root = env::current_dir().context("failed to resolve current directory")?; - let mut args = env::args().skip(1); - while let Some(arg) = args.next() { - match arg.as_str() { - "--root" => root = take_path(&mut args, "--root")?, - other => return Err(anyhow!("unexpected argument {other:?}")), - } - } - Ok(Self { root }) - } -} - -fn run_smoke(args: Args) -> Result { - let root = args - .root - .canonicalize() - .with_context(|| format!("failed to resolve {}", args.root.display()))?; - let smoke_root = root.join(".epiphany-smoke"); - let manifest = root.join("epiphany-core").join("Cargo.toml"); - if !manifest.exists() { - return Err(anyhow!( - "could not find epiphany-core manifest at {}", - manifest.display() - )); - } - fs::create_dir_all(&smoke_root) - .with_context(|| format!("failed to create {}", smoke_root.display()))?; - let stamp = Utc::now().format("%Y%m%d-%H%M%S").to_string(); - let smoke_dir = smoke_root.join(format!("repo-mind-path-guard-{stamp}")); - fs::create_dir(&smoke_dir) - .with_context(|| format!("failed to claim fresh smoke dir {}", smoke_dir.display()))?; - - let repo = smoke_dir.join("repo-body"); - fs::create_dir_all(&repo).with_context(|| format!("failed to create {}", repo.display()))?; - git(["init"], &repo)?; - git( - ["config", "user.email", "epiphany-smoke@example.invalid"], - &repo, - )?; - git(["config", "user.name", "Epiphany Smoke"], &repo)?; - fs::write( - repo.join("README.md"), - "# Repo Mind Path Guard Smoke\n\nThis repository proves Mind refuses mismatched requested paths before Hands authority.\n", - ) - .with_context(|| format!("failed to seed {}", repo.join("README.md").display()))?; - git(["add", "README.md"], &repo)?; - git( - ["commit", "-m", "Seed repo mind path guard smoke body"], - &repo, - )?; - git(["switch", "-c", "epiphany/repo-mind-path-guard"], &repo)?; - - let item = "repo-mind-path-guard"; - let planned_path = "notes/epiphany-work/repo-mind-path-guard.md"; - let tampered_requested_path = "notes/epiphany-work/alien-path.md"; - let local_verse = repo.join(".epiphany").join("local-verse.ccmp"); - cargo_json( - &manifest, - "epiphany-repo", - &[ - "init", - "--workspace", - path_str(&repo)?, - "--epiphany-root", - path_str(&root)?, - "--swarm-id", - "repo-mind-path-guard-smoke", - "--topic", - "repo-mind-path-guard", - ], - &root, - )?; - cargo_json( - &manifest, - "epiphany-swarm", - &[ - "online", - "--workspace", - path_str(&repo)?, - "--epiphany-root", - path_str(&root)?, - "--runtime-id", - "repo-mind-path-guard-smoke", - "--local-verse-store", - path_str(&local_verse)?, - ], - &root, - )?; - cargo_json( - &manifest, - "epiphany-work", - &[ - "accept", - "--workspace", - path_str(&repo)?, - "--from", - "persona", - "--item", - item, - "--summary", - "Ask Mind to reject a tampered requested path before Hands receives authority.", - "--candidate-action-ref", - "candidate-action://repo-mind-path-guard/tampered-path", - "--public-discussion-ref", - "epiphany-global/persona-collaboration/repo-mind-path-guard", - ], - &root, - )?; - let plan = cargo_json( - &manifest, - "epiphany-work", - &[ - "derive-plan", - "--workspace", - path_str(&repo)?, - "--item", - item, - "--action-family", - "planning-note", - "--model-ref", - "repo-mind-path-guard-smoke-imagination-v1", - "--model-authored", - ], - &root, - )?; - let run = cargo_json( - &manifest, - "epiphany-work", - &[ - "run", - "--workspace", - path_str(&repo)?, - "--epiphany-root", - path_str(&root)?, - "--item", - item, - "--path", - planned_path, - ], - &root, - )?; - - let plan_path = value_at_path(&plan, &["receiptPath"]) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("plan output had no receiptPath"))?; - let mut tampered_plan = read_json(Path::new(plan_path))?; - tampered_plan["derivation"]["actionItemReceipt"]["requestedPaths"] = - json!([tampered_requested_path]); - let tampered_path = smoke_dir.join("tampered-plan.json"); - write_json(&tampered_path, &tampered_plan)?; - - let adopt_failure = cargo_json_expect_failure( - &manifest, - "epiphany-work", - &[ - "adopt", - "--workspace", - path_str(&repo)?, - "--epiphany-root", - path_str(&root)?, - "--item", - item, - "--run-receipt", - value_at_path(&run, &["receiptPath"]) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("run output had no receiptPath"))?, - "--from-plan", - path_str(&tampered_path)?, - ], - &root, - )?; - if !adopt_failure.contains("do not match plan changed paths") { - return Err(anyhow!( - "adopt failure did not name requested-path mismatch: {adopt_failure}" - )); - } - - let refusal_path = repo - .join(".epiphany") - .join("work") - .join("work-mind-adopt-repo-mind-path-guard.json"); - let refusal = read_json(&refusal_path)?; - require_eq( - &refusal, - &["schemaVersion"], - "epiphany.repo_work_mind_adoption_decision.v0", - )?; - require_eq(&refusal, &["status"], "refused-requested-path-mismatch")?; - require_eq( - &refusal, - &["interpretation", "inputSummary", "safeActionFamily"], - "repo.markdown_planning_note", - )?; - require_eq( - &refusal, - &["interpretation", "inputSummary", "requestedPaths", "0"], - tampered_requested_path, - )?; - require_eq( - &refusal, - &["interpretation", "inputSummary", "planChangedPaths", "0"], - planned_path, - )?; - require_bool( - &refusal, - &["interpretation", "classification", "actionItemAccepted"], - false, - )?; - require_bool( - &refusal, - &["interpretation", "classification", "safeFamilyRecognized"], - true, - )?; - require_bool( - &refusal, - &["interpretation", "classification", "requestedPathsDeclared"], - true, - )?; - require_bool( - &refusal, - &[ - "interpretation", - "classification", - "requestedPathsMatchPlan", - ], - false, - )?; - require_text_field( - &refusal, - &["interpretation", "refusalReasons", "0"], - "do not match plan changed paths", - )?; - require_bool(&refusal, &["gates", "safeFamilyRecognized"], true)?; - require_bool(&refusal, &["gates", "requestedPathsMatchPlan"], false)?; - require_bool(&refusal, &["gates", "branchLocalOnly"], false)?; - require_bool(&refusal, &["authority", "handsAuthorityGranted"], false)?; - require_bool(&refusal, &["authority", "durableStateAdmitted"], false)?; - require_bool(&refusal, &["authority", "serviceLifecycleAuthority"], false)?; - require_bool(&refusal, &["authority", "privateStateExposed"], false)?; - require_eq( - &refusal, - &["authority", "nextGate"], - "imagination.replan_with_matching_requested_paths", - )?; - require_bool(&refusal, &["privateStateExposed"], false)?; - let adoption_receipt_path = repo - .join(".epiphany") - .join("work") - .join("work-adopt-repo-mind-path-guard.json"); - if adoption_receipt_path.exists() { - return Err(anyhow!( - "adoption receipt was written despite requested-path mismatch refusal: {}", - adoption_receipt_path.display() - )); - } - - Ok(json!({ - "schemaVersion": "epiphany.repo_work_mind_path_guard_smoke.v0", - "status": "ok", - "smokeDir": smoke_dir, - "repo": repo, - "item": item, - "tamperedRequestedPath": tampered_requested_path, - "planChangedPath": planned_path, - "mindDecisionStatus": refusal["status"], - "actionItemAccepted": false, - "safeFamilyRecognized": true, - "requestedPathsMatchPlan": false, - "handsAuthorityGranted": false, - "durableStateAdmitted": false, - "serviceLifecycleAuthority": false, - "adoptionReceiptWritten": false, - "privateStateExposed": false - })) -} - -fn cargo_json(manifest_path: &Path, bin_name: &str, args: &[&str], cwd: &Path) -> Result { - let output = Command::new("cargo") - .arg("run") - .arg("--quiet") - .arg("--manifest-path") - .arg(manifest_path) - .arg("--bin") - .arg(bin_name) - .arg("--") - .args(args) - .current_dir(cwd) - .output() - .with_context(|| format!("failed to spawn cargo run --bin {bin_name}"))?; - if !output.status.success() { - return Err(anyhow!( - "cargo run --bin {bin_name} failed:\nstdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )); - } - serde_json::from_slice(&output.stdout) - .with_context(|| format!("{bin_name} returned invalid JSON")) -} - -fn cargo_json_expect_failure( - manifest_path: &Path, - bin_name: &str, - args: &[&str], - cwd: &Path, -) -> Result { - let output = Command::new("cargo") - .arg("run") - .arg("--quiet") - .arg("--manifest-path") - .arg(manifest_path) - .arg("--bin") - .arg(bin_name) - .arg("--") - .args(args) - .current_dir(cwd) - .output() - .with_context(|| format!("failed to spawn cargo run --bin {bin_name}"))?; - if output.status.success() { - return Err(anyhow!("{bin_name} unexpectedly succeeded")); - } - Ok(format!( - "{}\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )) -} - -fn git(args: [&str; N], cwd: &Path) -> Result<()> { - let output = Command::new("git") - .args(args) - .current_dir(cwd) - .output() - .with_context(|| format!("failed to spawn git in {}", cwd.display()))?; - if output.status.success() { - Ok(()) - } else { - Err(anyhow!( - "git failed in {}:\n{}", - cwd.display(), - String::from_utf8_lossy(&output.stderr) - )) - } -} - -fn read_json(path: &Path) -> Result { - let raw = - fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?; - serde_json::from_str(&raw).with_context(|| format!("failed to decode {}", path.display())) -} - -fn write_json(path: &Path, value: &Value) -> Result<()> { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("failed to create {}", parent.display()))?; - } - fs::write(path, serde_json::to_vec_pretty(value)?) - .with_context(|| format!("failed to write {}", path.display())) -} - -fn require_eq(value: &Value, path: &[&str], expected: &str) -> Result<()> { - let actual = value_at_path(value, path) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("missing string at {}", path.join(".")))?; - if actual == expected { - Ok(()) - } else { - Err(anyhow!( - "expected {} to be {:?}, got {:?}", - path.join("."), - expected, - actual - )) - } -} - -fn require_bool(value: &Value, path: &[&str], expected: bool) -> Result<()> { - let actual = value_at_path(value, path) - .and_then(Value::as_bool) - .ok_or_else(|| anyhow!("missing bool at {}", path.join(".")))?; - if actual == expected { - Ok(()) - } else { - Err(anyhow!( - "expected {} to be {}, got {}", - path.join("."), - expected, - actual - )) - } -} - -fn require_text_field(value: &Value, path: &[&str], needle: &str) -> Result<()> { - let actual = value_at_path(value, path) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("missing string at {}", path.join(".")))?; - if actual.contains(needle) { - Ok(()) - } else { - Err(anyhow!( - "expected {} to contain {:?}, got {:?}", - path.join("."), - needle, - actual - )) - } -} - -fn value_at_path<'a>(value: &'a Value, path: &[&str]) -> Option<&'a Value> { - let mut cursor = value; - for segment in path { - if let Ok(index) = segment.parse::() { - cursor = cursor.as_array()?.get(index)?; - } else { - cursor = cursor.get(*segment)?; - } - } - Some(cursor) -} - -fn path_str(path: &Path) -> Result<&str> { - path.to_str() - .ok_or_else(|| anyhow!("path is not valid UTF-8: {}", path.display())) -} - -fn take_path(args: &mut impl Iterator, name: &str) -> Result { - Ok(PathBuf::from( - args.next() - .ok_or_else(|| anyhow!("missing value for {name}"))?, - )) -} diff --git a/epiphany-core/src/bin/epiphany-repo-mind-safe-family-guard-smoke.rs b/epiphany-core/src/bin/epiphany-repo-mind-safe-family-guard-smoke.rs deleted file mode 100644 index 78b86ff0..00000000 --- a/epiphany-core/src/bin/epiphany-repo-mind-safe-family-guard-smoke.rs +++ /dev/null @@ -1,433 +0,0 @@ -use anyhow::Context; -use anyhow::Result; -use anyhow::anyhow; -use chrono::Utc; -use serde_json::Value; -use serde_json::json; -use std::env; -use std::fs; -use std::path::Path; -use std::path::PathBuf; -use std::process::Command; - -fn main() -> Result<()> { - let args = Args::parse()?; - let result = run_smoke(args)?; - println!("{}", serde_json::to_string_pretty(&result)?); - Ok(()) -} - -#[derive(Clone, Debug)] -struct Args { - root: PathBuf, -} - -impl Args { - fn parse() -> Result { - let mut root = env::current_dir().context("failed to resolve current directory")?; - let mut args = env::args().skip(1); - while let Some(arg) = args.next() { - match arg.as_str() { - "--root" => root = take_path(&mut args, "--root")?, - other => return Err(anyhow!("unexpected argument {other:?}")), - } - } - Ok(Self { root }) - } -} - -fn run_smoke(args: Args) -> Result { - let root = args - .root - .canonicalize() - .with_context(|| format!("failed to resolve {}", args.root.display()))?; - let smoke_root = root.join(".epiphany-smoke"); - let manifest = root.join("epiphany-core").join("Cargo.toml"); - if !manifest.exists() { - return Err(anyhow!( - "could not find epiphany-core manifest at {}", - manifest.display() - )); - } - fs::create_dir_all(&smoke_root) - .with_context(|| format!("failed to create {}", smoke_root.display()))?; - let stamp = Utc::now().format("%Y%m%d-%H%M%S").to_string(); - let smoke_dir = smoke_root.join(format!("repo-mind-safe-family-guard-{stamp}")); - fs::create_dir(&smoke_dir) - .with_context(|| format!("failed to claim fresh smoke dir {}", smoke_dir.display()))?; - - let repo = smoke_dir.join("repo-body"); - fs::create_dir_all(&repo).with_context(|| format!("failed to create {}", repo.display()))?; - git(["init"], &repo)?; - git( - ["config", "user.email", "epiphany-smoke@example.invalid"], - &repo, - )?; - git(["config", "user.name", "Epiphany Smoke"], &repo)?; - fs::write( - repo.join("README.md"), - "# Repo Mind Safe Family Guard Smoke\n\nThis repository proves Mind refuses alien safe-family labels before Hands authority.\n", - ) - .with_context(|| format!("failed to seed {}", repo.join("README.md").display()))?; - git(["add", "README.md"], &repo)?; - git( - [ - "commit", - "-m", - "Seed repo mind safe family guard smoke body", - ], - &repo, - )?; - git( - ["switch", "-c", "epiphany/repo-mind-safe-family-guard"], - &repo, - )?; - - let item = "repo-mind-safe-family-guard"; - let local_verse = repo.join(".epiphany").join("local-verse.ccmp"); - cargo_json( - &manifest, - "epiphany-repo", - &[ - "init", - "--workspace", - path_str(&repo)?, - "--epiphany-root", - path_str(&root)?, - "--swarm-id", - "repo-mind-safe-family-guard-smoke", - "--topic", - "repo-mind-safe-family-guard", - ], - &root, - )?; - cargo_json( - &manifest, - "epiphany-swarm", - &[ - "online", - "--workspace", - path_str(&repo)?, - "--epiphany-root", - path_str(&root)?, - "--runtime-id", - "repo-mind-safe-family-guard-smoke", - "--local-verse-store", - path_str(&local_verse)?, - ], - &root, - )?; - cargo_json( - &manifest, - "epiphany-work", - &[ - "accept", - "--workspace", - path_str(&repo)?, - "--from", - "persona", - "--item", - item, - "--summary", - "Ask Mind to reject a tampered safe-family label before Hands receives authority.", - "--candidate-action-ref", - "candidate-action://repo-mind-safe-family-guard/tampered-family", - "--public-discussion-ref", - "epiphany-global/persona-collaboration/repo-mind-safe-family-guard", - ], - &root, - )?; - let plan = cargo_json( - &manifest, - "epiphany-work", - &[ - "derive-plan", - "--workspace", - path_str(&repo)?, - "--item", - item, - "--action-family", - "planning-note", - "--model-ref", - "repo-mind-safe-family-guard-smoke-imagination-v1", - "--model-authored", - ], - &root, - )?; - let run = cargo_json( - &manifest, - "epiphany-work", - &[ - "run", - "--workspace", - path_str(&repo)?, - "--epiphany-root", - path_str(&root)?, - "--item", - item, - "--path", - "notes/epiphany-work/repo-mind-safe-family-guard.md", - ], - &root, - )?; - - let plan_path = value_at_path(&plan, &["receiptPath"]) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("plan output had no receiptPath"))?; - let mut tampered_plan = read_json(Path::new(plan_path))?; - tampered_plan["derivation"]["actionItemReceipt"]["safeActionFamily"] = - json!("idunn.service_lifecycle"); - let tampered_path = smoke_dir.join("tampered-plan.json"); - write_json(&tampered_path, &tampered_plan)?; - - let adopt_failure = cargo_json_expect_failure( - &manifest, - "epiphany-work", - &[ - "adopt", - "--workspace", - path_str(&repo)?, - "--epiphany-root", - path_str(&root)?, - "--item", - item, - "--run-receipt", - value_at_path(&run, &["receiptPath"]) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("run output had no receiptPath"))?, - "--from-plan", - path_str(&tampered_path)?, - ], - &root, - )?; - if !adopt_failure.contains("unsupported repo-work safe family") { - return Err(anyhow!( - "adopt failure did not name unsupported safe family: {adopt_failure}" - )); - } - - let refusal_path = repo - .join(".epiphany") - .join("work") - .join("work-mind-adopt-repo-mind-safe-family-guard.json"); - let refusal = read_json(&refusal_path)?; - require_eq( - &refusal, - &["schemaVersion"], - "epiphany.repo_work_mind_adoption_decision.v0", - )?; - require_eq(&refusal, &["status"], "refused-unsupported-safe-family")?; - require_eq( - &refusal, - &["interpretation", "inputSummary", "safeActionFamily"], - "idunn.service_lifecycle", - )?; - require_bool( - &refusal, - &["interpretation", "classification", "actionItemAccepted"], - false, - )?; - require_bool( - &refusal, - &["interpretation", "classification", "safeFamilyRecognized"], - false, - )?; - require_text_field( - &refusal, - &["interpretation", "refusalReasons", "0"], - "Unsupported repo-work safe action family idunn.service_lifecycle", - )?; - require_bool(&refusal, &["authority", "handsAuthorityGranted"], false)?; - require_bool(&refusal, &["authority", "durableStateAdmitted"], false)?; - require_bool(&refusal, &["authority", "serviceLifecycleAuthority"], false)?; - require_bool(&refusal, &["authority", "privateStateExposed"], false)?; - require_eq( - &refusal, - &["authority", "nextGate"], - "imagination.replan_with_allowed_safe_family", - )?; - require_bool(&refusal, &["privateStateExposed"], false)?; - let adoption_receipt_path = repo - .join(".epiphany") - .join("work") - .join("work-adopt-repo-mind-safe-family-guard.json"); - if adoption_receipt_path.exists() { - return Err(anyhow!( - "adoption receipt was written despite unsupported safe-family refusal: {}", - adoption_receipt_path.display() - )); - } - - Ok(json!({ - "schemaVersion": "epiphany.repo_work_mind_safe_family_guard_smoke.v0", - "status": "ok", - "smokeDir": smoke_dir, - "repo": repo, - "item": item, - "tamperedSafeActionFamily": "idunn.service_lifecycle", - "mindDecisionStatus": refusal["status"], - "actionItemAccepted": false, - "safeFamilyRecognized": false, - "handsAuthorityGranted": false, - "durableStateAdmitted": false, - "serviceLifecycleAuthority": false, - "adoptionReceiptWritten": false, - "privateStateExposed": false - })) -} - -fn cargo_json(manifest_path: &Path, bin_name: &str, args: &[&str], cwd: &Path) -> Result { - let output = Command::new("cargo") - .arg("run") - .arg("--quiet") - .arg("--manifest-path") - .arg(manifest_path) - .arg("--bin") - .arg(bin_name) - .arg("--") - .args(args) - .current_dir(cwd) - .output() - .with_context(|| format!("failed to spawn cargo run --bin {bin_name}"))?; - if !output.status.success() { - return Err(anyhow!( - "cargo run --bin {bin_name} failed:\nstdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )); - } - serde_json::from_slice(&output.stdout) - .with_context(|| format!("{bin_name} returned invalid JSON")) -} - -fn cargo_json_expect_failure( - manifest_path: &Path, - bin_name: &str, - args: &[&str], - cwd: &Path, -) -> Result { - let output = Command::new("cargo") - .arg("run") - .arg("--quiet") - .arg("--manifest-path") - .arg(manifest_path) - .arg("--bin") - .arg(bin_name) - .arg("--") - .args(args) - .current_dir(cwd) - .output() - .with_context(|| format!("failed to spawn cargo run --bin {bin_name}"))?; - if output.status.success() { - return Err(anyhow!("{bin_name} unexpectedly succeeded")); - } - Ok(format!( - "{}\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )) -} - -fn git(args: [&str; N], cwd: &Path) -> Result<()> { - let output = Command::new("git") - .args(args) - .current_dir(cwd) - .output() - .with_context(|| format!("failed to spawn git in {}", cwd.display()))?; - if output.status.success() { - Ok(()) - } else { - Err(anyhow!( - "git failed in {}:\n{}", - cwd.display(), - String::from_utf8_lossy(&output.stderr) - )) - } -} - -fn read_json(path: &Path) -> Result { - let raw = - fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?; - serde_json::from_str(&raw).with_context(|| format!("failed to decode {}", path.display())) -} - -fn write_json(path: &Path, value: &Value) -> Result<()> { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("failed to create {}", parent.display()))?; - } - fs::write(path, serde_json::to_vec_pretty(value)?) - .with_context(|| format!("failed to write {}", path.display())) -} - -fn require_eq(value: &Value, path: &[&str], expected: &str) -> Result<()> { - let actual = value_at_path(value, path) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("missing string at {}", path.join(".")))?; - if actual == expected { - Ok(()) - } else { - Err(anyhow!( - "expected {} to be {:?}, got {:?}", - path.join("."), - expected, - actual - )) - } -} - -fn require_bool(value: &Value, path: &[&str], expected: bool) -> Result<()> { - let actual = value_at_path(value, path) - .and_then(Value::as_bool) - .ok_or_else(|| anyhow!("missing bool at {}", path.join(".")))?; - if actual == expected { - Ok(()) - } else { - Err(anyhow!( - "expected {} to be {}, got {}", - path.join("."), - expected, - actual - )) - } -} - -fn require_text_field(value: &Value, path: &[&str], needle: &str) -> Result<()> { - let actual = value_at_path(value, path) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("missing string at {}", path.join(".")))?; - if actual.contains(needle) { - Ok(()) - } else { - Err(anyhow!( - "expected {} to contain {:?}, got {:?}", - path.join("."), - needle, - actual - )) - } -} - -fn value_at_path<'a>(value: &'a Value, path: &[&str]) -> Option<&'a Value> { - let mut cursor = value; - for segment in path { - if let Ok(index) = segment.parse::() { - cursor = cursor.as_array()?.get(index)?; - } else { - cursor = cursor.get(*segment)?; - } - } - Some(cursor) -} - -fn path_str(path: &Path) -> Result<&str> { - path.to_str() - .ok_or_else(|| anyhow!("path is not valid UTF-8: {}", path.display())) -} - -fn take_path(args: &mut impl Iterator, name: &str) -> Result { - Ok(PathBuf::from( - args.next() - .ok_or_else(|| anyhow!("missing value for {name}"))?, - )) -} diff --git a/epiphany-core/src/bin/epiphany-repo-planning-facets-smoke.rs b/epiphany-core/src/bin/epiphany-repo-planning-facets-smoke.rs deleted file mode 100644 index b37ebe8a..00000000 --- a/epiphany-core/src/bin/epiphany-repo-planning-facets-smoke.rs +++ /dev/null @@ -1,616 +0,0 @@ -use anyhow::Context; -use anyhow::Result; -use anyhow::anyhow; -use chrono::Utc; -use serde_json::Value; -use serde_json::json; -use std::env; -use std::fs; -use std::path::Path; -use std::path::PathBuf; -use std::process::Command; - -fn main() -> Result<()> { - let args = Args::parse()?; - let result = run_smoke(args)?; - println!("{}", serde_json::to_string_pretty(&result)?); - Ok(()) -} - -#[derive(Clone, Debug)] -struct Args { - root: PathBuf, -} - -impl Args { - fn parse() -> Result { - let mut root = env::current_dir().context("failed to resolve current directory")?; - let mut args = env::args().skip(1).peekable(); - while let Some(arg) = args.next() { - match arg.as_str() { - "--root" => root = take_path(&mut args, "--root")?, - other => return Err(anyhow!("unexpected argument {other:?}")), - } - } - Ok(Self { root }) - } -} - -fn run_smoke(args: Args) -> Result { - let root = args - .root - .canonicalize() - .with_context(|| format!("failed to resolve {}", args.root.display()))?; - let smoke_root = root.join(".epiphany-smoke"); - let manifest = root.join("epiphany-core").join("Cargo.toml"); - if !manifest.exists() { - return Err(anyhow!( - "could not find epiphany-core manifest at {}", - manifest.display() - )); - } - fs::create_dir_all(&smoke_root) - .with_context(|| format!("failed to create {}", smoke_root.display()))?; - let stamp = Utc::now().format("%Y%m%d-%H%M%S").to_string(); - let smoke_dir = smoke_root.join(format!("repo-planning-facets-{stamp}")); - fs::create_dir(&smoke_dir) - .with_context(|| format!("failed to claim fresh smoke dir {}", smoke_dir.display()))?; - - let repo = smoke_dir.join("repo-body"); - fs::create_dir_all(&repo).with_context(|| format!("failed to create {}", repo.display()))?; - git(["init"], &repo)?; - git( - ["config", "user.email", "epiphany-smoke@example.invalid"], - &repo, - )?; - git(["config", "user.name", "Epiphany Smoke"], &repo)?; - fs::write( - repo.join("README.md"), - "# Repo Planning Facets Smoke\n\nThis repository proves richer Imagination planning cargo without command authority.\n", - ) - .with_context(|| format!("failed to seed {}", repo.join("README.md").display()))?; - git(["add", "README.md"], &repo)?; - git( - ["commit", "-m", "Seed repo planning facets smoke body"], - &repo, - )?; - git(["switch", "-c", "epiphany/repo-planning-facets"], &repo)?; - - let item = "repo-planning-facets"; - let local_verse = repo.join(".epiphany").join("local-verse.ccmp"); - cargo_json( - &manifest, - "epiphany-repo", - &[ - "init", - "--workspace", - path_str(&repo)?, - "--epiphany-root", - path_str(&root)?, - "--swarm-id", - "repo-planning-facets-smoke", - "--topic", - "repo-planning-facets", - ], - &root, - )?; - cargo_json( - &manifest, - "epiphany-swarm", - &[ - "online", - "--workspace", - path_str(&repo)?, - "--epiphany-root", - path_str(&root)?, - "--runtime-id", - "repo-planning-facets-smoke", - "--local-verse-store", - path_str(&local_verse)?, - ], - &root, - )?; - cargo_json( - &manifest, - "epiphany-work", - &[ - "accept", - "--workspace", - path_str(&repo)?, - "--from", - "persona", - "--item", - item, - "--summary", - "Let Imagination preserve richer planning facets before Self chooses the safe move.", - "--candidate-action-ref", - "candidate-action://repo-planning-facets/action-items", - "--public-discussion-ref", - "epiphany-global/persona-collaboration/repo-planning-facets", - ], - &root, - )?; - let plan = cargo_json( - &manifest, - "epiphany-work", - &[ - "derive-plan", - "--workspace", - path_str(&repo)?, - "--item", - item, - "--action-family", - "planning-note", - "--model-ref", - "repo-planning-facets-smoke-imagination-v1", - "--model-authored", - "--assumption", - "Persona feedback is public enough to route into Imagination.", - "--constraint", - "Hands must only write the declared planning note path.", - "--non-goal", - "Do not publish, merge, deploy, or mutate service lifecycle state.", - "--open-question", - "Should Self adopt this as the next branch-local move?", - "--decision-point", - "Self may adopt, defer, or ask Imagination for a narrower safe family.", - "--evidence-need", - "Soul needs the action-item receipt, plan receipt, changed-path proof, and private-state seal.", - ], - &root, - )?; - - let action_items_path = repo - .join(".epiphany") - .join("work") - .join("work-action-items-repo-planning-facets.json"); - let action_items = read_json(&action_items_path)?; - require_eq( - &action_items, - &["schemaVersion"], - "epiphany.repo_work_imagination_action_items_receipt.v0", - )?; - require_eq(&action_items, &["status"], "proposed-for-self-mind-review")?; - require_eq( - &action_items, - &["actionItems", "0", "safeActionFamily"], - "repo.markdown_planning_note", - )?; - require_bool( - &action_items, - &["model", "operatorAuthoredShellDetails"], - false, - )?; - require_bool( - &action_items, - &["authority", "handsAuthorityGranted"], - false, - )?; - require_bool(&action_items, &["authority", "durableStateAdmitted"], false)?; - require_bool(&action_items, &["privateStateExposed"], false)?; - require_text_field( - &action_items, - &["actionItems", "0", "planningFacets", "assumptions", "0"], - "Persona feedback is public enough", - )?; - require_text_field( - &action_items, - &["actionItems", "0", "planningFacets", "constraints", "0"], - "declared planning note path", - )?; - require_text_field( - &action_items, - &["actionItems", "0", "planningFacets", "nonGoals", "0"], - "Do not publish", - )?; - require_text_field( - &action_items, - &["actionItems", "0", "planningFacets", "openQuestions", "0"], - "Should Self adopt", - )?; - require_text_field( - &action_items, - &["actionItems", "0", "planningFacets", "decisionPoints", "0"], - "Self may adopt", - )?; - require_text_field( - &action_items, - &["actionItems", "0", "planningFacets", "evidenceNeeds", "0"], - "Soul needs", - )?; - require_bool( - &action_items, - &[ - "actionItems", - "0", - "planningFacets", - "handsCommandAuthority", - ], - false, - )?; - require_bool( - &action_items, - &[ - "actionItems", - "0", - "planningFacets", - "durableStateAuthority", - ], - false, - )?; - require_eq( - &plan, - &[ - "derivation", - "actionItemReceipt", - "planningFacets", - "constraints", - "0", - ], - "Hands must only write the declared planning note path.", - )?; - - for _ in 0..4 { - cargo_json( - &manifest, - "epiphany-work", - &[ - "tick", - "--workspace", - path_str(&repo)?, - "--epiphany-root", - path_str(&root)?, - "--item", - item, - "--cooldown-seconds", - "0", - ], - &root, - )?; - } - let adoption = read_json( - &repo - .join(".epiphany") - .join("work") - .join(format!("work-adopt-{item}.json")), - )?; - require_eq( - &adoption, - &["schemaVersion"], - "epiphany.repo_work_adoption_receipt.v0", - )?; - require_eq( - &adoption, - &["status"], - "approved-for-branch-local-hands-action", - )?; - require_eq( - &adoption, - &["mindAdoptionDecision", "schemaVersion"], - "epiphany.repo_work_mind_adoption_decision.v0", - )?; - require_eq( - &adoption, - &["mindAdoptionDecision", "status"], - "adopted-for-branch-local-hands", - )?; - require_eq(&adoption, &["mindAdoptionDecision", "owner"], "Mind")?; - require_bool( - &adoption, - &["mindAdoptionDecision", "gates", "selfPresentedActionItem"], - true, - )?; - require_bool( - &adoption, - &["mindAdoptionDecision", "gates", "mindReviewedEvidence"], - true, - )?; - require_bool( - &adoption, - &["mindAdoptionDecision", "gates", "mindInterpretedActionItem"], - true, - )?; - require_bool( - &adoption, - &["mindAdoptionDecision", "gates", "branchLocalOnly"], - true, - )?; - require_bool( - &adoption, - &["mindAdoptionDecision", "authority", "handsAuthorityGranted"], - false, - )?; - require_bool( - &adoption, - &["mindAdoptionDecision", "authority", "durableStateAdmitted"], - false, - )?; - require_bool( - &adoption, - &["mindAdoptionDecision", "authority", "publicationAuthorized"], - false, - )?; - require_bool( - &adoption, - &[ - "mindAdoptionDecision", - "authority", - "serviceLifecycleAuthority", - ], - false, - )?; - require_bool( - &adoption, - &["mindAdoptionDecision", "authority", "privateStateExposed"], - false, - )?; - let mind_adoption_path = value_at_path(&adoption, &["mindAdoptionDecision", "receiptPath"]) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("missing Mind adoption decision receipt path"))?; - let mind_adoption = read_json(Path::new(mind_adoption_path))?; - require_eq( - &mind_adoption, - &["schemaVersion"], - "epiphany.repo_work_mind_adoption_decision.v0", - )?; - require_eq( - &mind_adoption, - &["interpretation", "schemaVersion"], - "epiphany.repo_work_mind_interpretation.v0", - )?; - require_eq( - &mind_adoption, - &["interpretation", "classification", "decisionKind"], - "branch-local-hands-adoption", - )?; - require_eq( - &mind_adoption, - &["interpretation", "inputSummary", "safeActionFamily"], - "repo.markdown_planning_note", - )?; - require_bool( - &mind_adoption, - &["interpretation", "classification", "actionItemAccepted"], - true, - )?; - require_bool( - &mind_adoption, - &["interpretation", "classification", "safeFamilyRecognized"], - true, - )?; - require_bool( - &mind_adoption, - &["interpretation", "classification", "requestedPathsDeclared"], - true, - )?; - require_eq( - &mind_adoption, - &["interpretation", "classification", "durableStateAdmission"], - "not-admitted", - )?; - require_eq( - &mind_adoption, - &["interpretation", "allowedTransitions", "0"], - "hands.branch_local_action", - )?; - require_eq( - &mind_adoption, - &["interpretation", "forbiddenTransitions", "0"], - "mind.durable_state_commit", - )?; - require_bool( - &mind_adoption, - &["interpretation", "privateStateExposed"], - false, - )?; - require_eq( - &mind_adoption, - &["adoptedActionItem", "safeActionFamily"], - "repo.markdown_planning_note", - )?; - require_text_field(&mind_adoption, &["rationale"], "Mind adopted")?; - require_bool(&mind_adoption, &["privateStateExposed"], false)?; - require_eq( - &adoption, - &["adoptedActionItem", "safeActionFamily"], - "repo.markdown_planning_note", - )?; - require_text_field( - &adoption, - &["adoptedActionItem", "planningFacets", "decisionPoints", "0"], - "Self may adopt", - )?; - require_bool( - &adoption, - &["adoptedActionItem", "handsCommandAuthority"], - false, - )?; - require_bool( - &adoption, - &["adoptedActionItem", "durableStateAuthority"], - false, - )?; - require_bool( - &adoption, - &["adoptedActionItem", "publicationAuthorized"], - false, - )?; - require_bool( - &adoption, - &["adoptedActionItem", "privateStateExposed"], - false, - )?; - require_bool(&adoption, &["authority", "handsAuthorityGranted"], true)?; - require_bool(&adoption, &["authority", "durableStateAdmitted"], false)?; - require_bool(&adoption, &["privateStateExposed"], false)?; - let close = read_json( - &repo - .join(".epiphany") - .join("work") - .join(format!("work-close-{item}.json")), - )?; - require_eq(&close, &["status"], "closed")?; - require_eq(&close, &["soul", "verdict"], "passed")?; - require_eq( - &close, - &["closureReview", "familyAssertions", "status"], - "passed", - )?; - require_bool( - &close, - &["closureReview", "sourceGrounding", "pathScopeMatched"], - true, - )?; - require_bool(&close, &["privateStateExposed"], false)?; - - Ok(json!({ - "schemaVersion": "epiphany.repo_planning_facets_smoke.v0", - "status": "ok", - "smokeDir": smoke_dir, - "repo": repo, - "item": item, - "safeActionFamily": "repo.markdown_planning_note", - "modelAuthored": true, - "assumptionsRecorded": true, - "constraintsRecorded": true, - "nonGoalsRecorded": true, - "openQuestionsRecorded": true, - "decisionPointsRecorded": true, - "evidenceNeedsRecorded": true, - "adoptedActionItemRecorded": true, - "mindAdoptionDecisionRecorded": true, - "mindInterpretationRecorded": true, - "mindDecisionHandsAuthorityGranted": false, - "mindDecisionDurableStateAdmitted": false, - "adoptionFacetsRecorded": true, - "handsAuthorityGranted": false, - "durableStateAdmitted": false, - "handsCommandAuthority": false, - "durableStateAuthority": false, - "adoptionHandsAuthorityGranted": true, - "adoptionDurableStateAdmitted": false, - "closeStatus": "closed", - "soulVerdict": "passed", - "familyAssertionsStatus": "passed", - "pathScopeMatched": true, - "privateStateExposed": false - })) -} - -fn cargo_json(manifest_path: &Path, bin_name: &str, args: &[&str], cwd: &Path) -> Result { - let output = Command::new("cargo") - .arg("run") - .arg("--quiet") - .arg("--manifest-path") - .arg(manifest_path) - .arg("--bin") - .arg(bin_name) - .arg("--") - .args(args) - .current_dir(cwd) - .output() - .with_context(|| format!("failed to spawn cargo run --bin {bin_name}"))?; - if !output.status.success() { - return Err(anyhow!( - "cargo run --bin {bin_name} failed:\n{}", - String::from_utf8_lossy(&output.stderr) - )); - } - serde_json::from_slice(&output.stdout) - .with_context(|| format!("{bin_name} returned invalid JSON")) -} - -fn git(args: [&str; N], cwd: &Path) -> Result<()> { - let output = Command::new("git") - .args(args) - .current_dir(cwd) - .output() - .with_context(|| format!("failed to spawn git in {}", cwd.display()))?; - if output.status.success() { - Ok(()) - } else { - Err(anyhow!( - "git failed in {}:\n{}", - cwd.display(), - String::from_utf8_lossy(&output.stderr) - )) - } -} - -fn read_json(path: &Path) -> Result { - let raw = - fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?; - serde_json::from_str(&raw).with_context(|| format!("failed to decode {}", path.display())) -} - -fn require_eq(value: &Value, path: &[&str], expected: &str) -> Result<()> { - let actual = value_at_path(value, path) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("missing string at {}", path.join(".")))?; - if actual == expected { - Ok(()) - } else { - Err(anyhow!( - "expected {} to be {:?}, got {:?}", - path.join("."), - expected, - actual - )) - } -} - -fn require_bool(value: &Value, path: &[&str], expected: bool) -> Result<()> { - let actual = value_at_path(value, path) - .and_then(Value::as_bool) - .ok_or_else(|| anyhow!("missing bool at {}", path.join(".")))?; - if actual == expected { - Ok(()) - } else { - Err(anyhow!( - "expected {} to be {}, got {}", - path.join("."), - expected, - actual - )) - } -} - -fn require_text_field(value: &Value, path: &[&str], needle: &str) -> Result<()> { - let actual = value_at_path(value, path) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("missing string at {}", path.join(".")))?; - if actual.contains(needle) { - Ok(()) - } else { - Err(anyhow!( - "expected {} to contain {:?}, got {:?}", - path.join("."), - needle, - actual - )) - } -} - -fn value_at_path<'a>(value: &'a Value, path: &[&str]) -> Option<&'a Value> { - let mut cursor = value; - for segment in path { - if let Ok(index) = segment.parse::() { - cursor = cursor.as_array()?.get(index)?; - } else { - cursor = cursor.get(*segment)?; - } - } - Some(cursor) -} - -fn path_str(path: &Path) -> Result<&str> { - path.to_str() - .ok_or_else(|| anyhow!("path is not valid UTF-8: {}", path.display())) -} - -fn take_path(args: &mut impl Iterator, name: &str) -> Result { - Ok(PathBuf::from( - args.next() - .ok_or_else(|| anyhow!("missing value for {name}"))?, - )) -} diff --git a/epiphany-core/src/bin/epiphany-work.rs b/epiphany-core/src/bin/epiphany-work.rs index 66134c10..1398481e 100644 --- a/epiphany-core/src/bin/epiphany-work.rs +++ b/epiphany-core/src/bin/epiphany-work.rs @@ -28,13 +28,16 @@ use epiphany_core::MIND_GATEWAY_REVIEW_SCHEMA_VERSION; use epiphany_core::MindGatewayDecision; use epiphany_core::MindGatewayReview; use epiphany_core::PersonaMemoryCacheConfig; +use epiphany_core::REPO_WORK_HANDS_GRANT_SCHEMA_VERSION; use epiphany_core::REPO_WORK_MAP_ENTRY_SCHEMA_VERSION; use epiphany_core::REPO_WORK_MODELING_OUTPUT_CONTRACT_ID; use epiphany_core::REPO_WORK_MODELING_REQUEST_SCHEMA_VERSION; use epiphany_core::REPO_WORK_MODELING_ROUTE_SCHEMA_VERSION; +use epiphany_core::RepoWorkHandsGrant; use epiphany_core::RepoWorkMapEntry; use epiphany_core::RepoWorkModelingRequest; use epiphany_core::RepoWorkModelingRoute; +use epiphany_core::RepoWorkPlanAdoptionDecision; use epiphany_core::RuntimeSpineHeartbeatJobOptions; use epiphany_core::RuntimeSpineInitOptions; use epiphany_core::SOUL_VERDICT_RECEIPT_SCHEMA_VERSION; @@ -45,6 +48,7 @@ use epiphany_core::advance_repo_work_modeling_route; use epiphany_core::build_weksa_interlingua_packet; use epiphany_core::build_weksa_target_lowering_request; use epiphany_core::commit_initial_repo_work_modeling_route; +use epiphany_core::commit_repo_work_hands_grant; use epiphany_core::commit_repo_work_map_admission; use epiphany_core::default_launch_organ_contract; use epiphany_core::hands_action_review_for_intent; @@ -77,11 +81,14 @@ use epiphany_core::runtime_hands_action_intent; use epiphany_core::runtime_hands_action_review; use epiphany_core::runtime_hands_commit_receipt; use epiphany_core::runtime_job_snapshot; +use epiphany_core::runtime_repo_work_hands_grant; use epiphany_core::runtime_repo_work_map_entry; use epiphany_core::runtime_repo_work_modeling_finding; use epiphany_core::runtime_repo_work_modeling_request; use epiphany_core::runtime_repo_work_modeling_route; +use epiphany_core::runtime_repo_work_plan_adoption_review; use epiphany_core::substrate_gate_repo_work_planning_grant; +use epiphany_core::validate_hands_action_authority; use epiphany_core::write_epiphany_cultmesh_repo_work_map_entry; use epiphany_core::write_epiphany_cultmesh_repo_work_overview; use epiphany_core::write_epiphany_cultmesh_repo_work_public_proof; @@ -249,15 +256,12 @@ struct PlanReceiptInputs { #[derive(Clone, Debug)] struct AdoptArgs { workspace: PathBuf, - epiphany_root: PathBuf, item: Option, run_receipt: Option, plan_receipt: Option, runtime_store: Option, artifact_dir: Option, - plan_summary: Option, - adoption_evidence_refs: Vec, - mind_adoption_rationale: Option, + mind_review_id: String, } #[derive(Clone, Debug)] @@ -723,21 +727,17 @@ fn parse_derive_plan_args(args: impl Iterator) -> Result) -> Result { let mut workspace = None; - let mut epiphany_root = None; let mut item = None; let mut run_receipt = None; let mut plan_receipt = None; let mut runtime_store = None; let mut artifact_dir = None; - let mut plan_summary = None; - let mut adoption_evidence_refs = Vec::new(); - let mut mind_adoption_rationale = None; + let mut mind_review_id = None; let mut args = args.peekable(); while let Some(arg) = args.next() { match arg.as_str() { "--workspace" => workspace = Some(take_path(&mut args, "--workspace")?), - "--epiphany-root" => epiphany_root = Some(take_path(&mut args, "--epiphany-root")?), "--item" => item = Some(take_string(&mut args, "--item")?), "--run-receipt" => run_receipt = Some(take_path(&mut args, "--run-receipt")?), "--from-plan" | "--plan-receipt" => { @@ -745,34 +745,20 @@ fn parse_adopt_args(args: impl Iterator) -> Result { } "--runtime-store" => runtime_store = Some(take_path(&mut args, "--runtime-store")?), "--artifact-dir" => artifact_dir = Some(take_path(&mut args, "--artifact-dir")?), - "--plan-summary" => plan_summary = Some(take_string(&mut args, "--plan-summary")?), - "--adoption-evidence-ref" | "--evidence-ref" => { - adoption_evidence_refs.push(take_string(&mut args, "--adoption-evidence-ref")?); - } - "--mind-adoption-rationale" | "--adoption-rationale" => { - mind_adoption_rationale = - Some(take_string(&mut args, "--mind-adoption-rationale")?); + "--mind-review-id" => { + mind_review_id = Some(take_string(&mut args, "--mind-review-id")?) } other => return Err(anyhow!("unexpected adopt argument {other:?}")), } } - if adoption_evidence_refs.is_empty() && plan_receipt.is_none() { - return Err(anyhow!( - "adopt requires at least one --adoption-evidence-ref or --from-plan proving Imagination/Self/Mind adoption" - )); - } Ok(AdoptArgs { workspace: workspace.context("missing --workspace")?, - epiphany_root: epiphany_root - .unwrap_or(env::current_dir().context("failed to resolve current directory")?), item, run_receipt, plan_receipt, runtime_store, artifact_dir, - plan_summary, - adoption_evidence_refs, - mind_adoption_rationale, + mind_review_id: mind_review_id.context("adopt requires --mind-review-id")?, }) } @@ -5146,7 +5132,7 @@ fn derive_repo_doctrine_update_request_plan( "[required_receipts]".to_string(), "imagination_plan = \"epiphany.repo_work_imagination_action_items_receipt.v0\"" .to_string(), - "mind_adoption = \"epiphany.repo_work_mind_adoption_decision.v0\"".to_string(), + "mind_adoption = \"epiphany.repo_work_plan_adoption_review.v0\"".to_string(), "soul_review = \"epiphany.repo_work_closure_review.v0\"".to_string(), "maintainer_review = \"gamecult.maintainer.review_receipt.v0\"".to_string(), "hands_commit = \"epiphany.hands.commit_receipt\"".to_string(), @@ -5275,7 +5261,7 @@ fn derive_repo_secret_policy_request_plan( "[required_receipts]".to_string(), "source_grounding = \"epiphany.eyes.evidence_packet\"".to_string(), "soul_review = \"epiphany.repo_work_closure_review.v0\"".to_string(), - "mind_adoption = \"epiphany.repo_work_mind_adoption_decision.v0\"".to_string(), + "mind_adoption = \"epiphany.repo_work_plan_adoption_review.v0\"".to_string(), "maintainer_review = \"gamecult.maintainer.review_receipt.v0\"".to_string(), "bifrost_publication_review = \"gamecult.bifrost.publication_review_receipt.v0\"" .to_string(), @@ -5415,7 +5401,7 @@ fn derive_repo_dependency_policy_request_plan( "[required_receipts]".to_string(), "source_grounding = \"epiphany.eyes.evidence_packet\"".to_string(), "soul_review = \"epiphany.repo_work_closure_review.v0\"".to_string(), - "mind_adoption = \"epiphany.repo_work_mind_adoption_decision.v0\"".to_string(), + "mind_adoption = \"epiphany.repo_work_plan_adoption_review.v0\"".to_string(), "maintainer_review = \"gamecult.maintainer.review_receipt.v0\"".to_string(), "bifrost_publication_review = \"gamecult.bifrost.publication_review_receipt.v0\"" .to_string(), @@ -5556,7 +5542,7 @@ fn derive_repo_deployment_request_plan( String::new(), "[required_receipts]".to_string(), "source_grounding = \"epiphany.eyes.evidence_packet\"".to_string(), - "mind_adoption = \"epiphany.repo_work_mind_adoption_decision.v0\"".to_string(), + "mind_adoption = \"epiphany.repo_work_plan_adoption_review.v0\"".to_string(), "soul_review = \"epiphany.repo_work_closure_review.v0\"".to_string(), "maintainer_review = \"gamecult.maintainer.review_receipt.v0\"".to_string(), "secret_policy = \"epiphany.repo_secret_policy_request.v0\"".to_string(), @@ -5696,7 +5682,7 @@ fn derive_repo_deployment_config_plan( "daemon_owns_execution = true".to_string(), String::new(), "[required_receipts]".to_string(), - "mind_adoption = \"epiphany.repo_work_mind_adoption_decision.v0\"".to_string(), + "mind_adoption = \"epiphany.repo_work_plan_adoption_review.v0\"".to_string(), "soul_review = \"epiphany.repo_work_closure_review.v0\"".to_string(), "maintainer_review = \"gamecult.maintainer.review_receipt.v0\"".to_string(), "secret_policy = \"epiphany.repo_secret_policy_request.v0\"".to_string(), @@ -7540,143 +7526,85 @@ fn closure_mind_adoption_review(execute_receipt: &Value) -> Result<(Value, bool) false, )); } - let Some(mind_decision) = adopt_receipt.get("mindAdoptionDecision") else { + let runtime_store = path_from_json(execute_receipt, &["runtimeStore"]) + .or_else(|| path_from_json(&adopt_receipt, &["runtimeStore"])); + let mind_review_id = string_from_json(&adopt_receipt, &["mindReviewId"]); + let hands_grant_id = string_from_json(&adopt_receipt, &["handsGrantId"]); + let (Some(runtime_store), Some(mind_review_id), Some(hands_grant_id)) = + (runtime_store, mind_review_id, hands_grant_id) + else { return Ok(( json!({ "schemaVersion": "epiphany.repo_work_mind_adoption_closure_review.v0", "status": "failed", - "reason": "adopt receipt has no mindAdoptionDecision", + "reason": "adopt receipt does not bind runtime-spine Mind review and Hands grant ids", "adoptReceiptPath": adopt_receipt_path, "assertions": [] }), false, )); }; - let plan_derived = execute_receipt.get("planReceiptPath").is_some(); + let mind = runtime_repo_work_plan_adoption_review(&runtime_store, &mind_review_id)?; + let grant = runtime_repo_work_hands_grant(&runtime_store, &hands_grant_id)?; let mut assertions = Vec::new(); push_assertion( &mut assertions, - "decision-schema", - string_from_json(mind_decision, &["schemaVersion"]).as_deref() - == Some("epiphany.repo_work_mind_adoption_decision.v0"), - "Mind adoption decision carries the expected schema.".to_string(), + "typed-mind-review-present", + mind.is_some(), + "Runtime-spine contains the typed Mind adoption review.".to_string(), ); push_assertion( &mut assertions, - "decision-status-adopted", - string_from_json(mind_decision, &["status"]).as_deref() - == Some("adopted-for-branch-local-hands"), - "Mind adoption decision accepted branch-local Hands authority.".to_string(), + "typed-hands-grant-present", + grant.is_some(), + "Runtime-spine contains the typed Hands grant derived from Mind adoption.".to_string(), ); - push_assertion( - &mut assertions, - "decision-owner-mind", - string_from_json(mind_decision, &["owner"]).as_deref() == Some("Mind"), - "Mind is the recorded adoption decision owner.".to_string(), - ); - push_assertion( - &mut assertions, - "decision-interpreter-mind", - string_from_json(mind_decision, &["interpreter"]).as_deref() == Some("Mind"), - "Mind is the recorded adoption interpreter.".to_string(), - ); - push_assertion( - &mut assertions, - "action-item-accepted", - bool_from_json( - mind_decision, - &["interpretation", "classification", "actionItemAccepted"], - ) == Some(true), - "Mind classified the plan cargo as an accepted action item.".to_string(), - ); - push_assertion( - &mut assertions, - "safe-family-recognized", - bool_from_json( - mind_decision, - &["interpretation", "classification", "safeFamilyRecognized"], - ) == Some(true) - && bool_from_json(mind_decision, &["gates", "safeFamilyRequired"]) == Some(true), - "Mind recognized the safe family and recorded the safe-family gate.".to_string(), - ); - push_assertion( - &mut assertions, - "requested-paths-match-plan", - !plan_derived - || (bool_from_json( - mind_decision, - &[ - "interpretation", - "classification", - "requestedPathsMatchPlan", - ], - ) == Some(true) - && bool_from_json(mind_decision, &["gates", "branchLocalOnly"]) == Some(true)), - "Mind confirmed requested paths match the plan before branch-local Hands authority." - .to_string(), - ); - push_assertion( - &mut assertions, - "private-state-sealed", - bool_from_json(mind_decision, &["privateStateExposed"]) == Some(false), - "Mind adoption decision preserves the private-state seal.".to_string(), - ); - - let mind_decision_path = path_from_json(mind_decision, &["receiptPath"]); - let standalone_review = if let Some(path) = mind_decision_path.as_ref() { - match read_json_if_exists(path)? { - Some(standalone) => { - let matching_decision_id = string_from_json(&standalone, &["decisionId"]) - == string_from_json(mind_decision, &["decisionId"]); - let matching_status = string_from_json(&standalone, &["status"]) - == string_from_json(mind_decision, &["status"]); - let matching_schema = string_from_json(&standalone, &["schemaVersion"]) - == string_from_json(mind_decision, &["schemaVersion"]); - let matching_private_seal = bool_from_json(&standalone, &["privateStateExposed"]) - == bool_from_json(mind_decision, &["privateStateExposed"]); - push_assertion( - &mut assertions, - "standalone-decision-matches-snapshot", - matching_decision_id - && matching_status - && matching_schema - && matching_private_seal, - "Standalone Mind decision matches the adopt receipt snapshot.".to_string(), - ); - json!({ - "status": "present", - "receiptPath": path, - "decisionIdMatched": matching_decision_id, - "statusMatched": matching_status, - "schemaMatched": matching_schema, - "privateStateSealMatched": matching_private_seal - }) - } - None => { - push_assertion( - &mut assertions, - "standalone-decision-present", - false, - "Standalone Mind decision path exists in snapshot but the file is missing." - .to_string(), - ); - json!({ - "status": "missing", - "receiptPath": path - }) - } - } - } else { + if let (Some(mind), Some(grant)) = (mind.as_ref(), grant.as_ref()) { + let mind_sha256 = format!("{:x}", Sha256::digest(serde_json::to_vec(mind)?)); + let approved = + runtime_hands_action_review(&runtime_store, &grant.approved_hands_review_id)?; push_assertion( &mut assertions, - "standalone-decision-path-present", - false, - "Adopt receipt snapshot names the standalone Mind decision receipt path.".to_string(), + "mind-review-adopts-exact-plan", + mind.schema_version == epiphany_core::REPO_WORK_PLAN_ADOPTION_REVIEW_SCHEMA_VERSION + && mind.decision == RepoWorkPlanAdoptionDecision::Adopt + && !mind.private_state_exposed, + "Typed Mind review adopts one sealed immutable plan binding.".to_string(), ); - json!({ - "status": "missing-path" - }) - }; + push_assertion( + &mut assertions, + "hands-grant-binds-mind-review", + grant.schema_version == REPO_WORK_HANDS_GRANT_SCHEMA_VERSION + && grant.adoption_review_id == mind.review_id + && grant.adoption_review_sha256 == mind_sha256 + && grant.workspace_identity == mind.workspace_identity + && grant.plan_id == mind.plan_id + && grant.plan_sha256 == mind.plan_sha256 + && grant.run_receipt_sha256 == mind.run_receipt_sha256 + && grant.changed_paths == mind.changed_paths + && !grant.private_state_exposed, + "Typed Hands grant exactly binds the persisted Mind review and plan/run identity." + .to_string(), + ); + push_assertion( + &mut assertions, + "approved-hands-review-bound", + approved.as_ref().is_some_and(|review| { + review.review_id == grant.approved_hands_review_id + && review.intent_id == grant.hands_intent_id + && review.decision == "approved" + && review.allowed_operations == grant.allowed_operations + }), + "Approved Hands review is the review named by the typed grant.".to_string(), + ); + push_assertion( + &mut assertions, + "execute-receipt-binds-adoption-chain", + execute_receipt_binds_adoption_chain(execute_receipt, mind, grant), + "Execute receipt is cross-bound to the same workspace, plan, run, action, Hands, and Substrate authority chain." + .to_string(), + ); + } let passed = assertions .iter() @@ -7686,16 +7614,63 @@ fn closure_mind_adoption_review(execute_receipt: &Value) -> Result<(Value, bool) "schemaVersion": "epiphany.repo_work_mind_adoption_closure_review.v0", "status": if passed { "passed" } else { "failed" }, "adoptReceiptPath": adopt_receipt_path, - "mindDecisionPath": mind_decision_path, - "planDerived": plan_derived, + "runtimeStore": runtime_store, + "mindReviewId": mind_review_id, + "handsGrantId": hands_grant_id, "assertions": assertions, - "standaloneDecisionReview": standalone_review, "privateStateExposed": false }), passed, )) } +fn execute_receipt_binds_adoption_chain( + execute_receipt: &Value, + mind: &epiphany_core::RepoWorkPlanAdoptionReview, + grant: &RepoWorkHandsGrant, +) -> bool { + let execute_workspace = path_from_json(execute_receipt, &["workspace"]) + .and_then(|path| path.canonicalize().ok()) + .map(|path| normalize_path_for_receipt(&path)); + let execute_plan_path = path_from_json(execute_receipt, &["planReceiptPath"]) + .and_then(|path| path.canonicalize().ok()) + .map(|path| normalize_path_for_receipt(&path)); + execute_workspace.as_deref() == Some(grant.workspace_identity.as_str()) + && string_from_json(execute_receipt, &["item"]).as_deref() == Some(grant.item.as_str()) + && execute_plan_path.as_deref() == Some(grant.plan_receipt_path.as_str()) + && string_from_json(execute_receipt, &["adoptionAuthority", "mindReviewId"]).as_deref() + == Some(mind.review_id.as_str()) + && string_from_json(execute_receipt, &["adoptionAuthority", "handsGrantId"]).as_deref() + == Some(grant.grant_id.as_str()) + && string_from_json(execute_receipt, &["adoptionAuthority", "planId"]).as_deref() + == Some(grant.plan_id.as_str()) + && string_from_json(execute_receipt, &["adoptionAuthority", "planSha256"]).as_deref() + == Some(grant.plan_sha256.as_str()) + && string_from_json(execute_receipt, &["adoptionAuthority", "runReceiptPath"]).as_deref() + == Some(grant.run_receipt_path.as_str()) + && string_from_json(execute_receipt, &["adoptionAuthority", "runReceiptSha256"]).as_deref() + == Some(grant.run_receipt_sha256.as_str()) + && string_from_json(execute_receipt, &["adoptionAuthority", "actionId"]).as_deref() + == Some(grant.action_id.as_str()) + && string_from_json(execute_receipt, &["adoptionAuthority", "handsIntentId"]).as_deref() + == Some(grant.hands_intent_id.as_str()) + && string_from_json( + execute_receipt, + &["adoptionAuthority", "approvedHandsReviewId"], + ) + .as_deref() + == Some(grant.approved_hands_review_id.as_str()) + && string_from_json( + execute_receipt, + &["adoptionAuthority", "substrateGrantReceiptId"], + ) + .as_deref() + == Some(grant.substrate_grant_receipt_id.as_str()) + && string_array_from_json(execute_receipt, &["changedPaths"]) == grant.changed_paths + && string_from_json(execute_receipt, &["command"]).as_deref() + == Some(grant.action_command.as_str()) +} + fn record_repo_work_map_admission( runtime_store: &Path, item: &str, @@ -8246,459 +8221,142 @@ fn write_plan_receipt( } fn run_adopt(args: AdoptArgs) -> Result { - let workspace = args - .workspace - .canonicalize() - .with_context(|| format!("failed to resolve {}", args.workspace.display()))?; + let workspace = args.workspace.canonicalize()?; ensure_git_repo(&workspace)?; - let _epiphany_root = args - .epiphany_root - .canonicalize() - .with_context(|| format!("failed to resolve {}", args.epiphany_root.display()))?; - let run_receipt_path = resolve_run_receipt(&workspace, args.item.as_deref(), args.run_receipt)?; - let run_receipt = read_json(&run_receipt_path)?; - let plan_receipt_path = if let Some(path) = args.plan_receipt { - Some(path) - } else { - None - }; - let plan_receipt = if let Some(path) = plan_receipt_path.as_ref() { - Some(read_json(path)?) - } else { - None - }; - let runtime_store = args.runtime_store.unwrap_or_else(|| { - path_from_json(&run_receipt, &["runtimeStore"]).unwrap_or_else(|| { - workspace - .join(".epiphany") - .join("state") - .join("runtime-spine.msgpack") - }) - }); - let artifact_dir = args - .artifact_dir - .unwrap_or_else(|| workspace.join(".epiphany").join("work")); - fs::create_dir_all(&artifact_dir) - .with_context(|| format!("failed to create {}", artifact_dir.display()))?; - let gate = run_receipt + let run_path = + resolve_run_receipt(&workspace, args.item.as_deref(), args.run_receipt)?.canonicalize()?; + let plan_path = args + .plan_receipt + .ok_or_else(|| anyhow!("adopt requires --from-plan"))? + .canonicalize()?; + let run = read_json(&run_path)?; + let plan = read_json(&plan_path)?; + let store = args + .runtime_store + .or_else(|| path_from_json(&run, &["runtimeStore"])) + .unwrap_or_else(|| workspace.join(".epiphany/state/runtime-spine.msgpack")); + let gate = run .get("handsActionGate") .ok_or_else(|| anyhow!("run receipt has no handsActionGate"))?; - let intent_id = gate - .get("intentId") - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("run receipt handsActionGate has no intentId"))?; - let review_id = gate - .get("reviewId") - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("run receipt handsActionGate has no reviewId"))?; - let intent = runtime_hands_action_intent(&runtime_store, intent_id)? - .ok_or_else(|| anyhow!("runtime-spine has no Hands intent {intent_id}"))?; - let queued_review = runtime_hands_action_review(&runtime_store, review_id)? - .ok_or_else(|| anyhow!("runtime-spine has no Hands review {review_id}"))?; - if queued_review.intent_id != intent.intent_id { + let intent_id = + string_from_value(gate, "intentId").ok_or_else(|| anyhow!("run gate has no intentId"))?; + let queued_id = + string_from_value(gate, "reviewId").ok_or_else(|| anyhow!("run gate has no reviewId"))?; + let intent = runtime_hands_action_intent(&store, &intent_id)? + .ok_or_else(|| anyhow!("missing Hands intent"))?; + let queued = runtime_hands_action_review(&store, &queued_id)? + .ok_or_else(|| anyhow!("missing queued Hands review"))?; + if queued.intent_id != intent.intent_id || queued.decision != "queued-for-adoption" { + return Err(anyhow!("invalid queued Hands review")); + } + let mind = runtime_repo_work_plan_adoption_review(&store, &args.mind_review_id)? + .ok_or_else(|| anyhow!("missing typed Mind adoption review {}", args.mind_review_id))?; + if mind.decision != RepoWorkPlanAdoptionDecision::Adopt { + return Err(anyhow!("Mind did not adopt this plan")); + } + let workspace_id = normalize_path_for_receipt(&workspace); + let item = string_from_json(&run, &["item"]).ok_or_else(|| anyhow!("run has no item"))?; + let plan_schema = string_from_json(&plan, &["schemaVersion"]) + .ok_or_else(|| anyhow!("plan has no schemaVersion"))?; + if plan_schema != "epiphany.repo_work_action_plan_receipt.v0" { + return Err(anyhow!("unsupported repo-work plan schema {plan_schema}")); + } + let plan_id = + string_from_json(&plan, &["planId"]).ok_or_else(|| anyhow!("plan has no planId"))?; + let action = first_plan_action(&plan).ok_or_else(|| anyhow!("plan has no action"))?; + let action_id = + string_from_value(action, "actionId").ok_or_else(|| anyhow!("action has no actionId"))?; + let action_command = + string_from_value(action, "command").ok_or_else(|| anyhow!("action has no command"))?; + let action_commit_message = string_from_value(action, "commitMessage") + .ok_or_else(|| anyhow!("action has no commitMessage"))?; + let raw_paths = string_array_field(action, "changedPaths"); + let paths = strict_repo_work_paths(raw_paths)?; + let plan_hash = file_sha256(&plan_path)?; + let run_hash = file_sha256(&run_path)?; + let plan_path_id = normalize_path_for_receipt(&plan_path); + let run_path_id = normalize_path_for_receipt(&run_path); + if mind.workspace_identity != workspace_id + || mind.item != item + || mind.plan_schema_version != plan_schema + || mind.plan_id != plan_id + || mind.plan_sha256 != plan_hash + || mind.run_receipt_sha256 != run_hash + || mind.plan_receipt_path != plan_path_id + || mind.run_receipt_path != run_path_id + || mind.hands_intent_id != intent.intent_id + || mind.queued_hands_review_id != queued.review_id + || mind.substrate_grant_receipt_id != intent.substrate_gate_grant_receipt_id + || mind.action_id != action_id + || mind.action_command != action_command + || mind.action_commit_message != action_commit_message + || mind.changed_paths != paths + { return Err(anyhow!( - "Hands review {} belongs to {}, not {}", - queued_review.review_id, - queued_review.intent_id, - intent.intent_id + "Mind review does not exactly bind current plan/run bytes and authority chain" )); } - if queued_review.decision != "queued-for-adoption" { - return Err(anyhow!( - "Hands review {} decision is {}, not queued-for-adoption", - queued_review.review_id, - queued_review.decision - )); - } - let item = run_receipt - .get("item") - .and_then(Value::as_str) - .unwrap_or("work-item") - .to_string(); - let item_slug = sanitize(&item); let now = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true); - let plan_summary = if let Some(summary) = args.plan_summary { - summary - } else if let Some(plan) = plan_receipt.as_ref() { - string_from_json(plan, &["planSummary"]) - .ok_or_else(|| anyhow!("plan receipt has no planSummary"))? - } else { - return Err(anyhow!("missing --plan-summary or --from-plan")); - }; - let mut adoption_evidence_refs = args.adoption_evidence_refs; - if let Some(plan) = plan_receipt.as_ref() { - adoption_evidence_refs.extend(string_array_from_json(plan, &["adoptionEvidenceRefs"])); - if adoption_evidence_refs.is_empty() { - adoption_evidence_refs.push(format!( - "repo-work-plan:{}", - plan.get("planId") - .and_then(Value::as_str) - .unwrap_or("unknown") - )); - } - } - let adopted_action_item = plan_receipt - .as_ref() - .map(adopted_action_item_from_plan) - .unwrap_or(Value::Null); - let action_item_receipt_id = adopted_action_item - .get("receiptId") - .and_then(Value::as_str) - .unwrap_or("manual-plan"); - let action_item_summary = adopted_action_item - .get("summary") - .and_then(Value::as_str) - .unwrap_or(&plan_summary); - let action_item_safe_family = adopted_action_item - .get("safeActionFamily") - .and_then(Value::as_str) - .unwrap_or("manual-plan"); - let requested_path_count = adopted_action_item - .get("requestedPaths") - .and_then(Value::as_array) - .map(Vec::len) - .unwrap_or(0); - let requested_paths = sorted_normalized_paths(string_array_from_json( - &adopted_action_item, - &["requestedPaths"], - )); - let plan_changed_paths = plan_receipt - .as_ref() - .and_then(|plan| first_plan_action(plan)) - .map(|action| sorted_normalized_paths(string_array_field(action, "changedPaths"))) - .unwrap_or_default(); - let requested_paths_match_plan = plan_receipt.is_none() - || (!requested_paths.is_empty() && requested_paths == plan_changed_paths); - let planning_facets_present = adopted_action_item - .get("planningFacets") - .is_some_and(|value| !value.is_null()); - let verification_asks = string_array_from_json(&adopted_action_item, &["verificationAsks"]); - let verification_asks_present = !verification_asks.is_empty(); - let verification_ask_count = verification_asks.len(); - let evidence_needs = - string_array_from_json(&adopted_action_item, &["planningFacets", "evidenceNeeds"]); - let evidence_needs_present = !evidence_needs.is_empty(); - let evidence_need_count = evidence_needs.len(); - let action_item_model_authored = adopted_action_item - .get("modelAuthored") - .and_then(Value::as_bool) - .unwrap_or(false); - let evidence_ref_count = adoption_evidence_refs.len(); - let safe_family_recognized = repo_work_safe_family_is_recognized(action_item_safe_family); - let unsupported_plan_family = plan_receipt.is_some() && !safe_family_recognized; - let path_scope_mismatch = plan_receipt.is_some() && !requested_paths_match_plan; - let evidence_readiness_missing = - plan_receipt.is_some() && (!verification_asks_present || !evidence_needs_present); - let mut refusal_reasons = Vec::new(); - if unsupported_plan_family { - refusal_reasons.push(format!( - "Unsupported repo-work safe action family {action_item_safe_family}; Mind refused to convert this plan into branch-local Hands authority." - )); - } - if path_scope_mismatch { - refusal_reasons.push(format!( - "Requested paths {:?} do not match plan changed paths {:?}; Mind refused to convert this plan into branch-local Hands authority.", - requested_paths, plan_changed_paths - )); - } - if evidence_readiness_missing { - refusal_reasons.push(format!( - "Action item lacks verification or evidence needs; Mind refused to convert this plan into branch-local Hands authority until Soul proof targets are explicit. verificationAsksPresent={verification_asks_present}, evidenceNeedsPresent={evidence_needs_present}." - )); - } - let mind_refused_adoption = - unsupported_plan_family || path_scope_mismatch || evidence_readiness_missing; - let mind_interpretation = json!({ - "schemaVersion": "epiphany.repo_work_mind_interpretation.v0", - "owner": "Mind", - "interpreter": "Mind", - "router": "Self", - "source": if plan_receipt.is_some() { "plan-receipt" } else { "manual-adoption-evidence" }, - "inputSummary": { - "planReceiptPresent": plan_receipt.is_some(), - "runReceiptPresent": true, - "actionItemReceiptId": action_item_receipt_id, - "safeActionFamily": action_item_safe_family, - "requestedPathCount": requested_path_count, - "requestedPaths": requested_paths, - "planChangedPaths": plan_changed_paths, - "modelAuthored": action_item_model_authored, - "planningFacetsPresent": planning_facets_present, - "verificationAsks": verification_asks, - "verificationAskCount": verification_ask_count, - "evidenceNeeds": evidence_needs, - "evidenceNeedCount": evidence_need_count, - "adoptionEvidenceRefCount": evidence_ref_count - }, - "classification": { - "decisionKind": "branch-local-hands-adoption", - "actionItemAccepted": !mind_refused_adoption, - "safeFamilyRecognized": safe_family_recognized, - "requestedPathsDeclared": requested_path_count > 0, - "requestedPathsMatchPlan": requested_paths_match_plan, - "verificationAsksPresent": verification_asks_present, - "evidenceNeedsPresent": evidence_needs_present, - "evidenceRefsPresent": evidence_ref_count > 0, - "durableStateAdmission": "not-admitted", - "publicationGate": "Bifrost", - "closureGate": "Soul", - "privateStateSeal": true - }, - "allowedTransitions": [ - "hands.branch_local_action" - ], - "forbiddenTransitions": [ - "mind.durable_state_commit", - "bifrost.publication", - "git.merge", - "idunn.service_lifecycle", - "cross_body_mutation", - "private_verse_export" - ], - "refusalReasons": refusal_reasons, - "privateStateExposed": false - }); - let mind_adoption_id = format!("repo-work-mind-adoption-{item_slug}"); - if mind_refused_adoption { - let status = if unsupported_plan_family { - "refused-unsupported-safe-family" - } else if evidence_readiness_missing { - "refused-missing-evidence-needs" - } else { - "refused-requested-path-mismatch" - }; - let next_gate = if unsupported_plan_family { - "imagination.replan_with_allowed_safe_family" - } else if evidence_readiness_missing { - "imagination.replan_with_explicit_soul_evidence_needs" - } else { - "imagination.replan_with_matching_requested_paths" - }; - let next_safe_move = if unsupported_plan_family { - "Ask Imagination for an allowed repo-work safe family before Hands authority can be granted." - } else if evidence_readiness_missing { - "Ask Imagination to add explicit verification asks and evidence needs before Hands authority can be granted." - } else { - "Ask Imagination for an action item whose requested paths exactly match the plan changed paths before Hands authority can be granted." - }; - let refusal_decision = json!({ - "schemaVersion": "epiphany.repo_work_mind_adoption_decision.v0", - "createdAt": now, - "workspace": workspace, - "runtimeId": run_receipt["runtimeId"], - "runtimeStore": runtime_store, - "decisionId": mind_adoption_id, - "item": item, - "status": status, - "owner": "Mind", - "interpreter": "Mind", - "router": "Self", - "sourcePlanReceiptPath": plan_receipt_path, - "sourceRunReceiptPath": run_receipt_path, - "adoptedActionItemReceiptId": action_item_receipt_id, - "adoptedActionItem": adopted_action_item, - "adoptionEvidenceRefs": adoption_evidence_refs, - "interpretation": mind_interpretation, - "rationale": refusal_reasons.join(" "), - "gates": { - "selfPresentedActionItem": true, - "mindReviewedEvidence": true, - "mindInterpretedActionItem": true, - "safeFamilyRequired": true, - "safeFamilyRecognized": safe_family_recognized, - "requestedPathsMatchPlan": requested_paths_match_plan, - "verificationAsksPresent": verification_asks_present, - "evidenceNeedsPresent": evidence_needs_present, - "branchLocalOnly": false, - "bifrostPublicationRequired": true, - "soulClosureRequired": true - }, - "authority": { - "durableStateAdmitted": false, - "handsAuthorityGranted": false, - "publicationAuthorized": false, - "mergeAuthorized": false, - "serviceLifecycleAuthority": false, - "crossRepoMutation": false, - "privateStateExposed": false, - "nextGate": next_gate - }, - "privateStateExposed": false, - "nextSafeMove": next_safe_move - }); - let mind_adoption_path = artifact_dir.join(format!("work-mind-adopt-{item_slug}.json")); - write_json(&mind_adoption_path, &refusal_decision)?; - if unsupported_plan_family { - return Err(anyhow!( - "Mind refused adoption for unsupported repo-work safe family {action_item_safe_family}: {}; refusal decision written to {}", - refusal_reasons.join(" "), - mind_adoption_path.display() - )); - } - return Err(anyhow!( - "Mind refused adoption: {}; refusal decision written to {}", - refusal_reasons.join(" "), - mind_adoption_path.display() - )); - } - let mind_adoption_rationale = args.mind_adoption_rationale.unwrap_or_else(|| { - format!( - "Mind adopted the selected Imagination action item for branch-local Hands work because Self presented explicit adoption evidence and the safe family remains bounded: {}", - compact_line(action_item_summary) - ) - }); - let mind_adoption_decision = json!({ - "schemaVersion": "epiphany.repo_work_mind_adoption_decision.v0", - "createdAt": now, - "workspace": workspace, - "runtimeId": run_receipt["runtimeId"], - "runtimeStore": runtime_store, - "decisionId": mind_adoption_id, - "item": item, - "status": "adopted-for-branch-local-hands", - "owner": "Mind", - "interpreter": "Mind", - "router": "Self", - "sourcePlanReceiptPath": plan_receipt_path, - "sourceRunReceiptPath": run_receipt_path, - "adoptedActionItemReceiptId": action_item_receipt_id, - "adoptedActionItem": adopted_action_item, - "adoptionEvidenceRefs": adoption_evidence_refs, - "interpretation": mind_interpretation, - "rationale": mind_adoption_rationale, - "gates": { - "selfPresentedActionItem": true, - "mindReviewedEvidence": true, - "mindInterpretedActionItem": true, - "safeFamilyRequired": true, - "branchLocalOnly": true, - "bifrostPublicationRequired": true, - "soulClosureRequired": true - }, - "authority": { - "durableStateAdmitted": false, - "handsAuthorityGranted": false, - "publicationAuthorized": false, - "mergeAuthorized": false, - "serviceLifecycleAuthority": false, - "crossRepoMutation": false, - "privateStateExposed": false, - "nextGate": "hands.branch_local_action" - }, - "privateStateExposed": false, - "nextSafeMove": "The adoption decision may be cited by the Hands review; it does not itself execute, publish, merge, or mutate durable project state." - }); - let mind_adoption_path = artifact_dir.join(format!("work-mind-adopt-{item_slug}.json")); - write_json(&mind_adoption_path, &mind_adoption_decision)?; - let reread_mind_adoption = read_json(&mind_adoption_path)?; - if reread_mind_adoption["schemaVersion"] != "epiphany.repo_work_mind_adoption_decision.v0" { - return Err(anyhow!( - "Mind adoption decision {} failed reread schema verification", - mind_adoption_path.display() - )); - } - if reread_mind_adoption["privateStateExposed"] != json!(false) { - return Err(anyhow!( - "Mind adoption decision {} exposed private state", - mind_adoption_path.display() - )); - } - let mut approved_review = hands_action_review_for_intent( - review_id.to_string(), + let approved_id = format!("{}-approved-by-{}", queued.review_id, mind.review_id); + let operations = vec!["patch".into(), "command".into(), "commit".into()]; + let mut approved = hands_action_review_for_intent( + approved_id.clone(), &intent, - "approved".to_string(), - vec![ - "patch".to_string(), - "command".to_string(), - "commit".to_string(), - ], - vec![ - format!("Adopted plan: {plan_summary}"), - format!( - "Adoption evidence refs: {}", - adoption_evidence_refs.join(", ") - ), - "Bifrost still gates publication and merge.".to_string(), - ], + "approved".into(), + operations.clone(), + vec![format!("Mind review {}", mind.review_id)], now.clone(), ); - approved_review.required_receipts = vec![ - HANDS_PATCH_RECEIPT_TYPE.to_string(), - HANDS_COMMAND_RECEIPT_TYPE.to_string(), - HANDS_COMMIT_RECEIPT_TYPE.to_string(), + approved.required_receipts = vec![ + HANDS_PATCH_RECEIPT_TYPE.into(), + HANDS_COMMAND_RECEIPT_TYPE.into(), + HANDS_COMMIT_RECEIPT_TYPE.into(), ]; - put_hands_action_review(&runtime_store, &approved_review)?; - - let adoption_receipt = json!({ - "schemaVersion": "epiphany.repo_work_adoption_receipt.v0", - "createdAt": now, - "workspace": workspace, - "runtimeId": run_receipt["runtimeId"], - "runtimeStore": runtime_store, - "runReceiptPath": run_receipt_path, - "planReceiptPath": plan_receipt_path, - "item": item, - "status": "approved-for-branch-local-hands-action", - "planSummary": plan_summary, - "adoptionEvidenceRefs": adoption_evidence_refs, - "mindAdoptionDecision": { - "decisionId": mind_adoption_decision["decisionId"], - "receiptPath": mind_adoption_path, - "schemaVersion": mind_adoption_decision["schemaVersion"], - "status": mind_adoption_decision["status"], - "owner": mind_adoption_decision["owner"], - "interpreter": mind_adoption_decision["interpreter"], - "interpretation": mind_adoption_decision["interpretation"], - "rationale": mind_adoption_decision["rationale"], - "gates": mind_adoption_decision["gates"], - "authority": mind_adoption_decision["authority"], - "privateStateExposed": false - }, - "adoptedActionItem": adopted_action_item, - "handsActionGate": { - "intentId": intent.intent_id, - "reviewId": approved_review.review_id, - "runtimeJobId": intent.runtime_job_id, - "substrateGateGrantReceiptId": intent.substrate_gate_grant_receipt_id, - "decision": approved_review.decision, - "allowedOperations": approved_review.allowed_operations, - "requiredReceipts": approved_review.required_receipts, - "recordPassCommand": format!( - "epiphany-hands-action --store {} record-pass --intent-id {} --review-id {} --summary --changed-path --command --exit-code --stdout-artifact --stderr-artifact --commit-sha --branch ", - runtime_store.display(), - intent.intent_id, - approved_review.review_id - ) - }, - "authority": { - "handsAuthorityGranted": true, - "durableStateAdmitted": false, - "publicationAuthorized": false, - "publicationGate": "Bifrost", - "requiredReceiptsBeforeCompletion": approved_review.required_receipts - }, - "privateStateExposed": false, - "nextSafeMove": "Execute branch-local work through Hands and record patch/command/commit receipts; Bifrost still gates publish/merge." - }); - let receipt_path = artifact_dir.join(format!("work-adopt-{item_slug}.json")); - write_json(&receipt_path, &adoption_receipt)?; - Ok(json!({ - "schemaVersion": "epiphany.repo_work_adoption.v0", - "status": "approved-for-branch-local-hands-action", - "workspace": adoption_receipt["workspace"], - "runtimeId": adoption_receipt["runtimeId"], - "runtimeStore": adoption_receipt["runtimeStore"], - "receiptPath": receipt_path, - "item": adoption_receipt["item"], - "mindAdoptionDecision": adoption_receipt["mindAdoptionDecision"], - "adoptedActionItem": adoption_receipt["adoptedActionItem"], - "handsActionGate": adoption_receipt["handsActionGate"], - "authority": adoption_receipt["authority"], - "privateStateExposed": adoption_receipt["privateStateExposed"], - "nextSafeMove": adoption_receipt["nextSafeMove"], - })) + let grant_id = format!("repo-work-hands-grant-{}", mind.review_id); + let grant = RepoWorkHandsGrant { + schema_version: REPO_WORK_HANDS_GRANT_SCHEMA_VERSION.into(), + grant_id: grant_id.clone(), + adoption_review_id: mind.review_id.clone(), + adoption_review_sha256: format!("{:x}", Sha256::digest(serde_json::to_vec(&mind)?)), + workspace_identity: workspace_id.clone(), + item: item.clone(), + plan_id: plan_id.clone(), + plan_sha256: plan_hash.clone(), + run_receipt_sha256: run_hash.clone(), + plan_receipt_path: plan_path_id, + run_receipt_path: run_path_id, + hands_intent_id: intent.intent_id.clone(), + queued_hands_review_id: queued.review_id.clone(), + approved_hands_review_id: approved_id.clone(), + substrate_grant_receipt_id: intent.substrate_gate_grant_receipt_id.clone(), + action_id, + action_command, + action_commit_message, + allowed_operations: operations, + changed_paths: paths, + granted_at: now.clone(), + private_state_exposed: false, + }; + commit_repo_work_hands_grant(&store, &grant, &approved)?; + let dir = args + .artifact_dir + .unwrap_or_else(|| workspace.join(".epiphany/work")); + fs::create_dir_all(&dir)?; + let receipt_path = dir.join(format!("work-adopt-{}.json", sanitize(&item))); + let receipt = json!({"schemaVersion":"epiphany.repo_work_adoption_receipt.v1","createdAt":now, + "workspace":workspace_id,"runtimeStore":store,"runReceiptPath":run_path,"runReceiptSha256":run_hash, + "planReceiptPath":plan_path,"planId":plan_id,"planSha256":plan_hash,"item":item, + "status":"approved-for-branch-local-hands-action","mindReviewId":mind.review_id,"handsGrantId":grant_id, + "handsActionGate":{"intentId":intent.intent_id,"queuedReviewId":queued.review_id,"reviewId":approved_id, + "substrateGateGrantReceiptId":intent.substrate_gate_grant_receipt_id}, + "authority":{"handsAuthorityGranted":true,"publicationAuthorized":false,"publicationGate":"Bifrost"},"privateStateExposed":false}); + write_json(&receipt_path, &receipt)?; + Ok( + json!({"schemaVersion":"epiphany.repo_work_adoption.v1","status":receipt["status"],"workspace":receipt["workspace"], + "runtimeStore":receipt["runtimeStore"],"receiptPath":receipt_path,"item":receipt["item"],"mindReviewId":receipt["mindReviewId"], + "handsGrantId":receipt["handsGrantId"],"handsActionGate":receipt["handsActionGate"],"authority":receipt["authority"],"privateStateExposed":false}), + ) } - fn run_execute(args: ExecuteArgs) -> Result { let workspace = args .workspace @@ -8750,6 +8408,42 @@ fn run_execute(args: ExecuteArgs) -> Result { .ok_or_else(|| anyhow!("runtime-spine has no Hands intent {intent_id}"))?; let review = runtime_hands_action_review(&runtime_store, review_id)? .ok_or_else(|| anyhow!("runtime-spine has no Hands review {review_id}"))?; + let grant_id = string_from_json(&adopt_receipt, &["handsGrantId"]) + .ok_or_else(|| anyhow!("adopt receipt has no typed Hands grant id"))?; + let mind_review_id = string_from_json(&adopt_receipt, &["mindReviewId"]) + .ok_or_else(|| anyhow!("adopt receipt has no typed Mind review id"))?; + let grant = runtime_repo_work_hands_grant(&runtime_store, &grant_id)? + .ok_or_else(|| anyhow!("runtime-spine has no repo-work Hands grant {grant_id}"))?; + let mind = runtime_repo_work_plan_adoption_review(&runtime_store, &mind_review_id)? + .ok_or_else(|| anyhow!("runtime-spine has no Mind adoption review {mind_review_id}"))?; + if mind.decision != RepoWorkPlanAdoptionDecision::Adopt + || grant.adoption_review_id != mind.review_id + || grant.approved_hands_review_id != review.review_id + || grant.hands_intent_id != intent.intent_id + || grant.workspace_identity != normalize_path_for_receipt(&workspace) + || grant.adoption_review_sha256 + != format!("{:x}", Sha256::digest(serde_json::to_vec(&mind)?)) + { + return Err(anyhow!( + "execute authority is not bound to its typed Mind review and Hands grant" + )); + } + let plan_path = plan_receipt_path + .as_ref() + .ok_or_else(|| anyhow!("execute requires the exact reviewed plan receipt"))?; + let run_path = path_from_json(&adopt_receipt, &["runReceiptPath"]) + .ok_or_else(|| anyhow!("adopt receipt has no reviewed run receipt path"))?; + let canonical_plan_path = plan_path.canonicalize()?; + let canonical_run_path = run_path.canonicalize()?; + if file_sha256(plan_path)? != grant.plan_sha256 + || file_sha256(&run_path)? != grant.run_receipt_sha256 + || normalize_path_for_receipt(&canonical_plan_path) != grant.plan_receipt_path + || normalize_path_for_receipt(&canonical_run_path) != grant.run_receipt_path + { + return Err(anyhow!( + "reviewed plan or run receipt bytes changed after Mind adoption" + )); + } ensure_hands_review_allows(&intent, &review, "patch")?; ensure_hands_review_allows(&intent, &review, "command")?; ensure_hands_review_allows(&intent, &review, "commit")?; @@ -8770,6 +8464,11 @@ fn run_execute(args: ExecuteArgs) -> Result { } else { args.changed_paths }; + if strict_repo_work_paths(changed_paths.clone())? != grant.changed_paths { + return Err(anyhow!( + "execute changed paths differ from the typed Hands grant scope" + )); + } if changed_paths.is_empty() { return Err(anyhow!( "execute requires at least one changed path from --changed-path or --from-plan" @@ -8783,7 +8482,47 @@ fn run_execute(args: ExecuteArgs) -> Result { } else { return Err(anyhow!("missing --commit-message or --from-plan")); }; + let plan = plan_receipt + .as_ref() + .ok_or_else(|| anyhow!("execute requires reviewed plan"))?; + let item = string_from_json(&adopt_receipt, &["item"]) + .ok_or_else(|| anyhow!("adopt receipt has no item"))?; + let action = planned_action + .as_ref() + .ok_or_else(|| anyhow!("reviewed plan has no action"))?; + if string_from_json(plan, &["schemaVersion"]).as_deref() + != Some("epiphany.repo_work_action_plan_receipt.v0") + || string_from_json(plan, &["planId"]).as_deref() != Some(grant.plan_id.as_str()) + || item != grant.item + || mind.item != grant.item + || mind.plan_receipt_path != grant.plan_receipt_path + || mind.run_receipt_path != grant.run_receipt_path + || mind.action_id != grant.action_id + || string_from_value(action, "actionId").as_deref() != Some(grant.action_id.as_str()) + || command != grant.action_command + || commit_message != grant.action_commit_message + || intent.requested_paths != grant.changed_paths + || grant.queued_hands_review_id + != string_from_json(&adopt_receipt, &["handsActionGate", "queuedReviewId"]) + .unwrap_or_default() + || grant.substrate_grant_receipt_id != intent.substrate_gate_grant_receipt_id + { + return Err(anyhow!( + "execute inputs or authority bindings differ from the reviewed action" + )); + } validate_paths_within_gate(&intent, &changed_paths)?; + for operation in ["patch", "command", "commit"] { + validate_hands_action_authority( + &runtime_store, + &intent.intent_id, + &review.review_id, + &intent.runtime_job_id, + operation, + &changed_paths, + &grant.substrate_grant_receipt_id, + )?; + } let branch = git_output(&workspace, &["branch", "--show-current"])?; if !branch.starts_with("epiphany/") { return Err(anyhow!( @@ -8791,11 +8530,6 @@ fn run_execute(args: ExecuteArgs) -> Result { )); } - let item = adopt_receipt - .get("item") - .and_then(Value::as_str) - .unwrap_or("work-item") - .to_string(); let item_slug = sanitize(&item); let stdout_artifact = artifact_dir.join(format!("work-execute-{item_slug}.stdout.log")); let stderr_artifact = artifact_dir.join(format!("work-execute-{item_slug}.stderr.log")); @@ -8915,6 +8649,18 @@ fn run_execute(args: ExecuteArgs) -> Result { "commitReceiptId": commit_receipt.receipt_id, "commitSha": commit_receipt.commit_sha }, + "adoptionAuthority": { + "mindReviewId": mind.review_id, + "handsGrantId": grant.grant_id, + "planId": grant.plan_id, + "planSha256": grant.plan_sha256, + "runReceiptPath": grant.run_receipt_path, + "runReceiptSha256": grant.run_receipt_sha256, + "actionId": grant.action_id, + "handsIntentId": grant.hands_intent_id, + "approvedHandsReviewId": grant.approved_hands_review_id, + "substrateGrantReceiptId": grant.substrate_grant_receipt_id + }, "authority": { "branchLocalCommitCreated": true, "publicationAuthorized": false, @@ -10516,7 +10262,7 @@ fn run_deployment_config_audit(args: DeploymentConfigAuditArgs) -> Result "deployment-config-receipt-contract", parsed_config.as_ref().is_some_and(|config| { let receipts = &config.required_receipts; - receipts.mind_adoption == "epiphany.repo_work_mind_adoption_decision.v0" + receipts.mind_adoption == "epiphany.repo_work_plan_adoption_review.v0" && receipts.soul_review == "epiphany.repo_work_closure_review.v0" && receipts.maintainer_review == "gamecult.maintainer.review_receipt.v0" && receipts.secret_policy == "epiphany.repo_secret_policy_request.v0" @@ -12152,34 +11898,11 @@ fn run_tick(args: TickArgs) -> Result { next_safe_move = "Write an Imagination/Self action plan before adopting Hands authority." .to_string(); - } else if args.dry_run { - action = "adopt-from-plan".to_string(); - status = "would-advance".to_string(); - reason = "queued run packet and plan receipt exist".to_string(); - next_safe_move = - "Rerun without --dry-run to adopt the plan into branch-local Hands authority." - .to_string(); } else { - action = "adopt-from-plan".to_string(); - advanced_result = run_adopt(AdoptArgs { - workspace: workspace.clone(), - epiphany_root: epiphany_root.clone(), - item: Some(item.clone()), - run_receipt: Some(run_receipt_path.clone()), - plan_receipt: Some(plan_receipt_path.clone()), - runtime_store: args.runtime_store.clone(), - artifact_dir: Some(artifact_dir.clone()), - plan_summary: None, - adoption_evidence_refs: vec![format!("self.scheduler:repo-work-tick-{item_slug}")], - mind_adoption_rationale: Some(format!( - "Mind adopted the scheduler-presented plan for item {item} after Self found a queued run packet and matching Imagination plan receipt." - )), - })?; - status = "advanced".to_string(); - reason = "adopted queued Hands run packet from typed action plan".to_string(); - next_safe_move = - "Run another scheduler pulse to execute the approved branch-local plan." - .to_string(); + action = "await-mind-review".to_string(); + status = "awaiting-mind-review".to_string(); + reason = "Self found a queued run and plan, but no typed Mind adoption review has granted authority".to_string(); + next_safe_move = "Mind must persist an exact epiphany.repo_work_plan_adoption_review.v0 receipt; the scheduler cannot manufacture its verdict.".to_string(); } } else if plan_receipt_path.exists() { let plan_receipt = read_json(&plan_receipt_path)?; @@ -13157,6 +12880,24 @@ fn sorted_normalized_paths(paths: Vec) -> Vec { normalized } +fn strict_repo_work_paths(paths: Vec) -> Result> { + let original_count = paths.len(); + let normalized = sorted_normalized_paths(paths); + if normalized.len() != original_count + || normalized.iter().any(|path| { + Path::new(path).is_absolute() + || Path::new(path) + .components() + .any(|part| matches!(part, std::path::Component::ParentDir)) + }) + { + return Err(anyhow!( + "repo-work paths must be non-empty, unique, normalized relative paths without traversal" + )); + } + Ok(normalized) +} + fn powershell_single_quoted(value: &str) -> String { format!("'{}'", value.replace('\'', "''")) } @@ -13407,6 +13148,7 @@ mod authority_tests { use epiphany_core::REPO_WORK_MODELING_FINDING_SCHEMA_VERSION; use epiphany_core::RepoWorkModelingFinding; use epiphany_core::put_repo_work_modeling_finding; + use tempfile::tempdir; fn deployment_config_fixture() -> String { r#" @@ -13439,7 +13181,7 @@ aftercare_contract = "gamecult.idunn.deployment_aftercare_audit.v0" daemon_owns_execution = true [required_receipts] -mind_adoption = "epiphany.repo_work_mind_adoption_decision.v0" +mind_adoption = "epiphany.repo_work_plan_adoption_review.v0" soul_review = "epiphany.repo_work_closure_review.v0" maintainer_review = "gamecult.maintainer.review_receipt.v0" secret_policy = "epiphany.repo_secret_policy_request.v0" @@ -13647,7 +13389,7 @@ bifrost_publication_review_required = true [required_receipts] source_grounding = "epiphany.eyes.evidence_packet" soul_review = "epiphany.repo_work_closure_review.v0" -mind_adoption = "epiphany.repo_work_mind_adoption_decision.v0" +mind_adoption = "epiphany.repo_work_plan_adoption_review.v0" maintainer_review = "gamecult.maintainer.review_receipt.v0" bifrost_publication_review = "gamecult.bifrost.publication_review_receipt.v0" @@ -13732,7 +13474,7 @@ bifrost_publication_review_required = true [required_receipts] source_grounding = "epiphany.eyes.evidence_packet" soul_review = "epiphany.repo_work_closure_review.v0" -mind_adoption = "epiphany.repo_work_mind_adoption_decision.v0" +mind_adoption = "epiphany.repo_work_plan_adoption_review.v0" maintainer_review = "gamecult.maintainer.review_receipt.v0" bifrost_publication_review = "gamecult.bifrost.publication_review_receipt.v0" dependency_audit = "gamecult.supply_chain.dependency_audit_receipt.v0" @@ -13817,7 +13559,7 @@ secret_policy_review_required = true bifrost_publication_review_required = true [required_receipts] source_grounding = "epiphany.eyes.evidence_packet" -mind_adoption = "epiphany.repo_work_mind_adoption_decision.v0" +mind_adoption = "epiphany.repo_work_plan_adoption_review.v0" soul_review = "epiphany.repo_work_closure_review.v0" maintainer_review = "gamecult.maintainer.review_receipt.v0" secret_policy = "epiphany.repo_secret_policy_request.v0" @@ -13964,6 +13706,140 @@ idunn_deployment_authority_required = true assert!(!source.contains(&["repo-work-map", ".msgpack"].concat())); } + #[test] + fn repo_work_routing_cannot_author_mind_adoption_or_approve_hands_directly() { + let source = include_str!("epiphany-work.rs"); + let adopt_start = source.find("fn run_adopt").expect("run_adopt"); + let adopt_end = source[adopt_start..] + .find("\nfn run_execute") + .map(|offset| adopt_start + offset) + .expect("run_execute follows run_adopt"); + let adopt = &source[adopt_start..adopt_end]; + assert!(adopt.contains("runtime_repo_work_plan_adoption_review")); + assert!(adopt.contains("commit_repo_work_hands_grant")); + assert!(!adopt.contains("put_repo_work_plan_adoption_review")); + assert!(!adopt.contains("put_hands_action_review")); + assert!(!adopt.contains("RepoWorkPlanAdoptionReview {")); + assert!(!adopt.contains("mind_interpretation")); + assert!(!adopt.contains("\"owner\": \"Mind\"")); + + let tick_start = source.find("fn run_tick").expect("run_tick"); + let tick_end = source[tick_start..] + .find("\nfn resolve_accept_receipt") + .map(|offset| tick_start + offset) + .expect("receipt helpers follow run_tick"); + let tick = &source[tick_start..tick_end]; + assert!(!tick.contains("run_adopt(")); + assert!(!tick.contains("put_repo_work_plan_adoption_review")); + assert!(!tick.contains("RepoWorkPlanAdoptionReview {")); + } + + #[test] + fn closure_rejects_swapped_valid_adoption_chain() -> Result<()> { + let temp = tempdir()?; + let workspace = temp.path().canonicalize()?; + let plan_path = workspace.join("plan.json"); + write_json(&plan_path, &json!({"plan": true}))?; + let mind = epiphany_core::RepoWorkPlanAdoptionReview { + schema_version: epiphany_core::REPO_WORK_PLAN_ADOPTION_REVIEW_SCHEMA_VERSION.into(), + review_id: "mind-review-a".into(), + decision: RepoWorkPlanAdoptionDecision::Adopt, + workspace_identity: normalize_path_for_receipt(&workspace), + item: "item-a".into(), + plan_schema_version: "epiphany.repo_work_action_plan_receipt.v0".into(), + plan_id: "plan-a".into(), + plan_sha256: "a".repeat(64), + run_receipt_sha256: "b".repeat(64), + plan_receipt_path: normalize_path_for_receipt(&plan_path.canonicalize()?), + run_receipt_path: normalize_path_for_receipt(&workspace.join("run.json")), + hands_intent_id: "intent-a".into(), + queued_hands_review_id: "queued-a".into(), + substrate_grant_receipt_id: "substrate-a".into(), + action_id: "action-a".into(), + action_command: "cargo test --lib".into(), + action_commit_message: "Test adoption".into(), + changed_paths: vec!["src/lib.rs".into()], + reviewed_at: "2026-07-13T00:00:00Z".into(), + private_state_exposed: false, + }; + let grant = RepoWorkHandsGrant { + schema_version: REPO_WORK_HANDS_GRANT_SCHEMA_VERSION.into(), + grant_id: "grant-a".into(), + adoption_review_id: mind.review_id.clone(), + adoption_review_sha256: "c".repeat(64), + workspace_identity: mind.workspace_identity.clone(), + item: mind.item.clone(), + plan_id: mind.plan_id.clone(), + plan_sha256: mind.plan_sha256.clone(), + run_receipt_sha256: mind.run_receipt_sha256.clone(), + plan_receipt_path: mind.plan_receipt_path.clone(), + run_receipt_path: mind.run_receipt_path.clone(), + hands_intent_id: mind.hands_intent_id.clone(), + queued_hands_review_id: mind.queued_hands_review_id.clone(), + approved_hands_review_id: "approved-a".into(), + substrate_grant_receipt_id: mind.substrate_grant_receipt_id.clone(), + action_id: mind.action_id.clone(), + action_command: mind.action_command.clone(), + action_commit_message: mind.action_commit_message.clone(), + allowed_operations: vec!["patch".into(), "command".into(), "commit".into()], + changed_paths: mind.changed_paths.clone(), + granted_at: "2026-07-13T00:00:01Z".into(), + private_state_exposed: false, + }; + let execute = json!({ + "workspace": workspace, + "item": grant.item, + "planReceiptPath": plan_path, + "changedPaths": grant.changed_paths, + "command": grant.action_command, + "adoptionAuthority": { + "mindReviewId": mind.review_id, + "handsGrantId": grant.grant_id, + "planId": grant.plan_id, + "planSha256": grant.plan_sha256, + "runReceiptPath": grant.run_receipt_path, + "runReceiptSha256": grant.run_receipt_sha256, + "actionId": grant.action_id, + "handsIntentId": grant.hands_intent_id, + "approvedHandsReviewId": grant.approved_hands_review_id, + "substrateGrantReceiptId": grant.substrate_grant_receipt_id + } + }); + assert!(execute_receipt_binds_adoption_chain( + &execute, &mind, &grant + )); + let mut unrelated_mind = mind.clone(); + unrelated_mind.review_id = "mind-review-b".into(); + let mut unrelated_grant = grant.clone(); + unrelated_grant.grant_id = "grant-b".into(); + unrelated_grant.adoption_review_id = unrelated_mind.review_id.clone(); + assert!(!execute_receipt_binds_adoption_chain( + &execute, + &unrelated_mind, + &unrelated_grant + )); + Ok(()) + } + + #[test] + fn adopt_cli_rejects_removed_caller_authored_mind_rationale() { + let error = parse_adopt_args( + [ + "--workspace".to_string(), + ".".to_string(), + "--from-plan".to_string(), + "plan.json".to_string(), + "--mind-review-id".to_string(), + "mind-review-1".to_string(), + "--mind-adoption-rationale".to_string(), + "I, the caller, decree adoption.".to_string(), + ] + .into_iter(), + ) + .expect_err("caller-authored Mind rationale must remain deleted"); + assert!(error.to_string().contains("unexpected adopt argument")); + } + #[test] fn typed_closure_families_have_no_substring_authority() { let source = include_str!("epiphany-work.rs"); @@ -14648,105 +14524,6 @@ direct_hands_authority = true } } -fn adopted_action_item_from_plan(plan: &Value) -> Value { - let action_item = plan - .get("derivation") - .and_then(|value| value.get("actionItemReceipt")); - let plan_action = plan - .get("actions") - .and_then(Value::as_array) - .and_then(|actions| actions.first()); - json!({ - "source": "plan.derivation.actionItemReceipt", - "planId": plan.get("planId").cloned().unwrap_or(Value::Null), - "receiptId": action_item - .and_then(|value| value.get("receiptId")) - .cloned() - .unwrap_or(Value::Null), - "receiptPath": action_item - .and_then(|value| value.get("receiptPath")) - .cloned() - .unwrap_or(Value::Null), - "schemaVersion": action_item - .and_then(|value| value.get("schemaVersion")) - .cloned() - .unwrap_or(Value::Null), - "status": action_item - .and_then(|value| value.get("status")) - .cloned() - .unwrap_or(Value::Null), - "modelAuthored": action_item - .and_then(|value| value.get("modelAuthored")) - .cloned() - .unwrap_or(json!(false)), - "safeActionFamily": action_item - .and_then(|value| value.get("safeActionFamily")) - .cloned() - .unwrap_or(Value::Null), - "requestedPaths": action_item - .and_then(|value| value.get("requestedPaths")) - .or_else(|| plan_action.and_then(|value| value.get("changedPaths"))) - .cloned() - .unwrap_or(json!([])), - "verificationAsks": action_item - .and_then(|value| value.get("verificationAsks")) - .or_else(|| plan_action.and_then(|value| value.get("verificationAsks"))) - .cloned() - .unwrap_or(json!([])), - "summary": plan.get("planSummary").cloned().unwrap_or(Value::Null), - "planningFacets": action_item - .and_then(|value| value.get("planningFacets")) - .cloned() - .unwrap_or(Value::Null), - "adoptionEvidenceRefs": plan - .get("adoptionEvidenceRefs") - .cloned() - .unwrap_or(json!([])), - "handsCommandAuthority": false, - "durableStateAuthority": false, - "publicationAuthorized": false, - "mergeAuthorized": false, - "serviceLifecycleAuthority": false, - "crossRepoMutation": false, - "privateStateExposed": false - }) -} - -fn repo_work_safe_family_is_recognized(safe_family: &str) -> bool { - matches!( - safe_family, - "repo.append_worklog" - | "repo.markdown_planning_note" - | "repo.checklist_note" - | "repo.markdown_managed_section" - | "repo.status_section" - | "repo.task_card" - | "repo.tool_request" - | "repo.collaboration_policy" - | "repo.collaboration_topic" - | "repo.consensus_brief" - | "repo.interpreter_brief" - | "repo.objective_draft" - | "repo.adoption_request" - | "repo.scheduling_request" - | "repo.work_order" - | "repo.verification_request" - | "repo.publication_request" - | "repo.sync_request" - | "repo.maintainer_review_request" - | "repo.pr_request" - | "repo.credit_request" - | "repo.artifact_acceptance_request" - | "repo.metrics_request" - | "repo.readiness_review_request" - | "repo.doctrine_update_request" - | "repo.secret_policy_request" - | "repo.dependency_policy_request" - | "repo.deployment_config" - | "repo.deployment_request" - ) -} - fn compact_text(text: &str, limit: usize) -> String { let compact = text.split_whitespace().collect::>().join(" "); if compact.chars().count() <= limit { @@ -14866,7 +14643,7 @@ fn print_usage() { derive-plan --workspace [--item ] [--accept-receipt ] [--action-family append-worklog|planning-note|checklist-note|section-note|repo-status-section|task-card|repo-tool-request|repo-collaboration-policy|repo-collaboration-topic|repo-consensus-brief|repo-interpreter-brief|repo-objective-draft|repo-adoption-request|repo-scheduling-request|repo-work-order|repo-verification-request|repo-publication-request|repo-sync-request|repo-maintainer-review-request|repo-pr-request|repo-credit-request|repo-artifact-acceptance-request|repo-metrics-request|repo-readiness-review-request|repo-doctrine-update-request|repo-secret-policy-request|repo-dependency-policy-request|repo-deployment-config|repo-deployment-request] [--target-path ] [--model-ref ] [--model-authored] [--action-summary ] [--verification-ask ] [--stop-condition ] [--escalation-reason ] [--assumption ] [--constraint ] [--non-goal ] [--open-question ] [--decision-point ] [--evidence-need ]\n\ plan --workspace [--item ] --objective --plan-summary --command --changed-path --commit-message [--adoption-evidence-ref ]\n\ run --workspace [--item ] [--accept-receipt ] [--runtime-store ] [--requested-path ]\n\ - adopt --workspace [--item ] [--run-receipt ] [--from-plan ] [--plan-summary ] [--adoption-evidence-ref ] [--mind-adoption-rationale ]\n\ + adopt --workspace [--item ] [--run-receipt ] --from-plan --mind-review-id \n\ execute --workspace [--item ] [--from-plan ] [--command ] [--changed-path ] [--commit-message ]\n\ close --workspace [--item ] [--execute-receipt ]\n\ overview --workspace [--item ] [--accept-receipt ] [--no-write]\n\ diff --git a/epiphany-core/src/bin/epiphany_work/closure_contracts/operations.rs b/epiphany-core/src/bin/epiphany_work/closure_contracts/operations.rs index 72eb3a10..94f84fdf 100644 --- a/epiphany-core/src/bin/epiphany_work/closure_contracts/operations.rs +++ b/epiphany-core/src/bin/epiphany_work/closure_contracts/operations.rs @@ -45,7 +45,7 @@ impl RepoDeploymentConfig { pub(super) fn has_required_receipt_contract(&self) -> bool { let receipts = &self.required_receipts; - receipts.mind_adoption == "epiphany.repo_work_mind_adoption_decision.v0" + receipts.mind_adoption == "epiphany.repo_work_plan_adoption_review.v0" && receipts.soul_review == "epiphany.repo_work_closure_review.v0" && receipts.maintainer_review == "gamecult.maintainer.review_receipt.v0" && receipts.secret_policy == "epiphany.repo_secret_policy_request.v0" @@ -226,7 +226,7 @@ impl RepoSecretPolicyRequest { let receipts = &self.required_receipts; receipts.source_grounding == "epiphany.eyes.evidence_packet" && receipts.soul_review == "epiphany.repo_work_closure_review.v0" - && receipts.mind_adoption == "epiphany.repo_work_mind_adoption_decision.v0" + && receipts.mind_adoption == "epiphany.repo_work_plan_adoption_review.v0" && receipts.maintainer_review == "gamecult.maintainer.review_receipt.v0" && receipts.bifrost_publication_review == "gamecult.bifrost.publication_review_receipt.v0" @@ -382,7 +382,7 @@ impl RepoDependencyPolicyRequest { let value = &self.required_receipts; value.source_grounding == "epiphany.eyes.evidence_packet" && value.soul_review == "epiphany.repo_work_closure_review.v0" - && value.mind_adoption == "epiphany.repo_work_mind_adoption_decision.v0" + && value.mind_adoption == "epiphany.repo_work_plan_adoption_review.v0" && value.maintainer_review == "gamecult.maintainer.review_receipt.v0" && value.bifrost_publication_review == "gamecult.bifrost.publication_review_receipt.v0" && value.dependency_audit == "gamecult.supply_chain.dependency_audit_receipt.v0" @@ -536,7 +536,7 @@ impl RepoDeploymentRequest { pub(super) fn has_receipt_contract(&self) -> bool { let value = &self.required_receipts; value.source_grounding == "epiphany.eyes.evidence_packet" - && value.mind_adoption == "epiphany.repo_work_mind_adoption_decision.v0" + && value.mind_adoption == "epiphany.repo_work_plan_adoption_review.v0" && value.soul_review == "epiphany.repo_work_closure_review.v0" && value.maintainer_review == "gamecult.maintainer.review_receipt.v0" && value.secret_policy == "epiphany.repo_secret_policy_request.v0" @@ -626,7 +626,7 @@ impl RepoDoctrineUpdateRequest { ), ( "mind_adoption", - "epiphany.repo_work_mind_adoption_decision.v0", + "epiphany.repo_work_plan_adoption_review.v0", ), ("soul_review", "epiphany.repo_work_closure_review.v0"), ("maintainer_review", "gamecult.maintainer.review_receipt.v0"), diff --git a/epiphany-core/src/lib.rs b/epiphany-core/src/lib.rs index a926babd..72f40e50 100644 --- a/epiphany-core/src/lib.rs +++ b/epiphany-core/src/lib.rs @@ -654,6 +654,13 @@ pub use mind_gateway::MindGatewayDecision; pub use mind_gateway::MindGatewayReview; pub use mind_gateway::MindInterpreterPromptInput; pub use mind_gateway::MindStateCommitReceipt; +pub use mind_gateway::REPO_WORK_HANDS_GRANT_SCHEMA_VERSION; +pub use mind_gateway::REPO_WORK_HANDS_GRANT_TYPE; +pub use mind_gateway::REPO_WORK_PLAN_ADOPTION_REVIEW_SCHEMA_VERSION; +pub use mind_gateway::REPO_WORK_PLAN_ADOPTION_REVIEW_TYPE; +pub use mind_gateway::RepoWorkHandsGrant; +pub use mind_gateway::RepoWorkPlanAdoptionDecision; +pub use mind_gateway::RepoWorkPlanAdoptionReview; pub use mind_gateway::build_mind_interpreter_prompt; pub use mind_gateway::default_mind_cultnet_contracts; pub use mind_gateway::mind_review_allows_state; @@ -787,6 +794,7 @@ pub use runtime_spine::advance_repo_work_modeling_route; pub use runtime_spine::append_runtime_event; pub use runtime_spine::clear_epiphany_job_binding_backend; pub use runtime_spine::commit_initial_repo_work_modeling_route; +pub use runtime_spine::commit_repo_work_hands_grant; pub use runtime_spine::commit_repo_work_map_admission; pub use runtime_spine::complete_runtime_job; pub use runtime_spine::create_runtime_job; @@ -810,6 +818,7 @@ pub use runtime_spine::put_mind_gateway_review; pub use runtime_spine::put_mind_state_commit_receipt; pub use runtime_spine::put_repo_work_modeling_finding; pub use runtime_spine::put_repo_work_modeling_request; +pub use runtime_spine::put_repo_work_plan_adoption_review; pub use runtime_spine::put_runtime_reorient_worker_result; pub use runtime_spine::put_runtime_role_worker_result; pub use runtime_spine::put_soul_verdict_receipt; @@ -831,10 +840,12 @@ pub use runtime_spine::runtime_mind_gateway_review; pub use runtime_spine::runtime_mind_state_commit_receipt; pub use runtime_spine::runtime_registered_document_types; pub use runtime_spine::runtime_reorient_worker_result; +pub use runtime_spine::runtime_repo_work_hands_grant; pub use runtime_spine::runtime_repo_work_map_entry; pub use runtime_spine::runtime_repo_work_modeling_finding; pub use runtime_spine::runtime_repo_work_modeling_request; pub use runtime_spine::runtime_repo_work_modeling_route; +pub use runtime_spine::runtime_repo_work_plan_adoption_review; pub use runtime_spine::runtime_role_worker_result; pub use runtime_spine::runtime_schema_catalog_response; pub use runtime_spine::runtime_soul_verdict_receipt; @@ -843,6 +854,7 @@ pub use runtime_spine::runtime_spine_status; pub use runtime_spine::runtime_substrate_gate_repo_access_grant_receipt; pub use runtime_spine::runtime_tool_invocation_statuses; pub use runtime_spine::runtime_worker_launch_request; +pub use runtime_spine::validate_hands_action_authority; pub use runtime_spine::write_runtime_hello_frame; pub use runtime_spine::write_runtime_schema_catalog_json; pub use soul_gateway::*; diff --git a/epiphany-core/src/mind_gateway.rs b/epiphany-core/src/mind_gateway.rs index 77c3b85c..711bfc81 100644 --- a/epiphany-core/src/mind_gateway.rs +++ b/epiphany-core/src/mind_gateway.rs @@ -22,6 +22,118 @@ pub const MIND_STATE_REJECTION_RECEIPT_SCHEMA_VERSION: &str = "epiphany.mind.state_rejection_receipt.v0"; pub const MIND_VERSE_ADOPTION_RECEIPT_SCHEMA_VERSION: &str = "epiphany.mind.verse_adoption_receipt.v0"; +pub const REPO_WORK_PLAN_ADOPTION_REVIEW_TYPE: &str = "epiphany.repo_work.plan_adoption_review"; +pub const REPO_WORK_PLAN_ADOPTION_REVIEW_SCHEMA_VERSION: &str = + "epiphany.repo_work_plan_adoption_review.v0"; +pub const REPO_WORK_HANDS_GRANT_TYPE: &str = "epiphany.repo_work.hands_grant"; +pub const REPO_WORK_HANDS_GRANT_SCHEMA_VERSION: &str = "epiphany.repo_work_hands_grant.v0"; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum RepoWorkPlanAdoptionDecision { + Adopt, + Refuse, + Hold, +} + +/// Mind-authored review of one immutable repo-work plan and its complete authority chain. +#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.repo_work.plan_adoption_review", + schema = "RepoWorkPlanAdoptionReview" +)] +pub struct RepoWorkPlanAdoptionReview { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub review_id: String, + #[cultcache(key = 2)] + pub decision: RepoWorkPlanAdoptionDecision, + #[cultcache(key = 3)] + pub workspace_identity: String, + #[cultcache(key = 4)] + pub item: String, + #[cultcache(key = 5)] + pub plan_schema_version: String, + #[cultcache(key = 6)] + pub plan_id: String, + #[cultcache(key = 7)] + pub plan_sha256: String, + #[cultcache(key = 8)] + pub run_receipt_sha256: String, + #[cultcache(key = 9)] + pub plan_receipt_path: String, + #[cultcache(key = 10)] + pub run_receipt_path: String, + #[cultcache(key = 11)] + pub hands_intent_id: String, + #[cultcache(key = 12)] + pub queued_hands_review_id: String, + #[cultcache(key = 13)] + pub substrate_grant_receipt_id: String, + #[cultcache(key = 14)] + pub action_id: String, + #[cultcache(key = 15)] + pub action_command: String, + #[cultcache(key = 16)] + pub action_commit_message: String, + #[cultcache(key = 17)] + pub changed_paths: Vec, + #[cultcache(key = 18)] + pub reviewed_at: String, + #[cultcache(key = 19)] + pub private_state_exposed: bool, +} + +/// The only capability that converts an accepted Mind review into Hands authority. +#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] +#[cultcache(type = "epiphany.repo_work.hands_grant", schema = "RepoWorkHandsGrant")] +pub struct RepoWorkHandsGrant { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub grant_id: String, + #[cultcache(key = 2)] + pub adoption_review_id: String, + #[cultcache(key = 3)] + pub adoption_review_sha256: String, + #[cultcache(key = 4)] + pub workspace_identity: String, + #[cultcache(key = 5)] + pub item: String, + #[cultcache(key = 6)] + pub plan_id: String, + #[cultcache(key = 7)] + pub plan_sha256: String, + #[cultcache(key = 8)] + pub run_receipt_sha256: String, + #[cultcache(key = 9)] + pub plan_receipt_path: String, + #[cultcache(key = 10)] + pub run_receipt_path: String, + #[cultcache(key = 11)] + pub hands_intent_id: String, + #[cultcache(key = 12)] + pub queued_hands_review_id: String, + #[cultcache(key = 13)] + pub approved_hands_review_id: String, + #[cultcache(key = 14)] + pub substrate_grant_receipt_id: String, + #[cultcache(key = 15)] + pub action_id: String, + #[cultcache(key = 16)] + pub action_command: String, + #[cultcache(key = 17)] + pub action_commit_message: String, + #[cultcache(key = 18)] + pub allowed_operations: Vec, + #[cultcache(key = 19)] + pub changed_paths: Vec, + #[cultcache(key = 20)] + pub granted_at: String, + #[cultcache(key = 21)] + pub private_state_exposed: bool, +} #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/epiphany-core/src/runtime_spine.rs b/epiphany-core/src/runtime_spine.rs index 6bdb5087..1f325dc6 100644 --- a/epiphany-core/src/runtime_spine.rs +++ b/epiphany-core/src/runtime_spine.rs @@ -39,6 +39,9 @@ use crate::mind_gateway::MIND_VERSE_ADOPTION_RECEIPT_TYPE; use crate::mind_gateway::MindGatewayDecision; use crate::mind_gateway::MindGatewayReview; use crate::mind_gateway::MindStateCommitReceipt; +use crate::mind_gateway::RepoWorkHandsGrant; +use crate::mind_gateway::RepoWorkPlanAdoptionDecision; +use crate::mind_gateway::RepoWorkPlanAdoptionReview; use crate::modeling_gateway::REPO_WORK_MAP_ENTRY_SCHEMA_VERSION; use crate::modeling_gateway::REPO_WORK_MAP_ENTRY_TYPE; use crate::modeling_gateway::REPO_WORK_MODELING_FINDING_SCHEMA_VERSION; @@ -106,6 +109,8 @@ use epiphany_tool_adapter::EpiphanyToolInvocationIntent; use epiphany_tool_adapter::EpiphanyToolInvocationReceipt; use serde::Deserialize; use serde::Serialize; +use sha2::Digest; +use sha2::Sha256; use std::collections::BTreeMap; use std::collections::BTreeSet; use std::fs; @@ -734,6 +739,8 @@ pub fn runtime_spine_cache(store_path: impl AsRef) -> Result { cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; + cache.register_entry_type::()?; + cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; @@ -1508,6 +1515,204 @@ pub fn runtime_hands_action_review( cache.get::(review_id) } +pub fn put_repo_work_plan_adoption_review( + store_path: impl AsRef, + review: &RepoWorkPlanAdoptionReview, +) -> Result<()> { + validate_repo_work_plan_adoption_review(review)?; + let mut cache = runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + require_identity(&cache)?; + if let Some(existing) = cache.get::(&review.review_id)? { + if existing == *review { + return Ok(()); + } + return Err(anyhow!( + "repo-work plan adoption review ids are immutable; {} already names different bytes", + review.review_id + )); + } + cache.put(&review.review_id, review)?; + Ok(()) +} + +pub fn runtime_repo_work_plan_adoption_review( + store_path: impl AsRef, + review_id: &str, +) -> Result> { + validate_non_empty(review_id, "repo-work plan adoption review id")?; + let mut cache = runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + cache.get::(review_id) +} + +pub fn runtime_repo_work_hands_grant( + store_path: impl AsRef, + grant_id: &str, +) -> Result> { + validate_non_empty(grant_id, "repo-work Hands grant id")?; + let mut cache = runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + cache.get::(grant_id) +} + +/// Atomically publishes the capability and its approved generic Hands review. +/// Neither document is observable unless both pass validation and the backing-store swap succeeds. +pub fn commit_repo_work_hands_grant( + store_path: impl AsRef, + grant: &RepoWorkHandsGrant, + approved_review: &HandsActionReview, +) -> Result<()> { + let mut cache = runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + require_identity(&cache)?; + let adoption = cache + .get::(&grant.adoption_review_id)? + .ok_or_else(|| { + anyhow!("repo-work Hands grant requires its persisted Mind adoption review") + })?; + validate_repo_work_plan_adoption_review(&adoption)?; + let persisted_adoption_sha256 = format!("{:x}", Sha256::digest(serde_json::to_vec(&adoption)?)); + if adoption.decision != RepoWorkPlanAdoptionDecision::Adopt + || grant.schema_version != crate::mind_gateway::REPO_WORK_HANDS_GRANT_SCHEMA_VERSION + || grant.private_state_exposed + || !valid_lower_sha256(&grant.adoption_review_sha256) + || grant.adoption_review_sha256 != persisted_adoption_sha256 + || !valid_lower_sha256(&grant.plan_sha256) + || !valid_lower_sha256(&grant.run_receipt_sha256) + || chrono::DateTime::parse_from_rfc3339(&grant.granted_at).is_err() + || !safe_sorted_unique_paths(&grant.changed_paths) + || grant.adoption_review_id != adoption.review_id + || grant.workspace_identity != adoption.workspace_identity + || grant.item != adoption.item + || grant.plan_id != adoption.plan_id + || grant.plan_sha256 != adoption.plan_sha256 + || grant.run_receipt_sha256 != adoption.run_receipt_sha256 + || grant.plan_receipt_path != adoption.plan_receipt_path + || grant.run_receipt_path != adoption.run_receipt_path + || grant.hands_intent_id != adoption.hands_intent_id + || grant.queued_hands_review_id != adoption.queued_hands_review_id + || grant.substrate_grant_receipt_id != adoption.substrate_grant_receipt_id + || grant.action_id != adoption.action_id + || grant.action_command != adoption.action_command + || grant.action_commit_message != adoption.action_commit_message + || grant.changed_paths != adoption.changed_paths + || approved_review.review_id != grant.approved_hands_review_id + || approved_review.intent_id != grant.hands_intent_id + || approved_review.decision != "approved" + || approved_review.allowed_operations != grant.allowed_operations + { + return Err(anyhow!( + "repo-work Hands grant does not exactly bind its accepted Mind review and approved Hands review" + )); + } + let intent = cache + .get::(&grant.hands_intent_id)? + .ok_or_else(|| anyhow!("repo-work Hands grant requires its persisted Hands intent"))?; + let queued = cache + .get::(&grant.queued_hands_review_id)? + .ok_or_else(|| anyhow!("repo-work Hands grant requires its queued Hands review"))?; + if intent.intent_id != queued.intent_id + || queued.decision != "queued-for-adoption" + || intent.substrate_gate_grant_receipt_id != grant.substrate_grant_receipt_id + || intent.requested_paths != grant.changed_paths + { + return Err(anyhow!( + "repo-work Hands grant authority chain is inconsistent" + )); + } + let existing_grant = cache.get::(&grant.grant_id)?; + let existing_review = cache.get::(&approved_review.review_id)?; + match (existing_grant, existing_review) { + (None, None) => {} + (Some(existing_grant), Some(existing_review)) + if existing_grant == *grant && existing_review == *approved_review => + { + return Ok(()); + } + _ => { + return Err(anyhow!( + "repo-work Hands grant and approved review ids are an immutable pair" + )); + } + } + let (grant_entry, _) = cache.prepare_entry(&grant.grant_id, grant)?; + let (review_entry, _) = cache.prepare_entry(&approved_review.review_id, approved_review)?; + cache.put_prepared_batch(vec![grant_entry, review_entry]) +} + +fn validate_repo_work_plan_adoption_review(review: &RepoWorkPlanAdoptionReview) -> Result<()> { + validate_non_empty(&review.review_id, "repo-work plan adoption review id")?; + validate_non_empty(&review.workspace_identity, "repo-work workspace identity")?; + validate_non_empty(&review.item, "repo-work item")?; + validate_non_empty(&review.plan_id, "repo-work plan id")?; + validate_non_empty(&review.plan_sha256, "repo-work plan SHA-256")?; + validate_non_empty(&review.run_receipt_sha256, "repo-work run receipt SHA-256")?; + validate_non_empty(&review.plan_receipt_path, "repo-work plan receipt path")?; + validate_non_empty(&review.run_receipt_path, "repo-work run receipt path")?; + validate_non_empty(&review.hands_intent_id, "repo-work Hands intent")?; + validate_non_empty( + &review.queued_hands_review_id, + "repo-work queued Hands review", + )?; + validate_non_empty( + &review.substrate_grant_receipt_id, + "repo-work Substrate grant", + )?; + validate_non_empty(&review.action_id, "repo-work action id")?; + validate_non_empty(&review.action_command, "repo-work action command")?; + validate_non_empty( + &review.action_commit_message, + "repo-work action commit message", + )?; + validate_non_empty(&review.reviewed_at, "repo-work adoption review timestamp")?; + let valid_sha256 = + |value: &str| value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit()); + let normalized_paths = review + .changed_paths + .iter() + .map(|path| path.replace('\\', "/")) + .collect::>(); + let mut sorted_paths = normalized_paths.clone(); + sorted_paths.sort(); + sorted_paths.dedup(); + if review.schema_version != crate::mind_gateway::REPO_WORK_PLAN_ADOPTION_REVIEW_SCHEMA_VERSION + || review.plan_schema_version != "epiphany.repo_work_action_plan_receipt.v0" + || !valid_sha256(&review.plan_sha256) + || !valid_sha256(&review.run_receipt_sha256) + || review.private_state_exposed + || review.changed_paths.is_empty() + || !valid_lower_sha256(&review.plan_sha256) + || !valid_lower_sha256(&review.run_receipt_sha256) + || !Path::new(&review.plan_receipt_path).is_absolute() + || !Path::new(&review.run_receipt_path).is_absolute() + || chrono::DateTime::parse_from_rfc3339(&review.reviewed_at).is_err() + || !safe_sorted_unique_paths(&review.changed_paths) + || review.changed_paths != sorted_paths + { + return Err(anyhow!("invalid repo-work plan adoption review contract")); + } + Ok(()) +} + +fn valid_lower_sha256(value: &str) -> bool { + value.len() == 64 + && value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) +} + +fn safe_sorted_unique_paths(paths: &[String]) -> bool { + paths.windows(2).all(|pair| pair[0] < pair[1]) + && paths.iter().all(|path| { + !path.is_empty() + && !Path::new(path).is_absolute() + && !Path::new(path) + .components() + .any(|part| matches!(part, std::path::Component::ParentDir)) + }) +} + fn validate_hands_consequence_grant( store_path: &Path, intent_id: &str, @@ -1561,6 +1766,30 @@ fn validate_hands_consequence_grant( Ok(()) } +/// Revalidates the persisted Hands/Substrate authority chain before an actuator +/// performs a consequence. Receipt writers call the same primitive again after +/// the consequence; this preflight prevents a stale or substituted grant from +/// authorizing the consequence in the first place. +pub fn validate_hands_action_authority( + store_path: impl AsRef, + intent_id: &str, + review_id: &str, + runtime_job_id: &str, + operation: &str, + changed_paths: &[String], + stated_grant_id: &str, +) -> Result<()> { + validate_hands_consequence_grant( + store_path.as_ref(), + intent_id, + review_id, + runtime_job_id, + operation, + changed_paths, + Some(stated_grant_id), + ) +} + pub fn put_hands_patch_receipt( store_path: impl AsRef, receipt: &HandsPatchReceipt, @@ -4052,6 +4281,243 @@ mod tests { use cultnet_rs::decode_cultnet_message_from_slice; use tempfile::tempdir; + struct RepoWorkAdoptionFixture { + store: PathBuf, + review: RepoWorkPlanAdoptionReview, + grant: RepoWorkHandsGrant, + approved: HandsActionReview, + } + + fn repo_work_adoption_fixture() -> Result<(tempfile::TempDir, RepoWorkAdoptionFixture)> { + let temp = tempdir()?; + let store = temp.path().join("runtime.msgpack"); + initialize_runtime_spine( + &store, + RuntimeSpineInitOptions { + runtime_id: "epiphany-adoption-test".to_string(), + display_name: "Epiphany Adoption Test".to_string(), + created_at: "2026-07-13T00:00:00Z".to_string(), + }, + )?; + let paths = vec!["src/lib.rs".to_string()]; + let substrate = crate::substrate_gate_coordinator_implementation_grant( + "substrate-adoption-1".to_string(), + "hands-job-adoption-1".to_string(), + paths.clone(), + "2026-07-13T00:00:01Z".to_string(), + ); + put_substrate_gate_repo_access_grant_receipt(&store, &substrate)?; + let intent = HandsActionIntent { + schema_version: crate::HANDS_ACTION_INTENT_SCHEMA_VERSION.to_string(), + intent_id: "hands-intent-adoption-1".to_string(), + runtime_job_id: "hands-job-adoption-1".to_string(), + binding_id: "implementation-worker".to_string(), + role: "epiphany-hands".to_string(), + authority_scope: "epiphany.role.implementation".to_string(), + requested_action: "runAcceptedWorkItem".to_string(), + requested_paths: paths.clone(), + substrate_gate_grant_receipt_id: substrate.receipt_id.clone(), + requested_at: "2026-07-13T00:00:02Z".to_string(), + contract: "Mind adoption test intent.".to_string(), + }; + put_hands_action_intent(&store, &intent)?; + let queued = crate::hands_action_review_for_intent( + "hands-review-queued-adoption-1".to_string(), + &intent, + "queued-for-adoption".to_string(), + vec![ + "patch".to_string(), + "command".to_string(), + "commit".to_string(), + ], + vec!["Mind decision required.".to_string()], + "2026-07-13T00:00:03Z".to_string(), + ); + put_hands_action_review(&store, &queued)?; + let review = RepoWorkPlanAdoptionReview { + schema_version: crate::mind_gateway::REPO_WORK_PLAN_ADOPTION_REVIEW_SCHEMA_VERSION + .to_string(), + review_id: "mind-adoption-1".to_string(), + decision: crate::mind_gateway::RepoWorkPlanAdoptionDecision::Adopt, + workspace_identity: "workspace-1".to_string(), + item: "item-1".to_string(), + plan_schema_version: "epiphany.repo_work_action_plan_receipt.v0".to_string(), + plan_id: "plan-1".to_string(), + plan_sha256: "a".repeat(64), + run_receipt_sha256: "b".repeat(64), + plan_receipt_path: temp.path().join("plan.json").display().to_string(), + run_receipt_path: temp.path().join("run.json").display().to_string(), + hands_intent_id: intent.intent_id.clone(), + queued_hands_review_id: queued.review_id.clone(), + substrate_grant_receipt_id: substrate.receipt_id.clone(), + action_id: "action-1".to_string(), + action_command: "cargo test --lib".to_string(), + action_commit_message: "Test immutable adoption".to_string(), + changed_paths: paths.clone(), + reviewed_at: "2026-07-13T00:00:04Z".to_string(), + private_state_exposed: false, + }; + put_repo_work_plan_adoption_review(&store, &review)?; + let approved = crate::hands_action_review_for_intent( + "hands-review-approved-adoption-1".to_string(), + &intent, + "approved".to_string(), + vec![ + "patch".to_string(), + "command".to_string(), + "commit".to_string(), + ], + vec!["Bound to immutable Mind review.".to_string()], + "2026-07-13T00:00:05Z".to_string(), + ); + let grant = RepoWorkHandsGrant { + schema_version: crate::mind_gateway::REPO_WORK_HANDS_GRANT_SCHEMA_VERSION.to_string(), + grant_id: "hands-grant-adoption-1".to_string(), + adoption_review_id: review.review_id.clone(), + adoption_review_sha256: format!("{:x}", Sha256::digest(serde_json::to_vec(&review)?)), + workspace_identity: review.workspace_identity.clone(), + item: review.item.clone(), + plan_id: review.plan_id.clone(), + plan_sha256: review.plan_sha256.clone(), + run_receipt_sha256: review.run_receipt_sha256.clone(), + plan_receipt_path: review.plan_receipt_path.clone(), + run_receipt_path: review.run_receipt_path.clone(), + hands_intent_id: review.hands_intent_id.clone(), + queued_hands_review_id: review.queued_hands_review_id.clone(), + approved_hands_review_id: approved.review_id.clone(), + substrate_grant_receipt_id: review.substrate_grant_receipt_id.clone(), + action_id: review.action_id.clone(), + action_command: review.action_command.clone(), + action_commit_message: review.action_commit_message.clone(), + allowed_operations: approved.allowed_operations.clone(), + changed_paths: review.changed_paths.clone(), + granted_at: "2026-07-13T00:00:05Z".to_string(), + private_state_exposed: false, + }; + Ok(( + temp, + RepoWorkAdoptionFixture { + store, + review, + grant, + approved, + }, + )) + } + + #[test] + fn repo_work_mind_review_is_create_once_and_idempotent() -> Result<()> { + let (_temp, fixture) = repo_work_adoption_fixture()?; + put_repo_work_plan_adoption_review(&fixture.store, &fixture.review)?; + let mut counterfeit = fixture.review.clone(); + counterfeit.action_command = "cargo test --all".to_string(); + assert!(put_repo_work_plan_adoption_review(&fixture.store, &counterfeit).is_err()); + assert_eq!( + runtime_repo_work_plan_adoption_review(&fixture.store, &fixture.review.review_id)?, + Some(fixture.review) + ); + Ok(()) + } + + #[test] + fn repo_work_refuse_and_hold_cannot_mint_hands_authority() -> Result<()> { + for decision in [ + crate::mind_gateway::RepoWorkPlanAdoptionDecision::Refuse, + crate::mind_gateway::RepoWorkPlanAdoptionDecision::Hold, + ] { + let (_temp, mut fixture) = repo_work_adoption_fixture()?; + fixture.review.review_id = format!("mind-{decision:?}").to_lowercase(); + fixture.review.decision = decision; + fixture.grant.adoption_review_id = fixture.review.review_id.clone(); + put_repo_work_plan_adoption_review(&fixture.store, &fixture.review)?; + assert!( + commit_repo_work_hands_grant(&fixture.store, &fixture.grant, &fixture.approved) + .is_err() + ); + assert!( + runtime_repo_work_hands_grant(&fixture.store, &fixture.grant.grant_id)?.is_none() + ); + assert!( + runtime_hands_action_review(&fixture.store, &fixture.approved.review_id)?.is_none() + ); + } + Ok(()) + } + + #[test] + fn repo_work_swapped_binding_fails_atomically() -> Result<()> { + let (_temp, mut fixture) = repo_work_adoption_fixture()?; + fixture.grant.plan_receipt_path = fixture.review.run_receipt_path.clone(); + assert!( + commit_repo_work_hands_grant(&fixture.store, &fixture.grant, &fixture.approved) + .is_err() + ); + assert!(runtime_repo_work_hands_grant(&fixture.store, &fixture.grant.grant_id)?.is_none()); + assert!( + runtime_hands_action_review(&fixture.store, &fixture.approved.review_id)?.is_none() + ); + Ok(()) + } + + #[test] + fn repo_work_counterfeit_review_digest_fails_atomically() -> Result<()> { + let (_temp, mut fixture) = repo_work_adoption_fixture()?; + fixture.grant.adoption_review_sha256 = "c".repeat(64); + assert!( + commit_repo_work_hands_grant(&fixture.store, &fixture.grant, &fixture.approved) + .is_err() + ); + assert!(runtime_repo_work_hands_grant(&fixture.store, &fixture.grant.grant_id)?.is_none()); + assert!( + runtime_hands_action_review(&fixture.store, &fixture.approved.review_id)?.is_none() + ); + Ok(()) + } + + #[test] + fn repo_work_exact_adoption_chain_commits_grant_and_review_together() -> Result<()> { + let (_temp, fixture) = repo_work_adoption_fixture()?; + commit_repo_work_hands_grant(&fixture.store, &fixture.grant, &fixture.approved)?; + assert_eq!( + runtime_repo_work_hands_grant(&fixture.store, &fixture.grant.grant_id)?, + Some(fixture.grant) + ); + assert_eq!( + runtime_hands_action_review(&fixture.store, &fixture.approved.review_id)?, + Some(fixture.approved) + ); + Ok(()) + } + + #[test] + fn repo_work_grant_and_approved_review_are_create_once_as_a_pair() -> Result<()> { + let (_temp, fixture) = repo_work_adoption_fixture()?; + commit_repo_work_hands_grant(&fixture.store, &fixture.grant, &fixture.approved)?; + commit_repo_work_hands_grant(&fixture.store, &fixture.grant, &fixture.approved)?; + + let mut counterfeit_grant = fixture.grant.clone(); + counterfeit_grant.action_command = "cargo test --all".to_string(); + assert!( + commit_repo_work_hands_grant(&fixture.store, &counterfeit_grant, &fixture.approved) + .is_err() + ); + let mut counterfeit_review = fixture.approved.clone(); + counterfeit_review.reasons = vec!["replacement opinion".to_string()]; + assert!( + commit_repo_work_hands_grant(&fixture.store, &fixture.grant, &counterfeit_review) + .is_err() + ); + assert_eq!( + runtime_repo_work_hands_grant(&fixture.store, &fixture.grant.grant_id)?, + Some(fixture.grant) + ); + assert_eq!( + runtime_hands_action_review(&fixture.store, &fixture.approved.review_id)?, + Some(fixture.approved) + ); + Ok(()) + } + #[test] fn runtime_spine_initializes_sessions_events_and_status() -> Result<()> { let temp = tempdir()?; diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 1760a13e..f34b55b8 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -63,10 +63,23 @@ validator; missing/changed node, edge, or link anchors stale their summaries. There is still no production semantic index for graph claims, so Modeling index availability is honestly unavailable rather than inferred from code search. -Next P0 cut: `epiphany-work run_adopt` currently synthesizes Mind ownership and -an approved Hands review from local JSON predicates. Route adoption through the -existing typed Mind gateway, immutably bound to the exact plan, and delete the -scheduler's ability to author Mind/Hands consequence. +The repo-work adoption authority rebuild is now implemented. `run_adopt` +consumes an immutable typed `RepoWorkPlanAdoptionReview` bound to exact +workspace, plan/run paths and SHA-256 bytes, action, command, commit message, +changed paths, queued Hands review, and Substrate grant. It atomically creates +an immutable `RepoWorkHandsGrant` plus approved Hands review; Refuse/Hold, +counterfeit digests, swapped paths, and same-ID replacement are rejected. +`run_tick` can only report `awaiting-mind-review`. `run_execute` rereads the +whole chain and revalidates persisted Substrate authority before PowerShell. +Soul closure now reloads typed runtime truth and cross-binds it to the execute +receipt; a swapped valid adoption chain fails. Six compiled smokes that taught +the deleted caller-writable `mindAdoptionDecision` model were removed. + +Next architectural cut: finish the Aetheria lesson rather than polishing this +gate. Modeling needs a production semantic index over typed graph claims and a +durable frontier projection that automatically targets later Eyes/Hands +prompts. Memory-graph freshness is substrate; the living migration map is the +organ. Epiphany is in an authority-provenance purification pass. The live question is not whether a command can produce a plausible document; it is whether the subsystem writing that document owns the fact it asserts. diff --git a/schemas/cultnet/epiphany.repo_work_hands_grant.schema.json b/schemas/cultnet/epiphany.repo_work_hands_grant.schema.json new file mode 100644 index 00000000..8e3963b3 --- /dev/null +++ b/schemas/cultnet/epiphany.repo_work_hands_grant.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://gamecult.dev/epiphany/cultnet/epiphany.repo_work_hands_grant.schema.json", + "title": "Repo Work Hands Grant", + "type": "object", "additionalProperties": false, + "required": ["schemaVersion","grantId","adoptionReviewId","adoptionReviewSha256","workspaceIdentity","item","planId","planSha256","runReceiptSha256","planReceiptPath","runReceiptPath","handsIntentId","queuedHandsReviewId","approvedHandsReviewId","substrateGrantReceiptId","actionId","actionCommand","actionCommitMessage","allowedOperations","changedPaths","grantedAt","privateStateExposed"], + "properties": { + "schemaVersion":{"const":"epiphany.repo_work_hands_grant.v0"}, "grantId":{"type":"string","minLength":1}, + "adoptionReviewId":{"type":"string","minLength":1}, "adoptionReviewSha256":{"type":"string","pattern":"^[0-9a-f]{64}$"}, + "workspaceIdentity":{"type":"string","minLength":1}, "item":{"type":"string","minLength":1}, "planId":{"type":"string","minLength":1}, + "planSha256":{"type":"string","pattern":"^[0-9a-f]{64}$"}, "runReceiptSha256":{"type":"string","pattern":"^[0-9a-f]{64}$"}, + "planReceiptPath":{"type":"string","minLength":1}, "runReceiptPath":{"type":"string","minLength":1}, + "handsIntentId":{"type":"string","minLength":1}, "queuedHandsReviewId":{"type":"string","minLength":1}, "approvedHandsReviewId":{"type":"string","minLength":1}, + "substrateGrantReceiptId":{"type":"string","minLength":1}, "actionId":{"type":"string","minLength":1}, + "actionCommand":{"type":"string","minLength":1}, "actionCommitMessage":{"type":"string","minLength":1}, + "allowedOperations":{"type":"array","minItems":1,"uniqueItems":true,"items":{"enum":["patch","command","commit"]}}, + "changedPaths":{"type":"array","minItems":1,"uniqueItems":true,"items":{"type":"string","minLength":1}}, + "grantedAt":{"type":"string","format":"date-time"}, "privateStateExposed":{"const":false} + } +} diff --git a/schemas/cultnet/epiphany.repo_work_plan_adoption_review.schema.json b/schemas/cultnet/epiphany.repo_work_plan_adoption_review.schema.json new file mode 100644 index 00000000..13db7630 --- /dev/null +++ b/schemas/cultnet/epiphany.repo_work_plan_adoption_review.schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://gamecult.dev/epiphany/cultnet/epiphany.repo_work_plan_adoption_review.schema.json", + "title": "Repo Work Plan Adoption Review", + "type": "object", + "additionalProperties": false, + "required": ["schemaVersion","reviewId","decision","workspaceIdentity","item","planSchemaVersion","planId","planSha256","runReceiptSha256","planReceiptPath","runReceiptPath","handsIntentId","queuedHandsReviewId","substrateGrantReceiptId","actionId","actionCommand","actionCommitMessage","changedPaths","reviewedAt","privateStateExposed"], + "properties": { + "schemaVersion": {"const":"epiphany.repo_work_plan_adoption_review.v0"}, + "reviewId":{"type":"string","minLength":1}, "decision":{"enum":["adopt","refuse","hold"]}, + "workspaceIdentity":{"type":"string","minLength":1}, "item":{"type":"string","minLength":1}, + "planSchemaVersion":{"type":"string","minLength":1}, "planId":{"type":"string","minLength":1}, + "planSha256":{"type":"string","pattern":"^[0-9a-f]{64}$"}, "runReceiptSha256":{"type":"string","pattern":"^[0-9a-f]{64}$"}, + "planReceiptPath":{"type":"string","minLength":1}, "runReceiptPath":{"type":"string","minLength":1}, + "handsIntentId":{"type":"string","minLength":1}, "queuedHandsReviewId":{"type":"string","minLength":1}, + "substrateGrantReceiptId":{"type":"string","minLength":1}, "actionId":{"type":"string","minLength":1}, + "actionCommand":{"type":"string","minLength":1}, "actionCommitMessage":{"type":"string","minLength":1}, + "changedPaths":{"type":"array","minItems":1,"uniqueItems":true,"items":{"type":"string","minLength":1}}, + "reviewedAt":{"type":"string","format":"date-time"}, "privateStateExposed":{"const":false} + } +} diff --git a/schemas/cultnet/index.json b/schemas/cultnet/index.json index 021a39a9..3a69edc2 100644 --- a/schemas/cultnet/index.json +++ b/schemas/cultnet/index.json @@ -1,6 +1,20 @@ { "schemaVersion": "epiphany.cultnet_catalog.v0", "schemas": [ + { + "schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.repo_work_plan_adoption_review.schema.json", + "kind": "document_payload", "wireContracts": ["cultnet.schema.v0"], + "schemaVersion": "epiphany.repo_work_plan_adoption_review.v0", + "documentType": "epiphany.repo_work.plan_adoption_review", + "title": "Repo Work Plan Adoption Review", "path": "epiphany.repo_work_plan_adoption_review.schema.json" + }, + { + "schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.repo_work_hands_grant.schema.json", + "kind": "document_payload", "wireContracts": ["cultnet.schema.v0"], + "schemaVersion": "epiphany.repo_work_hands_grant.v0", + "documentType": "epiphany.repo_work.hands_grant", + "title": "Repo Work Hands Grant", "path": "epiphany.repo_work_hands_grant.schema.json" + }, { "schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.runtime.identity.schema.json", "kind": "document_payload", diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index 1f1c957f..fc4989be 100644 Binary files a/state/ledgers.msgpack and b/state/ledgers.msgpack differ diff --git a/state/map.yaml b/state/map.yaml index 667931d9..a141319e 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -661,4 +661,4 @@ current_status: - Service lifecycle receipt history is now event-only: `load_epiphany_cultmesh_daemon_service_lifecycle_receipts` filters out the `epiphany-local/daemon-service-lifecycle-receipt/latest` mirror, while `load_latest_epiphany_cultmesh_daemon_service_lifecycle_receipt` remains the latest-state reader. Focused unit test `service_lifecycle_receipt_history_excludes_latest_mirror` proves two per-receipt writes return exactly two history records and latest points at the second. Fresh smoke `verse-query-lifecycle-history-filter-smoke` and live overview/triage `local-20260618-145632-497-efac6f0d` / `local-20260618-145632-321-867947af` prove compact swarm sights still expose service lifecycle attention plus the present elevated runbook artifact without daemon pokes or service mutation. Do not use a broad unscoped cargo test filter for this check on Windows; it fans out across bin harnesses and hit a broken pipe during this pass. - `epiphany-verse-query seed-compact` is the compact seed receipt for wrappers and read-only rites. The older `seed` command still prints the full local Verse context for explicit debugging, but compact wrapper modes now avoid full `seed`/final `query` readback after the combined overview proof exposed Windows debug stack overflow from full-context serialization pressure. last_completed_repo_swarm_cut: Huginn live-fire has crossed the first real repo Body from Persona pressure to branch-local Hands commit and Soul/Mind closure, and now has repeatable closure and redacted-public-proof mouths. On `E:\Projects\Huginn` branch `epiphany/huginn/first-awakening`, `tools/epiphany_local_run.ps1 -Mode repo-persona-intake` accepted item `huginn-inspection-readback-livefire`, Weksa/Persona intake remained sealed, and `epiphany-work derive-plan` lowered it to model-authored safe family `repo.status_section` for `README.md` only. `tools/epiphany_local_run.ps1 -Mode repo-swarm-run` advanced four bounded pulses: run-from-plan, adopt-from-plan, execute-from-plan, and close-from-execute. Huginn commit `e9b07c38fceabbdc89ea35c2156326c80c0393a2` updated only `README.md`; closure receipt `.epiphany\work\work-close-huginn-inspection-readback-livefire.json` records status `closed`, Soul verdict `passed`, Mind state commit `repo-work-close-huginn-inspection-readback-livefire-mind-commit`, safe family `repo.status_section`, path scope `README.md`, Bifrost publication gate, and `privateStateExposed=false`. Verification also ran Huginn's TypeScript compiler through concrete Node path: `C:\Program Files\nodejs\node.exe E:\Projects\Huginn\node_modules\typescript\bin\tsc -p tsconfig.json` passed. Native verifier `epiphany-repo-livefire-closure-smoke --workspace E:\Projects\Huginn --item huginn-inspection-readback-livefire --expected-path README.md --expected-family repo.status_section --expected-gate awaiting-publication --expected-blocker bifrost-publication-missing` checks closure receipt, Mind map entry, overview gate/blocker, authority seals, and `git show` changed paths, returning `status=ok`, `soulVerdict=passed`, `publicationGate=Bifrost`, and `privateStateExposed=false`. Wrapper `tools/epiphany_local_run.ps1 -Mode repo-livefire-closure -Workspace E:\Projects\Huginn -RepoWorkItem huginn-inspection-readback-livefire -SkipBuild` exposes the same read-only proof through the local operator mouth; artifact `local-20260621-190156-803-2f85ba15` printed commit, path, gate, blocker, Soul verdict, Mind commit receipt, and `privateStateExposed=False` without push, merge, publication, or target mutation. New wrapper `tools/epiphany_local_run.ps1 -Mode repo-work-public-proof -Workspace E:\Projects\Huginn -RepoWorkItem huginn-inspection-readback-livefire -SkipBuild` exports the redacted proof bundle and repo-local Verse index without publishing; artifact `local-20260621-190645-345-4555b1dc` wrote `.epiphany\public\proof-bundles\repo-work-public-proof-huginn-inspection-readback-livefire.json`, proof id `repo-work-public-proof-huginn-inspection-readback-livefire`, SHA-256 `857545b199f3e37eb06c2e757e5cfc201958e46667bb1f8403fd333a22620f63`, nine artifact rows, zero publication rows, and `privateStateExposed=False`. swarm overview readback `local-20260621-190701-140-b9519ec6` sees the `REPO-WORK-PUBLIC-PROOF` row and action row from Huginn's repo-local Verse with the same SHA-256 while Bifrost publication remains missing. Launcher compact modes are now named compact operator-context modes instead of read-only modes, and `bifrost-public-proof` summaries print item, target public Verse, room, proof SHA-256, review receipts, and credit receipts so publication receipts are auditable without claiming upstream sync. - next_action: Cut repo-work Mind impersonation. `epiphany-work run_adopt` currently manufactures a Mind-owned adoption document and approved Hands review from local JSON predicates. Self/scheduler may route a plan, but only a resolved typed Mind gateway review immutably bound to the exact plan may authorize the admission primitive that creates Hands review. Delete the synthetic owner/interpreter path and prove that a structurally plausible plan without Mind receipt creates neither adoption nor Hands authority. Preserve the Aetheria lesson: memory-graph v1 now binds Modeling anatomy to state-store identity, accepted revision, and SHA-256 source bytes; launch and explicit refresh share validation; missing/changed anchors stale summaries. Modeling semantic graph indexing remains honestly unavailable until a real graph-claim indexer exists. The intentionally absent Other-world action still requires a typed intent and real owner. Keep closure's zero-substring invariant. + next_action: Build the Aetheria-shaped Modeling organ, not another freshness badge. Repo-work Mind impersonation is cut: adoption consumes an immutable typed Mind review, atomically publishes an immutable Hands grant/review pair, execution revalidates plan/run/Substrate authority before consequence, closure cross-binds runtime truth to the execute receipt, and hostile substitution/overwrite tests pass. The next owner is a production semantic index over typed Modeling graph claims plus a durable migration frontier automatically projected into Eyes/Hands prompts. Memory-graph v1 already binds anatomy to state-store identity, accepted revision, and source SHA-256; use that as substrate, then make Modeling name current migration bodies, invalidated beliefs, dependency order, and next bounded cuts across rehydration. The intentionally absent Other-world action still requires a typed intent and real owner. Keep closure's zero-substring invariant.