@@ -68,12 +68,21 @@ impl DeliveryTransport {
6868 ) ,
6969 }
7070 }
71+
72+ pub fn session_driver ( self ) -> SessionDriver {
73+ match self {
74+ Self :: Mcp => SessionDriver :: Claude ,
75+ Self :: AppServer => SessionDriver :: Codex ,
76+ Self :: PiChannel => SessionDriver :: Pi ,
77+ }
78+ }
7179}
7280/// The native session driver entered by an otherwise opaque launch.
7381///
7482/// This is an ownership assertion only. It does not render a provider launch or select a message
7583/// delivery transport.
76- #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
84+ #[ derive( Debug , Clone , Copy , PartialEq , Eq , Serialize , Deserialize ) ]
85+ #[ serde( rename_all = "lowercase" ) ]
7786pub enum SessionDriver {
7887 Claude ,
7988 Codex ,
@@ -105,6 +114,71 @@ impl SessionDriver {
105114 ) ,
106115 }
107116 }
117+
118+ /// Parse the canonical driver name carried by `session-driver`.
119+ pub fn from_name ( value : & str ) -> anyhow:: Result < Self > {
120+ Self :: parse ( value)
121+ }
122+ }
123+
124+ /// Non-secret facts that prove how a managed session can be admitted for delivery.
125+ ///
126+ /// This is deliberately separate from activity and runtime health. A credential-backed seat names
127+ /// only its opaque account identifier; an anonymous seat names the exact harness and model allowlist
128+ /// it can launch without credentials.
129+ #[ derive( Debug , Clone , PartialEq , Eq , Serialize , Deserialize ) ]
130+ #[ serde( rename_all = "camelCase" , tag = "kind" , deny_unknown_fields) ]
131+ pub enum DeliveryReadiness {
132+ Credential {
133+ account_id : Option < String > ,
134+ } ,
135+ Anonymous {
136+ harness : SessionDriver ,
137+ models : Vec < String > ,
138+ } ,
139+ }
140+
141+ impl DeliveryReadiness {
142+ pub fn validate ( & mut self ) -> anyhow:: Result < ( ) > {
143+ match self {
144+ Self :: Credential { account_id } => {
145+ if let Some ( account_id) = account_id {
146+ validate_delivery_readiness_value ( "account-id" , account_id) ?;
147+ }
148+ }
149+ Self :: Anonymous { harness : _, models } => {
150+ anyhow:: ensure!(
151+ !models. is_empty( ) ,
152+ "anonymous delivery-readiness requires at least one model"
153+ ) ;
154+ anyhow:: ensure!(
155+ models. len( ) <= 32 ,
156+ "anonymous delivery-readiness accepts at most 32 models"
157+ ) ;
158+ for model in models. iter ( ) {
159+ validate_delivery_readiness_value ( "model" , model) ?;
160+ }
161+ models. sort ( ) ;
162+ models. dedup ( ) ;
163+ }
164+ }
165+ Ok ( ( ) )
166+ }
167+ }
168+
169+ fn validate_delivery_readiness_value ( field : & str , value : & str ) -> anyhow:: Result < ( ) > {
170+ anyhow:: ensure!(
171+ !value. is_empty( ) && value. len( ) <= 200 ,
172+ "delivery-readiness {field} must be 1..=200 UTF-8 bytes"
173+ ) ;
174+ anyhow:: ensure!(
175+ value. trim( ) == value
176+ && !value
177+ . chars( )
178+ . any( |character| character. is_control( ) || matches!( character, '\u{2028}' | '\u{2029}' ) ) ,
179+ "delivery-readiness {field} must have no surrounding whitespace, controls, or line separators"
180+ ) ;
181+ Ok ( ( ) )
108182}
109183
110184
@@ -130,6 +204,16 @@ impl Driver {
130204 Self :: Omp ( _) => "omp" ,
131205 }
132206 }
207+
208+ pub fn session_driver ( & self ) -> SessionDriver {
209+ match self {
210+ Self :: Claude ( _) => SessionDriver :: Claude ,
211+ Self :: Codex ( _) => SessionDriver :: Codex ,
212+ Self :: Pi ( _) => SessionDriver :: Pi ,
213+ Self :: OpenCode ( _) => SessionDriver :: OpenCode ,
214+ Self :: Omp ( _) => SessionDriver :: Omp ,
215+ }
216+ }
133217}
134218
135219/// Typed fields accepted by a `claude {}` driver block.
@@ -258,6 +342,8 @@ pub struct AgentSpec {
258342 pub session_driver : Option < SessionDriver > ,
259343 /// Typed harness declaration used by task and render compilation.
260344 pub driver : Option < Driver > ,
345+ /// Non-secret admission facts for the managed delivery path.
346+ pub delivery_readiness : Option < DeliveryReadiness > ,
261347 /// Named typed references used by the agent. st2 preserves these for readers but does not
262348 /// resolve them or assign launch, readiness, access, or lifecycle semantics.
263349 pub resources : Vec < Resource > ,
@@ -270,6 +356,14 @@ pub struct AgentSpec {
270356 pub path : PathBuf ,
271357}
272358
359+ impl AgentSpec {
360+ /// The explicit native session owner after typed-driver normalization.
361+ pub fn effective_session_driver ( & self ) -> Option < SessionDriver > {
362+ self . session_driver
363+ . or_else ( || self . driver . as_ref ( ) . map ( Driver :: session_driver) )
364+ }
365+ }
366+
273367fn deserialize_optional_selector < ' de , D > (
274368 deserializer : D ,
275369) -> Result < Option < serde_json:: Value > , D :: Error >
@@ -603,6 +697,8 @@ pub(crate) struct RawSpec {
603697 /// Native session ownership asserted for an otherwise opaque launch.
604698 #[ serde( default , deserialize_with = "deserialize_explicit_optional" ) ]
605699 pub session_driver : Option < Option < String > > ,
700+ /// Non-secret facts used to admit the managed delivery path.
701+ pub delivery_readiness : Option < DeliveryReadiness > ,
606702 /// Direct typed provider driver block.
607703 #[ serde( flatten) ]
608704 pub driver : RawDriver ,
@@ -1124,6 +1220,7 @@ impl RawSpec {
11241220 || self . ding
11251221 || self . deliver . is_some ( )
11261222 || self . session_driver . is_some ( )
1223+ || self . delivery_readiness . is_some ( )
11271224 || self . driver . claude . is_some ( )
11281225 || self . driver . codex . is_some ( )
11291226 // pi predates this predicate gaining driver awareness and was silently skipped too:
@@ -1171,26 +1268,43 @@ impl RawSpec {
11711268 . transpose ( ) ?;
11721269 let driver = self . driver . lower ( & identity) ?;
11731270 let has_driver = driver. is_some ( ) ;
1271+ let mut delivery_readiness = self . delivery_readiness ;
1272+ if let Some ( readiness) = delivery_readiness. as_mut ( ) {
1273+ readiness. validate ( ) ?;
1274+ }
11741275 anyhow:: ensure!(
11751276 !( self . ding && delivery. is_some( ) ) ,
11761277 "agent '{identity}' declares both `ding` and `deliver`; choose one transport"
11771278 ) ;
11781279 anyhow:: ensure!(
1179- !( self . ding && has_driver) ,
1180- "agent '{identity}' declares both `ding` and a typed driver; choose one session owner "
1280+ !( self . ding && ( has_driver || session_driver . is_some ( ) || delivery_readiness . is_some ( ) ) ) ,
1281+ "agent '{identity}' declares managed native delivery together with `ding`; generic Ding is only for opaque non-harness PTYs "
11811282 ) ;
1182- if session_driver. is_some ( ) {
1183- anyhow:: ensure!(
1184- !self . ding,
1185- "agent '{identity}' declares both `session-driver` and `ding`; choose one session owner"
1186- ) ;
1283+ anyhow:: ensure!(
1284+ !( session_driver. is_some( ) && has_driver) ,
1285+ "agent '{identity}' declares both `session-driver` and a typed driver; choose one session owner"
1286+ ) ;
1287+ let effective_session_driver =
1288+ session_driver. or_else ( || driver. as_ref ( ) . map ( Driver :: session_driver) ) ;
1289+ if let ( Some ( delivery) , Some ( effective) ) = ( delivery, effective_session_driver) {
11871290 anyhow:: ensure!(
1188- delivery. is_none( ) ,
1189- "agent '{identity}' declares both `session-driver` and `deliver`; choose one session owner"
1291+ delivery. session_driver( ) == effective,
1292+ "agent '{identity}' delivery transport '{}' requires session-driver '{}', not '{}'" ,
1293+ delivery. as_str( ) ,
1294+ delivery. session_driver( ) . as_str( ) ,
1295+ effective. as_str( )
11901296 ) ;
1297+ }
1298+ if let (
1299+ Some ( DeliveryReadiness :: Anonymous { harness, .. } ) ,
1300+ Some ( effective) ,
1301+ ) = ( delivery_readiness. as_ref ( ) , effective_session_driver)
1302+ {
11911303 anyhow:: ensure!(
1192- !has_driver,
1193- "agent '{identity}' declares both `session-driver` and a typed driver; choose one session owner"
1304+ * harness == effective,
1305+ "agent '{identity}' anonymous delivery-readiness harness '{}' does not match effective session-driver '{}'" ,
1306+ harness. as_str( ) ,
1307+ effective. as_str( )
11941308 ) ;
11951309 }
11961310 validate_launch (
@@ -1345,6 +1459,7 @@ impl RawSpec {
13451459 delivery,
13461460 session_driver,
13471461 driver,
1462+ delivery_readiness,
13481463 resources,
13491464 streams,
13501465 tasks,
0 commit comments