use std::{
net::IpAddr,
sync::{Arc, Mutex, OnceLock},
};
use murmur_artifact::{ControlConfig, ControllableSetting, InferenceConfig};
use serde_json::{json, Value};
use tokio::io::{AsyncRead, AsyncReadExt};
use crate::{
driver_choice::{DriverChoice, DriverChoices},
mac_token::{self, MintKey},
resource_plane::ResourceResponse,
trace::{ControlChange, ResourceTraceAppender},
};
pub(crate) const CONTROL_PATH: &str = "/control";
pub(crate) const TOKEN_TAG: &str = "ctl1";
pub(crate) const TOKEN_DOMAIN: &[u8] = b"murmur-control-token-v1";
pub(crate) const SETTINGS_BODY_MAX_BYTES: usize = 1024;
pub(crate) const SECRET_BODY_MAX_BYTES: usize = 8192;
pub(crate) const APPLIES_FROM: &str = "next_inference_call";
const BEARER_CHALLENGE: &str = "Bearer realm=\"murmur-control\"";
const SETTINGS_PREFIX: &str = "/control/settings/";
const SECRETS_PREFIX: &str = "/control/secrets/";
pub(crate) struct SecretValue(Vec<u8>);
impl SecretValue {
fn from_bytes(bytes: Vec<u8>) -> Self {
Self(bytes)
}
fn is_header_safe(&self) -> bool {
!self.0.is_empty() && self.0.iter().all(|byte| (0x20..=0x7e).contains(byte))
}
fn expose(&self) -> &str {
std::str::from_utf8(&self.0).unwrap_or_default()
}
fn zeroize(&mut self) {
mac_token::zeroize(&mut self.0);
}
}
impl Drop for SecretValue {
fn drop(&mut self) {
self.zeroize();
}
}
impl std::fmt::Debug for SecretValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("<redacted>")
}
}
pub(crate) struct InjectedSecrets {
slots: Mutex<Vec<(String, Option<SecretValue>)>>,
}
impl InjectedSecrets {
pub(crate) fn new(names: &[String]) -> Self {
Self {
slots: Mutex::new(names.iter().map(|name| (name.clone(), None)).collect()),
}
}
fn with_slot<T>(&self, name: &str, f: impl FnOnce(&mut Option<SecretValue>) -> T) -> Option<T> {
let mut slots = self.slots.lock().unwrap_or_else(|e| e.into_inner());
slots
.iter_mut()
.find(|(declared, _)| declared == name)
.map(|(_, slot)| f(slot))
}
pub(crate) fn declares(&self, name: &str) -> bool {
self.with_slot(name, |_| ()).is_some()
}
pub(crate) fn holds(&self, name: &str) -> bool {
self.with_slot(name, |slot| slot.is_some()).unwrap_or(false)
}
pub(crate) fn value(&self, name: &str) -> Option<String> {
self.with_slot(name, |slot| {
slot.as_ref().map(|value| value.expose().to_string())
})
.flatten()
}
fn set(&self, name: &str, value: SecretValue) -> Option<bool> {
self.with_slot(name, |slot| slot.replace(value).is_some())
}
fn forget(&self, name: &str) -> Option<bool> {
self.with_slot(name, |slot| slot.take().is_some())
}
fn listing(&self) -> Vec<(String, bool)> {
let slots = self.slots.lock().unwrap_or_else(|e| e.into_inner());
slots
.iter()
.map(|(name, slot)| (name.clone(), slot.is_some()))
.collect()
}
}
impl std::fmt::Debug for InjectedSecrets {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut map = f.debug_map();
for (name, set) in self.listing() {
map.entry(&name, &if set { "<redacted>" } else { "<unset>" });
}
map.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Principal {
Controller,
Agent,
}
impl Principal {
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Controller => "controller",
Self::Agent => "agent",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SettingValue {
Tokens(u32),
Driver(usize),
}
impl SettingValue {
fn to_json(self, choices: &DriverChoices) -> Value {
match self {
Self::Tokens(count) => json!(count),
Self::Driver(index) => json!(choices.get(index).map(|choice| choice.name.as_str())),
}
}
pub(crate) fn tokens(self) -> Option<u32> {
match self {
Self::Tokens(count) => Some(count),
Self::Driver(_) => None,
}
}
pub(crate) fn driver(self) -> Option<usize> {
match self {
Self::Driver(index) => Some(index),
Self::Tokens(_) => None,
}
}
}
fn launch_value(setting: ControllableSetting, inference: &InferenceConfig) -> SettingValue {
match setting {
ControllableSetting::InferenceMaxTokens => SettingValue::Tokens(
inference
.max_tokens
.unwrap_or(crate::agent::DEFAULT_MAX_OUTPUT_TOKENS),
),
ControllableSetting::InferenceDriver => SettingValue::Driver(0),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ChangeRefused {
pub(crate) status: u16,
pub(crate) reason: &'static str,
pub(crate) message: String,
}
impl ChangeRefused {
fn new(status: u16, reason: &'static str, message: impl Into<String>) -> Self {
Self {
status,
reason,
message: message.into(),
}
}
}
fn parse_setting_value(
setting: ControllableSetting,
value: &Value,
choices: &DriverChoices,
) -> Result<SettingValue, ChangeRefused> {
match setting {
ControllableSetting::InferenceMaxTokens => value
.as_u64()
.and_then(|count| u32::try_from(count).ok())
.filter(|count| *count >= 1)
.map(SettingValue::Tokens)
.ok_or_else(|| {
ChangeRefused::new(
422,
"invalid_value",
format!(
"{} takes an integer from 1 to {}",
setting.wire_name(),
u32::MAX
),
)
}),
ControllableSetting::InferenceDriver => value
.as_str()
.and_then(|name| choices.index_of(name))
.map(SettingValue::Driver)
.ok_or_else(|| {
ChangeRefused::new(
422,
"undeclared_driver",
format!(
"{} takes the name of a declared driver choice; this capsule declares: {}",
setting.wire_name(),
choices.names()
),
)
}),
}
}
struct SettingSlot {
setting: ControllableSetting,
value: SettingValue,
change_id: Option<String>,
last_used: SettingValue,
controller: bool,
agent: bool,
}
impl SettingSlot {
fn permits(&self, principal: Principal) -> bool {
match principal {
Principal::Controller => self.controller,
Principal::Agent => self.agent,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct AcceptedChange {
pub(crate) change_id: String,
pub(crate) previous: Value,
pub(crate) value: Value,
}
pub(crate) struct ControlState {
settings: Mutex<Vec<SettingSlot>>,
secrets: Arc<InjectedSecrets>,
choices: DriverChoices,
}
impl std::fmt::Debug for ControlState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ControlState")
.field("secrets", &self.secrets)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SettingForCall {
pub(crate) value: SettingValue,
pub(crate) applies_change: Option<String>,
}
impl ControlState {
pub(crate) fn new(
config: &ControlConfig,
inference: &InferenceConfig,
choices: DriverChoices,
secrets: Arc<InjectedSecrets>,
) -> Self {
let declared = config
.settings
.iter()
.chain(
config
.agent_settings
.iter()
.filter(|setting| !config.settings.contains(setting)),
)
.map(|setting| {
let value = launch_value(*setting, inference);
SettingSlot {
setting: *setting,
value,
change_id: None,
last_used: value,
controller: config.controller_may_set(*setting),
agent: config.agent_may_set(*setting),
}
})
.collect();
Self {
settings: Mutex::new(declared),
secrets,
choices,
}
}
pub(crate) fn choices(&self) -> &DriverChoices {
&self.choices
}
pub(crate) fn has_controller_surface(&self) -> bool {
self.lock_settings().iter().any(|slot| slot.controller)
|| !self.secrets.listing().is_empty()
}
fn lock_settings(&self) -> std::sync::MutexGuard<'_, Vec<SettingSlot>> {
self.settings.lock().unwrap_or_else(|e| e.into_inner())
}
pub(crate) fn permits(&self, principal: Principal, setting: ControllableSetting) -> bool {
self.lock_settings()
.iter()
.any(|slot| slot.setting == setting && slot.permits(principal))
}
fn choice_available(&self, choice: &DriverChoice) -> bool {
choice.staged()
&& choice
.injected_secret
.as_deref()
.is_none_or(|name| self.secrets.holds(name))
}
fn unresolvable(&self, choice: &DriverChoice) -> Option<ChangeRefused> {
if let Some(credential) = &choice.unresolved_credential {
return Some(ChangeRefused::new(
409,
"credential_unresolvable",
format!(
"driver choice '{}' cannot be selected: its credential {credential} was not \
found when the capsule launched",
choice.name
),
));
}
let name = choice
.injected_secret
.as_deref()
.filter(|name| !self.secrets.holds(name))?;
Some(ChangeRefused::new(
409,
"credential_unresolvable",
format!(
"driver choice '{}' cannot be selected: its credential {name} is a control.secrets \
name with no value injected; supply it with `mur control secret {name}` first",
choice.name
),
))
}
pub(crate) fn request_change(
&self,
principal: Principal,
setting: ControllableSetting,
requested: &Value,
) -> Result<AcceptedChange, ChangeRefused> {
let value = parse_setting_value(setting, requested, &self.choices)?;
let mut settings = self.lock_settings();
let Some(slot) = settings
.iter_mut()
.find(|slot| slot.setting == setting && slot.permits(principal))
else {
return Err(ChangeRefused::new(
404,
"undeclared_setting",
"this capsule's control: block does not declare that setting",
));
};
if let Some(refusal) = value
.driver()
.and_then(|index| self.choices.get(index))
.and_then(|choice| self.unresolvable(choice))
{
return Err(refusal);
}
let change_id = crate::trace::new_event_id();
let previous = slot.value;
slot.value = value;
slot.change_id = Some(change_id.clone());
Ok(AcceptedChange {
change_id,
previous: previous.to_json(&self.choices),
value: value.to_json(&self.choices),
})
}
fn forget_secret(&self, name: &str) -> Result<Option<bool>, ChangeRefused> {
let settings = self.lock_settings();
let in_use = settings
.iter()
.find(|slot| slot.setting == ControllableSetting::InferenceDriver)
.and_then(|slot| slot.value.driver())
.and_then(|index| self.choices.get(index))
.filter(|choice| choice.injected_secret.as_deref() == Some(name));
if let Some(choice) = in_use {
return Err(ChangeRefused::new(
409,
"in_use",
format!(
"{name} keys driver choice '{}', which is in use; switch inference.driver to \
another choice before forgetting it",
choice.name
),
));
}
let forgotten = self.secrets.forget(name);
drop(settings);
Ok(forgotten)
}
pub(crate) fn selected(&self, setting: ControllableSetting) -> Option<SettingValue> {
self.lock_settings()
.iter()
.find(|slot| slot.setting == setting)
.map(|slot| slot.value)
}
pub(crate) fn for_call(&self, setting: ControllableSetting) -> Option<SettingForCall> {
let mut settings = self.lock_settings();
let slot = settings.iter_mut().find(|slot| slot.setting == setting)?;
let applies_change = (slot.value != slot.last_used)
.then(|| slot.change_id.clone())
.flatten();
slot.last_used = slot.value;
Some(SettingForCall {
value: slot.value,
applies_change,
})
}
pub(crate) fn session_control(&self) -> crate::trace::SessionControl {
let settings = self.lock_settings();
let names = |principal: Principal| {
settings
.iter()
.filter(|slot| slot.permits(principal))
.map(|slot| slot.setting.wire_name())
.collect()
};
crate::trace::SessionControl {
settings: names(Principal::Controller),
secrets: self
.secrets
.listing()
.into_iter()
.map(|(name, _)| name)
.collect(),
agent_settings: names(Principal::Agent),
}
}
fn settings_listing(&self) -> Vec<Value> {
let settings = self.lock_settings();
settings
.iter()
.filter(|slot| slot.controller)
.map(|slot| {
let mut entry = json!({
"name": slot.setting.wire_name(),
"value": slot.value.to_json(&self.choices),
});
if slot.setting == ControllableSetting::InferenceDriver {
entry["choices"] =
json!(self.choices.listing(|choice| self.choice_available(choice)));
}
entry
})
.collect()
}
}
pub(crate) fn mint_control_token(key: &MintKey, session_id: &str) -> Result<String, String> {
let payload = json!({
"session_id": session_id,
"nonce": mac_token::random_hex(mac_token::NONCE_BYTES)?,
});
Ok(mac_token::seal(
key,
TOKEN_TAG,
TOKEN_DOMAIN,
payload.to_string().as_bytes(),
&[session_id],
))
}
pub(crate) fn verify_control_token(key: &MintKey, session_id: &str, token: &str) -> bool {
let Ok(payload) = mac_token::open(key, TOKEN_TAG, TOKEN_DOMAIN, token, &[session_id]) else {
return false;
};
serde_json::from_slice::<Value>(&payload)
.ok()
.and_then(|payload| payload["session_id"].as_str().map(|id| id == session_id))
.unwrap_or(false)
}
pub(crate) fn is_loopback_peer(peer: IpAddr) -> bool {
match peer {
IpAddr::V4(v4) => v4.is_loopback(),
IpAddr::V6(v6) => {
v6.is_loopback() || v6.to_ipv4_mapped().is_some_and(|v4| v4.is_loopback())
}
}
}
struct Declared {
key: MintKey,
state: Arc<ControlState>,
}
pub(crate) struct ControlPlane {
declared: Option<Declared>,
session_id: String,
trace: OnceLock<Arc<ResourceTraceAppender>>,
}
impl std::fmt::Debug for ControlPlane {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ControlPlane")
.field("session_id", &self.session_id)
.field("declared", &self.declared.is_some())
.finish_non_exhaustive()
}
}
impl ControlPlane {
pub(crate) fn undeclared(session_id: String) -> Self {
Self {
declared: None,
session_id,
trace: OnceLock::new(),
}
}
pub(crate) fn declared(
session_id: String,
state: Arc<ControlState>,
) -> Result<(Self, String), String> {
let key = MintKey::generate()?;
let token = mint_control_token(&key, &session_id)?;
Ok((
Self {
declared: Some(Declared { key, state }),
session_id,
trace: OnceLock::new(),
},
token,
))
}
pub(crate) fn attach_trace(&self, trace: Arc<ResourceTraceAppender>) {
let _ = self.trace.set(trace);
}
async fn record_refused(
&self,
refusal: &Refusal<'_>,
authenticated: Option<&Authenticated<'_>>,
) {
let Some(trace) = self.trace.get() else {
return;
};
let (kind, name, token_id) = match authenticated {
Some(auth) => (
refusal.target.map(|target| target.kind),
refusal.target.map(|target| target.name),
Some(auth.token_id.as_str()),
),
None => (None, None, None),
};
trace
.write_control_refused(
Principal::Controller,
refusal.status,
refusal.reason,
kind,
name,
token_id,
)
.await;
}
async fn record_change(&self, change: ControlChange<'_>) {
if let Some(trace) = self.trace.get() {
trace.write_control_change(change).await;
}
}
}
pub(crate) fn is_control_path(path: &str) -> bool {
path == CONTROL_PATH || path.starts_with("/control/") || path.starts_with("/control?")
}
pub(crate) struct ControlRequest<'a> {
pub(crate) method: &'a str,
pub(crate) path: &'a str,
pub(crate) authorization: Option<&'a str>,
pub(crate) peer: Option<IpAddr>,
pub(crate) content_length: usize,
pub(crate) is_json: bool,
}
#[derive(Clone, Copy)]
struct Target<'a> {
kind: &'static str,
name: &'a str,
}
impl<'a> Target<'a> {
fn setting(name: &'a str) -> Self {
Self {
kind: "setting",
name,
}
}
fn secret(name: &'a str) -> Self {
Self {
kind: "secret",
name,
}
}
}
struct Authenticated<'a> {
token_id: String,
state: &'a ControlState,
}
struct Refusal<'a> {
status: u16,
reason: &'static str,
message: String,
target: Option<Target<'a>>,
allow: Option<&'static str>,
}
impl<'a> Refusal<'a> {
fn new(status: u16, reason: &'static str, message: impl Into<String>) -> Self {
Self {
status,
reason,
message: message.into(),
target: None,
allow: None,
}
}
fn naming(mut self, target: Target<'a>) -> Self {
self.target = Some(target);
self
}
fn response(&self) -> ResourceResponse {
let mut headers = vec![("content-type".to_string(), "application/json".to_string())];
if self.status == 401 {
headers.push(("www-authenticate".to_string(), BEARER_CHALLENGE.to_string()));
}
if let Some(allow) = self.allow {
headers.push(("allow".to_string(), allow.to_string()));
}
json_response(self.status, headers, &json!({"error": self.message}))
}
}
impl From<ChangeRefused> for Refusal<'_> {
fn from(refused: ChangeRefused) -> Self {
Self::new(refused.status, refused.reason, refused.message)
}
}
fn json_response(status: u16, headers: Vec<(String, String)>, body: &Value) -> ResourceResponse {
ResourceResponse::framed(status, headers, body.to_string().into_bytes())
}
fn ok(body: Value) -> ResourceResponse {
json_response(
200,
vec![("content-type".to_string(), "application/json".to_string())],
&body,
)
}
pub(crate) async fn handle_control_request<R: AsyncRead + Unpin>(
plane: &ControlPlane,
request: ControlRequest<'_>,
body: &mut R,
) -> ResourceResponse {
let Some(declared) = plane.declared.as_ref() else {
return Refusal::new(
404,
"undeclared",
"this capsule declares no control surface",
)
.response();
};
let token = request
.authorization
.and_then(crate::door_auth::bearer_token);
let Some(token) =
token.filter(|token| verify_control_token(&declared.key, &plane.session_id, token))
else {
let refusal = Refusal::new(
401,
"unauthenticated",
"the control surface takes this session's control token as Authorization: Bearer",
);
plane.record_refused(&refusal, None).await;
return refusal.response();
};
let auth = Authenticated {
token_id: mac_token::token_id(token),
state: &declared.state,
};
let outcome = serve_authenticated(plane, &auth, &request, body).await;
match outcome {
Ok(response) => response,
Err(refusal) => {
plane.record_refused(&refusal, Some(&auth)).await;
refusal.response()
}
}
}
async fn serve_authenticated<'r, R: AsyncRead + Unpin>(
plane: &ControlPlane,
auth: &Authenticated<'_>,
request: &ControlRequest<'r>,
body: &mut R,
) -> Result<ResourceResponse, Refusal<'r>> {
let path = request.path.split('?').next().unwrap_or_default();
let not_found = || Refusal::new(404, "unknown_path", "no control resource at this path");
let wrong_method = |allow: &'static str, target: Option<Target<'r>>| Refusal {
status: 405,
reason: "method_not_allowed",
message: format!("this control resource answers {allow}"),
target,
allow: Some(allow),
};
if path == CONTROL_PATH {
if request.method != "GET" {
return Err(wrong_method("GET", None));
}
return Ok(ok(listing(&plane.session_id, auth.state)));
}
if let Some(name) = path.strip_prefix(SETTINGS_PREFIX) {
let target = Target::setting(name);
let Some(setting) = ControllableSetting::from_wire_name(name)
.filter(|setting| auth.state.permits(Principal::Controller, *setting))
else {
return Err(Refusal::new(
404,
"undeclared_setting",
"this capsule's control: block does not declare that setting",
)
.naming(target));
};
if request.method != "PUT" {
return Err(wrong_method("PUT", Some(target)));
}
let value = read_setting_body(request, body)
.await
.map_err(|refusal| refusal.naming(target))?;
let change = auth
.state
.request_change(Principal::Controller, setting, &value)
.map_err(|refused| Refusal::from(refused).naming(target))?;
let answer = json!({
"name": setting.wire_name(),
"previous": change.previous,
"value": change.value,
"applies_from": APPLIES_FROM,
});
plane
.record_change(ControlChange {
event_id: change.change_id,
principal: Principal::Controller,
token_id: Some(&auth.token_id),
kind: "setting",
name: setting.wire_name(),
action: "set",
previous: Some(change.previous),
value: Some(change.value),
applies_from: Some(APPLIES_FROM),
replaced: None,
})
.await;
return Ok(ok(answer));
}
if let Some(name) = path.strip_prefix(SECRETS_PREFIX) {
let target = Target::secret(name);
if !auth.state.secrets.declares(name) {
return Err(Refusal::new(
404,
"undeclared_secret",
"this capsule's control: block does not declare that secret",
)
.naming(target));
}
if !matches!(request.method, "PUT" | "DELETE") {
return Err(wrong_method("PUT, DELETE", Some(target)));
}
if !request.peer.is_some_and(is_loopback_peer) {
return Err(Refusal::new(
403,
"not_loopback",
"a secret is accepted only over a loopback connection, because the door has no TLS",
)
.naming(target));
}
if request.method == "DELETE" {
auth.state
.forget_secret(name)
.map_err(|refused| Refusal::from(refused).naming(target))?;
plane
.record_change(ControlChange {
event_id: crate::trace::new_event_id(),
principal: Principal::Controller,
token_id: Some(&auth.token_id),
kind: "secret",
name,
action: "forget",
previous: None,
value: None,
applies_from: None,
replaced: None,
})
.await;
return Ok(ok(json!({"name": name, "set": false})));
}
let value = read_secret_body(request, body)
.await
.map_err(|refusal| refusal.naming(target))?;
let replaced = auth.state.secrets.set(name, value).ok_or_else(not_found)?;
plane
.record_change(ControlChange {
event_id: crate::trace::new_event_id(),
principal: Principal::Controller,
token_id: Some(&auth.token_id),
kind: "secret",
name,
action: "set",
previous: None,
value: None,
applies_from: None,
replaced: Some(replaced),
})
.await;
return Ok(ok(json!({"name": name, "set": true, "replaced": replaced})));
}
Err(not_found())
}
fn listing(session_id: &str, state: &ControlState) -> Value {
let settings = state.settings_listing();
let secrets: Vec<Value> = state
.secrets
.listing()
.into_iter()
.map(|(name, set)| json!({"name": name, "set": set}))
.collect();
json!({"session_id": session_id, "settings": settings, "secrets": secrets})
}
fn checked_length(request: &ControlRequest<'_>, max: usize) -> Result<usize, Refusal<'static>> {
if request.content_length > max {
return Err(Refusal::new(
413,
"body_too_large",
format!("the body is longer than the {max} bytes this resource reads"),
));
}
if request.content_length == 0 {
return Err(Refusal::new(
422,
"missing_body",
"a PUT carries its value as a body with a content-length",
));
}
Ok(request.content_length)
}
async fn read_exact_body<R: AsyncRead + Unpin>(
body: &mut R,
length: usize,
) -> Result<Vec<u8>, Refusal<'static>> {
let mut bytes = vec![0u8; length];
match body.read_exact(&mut bytes).await {
Ok(_) => Ok(bytes),
Err(_) => {
drop(SecretValue::from_bytes(bytes));
Err(Refusal::new(
400,
"truncated_body",
"the connection ended before content-length bytes of body arrived",
))
}
}
}
async fn read_setting_body<R: AsyncRead + Unpin>(
request: &ControlRequest<'_>,
body: &mut R,
) -> Result<Value, Refusal<'static>> {
let length = checked_length(request, SETTINGS_BODY_MAX_BYTES)?;
if !request.is_json {
return Err(Refusal::new(
415,
"unsupported_media_type",
"a setting is sent as content-type: application/json",
));
}
let bytes = read_exact_body(body, length).await?;
let invalid = || {
Refusal::new(
422,
"invalid_value",
"a setting body is a JSON object {\"value\": <value>}",
)
};
let parsed: Value = serde_json::from_slice(&bytes).map_err(|_| invalid())?;
parsed.get("value").cloned().ok_or_else(invalid)
}
async fn read_secret_body<R: AsyncRead + Unpin>(
request: &ControlRequest<'_>,
body: &mut R,
) -> Result<SecretValue, Refusal<'static>> {
let length = checked_length(request, SECRET_BODY_MAX_BYTES)?;
let value = SecretValue::from_bytes(read_exact_body(body, length).await?);
if !value.is_header_safe() {
return Err(Refusal::new(
422,
"invalid_value",
"a secret is one line of visible ASCII and spaces, with no control bytes; the value \
is not echoed",
));
}
Ok(value)
}
#[cfg(test)]
pub(crate) mod tests_support {
use super::{InjectedSecrets, SecretValue};
pub(crate) fn set(secrets: &InjectedSecrets, name: &str, value: &str) {
secrets
.set(name, SecretValue::from_bytes(value.as_bytes().to_vec()))
.expect("declared");
}
pub(crate) fn forget(secrets: &InjectedSecrets, name: &str) {
secrets.forget(name).expect("declared");
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{Ipv4Addr, Ipv6Addr};
const SESSION: &str = "ses_0199aaaabbbbccccddddeeeeffff0000";
const MARKER: &str = "ctl-marker-7f3e2a91";
fn inference(max_tokens: Option<u32>) -> InferenceConfig {
InferenceConfig {
transport: "http".to_string(),
model: "m".to_string(),
driver: None,
command: None,
compaction: None,
system_prompt: None,
system_prompt_file: None,
system_prompt_artifact: None,
max_turns: 10,
max_tokens,
max_session_tokens: None,
tool_refresh: murmur_artifact::ToolRefresh::Compaction,
alternates: Vec::new(),
}
}
fn state_for(config: &ControlConfig, inference: &InferenceConfig) -> ControlState {
ControlState::new(
config,
inference,
DriverChoices::declared(inference),
Arc::new(InjectedSecrets::new(&config.secrets)),
)
}
fn config() -> ControlConfig {
ControlConfig {
settings: vec![ControllableSetting::InferenceMaxTokens],
secrets: vec!["CARD_TOKEN".to_string()],
agent_settings: Vec::new(),
}
}
struct Fixture {
plane: ControlPlane,
token: String,
state: Arc<ControlState>,
workdir: tempfile::TempDir,
}
impl Fixture {
async fn new() -> Self {
let workdir = tempfile::tempdir().unwrap();
let state = Arc::new(state_for(&config(), &inference(Some(4096))));
let (plane, token) =
ControlPlane::declared(SESSION.to_string(), Arc::clone(&state)).unwrap();
let appender = ResourceTraceAppender::open(
workdir.path(),
SESSION.to_string(),
"evt_session".to_string(),
)
.await
.unwrap();
plane.attach_trace(Arc::new(appender));
Self {
plane,
token,
state,
workdir,
}
}
fn bearer(&self) -> String {
format!("Bearer {}", self.token)
}
fn trace(&self) -> String {
std::fs::read_to_string(self.workdir.path().join("trace.jsonl")).unwrap_or_default()
}
fn trace_events(&self) -> Vec<Value> {
self.trace()
.lines()
.map(|line| serde_json::from_str(line).unwrap())
.collect()
}
}
struct Sent<'a> {
method: &'a str,
path: &'a str,
authorization: Option<&'a str>,
peer: IpAddr,
is_json: bool,
body: &'a [u8],
content_length: Option<usize>,
}
impl<'a> Sent<'a> {
fn new(method: &'a str, path: &'a str, authorization: Option<&'a str>) -> Self {
Self {
method,
path,
authorization,
peer: IpAddr::V4(Ipv4Addr::LOCALHOST),
is_json: false,
body: b"",
content_length: None,
}
}
fn body(mut self, body: &'a [u8]) -> Self {
self.body = body;
self
}
fn json(mut self) -> Self {
self.is_json = true;
self
}
fn peer(mut self, peer: IpAddr) -> Self {
self.peer = peer;
self
}
fn length(mut self, length: usize) -> Self {
self.content_length = Some(length);
self
}
async fn to(self, plane: &ControlPlane) -> (ResourceResponse, usize) {
let mut reader: &[u8] = self.body;
let response = handle_control_request(
plane,
ControlRequest {
method: self.method,
path: self.path,
authorization: self.authorization,
peer: Some(self.peer),
content_length: self.content_length.unwrap_or(self.body.len()),
is_json: self.is_json,
},
&mut reader,
)
.await;
(response, reader.len())
}
}
fn body_json(response: &ResourceResponse) -> Value {
serde_json::from_slice(&response.body).unwrap()
}
fn header<'r>(response: &'r ResourceResponse, name: &str) -> Option<&'r str> {
response
.headers
.iter()
.find(|(n, _)| n == name)
.map(|(_, v)| v.as_str())
}
#[test]
fn loopback_peers_are_classified() {
for loopback in ["127.0.0.1", "127.255.0.1", "::1", "::ffff:127.0.0.1"] {
assert!(
is_loopback_peer(loopback.parse().unwrap()),
"{loopback} is loopback"
);
}
for remote in ["10.0.0.1", "::ffff:10.0.0.1", "fe80::1", "0.0.0.0", "::"] {
assert!(
!is_loopback_peer(remote.parse().unwrap()),
"{remote} is not loopback"
);
}
}
#[test]
fn a_token_verifies_only_for_its_own_key_and_session() {
let key = MintKey::generate().unwrap();
let token = mint_control_token(&key, SESSION).unwrap();
assert!(token.starts_with("ctl1."));
assert!(verify_control_token(&key, SESSION, &token));
assert!(!verify_control_token(&key, "ses_other", &token));
assert!(!verify_control_token(
&MintKey::generate().unwrap(),
SESSION,
&token
));
assert!(!verify_control_token(&key, SESSION, &token.to_uppercase()));
let swapped = token.replacen("ctl1.", "pfh1.", 1);
assert!(!verify_control_token(&key, SESSION, &swapped));
}
#[test]
fn secrets_and_their_stores_debug_print_redacted() {
let value = SecretValue::from_bytes(MARKER.as_bytes().to_vec());
assert_eq!(format!("{value:?}"), "<redacted>");
let store = InjectedSecrets::new(&["CARD_TOKEN".to_string()]);
store.set("CARD_TOKEN", value).unwrap();
let printed = format!("{store:?}");
assert!(printed.contains("<redacted>"), "{printed}");
assert!(!printed.contains(MARKER), "{printed}");
let state = state_for(&config(), &inference(None));
state
.secrets
.set(
"CARD_TOKEN",
SecretValue::from_bytes(MARKER.as_bytes().to_vec()),
)
.unwrap();
let printed = format!("{state:?}");
assert!(!printed.contains(MARKER), "{printed}");
let credential = crate::gateway_credential::GatewayCredential::injected(
"card-api",
"CARD_TOKEN",
Arc::clone(&state.secrets),
);
let printed = format!("{credential:?}");
assert!(printed.contains("<redacted>"), "{printed}");
assert!(!printed.contains(MARKER), "{printed}");
}
#[test]
fn zeroizing_a_secret_clears_every_byte() {
let mut value = SecretValue::from_bytes(MARKER.as_bytes().to_vec());
value.zeroize();
assert!(value.0.iter().all(|byte| *byte == 0));
}
#[test]
fn the_secret_type_derives_nothing_and_implements_only_debug_and_drop() {
let source = include_str!("control_plane.rs");
let (before, _) = source
.split_once("pub(crate) struct SecretValue(")
.expect("SecretValue is declared");
let preceding: Vec<&str> = before
.lines()
.rev()
.take_while(|line| {
line.trim_start().starts_with("///") || line.trim().starts_with("#[")
})
.collect();
assert!(
preceding.iter().all(|line| !line.contains("derive")),
"SecretValue carries a derive: {preceding:?}"
);
let impls: Vec<&str> = source
.lines()
.filter(|line| line.starts_with("impl") && line.contains(" for SecretValue"))
.collect();
assert_eq!(
impls,
vec![
"impl Drop for SecretValue {",
"impl std::fmt::Debug for SecretValue {"
]
);
}
#[test]
fn launch_value_is_the_manifest_value_or_the_default() {
let state = state_for(&config(), &inference(None));
assert_eq!(
state.selected(ControllableSetting::InferenceMaxTokens),
Some(SettingValue::Tokens(
crate::agent::DEFAULT_MAX_OUTPUT_TOKENS
))
);
}
#[test]
fn the_first_call_to_read_a_changed_value_names_the_change() {
let state = state_for(&config(), &inference(Some(4096)));
let setting = ControllableSetting::InferenceMaxTokens;
let first = state.for_call(setting).unwrap();
assert_eq!(first.value, SettingValue::Tokens(4096));
assert_eq!(first.applies_change, None);
let set = |value: u32| {
state
.request_change(Principal::Controller, setting, &json!(value))
.unwrap()
.change_id
};
set(1234);
let second = set(2048);
let changed = state.for_call(setting).unwrap();
assert_eq!(changed.value, SettingValue::Tokens(2048));
assert_eq!(changed.applies_change, Some(second));
assert_eq!(state.for_call(setting).unwrap().applies_change, None);
set(2048);
assert_eq!(state.for_call(setting).unwrap().applies_change, None);
}
#[tokio::test]
async fn an_undeclared_plane_answers_404_and_reads_nothing() {
let plane = ControlPlane::undeclared(SESSION.to_string());
let (response, unread) = Sent::new("PUT", "/control/secrets/CARD_TOKEN", Some("Bearer x"))
.body(MARKER.as_bytes())
.to(&plane)
.await;
assert_eq!(response.status, 404);
assert_eq!(unread, MARKER.len());
}
#[tokio::test]
async fn a_wrong_token_is_401_before_the_body_is_read_and_names_nothing() {
let fixture = Fixture::new().await;
let wrong_case = fixture.bearer().to_uppercase();
for authorization in [
None,
Some("Bearer garbage"),
Some(wrong_case.as_str()),
Some("Basic Zm9vOmJhcg=="),
] {
let (response, unread) = Sent::new("PUT", "/control/secrets/CARD_TOKEN", authorization)
.body(MARKER.as_bytes())
.to(&fixture.plane)
.await;
assert_eq!(response.status, 401);
assert_eq!(
header(&response, "www-authenticate"),
Some("Bearer realm=\"murmur-control\"")
);
assert_eq!(unread, MARKER.len(), "the body was read");
assert!(!String::from_utf8_lossy(&response.body).contains("CARD_TOKEN"));
}
let events = fixture.trace_events();
assert_eq!(events.len(), 4);
for event in events {
assert_eq!(event["event_type"], "control_refused");
assert_eq!(event["status"], 401);
assert!(event.get("name").is_none(), "{event}");
assert!(event.get("token_id").is_none(), "{event}");
}
assert!(!fixture.trace().contains(MARKER));
}
#[tokio::test]
async fn a_setting_change_answers_and_records_previous_and_value() {
let fixture = Fixture::new().await;
let bearer = fixture.bearer();
let (response, _) = Sent::new(
"PUT",
"/control/settings/inference.max_tokens",
Some(&bearer),
)
.json()
.body(br#"{"value": 1234}"#)
.to(&fixture.plane)
.await;
assert_eq!(response.status, 200);
assert_eq!(
body_json(&response),
json!({"name": "inference.max_tokens", "previous": 4096, "value": 1234, "applies_from": "next_inference_call"})
);
let events = fixture.trace_events();
assert_eq!(events.len(), 1);
let change = &events[0];
assert_eq!(change["event_type"], "control_change");
assert_eq!(change["principal"], "controller");
assert_eq!(change["token_id"], mac_token::token_id(&fixture.token));
assert_eq!(change["kind"], "setting");
assert_eq!(change["action"], "set");
assert_eq!(change["previous"], 4096);
assert_eq!(change["value"], 1234);
let applied = fixture
.state
.for_call(ControllableSetting::InferenceMaxTokens)
.unwrap();
assert_eq!(
applied.applies_change.as_deref(),
change["event_id"].as_str()
);
}
#[tokio::test]
async fn listing_names_settings_and_secrets_and_never_a_value() {
let fixture = Fixture::new().await;
let bearer = fixture.bearer();
let (response, _) = Sent::new("PUT", "/control/secrets/CARD_TOKEN", Some(&bearer))
.body(MARKER.as_bytes())
.to(&fixture.plane)
.await;
assert_eq!(
body_json(&response),
json!({"name": "CARD_TOKEN", "set": true, "replaced": false})
);
let (response, _) = Sent::new("GET", "/control", Some(&bearer))
.to(&fixture.plane)
.await;
assert_eq!(response.status, 200);
assert_eq!(
body_json(&response),
json!({
"session_id": SESSION,
"settings": [{"name": "inference.max_tokens", "value": 4096}],
"secrets": [{"name": "CARD_TOKEN", "set": true}],
})
);
assert!(!String::from_utf8_lossy(&response.body).contains(MARKER));
assert_eq!(
fixture.state.secrets.value("CARD_TOKEN").as_deref(),
Some(MARKER)
);
let (response, _) = Sent::new("DELETE", "/control/secrets/CARD_TOKEN", Some(&bearer))
.to(&fixture.plane)
.await;
assert_eq!(
body_json(&response),
json!({"name": "CARD_TOKEN", "set": false})
);
assert_eq!(fixture.state.secrets.value("CARD_TOKEN"), None);
assert!(!fixture.trace().contains(MARKER));
}
#[tokio::test]
async fn every_secret_refusal_leaks_nothing_of_the_body() {
let fixture = Fixture::new().await;
let bearer = fixture.bearer();
let remote = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
let with_cr = format!("{MARKER}\r");
let with_lf = format!("{MARKER}\nmore");
let with_nul = format!("{MARKER}\0");
let oversized = format!("{MARKER}{}", "x".repeat(SECRET_BODY_MAX_BYTES));
let cases: Vec<(Sent<'_>, u16, &str)> = vec![
(
Sent::new("PUT", "/control/secrets/CARD_TOKEN", Some("Bearer nope"))
.body(MARKER.as_bytes()),
401,
"unauthenticated",
),
(
Sent::new("PUT", "/control/secrets/OTHER", Some(&bearer)).body(MARKER.as_bytes()),
404,
"undeclared_secret",
),
(
Sent::new("POST", "/control/secrets/CARD_TOKEN", Some(&bearer))
.body(MARKER.as_bytes()),
405,
"method_not_allowed",
),
(
Sent::new("PUT", "/control/secrets/CARD_TOKEN", Some(&bearer))
.body(MARKER.as_bytes())
.peer(remote),
403,
"not_loopback",
),
(
Sent::new("PUT", "/control/secrets/CARD_TOKEN", Some(&bearer))
.body(oversized.as_bytes()),
413,
"body_too_large",
),
(
Sent::new("PUT", "/control/secrets/CARD_TOKEN", Some(&bearer)),
422,
"missing_body",
),
(
Sent::new("PUT", "/control/secrets/CARD_TOKEN", Some(&bearer))
.body(with_cr.as_bytes()),
422,
"invalid_value",
),
(
Sent::new("PUT", "/control/secrets/CARD_TOKEN", Some(&bearer))
.body(with_lf.as_bytes()),
422,
"invalid_value",
),
(
Sent::new("PUT", "/control/secrets/CARD_TOKEN", Some(&bearer))
.body(with_nul.as_bytes()),
422,
"invalid_value",
),
(
Sent::new("PUT", "/control/secrets/CARD_TOKEN", Some(&bearer))
.body(MARKER.as_bytes())
.length(MARKER.len() + 10),
400,
"truncated_body",
),
];
let count = cases.len();
for (sent, status, reason) in cases {
let (response, _) = sent.to(&fixture.plane).await;
assert_eq!(response.status, status, "{reason}");
let text = String::from_utf8_lossy(&response.body);
assert!(!text.contains(MARKER), "{reason}: {text}");
let error = body_json(&response)["error"].as_str().unwrap().to_string();
assert!(!error.is_empty());
}
let events = fixture.trace_events();
assert_eq!(events.len(), count);
assert!(events.iter().all(|e| e["event_type"] == "control_refused"));
assert!(!fixture.trace().contains(MARKER));
assert_eq!(fixture.state.secrets.value("CARD_TOKEN"), None);
}
#[tokio::test]
async fn setting_values_outside_the_range_are_422_and_change_nothing() {
let fixture = Fixture::new().await;
let bearer = fixture.bearer();
for body in [
r#"{"value": 0}"#,
r#"{"value": -1}"#,
r#"{"value": "2048"}"#,
r#"{"value": 4294967296}"#,
r#"{"value": 1.5}"#,
r#"{"other": 5}"#,
"not json",
] {
let (response, _) = Sent::new(
"PUT",
"/control/settings/inference.max_tokens",
Some(&bearer),
)
.json()
.body(body.as_bytes())
.to(&fixture.plane)
.await;
assert_eq!(response.status, 422, "{body}");
}
let (response, _) = Sent::new(
"PUT",
"/control/settings/inference.max_tokens",
Some(&bearer),
)
.body(br#"{"value": 5}"#)
.to(&fixture.plane)
.await;
assert_eq!(response.status, 415);
let (response, unread) = Sent::new(
"PUT",
"/control/settings/inference.max_tokens",
Some(&bearer),
)
.json()
.body(&[b' '; SETTINGS_BODY_MAX_BYTES + 1])
.to(&fixture.plane)
.await;
assert_eq!(response.status, 413);
assert_eq!(unread, SETTINGS_BODY_MAX_BYTES + 1);
assert_eq!(
fixture
.state
.selected(ControllableSetting::InferenceMaxTokens),
Some(SettingValue::Tokens(4096))
);
for event in fixture.trace_events() {
assert_eq!(event["event_type"], "control_refused");
assert_eq!(event["name"], "inference.max_tokens");
assert_eq!(event["kind"], "setting");
}
}
#[tokio::test]
async fn a_settings_change_is_accepted_from_a_remote_peer() {
let fixture = Fixture::new().await;
let bearer = fixture.bearer();
let (response, _) = Sent::new(
"PUT",
"/control/settings/inference.max_tokens",
Some(&bearer),
)
.json()
.body(br#"{"value": 2048}"#)
.peer(IpAddr::V6(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1)))
.to(&fixture.plane)
.await;
assert_eq!(response.status, 200);
}
#[tokio::test]
async fn unknown_paths_and_methods_are_refused_and_recorded() {
let fixture = Fixture::new().await;
let bearer = fixture.bearer();
for (method, path, status) in [
("GET", "/control/nothing", 404),
("GET", "/control/settings/inference.model", 404),
("PUT", "/control/settings/context.max_tokens", 404),
("POST", "/control", 405),
("GET", "/control/settings/inference.max_tokens", 405),
] {
let (response, _) = Sent::new(method, path, Some(&bearer))
.to(&fixture.plane)
.await;
assert_eq!(response.status, status, "{method} {path}");
if status == 405 {
assert!(header(&response, "allow").is_some());
}
}
let events = fixture.trace_events();
assert_eq!(events.len(), 5);
assert_eq!(events[4]["name"], "inference.max_tokens");
}
const ALT_KEY: &str = "ALT_KEY";
fn switchable(agent: bool) -> (ControlConfig, InferenceConfig, DriverChoices) {
let mut inference = inference(Some(4096));
inference.driver = Some(murmur_artifact::InferenceDriver {
artifact: "anthropic".to_string(),
config: None,
});
for (name, model, driver) in [
("haiku", "claude-haiku-4-5", "anthropic"),
("gpt", "gpt-5", "openai"),
("mini", "gpt-5-mini", "other"),
] {
inference
.alternates
.push(murmur_artifact::InferenceAlternate {
name: name.to_string(),
model: model.to_string(),
driver: driver.to_string(),
});
}
let mut choices = DriverChoices::declared(&inference);
choices.record_credential("anthropic", "config", None, None);
choices.record_credential("openai", "injected", Some(ALT_KEY), None);
choices.record_credential("other", "none", None, Some("MINI_KEY"));
let config = ControlConfig {
settings: if agent {
Vec::new()
} else {
vec![ControllableSetting::InferenceDriver]
},
secrets: vec![ALT_KEY.to_string()],
agent_settings: if agent {
vec![ControllableSetting::InferenceDriver]
} else {
Vec::new()
},
};
(config, inference, choices)
}
async fn switchable_fixture(agent: bool) -> Fixture {
let workdir = tempfile::tempdir().unwrap();
let (config, inference, choices) = switchable(agent);
let state = Arc::new(ControlState::new(
&config,
&inference,
choices,
Arc::new(InjectedSecrets::new(&config.secrets)),
));
let (plane, token) =
ControlPlane::declared(SESSION.to_string(), Arc::clone(&state)).unwrap();
let appender = ResourceTraceAppender::open(
workdir.path(),
SESSION.to_string(),
"evt_session".to_string(),
)
.await
.unwrap();
plane.attach_trace(Arc::new(appender));
Fixture {
plane,
token,
state,
workdir,
}
}
async fn put_driver(fixture: &Fixture, body: &str) -> ResourceResponse {
let bearer = fixture.bearer();
Sent::new("PUT", "/control/settings/inference.driver", Some(&bearer))
.json()
.body(body.as_bytes())
.to(&fixture.plane)
.await
.0
}
fn selected_driver(fixture: &Fixture) -> Option<usize> {
fixture
.state
.selected(ControllableSetting::InferenceDriver)
.and_then(SettingValue::driver)
}
#[tokio::test]
async fn driver_choice_a_controller_switch_answers_and_records_choice_names() {
let fixture = switchable_fixture(false).await;
let response = put_driver(&fixture, r#"{"value": "haiku"}"#).await;
assert_eq!(response.status, 200);
assert_eq!(
body_json(&response),
json!({"name": "inference.driver", "previous": "primary", "value": "haiku", "applies_from": "next_inference_call"})
);
let change = &fixture.trace_events()[0];
assert_eq!(change["event_type"], "control_change");
assert_eq!(change["principal"], "controller");
assert_eq!(change["token_id"], mac_token::token_id(&fixture.token));
assert_eq!(change["previous"], "primary");
assert_eq!(change["value"], "haiku");
let reading = fixture
.state
.for_call(ControllableSetting::InferenceDriver)
.unwrap();
assert_eq!(reading.value, SettingValue::Driver(1));
assert_eq!(
reading.applies_change.as_deref(),
change["event_id"].as_str()
);
}
#[tokio::test]
async fn driver_choice_an_undeclared_name_is_422_and_changes_nothing() {
let fixture = switchable_fixture(false).await;
for body in [
r#"{"value": "claude-opus"}"#,
r#"{"value": "openai"}"#,
r#"{"value": 3}"#,
r#"{"value": null}"#,
] {
let response = put_driver(&fixture, body).await;
assert_eq!(response.status, 422, "{body}");
let error = body_json(&response)["error"].as_str().unwrap().to_string();
assert!(
error.contains("primary, haiku, gpt, mini"),
"{body}: {error}"
);
}
assert_eq!(selected_driver(&fixture), Some(0));
for event in fixture.trace_events() {
assert_eq!(event["event_type"], "control_refused");
assert_eq!(event["reason"], "undeclared_driver");
assert_eq!(event["principal"], "controller");
assert_eq!(event["name"], "inference.driver");
}
}
#[tokio::test]
async fn driver_choice_an_injected_credential_gates_the_switch_and_the_forget() {
let fixture = switchable_fixture(false).await;
let bearer = fixture.bearer();
let response = put_driver(&fixture, r#"{"value": "gpt"}"#).await;
assert_eq!(response.status, 409);
let error = body_json(&response)["error"].as_str().unwrap().to_string();
assert!(error.contains(ALT_KEY), "{error}");
assert_eq!(selected_driver(&fixture), Some(0));
let (response, _) = Sent::new("PUT", "/control/secrets/ALT_KEY", Some(&bearer))
.body(MARKER.as_bytes())
.to(&fixture.plane)
.await;
assert_eq!(response.status, 200);
assert_eq!(
put_driver(&fixture, r#"{"value": "gpt"}"#).await.status,
200
);
assert_eq!(selected_driver(&fixture), Some(2));
let (response, _) = Sent::new("DELETE", "/control/secrets/ALT_KEY", Some(&bearer))
.to(&fixture.plane)
.await;
assert_eq!(response.status, 409);
assert!(fixture.state.secrets.holds(ALT_KEY), "the secret is kept");
assert_eq!(
put_driver(&fixture, r#"{"value": "primary"}"#).await.status,
200
);
let (response, _) = Sent::new("DELETE", "/control/secrets/ALT_KEY", Some(&bearer))
.to(&fixture.plane)
.await;
assert_eq!(response.status, 200);
assert!(!fixture.state.secrets.holds(ALT_KEY));
let reasons: Vec<String> = fixture
.trace_events()
.iter()
.filter(|event| event["event_type"] == "control_refused")
.map(|event| event["reason"].as_str().unwrap().to_string())
.collect();
assert_eq!(reasons, vec!["credential_unresolvable", "in_use"]);
assert!(!fixture.trace().contains(MARKER));
}
#[tokio::test]
async fn driver_choice_a_choice_staged_unavailable_is_409() {
let fixture = switchable_fixture(false).await;
let response = put_driver(&fixture, r#"{"value": "mini"}"#).await;
assert_eq!(response.status, 409);
let error = body_json(&response)["error"].as_str().unwrap().to_string();
assert!(error.contains("MINI_KEY"), "{error}");
assert_eq!(selected_driver(&fixture), Some(0));
}
#[tokio::test]
async fn driver_choice_listing_names_every_choice_and_whether_it_can_be_selected() {
let fixture = switchable_fixture(false).await;
let bearer = fixture.bearer();
let (response, _) = Sent::new("GET", "/control", Some(&bearer))
.to(&fixture.plane)
.await;
assert_eq!(
body_json(&response)["settings"],
json!([{
"name": "inference.driver",
"value": "primary",
"choices": [
{"name": "primary", "driver": "anthropic", "model": "m", "available": true},
{"name": "haiku", "driver": "anthropic", "model": "claude-haiku-4-5", "available": true},
{"name": "gpt", "driver": "openai", "model": "gpt-5", "available": false},
{"name": "mini", "driver": "other", "model": "gpt-5-mini", "available": false},
],
}])
);
}
#[tokio::test]
async fn driver_choice_an_agent_only_setting_is_undeclared_to_a_controller() {
let fixture = switchable_fixture(true).await;
assert!(
fixture.state.has_controller_surface(),
"ALT_KEY is a secret"
);
let response = put_driver(&fixture, r#"{"value": "haiku"}"#).await;
assert_eq!(response.status, 404);
assert_eq!(fixture.trace_events()[0]["reason"], "undeclared_setting");
let change = fixture
.state
.request_change(
Principal::Agent,
ControllableSetting::InferenceDriver,
&json!("haiku"),
)
.unwrap();
assert_eq!(change.previous, json!("primary"));
assert_eq!(change.value, json!("haiku"));
let refused = fixture
.state
.request_change(
Principal::Agent,
ControllableSetting::InferenceDriver,
&json!("nope"),
)
.unwrap_err();
assert_eq!((refused.status, refused.reason), (422, "undeclared_driver"));
assert_eq!(
fixture
.state
.request_change(
Principal::Controller,
ControllableSetting::InferenceDriver,
&json!("primary"),
)
.unwrap_err()
.status,
404
);
assert_eq!(
fixture.state.session_control(),
crate::trace::SessionControl {
settings: Vec::new(),
secrets: vec![ALT_KEY.to_string()],
agent_settings: vec!["inference.driver"],
}
);
let (config, inference, choices) = switchable(true);
let config = ControlConfig {
secrets: Vec::new(),
..config
};
let agent_only = ControlState::new(
&config,
&inference,
choices,
Arc::new(InjectedSecrets::new(&[])),
);
assert!(!agent_only.has_controller_surface());
}
#[test]
fn driver_choice_launch_value_is_primary() {
let (config, inference, choices) = switchable(false);
let state = ControlState::new(
&config,
&inference,
choices,
Arc::new(InjectedSecrets::new(&config.secrets)),
);
assert_eq!(
state.selected(ControllableSetting::InferenceDriver),
Some(SettingValue::Driver(0))
);
}
}