Repository navigation
fix(a2a): accept dashboard session auth on /a2a route (#12888) - #13271
Merged
Merged
Conversation
The A2A dashboard's Run message/send and Run message/stream buttons
fetch("/a2a", ...) with no Authorization header. authenticateA2ARequest()
only checked for a Bearer/x-api-key, unlike clientApiPolicy (/api/v1/*)
which falls back to isDashboardSessionAuthenticated() when no key is
present. Add that same fallback to the A2A auth helper, give a
session-authenticated caller with no API key a stable 'dashboard' owner
id in resolveA2AOwner(), and send credentials: same-origin from the
dashboard playground fetches so the session cookie always rides along.
Githab-capibara
added a commit
to Githab-capibara/OmniRoute
that referenced
this pull request
Sep 17, 2026
…nce#12888) (cheaper-inference#13271) Merged as part of the owner batch of 2026-09-11. This PR had a live worktree in another session, so it sat outside the main 39. Merged on your explicit call, validated first rather than taken on trust: boarded with the other 10 worktree-held PRs into a consolidated worktree off `release/v3.8.51`. - ESLint over every changed file: no errors - `typecheck:core` clean; `check:dashboard-typecheck` OK; `check:changelog-integrity` OK - complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 - 203 of 208 assertions green. The 5 remaining (`guide-settings-route` ×4, `hard-session-lease-bypass-inventory` ×1) reproduce on the pure tip with nothing from this batch applied. - `imageGeneration.ts` rebaselined 3259 → 3293 for cheaper-inference#12945's image-only-model guard, landed separately in cheaper-inference#13392 so nothing was pushed onto a live branch.⚠️ base-red inherited: cheaper-inference#12732 — provider count 356 vs 358 and `open-sse/utils/stream.ts` 3115 > frozen 3098, both reproducing on the pure tip.
muhamadgalihsaputra
pushed a commit
to niyatna/NiyatnaRoute
that referenced
this pull request
Sep 27, 2026
…nce#12888) (cheaper-inference#13271) Merged as part of the owner batch of 2026-09-11. This PR had a live worktree in another session, so it sat outside the main 39. Merged on your explicit call, validated first rather than taken on trust: boarded with the other 10 worktree-held PRs into a consolidated worktree off `release/v3.8.51`. - ESLint over every changed file: no errors - `typecheck:core` clean; `check:dashboard-typecheck` OK; `check:changelog-integrity` OK - complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 - 203 of 208 assertions green. The 5 remaining (`guide-settings-route` ×4, `hard-session-lease-bypass-inventory` ×1) reproduce on the pure tip with nothing from this batch applied. - `imageGeneration.ts` rebaselined 3259 → 3293 for cheaper-inference#12945's image-only-model guard, landed separately in cheaper-inference#13392 so nothing was pushed onto a live branch.⚠️ base-red inherited: cheaper-inference#12732 — provider count 356 vs 358 and `open-sse/utils/stream.ts` 3115 > frozen 3098, both reproducing on the pure tip.
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.
Closes #12888
Root cause
Two related gaps:
A2ADashboard.tsx) callfetch("/a2a", ...)with only aContent-Typeheader — noAuthorization.authenticateA2ARequest()(src/lib/a2a/authenticate.ts) only checks for a Bearer/x-api-key. UnlikeclientApiPolicy(used for/api/v1/*), which falls back toisDashboardSessionAuthenticated()when no key is present,/a2anever consulted the dashboard's session cookie at all — so even a logged-in dashboard request hitUnauthorized: missing or invalid API keywheneverREQUIRE_API_KEYorOMNIROUTE_API_KEYwas configured.Fix
authenticateA2ARequest()now falls back toisDashboardSessionAuthenticated(req)when no API key is present, mirroringclientApiPolicy's behavior — only adding the dashboard-session path, never widening anonymous access (REQUIRE_API_KEY / OMNIROUTE_API_KEY postures are unchanged for non-dashboard callers).resolveA2AOwner()now returns a stable"dashboard"owner id for a session-authenticated caller with no API key, instead ofundefined, so dashboard-originated tasks are scoped consistently (it's nowasync— the two call sites insrc/app/a2a/route.tsandsrc/app/api/a2a/_auth.tswere updated toawaitit).A2ADashboard.tsx'shandleSmokeSend/handleSmokeStreamfetches now sendcredentials: "same-origin"(defense-in-depth; same-origin fetch already sends cookies by default, but explicit matches the existingplaygroundAuth.tsconvention).Regression test
tests/unit/a2a-dashboard-session-auth.test.ts(new):isDashboardSessionAuthenticated()butauthenticateA2ARequest()returnedfalse.authenticateA2ARequest(); a request with neither a valid key nor a valid session cookie is still rejected;resolveA2AOwner()returns"dashboard"for the session-only caller.Also updated two existing tests (
tests/unit/a2a-task-owner-idor.test.ts,tests/unit/a2a-history-route.test.ts) toawait resolveA2AOwner(...)now that it isasync— legitimate alignment to the corrected (now-async) contract, no assertions weakened.Gates run
node --import tsx/esm --test tests/unit/a2a-dashboard-session-auth.test.ts— 3/3 passnode --import tsx/esm --test tests/unit/a2a-task-owner-idor.test.ts tests/unit/a2a-history-route.test.ts tests/unit/a2a-tasks-auth.test.ts tests/unit/dashboard-shell-tabs.test.ts— 36/36 pass (existing A2A/dashboard coverage, unaffected)node scripts/check/check-file-size.mjs— OKnode scripts/check/check-complexity.mjs— OK (2798 violations, baseline 3218)node scripts/check/check-cognitive-complexity.mjs— OK (1265 violations, baseline 1437)npm run typecheck:core— exit 0npm run check:dashboard-typecheck— exit 0npx eslint --suppressions-location config/quality/eslint-suppressions.json <changed files>— exit 0node scripts/check/check-changelog-integrity.mjs— OKScope note
Per task instructions,
src/server/authz/policies/clientApi.tswas left untouched (another in-flight branch, fix/12745-memory-rerank-auth, also touches it).CodeQL alert #998 — dismissed as false positive (Hard Rule #14)
js/insufficient-password-hashatsrc/lib/a2a/authenticate.ts:60.That line is
resolveA2AOwner(), which derives a stable, non-secret owner-scoping id for A2Atask visibility (GHSA-jcm5-6wpp-wjj8). It is never compared against a stored credential and never
gates authentication. Real API-key verification lives in the same file:
timingSafeEqual(
tokensMatch) andisValidApiKey. Swapping in a KDF would only change the id format and orphanalready-persisted task ownership, with no security gain.
The flagged expression is byte-identical to
release/v3.8.51— this PR only changed thesurrounding control flow, which is why CodeQL re-reported a pre-existing line as new: