ADR-008: Task Governance is Relay-Only -- Hangar is a Task Relay, Not a Task Executor
Status: Accepted Date: 2026-07-02 Authors: MCP Hangar Team
Context
The MCP Tasks extension (SEP-2663) lets a tools/call return a task handle
(CreateTaskResult) that the client then drives via tasks/get / tasks/update
/ tasks/cancel. Hangar has built and unit-tested full per-task governance --
ownership authorization and digest re-verification, both fail-closed, wired
through GovernedTaskStore and enable_tasks(...). That governance is currently
dormant: Hangar never emits a task handle, and no MCP server in the registry
returns one either.
Hangar's product thesis is govern the call path -- it is a proxy, not a job runner. The question "should Hangar emit task handles" reduces to two forks: relay vs executor, and now vs later. A relay tracks and governs tasks that an upstream creates. An executor decides to convert calls into tasks and owns the background work itself.
One concrete defect exists today: an upstream task result passes through the proxy
untouched, the client receives the handle, then tasks/get finds no local task and
returns "Task not found". That is fail-closed by accident -- a dead handle with a
misleading error, the async form of "an advertised capability that never runs".
Decision
-
Hangar is a relay, not an executor -- permanently. The executor role (Hangar deciding to make tasks and owning background lifecycle, result storage, TTL/GC, cancellation correctness, and the worker-thread to main-loop context bridge where a single bug means governance silently does not bind) is a change of species, not a role expansion. Envoy does not run your cron jobs. This closes the "convert long-running calls" / "per-tool" / executor variants.
-
Client-task-augmented calls are not a separate model. A
tools/callcarrying ataskparameter is forwarded upstream; the upstream decides whether to emit a handle; Hangar governs what returns. That collapses into the relay. -
Do not build the relay yet. No registry upstream emits
CreateTaskResult, and the extension ships with the 2026-07-28 RC. Building a relay for upstreams that do not exist is the executor's speculative trap at smaller scale. -
Interim (do now): reject upstream task handles explicitly. Detect an upstream task result in the proxy path and return a clear error (
TaskRelayNotSupported) instead of a dead handle -- deliberate fail-closed, not accidental. -
Trigger to build the relay -- both conditions, not either: (a) a real upstream emitting tasks in production use, and (b) the mcp task API graduates out of
mcp.server.experimental.
Consequences
- The built governance (
GovernedTaskStore, ownership#319, digest#320, and the consent primitive#322) stays dormant until the relay is built. This must be stated plainly in the docs -- built, tested, asleep -- before anyone infers it is active. - Decisions pre-committed for when the relay is built:
- Digest drift: re-verifying the tool digest at
get_resultis correct (that is the moment trust is delivered); on drift, fail the task, do not merely refuse the result -- a task whose result is permanently unavailable is a zombie. - Cancellation: the relay proxies
tasks/cancelbest-effort, exactly as the spec promises -- no more responsibility than that. - Consent is decoupled: a task entering
input_requiredwith no elicitation subsystem is failed fail-closed by the relay; consent (#322, needs an elicitation relay) does not gate the relay and lands separately if ever. - Mapping: an in-memory
taskIdmapping suffices for a relay; a durable or distributed task store is the executor's problem, which this ADR rejects.
- Digest drift: re-verifying the tool digest at
- When both trigger conditions hold, the relay is weeks, not quarters: a
taskIdmapping plus relayingtasks/get/tasks/cancelwith the existing fail-closed authorization, on top of the store already built.
Alternatives Considered
- Executor (Hangar converts calls to tasks / owns async execution). Rejected: it makes Hangar a job runner with new failure modes (leaked background work, result-store growth, cancellation correctness, the silent context-bridge bug), off the "govern the call path" thesis, and coupled deeply to an experimental API.
- Do nothing. Rejected as incomplete: the accidental dead-handle needs the explicit rejection (decision 4). Waiting on the relay is correct; waiting on the posture fix is not.
References
- SEP-2663 (Tasks extension);
mcp.server.experimental(task support, 1.26.0). src/mcp_hangar/application/tasks/governed_task_store.py; task primitivestask_ownership.py/task_digest_guard.py/task_consent.py.- Issues:
#302(WS-4 epic, full status),#319/#320(shipped, dormant),#322(consent, deferred).