44#![ cfg( feature = "e2e" ) ]
55
66//! Local-driver E2E regression for sandbox bootstrap JWT material and terminal
7- //! main-process semantics. Local single-player gateways mint non-expiring
8- //! bootstrap tokens. Stopping a Docker or Podman sandbox container is terminal
7+ //! main-process semantics. Launch-scoped session tokens have finite lifetimes.
8+ //! Stopping a Docker or Podman sandbox container is terminal
99//! and must not relaunch its canonical process; restarting the VM gateway still
1010//! exercises driver recovery without deliberately stopping the sandbox.
1111
1212use std:: fs;
1313use std:: path:: PathBuf ;
1414use std:: process:: Stdio ;
15- use std:: time:: { Duration , Instant } ;
15+ use std:: time:: { Duration , Instant , SystemTime , UNIX_EPOCH } ;
1616
1717use base64:: Engine as _;
1818use openshell_e2e:: harness:: cli:: { wait_for_healthy, wait_for_sandbox_exec_contains} ;
@@ -63,16 +63,17 @@ impl LocalDriver {
6363 matches ! ( self , Self :: Docker | Self :: Podman )
6464 }
6565
66- fn container_filters ( self , namespace : & str , sandbox_name : & str ) -> Vec < String > {
66+ fn container_filters ( self , namespace : & str , sandbox_name : & str , role : & str ) -> Vec < String > {
6767 match self {
6868 Self :: Docker => vec ! [
6969 "label=openshell.ai/managed-by=openshell" . to_string( ) ,
70- "label=openshell.ai/isolation-role=sandbox" . to_string ( ) ,
70+ format! ( "label=openshell.ai/isolation-role={role}" ) ,
7171 format!( "label=openshell.ai/sandbox-namespace={namespace}" ) ,
7272 format!( "label=openshell.ai/sandbox-name={sandbox_name}" ) ,
7373 ] ,
7474 Self :: Podman => vec ! [
7575 "label=openshell.managed=true" . to_string( ) ,
76+ format!( "label=openshell.io/isolation-role={role}" ) ,
7677 format!( "label=openshell.ai/sandbox-name={sandbox_name}" ) ,
7778 ] ,
7879 Self :: Vm => Vec :: new ( ) ,
@@ -107,9 +108,10 @@ fn sandbox_container_id(
107108 driver : LocalDriver ,
108109 namespace : & str ,
109110 sandbox_name : & str ,
111+ role : & str ,
110112) -> Result < String , String > {
111113 let mut args = vec ! [ "ps" . to_string( ) , "-aq" . to_string( ) ] ;
112- for filter in driver. container_filters ( namespace, sandbox_name) {
114+ for filter in driver. container_filters ( namespace, sandbox_name, role ) {
113115 args. push ( "--filter" . to_string ( ) ) ;
114116 args. push ( filter) ;
115117 }
@@ -176,15 +178,19 @@ async fn wait_for_container_running(
176178}
177179
178180fn read_bootstrap_token ( engine : & ContainerEngine , container_id : & str ) -> Result < String , String > {
181+ // The Podman supervisor image has no shell tools. Copy its private secret
182+ // out through Podman's archive API rather than execing `cat` inside it.
183+ let temporary = tempfile:: tempdir ( ) . map_err ( |err| format ! ( "create token tempdir: {err}" ) ) ?;
184+ let destination = temporary. path ( ) . join ( "sandbox.jwt" ) ;
179185 run_engine (
180186 engine,
181187 & [
182- "exec" . to_string ( ) ,
183- container_id. to_string ( ) ,
184- "cat" . to_string ( ) ,
185- CONTAINER_TOKEN_MOUNT_PATH . to_string ( ) ,
188+ "cp" . to_string ( ) ,
189+ format ! ( "{container_id}:{CONTAINER_TOKEN_MOUNT_PATH}" ) ,
190+ destination. to_string_lossy ( ) . into_owned ( ) ,
186191 ] ,
187- )
192+ ) ?;
193+ fs:: read_to_string ( & destination) . map_err ( |err| format ! ( "read copied bootstrap token: {err}" ) )
188194}
189195
190196fn read_vm_bootstrap_token ( sandbox_name : & str ) -> Result < String , String > {
@@ -260,10 +266,16 @@ fn token_exp_claim(token: &str) -> Result<i64, String> {
260266 . ok_or_else ( || format ! ( "sandbox JWT missing integer exp claim: {claims}" ) )
261267}
262268
263- fn require_non_expiring_token ( token : & str , context : & str ) -> Result < ( ) , String > {
269+ fn require_finite_session_token ( token : & str , context : & str ) -> Result < ( ) , String > {
264270 let exp = token_exp_claim ( token) ?;
265- if exp != 0 {
266- return Err ( format ! ( "{context} should use exp=0, got exp={exp}" ) ) ;
271+ let now = SystemTime :: now ( )
272+ . duration_since ( UNIX_EPOCH )
273+ . map_err ( |err| format ! ( "read current time: {err}" ) ) ?
274+ . as_secs ( ) as i64 ;
275+ if exp <= now || exp > now + 3600 {
276+ return Err ( format ! (
277+ "{context} should expire within one hour, got exp={exp}, now={now}"
278+ ) ) ;
267279 }
268280 Ok ( ( ) )
269281}
@@ -274,8 +286,25 @@ async fn stop_container_sandbox(
274286 namespace : & str ,
275287 sandbox_name : & str ,
276288) -> Result < ( ) , String > {
277- let container_id = sandbox_container_id ( engine, driver, namespace, sandbox_name) ?;
278- if driver == LocalDriver :: Docker {
289+ let container_id = sandbox_container_id ( engine, driver, namespace, sandbox_name, "sandbox" ) ?;
290+ if driver == LocalDriver :: Podman {
291+ let temporary =
292+ tempfile:: tempdir ( ) . map_err ( |err| format ! ( "create token tempdir: {err}" ) ) ?;
293+ let destination = temporary. path ( ) . join ( "workload-sandbox.jwt" ) ;
294+ let copy_result = run_engine (
295+ engine,
296+ & [
297+ "cp" . to_string ( ) ,
298+ format ! ( "{container_id}:{CONTAINER_TOKEN_MOUNT_PATH}" ) ,
299+ destination. to_string_lossy ( ) . into_owned ( ) ,
300+ ] ,
301+ ) ;
302+ match copy_result {
303+ Err ( error) if error. contains ( "no such file or directory" ) => { }
304+ Err ( error) => return Err ( format ! ( "inspect workload bootstrap JWT path: {error}" ) ) ,
305+ Ok ( _) => return Err ( "sandbox workload contains its bootstrap JWT" . to_string ( ) ) ,
306+ }
307+ } else {
279308 run_engine (
280309 engine,
281310 & [
@@ -287,11 +316,14 @@ async fn stop_container_sandbox(
287316 ] ,
288317 )
289318 . map_err ( |error| {
290- format ! ( "Docker sandbox workload must not be able to read its bootstrap JWT: {error}" )
319+ format ! ( "sandbox workload must not be able to read its bootstrap JWT: {error}" )
291320 } ) ?;
292- } else {
293- let token = read_bootstrap_token ( engine, & container_id) ?;
294- require_non_expiring_token ( & token, "local-driver bootstrap JWT" ) ?;
321+ }
322+ if driver == LocalDriver :: Podman {
323+ let supervisor_id =
324+ sandbox_container_id ( engine, driver, namespace, sandbox_name, "supervisor" ) ?;
325+ let token = read_bootstrap_token ( engine, & supervisor_id) ?;
326+ require_finite_session_token ( & token, "local-driver session JWT" ) ?;
295327 }
296328
297329 run_engine ( engine, & [ "stop" . to_string ( ) , container_id. clone ( ) ] ) ?;
@@ -327,7 +359,7 @@ async fn wait_for_sandbox_error(sandbox_name: &str, timeout: Duration) -> Result
327359
328360async fn restart_vm_sandbox ( gateway : & ManagedGateway , sandbox_name : & str ) -> Result < ( ) , String > {
329361 let token = read_vm_bootstrap_token ( sandbox_name) ?;
330- require_non_expiring_token ( & token, "VM bootstrap JWT" ) ?;
362+ require_finite_session_token ( & token, "VM session JWT" ) ?;
331363
332364 gateway. stop ( ) ?;
333365 gateway. start ( ) ?;
0 commit comments