Collector v0 (Layer 1) #1

Merged
foreman merged 7 commits from foreman/collector-scaffold into main 2026-05-06 17:47:49 -03:00
5 changed files with 324 additions and 6 deletions
Showing only changes of commit 4ff8c3f78d - Show all commits

95
INSTALL.md Normal file
View file

@ -0,0 +1,95 @@
# INSTALL — agent-watcher Collector (Layer 1)
The MCP Watcher (Layer 2) has its own install path; this file covers Layer 1 only.
## Prerequisites
- Linux with systemd
- Go 1.22+ (`sudo apt install golang-go` on Ubuntu/Mint)
- An existing agent-ping install on this host (the Collector writes to its inbox dir)
## Install
Per CLAUDE.md rule #2, **Angus runs the install commands** — agents do not modify their own configuration.
```sh
git clone https://git.botbought.ai/foreman/agent-watcher ~/agent-watcher
cd ~/agent-watcher
./install.sh
```
The script builds, installs to `~/.local/bin/agent-watcher`, drops the systemd unit into `~/.config/systemd/user/`, copies the example config to `~/.config/agent-watcher/collector.yaml` (if absent), reloads systemd, and starts the service.
To install without auto-starting (so you can edit the config first):
```sh
./install.sh --no-start
```
## Configure
Edit `~/.config/agent-watcher/collector.yaml`. The example has both sources configured for `agent: foreman` with placeholder routes — adapt to this host. At least one of `webhook:` or `drop_folder:` must be configured.
After editing:
```sh
systemctl --user restart agent-watcher
```
## Verify
```sh
# logs
journalctl --user -u agent-watcher -f
# health
curl http://127.0.0.1:18790/health
# end-to-end via drop folder
echo '{"recipient":"bob","type":"INFO","payload":"hello from drop"}' \
> ~/Nyx/workspace/incoming/test.json
# end-to-end via webhook
curl -X POST http://127.0.0.1:18790/forgejo/push \
-H 'Content-Type: application/json' \
-d '{"repo":"agent-ping","actor":"angus"}'
# the lines should appear in the recipient's inbox
tail -2 ~/Nyx/workspace/pings/bob.inbox
```
## Survive logout
`systemctl --user` units stop when the user logs out. To keep the Collector running across logouts:
```sh
sudo loginctl enable-linger $USER
```
## Uninstall
```sh
systemctl --user disable --now agent-watcher
rm ~/.local/bin/agent-watcher
rm ~/.config/systemd/user/agent-watcher.service
systemctl --user daemon-reload
# config and example left behind; remove if desired:
# rm -rf ~/.config/agent-watcher
```
## Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| `journalctl` shows `config: ... is required` | YAML field missing | Match the example, save, restart. |
| `systemctl --user status` shows `address already in use` | port 18790 taken by something else | Edit `webhook.listen` to a free port. |
| Drop file disappears but no inbox line | check `.dead-letter/` for the file + `.reason` sidecar | Schema validation failed — fix the producer. |
| Webhook returns 404 | path doesn't match a configured route | Routes must match exactly; check trailing slashes. |
| Service won't start across reboot | `linger` not enabled | `sudo loginctl enable-linger $USER` |
| Drop file syncs back from another host before processing | drop folder is in Syncthing scope | This is intended (lets producers on other hosts deliver). Lifecycle artifacts (`.tmp`, `.dead-letter/`) are excluded by `install.sh`. |
## What this does NOT install
- The agent-ping system. Install that first (`~/agent-ping/install.sh <name>`).
- Layer 2 (MCP Watcher). Different binary, different runtime, different unit — separate install when it lands.
- Caddy / reverse-proxy webhook auth. v1 is loopback-only; v2 will document the upgrade path.

View file

