fix(gateway): preserve approval identity provenance

This commit is contained in:
Rod Boev committed 2026-07-22 12:31:12 -04:00
1 parent f30178e18a
commit a91bd88c70
5 files changed
+38 -1

No files matched your search

+2
View File
@@ -505,6 +505,8 @@ def _run_gateway_runs_api_streaming(
approval_data = _gateway_runs_approval_event(payload)
if approval_data:
approval_data["run_id"] = run_id
from api.config import gateway_supports_approval_identity_v1
approval_data["_gateway_agent_identity_v1"] = gateway_supports_approval_identity_v1(base_url, api_key)
from api.route_approvals import submit_gateway_pending_mirror
head, total = submit_gateway_pending_mirror(session_id, approval_data)
put_gateway_event("approval", {**(head or approval_data), "pending_count": total})
+5
View File
@@ -47,6 +47,7 @@ _approval_sse_subscribers: dict[str, list[queue.Queue]] = {}
_GATEWAY_MIRROR_FLAG = "_gateway_mirror"
_GATEWAY_MIRROR_TOKEN = "_gateway_mirror_token"
_GATEWAY_ENTRY_DATA_TOKEN_KEY = "_webui_mirror_token"
_GATEWAY_AGENT_IDENTITY_V1 = "_gateway_agent_identity_v1"
def _approval_sse_subscribe(session_id: str) -> queue.Queue:
@@ -323,6 +324,10 @@ def retire_gateway_pending_mirror(session_key: str, approval_id: str = "", run_i
gateway_queue_changed = False
if approval_id:
match = _gateway_pending_mirror_locked(session_key, approval_id, run_id)
if match is None and not normalized_run_id:
match = next((entry for entry in entries if _is_gateway_mirror_entry(entry)
and not str(entry.get("run_id") or "").strip()
and str(entry.get("approval_id") or "").strip() == approval_id), None)
retired = [match] if match else []
else:
retired = [
+3 -1
View File
@@ -23999,6 +23999,7 @@ def _handle_approval_respond(handler, body):
webui_gateway_chat_enabled,
)
from api.config import get_config as _get_config, gateway_supports_approval_identity_v1
from api.route_approvals import _GATEWAY_AGENT_IDENTITY_V1
s = get_session(sid)
_candidate_run_id = None
if s is not None:
@@ -24105,7 +24106,7 @@ def _handle_approval_respond(handler, body):
_base = _gateway_base_url(_cfg)
_key = _gateway_api_key()
try:
identity_v1 = gateway_supports_approval_identity_v1(_base, _key)
identity_v1 = bool(matched_mirror.get(_GATEWAY_AGENT_IDENTITY_V1)) and gateway_supports_approval_identity_v1(_base, _key)
HttpRunnerClient(base_url=_base, api_key=_key).respond_approval(
_run_id, matched_mirror["approval_id"] if identity_v1 else "", choice
)
@@ -24127,6 +24128,7 @@ def _handle_approval_respond(handler, body):
if webui_gateway_chat_enabled(_get_config()) and _gateway_pending_approval_without_run_id(
sid, approval_id
):
resolve_gateway_pending_local(sid, approval_id, choice)
retire_gateway_pending_mirror(sid, approval_id=approval_id)
return j(handler, {"ok": True, "choice": choice, "local_retired": True})
except Exception:
+23
View File
@@ -2135,6 +2135,29 @@ def test_gateway_approval_response_relay():
approvals._pending.pop("sess-relay", None)
def test_synthetic_gateway_identity_stays_fifo_after_capability_upgrade():
"""A local gwrun ID never becomes remote authority after a capability refresh."""
from api import routes
from api.gateway_chat import _STREAM_RUN_IDS
import api.route_approvals as approvals
_STREAM_RUN_IDS["sid-upgrade"] = "run-upgrade"
approvals.submit_gateway_pending_mirror("sess-upgrade", {
"run_id": "run-upgrade", "approval_id": "gwrun:run-upgrade:local", "command": "echo x"
})
handler = MagicMock()
handler.wfile = io.BytesIO()
with patch("api.routes.get_session", return_value=SimpleNamespace(active_stream_id="sid-upgrade")), \
patch("api.config.gateway_supports_approval_identity_v1", return_value=True), \
patch("api.runner_client.HttpRunnerClient.respond_approval") as respond:
routes._handle_approval_respond(handler, {
"session_id": "sess-upgrade", "choice": "once", "approval_id": "gwrun:run-upgrade:local",
})
respond.assert_called_once_with("run-upgrade", "", "once")
_STREAM_RUN_IDS.pop("sid-upgrade", None)
approvals._pending.pop("sess-upgrade", None)
def test_gateway_approval_response_without_approval_id_409s():
"""Gateway relay requires the emitted per-approval id, not bare run state."""
from api.gateway_chat import _STREAM_RUN_IDS
@@ -396,6 +396,11 @@ def test_gateway_mirrored_approval_without_run_retires_locally():
resp = handler.json()
assert handler.status == 200, f"expected 200, got {handler.status}: {resp}"
assert resp == {"ok": True, "choice": "once", "local_retired": True}
assert entry.event.is_set()
assert entry.result == "once"
with ta._lock:
assert sid not in ta._pending
assert sid not in ta._gateway_queues
finally:
_cleanup(sid)