Decision 008: A remote push is a doorbell, verified in a workflow update, and its token is kept only as a digest
- Status: proposed (accepted when the issue-27 pull request is approved)
- Date: 2026-10-12
- Deciders: @MadaraUchiha-314 (approver), the-loop (proposer)
- Work item: issue #27
Context
Issue #27 makes a delegated remote sub-task's stand-in hear the remote task change while it is parked. A2A gives two mechanisms:
- Push notifications. The client registers a URL and a token. The agent POSTs a
StreamResponsewith the token inX-A2A-Notification-Token. SubscribeToTask. The client holds a stream open.
Push needs an inbound endpoint, which is new attack surface on a server that has no authentication of its own (decision-003). The token is a bearer credential. Temporal records every activity input and result in workflow history, and the issue requires that the token is never persisted in clear text.
Decision
- The push is a doorbell. A verified push only wakes the stand-in. The stand-in then fetches the task with
GetTaskfrom the agent in its registry, and records that state, never the body's. The body's claimed state decides only whether to fetch again: up to three times, after 1, 2 and 4 s, while the fetch disagrees. That covers a remote that pushes before its ownGetTaskshows the change. - Verification is a Temporal update validator.
- The endpoint hashes the presented token.
- It sends a
remote_pushupdate to the workflow its path names. - The validator compares the digests with
hmac.compare_digestand checks the remote task id, the watch mode and that the stand-in is still open. - A rejected update records no update and runs no activity, and the endpoint answers every refusal with one
401.
- Only the digest is kept. The token is generated inside the activity that registers it,
register_remote_push. It goes to the remote agent and nowhere else: not workflow history, not the store, not logs. Exported traces are covered too, since spans started during registration are never exported. - Follow is the fallback, chosen per agent.
updates = "auto" | "follow" | "off".autouses push when the card advertises it and the server has a base URL.
Consequences
- A leaked token can cost at most four
GetTaskcalls per push, within the rate limit; it cannot change state. - Each push costs one round trip to the remote agent.
- Rejected pushes can still leave empty workflow tasks on Temporal versions without speculative workflow tasks. Temporal's time-skipping test server is one of them.
- Response timing may differ between an unknown task id and a wrong token. Task ids are not secret.
Alternatives considered
- Apply the push body. It is cheaper, but it trusts the token holder with state and is exposed to out-of-order delivery. A stale
WORKINGpush could roll a row back. - Query the digest, then signal. That takes two RPCs and is not atomic. A signal is always recorded, so a flood of bad pushes would grow history.
- Encrypt the token at rest. That needs key management, which issue-23 removed. The digest makes it unnecessary.