Skip to content

github-copilot (Enterprise): first prompt after Location boot sends the Enterprise token to api.githubcopilot.com #62959

Open

Description

@magro

Description

Follow-up to #55490, as requested when it was closed.

With a GitHub Copilot Enterprise (ghe.com) credential, the first prompt after a Location boots (e.g. after idle eviction) is sent to the public api.githubcopilot.com instead of the account's copilot-api.<enterprise>.ghe.com endpoint. It carries the Enterprise OAuth token in the Authorization header. A retry a moment later goes to the Enterprise endpoint and succeeds.

This is the race described in this comment on #55490 (loaded.baseURL in github-copilot.ts is only set after the initial /models fetch). The comment notes that the actual destination of the failed request was not captured. I can confirm it: opencode2 runs inside a sandbox whose egress proxy only injects the token for the Enterprise hosts, and its log records the blocked request:

2026-11-09T20:55:12.923Z WARN secret violation: placeholder detected where substitution or passthrough is not permitted
  host=api.githubcopilot.com method=POST path=/responses location=header

The opencode log for the same moment:

20:55:10.908  config load diagnostics (Location boot after ~3.5 h idle)
20:55:12.375  event.type=provider.updated
20:55:12.390  event.type=provider.updated
20:55:13.102  ERROR Failed to drain Session cause="AI.Error: Request blocked by secret policy."

It happened twice that day, both times on the first prompt after a long idle period (17:11 and 20:55 UTC). Outside the sandbox (same Enterprise account, plain opencode2 on the host), the first prompt after idle fails with Provider request failed with HTTP 401 and the retry succeeds, which matches the token being sent to the public endpoint.

Besides the failed first prompt, this sends an Enterprise credential to a host it is not meant for. Data-residency accounts in particular would expect their token to stay on their own endpoint.

Expected: requests for an Enterprise credential never use the default host. The base URL derived from the credential's enterpriseUrl metadata is known before the model fetch, so it could be applied immediately instead of after /models completes (or the first request could wait for the initial sync, as proposed in #38204).

Plugins

None

OpenCode version

2.2.6 (sandbox), 2.2.5 (host)

Steps to reproduce

  1. Log in to the github-copilot provider with a GitHub Enterprise (ghe.com) account.
  2. Leave a session idle until its Location is evicted.
  3. Send a prompt: it fails (HTTP 401, or blocked by an egress proxy that only allows the Enterprise host).
  4. Send it again: it succeeds.

Operating System

Linux (Fedora 44, x86_64); sandbox guest is Debian

Terminal

Not terminal-related

