Skip to main content

cpex_core/elicitation/
hook.rs

1// Location: ./crates/cpex-core/src/elicitation/hook.rs
2// Copyright 2026
3// SPDX-License-Identifier: Apache-2.0
4// Authors: Teryl Taylor
5//
6// `ElicitationHook` — the `HookTypeDef` marker for the Elicitation hook
7// family. Plugins implement `HookHandler<ElicitationHook>`; the apl-cpex
8// bridge dispatches into it to drive a human-in-the-loop step (approval,
9// confirmation, step-up, …).
10//
11// Single hook name: `"elicit"`. The three touch-points (dispatch /
12// check / validate) are NOT separate hook names — they share one
13// payload shape and are distinguished by `ElicitationPayload::operation`.
14// This keeps registration and the plan trivial: one plugin, one entry,
15// resolved `name → entry` exactly like delegation's `token.delegate`.
16
17use crate::hooks::trait_def::PluginResult;
18
19use super::payload::ElicitationPayload;
20
21/// Hook name for Elicitation handlers.
22pub const HOOK_ELICIT: &str = "elicit";
23
24crate::define_hook! {
25    /// Elicitation hook — drives a human-in-the-loop step through a
26    /// channel plugin (Keycloak CIBA, Slack, in-band, …).
27    ///
28    /// **Payload** ([`ElicitationPayload`]) — unified input + accumulator.
29    /// The apl-cpex bridge sets the input fields (`operation`,
30    /// `elicitation_id`, `kind`, `from`, `purpose`, `scope`, `timeout`,
31    /// `channel`) and invokes the hook; the handler populates the output
32    /// fields (`id`, `status`, `outcome`, `approver`, `intent_id`,
33    /// `expires_at`, `valid`, `reason`) on a clone of the running payload
34    /// and returns it via [`PluginResult::modify_payload`]. Input fields
35    /// are private and read through accessors.
36    ///
37    /// **Result** ([`PluginResult<ElicitationPayload>`][PluginResult]) —
38    /// the executor's standard envelope. `modified_payload` carries the
39    /// updated payload. `continue_processing = false` halts (the handler
40    /// could not service the operation — e.g. unknown channel error); the
41    /// bridge maps that to an `ElicitationError`.
42    ///
43    /// **Three operations, one hook.** [`ElicitationPayload::operation`]
44    /// tells the handler whether this is a `Dispatch`, `Check`, or
45    /// `Validate` call. A handler typically `match`es on it. The three
46    /// short, synchronous calls span the (possibly hours-long) human gap,
47    /// which is owned by the channel — never by a handler call.
48    ///
49    /// **Handler signature:**
50    ///
51    /// ```rust,ignore
52    /// impl HookHandler<ElicitationHook> for CibaApprover {
53    ///     async fn handle(
54    ///         &self,
55    ///         payload: &ElicitationPayload,
56    ///         _ext: &Extensions,
57    ///         _ctx: &mut PluginContext,
58    ///     ) -> PluginResult<ElicitationPayload> {
59    ///         let mut out = payload.clone();
60    ///         match payload.operation() {
61    ///             ElicitationOp::Dispatch => { /* register intent + CIBA backchannel */ }
62    ///             ElicitationOp::Check    => { /* poll Keycloak token endpoint */ }
63    ///             ElicitationOp::Validate => { /* verify token + intent binding */ }
64    ///         }
65    ///         PluginResult::modify_payload(out)
66    ///     }
67    /// }
68    /// ```
69    ///
70    /// **Registration:**
71    /// `manager.register_handler_for_names::<ElicitationHook, _>(plugin, config, &["elicit"])`.
72    ///
73    /// [PluginResult]: crate::hooks::trait_def::PluginResult
74    ElicitationHook, "elicit" => {
75        payload: ElicitationPayload,
76        result: PluginResult<ElicitationPayload>,
77    }
78}