Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion builtins/plugins/delegator-oauth/tests/oauth_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,7 @@ async fn workload_subject_authenticates_by_svid_then_exchanges() {
leg2.assert_async().await;
}

/// A6: a leg-1 rejection must not echo submitted credential material. Even
/// A leg-1 rejection must not echo submitted credential material. Even
/// when the IdP hostilely parrots the SVID back in `error_description`, the
/// caller-visible violation carries only the OAuth error code — never the
/// `client_assertion` bytes.
Expand Down
4 changes: 2 additions & 2 deletions crates/apl-cmf/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ impl BagBuilder {
self
}

/// Flatten a static attribute tree into the `data.*` namespace
/// (design §4.2). The tree is shared and startup-loaded, but this
/// Flatten a static attribute tree into the `data.*` namespace.
/// The tree is shared and startup-loaded, but this
/// re-walks it and re-inserts every leaf into the bag on **each call**
/// (a `format!` per node, a `bag.set` per leaf) — so `data.*` reads are
/// **not** free on the request hot path. The route handler invokes this
Expand Down
11 changes: 6 additions & 5 deletions crates/apl-cmf/src/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ pub fn extract_result(result: &Value, bag: &mut AttributeBag) {
walk(result, BAG_RESULT_PREFIX.trim_end_matches('.'), bag);
}

/// Flatten a static attribute tree into `data.*` keys (design §4.2).
/// Same walk as args/result — nested objects recurse, string arrays
/// become `StringSet`s (so `data.tenants.x.allowed_models` supports
/// `contains` and R3b `restrict` references).
/// Flatten a static attribute tree into `data.*` keys. Same walk as
/// args/result — nested objects recurse, string arrays become
/// `StringSet`s (so `data.tenants.x.allowed_models` supports `contains`
/// and interpolated `restrict` references).
pub fn extract_data(tree: &apl_core::AttributeTree, bag: &mut AttributeBag) {
walk(tree.as_value(), "data", bag);
}
Expand Down Expand Up @@ -176,7 +176,8 @@ mod tests {
bag.get_string("data.tenants.acme-eu.data_region"),
Some("eu")
);
// String arrays become a StringSet (ready for `contains` / R3b).
// String arrays become a StringSet (ready for `contains` /
// interpolated path lookups).
assert!(bag.set_contains("data.tenants.acme-eu.allowed_models", "anthropic/*"));
assert!(bag.set_contains("data.tenants.acme-eu.allowed_models", "vllm/*"));
}
Expand Down
3 changes: 1 addition & 2 deletions crates/apl-core/src/attribute_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
// that aren't carried by any token or fetched from anywhere: backend-free
// policy constants like tenant→region maps, per-agent model allow-lists,
// org defaults. Those come from a plain, operator-organized data tree that
// lands in the evaluation bag under `data.*` (see
// docs/apl-restrict-effect-design.md §4).
// lands in the evaluation bag under `data.*`.
//
// This module is the pure contract: the `AttributeSource` trait (where a
// tree comes from) and the `AttributeTree` value (what it is). The default
Expand Down
2 changes: 1 addition & 1 deletion crates/apl-core/src/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ impl AttributeBag {
}

/// Resolve an attribute path to its concrete flat key, expanding any
/// `[inner]` interpolation groups (design §4.3). Each `[inner]` looks
/// `[inner]` interpolation groups. Each `[inner]` looks
/// `inner` up in this bag and substitutes `.` + its scalar value:
/// `data.tenants[subject.tenant].data_region` with `subject.tenant =
/// "acme-eu"` → `data.tenants.acme-eu.data_region`. The common
Expand Down
5 changes: 2 additions & 3 deletions crates/apl-core/src/constraint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
// Backend candidate-constraint IR for the `restrict` effect.
//
// `restrict` narrows the set of backends the host's router/load-balancer
// may select from — it never picks a backend (see
// docs/apl-restrict-effect-design.md). It normally does not allow/deny the
// may select from — it never picks a backend. It normally does not allow/deny the
// request either; the one exception is fail-closed integrity — an
// unresolvable `deny_models` reference denies, since a deny-list cannot fail
// open (see `RestrictResolveError`). It is an accumulating
Expand Down Expand Up @@ -116,7 +115,7 @@ pub enum OnEmpty {
}

