From 7b1ad5ca826b00211a9e00324b55d28d09001167 Mon Sep 17 00:00:00 2001 From: "Mos (W-jarvis)" Date: Mon, 13 Jul 2026 16:02:30 -0500 Subject: [PATCH] tools: add mos-comms cross-agent git-relay package Portable bash utility for two Mos orchestrators on separate hosts to relay via a shared git branch: one-file-per-message (conflict-free), mechanical systemd-timer poll, STATIC tmux wake-injection (no untrusted data in send-keys), 3x rebase-retry send, first-poll baseline (no history dump). See tools/mos-comms/MOS-COMMS-PROTOCOL.md for the spec, threat model, and install. Co-Authored-By: Claude Opus 4.8 --- tools/mos-comms/MOS-COMMS-PROTOCOL.md | 135 ++++++++++ tools/mos-comms/mos-comms.config.example | 23 ++ tools/mos-comms/mos-comms.sh | 291 ++++++++++++++++++++++ tools/mos-comms/systemd/mos-comms.service | 6 + tools/mos-comms/systemd/mos-comms.timer | 9 + 5 files changed, 464 insertions(+) create mode 100644 tools/mos-comms/MOS-COMMS-PROTOCOL.md create mode 100644 tools/mos-comms/mos-comms.config.example create mode 100755 tools/mos-comms/mos-comms.sh create mode 100644 tools/mos-comms/systemd/mos-comms.service create mode 100644 tools/mos-comms/systemd/mos-comms.timer diff --git a/tools/mos-comms/MOS-COMMS-PROTOCOL.md b/tools/mos-comms/MOS-COMMS-PROTOCOL.md new file mode 100644 index 0000000..2904030 --- /dev/null +++ b/tools/mos-comms/MOS-COMMS-PROTOCOL.md @@ -0,0 +1,135 @@ +# Mos Cross-Agent Git Communications Protocol + +`mos-comms.sh` is a portable, append-only relay for orchestrators on separate +hosts. It uses a git repository supplied by the operator as transport. The +relay is intentionally small: Bash, git, Python 3 (for host environments that +need it), and tmux only. It does not require an LLM, service API, database, or +secret file in the repository. + +## Canonical protocol + +- Hosts share one git repository and use a dedicated branch, defaulting to + `mos-comms`. +- Every message is exactly one new file under `comms/`; existing message files + are never edited or deleted. +- A sender writes + `comms/__from-__.md` with YAML-like frontmatter: + + ```text + --- + from: host-a-mos + to: all + utc: 20260713T120000Z + --- + + Message body + ``` + +- The sender commits, rebases on the remote branch, and retries a rejected push + up to three times. Independent files make normal concurrent sends + conflict-free. +- `poll` fetches the dedicated branch and compares its commit SHA with + `STATE_DIR/last_notified_sha`. On the first run it records a baseline without + notifying historical messages. Later runs count newly changed peer files and + notify tmux only when that count is nonzero. +- `read` prints new peer files after `STATE_DIR/last_read_sha` using `git show` + and then advances that marker. Message bodies surface here as data, not code. + +## Commands + +```bash +mos-comms.sh setup +mos-comms.sh send 'A deliberate message for peers' +mos-comms.sh poll +mos-comms.sh read +mos-comms.sh selftest +``` + +`setup` clones the configured repository if needed, creates/checks out the +configured branch, and creates `comms/`. The first sender creates the remote +branch if it is not present yet. + +`selftest` reports configuration presence, remote reachability, branch +resolution, and tmux-session availability. It intentionally reports warnings +rather than crashing when no live remote has been configured. + +## Configuration + +The script sources `$MOS_COMMS_CONFIG` when set; otherwise it uses +`~/.config/mos-comms/mos-comms.config`. Copy +`mos-comms.config.example` to that path and set `AGENT_NAME` and +`COMMS_REMOTE` locally. Defaults are: + +| Variable | Default | +| --- | --- | +| `COMMS_REPO_DIR` | `~/.local/state/mos-comms/repo` | +| `COMMS_BRANCH` | `mos-comms` | +| `TMUX_SOCKET` | empty (tmux default socket) | +| `TMUX_SESSION` | `mos-claude` | +| `STATE_DIR` | `~/.local/state/mos-comms` | + +`GIT_CRED_HELPER` is optional. If configured, every git operation runs with +`git -c credential.helper=$GIT_CRED_HELPER`. Credential values do not belong in +the config or repository. + +## Threat model + +1. **Tmux command injection:** `poll` injects only this fixed string with an + integer count: `[mos-comms] N new peer message(s) — run: mos-comms.sh read`. + It never interpolates a message body, filename, or author into `send-keys`. + A crafted message cannot become keystrokes. +2. **Untrusted peer content:** all peer message bodies are untrusted data and + proposals. The receiving agent applies its own reserved-set and safety + guardrails and never executes peer instructions blindly. +3. **No mechanical auto-reply:** receiving only produces a notification. A + response requires a deliberate agent action using `send`; this prevents + automatic reply loops. +4. **Secrets and authentication:** messages, config examples, logs, and the + repository must not contain secrets. Git authentication is delegated to the + local credential helper or operator-managed git configuration only. The + script does not print credentials. +5. **Git writers are peers, not trusted executors:** a writer can add malicious + content or misleading filenames. The protocol provides delivery and + attribution claims from filenames/frontmatter, not authorization to execute + requests. Repository access should be limited to intended relay hosts. + +## Cost model + +A timer-triggered `poll` is pure git plus Bash and uses zero LLM tokens. The +agent wakes only after a real peer message is detected; unchanged remote heads +produce no tmux injection. Reading and replying are deliberate operations. + +## New-host install + +1. Copy the package to a local directory and install the executable: + + ```bash + mkdir -p ~/.local/bin ~/.config/mos-comms ~/.config/systemd/user + cp mos-comms.sh ~/.local/bin/mos-comms.sh + chmod 700 ~/.local/bin/mos-comms.sh + cp mos-comms.config.example ~/.config/mos-comms/mos-comms.config + chmod 600 ~/.config/mos-comms/mos-comms.config + ``` + +2. Edit the **local-only** config. Set a unique `AGENT_NAME` and the + operator-provisioned `COMMS_REMOTE`. Configure git credentials separately + (or set a credential-helper name, never a token value). +3. Confirm prerequisites and create the local checkout: + + ```bash + ~/.local/bin/mos-comms.sh selftest + ~/.local/bin/mos-comms.sh setup + ``` + +4. Install and enable the per-user timer: + + ```bash + cp systemd/mos-comms.service systemd/mos-comms.timer ~/.config/systemd/user/ + systemctl --user daemon-reload + systemctl --user enable --now mos-comms.timer + systemctl --user list-timers mos-comms.timer + ``` + +5. Send a deliberate test message from one host, then run `read` on the other + host after its poll notification. Keep the relay branch dedicated to this + protocol; do not share it with source-code work. diff --git a/tools/mos-comms/mos-comms.config.example b/tools/mos-comms/mos-comms.config.example new file mode 100644 index 0000000..589091b --- /dev/null +++ b/tools/mos-comms/mos-comms.config.example @@ -0,0 +1,23 @@ +# Copy to ~/.config/mos-comms/mos-comms.config (mode 600) or point +# MOS_COMMS_CONFIG at another local-only file. Do not commit this file with +# operator values or credentials. + +# Required deployment identity; letters, numbers, dots, and hyphens only. +AGENT_NAME="host-a-mos" + +# Required: URL/path of the shared, operator-provisioned git repository. +# Authentication is delegated to git or the optional credential helper below. +COMMS_REMOTE="" + +# Optional local paths and branch. +COMMS_REPO_DIR="$HOME/.local/state/mos-comms/repo" +COMMS_BRANCH="mos-comms" +STATE_DIR="$HOME/.local/state/mos-comms" + +# Empty selects tmux's default socket. Set only when a named socket is used. +TMUX_SOCKET="" +TMUX_SESSION="mos-claude" + +# Optional git credential-helper name/path. Never place credential values here. +# Example: GIT_CRED_HELPER="cache" +GIT_CRED_HELPER="" diff --git a/tools/mos-comms/mos-comms.sh b/tools/mos-comms/mos-comms.sh new file mode 100755 index 0000000..1eae3a5 --- /dev/null +++ b/tools/mos-comms/mos-comms.sh @@ -0,0 +1,291 @@ +#!/usr/bin/env bash +# Portable, append-only git relay for deliberate cross-agent communication. +set -euo pipefail + +CONFIG_PATH="${MOS_COMMS_CONFIG:-$HOME/.config/mos-comms/mos-comms.config}" +CONFIG_PRESENT=0 +if [[ -f "$CONFIG_PATH" ]]; then + # Configuration is local operator input. It must not be committed to the relay repo. + # shellcheck source=/dev/null + source "$CONFIG_PATH" + CONFIG_PRESENT=1 +fi + +AGENT_NAME="${AGENT_NAME:-}" +COMMS_REMOTE="${COMMS_REMOTE:-}" +COMMS_REPO_DIR="${COMMS_REPO_DIR:-$HOME/.local/state/mos-comms/repo}" +COMMS_BRANCH="${COMMS_BRANCH:-mos-comms}" +TMUX_SOCKET="${TMUX_SOCKET:-}" +TMUX_SESSION="${TMUX_SESSION:-mos-claude}" +STATE_DIR="${STATE_DIR:-$HOME/.local/state/mos-comms}" +GIT_CRED_HELPER="${GIT_CRED_HELPER:-}" + +usage() { + cat <<'EOF' +Usage: mos-comms.sh setup | poll | read | send | selftest + +Messages are append-only files on the configured shared git branch. `poll` is +mechanical: it only checks git and emits a fixed tmux notification. +EOF +} + +die() { + printf 'mos-comms: %s\n' "$*" >&2 + exit 1 +} + +warn() { + printf 'mos-comms: warning: %s\n' "$*" >&2 +} + +gitc() { + if [[ -n "$GIT_CRED_HELPER" ]]; then + git -c credential.helper="$GIT_CRED_HELPER" "$@" + else + git "$@" + fi +} + +require_identity() { + [[ -n "$AGENT_NAME" ]] || die 'AGENT_NAME is required in the local config' + [[ "$AGENT_NAME" =~ ^[A-Za-z0-9.-]+$ ]] || die 'AGENT_NAME may contain only letters, numbers, dots, and hyphens' +} + +require_remote() { + [[ -n "$COMMS_REMOTE" ]] || die 'COMMS_REMOTE is required in the local config' +} + +ensure_state_dir() { + mkdir -p "$STATE_DIR" + chmod 700 "$STATE_DIR" +} + +remote_branch_exists() { + gitc -C "$COMMS_REPO_DIR" show-ref --verify --quiet "refs/remotes/origin/$COMMS_BRANCH" +} + +setup() { + require_remote + require_identity + mkdir -p "$(dirname "$COMMS_REPO_DIR")" + + if [[ ! -d "$COMMS_REPO_DIR/.git" ]]; then + gitc clone "$COMMS_REMOTE" "$COMMS_REPO_DIR" + fi + + gitc -C "$COMMS_REPO_DIR" remote set-url origin "$COMMS_REMOTE" + gitc -C "$COMMS_REPO_DIR" fetch origin + if remote_branch_exists; then + # Preserve any local append-only commit that is awaiting a retry; do not + # reset a checked-out branch to origin here. + if gitc -C "$COMMS_REPO_DIR" show-ref --verify --quiet "refs/heads/$COMMS_BRANCH"; then + gitc -C "$COMMS_REPO_DIR" checkout "$COMMS_BRANCH" + else + gitc -C "$COMMS_REPO_DIR" checkout --track -b "$COMMS_BRANCH" "origin/$COMMS_BRANCH" + fi + gitc -C "$COMMS_REPO_DIR" branch --set-upstream-to="origin/$COMMS_BRANCH" "$COMMS_BRANCH" + else + gitc -C "$COMMS_REPO_DIR" checkout -B "$COMMS_BRANCH" + fi + + mkdir -p "$COMMS_REPO_DIR/comms" + ensure_state_dir +} + +ensure_remote_branch() { + if ! remote_branch_exists; then + warn "origin branch '$COMMS_BRANCH' does not exist yet" + return 1 + fi +} + +message_is_from_self() { + local file="$1" + [[ "$file" == *"__from-${AGENT_NAME}__"* ]] +} + +changed_message_files() { + local from_sha="$1" + local to_sha="$2" + if [[ -n "$from_sha" ]] && gitc -C "$COMMS_REPO_DIR" cat-file -e "$from_sha^{commit}" 2>/dev/null; then + gitc -C "$COMMS_REPO_DIR" diff --name-only "$from_sha" "$to_sha" -- comms/ + else + gitc -C "$COMMS_REPO_DIR" ls-tree -r --name-only "$to_sha" -- comms/ + fi +} + +tmuxc() { + if [[ -n "$TMUX_SOCKET" ]]; then + tmux -L "$TMUX_SOCKET" "$@" + else + tmux "$@" + fi +} + +send() { + require_identity + require_remote + [[ $# -ge 1 ]] || die 'send requires message text' + setup + + local body="$*" + local utc filename path + utc="$(date -u +%Y%m%dT%H%M%SZ)" + filename="${utc}__from-${AGENT_NAME}__${RANDOM}${RANDOM}.md" + path="$COMMS_REPO_DIR/comms/$filename" + umask 077 + { + printf '%s\n' '---' + printf 'from: %s\n' "$AGENT_NAME" + printf '%s\n' 'to: all' + printf 'utc: %s\n' "$utc" + printf '%s\n\n' '---' + printf '%s\n' "$body" + } >"$path" + + gitc -C "$COMMS_REPO_DIR" add -- "comms/$filename" + if ! gitc -C "$COMMS_REPO_DIR" config user.name >/dev/null; then + gitc -C "$COMMS_REPO_DIR" config user.name "$AGENT_NAME mos-comms" + fi + if ! gitc -C "$COMMS_REPO_DIR" config user.email >/dev/null; then + gitc -C "$COMMS_REPO_DIR" config user.email "$AGENT_NAME@localhost" + fi + gitc -C "$COMMS_REPO_DIR" commit -m "comms: ${AGENT_NAME} ${utc}" + + local attempt + for attempt in 1 2 3; do + if remote_branch_exists; then + gitc -C "$COMMS_REPO_DIR" pull --rebase origin "$COMMS_BRANCH" + fi + if gitc -C "$COMMS_REPO_DIR" push origin "HEAD:refs/heads/$COMMS_BRANCH"; then + printf 'sent: %s\n' "$filename" + return 0 + fi + if [[ "$attempt" -lt 3 ]]; then + warn "push rejected; retrying ($attempt/3)" + gitc -C "$COMMS_REPO_DIR" fetch origin + fi + done + die 'push was rejected after 3 attempts; local message commit remains available for retry' +} + +poll() { + require_identity + require_remote + setup + gitc -C "$COMMS_REPO_DIR" fetch origin + ensure_remote_branch || return 0 + + local head marker last_notified + head="$(gitc -C "$COMMS_REPO_DIR" rev-parse "origin/$COMMS_BRANCH")" + marker="$STATE_DIR/last_notified_sha" + if [[ ! -f "$marker" ]]; then + printf '%s\n' "$head" >"$marker" + printf 'poll: baseline recorded; existing history was not notified\n' + return 0 + fi + last_notified="$(<"$marker")" + if [[ "$last_notified" == "$head" ]]; then + return 0 + fi + + local file count=0 + while IFS= read -r file; do + [[ -n "$file" ]] || continue + if ! message_is_from_self "$file"; then + ((count += 1)) + fi + done < <(changed_message_files "$last_notified" "$head") + + if ((count > 0)); then + # Intentionally static except for an integer: never inject peer data into tmux. + tmuxc send-keys -t "$TMUX_SESSION" "[mos-comms] $count new peer message(s) — run: mos-comms.sh read" + tmuxc send-keys -t "$TMUX_SESSION" Enter + fi + printf '%s\n' "$head" >"$marker" +} + +read_messages() { + require_identity + require_remote + setup + gitc -C "$COMMS_REPO_DIR" fetch origin + ensure_remote_branch || return 0 + + local head marker last_read file found=0 + head="$(gitc -C "$COMMS_REPO_DIR" rev-parse "origin/$COMMS_BRANCH")" + marker="$STATE_DIR/last_read_sha" + last_read="" + if [[ -f "$marker" ]]; then + last_read="$(<"$marker")" + fi + + while IFS= read -r file; do + [[ -n "$file" ]] || continue + if message_is_from_self "$file"; then + continue + fi + found=1 + printf '%s\n' "===== $file =====" + # Peer content is printed as data. It is never evaluated or sent to tmux. + gitc -C "$COMMS_REPO_DIR" show "origin/$COMMS_BRANCH:$file" + printf '\n' + done < <(changed_message_files "$last_read" "$head") + + printf '%s\n' "$head" >"$marker" + if ((found == 0)); then + printf 'No new peer messages.\n' + fi +} + +selftest() { + local remote_ok=0 branch_ok=0 tmux_ok=0 config_ok=0 + if ((CONFIG_PRESENT == 1)); then + printf 'PASS config present: %s\n' "$CONFIG_PATH" + config_ok=1 + else + printf 'FAIL config present: %s\n' "$CONFIG_PATH" + fi + + if [[ -z "$COMMS_REMOTE" ]]; then + printf 'WARN remote reachable: COMMS_REMOTE is not configured\n' + elif gitc ls-remote "$COMMS_REMOTE" >/dev/null 2>&1; then + printf 'PASS remote reachable\n' + remote_ok=1 + else + printf 'FAIL remote reachable\n' + fi + + if ((remote_ok == 1)) && gitc ls-remote --exit-code --heads "$COMMS_REMOTE" "$COMMS_BRANCH" >/dev/null 2>&1; then + printf 'PASS branch resolves: %s\n' "$COMMS_BRANCH" + branch_ok=1 + elif [[ -z "$COMMS_REMOTE" ]]; then + printf 'WARN branch resolves: skipped without a remote\n' + else + printf 'FAIL branch resolves: %s\n' "$COMMS_BRANCH" + fi + + if tmuxc has-session -t "$TMUX_SESSION" 2>/dev/null; then + printf 'PASS tmux session exists: %s\n' "$TMUX_SESSION" + tmux_ok=1 + else + printf 'FAIL tmux session exists: %s\n' "$TMUX_SESSION" + fi + + # Report all checks without making a no-remote first-run unusable. + if ((config_ok == 1 && remote_ok == 1 && branch_ok == 1 && tmux_ok == 1)); then + printf 'PASS selftest complete\n' + else + printf 'FAIL selftest incomplete (see checks above)\n' + fi +} + +case "${1:-}" in + setup) [[ $# -eq 1 ]] || usage; setup ;; + poll) [[ $# -eq 1 ]] || usage; poll ;; + read) [[ $# -eq 1 ]] || usage; read_messages ;; + send) shift; send "$@" ;; + selftest) [[ $# -eq 1 ]] || usage; selftest ;; + -h|--help|help) usage ;; + *) usage; exit 2 ;; +esac diff --git a/tools/mos-comms/systemd/mos-comms.service b/tools/mos-comms/systemd/mos-comms.service new file mode 100644 index 0000000..30f60f8 --- /dev/null +++ b/tools/mos-comms/systemd/mos-comms.service @@ -0,0 +1,6 @@ +[Unit] +Description=Mos cross-host git communications poll + +[Service] +Type=oneshot +ExecStart=%h/.local/bin/mos-comms.sh poll diff --git a/tools/mos-comms/systemd/mos-comms.timer b/tools/mos-comms/systemd/mos-comms.timer new file mode 100644 index 0000000..e0a31a3 --- /dev/null +++ b/tools/mos-comms/systemd/mos-comms.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Poll Mos cross-host git communications + +[Timer] +OnBootSec=2min +OnUnitActiveSec=15min + +[Install] +WantedBy=timers.target