feat: Human-in-the-loop effect for APL and elicitation plugin#115
Merged
Conversation
Signed-off-by: Teryl Taylor <terylt@ibm.com>
Signed-off-by: Teryl Taylor <terylt@ibm.com>
Signed-off-by: Teryl Taylor <terylt@ibm.com>
Signed-off-by: Teryl Taylor <terylt@ibm.com>
- Fail closed when an elicitation `from` attribute reference does not resolve, instead of dispatching the literal to the channel as a bogus login_hint; literal identities still pass through. Adds tests. - Rename `ElicitationError::Dispatch` to `Handler` and thread the operation name so check/validate failures no longer read as dispatch failures. - Correct the stale `dispatch_elicitation` doc comment to describe the implemented suspend/-32120 behavior. - Document the numeric_compare string coercion as an all-comparisons behavior change in the CHANGELOG, plus the elicitation feature entry. - Add docs/apl/elicitation.md (effect example, suspend/resume model, CIBA config, attributes, genuineness-vs-sufficiency/JWT-trust note); update the effects page and APL index. Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
araujof
approved these changes
Jul 15, 2026
araujof
left a comment
Contributor
There was a problem hiding this comment.
Nice work! This is a very useful feature. Merging it with a few additional fixes. Further enhancements should be made in a separate PR.
I found a few issues while reviewing the PR, which I addressed on the branch.
Addressed
fromfail-open (auth boundary). An unresolved attributefrom(e.g.claim.managerwith the claim absent) no longer dispatches to a boguslogin_hint— it fails closed (subject toon_error). Literal identities (emails/usernames) still pass through. Tests added.- Global evaluator behavior change surfaced. The
numeric_comparestring→f64 coercion affects all order comparisons, not just elicitationscope; now documented in the CHANGELOG. - JWT signature not verified. Acknowledged and correct given the token's provenance (client-authenticated TLS poll to the OP). The transport-trust assumption is now documented in the elicitation docs page for deployers.
- Docs. New
docs/apl/elicitation.md(effect example, suspend/resume model, CIBA config, attributes, genuineness-vs-sufficiency note); effects page and index updated. - Minor. Stale
dispatch_elicitationdoc comment corrected;ElicitationError::Dispatch→Handlerwith the operation named, so check/validate failures no longer read as dispatch failures.
Status
cargo test --workspace: all passing
Resolve conflicts: - Cargo.toml: adopt dev's 0.2.1 workspace version for all path deps, keeping the new cpex-plugin-elicitation-ciba entry (bumped to 0.2.1). - route_handler.rs: keep both dev's per-site denyWith decoration and the elicitation pending (-32120) / peek (-32121) handling; the retry/confirm signals stay undecorated by design. - CHANGELOG.md: move the #115 elicitation entries into a fresh [Unreleased] section above the released 0.2.1, and fix the compare links. Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Satisfies the CI lint job (`cargo fmt --all -- --check`); no behavior change. Clippy (`--workspace --all-targets -D warnings`) is clean. Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
cargo-machete flagged both as unused — the crate surfaces errors via cpex-core's PluginError/PluginViolation and emits no tracing. Also switch its cpex-core dep to `workspace = true` (matching the other builtin plugins), which drops the stale 0.2.0 pin left by the dev merge. Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
araujof
added a commit
that referenced
this pull request
Jul 15, 2026
Bring in the human-in-the-loop (HIL) elicitation feature and the numeric string-coercion change for order comparisons (#115), plus the new elicitation/ciba builtin plugin. Conflict resolution: - Cargo.toml: keep the 0.2.2 workspace version across the internal-dep table and add the new cpex-plugin-elicitation-ciba entry at 0.2.2. - CHANGELOG.md: fold dev's [Unreleased] entries (HIL elicitation; numeric order-comparison coercion) into the existing [0.2.2] release entry alongside the read_headers change, dropping the separate [Unreleased] section. Workspace builds; fmt + clippy (-D warnings) clean; full test suite passes (936); hugo docs build clean. Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
araujof
added a commit
that referenced
this pull request
Jul 15, 2026
…otent The v0.2.2 release failed at `cpex-builtins` because the new `cpex-plugin-elicitation-ciba` crate (added with the elicitation feature, #115) was never added to the ordered publish list, so its dependent `cpex-builtins` could not resolve it on crates.io. - Add `cpex-plugin-elicitation-ciba` to the publish order, before `cpex-builtins`. - Make the publish loop idempotent: skip a crate whose version is already on the index (crates.io rejects duplicate versions), so a re-run of a partially-published tag finishes the remaining crates instead of aborting on the first already-published one. Genuine errors still fail. Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
araujof
added a commit
that referenced
this pull request
Jul 17, 2026
* ci(release): publish cpex-plugin-elicitation-ciba; make publish idempotent The v0.2.2 release failed at `cpex-builtins` because the new `cpex-plugin-elicitation-ciba` crate (added with the elicitation feature, #115) was never added to the ordered publish list, so its dependent `cpex-builtins` could not resolve it on crates.io. - Add `cpex-plugin-elicitation-ciba` to the publish order, before `cpex-builtins`. - Make the publish loop idempotent: skip a crate whose version is already on the index (crates.io rejects duplicate versions), so a re-run of a partially-published tag finishes the remaining crates instead of aborting on the first already-published one. Genuine errors still fail. Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com> * docs: add runnable CPEX tutorial and reorganize docs outline Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com> * docs: canonicalize route form, runnable quickstart, config/testing guides, link check Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com> * ci: fix docs link-check and exclude tutorial crate from package dry-run Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com> * ci: gate tutorial IdP readiness with a probe; prune unused tutorial deps Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com> --------- Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
araujof
added a commit
that referenced
this pull request
Jul 17, 2026
* ci(release): publish cpex-plugin-elicitation-ciba; make publish idempotent The v0.2.2 release failed at `cpex-builtins` because the new `cpex-plugin-elicitation-ciba` crate (added with the elicitation feature, #115) was never added to the ordered publish list, so its dependent `cpex-builtins` could not resolve it on crates.io. - Add `cpex-plugin-elicitation-ciba` to the publish order, before `cpex-builtins`. - Make the publish loop idempotent: skip a crate whose version is already on the index (crates.io rejects duplicate versions), so a re-run of a partially-published tag finishes the remaining crates instead of aborting on the first already-published one. Genuine errors still fail. Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com> * docs: add runnable CPEX tutorial and reorganize docs outline Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com> * docs: canonicalize route form, runnable quickstart, config/testing guides, link check Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com> * ci: fix docs link-check and exclude tutorial crate from package dry-run Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com> * ci: gate tutorial IdP readiness with a probe; prune unused tutorial deps Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com> --------- Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds human-in-the-loop (HIL) elicitation to APL: a policy can pause an operation to ask a human — manager approval, a confirm, a step-up re-auth, an attestation — and resume once the human responds, without blocking the request path. The human's decision is bound to the actual request args and recorded from CPEX-owned state, so it is the source of truth for "what was approved," never an LLM summary. Includes a working Keycloak CIBA channel plugin.
A policy expresses it with a sugar verb that parses to a single
Step::Elicit:fromis who to ask (approver, resolved from the bag — may differ from the subject);scopeis the args-binding the runtime checks against the live request;purposeis the audited, human-readable description.The model: async, retry-based resume
An elicitation has three short, synchronous touch-points — the hours-long human gap lives in the channel (Keycloak CIBA), never in a blocking call:
Pending/Resolved{approved|denied}/Expired) without blocking.scope-over-args sufficiency check before honoring the approval.While pending, the phase suspends rather than denies.
Decisionstays binary — a suspended phase reportsAllow+Some(PendingElicitation), and the host maps that to JSON-RPC-32120("not complete, retry echoing this id"). The forwarding rule stays one clause: forward iffAllowANDpending.is_none(). Expiry / channel error fails closed (on_error: denydefault).What's included
apl-core)require_approval/confirm/step_up/attestation/info/review— sugar over oneStep::Elicit(ElicitStep)with anElicitKind. Fields:from,purpose,scope,timeout,channel,config_override,on_error. Parser + evaluator (dispatch/check/validate flow, pending short-circuit,scope-over-args) +route.rsresume wiring.cpex-core)ElicitationHook(single hook name"elicit") +ElicitationPayloadwith aDispatch/Check/Validateoperation discriminator — mirrors the delegation hook layout; dispatch is free viainvoke_entries::<ElicitationHook>. cpex-core keeps its ownElicitationOp/ElicitationStatusKind/ElicitationOutcomeKind(no apl-core dep); the bridge maps.apl-cpex)ElicitationInvoker(dispatch/check/validate) +elicitation_invoker.rs; dispatch-plan resolves anelicitentry by name;route_handlerdrives the pending/resume flow and surfaceselicitation.*bag attrs (id,approver,intent_id,channel) for audit.builtins/plugins/elicitation-ciba, new crate)kind: elicitation/ciba— aHookHandler<ElicitationHook>implementing the three operations against Keycloak CIBA:from→login_hint,purpose→binding_message,timeout→requested_expiry. Intent store, config, factory. Registered viacpex-builtins.scopeargs.amount <= 25000) the runtime evaluates at validate — kept in APL because Keycloak has no RFC 9396 RAR.Notable design decisions
fromresolves the party to ask from the request bag (e.g.claim.manager); the resolved identity is cross-checked against the actual responder at validate.scopeover live args) stays in the runtime because it is an APL expression the plugin can't evaluate.elicithook + payload discriminator (not three hook names) keeps registration and the dispatch plan trivial — one plugin, one entry, resolved by name liketoken.delegate.Decision.PendingElicitationrides alongside anAllowso the deny path stays untouched; the host's one-clause forward rule and-32120retry contract carry the suspend/resume.on_error).ElicitKind/ElicitationStatus/ElicitationOutcome.Public API surface
cpex-core::elicitation:ElicitationHook,HOOK_ELICIT("elicit"),ElicitationPayload,ElicitationOp,ElicitationStatusKind,ElicitationOutcomeKind.apl-core::step:Step::Elicit,ElicitStep,ElicitKind,ElicitationInvoker(dispatch/check/validate),ElicitationDispatch,ElicitationStatus,ElicitationOutcome,ElicitationValidation,PendingElicitation,ElicitationError.apl-cpex:ElicitationInvokerbridge, dispatch-planelicitation_entries, route-handler pending/resume wiring.builtins/plugins/elicitation-ciba:kind = "elicitation/ciba"(CibaApproverFactory), registered throughcpex-builtins.Testing
builtins/plugins/elicitation-ciba/tests/ciba_e2e.rs(7 tests) +live_keycloak.rs(1 live integration test, ignored by default).tests/elicit_step_e2e.rs: end-to-end through a realPluginManager(dispatch → pending → resolve → validate; approve / deny / expire; args-binding). Existing suites (delegate_step_e2e,end_to_end_route,cmf_invoker_dispatch) updated for the new step/plan shape.step.rs/route.rsunit tests;tests/yaml_end_to_end.rsupdated for the elicit verbs;apl-cmf/tests/end_to_end.rstouched.Follow-ups
docs/content/docs/apl/(none in this PR).elicithook; richer per-kind validation contracts.