@@ -451,6 +451,30 @@ pub(crate) async fn sandbox_exec_without_exec(
451451///
452452/// This replaces the old rsync-based sync. Files are streamed as a tar archive
453453/// to `ssh ... tar xf - -C <dest>` on the sandbox side.
454+ fn sandbox_extract_command ( dest : & str , local_is_file : bool ) -> String {
455+ let dest = dest. trim_end_matches ( '/' ) ;
456+
457+ if local_is_file {
458+ let parent = dest
459+ . rfind ( '/' )
460+ . map_or ( "." , |pos| if pos == 0 { "/" } else { & dest[ ..pos] } ) ;
461+ let name = dest. rfind ( '/' ) . map_or ( dest, |pos| & dest[ pos + 1 ..] ) ;
462+
463+ format ! (
464+ "mkdir -p {parent} && cat | tar xf - -C {parent} && if [ \" {name}\" != \" $(basename {name_escaped})\" ]; then exit 1; fi" ,
465+ parent = shell_escape( parent) ,
466+ name = name,
467+ name_escaped = shell_escape( name) ,
468+ )
469+ } else {
470+ format ! (
471+ "mkdir -p {} && cat | tar xf - -C {}" ,
472+ shell_escape( dest) ,
473+ shell_escape( dest)
474+ )
475+ }
476+ }
477+
454478pub async fn sandbox_sync_up_files (
455479 server : & str ,
456480 name : & str ,
@@ -464,17 +488,14 @@ pub async fn sandbox_sync_up_files(
464488 }
465489
466490 let session = ssh_session_config ( server, name, tls) . await ?;
491+ let local_is_file = files. len ( ) == 1 && base_dir. join ( & files[ 0 ] ) . is_file ( ) ;
467492
468493 let mut ssh = ssh_base_command ( & session. proxy_command ) ;
469494 ssh. arg ( "-T" )
470495 . arg ( "-o" )
471496 . arg ( "RequestTTY=no" )
472497 . arg ( "sandbox" )
473- . arg ( format ! (
474- "mkdir -p {} && cat | tar xf - -C {}" ,
475- shell_escape( dest) ,
476- shell_escape( dest)
477- ) )
498+ . arg ( sandbox_extract_command ( dest, local_is_file) )
478499 . stdin ( Stdio :: piped ( ) )
479500 . stdout ( Stdio :: inherit ( ) )
480501 . stderr ( Stdio :: inherit ( ) ) ;
@@ -539,11 +560,7 @@ pub async fn sandbox_sync_up(
539560 . arg ( "-o" )
540561 . arg ( "RequestTTY=no" )
541562 . arg ( "sandbox" )
542- . arg ( format ! (
543- "mkdir -p {} && cat | tar xf - -C {}" ,
544- shell_escape( sandbox_path) ,
545- shell_escape( sandbox_path)
546- ) )
563+ . arg ( sandbox_extract_command ( sandbox_path, local_path. is_file ( ) ) )
547564 . stdin ( Stdio :: piped ( ) )
548565 . stdout ( Stdio :: inherit ( ) )
549566 . stderr ( Stdio :: inherit ( ) ) ;
@@ -1050,6 +1067,34 @@ mod tests {
10501067 use super :: * ;
10511068 use crate :: TEST_ENV_LOCK ;
10521069
1070+ #[ test]
1071+ fn sandbox_extract_command_uses_parent_dir_for_explicit_file_destination ( ) {
1072+ let command = sandbox_extract_command ( "/sandbox/.local/share/opencode/auth.json" , true ) ;
1073+
1074+ assert ! (
1075+ command. contains( "mkdir -p /sandbox/.local/share/opencode" ) ,
1076+ "expected parent directory creation, got: {command}"
1077+ ) ;
1078+ assert ! (
1079+ command. contains( "tar xf - -C /sandbox/.local/share/opencode" ) ,
1080+ "expected extraction in parent directory, got: {command}"
1081+ ) ;
1082+ assert ! (
1083+ !command. contains( "mkdir -p /sandbox/.local/share/opencode/auth.json" ) ,
1084+ "must not treat explicit file path as directory: {command}"
1085+ ) ;
1086+ }
1087+
1088+ #[ test]
1089+ fn sandbox_extract_command_keeps_directory_destinations_as_directories ( ) {
1090+ let command = sandbox_extract_command ( "/sandbox/src" , false ) ;
1091+
1092+ assert_eq ! (
1093+ command,
1094+ "mkdir -p /sandbox/src && cat | tar xf - -C /sandbox/src"
1095+ ) ;
1096+ }
1097+
10531098 #[ test]
10541099 fn upsert_host_block_appends_when_missing ( ) {
10551100 let input = "Host existing\n HostName example.com\n " ;
0 commit comments