Activity

  1. opencodejr commented on Nov 9, 2026

    @opencodejr

    Thanks for the follow-up and the proxy log, they pinned this down.

    I reproduced it on 2.2.6. The script below:

    • imports a github-copilot OAuth credential with enterpriseUrl and apiEndpoint metadata;
    • points the account endpoint at a local mock that answers /models after a few seconds;
    • routes everything else through a logging proxy that refuses it;
    • sends a prompt with opencode run --standalone on a fresh Location.

    While /models is still loading, the prompt's requests go to api.githubcopilot.com:443, which the proxy refuses. Once the model list has loaded, requests go to the account's own endpoint. It still happens on the latest dev (1830078).

    The cause looks like the one described on #55490. The plugin's initial load runs in the background, and until it finishes, Copilot models fall back to the default api.githubcopilot.com base URL. The credential's own endpoint is already known at that point but isn't used.

    Related:

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Reproduces anomalyco/opencode#62959: with a GitHub Copilot Enterprise credential, the first prompt after a
    # cold start (fresh Location) goes to the public api.githubcopilot.com with the Enterprise token.
    #
    # Setup:
    #  - an HTTPS proxy (HTTPS_PROXY) logs every CONNECT host and refuses it (stands in for the egress proxy),
    #  - a mock Copilot API on 127.0.0.1 is the account's endpoint (credential metadata.apiEndpoint), and it answers
    #    /models after 8 s, standing in for a slow initial model sync,
    #  - a github-copilot OAuth credential with metadata { enterpriseUrl: "acme.ghe.com", apiEndpoint: <mock> }.
    # Then `opencode run --standalone` boots a fresh Location and sends a prompt immediately.
    #
    # Usage: bash repro.sh            (uses `opencode` from PATH)
    #        OC_CWD=<repo>/packages/cli OC="bun --conditions=browser <repo>/packages/cli/src/index.ts" bash repro.sh   (from source)
    set -u
    OC=${OC:-opencode}
    W=$(mktemp -d /tmp/opencode/repro.XXXX)
    mkdir -p "$W/home" "$W/project"
    export HOME="$W/home" XDG_CONFIG_HOME="$W/home/.config" XDG_DATA_HOME="$W/home/.local/share" \
      XDG_STATE_HOME="$W/home/.local/state" XDG_CACHE_HOME="$W/home/.cache"
    
    cat > "$W/servers.ts" <<'EOF'
    import net from "node:net"
    const log = (s: string) => console.log(new Date().toISOString(), s)
    // HTTPS proxy: log the CONNECT target and the Authorization of plain requests, refuse everything.
    net.createServer((sock) => {
      sock.once("data", (buf) => {
        const line = buf.toString().split("\r\n")[0]
        const host = line.split(" ")[1] ?? ""
        // Let the models.dev catalog through so the github-copilot models exist; refuse every other host.
        if (host.startsWith("models.opencode.ai:") || host.startsWith("models.dev:")) {
          const [h, p] = host.split(":")
          const up = net.connect(Number(p), h, () => {
            sock.write("HTTP/1.1 200 Connection Established\r\n\r\n")
            up.pipe(sock)
            sock.pipe(up)
          })
          up.on("error", () => sock.destroy())
          return
        }
        log(`PROXY BLOCKED ${line}`)
        sock.end("HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n")
      })
    }).listen(18080)
    // Mock enterprise Copilot API (the account's endpoint).
    Bun.serve({
      port: 18081,
      async fetch(req) {
        const url = new URL(req.url)
        log(`MOCK ${req.method} ${url.pathname} auth=${req.headers.get("authorization")}`)
        if (url.pathname.endsWith("/models")) {
          await Bun.sleep(8000)
          return Response.json({ data: [] })
        }
        return new Response("mock enterprise endpoint", { status: 500 })
      },
    })
    log("servers up")
    EOF
    bun "$W/servers.ts" > "$W/servers.log" 2>&1 &
    SRV=$!
    sleep 1
    
    cat > "$W/cred.json" <<'EOF'
    [{"id":"cred_enterprise_repro","integrationID":"github-copilot","label":"acme","active":true,
      "value":{"type":"oauth","methodID":"device","refresh":"gho_ENTERPRISE_TOKEN","access":"gho_ENTERPRISE_TOKEN","expires":0,
      "metadata":{"enterpriseUrl":"acme.ghe.com","apiEndpoint":"http://127.0.0.1:18081"}}}]
    EOF
    
    export HTTPS_PROXY=http://127.0.0.1:18080 https_proxy=http://127.0.0.1:18080 NO_PROXY=127.0.0.1,localhost no_proxy=127.0.0.1,localhost
    cd "${OC_CWD:-$W/project}"   # from source, set OC_CWD=/workspace/repo/packages/cli so its bunfig.toml applies
    $OC auth import --standalone "$W/cred.json"
    echo "--- servers log before the prompt"; cat "$W/servers.log"; : > "$W/servers.log"
    echo "--- opencode run (fresh Location, prompt sent immediately)"
    timeout 60 $OC run --standalone -m github-copilot/claude-opus-5.5 "hello" 2>&1 | tail -15
    sleep 1
    echo "--- servers log during the run"
    cat "$W/servers.log"
    kill $SRV

    Run it: bash repro.sh

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions