Complete authenticated interactive prompt routing
This commit is contained in:
parent
6816f0632e
commit
46b52b4b43
4 changed files with 595 additions and 96 deletions
|
|
@ -34,6 +34,7 @@ use crate::runtime::{
|
|||
StateAnchorAbsenceObservation,
|
||||
};
|
||||
use crate::state_anchor::{StateAnchorCoordinator, StateAnchorReleaseAuthority};
|
||||
use crate::terminal::SessionPromptTransport;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProductionLifecycleError(String);
|
||||
|
|
@ -74,6 +75,7 @@ pub struct ProductionBootstrapPlan<'a> {
|
|||
pub invoking_uid: u32,
|
||||
pub non_interactive_executor: Arc<dyn NonInteractiveSessionExecutor>,
|
||||
pub interactive_executor: Arc<dyn InteractiveSessionExecutor>,
|
||||
pub prompt_transport: Arc<SessionPromptTransport>,
|
||||
pub(crate) runtime_staging: &'a crate::production::RuntimeStaging,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -325,6 +325,16 @@ impl<'a> ProductionColdStart<'a> {
|
|||
}
|
||||
|
||||
fn take_plan(&mut self) -> Result<ProductionBootstrapPlan<'_>, ProductionColdStartError> {
|
||||
let interactive_executor = RunscInteractiveSessionExecutor::new(
|
||||
Arc::clone(&self.store),
|
||||
&self.generation_identity,
|
||||
self.container_id,
|
||||
current_process_identity().map_err(failure)?,
|
||||
self.sandbox_bundle.command(),
|
||||
self.runtime_root.clone(),
|
||||
)
|
||||
.map_err(failure)?;
|
||||
let prompt_transport = interactive_executor.prompt_transport();
|
||||
Ok(ProductionBootstrapPlan {
|
||||
store: Arc::clone(&self.store),
|
||||
catalog: self.catalog,
|
||||
|
|
@ -350,17 +360,8 @@ impl<'a> ProductionColdStart<'a> {
|
|||
)
|
||||
.map_err(failure)?,
|
||||
),
|
||||
interactive_executor: Arc::new(
|
||||
RunscInteractiveSessionExecutor::new(
|
||||
Arc::clone(&self.store),
|
||||
&self.generation_identity,
|
||||
self.container_id,
|
||||
current_process_identity().map_err(failure)?,
|
||||
self.sandbox_bundle.command(),
|
||||
self.runtime_root.clone(),
|
||||
)
|
||||
.map_err(failure)?,
|
||||
),
|
||||
interactive_executor: Arc::new(interactive_executor),
|
||||
prompt_transport,
|
||||
runtime_staging: &self.staging,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,13 +13,14 @@ use lazaret_core::identity::{
|
|||
GuestProcessGroupId, GuestProcessId, GuestProcessStartTimeTicks, InodeId,
|
||||
PidfsProcessIdentityV1, SessionId,
|
||||
};
|
||||
use lazaret_core::protocol::VerifiedSession;
|
||||
use lazaret_core::records::{
|
||||
DomainStore, DurableDecision, GuestProcessIdentity, InteractiveSessionBatch,
|
||||
DeveloperDecision, DomainStore, DurableDecision, GuestProcessIdentity, InteractiveSessionBatch,
|
||||
InteractiveSessionEvent, InteractiveSessionMessage, InteractiveSessionRequest,
|
||||
ParticipantOwnership, ParticipantRole, ParticipantState, PersistedHostProcessOwnership,
|
||||
ProcessOwnershipCommitAck, ProtocolVersion, PtyIdentity, SessionExit, SessionPhase,
|
||||
SessionTerminal, SessionTerminalEvent, SessionTerminalFields, SessionTerminalMessage,
|
||||
SessionTerminalState, StagedProcessOwnership, TerminalOutput, TerminalResize,
|
||||
SessionTerminalState, StagedProcessOwnership, TerminalOutput, TerminalPrompt, TerminalResize,
|
||||
TerminalSignalKind,
|
||||
};
|
||||
use lazaret_linux::process::{
|
||||
|
|
@ -29,6 +30,7 @@ use lazaret_linux::process::{
|
|||
use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl, fstat};
|
||||
use rustix::pty::{OpenptFlags, grantpt, ioctl_tiocgptpeer, openpt, unlockpt};
|
||||
use rustix::termios::{Winsize, tcsetwinsize};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::control::{ControlError, InteractiveControlSession, InteractiveSessionExecutor};
|
||||
use crate::domain_store::{DomainStoreError, DomainStoreMutation, HostCoordinatorDomainStore};
|
||||
|
|
@ -87,6 +89,8 @@ pub enum TerminalError {
|
|||
GuestGroupStillPresent,
|
||||
InvalidExit,
|
||||
AuthorityMismatch,
|
||||
DuplicatePrompt,
|
||||
PromptValidation,
|
||||
AcquisitionCleanup {
|
||||
original: Box<TerminalError>,
|
||||
cleanup: Option<Box<TerminalError>>,
|
||||
|
|
@ -119,6 +123,8 @@ impl std::fmt::Display for TerminalError {
|
|||
Self::AuthorityMismatch => {
|
||||
formatter.write_str("Session Terminal runtime authority does not match")
|
||||
}
|
||||
Self::DuplicatePrompt => formatter.write_str("Session already has an active prompt"),
|
||||
Self::PromptValidation => formatter.write_str("prompt authority validation failed"),
|
||||
Self::AcquisitionCleanup { original, cleanup } => {
|
||||
write!(formatter, "Session Terminal acquisition failed: {original}")?;
|
||||
if let Some(cleanup) = cleanup {
|
||||
|
|
@ -146,7 +152,9 @@ impl std::error::Error for TerminalError {
|
|||
| Self::GuestIdentityMismatch
|
||||
| Self::GuestGroupStillPresent
|
||||
| Self::InvalidExit
|
||||
| Self::AuthorityMismatch => None,
|
||||
| Self::AuthorityMismatch
|
||||
| Self::DuplicatePrompt
|
||||
| Self::PromptValidation => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -176,6 +184,185 @@ impl PromptRouteCancellation for NoActivePrompt {
|
|||
}
|
||||
}
|
||||
|
||||
struct PendingPrompt {
|
||||
authority: VerifiedSession,
|
||||
prompt: TerminalPrompt,
|
||||
emitted: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct RoutedPromptDecision {
|
||||
pub signed_decision: Vec<u8>,
|
||||
pub decision: DeveloperDecision,
|
||||
}
|
||||
|
||||
pub struct SessionPromptTransport {
|
||||
store: Arc<HostCoordinatorDomainStore>,
|
||||
pending: Mutex<HashMap<SessionId, PendingPrompt>>,
|
||||
routed: Mutex<HashMap<SessionId, RoutedPromptDecision>>,
|
||||
}
|
||||
|
||||
impl SessionPromptTransport {
|
||||
#[must_use]
|
||||
pub fn new(store: Arc<HostCoordinatorDomainStore>) -> Self {
|
||||
Self {
|
||||
store,
|
||||
pending: Mutex::new(HashMap::new()),
|
||||
routed: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Queues one sandbox-signed request for its exact registered Session.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses invalid authority, stale Session state, and concurrent prompts.
|
||||
pub fn queue_signed_request(
|
||||
&self,
|
||||
authority: VerifiedSession,
|
||||
signed_request: &[u8],
|
||||
) -> Result<TerminalPrompt, TerminalError> {
|
||||
let (request, digest) = authority
|
||||
.verify_request(signed_request)
|
||||
.map_err(|_| TerminalError::PromptValidation)?;
|
||||
let binding = authority.session_binding();
|
||||
let anchor_id = lazaret_core::identity::SessionAnchorId::new(
|
||||
Sha256::digest(&binding.session_anchor_public_key).into(),
|
||||
);
|
||||
let snapshot = self.store.snapshot()?;
|
||||
let mut session = snapshot
|
||||
.sessions
|
||||
.into_iter()
|
||||
.find(|session| {
|
||||
session.generation_id == binding.generation_identity.generation_id
|
||||
&& session.session_id == binding.session_id
|
||||
&& session.cli_id == binding.cli_id
|
||||
&& session.session_anchor_id == anchor_id
|
||||
&& session.session_phase == SessionPhase::Attached
|
||||
})
|
||||
.ok_or(TerminalError::AuthorityMismatch)?;
|
||||
let prompt = TerminalPrompt {
|
||||
session_id: binding.session_id,
|
||||
request_id: request.request_id(),
|
||||
request_sha256: digest,
|
||||
agent_profile: binding.generation_identity.agent_profile.clone(),
|
||||
command_argv: request.command_argv().to_vec(),
|
||||
justification: request.justification().to_owned(),
|
||||
};
|
||||
let mut pending = self
|
||||
.pending
|
||||
.lock()
|
||||
.map_err(|_| TerminalError::PromptValidation)?;
|
||||
if pending.contains_key(&binding.session_id) {
|
||||
return Err(TerminalError::DuplicatePrompt);
|
||||
}
|
||||
session.session_phase = SessionPhase::Prompting;
|
||||
self.store
|
||||
.apply(&[DomainStoreMutation::PutSession(session)])?;
|
||||
pending.insert(
|
||||
binding.session_id,
|
||||
PendingPrompt {
|
||||
authority,
|
||||
prompt: prompt.clone(),
|
||||
emitted: false,
|
||||
},
|
||||
);
|
||||
Ok(prompt)
|
||||
}
|
||||
|
||||
fn prompt_for(&self, session_id: SessionId) -> Result<Option<TerminalPrompt>, TerminalError> {
|
||||
let mut pending = self
|
||||
.pending
|
||||
.lock()
|
||||
.map_err(|_| TerminalError::PromptValidation)?;
|
||||
let Some(pending) = pending.get_mut(&session_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if pending.emitted {
|
||||
return Ok(None);
|
||||
}
|
||||
pending.emitted = true;
|
||||
Ok(Some(pending.prompt.clone()))
|
||||
}
|
||||
|
||||
fn route_decision(
|
||||
&self,
|
||||
session_id: SessionId,
|
||||
signed_decision: Vec<u8>,
|
||||
) -> Result<DeveloperDecision, TerminalError> {
|
||||
let mut prompts = self
|
||||
.pending
|
||||
.lock()
|
||||
.map_err(|_| TerminalError::PromptValidation)?;
|
||||
let pending = prompts
|
||||
.get(&session_id)
|
||||
.ok_or(TerminalError::InactiveRoute)?;
|
||||
let decision = pending
|
||||
.authority
|
||||
.verify_developer_decision(
|
||||
&signed_decision,
|
||||
pending.prompt.request_id,
|
||||
pending.prompt.request_sha256,
|
||||
)
|
||||
.map_err(|_| TerminalError::PromptValidation)?;
|
||||
prompts.remove(&session_id);
|
||||
drop(prompts);
|
||||
self.routed
|
||||
.lock()
|
||||
.map_err(|_| TerminalError::PromptValidation)?
|
||||
.insert(
|
||||
session_id,
|
||||
RoutedPromptDecision {
|
||||
signed_decision,
|
||||
decision: decision.clone(),
|
||||
},
|
||||
);
|
||||
Ok(decision)
|
||||
}
|
||||
|
||||
fn restore_sandbox_route(&self, session_id: SessionId) -> Result<(), TerminalError> {
|
||||
let snapshot = self.store.snapshot()?;
|
||||
let mut session = snapshot
|
||||
.sessions
|
||||
.into_iter()
|
||||
.find(|session| session.session_id == session_id)
|
||||
.ok_or(TerminalError::UnknownSession)?;
|
||||
if session.session_phase != SessionPhase::Prompting {
|
||||
return Err(TerminalError::InactiveRoute);
|
||||
}
|
||||
session.session_phase = SessionPhase::Attached;
|
||||
self.store
|
||||
.apply(&[DomainStoreMutation::PutSession(session)])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
/// Refuses access after internal synchronization failure.
|
||||
pub fn take_decision(
|
||||
&self,
|
||||
session_id: SessionId,
|
||||
) -> Result<Option<RoutedPromptDecision>, TerminalError> {
|
||||
Ok(self
|
||||
.routed
|
||||
.lock()
|
||||
.map_err(|_| TerminalError::PromptValidation)?
|
||||
.remove(&session_id))
|
||||
}
|
||||
}
|
||||
|
||||
impl PromptRouteCancellation for SessionPromptTransport {
|
||||
fn cancel_for_close(&self, session_id: SessionId) -> Result<(), TerminalError> {
|
||||
self.pending
|
||||
.lock()
|
||||
.map_err(|_| TerminalError::PromptValidation)?
|
||||
.remove(&session_id);
|
||||
self.routed
|
||||
.lock()
|
||||
.map_err(|_| TerminalError::PromptValidation)?
|
||||
.remove(&session_id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_errors_doc)]
|
||||
pub trait SessionTerminalProcess: Send {
|
||||
fn write_input(&mut self, bytes: &[u8]) -> io::Result<usize>;
|
||||
|
|
@ -411,6 +598,7 @@ pub struct SessionTerminalEngine {
|
|||
|
||||
pub struct RunscInteractiveSessionExecutor {
|
||||
engine: Arc<Mutex<SessionTerminalEngine>>,
|
||||
prompt_transport: Arc<SessionPromptTransport>,
|
||||
domain_id: DomainId,
|
||||
generation_id: GenerationId,
|
||||
coordinator_id: CoordinatorId,
|
||||
|
|
@ -442,12 +630,14 @@ impl RunscInteractiveSessionExecutor {
|
|||
if run + 3 != command.arguments().len() {
|
||||
return Err(TerminalError::AuthorityMismatch);
|
||||
}
|
||||
let prompt_transport = Arc::new(SessionPromptTransport::new(Arc::clone(&store)));
|
||||
Ok(Self {
|
||||
engine: Arc::new(Mutex::new(SessionTerminalEngine::new(
|
||||
store,
|
||||
Arc::new(RunscSessionTerminalFactory),
|
||||
Arc::new(NoActivePrompt),
|
||||
prompt_transport.clone(),
|
||||
))),
|
||||
prompt_transport,
|
||||
domain_id: generation.domain_id,
|
||||
generation_id: generation.generation_id,
|
||||
coordinator_id: generation.coordinator_id,
|
||||
|
|
@ -458,6 +648,11 @@ impl RunscInteractiveSessionExecutor {
|
|||
runtime_root,
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn prompt_transport(&self) -> Arc<SessionPromptTransport> {
|
||||
Arc::clone(&self.prompt_transport)
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveSessionExecutor for RunscInteractiveSessionExecutor {
|
||||
|
|
@ -503,6 +698,7 @@ impl InteractiveSessionExecutor for RunscInteractiveSessionExecutor {
|
|||
drop(engine);
|
||||
Ok(Box::new(RunscInteractiveControlSession {
|
||||
engine: Arc::clone(&self.engine),
|
||||
prompt_transport: Arc::clone(&self.prompt_transport),
|
||||
generation_id: self.generation_id,
|
||||
session_id: request.session_id,
|
||||
}))
|
||||
|
|
@ -511,6 +707,7 @@ impl InteractiveSessionExecutor for RunscInteractiveSessionExecutor {
|
|||
|
||||
struct RunscInteractiveControlSession {
|
||||
engine: Arc<Mutex<SessionTerminalEngine>>,
|
||||
prompt_transport: Arc<SessionPromptTransport>,
|
||||
generation_id: GenerationId,
|
||||
session_id: SessionId,
|
||||
}
|
||||
|
|
@ -528,9 +725,40 @@ impl InteractiveControlSession for RunscInteractiveControlSession {
|
|||
InteractiveSessionEvent::Poll { session_id } if session_id == self.session_id => {
|
||||
Vec::new()
|
||||
}
|
||||
InteractiveSessionEvent::PromptDecision {
|
||||
session_id,
|
||||
signed_decision,
|
||||
} if session_id == self.session_id => {
|
||||
let decision = self
|
||||
.prompt_transport
|
||||
.route_decision(session_id, signed_decision)
|
||||
.map_err(|error| terminal_control_error(&error))?;
|
||||
if decision.decision == lazaret_core::records::DeveloperDecisionKind::Deny {
|
||||
self.prompt_transport
|
||||
.restore_sandbox_route(session_id)
|
||||
.map_err(|error| terminal_control_error(&error))?;
|
||||
engine
|
||||
.set_route(session_id, self.generation_id, TerminalRoute::Sandbox)
|
||||
.map_err(|error| terminal_control_error(&error))?;
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
InteractiveSessionEvent::PromptDecision { .. }
|
||||
| InteractiveSessionEvent::Poll { .. } => return Err(ControlError::InvalidRequest),
|
||||
};
|
||||
if let Some(prompt) = self
|
||||
.prompt_transport
|
||||
.prompt_for(self.session_id)
|
||||
.map_err(|error| terminal_control_error(&error))?
|
||||
{
|
||||
engine
|
||||
.set_route(self.session_id, self.generation_id, TerminalRoute::Prompt)
|
||||
.map_err(|error| terminal_control_error(&error))?;
|
||||
return Ok(InteractiveSessionBatch {
|
||||
session_id: self.session_id,
|
||||
messages: vec![InteractiveSessionMessage::Prompt(prompt)],
|
||||
});
|
||||
}
|
||||
if !messages
|
||||
.iter()
|
||||
.any(|message| matches!(message, SessionTerminalMessage::Exit(_)))
|
||||
|
|
|
|||
|
|
@ -4,18 +4,22 @@ use std::io;
|
|||
use std::num::NonZeroU16;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use ed25519_dalek::SigningKey;
|
||||
use lazaret_core::identity::{
|
||||
BootId, BundleDigest, ClientId, ConfigurationFingerprint, HostPath, HostPidLocator,
|
||||
NetworkSidecarId, PidfsDeviceId, PidfsInodeId, PidfsProcessAuthorityV1, ProcessStartTimeTicks,
|
||||
SandboxId, SessionAnchorId,
|
||||
RequestId, SandboxId, SessionAnchorId,
|
||||
};
|
||||
use lazaret_core::protocol::{VerifiedSession, encode_signed, issue_session_capability};
|
||||
use lazaret_core::records::{
|
||||
Domain, DurableDecision, Generation, Participant, SandboxOwnership, Session, TerminalInput,
|
||||
TerminalSignal,
|
||||
DeveloperDecisionKind, Domain, DurableDecision, Generation, GenerationIdentity, MailboxRequest,
|
||||
Participant, SandboxOwnership, Session, SessionBinding, TerminalInput, TerminalSignal,
|
||||
};
|
||||
use lazaret_linux::filesystem::try_exclusive_flock;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::control::TrustAnchor;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn bytes(value: u8) -> [u8; 32] {
|
||||
|
|
@ -533,6 +537,158 @@ fn engine_for(fixture: &Fixture) -> (Arc<MockFactory>, SessionTerminalEngine) {
|
|||
engine_for_store(store)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn production_prompt_adapter_routes_exact_authenticated_denial() {
|
||||
let generation = GenerationIdentity {
|
||||
agent_profile: "trusted".into(),
|
||||
domain_id: DomainId::new(bytes(1)),
|
||||
generation_id: GenerationId::new(7),
|
||||
configuration_fingerprint: ConfigurationFingerprint::new(bytes(2)),
|
||||
coordinator_id: CoordinatorId::new(bytes(3)),
|
||||
sandbox_id: SandboxId::new(bytes(5)),
|
||||
};
|
||||
let mut trust_anchor = TrustAnchor::create(generation.clone()).unwrap();
|
||||
let publication = trust_anchor.publication_identity();
|
||||
let coordinator_key = trust_anchor.coordinator_protocol_key();
|
||||
let credentials = trust_anchor.take_sandbox_credential_bundle().unwrap();
|
||||
let sandbox_key = SigningKey::from_bytes(
|
||||
credentials
|
||||
.sandbox_private_key
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
);
|
||||
let anchor_key = SigningKey::from_bytes(&bytes(61));
|
||||
let binding = SessionBinding {
|
||||
generation_identity: generation,
|
||||
cli_id: ClientId::new(bytes(62)),
|
||||
session_id: SessionId::new(bytes(63)),
|
||||
session_anchor_public_key: anchor_key.verifying_key().to_bytes().to_vec(),
|
||||
};
|
||||
let capability = issue_session_capability(
|
||||
binding.clone(),
|
||||
credentials.coordinator_protocol_credential().credential_id,
|
||||
credentials.sandbox_protocol_credential().credential_id,
|
||||
&coordinator_key,
|
||||
)
|
||||
.unwrap();
|
||||
let authority = VerifiedSession::new(
|
||||
publication.trust_manifest_sha256,
|
||||
credentials.trust_manifest.tls_ca_sha256,
|
||||
&credentials.trust_manifest,
|
||||
binding.clone(),
|
||||
credentials.coordinator_protocol_credential(),
|
||||
credentials.sandbox_protocol_credential(),
|
||||
capability.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
let fixture = Fixture::new(&[]);
|
||||
let session_anchor_id =
|
||||
SessionAnchorId::new(Sha256::digest(&binding.session_anchor_public_key).into());
|
||||
let mut session = Session {
|
||||
generation_id: GenerationId::new(7),
|
||||
session_id: binding.session_id,
|
||||
cli_id: binding.cli_id,
|
||||
session_anchor_id,
|
||||
session_phase: SessionPhase::Registering,
|
||||
};
|
||||
fixture
|
||||
.store
|
||||
.apply(&[DomainStoreMutation::PutSession(session.clone())])
|
||||
.unwrap();
|
||||
session.session_phase = SessionPhase::Attached;
|
||||
fixture
|
||||
.store
|
||||
.apply(&[DomainStoreMutation::PutSession(session)])
|
||||
.unwrap();
|
||||
|
||||
let prompt_transport = Arc::new(SessionPromptTransport::new(Arc::clone(&fixture.store)));
|
||||
let factory = Arc::new(MockFactory::default());
|
||||
let mut engine = SessionTerminalEngine::new(
|
||||
Arc::clone(&fixture.store),
|
||||
factory,
|
||||
prompt_transport.clone(),
|
||||
);
|
||||
engine.acquire(&acquisition(binding.session_id)).unwrap();
|
||||
let request = MailboxRequest::new(
|
||||
binding.clone(),
|
||||
RequestId::new(bytes(64)),
|
||||
capability,
|
||||
credentials.sandbox_protocol_credential().credential_id,
|
||||
vec![b"/usr/bin/tool".to_vec()],
|
||||
"update host metadata".into(),
|
||||
)
|
||||
.unwrap();
|
||||
let signed_request = encode_signed(&request, &sandbox_key).unwrap();
|
||||
let expected_prompt = prompt_transport
|
||||
.queue_signed_request(authority, &signed_request)
|
||||
.unwrap();
|
||||
let engine = Arc::new(Mutex::new(engine));
|
||||
let mut control = RunscInteractiveControlSession {
|
||||
engine: Arc::clone(&engine),
|
||||
prompt_transport: prompt_transport.clone(),
|
||||
generation_id: GenerationId::new(7),
|
||||
session_id: binding.session_id,
|
||||
};
|
||||
let batch = control
|
||||
.exchange(InteractiveSessionEvent::Poll {
|
||||
session_id: binding.session_id,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
batch.messages,
|
||||
vec![InteractiveSessionMessage::Prompt(expected_prompt.clone())]
|
||||
);
|
||||
let signed_decision = encode_signed(
|
||||
&DeveloperDecision {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding: binding.clone(),
|
||||
request_id: expected_prompt.request_id,
|
||||
request_sha256: expected_prompt.request_sha256,
|
||||
decision: DeveloperDecisionKind::Deny,
|
||||
approved_command_argv: Vec::new(),
|
||||
},
|
||||
&anchor_key,
|
||||
)
|
||||
.unwrap();
|
||||
control
|
||||
.exchange(InteractiveSessionEvent::PromptDecision {
|
||||
session_id: binding.session_id,
|
||||
signed_decision: signed_decision.clone(),
|
||||
})
|
||||
.unwrap();
|
||||
let routed = prompt_transport
|
||||
.take_decision(binding.session_id)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(routed.signed_decision, signed_decision);
|
||||
assert_eq!(routed.decision.decision, DeveloperDecisionKind::Deny);
|
||||
assert_eq!(
|
||||
fixture
|
||||
.store
|
||||
.snapshot()
|
||||
.unwrap()
|
||||
.sessions
|
||||
.into_iter()
|
||||
.find(|session| session.session_id == binding.session_id)
|
||||
.unwrap()
|
||||
.session_phase,
|
||||
SessionPhase::Attached
|
||||
);
|
||||
engine
|
||||
.lock()
|
||||
.unwrap()
|
||||
.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Input(TerminalInput {
|
||||
session_id: binding.session_id,
|
||||
bytes: b"sandbox-input".to_vec(),
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn engine_for_store(
|
||||
store: Arc<dyn TerminalDomainStore>,
|
||||
) -> (Arc<MockFactory>, SessionTerminalEngine) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue