//! Execution target descriptors: configured identity vs verified identity.
//!
//! A target descriptor names a configured machine/account and a CHECKED
//! workspace, never just a friendly hostname. The SSH alias stays
//! configuration; the identity is what a connect boundary actually returned
//! and verified. A friendly hostname that resolves to a different account or
//! platform than configured is a typed mismatch, never a fallback: there is
//! no home-directory or local substitution anywhere in this module.
//!
//! Ownership: m1-protocol (klbr-m7x). Live callers: m1-backend's backend
//! trait (session placement), m1-relay's launcher, m1-bootstrap's
//! readiness/bundle identity, and the protocol envelope binding below.
use anyhow::{ensure, Result};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
/// A config-level target name, e.g. "linux-build". This is the selector the
/// operator writes, NOT a verified machine identity.
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct TargetId(String);
impl TargetId {
pub fn parse(value: impl Into<String>) -> Result<Self> {
let value = value.into();
ensure!(
!value.is_empty()
&& value.len() <= 128
&& value
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b"_.-".contains(&b)),
"invalid target id"
);
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for TargetId {
type Error = String;
fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
Self::parse(value).map_err(|e| e.to_string())
}
}
impl From<TargetId> for String {
fn from(id: TargetId) -> Self {
id.0
}
}
impl fmt::Display for TargetId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
/// Auxiliary namespace identity. M2 opens additional kernels; the identity
/// exists now so every envelope binds namespaces uniformly.
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct NamespaceId(String);
impl NamespaceId {
pub fn parse(value: impl Into<String>) -> Result<Self> {
let value = value.into();
ensure!(
!value.is_empty()
&& value.len() <= 128
&& value
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b"_.-".contains(&b)),
"invalid namespace id"
);
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for NamespaceId {
type Error = String;
fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
Self::parse(value).map_err(|e| e.to_string())
}
}
impl From<NamespaceId> for String {
fn from(id: NamespaceId) -> Self {
id.0
}
}
impl fmt::Display for NamespaceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
/// Namespace generation: a NEW generation is produced on every placement
/// change or transport loss. Reattachment (M2) is a distinct explicit
/// contract and never reuses a generation.
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct NamespaceGeneration(String);
impl NamespaceGeneration {
#[must_use]
pub fn fresh() -> Self {
Self(uuid::Uuid::new_v4().to_string())
}
pub fn parse(value: impl Into<String>) -> Result<Self> {
let value = value.into();
ensure!(
!value.is_empty()
&& value.len() <= 128
&& value
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b"_.-".contains(&b)),
"invalid namespace generation"
);
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for NamespaceGeneration {
type Error = String;
fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
Self::parse(value).map_err(|e| e.to_string())
}
}
impl From<NamespaceGeneration> for String {
fn from(id: NamespaceGeneration) -> Self {
id.0
}
}
impl fmt::Display for NamespaceGeneration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
/// Opaque content identity of a provisioned environment (interpreter/ABI,
/// skill source revision, dependency lock, bootstrap recipe). Bootstrap (M1
/// bootstrap workstream) produces it; this vocabulary carries it verbatim
/// and compares it for equality only.
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct EnvironmentFingerprint(String);
impl EnvironmentFingerprint {
pub fn parse(value: impl Into<String>) -> Result<Self> {
let value = value.into();
ensure!(
!value.is_empty()
&& value.len() <= 128
&& value
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b"_.-".contains(&b)),
"invalid environment fingerprint"
);
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for EnvironmentFingerprint {
type Error = String;
fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
Self::parse(value).map_err(|e| e.to_string())
}
}
impl From<EnvironmentFingerprint> for String {
fn from(id: EnvironmentFingerprint) -> Self {
id.0
}
}
impl fmt::Display for EnvironmentFingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
/// Where a session's workbench runs. Local execution needs no descriptor;
/// remote execution is always a full configured descriptor.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "placement", rename_all = "snake_case", deny_unknown_fields)]
pub enum KernelTarget {
Local,
Remote(RemoteTarget),
}
impl FromStr for TargetId {
type Err = anyhow::Error;
fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
Self::parse(value.to_owned())
}
}
impl FromStr for NamespaceId {
type Err = anyhow::Error;
fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
Self::parse(value.to_owned())
}
}
impl FromStr for NamespaceGeneration {
type Err = anyhow::Error;
fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
Self::parse(value.to_owned())
}
}
impl FromStr for EnvironmentFingerprint {
type Err = anyhow::Error;
fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
Self::parse(value.to_owned())
}
}
/// A durable placement descriptor for a session's workbench, persisted by
/// the host (store.rs, m1-backend writes it) and re-checked at read time.
/// A restarted daemon refuses remote work without a backend instead of
/// silently falling back to local: Some(descriptor) means the placement is
/// STILL the contract.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SessionPlacement {
pub kernel_target: KernelTarget,
pub namespace_id: NamespaceId,
pub namespace_generation: NamespaceGeneration,
}
impl SessionPlacement {
/// Set-time validation: the serialized shape must be the typed
/// descriptor, and a remote placement must carry an explicitly
/// configured workspace. A missing workspace is a typed refusal, never
/// a home fallback; machine identity is verified separately by
/// RemoteTarget::verify_identity once a connect boundary resolves.
pub fn from_json(value: &serde_json::Value) -> Result<Self> {
let placement: Self = serde_json::from_value(value.clone())
.map_err(|e| anyhow::anyhow!("invalid session placement descriptor: {e}"))?;
if let KernelTarget::Remote(target) = &placement.kernel_target {
ensure!(
!target.workspace.path.trim().is_empty(),
TargetMismatch::WorkspaceUnusable {
target: target.id.clone(),
workspace: target.workspace.path.clone(),
}
);
}
Ok(placement)
}
#[must_use]
pub fn to_json(&self) -> serde_json::Value {
serde_json::to_value(self).unwrap_or(serde_json::Value::Null)
}
}
/// An explicitly configured workspace for a remote target. A missing or
/// unusable configured workspace is a typed failure at the selection
/// boundary; it never falls back to a home directory.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RemoteWorkspace {
pub path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub revision: Option<String>,
}
/// Configured remote target descriptor. Everything here is DECLARED
/// configuration; nothing here was verified by a connection.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RemoteTarget {
pub id: TargetId,
/// SSH alias or host as configured. Configuration, not identity.
pub ssh_alias: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub account: Option<String>,
pub workspace: RemoteWorkspace,
/// Explicitly configured interpreter override. A configured interpreter
/// must still pass the same capability/protocol checks as a default one.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub interpreter: Option<String>,
/// Platform the descriptor requires, when the configuration declares
/// one (e.g. a linux-build target). None accepts any platform.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub required_platform: Option<String>,
}
/// The identity a connect/launch boundary actually verified, once per
/// launch. The agent sees exactly this. Values are non-optional: an
/// identity with unchecked values cannot be constructed.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TargetIdentity {
/// Canonical account as the machine reports it, e.g. "mayer".
pub account: String,
/// Canonical machine identity as returned (uname nodename), not the
/// configured alias.
pub machine: String,
/// Platform as returned, e.g. "Darwin", "Linux", "NixOS".
pub platform: String,
pub arch: String,
/// Absolute interpreter path as resolved on the target.
pub interpreter_path: String,
/// Interpreter version string as reported.
pub interpreter_version: String,
/// Working directory the boundary actually chdir'd into and verified.
pub cwd: String,
/// Verified environment fingerprint, when the deployment records one.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub environment_fingerprint: Option<EnvironmentFingerprint>,
/// Highest capability/protocol revision the target serves.
pub capability_revision: u16,
}
impl TargetIdentity {
/// Fallible constructor: an identity cannot be constructed from
/// unchecked values.
#[allow(clippy::too_many_arguments)]
pub fn new(
account: impl Into<String>,
machine: impl Into<String>,
platform: impl Into<String>,
arch: impl Into<String>,
interpreter_path: impl Into<String>,
interpreter_version: impl Into<String>,
cwd: impl Into<String>,
environment_fingerprint: Option<EnvironmentFingerprint>,
capability_revision: u16,
) -> Result<Self> {
let identity = Self {
account: account.into(),
machine: machine.into(),
platform: platform.into(),
arch: arch.into(),
interpreter_path: interpreter_path.into(),
interpreter_version: interpreter_version.into(),
cwd: cwd.into(),
environment_fingerprint,
capability_revision,
};
ensure!(
!identity.account.trim().is_empty(),
"target identity account is empty"
);
ensure!(
!identity.machine.trim().is_empty(),
"target identity machine is empty"
);
ensure!(
!identity.platform.trim().is_empty(),
"target identity platform is empty"
);
ensure!(
!identity.arch.trim().is_empty(),
"target identity arch is empty"
);
ensure!(
!identity.interpreter_path.trim().is_empty(),
"target identity interpreter path is empty"
);
ensure!(
!identity.interpreter_version.trim().is_empty(),
"target identity interpreter version is empty"
);
ensure!(
!identity.cwd.trim().is_empty(),
"target identity cwd is empty"
);
Ok(identity)
}
}
/// One typed mismatch kind per check that can fail when a resolved identity
/// contradicts the configured target. No fallback variant exists: a
/// mismatch is fatal at the selection boundary.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TargetMismatch {
AccountMismatch {
target: TargetId,
configured: String,
resolved: String,
},
PlatformMismatch {
target: TargetId,
configured: String,
resolved: String,
},
WorkspaceUnusable {
target: TargetId,
workspace: String,
},
}
impl fmt::Display for TargetMismatch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AccountMismatch {
target,
configured,
resolved,
} => write!(
f,
"target {target}: account mismatch: configured {configured:?}, resolved {resolved:?}"
),
Self::PlatformMismatch {
target,
configured,
resolved,
} => write!(
f,
"target {target}: platform mismatch: configured {configured:?}, resolved {resolved:?}"
),
Self::WorkspaceUnusable { target, workspace } => write!(
f,
"target {target}: workspace {workspace:?} does not exist or is unusable on the target"
),
}
}
}
impl std::error::Error for TargetMismatch {}
/// A target whose configured and verified identities agree, plus the
/// identity that was actually checked.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResolvedTarget {
pub target: RemoteTarget,
pub identity: TargetIdentity,
}
impl RemoteTarget {
/// Which platform identity this descriptor requires, if it declares one
/// (e.g. a linux-build target may require Linux). None means any.
#[must_use]
pub fn required_platform(&self) -> Option<String> {
self.required_platform.clone()
}
/// Check a resolved identity against this descriptor. This is the
/// "no wrong-machine, no wrong-account" contract: mismatch is typed,
/// fatal, and reported - never silently retried elsewhere.
pub fn verify_identity(&self, resolved: &TargetIdentity) -> Result<ResolvedTarget> {
if let Some(expected_account) = &self.account {
ensure!(
resolved.account == *expected_account,
TargetMismatch::AccountMismatch {
target: self.id.clone(),
configured: expected_account.clone(),
resolved: resolved.account.clone(),
}
);
}
if let Some(expected_platform) = &self.required_platform {
ensure!(
resolved.platform == *expected_platform,
TargetMismatch::PlatformMismatch {
target: self.id.clone(),
configured: expected_platform.clone(),
resolved: resolved.platform.clone(),
}
);
}
ensure!(
resolved.cwd == self.workspace.path,
TargetMismatch::WorkspaceUnusable {
target: self.id.clone(),
workspace: self.workspace.path.clone(),
}
);
Ok(ResolvedTarget {
target: self.clone(),
identity: resolved.clone(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn identity(account: &str, platform: &str, cwd: &str) -> TargetIdentity {
TargetIdentity::new(
account,
"chernobog.internal",
platform,
"arm64",
"/usr/bin/python3",
"3.11.9",
cwd,
None,
1,
)
.expect("identity")
}
fn remote_target() -> RemoteTarget {
serde_json::from_value(serde_json::json!({
"id": "linux-build",
"ssh_alias": "linux-build",
"account": "mayer",
"workspace": {"path": "/srv/work/tangled"},
"required_platform": "Linux"
}))
.expect("target")
}
#[test]
fn target_id_rejects_bad_names() {
assert!(TargetId::parse("").is_err());
assert!(TargetId::parse("a b").is_err());
assert!(TargetId::parse("linux-build").is_ok());
}
#[test]
fn mismatching_account_is_a_typed_failure_not_a_fallback() {
let target = remote_target();
let resolved = identity("root", "Linux", "/srv/work/tangled");
let err = target.verify_identity(&resolved).expect_err("mismatch");
assert!(matches!(
err.downcast_ref::<TargetMismatch>(),
Some(TargetMismatch::AccountMismatch { .. })
));
}
#[test]
fn mismatching_platform_is_a_typed_failure_not_a_fallback() {
let target = remote_target();
let resolved = identity("mayer", "Darwin", "/srv/work/tangled");
let err = target.verify_identity(&resolved).expect_err("mismatch");
assert!(matches!(
err.downcast_ref::<TargetMismatch>(),
Some(TargetMismatch::PlatformMismatch { .. })
));
}
#[test]
fn wrong_directory_is_never_a_home_fallback() {
let target = remote_target();
let resolved = identity("mayer", "Linux", "/home/mayer");
let err = target.verify_identity(&resolved).expect_err("mismatch");
assert!(matches!(
err.downcast_ref::<TargetMismatch>(),
Some(TargetMismatch::WorkspaceUnusable { .. })
));
}
#[test]
fn verified_identity_binds() {
let target = remote_target();
let resolved = identity("mayer", "Linux", "/srv/work/tangled");
let bound = target.verify_identity(&resolved).expect("bound");
assert_eq!(bound.target.id.as_str(), "linux-build");
assert_eq!(bound.identity.account, "mayer");
}
}