Skip to main content

robonix_executor/
config.rs

1// SPDX-License-Identifier: MulanPSL-2.0
2// Author: wheatfox <wheatfox17@icloud.com>
3//
4// Executor config — same three-source resolution as pilot:
5//   compiled defaults < YAML at $ROBONIX_CONFIG_PATH < CLI flags / env.
6
7use anyhow::{Context, Result};
8use clap::Parser;
9use serde::Deserialize;
10use std::path::{Path, PathBuf};
11
12pub const DEFAULT_EXECUTOR_PROVIDER_ID: &str = "executor";
13pub const EXECUTOR_NAMESPACE: &str = "robonix/system/executor";
14pub const DEFAULT_ATLAS_ENDPOINT: &str = "127.0.0.1:50051";
15pub const DEFAULT_LISTEN: &str = "127.0.0.1:50061";
16
17#[derive(Debug, Clone)]
18pub struct ExecutorConfig {
19    pub atlas_endpoint: String,
20    pub listen: String,
21    pub id: String,
22}
23
24#[derive(Parser, Debug)]
25#[command(
26    name = "robonix-executor",
27    about = "Robonix Executor — tool-call dispatch runtime"
28)]
29pub struct Args {
30    /// Atlas control-plane endpoint.
31    #[arg(long, env = "ROBONIX_ATLAS_ENDPOINT")]
32    pub atlas: Option<String>,
33
34    /// Address the SystemExecutor gRPC service binds to.
35    #[arg(long, env = "ROBONIX_EXECUTOR_LISTEN")]
36    pub listen: Option<String>,
37
38    /// Override executor's id (singleton; rarely needed).
39    #[arg(long, env = "ROBONIX_EXECUTOR_PROVIDER_ID")]
40    pub id: Option<String>,
41
42    /// Optional YAML config file (rbnx writes this; CLI/env still override).
43    #[arg(long, env = "ROBONIX_CONFIG_PATH")]
44    pub config: Option<PathBuf>,
45
46    /// Log filter (env_logger syntax; e.g. `info`, `robonix_executor=debug`).
47    /// Default: `robonix_executor=info`. Falls back to `RUST_LOG` if unset.
48    #[arg(long)]
49    pub log: Option<String>,
50}
51
52#[derive(Default, Deserialize)]
53struct FileConfig {
54    #[serde(default)]
55    atlas_endpoint: Option<String>,
56    #[serde(default)]
57    listen: Option<String>,
58    #[serde(default)]
59    id: Option<String>,
60}
61
62impl ExecutorConfig {
63    pub fn resolve(args: Args) -> Result<Self> {
64        let file_cfg: FileConfig = match &args.config {
65            Some(path) => load_yaml(path)?,
66            None => FileConfig::default(),
67        };
68        Ok(Self {
69            atlas_endpoint: args
70                .atlas
71                .or(file_cfg.atlas_endpoint)
72                .unwrap_or_else(|| DEFAULT_ATLAS_ENDPOINT.to_string()),
73            listen: args
74                .listen
75                .or(file_cfg.listen)
76                .unwrap_or_else(|| DEFAULT_LISTEN.to_string()),
77            id: args
78                .id
79                .or(file_cfg.id)
80                .unwrap_or_else(|| DEFAULT_EXECUTOR_PROVIDER_ID.to_string()),
81        })
82    }
83}
84
85fn load_yaml(path: &Path) -> Result<FileConfig> {
86    let raw = std::fs::read_to_string(path)
87        .with_context(|| format!("read executor config '{}'", path.display()))?;
88    serde_yaml::from_str(&raw)
89        .with_context(|| format!("parse executor config '{}'", path.display()))
90}