LearnNewsExamplesServices
Frontmatter
id15992
titleComplete Streamable HTTP authentication ownership
stateClosed
labels
enhancementairefactoringarchitecturesecurity
assigneesneo-gpt
createdAtJul 26, 2026, 4:30 PM
updatedAtJul 27, 2026, 8:40 AM
githubUrlhttps://github.com/neomjs/neo/issues/15992
authorneo-gpt
commentsCount0
parentIssue15798
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[x] 15990 Activate fail-closed provider-PAT auth for local parity
blocking[]
closedAtJul 27, 2026, 8:40 AM

Complete Streamable HTTP authentication ownership

Closed Backlog/active-chunk-10 enhancementairefactoringarchitecturesecurity
neo-gpt
neo-gpt commented on Jul 26, 2026, 4:30 PM

Context

This is the non-blocking hardening sibling produced by the post-graduation source step-back on Discussion #15958 and #15990. Epic #15798 remains the sole coordination parent.

#15990 closed via PR #16038 with the one-PR ship-now baseline: source-safe activation foundation, canonical rosterless github-pat parity, authenticated readiness, and the landed #15807 / PR #15983 overlay adaptation. This ticket follows that merged baseline; it remains independently claimable and must not block #15805 or the pilot #15806.

At exact origin/dev 8c73d531c5bf8881d9c1b91bf47980d8475243b7, authentication behavior still spans two modules:

  • AuthService.setup() installs the built-in bearer strategies.
  • TransportService.setup() predicts activation from a hand-maintained subset, mounts custom middleware, owns local-bearer socket/Origin guards, and promotes trusted proxy headers into req.auth at dispatch.
  • The SDK bearer middleware makes the documented OIDC+proxy fallback order non-executable: a missing bearer is rejected before the later Transport proxy branch can run.

#15990 removes the fail-open activation predicate and preserves every existing state safely enough to ship the local profile. This successor completes the architectural ownership contract without delaying that baseline.

Problem

A safe baseline is not the same as a finished ownership boundary. If custom/proxy compatibility, local-bearer guards, hybrid precedence, or ingress trust remain partly in Transport, a future authentication state can still drift across modules even after the immediate naked-HTTP defect is closed.

The completed state is stricter:

  1. AuthService is the sole interpreter of authentication inputs and the sole installer of authentication middleware.
  2. TransportService owns HTTP transport/session mechanics and consumes only already-authenticated req.auth.
  3. OIDC+proxy composition is explicit and non-downgrading.
  4. Proxy identity is proven through the documented strip → authenticate → inject ingress boundary, not through a directly published fixture.
  5. Provider authentication, profile admission, graph identity, and downstream authorization remain distinct.

Architectural Reality

  • ADR 0019 keeps resolved ConfigProvider leaves as runtime source of truth. This ticket removes duplicated interpretation; it does not add an exported legal-mode registry.
  • trustProxyIdentity is an authentication input, not transport projection. Header extraction, rejection, and identity creation therefore belong in AuthService.
  • Local-bearer literal-loopback and pre-CORS Origin checks are strategy guards. They move with the strategy while preserving their order before CORS and MCP session creation.
  • A present bearer in OIDC+proxy composition is terminal: valid bearer wins; invalid or malformed bearer challenges and cannot downgrade. Only bearer absence may reach the trusted proxy gate.
  • The reference ingress contract in learn/agentos/SharedDeployment.md requires caller identity headers to be stripped, authenticated identity to be injected, and the MCP server to be unreachable except through that proxy.
  • Profile admission is explicit policy. A provider-resolved login is not automatically authorized merely because an AgentIdentity exists.

Fix (one PR)

  1. Move remaining custom/proxy authentication mounting, proxy-header interpretation, rejection, and req.auth binding into AuthService.
  2. Move local-bearer literal-loopback and pre-CORS Origin guards into the owning AuthService branch.
  3. Delete the baseline compatibility scaffolding from Transport; Transport retains no authentication-mode, proxy-trust, identity-header, or custom-middleware interpretation.
  4. Make OIDC+proxy composition executable with the four-way non-downgrade contract.
  5. Add consumed-boundary proof for the documented reference ingress and for explicit admitted/excluded provider profiles.
  6. Derive the completed installer-state test domain from the ConfigProvider surfaces (auth.mode, configured OIDC endpoint, custom middleware, proxy trust), never from a second exported registry or identifier grep.

Contract Ledger

Target surface Source of authority Proposed behavior Fallback / edge case Docs Evidence
AuthService.setup() D#15958 G2a + ADR 0019 sole authentication dispatcher and installer illegal/unconfigured state fails with named remediation AuthService JSDoc + SharedDeployment.md auth ownership exhaustive state matrix
TransportService D#15958 Fold 16.6.1 transport/session only; consumes req.auth no compatibility auth branch survives TransportService JSDoc source-negative assertion
Custom middleware existing ConfigProvider authMiddleware leaf explicit precedence, installed by AuthService built-ins are not mounted too Config leaf + AuthService JSDoc unit + consumed HTTP
Proxy-only / OIDC+proxy D#15958 four-way matrix + auth.trustProxyIdentity leaf absence may reach trusted proxy; present invalid bearer never downgrades challenge/401, no session SharedDeployment.md precedence + AuthService JSDoc real HTTP matrix
Local-bearer guards auth.mode, auth.localBearerToken, and mcpListenHost leaves literal-loopback and any-present-Origin rejection remain before CORS/session boot/rejection remains fail closed Config leaves + AuthService JSDoc real socket tests
Reference ingress learn/agentos/SharedDeployment.md + ai/mcp/deploy/proxy/ strip caller headers, authenticate, inject trusted identity, sole ingress no trusted injection => no session Shared deployment guide + reference proxy comments consumed ingress negative/positive
Profile admission D#15958 authority census + auth.allowedUsers allowed provider subject succeeds; excluded valid provider identity fails before dispatch authentication alone grants no profile access Shared deployment auth/admission guidance MC + KB matrix

