a3s-use-core 0.2.8

Shared typed contracts for A3S Use domains
Documentation
use a3s_use_core::{PluginManagerToolset, PLUGIN_MANAGER_TOOLSET_SCHEMA_V4};

const TOOLSET: &[u8] = include_bytes!("../fixtures/plugins/manager-toolset-v4.json");
const TOOLSET_DIGEST: &str =
    include_str!("../fixtures/plugins/manager-toolset-v4.sha256").trim_ascii_end();

fn canonical_fixture(bytes: &[u8]) -> &[u8] {
    bytes.strip_suffix(b"\n").unwrap_or(bytes)
}

#[test]
fn manager_toolset_exposes_only_reviewed_bounded_lifecycle_operations() {
    let toolset = PluginManagerToolset::v4();
    toolset.validate().unwrap();

    let names = toolset
        .tools
        .iter()
        .map(|tool| tool.name.as_str())
        .collect::<Vec<_>>();
    assert_eq!(
        names,
        vec![
            "plugin_search",
            "plugin_inspect",
            "plugin_list_installed",
            "plugin_status",
            "plugin_plan_install",
            "plugin_plan_upgrade",
            "plugin_plan_uninstall",
            "plugin_apply_plan",
            "plugin_plan_enable",
            "plugin_plan_disable",
        ]
    );
    assert!(!names.contains(&"plugin_enable"));
    assert!(!names.contains(&"plugin_disable"));
    assert!(!names.contains(&"plugin_execute"));

    let apply = toolset.tool("plugin_apply_plan").unwrap();
    assert!(apply.annotations.destructive_hint);
    assert!(apply.annotations.idempotent_hint);
    assert!(!apply.annotations.read_only_hint);
    assert_eq!(
        apply.input_schema["required"],
        serde_json::json!(["operationId", "planDigest"])
    );
    assert_eq!(
        apply.input_schema["properties"].as_object().unwrap().len(),
        2
    );

    let install = toolset.tool("plugin_plan_install").unwrap();
    assert_eq!(
        install.input_schema["properties"]["registryName"],
        serde_json::json!({
            "type": "string",
            "pattern": "^[a-z][a-z0-9-]{0,62}$"
        })
    );
    assert!(
        toolset.tool("plugin_plan_upgrade").unwrap().input_schema["properties"]
            .get("registryName")
            .is_none()
    );

    for name in ["plugin_plan_enable", "plugin_plan_disable"] {
        let tool = toolset.tool(name).unwrap();
        assert!(tool.annotations.read_only_hint);
        assert!(!tool.annotations.destructive_hint);
        assert!(!tool.annotations.idempotent_hint);
    }

    for tool in &toolset.tools {
        let schema = tool.input_schema.to_string().to_ascii_lowercase();
        for forbidden in [
            "command",
            "endpoint",
            "executable",
            "provider",
            "secret",
            "\"path\"",
            "\"url\"",
        ] {
            assert!(
                !schema.contains(forbidden),
                "{} exposes forbidden input authority: {forbidden}",
                tool.name
            );
        }
    }
}

#[test]
fn current_manager_toolset_fixture_is_canonical_and_frozen() {
    let toolset = PluginManagerToolset::from_json(TOOLSET).unwrap();
    assert_eq!(toolset, PluginManagerToolset::v4());
    assert_eq!(toolset.schema, PLUGIN_MANAGER_TOOLSET_SCHEMA_V4);
    assert_eq!(
        toolset.canonical_bytes().unwrap(),
        canonical_fixture(TOOLSET)
    );
    assert_eq!(toolset.descriptor_digest().unwrap(), TOOLSET_DIGEST);

    let mut drift: serde_json::Value = serde_json::from_slice(TOOLSET).unwrap();
    drift["tools"][7]["inputSchema"]["properties"]["url"] = serde_json::json!({"type":"string"});
    let error = PluginManagerToolset::from_json(&serde_json::to_vec(&drift).unwrap()).unwrap_err();
    assert_eq!(error.code, "use.plugin.manager_toolset_invalid");

    let mut retired = serde_json::to_value(PluginManagerToolset::v4()).unwrap();
    retired["schema"] = serde_json::json!("a3s.use.plugin-manager-tools.v3");
    let error =
        PluginManagerToolset::from_json(&serde_json::to_vec(&retired).unwrap()).unwrap_err();
    assert_eq!(error.code, "use.plugin.manager_toolset_invalid");
}