fix(grid-agent): move setup into YAML preferences (#135)
Some checks failed
CI / rust-skia (Rust only) (push) Successful in 2m46s
CI / required (push) Failing after 54s

This commit is contained in:
2026-08-18 21:34:14 +02:00
parent b56b28043f
commit db25a977b7
27 changed files with 1242 additions and 482 deletions

View File

@@ -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");

View File

@@ -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",

View File

@@ -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();

View 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();
}