@@ -48,44 +48,56 @@ impl HookHandler<CmfHook> for IdentityChecker {
4848 extensions : & Extensions ,
4949 _ctx : & mut PluginContext ,
5050 ) -> PluginResult < MessagePayload > {
51- let tool_name = payload. message . get_tool_calls ( )
52- . first ( )
53- . map ( |tc| tc. name . as_str ( ) )
54- . unwrap_or ( "unknown" ) ;
55-
56- // Check security labels (capability: read_labels)
57- if let Some ( ref security) = extensions. security {
58- let labels: Vec < & String > = security. labels . iter ( ) . collect ( ) ;
59- println ! ( " [identity-checker] Security labels visible: {:?}" , labels) ;
60- println ! ( " [identity-checker] Classification: {:?}" , security. classification) ;
61-
62- // Check subject (capability: read_subject, read_roles)
63- if let Some ( ref subject) = security. subject {
64- println ! ( " [identity-checker] Subject: {:?}" , subject. id) ;
65- let roles: Vec < & String > = subject. roles . iter ( ) . collect ( ) ;
66- println ! ( " [identity-checker] Roles: {:?}" , roles) ;
67-
68- if security. has_label ( "PII" ) && !subject. roles . contains ( "hr_admin" ) {
69- return PluginResult :: deny ( PluginViolation :: new (
70- "insufficient_role" ,
71- format ! ( "Tool '{}' requires 'hr_admin' role for PII data" , tool_name) ,
72- ) ) ;
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. message . get_tool_results ( )
57+ . first ( )
58+ . map ( |tr| tr. tool_name . as_str ( ) )
59+ . unwrap_or ( "unknown" ) ;
60+ println ! ( " [identity-checker] POST-INVOKE: verifying result from '{}'" , tool_name) ;
61+
62+ if let Some ( ref security) = extensions. security {
63+ if let Some ( ref subject) = security. subject {
64+ println ! ( " [identity-checker] Result authorized for subject: {:?}" , subject. id) ;
7365 }
74- } else {
75- println ! ( " [identity-checker] No subject visible (missing capability)" ) ;
7666 }
67+ println ! ( " [identity-checker] POST-INVOKE ALLOWED" ) ;
7768 } else {
78- println ! ( " [identity-checker] No security extension visible" ) ;
79- }
69+ // PRE-INVOKE: check caller identity and roles
70+ let tool_name = payload. message . get_tool_calls ( )
71+ . first ( )
72+ . map ( |tc| tc. name . as_str ( ) )
73+ . unwrap_or ( "unknown" ) ;
74+ println ! ( " [identity-checker] PRE-INVOKE: checking identity for '{}'" , tool_name) ;
75+
76+ if let Some ( ref security) = extensions. security {
77+ let labels: Vec < & String > = security. labels . iter ( ) . collect ( ) ;
78+ println ! ( " [identity-checker] Security labels: {:?}" , labels) ;
79+
80+ if let Some ( ref subject) = security. subject {
81+ println ! ( " [identity-checker] Subject: {:?}, Roles: {:?}" ,
82+ subject. id, subject. roles. iter( ) . collect:: <Vec <_>>( ) ) ;
83+
84+ if security. has_label ( "PII" ) && !subject. roles . contains ( "hr_admin" ) {
85+ return PluginResult :: deny ( PluginViolation :: new (
86+ "insufficient_role" ,
87+ format ! ( "Tool '{}' requires 'hr_admin' role for PII data" , tool_name) ,
88+ ) ) ;
89+ }
90+ }
91+ }
8092
81- // Check HTTP (should NOT be visible — no read_headers capability)
82- if extensions. http . is_some ( ) {
83- println ! ( " [identity-checker] WARNING: HTTP visible (unexpected!)" ) ;
84- } else {
85- println ! ( " [identity-checker] HTTP: not visible (no read_headers capability)" ) ;
93+ if extensions. http . is_some ( ) {
94+ println ! ( " [identity-checker] WARNING: HTTP visible (unexpected!)" ) ;
95+ } else {
96+ println ! ( " [identity-checker] HTTP: not visible (correct — no read_headers)" ) ;
97+ }
98+ println ! ( " [identity-checker] PRE-INVOKE ALLOWED" ) ;
8699 }
87100
88- println ! ( " [identity-checker] ALLOWED: tool '{}' for authorized user" , tool_name) ;
89101 PluginResult :: allow ( )
90102 }
91103}
@@ -114,7 +126,7 @@ impl HookHandler<CmfHook> for HeaderInjector {
114126 ) -> PluginResult < MessagePayload > {
115127 // Can see HTTP (has read_headers)
116128 if let Some ( ref http) = extensions. http {
117- println ! ( " [header-injector] HTTP headers visible: {:?}" , http. read( ) . headers ) ;
129+ println ! ( " [header-injector] HTTP headers visible: {:?}" , http. read( ) . request_headers ) ;
118130 }
119131
120132 // Can NOT see security subject (no read_subject)
@@ -167,12 +179,22 @@ impl HookHandler<CmfHook> for AuditLogger {
167179 extensions : & Extensions ,
168180 _ctx : & mut PluginContext ,
169181 ) -> PluginResult < MessagePayload > {
170- let tool_name = payload. message . get_tool_calls ( )
171- . first ( )
172- . map ( |tc| tc. name . as_str ( ) )
173- . unwrap_or ( "unknown" ) ;
182+ let is_result = payload. message . is_tool_result ( ) ;
183+ let phase = if is_result { "POST" } else { "PRE" } ;
184+
185+ let tool_name = if is_result {
186+ payload. message . get_tool_results ( )
187+ . first ( )
188+ . map ( |tr| tr. tool_name . as_str ( ) )
189+ . unwrap_or ( "unknown" )
190+ } else {
191+ payload. message . get_tool_calls ( )
192+ . first ( )
193+ . map ( |tc| tc. name . as_str ( ) )
194+ . unwrap_or ( "unknown" )
195+ } ;
174196
175- print ! ( " [audit-logger] AUDIT: tool='{}' " , tool_name) ;
197+ print ! ( " [audit-logger] AUDIT[{}] : tool='{}' " , phase , tool_name) ;
176198
177199 if let Some ( ref security) = extensions. security {
178200 let labels: Vec < & String > = security. labels . iter ( ) . collect ( ) ;
@@ -185,8 +207,12 @@ impl HookHandler<CmfHook> for AuditLogger {
185207 }
186208 }
187209
188- if let Some ( ref meta) = extensions. meta {
189- print ! ( "entity='{:?}' " , meta. entity_name) ;
210+ if is_result {
211+ let is_error = payload. message . get_tool_results ( )
212+ . first ( )
213+ . map ( |tr| tr. is_error )
214+ . unwrap_or ( false ) ;
215+ print ! ( "error={} " , is_error) ;
190216 }
191217
192218 println ! ( ) ;
@@ -205,7 +231,8 @@ impl PluginFactory for IdentityCheckerFactory {
205231 Ok ( PluginInstance {
206232 plugin : plugin. clone ( ) ,
207233 handlers : vec ! [
208- ( "cmf.tool_pre_invoke" , Arc :: new( TypedHandlerAdapter :: <CmfHook , _>:: new( plugin) ) ) ,
234+ ( "cmf.tool_pre_invoke" , Arc :: new( TypedHandlerAdapter :: <CmfHook , _>:: new( plugin. clone( ) ) ) ) ,
235+ ( "cmf.tool_post_invoke" , Arc :: new( TypedHandlerAdapter :: <CmfHook , _>:: new( plugin) ) ) ,
209236 ] ,
210237 } )
211238 }
@@ -231,7 +258,8 @@ impl PluginFactory for AuditLoggerFactory {
231258 Ok ( PluginInstance {
232259 plugin : plugin. clone ( ) ,
233260 handlers : vec ! [
234- ( "cmf.tool_pre_invoke" , Arc :: new( TypedHandlerAdapter :: <CmfHook , _>:: new( plugin) ) ) ,
261+ ( "cmf.tool_pre_invoke" , Arc :: new( TypedHandlerAdapter :: <CmfHook , _>:: new( plugin. clone( ) ) ) ) ,
262+ ( "cmf.tool_post_invoke" , Arc :: new( TypedHandlerAdapter :: <CmfHook , _>:: new( plugin) ) ) ,
235263 ] ,
236264 } )
237265 }
@@ -262,7 +290,7 @@ async fn main() {
262290 // --- Build CMF Message ---
263291 let payload = MessagePayload {
264292 message : Message {
265- schema_version : "2.0" . into ( ) ,
293+ schema_version : cpex_core :: cmf :: constants :: SCHEMA_VERSION . into ( ) ,
266294 role : Role :: Assistant ,
267295 content : vec ! [
268296 ContentPart :: Text { text: "Looking up compensation." . into( ) } ,
@@ -313,27 +341,95 @@ async fn main() {
313341 ..Default :: default ( )
314342 } ;
315343
316- // --- Invoke ---
317- println ! ( "--- Invoking cmf.tool_pre_invoke ---\n " ) ;
318- let boxed: Box < dyn cpex_core:: hooks:: PluginPayload > = Box :: new ( payload) ;
319- let ( result, bg) = mgr. invoke_by_name ( "cmf.tool_pre_invoke" , boxed, ext, None ) . await ;
344+ // --- Pre-invoke: type-safe dispatch via invoke_named ---
345+ println ! ( "=== Phase 1: cmf.tool_pre_invoke ===\n " ) ;
346+
347+ // invoke_named<CmfHook> gives compile-time payload type checking
348+ // while routing to the specific "cmf.tool_pre_invoke" hook name
349+ let ( pre_result, bg) = mgr. invoke_named :: < CmfHook > (
350+ "cmf.tool_pre_invoke" ,
351+ payload,
352+ ext,
353+ None , // first hook — no context table
354+ ) . await ;
320355
321356 println ! ( ) ;
322- if result . continue_processing {
323- println ! ( "Result : ALLOWED" ) ;
324- if let Some ( ref modified_ext) = result . modified_extensions {
357+ if pre_result . continue_processing {
358+ println ! ( "Pre-invoke result : ALLOWED" ) ;
359+ if let Some ( ref modified_ext) = pre_result . modified_extensions {
325360 if let Some ( ref sec) = modified_ext. security {
326361 let labels: Vec < & String > = sec. labels . iter ( ) . collect ( ) ;
327- println ! ( "Final labels : {:?}" , labels) ;
362+ println ! ( " Labels after pre-invoke : {:?}" , labels) ;
328363 }
329364 if let Some ( ref http) = modified_ext. http {
330- println ! ( "Final headers : {:?}" , http. read( ) . headers ) ;
365+ println ! ( " Headers after pre-invoke : {:?}" , http. read( ) . request_headers ) ;
331366 }
332367 }
333368 } else {
334- println ! ( "Result: DENIED — {}" , result. violation. as_ref( ) . unwrap( ) . reason) ;
369+ println ! ( "Pre-invoke result: DENIED — {}" , pre_result. violation. as_ref( ) . unwrap( ) . reason) ;
370+ bg. wait_for_background_tasks ( ) . await ;
371+ println ! ( "\n === Demo complete ===" ) ;
372+ return ;
335373 }
336-
337374 bg. wait_for_background_tasks ( ) . await ;
375+
376+ // --- Simulate tool execution ---
377+ println ! ( "\n --- Tool 'get_compensation' executes... ---" ) ;
378+ println ! ( " Result: {{\" salary\" : 150000, \" currency\" : \" USD\" }}\n " ) ;
379+
380+ // --- Post-invoke: different CMF message with tool result ---
381+ println ! ( "=== Phase 2: cmf.tool_post_invoke ===\n " ) ;
382+
383+ let post_payload = MessagePayload {
384+ message : Message {
385+ schema_version : cpex_core:: cmf:: constants:: SCHEMA_VERSION . into ( ) ,
386+ role : Role :: Tool ,
387+ content : vec ! [
388+ ContentPart :: ToolResult {
389+ content: cpex_core:: cmf:: ToolResult {
390+ tool_call_id: "tc_001" . into( ) ,
391+ tool_name: "get_compensation" . into( ) ,
392+ content: serde_json:: json!( { "salary" : 150000 , "currency" : "USD" } ) ,
393+ is_error: false ,
394+ } ,
395+ } ,
396+ ] ,
397+ channel : None ,
398+ } ,
399+ } ;
400+
401+ // Build post-invoke extensions — carry forward any modifications
402+ // from pre-invoke via the context table
403+ let post_ext = pre_result. modified_extensions . unwrap_or_else ( || {
404+ // Rebuild if no modifications
405+ let mut security = SecurityExtension :: default ( ) ;
406+ security. add_label ( "PII" ) ;
407+ Extensions {
408+ security : Some ( security) ,
409+ meta : Some ( Arc :: new ( MetaExtension {
410+ entity_type : Some ( "tool" . into ( ) ) ,
411+ entity_name : Some ( "get_compensation" . into ( ) ) ,
412+ ..Default :: default ( )
413+ } ) ) ,
414+ ..Default :: default ( )
415+ }
416+ } ) ;
417+
418+ // Thread the context table from pre-invoke to preserve plugin state
419+ let ( post_result, post_bg) = mgr. invoke_named :: < CmfHook > (
420+ "cmf.tool_post_invoke" ,
421+ post_payload,
422+ post_ext,
423+ Some ( pre_result. context_table ) ,
424+ ) . await ;
425+
426+ println ! ( ) ;
427+ if post_result. continue_processing {
428+ println ! ( "Post-invoke result: ALLOWED" ) ;
429+ } else {
430+ println ! ( "Post-invoke result: DENIED — {}" , post_result. violation. as_ref( ) . unwrap( ) . reason) ;
431+ }
432+
433+ post_bg. wait_for_background_tasks ( ) . await ;
338434 println ! ( "\n === Demo complete ===" ) ;
339435}
0 commit comments