1515use std:: os:: raw:: { c_char, c_int} ;
1616use std:: panic:: { catch_unwind, AssertUnwindSafe } ;
1717use std:: ptr;
18- use std:: sync:: OnceLock ;
18+ use std:: sync:: { Arc , OnceLock } ;
1919use std:: time:: Duration ;
2020
2121use cpex_core:: context:: PluginContextTable ;
@@ -24,6 +24,9 @@ use cpex_core::extensions::Extensions;
2424use cpex_core:: hooks:: payload:: PluginPayload ;
2525use cpex_core:: manager:: PluginManager ;
2626
27+ // APL governance wiring — the `cpex_apl_install` extern "C" entry point.
28+ mod apl;
29+
2730// ---------------------------------------------------------------------------
2831// FFI Result Codes
2932// ---------------------------------------------------------------------------
@@ -90,7 +93,7 @@ pub const RC_PANIC: c_int = -7;
9093
9194/// FFI ABI version. Bump on breaking C-surface changes; see module
9295/// docs above for what counts as breaking.
93- pub const FFI_ABI_VERSION : u32 = 1 ;
96+ pub const FFI_ABI_VERSION : u32 = 2 ;
9497
9598/// Returns the FFI ABI version this `libcpex_ffi` was built with.
9699/// Language bindings call this at `init` and panic on mismatch
@@ -376,7 +379,11 @@ fn serialize_payload(payload: &dyn PluginPayload) -> Result<(u8, Vec<u8>), Strin
376379/// All managers share the process-singleton runtime returned by
377380/// `shared_runtime()` — see the `SHARED_RUNTIME` doc-comment for why.
378381pub struct CpexManagerInner {
379- pub manager : PluginManager ,
382+ /// Held as `Arc` so the APL config visitor — registered via
383+ /// `cpex_apl_install` — can keep a `Weak<PluginManager>` that upgrades
384+ /// during `load_config_yaml`. See `apl::cpex_apl_install` and
385+ /// `apl_cpex::register_apl`.
386+ pub manager : Arc < PluginManager > ,
380387}
381388
382389/// Opaque handle to a ContextTable (Rust-owned, not serialized).
@@ -475,9 +482,12 @@ pub unsafe extern "C" fn cpex_manager_new(
475482 // silently no-op.
476483 let _ = shared_runtime ( ) ;
477484
478- let manager = PluginManager :: default ( ) ;
485+ let manager = Arc :: new ( PluginManager :: default ( ) ) ;
479486
480- // Load config — factories must be registered separately via cpex_register_factory
487+ // Load config — factories must be registered separately via cpex_register_factory.
488+ // Note: this one-shot path uses `load_config` (no visitor walk), so APL is
489+ // NOT wired here. APL requires the cpex_manager_new_default →
490+ // cpex_apl_install → cpex_load_config flow.
481491 if let Err ( e) = manager. load_config ( cpex_config) {
482492 tracing:: error!( "cpex_manager_new: load_config failed: {}" , e) ;
483493 return ptr:: null_mut ( ) ;
@@ -492,7 +502,7 @@ pub unsafe extern "C" fn cpex_manager_new(
492502#[ no_mangle]
493503pub extern "C" fn cpex_manager_new_default ( ) -> * mut CpexManagerInner {
494504 let _ = shared_runtime ( ) ;
495- let manager = PluginManager :: default ( ) ;
505+ let manager = Arc :: new ( PluginManager :: default ( ) ) ;
496506 Box :: into_raw ( Box :: new ( CpexManagerInner { manager } ) )
497507}
498508
@@ -523,17 +533,22 @@ pub unsafe extern "C" fn cpex_load_config(
523533 None => return RC_INVALID_INPUT ,
524534 } ;
525535
526- let cpex_config = match cpex_core:: config:: parse_config ( yaml) {
527- Ok ( c) => c,
528- Err ( e) => {
529- tracing:: error!( "cpex_load_config: config parse failed: {}" , e) ;
530- return RC_PARSE_ERROR ;
531- }
532- } ;
536+ // Validate first (duplicate plugin names, route shape) — preserves the
537+ // RC_PARSE_ERROR contract. We discard the parsed value and hand the raw
538+ // YAML to `load_config_yaml`, which re-parses into both a typed
539+ // CpexConfig and a raw serde_yaml::Value so registered config visitors
540+ // (e.g. the APL visitor installed by cpex_apl_install) can walk the
541+ // `apl:` blocks and install per-route handlers. Plain `load_config`
542+ // does NOT run that visitor walk.
543+ if let Err ( e) = cpex_core:: config:: parse_config ( yaml) {
544+ tracing:: error!( "cpex_load_config: config parse failed: {}" , e) ;
545+ return RC_PARSE_ERROR ;
546+ }
533547
534- // load_config is sync (no .await), but we still wrap in catch_unwind
535- // so a panic in serde / config validation doesn't unwind across FFI.
536- let load_result = catch_unwind ( AssertUnwindSafe ( || inner. manager . load_config ( cpex_config) ) ) ;
548+ // load_config_yaml is sync (no .await), but we still wrap in catch_unwind
549+ // so a panic in serde / config validation / a visitor doesn't unwind
550+ // across FFI.
551+ let load_result = catch_unwind ( AssertUnwindSafe ( || inner. manager . load_config_yaml ( yaml) ) ) ;
537552 match load_result {
538553 Ok ( Ok ( ( ) ) ) => RC_OK ,
539554 Ok ( Err ( e) ) => {
@@ -1093,7 +1108,7 @@ mod tests {
10931108 // Touch the shared runtime so it's initialized; tests use it
10941109 // rather than a per-manager runtime.
10951110 let _ = shared_runtime ( ) ;
1096- let manager = cpex_core:: manager:: PluginManager :: default ( ) ;
1111+ let manager = Arc :: new ( cpex_core:: manager:: PluginManager :: default ( ) ) ;
10971112 Box :: into_raw ( Box :: new ( CpexManagerInner { manager } ) )
10981113 }
10991114
@@ -1354,4 +1369,53 @@ mod tests {
13541369 assert_eq ! ( cpex_is_initialized( ptr:: null( ) ) , 0 ) ;
13551370 }
13561371 }
1372+
1373+ #[ test]
1374+ fn cpex_apl_install_rejects_null_handle ( ) {
1375+ unsafe {
1376+ assert_eq ! ( crate :: apl:: cpex_apl_install( ptr:: null( ) ) , RC_INVALID_HANDLE ) ;
1377+ }
1378+ }
1379+
1380+ /// Full APL flow through the FFI surface: default manager →
1381+ /// cpex_apl_install (registers bundled factories + APL visitor) →
1382+ /// cpex_load_config over an `apl:`-annotated YAML using a bundled
1383+ /// plugin kind (`audit/logger`) → cpex_initialize. Proves the visitor
1384+ /// walk runs (load uses load_config_yaml) and the bundled factory is
1385+ /// reachable, so the plugin actually instantiates.
1386+ #[ test]
1387+ fn cpex_apl_install_then_load_apl_config_initializes ( ) {
1388+ const YAML : & str = r#"
1389+ plugins:
1390+ - name: auditor
1391+ kind: audit/logger
1392+ hooks: [cmf.tool_pre_invoke]
1393+ routes:
1394+ - tool: get_weather
1395+ apl:
1396+ policy:
1397+ - "plugin(auditor)"
1398+ "# ;
1399+ unsafe {
1400+ let mgr = build_test_manager ( ) ;
1401+
1402+ assert_eq ! ( crate :: apl:: cpex_apl_install( mgr) , RC_OK ) ;
1403+
1404+ let rc = cpex_load_config ( mgr, YAML . as_ptr ( ) as * const c_char , YAML . len ( ) as c_int ) ;
1405+ assert_eq ! ( rc, RC_OK , "load of APL config should succeed" ) ;
1406+
1407+ assert_eq ! ( cpex_initialize( mgr) , RC_OK ) ;
1408+
1409+ // The bundled `audit/logger` factory instantiated a plugin on
1410+ // cmf.tool_pre_invoke — proves cpex_apl_install wired the kind.
1411+ assert ! ( cpex_plugin_count( mgr) >= 1 ) ;
1412+ let hook = "cmf.tool_pre_invoke" ;
1413+ assert_eq ! (
1414+ cpex_has_hooks_for( mgr, hook. as_ptr( ) as * const c_char, hook. len( ) as c_int) ,
1415+ 1 ,
1416+ ) ;
1417+
1418+ cpex_shutdown ( mgr) ;
1419+ }
1420+ }
13571421}
0 commit comments