Conversation
… [SAO-17523] When the upstream authorization service rejected a check, the log recorded only the operation and the status code. During the 2026-09-23 multitenant incident that left the cause unknown: the pods showed Orbit returning 422 and Agent Control translating it to 502, with nothing to say which field was rejected. Attach three things to that warning: the operation, the shape of the target context that was sent, and the field path plus error kind from the upstream validation body. Orbit answers with the standard FastAPI validation envelope, where `type` and `loc` alone separate a malformed target_id (`uuid_parsing` at body.context.target_id) from an unsupported target_type (`enum` at body.context.target_type) from an unknown operation (`enum` at body.operation). Caller data stays out. Only `type` and `loc` are taken from each entry; `input`, `ctx`, and `msg` echo the caller's values and are dropped. Target context is described by shape rather than value, so absent, null, empty, and wrongly typed stay distinguishable without logging an identifier. The logged list is capped, with the total reported separately so a truncated list never reads as complete. Nothing Orbit-specific is added. Target values remain opaque, with no checks for `log_stream` or UUID format, and the parser never raises: an unexpected rejection body degrades to an empty summary instead of turning a 502 into a 500.
SQLAlchemy 2.1.0 no longer lets mypy infer the element type through `result.scalars().all()`, so CI fails typecheck with var-annotated on a line that is unchanged. The locked 2.0.51 still infers it, which is why this only appears in CI. Annotate the local explicitly; it holds under both versions.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When the upstream authorization service rejects a check, the log line records only the operation and the status code. This PR attaches sanitized, value-free diagnostics to it so the cause is identifiable from logs alone.
What happened
During the 2026-09-23 multitenant incident,
POST /api/v1/auth/runtime-token-exchangereturned502. Direct pod inspection showed the sequence, twice:That second line is the log this PR changes. It carries the operation and the status, and nothing else, so the rejected field was never identified. The root cause of that incident remains unconfirmed for exactly this reason.
What changed
The
400 <= status < 500branch ofHttpUpstreamAuthProvider._handle_responsenow logs three additional fields:operation/status_codetarget_contextupstream_validationtypeandlocper entry, cappedupstream_validation_totalOrbit answers with the standard FastAPI validation envelope, and
typepluslocalone separate the plausible causes:typeloctarget_iduuid_parsingbody.context.target_idtarget_typeenumbody.context.target_typeenumbody.operationWhat is deliberately not logged
input,ctx, andmsgfrom each validation entry. All three echo caller-supplied values._field_shapereportsnull,empty,non_string:<type>, orstring:len=Ninstead of the value.The length is kept because whether a
target_idwas UUID-shaped is precisely the question the incident could not answer. That is a deliberate tradeoff against the no-sensitive-data goal, and the conservative alternative would be a barestring.Scope
server/src/agent_control_server/auth_framework/providers/http_upstream.pyserver/tests/test_auth_framework.pyserver/src/agent_control_server/services/control_bindings.py, one type annotation. Unrelated to this feature: SQLAlchemy 2.1.0 stopped letting mypy infer the element type throughresult.scalars().all(), so CI began failing typecheck on an unchanged line. The locked 2.0.51 still infers it, which is why it only shows in CI. Reproduced locally withmypy==2.3.1andsqlalchemy==2.1.0, and the annotation holds under both versions.APIError, its status, and its message are untouched; this is additive logging.Out of scope:
Risk and Rollout
detailthat is not a list, and a non-object entry all degrade to an empty summary rather than turning a 502 into a 500. Each case has a test.Testing
server/tests/test_auth_framework.py:134 passed.912 passed.ruff check server/srcandmypy server/srcclean (51 files).docker-publish.ymlonly pushes images onmainor av*tag, so there is no deployable image until this merges.Note:
server/MakefilepinsAGENT_CONTROL_DB_PORT=5432insideTEST_DB_ENV, so the port must be overridden when another Postgres already holds 5432.Checklist
AI Tool Assistance Usage Statement