From dabda02e4b6fee439cec46058d277a0c03c85c4c Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sun, 1 Nov 2026 14:28:01 +0000 Subject: [PATCH] Install contact task runners for every Fleet agent --- dev2/agents/README.md | 28 +++--- dev2/agents/agent-mail-claim | 25 +++-- dev2/agents/install-agent-runners.sh | 137 +++++++++++++++++++++++++++ dev2/agents/install-riotcoder.sh | 82 +--------------- dev2/agents/qc-agent-responder.py | 28 +++--- 5 files changed, 181 insertions(+), 119 deletions(-) create mode 100755 dev2/agents/install-agent-runners.sh diff --git a/dev2/agents/README.md b/dev2/agents/README.md index 57f0560..c1ec0bd 100644 --- a/dev2/agents/README.md +++ b/dev2/agents/README.md @@ -1,17 +1,17 @@ -# RiotCoder contact task runner +# Fleet agent contact task runners -`qc-agent-responder.py` handles QryptChat for the Linux user who owns the QryptChat session. It -keeps ordinary replies in chat mode and queues authenticated Anthony instructions for -`agent-llm riotcoder full`. The worker runs with RiotCoder's UID, writes private task state under -his home, and sends its result to the originating QryptChat conversation. +Each agent's QryptChat responder runs with that agent's Linux UID. Normal messages stay in chat +mode; authenticated instructions from Anthony enter that user's `agent-llm full` runner. +Task state stays in the agent's home and results return to the originating QryptChat conversation. -The group.call phone worker sends trusted caller requests to the same queue. `agent-mail-poll` -polls only RiotCoder's mailbox; before work, the agent checks the Forward Email DMARC verdict and -claims the message, then replies from RiotCoder's own SMTP settings. Do not place RiotCoder's mail -configuration or task state in Anthony's home. +Each agent has a separately scoped group.call worker key, private phone settings, and an email +poller running under that agent's UID. Email actions require the trusted Anthony DMARC verdict; +the shared claim directory prevents forwarded copies from triggering duplicate work. Mailbox +credentials, QryptChat sessions, model logins, and task state stay in their owner's home. -On dev2, deploy the CLI package and its production dependencies to `/opt/riotcoder-cli`, provision -`/home/riotcoder/.config/cli-tools/mail.json` with only the RiotCoder account, then run -`bash dev2/agents/install-riotcoder.sh` as root. The installer grants RiotCoder's group read -access to the deployed CLI and places the responder and mail tools in the correct homes. -The mail poller runs as UID RiotCoder. +On dev2, deploy the CLI package and its production dependencies to `/opt/riotcoder-cli`, give each +ready agent a private `agent-llm.json`, QryptChat session, model login, and one-account +`mail.json`, then run `bash dev2/agents/install-agent-runners.sh` as root. The installer grants +the non-admin Fleet agent group read access to the CLI and installs a mail timer for every ready +account. An agent missing its own credentials is reported and skipped; the installer never borrows +Anthony's or another agent's login. diff --git a/dev2/agents/agent-mail-claim b/dev2/agents/agent-mail-claim index 83b6b27..96bae06 100755 --- a/dev2/agents/agent-mail-claim +++ b/dev2/agents/agent-mail-claim @@ -1,19 +1,28 @@ #!/bin/sh -# agent-mail-claim : one email, one agent. Anthony often sends a forward to -# bot@, cha0spunk@ and riotcoder@ at once, so both mailboxes hold it. The first agent to claim the -# Message-ID owns it (mkdir is atomic); the other sees who did and leaves it. +# agent-mail-claim : one email, one agent. When mail is +# delivered to more than one agent, the shared private claim directory makes +# the first claim atomic; the other agent sees who owns it and leaves it. # exit 0 + "claimed" this agent owns it now (or already did) # exit 1 + "taken by " the other agent owns it set -e -umask 077 +umask 0007 agent="$1"; id="$2" [ -n "$agent" ] && [ -n "$id" ] || { echo "usage: agent-mail-claim " >&2; exit 2; } -dir="$HOME/.local/share/agent-mail/claims" -mkdir -p "$dir" -chmod 0700 "$dir" +if [ -n "${AGENT_MAIL_CLAIM_ROOT:-}" ]; then + dir="$AGENT_MAIL_CLAIM_ROOT" + [ -d "$dir" ] && [ -w "$dir" ] || { echo "shared mail claim directory is unavailable" >&2; exit 1; } +else + dir="$HOME/.local/share/agent-mail/claims" + mkdir -p "$dir" + chmod 0700 "$dir" +fi key=$(printf '%s' "$id" | sha256sum | cut -c1-32) if mkdir "$dir/$key" 2>/dev/null; then - printf '%s\n%s\n%s\n' "$agent" "$id" "$(date -u +%FT%TZ)" > "$dir/$key/owner" + # The shared directory contains only the handle that won the atomic claim. + # Keep message IDs private in each agent's own queue and logs. + printf '%s\n' "$agent" > "$dir/$key/owner" + chmod 2770 "$dir/$key" + chmod 0640 "$dir/$key/owner" echo claimed; exit 0 fi owner=$(head -1 "$dir/$key/owner" 2>/dev/null || echo unknown) diff --git a/dev2/agents/install-agent-runners.sh b/dev2/agents/install-agent-runners.sh new file mode 100755 index 0000000..a8a5b69 --- /dev/null +++ b/dev2/agents/install-agent-runners.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ "$(id -u)" -ne 0 ]; then + echo "run this installer as root" >&2 + exit 1 +fi + +SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +AGENTS=(cha0spunk coinpay knox nora quill riotcoder rootkey) +MAIL_GROUP=fleet-agent-mail +CLI_DIR=/opt/riotcoder-cli +NODE_DIR=/home/riotcoder/.local/share/agent-phone +CLAIM_DIR=/var/lib/fleet-agent-mail/claims + +id riotcoder >/dev/null +[ -x "$NODE_DIR/node" ] || { echo "deploy group.call first to install private Node runtimes" >&2; exit 1; } +[ -d "$CLI_DIR/node_modules" ] || { echo "CLI runtime dependencies are missing at $CLI_DIR" >&2; exit 1; } +getent group "$MAIL_GROUP" >/dev/null || groupadd --system "$MAIL_GROUP" +for agent in "${AGENTS[@]}"; do + id "$agent" >/dev/null || { echo "missing Linux account: $agent" >&2; exit 1; } + groups=" $(id -nG "$agent") " + case "$groups" in *" sudo "*|*" wheel "*|*" admin "*) echo "$agent has an admin group" >&2; exit 1;; esac + usermod -a -G "$MAIL_GROUP" "$agent" +done + +# Root owns the deployed code. Only Fleet agent accounts need to read it. +chgrp -R "$MAIL_GROUP" "$CLI_DIR" +chmod -R o-rwx "$CLI_DIR" +chmod -R g+rX "$CLI_DIR" +install -d -o root -g "$MAIL_GROUP" -m 2770 /var/lib/fleet-agent-mail "$CLAIM_DIR" +install -o root -g root -m 0755 "$SOURCE_DIR/qc-agent-responder.py" /usr/local/libexec/qc-agent-responder + +ready=() +skipped=() +for agent in "${AGENTS[@]}"; do + home="$(getent passwd "$agent" | cut -d: -f6)" + group="$(id -gn "$agent")" + node="$home/.local/share/agent-phone/node" + install -d -o "$agent" -g "$group" -m 0750 "$home/.config/agent-identities/$agent" + identity_source="/home/anthony/.config/agent-identities/$agent" + if [ -f "$identity_source/mail.env" ]; then + install -o "$agent" -g "$group" -m 0600 "$identity_source/mail.env" "$home/.config/agent-identities/$agent/mail.env" + rm -f "$identity_source/mail.env" + fi + if [ -f "$identity_source/mail-prompt.md" ]; then + install -o "$agent" -g "$group" -m 0640 "$identity_source/mail-prompt.md" "$home/.config/agent-identities/$agent/mail-prompt.md" + fi + if [ -f "$identity_source/AGENTS.md" ]; then + install -o "$agent" -g "$group" -m 0640 "$identity_source/AGENTS.md" "$home/.config/agent-identities/$agent/AGENTS.md" + fi + if [ -f "$identity_source/profile.json" ]; then + install -o "$agent" -g "$group" -m 0640 "$identity_source/profile.json" "$home/.config/agent-identities/$agent/profile.json" + fi + missing=() + for path in \ + "$home/.config/agent-llm.json" \ + "$home/.config/cli-tools/mail.json" \ + "$home/.codex/auth.json" \ + "$home/.config/qc/session.json" \ + "$home/.config/qc-agent/qc-passphrase" \ + "$home/.config/qc-agent/persona.md"; do + [ -f "$path" ] || missing+=("${path#"$home"/}") + done + if [ "${#missing[@]}" -gt 0 ]; then + skipped+=("$agent (${missing[*]})") + continue + fi + if [ ! -x "$node" ]; then + skipped+=("$agent (missing private Node runtime)") + continue + fi + + install -d -o "$agent" -g "$group" -m 0750 "$home/.local/bin" "$home/.local/state/agent-mail-poll" + install -o "$agent" -g "$group" -m 0750 "$SOURCE_DIR/agent-mail-trusted.py" "$home/.local/bin/agent-mail-trusted" + install -o "$agent" -g "$group" -m 0750 "$SOURCE_DIR/agent-mail-claim" "$home/.local/bin/agent-mail-claim" + + cat > "$home/.local/bin/mail.tmp" < "$home/.local/bin/agent-mail-poll.tmp" < "/etc/systemd/system/agent-mail-poll-$agent.service" < "/etc/systemd/system/agent-mail-poll-$agent.timer" </dev/null +done +printf 'Installed contact runners for: %s\n' "${ready[*]:-(none)}" +if [ "${#skipped[@]}" -gt 0 ]; then + printf 'Skipped accounts missing their own credentials: %s\n' "${skipped[*]}" +fi diff --git a/dev2/agents/install-riotcoder.sh b/dev2/agents/install-riotcoder.sh index 13cdde1..9f6b884 100755 --- a/dev2/agents/install-riotcoder.sh +++ b/dev2/agents/install-riotcoder.sh @@ -1,84 +1,4 @@ #!/usr/bin/env bash set -euo pipefail - -if [ "$(id -u)" -ne 0 ]; then - echo "run this installer as root" >&2 - exit 1 -fi - SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_DIR="$(cd "$SOURCE_DIR/../.." && pwd)" -USER_NAME=riotcoder -USER_HOME=/home/riotcoder -GROUP_NAME=riotcoder -NODE="$USER_HOME/.local/share/agent-phone/node" -CLI_DIR=/opt/riotcoder-cli - -id "$USER_NAME" >/dev/null -[ -x "$NODE" ] || { echo "RiotCoder's private Node runtime is missing; deploy group.call first" >&2; exit 1; } -[ -d "$CLI_DIR/node_modules" ] || { echo "CLI runtime dependencies are missing at $CLI_DIR" >&2; exit 1; } -[ -f "$USER_HOME/.config/cli-tools/mail.json" ] || { echo "RiotCoder's private mail configuration is missing" >&2; exit 1; } - -# The deploy checkout and dependencies are installed by root; grant RiotCoder's -# own group read/traverse access so the account can execute its CLI tools. -chgrp -R "$GROUP_NAME" "$CLI_DIR" -chmod -R g+rX "$CLI_DIR" - -install -d -o "$USER_NAME" -g "$GROUP_NAME" -m 0750 \ - "$USER_HOME/.local/bin" "$USER_HOME/.config/agent-identities/riotcoder" "$USER_HOME/.local/state/agent-mail-poll" -install -o root -g root -m 0755 "$SOURCE_DIR/qc-agent-responder.py" /usr/local/libexec/qc-agent-responder -install -o "$USER_NAME" -g "$GROUP_NAME" -m 0755 "$SOURCE_DIR/agent-mail-trusted.py" "$USER_HOME/.local/bin/agent-mail-trusted" -install -o "$USER_NAME" -g "$GROUP_NAME" -m 0755 "$SOURCE_DIR/agent-mail-claim" "$USER_HOME/.local/bin/agent-mail-claim" -install -o "$USER_NAME" -g "$GROUP_NAME" -m 0644 "$SOURCE_DIR/riotcoder-mail-prompt.md" "$USER_HOME/.config/agent-identities/riotcoder/mail-prompt.md" - -cat > "$USER_HOME/.local/bin/mail.tmp" < "$USER_HOME/.local/bin/agent-mail-poll.tmp" < /etc/systemd/system/agent-mail-poll-riotcoder.service < /etc/systemd/system/agent-mail-poll-riotcoder.timer <<'EOF' -[Unit] -Description=Poll RiotCoder's inbox every five minutes - -[Timer] -OnCalendar=*-*-* *:0/5:00 -AccuracySec=30s -Persistent=true - -[Install] -WantedBy=timers.target -EOF - -chmod 0644 /etc/systemd/system/agent-mail-poll-riotcoder.{service,timer} -systemctl daemon-reload -systemctl enable --now agent-mail-poll-riotcoder.timer >/dev/null -echo "RiotCoder QryptChat and mail task runners installed under UID $(id -u "$USER_NAME")." +exec "$SOURCE_DIR/install-agent-runners.sh" "$@" diff --git a/dev2/agents/qc-agent-responder.py b/dev2/agents/qc-agent-responder.py index 2a75db9..fc08328 100644 --- a/dev2/agents/qc-agent-responder.py +++ b/dev2/agents/qc-agent-responder.py @@ -2,7 +2,7 @@ """QryptChat responder and private task queue for Fleet agents. The responder process runs as its matching Linux user. Only authenticated -Anthony messages can create RiotCoder work; tasks run with that user's full +Anthony messages can create agent work; tasks run with that user's full agent-llm profile and report back to the originating QryptChat conversation. """ import fcntl @@ -16,13 +16,12 @@ from datetime import datetime, timezone from pathlib import Path -AGENTS = ("cha0spunk", "coinpay", "knox", "nora", "quill", "riotcoder") -TASK_AGENT = "riotcoder" +AGENTS = ("cha0spunk", "coinpay", "knox", "nora", "quill", "riotcoder", "rootkey") OPERATOR_ID = "4826dea7-225a-45df-a56f-6f380bd74ecf" HOME = Path.home() AGENT = os.environ.get("QC_AGENT", "") if AGENT not in AGENTS: - raise SystemExit("QC_AGENT must be one of the six configured identities") + raise SystemExit("QC_AGENT must be one of the configured identities") CONFIG = HOME / ".config/qc-agent" STATE = HOME / ".local/state/qc-agent" @@ -45,6 +44,7 @@ "quill": ("call", "meeting", "minutes", "notes", "decision", "action item", "transcript"), "cha0spunk": ("email", "inbox", "bot@", "customer support", "support queue"), "riotcoder": ("code", "bug", "deploy", "engineering", "server", "fleet", "build", "software", "technical"), + "rootkey": ("security", "threat", "vulnerability", "incident", "key rotation", "access control", "audit"), } CONVERSATIONS = {} @@ -105,7 +105,7 @@ def discover_chats(env): def mentioned_handles(text): low = (text or "").lower() - if "all six" in low or "all agents" in low or "everyone" in low: + if "all seven" in low or "all six" in low or "all agents" in low or "everyone" in low: return list(AGENTS) return [name for name in AGENTS if re.search(rf"(?