pub struct Message {
pub schema_version: String,
pub role: Role,
pub content: Vec<ContentPart>,
pub channel: Option<Channel>,
}Expand description
Canonical CMF message representing a single turn in a conversation.
All content is carried as typed ContentPart variants. Extensions (identity, security, HTTP, agent context) are passed separately to handlers — not inside the message.
Mirrors the Python Message in cpex/framework/cmf/message.py.
Fields§
§schema_version: StringMessage schema version.
role: RoleWho is speaking.
content: Vec<ContentPart>List of typed content parts (multimodal).
channel: Option<Channel>Optional output classification.
Implementations§
Source§impl Message
impl Message
Sourcepub fn with_content(role: Role, content: Vec<ContentPart>) -> Self
pub fn with_content(role: Role, content: Vec<ContentPart>) -> Self
Create a message from an arbitrary list of typed content
parts. The schema version is set from SCHEMA_VERSION —
callers never hardcode it. Use this when the content isn’t a
single text blob (tool calls, prompt requests, resource refs,
multimodal mixes).
Sourcepub fn get_text_content(&self) -> String
pub fn get_text_content(&self) -> String
Extract all text content from the message.
Concatenates text from all Text content parts.
Sourcepub fn get_thinking_content(&self) -> Option<String>
pub fn get_thinking_content(&self) -> Option<String>
Extract thinking/reasoning content if present.
Sourcepub fn get_tool_calls(&self) -> Vec<&ToolCall>
pub fn get_tool_calls(&self) -> Vec<&ToolCall>
Get all tool calls in this message.
Examples found in repository?
45 async fn handle(
46 &self,
47 payload: &MessagePayload,
48 extensions: &Extensions,
49 _ctx: &mut PluginContext,
50 ) -> PluginResult<MessagePayload> {
51 // Determine if this is pre or post invoke based on message content
52 let is_result = payload.message.is_tool_result();
53
54 if is_result {
55 // POST-INVOKE: verify the tool result came from an authorized call
56 let tool_name = payload
57 .message
58 .get_tool_results()
59 .first()
60 .map(|tr| tr.tool_name.as_str())
61 .unwrap_or("unknown");
62 println!(
63 " [identity-checker] POST-INVOKE: verifying result from '{}'",
64 tool_name
65 );
66
67 if let Some(ref security) = extensions.security {
68 if let Some(ref subject) = security.subject {
69 println!(
70 " [identity-checker] Result authorized for subject: {:?}",
71 subject.id
72 );
73 }
74 }
75 println!(" [identity-checker] POST-INVOKE ALLOWED");
76 } else {
77 // PRE-INVOKE: check caller identity and roles
78 let tool_name = payload
79 .message
80 .get_tool_calls()
81 .first()
82 .map(|tc| tc.name.as_str())
83 .unwrap_or("unknown");
84 println!(
85 " [identity-checker] PRE-INVOKE: checking identity for '{}'",
86 tool_name
87 );
88
89 if let Some(ref security) = extensions.security {
90 let labels: Vec<&String> = security.labels.iter().collect();
91 println!(" [identity-checker] Security labels: {:?}", labels);
92
93 if let Some(ref subject) = security.subject {
94 println!(
95 " [identity-checker] Subject: {:?}, Roles: {:?}",
96 subject.id,
97 subject.roles.iter().collect::<Vec<_>>()
98 );
99
100 if security.has_label("PII") && !subject.roles.contains("hr_admin") {
101 return PluginResult::deny(PluginViolation::new(
102 "insufficient_role",
103 format!("Tool '{}' requires 'hr_admin' role for PII data", tool_name),
104 ));
105 }
106 }
107 }
108
109 if extensions.http.is_some() {
110 println!(" [identity-checker] WARNING: HTTP visible (unexpected!)");
111 } else {
112 println!(" [identity-checker] HTTP: not visible (correct — no read_headers)");
113 }
114 println!(" [identity-checker] PRE-INVOKE ALLOWED");
115 }
116
117 PluginResult::allow()
118 }
119}
120
121// ---------------------------------------------------------------------------
122// Plugin: HeaderInjector
123// Has read_headers, write_headers, append_labels capabilities.
124// Uses COW to add a security label and inject a header.
125// ---------------------------------------------------------------------------
126
127struct HeaderInjector {
128 cfg: PluginConfig,
129}
130
131#[async_trait]
132impl Plugin for HeaderInjector {
133 fn config(&self) -> &PluginConfig {
134 &self.cfg
135 }
136}
137
138impl HookHandler<CmfHook> for HeaderInjector {
139 async fn handle(
140 &self,
141 _payload: &MessagePayload,
142 extensions: &Extensions,
143 _ctx: &mut PluginContext,
144 ) -> PluginResult<MessagePayload> {
145 // Can see HTTP (has read_headers)
146 if let Some(ref http) = extensions.http {
147 println!(
148 " [header-injector] HTTP headers visible: {:?}",
149 http.request_headers
150 );
151 }
152
153 // Can NOT see security subject (no read_subject)
154 if let Some(ref security) = extensions.security {
155 if security.subject.is_some() {
156 println!(" [header-injector] WARNING: Subject visible (unexpected!)");
157 } else {
158 println!(" [header-injector] Security subject: not visible (no read_subject)");
159 }
160 }
161
162 // COW copy to modify — tokens propagate from the executor
163 let mut modified = extensions.cow_copy();
164
165 // Add a label via MonotonicSet (has append_labels)
166 if modified.labels_write_token.is_some() {
167 modified.security.as_mut().unwrap().add_label("PROCESSED");
168 println!(" [header-injector] Added label 'PROCESSED'");
169 }
170
171 // Inject a header via Guarded (has write_headers)
172 if let Some(ref token) = modified.http_write_token {
173 modified
174 .http
175 .as_mut()
176 .unwrap()
177 .write(token)
178 .set_header("X-Processed-By", "header-injector");
179 println!(" [header-injector] Injected header 'X-Processed-By'");
180 }
181
182 PluginResult::modify_extensions(modified)
183 }
184}
185
186// ---------------------------------------------------------------------------
187// Plugin: AuditLogger
188// Has read_headers, read_security, read_labels capabilities.
189// Read-only — just logs what it can see.
190// ---------------------------------------------------------------------------
191
192struct AuditLogger {
193 cfg: PluginConfig,
194}
195
196#[async_trait]
197impl Plugin for AuditLogger {
198 fn config(&self) -> &PluginConfig {
199 &self.cfg
200 }
201}
202
203impl HookHandler<CmfHook> for AuditLogger {
204 async fn handle(
205 &self,
206 payload: &MessagePayload,
207 extensions: &Extensions,
208 _ctx: &mut PluginContext,
209 ) -> PluginResult<MessagePayload> {
210 let is_result = payload.message.is_tool_result();
211 let phase = if is_result { "POST" } else { "PRE" };
212
213 let tool_name = if is_result {
214 payload
215 .message
216 .get_tool_results()
217 .first()
218 .map(|tr| tr.tool_name.as_str())
219 .unwrap_or("unknown")
220 } else {
221 payload
222 .message
223 .get_tool_calls()
224 .first()
225 .map(|tc| tc.name.as_str())
226 .unwrap_or("unknown")
227 };
228
229 print!(" [audit-logger] AUDIT[{}]: tool='{}' ", phase, tool_name);
230
231 if let Some(ref security) = extensions.security {
232 let labels: Vec<&String> = security.labels.iter().collect();
233 print!("labels={:?} ", labels);
234 }
235
236 if let Some(ref http) = extensions.http {
237 if let Some(req_id) = http.get_header("X-Request-ID") {
238 print!("request_id='{}' ", req_id);
239 }
240 }
241
242 if is_result {
243 let is_error = payload
244 .message
245 .get_tool_results()
246 .first()
247 .map(|tr| tr.is_error)
248 .unwrap_or(false);
249 print!("error={} ", is_error);
250 }
251
252 println!();
253 PluginResult::allow()
254 }Sourcepub fn get_tool_results(&self) -> Vec<&ToolResult>
pub fn get_tool_results(&self) -> Vec<&ToolResult>
Get all tool results in this message.
Examples found in repository?
45 async fn handle(
46 &self,
47 payload: &MessagePayload,
48 extensions: &Extensions,
49 _ctx: &mut PluginContext,
50 ) -> PluginResult<MessagePayload> {
51 // Determine if this is pre or post invoke based on message content
52 let is_result = payload.message.is_tool_result();
53
54 if is_result {
55 // POST-INVOKE: verify the tool result came from an authorized call
56 let tool_name = payload
57 .message
58 .get_tool_results()
59 .first()
60 .map(|tr| tr.tool_name.as_str())
61 .unwrap_or("unknown");
62 println!(
63 " [identity-checker] POST-INVOKE: verifying result from '{}'",
64 tool_name
65 );
66
67 if let Some(ref security) = extensions.security {
68 if let Some(ref subject) = security.subject {
69 println!(
70 " [identity-checker] Result authorized for subject: {:?}",
71 subject.id
72 );
73 }
74 }
75 println!(" [identity-checker] POST-INVOKE ALLOWED");
76 } else {
77 // PRE-INVOKE: check caller identity and roles
78 let tool_name = payload
79 .message
80 .get_tool_calls()
81 .first()
82 .map(|tc| tc.name.as_str())
83 .unwrap_or("unknown");
84 println!(
85 " [identity-checker] PRE-INVOKE: checking identity for '{}'",
86 tool_name
87 );
88
89 if let Some(ref security) = extensions.security {
90 let labels: Vec<&String> = security.labels.iter().collect();
91 println!(" [identity-checker] Security labels: {:?}", labels);
92
93 if let Some(ref subject) = security.subject {
94 println!(
95 " [identity-checker] Subject: {:?}, Roles: {:?}",
96 subject.id,
97 subject.roles.iter().collect::<Vec<_>>()
98 );
99
100 if security.has_label("PII") && !subject.roles.contains("hr_admin") {
101 return PluginResult::deny(PluginViolation::new(
102 "insufficient_role",
103 format!("Tool '{}' requires 'hr_admin' role for PII data", tool_name),
104 ));
105 }
106 }
107 }
108
109 if extensions.http.is_some() {
110 println!(" [identity-checker] WARNING: HTTP visible (unexpected!)");
111 } else {
112 println!(" [identity-checker] HTTP: not visible (correct — no read_headers)");
113 }
114 println!(" [identity-checker] PRE-INVOKE ALLOWED");
115 }
116
117 PluginResult::allow()
118 }
119}
120
121// ---------------------------------------------------------------------------
122// Plugin: HeaderInjector
123// Has read_headers, write_headers, append_labels capabilities.
124// Uses COW to add a security label and inject a header.
125// ---------------------------------------------------------------------------
126
127struct HeaderInjector {
128 cfg: PluginConfig,
129}
130
131#[async_trait]
132impl Plugin for HeaderInjector {
133 fn config(&self) -> &PluginConfig {
134 &self.cfg
135 }
136}
137
138impl HookHandler<CmfHook> for HeaderInjector {
139 async fn handle(
140 &self,
141 _payload: &MessagePayload,
142 extensions: &Extensions,
143 _ctx: &mut PluginContext,
144 ) -> PluginResult<MessagePayload> {
145 // Can see HTTP (has read_headers)
146 if let Some(ref http) = extensions.http {
147 println!(
148 " [header-injector] HTTP headers visible: {:?}",
149 http.request_headers
150 );
151 }
152
153 // Can NOT see security subject (no read_subject)
154 if let Some(ref security) = extensions.security {
155 if security.subject.is_some() {
156 println!(" [header-injector] WARNING: Subject visible (unexpected!)");
157 } else {
158 println!(" [header-injector] Security subject: not visible (no read_subject)");
159 }
160 }
161
162 // COW copy to modify — tokens propagate from the executor
163 let mut modified = extensions.cow_copy();
164
165 // Add a label via MonotonicSet (has append_labels)
166 if modified.labels_write_token.is_some() {
167 modified.security.as_mut().unwrap().add_label("PROCESSED");
168 println!(" [header-injector] Added label 'PROCESSED'");
169 }
170
171 // Inject a header via Guarded (has write_headers)
172 if let Some(ref token) = modified.http_write_token {
173 modified
174 .http
175 .as_mut()
176 .unwrap()
177 .write(token)
178 .set_header("X-Processed-By", "header-injector");
179 println!(" [header-injector] Injected header 'X-Processed-By'");
180 }
181
182 PluginResult::modify_extensions(modified)
183 }
184}
185
186// ---------------------------------------------------------------------------
187// Plugin: AuditLogger
188// Has read_headers, read_security, read_labels capabilities.
189// Read-only — just logs what it can see.
190// ---------------------------------------------------------------------------
191
192struct AuditLogger {
193 cfg: PluginConfig,
194}
195
196#[async_trait]
197impl Plugin for AuditLogger {
198 fn config(&self) -> &PluginConfig {
199 &self.cfg
200 }
201}
202
203impl HookHandler<CmfHook> for AuditLogger {
204 async fn handle(
205 &self,
206 payload: &MessagePayload,
207 extensions: &Extensions,
208 _ctx: &mut PluginContext,
209 ) -> PluginResult<MessagePayload> {
210 let is_result = payload.message.is_tool_result();
211 let phase = if is_result { "POST" } else { "PRE" };
212
213 let tool_name = if is_result {
214 payload
215 .message
216 .get_tool_results()
217 .first()
218 .map(|tr| tr.tool_name.as_str())
219 .unwrap_or("unknown")
220 } else {
221 payload
222 .message
223 .get_tool_calls()
224 .first()
225 .map(|tc| tc.name.as_str())
226 .unwrap_or("unknown")
227 };
228
229 print!(" [audit-logger] AUDIT[{}]: tool='{}' ", phase, tool_name);
230
231 if let Some(ref security) = extensions.security {
232 let labels: Vec<&String> = security.labels.iter().collect();
233 print!("labels={:?} ", labels);
234 }
235
236 if let Some(ref http) = extensions.http {
237 if let Some(req_id) = http.get_header("X-Request-ID") {
238 print!("request_id='{}' ", req_id);
239 }
240 }
241
242 if is_result {
243 let is_error = payload
244 .message
245 .get_tool_results()
246 .first()
247 .map(|tr| tr.is_error)
248 .unwrap_or(false);
249 print!("error={} ", is_error);
250 }
251
252 println!();
253 PluginResult::allow()
254 }Sourcepub fn is_tool_call(&self) -> bool
pub fn is_tool_call(&self) -> bool
Whether this message contains any tool calls.
Sourcepub fn is_tool_result(&self) -> bool
pub fn is_tool_result(&self) -> bool
Whether this message contains any tool results.
Examples found in repository?
45 async fn handle(
46 &self,
47 payload: &MessagePayload,
48 extensions: &Extensions,
49 _ctx: &mut PluginContext,
50 ) -> PluginResult<MessagePayload> {
51 // Determine if this is pre or post invoke based on message content
52 let is_result = payload.message.is_tool_result();
53
54 if is_result {
55 // POST-INVOKE: verify the tool result came from an authorized call
56 let tool_name = payload
57 .message
58 .get_tool_results()
59 .first()
60 .map(|tr| tr.tool_name.as_str())
61 .unwrap_or("unknown");
62 println!(
63 " [identity-checker] POST-INVOKE: verifying result from '{}'",
64 tool_name
65 );
66
67 if let Some(ref security) = extensions.security {
68 if let Some(ref subject) = security.subject {
69 println!(
70 " [identity-checker] Result authorized for subject: {:?}",
71 subject.id
72 );
73 }
74 }
75 println!(" [identity-checker] POST-INVOKE ALLOWED");
76 } else {
77 // PRE-INVOKE: check caller identity and roles
78 let tool_name = payload
79 .message
80 .get_tool_calls()
81 .first()
82 .map(|tc| tc.name.as_str())
83 .unwrap_or("unknown");
84 println!(
85 " [identity-checker] PRE-INVOKE: checking identity for '{}'",
86 tool_name
87 );
88
89 if let Some(ref security) = extensions.security {
90 let labels: Vec<&String> = security.labels.iter().collect();
91 println!(" [identity-checker] Security labels: {:?}", labels);
92
93 if let Some(ref subject) = security.subject {
94 println!(
95 " [identity-checker] Subject: {:?}, Roles: {:?}",
96 subject.id,
97 subject.roles.iter().collect::<Vec<_>>()
98 );
99
100 if security.has_label("PII") && !subject.roles.contains("hr_admin") {
101 return PluginResult::deny(PluginViolation::new(
102 "insufficient_role",
103 format!("Tool '{}' requires 'hr_admin' role for PII data", tool_name),
104 ));
105 }
106 }
107 }
108
109 if extensions.http.is_some() {
110 println!(" [identity-checker] WARNING: HTTP visible (unexpected!)");
111 } else {
112 println!(" [identity-checker] HTTP: not visible (correct — no read_headers)");
113 }
114 println!(" [identity-checker] PRE-INVOKE ALLOWED");
115 }
116
117 PluginResult::allow()
118 }
119}
120
121// ---------------------------------------------------------------------------
122// Plugin: HeaderInjector
123// Has read_headers, write_headers, append_labels capabilities.
124// Uses COW to add a security label and inject a header.
125// ---------------------------------------------------------------------------
126
127struct HeaderInjector {
128 cfg: PluginConfig,
129}
130
131#[async_trait]
132impl Plugin for HeaderInjector {
133 fn config(&self) -> &PluginConfig {
134 &self.cfg
135 }
136}
137
138impl HookHandler<CmfHook> for HeaderInjector {
139 async fn handle(
140 &self,
141 _payload: &MessagePayload,
142 extensions: &Extensions,
143 _ctx: &mut PluginContext,
144 ) -> PluginResult<MessagePayload> {
145 // Can see HTTP (has read_headers)
146 if let Some(ref http) = extensions.http {
147 println!(
148 " [header-injector] HTTP headers visible: {:?}",
149 http.request_headers
150 );
151 }
152
153 // Can NOT see security subject (no read_subject)
154 if let Some(ref security) = extensions.security {
155 if security.subject.is_some() {
156 println!(" [header-injector] WARNING: Subject visible (unexpected!)");
157 } else {
158 println!(" [header-injector] Security subject: not visible (no read_subject)");
159 }
160 }
161
162 // COW copy to modify — tokens propagate from the executor
163 let mut modified = extensions.cow_copy();
164
165 // Add a label via MonotonicSet (has append_labels)
166 if modified.labels_write_token.is_some() {
167 modified.security.as_mut().unwrap().add_label("PROCESSED");
168 println!(" [header-injector] Added label 'PROCESSED'");
169 }
170
171 // Inject a header via Guarded (has write_headers)
172 if let Some(ref token) = modified.http_write_token {
173 modified
174 .http
175 .as_mut()
176 .unwrap()
177 .write(token)
178 .set_header("X-Processed-By", "header-injector");
179 println!(" [header-injector] Injected header 'X-Processed-By'");
180 }
181
182 PluginResult::modify_extensions(modified)
183 }
184}
185
186// ---------------------------------------------------------------------------
187// Plugin: AuditLogger
188// Has read_headers, read_security, read_labels capabilities.
189// Read-only — just logs what it can see.
190// ---------------------------------------------------------------------------
191
192struct AuditLogger {
193 cfg: PluginConfig,
194}
195
196#[async_trait]
197impl Plugin for AuditLogger {
198 fn config(&self) -> &PluginConfig {
199 &self.cfg
200 }
201}
202
203impl HookHandler<CmfHook> for AuditLogger {
204 async fn handle(
205 &self,
206 payload: &MessagePayload,
207 extensions: &Extensions,
208 _ctx: &mut PluginContext,
209 ) -> PluginResult<MessagePayload> {
210 let is_result = payload.message.is_tool_result();
211 let phase = if is_result { "POST" } else { "PRE" };
212
213 let tool_name = if is_result {
214 payload
215 .message
216 .get_tool_results()
217 .first()
218 .map(|tr| tr.tool_name.as_str())
219 .unwrap_or("unknown")
220 } else {
221 payload
222 .message
223 .get_tool_calls()
224 .first()
225 .map(|tc| tc.name.as_str())
226 .unwrap_or("unknown")
227 };
228
229 print!(" [audit-logger] AUDIT[{}]: tool='{}' ", phase, tool_name);
230
231 if let Some(ref security) = extensions.security {
232 let labels: Vec<&String> = security.labels.iter().collect();
233 print!("labels={:?} ", labels);
234 }
235
236 if let Some(ref http) = extensions.http {
237 if let Some(req_id) = http.get_header("X-Request-ID") {
238 print!("request_id='{}' ", req_id);
239 }
240 }
241
242 if is_result {
243 let is_error = payload
244 .message
245 .get_tool_results()
246 .first()
247 .map(|tr| tr.is_error)
248 .unwrap_or(false);
249 print!("error={} ", is_error);
250 }
251
252 println!();
253 PluginResult::allow()
254 }Sourcepub fn get_resources(&self) -> Vec<&Resource>
pub fn get_resources(&self) -> Vec<&Resource>
Get all embedded resources in this message.
Sourcepub fn get_resource_refs(&self) -> Vec<&ResourceReference>
pub fn get_resource_refs(&self) -> Vec<&ResourceReference>
Get all resource references in this message.
Sourcepub fn get_all_resource_uris(&self) -> Vec<&str>
pub fn get_all_resource_uris(&self) -> Vec<&str>
Get all resource URIs (both embedded and references).
Sourcepub fn has_resources(&self) -> bool
pub fn has_resources(&self) -> bool
Whether this message contains any resources or resource references.
Sourcepub fn get_prompt_requests(&self) -> Vec<&PromptRequest>
pub fn get_prompt_requests(&self) -> Vec<&PromptRequest>
Get all prompt requests in this message.
Sourcepub fn get_prompt_results(&self) -> Vec<&PromptResult>
pub fn get_prompt_results(&self) -> Vec<&PromptResult>
Get all prompt results in this message.
Source§impl Message
impl Message
Sourcepub fn iter_views<'a>(
&'a self,
hook: Option<&'a str>,
extensions: Option<&'a Extensions>,
) -> impl Iterator<Item = MessageView<'a>>
pub fn iter_views<'a>( &'a self, hook: Option<&'a str>, extensions: Option<&'a Extensions>, ) -> impl Iterator<Item = MessageView<'a>>
Decompose this message into individually addressable MessageViews.
Yields one view per content part. Each view provides a uniform interface for policy evaluation regardless of content type.