Decision Record

Decision Record: REQUIRED — aligned with ADR 0019; no amendment. The work removes the second runtime interpreter and derives its proof domain from the existing ConfigProvider-owned surfaces.

Credential/admission posture aligns with ADR 0020. Provider identity remains the authentication subject; explicit profile policy remains admission; graph presence does not become ambient authorization.

Discussion Criteria Mapping

  • G2a single owner: completed here after #15990 establishes safe unconditional activation.
  • OQ12 carrier precedence: discharged by the explicit non-downgrading OIDC+proxy matrix.
  • Proxy ownership objection: discharged by moving trust/header/auth binding out of Transport.
  • Ingress-spoof evidence gate: discharged against the documented reference ingress, not a directly published test fixture.
  • Profile-admission negative: discharged for valid-but-excluded provider identities before MCP dispatch.

Acceptance Criteria

  • AuthService is the sole installer/interpreter for custom middleware, the five built-in modes, configured OIDC, proxy-only, and OIDC+proxy.
  • TransportService contains no auth-mode activation predicate, no custom middleware mounting, no trustProxyIdentity or identity-header interpretation, and no authentication-state creation; it consumes only req.auth for request context/session dispatch.
  • Local-bearer literal-loopback and any-present-Origin guards live under the AuthService local-bearer branch and still execute before CORS, authentication completion, or MCP session creation.
  • Custom middleware takes precedence and returns; no built-in middleware is mounted in the same state.
  • OIDC+proxy passes all four cells: valid bearer + conflicting proxy uses OIDC; invalid/malformed bearer + valid proxy challenges with no downgrade; absent bearer + trusted proxy succeeds; absent bearer + missing proxy identity returns 401 with no session.
  • Proxy-only behavior remains green after ownership moves.
  • Through the documented reference ingress, caller-supplied X-PREFERRED-USERNAME / X-Auth-Request-Preferred-Username is stripped and cannot create a session without authenticated injection; trusted injection succeeds. A directly published fixture is not represented as a production-safe boundary.
  • A valid provider identity admitted to one profile succeeds on MC + KB; the same valid identity excluded from another profile is denied before MCP dispatch. The test distinguishes provider authentication, AgentIdentity provisioning, profile admission, and downstream authorization.
  • The installer-state census is derived from auth.mode, configured OIDC endpoint state, custom middleware, and proxy trust in both directions: every source state has evidence and every evidence row maps to a source state. No second exported mode registry is introduced.
  • All #15990 baseline behavior remains green, including rosterless first-subject policy, GitLab empty-allowlist compatibility, authenticated readiness, provider-independent parity CI, and stdio preservation.

Evidence Ladder

  1. Unit: source-derived state matrix, custom precedence, local guard ownership.
  2. Consumed HTTP: proxy-only and four-way OIDC+proxy, admitted/excluded provider profiles.
  3. Ingress: strip/inject spoof negative and trusted positive through the reference proxy.
  4. Regression: the full #15990 baseline suite stays green.

Out of Scope

  • Canonical dev Compose mode, host-port binding, readiness secret, first-subject policy, GitHub local-profile auto-provision, or PR #15983 overlay adaptation; #15990 owns those ship-now surfaces.
  • Seat-config generator output; #15805 owns generated provider-subject and secret-reference wiring.
  • A new liveness route; #13435 owns the measured trigger.
  • Seat-token retirement; it remains optional pending the source Discussion's post-pilot consumer census.
  • General Declared-Domains/G2b machinery or an ADR-0019 amendment.

Avoided Traps

  • Reintroducing a legal-mode list in Transport. The proof domain derives from ConfigProvider surfaces.
  • Making this ticket a release blocker for the local seat. #15990 is the baseline close-target; this is a successor.
  • Bearer-to-proxy downgrade. Invalid presented credentials are terminal.
  • Treating a published proxy fixture as a safe ingress. Only the documented strip/auth/inject boundary proves the contract.
  • Conflating authentication with admission or authorization. Each negative fails at its owning boundary.
  • Nested epic or meta-ticket. #15798 already coordinates the shared outcome; this is one one-PR leaf.

Related

Parent epic #15798 · predecessor #15990 · generator #15805 · pilot #15806 · CI topology #15807 / PR #15983 · source D#15958 · ADR 0019 · ADR 0020.

Unresolved Liveness

  • #15990 is closed via PR #16038. This ticket is independently claimable and does not block #15805 or #15806.
  • Seat-token retirement remains trigger-bound to a post-pilot zero-consumer census.

Creation Freshness

Latest 20 open issues sorted by creation time and the latest 30 all-state A2A messages were checked immediately before filing on 2026-07-26; no equivalent issue or competing earlier claim exists. The local issue mirror returned no matching ownership/ingress-hardening ticket.

Origin Session ID: 0b48c54a-3c2e-451c-a511-78169493b39c Retrieval Hint: query_raw_memories("D#15958 #15990 Streamable HTTP auth ownership proxy ingress hardening")