fix(api-server): widen error redaction to cron-endpoint + SSE sites

Follow-up to the salvaged #37733 fix. The contributor centralized
redaction at _openai_error and the chat/responses failure paths, which
covers the OpenAI-compatible envelopes transitively. Two sibling classes
crossed the same authenticated HTTP boundary unredacted:

- 8x cron-management endpoints returning {"error": str(e)} on 500
- the session-chat SSE error event ({"message": str(exc)})

Route both through the same _redact_api_error_text(force=True) helper.
Add AUTHOR_MAP entry for coygeek and a TestRedactApiErrorText guard
covering mask/force/limit/passthrough behavior.
This commit is contained in:
teknium1 2026-06-28 01:35:48 -07:00 committed by Teknium
parent 5e774de76e
commit 58c36b1798
3 changed files with 39 additions and 9 deletions

View file

@ -1768,7 +1768,7 @@ class APIServerAdapter(BasePlatformAdapter):
}))
except Exception as exc:
logger.exception("[api_server] session chat stream failed")
await queue.put(_event_payload("error", {"message": str(exc)}))
await queue.put(_event_payload("error", {"message": _redact_api_error_text(exc)}))
finally:
await queue.put(_event_payload("done", {}))
await queue.put(None)
@ -3297,7 +3297,7 @@ class APIServerAdapter(BasePlatformAdapter):
jobs = _cron_list(include_disabled=include_disabled)
return web.json_response({"jobs": jobs})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
return web.json_response({"error": _redact_api_error_text(e)}, status=500)
async def _handle_create_job(self, request: "web.Request") -> "web.Response":
"""POST /api/jobs — create a new cron job."""
@ -3351,7 +3351,7 @@ class APIServerAdapter(BasePlatformAdapter):
_notify_cron_provider_jobs_changed()
return web.json_response({"job": job})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
return web.json_response({"error": _redact_api_error_text(e)}, status=500)
async def _handle_get_job(self, request: "web.Request") -> "web.Response":
"""GET /api/jobs/{job_id} — get a single cron job."""
@ -3370,7 +3370,7 @@ class APIServerAdapter(BasePlatformAdapter):
return web.json_response({"error": "Job not found"}, status=404)
return web.json_response({"job": job})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
return web.json_response({"error": _redact_api_error_text(e)}, status=500)
async def _handle_update_job(self, request: "web.Request") -> "web.Response":
"""PATCH /api/jobs/{job_id} — update a cron job."""
@ -3408,7 +3408,7 @@ class APIServerAdapter(BasePlatformAdapter):
_notify_cron_provider_jobs_changed()
return web.json_response({"job": job})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
return web.json_response({"error": _redact_api_error_text(e)}, status=500)
async def _handle_delete_job(self, request: "web.Request") -> "web.Response":
"""DELETE /api/jobs/{job_id} — delete a cron job."""
@ -3428,7 +3428,7 @@ class APIServerAdapter(BasePlatformAdapter):
_notify_cron_provider_jobs_changed()
return web.json_response({"ok": True})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
return web.json_response({"error": _redact_api_error_text(e)}, status=500)
async def _handle_pause_job(self, request: "web.Request") -> "web.Response":
"""POST /api/jobs/{job_id}/pause — pause a cron job."""
@ -3448,7 +3448,7 @@ class APIServerAdapter(BasePlatformAdapter):
_notify_cron_provider_jobs_changed()
return web.json_response({"job": job})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
return web.json_response({"error": _redact_api_error_text(e)}, status=500)
async def _handle_resume_job(self, request: "web.Request") -> "web.Response":
"""POST /api/jobs/{job_id}/resume — resume a paused cron job."""
@ -3468,7 +3468,7 @@ class APIServerAdapter(BasePlatformAdapter):
_notify_cron_provider_jobs_changed()
return web.json_response({"job": job})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
return web.json_response({"error": _redact_api_error_text(e)}, status=500)
async def _handle_run_job(self, request: "web.Request") -> "web.Response":
"""POST /api/jobs/{job_id}/run — trigger immediate execution."""
@ -3487,7 +3487,7 @@ class APIServerAdapter(BasePlatformAdapter):
return web.json_response({"error": "Job not found"}, status=404)
return web.json_response({"job": job})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
return web.json_response({"error": _redact_api_error_text(e)}, status=500)
async def _handle_cron_fire(self, request: "web.Request") -> "web.Response":
"""POST /api/cron/fire — Chronos managed-cron fire webhook (NAS → agent).

View file

@ -59,6 +59,7 @@ AUTHOR_MAP = {
"267614622+agt-user@users.noreply.github.com": "agt-user", # PR #48496 salvage (telegram CLOSE-WAIT polling heartbeat, #48495)
"80915+DavidMetcalfe@users.noreply.github.com": "DavidMetcalfe", # PR #52272 salvage (route reasoning-model thinking-timeouts to timeout not context_overflow + reasoning-specific guidance; #52271)
"66773372+Tranquil-Flow@users.noreply.github.com": "Tranquil-Flow", # PR #52623 salvage (auxiliary Anthropic base_url host validation; #52608)
"65363919+coygeek@users.noreply.github.com": "coygeek", # PR #37735 salvage (redact provider error text at api-server HTTP boundary; #37733)
"moonsong@nousresearch.local": "Tranquil-Flow", # PR #52623 salvage (auxiliary Anthropic base_url host validation; #52608)
"140971685+Dr1985@users.noreply.github.com": "Dr1985", # PR #42567 salvage (launchd supervision detection + status reporting; #42524)
"8180647+herbalizer404@users.noreply.github.com": "herbalizer404", # PR #49076 + #51835 salvage (auxiliary compression fallback: 403/session-usage payment errors + honor fallback chain when aux provider auth unavailable)

View file

@ -30,6 +30,7 @@ from gateway.platforms.api_server import (
ResponseStore,
_IdempotencyCache,
_derive_chat_session_id,
_redact_api_error_text,
check_api_server_requirements,
cors_middleware,
security_headers_middleware,
@ -50,6 +51,34 @@ class TestCheckRequirements:
assert check_api_server_requirements() is False
# ---------------------------------------------------------------------------
# _redact_api_error_text — guards every outward error site (envelopes, SSE
# error events, cron-endpoint 500 bodies) that routes raw exception text to
# authenticated HTTP clients. #37733
# ---------------------------------------------------------------------------
class TestRedactApiErrorText:
def test_masks_secret_value_but_preserves_structure(self):
secret = "sk-api-server-leak-1234567890"
out = _redact_api_error_text(Exception(f"auth failed OPENAI_API_KEY={secret}"))
assert secret not in out
assert "OPENAI_API_KEY=" in out
def test_redacts_regardless_of_global_redaction_setting(self):
# force=True must mask even when global redaction is disabled.
secret = "sk-forced-redaction-0987654321"
with patch("agent.redact._REDACT_ENABLED", False):
out = _redact_api_error_text(Exception(f"boom AWS_SECRET_ACCESS_KEY={secret}"))
assert secret not in out
def test_limit_truncates_after_redaction(self):
assert len(_redact_api_error_text("x" * 500, limit=50)) == 50
def test_clean_text_passes_through_unchanged(self):
assert _redact_api_error_text("Job not found") == "Job not found"
# ---------------------------------------------------------------------------
# ResponseStore
# ---------------------------------------------------------------------------