diff --git a/org-ops-core/src/playbook_commands.rs b/org-ops-core/src/playbook_commands.rs index 69ad3e6..9bc2038 100644 --- a/org-ops-core/src/playbook_commands.rs +++ b/org-ops-core/src/playbook_commands.rs @@ -3,7 +3,8 @@ //! guildhouse-ops playbook run — validates corpus, runs ansible-playbook, Chronicle //! guildhouse-ops playbook list — lists governed playbooks -use crate::gsap_client::{self, AuthorizeRequest, GsapClient, CompletionReceiptPayload}; +use crate::chronicle_client::ChronicleClient; +use crate::gsap_client::{self, AuthorizeRequest, GsapClient}; use crate::session::SessionContext; use crate::traits::OrgCommands; use std::collections::HashMap; @@ -43,21 +44,8 @@ impl PlaybookCommands { if path.is_empty() { None } else { Some(path) } } - fn emit_chronicle(&self, kind: &str, actor_did: &str, message: &str) -> bool { - let body = serde_json::json!({ - "pusher": {"login": actor_did}, - "ref": format!("refs/playbook/{}", kind), - "repository": {"full_name": "platform/ansible-governance"}, - "commits": [{"message": format!("{}: {}", kind, message)}], - }); - reqwest::blocking::Client::new() - .post(&self.chronicle_webhook) - .header("X-Forgejo-Event", "push") - .json(&body) - .timeout(std::time::Duration::from_secs(5)) - .send() - .map(|r| r.status().is_success()) - .unwrap_or(false) + fn chronicle(&self) -> ChronicleClient { + ChronicleClient::from_legacy_webhook(&self.chronicle_webhook) } fn cmd_run( @@ -103,10 +91,18 @@ impl PlaybookCommands { let actor_did = format!("did:web:{}/user/operator", ctx.trust_domain); let pb_cid = entry.get("spec").and_then(|s| s.get("cid")).and_then(|v| v.as_str()).unwrap_or("?"); - self.emit_chronicle( - "PLAYBOOK_STARTED", + let chronicle = self.chronicle(); + chronicle.emit( + "GOV_PLAYBOOK_STARTED", &actor_did, - &format!("{} cid={} target={}", name, pb_cid, target.unwrap_or("all")), + &ChronicleClient::generate_id(), + serde_json::json!({ + "kind": "GOV_PLAYBOOK_STARTED", + "description": format!("{} cid={} target={}", name, pb_cid, target.unwrap_or("all")), + "playbook": name, + "corpus_entry_cid": pb_cid, + "target": target.unwrap_or("all"), + }), ); // Helper to build ansible-playbook command @@ -132,10 +128,17 @@ impl PlaybookCommands { Ok(policy) => policy, Err(e) => { eprintln!("\n BLOCKED: Accord load failed: {}", e); - self.emit_chronicle( - "ACCORD_LOAD_FAILED", + chronicle.emit( + "GOV_ACCORD_LOAD_FAILED", &actor_did, - &format!("{} accord={} error={}", name, accord_name, e), + &ChronicleClient::generate_id(), + serde_json::json!({ + "kind": "GOV_ACCORD_LOAD_FAILED", + "description": format!("{} accord={} error={}", name, accord_name, e), + "playbook": name, + "accord": accord_name, + "error": e.to_string(), + }), ); anyhow::bail!( "Accord '{}' could not be loaded. Governed operation blocked. {}", @@ -198,7 +201,17 @@ impl PlaybookCommands { if dry_run { print!("{}", diff); println!("\n [--check] Dry run complete."); - self.emit_chronicle("PLAYBOOK_COMPLETED", &actor_did, &format!("{} dry_run=true", name)); + chronicle.emit( + "GOV_PLAYBOOK_COMPLETED", + &actor_did, + &ChronicleClient::generate_id(), + serde_json::json!({ + "kind": "GOV_PLAYBOOK_COMPLETED", + "description": format!("{} dry_run=true", name), + "playbook": name, + "dry_run": true, + }), + ); return Ok(()); } @@ -215,8 +228,18 @@ impl PlaybookCommands { let recheck_hash = crate::apply_gate::hash_diff(&recheck_diff); if recheck_hash != diff_hash { eprintln!("\n BLOCKED: Diff changed since MFA sign-off!"); - self.emit_chronicle("DIFF_MISMATCH_DETECTED", &actor_did, - &format!("{} signed={} actual={}", name, &diff_hash[..24], &recheck_hash[..24])); + chronicle.emit( + "GOV_DIFF_MISMATCH_DETECTED", + &actor_did, + &ChronicleClient::generate_id(), + serde_json::json!({ + "kind": "GOV_DIFF_MISMATCH_DETECTED", + "description": format!("{} signed={} actual={}", name, &diff_hash[..24], &recheck_hash[..24]), + "playbook": name, + "signed_hash": &diff_hash[..24], + "actual_hash": &recheck_hash[..24], + }), + ); anyhow::bail!("Apply blocked: diff changed after MFA."); } } else if !diff.trim().is_empty() { @@ -230,11 +253,19 @@ impl PlaybookCommands { let duration = start.elapsed(); let rc = status.code().unwrap_or(-1); - // Chronicle: PLAYBOOK_COMPLETED - self.emit_chronicle( - "PLAYBOOK_COMPLETED", + // Chronicle: GOV_PLAYBOOK_COMPLETED + chronicle.emit( + "GOV_PLAYBOOK_COMPLETED", &actor_did, - &format!("{} rc={} duration={}s", name, rc, duration.as_secs()), + &ChronicleClient::generate_id(), + serde_json::json!({ + "kind": "GOV_PLAYBOOK_COMPLETED", + "description": format!("{} rc={} duration={}s", name, rc, duration.as_secs()), + "playbook": name, + "exit_code": rc, + "duration_secs": duration.as_secs(), + "success": status.success(), + }), ); // ── GSAP: Post Completion Receipt ───────────── @@ -257,7 +288,7 @@ impl PlaybookCommands { if status.success() { println!("\nPlaybook complete. ({:.1}s)", duration.as_secs_f64()); - println!("Chronicle: PLAYBOOK_COMPLETED recorded"); + println!("Chronicle: GOV_PLAYBOOK_COMPLETED recorded"); } else { anyhow::bail!("ansible-playbook exited with code {}", rc); }