feat: wire GSAP into playbook runner — full AC→shell→CR loop
The governed playbook runner now: 1. Requests an AC from the GSAP broker before execution 2. Validates corpus CID + parameters CID + single-use 3. Executes the ansible playbook (unchanged) 4. Posts a Completion Receipt to the broker after execution Environment variables: GSAP_BROKER_URL — Capstone broker endpoint GSAP_BEARER_TOKEN — JWT for broker auth GSAP_DRIVER_ID — identity driver (default: keycloak-guildhouse) GSAP_ACCORD_TEMPLATE — accord template (default: from GUILDHOUSE_ACCORD) GSAP_SESSION_DIR — local session state directory Self-authorized mode: If GSAP_BROKER_URL not set, execution proceeds without AC/CR. Valid for development (GSAP §1.3). Not for production. Error handling: ElevationRequired → shows activation instructions, aborts Denied → shows reason, aborts CorpusMismatch → shows CID diff, aborts CR delivery failure → stores locally, warns, does not abort 4/4 gsap_client unit tests passing. Build clean with zero errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
aa5853d168
commit
7107b2860a
1 changed files with 65 additions and 0 deletions
|
|
@ -3,9 +3,11 @@
|
|||
//! guildhouse-ops playbook run <name> — validates corpus, runs ansible-playbook, Chronicle
|
||||
//! guildhouse-ops playbook list — lists governed playbooks
|
||||
|
||||
use crate::gsap_client::{self, AuthorizeRequest, GsapClient, CompletionReceiptPayload};
|
||||
use crate::session::SessionContext;
|
||||
use crate::traits::OrgCommands;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
pub struct PlaybookCommands {
|
||||
|
|
@ -143,6 +145,51 @@ impl PlaybookCommands {
|
|||
}
|
||||
};
|
||||
|
||||
// ── GSAP: Request Authorization Context ──────────
|
||||
let gsap_ac = if let Ok(broker_url) = std::env::var("GSAP_BROKER_URL") {
|
||||
let token = std::env::var("GSAP_BEARER_TOKEN").unwrap_or_default();
|
||||
let driver_id = std::env::var("GSAP_DRIVER_ID")
|
||||
.unwrap_or_else(|_| "keycloak-guildhouse".into());
|
||||
let accord = std::env::var("GSAP_ACCORD_TEMPLATE")
|
||||
.unwrap_or_else(|_| accord_name.clone());
|
||||
let session_dir = std::env::var("GSAP_SESSION_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| std::env::temp_dir().join("bxnet-gsap"));
|
||||
|
||||
let client = GsapClient::new(broker_url, token, session_dir);
|
||||
let params_json = serde_json::to_string(&extra_vars).unwrap_or_default();
|
||||
let params_cid = gsap_client::compute_cid(params_json.as_bytes());
|
||||
|
||||
let request = AuthorizeRequest {
|
||||
playbook: name.to_string(),
|
||||
corpus_entry_cid: pb_cid.to_string(),
|
||||
parameters_cid: params_cid.clone(),
|
||||
accord_template: accord,
|
||||
driver_id,
|
||||
};
|
||||
|
||||
match client.authorize(&request, pb_cid, ¶ms_cid) {
|
||||
Ok(ac) => {
|
||||
println!("\x1b[32m✓ GSAP: Operation authorized\x1b[0m");
|
||||
println!(" AC: {}...", &ac.context_id[..8]);
|
||||
println!(" Principal: {}", ac.principal.did);
|
||||
println!(" Accord: {}", ac.accord.template);
|
||||
Some((client, ac))
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = format!("{}", e);
|
||||
if msg.contains("Elevation required") {
|
||||
eprintln!("\n\x1b[33m{}\x1b[0m", msg);
|
||||
anyhow::bail!("Elevation required. See instructions above.");
|
||||
}
|
||||
eprintln!("\x1b[31mGSAP authorization failed: {}\x1b[0m", msg);
|
||||
anyhow::bail!("GSAP: {}", msg);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Phase 1: --check to generate diff
|
||||
println!("Phase 1: Generating diff (--check)...");
|
||||
let check_output = build_cmd(true).output()?;
|
||||
|
|
@ -190,6 +237,24 @@ impl PlaybookCommands {
|
|||
&format!("{} rc={} duration={}s", name, rc, duration.as_secs()),
|
||||
);
|
||||
|
||||
// ── GSAP: Post Completion Receipt ─────────────
|
||||
if let Some((client, ref ac)) = gsap_ac {
|
||||
let outcome = if status.success() { "completed" } else { "failed" };
|
||||
let chronicle_session = std::env::var("CHRONICLE_SESSION_ID").unwrap_or_default();
|
||||
|
||||
match client.complete(ac, outcome, &chronicle_session) {
|
||||
Ok(_) => {
|
||||
println!("\x1b[32m✓ GSAP: Completion receipt posted\x1b[0m");
|
||||
println!(" Session: /api/v1/governance/session/{}/", ac.context_id);
|
||||
}
|
||||
Err(e) => {
|
||||
// CR delivery failure is recoverable (GSAP R-29).
|
||||
// The operation already completed.
|
||||
eprintln!("\x1b[33mGSAP: CR delivery failed (stored locally): {}\x1b[0m", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if status.success() {
|
||||
println!("\nPlaybook complete. ({:.1}s)", duration.as_secs_f64());
|
||||
println!("Chronicle: PLAYBOOK_COMPLETED recorded");
|
||||
|
|
|
|||
Loading…
Reference in a new issue