adom-courier
Public Made by Adomby adom
App-neutral Google Chat human-in-the-loop middleware for Adom apps. Two directions: apps POST /requests to ask a human (Marv posts via adom-gchat, the reply comes back with identity + attachment bytes
| Name | Last updated |
|---|---|
| bin | 28d ago |
| docs | 14d ago |
| screenshots | 27d ago |
| service | 14d ago |
| skills | 14d ago |
| config.json | 24d ago |
| install.sh | 14d ago |
| LICENSE | 29d ago |
| package.json | 14d ago |
| page.json | 14d ago |
| README.md | 14d ago |
| server.js | 14d ago |
| uninstall.sh | 29d ago |
adom-courier
App-neutral Google Chat human-in-the-loop middleware for Adom apps. An always-on poller that posts a prompt/ask as Marv into a Chat space and carries the human's reply back — responder identity + downloaded attachment bytes — to the app that asked. Apps never touch Google.
adom-gchat POST as Marv (webhook, bot-badged) ── prompts · acks · outcomes
▲
adom-courier post → correlate reply→request → resolve identity → download files → dispatch to app
▼
adom-google RECEIVE: poll Chat · resolve responder (People API) · download attachment bytes

Two directions over the same machinery. Apps ask a human (POST /requests → Marv posts the
prompt → the reply comes back with responder identity + attachment bytes). And people can start a
thread that the courier routes to that space's app, which replies as Marv (type:inbound) —
turning Chat into a front-end for AI-first apps. Strict 1:1 space → app; opt in per space.
Why two transports
- Post =
adom-gchat(incoming webhook). Prompts appear asMarv · App— bot-badged, no seat, no per-app OAuth. Webhooks are send-only. - Receive =
adom-google(OAuth REST, pull). The only way to read replies, resolve who answered, and download attachment bytes. One OAuth identity for the whole fleet, on the courier's container only. - Pull, not push — Google's inbound interaction POSTs are dropped by the Cloudflare edge on
*.adom.cloud; polling sidesteps that and the daily-auth friction (long-lived gateway token).
Correlation
The webhook doesn't expose its threadKey on read and returns no message handle, so after posting
the courier finds its own Marv message (the BOT message with that exact text) to learn the
thread.name, then matches human replies to the request by thread.name. Marv's own posts are
BOT-typed, so the poller skips them for free.
App-facing API
POST /requests {app, space, itemKey, text} |
post a threaded prompt as Marv; returns {requestId, threadName} |
POST /reply {requestId|threadName, text, expectReply?} |
post the outcome or a clarifying Q; expectReply re-opens for multi-turn |
GET /state |
open requests + conversations + recent responses (identity, text, attachments, delivery status) + stale-space alarms |
GET /health |
{ok:true} — adds a count-only {ok:true, stale:N} when a space's poll is stuck (externally monitorable, leaks no names) |
On a reply the courier POSTs {type, requestId, app, itemKey, turn, responder, text, attachments[]}
to the app's dispatchUrl and auto-acks receipt in-thread as Marv. Human-initiated messages
dispatch the same way as type:"inbound". Every dispatch is tracked — a failed push is flagged
delivered:false in /state rather than vanishing into a log line. All domain logic (image
validation, pass/fail, any LLM/vision) lives in the app — the courier is dumb, reusable transport.

CLI
adom-courier health | state
adom-courier request --app smokey --space smokey-notifications --item fe-3 --text "..."
adom-courier reply --request req-1 --text "Logged ✅" [--expect-reply]
Talks to $ADOM_COURIER_URL (default http://127.0.0.1:8870); add --json for raw output.
Deploy (its own container — one service per box)
# one-time on a fresh box — get the adom-wiki CLI:
curl -fsSL https://wiki.adom.inc/download/adom/adom-wiki-cli/1.0.2/adom-wiki-cli-v1.0.2 -o ~/.local/bin/adom-wiki && chmod +x ~/.local/bin/adom-wiki
adom-wiki pkg install adom/adom-courier # + deps (adom-google, adom-gchat)
bash ~/project/adom_modules/adom/adom-courier/service/deploy.sh
# then the interactive steps:
adom-google auth # the reading identity
adom-gchat setup # add each space's Marv webhook
# edit ~/.config/adom-courier/config.json, then map the port so apps can reach it
The watchdog (cron, every 2 min) keeps /health green and pulls published updates from the
registry (registry auto-update, ≤15 min cadence) — no git auth needed. Config + state live in
~/.config/adom-courier/ so updates never clobber them.
Config (external — ~/.config/adom-courier/config.json)
{
"pollIntervalMs": 8000,
"ackReceipt": true,
"adminToken": "<random — gates /state + /reset>",
"spaces": { "smokey-notifications": { "id": "spaces/…", "gchatSpace": "smokey-notifications" } },
"apps": { "smokey": { "dispatchUrl": "https://…/notify", "apiKey": "<inbound bearer>", "dispatchToken": "<outbound bearer>" } }
}
A space without gchatSpace falls back to posting via adom-google (as the authed user).
Auth (bearer, both directions)
- App → courier:
/requests+/replyrequireAuthorization: Bearer <apps[app].apiKey>; the courier verifies it (timingSafeEqual) and that the token's app matches the request'sapp./state+/resetrequireadminToken;/healthis open. With noapiKeys set the write endpoints are open (dev) — the boot log warns. - Courier → app: if an app sets
dispatchToken, the courier sendsAuthorization: Bearer <dispatchToken>on the dispatch POST, so your receiver can verify the call genuinely came from the courier. - CLI: pass
--tokenor set$ADOM_COURIER_TOKEN.
Status
Proven end-to-end against smokey-notifications, both directions: (1) Marv posts an inspection
prompt → a human replies from their phone with a gauge photo → the courier resolves the responder
(name + email), downloads the image bytes, dispatches to the app, and Marv auto-acks; and (2) a human
starts a thread → the courier routes it to the app as type:"inbound" → the app records it and
replies as Marv. Multi-turn, outcome-reply, per-app bearer auth, attachment caps, dispatch delivery
status, and a per-space staleness alarm all supported.
Related
- adom-google — the OAuth Google Workspace CLI the courier uses to receive (read replies, resolve identity, download attachment bytes).
- adom-gchat — the webhook CLI the courier uses to post as the bot (Marv).
- Integrating an app? See the Integrating your app section in the bundled skill (
skills/adom-courier/SKILL.md) — the 4-part contract + a worked example.
License
MIT — see LICENSE.