actrpc-interceptor 0.1.0

Concrete interceptors for ActRPC.
Documentation
use actrpc_core::{
    action::ActionSpec,
    interception::{InterceptionRequest, InterceptorContinuation},
    json_rpc::{
        JsonRpcId, JsonRpcMessage, JsonRpcParams, JsonRpcRequest, JsonRpcResponse,
        JsonRpcSingleMessage, JsonRpcSuccessResponse, JsonRpcVersion,
    },
    participant::{Participant, ParticipantType},
};
use actrpc_interceptor::interceptors::policy::{
    PolicyInterceptor,
    config::{
        MatchExpr, PolicyApply, PolicyConfig, PolicyEffect, PolicyMatcher, PolicyRule,
        RejectCallEffect,
    },
};
use actrpc_orchestrator::{action::actions::reject_call::RejectCall, interceptor::Interceptor};
use serde_json::json;

#[tokio::test]
async fn phase_fact_matches_outbound_only() {
    let interceptor = PolicyInterceptor::new(outbound_write_reject_policy()).unwrap();

    let request = outbound_request("write_file", json!({ "path": "/tmp/a.txt" }));

    let response = interceptor.intercept(&request).await.unwrap();

    assert_eq!(response.continuation, InterceptorContinuation::Stop);
    assert_eq!(response.actions.len(), 1);
    assert_eq!(response.actions[0].kind, RejectCall::action_kind());
}

#[tokio::test]
async fn outbound_phase_rule_does_not_match_inbound_response() {
    let interceptor = PolicyInterceptor::new(outbound_write_reject_policy()).unwrap();

    let request = inbound_success_response(json!({ "ok": true }));

    let response = interceptor.intercept(&request).await.unwrap();

    assert_eq!(response.continuation, InterceptorContinuation::Stop);
    assert!(response.actions.is_empty());
}

fn outbound_write_reject_policy() -> PolicyConfig {
    PolicyConfig {
        rules: vec![PolicyRule {
            name: "reject_outbound_write".to_owned(),
            match_expr: MatchExpr::all(vec![
                MatchExpr::condition("phase", PolicyMatcher::exact("outbound")),
                MatchExpr::condition("message.method", PolicyMatcher::exact("write_file")),
            ]),
            apply: PolicyApply {
                immediate: vec![PolicyEffect::RejectCall {
                    reject_call: RejectCallEffect {
                        error: actrpc_core::json_rpc::JsonRpcError {
                            code: -32010,
                            message: "outbound write denied".to_owned(),
                            data: None,
                        },
                    },
                }],
                review: None,
            },
        }],
    }
}

fn outbound_request(method: &str, params: serde_json::Value) -> InterceptionRequest {
    let params = match params {
        serde_json::Value::Array(values) => Some(JsonRpcParams::Array(values)),
        serde_json::Value::Object(map) => Some(JsonRpcParams::Object(map)),
        other => panic!("test params must be array or object, got {other}"),
    };

    InterceptionRequest {
        origin: Participant {
            kind: ParticipantType::Orchestrator,
            id: "orchestrator".to_owned(),
        },
        message: JsonRpcMessage::Single(JsonRpcSingleMessage::Request(JsonRpcRequest {
            jsonrpc: JsonRpcVersion::V2_0,
            id: JsonRpcId::Number(1.into()),
            method: method.to_owned(),
            params,
        })),
        resolved_action_history: vec![],
    }
}

fn inbound_success_response(result: serde_json::Value) -> InterceptionRequest {
    InterceptionRequest {
        origin: Participant {
            kind: ParticipantType::Orchestrator,
            id: "orchestrator".to_owned(),
        },
        message: JsonRpcMessage::Single(JsonRpcSingleMessage::Response(JsonRpcResponse::Success(
            JsonRpcSuccessResponse {
                jsonrpc: JsonRpcVersion::V2_0,
                id: JsonRpcId::Number(1.into()),
                result,
            },
        ))),
        resolved_action_history: vec![],
    }
}