(
state: &Arc<ServerState>,
request: Request<ExecSandboxRequest>,
)
| 801 | // --------------------------------------------------------------------------- |
| 802 | |
| 803 | pub(super) async fn handle_exec_sandbox( |
| 804 | state: &Arc<ServerState>, |
| 805 | request: Request<ExecSandboxRequest>, |
| 806 | ) -> Result<Response<ReceiverStream<Result<ExecSandboxEvent, Status>>>, Status> { |
| 807 | use openshell_core::ObjectId; |
| 808 | |
| 809 | let req = request.into_inner(); |
| 810 | if req.sandbox_id.is_empty() { |
| 811 | return Err(Status::invalid_argument("sandbox_id is required")); |
| 812 | } |
| 813 | if req.command.is_empty() { |
| 814 | return Err(Status::invalid_argument("command is required")); |
| 815 | } |
| 816 | if req.environment.keys().any(|key| !is_valid_env_key(key)) { |
| 817 | return Err(Status::invalid_argument( |
| 818 | "environment keys must match ^[A-Za-z_][A-Za-z0-9_]*$", |
| 819 | )); |
| 820 | } |
| 821 | validate_exec_request_fields(&req)?; |
| 822 | |
| 823 | let sandbox = state |
| 824 | .store |
| 825 | .get_message::<Sandbox>(&req.sandbox_id) |
| 826 | .await |
| 827 | .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? |
| 828 | .ok_or_else(|| Status::not_found("sandbox not found"))?; |
| 829 | |
| 830 | if SandboxPhase::try_from(sandbox.phase()).ok() != Some(SandboxPhase::Ready) { |
| 831 | return Err(Status::failed_precondition("sandbox is not ready")); |
| 832 | } |
| 833 | |
| 834 | // Open a relay channel through the supervisor session. Use a 15s |
| 835 | // session-wait timeout, enough to cover a transient supervisor reconnect |
| 836 | // while still failing quickly during normal operation. |
| 837 | let (channel_id, relay_rx) = state |
| 838 | .supervisor_sessions |
| 839 | .open_relay(sandbox.object_id(), std::time::Duration::from_secs(15)) |
| 840 | .await |
| 841 | .map_err(|e| Status::unavailable(format!("supervisor relay failed: {e}")))?; |
| 842 | |
| 843 | let command_str = build_remote_exec_command(&req) |
| 844 | .map_err(|e| Status::invalid_argument(format!("command construction failed: {e}")))?; |
| 845 | let stdin_payload = req.stdin; |
| 846 | let timeout_seconds = req.timeout_seconds; |
| 847 | let request_tty = req.tty; |
| 848 | |
| 849 | let sandbox_id = sandbox.object_id().to_string(); |
| 850 | |
| 851 | let (tx, rx) = mpsc::channel::<Result<ExecSandboxEvent, Status>>(256); |
| 852 | tokio::spawn(async move { |
| 853 | // Wait for the supervisor's reverse CONNECT to deliver the relay stream. |
| 854 | let Some(relay_stream) = |
| 855 | await_relay_stream(relay_rx, &tx, &sandbox_id, &channel_id, "ExecSandbox").await |
| 856 | else { |
| 857 | return; |
| 858 | }; |
| 859 | |
| 860 | if let Err(err) = stream_exec_over_relay( |
no test coverage detected