Skip to main content

frame_core/
action.rs

1//! Component action declarations and the authoritative action snapshot
2//! vocabulary (F-6a R1).
3//!
4//! Shared declaration types live here, beside [`ComponentMeta`]'s `provides`
5//! and `needs`, so every consumer of the action surface — direct dispatch and
6//! generated tooling alike — reads ONE vocabulary owned by frame-core.
7//! Registration is the only door: every declaration is validated there,
8//! atomically per component, and the registry's
9//! [`action_snapshot`](crate::registry::ComponentRegistry::action_snapshot)
10//! is the sole authority for which actions exist right now.
11//!
12//! [`ComponentMeta`]: crate::component::ComponentMeta
13
14use serde::{Deserialize, Serialize};
15
16use crate::capability::Capability;
17use crate::component::ComponentId;
18
19/// Component-local action identity.
20///
21/// Uniqueness is scoped by [`ActionKey`]; two components may intentionally
22/// declare the same local id. Registration refuses an empty id — the id's
23/// UTF-8 bytes participate in generated tool names, and nonemptiness is part
24/// of the by-construction collision-freedom argument (F-6a R2).
25#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
26pub struct ActionId(String);
27
28impl ActionId {
29    /// Wraps a raw id without validating; registration validates every id it
30    /// admits, so an unregistered invalid id can never enter the authority.
31    #[must_use]
32    pub fn new(value: impl Into<String>) -> Self {
33        Self(value.into())
34    }
35
36    /// Returns the id text.
37    #[must_use]
38    pub fn as_str(&self) -> &str {
39        &self.0
40    }
41
42    /// Returns the canonical UTF-8 bytes used for ordering and tool naming.
43    #[must_use]
44    pub fn as_bytes(&self) -> &[u8] {
45        self.0.as_bytes()
46    }
47}
48
49/// Globally unique action identity: component identity plus local id.
50#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
51pub struct ActionKey {
52    /// The declaring component.
53    pub component_id: ComponentId,
54    /// The component-local action id.
55    pub action_id: ActionId,
56}
57
58impl ActionKey {
59    /// Canonical ordering bytes: the 32 fixed-width component identity bytes,
60    /// then the action id's UTF-8 bytes. The fixed component width makes the
61    /// concatenation injective (F-6a R2's ordering and naming argument).
62    #[must_use]
63    pub fn canonical_bytes(&self) -> Vec<u8> {
64        let mut bytes = Vec::with_capacity(32 + self.action_id.as_bytes().len());
65        bytes.extend_from_slice(self.component_id.as_bytes());
66        bytes.extend_from_slice(self.action_id.as_bytes());
67        bytes
68    }
69}
70
71impl Ord for ActionKey {
72    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
73        self.component_id
74            .as_bytes()
75            .cmp(other.component_id.as_bytes())
76            .then_with(|| self.action_id.as_bytes().cmp(other.action_id.as_bytes()))
77    }
78}
79
80impl PartialOrd for ActionKey {
81    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
82        Some(self.cmp(other))
83    }
84}
85
86/// Typed reference to an S3 schema source: the canonical Rust path of the
87/// serde message type that IS the schema (F-3a's pinned ruling — schema is
88/// the typed message, validation is serde). Under the F-6a constraint-5 (c)
89/// ruling this reference is what generated minimal schemas cite; no schema
90/// bytes are derived from it.
91#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
92pub struct SchemaSource(String);
93
94impl SchemaSource {
95    /// Wraps a raw type path without validating; registration refuses an
96    /// empty path.
97    #[must_use]
98    pub fn new(value: impl Into<String>) -> Self {
99        Self(value.into())
100    }
101
102    /// Returns the referenced type path.
103    #[must_use]
104    pub fn as_str(&self) -> &str {
105        &self.0
106    }
107}
108
109/// One declared component action.
110///
111/// Declared inside [`ComponentMeta::actions`] and validated at registration:
112/// nonempty id, description, and schema sources; no duplicate local ids; no
113/// duplicate requirements; every requirement covered by a declared need (an
114/// action requiring an undeclarable capability would be a permanently dead
115/// tool — refused at the door, F-6a R1's tear condition).
116///
117/// [`ComponentMeta::actions`]: crate::component::ComponentMeta::actions
118#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
119pub struct ActionDeclaration {
120    /// Component-local identity.
121    pub id: ActionId,
122    /// Human-readable description; carried into generated tool descriptions
123    /// verbatim (mandatory readable identity, F-6a R2's naming decision).
124    pub description: String,
125    /// The typed request message this action validates against (S3).
126    pub request_schema: SchemaSource,
127    /// The typed response message this action replies with (S3).
128    pub response_schema: SchemaSource,
129    /// Capability requirements checked, in this exact order, by the shared
130    /// dispatch entry point's consuming act — never authority mutation.
131    pub requirements: Vec<Capability>,
132}
133
134/// Opaque monotonic token for one entry into Running.
135///
136/// Minted by the registry each time a component transition into Running
137/// commits; a retained handle carrying an older incarnation can never reach a
138/// replacement incarnation's dispatch surface (F-2a's held-vs-current rule,
139/// applied to actions).
140#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
141pub struct ActionIncarnation(u64);
142
143impl ActionIncarnation {
144    /// Wraps a raw incarnation ordinal (registry-minted; test fixtures only).
145    #[must_use]
146    pub const fn new(value: u64) -> Self {
147        Self(value)
148    }
149
150    /// Returns the ordinal for deterministic derivation (dispatch naming).
151    #[must_use]
152    pub const fn ordinal(self) -> u64 {
153        self.0
154    }
155}
156
157/// One row of the registry's authoritative action snapshot.
158///
159/// Rows exist only for Running component incarnations and carry everything
160/// S1 dispatch and R2 generation need — never host-facade authority. The
161/// dispatch identity S1 needs is `(key, incarnation)`: frame-conv derives
162/// the conversation address from those two values mechanically, so a stale
163/// incarnation's derived address is one no replacement incarnation ever
164/// attaches.
165#[derive(Clone, Debug, Eq, PartialEq)]
166pub struct ActionRow {
167    /// Globally unique action identity.
168    pub key: ActionKey,
169    /// Human-readable component name (tool description material).
170    pub component_name: String,
171    /// Declared description, passed through without rewriting.
172    pub description: String,
173    /// The typed request message reference (S3).
174    pub request_schema: SchemaSource,
175    /// The typed response message reference (S3).
176    pub response_schema: SchemaSource,
177    /// Canonical requirement sequence for the consuming act.
178    pub requirements: Vec<Capability>,
179    /// The Running incarnation these rows belong to.
180    pub incarnation: ActionIncarnation,
181}
182
183impl ActionRow {
184    /// The dispatch ordinal S1 addressing derives from: BLAKE3 over a domain
185    /// tag, the key's canonical bytes (injective — fixed-width component
186    /// identity then action id UTF-8), and the little-endian incarnation
187    /// ordinal, truncated to the first eight digest bytes.
188    ///
189    /// Pure and mechanical: byte-identical rows derive the byte-identical
190    /// ordinal in any process. frame-core stays ignorant of conversations —
191    /// this is an opaque address ordinal; frame-conv interprets it. A stale
192    /// incarnation's ordinal is one no replacement incarnation ever derives,
193    /// so a retained handle can never reach the replacement (F-6a R5's
194    /// stopped-target bound).
195    #[must_use]
196    pub fn dispatch_ordinal(&self) -> u64 {
197        let mut hasher = blake3::Hasher::new();
198        hasher.update(b"frame/action-dispatch/v1");
199        hasher.update(&self.key.canonical_bytes());
200        hasher.update(&self.incarnation.ordinal().to_le_bytes());
201        let digest = hasher.finalize();
202        let mut raw = [0u8; 8];
203        raw.copy_from_slice(&digest.as_bytes()[..8]);
204        u64::from_le_bytes(raw)
205    }
206}