Retain prompt resume resize until route transition

This commit is contained in:
ms2k 2026-07-17 07:27:47 +09:00
commit 29bc02fd7b
2 changed files with 84 additions and 13 deletions

View file

@ -188,12 +188,14 @@ struct PendingPrompt {
authority: VerifiedSession, authority: VerifiedSession,
prompt: TerminalPrompt, prompt: TerminalPrompt,
emitted: bool, emitted: bool,
resumed_size: Option<TerminalResize>,
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct RoutedPromptDecision { pub struct RoutedPromptDecision {
pub signed_decision: Vec<u8>, pub signed_decision: Vec<u8>,
pub decision: DeveloperDecision, pub decision: DeveloperDecision,
pub resumed_size: Option<TerminalResize>,
} }
pub struct SessionPromptTransport { pub struct SessionPromptTransport {
@ -264,6 +266,7 @@ impl SessionPromptTransport {
authority, authority,
prompt: prompt.clone(), prompt: prompt.clone(),
emitted: false, emitted: false,
resumed_size: None,
}, },
); );
Ok(prompt) Ok(prompt)
@ -288,7 +291,7 @@ impl SessionPromptTransport {
&self, &self,
session_id: SessionId, session_id: SessionId,
signed_decision: Vec<u8>, signed_decision: Vec<u8>,
) -> Result<DeveloperDecision, TerminalError> { ) -> Result<RoutedPromptDecision, TerminalError> {
let mut prompts = self let mut prompts = self
.pending .pending
.lock() .lock()
@ -304,19 +307,40 @@ impl SessionPromptTransport {
pending.prompt.request_sha256, pending.prompt.request_sha256,
) )
.map_err(|_| TerminalError::PromptValidation)?; .map_err(|_| TerminalError::PromptValidation)?;
let resumed_size = pending.resumed_size;
prompts.remove(&session_id); prompts.remove(&session_id);
drop(prompts); Ok(RoutedPromptDecision {
signed_decision,
decision,
resumed_size,
})
}
fn retain_prompt_resize(&self, resize: TerminalResize) -> Result<bool, TerminalError> {
let mut prompts = self
.pending
.lock()
.map_err(|_| TerminalError::PromptValidation)?;
let Some(pending) = prompts.get_mut(&resize.session_id) else {
return Ok(false);
};
if !pending.emitted {
return Err(TerminalError::InactiveRoute);
}
pending.resumed_size = Some(resize);
Ok(true)
}
fn publish_decision(
&self,
session_id: SessionId,
decision: RoutedPromptDecision,
) -> Result<(), TerminalError> {
self.routed self.routed
.lock() .lock()
.map_err(|_| TerminalError::PromptValidation)? .map_err(|_| TerminalError::PromptValidation)?
.insert( .insert(session_id, decision);
session_id, Ok(())
RoutedPromptDecision {
signed_decision,
decision: decision.clone(),
},
);
Ok(decision)
} }
fn restore_sandbox_route(&self, session_id: SessionId) -> Result<(), TerminalError> { fn restore_sandbox_route(&self, session_id: SessionId) -> Result<(), TerminalError> {
@ -719,6 +743,19 @@ impl InteractiveControlSession for RunscInteractiveControlSession {
) -> Result<InteractiveSessionBatch, ControlError> { ) -> Result<InteractiveSessionBatch, ControlError> {
let mut engine = self.engine.lock().map_err(|_| ControlError::LockPoisoned)?; let mut engine = self.engine.lock().map_err(|_| ControlError::LockPoisoned)?;
let mut messages = match event { let mut messages = match event {
InteractiveSessionEvent::Terminal(SessionTerminalEvent::Resize(resize)) => {
if self
.prompt_transport
.retain_prompt_resize(resize)
.map_err(|error| terminal_control_error(&error))?
{
Vec::new()
} else {
engine
.handle(self.generation_id, SessionTerminalEvent::Resize(resize))
.map_err(|error| terminal_control_error(&error))?
}
}
InteractiveSessionEvent::Terminal(event) => engine InteractiveSessionEvent::Terminal(event) => engine
.handle(self.generation_id, event) .handle(self.generation_id, event)
.map_err(|error| terminal_control_error(&error))?, .map_err(|error| terminal_control_error(&error))?,
@ -733,14 +770,23 @@ impl InteractiveControlSession for RunscInteractiveControlSession {
.prompt_transport .prompt_transport
.route_decision(session_id, signed_decision) .route_decision(session_id, signed_decision)
.map_err(|error| terminal_control_error(&error))?; .map_err(|error| terminal_control_error(&error))?;
if decision.decision == lazaret_core::records::DeveloperDecisionKind::Deny { if decision.decision.decision == lazaret_core::records::DeveloperDecisionKind::Deny
{
self.prompt_transport self.prompt_transport
.restore_sandbox_route(session_id) .restore_sandbox_route(session_id)
.map_err(|error| terminal_control_error(&error))?; .map_err(|error| terminal_control_error(&error))?;
engine engine
.set_route(session_id, self.generation_id, TerminalRoute::Sandbox) .set_route(session_id, self.generation_id, TerminalRoute::Sandbox)
.map_err(|error| terminal_control_error(&error))?; .map_err(|error| terminal_control_error(&error))?;
if let Some(resize) = decision.resumed_size {
engine
.handle(self.generation_id, SessionTerminalEvent::Resize(resize))
.map_err(|error| terminal_control_error(&error))?;
}
} }
self.prompt_transport
.publish_decision(session_id, decision)
.map_err(|error| terminal_control_error(&error))?;
Vec::new() Vec::new()
} }
InteractiveSessionEvent::PromptDecision { .. } InteractiveSessionEvent::PromptDecision { .. }

View file

@ -539,7 +539,7 @@ fn engine_for(fixture: &Fixture) -> (Arc<MockFactory>, SessionTerminalEngine) {
#[test] #[test]
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
fn production_prompt_adapter_routes_exact_authenticated_denial() { fn production_prompt_adapter_retains_resume_resize_before_authenticated_denial() {
let generation = GenerationIdentity { let generation = GenerationIdentity {
agent_profile: "trusted".into(), agent_profile: "trusted".into(),
domain_id: DomainId::new(bytes(1)), domain_id: DomainId::new(bytes(1)),
@ -607,7 +607,7 @@ fn production_prompt_adapter_routes_exact_authenticated_denial() {
let factory = Arc::new(MockFactory::default()); let factory = Arc::new(MockFactory::default());
let mut engine = SessionTerminalEngine::new( let mut engine = SessionTerminalEngine::new(
Arc::clone(&fixture.store), Arc::clone(&fixture.store),
factory, factory.clone(),
prompt_transport.clone(), prompt_transport.clone(),
); );
engine.acquire(&acquisition(binding.session_id)).unwrap(); engine.acquire(&acquisition(binding.session_id)).unwrap();
@ -640,6 +640,18 @@ fn production_prompt_adapter_routes_exact_authenticated_denial() {
batch.messages, batch.messages,
vec![InteractiveSessionMessage::Prompt(expected_prompt.clone())] vec![InteractiveSessionMessage::Prompt(expected_prompt.clone())]
); );
let resumed_size = TerminalResize {
session_id: binding.session_id,
rows: NonZeroU16::new(55).unwrap(),
columns: NonZeroU16::new(144).unwrap(),
pixel_width: 1_440,
pixel_height: 900,
};
control
.exchange(InteractiveSessionEvent::Terminal(
SessionTerminalEvent::Resize(resumed_size),
))
.unwrap();
let signed_decision = encode_signed( let signed_decision = encode_signed(
&DeveloperDecision { &DeveloperDecision {
protocol_version: ProtocolVersion::V1, protocol_version: ProtocolVersion::V1,
@ -664,6 +676,19 @@ fn production_prompt_adapter_routes_exact_authenticated_denial() {
.unwrap(); .unwrap();
assert_eq!(routed.signed_decision, signed_decision); assert_eq!(routed.signed_decision, signed_decision);
assert_eq!(routed.decision.decision, DeveloperDecisionKind::Deny); assert_eq!(routed.decision.decision, DeveloperDecisionKind::Deny);
assert_eq!(routed.resumed_size, Some(resumed_size));
assert_eq!(
factory.log.lock().unwrap().as_slice(),
["resize:55x144", "signal:WINCH"]
);
assert!(
!factory
.log
.lock()
.unwrap()
.iter()
.any(|event| event == "signal:TSTP")
);
assert_eq!( assert_eq!(
fixture fixture
.store .store