Skip to content

feat: improve APL ergonomics#71

Merged
araujof merged 5 commits into
devfrom
feat/apl_misc
Jun 18, 2026
Merged

feat: improve APL ergonomics#71
araujof merged 5 commits into
devfrom
feat/apl_misc

Conversation

@araujof

@araujof araujof commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Improves APL ergonomics while maintaining backwards compatibility with original syntax.

Changes

make the apl: wrapper optional

APL config blocks previously had to be nested under an apl: key at every level (route -> apl -> policy, global -> apl -> pdp, ...). The visitor's apl_subblock now accepts APL terms written directly on the section when the wrapper is omitted (route -> policy), while the explicit apl: form still takes precedence.

When apl: is absent, a synthetic block is assembled from the recognized APL keys present on the container — policy, post_policy, args, result, pdp — so structural keys (tool / identity / defaults / ...) are never misread. plugins is shape-gated: a map (the apl-override form) is included, a list (structural plugin-refs on RouteEntry / PolicyGroup) is left alone, avoiding a key-shape clash. Empty sections still return None (the "no contribution, skip" path).

Applies uniformly to the global / defaults / policy-bundle / route visit sites. Add unit tests for the shape rules (wrapper precedence, flat keys, plugins map-vs-list, null/empty) and end-to-end tests for a wrapper-less route on both the allow and deny paths.

accept run as an alias for plugin

