From 0cfa1f041f751f7b4939c874fcfa944e09c26edc Mon Sep 17 00:00:00 2001 From: Minglong Pan <56749246+minglong51@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:35:33 -0700 Subject: [PATCH] fix(pkg): ship a sanitized default config instead of the maintainer's live one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit agent_usage_manager/agents.yaml served two masters: it is the maintainer's live config (launchd points at it directly) AND the fallback the wheel ships. So every `pip install` / `uvx` user received a default whose alerts.command shells out to "$HOME/workspace/founder-agent-os/bin/attention" — a private repo path that exits 127 on any other machine, while README documents alerts as something the user adds with a terminal-notifier example. The shipped default contradicted the docs and leaked private infrastructure naming into a public package. Before, on a clean machine: $ agent-usage-manager test-alert FAILED: exit 127 - /bin/sh: ~/workspace/founder-agent-os/bin/attention: No such file After: no alerts.command configured in .../agents.default.yaml - nothing to test - add agent_usage_manager/agents.default.yaml: same agents/protect/ignore rules and docs, with launchd_labels, tmux_labels, idle_ok and alerts commented out as generic examples. Nothing is wired by default. - _resolve_config() falls back to agents.default.yaml only when agents.yaml is absent, so source checkouts (and the maintainer's launchd job) are unchanged. - exclude agents.yaml from both wheel and sdist targets. Also, pre-launch packaging polish: - absolute raw.githubusercontent URLs for the two README images, which were relative and therefore broken on the PyPI project page. - 14 trove classifiers (there were none - the standard unfinished-package tell). - Issues and Newsletter project URLs; Newsletter puts the waitlist in PyPI's permanent sidebar. - second waitlist mention after Quick start; the only one sat at 99.5% README depth, below Troubleshooting. 74 tests pass. Wheel verified to contain agents.default.yaml only, with no occurrence of the private path. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015swHyENHGowhH1cwMXxiE2 --- README.md | 9 +- agent_usage_manager/agents.default.yaml | 129 ++++++++++++++++++++++++ agent_usage_manager/app.py | 5 +- pyproject.toml | 26 ++++- uv.lock | 2 +- 5 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 agent_usage_manager/agents.default.yaml diff --git a/README.md b/README.md index 2b1111a..65b070f 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,14 @@ A small web dashboard for **headless AI agents** running on a machine — OpenClaw, Hermes, Claude Code, Ollama, vLLM, llama.cpp, or anything you name. It shows which agents are alive and what they're costing you (CPU, memory, GPU), and gives you a **kill button** per agent. Think `htop`, scoped to just your agents — -the [screenshot below](docs/dashboard.png) is a real run on a fleet node. +the [screenshot below](https://raw.githubusercontent.com/minglong51/agent-usage-manager/main/docs/dashboard.png) is a real run on a fleet node. No database, no auth framework (one static token file gates the kill switch), four direct dependencies (FastAPI, uvicorn, psutil, PyYAML). Runs on macOS and Linux. It is a per-node monitor and guarded local control panel: fleet schedulers may consume its read-only telemetry, but should own their own scheduling and actuation. -![agent-usage-manager — live dashboard](docs/dashboard.png) +![agent-usage-manager — live dashboard](https://raw.githubusercontent.com/minglong51/agent-usage-manager/main/docs/dashboard.png) *A real run: ten agents grouped by process tree (`+N` = children rolled up), per-agent CPU/memory/uptime, launchd-supervised jobs flagged, and a kill button @@ -39,7 +39,10 @@ uvx agent-usage-manager # then open http://127.0.0.1:8765 (opens automatica One command — no install, no virtualenv, no leftovers. Other install options, config, and flags: [Install & run](#install--run). -![one command to a live dashboard](docs/demo.gif) +![one command to a live dashboard](https://raw.githubusercontent.com/minglong51/agent-usage-manager/main/docs/demo.gif) + +*Field notes on running agents with discipline go out on the +[Agent Discipline](https://buttondown.com/minglong51) list — a few emails a month.* ## What it does diff --git a/agent_usage_manager/agents.default.yaml b/agent_usage_manager/agents.default.yaml new file mode 100644 index 0000000..b15cc6f --- /dev/null +++ b/agent_usage_manager/agents.default.yaml @@ -0,0 +1,129 @@ +# Default configuration shipped with the package. It is used only when you have +# no agents.yaml of your own: `--config` wins, then ./agents.yaml in the current +# directory, then this file. Copy it somewhere and edit rather than editing it +# in place — a package upgrade replaces this file. +# +# Which processes count as "agents". A process matches if the pattern hits its +# executable name + first few arguments (case-insensitive substring or regex) — +# NOT the entire command line, so a process that merely mentions an agent name +# deep in its args (e.g. inside a system prompt) is not misclassified. +# Only processes that match here can be listed AND killed — the kill endpoint +# re-checks the match before sending a signal, so the dashboard can never be +# used to kill an arbitrary unrelated PID. +# +# Matched processes are grouped by process tree: one row per agent (the root), +# with child processes rolled up. CPU/mem/GPU are tree totals, and "kill" stops +# the whole tree (root + children) so spawned helpers don't leak resources. +agents: + - label: openclaw + match: openclaw + - label: hermes + match: hermes + - label: claude-code + match: "claude(\\s|$|-code)" + regex: true + - label: kiro + match: "\\bkiro\\b" # word-anchored so it doesn't hit any kiro-named path + regex: true + - label: aider + match: "\\baider\\b" # anchored: bare 'aider' matches 'raider', 'spider'… + regex: true + - label: codex + match: "\\bcodex\\b" + regex: true + - label: cline + match: "\\bcline\\b" # anchored: bare 'cline' matches 'decline', 'incline' + regex: true + - label: ollama + match: ollama + - label: vllm + match: vllm + - label: llama.cpp + match: llama-server + +# Patterns that must NEVER be killed even if they match an agent pattern above. +# Self (this monitor's own PID) and PID 1 are always protected in code. +protect: + - agent-usage-manager + - uvicorn + +# Patterns that disqualify a process from being an agent at all — not listed, +# not killable. Use this for incidental processes that share a name or bundle +# path with a real agent. Checked against the same match target (executable +# basename + first args + macOS .app bundle name), case-insensitive substring. +# +# Why these: a GUI agent like Kiro.app launches its main process as "Electron", +# so agents are matched by the .app bundle name ("Kiro"). That also sweeps in the +# app's crash handler, its Squirrel auto-updater (ShipIt), and the editor's +# integrated-terminal shells (titled "kiro-cli-term") — none of which are the +# agent. Ignoring them leaves one clean row for the app (helpers roll up under +# it) instead of a cluster of un-killable, self-respawning fragments. +ignore: + - crashpad # chrome_crashpad_handler — the bundle's crash reporter + - shipit # Squirrel.framework auto-updater + - kiro-cli-term # Kiro's integrated-terminal shells (zsh), not the agent + - autoupdate # Sparkle's Autoupdate helper — Codex.app's ShipIt equivalent + - updater # Sparkle's Updater.app; argv[1] is the app path, so it matches + - for chrome # "Codex for Chrome" extension host, not the agent + - tmux attach # tmux clients: `attach -t ` carries the agent's name + - chatgpt codex # ChatGPT.app's embedded Codex helpers ("Codex (Renderer)", + # "Codex (Service)", Resources/codex app-server) — GUI-app + # plumbing, matched via the "ChatGPT" bundle prefix; not agents + - codex computer use # ~/.codex/computer-use desktop-automation helper app + +# Optional: per-instance labels from launchd job labels — the supervised-fleet +# counterpart of tmux_labels below. Several LaunchAgents running the same +# binary all hit one agents: entry and land as indistinguishable rows, and tmux +# never sees them. When a matched root's own launchd job label matches this +# regex, the first capture group (the whole label if no group) becomes the row +# label — churn tracking, alert transitions, and /metrics all get per-instance +# identity. tmux_labels wins when both apply. Empty/absent = off. +# +# launchd_labels: "^com\\.example\\.worker-(.+)$" # → worker-1, worker-2 + +# Optional: per-instance labels from tmux session names. A fleet of identical +# agents (e.g. several claude-code bots, one per tmux session) all hit one +# agents: entry above and land as N indistinguishable rows — their cmdlines +# can't tell them apart, because matching deliberately sees only the executable +# + first args. The tmux session each one runs in IS its identity: when a +# matched root (or an ancestor) is a tmux pane whose session name matches this +# regex, the row is labeled with the first capture group (the whole session +# name if there is no group). Sessions that don't match keep their agents: +# label, so incidental tmux use never renames rows. +# +# tmux_labels: "^bot-(.+)$" # session bot-worker1 → row label worker1 + +# Labels whose idle state is NORMAL — agents that wait for work (bots parked on +# a chat poll, gateways waiting for requests). They get no "idle" badge: badging +# the whole waiting fleet wallpapers the dashboard and trains badge-blindness. +# Same reasoning as `idle` being opt-in for alerts below. Case-insensitive +# substrings of the row label (incl. tmux-derived ones), like ignore:. +# +# idle_ok: +# - gateway +# - worker + +# GPU sampling: nvidia-smi is used automatically when present (Linux/NVIDIA). +# On Apple Silicon there is no per-process GPU API, so the GPU column is blank. + +# Optional: run a command when a badge appears on an agent — a dashboard only +# helps while someone is looking at it. The command runs through the shell with +# details in env vars: $AUM_MSG (ready-made one-liner), $AUM_LABEL, $AUM_FLAG, +# $AUM_PID, $AUM_CPU, $AUM_MEM_MB, $AUM_RESTARTS, $AUM_HOST. Fires on a flag +# APPEARING (once per transition, with a per-agent-per-flag cooldown); server +# mode only — `list` never alerts. By default only hot/churn/leak alert: for a +# fleet of agents that wait for work, idle is the NORMAL state, and alerting on +# it floods the channel every time the server restarts and re-learns the fleet. +# +# Nothing is wired by default — uncomment and point it at your own channel, then +# prove it with `agent-usage-manager test-alert` before a real badge depends on +# it. Two things worth knowing before you turn `hot` on: for inference agents +# pegged CPU IS the job, and spawn-heavy agents look like churn while working. +# +# alerts: +# command: 'terminal-notifier -title agent-usage-manager -message "$AUM_MSG"' +# cooldown: 600 # seconds, default 600 +# flags: [hot, churn, leak] # default; add idle only if you really want it +# leak_floor_mb: 1536 # only alert on leak once the absolute footprint +# # is large — long-running agents legitimately +# # ratchet RSS as their context grows diff --git a/agent_usage_manager/app.py b/agent_usage_manager/app.py index 12d057b..8078812 100644 --- a/agent_usage_manager/app.py +++ b/agent_usage_manager/app.py @@ -41,7 +41,10 @@ def _resolve_config() -> Path: cwd_cfg = Path.cwd() / "agents.yaml" if cwd_cfg.exists(): return cwd_cfg - return BASE / "agents.yaml" + local = BASE / "agents.yaml" + if local.exists(): + return local + return BASE / "agents.default.yaml" CONFIG_PATH = _resolve_config() diff --git a/pyproject.toml b/pyproject.toml index 3b1c5d9..c1aeef2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,12 +4,28 @@ build-backend = "hatchling.build" [project] name = "agent-usage-manager" -version = "0.2.5" +version = "0.2.6" description = "htop for AI agents — liveness, CPU/mem/GPU usage, and a kill switch for headless agents (openclaw, hermes, ollama, vllm, claude-code)." readme = "README.md" license = { text = "MIT" } requires-python = ">=3.9" keywords = ["ai-agents", "monitoring", "gpu", "ollama", "vllm", "llm", "observability"] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: MIT License", + "Operating System :: MacOS :: MacOS X", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: System :: Monitoring", + "Topic :: System :: Systems Administration", +] dependencies = [ "fastapi>=0.110", "uvicorn[standard]>=0.27", @@ -23,9 +39,17 @@ dev = ["pytest>=7", "httpx>=0.27"] [project.urls] Homepage = "https://github.com/minglong51/agent-usage-manager" Repository = "https://github.com/minglong51/agent-usage-manager" +Issues = "https://github.com/minglong51/agent-usage-manager/issues" +Newsletter = "https://buttondown.com/minglong51" [project.scripts] agent-usage-manager = "agent_usage_manager.cli:main" [tool.hatch.build.targets.wheel] packages = ["agent_usage_manager"] +# agents.yaml is the maintainer's live config in a source checkout, not a +# shippable default — the wheel carries agents.default.yaml instead. +exclude = ["agent_usage_manager/agents.yaml"] + +[tool.hatch.build.targets.sdist] +exclude = ["agent_usage_manager/agents.yaml"] diff --git a/uv.lock b/uv.lock index c243444..419d156 100644 --- a/uv.lock +++ b/uv.lock @@ -8,7 +8,7 @@ resolution-markers = [ [[package]] name = "agent-usage-manager" -version = "0.2.4" +version = "0.2.6" source = { editable = "." } dependencies = [ { name = "fastapi", version = "0.128.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },