fix(grid-agent): move setup into YAML preferences (#135)
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
use metacrate_grid_agent::{
|
||||
ACCEPTANCE_SCHEMA_VERSION, ACCEPTANCE_SECRET_CANARY, AcceptanceBudgets,
|
||||
AcceptanceEvidenceWriter, AcceptanceRecord, AcceptanceStatus, LiveGridOptIns,
|
||||
run_deterministic_acceptance,
|
||||
AcceptanceEvidenceWriter, AcceptanceRecord, AcceptanceStatus, run_deterministic_acceptance,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
@@ -62,23 +60,6 @@ async fn deterministic_gate_writes_ordered_redacted_evidence_and_drains() {
|
||||
fs::remove_file(path).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn live_actions_require_exact_independent_confirmations() {
|
||||
let mut values = BTreeMap::new();
|
||||
values.insert("METACRATE_AGENT_LIVE_CHAT_IM", "CHAT-IM");
|
||||
let action_without_login =
|
||||
LiveGridOptIns::from_environment(|name| values.get(name).map(ToString::to_string));
|
||||
assert!(action_without_login.validate().is_err());
|
||||
values.insert("METACRATE_AGENT_LIVE_LOGIN", "LOGIN");
|
||||
values.insert("METACRATE_AGENT_LIVE_BUILD", "wrong");
|
||||
let confirmed =
|
||||
LiveGridOptIns::from_environment(|name| values.get(name).map(ToString::to_string))
|
||||
.validate()
|
||||
.unwrap();
|
||||
assert!(confirmed.login && confirmed.chat_and_im);
|
||||
assert!(!confirmed.reversible_build);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evidence_writer_rejects_secret_markers_and_schema_is_committed() {
|
||||
let path = temporary("unsafe.jsonl");
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::BTreeSet;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const ALLOWED_DEPENDENCIES: [&str; 17] = [
|
||||
const ALLOWED_DEPENDENCIES: [&str; 18] = [
|
||||
"base64",
|
||||
"crossterm",
|
||||
"libremetaverse",
|
||||
@@ -15,6 +15,7 @@ const ALLOWED_DEPENDENCIES: [&str; 17] = [
|
||||
"rustls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml_ng",
|
||||
"sha2",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use metacrate_grid_agent::{CONFIG_SCHEMA_VERSION, ConfigLoader};
|
||||
use serde_json::Value;
|
||||
use serde_yaml_ng::Value;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -15,19 +15,22 @@ fn workspace() -> PathBuf {
|
||||
fn examples_are_versioned_placeholder_only_and_offline_validation_has_no_io_peer() {
|
||||
let root = workspace();
|
||||
for name in [
|
||||
"grid-agent.example.json",
|
||||
"grid-agent.integrated.example.json",
|
||||
"grid-agent.split.example.json",
|
||||
"grid-agent.example.yml",
|
||||
"grid-agent.integrated.example.yml",
|
||||
"grid-agent.split.example.yml",
|
||||
] {
|
||||
let bytes = fs::read(root.join("config").join(name)).expect("example");
|
||||
let value: Value = serde_json::from_slice(&bytes).expect("valid JSON");
|
||||
assert_eq!(value["schema_version"], CONFIG_SCHEMA_VERSION);
|
||||
let value: Value = serde_yaml_ng::from_slice(&bytes).expect("valid YAML");
|
||||
assert_eq!(
|
||||
value["schema_version"].as_u64(),
|
||||
Some(u64::from(CONFIG_SCHEMA_VERSION))
|
||||
);
|
||||
let text = String::from_utf8(bytes).unwrap();
|
||||
for forbidden in ["Bearer ", "sk-", "password123", "SECRET_CANARY"] {
|
||||
assert!(!text.contains(forbidden), "{name} contains {forbidden}");
|
||||
}
|
||||
}
|
||||
let offline = root.join("config/grid-agent.example.json");
|
||||
let offline = root.join("config/grid-agent.example.yml");
|
||||
let config = ConfigLoader::new().with_file(offline).load().unwrap();
|
||||
assert!(config.grid.is_none());
|
||||
}
|
||||
@@ -46,10 +49,11 @@ fn service_and_installers_preserve_state_secrets_and_graceful_shutdown() {
|
||||
"ProtectSystem=strict",
|
||||
"NoNewPrivileges=true",
|
||||
"ReadWritePaths=/var/lib/metacrate/grid-agent",
|
||||
"/etc/metacrate/config.yml",
|
||||
] {
|
||||
assert!(unit.contains(required), "unit lacks {required}");
|
||||
}
|
||||
for forbidden in ["API_KEY=", "PASSWORD=", "TOKEN="] {
|
||||
for forbidden in ["API_KEY=", "PASSWORD=", "TOKEN=", "EnvironmentFile="] {
|
||||
assert!(!unit.contains(forbidden), "unit embeds {forbidden}");
|
||||
}
|
||||
let shell = fs::read_to_string(root.join("packaging/metacrate-grid-agent/install.sh")).unwrap();
|
||||
|
||||
110
crates/metacrate-grid-agent/tests/preferences_config.rs
Normal file
110
crates/metacrate-grid-agent/tests/preferences_config.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use metacrate_grid_agent::{
|
||||
AgentPreferences, ConfigLoader, MapEnvironment, OperatingMode, OperatorScreen, OperatorTui,
|
||||
PreferencesPanel, TuiInput, TuiRenderOptions,
|
||||
};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
fn temporary(name: &str) -> PathBuf {
|
||||
let directory = std::env::temp_dir().join(format!(
|
||||
"metacrate-preferences-{}-{name}",
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
));
|
||||
fs::create_dir_all(&directory).unwrap();
|
||||
directory
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn yaml_is_primary_and_preferences_preserve_unknown_settings() {
|
||||
let directory = temporary("yaml");
|
||||
let path = directory.join("config.yml");
|
||||
fs::write(
|
||||
&path,
|
||||
r"schema_version: 1
|
||||
integrated: true
|
||||
llm:
|
||||
endpoint_url: https://ai.invalid/v1/chat/completions
|
||||
api_key: private-key
|
||||
model: local-model
|
||||
grid:
|
||||
login_url: https://grid.invalid/login
|
||||
avatar_name: Service Avatar
|
||||
password: private-password
|
||||
authorized_avatar_uuids:
|
||||
- 00000000-0000-4000-8000-000000000001
|
||||
behavior:
|
||||
idle_interval_seconds: 321
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
let config = ConfigLoader::new()
|
||||
.with_file(&path)
|
||||
.with_environment(MapEnvironment::default())
|
||||
.load()
|
||||
.unwrap();
|
||||
assert_eq!(config.mode, OperatingMode::Integrated);
|
||||
assert_eq!(config.llm.model.as_deref(), Some("local-model"));
|
||||
assert_eq!(config.behavior.idle_interval.as_secs(), 321);
|
||||
|
||||
let mut preferences = AgentPreferences::load(&path).unwrap();
|
||||
preferences.set_llm_model("replacement-model".into());
|
||||
preferences.save(&path).unwrap();
|
||||
let saved = fs::read_to_string(&path).unwrap();
|
||||
assert!(saved.contains("idle_interval_seconds: 321"));
|
||||
assert!(saved.contains("model: replacement-model"));
|
||||
assert!(!format!("{preferences:?}").contains("private-key"));
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
assert_eq!(
|
||||
fs::metadata(&path).unwrap().permissions().mode() & 0o777,
|
||||
0o600
|
||||
);
|
||||
}
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dotenv_import_is_explicit_and_preferences_panel_never_renders_secrets() {
|
||||
let directory = temporary("import");
|
||||
let dotenv = directory.join("legacy.env");
|
||||
let config = directory.join("config.yml");
|
||||
fs::write(
|
||||
&dotenv,
|
||||
"OPENAPI_URL=https://ai.invalid/v1/chat/completions\nOPENAPI_KEY=very-secret-key\nOPENAPI_MODEL=test-model\nGRID_LOGIN_URL=https://grid.invalid/login\nGRID_USER=Test Avatar\nGRID_PASSWORD=very-secret-password\n",
|
||||
)
|
||||
.unwrap();
|
||||
let mut preferences = AgentPreferences::load(&config).unwrap();
|
||||
preferences.import_dotenv(&dotenv).unwrap();
|
||||
preferences.set_privileged_users(vec!["00000000-0000-4000-8000-000000000001".into()]);
|
||||
preferences.save(&config).unwrap();
|
||||
|
||||
let panel = PreferencesPanel::open(config.clone()).unwrap();
|
||||
let summary = panel.summary();
|
||||
assert_eq!(summary.llm_model.as_deref(), Some("test-model"));
|
||||
assert!(summary.llm_api_key_configured && summary.grid_password_configured);
|
||||
assert_eq!(summary.privileged_users.len(), 1);
|
||||
|
||||
let mut tui = OperatorTui::with_preferences(config).unwrap();
|
||||
tui.screen = OperatorScreen::Preferences;
|
||||
let rendered = tui.render(TuiRenderOptions {
|
||||
width: 120,
|
||||
height: 30,
|
||||
color: false,
|
||||
});
|
||||
assert!(rendered.contains("LLM API key: [configured]"));
|
||||
assert!(rendered.contains("grid password: [configured]"));
|
||||
assert!(!rendered.contains("very-secret"));
|
||||
let _ = tui.reduce(TuiInput::PreferenceNext);
|
||||
let _ = tui.reduce(TuiInput::PreferenceEditOrCommit);
|
||||
for value in "https://replacement.invalid/v1/chat".chars() {
|
||||
let _ = tui.reduce(TuiInput::PreferenceCharacter(value));
|
||||
}
|
||||
let _ = tui.reduce(TuiInput::PreferenceEditOrCommit);
|
||||
let _ = tui.reduce(TuiInput::PreferenceSave);
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
Reference in New Issue
Block a user