Skip to content

Repository files navigation

agentd 🏃

AI agent management daemon for stereOS. Starts, manages, and stops AI agents.

Each agent runs in its own tmux session, allowing operators to tmux attach and observe the agent in real time. agentd handles restart policies, timeouts, graceful shutdown, and serves a read-only API for status polling.

agentd operates on a reconciliation loop. Every few seconds it re-reads the jcard.toml configuration and secrets directory from disk. If either has changed, the running agent is stopped and relaunched with the new configuration.

Harnesses

A harness maps a jcard.toml harness name to a binary and argument format. The Harness interface:

typeHarnessinterface {
Name() stringBuildCommand(promptstring) (binstring, args []string)
}

Built-in harnesses:

NameBinaryPrompt flagInteractive (no prompt)
claude-codeclaude-p <prompt>claude
opencodeopencode--prompt <prompt>opencode
gemini-cligemini<prompt> (positional)gemini
customagent (configurable)<prompt> (positional)agent

Adding a new harness: implement the interface, register it in harness.go's registry map, and add the name to config validation.

Manager

The manager handles agent lifecycle within a tmux session.

Start: If timeout is set, wraps context with context.WithTimeout. Creates a tmux session, launches the harness command via send-keys, spawns a monitoring goroutine.

Monitor loop: Polls tmux.IsSessionRunning() every 2 seconds. On agent exit, evaluates shouldRestart():

Restart policyBehavior
noNever restart
on-failureRestart unless max_restarts reached
alwaysRestart unless max_restarts reached

Restart backoff: 3 seconds between attempts.

Graceful stop: Sends C-c (SIGINT) to the tmux session. Waits up to the grace period (default 30s) for the session to exit. If the grace period expires, forcibly destroys the session.

tmux sessions

agentd uses a dedicated tmux server socket (/run/agentd/tmux.sock) isolated from user sessions. All tmux commands are wrapped in sudo -u agent -- because tmux enforces UID-based ownership checks on socket connections.

Session creation:

  1. Create a detached session with a default shell (tmux new-session -d)
  2. Set environment variables via -e flags
  3. Set working directory via -c
  4. Use send-keys to type the command + press Enter

This approach keeps the underlying shell alive after the agent exits, allowing inspection. Socket permissions are set to 0770 with admin group ownership so admin users can attach.

# As admin user inside the VM:
sudo tmux -S /run/agentd/tmux.sock attach -t claude-code

Configuration

agentd reads only the [agent] section from jcard.toml, ignoring everything else.

[agent]
harness = "claude-code"prompt = "review the code and fix failing tests"# prompt_file = "./prompts/review.md" # takes precedence over promptworkdir = "/home/agent/workspace"restart = "on-failure"max_restarts = 5timeout = "2h"grace_period = "30s"session = "my-session"
[agent.env]
MY_VAR = "my_value"
FieldTypeDefaultDescription
harnessstring(required)claude-code, opencode, gemini-cli, custom
promptstring""Prompt given to the agent (empty = interactive)
prompt_filestringPath to a prompt file (takes precedence over prompt)
workdirstring/home/agent/workspaceAgent working directory
restartstring"no"no, on-failure, always
max_restartsint0 (unlimited)Max restart attempts (0 = no limit)
timeoutstringAgent timeout as Go duration (e.g. "2h")
grace_periodstring"30s"SIGTERM grace period before force kill
sessionstringharness nametmux session name
envmap{}Environment variables for the agent process

Secrets

Secrets are files on disk in a directory (default /run/stereos/secrets/), written by stereosd to an admin accessible tmpfs. Each file represents one secret: filename = env var name, content = value.

  • Hidden files (. prefix) and directories are skipped
  • Trailing newlines are trimmed
  • If the directory does not exist, an empty map is returned
  • Secrets are merged into the agent environment; [agent.env] values override secrets with the same name

API

HTTP over Unix domain socket (/run/stereos/agentd.sock, mode 0660, group admin). This API is read-only.

MethodPathResponse
GET/v1/health{"state":"running","uptime_seconds":123}
GET/v1/agents[{"name":"claude-code","running":true,"session":"claude-code","restarts":0}]
GET/v1/agents/{name}Single agent status (404 if not found, case-insensitive match)

NixOS module

The flake exports nixosModules.default:

OptionTypeDefaultDescription
services.agentd.enableboolfalseEnable the agentd daemon
services.agentd.packagepackageflake defaultThe agentd package
services.agentd.extraArgslist of str[]Additional CLI arguments

The systemd unit includes tmux and sudo in its PATH (required for session management as the agent user). DynamicUser=true in the base module; overridden to false by the stereOS NixOS module since agentd needs root to manage tmux sessions for the agent user.

Local development

agentd is designed to run inside stereOS managed by systemd. For local testing, override the paths:

./build/agentd \
-config /tmp/agentd-test/jcard.toml \
-secret-dir /tmp/agentd-test/secrets \
-tmux-socket /tmp/agentd-test/tmux.sock \
-api-socket /tmp/agentd-test/agentd.sock \
-debug

Debug mode logs the full command, working directory, environment key names, and captures tmux pane output on exit.

About

The StereOS agent management daemon.

Resources

Contributing

Security policy

Stars

47 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages