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/dev8c73d531c5bf8881d9c1b91bf47980d8475243b7, 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:
AuthService is the sole interpreter of authentication inputs and the sole installer of authentication middleware.
TransportService owns HTTP transport/session mechanics and consumes only already-authenticated req.auth.
OIDC+proxy composition is explicit and non-downgrading.
Proxy identity is proven through the documented strip → authenticate → inject ingress boundary, not through a directly published fixture.
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)
Move remaining custom/proxy authentication mounting, proxy-header interpretation, rejection, and req.auth binding into AuthService.
Move local-bearer literal-loopback and pre-CORS Origin guards into the owning AuthService branch.
Delete the baseline compatibility scaffolding from Transport; Transport retains no authentication-mode, proxy-trust, identity-header, or custom-middleware interpretation.
Make OIDC+proxy composition executable with the four-way non-downgrade contract.
Add consumed-boundary proof for the documented reference ingress and for explicit admitted/excluded provider profiles.
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
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
Unit: source-derived state matrix, custom precedence, local guard ownership.
Consumed HTTP: proxy-only and four-way OIDC+proxy, admitted/excluded provider profiles.
Ingress: strip/inject spoof negative and trusted positive through the reference proxy.
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.
#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.
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-patparity, 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/dev8c73d531c5bf8881d9c1b91bf47980d8475243b7, 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 intoreq.authat dispatch.#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:
AuthServiceis the sole interpreter of authentication inputs and the sole installer of authentication middleware.TransportServiceowns HTTP transport/session mechanics and consumes only already-authenticatedreq.auth.Architectural Reality
trustProxyIdentityis an authentication input, not transport projection. Header extraction, rejection, and identity creation therefore belong in AuthService.learn/agentos/SharedDeployment.mdrequires caller identity headers to be stripped, authenticated identity to be injected, and the MCP server to be unreachable except through that proxy.AgentIdentityexists.Fix (one PR)
req.authbinding intoAuthService.auth.mode, configured OIDC endpoint, custom middleware, proxy trust), never from a second exported registry or identifier grep.Contract Ledger
AuthService.setup()SharedDeployment.mdauth ownershipTransportServicereq.authauthMiddlewareleafauth.trustProxyIdentityleafSharedDeployment.mdprecedence + AuthService JSDocauth.mode,auth.localBearerToken, andmcpListenHostleaveslearn/agentos/SharedDeployment.md+ai/mcp/deploy/proxy/auth.allowedUsersDecision 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
Acceptance Criteria
AuthServiceis the sole installer/interpreter for custom middleware, the five built-in modes, configured OIDC, proxy-only, and OIDC+proxy.TransportServicecontains no auth-mode activation predicate, no custom middleware mounting, notrustProxyIdentityor identity-header interpretation, and no authentication-state creation; it consumes onlyreq.authfor request context/session dispatch.X-PREFERRED-USERNAME/X-Auth-Request-Preferred-Usernameis stripped and cannot create a session without authenticated injection; trusted injection succeeds. A directly published fixture is not represented as a production-safe boundary.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.Evidence Ladder
Out of Scope
Avoided Traps
Related
Parent epic #15798 · predecessor #15990 · generator #15805 · pilot #15806 · CI topology #15807 / PR #15983 · source D#15958 · ADR 0019 · ADR 0020.
Unresolved Liveness
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")