@@ -56,6 +56,8 @@ struct Admission {
5656 #[ serde( default ) ]
5757 protection : Option < Protection > ,
5858 workspace_id : Option < String > ,
59+ #[ serde( default ) ]
60+ target_id : Option < String > ,
5961 success : Option < Success > ,
6062 completed_at_ms : Option < i64 > ,
6163}
@@ -65,6 +67,8 @@ struct Protection {
6567 key_id : String ,
6668 effective_payload_hash : String ,
6769 effective_workspace_id : Option < String > ,
70+ #[ serde( default ) ]
71+ effective_target_id : Option < String > ,
6872}
6973
7074/// Gateway-owned, in-memory input before hydration and interceptor modification.
@@ -78,19 +82,27 @@ pub(super) enum Success {
7882 Resource { id : String , version : u64 } ,
7983 Deletion { outcome : i32 } ,
8084 Ordinary ( ordinary:: Outcome ) ,
85+ StreamTerminal ,
8186}
8287
8388pub ( super ) struct Scope {
8489 name : String ,
8590 workspace_id : Option < String > ,
91+ // Name-addressed exec requests must remain bound to one sandbox instance.
92+ target_id : Option < String > ,
8693}
8794
8895#[ tonic:: async_trait]
8996pub ( super ) trait Mutation : Message + Default + Send + Sync + ' static {
90- type Output : Message + Default + Send + ' static ;
97+ type Output : Send + ' static ;
9198 const METHOD : & ' static str ;
9299 const PROTECTED : bool = false ;
100+ // Streaming producers, not returned stream handles, confirm completion.
101+ const DEFERRED : bool = false ;
93102 fn request_id ( & self ) -> & str ;
103+ fn canonical_message ( & self ) -> Result < DynamicMessage , Status > {
104+ decode_request_message ( & format ! ( "{}Request" , Self :: METHOD ) , self )
105+ }
94106 async fn authorize ( & self , state : & ServerState , principal : & Principal ) -> Result < Scope , Status > ;
95107 async fn execute (
96108 state : & Arc < ServerState > ,
@@ -141,6 +153,7 @@ pub(super) async fn run<M: Mutation>(
141153 key_id : key. id ( ) ,
142154 effective_payload_hash : key. fingerprint ( request. get_ref ( ) ) ?,
143155 effective_workspace_id : effective_scope. workspace_id ,
156+ effective_target_id : effective_scope. target_id ,
144157 } ) ,
145158 )
146159 } else {
@@ -207,11 +220,12 @@ async fn execute_owned<M: Mutation>(
207220 protection : Option < Protection > ,
208221 scope : Scope ,
209222) -> Result < Response < M :: Output > , Status > {
210- let mut admission = Admission {
223+ let admission = Admission {
211224 format_version : 1 ,
212225 payload_hash,
213226 protection,
214227 workspace_id : scope. workspace_id ,
228+ target_id : scope. target_id ,
215229 success : None ,
216230 completed_at_ms : None ,
217231 } ;
@@ -256,6 +270,7 @@ async fn execute_owned<M: Mutation>(
256270 ) ) ;
257271 }
258272 if previous. workspace_id != admission. workspace_id
273+ || previous. target_id != admission. target_id
259274 || previous. protection != admission. protection
260275 {
261276 return Err ( replay_unavailable ( ) ) ;
@@ -302,32 +317,85 @@ async fn execute_owned<M: Mutation>(
302317 } ;
303318 // Any error or panic from here leaves the claim unresolved. Status codes
304319 // do not establish that a handler performed no effects.
320+ let completion = Completion {
321+ store : state. store . clone ( ) ,
322+ claim_id,
323+ key : key. into ( ) ,
324+ bucket : bucket. into ( ) ,
325+ version,
326+ admission : payload,
327+ } ;
328+ if M :: DEFERRED {
329+ request. extensions_mut ( ) . insert ( completion. clone ( ) ) ;
330+ }
305331 let facts = ordinary:: Facts :: default ( ) ;
306332 request. extensions_mut ( ) . insert ( facts. clone ( ) ) ;
307333 let mut response = M :: execute ( state, request) . await ?;
334+ if M :: DEFERRED {
335+ return Ok ( response) ;
336+ }
308337 response. extensions_mut ( ) . insert ( facts) ;
309- admission. success = Some ( M :: capture ( & response) ?) ;
338+ completion. complete ( M :: capture ( & response) ?) . await ?;
339+ return Ok ( response) ;
340+ }
341+ Err ( uncertain ( ) )
342+ }
343+
344+ /// Only the admitted owner can finalize this exact incarnation of a claim.
345+ /// A dropped finalizer leaves a permanent unresolved admission, never a lease.
346+ #[ derive( Clone ) ]
347+ pub ( super ) struct Completion {
348+ store : Arc < Store > ,
349+ claim_id : String ,
350+ key : String ,
351+ bucket : String ,
352+ version : u64 ,
353+ admission : Vec < u8 > ,
354+ }
355+
356+ impl Completion {
357+ /// Reject replacement between admission authorization and relay creation.
358+ pub ( super ) fn ensure_target ( & self , target_id : & str ) -> Result < ( ) , Status > {
359+ let admission: Admission =
360+ serde_json:: from_slice ( & self . admission ) . map_err ( |_| uncertain ( ) ) ?;
361+ if admission
362+ . protection
363+ . as_ref ( )
364+ . and_then ( |protection| protection. effective_target_id . as_deref ( ) )
365+ != Some ( target_id)
366+ {
367+ return Err ( uncertain ( ) ) ;
368+ }
369+ Ok ( ( ) )
370+ }
371+
372+ pub ( super ) async fn stream_terminal ( & self ) -> Result < ( ) , Status > {
373+ self . complete ( Success :: StreamTerminal ) . await
374+ }
375+
376+ async fn complete ( & self , success : Success ) -> Result < ( ) , Status > {
377+ let mut admission: Admission =
378+ serde_json:: from_slice ( & self . admission ) . map_err ( |_| uncertain ( ) ) ?;
379+ admission. success = Some ( success) ;
310380 admission. completed_at_ms = Some ( current_time_ms ( ) ) ;
311381 let payload = serde_json:: to_vec ( & admission) . map_err ( |_| uncertain ( ) ) ?;
312382 if payload. len ( ) > 64 * 1024 {
313383 return Err ( uncertain ( ) ) ;
314384 }
315- state
316- . store
385+ self . store
317386 . put_if (
318387 OBJECT_TYPE ,
319- & claim_id,
320- key,
321- bucket,
388+ & self . claim_id ,
389+ & self . key ,
390+ & self . bucket ,
322391 & payload,
323392 None ,
324- WriteCondition :: MatchResourceVersion ( version) ,
393+ WriteCondition :: MatchResourceVersion ( self . version ) ,
325394 )
326395 . await
327396 . map_err ( |_| uncertain ( ) ) ?;
328- return Ok ( response ) ;
397+ Ok ( ( ) )
329398 }
330- Err ( uncertain ( ) )
331399}
332400
333401async fn prune_expired ( store : & Store , bucket : & str ) -> Result < bool , Status > {
@@ -377,12 +445,16 @@ fn validate_request_id(value: &str) -> Result<String, Status> {
377445 Ok ( id. hyphenated ( ) . to_string ( ) )
378446}
379447
380- fn fingerprint < M : Mutation > ( request : & M ) -> Result < String , Status > {
448+ fn decode_request_message ( name : & str , request : & impl Message ) -> Result < DynamicMessage , Status > {
381449 let descriptor = DESCRIPTORS
382- . get_message_by_name ( & format ! ( "openshell.v1.{}Request" , M :: METHOD ) )
450+ . get_message_by_name ( & format ! ( "openshell.v1.{name}" ) )
383451 . ok_or_else ( || Status :: internal ( "mutation request descriptor missing" ) ) ?;
384- let mut message = DynamicMessage :: decode ( descriptor, request. encode_to_vec ( ) . as_slice ( ) )
385- . map_err ( |_| Status :: internal ( "decode mutation request" ) ) ?;
452+ DynamicMessage :: decode ( descriptor, request. encode_to_vec ( ) . as_slice ( ) )
453+ . map_err ( |_| Status :: internal ( "decode mutation request" ) )
454+ }
455+
456+ fn fingerprint < M : Mutation > ( request : & M ) -> Result < String , Status > {
457+ let mut message = request. canonical_message ( ) ?;
386458 message. clear_field_by_name ( "request_id" ) ;
387459 let value = serde_json:: to_value ( message)
388460 . map_err ( |_| Status :: internal ( "canonicalize mutation request" ) ) ?;
@@ -492,6 +564,7 @@ async fn named_scope(state: &ServerState, name: &str) -> Result<Scope, Status> {
492564 Ok ( Scope {
493565 name : name. into ( ) ,
494566 workspace_id : Some ( resource. object_id ( ) . into ( ) ) ,
567+ target_id : None ,
495568 } )
496569}
497570
@@ -500,6 +573,7 @@ fn global_scope(state: &ServerState, principal: &Principal) -> Result<Scope, Sta
500573 Ok ( Scope {
501574 name : String :: new ( ) ,
502575 workspace_id : None ,
576+ target_id : None ,
503577 } )
504578}
505579
@@ -719,3 +793,4 @@ deletion_mutation!(
719793mod tests;
720794
721795pub ( super ) mod ordinary;
796+ pub ( super ) mod streaming;
0 commit comments