Skip to content

fix(server): Reject malformed runtime-token target context before authorization [SAO-17522] - #268

Open
josjeon wants to merge 1 commit into
mainfrom
fix/sao-17418-runtime-token-auth
Open

josjeon wants to merge 1 commit into
mainfrom
fix/sao-17418-runtime-token-auth

Conversation

@josjeon

@josjeon josjeon commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This PR hardens POST /api/v1/auth/runtime-token-exchange so malformed target context is rejected by Agent Control with a local 422 before any authorization provider is called.

It preserves the OSS contract: target_type and target_id are required, non-empty strings, but their values remain opaque. Agent Control does not enforce Orbit-specific values such as target_type="log_stream" or a UUID-shaped target_id.

What happened

In the multitenant environment, runtime-token exchange intermittently returned 502. Agent Control logs showed the upstream authorization service returning 422 for runtime.token_exchange. The same incident window also contained upstream 422 responses for control_bindings.read.

The exact rejected runtime-token payload was not captured, so this PR does not claim that missing target fields were the confirmed production root cause.

Code inspection did identify a concrete boundary problem: the authorization dependency builds its context before FastAPI binds and validates RuntimeTokenExchangeRequest. The previous context builder used body.get(...), so malformed requests could reach the provider with missing, null, empty, or incorrectly typed target fields. An upstream 4xx is then exposed by Agent Control as 502, rather than returning a useful local validation error.

Goal

  • Never call an authorization provider with missing or invalid runtime target context.
  • Return a deterministic, structured local 422 for malformed runtime-token requests.
  • Keep Agent Control generic and accept valid opaque target values.
  • Avoid adding Orbit-specific log_stream or UUID validation to the OSS service.

What changed

  • Validate that the exchange body is a JSON object before authorization.
  • Validate only the two fields required by the authorization provider:
    • target_type: required, string, non-empty;
    • target_id: required, string, non-empty.
  • Return field-level ValidationErrorItem entries whose codes and messages match Pydantic v2 (missing, string_type, and string_too_short).
  • Do not echo invalid target values in validation errors.
  • Pass valid target values to the provider unchanged.
  • Keep unrelated body validation, including extra="forbid", in the existing Pydantic request model.
Request Result
Invalid JSON or a non-object body Local 422; authorization provider is not called
Missing, null, empty, or non-string target field Structured local 422; authorization provider is not called
Valid opaque target values Passed through unchanged
Unknown non-target field Provider receives only the valid target context; the existing Pydantic model then returns 422

Scope

  • Changed files:
    • server/src/agent_control_server/endpoints/auth.py
    • server/tests/test_runtime_token_exchange_endpoint.py
  • User-visible change: malformed runtime-token target context now returns local 422 instead of reaching the upstream provider and potentially surfacing as 502.
  • Valid requests are unchanged.

Out of scope for this PR:

  • Changes to control_bindings or attachment context builders.
  • New upstream 4xx logging or diagnostic parsing. Tracked separately in SAO-17523, which touches auth_framework/providers/http_upstream.py and proceeds in parallel.
  • Identifying or changing the Orbit caller responsible for the observed production 422.
  • Orbit's authorization contract. Orbit currently accepts only log_stream plus a log-stream UUID; Agent Control intentionally does not duplicate that validation.
  • Project-scoped target support.

Risk and Rollout

  • Risk level: low. The behavior change is limited to malformed runtime-token exchange requests before authorization.
  • No migration or configuration change is required.
  • Rollback plan: revert this PR.

Testing

  • Added or updated automated tests.
  • Focused runtime-token endpoint suite: 26 passed.
  • Focused endpoint statement and branch coverage: 100%.
  • Ruff and mypy passed for the changed code.
  • Required GitHub checks passed for the current head: ci, validate, sdk-ts-ci, ui-ci, build-and-push-server, and codecov/patch.
  • Ran the make check suites locally against the rebased tree: server tests 912 passed, make lint clean across models/server/sdk, and mypy server/src clean (51 files). Note server/Makefile pins AGENT_CONTROL_DB_PORT=5432 in TEST_DB_ENV, so the port must be overridden when another Postgres holds 5432. make typecheck reports 3 pre-existing opentelemetry.sdk.trace import errors in sdks/python, unrelated to this change and also present on main.
  • Validated in multitenant staging: not possible from a PR branch. docker-publish.yml only pushes images on main or a v* tag (push: github.event_name != 'pull_request'), so there is no deployable image until this merges. Tracked under the parent story.

Checklist

  • Linked issue: SAO-17522, a subtask of SAO-17418.
  • Documentation/examples: no update required; valid API behavior and the generic target contract are unchanged.
  • Follow-up boundary documented above: Orbit caller/root-cause investigation and control_bindings.read are not changed here.

AI Tool Assistance Usage Statement

  • AI assistance was used to draft parts of the implementation, that was subsequently modified and extended.
  • AI assistance was used in generating tests/documentation/comments for this change.
  • AI assistance was used for optimizing/troubleshooting/refactoring existing code in this change.
  • AI assistance was used to draft this entire change as is.

@codecov

codecov Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@josjeon josjeon changed the title SAO-17418: fix: Harden runtime-token authorization boundary fix(server): Harden runtime-token authorization boundary [SAO-17418] Sep 23, 2026
@josjeon josjeon changed the title fix(server): Harden runtime-token authorization boundary [SAO-17418] fix(server): Harden runtime-token authorization boundary [SAO-17522] Sep 24, 2026
@josjeon josjeon changed the title fix(server): Harden runtime-token authorization boundary [SAO-17522] fix(server): Reject malformed runtime-token target context before authorization [SAO-17522] Sep 24, 2026
…horization [SAO-17522]

The authorization dependency builds its context before FastAPI binds and
validates RuntimeTokenExchangeRequest. The context builder used body.get(...),
so a request could reach the authorization provider with missing, null, empty,
or wrongly typed target fields, and the resulting upstream 4xx surfaced to the
caller as a 502 instead of a local validation error.

Validate the body is a JSON object and that target_type and target_id are
non-empty strings before authorization runs. Report failures as field-level
ValidationErrorItem entries whose codes and messages match what Pydantic v2
emits for the same failures, so a rejection from the context builder is
indistinguishable to a client from one by the endpoint's own body model, and
without echoing the invalid values back.

The OSS contract is unchanged: both fields stay opaque, with no validation for
Orbit-specific values such as target_type="log_stream" or a UUID-shaped
target_id. Unrelated body validation, including extra="forbid", stays in the
Pydantic model, so an unknown field still reaches authorization first and is
then rejected by the model.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants