a3s-use-core 0.2.10

Shared typed contracts for A3S Use domains
Documentation
use a3s_use_core::{
    PlanActor, PlanPolicyDecision, PluginPermissionCeiling, PluginWorkspaceGrant,
    WorkspaceGrantAuthority, PLUGIN_WORKSPACE_GRANT_SCHEMA,
};

const PACKAGE_DIGEST: &str =
    "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const POLICY_DIGEST: &str =
    "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
const CONFIRMATION_DIGEST: &str =
    "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
const WORKSPACE_GRANT: &[u8] = include_bytes!("../fixtures/plugins/workspace-grant-v1.json");
const WORKSPACE_GRANT_DIGEST: &str =
    include_str!("../fixtures/plugins/workspace-grant-v1.sha256").trim_ascii_end();

#[test]
fn workspace_grant_is_canonical_scope_and_package_bound_evidence() {
    let ceiling = permission_ceiling();
    let grant = grant(
        ceiling.clone(),
        PlanActor::User,
        PlanPolicyDecision::Ask,
        Some(CONFIRMATION_DIGEST),
    );

    grant.validate_active_against(&ceiling, 1_500).unwrap();
    grant.validate_at(1_500).unwrap();
    assert_eq!(
        grant.validate_at(2_000).unwrap_err().code,
        "use.plugin.grant_expired"
    );
    let canonical = grant.canonical_bytes().unwrap();
    assert_eq!(canonical, canonical_fixture(WORKSPACE_GRANT));
    assert_eq!(
        PluginWorkspaceGrant::from_json(WORKSPACE_GRANT).unwrap(),
        grant
    );
    assert_eq!(grant.descriptor_digest().unwrap(), WORKSPACE_GRANT_DIGEST);
}

#[test]
fn secret_grants_require_an_explicit_user_confirmation() {
    let ceiling = permission_ceiling();
    let agent = grant(
        ceiling.clone(),
        PlanActor::Agent,
        PlanPolicyDecision::Allow,
        None,
    );
    assert_eq!(
        agent.validate_against(&ceiling).unwrap_err().code,
        "use.plugin.grant_invalid"
    );

    let user_without_confirmation = grant(
        ceiling.clone(),
        PlanActor::User,
        PlanPolicyDecision::Allow,
        None,
    );
    assert_eq!(
        user_without_confirmation
            .validate_against(&ceiling)
            .unwrap_err()
            .code,
        "use.plugin.grant_invalid"
    );
}

#[test]
fn resolved_permissions_cannot_exceed_the_signed_ceiling() {
    let ceiling = permission_ceiling();
    let mut permissions = ceiling.clone();
    let convert = permissions
        .surfaces
        .iter_mut()
        .find(|permission| permission.surface.id == "convert")
        .unwrap();
    convert.resources.as_mut().unwrap().cpu_millis = 2_000;
    let escalated = grant(
        permissions,
        PlanActor::User,
        PlanPolicyDecision::Ask,
        Some(CONFIRMATION_DIGEST),
    );

    assert!(!escalated.permissions.is_within(&ceiling).unwrap());
    assert_eq!(
        escalated.validate_against(&ceiling).unwrap_err().code,
        "use.plugin.grant_exceeds_ceiling"
    );
}

#[test]
fn a_narrow_secret_free_agent_grant_is_within_the_ceiling() {
    let ceiling = permission_ceiling();
    let mut permissions = ceiling.clone();
    let convert = permissions
        .surfaces
        .iter_mut()
        .find(|permission| permission.surface.id == "convert")
        .unwrap();
    convert.secrets.clear();
    convert.filesystem.retain(|path| path.path == "inputs");
    convert.resources.as_mut().unwrap().cpu_millis = 500;
    let grant = grant(
        permissions,
        PlanActor::Agent,
        PlanPolicyDecision::Allow,
        None,
    );

    grant.validate_against(&ceiling).unwrap();
    assert!(grant.permissions.is_within(&ceiling).unwrap());
}

#[test]
fn filesystem_network_and_ui_authority_must_only_narrow() {
    let ceiling = permission_ceiling();

    let mut filesystem = ceiling.clone();
    let convert = filesystem
        .surfaces
        .iter_mut()
        .find(|permission| permission.surface.id == "convert")
        .unwrap();
    convert
        .filesystem
        .iter_mut()
        .find(|permission| permission.path == "inputs")
        .unwrap()
        .path = ".".to_string();
    assert_exceeds(filesystem, &ceiling);

    let mut network = ceiling.clone();
    let convert = network
        .surfaces
        .iter_mut()
        .find(|permission| permission.surface.id == "convert")
        .unwrap();
    convert.network_egress[0].ports = vec![80];
    assert_exceeds(network, &ceiling);

    let mut ui = ceiling.clone();
    let review = ui
        .surfaces
        .iter_mut()
        .find(|permission| permission.surface.id == "review")
        .unwrap();
    review.ui_http[0].path_prefixes = vec!["/".to_string()];
    assert_exceeds(ui, &ceiling);
}

fn permission_ceiling() -> PluginPermissionCeiling {
    PluginPermissionCeiling::from_json(include_bytes!(
        "../fixtures/plugins/permission-ceiling-v1.json"
    ))
    .unwrap()
}

fn grant(
    permissions: PluginPermissionCeiling,
    actor: PlanActor,
    decision: PlanPolicyDecision,
    confirmation_digest: Option<&str>,
) -> PluginWorkspaceGrant {
    let ceiling = permission_ceiling();
    PluginWorkspaceGrant {
        schema: PLUGIN_WORKSPACE_GRANT_SCHEMA.to_string(),
        scope_id: "workspace-01".to_string(),
        package_id: "acme/research".to_string(),
        package_digest: PACKAGE_DIGEST.to_string(),
        permission_ceiling_digest: ceiling.descriptor_digest().unwrap(),
        permissions_digest: permissions.descriptor_digest().unwrap(),
        permissions,
        authority: WorkspaceGrantAuthority {
            actor,
            decision,
            policy_digest: POLICY_DIGEST.to_string(),
            confirmation_digest: confirmation_digest.map(str::to_string),
        },
        granted_at_ms: 1_000,
        expires_at_ms: Some(2_000),
    }
}

fn assert_exceeds(permissions: PluginPermissionCeiling, ceiling: &PluginPermissionCeiling) {
    let grant = grant(
        permissions,
        PlanActor::User,
        PlanPolicyDecision::Ask,
        Some(CONFIRMATION_DIGEST),
    );
    assert!(!grant.permissions.is_within(ceiling).unwrap());
    assert_eq!(
        grant.validate_against(ceiling).unwrap_err().code,
        "use.plugin.grant_exceeds_ceiling"
    );
}

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