@@ -201,56 +201,55 @@ pub struct AgentSpec {
201201
202202/// One agent-local semantic binding to an externally identified resource.
203203///
204- /// `name` is the role the resource plays for this agent and `uri` is the exact absolute identity.
205- /// The URI scheme selects the downstream resource profile. The envelope deliberately carries no
206- /// policy .
204+ /// `name` is an agent-local label and `uri` is the exact absolute identity. `reason` explains why
205+ /// the reference belongs in this Agent Spec. `inactive_reason` preserves a reference that is no
206+ /// longer active for this agent without asserting anything about the resource itself .
207207#[ derive( Debug , Clone , PartialEq , Eq , Serialize ) ]
208208pub struct Resource {
209209 name : String ,
210210 uri : String ,
211+ reason : String ,
211212 #[ serde( skip_serializing_if = "Option::is_none" ) ]
212- relation : Option < String > ,
213- #[ serde( skip_serializing_if = "Option::is_none" ) ]
214- reason : Option < String > ,
213+ inactive_reason : Option < String > ,
215214}
216215
217216#[ derive( Deserialize ) ]
218217#[ serde( deny_unknown_fields) ]
219218struct ResourceDescriptor {
220219 name : String ,
221220 uri : String ,
222- relation : Option < String > ,
223- reason : Option < String > ,
221+ reason : String ,
222+ inactive_reason : Option < String > ,
224223}
225224
226225impl Resource {
227226 /// Construct a descriptor after enforcing the same invariants as catalog parsing.
228- pub fn new ( name : String , uri : String ) -> Result < Self , String > {
227+ pub fn new ( name : String , uri : String , reason : String ) -> Result < Self , String > {
229228 if name. is_empty ( ) {
230229 return Err ( "resource binding name cannot be empty" . into ( ) ) ;
231230 }
232231 validate_absolute_uri ( & uri) . map_err ( |reason| {
233232 format ! ( "resource binding '{name}' `uri` must be an exact absolute URI: {reason}" )
234233 } ) ?;
234+ validate_resource_explanation ( & name, "reason" , & reason) ?;
235235 Ok ( Self {
236236 name,
237237 uri,
238- relation : None ,
239- reason : None ,
238+ reason ,
239+ inactive_reason : None ,
240240 } )
241241 }
242242
243- /// Construct a descriptor with an explicit semantic relation and human-facing rationale .
244- pub fn new_with_relation_reason (
243+ /// Construct a preserved reference that is inactive for this agent .
244+ pub fn new_inactive (
245245 name : String ,
246246 uri : String ,
247- relation : String ,
248247 reason : String ,
248+ inactive_reason : String ,
249249 ) -> Result < Self , String > {
250- let mut resource = Self :: new ( name, uri) ?;
251- validate_relation_reason ( & resource. name , & relation, & reason) ?;
252- resource. relation = Some ( relation) ;
253- resource. reason = Some ( reason) ;
250+ let mut resource = Self :: new ( name, uri, reason) ?;
251+ validate_resource_explanation ( & resource. name , "inactive-reason" , & inactive_reason) ?;
252+ resource. inactive_reason = Some ( inactive_reason) ;
254253 Ok ( resource)
255254 }
256255
@@ -262,12 +261,12 @@ impl Resource {
262261 & self . uri
263262 }
264263
265- pub fn relation ( & self ) -> Option < & str > {
266- self . relation . as_deref ( )
264+ pub fn reason ( & self ) -> & str {
265+ & self . reason
267266 }
268267
269- pub fn reason ( & self ) -> Option < & str > {
270- self . reason . as_deref ( )
268+ pub fn inactive_reason ( & self ) -> Option < & str > {
269+ self . inactive_reason . as_deref ( )
271270 }
272271}
273272
@@ -277,19 +276,14 @@ impl<'de> Deserialize<'de> for Resource {
277276 D : serde:: Deserializer < ' de > ,
278277 {
279278 let descriptor = ResourceDescriptor :: deserialize ( deserializer) ?;
280- let resource = match ( descriptor. relation , descriptor. reason ) {
281- ( None , None ) => Self :: new ( descriptor. name , descriptor. uri ) ,
282- ( Some ( relation) , Some ( reason) ) => {
283- Self :: new_with_relation_reason ( descriptor. name , descriptor. uri , relation, reason)
284- }
285- ( Some ( _) , None ) => Err ( format ! (
286- "resource binding '{}' with `relation` must also declare string `reason`" ,
287- descriptor. name
288- ) ) ,
289- ( None , Some ( _) ) => Err ( format ! (
290- "resource binding '{}' with `reason` must also declare string `relation`" ,
291- descriptor. name
292- ) ) ,
279+ let resource = match descriptor. inactive_reason {
280+ None => Self :: new ( descriptor. name , descriptor. uri , descriptor. reason ) ,
281+ Some ( inactive_reason) => Self :: new_inactive (
282+ descriptor. name ,
283+ descriptor. uri ,
284+ descriptor. reason ,
285+ inactive_reason,
286+ ) ,
293287 } ;
294288 resource. map_err ( de:: Error :: custom)
295289 }
@@ -606,8 +600,8 @@ pub(crate) struct RawResources(BTreeMap<String, RawResource>);
606600#[ serde( deny_unknown_fields) ]
607601pub ( crate ) struct RawResource {
608602 pub ( crate ) uri : String ,
609- pub ( crate ) relation : Option < String > ,
610- pub ( crate ) reason : Option < String > ,
603+ pub ( crate ) reason : String ,
604+ pub ( crate ) inactive_reason : Option < String > ,
611605}
612606
613607#[ derive( Debug , Default , Deserialize ) ]
@@ -667,17 +661,11 @@ impl RawResources {
667661 self . 0
668662 . into_iter ( )
669663 . map ( |( name, resource) | {
670- match ( resource. relation , resource. reason ) {
671- ( None , None ) => Resource :: new ( name, resource. uri ) ,
672- ( Some ( relation) , Some ( reason) ) => Resource :: new_with_relation_reason (
673- name, resource. uri , relation, reason,
674- ) ,
675- ( Some ( _) , None ) => Err ( format ! (
676- "resource binding '{name}' with `relation` must also declare string `reason`"
677- ) ) ,
678- ( None , Some ( _) ) => Err ( format ! (
679- "resource binding '{name}' with `reason` must also declare string `relation`"
680- ) ) ,
664+ match resource. inactive_reason {
665+ None => Resource :: new ( name, resource. uri , resource. reason ) ,
666+ Some ( inactive_reason) => {
667+ Resource :: new_inactive ( name, resource. uri , resource. reason , inactive_reason)
668+ }
681669 }
682670 . map_err ( anyhow:: Error :: msg)
683671 } )
@@ -719,35 +707,19 @@ impl<'de> Deserialize<'de> for RawResources {
719707 }
720708}
721709
722- fn validate_relation_reason ( name : & str , relation : & str , reason : & str ) -> Result < ( ) , String > {
723- let relation_bytes = relation. as_bytes ( ) ;
724- let valid_relation = ( 1 ..=64 ) . contains ( & relation_bytes. len ( ) )
725- && relation_bytes
726- . iter ( )
727- . all ( |byte| byte. is_ascii_lowercase ( ) || byte. is_ascii_digit ( ) || * byte == b'-' )
728- && relation_bytes
729- . first ( )
730- . is_some_and ( u8:: is_ascii_alphanumeric)
731- && relation_bytes. last ( ) . is_some_and ( u8:: is_ascii_alphanumeric)
732- && !relation_bytes. windows ( 2 ) . any ( |pair| pair == b"--" ) ;
733- if !valid_relation {
734- return Err ( format ! (
735- "resource binding '{name}' `relation` must be ASCII kebab-case of 1..64 bytes"
736- ) ) ;
737- }
738-
739- if reason. is_empty ( ) || reason. len ( ) > 160 {
710+ fn validate_resource_explanation ( name : & str , field : & str , value : & str ) -> Result < ( ) , String > {
711+ if value. is_empty ( ) || value. len ( ) > 160 {
740712 return Err ( format ! (
741- "resource binding '{name}' `reason ` must be 1..160 UTF-8 bytes"
713+ "resource binding '{name}' `{field} ` must be 1..160 UTF-8 bytes"
742714 ) ) ;
743715 }
744- if reason . trim ( ) != reason
745- || reason
716+ if value . trim ( ) != value
717+ || value
746718 . chars ( )
747719 . any ( |character| character. is_control ( ) || matches ! ( character, '\u{2028}' | '\u{2029}' ) )
748720 {
749721 return Err ( format ! (
750- "resource binding '{name}' `reason ` must have no surrounding Unicode whitespace, controls, or line separators"
722+ "resource binding '{name}' `{field} ` must have no surrounding Unicode whitespace, controls, or line separators"
751723 ) ) ;
752724 }
753725 Ok ( ( ) )
0 commit comments