clawdstrike 0.2.5

Security guards and policy engine for AI agent execution
Documentation
use std::path::{Path, PathBuf};

use clawdstrike::{
    MarketplaceEntry, MarketplaceFeed, SignedMarketplaceFeed, SignedPolicyBundle,
    MARKETPLACE_FEED_SCHEMA_VERSION,
};
use hush_core::Keypair;

fn main() {
    let mut bundles_dir: Option<PathBuf> = None;
    let mut output: PathBuf = PathBuf::from("feed.signed.json");
    let mut feed_id: String = "builtin".to_string();
    let mut seq: u64 = 1;
    let mut published_at: Option<String> = None;
    let mut bundle_uri_prefix: String = "builtin://bundles/".to_string();

    let mut args = std::env::args().skip(1);
    while let Some(arg) = args.next() {
        match arg.as_str() {
            "--bundles-dir" => {
                bundles_dir = args.next().map(PathBuf::from);
            }
            "--output" | "-o" => {
                output = args.next().map(PathBuf::from).unwrap_or(output);
            }
            "--feed-id" => {
                feed_id = args.next().unwrap_or(feed_id);
            }
            "--seq" => {
                if let Some(v) = args.next() {
                    if let Ok(n) = v.parse::<u64>() {
                        seq = n;
                    }
                }
            }
            "--published-at" => {
                published_at = args.next();
            }
            "--bundle-uri-prefix" => {
                bundle_uri_prefix = args.next().unwrap_or(bundle_uri_prefix);
            }
            "--help" | "-h" => {
                eprintln!(
                    "Usage: marketplace_feed_gen --bundles-dir DIR [--output PATH] [--feed-id ID] [--seq N] [--published-at ISO8601] [--bundle-uri-prefix PREFIX]\n\nEnvironment:\n  MARKETPLACE_FEED_SIGNING_KEY  hex seed (32 bytes) used to sign the feed."
                );
                std::process::exit(0);
            }
            _ => {}
        }
    }

    let Some(bundles_dir) = bundles_dir else {
        eprintln!("Error: --bundles-dir is required");
        std::process::exit(2);
    };

    let signing_key_hex = match std::env::var("MARKETPLACE_FEED_SIGNING_KEY") {
        Ok(v) => v,
        Err(_) => {
            eprintln!("Error: MARKETPLACE_FEED_SIGNING_KEY is not set");
            std::process::exit(2);
        }
    };
    let keypair = match Keypair::from_hex(signing_key_hex.trim()) {
        Ok(v) => v,
        Err(e) => {
            eprintln!("Error: invalid signing key: {e}");
            std::process::exit(2);
        }
    };

    let published_at = published_at.unwrap_or_else(|| chrono::Utc::now().to_rfc3339());

    let entries = match build_entries(&bundles_dir, &bundle_uri_prefix) {
        Ok(v) => v,
        Err(e) => {
            eprintln!("Error: failed to build entries: {e}");
            std::process::exit(2);
        }
    };

    let feed = MarketplaceFeed {
        version: MARKETPLACE_FEED_SCHEMA_VERSION.to_string(),
        feed_id,
        published_at,
        seq,
        entries,
        metadata: None,
    };

    let signed = match SignedMarketplaceFeed::sign_with_public_key(feed, &keypair) {
        Ok(v) => v,
        Err(e) => {
            eprintln!("Error: failed to sign feed: {e}");
            std::process::exit(2);
        }
    };

    let json = match serde_json::to_string_pretty(&signed) {
        Ok(v) => v,
        Err(e) => {
            eprintln!("Error: failed to encode feed: {e}");
            std::process::exit(2);
        }
    };

    if let Err(e) = std::fs::write(&output, json) {
        eprintln!("Error: failed to write {}: {e}", output.display());
        std::process::exit(2);
    }

    eprintln!("Signed feed written: {}", output.display());
    eprintln!("Feed public key: {}", keypair.public_key().to_hex());
}

fn build_entries(
    bundles_dir: &Path,
    bundle_uri_prefix: &str,
) -> std::io::Result<Vec<MarketplaceEntry>> {
    let mut files: Vec<PathBuf> = Vec::new();
    for entry in std::fs::read_dir(bundles_dir)? {
        let entry = entry?;
        let path = entry.path();
        if !path.is_file() {
            continue;
        }
        if let Some(name) = path.file_name().and_then(|s| s.to_str()) {
            if name.ends_with(".signed_bundle.json") {
                files.push(path);
            }
        }
    }
    files.sort();

    let mut entries = Vec::new();
    for path in files {
        let Some(file_name) = path.file_name().and_then(|s| s.to_str()) else {
            continue;
        };
        let entry_id = file_name
            .strip_suffix(".signed_bundle.json")
            .unwrap_or(file_name)
            .to_string();

        let data = std::fs::read(&path)?;
        let signed_bundle: SignedPolicyBundle = match serde_json::from_slice(&data) {
            Ok(v) => v,
            Err(e) => {
                return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e));
            }
        };

        let bundle_valid = signed_bundle.verify_embedded().unwrap_or(false);
        if !bundle_valid {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidData,
                format!("Bundle signature failed verification: {}", file_name),
            ));
        }

        let policy = &signed_bundle.bundle.policy;
        entries.push(MarketplaceEntry {
            entry_id,
            bundle_uri: format!("{bundle_uri_prefix}{file_name}"),
            title: if policy.name.is_empty() {
                None
            } else {
                Some(policy.name.clone())
            },
            description: if policy.description.is_empty() {
                None
            } else {
                Some(policy.description.clone())
            },
            category: None,
            tags: Vec::new(),
            author: Some("Clawdstrike".to_string()),
            author_url: None,
            icon: None,
            created_at: None,
            updated_at: None,
            provenance: None,
            curator_public_key: None,
            content_ids: None,
            gateway_hints: Vec::new(),
        });
    }

    Ok(entries)
}