feat(ax): config-declared entry auth — tell agents where to authenticate - #25
Merged
Merged
Conversation
An entry override in ax.config can now declare a first-class `auth` descriptor (exactly Ora's secret-free EntryAuth shape: status, OAuth endpoint URLs, scope keys, docsUrl). Declared once, it flows to the catalog entry, the MCP server card, and the generated /auth.md — most usefully for withMcpAuth-wrapped MCP mounts, which detection can only ever describe as "requires auth, scheme unknown". - config-schema: `auth` typed on AxEntryOverride and validated by a closed sub-schema — status enum required, URL fields must be absolute http(s) within 256 chars, lists capped, unknown keys fail loudly. - auth: sanitizeDeclaredAuth() gives declared descriptors the same secret-guard discipline as detected ones (safeHttpUrl, caps, only EntryAuth fields cross), reporting every dropped field. - detect-mcp: applyDeclaredMountAuth() routes a declared descriptor into the mount itself (matched by the entry URN), so the server card and auth.md read it too; runs before gating, so declaring auth marks the mount gated and reviewed. A declared "none" can't un-gate a detected wrapper — warned and ignored. - entries: overrides sanitized once up front; a declared status that contradicts a detected one wins but warns. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed declarations For each MCP server the wizard just gated, `ax init` now asks the optional follow-ups gating never covered: the OAuth authorization/token endpoints and a docs URL where a human obtains access. Answers land as a declared `auth` entry override in the generated ax.config — the same block a user would hand-write — so the next build publishes them to the catalog entry, the server card, and /auth.md. Enter skips any question; skipping all writes no entries key. Same prefill posture as siteUrl: a new detect-auth-metadata detector reads the app's committed declarations (never probes, never guesses provider paths) — - a static RFC 8414 public/.well-known/oauth-authorization-server document states endpoints verbatim → editable defaults, named with their source; - mcp-handler's protectedResourceHandler `authServerUrls` literals and a static RFC 9728 protected-resource document name the authorization server → printed as context above the questions (its endpoints live behind its own /.well-known, which ax won't fetch). The conventional app/.well-known/oauth-protected-resource/route.* is checked directly, since the route walker skips hidden directories. Answers are validated with the same safeHttpUrl guard the config schema enforces, so the wizard can't write a config the very next build rejects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…auth section Most real apps' agent lane is an API key behind their existing login, not OAuth — the wizard and the report now reflect that. - init: a scheme select precedes the endpoint questions — OAuth 2.0 (asks the two endpoints), API key/bearer (declares status "api_key", asks only the docs URL), or skip. OAuth is the default only when the source tree shows OAuth evidence (committed metadata / declared authorization server); otherwise the default is skip, so Enter-through never steers a basic-auth user into OAuth questions they can't answer and never writes a declaration they didn't actively choose. An active api_key choice is a declaration by itself, docs URL or not. - report: a structured `auth` section — one entry per published gated surface (scheme, declared-vs-derived, endpoints present, docsUrl, and an actionable note when something's missing) plus the detected auth provider (@clerk/nextjs, better-auth, @auth0/nextjs-auth0, next-auth / @auth/core) with a durable note on what it's worth for agent auth — deliberately no wiring snippets, which rot as provider APIs change; next-auth's note steers to the api_key lane since it signs humans in but exposes no authorization server agents can use. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s never carry secrets The declared-auth surface only ever collects URLs, and safeHttpUrl already keeps a pasted bare token out (not a URL). This closes the remaining leak vector: a real token embedded *inside* an otherwise-valid URL (?api_key=sk-live-…), which would have shipped verbatim in the public catalog and auth.md. credentialQueryParam() flags credential-named query parameters; the wizard re-asks with an explanation (the page itself can require login — the published URL must be clean), and sanitizeDeclaredAuth drops the field with a named reason for config-declared values, so both entry paths are covered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Provider-flavored RFC 9728 handlers (@clerk/mcp-tools'
protectedResourceHandlerClerk) resolve the authorization server from env
at runtime, so there is no committed issuer literal to read — but the
wiring itself is committed evidence the gated surface speaks OAuth. The
detector now records the route (resourceMetadataRoute); the wizard uses
it to default the scheme to OAuth, prints it as context ("endpoints are
agent-discoverable at runtime"), and lets an evidence-backed bare
status "oauth2" stand instead of downgrading to "unknown" — endpoint
questions stay optional because agents walk the metadata chain
themselves.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…auth checks - Interactive select/multi-select markers are ASCII-only (> (*) [x], plain hint text): the previous glyphs (❯ • · ↑↓) are ambiguous-width Unicode that some terminal/font configs render two columns wide, silently wrapping rows the redraw arithmetic counted as one physical row — the reported first-row-disappears corruption. - The scheme question defaults to API key when no OAuth evidence exists (skip is never the default): a gated surface can nearly always truthfully declare api_key, so an Enter-through now declares it instead of writing nothing. - Tighter intro copy, "(the MCP-spec path)" dropped from the OAuth label, and the docs-URL question now says what the URL is for (agents send their user there when they hit the auth wall). - report.ora.checks now speaks to Ora's auth checks: mcp-auth-mechanism (actionable while any gated surface ships status "unknown", with the declare-it note) and oauth-protected-resource (only when OAuth is declared — an api_key site doesn't get the OAuth ceiling held against it; addressed once RFC 9728 metadata is wired/committed/declared). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live Clerk-path testing surfaced the miss: the wizard asked for OAuth endpoint URLs even when a wired RFC 9728 protected-resource route proved agents discover them at runtime. Endpoint questions now run only when the user chose OAuth and the source tree has no OAuth to read — the one case a declaration is the sole way agents learn the endpoints. Committed RFC 8414 metadata is adopted verbatim (announced with its source, never asked); a wired protected-resource route or declared authorization server asks nothing, and the issuer context line now says "agents discover its endpoints from the metadata chain at runtime" instead of "paste below". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…OAuth access line
More live-testing feedback:
- Dropped the RFC 9728 context line and the per-mount "will declare
auth" recap — the config write confirmation covers it.
- Every CLI y/N now carries the [ax] prefix (added once, in
defaultConfirm, so injected test confirms see the bare question).
- The docs-URL question prefills the site origin so the user only types
the path; an unedited prefill means skip — a bare homepage is never a
docs URL someone chose on purpose.
- auth.md's "Get access" line for an OAuth surface without a docsUrl now
says the truth ("sign in through your MCP client via OAuth") instead
of nagging for a docs page that has no job in a self-service flow.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
OAuth is self-service — the agent's client runs the sign-in flow, so there is no credential page to ask about; the Clerk-path wizard now ends at the scheme choice. The docs question remains only for api_key, where a human genuinely fetches a credential somewhere, and its wording now matches its behavior: "complete the path; submitting it unchanged skips" (the prefill is a typing head start, never a value Enter would save). An OAuth answer with no backing (no evidence, no endpoints) declares nothing — the docs-only "unknown" path is gone with the question that fed it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The BuildReport gained the structured auth block (gated surfaces + detected provider) and new ora-check entries, so every fixture's committed golden was stale — CI's fixtures-build job diffs them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d gating Field-tested failure: a site wired withAx around a User-Agent bot gate whose matcher covered everything — so the catalog, server cards, llms.txt, auth.md, openapi.json, and the twins all returned the gate's 403 to script UAs (including Ora's own journey engine, Go-http-client), and the agent concluded the site published nothing. The build published; the middleware un-published. withAx now short-circuits requests for the manifest-listed discovery artifacts (and the twins' own paths) before the wrapped middleware runs — these files exist precisely to be read by agents the site has never heard of, so a reachable publication is part of withAx's contract, not the wrapped gate's jurisdiction. Routes and gated paths are untouched (page gating stays the app's policy), and protectDiscovery: false opts out for sites that genuinely gate their artifacts. The wiring recommendation now mentions the guarantee. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The previous regen ran against a stale dist/ (built before the wiring- recommendation text changed), so the goldens verified locally against old code while CI, building fresh, saw the new string in 16 fixtures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Aug 27, 2026
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.
What
Site owners can declare where agents authenticate,
ax initasks for it (prefilled when the source tree already states it), and the build report gets a structured auth section. Designed around the realistic maturity ladder: most apps' agent lane is an API key behind their existing login; OAuth is the spec-track upgrade for those whose auth provider already offers it.1. Config-declared entry auth
An entry override in
ax.configcan declare a first-classauthdescriptor (exactly Ora's secret-freeEntryAuthshape):Declared once, it flows to the catalog entry, the MCP server card's
authenticationblock, and the generated/auth.md. ForwithMcpAuth-wrapped MCP mounts — which detection can only describe as "requires auth, scheme unknown" — the declaration is routed into the mount itself, so all three surfaces agree.authtyped + validated by a closed sub-schema (status enum, absolute http(s) URLs ≤256 chars, capped lists, unknown keys fail loudly).sanitizeDeclaredAuth()applies the same secret-guard as detected descriptors (safeHttpUrl, caps, whitelist-only fields).applyDeclaredMountAuth()routes declarations into mounts pre-gating (declaring auth marks the mount gated + reviewed); a declared"none"can't un-gate a detected wrapper.2.
ax initasks how agents authenticateFor each gated MCP server, a scheme select then targeted follow-ups:
status "api_key", asks only for the docs URL where a human obtains a key.oauth-authorization-serverdocument states them (newdetect-auth-metadatadetector; also surfacesauthServerUrlsliterals fromprotectedResourceHandlerroutes — including underapp/.well-known/, which the route walker skips as hidden — and RFC 9728 documents as context).The default is OAuth only when the source tree shows OAuth evidence; otherwise skip — Enter-through never steers a basic-auth user into questions they can't answer, and never writes a declaration that wasn't an active choice. Answers are validated with the schema's own URL rule, so init can't write a config the next build rejects.
3. Report
authsection.ora/report.jsongains a structuredauthblock: one entry per published gated surface (path, scheme, declared-vs-derived, endpoints present, docsUrl, and an actionablenotewhen something's missing) plus the detected auth provider (@clerk/nextjs,better-auth,@auth0/nextjs-auth0,next-auth/@auth/core) with a durable note on what it means for agent auth. Deliberately no wiring snippets — provider APIs drift; the notes state stable facts and name the provider's own docs. next-auth's note steers to the api_key lane, since it signs humans in but exposes no authorization server agents can use.Testing
generateCatalog(catalog/card/auth.md/report.auth), auth-metadata detector (incl. comment scrubbing + hidden-dir route), provider detector, config rendering round-tripped through the real schema gate, and scripted-prompter init flows (oauth collect, prefill-approve, api_key, skip, docs-only, invalid-URL re-prompt, nothing-gated, evidence-based default).ax.config, and the next build publishes it to the catalog, the server card, auth.md, andreport.auth.🤖 Generated with Claude Code