1use crate::errors::{IicpError, Result};
9
10pub const POLICY_REFUSAL_CODE: &str = "IICP-POLICY-001";
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum IntentRiskCategory {
14 Prohibited,
15 HighRisk,
16 TransparencyRisk,
17 MinimalOrGeneral,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct ProhibitedIntentRule {
22 pub rule_id: &'static str,
23 pub label: &'static str,
24 pub fragments: &'static [&'static str],
25}
26
27pub const PROHIBITED_INTENT_RULES: &[ProhibitedIntentRule] = &[
28 ProhibitedIntentRule {
29 rule_id: "eu-ai-act-social-scoring",
30 label: "social scoring",
31 fragments: &["social-scoring", "social_scoring", "social:scoring"],
32 },
33 ProhibitedIntentRule {
34 rule_id: "eu-ai-act-criminal-risk",
35 label: "individual criminal risk prediction",
36 fragments: &[
37 "criminal-risk",
38 "criminal_risk",
39 "criminal:risk",
40 "predict-crime",
41 ],
42 },
43 ProhibitedIntentRule {
44 rule_id: "eu-ai-act-workplace-education-emotion",
45 label: "workplace or education emotion recognition",
46 fragments: &[
47 "emotion:workplace",
48 "emotion:education",
49 "workplace-monitoring",
50 "education-monitoring",
51 "worker-monitoring",
52 ],
53 },
54 ProhibitedIntentRule {
55 rule_id: "eu-ai-act-protected-trait-biometric",
56 label: "biometric protected-trait classification",
57 fragments: &["protected-trait", "protected_trait", "biometric:protected"],
58 },
59 ProhibitedIntentRule {
60 rule_id: "eu-ai-act-untargeted-face-scraping",
61 label: "untargeted facial image scraping for recognition databases",
62 fragments: &[
63 "untargeted-scraping",
64 "untargeted_scraping",
65 "face-scraping",
66 "facial-scraping",
67 ],
68 },
69 ProhibitedIntentRule {
70 rule_id: "eu-ai-act-realtime-remote-biometric-id",
71 label: "real-time remote biometric identification",
72 fragments: &[
73 "remote-biometric:realtime",
74 "realtime-remote-biometric",
75 "real-time-remote-biometric",
76 ],
77 },
78 ProhibitedIntentRule {
79 rule_id: "eu-ai-act-nonconsensual-sexual-deepfake",
80 label: "non-consensual sexual deepfake or CSAM generation",
81 fragments: &[
82 "nonconsensual-sexual",
83 "non-consensual-sexual",
84 "child-sexual-abuse",
85 "csam",
86 ],
87 },
88];
89
90pub const HIGH_RISK_INTENT_RULES: &[ProhibitedIntentRule] = &[
91 ProhibitedIntentRule {
92 rule_id: "eu-ai-act-employment-workforce",
93 label: "employment or workforce decision",
94 fragments: &[
95 "employment:hiring",
96 "employment:screen",
97 "employment:rank",
98 "recruitment:decision",
99 "workforce:decision",
100 "worker-management",
101 "worker:performance",
102 "worker:discipline",
103 ],
104 },
105 ProhibitedIntentRule {
106 rule_id: "eu-ai-act-education-admission-grading",
107 label: "education admission or grading decision",
108 fragments: &[
109 "education:admission",
110 "education:grading",
111 "education:grade",
112 "student:admission",
113 "student:assess",
114 "exam-grading",
115 ],
116 },
117 ProhibitedIntentRule {
118 rule_id: "eu-ai-act-credit-essential-services",
119 label: "credit or essential-services decision",
120 fragments: &[
121 "credit-scoring",
122 "credit:score",
123 "credit:decision",
124 "essential-services",
125 "benefits:eligibility",
126 "public-benefit:eligibility",
127 ],
128 },
129 ProhibitedIntentRule {
130 rule_id: "eu-ai-act-law-enforcement-border-justice",
131 label: "law enforcement, border, justice or democratic-process decision",
132 fragments: &[
133 "law-enforcement",
134 "law_enforcement",
135 "migration:decision",
136 "asylum:decision",
137 "border-control",
138 "justice:decision",
139 "democratic-process",
140 "election:decision",
141 ],
142 },
143 ProhibitedIntentRule {
144 rule_id: "eu-ai-act-healthcare-critical-infrastructure",
145 label: "healthcare or critical-infrastructure safety decision",
146 fragments: &[
147 "healthcare:decision",
148 "medical:diagnosis",
149 "medical:triage",
150 "clinical:decision",
151 "critical-infrastructure",
152 "grid:stabilize",
153 "hospital:surge-capacity",
154 ],
155 },
156 ProhibitedIntentRule {
157 rule_id: "eu-ai-act-physical-world-control",
158 label: "physical-world control",
159 fragments: &[
160 "robotics:control",
161 "robotics:fleet",
162 "drone:control",
163 "drone:search",
164 "iot:actuate",
165 "physical-world",
166 "system_control",
167 ],
168 },
169];
170
171const TRANSPARENCY_FRAGMENTS: &[&str] = &[
172 "chatbot",
173 "ai-assistant",
174 "synthetic-media",
175 "deepfake:labelled",
176 "content:generate-public",
177 "creative:generate",
178];
179
180pub fn classify_intent(intent: &str) -> IntentRiskCategory {
181 let normalized = intent.trim().to_ascii_lowercase();
182 if PROHIBITED_INTENT_RULES
183 .iter()
184 .any(|r| r.fragments.iter().any(|f| normalized.contains(f)))
185 {
186 return IntentRiskCategory::Prohibited;
187 }
188 if HIGH_RISK_INTENT_RULES
189 .iter()
190 .any(|r| r.fragments.iter().any(|f| normalized.contains(f)))
191 {
192 return IntentRiskCategory::HighRisk;
193 }
194 if TRANSPARENCY_FRAGMENTS
195 .iter()
196 .any(|f| normalized.contains(f))
197 {
198 return IntentRiskCategory::TransparencyRisk;
199 }
200 IntentRiskCategory::MinimalOrGeneral
201}
202
203pub fn prohibited_intent_reason(intent: &str) -> Option<String> {
204 let normalized = intent.trim().to_ascii_lowercase();
205 for rule in PROHIBITED_INTENT_RULES {
206 if rule
207 .fragments
208 .iter()
209 .any(|fragment| normalized.contains(fragment))
210 {
211 return Some(format!("{} ({})", rule.label, rule.rule_id));
212 }
213 }
214 None
215}
216
217pub fn ensure_intent_allowed(intent: &str) -> Result<()> {
218 let category = classify_intent(intent);
219 let normalized = intent.trim().to_ascii_lowercase();
220 let rule = PROHIBITED_INTENT_RULES
221 .iter()
222 .chain(HIGH_RISK_INTENT_RULES.iter())
223 .find(|r| r.fragments.iter().any(|f| normalized.contains(f)));
224 if matches!(
225 category,
226 IntentRiskCategory::Prohibited | IntentRiskCategory::HighRisk
227 ) {
228 let reason = rule
229 .map(|r| format!("{} ({})", r.label, r.rule_id))
230 .unwrap_or_else(|| "restricted intent".into());
231 return Err(IicpError::PolicyRefused {
232 code: POLICY_REFUSAL_CODE.into(),
233 message: format!(
234 "Intent refused by IICP client policy before discovery/routing: {reason} [{category:?}]. Use an explicit private, documented, human-reviewed compliance path outside the public mesh for restricted/high-risk workflows."
235 ),
236 });
237 }
238 Ok(())
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn refuses_prohibited_intent() {
247 let err = ensure_intent_allowed("urn:iicp:intent:social-scoring:score:v1").unwrap_err();
248 assert!(matches!(err, IicpError::PolicyRefused { .. }));
249 }
250
251 #[test]
252 fn allows_normal_chat_intent() {
253 ensure_intent_allowed("urn:iicp:intent:llm:chat:v1").unwrap();
254 }
255}