Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
AGENT_PORTAL_HOST_GH=$HOME/.nix-profile/bin/gh
1 change: 1 addition & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
use flake
export PATH=$PWD/target/debug:$PATH
dotenv
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/target
demo.gif
result
.direnv/
.direnv/
.env
6 changes: 5 additions & 1 deletion ab/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)?))
}

Expand Down
14 changes: 12 additions & 2 deletions docs/src/how-to/portal/debug-wrapper-failures.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions docs/src/reference/common/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 4 additions & 0 deletions docs/src/reference/portal/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
23 changes: 21 additions & 2 deletions portal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<socket-name>.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:
Expand Down
39 changes: 21 additions & 18 deletions portal/src/bin/agent-portal-host.rs
Original file line number Diff line number Diff line change
@@ -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")]
Expand All @@ -17,33 +15,38 @@ struct Cli {
socket: Option<String>,
}

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::<Vec<_>>();
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(
Expand Down
1 change: 1 addition & 0 deletions portal/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod host;
pub mod logging;
117 changes: 117 additions & 0 deletions portal/src/logging.rs
Original file line number Diff line number Diff line change
@@ -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<PathBuf> = OnceLock::new();

#[derive(Clone)]
struct SharedFileWriter {
file: Arc<Mutex<File>>,
}

impl Write for SharedFileWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
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<PathBuf> {
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)
}
Loading