power_house 0.3.7

Deterministic verification and provenance system for portable computational identities using .pha artifacts and Rootprint graphs.
Documentation
use power_house::provenance::PhaArtifact;
use serde_json::json;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};

fn temp_dir() -> PathBuf {
    let suffix = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    let path = std::env::temp_dir().join(format!("power-house-identity-{suffix}"));
    fs::create_dir_all(&path).unwrap();
    path
}

fn write_json(path: &Path, value: &impl serde::Serialize) {
    fs::write(path, serde_json::to_vec_pretty(value).unwrap()).unwrap();
}

fn run(args: &[&str]) -> String {
    let output = Command::new(env!("CARGO_BIN_EXE_julian"))
        .args(args)
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "julian {:?} failed:\nstdout={}\nstderr={}",
        args,
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
    String::from_utf8(output.stdout).unwrap()
}

fn artifact(value: u64) -> PhaArtifact {
    PhaArtifact::new(
        json!({"source": "identity-cli"}),
        "power-house/identity-cli/v1",
        json!({"value": value}),
        json!({"accepted": true}),
    )
    .unwrap()
}

#[test]
fn cli_runs_complete_offline_identity_workflow() {
    let dir = temp_dir();
    let root_pha = dir.join("root.pha");
    let shared_pha = dir.join("shared.pha");
    let merged_pha = dir.join("merged.pha");
    let bound_pha = dir.join("root-bound.pha");
    let graph = dir.join("graph.json");
    let root_identity = dir.join("root.identity.json");
    let left_identity = dir.join("left.identity.json");
    let right_identity = dir.join("right.identity.json");
    let merged_identity = dir.join("merged.identity.json");
    let replay = dir.join("replay.json");

    write_json(&root_pha, &artifact(1));
    write_json(&shared_pha, &artifact(2));
    write_json(&merged_pha, &artifact(3));

    run(&[
        "identity",
        "create",
        root_pha.to_str().unwrap(),
        "--label",
        "main",
        "--identity-output",
        root_identity.to_str().unwrap(),
        "--rootprint-output",
        graph.to_str().unwrap(),
        "--artifact-output",
        bound_pha.to_str().unwrap(),
    ]);
    run(&[
        "identity",
        "fork",
        root_identity.to_str().unwrap(),
        graph.to_str().unwrap(),
        shared_pha.to_str().unwrap(),
        "--label",
        "left",
        "--identity-output",
        left_identity.to_str().unwrap(),
    ]);
    run(&[
        "identity",
        "fork",
        root_identity.to_str().unwrap(),
        graph.to_str().unwrap(),
        shared_pha.to_str().unwrap(),
        "--label",
        "right",
        "--identity-output",
        right_identity.to_str().unwrap(),
    ]);
    assert_eq!(
        run(&[
            "identity",
            "equivalent",
            left_identity.to_str().unwrap(),
            right_identity.to_str().unwrap(),
            graph.to_str().unwrap(),
        ])
        .trim(),
        "equivalent"
    );
    run(&[
        "identity",
        "merge",
        left_identity.to_str().unwrap(),
        right_identity.to_str().unwrap(),
        graph.to_str().unwrap(),
        merged_pha.to_str().unwrap(),
        "--label",
        "accepted",
        "--identity-output",
        merged_identity.to_str().unwrap(),
    ]);
    assert!(run(&[
        "identity",
        "verify",
        merged_identity.to_str().unwrap(),
        graph.to_str().unwrap(),
    ])
    .contains("verified offline"));
    run(&[
        "identity",
        "replay",
        merged_identity.to_str().unwrap(),
        graph.to_str().unwrap(),
        "--output",
        replay.to_str().unwrap(),
    ]);

    let replay_value: serde_json::Value =
        serde_json::from_slice(&fs::read(replay).unwrap()).unwrap();
    assert!(replay_value["graph"]["state_fingerprint"]
        .as_str()
        .unwrap()
        .starts_with("sha256:"));
    let bound_value: serde_json::Value =
        serde_json::from_slice(&fs::read(bound_pha).unwrap()).unwrap();
    assert!(bound_value["identity_root"]
        .as_str()
        .unwrap()
        .starts_with("sha256:"));

    fs::remove_dir_all(dir).unwrap();
}