tirea_extension_permission/
plugin.rs

1use super::actions::{apply_tool_policy, reject_out_of_scope};
2use super::mechanism::{enforce_permission, PermissionMechanismDecision, PermissionMechanismInput};
3use super::state::{PermissionOverrides, PermissionPolicy};
4use super::strategy::evaluate_tool_permission;
5use async_trait::async_trait;
6use tirea_contract::runtime::behavior::{AgentBehavior, ReadOnlyContext};
7use tirea_contract::runtime::phase::{ActionSet, BeforeInferenceAction, BeforeToolExecuteAction};
8use tirea_contract::scope;
9
10/// Stable plugin id for permission actions.
11pub const PERMISSION_PLUGIN_ID: &str = "permission";
12
13/// Permission strategy plugin.
14///
15/// Checks permissions in `before_tool_execute` and delegates rule evaluation,
16/// runtime gating, and frontend form construction to dedicated modules.
17pub struct PermissionPlugin;
18
19#[async_trait]
20impl AgentBehavior for PermissionPlugin {
21    fn id(&self) -> &str {
22        PERMISSION_PLUGIN_ID
23    }
24
25    tirea_contract::declare_plugin_states!(PermissionPolicy, PermissionOverrides);
26
27    async fn before_tool_execute(
28        &self,
29        ctx: &ReadOnlyContext<'_>,
30    ) -> ActionSet<BeforeToolExecuteAction> {
31        let Some(tool_id) = ctx.tool_name() else {
32            return ActionSet::empty();
33        };
34
35        let snapshot = ctx.snapshot();
36        let ruleset = super::state::permission_rules_from_snapshot(&snapshot);
37        let evaluation = evaluate_tool_permission(&ruleset, tool_id);
38        let decision = enforce_permission(
39            PermissionMechanismInput {
40                tool_id,
41                tool_args: ctx.tool_args().cloned().unwrap_or_default(),
42                call_id: ctx.tool_call_id(),
43                resume_action: ctx.resume_input().map(|resume| resume.action.clone()),
44            },
45            &evaluation,
46        );
47
48        match decision {
49            PermissionMechanismDecision::Proceed => ActionSet::empty(),
50            PermissionMechanismDecision::Action(action) => ActionSet::single(*action),
51        }
52    }
53}
54
55/// Tool scope policy plugin.
56///
57/// Enforces allow/deny list filtering via typed `RunPolicy` policy fields.
58/// Install before [`PermissionPlugin`] so out-of-scope tools are blocked first.
59pub struct ToolPolicyPlugin;
60
61#[async_trait]
62impl AgentBehavior for ToolPolicyPlugin {
63    fn id(&self) -> &str {
64        "tool_policy"
65    }
66
67    async fn before_inference(
68        &self,
69        ctx: &ReadOnlyContext<'_>,
70    ) -> ActionSet<BeforeInferenceAction> {
71        let run_policy = ctx.run_policy();
72        let allowed = run_policy.allowed_tools();
73        let excluded = run_policy.excluded_tools();
74
75        if allowed.is_none() && excluded.is_none() {
76            return ActionSet::empty();
77        }
78        apply_tool_policy(
79            allowed.map(|values| values.to_vec()),
80            excluded.map(|values| values.to_vec()),
81        )
82        .into()
83    }
84
85    async fn before_tool_execute(
86        &self,
87        ctx: &ReadOnlyContext<'_>,
88    ) -> ActionSet<BeforeToolExecuteAction> {
89        let Some(tool_id) = ctx.tool_name() else {
90            return ActionSet::empty();
91        };
92
93        let run_policy = ctx.run_policy();
94        if !scope::is_scope_allowed(Some(run_policy), tool_id, scope::ScopeDomain::Tool) {
95            ActionSet::single(reject_out_of_scope(tool_id))
96        } else {
97            ActionSet::empty()
98        }
99    }
100}