@ -1,13 +1,13 @@
# agent-watcher
Push-delivery layer for [`agent-ping`](http://localhost:3300/angus/agent-ping). The "secondary nervous system" for Claude Code agents on this network.
Push-delivery layer for [`agent-ping`](https://git.botbought.ai/foreman/agent-ping). The "secondary nervous system" for Claude Code agents on this network.
`agent-ping` queues messages in inbox files; `agent-watcher` notices them (and other external events) and wakes the recipient agent without a human in the loop.
Two layers:
- **Collector** — small Go daemon, `systemd --user`, always on, brain-blind. Converts external events (HTTP webhooks, drop-folder file arrivals) into ping inbox writes. Runs whether or not any agent is alive.
- **MCP Watcher** — Claude Code MCP subprocess, declared in each agent's `mcp.json`. Watches the agent's inbox via inotify and surfaces events into the live session via Channels (research preview). Provides reply tools (`ack`, `respond`, `mark_handled`).
- **Collector** (this repo, Go) — small daemon under `systemd --user`. Always on, brain-blind. Converts external events (HTTP webhooks, drop-folder file arrivals) into ping inbox writes. Runs whether or not any agent is alive.
- **MCP Watcher** (Python, in progress) — Claude Code MCP subprocess declared in each agent's `mcp.json`. Watches the agent's inbox via inotify and surfaces events into the live session via Channels. Provides reply tools (`ack`, `respond`, `mark_handled`).
Filesystem is the queue. OpenBrain is not involved.
@ -15,10 +15,42 @@ Filesystem is the queue. OpenBrain is not involved.
[`spec/agent-watcher.md`](spec/agent-watcher.md). Read that for architecture, decisions, scope.
Channels reference docs (snapshot of Anthropic's official docs, used by Layer 2): [`docs/channels/`](docs/channels/).
## Status
v1 spec signed off by Bob (VPS) 2026-05-06. Implementation pending.
| Layer | Lane | Status |
|---|---|---|
| Spec v1 | — | Signed off by Bob 2026-05-06 |
| Layer 1: Collector | Foreman / Go | **v0 working: 43 tests passing.** End-to-end exercised; binary builds. systemd unit + INSTALL.md ready. |
| Layer 2: MCP Watcher | Bob / Python | In progress — sandbox CC session being set up on VPS for testing. |
## Install
## Install (Layer 1)
Per CLAUDE.md rule #2, **Angus runs the install commands** — agents do not modify their own configuration. Install script will land alongside `INSTALL.md` once the binaries are built.
```sh
git clone https://git.botbought.ai/foreman/agent-watcher ~/agent-watcher
cd ~/agent-watcher
./install.sh
```
Then edit `~/.config/agent-watcher/collector.yaml` and `systemctl --user restart agent-watcher`.
See [`INSTALL.md`](INSTALL.md) for verify steps, troubleshooting, and the `loginctl enable-linger` step required to keep the daemon running across logouts.
Per CLAUDE.md rule #2, **Angus runs the install commands** — agents do not modify their own configuration.
## Quick reference (Layer 1)
```
Inputs Output
───────── ──────
HTTP POST → port 18790 ┐
(routed via │ <recipient>.inbox (JSONL, ping-shaped)
YAML table) │ identical format to
├─→ what `ping <recipient> <payload>` writes;
File drop in │ the existing UserPromptSubmit hook and the
~/Nyx/workspace/incoming/ │ future MCP Watcher consume the stream
*.json ┘ without distinguishing source.
```
`/health` on the same webhook port returns `{received, emitted, errors, uptime_sec}` for journalctl correlation.

51
examples/collector.yaml Normal file
View file

@ -0,0 +1,51 @@
# agent-watcher Collector configuration
#
# Lives at: ~/.config/agent-watcher/collector.yaml
# Override with --config or AGENT_WATCHER_CONFIG.
#
# At least one source (webhook OR drop_folder) must be configured.
# This host's identity. Used in logs only; the inbox writer routes by
# the recipient field on each event, not this.
agent: foreman
# Optional. Where to write <recipient>.inbox files. Default shown.
# inbox_dir: ~/Nyx/workspace/pings
sources:
# HTTP webhook source.
# v1 binds loopback only — Caddy + bearer-token reverse-proxy is the
# v2 upgrade path for accepting webhooks from external producers.
webhook:
listen: 127.0.0.1:18790
routes:
# Path → which inbox to land in, with a Go text/template payload.
# Variables come from the request body decoded as JSON.
/forgejo/push:
recipient: bob
type: INFO
payload_template: "forgejo push to {{ .repo }} by {{ .actor }}"
# Empty-body posts work too — fixed-string templates render without
# any data.
/openrouter/billing-alert:
recipient: bob
type: NEEDS-RESPONSE
priority: urgent
payload_template: "billing alert: {{ .message }}"
# Drop-folder source.
# Watches a directory via inotify for *.json files matching the spec
# §3.1.2 schema:
# {
# "recipient": "bob",
# "type": "INFO" | "NEEDS-RESPONSE" | "ACK-REQUEST",
# "priority": "normal" | "urgent", # optional
# "payload": "...",
# "sentinel": "/path/optional" # optional
# }
# Valid → emit + delete. Invalid → moved to .dead-letter/ with reason.
drop_folder:
path: ~/Nyx/workspace/incoming/
poll_fallback_seconds: 30 # safety net if inotify misses

114
install.sh Executable file
View file

@ -0,0 +1,114 @@
#!/usr/bin/env bash
# install.sh — set up the agent-watcher Collector on this machine.
#
# Usage:
# ./install.sh # build + install + enable + start
# ./install.sh --no-start # everything but `systemctl start`
#
# What it does:
# 1. Builds the agent-watcher binary (requires Go 1.22+).
# 2. Installs to ~/.local/bin/agent-watcher.
# 3. Drops the systemd --user unit into ~/.config/systemd/user/.
# 4. Drops a starter ~/.config/agent-watcher/collector.yaml if missing
# (copies examples/collector.yaml; you edit it before first start).
# 5. Reloads systemd, enables, and (unless --no-start) starts the unit.
# 6. Adds drop-folder lifecycle patterns to the workspace .stignore so
# drop files do not Syncthing-replicate during local processing.
#
# Per CLAUDE.md rule #2, this is intended to be run by a human. The
# Collector itself, like agent-ping, never invokes this script.
#
# Layer 2 (MCP Watcher) has its own install path — different language,
# different runtime, different unit. See its README when it lands.
set -euo pipefail
NO_START=0
for arg in "$@"; do
case "$arg" in
--no-start) NO_START=1 ;;
*) echo "unknown arg: $arg" >&2; exit 2 ;;
esac
done
REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
BIN_DIR="$HOME/.local/bin"
CONF_DIR="$HOME/.config/agent-watcher"
UNIT_DIR="$HOME/.config/systemd/user"
WORKSPACE="$HOME/Nyx/workspace"
STIGNORE="$WORKSPACE/.stignore"
echo "agent-watcher install"
echo "repo: $REPO_DIR"
echo
# 1. Check Go
echo "[1/6] checking Go toolchain"
if ! command -v go >/dev/null 2>&1; then
echo " ERROR: 'go' not found. Install Go 1.22+ first." >&2
echo " e.g. sudo apt install golang-go" >&2
exit 1
fi
GO_VERSION=$(go version | awk '{print $3}')
echo " $GO_VERSION"
# 2. Build
echo "[2/6] building agent-watcher binary"
mkdir -p "$BIN_DIR"
( cd "$REPO_DIR" && go build -o "$BIN_DIR/agent-watcher" ./cmd/agent-watcher )
echo " installed: $BIN_DIR/agent-watcher"
# 3. systemd unit
echo "[3/6] installing systemd --user unit"
mkdir -p "$UNIT_DIR"
install -m 0644 "$REPO_DIR/systemd/agent-watcher.service" "$UNIT_DIR/agent-watcher.service"
echo " installed: $UNIT_DIR/agent-watcher.service"
# 4. Config skeleton
echo "[4/6] config skeleton"
mkdir -p "$CONF_DIR"
if [ -f "$CONF_DIR/collector.yaml" ]; then
echo " $CONF_DIR/collector.yaml exists — leaving as-is"
else
install -m 0644 "$REPO_DIR/examples/collector.yaml" "$CONF_DIR/collector.yaml"
echo " installed example: $CONF_DIR/collector.yaml — EDIT BEFORE START"
fi
# 5. .stignore — drop folder is replicated, but lifecycle artifacts shouldn't be
echo "[5/6] .stignore (drop-folder lifecycle artifacts are local-only)"
if [ -d "$WORKSPACE" ]; then
touch "$STIGNORE"
for pattern in "incoming/.dead-letter/" "incoming/*.json.tmp"; do
if ! grep -qxF "$pattern" "$STIGNORE"; then
echo "$pattern" >> "$STIGNORE"
echo " added: $pattern"
fi
done
else
echo " $WORKSPACE not present — skipping (Syncthing not set up here)"
fi
# 6. systemd reload + enable + (optionally) start
echo "[6/6] systemd reload + enable"
systemctl --user daemon-reload
systemctl --user enable agent-watcher.service >/dev/null
echo " enabled at user login (loginctl enable-linger required to run when logged out)"
if [ "$NO_START" -eq 0 ]; then
echo " starting…"
systemctl --user restart agent-watcher.service
sleep 1
systemctl --user status agent-watcher.service --no-pager -n 5 || true
else
echo " --no-start: not starting; run 'systemctl --user start agent-watcher' when ready"
fi
echo
echo "installation complete."
echo
echo "next steps:"
echo " 1. EDIT $CONF_DIR/collector.yaml to suit this host."
echo " 2. systemctl --user restart agent-watcher (after edits)."
echo " 3. journalctl --user -u agent-watcher -f (watch logs)."
echo " 4. curl http://127.0.0.1:18790/health (sanity check)."
echo " 5. To survive logout: loginctl enable-linger \$USER"

View file

@ -0,0 +1,26 @@
[Unit]
Description=agent-watcher Collector — converts external events to ping inbox writes
Documentation=https://git.botbought.ai/foreman/agent-watcher
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=%h/.local/bin/agent-watcher --json-log
Restart=on-failure
RestartSec=5
# small daemon; no need for elevated limits
LimitNOFILE=4096
# read-only by intent; the daemon writes only to the inbox dir which is
# inside $HOME and unaffected by ProtectSystem.
ProtectSystem=strict
ProtectHome=read-write
PrivateTmp=yes
NoNewPrivileges=yes
# stdout/stderr go to journald automatically; --json-log makes them parseable
StandardOutput=journal
StandardError=journal
SyslogIdentifier=agent-watcher
[Install]
WantedBy=default.target