Context
Optional shared hardening surfaced in Vega's cross-family review of PR #15601 (github-pat auth mode): neither PAT verifier bounds its upstream identity fetch with a timeout. The defect pre-existed in gitlab-pat and was faithfully mirrored into github-pat; this ticket owns the shared repair.
Live and semantic sweeps on 2026-07-22 found no equivalent ticket or implementation. Exact current source still performs the upstream fetches without a signal.
The Problem
AuthService.createGitlabPatVerifier and createGithubPatVerifier call fetch() against provider identity endpoints with no timeout. Node's fetch has no default request deadline, so a stalled provider API can leave the MCP request pending indefinitely and consume request capacity without a bounded failure.
GitLab has a second observable edge: when auth.allowedClientIds is non-empty, one verification performs /api/v4/user and then /oauth/token/info. Two independent five-second timeouts would permit a nearly ten-second verifier, so the budget must be defined for the whole upstream sequence.
The Architectural Reality
ai/mcp/server/shared/services/AuthService.mjs owns both PAT verifier factories and their success-only caches.
ai/configBase.mjs is the declarative auth-config SSOT per ADR-0019; consumers read the resolved leaf directly at the use site.
Neo.util.Env.parseNumber already treats an absent or non-finite env token as no override, warning and falling back to the declared default.
- Node's timer layer warns and collapses delays above
2_147_483_647 ms to 1 ms even though AbortSignal.timeout() accepts larger unsigned values. The verifier must reject that range rather than silently create an immediate deadline.
- Existing failure caching semantics are correct: only successful validations are cached.
The Fix
Add the declarative leaf:
patValidationTimeoutMs: leaf(5000, 'NEO_AUTH_PAT_VALIDATION_TIMEOUT_MS', 'number')
Read it directly in each PAT verifier factory and fail fast at verifier construction unless the resolved value is an integer in 1..2_147_483_647. Exact error:
AuthService: auth.patValidationTimeoutMs must be an integer from 1 to 2147483647
Absent or non-finite env strings follow the existing config parser contract and resolve to the declared 5000 default. Resolved zero, negative, fractional, or oversized values are configuration errors; there is no implementation-local fallback.
On every cache miss, create one AbortSignal.timeout(patValidationTimeoutMs) and reuse it for the complete provider validation sequence:
- GitLab:
/api/v4/user plus conditional /oauth/token/info share the one wall-clock deadline.
- GitHub:
/user consumes the same one-request deadline shape.
If that owned signal expires, delete any stale cache entry and map the failure to:
InvalidTokenError('GitLab PAT validation timed out after <ms>ms'), or
InvalidTokenError('GitHub PAT validation timed out after <ms>ms').
Timeout failures are never cached, so the next request revalidates after provider recovery. Non-timeout thrown errors, non-OK HTTP mappings, allowlist behavior, and success-cache semantics remain unchanged.
Document the leaf on the cloud auth/config surfaces and regenerate the config-leaf parity snapshot in the same commit.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Exact Contract |
Fallback / Edge Case |
Docs |
Evidence |
auth.patValidationTimeoutMs |
ai/configBase.mjs / ADR-0019 |
leaf(5000, 'NEO_AUTH_PAT_VALIDATION_TIMEOUT_MS', 'number'); resolved value must be integer 1..2_147_483_647 |
absent/non-finite env override warns + uses SSOT default; zero/negative/fractional/oversized resolved values fail verifier construction with the exact configuration error above |
Configuration + ClientAuthentication |
config-provider/parity lint + invalid-value factory specs |
GitLab /api/v4/user + conditional /oauth/token/info |
AuthService.createGitlabPatVerifier |
one signal per cache miss shared across both sequential fetches; total upstream sequence has one budget |
owned timeout → provider-specific InvalidTokenError; delete stale cache entry; no retry and no failure cache |
AuthService JSDoc |
hung first/second fetch, recovery/no-cache, signal reuse, within-budget success |
GitHub /user |
AuthService.createGithubPatVerifier |
one signal per cache miss passed to the identity fetch |
owned timeout → provider-specific InvalidTokenError; delete stale cache entry; no retry and no failure cache |
AuthService JSDoc |
hung fetch, recovery/no-cache, within-budget success |
| Non-timeout behavior |
existing verifier contracts |
non-OK HTTP, allowlist rejection, network errors, identity projection, scopes, and success cache remain unchanged |
no new coercion of non-timeout errors |
existing auth docs |
existing verifier + middleware suites |
| Template parity |
config-leaf-parity.json |
snapshot includes auth.patValidationTimeoutMs on the root template surface |
lint fails if declaration and snapshot diverge |
lint substrate |
ai:lint-config-template-ssot |
Decision Record impact
Aligned with ADR-0019: one declarative leaf in the root config SSOT, direct use-site reads, no env re-read, pass-along config object, optional chaining, runtime mutation, or hidden default. No ADR change.
Acceptance Criteria
Out of Scope
- Retry/backoff policy; PAT validation remains fail-fast and retries belong to a later caller decision.
- OIDC discovery/introspection timeouts; those are a separate strategy and error taxonomy.
- Caller-supplied cancellation; this ticket introduces the owned PAT-validation deadline only.
- Changing successful validation TTLs, allowlists, identity projection, or bearer challenge shape.
Avoided Traps
- Per-fetch GitLab deadlines — rejected because two sequential budgets double the observable verification ceiling.
- Implementation-local
|| 5000 fallback — rejected by ADR-0019; the config leaf is the sole default authority.
- Mapping every fetch error to timeout — rejected; only expiry of the owned signal gets the timeout taxonomy.
- Caching timeout failures — rejected because provider recovery must be observable on the next request.
Related
- PR
#15601 review origin
StdioIdentityResolver bounded-identity precedent
Origin Session ID: 8d4ce1c3-0bf2-4bb0-bad9-e49836248afe
Retrieval Hint: "PAT verifier end-to-end fetch timeout AbortSignal GitLab GitHub no failure cache"
Context
Optional shared hardening surfaced in Vega's cross-family review of PR
#15601(github-pat auth mode): neither PAT verifier bounds its upstream identity fetch with a timeout. The defect pre-existed ingitlab-patand was faithfully mirrored intogithub-pat; this ticket owns the shared repair.Live and semantic sweeps on 2026-07-22 found no equivalent ticket or implementation. Exact current source still performs the upstream fetches without a signal.
The Problem
AuthService.createGitlabPatVerifierandcreateGithubPatVerifiercallfetch()against provider identity endpoints with no timeout. Node'sfetchhas no default request deadline, so a stalled provider API can leave the MCP request pending indefinitely and consume request capacity without a bounded failure.GitLab has a second observable edge: when
auth.allowedClientIdsis non-empty, one verification performs/api/v4/userand then/oauth/token/info. Two independent five-second timeouts would permit a nearly ten-second verifier, so the budget must be defined for the whole upstream sequence.The Architectural Reality
ai/mcp/server/shared/services/AuthService.mjsowns both PAT verifier factories and their success-only caches.ai/configBase.mjsis the declarative auth-config SSOT per ADR-0019; consumers read the resolved leaf directly at the use site.Neo.util.Env.parseNumberalready treats an absent or non-finite env token as no override, warning and falling back to the declared default.2_147_483_647ms to1ms even thoughAbortSignal.timeout()accepts larger unsigned values. The verifier must reject that range rather than silently create an immediate deadline.The Fix
Add the declarative leaf:
patValidationTimeoutMs: leaf(5000, 'NEO_AUTH_PAT_VALIDATION_TIMEOUT_MS', 'number')Read it directly in each PAT verifier factory and fail fast at verifier construction unless the resolved value is an integer in
1..2_147_483_647. Exact error:Absent or non-finite env strings follow the existing config parser contract and resolve to the declared
5000default. Resolved zero, negative, fractional, or oversized values are configuration errors; there is no implementation-local fallback.On every cache miss, create one
AbortSignal.timeout(patValidationTimeoutMs)and reuse it for the complete provider validation sequence:/api/v4/userplus conditional/oauth/token/infoshare the one wall-clock deadline./userconsumes the same one-request deadline shape.If that owned signal expires, delete any stale cache entry and map the failure to:
InvalidTokenError('GitLab PAT validation timed out after <ms>ms'), orInvalidTokenError('GitHub PAT validation timed out after <ms>ms').Timeout failures are never cached, so the next request revalidates after provider recovery. Non-timeout thrown errors, non-OK HTTP mappings, allowlist behavior, and success-cache semantics remain unchanged.
Document the leaf on the cloud auth/config surfaces and regenerate the config-leaf parity snapshot in the same commit.
Contract Ledger Matrix
auth.patValidationTimeoutMsai/configBase.mjs/ ADR-0019leaf(5000, 'NEO_AUTH_PAT_VALIDATION_TIMEOUT_MS', 'number'); resolved value must be integer1..2_147_483_647/api/v4/user+ conditional/oauth/token/infoAuthService.createGitlabPatVerifierInvalidTokenError; delete stale cache entry; no retry and no failure cache/userAuthService.createGithubPatVerifierInvalidTokenError; delete stale cache entry; no retry and no failure cacheconfig-leaf-parity.jsonauth.patValidationTimeoutMson the root template surfaceai:lint-config-template-ssotDecision Record impact
Aligned with ADR-0019: one declarative leaf in the root config SSOT, direct use-site reads, no env re-read, pass-along config object, optional chaining, runtime mutation, or hidden default. No ADR change.
Acceptance Criteria
auth.patValidationTimeoutMs, default5000, with the exact positive-integer range and fail-fast configuration error above./api/v4/userand conditional/oauth/token/info; GitHub uses the same one-cache-miss deadline shape for/user.InvalidTokenError; the failure is not cached and the next request revalidates.Out of Scope
Avoided Traps
|| 5000fallback — rejected by ADR-0019; the config leaf is the sole default authority.Related
#15601review originStdioIdentityResolverbounded-identity precedentOrigin Session ID:
8d4ce1c3-0bf2-4bb0-bad9-e49836248afeRetrieval Hint: "PAT verifier end-to-end fetch timeout AbortSignal GitLab GitHub no failure cache"