Skip to main content

cpex_core/extensions/
tiers.rs

1// Location: ./crates/cpex-core/src/extensions/tiers.rs
2// Copyright 2025
3// SPDX-License-Identifier: Apache-2.0
4// Authors: Teryl Taylor
5//
6// Mutability tiers and capability definitions.
7//
8// Each extension slot has a mutability tier that controls how plugins
9// can interact with it. Capabilities gate per-plugin access.
10//
11// Mirrors cpex/framework/extensions/tiers.py.
12
13use serde::{Deserialize, Serialize};
14
15/// Mutability tier for an extension slot.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum MutabilityTier {
19    /// Cannot be modified after creation.
20    Immutable,
21    /// Can only grow (add-only sets, append-only chains).
22    Monotonic,
23    /// Can be freely modified by plugins with write capability.
24    Mutable,
25}
26
27/// Declared permission that controls extension access.
28///
29/// # Why no `Write*` for identity slots
30///
31/// The IdentityResolve and TokenDelegate hook families return result
32/// payloads that the framework consumes to mutate `Extensions`. Plugins
33/// never write to `security.subject` / `security.client` /
34/// `security.*_workload` / `raw_credentials.*` directly — those slots
35/// are owned by the framework on behalf of return-based handlers. The
36/// matching write capabilities are therefore absent from this enum
37/// until a use case appears for plugin-driven mutation of these slots
38/// outside the resolve/delegate hooks.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
40#[serde(rename_all = "snake_case")]
41pub enum Capability {
42    // ----- Subject (user identity) -----
43    /// Read the authenticated subject identity (`security.subject`).
44    /// Unlocks the slot but not its sub-fields — roles / teams /
45    /// claims / permissions each have their own cap below.
46    ReadSubject,
47    /// Read subject roles (`security.subject.roles`).
48    ReadRoles,
49    /// Read subject team memberships (`security.subject.teams`).
50    ReadTeams,
51    /// Read subject claims (`security.subject.claims`).
52    ReadClaims,
53    /// Read subject permissions (`security.subject.permissions`).
54    ReadPermissions,
55
56    // ----- Client (OAuth application identity) -----
57    /// Read the OAuth client / gateway-access identity
58    /// (`security.client`). Distinct from the user identity
59    /// (`subject`) — a single user can connect through different
60    /// clients (first-party browser, third-party agent) and policies
61    /// sometimes want to gate on the client.
62    ReadClient,
63
64    // ----- Workload (attested SPIFFE / mTLS identity) -----
65    /// Read either workload-identity slot — both
66    /// `security.caller_workload` (the inbound attested peer) and
67    /// `security.this_workload` (our own outbound identity). One
68    /// capability covers both: a plugin either has access to
69    /// attested-workload identity or it doesn't. Distinct from
70    /// `read_agent` which governs session / conversation context,
71    /// **NOT** identity.
72    ReadWorkload,
73
74    // ----- Agent execution context (session / conversation) -----
75    /// Read the agent execution context (`AgentExtension`).
76    /// **NOT a credential** — this carries session / conversation /
77    /// lineage state, not identity. Identity reads use
78    /// `read_subject` / `read_client` / `read_workload`.
79    ReadAgent,
80
81    // ----- HTTP wire layer -----
82    /// Read HTTP headers.
83    ReadHeaders,
84    /// Write (modify) HTTP headers.
85    WriteHeaders,
86
87    // ----- Security labels (taint flow) -----
88    /// Read security labels.
89    ReadLabels,
90    /// Append security labels (monotonic add-only).
91    AppendLabels,
92
93    // ----- Delegation chain (validated) -----
94    /// Read the delegation chain.
95    ReadDelegation,
96    /// Append to the delegation chain (monotonic).
97    AppendDelegation,
98
99    // ----- Raw credentials (Layer 3) -----
100    /// Read raw inbound tokens
101    /// (`raw_credentials.inbound_tokens`) — the bearer-token
102    /// strings captured at the wire layer before validation.
103    /// Narrowly scoped: only IdentityResolve handlers, forwarding
104    /// plugins, and a small set of audit plugins should declare it.
105    /// Out-of-process plugins can't see these tokens regardless of
106    /// capability — token fields are `#[serde(skip)]`.
107    ReadInboundCredentials,
108    /// Read minted outbound delegated tokens
109    /// (`raw_credentials.delegated_tokens`) — the credentials a
110    /// TokenDelegate handler produced for an upstream call. Held by
111    /// forwarding / proxy plugins that re-attach them on the outbound
112    /// request. Same out-of-process caveat as
113    /// `read_inbound_credentials`.
114    ReadDelegatedTokens,
115}
116
117/// Access policy for an extension slot.
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
119#[serde(rename_all = "snake_case")]
120pub enum AccessPolicy {
121    /// All plugins can access.
122    Unrestricted,
123    /// Only plugins with the declared capability can access.
124    CapabilityGated,
125}
126
127/// Policy for a single extension slot.
128///
129/// Declares the mutability tier, access policy, and required
130/// capabilities for reading and writing.
131#[derive(Debug, Clone)]
132pub struct SlotPolicy {
133    /// How the slot can be modified.
134    pub tier: MutabilityTier,
135    /// Whether access requires a capability.
136    pub access: AccessPolicy,
137    /// Capability required for reading (if capability-gated).
138    pub read_cap: Option<Capability>,
139    /// Capability required for writing (if capability-gated).
140    pub write_cap: Option<Capability>,
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_tier_serde() {
149        let tier = MutabilityTier::Monotonic;
150        let json = serde_json::to_string(&tier).unwrap();
151        assert_eq!(json, "\"monotonic\"");
152    }
153
154    #[test]
155    fn test_capability_serde() {
156        let cap = Capability::AppendLabels;
157        let json = serde_json::to_string(&cap).unwrap();
158        assert_eq!(json, "\"append_labels\"");
159    }
160}