Implement trusted-host configuration resolution

This commit is contained in:
ms2k 2026-07-15 00:43:42 +09:00
commit f9eb966341
3 changed files with 3468 additions and 39 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,7 @@
#![doc = "Shared production interfaces for Lazaret processes."]
pub mod codec;
pub mod configuration;
pub mod identity;
pub mod records;

View file

@ -1,3 +1,4 @@
use std::collections::BTreeMap;
use std::fmt;
use std::num::NonZeroU16;
@ -20,6 +21,199 @@ pub const STORAGE_CATALOG_SCHEMA_ID: &str = "lazaret-storage-catalog-v1";
pub const DOMAIN_STORE_SCHEMA_VERSION: u32 = 2;
pub const LEGACY_DOMAIN_STORE_SCHEMA_VERSION: u32 = 1;
pub const STORAGE_CATALOG_SCHEMA_VERSION: u32 = 1;
pub const TRUSTED_HOST_CONFIGURATION_SCHEMA_ID: &str = "lazaret-trusted-host-config-v1";
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct StorageConfigurationV1 {
#[serde(default)]
pub data_root: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct NetworkConfigurationV1 {
#[serde(default)]
pub default_allow: Vec<String>,
#[serde(default)]
pub default_deny: Vec<String>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum VolumeKind {
File,
Directory,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum VolumeAccess {
ReadOnly,
#[default]
ReadWrite,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct VolumeConfigurationV1 {
pub host: String,
pub container: String,
pub kind: VolumeKind,
#[serde(default)]
pub access: VolumeAccess,
#[serde(default)]
pub create: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AutoApprovalRuleConfigurationV1 {
pub executable: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub allow_extra_args: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct EscalationConfigurationV1 {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub auto_approve: Vec<AutoApprovalRuleConfigurationV1>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ProfileConfigurationV1 {
pub image: String,
#[serde(default = "default_isolate_gitignored")]
pub isolate_gitignored: bool,
#[serde(default)]
pub persistent_dirs: Vec<String>,
#[serde(default)]
pub allow_egress: Vec<String>,
#[serde(default)]
pub deny_egress: Vec<String>,
#[serde(default)]
pub env: BTreeMap<String, String>,
#[serde(default)]
pub volumes: Vec<VolumeConfigurationV1>,
#[serde(default)]
pub escalation: EscalationConfigurationV1,
}
const fn default_isolate_gitignored() -> bool {
true
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct TrustedHostConfigurationV1 {
#[serde(rename = "schema")]
schema_id: String,
storage: StorageConfigurationV1,
network: NetworkConfigurationV1,
profiles: BTreeMap<String, ProfileConfigurationV1>,
}
impl TrustedHostConfigurationV1 {
#[must_use]
pub fn schema_id(&self) -> &str {
&self.schema_id
}
#[must_use]
pub const fn storage(&self) -> &StorageConfigurationV1 {
&self.storage
}
#[must_use]
pub const fn network(&self) -> &NetworkConfigurationV1 {
&self.network
}
#[must_use]
pub const fn profiles(&self) -> &BTreeMap<String, ProfileConfigurationV1> {
&self.profiles
}
}
impl<'de> Deserialize<'de> for TrustedHostConfigurationV1 {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Fields {
#[serde(rename = "schema")]
schema_id: String,
#[serde(default)]
storage: StorageConfigurationV1,
#[serde(default)]
network: NetworkConfigurationV1,
profiles: BTreeMap<String, ProfileConfigurationV1>,
}
let fields = Fields::deserialize(deserializer)?;
if fields.schema_id != TRUSTED_HOST_CONFIGURATION_SCHEMA_ID {
return Err(serde::de::Error::custom(
"unknown trusted-host configuration schema",
));
}
if fields.profiles.is_empty() {
return Err(serde::de::Error::custom(
"trusted-host configuration has no profiles",
));
}
Ok(Self {
schema_id: fields.schema_id,
storage: fields.storage,
network: fields.network,
profiles: fields.profiles,
})
}
}
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct EnvironmentReference {
pub sandbox_name: String,
pub host_name: String,
}
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct ResolvedVolume {
pub canonical_host_source: HostPath,
pub container_path: String,
pub kind: VolumeKind,
pub access: VolumeAccess,
pub create: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct ResolvedAutoApprovalRule {
pub canonical_executable: HostPath,
pub args: Vec<String>,
pub allow_extra_args: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ConfigurationFingerprintRecordV1 {
pub selected_profile: String,
pub storage_root: HostPath,
pub runtime_root: HostPath,
pub image: String,
pub isolate_gitignored: bool,
pub persistent_dirs: Vec<String>,
pub environment_references: Vec<EnvironmentReference>,
pub volumes: Vec<ResolvedVolume>,
pub effective_allow_prefixes: Vec<([u8; 4], u8)>,
pub effective_deny_prefixes: Vec<([u8; 4], u8)>,
pub escalation_enabled: bool,
pub auto_approval_rules: Vec<ResolvedAutoApprovalRule>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct StoreMetadata {
@ -1493,15 +1687,138 @@ pub struct SignedProtocolRecord {
pub signature: Vec<u8>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct MailboxRequest {
pub protocol_version: ProtocolVersion,
pub session_binding: SessionBinding,
pub request_id: RequestId,
pub session_capability: SessionRequestCapability,
pub sandbox_credential_id: ProtocolCredentialId,
pub command_argv: Vec<Vec<u8>>,
pub justification: String,
protocol_version: ProtocolVersion,
session_binding: SessionBinding,
request_id: RequestId,
session_capability: SessionRequestCapability,
sandbox_credential_id: ProtocolCredentialId,
command_argv: Vec<Vec<u8>>,
justification: String,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MailboxRequestError {
EmptyCommand,
InvalidArgument,
NonAbsoluteExecutable,
}
impl fmt::Display for MailboxRequestError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyCommand => formatter.write_str("command argument vector is empty"),
Self::InvalidArgument => formatter.write_str("command argument is not NUL-free UTF-8"),
Self::NonAbsoluteExecutable => {
formatter.write_str("command executable is not an absolute path")
}
}
}
}
impl std::error::Error for MailboxRequestError {}
impl MailboxRequest {
/// # Errors
/// The command must contain an absolute executable followed by NUL-free UTF-8 arguments.
pub fn new(
session_binding: SessionBinding,
request_id: RequestId,
session_capability: SessionRequestCapability,
sandbox_credential_id: ProtocolCredentialId,
command_argv: Vec<Vec<u8>>,
justification: String,
) -> Result<Self, MailboxRequestError> {
let Some(executable) = command_argv.first() else {
return Err(MailboxRequestError::EmptyCommand);
};
if command_argv
.iter()
.any(|argument| argument.contains(&0) || std::str::from_utf8(argument).is_err())
{
return Err(MailboxRequestError::InvalidArgument);
}
if !executable.starts_with(b"/") {
return Err(MailboxRequestError::NonAbsoluteExecutable);
}
Ok(Self {
protocol_version: ProtocolVersion::V1,
session_binding,
request_id,
session_capability,
sandbox_credential_id,
command_argv,
justification,
})
}
#[must_use]
pub const fn protocol_version(&self) -> ProtocolVersion {
self.protocol_version
}
#[must_use]
pub const fn session_binding(&self) -> &SessionBinding {
&self.session_binding
}
#[must_use]
pub const fn request_id(&self) -> RequestId {
self.request_id
}
#[must_use]
pub const fn session_capability(&self) -> &SessionRequestCapability {
&self.session_capability
}
#[must_use]
pub const fn sandbox_credential_id(&self) -> ProtocolCredentialId {
self.sandbox_credential_id
}
#[must_use]
pub fn command_argv(&self) -> &[Vec<u8>] {
&self.command_argv
}
#[must_use]
pub fn justification(&self) -> &str {
&self.justification
}
}
impl<'de> Deserialize<'de> for MailboxRequest {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct Fields {
protocol_version: ProtocolVersion,
session_binding: SessionBinding,
request_id: RequestId,
session_capability: SessionRequestCapability,
sandbox_credential_id: ProtocolCredentialId,
command_argv: Vec<Vec<u8>>,
justification: String,
}
let fields = Fields::deserialize(deserializer)?;
if fields.protocol_version != ProtocolVersion::V1 {
return Err(serde::de::Error::custom("unknown mailbox request version"));
}
Self::new(
fields.session_binding,
fields.request_id,
fields.session_capability,
fields.sandbox_credential_id,
fields.command_argv,
fields.justification,
)
.map_err(serde::de::Error::custom)
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
@ -2701,13 +3018,13 @@ schema_types!(
"DeniedDestinationObservation" => DeniedDestinationObservation,
"PermittedDestinationObservation" => PermittedDestinationObservation,
"DnsObservation" => DnsObservation,
"TrustedHostConfigurationV1" => TrustedHostConfigurationV1,
"ConfigurationFingerprintRecordV1" => ConfigurationFingerprintRecordV1,
"EnvironmentReference" => EnvironmentReference,
"ResolvedVolume" => ResolvedVolume,
"ResolvedAutoApprovalRule" => ResolvedAutoApprovalRule,
"DomainStore" => DomainStore,
"StorageCatalog" => StorageCatalog;
"TrustedHostConfigurationV1",
"ConfigurationFingerprintRecordV1",
"EnvironmentReference",
"ResolvedVolume",
"ResolvedAutoApprovalRule",
);
pub const DECLARED_RUNTIME_SCHEMA_TYPE_NAMES: &[&str] = &[