openclaw/extensions/github-copilot/domain.ts
VasuBansal7576 104e67f55e
fix(auth): preserve Copilot tenant credentials during Doctor repair (#139131)
* fix(auth): preserve Copilot tenant credentials during Doctor repair

* test(auth): split Copilot ownership cases below file-size limit

* docs(copilot): clarify tenant credential sharing

Document that agents share Copilot credentials only when supported tenant scopes
match, and direct affected agents to authenticate for their intended tenant.

Co-authored-by: VasuBansal <148481383+VasuBansal7576@users.noreply.github.com>

* fix(auth): preserve Copilot tenant fences during peer settlement

Require refreshed shared Copilot credentials to match the fenced peer's
normalized routing scope before local fence removal. Cross-tenant peers remain
terminally fenced.

Co-authored-by: VasuBansal <148481383+VasuBansal7576@users.noreply.github.com>

* fix(auth): preserve Copilot tenant scope in ownership

Require Copilot OAuth ownership to validate normalized routing scope before
accepting an identical refresh generation. Same-tenant peers keep the shared
owner shortcut while cross-tenant credentials remain locally owned.

Co-authored-by: VasuBansal <148481383+VasuBansal7576@users.noreply.github.com>

* test(gateway): await plugin application receipt

* test(gateway): await node terminal events

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: VasuBansal <148481383+VasuBansal7576@users.noreply.github.com>
2026-09-24 17:40:17 -07:00

72 lines
2.7 KiB
TypeScript

// GitHub Copilot data-residency domain resolution.
//
// The allowlist and env/config precedence are provider policy. Deprecated SDK
// facades keep their dated compatibility copy until its removal window closes.
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
/** Public GitHub Copilot host used when no data-residency domain is configured. */
export const PUBLIC_GITHUB_COPILOT_DOMAIN = "github.com";
const GHE_DATA_RESIDENCY_HOST = /^[a-z0-9-]+\.ghe\.com$/;
export function isSupportedGithubCopilotDomain(raw: string | undefined | null): boolean {
const trimmed = (raw ?? "").trim().toLowerCase();
if (!trimmed) {
return true;
}
return (
/^[a-z0-9.-]+$/.test(trimmed) &&
(trimmed === PUBLIC_GITHUB_COPILOT_DOMAIN || GHE_DATA_RESIDENCY_HOST.test(trimmed))
);
}
export function normalizeGithubCopilotDomain(raw: string | undefined | null): string {
const trimmed = (raw ?? "").trim().toLowerCase();
return trimmed && isSupportedGithubCopilotDomain(trimmed)
? trimmed
: PUBLIC_GITHUB_COPILOT_DOMAIN;
}
/** Normalize legacy OAuth URL/domain spellings without accepting unsupported tenants. */
export function normalizeGithubCopilotOAuthScope(raw: string | undefined): string | undefined {
const trimmed = raw?.trim();
if (!trimmed) {
return PUBLIC_GITHUB_COPILOT_DOMAIN;
}
try {
const hostname = new URL(trimmed.includes("://") ? trimmed : `https://${trimmed}`).hostname;
return isSupportedGithubCopilotDomain(hostname) && hostname
? normalizeGithubCopilotDomain(hostname)
: undefined;
} catch {
return undefined;
}
}
function readConfiguredGithubCopilotDomain(config?: OpenClawConfig): string | undefined {
const params = config?.models?.providers?.["github-copilot"]?.params;
const value = params && typeof params === "object" ? params.githubDomain : undefined;
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
}
/**
* Resolve the GitHub Copilot host for this provider from (in priority order) the
* `COPILOT_GITHUB_DOMAIN` env override, the persisted
* `models.providers.github-copilot.params.githubDomain` config, then public
* `github.com`. The result always passes through the SDK allowlist
* (`normalizeGithubCopilotDomain`) so an unsafe value fails closed.
*/
export function resolveGithubCopilotDomain(params?: {
env?: NodeJS.ProcessEnv;
explicit?: string;
config?: OpenClawConfig;
}): string {
const env = params?.env ?? process.env;
const fromEnv = env.COPILOT_GITHUB_DOMAIN?.trim();
if (fromEnv) {
return normalizeGithubCopilotDomain(fromEnv);
}
if (params?.explicit) {
return normalizeGithubCopilotDomain(params.explicit);
}
return normalizeGithubCopilotDomain(readConfiguredGithubCopilotDomain(params?.config));
}