Add interactive client terminal transport

This commit is contained in:
ms2k 2026-07-17 06:59:39 +09:00
commit 6816f0632e

View file

@ -22,7 +22,8 @@ use lazaret_core::protocol::{
};
use lazaret_core::records::{
ClientEnrollmentRequest, ClientEnrollmentResponse, ControlRequest, ControlResponse,
DurableDecision, GenerationIdentity, NonInteractiveSessionMessage,
DurableDecision, GenerationIdentity, InteractiveSessionBatch, InteractiveSessionEvent,
InteractiveSessionMessage, InteractiveSessionRequest, NonInteractiveSessionMessage,
NonInteractiveSessionRequest, ProtocolCredential, ProtocolCredentialRole,
ProtocolPublicationIdentity, ProtocolSubjectId, ProtocolVersion, PublicationOwnership,
SandboxCredentialBundle, Session, SessionBinding, SessionExit, SessionPhase,
@ -834,6 +835,7 @@ pub struct PublishedControlFace {
server_config: Arc<ServerConfig>,
registry: Arc<SessionRegistry>,
executor: Arc<dyn NonInteractiveSessionExecutor>,
interactive_executor: Arc<dyn InteractiveSessionExecutor>,
}
pub trait NonInteractiveSessionExecutor: Send + Sync {
@ -858,6 +860,35 @@ impl NonInteractiveSessionExecutor for ExecutionUnavailable {
}
}
pub trait InteractiveControlSession: Send {
/// # Errors
/// Returns an exact-session terminal, prompt-routing, or transport failure.
fn exchange(
&mut self,
event: InteractiveSessionEvent,
) -> Result<InteractiveSessionBatch, ControlError>;
}
pub trait InteractiveSessionExecutor: Send + Sync {
/// # Errors
/// Returns an acquisition or exact-session identity failure.
fn start(
&self,
request: &InteractiveSessionRequest,
) -> Result<Box<dyn InteractiveControlSession>, ControlError>;
}
struct InteractiveExecutionUnavailable;
impl InteractiveSessionExecutor for InteractiveExecutionUnavailable {
fn start(
&self,
_request: &InteractiveSessionRequest,
) -> Result<Box<dyn InteractiveControlSession>, ControlError> {
Err(ControlError::InvalidRequest)
}
}
pub struct RunscNonInteractiveExecutor {
program: OsString,
global_arguments: Vec<OsString>,
@ -1009,6 +1040,7 @@ pub struct StagedControlFace {
server_config: Arc<ServerConfig>,
registry: Arc<SessionRegistry>,
executor: Arc<dyn NonInteractiveSessionExecutor>,
interactive_executor: Arc<dyn InteractiveSessionExecutor>,
}
impl StagedControlFace {
@ -1045,6 +1077,28 @@ impl StagedControlFace {
anchor: TrustAnchor,
registry: Arc<SessionRegistry>,
executor: Arc<dyn NonInteractiveSessionExecutor>,
) -> Result<Self, ControlError> {
Self::stage_with_executors(
runtime_domain_root,
uid,
anchor,
registry,
executor,
Arc::new(InteractiveExecutionUnavailable),
)
}
/// Stages a control face with generation-bound command and terminal execution.
///
/// # Errors
/// Refuses the same invalid staging conditions as [`Self::stage`].
pub fn stage_with_executors(
runtime_domain_root: &Path,
uid: u32,
anchor: TrustAnchor,
registry: Arc<SessionRegistry>,
executor: Arc<dyn NonInteractiveSessionExecutor>,
interactive_executor: Arc<dyn InteractiveSessionExecutor>,
) -> Result<Self, ControlError> {
if registry.generation_identity != anchor.generation_identity {
return Err(ControlError::StaleGeneration);
@ -1098,6 +1152,7 @@ impl StagedControlFace {
server_config,
registry,
executor,
interactive_executor,
})
}
@ -1156,6 +1211,7 @@ impl StagedControlFace {
server_config: Arc::clone(&self.server_config),
registry: Arc::clone(&self.registry),
executor: Arc::clone(&self.executor),
interactive_executor: Arc::clone(&self.interactive_executor),
})
}
}
@ -1379,6 +1435,33 @@ impl PublishedControlFace {
.expose()
}
/// Publishes one generation control face with command and terminal execution.
///
/// # Errors
/// Refuses the same invalid publication conditions as [`Self::publish`].
pub fn publish_with_executors(
runtime_domain_root: &Path,
uid: u32,
anchor: TrustAnchor,
store: Arc<HostCoordinatorDomainStore>,
executor: Arc<dyn NonInteractiveSessionExecutor>,
interactive_executor: Arc<dyn InteractiveSessionExecutor>,
) -> Result<Self, ControlError> {
let registry = Arc::new(SessionRegistry::new(
store,
anchor.generation_identity.clone(),
));
StagedControlFace::stage_with_executors(
runtime_domain_root,
uid,
anchor,
registry,
executor,
interactive_executor,
)?
.expose()
}
#[must_use]
pub fn publication_identity(&self) -> &ProtocolPublicationIdentity {
&self.publication_identity
@ -1534,32 +1617,50 @@ impl PublishedControlFace {
}
Err(error) => return Err(error),
};
let ControlRequest::Execute(request) = request else {
return Err(ControlError::InvalidRequest);
};
if request.session_id != session_id
|| request.argv.is_empty()
|| request.argv[0].is_empty()
|| request.argv.iter().any(|argument| argument.contains(&0))
{
return Err(ControlError::InvalidRequest);
}
let mut send_message = |message: NonInteractiveSessionMessage| {
if matches!(message, NonInteractiveSessionMessage::Exit(_)) {
return Err(ControlError::InvalidRequest);
match request {
ControlRequest::Execute(request) => {
validate_command(request.session_id, session_id, &request.argv)?;
let mut send_message = |message: NonInteractiveSessionMessage| {
if matches!(message, NonInteractiveSessionMessage::Exit(_)) {
return Err(ControlError::InvalidRequest);
}
require_non_interactive_session(&message, request.session_id)?;
write_frame(stream, &message)
};
let outcome = self.executor.execute(&request, &mut send_message)?;
require_session_id(outcome.session_id(), request.session_id)?;
write_frame(stream, &NonInteractiveSessionMessage::Exit(outcome))
}
ControlRequest::ExecuteInteractive(request) => {
validate_command(request.session_id, session_id, &request.argv)?;
if request.initial_size.session_id != session_id {
return Err(ControlError::InvalidRequest);
}
let mut terminal = self.interactive_executor.start(&request)?;
let mut event = InteractiveSessionEvent::Poll { session_id };
loop {
let batch = terminal.exchange(event)?;
require_interactive_batch(&batch, session_id)?;
let complete = batch.messages.iter().any(|message| {
matches!(
message,
InteractiveSessionMessage::Terminal(
lazaret_core::records::SessionTerminalMessage::Exit(_)
)
)
});
write_frame(stream, &batch)?;
if complete {
break Ok(());
}
event = read_frame(stream, MAX_CONTROL_BYTES)?;
require_interactive_event(&event, session_id)?;
}
}
ControlRequest::Enroll(_) | ControlRequest::Register(_) => {
Err(ControlError::InvalidRequest)
}
require_non_interactive_session(&message, request.session_id)?;
write_frame(stream, &message)
};
let outcome = self.executor.execute(&request, &mut send_message)?;
if outcome.session_id() != request.session_id {
return Err(ControlError::InvalidRequest);
}
require_non_interactive_session(
&NonInteractiveSessionMessage::Exit(outcome),
request.session_id,
)?;
write_frame(stream, &NonInteractiveSessionMessage::Exit(outcome))
})();
let disconnected = disconnect(session_id);
result.and(disconnected)
@ -1738,6 +1839,61 @@ fn require_non_interactive_session(
}
}
fn validate_command(
actual: SessionId,
expected: SessionId,
argv: &[Vec<u8>],
) -> Result<(), ControlError> {
require_session_id(actual, expected)?;
if argv.is_empty() || argv[0].is_empty() || argv.iter().any(|argument| argument.contains(&0)) {
return Err(ControlError::InvalidRequest);
}
Ok(())
}
fn require_session_id(actual: SessionId, expected: SessionId) -> Result<(), ControlError> {
if actual == expected {
Ok(())
} else {
Err(ControlError::InvalidRequest)
}
}
fn require_interactive_event(
event: &InteractiveSessionEvent,
expected: SessionId,
) -> Result<(), ControlError> {
let actual = match event {
InteractiveSessionEvent::Terminal(event) => match event {
lazaret_core::records::SessionTerminalEvent::Input(event) => event.session_id,
lazaret_core::records::SessionTerminalEvent::Resize(event) => event.session_id,
lazaret_core::records::SessionTerminalEvent::Signal(event) => event.session_id,
lazaret_core::records::SessionTerminalEvent::Close(event) => event.session_id,
},
InteractiveSessionEvent::PromptDecision { session_id, .. }
| InteractiveSessionEvent::Poll { session_id } => *session_id,
};
require_session_id(actual, expected)
}
fn require_interactive_batch(
batch: &InteractiveSessionBatch,
expected: SessionId,
) -> Result<(), ControlError> {
require_session_id(batch.session_id, expected)?;
for message in &batch.messages {
let actual = match message {
InteractiveSessionMessage::Terminal(message) => match message {
lazaret_core::records::SessionTerminalMessage::Output(output) => output.session_id,
lazaret_core::records::SessionTerminalMessage::Exit(exit) => exit.session_id(),
},
InteractiveSessionMessage::Prompt(prompt) => prompt.session_id,
};
require_session_id(actual, expected)?;
}
Ok(())
}
fn leaf_parameters(
common_name: &str,
usage: ExtendedKeyUsagePurpose,