http:
  routers:
    # Single public host: the one Go binary serves the UI, REST API, browser
    # terminal (/ws/<seg>/, via container labels) and MCP (/mcp) all from here.
    devgrail-web:
      rule: 'Host(`__DEVGRAIL_DOMAIN__`)'
      entryPoints: [websecure]
      service: devgrail-host
      tls:
        certResolver: letsencrypt

    devgrail-http:
      rule: 'Host(`__DEVGRAIL_DOMAIN__`)'
      entryPoints: [web]
      service: devgrail-host
      middlewares: [https-redirect]

    # Public MCP endpoint for external AI agents. Higher priority than the
    # catch-all web router so /mcp is tagged as external. The mcp-public
    # middleware overwrites X-DevGrail-Forwarded, so a client cannot spoof it.
    devgrail-mcp:
      rule: 'Host(`__DEVGRAIL_DOMAIN__`) && (Path(`/mcp`) || PathPrefix(`/mcp/`))'
      priority: 100
      entryPoints: [websecure]
      service: devgrail-host
      middlewares: [mcp-public]
      tls:
        certResolver: letsencrypt

    # OAuth 2.1 authorization server + discovery for the Claude.ai MCP connector
    # (internal/oauth). These endpoints are deliberately public and carry NO
    # X-DevGrail-Forwarded tag and NO ForwardAuth: discovery, registration and the
    # token endpoint must be reachable unauthenticated, and /oauth/authorize +
    # /oauth/consent do their own devgrail_session cookie gating in the app (the
    # cookie is forwarded with the request). Higher priority than the catch-all web
    # router; distinct paths from /api and /mcp so no conflict.
    devgrail-oauth:
      rule: 'Host(`__DEVGRAIL_DOMAIN__`) && (PathPrefix(`/oauth/`) || PathPrefix(`/.well-known/oauth-`))'
      priority: 100
      entryPoints: [websecure]
      service: devgrail-host
      tls:
        certResolver: letsencrypt

    # Traefik dashboard UI, exposed at /traefik/ under the DevGrail host and gated
    # behind admin auth. The dashboard's static bundle is internally served at
    # /dashboard/, so traefik-dashboard-rewrite maps /traefik back to /dashboard
    # before it reaches api@internal. (Its assets are relative, so they resolve
    # under the custom prefix; its data calls are NOT — see below.)
    traefik-dashboard:
      rule: 'Host(`__DEVGRAIL_DOMAIN__`) && PathPrefix(`/traefik`)'
      priority: 150
      entryPoints: [websecure]
      service: api@internal
      middlewares: [devgrail-admin-forwardauth, traefik-dashboard-rewrite]
      tls:
        certResolver: letsencrypt

    # The dashboard SPA hard-codes its data API at the root /api/ (it ignores any
    # path prefix), so that must be routed to api@internal too — it can't move
    # under /traefik. Same admin gate as the UI router.
    traefik-dashboard-api:
      rule: 'Host(`__DEVGRAIL_DOMAIN__`) && PathPrefix(`/api`)'
      priority: 150
      entryPoints: [websecure]
      service: api@internal
      middlewares: [devgrail-admin-forwardauth]
      tls:
        certResolver: letsencrypt

    # The dashboard claims the broad /api prefix, which would otherwise swallow
    # DevGrail's own REST API at /api/v1. Higher priority sends /api/v1 back to the
    # app; only the remaining /api/* (the dashboard's data calls) hit api@internal.
    devgrail-api:
      rule: 'Host(`__DEVGRAIL_DOMAIN__`) && PathPrefix(`/api/v1`)'
      priority: 200
      entryPoints: [websecure]
      service: devgrail-host
      tls:
        certResolver: letsencrypt

    # Internal MCP endpoint for AI agents running inside DevGrail workspaces. Bound
    # only to the unpublished "mcpinternal" entrypoint (:9100), so it is reachable
    # solely by containers on the edge network — never from the public internet.
    # The mcp-internal middleware strips X-DevGrail-Forwarded, so these requests are
    # treated as internal (internal API keys, no allow_external required). Any
    # Host matches: in-container agents target the Traefik service name directly.
    devgrail-mcp-internal:
      rule: 'Path(`/mcp`) || PathPrefix(`/mcp/`)'
      entryPoints: [mcpinternal]
      service: devgrail-host
      middlewares: [mcp-internal]

  services:
    devgrail-host:
      loadBalancer:
        servers:
          - url: "http://devgrail-server:12080"

  middlewares:
    devgrail-forwardauth:
      forwardAuth:
        address: "http://devgrail-server:12080/auth/forwardauth"
        trustForwardHeader: true

    # Gates the Traefik dashboard: the server validates the forwarded session
    # cookie and allows only admins. The Cookie header is forwarded with the
    # request, so no extra auth header configuration is needed.
    devgrail-admin-forwardauth:
      forwardAuth:
        address: "http://devgrail-server:12080/auth/dashboard-forwardauth"
        trustForwardHeader: true

    # Maps the public /traefik prefix onto /dashboard, the path the dashboard
    # bundle is internally served at. Slash-optional so both /traefik and
    # /traefik/ (and sub-paths) resolve.
    traefik-dashboard-rewrite:
      replacePathRegex:
        regex: "^/traefik/?(.*)"
        replacement: "/dashboard/$1"

    https-redirect:
      redirectScheme:
        scheme: https
        permanent: true

    # Marks requests as external (public) MCP access. Always overwrites the
    # header so a client-supplied value cannot impersonate internal traffic.
    mcp-public:
      headers:
        customRequestHeaders:
          X-DevGrail-Forwarded: "public"

    # Strips X-DevGrail-Forwarded on the internal entrypoint so a workspace cannot
    # supply its own value; an absent header is what the server treats as
    # internal. (Setting a custom request header to "" removes it in Traefik.)
    mcp-internal:
      headers:
        customRequestHeaders:
          X-DevGrail-Forwarded: ""
