use std::path::{Path, PathBuf};
use agent_mesh_protocol::MeshError;
pub use agent_mesh_protocol::UserKey;
use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
use newt_core::Caveats;
pub use agent_mesh_protocol::{AgentKey, AgentMetadata};
mod delegation;
pub use content_addressable::RawContentId;
pub use delegation::{verify_delegation, DelegationError, VerifiedDelegation};
#[must_use]
pub fn unbounded_debug_fallback() -> Caveats {
Caveats::top()
}
#[derive(Debug, thiserror::Error)]
pub enum IdentityError {
#[error("could not locate the home directory for ~/.newt/identity.pem")]
NoHome,
#[error("identity key error: {0}")]
Key(String),
}
impl From<MeshError> for IdentityError {
fn from(e: MeshError) -> Self {
Self::Key(e.to_string())
}
}
pub fn default_key_path() -> Result<PathBuf, IdentityError> {
newt_core::Config::user_config_path()
.map(|p| p.with_file_name("identity.pem"))
.ok_or(IdentityError::NoHome)
}
pub fn load_or_generate(path: &Path) -> Result<UserKey, IdentityError> {
if path.exists() {
Ok(UserKey::load(path)?)
} else {
let key = UserKey::generate();
key.save(path)?;
Ok(key)
}
}
pub fn load_user_key(path: &Path) -> Result<UserKey, IdentityError> {
if !path.exists() {
return Err(IdentityError::Key(format!(
"signing key not found at {} (newt never mints a key at a configured path)",
path.display()
)));
}
Ok(UserKey::load(path)?)
}
#[must_use]
pub fn session_root(user: &UserKey) -> AgentKey {
AgentKey::issue(user, meta("newt-session-root", Caveats::top()))
}
pub fn attenuate(parent: &AgentKey, caveats: &Caveats) -> Result<AgentKey, IdentityError> {
Ok(parent.delegate(meta("newt-session", caveats.clone()))?)
}
pub fn enforced_caveats(key: &AgentKey) -> Result<Caveats, IdentityError> {
key.cert().verify()?;
Ok(key.cert().metadata.caveats.clone())
}
fn meta(role: &str, caveats: Caveats) -> AgentMetadata {
plugin_child_metadata(role, caveats)
}
#[must_use]
pub fn plugin_child_metadata(role: &str, caveats: Caveats) -> AgentMetadata {
AgentMetadata {
role: role.to_string(),
host: "local".to_string(),
capabilities: Vec::new(),
issued_at: chrono::Utc::now().to_rfc3339(),
expires_at: None,
caveats,
}
}
#[derive(Debug, thiserror::Error)]
pub enum EnvelopeError {
#[error("requested child caveats amplify parent authority")]
Amplification,
#[error("envelope serialization failed: {0}")]
Serialize(String),
}
impl From<MeshError> for EnvelopeError {
fn from(err: MeshError) -> Self {
match err {
MeshError::CaveatAmplification => Self::Amplification,
other => Self::Serialize(other.to_string()),
}
}
}
pub fn serialize_for_plugin(
parent: &AgentKey,
child_metadata: AgentMetadata,
) -> Result<String, EnvelopeError> {
let child = parent.delegate(child_metadata)?;
let json = serde_json::to_string(child.cert())
.map_err(|e| EnvelopeError::Serialize(format!("cert chain: {e}")))?;
Ok(B64.encode(json.as_bytes()))
}
pub fn delegate_for_plugin(
parent: &AgentKey,
role: &str,
caveats: Caveats,
) -> Result<String, EnvelopeError> {
let metadata = plugin_child_metadata(role, caveats);
serialize_for_plugin(parent, metadata)
}
#[cfg(test)]
mod tests {
use super::*;
use newt_core::{PermissionPreset, ToolPermissions};
use tempfile::TempDir;
fn preset_caveats(p: PermissionPreset) -> Caveats {
ToolPermissions {
preset: p,
extra_exec: Vec::new(),
net: Vec::new(),
prompt: false,
..Default::default()
}
.to_caveats("/ws")
}
fn fresh_user(dir: &TempDir) -> UserKey {
load_or_generate(&dir.path().join("identity.pem")).unwrap()
}
#[test]
fn user_key_load_or_generate_is_stable() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("identity.pem");
let first = load_or_generate(&path).unwrap();
assert!(path.exists(), "first call must create the key");
let second = load_or_generate(&path).unwrap();
assert_eq!(
first.fingerprint(),
second.fingerprint(),
"second call must load the same key, not regenerate"
);
}
#[test]
fn load_user_key_errors_and_never_mints_when_absent() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("identity.pem");
assert!(load_user_key(&path).is_err());
assert!(!path.exists(), "load_user_key must never write a key");
}
#[test]
#[cfg(unix)]
fn load_or_generate_writes_a_0600_key() {
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let path = dir.path().join("identity.pem");
let _ = load_or_generate(&path).unwrap();
let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o600, "generated key must be 0600, got {mode:o}");
}
#[test]
fn attenuated_preset_is_signed_and_verifies() {
let dir = TempDir::new().unwrap();
let root = session_root(&fresh_user(&dir));
let wanted = preset_caveats(PermissionPreset::WorkspaceDev);
let op = attenuate(&root, &wanted).unwrap();
assert_eq!(enforced_caveats(&op).unwrap(), wanted);
op.cert().verify().unwrap();
}
#[test]
fn operating_key_cannot_widen() {
let dir = TempDir::new().unwrap();
let root = session_root(&fresh_user(&dir));
let op = attenuate(&root, &preset_caveats(PermissionPreset::WorkspaceDev)).unwrap();
let widen = attenuate(&op, &preset_caveats(PermissionPreset::FullAccess));
assert!(
widen.is_err(),
"an operating key must not be able to delegate wider authority than it holds"
);
}
#[test]
fn serialize_for_plugin_round_trips_and_roots_at_operator_user() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("identity.pem");
let user = load_or_generate(&path).unwrap();
let user_fp = user.fingerprint();
let session = session_root(&user);
let dispatch_caveats = preset_caveats(PermissionPreset::WorkspaceDev);
let worker = attenuate(&session, &dispatch_caveats).unwrap();
let mut plugin_caveats = dispatch_caveats.clone();
plugin_caveats.exec = newt_core::Scope::none();
plugin_caveats.fs_write = newt_core::Scope::none();
let envelope = delegate_for_plugin(&worker, "provider-plugin", plugin_caveats).unwrap();
use base64::Engine;
let json = base64::engine::general_purpose::STANDARD
.decode(&envelope)
.unwrap();
let cert: agent_mesh_protocol::CertChain = serde_json::from_slice(&json).unwrap();
cert.verify().expect("plugin cert chain must verify");
assert_eq!(
cert.user_fingerprint(),
user_fp,
"plugin leaf must chain back to the operator UserKey, \
NOT a freshly-generated synthetic key"
);
assert_eq!(cert.root_user_pubkey(), user.public());
}
#[test]
fn serialize_for_plugin_refuses_amplification() {
let dir = TempDir::new().unwrap();
let user = fresh_user(&dir);
let session = session_root(&user);
let narrow = preset_caveats(PermissionPreset::ReadOnly);
let worker = attenuate(&session, &narrow).unwrap();
let amplifying = preset_caveats(PermissionPreset::FullAccess);
let err = delegate_for_plugin(&worker, "evil-plugin", amplifying)
.expect_err("amplifying delegation must refuse");
assert!(
matches!(err, EnvelopeError::Amplification),
"expected Amplification, got {err:?}"
);
}
#[test]
fn serialize_for_plugin_three_link_chain_operator_worker_plugin() {
let dir = TempDir::new().unwrap();
let user = fresh_user(&dir);
let root = session_root(&user);
let worker = attenuate(&root, &preset_caveats(PermissionPreset::WorkspaceDev)).unwrap();
let envelope = delegate_for_plugin(
&worker,
"plugin",
preset_caveats(PermissionPreset::ReadOnly),
)
.unwrap();
use base64::Engine;
let json = base64::engine::general_purpose::STANDARD
.decode(&envelope)
.unwrap();
let leaf: agent_mesh_protocol::CertChain = serde_json::from_slice(&json).unwrap();
leaf.verify().expect("three-link chain must verify");
assert_eq!(leaf.user_fingerprint(), user.fingerprint());
match &leaf.issuer {
agent_mesh_protocol::Issuer::Agent { parent, .. } => match &parent.issuer {
agent_mesh_protocol::Issuer::Agent { parent: gp, .. } => match &gp.issuer {
agent_mesh_protocol::Issuer::User(u) => {
assert_eq!(u.fingerprint(), user.fingerprint());
}
_ => panic!("third link must be Issuer::User"),
},
_ => panic!("middle link must be Issuer::Agent"),
},
_ => panic!("leaf must be Issuer::Agent"),
}
}
#[test]
fn caveats_round_trip_through_sign_and_verify_is_identity() {
let dir = TempDir::new().unwrap();
let root = session_root(&fresh_user(&dir));
for p in [
PermissionPreset::ReadOnly,
PermissionPreset::WorkspaceEdit,
PermissionPreset::WorkspaceDev,
PermissionPreset::FullAccess,
] {
let label = format!("{p:?}");
let c = preset_caveats(p);
let op = attenuate(&root, &c).unwrap();
let back = enforced_caveats(&op).unwrap();
assert_eq!(c, back, "preset {label} must survive sign+verify");
}
}
}