use serde::{Deserialize, Serialize};
use std::fmt;
pub const EXEC_DM_PREFIX: &[u8] = b"x0x-exec-v1\0";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ExecRequestId(pub [u8; 16]);
impl ExecRequestId {
#[must_use]
pub fn new_random() -> Self {
let mut bytes = [0_u8; 16];
use rand::RngCore as _;
rand::thread_rng().fill_bytes(&mut bytes);
Self(bytes)
}
pub fn from_hex(input: &str) -> Result<Self, ProtocolError> {
let decoded =
hex::decode(input).map_err(|e| ProtocolError::InvalidRequestId(e.to_string()))?;
if decoded.len() != 16 {
return Err(ProtocolError::InvalidRequestId(format!(
"expected 16 bytes, got {}",
decoded.len()
)));
}
let mut out = [0_u8; 16];
out.copy_from_slice(&decoded);
Ok(Self(out))
}
#[must_use]
pub fn to_hex(self) -> String {
hex::encode(self.0)
}
}
impl fmt::Display for ExecRequestId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_hex())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ExecFrame {
Request {
request_id: ExecRequestId,
argv: Vec<String>,
stdin: Option<Vec<u8>>,
timeout_ms: u32,
cwd: Option<String>,
},
LeaseRenew { request_id: ExecRequestId },
Started { request_id: ExecRequestId, pid: u32 },
Stdout {
request_id: ExecRequestId,
seq: u32,
data: Vec<u8>,
},
Stderr {
request_id: ExecRequestId,
seq: u32,
data: Vec<u8>,
},
Warning {
request_id: ExecRequestId,
kind: WarningKind,
message: String,
},
Exit {
request_id: ExecRequestId,
code: Option<i32>,
signal: Option<i32>,
duration_ms: u64,
stdout_bytes_total: u64,
stderr_bytes_total: u64,
truncated: bool,
denial_reason: Option<DenialReason>,
},
Cancel { request_id: ExecRequestId },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum StreamKind {
Stdout,
Stderr,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum WarningKind {
StdoutCapHit,
StderrCapHit,
DurationApproachingCap,
StdoutApproachingCap,
StderrApproachingCap,
LeaseExpired,
PeerDisconnected,
Cancelled,
}
impl WarningKind {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::StdoutCapHit => "stdout_cap_hit",
Self::StderrCapHit => "stderr_cap_hit",
Self::DurationApproachingCap => "duration_approaching_cap",
Self::StdoutApproachingCap => "stdout_approaching_cap",
Self::StderrApproachingCap => "stderr_approaching_cap",
Self::LeaseExpired => "lease_expired",
Self::PeerDisconnected => "peer_disconnected",
Self::Cancelled => "cancelled",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DenialReason {
ExecDisabled,
UnverifiedSender,
TrustRejected,
AgentMachineNotInAcl,
ArgvNotAllowed,
StdinTooLarge,
TimeoutTooLarge,
CwdNotAllowed,
ConcurrencyLimitReached,
ShellMetacharInArgv,
SpawnFailed,
MalformedFrame,
}
impl DenialReason {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::ExecDisabled => "exec_disabled",
Self::UnverifiedSender => "unverified_sender",
Self::TrustRejected => "trust_rejected",
Self::AgentMachineNotInAcl => "agent_machine_not_in_acl",
Self::ArgvNotAllowed => "argv_not_allowed",
Self::StdinTooLarge => "stdin_too_large",
Self::TimeoutTooLarge => "timeout_too_large",
Self::CwdNotAllowed => "cwd_not_allowed",
Self::ConcurrencyLimitReached => "concurrency_limit_reached",
Self::ShellMetacharInArgv => "shell_metachar_in_argv",
Self::SpawnFailed => "spawn_failed",
Self::MalformedFrame => "malformed_frame",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecRunResult {
pub request_id: ExecRequestId,
pub code: Option<i32>,
pub signal: Option<i32>,
pub duration_ms: u64,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
pub stdout_bytes_total: u64,
pub stderr_bytes_total: u64,
pub truncated: bool,
pub denial_reason: Option<DenialReason>,
pub warnings: Vec<WarningKind>,
}
#[derive(Debug, thiserror::Error)]
pub enum ProtocolError {
#[error("payload is not an x0x exec frame")]
MissingPrefix,
#[error("invalid exec frame: {0}")]
Decode(String),
#[error("invalid request id: {0}")]
InvalidRequestId(String),
}
pub fn encode_frame_payload(frame: &ExecFrame) -> Result<Vec<u8>, ProtocolError> {
let encoded = bincode::serialize(frame).map_err(|e| ProtocolError::Decode(e.to_string()))?;
let mut payload = Vec::with_capacity(EXEC_DM_PREFIX.len().saturating_add(encoded.len()));
payload.extend_from_slice(EXEC_DM_PREFIX);
payload.extend_from_slice(&encoded);
Ok(payload)
}
pub fn decode_frame_payload(payload: &[u8]) -> Result<ExecFrame, ProtocolError> {
let Some(frame_bytes) = payload.strip_prefix(EXEC_DM_PREFIX) else {
return Err(ProtocolError::MissingPrefix);
};
bincode::deserialize(frame_bytes).map_err(|e| ProtocolError::Decode(e.to_string()))
}