Skip to content

Commit 0307dea

Browse files
tobiasoortgiodl73-repoCopilot
authored
feat(github-copilot): support GitHub Enterprise data-residency Copilot auth (#99221)
Adds GitHub Enterprise data-residency support to the existing bundled GitHub Copilot provider. Maintainer proof: - GitHub CI green on head 54010a6 - `check-lint`, `check-additional-extension-bundled`, and `check-shrinkwrap` passed in CI - local `pnpm lint:extensions:bundled`, `pnpm lint`, and focused GitHub Copilot Vitest passed - AWS Crabbox proof passed - live microsoft.ghe.com device-flow/token-exchange/model-catalog proof passed Co-authored-by: Tobias Oort <[email protected]> Co-authored-by: Gio Della-Libera <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent f85d438 commit 0307dea

22 files changed

Lines changed: 1458 additions & 66 deletions

docs/docs_map.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7485,6 +7485,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
74857485
- Route: /providers/github-copilot
74867486
- Headings:
74877487
- H2: Three ways to use Copilot in OpenClaw
7488+
- H2: GitHub Enterprise (data residency)
74887489
- H2: Optional flags
74897490
- H2: Non-interactive onboarding
74907491
- H2: Memory search embeddings

docs/providers/github-copilot.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,75 @@ provider or agent runtime in three different ways.
101101
</Tab>
102102
</Tabs>
103103

104+
## GitHub Enterprise (data residency)
105+
106+
If your organization uses a data-residency GitHub Enterprise tenant (a
107+
`*.ghe.com` host such as `your-org.ghe.com`), Copilot lives on tenant-local
108+
endpoints rather than public `github.com`. OpenClaw exposes this as a
109+
first-class auth choice so you do not have to hand-edit URLs.
110+
111+
<Steps>
112+
<Step title="Pick the Enterprise auth choice">
113+
In onboarding or `openclaw models auth`, choose
114+
**GitHub Copilot (Enterprise / data residency)**. You will be prompted for
115+
your Enterprise domain (for example `your-org.ghe.com`), then the device
116+
login runs against that tenant.
117+
118+
Enter the tenant root only (`your-org.ghe.com`). Derived service hosts such
119+
as `api.your-org.ghe.com` or `copilot-api.your-org.ghe.com` are not accepted;
120+
OpenClaw derives those endpoints from the tenant root automatically.
121+
122+
```bash
123+
openclaw models auth login --provider github-copilot --method device-enterprise
124+
```
125+
126+
</Step>
127+
<Step title="Domain is persisted to config">
128+
The chosen host is stored under the provider params so later token refreshes
129+
and completions target the tenant automatically:
130+
131+
```json5
132+
{
133+
models: {
134+
providers: {
135+
"github-copilot": { params: { githubDomain: "your-org.ghe.com" } },
136+
},
137+
},
138+
}
139+
```
140+
141+
</Step>
142+
</Steps>
143+
144+
The device flow, token exchange, and completions resolve to
145+
`https://your-org.ghe.com/login/device/code`,
146+
`https://api.your-org.ghe.com/copilot_internal/v2/token`, and
147+
`https://copilot-api.your-org.ghe.com` respectively. Data-residency tokens carry
148+
a tenant stamp and no proxy hint, so the completions base URL falls back to the
149+
tenant Copilot host instead of the public endpoint.
150+
151+
<Note>
152+
Switching domains always re-runs the device login. If you already have a stored
153+
Copilot token and pick a different domain (public `github.com` ↔ a `*.ghe.com`
154+
tenant, or one tenant to another), OpenClaw will not reuse the existing token —
155+
it forces a fresh login so the token is scoped to the domain being written to
156+
config. Re-running login for the *same* domain still offers to reuse the current
157+
token. Switching back to public `github.com` clears the persisted
158+
`githubDomain` so config returns to the default.
159+
</Note>
160+
161+
<Note>
162+
The `COPILOT_GITHUB_DOMAIN` environment variable overrides the resolved domain
163+
for every Copilot path that resolves it — the Enterprise device login
164+
(`--method device-enterprise`), the standalone
165+
`openclaw models auth login-github-copilot` shortcut, token refresh, embeddings,
166+
and completions. Set it to your `*.ghe.com` host for fully headless or CI
167+
setups. Leave it unset (and the config param absent) to use public `github.com`.
168+
Logins persist the domain they minted the token for (and clear it when logging
169+
in against public `github.com`), so routing stays correct even after the
170+
environment variable is unset.
171+
</Note>
172+
104173
## Optional flags
105174

106175
| Command | Flag | Description |

extensions/copilot-proxy/openclaw.plugin.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
"choiceId": "copilot-proxy",
1515
"choiceLabel": "Copilot Proxy",
1616
"choiceHint": "Configure base URL + model ids",
17+
"assistantPriority": 3,
1718
"groupId": "copilot",
1819
"groupLabel": "Copilot",
19-
"groupHint": "GitHub + local proxy"
20+
"groupHint": "GitHub, GitHub Enterprise + Local Proxy"
2021
}
2122
],
2223
"configSchema": {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { describe, expect, it } from "vitest";
2+
import { PUBLIC_GITHUB_COPILOT_DOMAIN, resolveGithubCopilotDomain } from "./domain.js";
3+
4+
describe("github-copilot domain resolution", () => {
5+
const withDomain = (githubDomain: string) =>
6+
({
7+
models: { providers: { "github-copilot": { params: { githubDomain } } } },
8+
}) as never;
9+
10+
it("defaults to the public github.com host", () => {
11+
expect(PUBLIC_GITHUB_COPILOT_DOMAIN).toBe("github.com");
12+
expect(resolveGithubCopilotDomain({ env: {} })).toBe("github.com");
13+
});
14+
15+
it("resolves domain by precedence env > config > default", () => {
16+
expect(resolveGithubCopilotDomain({ env: {}, config: withDomain("cfg.ghe.com") })).toBe(
17+
"cfg.ghe.com",
18+
);
19+
expect(
20+
resolveGithubCopilotDomain({
21+
env: { COPILOT_GITHUB_DOMAIN: "env.ghe.com" },
22+
config: withDomain("cfg.ghe.com"),
23+
}),
24+
).toBe("env.ghe.com");
25+
});
26+
27+
it("fails closed to github.com for unsafe or non-tenant hosts", () => {
28+
expect(resolveGithubCopilotDomain({ env: {}, config: withDomain("acme.ghe.co") })).toBe(
29+
"github.com",
30+
);
31+
expect(resolveGithubCopilotDomain({ env: {}, config: withDomain("api.acme.ghe.com") })).toBe(
32+
"github.com",
33+
);
34+
expect(resolveGithubCopilotDomain({ env: { COPILOT_GITHUB_DOMAIN: "evil.com" } })).toBe(
35+
"github.com",
36+
);
37+
});
38+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// GitHub Copilot data-residency domain resolution.
2+
//
3+
// Lives inside the provider so the shared plugin SDK only needs to export the
4+
// security-critical host allowlist (`normalizeGithubCopilotDomain`). The
5+
// env/config precedence below is GitHub Copilot provider policy, not a
6+
// plugin-SDK contract, so it is intentionally not part of the SDK surface.
7+
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
8+
import { normalizeGithubCopilotDomain } from "openclaw/plugin-sdk/provider-auth";
9+
10+
/** Public GitHub Copilot host used when no data-residency domain is configured. */
11+
export const PUBLIC_GITHUB_COPILOT_DOMAIN = "github.com";
12+
13+
function readConfiguredGithubCopilotDomain(config?: OpenClawConfig): string | undefined {
14+
const params = config?.models?.providers?.["github-copilot"]?.params;
15+
const value = params && typeof params === "object" ? params.githubDomain : undefined;
16+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
17+
}
18+
19+
/**
20+
* Resolve the GitHub Copilot host for this provider from (in priority order) the
21+
* `COPILOT_GITHUB_DOMAIN` env override, the persisted
22+
* `models.providers.github-copilot.params.githubDomain` config, then public
23+
* `github.com`. The result always passes through the SDK allowlist
24+
* (`normalizeGithubCopilotDomain`) so an unsafe value fails closed.
25+
*/
26+
export function resolveGithubCopilotDomain(params?: {
27+
env?: NodeJS.ProcessEnv;
28+
config?: OpenClawConfig;
29+
}): string {
30+
const env = params?.env ?? process.env;
31+
const fromEnv = env.COPILOT_GITHUB_DOMAIN?.trim();
32+
if (fromEnv) {
33+
return normalizeGithubCopilotDomain(fromEnv);
34+
}
35+
return normalizeGithubCopilotDomain(readConfiguredGithubCopilotDomain(params?.config));
36+
}

extensions/github-copilot/embeddings.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { resolveConfiguredSecretInputString } from "openclaw/plugin-sdk/secret-input-runtime";
1515
import { fetchWithSsrFGuard, type SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
1616
import { resolveFirstGithubToken } from "./auth.js";
17+
import { resolveGithubCopilotDomain } from "./domain.js";
1718
import { DEFAULT_COPILOT_API_BASE_URL, resolveCopilotApiToken } from "./token.js";
1819

1920
const COPILOT_EMBEDDING_PROVIDER_ID = "github-copilot";
@@ -58,6 +59,7 @@ type GitHubCopilotEmbeddingClient = {
5859
headers?: Record<string, string>;
5960
env?: NodeJS.ProcessEnv;
6061
fetchImpl?: typeof fetch;
62+
githubDomain?: string;
6163
};
6264

6365
function isCopilotSetupError(err: unknown): boolean {
@@ -206,6 +208,7 @@ async function resolveGitHubCopilotEmbeddingSession(client: GitHubCopilotEmbeddi
206208
githubToken: client.githubToken,
207209
env: client.env,
208210
fetchImpl: client.fetchImpl,
211+
githubDomain: client.githubDomain,
209212
});
210213
const baseUrl = client.baseUrl?.trim() || token.baseUrl || DEFAULT_COPILOT_API_BASE_URL;
211214
return {
@@ -295,9 +298,14 @@ export const githubCopilotMemoryEmbeddingProviderAdapter: MemoryEmbeddingProvide
295298
throw new Error("No GitHub token available for Copilot embedding provider");
296299
}
297300

301+
const githubDomain = resolveGithubCopilotDomain({
302+
env: process.env,
303+
config: options.config,
304+
});
298305
const { token: copilotToken, baseUrl: resolvedBaseUrl } = await resolveCopilotApiToken({
299306
githubToken,
300307
env: process.env,
308+
githubDomain,
301309
});
302310
const baseUrl =
303311
options.remote?.baseUrl?.trim() || resolvedBaseUrl || DEFAULT_COPILOT_API_BASE_URL;
@@ -321,6 +329,7 @@ export const githubCopilotMemoryEmbeddingProviderAdapter: MemoryEmbeddingProvide
321329
env: process.env,
322330
fetchImpl: fetch,
323331
githubToken,
332+
githubDomain,
324333
headers: options.remote?.headers,
325334
model,
326335
});

0 commit comments

Comments
 (0)