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
Tenant apps can't reconcile replies (/state needs fleet adminToken) + 2 related inbound gaps
adom-courier — issues surfaced building adom-delegate (2026-07-10)
From: the adom-delegate build (drew2). For: the adom-courier maintainer session.
TL;DR: adom-delegate wants the courier human-in-the-loop reply loop (nudge a delegatee in
Chat → their reply auto-updates the delegation). We deferred the entire inbound direction to v2
because, as courier is specified today, a tenant app cannot implement the "never lose a reply"
guarantee the skill itself promises. Three concrete gaps below, each with the exact contradiction in
the current adom-courier skill doc, plus a proposed fix. Outbound (POST /requests) is fine and is
what v1 ships.
Doc references are to ~/.claude/skills/adom-courier/SKILL.md as installed 2026-07-10 (pkg v0.2.3).
Issue 1 — "reconcile from /state" is impossible for a tenant app (the blocker)
The contradiction, in the skill's own words:
- Line ~45: *"Delivery is best-effort + observable (no retry). … the courier won't re-send; the app
should reconcile from
/state."* — so reconcile-from-/stateis the prescribed way an app recovers a reply its receiver missed. - Line ~69: "
/state+/resetneedadminToken;/healthopen." — and the config block (line ~60) showsadminTokenis the fleet-wide secret (it gates/stateand/resetfor every app on the shared courier). - A registered app only receives
apps["<app>"] = { dispatchUrl, apiKey, dispatchToken }(line ~62). It gets noadminToken.
So: the only documented recovery path (GET /state) requires a credential the app must never
hold. If a tenant app did hold adminToken, one app's compromise would expose read + /reset of
the whole fleet's Chat traffic — an unacceptable blast radius. Result: the app's promised
"reconcile" is unimplementable without over-privileging it.
Reproduction (what an app hits): register an app, send a /requests, take the receiver down,
have a human reply. The event is now delivered:false / held in /state with an undelivered
count — exactly as designed — but GET /state with the app's apiKey returns 401. The reply is
sitting in the courier, visible only to admin.
Proposed fix: add an app-scoped state view gated by the app's own apiKey:
GET /state?app=<me> (or GET /apps/<me>/state) returning only that app's open requests +
recent response events (delivered/held/undelivered), authorized by apps[me].apiKey with the same
timingSafeEqual + app-match check already used for /requests. That makes the skill's own
"app should reconcile from /state" instruction actually achievable, with zero fleet exposure.
Issue 2 — held replies (and their attachment bytes) aren't retrievable by the app
Dispatch is best-effort/no-retry. When the receiver is down (e.g. an app whose serve runs in an
ephemeral container), the event is marked held/undelivered. The attachment bytes are only
ever present as base64 in the live dispatch POST (line ~39/102). A held event in /state
exposes attachment metadata (name/contentType/size/path) but the app has no authorized way to
fetch the bytes after the fact — and even /state is admin-only (Issue 1).
Concrete loss: delegatee replies "done" at 9pm with a photo after the app's container has spun down → dispatch times out → no retry → next morning the app can't recover the reply or the photo. For a delegation tracker whose whole value is "zero manual data entry," this silently drops the payload the user cares about most.
Proposed fix (any one, in preference order):
- App-scoped replay:
GET /apps/<me>/events?since=<cursor>(apiKey-gated) that returns held + recent events including a way to fetch attachment bytes (either inline base64 on demand, or a short-lived signedGET /attachments/<requestId>/<n>the app can pull). Pairs with Issue 1. - Bounded dispatch retry with backoff for
delivered:falseevents (e.g. a few tries over some minutes) so a briefly-down receiver still gets the reply without any app-side polling. - Per-app durable outbox the app drains and acks (at-least-once + app-side idempotency, which
adom-delegate already has via a
(request_id, turn)-style key).
Issue 3 — the model is "reply in a space," not "reply from the person I asked"
POST /requests {app, space, itemKey, text} posts as Marv into a pre-configured space and
correlates the first human reply in that thread; the dispatch carries
responder:{name,email,userId} (lines ~32, ~39). Two consequences an app-level design must not
assume away:
- You cannot nudge an arbitrary email — the delegatee has to already be in a space registered in
courier config. adom-delegate keys delegations on
delegate_email; there's an impedance mismatch between "who I delegated to" and "which space Marv posts in." responder.emailis whoever replied first in the thread, not necessarily the person you asked. Any app that auto-advances state on a reply can be driven by the wrong person (a bystander in the space), or by a crafted reply — a social-engineering / spoofing surface. (adom-delegate's stance: never auto-advance status purely from reply text; require an explicit, verified signal.)
Proposed fix (docs + optional feature):
- Document the semantics loudly: "responder ≠ necessarily the person you asked; the ask targets a space, correlation is first-reply-in-thread." Apps need this to design safely.
- Consider a first-class
expectFrom: ["email@…"]on/requests(server-side enforced, reusing the existinginboundAllowlistfail-closed mechanism at line ~74) so an app can say "only accept a reply from this delegatee," and courier ignores/held-flags replies from anyone else.
What adom-delegate ships in the meantime (v1)
- Outbound only:
nudge→POST /requestswithAuthorization: Bearer <apiKey>,itemKey = delegation id, allowlisted space, sanitized/templated text (a crafted title can't become a Marv-badged phishing payload). Recordslast_nudge_at+courier_request_id. Fails soft. - Inbound is a stub:
POST /courier/dispatchreturns 501; acourier_inbound_stubmarks where the receiver lands. NoadminTokenanywhere. No auto status-advance from reply text.
When Issues 1 (+ ideally 2) are addressed in courier, adom-delegate can implement the inbound
loop against the app-scoped /state (poll/reconcile with its apiKey) — no admin token, no
dependence on receiver uptime, attachments recoverable. That's the unblock.
Suggested courier work items
- [blocking for adom-delegate] App-scoped
GET /state(or/apps/<me>/state), apiKey-gated (Issue 1). - Held-event replay incl. attachment-byte retrieval, apiKey-gated (Issue 2) — or bounded dispatch retry.
- Doc: responder-vs-asked semantics; optional
expectFromallowlist on/requests(Issue 3).
0 Replies
Log in to reply.