/// A `restrict` string-set field: either a literal set or a `data.*`/bag
/// reference resolved against the request at eval time (design §4.3). The
/// reference resolved against the request at eval time. The
/// YAML shape disambiguates — a sequence is a literal, a bare scalar is a
/// reference:
///
Expand Down
8 changes: 4 additions & 4 deletions crates/apl-core/src/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ async fn dispatch_effect(

Effect::Restrict { spec } => {
// Resolve any `data.*` field references against this request's
// bag (design §4.3). Allow-list references fail closed by
// bag. Allow-list references fail closed by
// resolving to the empty set; an unresolvable *deny*-list
// reference is an integrity failure that denies the request —
// a deny-list we can't resolve must never route unconstrained.
Expand Down Expand Up @@ -1632,7 +1632,7 @@ mod tests {
Expression::Condition(c)
}

// ----- R3b: data.* path interpolation -----
// ----- data.* path interpolation -----

/// Parse a predicate and evaluate it against `bag`.
fn eval_pred(src: &str, bag: &AttributeBag) -> bool {
Expand Down Expand Up @@ -3351,7 +3351,7 @@ mod tests {
);
}

// ----- R1: restrict effect accumulation -----
// ----- restrict effect accumulation -----

fn restrict_regions(regions: &[&str]) -> Effect {
use crate::constraint::{RestrictSpec, StringSetSpec};
Expand Down Expand Up @@ -3521,7 +3521,7 @@ mod tests {
assert_eq!(e.constraints[0].allow_models.as_deref(), Some(&[][..]));
}

// ----- R1b: deny_models references fail closed -----
// ----- deny_models references fail closed -----

fn restrict_deny_models_ref(path: &str) -> Effect {
use crate::constraint::{RestrictSpec, StringSetSpec};
Expand Down
20 changes: 9 additions & 11 deletions crates/apl-core/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ impl<'a> Lexer<'a> {
// An attribute path is ident-cont runs interleaved with `[...]`
// interpolation groups: `data.tenants[subject.tenant].data_region`.
// The bracket content is a nested attribute key the evaluator
// resolves at eval time (R3b) — the lexer only delimits it.
// resolves at eval time — the lexer only delimits it.
let mut has_bracket = false;
loop {
while let Some(b) = self.peek() {
Expand Down Expand Up @@ -1699,16 +1699,15 @@ fn parse_restrict_effect(body: &serde_yaml::Value, source: &str) -> Result<Effec
Ok(Effect::Restrict { spec })
}

/// Parse a `restrict:` body map into a [`RestrictSpec`]
/// (`docs/apl-restrict-effect-design.md` §2.3, §4.3). Every field is
/// Parse a `restrict:` body map into a [`RestrictSpec`]. Every field is
/// optional, but an entirely empty `restrict:` is rejected — it would
/// constrain nothing, so it's an author error. Unknown keys are a hard
/// error: the constraint is a fixed contract we ask the host's router to
/// honor, and a typo'd field must never silently widen the eligible set.
///
/// The string-set fields (`allow_models` / `deny_models` / `allow_regions`
/// / `allow_sites`) accept either a literal YAML list **or** a bare
/// scalar `data.*` reference resolved per request (§4.3).
/// scalar `data.*` reference resolved per request.
fn parse_restrict_spec(
body_val: &serde_yaml::Value,
source: &str,
Expand Down Expand Up @@ -1806,7 +1805,7 @@ fn parse_restrict_spec(

/// Parse a `restrict` string-set field. A YAML **sequence** is a literal
/// set of strings; a bare **scalar string** is a `data.*` reference
/// resolved per request (design §4.3) — e.g.
/// resolved per request — e.g.
/// `allow_models: data.agents[subject.id].allowed_models`.
fn parse_string_set_spec(
v: &serde_yaml::Value,
Expand Down Expand Up @@ -1851,9 +1850,8 @@ fn parse_string_list(v: &serde_yaml::Value) -> Result<Vec<String>, String> {

/// Parse a YAML value expected to be a flat map of `label: value`
/// pairs (the `custom` field). Scalar values (string / bool / number)
/// are coerced to their string form, matching the label-map contract
/// (design §2.3.1) — `custom` is equality-matched labels, not typed
/// values.
/// are coerced to their string form: `custom` is equality-matched
/// labels, not typed values.
fn parse_label_map(
v: &serde_yaml::Value,
) -> Result<std::collections::BTreeMap<String, String>, String> {
Expand Down Expand Up @@ -2944,7 +2942,7 @@ mod tests {
assert!(format!("{}", err).contains("expected `==`"));
}

// ----- R3b: interpolated attribute paths -----
// ----- interpolated attribute paths -----

#[test]
fn lex_interpolated_path_is_one_ident() {
Expand Down Expand Up @@ -3833,7 +3831,7 @@ sequential:
assert!(format!("{}", err).contains("empty"));
}

// ----- R1: restrict effect -----
// ----- restrict effect -----

/// A literal `StringSetSpec` for terse assertions.
fn lit(items: &[&str]) -> Option<crate::constraint::StringSetSpec> {
Expand Down Expand Up @@ -3945,7 +3943,7 @@ do:
#[test]
fn restrict_inside_pdp_on_allow() {
// `restrict` composes in a PDP reaction — authz says yes, then
// pin routing (design §2.1).
// pin routing.
let yaml = r#"
cedar:
action: read
Expand Down
2 changes: 1 addition & 1 deletion crates/apl-core/src/plugin_decl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ impl<'a> EffectivePlugin<'a> {
/// the effective view. Returns `None` if `name` isn't in the
/// registry — caller decides whether that's an error.
///
/// Spec §"Route-Level Plugin Config Overrides":
/// Route-level override precedence:
/// - Override `config` replaces the global `config` entirely.
/// - Override `capabilities` replaces global capabilities.
/// - Override `on_error` replaces global on_error.
Expand Down
2 changes: 1 addition & 1 deletion crates/apl-core/src/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ pub struct RouteDecision {
/// Backend candidate constraints emitted by `restrict` effects in any
/// phase. Empty unless a `restrict` fired. The host bridge (apl-cpex)
/// folds these into a `CandidateConstraintExtension` it serializes to
/// the router — see `docs/apl-restrict-effect-design.md` §2.5.
/// the router.
pub constraints: Vec<crate::constraint::CandidateConstraint>,
/// True if any args field was rewritten or omitted.
pub args_modified: bool,
Expand Down
2 changes: 1 addition & 1 deletion crates/apl-core/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ pub enum Effect {
/// picks a backend and never allows/denies. Accumulating, in the same
/// family as `Taint`: the evaluator collects the constraint, a higher
/// layer folds it into a `CandidateConstraintExtension` the host
/// serializes to its router. See docs/apl-restrict-effect-design.md.
/// serializes to its router.
Restrict {
spec: crate::constraint::RestrictSpec,
},
Expand Down
8 changes: 3 additions & 5 deletions crates/apl-core/src/step.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,7 @@ pub(crate) enum Step {
/// succeeds; never produces a Deny (accumulating, same family as
/// `Taint`). The evaluator collects the emitted constraint; a higher
/// layer (apl-cpex) folds it into a `CandidateConstraintExtension`
/// the host serializes to its router. See
/// `docs/apl-restrict-effect-design.md`.
/// the host serializes to its router.
Restrict {
spec: crate::constraint::RestrictSpec,
},
Expand Down Expand Up @@ -121,9 +120,8 @@ pub(crate) enum Step {
/// rules.
///
/// For fan-out flows that need multiple independently-queryable
/// grants, split into `pre_invocation:` + `post_invocation:` or reach for a
/// future per-step `as:` alias (not in v0; see the design doc's
/// "Open design questions" section).
/// grants, split into `pre_invocation:` + `post_invocation:`. There is
/// no per-step alias for naming an individual delegate's grants.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DelegateStep {
/// Plugin name — must reference an entry in the top-level
Expand Down
2 changes: 1 addition & 1 deletion crates/apl-cpex/src/attribute_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// SPDX-License-Identifier: Apache-2.0
// Authors: Teryl Taylor
//
// FileAttributeSource — the built-in `data.*` provider (design §4.4.1).
// FileAttributeSource — the built-in `data.*` provider.
//
// Reads a list of attribute files (YAML, each wrapping everything under a
// top-level `data:` mapping) and deep-merges them into one
Expand Down
3 changes: 1 addition & 2 deletions crates/apl-cpex/src/candidate_constraint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
// wire type) the host router reads off the returned `Extensions`. This is
// the bridge between the pure policy language and the framework's typed
// extension slot, the same role `apply_session_taints` plays for taints.
// See docs/apl-restrict-effect-design.md §2.4/§2.5.

use apl_core::constraint::{CandidateConstraint, OnEmpty as AplOnEmpty};
use cpex_core::extensions::{CandidateConstraintExtension, OnEmpty};
Expand Down Expand Up @@ -41,7 +40,7 @@ impl std::fmt::Display for ConstraintConflict {
/// to an unrestricting result). Order-independent — the input may arrive
/// in any order (constraints from parallel branches merge unsorted).
///
/// Monotone semantics (design §2.4): allow-sets **intersect**,
/// Monotone semantics: allow-sets **intersect**,
/// `deny_models` **union**, `max_cost_tier` ceilings **collect** into
/// `max_cost_tiers` (CPEX can't order tier names — the host reduces to the
/// min), `custom` **union**, `on_empty` takes the **strictest**
Expand Down
8 changes: 4 additions & 4 deletions crates/apl-cpex/src/route_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ impl AnyHookHandler for AplRouteHandler {
// No-op when no taints emitted.
invoker.apply_session_taints(&decision.taints).await;

// R2: fold this request's `restrict` constraints into one typed
// Fold this request's `restrict` constraints into one typed
// `CandidateConstraintExtension`. A custom-label contradiction
// (two restricts requiring the same label to differ) cannot be
// honored by any backend, so it fails closed below (mirrors the
Expand Down Expand Up @@ -491,11 +491,11 @@ impl AnyHookHandler for AplRouteHandler {
None
};

// R2: write the folded constraint into the typed
// Write the folded constraint into the typed
// `candidate_constraint` extension slot so the host router reads
// it TYPED off `PipelineResult.modified_extensions` — the same
// in-process, type-shared channel `raw_credentials.delegated_tokens`
// rides (design §2.5). `extensions_changed` doesn't track this
// rides. `extensions_changed` doesn't track this
// slot, so we force `modified_extensions` to `Some` here to
// guarantee the constraint reaches the executor's merge.
if let Some(constraint) = folded_constraint {
Expand Down Expand Up @@ -595,7 +595,7 @@ impl AnyHookHandler for AplRouteHandler {
}
}

// R2 fail-closed: a `restrict` custom-label contradiction means no
// Fail closed: a `restrict` custom-label contradiction means no
// backend can satisfy the request's routing constraints. Deny
// rather than emit an unhonorable constraint. On an already-denied
// request, keep the original policy attribution (same precedence
Expand Down
39 changes: 20 additions & 19 deletions crates/apl-cpex/src/session_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ fn short_hash(raw: &str) -> String {
/// module doc prescribes for the (previously raw) Agent and TokenClaim tiers,
/// so a session id chosen by one principal cannot address another principal's
/// session bucket. Returns `None` when there is no authenticated subject — a
/// bare client value has no safe scope, consistent with Tiers 2/3, which also
/// require a subject.
/// bare client value has no safe scope, consistent with the identity tier,
/// which also requires a subject.
fn subject_scoped(subject_id: Option<&str>, raw: &str) -> Option<String> {
let sub = subject_id?;
Some(short_hash(&format!("{}:{}", sub, raw)))
Expand Down Expand Up @@ -323,9 +323,9 @@ mod tests {

#[test]
fn tier0_wins_over_identity() {
// T0 (agent.session_id) must win over T2 (identity triple) when
// both are available. Pins the tier priority explicitly so a
// future refactor of the resolver's walk order regresses loudly.
// The agent tier (`agent.session_id`) must win over the identity
// tier (identity triple) when both are available. Pins the walk
// order explicitly so a future refactor regresses loudly.
let mut agent = AgentExtension::default();
agent.session_id = Some("from-agent".into());
let sec = SecurityExtension {
Expand All @@ -350,7 +350,7 @@ mod tests {
assert_eq!(
src,
SessionSource::Agent,
"T0 must win over T2 when both are available",
"agent tier must win over identity tier when both are available",
);
assert_eq!(sid, subject_scoped(Some("alice"), "from-agent").unwrap());
}
Expand Down Expand Up @@ -394,11 +394,11 @@ mod tests {

#[test]
fn tier1_same_session_id_claim_different_subjects_are_distinct() {
// The guarantee for T1. An issuer that reuses a
// session_id value across multiple principals (multi-tenant
// naming conventions, counters that don't carry the subject,
// etc.) must NOT let one principal land in another's session
// bucket. Direct mirror of the T0 cross-principal test.
// The cross-principal guarantee for the token-claim tier. An
// issuer that reuses a session_id value across multiple principals
// (multi-tenant naming conventions, counters that don't carry the
// subject, etc.) must NOT let one principal land in another's
// session bucket. Direct mirror of the agent-tier test above.
let mk = |sub: &str| -> SecurityExtension {
SecurityExtension {
subject: Some(subject_with_claims(
Expand Down Expand Up @@ -438,9 +438,9 @@ mod tests {
#[test]
fn tier1_no_subject_id_falls_through() {
// A JWT carries a `session_id` claim but has no `sub` (subject
// present but `id == None`). T1 has no safe scope without a
// subject — must fall through. T2 also requires a subject and
// therefore returns None overall.
// present but `id == None`). The token-claim tier has no safe
// scope without a subject — must fall through. The identity tier
// also requires a subject, so resolution returns None overall.
let sec = SecurityExtension {
subject: Some(SubjectExtension {
id: None,
Expand All @@ -461,9 +461,9 @@ mod tests {
#[test]
fn tier1_wins_over_identity() {
// Both a JWT session_id claim AND a full identity triple are
// present. T1 must win over T2. Pins the tier priority
// explicitly — the existing happy-path test happens to omit
// T2 inputs, so without this T1>T2 priority is only implicit.
// present. The token-claim tier must win over the identity tier.
// Pins the priority explicitly — the happy-path test above omits
// identity-tier inputs, so otherwise the ordering is only implicit.
let sec = SecurityExtension {
subject: Some(subject_with_claims(
Some("alice"),
Expand All @@ -485,12 +485,13 @@ mod tests {
assert_eq!(
src,
SessionSource::TokenClaim,
"T1 must win over T2 when both are available",
"token-claim tier must win over identity tier when both are available",
);
assert_eq!(sid, subject_scoped(Some("alice"), "from-claim").unwrap());
}

// Tier 2 (`X-CPEX-Session-Id` header) is intentionally absent.
// The client-supplied `X-CPEX-Session-Id` header tier is
// intentionally absent — it has no slot in the walk above.
//
// The Python `SessionResolver` included a header tier; cpex Rust
// does not. See the module-level doc comment for the threat model.
Expand Down
Loading
Loading