use std::process::Stdio;
use std::sync::Arc;
use async_trait::async_trait;
use newt_core::router::Tier;
use newt_identity::{AgentKey, AgentMetadata};
use plugins_protocol::{InitializeRequest, PluginClient, AGENT_KEY_ENV, PROTOCOL_VERSION};
use tokio::process::Command;
use crate::backend::{ChatReply, ChatRequest, InferenceBackend};
pub struct ProviderPluginBackend {
name: String,
command: String,
model_id: String,
env_pass: Vec<String>,
tiers: Vec<Tier>,
agent_key_envelope: Option<String>,
parent_key: Option<Arc<AgentKey>>,
child_metadata: Option<AgentMetadata>,
}
impl ProviderPluginBackend {
pub fn new(
name: impl Into<String>,
command: impl Into<String>,
model_id: impl Into<String>,
tiers: Vec<Tier>,
) -> Self {
Self {
name: name.into(),
command: command.into(),
model_id: model_id.into(),
env_pass: Vec::new(),
tiers,
agent_key_envelope: None,
parent_key: None,
child_metadata: None,
}
}
pub fn from_config(cfg: &newt_core::config::ProviderConfig) -> anyhow::Result<Self> {
let model = cfg
.model
.as_deref()
.filter(|m| !m.trim().is_empty())
.ok_or_else(|| anyhow::anyhow!("provider '{}' is missing required model", cfg.name))?;
Ok(Self::new(
cfg.name.clone(),
cfg.command.clone(),
model.to_string(),
cfg.tiers.clone(),
)
.with_env_pass(cfg.env_pass.clone()))
}
#[must_use]
pub fn with_env_pass(mut self, env_pass: Vec<String>) -> Self {
self.env_pass = env_pass;
self
}
#[must_use]
pub fn with_agent_key_envelope(mut self, envelope: impl Into<String>) -> Self {
let s = envelope.into();
self.agent_key_envelope = if s.is_empty() { None } else { Some(s) };
self
}
#[must_use]
pub fn agent_key_envelope(&self) -> Option<&str> {
self.agent_key_envelope.as_deref()
}
#[must_use]
pub fn with_parent_key(mut self, parent: Arc<AgentKey>, child_metadata: AgentMetadata) -> Self {
self.parent_key = Some(parent);
self.child_metadata = Some(child_metadata);
self
}
#[must_use]
pub fn has_parent_key(&self) -> bool {
self.parent_key.is_some()
}
#[must_use]
pub fn parent_key(&self) -> Option<&Arc<AgentKey>> {
self.parent_key.as_ref()
}
pub fn mint_plugin_envelope(&self) -> Option<Result<String, newt_identity::EnvelopeError>> {
let parent = self.parent_key.as_ref()?;
let metadata = self.child_metadata.as_ref()?;
Some(newt_identity::serialize_for_plugin(
parent.as_ref(),
metadata.clone(),
))
}
#[must_use]
pub fn spawn_command(&self) -> Command {
let mut cmd = Command::new(&self.command);
cmd.env_clear()
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
for name in &self.env_pass {
if let Ok(value) = std::env::var(name) {
cmd.env(name, value);
}
}
#[cfg(windows)]
for name in ["ComSpec", "SystemRoot", "PATHEXT"] {
if let Ok(value) = std::env::var(name) {
cmd.env(name, value);
}
}
if let Some(minted) = self.mint_plugin_envelope() {
match minted {
Ok(envelope) => {
cmd.env(AGENT_KEY_ENV, envelope);
}
Err(e) => {
tracing::error!(
error = %e,
plugin = %self.name,
"ProviderPluginBackend: failed to mint plugin envelope from parent key; \
stripping NEWT_AGENT_KEY"
);
cmd.env_remove(AGENT_KEY_ENV);
}
}
} else if let Some(envelope) = &self.agent_key_envelope {
cmd.env(AGENT_KEY_ENV, envelope);
} else {
cmd.env_remove(AGENT_KEY_ENV);
}
cmd
}
}
#[async_trait]
impl InferenceBackend for ProviderPluginBackend {
fn name(&self) -> &str {
&self.name
}
fn model_id(&self) -> &str {
&self.model_id
}
fn supports_tier(&self, tier: Tier) -> bool {
self.tiers.contains(&tier)
}
async fn complete(&self, req: ChatRequest) -> anyhow::Result<ChatReply> {
let mut client = PluginClient::spawn_command(self.spawn_command())?;
client
.initialize(InitializeRequest {
protocol_version: PROTOCOL_VERSION,
client_name: "newt-agent".to_string(),
client_version: env!("CARGO_PKG_VERSION").to_string(),
})
.await?;
let response = client
.complete(plugins_protocol::CompleteRequest {
model: self.model_id.clone(),
messages: req
.messages
.into_iter()
.map(|m| plugins_protocol::Message {
role: m.role,
content: m.content,
})
.collect(),
max_tokens: req.max_tokens,
})
.await?;
let _ = client.shutdown().await;
Ok(ChatReply {
content: response.content,
model_id: response.model_id,
usage: response.usage.map(|usage| newt_core::TokenUsage {
input_tokens: usage.input_tokens,
output_tokens: usage.output_tokens,
}),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_cloud_provider_plugin_prices_a_family_named_model_as_unknown() {
let plugin =
ProviderPluginBackend::new("openai", "newt-provider-openai", "deepseek-chat", vec![]);
let usage = newt_core::TokenUsage {
input_tokens: 1_000,
output_tokens: 500,
};
assert!(!plugin.is_local());
let cost = newt_core::PricingConfig::default().estimate_cost(
"deepseek-chat",
plugin.is_local(),
Some(&usage),
);
assert_eq!(cost, None);
}
use newt_core::Caveats;
use newt_identity::{load_or_generate, plugin_child_metadata, session_root};
use std::path::{Path, PathBuf};
use tempfile::TempDir;
#[test]
fn new_defaults_envelope_to_none() {
let b = ProviderPluginBackend::new("openai", "newt-provider-openai", "gpt-4", vec![]);
assert_eq!(b.agent_key_envelope(), None);
assert!(!b.has_parent_key(), "new backend must have no parent key");
}
#[test]
fn with_agent_key_envelope_stores_value() {
let b = ProviderPluginBackend::new("openai", "x", "gpt-4", vec![])
.with_agent_key_envelope("abc123==");
assert_eq!(b.agent_key_envelope(), Some("abc123=="));
}
#[test]
fn empty_envelope_is_treated_as_none() {
let b =
ProviderPluginBackend::new("openai", "x", "gpt-4", vec![]).with_agent_key_envelope("");
assert_eq!(b.agent_key_envelope(), None);
}
#[test]
fn supports_tier_matches_constructor_tiers() {
let b = ProviderPluginBackend::new("openai", "x", "gpt-4", vec![Tier::Fast, Tier::Complex]);
assert!(b.supports_tier(Tier::Fast));
assert!(b.supports_tier(Tier::Complex));
assert!(!b.supports_tier(Tier::Standard));
}
#[tokio::test]
async fn complete_round_trips_through_mock_plugin() {
let dir = tests_common::tempdir();
let init = r#"{"jsonrpc":"2.0","id":1,"result":{"plugin_name":"mock-openai","plugin_version":"0.0.0-test","supported_models":["gpt-test"]}}"#;
let completion = r#"{"jsonrpc":"2.0","id":2,"result":{"content":"mocked reply","model_id":"gpt-test","usage":{"input_tokens":7,"output_tokens":11}}}"#;
let shutdown = r#"{"jsonrpc":"2.0","id":3,"result":{}}"#;
let plugin = tests_common::mock_plugin_binary(dir.path(), &[init, completion, shutdown]);
let backend = ProviderPluginBackend::new(
"openai",
plugin.to_string_lossy(),
"gpt-test",
vec![Tier::Complex],
);
let reply = backend
.complete(ChatRequest::new().user("hello").max_tokens(32))
.await
.unwrap();
assert_eq!(reply.content, "mocked reply");
assert_eq!(reply.model_id, "gpt-test");
let usage = reply.usage.expect("usage propagated");
assert_eq!(usage.input_tokens, 7);
assert_eq!(usage.output_tokens, 11);
}
#[tokio::test]
async fn complete_passes_only_whitelisted_environment_variables() {
let dir = tests_common::tempdir();
let plugin = env_probe_plugin(dir.path());
std::env::set_var("NEWT_TEST_OPENAI_ALLOWED", "allowed");
std::env::set_var("NEWT_TEST_OPENAI_BLOCKED", "blocked");
let backend = ProviderPluginBackend::new(
"openai",
plugin.to_string_lossy(),
"gpt-test",
vec![Tier::Complex],
)
.with_env_pass(vec!["NEWT_TEST_OPENAI_ALLOWED".into()]);
let reply = backend
.complete(ChatRequest::new().user("hello"))
.await
.unwrap();
std::env::remove_var("NEWT_TEST_OPENAI_ALLOWED");
std::env::remove_var("NEWT_TEST_OPENAI_BLOCKED");
assert_eq!(reply.content, "allowed=allowed blocked=");
}
#[cfg(unix)]
fn env_probe_plugin(dir: &Path) -> PathBuf {
use std::io::Write;
use std::os::unix::fs::PermissionsExt;
let path = dir.join("env-probe");
let mut file = std::fs::File::create(&path).unwrap();
file.write_all(
br#"#!/bin/sh
IFS= read -r _request || exit 0
printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"plugin_name":"env-probe","plugin_version":"0.0.0-test","supported_models":["gpt-test"]}}'
IFS= read -r _request || exit 0
printf '{"jsonrpc":"2.0","id":2,"result":{"content":"allowed=%s blocked=%s","model_id":"gpt-test","usage":null}}\n' "$NEWT_TEST_OPENAI_ALLOWED" "$NEWT_TEST_OPENAI_BLOCKED"
IFS= read -r _request || exit 0
printf '%s\n' '{"jsonrpc":"2.0","id":3,"result":{}}'
"#,
)
.unwrap();
file.sync_all().unwrap();
drop(file);
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
path
}
#[cfg(windows)]
fn env_probe_plugin(dir: &Path) -> PathBuf {
let path = dir.join("env-probe.cmd");
std::fs::write(
&path,
concat!(
"@echo off\r\n",
"set /p _request=\r\n",
"echo {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"plugin_name\":\"env-probe\",\"plugin_version\":\"0.0.0-test\",\"supported_models\":[\"gpt-test\"]}}\r\n",
"set /p _request=\r\n",
"echo {\"jsonrpc\":\"2.0\",\"id\":2,\"result\":{\"content\":\"allowed=%NEWT_TEST_OPENAI_ALLOWED% blocked=%NEWT_TEST_OPENAI_BLOCKED%\",\"model_id\":\"gpt-test\",\"usage\":null}}\r\n",
"set /p _request=\r\n",
"echo {\"jsonrpc\":\"2.0\",\"id\":3,\"result\":{}}\r\n"
),
)
.unwrap();
path
}
#[test]
fn with_parent_key_stores_arc_and_metadata() {
let dir = TempDir::new().unwrap();
let user = load_or_generate(&dir.path().join("identity.pem")).unwrap();
let root = Arc::new(session_root(&user));
let metadata = plugin_child_metadata("provider-plugin", Caveats::top());
let b = ProviderPluginBackend::new("openai", "x", "gpt-4", vec![])
.with_parent_key(Arc::clone(&root), metadata);
assert!(b.has_parent_key());
assert!(
Arc::ptr_eq(b.parent_key().unwrap(), &root),
"parent_key must store the exact Arc the caller passed"
);
}
#[test]
fn mint_plugin_envelope_chain_roots_at_operator_userkey() {
use base64::Engine;
let dir = TempDir::new().unwrap();
let user = load_or_generate(&dir.path().join("identity.pem")).unwrap();
let user_fp = user.fingerprint();
let root = Arc::new(session_root(&user));
let metadata = plugin_child_metadata("provider-plugin", Caveats::top());
let b = ProviderPluginBackend::new("openai", "x", "gpt-4", vec![])
.with_parent_key(root, metadata);
let envelope = b
.mint_plugin_envelope()
.expect("parent_key set → envelope must be available")
.expect("delegation must succeed for ⊑ caveats");
let json = base64::engine::general_purpose::STANDARD
.decode(&envelope)
.expect("envelope is base64");
let cert: agent_mesh_protocol::CertChain =
serde_json::from_slice(&json).expect("envelope is JSON CertChain");
cert.verify().expect("chain must verify end to end");
assert_eq!(
cert.user_fingerprint(),
user_fp,
"leaf must chain back to the operator UserKey"
);
}
#[test]
fn mint_plugin_envelope_none_without_parent_key() {
let b = ProviderPluginBackend::new("openai", "x", "gpt-4", vec![]);
assert!(
b.mint_plugin_envelope().is_none(),
"no parent key → no envelope minted"
);
}
#[test]
fn mint_plugin_envelope_refuses_amplification() {
let dir = TempDir::new().unwrap();
let user = load_or_generate(&dir.path().join("identity.pem")).unwrap();
let root = session_root(&user);
let narrowed_caveats = Caveats {
exec: newt_core::Scope::none(),
..Caveats::top()
};
let worker = newt_identity::attenuate(&root, &narrowed_caveats).unwrap();
let plugin_meta = plugin_child_metadata("evil-plugin", Caveats::top());
let b = ProviderPluginBackend::new("openai", "x", "gpt-4", vec![])
.with_parent_key(Arc::new(worker), plugin_meta);
let err = b
.mint_plugin_envelope()
.expect("parent set → envelope path attempted")
.expect_err("amplifying delegation must refuse");
assert!(
matches!(err, newt_identity::EnvelopeError::Amplification),
"expected Amplification, got {err:?}"
);
}
#[test]
fn parent_key_takes_precedence_over_opaque_envelope() {
use base64::Engine;
let dir = TempDir::new().unwrap();
let user = load_or_generate(&dir.path().join("identity.pem")).unwrap();
let root = Arc::new(session_root(&user));
let metadata = plugin_child_metadata("plugin", Caveats::top());
let b = ProviderPluginBackend::new("openai", "x", "gpt-4", vec![])
.with_agent_key_envelope("opaque-legacy-string-should-be-ignored")
.with_parent_key(Arc::clone(&root), metadata);
let envelope = b
.mint_plugin_envelope()
.expect("parent_key set → minted envelope path wins")
.expect("delegation must succeed");
assert!(base64::engine::general_purpose::STANDARD
.decode(&envelope)
.is_ok());
assert_ne!(envelope, "opaque-legacy-string-should-be-ignored");
}
#[test]
fn three_link_chain_operator_worker_plugin_verifies() {
use base64::Engine;
let dir = TempDir::new().unwrap();
let user = load_or_generate(&dir.path().join("identity.pem")).unwrap();
let root = session_root(&user);
let dispatch_caveats = Caveats {
exec: newt_core::Scope::only(["git".to_string()]),
..Caveats::top()
};
let worker = newt_identity::attenuate(&root, &dispatch_caveats).unwrap();
let plugin_caveats = Caveats {
fs_write: newt_core::Scope::none(),
exec: newt_core::Scope::only(["git".to_string()]),
..Caveats::top()
};
let plugin_meta = plugin_child_metadata("plugin", plugin_caveats.clone());
let b = ProviderPluginBackend::new("p", "x", "m", vec![])
.with_parent_key(Arc::new(worker), plugin_meta);
let envelope = b.mint_plugin_envelope().unwrap().unwrap();
let bytes = base64::engine::general_purpose::STANDARD
.decode(&envelope)
.unwrap();
let leaf: agent_mesh_protocol::CertChain = serde_json::from_slice(&bytes).unwrap();
leaf.verify().unwrap();
assert_eq!(leaf.user_fingerprint(), user.fingerprint());
assert_eq!(leaf.metadata.caveats, plugin_caveats);
}
#[test]
fn each_spawn_mints_a_fresh_child_leaf_key() {
let dir = TempDir::new().unwrap();
let user = load_or_generate(&dir.path().join("identity.pem")).unwrap();
let root = Arc::new(session_root(&user));
let metadata = plugin_child_metadata("plugin", Caveats::top());
let b = ProviderPluginBackend::new("p", "x", "m", vec![]).with_parent_key(root, metadata);
let e1 = b.mint_plugin_envelope().unwrap().unwrap();
let e2 = b.mint_plugin_envelope().unwrap().unwrap();
assert_ne!(e1, e2, "every spawn must mint a fresh ephemeral child leaf");
}
}