Implement PTY-native session terminal control

This commit is contained in:
ms2k 2026-08-02 17:36:40 +09:00
commit 8cef037fa3
6 changed files with 1488 additions and 382 deletions

View file

@ -230,6 +230,7 @@ pub enum ControlRequest {
Register(SessionRegistrationRequest),
Execute(NonInteractiveSessionRequest),
ExecuteInteractive(super::InteractiveSessionRequest),
ExecuteInteractiveV2(super::InteractiveSessionRequestV2),
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]

View file

@ -4,25 +4,28 @@ use super::{
DefaultAction, DerivedNetworkPolicy, DeveloperDecision, DeveloperDecisionKind, Domain,
DomainStore, DomainStoreMetadata, EnvironmentReference, EscalationOutcome, Generation,
GenerationIdentity, GitRoutingOwnership, GuestProcessIdentity, HelperOwnership,
HostProcessIdentity, HostProcessOwnershipSlot, InteractiveSessionBatch,
InteractiveSessionEvent, InteractiveSessionMessage, InteractiveSessionRequest, Mailbox,
MailboxCancellation, MailboxRequest, MailboxResult, MailboxResultAck, MailboxResultChunk,
MountIdentity, NamespaceIdentity, NetworkCanaryObservation, NetworkObservation,
NetworkOwnership, NetworkReadinessAttestation, NetworkReadinessEvidence,
NetworkReadinessIdentity, NonInteractiveOutput, NonInteractiveSessionMessage,
NonInteractiveSessionRequest, Participant, ParticipantOwnership, ParticipantState,
PersistedHostProcessOwnership, PidfsProcessAuthorityV1, PidfsProcessIdentityV1, PolicyAction,
PolicyDelivery, PrefixRule, PrepareOperation, PrepareStageState, PreparedArtifact,
PreparedLowerOwnership, ProcessOwnershipCommitAck, ProcessOwnershipMessageVersion,
ProtocolCredential, ProtocolCredentialRole, ProtocolPublicationIdentity, PtyIdentity,
PublicationOwnership, ResolvedAutoApprovalRule, ResolvedVolume, Resolver, ResultStream,
RunscProcessOwnership, SandboxCredentialBundle, SandboxOwnership, Session, SessionBinding,
SessionClose, SessionExit, SessionExitOutcome, SessionRegistrationRequest,
SessionRegistrationResponse, SessionRequestCapability, SessionTerminal, SessionTerminalEvent,
SessionTerminalMessage, SessionTerminalState, SignedProtocolRecord, StagedProcessOwnership,
StateAnchorOwnership, StateAnchorReleaseEvidence, StorageCatalog, StorageCatalogMetadata,
StoreMetadata, TerminalInput, TerminalOutput, TerminalPrompt, TerminalResize, TerminalSignal,
TerminalSignalKind, TlsCredential, TlsRole, TrustManifest, TrustedHostConfigurationV1,
HostProcessIdentity, HostProcessOwnershipSlot, InteractiveProtocolVersion,
InteractiveSessionBatch, InteractiveSessionBatchV2, InteractiveSessionEvent,
InteractiveSessionEventV2, InteractiveSessionMessage, InteractiveSessionRequest,
InteractiveSessionRequestV2, Mailbox, MailboxCancellation, MailboxRequest, MailboxResult,
MailboxResultAck, MailboxResultChunk, MountIdentity, NamespaceIdentity,
NetworkCanaryObservation, NetworkObservation, NetworkOwnership, NetworkReadinessAttestation,
NetworkReadinessEvidence, NetworkReadinessIdentity, NonInteractiveOutput,
NonInteractiveSessionMessage, NonInteractiveSessionRequest, Participant, ParticipantOwnership,
ParticipantState, PersistedHostProcessOwnership, PidfsProcessAuthorityV1,
PidfsProcessIdentityV1, PolicyAction, PolicyDelivery, PrefixRule, PrepareOperation,
PrepareStageState, PreparedArtifact, PreparedLowerOwnership, ProcessOwnershipCommitAck,
ProcessOwnershipMessageVersion, ProtocolCredential, ProtocolCredentialRole,
ProtocolPublicationIdentity, PtyIdentity, PublicationOwnership, ResolvedAutoApprovalRule,
ResolvedVolume, Resolver, ResultStream, RunscProcessOwnership, SandboxCredentialBundle,
SandboxOwnership, Session, SessionBinding, SessionClose, SessionExit, SessionExitOutcome,
SessionRegistrationRequest, SessionRegistrationResponse, SessionRequestCapability,
SessionTerminal, SessionTerminalEvent, SessionTerminalEventV2, SessionTerminalMessage,
SessionTerminalState, SignedProtocolRecord, StagedProcessOwnership, StateAnchorOwnership,
StateAnchorReleaseEvidence, StorageCatalog, StorageCatalogMetadata, StoreMetadata,
TerminalBreak, TerminalInput, TerminalModesV2, TerminalOutput, TerminalPrompt, TerminalResize,
TerminalSignal, TerminalSignalKind, TlsCredential, TlsRole, TrustManifest,
TrustedHostConfigurationV1,
};
use crate::protocol::{BootstrapWaitOutcome, BootstrapWaitRequest};
@ -67,16 +70,23 @@ schema_types!(
"ProcessOwnershipCommitAck" => ProcessOwnershipCommitAck,
"GuestProcessIdentity" => GuestProcessIdentity,
"SessionTerminalEvent" => SessionTerminalEvent,
"SessionTerminalEventV2" => SessionTerminalEventV2,
"TerminalInput" => TerminalInput,
"TerminalResize" => TerminalResize,
"TerminalModesV2" => TerminalModesV2,
"TerminalBreak" => TerminalBreak,
"TerminalSignal" => TerminalSignal,
"TerminalSignalKind" => TerminalSignalKind,
"SessionClose" => SessionClose,
"SessionTerminalMessage" => SessionTerminalMessage,
"InteractiveSessionRequest" => InteractiveSessionRequest,
"InteractiveProtocolVersion" => InteractiveProtocolVersion,
"InteractiveSessionRequestV2" => InteractiveSessionRequestV2,
"InteractiveSessionEvent" => InteractiveSessionEvent,
"InteractiveSessionEventV2" => InteractiveSessionEventV2,
"InteractiveSessionMessage" => InteractiveSessionMessage,
"InteractiveSessionBatch" => InteractiveSessionBatch,
"InteractiveSessionBatchV2" => InteractiveSessionBatchV2,
"TerminalPrompt" => TerminalPrompt,
"NonInteractiveSessionRequest" => NonInteractiveSessionRequest,
"NonInteractiveOutput" => NonInteractiveOutput,

View file

@ -2,6 +2,11 @@ use crate::identity::{RequestDigest, RequestId};
use super::{Deserialize, NonZeroU16, ProtocolVersion, Serialize, SessionId};
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum InteractiveProtocolVersion {
V2,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct TerminalInput {
pub session_id: SessionId,
@ -17,6 +22,34 @@ pub struct TerminalResize {
pub pixel_height: u16,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct TerminalModesV2 {
pub input_modes: u32,
pub output_modes: u32,
pub control_modes: u32,
pub local_modes: u32,
pub line_discipline: u8,
pub input_speed: u32,
pub output_speed: u32,
pub interrupt: u8,
pub quit: u8,
pub erase: u8,
pub kill: u8,
pub eof: u8,
pub time: u8,
pub minimum: u8,
pub switch: u8,
pub start: u8,
pub stop: u8,
pub suspend: u8,
pub end_of_line: u8,
pub reprint: u8,
pub discard: u8,
pub word_erase: u8,
pub literal_next: u8,
pub end_of_line_2: u8,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum TerminalSignalKind {
Interrupt,
@ -35,6 +68,11 @@ pub struct SessionClose {
pub session_id: SessionId,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct TerminalBreak {
pub session_id: SessionId,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum SessionTerminalEvent {
Input(TerminalInput),
@ -43,6 +81,14 @@ pub enum SessionTerminalEvent {
Close(SessionClose),
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum SessionTerminalEventV2 {
Input(TerminalInput),
Resize(TerminalResize),
Break(TerminalBreak),
Close(SessionClose),
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct TerminalOutput {
pub session_id: SessionId,
@ -147,6 +193,15 @@ pub struct InteractiveSessionRequest {
pub initial_size: TerminalResize,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct InteractiveSessionRequestV2 {
pub protocol_version: InteractiveProtocolVersion,
pub session_id: SessionId,
pub argv: Vec<Vec<u8>>,
pub initial_size: TerminalResize,
pub initial_modes: TerminalModesV2,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct TerminalPrompt {
pub session_id: SessionId,
@ -169,6 +224,18 @@ pub enum InteractiveSessionEvent {
},
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum InteractiveSessionEventV2 {
Terminal(SessionTerminalEventV2),
PromptDecision {
session_id: SessionId,
signed_decision: Vec<u8>,
},
Poll {
session_id: SessionId,
},
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum InteractiveSessionMessage {
Terminal(SessionTerminalMessage),
@ -181,6 +248,12 @@ pub struct InteractiveSessionBatch {
pub messages: Vec<InteractiveSessionMessage>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct InteractiveSessionBatchV2 {
pub session_id: SessionId,
pub messages: Vec<InteractiveSessionMessage>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct NonInteractiveSessionRequest {
pub protocol_version: super::ProtocolVersion,

View file

@ -288,6 +288,35 @@ fn session_id() -> SessionId {
SessionId::new(opaque(5))
}
fn terminal_modes_v2() -> TerminalModesV2 {
TerminalModesV2 {
input_modes: 1,
output_modes: 2,
control_modes: 3,
local_modes: 4,
line_discipline: 0,
input_speed: 38_400,
output_speed: 38_400,
interrupt: 3,
quit: 28,
erase: 127,
kill: 21,
eof: 4,
time: 0,
minimum: 1,
switch: 0,
start: 17,
stop: 19,
suspend: 26,
end_of_line: 0,
reprint: 18,
discard: 15,
word_erase: 23,
literal_next: 22,
end_of_line_2: 0,
}
}
fn process(seed: u32) -> PidfsProcessIdentityV1 {
PidfsProcessIdentityV1::new(
PidfsProcessAuthorityV1 {

View file

@ -145,6 +145,12 @@ fn every_schema_shape_round_trips_and_rejects_malformed_or_unknown_versions() {
}),
&mut seen,
);
verify_shape(
SessionTerminalEventV2::Break(TerminalBreak {
session_id: session_id(),
}),
&mut seen,
);
verify_shape(
TerminalInput {
session_id: session_id(),
@ -162,6 +168,13 @@ fn every_schema_shape_round_trips_and_rejects_malformed_or_unknown_versions() {
},
&mut seen,
);
verify_shape(terminal_modes_v2(), &mut seen);
verify_shape(
TerminalBreak {
session_id: session_id(),
},
&mut seen,
);
verify_shape(
TerminalSignal {
session_id: session_id(),
@ -227,12 +240,29 @@ fn every_schema_shape_round_trips_and_rejects_malformed_or_unknown_versions() {
},
&mut seen,
);
verify_shape(InteractiveProtocolVersion::V2, &mut seen);
verify_shape(
InteractiveSessionRequestV2 {
protocol_version: InteractiveProtocolVersion::V2,
session_id: session_id(),
argv: vec![b"tool".to_vec()],
initial_size: interactive_resize,
initial_modes: terminal_modes_v2(),
},
&mut seen,
);
verify_shape(
InteractiveSessionEvent::Poll {
session_id: session_id(),
},
&mut seen,
);
verify_shape(
InteractiveSessionEventV2::Poll {
session_id: session_id(),
},
&mut seen,
);
let prompt = TerminalPrompt {
session_id: session_id(),
request_id: RequestId::new(opaque(54)),
@ -251,6 +281,13 @@ fn every_schema_shape_round_trips_and_rejects_malformed_or_unknown_versions() {
},
&mut seen,
);
verify_shape(
InteractiveSessionBatchV2 {
session_id: session_id(),
messages: Vec::new(),
},
&mut seen,
);
verify_shape(generation_identity(), &mut seen);
verify_shape(trust_manifest(), &mut seen);
verify_shape(tls_credential.clone(), &mut seen);
@ -649,8 +686,8 @@ persistent_dirs = []
assert_eq!(
seen.digest(),
[
54, 187, 13, 149, 227, 157, 1, 54, 234, 66, 127, 9, 34, 65, 173, 20, 214, 200, 126, 67,
1, 216, 152, 76, 167, 32, 177, 218, 0, 192, 175, 28,
201, 9, 82, 185, 208, 17, 32, 181, 237, 9, 161, 239, 26, 109, 45, 148, 192, 138, 104,
114, 203, 147, 61, 180, 162, 184, 139, 77, 227, 8, 27, 128,
]
);
}

View file

@ -220,6 +220,7 @@ fn control_corpus(digest: &mut Sha256) {
ControlRequest::Register(_) => ControlRequest::Register(registration_request()),
ControlRequest::Execute(_) => ControlRequest::Execute(NonInteractiveSessionRequest { protocol_version: ProtocolVersion::V1, session_id: session_id(), argv: vec![b"tool".to_vec()] }),
ControlRequest::ExecuteInteractive(_) => ControlRequest::ExecuteInteractive(InteractiveSessionRequest { protocol_version: ProtocolVersion::V1, session_id: session_id(), argv: vec![b"tool".to_vec()], initial_size: resize }),
ControlRequest::ExecuteInteractiveV2(_) => ControlRequest::ExecuteInteractiveV2(InteractiveSessionRequestV2 { protocol_version: InteractiveProtocolVersion::V2, session_id: session_id(), argv: vec![b"tool".to_vec()], initial_size: resize, initial_modes: terminal_modes_v2() }),
});
enum_corpus!(digest, ControlResponse, {
ControlResponse::Enrolled(_) => ControlResponse::Enrolled(enrollment_response()),
@ -231,6 +232,9 @@ fn credential_corpus(digest: &mut Sha256) {
enum_corpus!(digest, ProtocolVersion, {
ProtocolVersion::V1 => ProtocolVersion::V1,
});
enum_corpus!(digest, InteractiveProtocolVersion, {
InteractiveProtocolVersion::V2 => InteractiveProtocolVersion::V2,
});
enum_corpus!(digest, TlsRole, {
TlsRole::SessionCoordinator => TlsRole::SessionCoordinator,
TlsRole::ClientCli => TlsRole::ClientCli,
@ -325,8 +329,8 @@ fn every_serialized_enum_variant_has_fixed_shape_evidence() {
assert_eq!(
digest.finalize().as_slice(),
[
63, 126, 242, 211, 164, 165, 11, 185, 180, 135, 109, 135, 92, 142, 111, 169, 142, 215,
49, 4, 170, 169, 119, 237, 235, 161, 192, 89, 92, 93, 88, 35,
16, 136, 82, 10, 228, 231, 132, 63, 189, 59, 106, 16, 164, 103, 232, 11, 245, 31, 144,
53, 91, 201, 91, 74, 66, 14, 174, 78, 110, 17, 181, 21,
]
);
}