run(name) now invokes a named plugin everywhere plugin(name) does, so policies can read - run(audit-log) instead of - plugin(audit-log):

  • policy-step / effect form (parse_step_string) — covers top-level policy: items and do: lists, and detect_step_kind recognizes run( so misrouted strings still get a clear step-kind error;
  • field-pipeline stage form (args.x: "str | run(name)") — mirrors the step alias for symmetry.

plugin remains the long form; both parse to the same Step::Plugin / Stage::Plugin. Errors name whichever verb the author used.

Add tests for the step alias (string + compile paths), the field-stage alias, and the empty/malformed error path.

accept unconditional deny('reason') as a bare action

parse_rule's no-colon branch only recognized bare deny / allow via try_bare_action; the deny('reason') / deny('reason', 'code') call form was reachable only as the action half of a conditional rule (predicate: deny('reason')). That made it impossible to attach a reason/code to an unconditional reaction — notably on_deny: /on_allow: lists on PDP steps, which parse each item through parse_step -> parse_step_string -> parse_rule.

Fall through to try_parse_deny_call after try_bare_action so an unconditional deny('reason') / deny('reason', 'code') parses to an Always-guarded Deny. A malformed deny(...) surfaces its own error rather than being misread as a predicate downstream.

Add tests covering the reason-only and reason+code forms and the malformed-call error path.

araujof added 3 commits June 16, 2026 21:48
`parse_rule`'s no-colon branch only recognized bare `deny` / `allow` via
`try_bare_action`; the `deny('reason')` / `deny('reason', 'code')` call
form was reachable only as the action half of a conditional rule
(`predicate: deny('reason')`). That made it impossible to attach a
reason/code to an unconditional reaction — notably `on_deny:` /
`on_allow:` lists on PDP steps, which parse each item through
`parse_step -> parse_step_string -> parse_rule`.

Fall through to `try_parse_deny_call` after `try_bare_action` so an
unconditional `deny('reason')` / `deny('reason', 'code')` parses to an
`Always`-guarded `Deny`. A malformed `deny(...)` surfaces its own error
rather than being misread as a predicate downstream.

Add tests covering the reason-only and reason+code forms and the
malformed-call error path.

Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
`run(name)` now invokes a named plugin everywhere `plugin(name)` does,
so policies can read `- run(audit-log)` instead of `- plugin(audit-log)`:

- policy-step / effect form (`parse_step_string`) — covers top-level
  `policy:` items and `do:` lists, and `detect_step_kind` recognizes
  `run(` so misrouted strings still get a clear step-kind error;
- field-pipeline stage form (`args.x: "str | run(name)"`) — mirrors the
  step alias for symmetry.

`plugin` remains the long form; both parse to the same `Step::Plugin` /
`Stage::Plugin`. Errors name whichever verb the author used.

Add tests for the step alias (string + compile paths), the field-stage
alias, and the empty/malformed error path.

Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
APL config blocks previously had to be nested under an `apl:` key at
every level (`route -> apl -> policy`, `global -> apl -> pdp`, ...). The
visitor's `apl_subblock` now accepts APL terms written directly on the
section when the wrapper is omitted (`route -> policy`), while the
explicit `apl:` form still takes precedence.

When `apl:` is absent, a synthetic block is assembled from the
recognized APL keys present on the container — `policy`, `post_policy`,
`args`, `result`, `pdp` — so structural keys (tool / identity /
defaults / ...) are never misread. `plugins` is shape-gated: a map (the
apl-override form) is included, a list (structural plugin-refs on
RouteEntry / PolicyGroup) is left alone, avoiding a key-shape clash.
Empty sections still return None (the "no contribution, skip" path).

Applies uniformly to the global / defaults / policy-bundle / route
visit sites. Add unit tests for the shape rules (wrapper precedence,
flat keys, plugins map-vs-list, null/empty) and end-to-end tests for a
wrapper-less route on both the allow and deny paths.

Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
@araujof araujof requested review from jonpspri and terylt as code owners June 17, 2026 05:41
@araujof araujof added enhancement New feature or request framework Rust labels Jun 17, 2026
@araujof araujof added this to CPEX Jun 17, 2026
@github-project-automation github-project-automation Bot moved this to Backlog in CPEX Jun 17, 2026
@araujof araujof moved this from Backlog to In review in CPEX Jun 17, 2026
@araujof araujof added this to the 0.2.0 milestone Jun 17, 2026
@araujof araujof changed the title Feat/apl misc feat: improve APL ergonomics Jun 17, 2026
araujof and others added 2 commits June 18, 2026 11:27
Three follow-up fixes to the optional-`apl:`-wrapper work:

1. Flat `plugins:` map broke config loading at route / defaults /
   policy scope. The whole YAML deserializes into `CpexConfig` before
   any visitor runs, and `plugins` is a `Vec<PluginRouteRef>` there — so
   a wrapper-less override *map* (`plugins: { audit: { on_error: ... } }`)
   failed the structural parse with "invalid type: map, expected a
   sequence", reachable only under top-level `global:`. Give
   `RouteEntry.plugins` and `PolicyGroup.plugins` a `deserialize_with`
   that accepts either shape: a sequence stays the structural activation
   list; a map deserializes to an empty `Vec` (it is APL-override data,
   consumed by the visitor from the raw YAML). This mirrors the
   `apl: { plugins: {...} }` wrapper form exactly — the map never
   populated the structural list there either — so the flat and wrapped
   forms are behaviorally identical. A scalar `plugins:` now yields a
   shape-aware error instead of a cryptic serde message.

2. Flat `pdp:` was silently dropped at non-global scopes. Only
   `visit_global` builds PDPs; a `pdp:` under a default / policy-bundle
   / route block was folded into the policy body and discarded with no
   signal. Emit a `tracing::warn!` at those scopes (flat or wrapped) so
   the footgun is visible.

3. `plugin()` / `run()` with an empty name was accepted in field
   pipelines (`Stage::Plugin { name: "" }`), while the policy-step path
   already rejected it. Add the same empty-name guard to `parse_stage`
   so both paths fail with the same verb-named diagnostic.

Tests: unit coverage for the map-tolerant deserializer (list, map,
defaults/policies, scalar), the empty-name guard, and the pdp-warn
helper; end-to-end tests that drive a flat `plugins:` map through the
real `load_config_yaml` path — policy+map coexist and deny, defaults
inheritance, and flat-vs-wrapped equivalence — closing the gap the
isolated `apl_subblock` tests left open.

Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Teryl Taylor <terylt@ibm.com>

@terylt terylt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. I made some small changes and you addressed the issues from my review.

@araujof araujof merged commit c131a33 into dev Jun 18, 2026
@github-project-automation github-project-automation Bot moved this from In review to Done in CPEX Jun 18, 2026
@araujof araujof deleted the feat/apl_misc branch June 18, 2026 18:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request framework Rust

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants