diff --git a/.env b/.env new file mode 100644 index 0000000..7983fd6 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +AGENT_PORTAL_HOST_GH=$HOME/.nix-profile/bin/gh diff --git a/.envrc b/.envrc index 451da97..136570b 100644 --- a/.envrc +++ b/.envrc @@ -1,2 +1,3 @@ use flake export PATH=$PWD/target/debug:$PATH +dotenv diff --git a/.gitignore b/.gitignore index b40b472..718089c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target demo.gif result -.direnv/ \ No newline at end of file +.direnv/ +.env diff --git a/ab/src/main.rs b/ab/src/main.rs index 378ab4b..5f9cb87 100644 --- a/ab/src/main.rs +++ b/ab/src/main.rs @@ -35,9 +35,13 @@ fn maybe_start_managed_portal( return Ok(None); } + let socket_path = per_container_portal_socket_path(); + let log_path = agent_portal::logging::init(None, Some(&socket_path), false)?; + eprintln!("managed portal log file: {}", log_path.display()); + Ok(Some(agent_portal::host::spawn_managed( config.portal.clone(), - per_container_portal_socket_path(), + socket_path, )?)) } diff --git a/docs/src/how-to/portal/debug-wrapper-failures.md b/docs/src/how-to/portal/debug-wrapper-failures.md index a65484f..be425c9 100644 --- a/docs/src/how-to/portal/debug-wrapper-failures.md +++ b/docs/src/how-to/portal/debug-wrapper-failures.md @@ -20,10 +20,20 @@ Find and fix common failures for `wl-paste`/`gh` wrappers and other Portal clien ```bash echo "$AGENT_PORTAL_SOCKET" ``` -4. Enable host logs: +4. Enable host logging with `RUST_LOG`: ```bash - RUST_LOG=debug agent-portal-host + RUST_LOG=agent_portal=debug,agent_portal_host=trace agent-portal-host ``` + When `agent-portal-host` is run directly, logs are visible in the terminal and also written to the log file. Managed hosts started by `ab spawn` log only to files. +5. Inspect the log file: + - Log files live under: + ```text + ${XDG_STATE_HOME:-$HOME/.local/state}/agent-box/logs/ + ``` + - The log filename matches the socket filename, with `.sock` replaced by `.log`. + - Example: `portal.sock` -> `portal.log` + - In managed per-container mode (`[portal].global = false`), each spawned socket gets its own matching log file. + - Use `RUST_LOG=debug ab spawn ...` if you want more verbose managed-host logs. ## Common failures diff --git a/docs/src/reference/common/env-vars.md b/docs/src/reference/common/env-vars.md index 20501c4..554ff20 100644 --- a/docs/src/reference/common/env-vars.md +++ b/docs/src/reference/common/env-vars.md @@ -14,6 +14,12 @@ - `RUST_LOG` - Controls tracing filter for `agent-portal-host` and other Rust binaries using tracing subscriber. +- `XDG_STATE_HOME` + - Used to resolve the default Portal log directory. + - Default Portal log directory: `$XDG_STATE_HOME/agent-box/logs/` + - Fallback when unset: `~/.local/state/agent-box/logs/` + - Each Portal log filename is derived from the socket filename, replacing `.sock` with `.log` + ## Runtime passthrough Variables listed in `[runtime].env_passthrough` are copied from host into container at spawn time. diff --git a/docs/src/reference/portal/config.md b/docs/src/reference/portal/config.md index 4121118..3fd51fe 100644 --- a/docs/src/reference/portal/config.md +++ b/docs/src/reference/portal/config.md @@ -12,6 +12,10 @@ Portal config lives under `[portal]` in `~/.agent-box.toml`. - used directly when `global = true` - ignored by `ab spawn` when `global = false`, because `ab` allocates a unique per-container socket path - `prompt_command` (string|null, default: unset) +- Logging is controlled at process startup, not in config: + - `RUST_LOG=...` provides tracing filter control + - logs are written under `${XDG_STATE_HOME:-~/.local/state}/agent-box/logs/` + - each log filename is derived from the socket filename, replacing `.sock` with `.log` - `timeouts.request_ms` (u64, default: `0` = no timeout) - `timeouts.prompt_ms` (u64, default: `0` = no timeout) - `limits.max_inflight` (usize, default: `32`) diff --git a/portal/README.md b/portal/README.md index e5570ef..3873b9f 100644 --- a/portal/README.md +++ b/portal/README.md @@ -25,14 +25,33 @@ Policy mode is configured in `~/.agent-box.toml` via `portal.policy.defaults.gh_ ## Logging -`agent-portal-host` uses `tracing` + `RUST_LOG` filtering. +`agent-portal-host` uses `tracing` and writes logs to stderr and a log file when run directly. Managed per-container Portal instances started by `ab spawn` write logs only to files. + +Default log file location: + +```text +${XDG_STATE_HOME:-~/.local/state}/agent-box/logs/.log +``` + +The log filename is derived from the socket filename by replacing `.sock` with `.log`. + +Examples: + +```text +portal.sock -> portal.log +portal-12345-abc.sock -> portal-12345-abc.log +``` + +Use `RUST_LOG` for tracing filter control. Example: ```bash -RUST_LOG=debug agent-portal-host +RUST_LOG=agent_portal=debug,agent_portal_host=trace agent-portal-host ``` +Managed per-container Portal instances started by `ab spawn` also initialize logging this way, so each managed socket gets a matching per-instance log file. + ## Development From repo root: diff --git a/portal/src/bin/agent-portal-host.rs b/portal/src/bin/agent-portal-host.rs index 294e4f9..30bfa2e 100644 --- a/portal/src/bin/agent-portal-host.rs +++ b/portal/src/bin/agent-portal-host.rs @@ -1,12 +1,10 @@ use agent_box_common::config::load_config; use clap::Parser; use eyre::Result; -use std::io::IsTerminal; use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; use tracing::error; -use tracing_subscriber::EnvFilter; #[derive(Parser, Debug)] #[command(name = "agent-portal-host")] @@ -17,33 +15,38 @@ struct Cli { socket: Option, } -fn init_logging() { - let env_filter = EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::new("info,agent_portal_host=info")); - - tracing_subscriber::fmt() - .with_env_filter(env_filter) - .with_ansi(std::io::stderr().is_terminal()) - .init(); -} - fn main() { - init_logging(); + let cli = Cli::parse(); - if let Err(e) = run() { + let config = match load_config() { + Ok(config) => config, + Err(e) => { + eprintln!("Error: {e}"); + std::process::exit(1); + } + }; + let socket_path = PathBuf::from( + cli.socket + .clone() + .unwrap_or_else(|| config.portal.socket_path.clone()), + ); + + if let Err(e) = agent_portal::logging::init(None, Some(&socket_path), true) { + eprintln!("Error: {e}"); + std::process::exit(1); + } + + if let Err(e) = run(cli, config.portal) { error!(error = %e, "portal host failed"); std::process::exit(1); } } -fn run() -> Result<()> { +fn run(cli: Cli, portal: agent_box_common::portal::PortalConfig) -> Result<()> { let path = std::env::var("PATH").unwrap_or_default(); let path = path.split(':').collect::>(); tracing::info!(path = ?path, "PATH"); - let cli = Cli::parse(); - let config = load_config()?; - let portal = config.portal; let socket_path = PathBuf::from(cli.socket.unwrap_or_else(|| portal.socket_path.clone())); agent_portal::host::run_with_config_and_socket( diff --git a/portal/src/lib.rs b/portal/src/lib.rs index 5361e40..fc8fdb3 100644 --- a/portal/src/lib.rs +++ b/portal/src/lib.rs @@ -1 +1,2 @@ pub mod host; +pub mod logging; diff --git a/portal/src/logging.rs b/portal/src/logging.rs new file mode 100644 index 0000000..f659e1d --- /dev/null +++ b/portal/src/logging.rs @@ -0,0 +1,117 @@ +use eyre::{Result, WrapErr}; +use std::ffi::OsStr; +use std::fs::{self, File, OpenOptions}; +use std::io::{self, IsTerminal, Write}; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex, OnceLock}; +use tracing_subscriber::fmt; +use tracing_subscriber::prelude::*; +use tracing_subscriber::{EnvFilter, registry}; + +static LOG_PATH: OnceLock = OnceLock::new(); + +#[derive(Clone)] +struct SharedFileWriter { + file: Arc>, +} + +impl Write for SharedFileWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + let mut file = self + .file + .lock() + .map_err(|_| io::Error::other("failed to lock portal log file"))?; + file.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + let mut file = self + .file + .lock() + .map_err(|_| io::Error::other("failed to lock portal log file"))?; + file.flush() + } +} + +fn log_dir() -> PathBuf { + if let Some(state_home) = std::env::var_os("XDG_STATE_HOME") { + return PathBuf::from(state_home).join("agent-box").join("logs"); + } + + if let Some(home) = std::env::var_os("HOME") { + return PathBuf::from(home) + .join(".local") + .join("state") + .join("agent-box") + .join("logs"); + } + + std::env::temp_dir().join("agent-box").join("logs") +} + +pub fn default_log_path(socket_path: Option<&Path>) -> PathBuf { + let file_name = socket_path + .and_then(Path::file_name) + .unwrap_or_else(|| OsStr::new("agent-portal-host.sock")); + let mut log_name = PathBuf::from(file_name); + log_name.set_extension("log"); + log_dir().join(log_name) +} + +pub fn init(log_level: Option<&str>, socket_path: Option<&Path>, visible: bool) -> Result { + if let Some(path) = LOG_PATH.get() { + return Ok(path.clone()); + } + + let log_path = default_log_path(socket_path); + if let Some(parent) = log_path.parent() { + fs::create_dir_all(parent).wrap_err("failed to create portal log directory")?; + } + + let file = OpenOptions::new() + .create(true) + .append(true) + .open(&log_path) + .wrap_err_with(|| format!("failed to open portal log file {}", log_path.display()))?; + let file = Arc::new(Mutex::new(file)); + + let env_filter = match log_level { + Some(level) => EnvFilter::new(level), + None => EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), + }; + + if visible { + let file = Arc::clone(&file); + let file_layer = fmt::layer() + .with_ansi(false) + .with_writer(move || SharedFileWriter { + file: Arc::clone(&file), + }); + let stderr_layer = fmt::layer() + .with_ansi(std::io::stderr().is_terminal()) + .with_writer(std::io::stderr); + registry() + .with(env_filter) + .with(stderr_layer) + .with(file_layer) + .try_init() + .map_err(|e| eyre::eyre!("failed to initialize portal logging: {e}"))?; + } else { + let file = Arc::clone(&file); + let file_layer = fmt::layer() + .with_ansi(false) + .with_writer(move || SharedFileWriter { + file: Arc::clone(&file), + }); + registry() + .with(env_filter) + .with(file_layer) + .try_init() + .map_err(|e| eyre::eyre!("failed to initialize portal logging: {e}"))?; + } + + let _ = LOG_PATH.set(log_path.clone()); + tracing::info!(log_file = %log_path.display(), "portal logging initialized"); + + Ok(log_path) +}