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}