use super::operator::Operator;
use newt_core::agent_identity::Secret;
use newt_interaction::{
Control, ControlId, ControlKind, ControlValue, InteractionDefinition, InteractionKind,
Requirement, SecretRef,
};
pub(crate) const SECRET_CONTROL: &str = "secret";
pub(crate) fn secret_prompt(label: &str) -> InteractionDefinition {
InteractionDefinition::new(
InteractionKind::Prompt,
String::new(),
vec![Control {
id: ControlId::new(SECRET_CONTROL)
.expect("`secret` is a valid control id; it is a const"),
kind: ControlKind::Secret,
label: label.to_string(),
requirement: Requirement::Required,
}],
)
}
pub(super) fn ask_secret(op: &Operator<'_>, label: &str) -> std::io::Result<Option<Secret>> {
let typed = op.ask(&secret_prompt(label))?;
let typed = typed.trim();
Ok((!typed.is_empty()).then(|| Secret::new(typed)))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct SealedSecret(ControlValue);
impl SealedSecret {
pub(super) fn new(reference: &str) -> Result<Self, newt_interaction::ProtocolError> {
Ok(Self(ControlValue::Secret {
reference: SecretRef::new(reference)?,
}))
}
pub(super) fn handle(&self) -> &str {
match self.submitted() {
ControlValue::Secret { reference } => reference.as_str(),
ControlValue::Choice { .. }
| ControlValue::Text { .. }
| ControlValue::Toggle { .. } => {
unreachable!("SealedSecret only ever holds ControlValue::Secret")
}
}
}
pub(super) fn submitted(&self) -> &ControlValue {
&self.0
}
}
#[cfg(test)]
mod contract {
use super::{ask_secret, secret_prompt, SealedSecret, SECRET_CONTROL};
use newt_core::agent_identity::Secret;
use newt_core::markup::{headless, plain};
use newt_core::tty::Echo;
use newt_interaction::{
ControlId, ControlKind, ControlValue, ProtocolError, Requirement, Submission,
SurfaceFeature,
};
const PLANTED: &str = "sk-live-PLANTED-DO-NOT-LEAK-4f9a2c";
fn surfaces_for(label: &str, sealed: &SealedSecret) -> Vec<(&'static str, String)> {
let definition = secret_prompt(label);
let submission = Submission {
control: ControlId::new(SECRET_CONTROL).expect("valid"),
value: sealed.submitted().clone(),
};
vec![
("the plain projection", plain::render(&definition)),
(
"the definition record",
serde_json::to_string(&definition).expect("a definition serializes"),
),
(
"the submitted record",
serde_json::to_string(&submission).expect("a submission serializes"),
),
("the terminal while typing", Echo::Stars.display(PLANTED)),
("a debug/log line", format!("{:?}", Secret::new(PLANTED))),
]
}
#[test]
fn a_planted_secret_reaches_no_surface_and_no_record() {
let sealed = SealedSecret::new("~/.newt/backends/openai.token.abc123.age")
.expect("a non-empty reference");
for (where_, rendered) in surfaces_for("API key (echoes as *, Enter to skip)", &sealed) {
assert!(
!rendered.contains(PLANTED),
"the planted secret reached {where_}: {rendered}"
);
}
}
#[test]
fn the_detector_finds_a_secret_that_is_actually_there() {
let careless = Submission {
control: ControlId::new(SECRET_CONTROL).expect("valid"),
value: ControlValue::Text {
text: PLANTED.to_string(),
},
};
let rendered = serde_json::to_string(&careless).expect("serializes");
assert!(
rendered.contains(PLANTED),
"the detector cannot see a plaintext secret in a record it IS in: {rendered}"
);
assert!(Echo::Chars.display(PLANTED).contains(PLANTED));
let sealed = SealedSecret::new("ref").expect("valid");
for (where_, rendered) in surfaces_for("API key", &sealed) {
assert!(!rendered.is_empty(), "{where_} rendered nothing at all");
}
}
#[test]
fn the_prompt_shows_the_field_and_says_it_is_not_echoed() {
let shown = plain::render(&secret_prompt("API key"));
assert_eq!(shown, "API key: (secret, not echoed)");
}
#[test]
fn a_surface_that_cannot_hide_input_refuses_the_prompt() {
let definition = secret_prompt("API key");
let err = headless::present(&definition, &[]).expect_err("must fail closed");
assert!(
matches!(err, ProtocolError::UnsupportedFeature { ref feature, .. }
if feature == SurfaceFeature::SECRET_INPUT),
"unexpected refusal: {err:?}"
);
let capable = [SurfaceFeature::new(SurfaceFeature::SECRET_INPUT).expect("valid")];
let ok = headless::present(&definition, &capable).expect("a capable surface presents it");
assert_eq!(ok.text(), "API key: (secret, not echoed)");
}
#[test]
fn a_sealed_secret_is_a_handle_and_never_empty() {
let sealed = SealedSecret::new("~/.newt/backends/x.token.age").expect("valid");
assert_eq!(sealed.handle(), "~/.newt/backends/x.token.age");
assert!(
matches!(sealed.submitted(), ControlValue::Secret { .. }),
"the submitted value is a reference: {:?}",
sealed.submitted()
);
assert!(matches!(
SealedSecret::new(""),
Err(ProtocolError::InvalidId { .. })
));
}
#[test]
fn an_empty_answer_is_a_skip_not_an_empty_secret() {
use crate::setup::operator::Script;
let blank = Script::new(&[""]);
assert!(ask_secret(&blank.operator(), "API key").unwrap().is_none());
let spaces = Script::new(&[" "]);
assert!(ask_secret(&spaces.operator(), "API key").unwrap().is_none());
let real = Script::new(&[PLANTED]);
let got = ask_secret(&real.operator(), "API key")
.unwrap()
.expect("a real key is returned");
assert_eq!(got.expose(), PLANTED, "and it is returned intact");
}
#[test]
fn the_control_kind_is_secret() {
let definition = secret_prompt("API key");
assert!(matches!(definition.controls[0].kind, ControlKind::Secret));
assert_eq!(definition.controls[0].requirement, Requirement::Required);
}
}
#[cfg(test)]
mod no_duplicate_demand {
use super::secret_prompt;
#[test]
fn the_prompt_hand_writes_no_feature_demand() {
assert!(
secret_prompt("API key").features.is_empty(),
"the capability follows from ControlKind::Secret; restating it \
here is the drift a red run already demonstrated"
);
}
}