LearnNewsExamplesServices
Frontmatter
id15607
titleBound PAT verifier upstream fetches with a timeout (gitlab-pat + github-pat)
stateClosed
labels
enhancementaisecurity
assigneesneo-gpt-emmy
createdAtJul 20, 2026, 3:24 PM
updatedAtJul 22, 2026, 10:19 PM
githubUrlhttps://github.com/neomjs/neo/issues/15607
authorneo-kimi-phoebe
commentsCount2
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJul 22, 2026, 10:19 PM

Bound PAT verifier upstream fetches with a timeout (gitlab-pat + github-pat)

Closed Backlog/active-chunk-8 enhancementaisecurity
neo-kimi-phoebe
neo-kimi-phoebe commented on Jul 20, 2026, 3:24 PM

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

  1. Add the declarative leaf:

       patValidationTimeoutMs: leaf(5000, 'NEO_AUTH_PAT_VALIDATION_TIMEOUT_MS', 'number')
  2. 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.

  3. 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.
  4. 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.

  5. 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

  • AC1: both PAT verifiers use auth.patValidationTimeoutMs, default 5000, with the exact positive-integer range and fail-fast configuration error above.
  • AC2: GitLab uses one end-to-end upstream signal across /api/v4/user and conditional /oauth/token/info; GitHub uses the same one-cache-miss deadline shape for /user.
  • AC3: owned timeout maps to the exact provider-specific InvalidTokenError; the failure is not cached and the next request revalidates.
  • AC4: unit specs cover hung requests, GitLab second-fetch expiry, signal reuse, recovery/no-cache, invalid config values, and unaffected within-budget success for both modes.
  • AC5: cloud auth/config docs name the env leaf and config-leaf parity is regenerated; ADR-0019 lint remains green.

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"