feat(grid-agent): establish architecture and config (#118)
Some checks failed
CI / rust-skia (Rust only) (push) Successful in 2m47s
CI / required (push) Failing after 2m44s

This commit is contained in:
2026-08-17 20:15:37 +00:00
parent 1254cf24e1
commit 1e1e95a58a
14 changed files with 2647 additions and 0 deletions

View File

@@ -0,0 +1,135 @@
use metacrate_grid_agent::{
AgentService, ConfigLoader, GridEventKind, ObservableEvent, OperatingMode,
};
use std::error::Error;
use std::fmt;
use std::path::PathBuf;
const MAX_ARGUMENTS: usize = 8;
#[derive(Debug)]
struct CliError(String);
impl fmt::Display for CliError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
impl Error for CliError {}
#[derive(Default)]
struct Options {
config: Option<PathBuf>,
check_config: bool,
run_once: bool,
}
fn options() -> Result<Option<Options>, CliError> {
let mut result = Options::default();
let mut arguments = std::env::args_os().skip(1);
let mut count = 0;
while let Some(argument) = arguments.next() {
count += 1;
if count > MAX_ARGUMENTS {
return Err(CliError(format!(
"at most {MAX_ARGUMENTS} command-line arguments are accepted"
)));
}
if argument == "--help" || argument == "-h" {
println!(
"metacrate-grid-agent [--config PATH] [--check-config | --run-once]\n\
Configuration precedence: defaults < JSON < secret files < environment."
);
return Ok(None);
}
if argument == "--check-config" {
result.check_config = true;
} else if argument == "--run-once" {
result.run_once = true;
} else if argument == "--config" {
let path = arguments
.next()
.ok_or_else(|| CliError("--config requires a path".into()))?;
count += 1;
result.config = Some(PathBuf::from(path));
} else {
let argument = PathBuf::from(argument);
return Err(CliError(format!(
"unknown argument {}; use --help",
argument.display()
)));
}
}
if result.check_config && result.run_once {
return Err(CliError(
"--check-config and --run-once are mutually exclusive".into(),
));
}
Ok(Some(result))
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let Some(options) = options()? else {
return Ok(());
};
let mut loader = ConfigLoader::new();
if let Some(path) = options.config {
loader = loader.with_file(path);
}
let config = loader.load()?;
if options.check_config {
println!("configuration is valid for {:?} mode", config.mode);
return Ok(());
}
if config.mode != OperatingMode::OfflineFake {
return Err(CliError(
"this architecture issue starts only the offline backend; live login is owned by a later milestone issue"
.into(),
)
.into());
}
let startup_timeout = config.timeouts.startup;
let mut handle = AgentService::offline(config)?.start()?;
if options.run_once {
tokio::time::timeout(startup_timeout, async {
loop {
match handle.next_event().await {
Some(ObservableEvent::Grid(event))
if event.kind == GridEventKind::BackendReady =>
{
return Ok::<(), CliError>(());
}
Some(_) => {}
None => {
return Err(CliError("service stopped before backend readiness".into()));
}
}
}
})
.await
.map_err(|_| CliError("timed out waiting for offline backend readiness".into()))??;
handle.shutdown().await?;
println!("grid agent completed one offline startup/shutdown cycle");
return Ok(());
}
println!("grid agent started in offline/fake mode; press Ctrl-C to stop");
loop {
tokio::select! {
signal = tokio::signal::ctrl_c() => {
signal?;
break;
}
event = handle.next_event() => {
if event.is_none() {
break;
}
}
}
}
handle.shutdown().await?;
println!("grid agent stopped cleanly");
Ok(())
}