Follow-up to #5269: approval card is displayed, but approval_id is empty and approval cannot be submitted
#6,008 opened on Jul 13, 2026
Repository metrics
- Stars
- (13,820 stars)
- PR merge metrics
- (PR metrics pending)
Description
Hello,
Summary
This is a follow-up to #5269.
The original problem in #5269 was that no approval dialog or approval card was displayed at all. That part now appears to be fixed: the WebUI displays the approval request.
The remaining problem is that the displayed approval request has an empty approval_id. When the user tries to approve or deny it, the WebUI cannot submit the response. The approval request is visible, but it is not actionable.
Environment
Infrastructure: Oracle VPS Free Tier
OS: Ubuntu 24.04 ARM
Host resources: 4 vCPU, 24 GB RAM
Container runtime: Podman, rootless
Deployment: separate hermes-agent and hermes-webui containers
Hermes WebUI: v0.52.0
Hermes Agent: v0.18.2
WebUI chat backend: gateway
Gateway status in WebUI: Running
Runs API opt-in: HERMES_WEBUI_GATEWAY_USE_RUNS_API=true
Relevant non-secret configuration:
HERMES_WEBUI_CHAT_BACKEND=gateway
HERMES_WEBUI_GATEWAY_BASE_URL=http://<hermes-agent-host>:8642
HERMES_WEBUI_GATEWAY_USE_RUNS_API=true
The API server key and the WebUI gateway API key are supplied through environment files. Their values are not included here.
The same Hermes Agent can also be used through the separate Hermes Dashboard. The approval problem described here has not been reproduced through that Dashboard path.
Steps to reproduce
Run Hermes Agent with its API server enabled.
Connect Hermes WebUI to the remote gateway/API server.
Use the gateway-backed WebUI chat path with the Runs API enabled.
Ask the agent to perform an operation that requires approval, such as a guarded terminal or file operation.
Wait for the approval request.
Observe that the approval card or request is displayed.
Click an approval choice such as Allow once or Deny.
Expected behavior
The WebUI should assign or receive a non-empty identifier for the approval request and submit the selected choice to the Hermes Agent Runs API.
The approval should be resolved through:
POST /v1/runs/{run_id}/approval
The run should then continue for an allowed choice or stop cleanly for a denied choice.
Actual behavior
The approval request is visible, but the WebUI-side approval object has an empty approval_id.
The following represents the normalized approval object observed in the WebUI path. It is not intended to represent the raw SSE payload:
{
"event": "approval.request",
"run_id": "<redacted-run-id>",
"approval_id": ""
}
The exact run ID and timestamp are omitted because they are session-specific.
When the user submits the approval choice, the WebUI cannot complete the response because the gateway approval response path requires a non-empty approval_id.
The choice therefore cannot be validated from the browser, and the run remains blocked.
Source evidence
In WebUI v0.52.0, api/gateway_chat.py translates the identifier as follows:
approval_id = str(payload.get("approval_id") or payload.get("id") or "").strip()
The translated approval object contains this value as approval_id. If the upstream event contains neither approval_id nor id, the resulting value is the empty string.
In api/routes.py, the gateway response path contains this guard:
if _run_id:
if not approval_id:
return bad(handler, "approval_id is required for gateway approvals")
Therefore, an approval event without an identifier can be displayed but cannot pass through the WebUI approval response endpoint.
Relevant WebUI source:
https://github.com/nesquena/hermes-webui/blob/v0.52.0/api/gateway_chat.py
https://github.com/nesquena/hermes-webui/blob/v0.52.0/api/routes.py
https://github.com/nesquena/hermes-webui/blob/v0.52.0/api/runner_client.py
Agent/API contract observed in source
The Hermes Agent API-server Runs API emits an approval.request event containing the run identifier and approval metadata.
In gateway/platforms/api_server.py, the event builder adds fields including:
{
"event": "approval.request",
"run_id": run_id,
"timestamp": time.time(),
"choices": ["once", "session", "always", "deny"],
}
The event builder does not add an approval_id field in this path.
The corresponding Hermes Agent endpoint is:
POST /v1/runs/{run_id}/approval
Its handler uses the URL run_id to retrieve the active approval session and calls:
resolve_gateway_approval(
approval_session_key,
choice,
resolve_all=resolve_all,
)
The request body is validated for choice. The submitted approval_id is not used by that handler to identify the pending approval.
Relevant Hermes Agent source:
https://github.com/NousResearch/hermes-agent/blob/main/gateway/platforms/api_server.py
The API-server approval functionality was introduced as the resolution of Hermes Agent issue #15802 through PR #21899:
https://github.com/NousResearch/hermes-agent/issues/15802
https://github.com/NousResearch/hermes-agent/pull/21899
This report is narrower than #15802. The Runs API now exposes an approval event and a run-scoped resolution endpoint, but the WebUI adapter still produces an empty local identifier when the event does not contain one.
Historical context
The original issue #5269 reported that no approval request dialog was displayed. It used this deployment shape and reported errors such as:
Tool write_file returned error: Write denied: '<path>' is a protected system/credential file.
Issue #5269 was closed through PR #5640. The PR description states that the release batch contained documentation, environment examples, and tests, with zero application-code changes. The relevant gateway approval documentation was included through PR #5549.
Links:
Previous issue: https://github.com/nesquena/hermes-webui/issues/5269
Closing release PR: https://github.com/nesquena/hermes-webui/pull/5640
Gateway approval documentation PR: https://github.com/nesquena/hermes-webui/pull/5549
This new issue reports a later state: the approval card is now visible, but the response cannot be submitted because the local approval_id is empty.
Related approval follow-ups include #4203, #4479, and #4771:
https://github.com/nesquena/hermes-webui/issues/4203
https://github.com/nesquena/hermes-webui/issues/4479
https://github.com/nesquena/hermes-webui/issues/4771
Likely interoperability gap
The two sides currently use different identifiers for different purposes:
Hermes Agent resolves the pending approval from the run-scoped run_id and its server-side approval session.
Hermes WebUI requires a non-empty approval_id for its local approval-card and response state.
The Runs API approval event does not provide the WebUI-specific identifier.
This produces the following sequence:
Hermes Agent emits approval.request with run_id and no approval_id.
Hermes WebUI translates the missing value to approval_id == "".
The WebUI displays the approval request.
/api/approval/respond rejects the response before relaying it because approval_id is empty.
The Hermes Agent run remains waiting for approval.
Proposed fix
The WebUI Runs API adapter should not expose an empty approval identifier to the approval UI.
One possible WebUI-side fix is:
Preserve a gateway-provided approval_id when present.
Otherwise preserve a gateway-provided id when present.
Otherwise generate a non-empty WebUI-local correlation ID from the event identity, for example run_id, event timestamp, command, and pattern keys.
Keep the generated value stable when the same SSE event is replayed or translated again.
Continue using the Agent-provided run_id for the actual Runs API resolution request.
The generated identifier would be a WebUI correlation key, not a replacement for the Hermes Agent run identifier. Hermes Agent should remain responsible for resolving the pending approval by run_id.
An alternative would be to make the WebUI response path accept a missing local approval_id for Runs API approvals and route solely by run_id, but that would require careful handling of stale and concurrent approval cards. A local deterministic correlation ID appears to preserve the existing WebUI response contract with a smaller behavior change.
Suggested regression tests
Please consider adding tests that verify:
A Runs API approval.request payload without approval_id or id is translated to an approval object with a non-empty local identifier.
Translating the same payload twice produces the same local identifier.
A gateway-provided approval_id is preserved unchanged.
A gateway-provided id is used when approval_id is absent.
The generated identifier is accepted by /api/approval/respond.
The choice is relayed to POST /v1/runs/{run_id}/approval.
The request body sent to Hermes Agent contains the choice and does not rely on the local identifier for server-side approval lookup.
Concurrent approval requests do not accidentally share the same WebUI correlation identifier.
Verification status
The empty-ID behavior is confirmed by source inspection and by the observed WebUI behavior described above.
No claim is made here that a proposed patch has passed the WebUI test suite. A checkout of the exact target branch and a controlled end-to-end run would still be needed to validate the implementation and check behavior with concurrent or replayed approval events.
Maintainer question
Could you confirm whether the intended WebUI contract for Runs API approvals is:
a WebUI-generated local correlation ID combined with the Agent run_id, or
routing entirely by the Agent run_id without requiring a local approval_id?
The current v0.52.0 code appears to require both for the browser response path, while the Hermes Agent Runs API resolves the approval using run_id and the active approval session.
This version is deliberately cautious: it does not claim that the raw Agent event contains "approval_id": ""; it states that the Agent event omits the field and that the WebUI normalizes the missing value to an empty string.