greentic-pack-lib 1.1.5

Greentic pack builder and reader
Documentation
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

use assert_cmd::prelude::*;
use greentic_pack::reader::{SigningPolicy, open_pack};
use greentic_types::cbor::canonical;
use greentic_types::schemas::common::schema_ir::{AdditionalProperties, SchemaIr};
use greentic_types::schemas::component::v0_6_0::{
    ComponentDescribe, ComponentInfo, ComponentOperation, ComponentRunInput, ComponentRunOutput,
    schema_hash,
};
use sha2::{Digest, Sha256};
use tempfile::TempDir;

fn write_stub_wasm(path: &Path) {
    const STUB: &[u8] = &[0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).expect("create parent dirs");
    }
    fs::write(path, STUB).expect("write stub wasm");
}

fn write_describe_sidecar(wasm_path: &Path, component_id: &str) {
    let input_schema = SchemaIr::Object {
        properties: Default::default(),
        required: Vec::new(),
        additional: AdditionalProperties::Forbid,
    };
    let output_schema = SchemaIr::Object {
        properties: Default::default(),
        required: Vec::new(),
        additional: AdditionalProperties::Forbid,
    };
    let config_schema = SchemaIr::Object {
        properties: Default::default(),
        required: Vec::new(),
        additional: AdditionalProperties::Forbid,
    };
    let hash = schema_hash(&input_schema, &output_schema, &config_schema).expect("schema hash");
    let operation = ComponentOperation {
        id: "handle_message".to_string(),
        display_name: None,
        input: ComponentRunInput {
            schema: input_schema,
        },
        output: ComponentRunOutput {
            schema: output_schema,
        },
        defaults: Default::default(),
        redactions: Vec::new(),
        constraints: Default::default(),
        schema_hash: hash,
    };
    let describe = ComponentDescribe {
        info: ComponentInfo {
            id: component_id.to_string(),
            version: "0.1.0".to_string(),
            role: "tool".to_string(),
            display_name: None,
        },
        provided_capabilities: Vec::new(),
        outcomes: Vec::new(),
        required_capabilities: Vec::new(),
        metadata: Default::default(),
        operations: vec![operation],
        config_schema,
    };
    let bytes = canonical::to_canonical_cbor_allow_floats(&describe).expect("encode describe");
    let describe_path = format!("{}.describe.cbor", wasm_path.display());
    fs::write(describe_path, bytes).expect("write describe cache");
}

#[test]
fn open_pack_accepts_packc_gtpack() {
    let temp = TempDir::new().expect("temp dir");
    let pack_dir = temp.path().join("demo-pack");
    let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .unwrap()
        .parent()
        .unwrap()
        .to_path_buf();

    // Create a minimal pack with a local component and sidecar mapping.
    fs::create_dir_all(pack_dir.join("flows")).expect("flows dir");
    fs::create_dir_all(pack_dir.join("components")).expect("components dir");
    let wasm_path = pack_dir.join("components/demo.wasm");
    write_stub_wasm(&wasm_path);
    write_describe_sidecar(&wasm_path, "demo.component");
    let digest = format!(
        "sha256:{}",
        hex::encode(Sha256::digest(fs::read(&wasm_path).expect("read wasm")))
    );

    let pack_yaml = r#"pack_id: demo.test
version: 0.1.0
kind: application
publisher: Greentic

components:
  - id: "demo.component"
    version: "0.1.0"
    world: "greentic:component/component@0.5.0"
    supports: ["messaging"]
    profiles:
      default: "stateless"
      supported: ["stateless"]
    capabilities:
      wasi: {}
      host: {}
    operations:
      - name: "handle_message"
        input_schema: {}
        output_schema: {}
    wasm: "components/demo.wasm"

flows:
  - id: main
    file: flows/main.ygtc
    tags: [default]
    entrypoints: [main]
"#;
    fs::write(pack_dir.join("pack.yaml"), pack_yaml).expect("write pack.yaml");

    let flow_yaml = r#"id: main
type: messaging
start: call
nodes:
  call:
    component.exec:
      component: demo.component
      operation: handle_message
      input:
        text: "hi"
    routing:
      - out: true
"#;
    fs::write(pack_dir.join("flows/main.ygtc"), flow_yaml).expect("write flow");

    let sidecar = format!(
        r#"{{
  "schema_version": 1,
  "flow": "flows/main.ygtc",
  "nodes": {{
    "call": {{
      "source": {{
        "kind": "local",
        "path": "../components/demo.wasm",
        "digest": "{digest}"
      }}
    }}
  }}
}}"#
    );
    fs::write(pack_dir.join("flows/main.ygtc.resolve.json"), sidecar).expect("write sidecar");
    let summary = format!(
        r#"{{
  "schema_version": 1,
  "flow": "main.ygtc",
  "nodes": {{
    "call": {{
      "component_id": "demo.component",
      "source": {{
        "kind": "local",
        "path": "../components/demo.wasm"
      }},
      "digest": "{digest}"
    }}
  }}
}}"#
    );
    fs::write(
        pack_dir.join("flows/main.ygtc.resolve.summary.json"),
        summary,
    )
    .expect("write summary");

    let pack_path = temp.path().join("pack.gtpack");
    Command::new("cargo")
        .current_dir(&workspace_root)
        .env("GREENTIC_PACK_USE_DESCRIBE_CACHE", "1")
        .args([
            "run",
            "-p",
            "greentic-pack",
            "--bin",
            "greentic-pack",
            "--quiet",
            "--",
            "build",
            "--in",
            pack_dir.to_str().unwrap(),
            "--allow-pack-schema",
            "--gtpack-out",
            pack_path.to_str().unwrap(),
            "--offline",
        ])
        .assert()
        .success();

    let load = open_pack(&pack_path, SigningPolicy::DevOk)
        .expect("packc-generated gtpack should be accepted");

    assert_eq!(load.manifest.meta.pack_id, "demo.test");
    assert!(!load.report.signature_ok);
    assert!(load.report.sbom_ok);
    assert!(load.report.warnings.iter().all(|msg| !msg.contains("sbom")));
    assert!(!load.sbom.is_empty());
}