Setup & installation
What this seed does
This seed lets you hook up a Hermes AI agent to a real iMessage/SMS conversation through Plow Chat. Once set up, anything a person sends you over iMessage can be routed to Hermes, and Hermes' replies get sent back as text messages — no app, no extra interface, just a regular chat thread.
Installation is designed to be as simple as possible: you run one shell script to drop the plugin files into place, run a second script that walks you through a one-time phone verification (text a code from iMessage to confirm your number), and then start the container. No Python, no git, no Hermes command-line tools are needed on the host machine — just curl and Docker.
The seed handles the full lifecycle: activating and verifying the Plow chat, securely storing credentials in a local .env file (never committed), keeping the WebSocket connection alive, and gracefully recovering from dropped connections. It also supports multiple named profiles so you can connect several different phone identities or chat channels to the same Hermes instance.
When to use it
- I want my Hermes AI agent to respond to iMessage texts automatically, without building a custom app.
- I'm setting up a personal AI assistant that I can reach from my iPhone just by sending a text.
- I want to connect a team-listener profile and a personal profile to the same Hermes instance, each with its own Plow chat credentials.
- I'm running automated tests in a CI environment and need to skip the phone-verification step using test-mode credentials.
- The activation code expired before I could text it in and I need a clear error message and an exact command to get a fresh one.
›View raw SEED.md
# Purpose
> See [[README#Purpose]].
## Normative Language
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in RFC 2119.
`Implementation-defined` means the behavior is part of the implementation contract; this specification does not prescribe a single policy.
Sub-folder SEEDs in this tree inherit the RFC 2119 declaration. They MUST NOT re-declare it.
## Dependencies
### API
- The Plow Chat API surface — endpoints, headers, WebSocket frame types, auth model, chat creation, and verification semantics — is defined by the external SEED at `https://github.com/plow-pbc/seed-plow-chat`. A consumer MUST read that SEED first; this SEED depends on its `## Objects` and `## Actions` and does not restate them. ^dep-plow-chat-seed
### Runtime
- Hermes Agent MUST run in the Docker-backed `seed-hermes` shape: a host `compose.yaml`, whole `./data:/opt/data` mount, and `HERMES_HOME=/opt/data` inside the container. ^dep-hermes-docker
- Hermes' container runtime MUST have gateway/plugin support and Python `aiohttp`; the official Hermes image supplies the runtime dependencies for this plugin. ^dep-container-runtime
- The host setup path MUST NOT require host `hermes`, host Python, git, `hermes plugins install`, host GitHub token environment variables, or container-side network/plugin installation. ^dep-host-minimal
- The host setup path MAY use `curl` and standard shell tools. `gh` MAY be used by a higher-level seed as an accelerator, but it is not required by this SEED. ^dep-curl
## Objects
The named entities that exist on the Hermes side. For Plow Chat entities (chats, lines, members, messages, WebSocket frames), see [[https://github.com/plow-pbc/seed-plow-chat/blob/main/SEED.md#Objects]].
### plow_chat Hermes platform
- A Hermes gateway platform named `plow_chat`. ^obj-platform
- It maps a Plow chat to a Hermes session source with `platform='plow_chat'`, `chat_id=<chat.uid>`, `chat_type='dm'`, and member sender metadata as the user identity.
- It sends plain text responses through Plow's message creation endpoint.
- It receives user messages by minting short-lived WebSocket tickets and subscribing to `wss://api.plow.co/v1/ws?ticket=<ticket>`.
### Direct-mounted plugin
- The plugin MUST be placed on the host at `<seed-hermes-scaffold>/data/plugins/plow-chat-platform/`, which the Hermes container sees as `/opt/data/plugins/plow-chat-platform/`. ^obj-direct-plugin
- The mounted plugin directory MUST include `plugin.yaml`, `__init__.py`, and `ref/hermes-plugin/plow_chat/adapter.py`, preserving that layout. ^obj-required-files
- The root `__init__.py` MUST load and re-export `register(ctx)` from `ref/hermes-plugin/plow_chat/adapter.py`; if that adapter file is missing, it MUST raise `ImportError` at boot. ^obj-root-entrypoint
- Hermes config MUST enable the manifest name `plow-chat-platform` in `plugins.enabled`; the registered platform name remains `plow_chat`. ^obj-plugin-enable
### Host orchestration scripts
- `ref/scripts/install_direct_mount.sh` is the canonical curl/shell installer for this gateway. It targets a seed-hermes scaffold with `--scaffold <dir>` (default `./hermes-agent`) or an explicit `--data-dir <dir>`, places the required plugin file set under `data/plugins/plow-chat-platform/`, enables `plow-chat-platform` in `data/config.yaml`, and supports `PLOW_CHAT_PLUGIN_LOCAL_DIR` and `PLOW_CHAT_PLUGIN_REF` source overrides. ^obj-direct-install-script
- `ref/scripts/create_plow_chat_curl.sh` is a curl/shell helper that targets the same scaffold with `--scaffold <dir>`, `--profile <name>`, or `--data-dir <dir>`, starts Plow activation with `provision_chat=true`, prints the one-time activation message, writes `PLOW_CHAT_*` to the target profile's `.env`, and polls activation redeem until verified or timeout. ^obj-curl-chat-script
- The helper MUST resolve its target data dir as follows, highest precedence first: an explicit `--data-dir <dir>`; else `--profile <name>` (or `PLOW_CHAT_PROFILE`) resolving to `<scaffold>/data/profiles/<name>`; else the scaffold's `data/`. This MUST match the per-profile `.env` files that downstream install verification reads. ^obj-curl-profile-resolution
## Actions
The verbs performed by the Hermes-side objects. For Plow Chat actions (chat is created, chat is verified, message is sent, WebSocket subscription is opened, message is received), see [[https://github.com/plow-pbc/seed-plow-chat/blob/main/SEED.md#Actions]].
### plow_chat is installed by direct mount
- A host agent MUST run `ref/scripts/install_direct_mount.sh --scaffold <seed-hermes-scaffold>` to place the plugin files directly into `data/plugins/plow-chat-platform/`; it MUST NOT call `hermes plugins install`, `hermes plugins enable`, `git clone`, or any Python installer on the host. ^act-direct-mount
- A host agent MUST ensure `<seed-hermes-scaffold>/data/config.yaml` exists before first `docker compose up` and contains `plugins.enabled` with `plow-chat-platform`. ^act-enable-config
- A host agent SHOULD preserve any existing `config.yaml` values while adding this plugin. If no config exists, it MAY write the minimal plugin/terminal skeleton shown in the README. ^act-config-preserve
- A host agent MAY set `PLOW_CHAT_PLUGIN_LOCAL_DIR` to copy plugin files from a local checkout, or `PLOW_CHAT_PLUGIN_REF` to fetch a specific branch/SHA from GitHub raw URLs. ^act-source-overrides
- The direct-mount installer MUST NOT start the Hermes container. The host sequence is: install the plugin files, run `create_plow_chat_curl.sh` until verified, then start `docker compose up` once with `data/.env` populated. ^act-boot-sequence
### plow_chat is configured with Plow chat credentials
- A host agent MUST run `ref/scripts/create_plow_chat_curl.sh --scaffold <seed-hermes-scaffold>` (optionally with `--profile <name>`) before first Hermes boot for this gateway and write these values into the resolved profile `.env` (`data/.env`, or `data/profiles/<name>/.env`): `PLOW_CHAT_BASE_URL`, `PLOW_CHAT_CHAT_UID`, `PLOW_CHAT_TOKEN`, and `PLOW_CHAT_HOME_CHANNEL`. ^act-write-env
- On successful verification the helper MUST print a confirmation that names the profile and the exact env file written, e.g. `Profile <name> activated. Wrote PLOW_CHAT_CHAT_UID + PLOW_CHAT_TOKEN to <path>.`, so an operator can confirm Phase 4 succeeded without inspecting the env file. ^act-success-message
- Before writing, the helper MUST (re-)ensure the target data dir is writable, because seed-hermes prepare.sh and the running container can churn ownership/mode on the bind-mounted `data/` tree. If the env file cannot be written, the helper MUST exit non-zero with an actionable error (the unwritable path plus a remediation command); it MUST NOT silently skip the write. ^act-write-permissions
- The session token MUST be configured through `PLOW_CHAT_TOKEN`; it MUST NOT be committed, printed in logs, or placed in `config.yaml`. The shortcut activation returns a user-wide Bearer token that can read user identity surfaces such as `/v1/auth/owner-identity` and `/v1/me/channels`; treat it as a user credential, not a per-chat secret. `data/.env` MUST be mode `600` where the host filesystem supports it. ^act-secret-env
- When `PLOW_CHAT_CHAT_UID` and `PLOW_CHAT_TOKEN` are present, the plugin's `env_enablement_fn` SHOULD auto-enable `gateway.platforms.plow_chat` and set its home channel to the Plow chat uid. ^act-env-auto-enable
### Host creates and polls Plow chat with curl
- The host flow MUST call `POST /v1/auth/activate` with `provision_chat=true`, capture `activation_secret`, and surface `Plow Activate: <display_code>` plus `send_to` to the human. ^act-curl-create
- The host flow MUST poll `POST /v1/auth/activate/redeem` with `{"activation_secret":"..."}` until redeem returns `status:"verified"` or a local timeout expires. The poll MUST capture the HTTP status without aborting on non-2xx, so an expired or gone activation does not surface as a raw `curl` transport error. ^act-curl-poll
- When redeem returns HTTP 410 (the activation code expired), the helper MUST print a human-readable explanation and the exact command to re-run for a fresh code, then exit non-zero. It MUST NOT surface only the raw `curl: (22) ... 410` error. ^act-curl-expiry
- The host flow MUST write the verified redeem `token` and embedded `chat.uid` into the scaffold env as `PLOW_CHAT_TOKEN` and `PLOW_CHAT_CHAT_UID`.
- The host flow MUST write `data/.activation.json` with mode `600` for audit: activation metadata with `activation_secret` redacted, token last four only, chat uid, line/send-to metadata, and owner/channel snapshots fetched with the verified Bearer token. ^act-activation-audit
- The timeout path MUST tell the operator that the activation may not have arrived or the code may have expired, and SHOULD print the exact command to re-run; recovery is to start activation again. ^act-timeout
- Phase 4 verification depends on an external human texting the activation code from the target iPhone, which cannot complete in a headless DinD/CI environment. The helper MAY provide a `--test-mode` that, given operator-supplied `--test-chat-uid`/`--test-token` (or `PLOW_CHAT_TEST_CHAT_UID`/`PLOW_CHAT_TEST_TOKEN`), skips the phone-bind dance and writes those values to the profile `.env`. Test mode MUST NOT contact Plow, MUST record `status:"test-mode"` in the audit file, and is for test validation only — not for real operator activation. ^act-test-mode
### plow_chat sends a Hermes response
- The platform adapter implements the [[https://github.com/plow-pbc/seed-plow-chat/blob/main/SEED.md#Actions]] "Message is sent" action against the configured chat. ^act-send
- The adapter SHOULD strip or flatten rich markdown because the current backing channel is iMessage/SMS-style chat.
- The adapter MUST treat 409 `chat_not_ready` as a setup/verification problem, not a successful send.
### plow_chat receives a user message
- The adapter implements the [[https://github.com/plow-pbc/seed-plow-chat/blob/main/SEED.md#Actions]] "WebSocket subscription is opened" and "Message is received" actions. ^act-receive
- For inbound frames, the adapter constructs a Hermes `MessageEvent` with the message body, Plow message uid, chat uid, and member sender uid/name.
- Before dispatching an inbound member message into Hermes, the adapter SHOULD best-effort approve that member uid in Hermes' `plow_chat` pairing store so the verified Plow participant does not hit a second generic DM pairing flow. ^act-auto-pair
- On disconnect, the adapter SHOULD reconnect by minting a fresh ticket and SHOULD backfill missed messages with `GET /v1/chats/{chat_uid}/messages`.
### plow_chat handles status and activation frames
- The adapter MAY log `message_status_updated` frames for outbound delivery transitions. ^act-status
- If Plow emits `chat_active` while the adapter is connected, the adapter SHOULD send exactly one setup-success welcome message through the normal Plow message endpoint. ^act-activation-welcome
- The adapter SHOULD surface `chat_activation_failed` as a fatal setup error, because recovery is delete and recreate.
- The adapter SHOULD mark the platform connected only after the WebSocket's initial `connected` frame.
## Verify
1. **Direct file-set check.** Run `PLOW_CHAT_PLUGIN_LOCAL_DIR=. ref/scripts/install_direct_mount.sh --scaffold "$(mktemp -d)/hermes-agent"`. Does it create `data/plugins/plow-chat-platform/plugin.yaml`, `data/plugins/plow-chat-platform/__init__.py`, and `data/plugins/plow-chat-platform/ref/hermes-plugin/plow_chat/adapter.py`? Expected: yes. ^v-direct-files
2. **Config enablement check.** Inspect the generated `config.yaml`. Does `plugins.enabled` include `plow-chat-platform` before first boot? Expected: yes. ^v-config-enabled
3. **Hermes adapter-shape check.** Inspect `ref/hermes-plugin/plow_chat/adapter.py`. Does it define a `PlowChatAdapter` implementing `connect`, `disconnect`, and `send`, and a `register(ctx)` function that registers platform name `plow_chat`? Expected: yes. ^v-adapter-shape
4. **Host shell syntax check.** Run `bash -n ref/scripts/install_direct_mount.sh ref/scripts/create_plow_chat_curl.sh`. Does it exit 0? Expected: yes. ^v-shell-syntax
5. **Secret hygiene check.** Search committed files. Do they avoid literal-looking session tokens and one-time activation codes? Expected: yes. ^v-secret-hygiene
6. **Container plugin-load check.** Prepare `./data` with the direct file set, `plugins.enabled: [plow-chat-platform]`, and dummy or real `PLOW_CHAT_*`; run `docker compose up`. Do logs show platform `plow_chat` registered and no `ImportError` from the plugin root? Expected: yes. ^v-container-load
7. **Optional live chat check.** Run `ref/scripts/create_plow_chat_curl.sh --scaffold ./hermes-agent`, text the printed activation message from iMessage to the printed phone number, and let the script poll before starting Hermes. Does it report `verified`, write `PLOW_CHAT_TOKEN`, `PLOW_CHAT_CHAT_UID`, and a redacted `data/.activation.json`, and does a normal iMessage reply reach Hermes after first container start? Expected: yes. ^v-live-chat
## Open
- The sample plugin does not implement media attachments, streaming draft edits, delete-message, or native button UI for clarify prompts. ^o-media
- The sample plugin does not yet include production-grade reconnect backfill cursor persistence; it documents the expected behavior and includes a simple reconnect loop. ^o-backfill
- The Plow Chat API is pre-1.0 and may change without backwards compatibility guarantees; see [[seed-plow-chat#Open]]. ^o-api-stability
## Non-Goals
- This SEED does not document the Plow Chat API; see [[seed-plow-chat]]. ^ng-api
- This SEED does not store or publish session tokens, activation codes, phone numbers, or provider identities in committed files.
- This SEED does not require modifying Hermes core; a plugin-based adapter is the preferred shape when supported.
- This SEED does not install or enable Hermes plugins through the Hermes CLI on the target path.
›View raw README.md
# Hermes Plow Chat Platform Adapter SEED
## Purpose
This SEED provides the Hermes gateway platform plugin named `plow_chat`. It
wires Hermes to the [Plow Chat](https://github.com/plow-pbc/seed-plow-chat)
API: Hermes sends replies through `POST /v1/chats/{chat_uid}/messages` and
receives user replies from Plow's WebSocket stream.
The target install model is direct mount into a seed-hermes Docker scaffold.
The host places this repository's plugin files under
`./hermes-agent/data/plugins/plow-chat-platform/`, pre-enables the manifest name
in `./hermes-agent/data/config.yaml`, writes `PLOW_CHAT_*` to
`./hermes-agent/data/.env`, and then starts the Hermes container. The host does
not run `hermes plugins install`, clone git repositories, or depend on Python.
## Required plugin files
The mounted plugin directory must contain this exact file set:
```text
hermes-agent/data/plugins/plow-chat-platform/
plugin.yaml
__init__.py
ref/hermes-plugin/plow_chat/adapter.py
```
`plugin.yaml` is the manifest Hermes discovers. `__init__.py` loads
`ref/hermes-plugin/plow_chat/adapter.py` and raises `ImportError` during boot if
the adapter is missing, so preserving that layout is required.
## Direct-mount install
From the parent folder that contains the seed-hermes scaffold at
`./hermes-agent/`:
```bash
curl -fsSL https://raw.githubusercontent.com/plow-pbc/seed-hermes-plow-chat/main/ref/scripts/install_direct_mount.sh \
-o /tmp/install_plow_chat.sh
bash /tmp/install_plow_chat.sh --scaffold ./hermes-agent
```
Pin a published source with `PLOW_CHAT_PLUGIN_REF=<branch-or-sha>`. When running
from a local checkout, avoid network fetches by copying from the checkout:
```bash
PLOW_CHAT_PLUGIN_LOCAL_DIR=. ref/scripts/install_direct_mount.sh --scaffold ./hermes-agent
```
The resulting `hermes-agent/data/config.yaml` must include the manifest name:
```yaml
plugins:
enabled:
- plow-chat-platform
disabled: []
terminal:
cwd: /opt/data/workspace
```
## Create and verify a Plow chat
Use the curl-only host helper to start Plow activation with `provision_chat=true`,
capture the returned session token and chat uid after verification, and write
`PLOW_CHAT_*` to the target profile's `.env` before first container boot:
```bash
# Default profile (writes hermes-agent/data/.env):
ref/scripts/create_plow_chat_curl.sh --scaffold ./hermes-agent
```
### Per-profile activation
A multi-profile install (e.g. an owner profile `daniel` plus a team-listener
profile `daniel-team`) keeps each profile's credentials in its own env file
under `data/profiles/<name>/.env`. Pass `--profile <name>` and the helper
resolves the target automatically:
```bash
ref/scripts/create_plow_chat_curl.sh --scaffold ./hermes-agent --profile daniel
ref/scripts/create_plow_chat_curl.sh --scaffold ./hermes-agent --profile daniel-team
```
`--profile <name>` is equivalent to `--data-dir ./hermes-agent/data/profiles/<name>`;
use `--data-dir` directly when the profile lives outside the scaffold's
`data/profiles/` tree. Run the helper once per profile before first boot.
The script asks Plow to assign an available line and prints the instruction the
user needs: `Text Plow Activate: ABCDE from iMessage to +1...`. It does not
print the session token. It also writes a redacted `.activation.json` audit
file (in the same profile dir) with the activation secret removed and only the
token last four retained. Start Hermes with `docker compose up` after the
script writes `.env`, so the container boots once with the Plow Chat platform
enabled.
### Confirming activation succeeded
On success the helper prints a verification line naming the profile and the
exact env file it wrote, so you can confirm Phase 4 worked without opening the
file by hand:
```text
Verified: chat is active.
Chat uid: cht_...
Profile daniel activated. Wrote PLOW_CHAT_CHAT_UID + PLOW_CHAT_TOKEN to ./hermes-agent/data/profiles/daniel/.env.
Wrote redacted activation audit to ./hermes-agent/data/profiles/daniel/.activation.json
```
The resulting profile `.env` contains these values (mode `600`):
```bash
PLOW_CHAT_BASE_URL=https://api.plow.co
PLOW_CHAT_CHAT_UID=cht_<opaque-chat-id> # the provisioned Plow chat uid
PLOW_CHAT_TOKEN=<opaque-bearer-token> # user Bearer credential — never commit/log
PLOW_CHAT_HOME_CHANNEL=cht_<opaque-chat-id> # same value as PLOW_CHAT_CHAT_UID
```
### If the activation code expires
The displayed code is single-use and time-limited. If it expires before the
text arrives, Plow's redeem endpoint returns HTTP 410; the helper detects this,
prints `Activation code expired.` plus the exact command to re-run for a fresh
code, and exits non-zero (75) instead of surfacing a raw `curl: (22)` error.
Just run the same command again.
### Non-interactive test mode (testing/CI only)
Phase 4 normally requires a human texting the activation code from the target
iPhone, which cannot complete in a headless DinD/CI environment. For test
validation only, `--test-mode` skips the phone-bind dance and writes
operator-supplied credentials straight to the profile `.env`:
```bash
ref/scripts/create_plow_chat_curl.sh --scaffold ./hermes-agent --profile daniel \
--test-mode --test-chat-uid cht_known --test-token tok_known
```
This is **not** a real activation — it never contacts Plow and the audit file
records `"status": "test-mode"`. Never use it for a real operator install.
The host poll uses:
```bash
curl -sSL \
-H 'Content-Type: application/json' \
-d '{"activation_secret":"<secret>"}' \
"https://api.plow.co/v1/auth/activate/redeem"
```
If the adapter is connected when Plow emits `chat_active`, it sends exactly one
welcome message from Hermes through the normal Plow message endpoint. Set
`PLOW_CHAT_WELCOME_MESSAGE` to customize it or
`PLOW_CHAT_AUTO_WELCOME=false` to disable it.
## Runtime behavior
- `PLOW_CHAT_CHAT_UID` is the single Plow chat handled by this plugin instance.
- `PLOW_CHAT_TOKEN` stays in the profile's `.env` (scaffold `data/.env`, or
`data/profiles/<name>/.env` for a named profile); do not commit it or log it.
- The activation Bearer token is a user credential, not just a chat secret.
Keep the profile `.env` and `.activation.json` mode `600`.
- The adapter sends the welcome on the first `chat_active` frame it sees.
- Inbound WebSocket frames with `direction=outbound` are ignored so Hermes does
not answer itself.
- The adapter best-effort approves verified Plow member ids in Hermes'
`plow_chat` pairing store so the first inbound message reaches Hermes.
- Rich Markdown is flattened to plain text because the backing channel is
iMessage/SMS-style.
## License
MIT.
Version history
1 releaseInitial Seed Release.
Dependencies
6 required · 0 optional- Seedseed-plow-chatrequired· seed ›· Declared in SEED.md Dependencies/API section as the authoritative source for the Plow Chat API surface, objects, and actions.
- Seedseed-hermesrequired· seed ›· Declared in SEED.md: Hermes Agent MUST run in the Docker-backed seed-hermes shape (host compose.yaml, ./data:/opt/data mount, HERMES_HOME=/opt/data).
- SWDocker + Composerequired· link ›· Required by the seed-hermes scaffold shape; the plugin is delivered as a bind-mount into the Hermes container via docker compose up.
- APIPlow Chat APIrequired· link ›· Runtime dependency: the adapter calls POST /v1/chats/{chat_uid}/messages, POST /v1/auth/activate, POST /v1/auth/activate/redeem, and wss://api.plow.co/v1/ws for WebSocket delivery.
- SWcurl + standard shell toolsrequired· Declared in SEED.md: host setup path MAY use curl and standard shell tools; no other host tooling (Python, git, hermes CLI) is required.
- StatePLOW_CHAT_TOKEN and PLOW_CHAT_CHAT_UID env varsrequired· Must be written to the profile .env (data/.env or data/profiles/<name>/.env) by create_plow_chat_curl.sh before first container boot; the adapter will not activate without them.
Contributors
2 contributorsActivity
0 commentsYou need to be signed in with GitHub to comment.
No comments yet — be the first to share how this seed worked for you.
Similar seeds
Specification and reference examples for the Plow Chat API — activate users, send messages, and receive replies over SMS/iMessage via REST and WebSocket.
by @plow-pbcDirectly implements the Plow Chat API specification by connecting an AI agent to iMessage/SMS messaging through a gateway plugin.
Turns your short-term rental AI assistant into a full team coordinator — routing guest questions to staff over iMessage and pinging you only to approve the final reply.
by @plow-pbcAlso connects a Hermes AI agent to iMessage for routing messages, with similar Docker-based installation and credential management approach.
Run the Hermes AI agent locally in Docker with a browser dashboard, ChatGPT login, and files you can edit directly from your computer.
by @plow-pbcProvides the base Hermes AI agent runtime that this seed extends with iMessage/SMS chat capabilities.
Loads your full Hostex guest-conversation history into an AI brain so your vacation-rental assistant can answer guest questions from real past exchanges.
by @plow-pbcExtends Hermes to additional messaging channels, relevant if you want guest conversations from multiple platforms ingested into the knowledge base.