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}