hushspec 0.1.0

Portable specification types for AI agent security rules
Documentation
use hushspec::{Decision, EvaluationAction, HushSpec, evaluate};

#[test]
fn input_inject_denies_unlisted_type() {
    let spec = HushSpec::parse(
        r#"
hushspec: "0.1.0"
name: input-injection
rules:
  input_injection:
    enabled: true
    allowed_types:
      - keyboard
"#,
    )
    .expect("valid spec");

    let result = evaluate(
        &spec,
        &EvaluationAction {
            action_type: "input_inject".into(),
            target: Some("mouse".into()),
            ..Default::default()
        },
    );

    assert_eq!(result.decision, Decision::Deny);
    assert_eq!(
        result.matched_rule.as_deref(),
        Some("rules.input_injection.allowed_types")
    );
}

#[test]
fn computer_use_respects_remote_desktop_channel_blocks() {
    let spec = HushSpec::parse(
        r#"
hushspec: "0.1.0"
name: remote-desktop
rules:
  computer_use:
    enabled: true
    mode: observe
    allowed_actions:
      - remote.clipboard
  remote_desktop_channels:
    enabled: true
    clipboard: false
    file_transfer: false
    audio: true
    drive_mapping: false
"#,
    )
    .expect("valid spec");

    let result = evaluate(
        &spec,
        &EvaluationAction {
            action_type: "computer_use".into(),
            target: Some("remote.clipboard".into()),
            ..Default::default()
        },
    );

    assert_eq!(result.decision, Decision::Deny);
    assert_eq!(
        result.matched_rule.as_deref(),
        Some("rules.remote_desktop_channels.clipboard")
    );
}

#[test]
fn origin_profile_tool_access_still_respects_base_blocklist() {
    let spec = HushSpec::parse(
        r#"
hushspec: "0.1.0"
name: origin-tool-access
rules:
  tool_access:
    enabled: true
    allow:
      - "*"
    block:
      - dangerous_tool
    require_confirmation: []
    default: allow
extensions:
  origins:
    default_behavior: deny
    profiles:
      - id: slack
        match:
          provider: slack
        tool_access:
          enabled: true
          allow:
            - "*"
          block: []
          require_confirmation: []
          default: allow
"#,
    )
    .expect("valid spec");

    let result = evaluate(
        &spec,
        &EvaluationAction {
            action_type: "tool_call".into(),
            target: Some("dangerous_tool".into()),
            origin: Some(hushspec::OriginContext {
                provider: Some("slack".into()),
                ..Default::default()
            }),
            ..Default::default()
        },
    );

    assert_eq!(result.decision, Decision::Deny);
    assert_eq!(
        result.matched_rule.as_deref(),
        Some("rules.tool_access.block")
    );
    assert_eq!(result.origin_profile.as_deref(), Some("slack"));
}

#[test]
fn origin_profile_egress_cannot_bypass_base_default_block() {
    let spec = HushSpec::parse(
        r#"
hushspec: "0.1.0"
name: origin-egress
rules:
  egress:
    enabled: true
    allow:
      - api.safe.example.com
    block: []
    default: block
extensions:
  origins:
    default_behavior: deny
    profiles:
      - id: slack
        match:
          provider: slack
        egress:
          enabled: true
          allow: []
          block: []
          default: allow
"#,
    )
    .expect("valid spec");

    let result = evaluate(
        &spec,
        &EvaluationAction {
            action_type: "egress".into(),
            target: Some("evil.example.com".into()),
            origin: Some(hushspec::OriginContext {
                provider: Some("slack".into()),
                ..Default::default()
            }),
            ..Default::default()
        },
    );

    assert_eq!(result.decision, Decision::Deny);
    assert_eq!(
        result.matched_rule.as_deref(),
        Some("extensions.origins.profiles.slack.egress.default")
    );
    assert_eq!(result.origin_profile.as_deref(), Some("slack"));
}

#[test]
fn forbidden_path_exception_still_respects_path_allowlist() {
    let spec = HushSpec::parse(
        r#"
hushspec: "0.1.0"
name: path-guards
rules:
  forbidden_paths:
    enabled: true
    patterns:
      - "**/*.key"
    exceptions:
      - "/workspace/allowed.key"
  path_allowlist:
    enabled: true
    write:
      - "/workspace/reports/**"
"#,
    )
    .expect("valid spec");

    let result = evaluate(
        &spec,
        &EvaluationAction {
            action_type: "file_write".into(),
            target: Some("/workspace/allowed.key".into()),
            ..Default::default()
        },
    );

    assert_eq!(result.decision, Decision::Deny);
    assert_eq!(result.matched_rule.as_deref(), Some("rules.path_allowlist"));
}

#[test]
fn unknown_posture_state_fails_closed() {
    let spec = HushSpec::parse(
        r#"
hushspec: "0.1.0"
name: posture-unknown-state
extensions:
  posture:
    initial: standard
    states:
      standard:
        capabilities: [file_access]
    transitions: []
"#,
    )
    .expect("valid spec");

    let result = evaluate(
        &spec,
        &EvaluationAction {
            action_type: "file_read".into(),
            target: Some("/workspace/readme.md".into()),
            posture: Some(hushspec::PostureContext {
                current: Some("typo".into()),
                signal: None,
            }),
            ..Default::default()
        },
    );

    assert_eq!(result.decision, Decision::Deny);
    assert_eq!(
        result.matched_rule.as_deref(),
        Some("extensions.posture.states.typo")
    );
    assert_eq!(
        result.reason.as_deref(),
        Some("unknown posture state 'typo'")
    );
}