Package portable grid agent operation (#134)
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"schema_version": 1,
|
||||||
"integrated": false,
|
"integrated": false,
|
||||||
"split": false,
|
"split": false,
|
||||||
"llm": {
|
"llm": {
|
||||||
|
|||||||
20
config/grid-agent.integrated.example.json
Normal file
20
config/grid-agent.integrated.example.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"integrated": true,
|
||||||
|
"split": false,
|
||||||
|
"llm": {
|
||||||
|
"endpoint_url": "https://llm.example.invalid/v1/chat/completions"
|
||||||
|
},
|
||||||
|
"grid": {
|
||||||
|
"login_url": "https://grid.example.invalid/login",
|
||||||
|
"avatar_name": "Service Avatar"
|
||||||
|
},
|
||||||
|
"secret_files": {
|
||||||
|
"llm_api_key": "secrets/llm-api-key",
|
||||||
|
"grid_password": "secrets/grid-password"
|
||||||
|
},
|
||||||
|
"authorized_avatar_uuids": [
|
||||||
|
"00000000-0000-4000-8000-000000000001"
|
||||||
|
],
|
||||||
|
"storage_path": "data/grid-agent"
|
||||||
|
}
|
||||||
25
config/grid-agent.split.example.json
Normal file
25
config/grid-agent.split.example.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"integrated": false,
|
||||||
|
"split": true,
|
||||||
|
"llm": {
|
||||||
|
"endpoint_url": "https://llm.example.invalid/v1/chat/completions"
|
||||||
|
},
|
||||||
|
"grid": {
|
||||||
|
"login_url": "https://grid.example.invalid/login",
|
||||||
|
"avatar_name": "Service Avatar"
|
||||||
|
},
|
||||||
|
"secret_files": {
|
||||||
|
"llm_api_key": "secrets/llm-api-key",
|
||||||
|
"grid_password": "secrets/grid-password",
|
||||||
|
"control_operator_token": "secrets/control-operator-token",
|
||||||
|
"control_observer_token": "secrets/control-observer-token"
|
||||||
|
},
|
||||||
|
"authorized_avatar_uuids": [
|
||||||
|
"00000000-0000-4000-8000-000000000001"
|
||||||
|
],
|
||||||
|
"storage_path": "data/grid-agent",
|
||||||
|
"control": {
|
||||||
|
"listen": "127.0.0.1:9764"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,3 +64,7 @@ contract are specified in
|
|||||||
The embedded/split cross-platform operator interface, keyboard controls,
|
The embedded/split cross-platform operator interface, keyboard controls,
|
||||||
privacy boundary, and focused gates are documented in
|
privacy boundary, and focused gates are documented in
|
||||||
[`../../docs/grid-agent-tui.md`](../../docs/grid-agent-tui.md).
|
[`../../docs/grid-agent-tui.md`](../../docs/grid-agent-tui.md).
|
||||||
|
The complete quick start, platform paths, systemd/Windows service operation,
|
||||||
|
secret rotation, backup/upgrade/rollback, failure playbooks, resource defaults,
|
||||||
|
and unsupported-operation list are in
|
||||||
|
[`../../docs/grid-agent-operations.md`](../../docs/grid-agent-operations.md).
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const MAX_TLS_DER_BYTES: u64 = 1024 * 1024;
|
|||||||
const MAX_AUTHORIZED_AVATARS: usize = 1_024;
|
const MAX_AUTHORIZED_AVATARS: usize = 1_024;
|
||||||
const MAX_QUEUE_CAPACITY: usize = 8_192;
|
const MAX_QUEUE_CAPACITY: usize = 8_192;
|
||||||
const MAX_BACKGROUND_TASKS: usize = 2;
|
const MAX_BACKGROUND_TASKS: usize = 2;
|
||||||
|
pub const CONFIG_SCHEMA_VERSION: u32 = 1;
|
||||||
|
|
||||||
const ENV_INTEGRATED: &str = "METACRATE_AGENT_INTEGRATED";
|
const ENV_INTEGRATED: &str = "METACRATE_AGENT_INTEGRATED";
|
||||||
const ENV_SPLIT: &str = "METACRATE_AGENT_SPLIT";
|
const ENV_SPLIT: &str = "METACRATE_AGENT_SPLIT";
|
||||||
@@ -30,6 +31,7 @@ const ENV_GRID_AVATAR_NAME: &str = "METACRATE_AGENT_GRID_AVATAR_NAME";
|
|||||||
const ENV_GRID_PASSWORD: &str = "METACRATE_AGENT_GRID_PASSWORD";
|
const ENV_GRID_PASSWORD: &str = "METACRATE_AGENT_GRID_PASSWORD";
|
||||||
const ENV_GRID_PASSWORD_FILE: &str = "METACRATE_AGENT_GRID_PASSWORD_FILE";
|
const ENV_GRID_PASSWORD_FILE: &str = "METACRATE_AGENT_GRID_PASSWORD_FILE";
|
||||||
const ENV_AUTHORIZED_AVATARS: &str = "METACRATE_AGENT_AUTHORIZED_AVATAR_UUIDS";
|
const ENV_AUTHORIZED_AVATARS: &str = "METACRATE_AGENT_AUTHORIZED_AVATAR_UUIDS";
|
||||||
|
const ENV_STORAGE_PATH: &str = "METACRATE_AGENT_STORAGE_PATH";
|
||||||
const ENV_CONTROL_LISTEN: &str = "METACRATE_AGENT_CONTROL_LISTEN";
|
const ENV_CONTROL_LISTEN: &str = "METACRATE_AGENT_CONTROL_LISTEN";
|
||||||
const ENV_CONTROL_OPERATOR_TOKEN: &str = "METACRATE_AGENT_CONTROL_OPERATOR_TOKEN";
|
const ENV_CONTROL_OPERATOR_TOKEN: &str = "METACRATE_AGENT_CONTROL_OPERATOR_TOKEN";
|
||||||
const ENV_CONTROL_OPERATOR_TOKEN_FILE: &str = "METACRATE_AGENT_CONTROL_OPERATOR_TOKEN_FILE";
|
const ENV_CONTROL_OPERATOR_TOKEN_FILE: &str = "METACRATE_AGENT_CONTROL_OPERATOR_TOKEN_FILE";
|
||||||
@@ -431,6 +433,57 @@ pub trait Environment {
|
|||||||
fn get(&self, name: &str) -> Option<String>;
|
fn get(&self, name: &str) -> Option<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct PlatformPaths {
|
||||||
|
pub config_file: PathBuf,
|
||||||
|
pub data_directory: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlatformPaths {
|
||||||
|
#[must_use]
|
||||||
|
pub fn discover() -> Self {
|
||||||
|
Self::from_environment(&StdEnvironment)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn from_environment(environment: &impl Environment) -> Self {
|
||||||
|
if cfg!(windows) {
|
||||||
|
let config = environment
|
||||||
|
.get("APPDATA")
|
||||||
|
.map_or_else(|| PathBuf::from("config"), PathBuf::from);
|
||||||
|
let data = environment
|
||||||
|
.get("LOCALAPPDATA")
|
||||||
|
.map_or_else(|| PathBuf::from("data"), PathBuf::from);
|
||||||
|
return Self {
|
||||||
|
config_file: config.join("MetaCrate/grid-agent.json"),
|
||||||
|
data_directory: data.join("MetaCrate/grid-agent"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if cfg!(target_os = "macos") {
|
||||||
|
let home = environment
|
||||||
|
.get("HOME")
|
||||||
|
.map_or_else(|| PathBuf::from("."), PathBuf::from);
|
||||||
|
return Self {
|
||||||
|
config_file: home.join("Library/Application Support/MetaCrate/grid-agent.json"),
|
||||||
|
data_directory: home.join("Library/Application Support/MetaCrate/grid-agent"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let home = environment
|
||||||
|
.get("HOME")
|
||||||
|
.map_or_else(|| PathBuf::from("."), PathBuf::from);
|
||||||
|
let config = environment
|
||||||
|
.get("XDG_CONFIG_HOME")
|
||||||
|
.map_or_else(|| home.join(".config"), PathBuf::from);
|
||||||
|
let data = environment
|
||||||
|
.get("XDG_DATA_HOME")
|
||||||
|
.map_or_else(|| home.join(".local/share"), PathBuf::from);
|
||||||
|
Self {
|
||||||
|
config_file: config.join("metacrate/grid-agent.json"),
|
||||||
|
data_directory: data.join("metacrate/grid-agent"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
pub struct StdEnvironment;
|
pub struct StdEnvironment;
|
||||||
|
|
||||||
@@ -553,6 +606,10 @@ pub enum ConfigError {
|
|||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
reason: String,
|
reason: String,
|
||||||
},
|
},
|
||||||
|
UnsupportedSchemaVersion {
|
||||||
|
found: u32,
|
||||||
|
supported: u32,
|
||||||
|
},
|
||||||
Missing {
|
Missing {
|
||||||
field: &'static str,
|
field: &'static str,
|
||||||
required_for: &'static str,
|
required_for: &'static str,
|
||||||
@@ -606,6 +663,10 @@ impl fmt::Display for ConfigError {
|
|||||||
path.display()
|
path.display()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Self::UnsupportedSchemaVersion { found, supported } => write!(
|
||||||
|
formatter,
|
||||||
|
"configuration schema version {found} is unsupported; this binary accepts version {supported}; migrate a copy before replacing the active configuration"
|
||||||
|
),
|
||||||
Self::Missing {
|
Self::Missing {
|
||||||
field,
|
field,
|
||||||
required_for,
|
required_for,
|
||||||
@@ -652,6 +713,7 @@ impl Error for ConfigError {}
|
|||||||
#[derive(Clone, Default, Deserialize)]
|
#[derive(Clone, Default, Deserialize)]
|
||||||
#[serde(default, deny_unknown_fields)]
|
#[serde(default, deny_unknown_fields)]
|
||||||
struct FileConfig {
|
struct FileConfig {
|
||||||
|
schema_version: Option<u32>,
|
||||||
integrated: Option<bool>,
|
integrated: Option<bool>,
|
||||||
split: Option<bool>,
|
split: Option<bool>,
|
||||||
llm: RawLlm,
|
llm: RawLlm,
|
||||||
@@ -801,6 +863,14 @@ fn resolve<E: Environment>(
|
|||||||
environment: &E,
|
environment: &E,
|
||||||
config_path: Option<&Path>,
|
config_path: Option<&Path>,
|
||||||
) -> Result<AgentConfig, ConfigError> {
|
) -> Result<AgentConfig, ConfigError> {
|
||||||
|
if let Some(found) = raw.schema_version
|
||||||
|
&& found != CONFIG_SCHEMA_VERSION
|
||||||
|
{
|
||||||
|
return Err(ConfigError::UnsupportedSchemaVersion {
|
||||||
|
found,
|
||||||
|
supported: CONFIG_SCHEMA_VERSION,
|
||||||
|
});
|
||||||
|
}
|
||||||
let integrated = environment_boolean(environment, ENV_INTEGRATED)?
|
let integrated = environment_boolean(environment, ENV_INTEGRATED)?
|
||||||
.or(raw.integrated)
|
.or(raw.integrated)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
@@ -1116,9 +1186,11 @@ fn resolve<E: Environment>(
|
|||||||
authorized_avatar_uuids,
|
authorized_avatar_uuids,
|
||||||
timeouts,
|
timeouts,
|
||||||
limits,
|
limits,
|
||||||
storage_path: raw
|
storage_path: environment
|
||||||
.storage_path
|
.get(ENV_STORAGE_PATH)
|
||||||
.unwrap_or_else(|| PathBuf::from("data/grid-agent")),
|
.map(PathBuf::from)
|
||||||
|
.or(raw.storage_path)
|
||||||
|
.unwrap_or_else(|| PlatformPaths::from_environment(environment).data_directory),
|
||||||
behavior: BehaviorSettings {
|
behavior: BehaviorSettings {
|
||||||
heartbeat: checked_duration(
|
heartbeat: checked_duration(
|
||||||
"behavior.heartbeat_seconds",
|
"behavior.heartbeat_seconds",
|
||||||
@@ -1272,6 +1344,21 @@ fn resolve_path(base: &Path, path: &Path) -> PathBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn read_secret(field: &'static str, path: &Path) -> Result<SecretString, ConfigError> {
|
fn read_secret(field: &'static str, path: &Path) -> Result<SecretString, ConfigError> {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::MetadataExt as _;
|
||||||
|
let metadata = fs::symlink_metadata(path).map_err(|error| ConfigError::Io {
|
||||||
|
field,
|
||||||
|
path: path.to_owned(),
|
||||||
|
reason: error.to_string(),
|
||||||
|
})?;
|
||||||
|
if metadata.mode() & 0o077 != 0 {
|
||||||
|
return Err(ConfigError::InvalidSecret {
|
||||||
|
field,
|
||||||
|
reason: "secret file must not grant group or other permissions (use mode 0600 or stricter)",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
let bytes = read_bounded_regular_file(field, path, MAX_SECRET_BYTES)?;
|
let bytes = read_bounded_regular_file(field, path, MAX_SECRET_BYTES)?;
|
||||||
let value = String::from_utf8(bytes).map_err(|_| ConfigError::InvalidSecret {
|
let value = String::from_utf8(bytes).map_err(|_| ConfigError::InvalidSecret {
|
||||||
field,
|
field,
|
||||||
@@ -1462,6 +1549,12 @@ mod tests {
|
|||||||
let mut file = fs::File::create(&path).expect("create test file");
|
let mut file = fs::File::create(&path).expect("create test file");
|
||||||
file.write_all(contents.as_bytes())
|
file.write_all(contents.as_bytes())
|
||||||
.expect("write test file");
|
.expect("write test file");
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt as _;
|
||||||
|
fs::set_permissions(&path, fs::Permissions::from_mode(0o600))
|
||||||
|
.expect("restrict test file");
|
||||||
|
}
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1527,6 +1620,50 @@ mod tests {
|
|||||||
let _ = fs::remove_file(config_path);
|
let _ = fs::remove_file(config_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn schema_storage_precedence_and_platform_paths_are_explicit() {
|
||||||
|
let unsupported = temporary_file(
|
||||||
|
"unsupported-schema.json",
|
||||||
|
r#"{"schema_version":2,"llm":{"endpoint_url":"https://llm.invalid/chat","api_key":"placeholder"}}"#,
|
||||||
|
);
|
||||||
|
assert!(matches!(
|
||||||
|
ConfigLoader::new()
|
||||||
|
.with_file(&unsupported)
|
||||||
|
.with_environment(MapEnvironment::default())
|
||||||
|
.load(),
|
||||||
|
Err(ConfigError::UnsupportedSchemaVersion {
|
||||||
|
found: 2,
|
||||||
|
supported: CONFIG_SCHEMA_VERSION
|
||||||
|
})
|
||||||
|
));
|
||||||
|
let environment = MapEnvironment::from_pairs([
|
||||||
|
(ENV_LLM_ENDPOINT, "https://llm.invalid/chat"),
|
||||||
|
(ENV_LLM_API_KEY, "placeholder"),
|
||||||
|
(ENV_STORAGE_PATH, "operator-data"),
|
||||||
|
("HOME", "/operator"),
|
||||||
|
]);
|
||||||
|
let config = ConfigLoader::new()
|
||||||
|
.with_environment(environment.clone())
|
||||||
|
.load()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(config.storage_path, PathBuf::from("operator-data"));
|
||||||
|
let paths = PlatformPaths::from_environment(&environment);
|
||||||
|
assert!(paths.config_file.ends_with("grid-agent.json"));
|
||||||
|
assert!(paths.data_directory.ends_with("grid-agent"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn group_readable_secret_files_fail_closed() {
|
||||||
|
use std::os::unix::fs::PermissionsExt as _;
|
||||||
|
let secret = temporary_file("insecure-secret", "secret");
|
||||||
|
fs::set_permissions(&secret, fs::Permissions::from_mode(0o640)).unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
read_secret("test.secret", &secret),
|
||||||
|
Err(ConfigError::InvalidSecret { .. })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn endpoint_debug_redacts_credentials_and_query() {
|
fn endpoint_debug_redacts_credentials_and_query() {
|
||||||
let endpoint = EndpointUrl::parse(
|
let endpoint = EndpointUrl::parse(
|
||||||
|
|||||||
@@ -79,9 +79,10 @@ pub use build::{
|
|||||||
BuildService, BuildShape, BuildToolBackend, BuildValidation, PrimReceipt, build_policy_tools,
|
BuildService, BuildShape, BuildToolBackend, BuildValidation, PrimReceipt, build_policy_tools,
|
||||||
};
|
};
|
||||||
pub use config::{
|
pub use config::{
|
||||||
AgentConfig, BehaviorSettings, ConfigError, ConfigLoader, ControlSettings,
|
AgentConfig, BehaviorSettings, CONFIG_SCHEMA_VERSION, ConfigError, ConfigLoader,
|
||||||
ConversationSettings, EndpointUrl, Environment, GridConnection, Limits, LlmConnection,
|
ControlSettings, ConversationSettings, EndpointUrl, Environment, GridConnection, Limits,
|
||||||
MapEnvironment, OperatingMode, RemoteTlsSettings, SecretString, StdEnvironment, Timeouts,
|
LlmConnection, MapEnvironment, OperatingMode, PlatformPaths, RemoteTlsSettings, SecretString,
|
||||||
|
StdEnvironment, Timeouts,
|
||||||
};
|
};
|
||||||
pub use control_plane::{
|
pub use control_plane::{
|
||||||
AuditEventView, CONTROL_PROTOCOL_VERSION, ControlContext, ControlError, ControlErrorBody,
|
AuditEventView, CONTROL_PROTOCOL_VERSION, ControlContext, ControlError, ControlErrorBody,
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ enum Operation {
|
|||||||
RunOnce,
|
RunOnce,
|
||||||
Tui,
|
Tui,
|
||||||
TuiClient,
|
TuiClient,
|
||||||
|
PrintPaths,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -56,10 +57,14 @@ fn options() -> Result<Option<Options>, CliError> {
|
|||||||
}
|
}
|
||||||
if argument == "--help" || argument == "-h" {
|
if argument == "--help" || argument == "-h" {
|
||||||
println!(
|
println!(
|
||||||
"metacrate-grid-agent [--config PATH] [--check-config | --run-once | --tui | --tui-client]\n\
|
"metacrate-grid-agent [--config PATH] [--check-config | --run-once | --tui | --tui-client | --print-paths]\n\
|
||||||
Configuration precedence: defaults < JSON < secret files < environment."
|
Configuration precedence: defaults < JSON < secret files < environment.\n\
|
||||||
|
With no --config, the platform default is used when it exists."
|
||||||
);
|
);
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
} else if argument == "--version" || argument == "-V" {
|
||||||
|
println!("metacrate-grid-agent {}", env!("CARGO_PKG_VERSION"));
|
||||||
|
return Ok(None);
|
||||||
}
|
}
|
||||||
if argument == "--check-config" {
|
if argument == "--check-config" {
|
||||||
set_operation(&mut result, Operation::CheckConfig)?;
|
set_operation(&mut result, Operation::CheckConfig)?;
|
||||||
@@ -69,6 +74,8 @@ fn options() -> Result<Option<Options>, CliError> {
|
|||||||
set_operation(&mut result, Operation::Tui)?;
|
set_operation(&mut result, Operation::Tui)?;
|
||||||
} else if argument == "--tui-client" {
|
} else if argument == "--tui-client" {
|
||||||
set_operation(&mut result, Operation::TuiClient)?;
|
set_operation(&mut result, Operation::TuiClient)?;
|
||||||
|
} else if argument == "--print-paths" {
|
||||||
|
set_operation(&mut result, Operation::PrintPaths)?;
|
||||||
} else if argument == "--config" {
|
} else if argument == "--config" {
|
||||||
let path = arguments
|
let path = arguments
|
||||||
.next()
|
.next()
|
||||||
@@ -99,8 +106,20 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
let Some(options) = options()? else {
|
let Some(options) = options()? else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
let platform_paths = metacrate_grid_agent::PlatformPaths::discover();
|
||||||
|
if options.operation == Operation::PrintPaths {
|
||||||
|
println!("config={}", platform_paths.config_file.display());
|
||||||
|
println!("data={}", platform_paths.data_directory.display());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
let mut loader = ConfigLoader::new();
|
let mut loader = ConfigLoader::new();
|
||||||
if let Some(path) = options.config {
|
let config_path = options.config.or_else(|| {
|
||||||
|
platform_paths
|
||||||
|
.config_file
|
||||||
|
.is_file()
|
||||||
|
.then_some(platform_paths.config_file)
|
||||||
|
});
|
||||||
|
if let Some(path) = config_path {
|
||||||
loader = loader.with_file(path);
|
loader = loader.with_file(path);
|
||||||
}
|
}
|
||||||
let config = loader.load()?;
|
let config = loader.load()?;
|
||||||
|
|||||||
103
crates/metacrate-grid-agent/tests/operations_packaging.rs
Normal file
103
crates/metacrate-grid-agent/tests/operations_packaging.rs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
use metacrate_grid_agent::{CONFIG_SCHEMA_VERSION, ConfigLoader};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
fn workspace() -> PathBuf {
|
||||||
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.parent()
|
||||||
|
.and_then(Path::parent)
|
||||||
|
.expect("workspace")
|
||||||
|
.to_path_buf()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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",
|
||||||
|
] {
|
||||||
|
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 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 config = ConfigLoader::new().with_file(offline).load().unwrap();
|
||||||
|
assert!(config.grid.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn service_and_installers_preserve_state_secrets_and_graceful_shutdown() {
|
||||||
|
let root = workspace();
|
||||||
|
let unit = fs::read_to_string(
|
||||||
|
root.join("packaging/metacrate-grid-agent/systemd/metacrate-grid-agent.service"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
for required in [
|
||||||
|
"ExecStartPre=",
|
||||||
|
"--check-config",
|
||||||
|
"KillSignal=SIGINT",
|
||||||
|
"ProtectSystem=strict",
|
||||||
|
"NoNewPrivileges=true",
|
||||||
|
"ReadWritePaths=/var/lib/metacrate/grid-agent",
|
||||||
|
] {
|
||||||
|
assert!(unit.contains(required), "unit lacks {required}");
|
||||||
|
}
|
||||||
|
for forbidden in ["API_KEY=", "PASSWORD=", "TOKEN="] {
|
||||||
|
assert!(!unit.contains(forbidden), "unit embeds {forbidden}");
|
||||||
|
}
|
||||||
|
let shell = fs::read_to_string(root.join("packaging/metacrate-grid-agent/install.sh")).unwrap();
|
||||||
|
let powershell =
|
||||||
|
fs::read_to_string(root.join("packaging/metacrate-grid-agent/install.ps1")).unwrap();
|
||||||
|
assert!(shell.contains("metacrate-grid-agent.new"));
|
||||||
|
assert!(powershell.contains("metacrate-grid-agent.new.exe"));
|
||||||
|
for forbidden in ["grid-agent.json\"", "conversations/", "audit/", "secrets/"] {
|
||||||
|
assert!(!shell.contains(forbidden));
|
||||||
|
assert!(!powershell.contains(forbidden));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runbook_release_evidence_and_gitea_platform_policy_are_complete() {
|
||||||
|
let root = workspace();
|
||||||
|
let runbook = fs::read_to_string(root.join("docs/grid-agent-operations.md")).unwrap();
|
||||||
|
for required in [
|
||||||
|
"Quick start",
|
||||||
|
"Windows service operation",
|
||||||
|
"Linux systemd",
|
||||||
|
"Threat",
|
||||||
|
"privacy",
|
||||||
|
"Resource defaults",
|
||||||
|
"Failure playbooks",
|
||||||
|
"unsupported",
|
||||||
|
"upgrade",
|
||||||
|
"rollback",
|
||||||
|
"orphan",
|
||||||
|
"rotation",
|
||||||
|
"firewall",
|
||||||
|
"readiness",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
runbook.to_lowercase().contains(&required.to_lowercase()),
|
||||||
|
"runbook lacks {required}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let evidence = fs::read_to_string(root.join("docs/grid-agent-release-evidence.md")).unwrap();
|
||||||
|
assert!(evidence.contains("28,658,000 bytes"));
|
||||||
|
assert!(evidence.contains("192 unique Cargo"));
|
||||||
|
assert!(evidence.contains("no CLR/Mono/.NET"));
|
||||||
|
for workflow in ["ci.yml", "release.yml"] {
|
||||||
|
let text = fs::read_to_string(root.join(".gitea/workflows").join(workflow)).unwrap();
|
||||||
|
assert!(
|
||||||
|
text.lines()
|
||||||
|
.filter(|line| line.contains("runs-on:"))
|
||||||
|
.all(|line| line.contains("ubuntu-latest"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
262
docs/grid-agent-operations.md
Normal file
262
docs/grid-agent-operations.md
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
# MetaCrate grid-agent operations
|
||||||
|
|
||||||
|
This is the operator runbook for the portable `metacrate-grid-agent` binary.
|
||||||
|
The binary has no dependency on systemd, a Windows service manager, a CLR,
|
||||||
|
Python, Node.js, a provider SDK, or a shell at runtime. Linux service files and
|
||||||
|
install helpers are packaging conveniences around the same foreground process
|
||||||
|
and versioned control protocol.
|
||||||
|
|
||||||
|
## Quick start and modes
|
||||||
|
|
||||||
|
Build the credential-free binary and validate the example without contacting a
|
||||||
|
grid or LLM:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build --locked --release -p metacrate-grid-agent
|
||||||
|
target/release/metacrate-grid-agent --config config/grid-agent.example.json --check-config
|
||||||
|
target/release/metacrate-grid-agent --config config/grid-agent.example.json --run-once
|
||||||
|
```
|
||||||
|
|
||||||
|
`--check-config` parses files, resolves secret files/environment, validates all
|
||||||
|
bounds and TLS files, then exits before constructing the grid or LLM clients.
|
||||||
|
The example is fake/offline mode (`integrated=false`, `split=false`) and all
|
||||||
|
credential-looking values are placeholders.
|
||||||
|
|
||||||
|
For live foreground or an embedded TUI, build intentionally:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build --locked --release -p metacrate-grid-agent --features live-grid
|
||||||
|
target/release/metacrate-grid-agent --config /path/to/grid-agent.json
|
||||||
|
target/release/metacrate-grid-agent --config /path/to/grid-agent.json --tui
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `integrated=true` for the foreground and embedded-TUI commands. Set
|
||||||
|
`split=true` for a headless service, then run the same binary as the TUI client:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
metacrate-grid-agent --config /path/to/grid-agent.json
|
||||||
|
metacrate-grid-agent --config /path/to/grid-agent.json --tui-client
|
||||||
|
```
|
||||||
|
|
||||||
|
The TUI client and service must use the same control address and operator token.
|
||||||
|
`--tui-client` never needs the grid password or LLM key. `--run-once` performs
|
||||||
|
one supervised readiness/login and clean logout cycle. `--print-paths` prints
|
||||||
|
the platform defaults without loading configuration. If `--config` is omitted,
|
||||||
|
the default file is used only when it already exists:
|
||||||
|
|
||||||
|
| Platform | Configuration | Non-secret state |
|
||||||
|
|---|---|---|
|
||||||
|
| Linux/Unix | `$XDG_CONFIG_HOME/metacrate/grid-agent.json`, otherwise `$HOME/.config/metacrate/grid-agent.json` | `$XDG_DATA_HOME/metacrate/grid-agent`, otherwise `$HOME/.local/share/metacrate/grid-agent` |
|
||||||
|
| macOS | `$HOME/Library/Application Support/MetaCrate/grid-agent.json` | `$HOME/Library/Application Support/MetaCrate/grid-agent` |
|
||||||
|
| Windows | `%APPDATA%\MetaCrate\grid-agent.json` | `%LOCALAPPDATA%\MetaCrate\grid-agent` |
|
||||||
|
|
||||||
|
`METACRATE_AGENT_STORAGE_PATH` overrides the state directory. Relative paths in
|
||||||
|
a JSON document resolve relative to that document where specified; service
|
||||||
|
deployments should use absolute paths.
|
||||||
|
|
||||||
|
## Endpoint, grid identity, and authority
|
||||||
|
|
||||||
|
`llm.endpoint_url` is the exact OpenAI-compatible chat-completions URL. It is
|
||||||
|
not a base URL: MetaCrate does not append a path, discover models, select a
|
||||||
|
provider, or rewrite query parameters. `llm.api_key` is sent as the bearer key
|
||||||
|
only to that exact origin; redirects are refused. Prefer
|
||||||
|
`METACRATE_AGENT_LLM_API_KEY_FILE` over inline JSON or direct environment text.
|
||||||
|
|
||||||
|
Live modes require `grid.login_url`, `grid.avatar_name`, and a password from
|
||||||
|
`METACRATE_AGENT_GRID_PASSWORD_FILE`. `authorized_avatar_uuids` contains exact
|
||||||
|
grid UUIDs, never display names. Text claiming an authorized identity grants no
|
||||||
|
authority. Public chat can request bounded informational work and safe public
|
||||||
|
LSL delivery; movement, teleport, building, roaming changes, and administration
|
||||||
|
remain policy-gated and require an authenticated authorized IM, operator action,
|
||||||
|
or a narrowly bound scheduler grant as documented in the policy matrix.
|
||||||
|
|
||||||
|
## Configuration contract and migration
|
||||||
|
|
||||||
|
The JSON root uses `"schema_version": 1`. Omitting it is accepted as legacy
|
||||||
|
version 1. Any other value fails before startup with a migration message. There
|
||||||
|
is no automatic in-place migration: copy the file, update the copy using the
|
||||||
|
release notes/example, validate the copy, then atomically select it. Unknown
|
||||||
|
fields fail closed. Installers never overwrite or migrate operator files.
|
||||||
|
|
||||||
|
Precedence, lowest to highest, is:
|
||||||
|
|
||||||
|
1. bounded built-in defaults;
|
||||||
|
2. JSON configuration;
|
||||||
|
3. secret files named by JSON;
|
||||||
|
4. environment-referenced secret files;
|
||||||
|
5. direct environment values.
|
||||||
|
|
||||||
|
Mode, endpoints, credentials, authorization UUIDs, TLS, storage, queue/resource
|
||||||
|
limits, reconnect policy, behavior, and interaction settings are restart-only.
|
||||||
|
Runtime control can pause/resume autonomy, toggle the roaming job, decide an
|
||||||
|
existing approval, cancel an active action, expire/delete conversation state,
|
||||||
|
inject an operator message, reconnect, or shut down; it does not silently
|
||||||
|
rewrite the configuration. A future reloadable field must be explicitly added
|
||||||
|
to the versioned control/config contract.
|
||||||
|
|
||||||
|
## Secrets and privacy
|
||||||
|
|
||||||
|
Threat model: grid residents, object/avatar/inventory metadata, capability
|
||||||
|
replies, model output, public chat, IM text, and persisted files are untrusted.
|
||||||
|
Remote peers may inject instructions, spoof names, replay approvals/call IDs,
|
||||||
|
flood queues, delay or truncate replies, and attempt secret exfiltration. The
|
||||||
|
service identity, local config/secret ACLs, authenticated UUID/control role,
|
||||||
|
opaque policy authorization, generation fencing, bounded queues, and audit sink
|
||||||
|
are trust boundaries. Host/root compromise, a malicious binary/dependency, and
|
||||||
|
an operator deliberately approving a harmful action are outside the process
|
||||||
|
sandbox and require OS/supply-chain/operational controls.
|
||||||
|
|
||||||
|
Never place API keys, grid passwords, or control tokens in command arguments,
|
||||||
|
unit files, wrapper XML, logs, crash-report commands, issue reports, or TUI
|
||||||
|
screens. Secret wrappers redact `Debug` and `Display`, are not serializable, and
|
||||||
|
normal observability stores pseudonymous correlation IDs, result codes, bounds,
|
||||||
|
and hashes—not prompt bodies, credentials, visual pixels, or raw tool arguments.
|
||||||
|
|
||||||
|
Secret files must be regular, non-symlink, bounded UTF-8 files containing one
|
||||||
|
line. On Unix, startup rejects group/other permission bits; use mode `0600` or
|
||||||
|
stricter and ownership by the service identity. On Windows, set an NTFS ACL that
|
||||||
|
grants only the service identity and Administrators, for example with `icacls`;
|
||||||
|
Rust's portable metadata API cannot prove arbitrary Windows ACL semantics, and
|
||||||
|
FAT/network filesystems may not enforce them. Treat an unverifiable filesystem
|
||||||
|
as unsuitable for unattended secrets.
|
||||||
|
|
||||||
|
Rotate one credential at a time: write a new restricted file beside the old
|
||||||
|
one, atomically replace or repoint the `_FILE` setting, run `--check-config`,
|
||||||
|
then restart. Revoke the old credential only after readiness. Control observer
|
||||||
|
and operator tokens must differ from each other and from grid/LLM credentials.
|
||||||
|
|
||||||
|
## Linux systemd
|
||||||
|
|
||||||
|
The hardened example is
|
||||||
|
[`../packaging/metacrate-grid-agent/systemd/metacrate-grid-agent.service`](../packaging/metacrate-grid-agent/systemd/metacrate-grid-agent.service).
|
||||||
|
Create the unprivileged `metacrate-agent` identity, copy (do not overwrite) an
|
||||||
|
operator configuration to `/etc/metacrate/grid-agent.json`, create
|
||||||
|
`/var/lib/metacrate/grid-agent`, and copy the environment-file example. The
|
||||||
|
environment file contains only secret *paths*. Put the actual secret files in
|
||||||
|
`/etc/metacrate/secrets`, owned by the service identity with mode `0600`.
|
||||||
|
|
||||||
|
Install the unit, inspect the sandbox, validate, then start:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo systemd-analyze security packaging/metacrate-grid-agent/systemd/metacrate-grid-agent.service
|
||||||
|
sudo -u metacrate-agent /usr/local/bin/metacrate-grid-agent --config /etc/metacrate/grid-agent.json --check-config
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now metacrate-grid-agent.service
|
||||||
|
```
|
||||||
|
|
||||||
|
The unit uses `SIGINT`, matching foreground Ctrl-C and the owned graceful
|
||||||
|
shutdown path. Do not replace it with `SIGKILL`. `ProtectSystem=strict` permits
|
||||||
|
writes only under the state directory.
|
||||||
|
|
||||||
|
## Windows service operation
|
||||||
|
|
||||||
|
Run the same `.exe`, JSON, control TCP protocol, and shutdown path. Portable
|
||||||
|
foreground operation in PowerShell is the baseline:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& 'C:\Program Files\MetaCrate\metacrate-grid-agent.exe' --config 'C:\ProgramData\MetaCrate\grid-agent.json' --check-config
|
||||||
|
& 'C:\Program Files\MetaCrate\metacrate-grid-agent.exe' --config 'C:\ProgramData\MetaCrate\grid-agent.json'
|
||||||
|
```
|
||||||
|
|
||||||
|
For unattended use, configure a maintained Windows service wrapper (for
|
||||||
|
example WinSW) to launch exactly that command as a dedicated low-privilege
|
||||||
|
account and to translate SCM Stop into Ctrl-C/console control before its timeout.
|
||||||
|
Keep secrets in separate ACL-restricted files and expose only `_FILE` paths in
|
||||||
|
the wrapper environment. Configure restart-on-failure, not unconditional rapid
|
||||||
|
restart. Validate as the service identity before registration. The wrapper must
|
||||||
|
not capture environment values or command output into a world-readable log.
|
||||||
|
The supplied PowerShell installer replaces only the executable and never state.
|
||||||
|
|
||||||
|
## Control, network, and health
|
||||||
|
|
||||||
|
Split control defaults to loopback TCP. A non-loopback bind is rejected unless
|
||||||
|
an explicit Rustls certificate/private key is configured. Use host firewalls to
|
||||||
|
allow only operator networks, protect token files, and prefer loopback plus an
|
||||||
|
authenticated tunnel. Observer tokens are read-only. Protocol framing, role
|
||||||
|
permissions, cancellation, event gaps, and limits are in the control-plane doc.
|
||||||
|
|
||||||
|
Readiness is stricter than a TCP connection: the runtime view must show
|
||||||
|
`transport_connected=true` and `agent_ready=true`. The TUI Overview and
|
||||||
|
observer Runtime request are health/readiness checks; process existence alone
|
||||||
|
is not readiness. Alert on authentication-blocked, sustained degraded/backoff,
|
||||||
|
queue saturation/dropped-event counters, audit failure, orphan builds, and a
|
||||||
|
stale generation.
|
||||||
|
|
||||||
|
## Retention, backup, upgrade, and rollback
|
||||||
|
|
||||||
|
Conversation snapshots, landmark state, and audit journals are non-secret but
|
||||||
|
privacy-sensitive. Stop or pause mutation, take a filesystem-consistent backup
|
||||||
|
of the configured data directory, and encrypt/restrict the backup. Do not back
|
||||||
|
up secrets with ordinary state. Journal rotation is bounded by the configured
|
||||||
|
observability policy; ship rotated files to restricted storage before deletion
|
||||||
|
when retention policy requires it. Replay is diagnostic and never executes an
|
||||||
|
action.
|
||||||
|
|
||||||
|
Release installation replaces only the binary. The POSIX and PowerShell helpers
|
||||||
|
stage a temporary executable then move it into place; neither touches config,
|
||||||
|
secrets, conversations, landmarks, or journals. For upgrade: back up state,
|
||||||
|
install the new binary, validate the existing config, stop gracefully, start,
|
||||||
|
and verify readiness. For rollback: stop gracefully, restore the prior binary,
|
||||||
|
restore state only when the new version changed it incompatibly, validate, and
|
||||||
|
start. Never run two service generations against one writable data directory.
|
||||||
|
|
||||||
|
## Failure playbooks
|
||||||
|
|
||||||
|
- Authentication blocked: pause retries, verify login URL/avatar and rotate the
|
||||||
|
password file; never paste it into logs. Force reconnect after correction.
|
||||||
|
- LLM unavailable/rate limited: autonomy remains bounded; verify the exact URL,
|
||||||
|
firewall/DNS, and key. Multimodal rejection falls back to the textual scene
|
||||||
|
summary without resending the large image.
|
||||||
|
- Maintenance/disconnect: allow generation fencing and bounded backoff. Stale
|
||||||
|
inference/mutation results are discarded; do not bypass reconnect controls.
|
||||||
|
- Emergency: use operator Pause first, Cancel the exact active action when
|
||||||
|
appropriate, then Graceful Shutdown. Ctrl-C/SIGINT follows the same cleanup.
|
||||||
|
- Orphan build: keep the reported object IDs, inspect ownership in-world, and
|
||||||
|
manually recover only those IDs. Never bulk-delete by name or proximity.
|
||||||
|
- Corrupt persistence: preserve the quarantined file for restricted diagnosis;
|
||||||
|
the service recovers an older valid generation or starts fail-closed. Do not
|
||||||
|
hand-edit a live journal.
|
||||||
|
- Full disk/audit backpressure: pause autonomy, free space according to retention
|
||||||
|
policy, and restart only after the audit path is writable. Policy fails closed
|
||||||
|
when required audit records cannot be accepted.
|
||||||
|
|
||||||
|
## Resource defaults and unsupported operations
|
||||||
|
|
||||||
|
The example records all current queue, message, conversation, tool, behavior,
|
||||||
|
reconnect, and interaction defaults. Important defaults include 256 grid events,
|
||||||
|
32 control commands, 512 observations, four concurrent inference requests,
|
||||||
|
16 tool calls, 512 active senders/sessions, a 1 MiB transport body, and bounded
|
||||||
|
10-second shutdown. Vision defaults to a 320x180 synthetic image with bounded
|
||||||
|
entities, triangles, texture work, PNG bytes, time, and concurrency.
|
||||||
|
|
||||||
|
Unsupported by design: arbitrary raw packets or agent-control flags, arbitrary
|
||||||
|
shell/subprocess execution, provider SDK/model discovery, remote plaintext
|
||||||
|
control, unauthenticated mutation, unrestricted walking/teleport/touch/follow,
|
||||||
|
automatic config migration, persistence of viewport pixels, framebuffer/screen
|
||||||
|
capture, and treating untrusted grid/LLM text as instructions or authority.
|
||||||
|
|
||||||
|
## Release evidence
|
||||||
|
|
||||||
|
Build the intentional graphs and retain the commands/output with the release:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build --locked --release -p metacrate-grid-agent
|
||||||
|
cargo build --locked --release -p metacrate-grid-agent --features live-grid
|
||||||
|
cargo tree --locked -p metacrate-grid-agent --features live-grid -e normal,build
|
||||||
|
cargo audit
|
||||||
|
```
|
||||||
|
|
||||||
|
Record the binary byte size (`stat -c %s` on Linux or `Get-Item ... .Length` on
|
||||||
|
PowerShell) and the `cargo tree` inventory. Reject CLR/scripting/provider SDK,
|
||||||
|
subprocess adapters, Skia/GPU, or undeclared native libraries on the agent path.
|
||||||
|
The consolidated Gitea gate runs only on `ubuntu-latest`; Windows portability is
|
||||||
|
proved by the existing cross-target compile/static gate rather than a Windows
|
||||||
|
Gitea runner.
|
||||||
|
|
||||||
|
The milestone baseline is recorded in
|
||||||
|
[grid-agent-release-evidence.md](grid-agent-release-evidence.md).
|
||||||
|
|
||||||
|
Related contracts: [architecture](grid-agent-architecture.md),
|
||||||
|
[policy](grid-agent-policy.md), [session](grid-agent-session.md),
|
||||||
|
[control](grid-agent-control-plane.md), [observability](grid-agent-observability.md),
|
||||||
|
[TUI](grid-agent-tui.md), and [conversation storage](grid-agent-conversation.md).
|
||||||
34
docs/grid-agent-release-evidence.md
Normal file
34
docs/grid-agent-release-evidence.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Grid-agent release evidence
|
||||||
|
|
||||||
|
This checked-in measurement is the packaging baseline for milestone 14. It is
|
||||||
|
not a downloadable artifact or a substitute for regenerating evidence for each
|
||||||
|
release candidate.
|
||||||
|
|
||||||
|
- Measured: 2026-08-18
|
||||||
|
- Host: `aarch64-unknown-linux-gnu`
|
||||||
|
- Rust: `rustc 1.97.1 (8bab26f4f 2026-07-14)`
|
||||||
|
- Command: `cargo build --locked --release -p metacrate-grid-agent --features live-grid`
|
||||||
|
- Profile: workspace `release` (`opt-level=1`, `debug=0`, incremental disabled,
|
||||||
|
256 codegen units to stay within the packaging memory ceiling)
|
||||||
|
- Binary: `target/release/metacrate-grid-agent`
|
||||||
|
- Size: 28,658,000 bytes
|
||||||
|
- SHA-256: `c8ed694c6817ce9a897897f521e174fc8d90378ddf46565336c747e22d876dfc`
|
||||||
|
- Normal/build dependency inventory: 192 unique Cargo package/version/path
|
||||||
|
identities from `cargo tree --locked -p metacrate-grid-agent --features
|
||||||
|
live-grid -e normal,build`
|
||||||
|
- Dynamic ELF dependencies: the AArch64 loader, `libc`, `libm`, and `libgcc_s`
|
||||||
|
only. TLS's declared `aws-lc-sys` component is statically linked.
|
||||||
|
|
||||||
|
The audited tree contains the MetaCrate agent, the original compatibility
|
||||||
|
crates it consumes, the pure-Rust J2K/meshing/rendering path, Tokio, Reqwest,
|
||||||
|
Rustls, AWS-LC, terminal/serialization/bounds utilities, and their build-time
|
||||||
|
Rust tooling. It contains no CLR/Mono/.NET runtime, Python/Node/Lua scripting
|
||||||
|
runtime, provider SDK, Skia/GPU stack, OpenJPEG feature, shell/subprocess
|
||||||
|
adapter, or dynamically loaded undeclared application library.
|
||||||
|
|
||||||
|
Regenerate for a release candidate with the commands in
|
||||||
|
[grid-agent-operations.md](grid-agent-operations.md), retain the full unabridged
|
||||||
|
`cargo tree` output as CI/release evidence, compare size intentionally, and run
|
||||||
|
the dependency-policy and consolidated required gates. Binary hashes and sizes
|
||||||
|
will change when source, compiler, target, profile, or dependency versions
|
||||||
|
change.
|
||||||
15
packaging/metacrate-grid-agent/install.ps1
Normal file
15
packaging/metacrate-grid-agent/install.ps1
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$BuiltBinary,
|
||||||
|
[Parameter(Mandatory = $true)][string]$DestinationDirectory
|
||||||
|
)
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
if (-not (Test-Path -LiteralPath $BuiltBinary -PathType Leaf)) {
|
||||||
|
throw "BuiltBinary is not a regular file"
|
||||||
|
}
|
||||||
|
New-Item -ItemType Directory -Force -Path $DestinationDirectory | Out-Null
|
||||||
|
$temporary = Join-Path $DestinationDirectory ".metacrate-grid-agent.new.exe"
|
||||||
|
Copy-Item -LiteralPath $BuiltBinary -Destination $temporary -Force
|
||||||
|
Move-Item -LiteralPath $temporary -Destination (Join-Path $DestinationDirectory "metacrate-grid-agent.exe") -Force
|
||||||
|
|
||||||
|
# Configuration, secrets, conversations, landmarks, and journals live outside
|
||||||
|
# this directory and are deliberately never created, replaced, or removed.
|
||||||
18
packaging/metacrate-grid-agent/install.sh
Executable file
18
packaging/metacrate-grid-agent/install.sh
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
if [ "$#" -ne 2 ]; then
|
||||||
|
echo "usage: install.sh BUILT_BINARY DESTINATION_PREFIX" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
source_binary=$1
|
||||||
|
destination_prefix=$2
|
||||||
|
test -f "$source_binary"
|
||||||
|
install -d -m 0755 "$destination_prefix/bin"
|
||||||
|
temporary="$destination_prefix/bin/.metacrate-grid-agent.new"
|
||||||
|
install -m 0755 "$source_binary" "$temporary"
|
||||||
|
mv -f "$temporary" "$destination_prefix/bin/metacrate-grid-agent"
|
||||||
|
|
||||||
|
# Deliberately do not create, replace, migrate, or remove configuration,
|
||||||
|
# secrets, conversations, landmarks, or audit journals.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Values here are paths and non-secret mode settings only. Never put secret
|
||||||
|
# values in this file. Create every referenced file as mode 0600, owned by the
|
||||||
|
# metacrate-agent service identity.
|
||||||
|
METACRATE_AGENT_LLM_API_KEY_FILE=/etc/metacrate/secrets/llm-api-key
|
||||||
|
METACRATE_AGENT_GRID_PASSWORD_FILE=/etc/metacrate/secrets/grid-password
|
||||||
|
METACRATE_AGENT_CONTROL_OPERATOR_TOKEN_FILE=/etc/metacrate/secrets/control-operator-token
|
||||||
|
METACRATE_AGENT_STORAGE_PATH=/var/lib/metacrate/grid-agent
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=MetaCrate OpenSim grid agent
|
||||||
|
Documentation=https://git.rfc1437.de/hugo/MetaCrate
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=metacrate-agent
|
||||||
|
Group=metacrate-agent
|
||||||
|
EnvironmentFile=-/etc/metacrate/grid-agent.env
|
||||||
|
ExecStartPre=/usr/local/bin/metacrate-grid-agent --config /etc/metacrate/grid-agent.json --check-config
|
||||||
|
ExecStart=/usr/local/bin/metacrate-grid-agent --config /etc/metacrate/grid-agent.json
|
||||||
|
KillSignal=SIGINT
|
||||||
|
TimeoutStopSec=30s
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateDevices=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectClock=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
ProtectHome=true
|
||||||
|
ProtectHostname=true
|
||||||
|
ProtectKernelLogs=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ReadOnlyPaths=/etc/metacrate
|
||||||
|
ReadWritePaths=/var/lib/metacrate/grid-agent
|
||||||
|
RestrictAddressFamilies=AF_INET AF_INET6
|
||||||
|
RestrictNamespaces=true
|
||||||
|
RestrictRealtime=true
|
||||||
|
SystemCallArchitectures=native
|
||||||
|
UMask=0077
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Reference in New Issue
Block a user