Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions packages/code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,14 +180,17 @@ verification are implemented; use macOS, Linux, or WSL2. This also applies to Gi
private keys.

Git receives authentication through process-scoped `GIT_CONFIG_*` variables.
The same isolated config supplies the standard Git LFS filters; hosts using LFS
must install `git-lfs`, and checkout fails instead of silently leaving pointer
files when it is unavailable.
SRT replaces only the bearer-token portion with a sentinel inside the sandbox
and substitutes the real value in its host proxy only for `github.com` HTTPS
traffic. TLS termination is enabled for that substitution. The worker restores
the parent environment immediately after constructing the sandbox command; it
never writes credentials into the repository, a remote URL, or Git config.
When the GitHub CLI is installed, `gh api`, pull-request, issue, and workflow
commands receive the same installation scope through `GH_TOKEN` (or
`GH_ENTERPRISE_TOKEN` for GHES). The same isolated Git config supplies the
standard Git LFS filters; hosts using LFS must install `git-lfs`, and checkout
fails instead of silently leaving pointer files when it is unavailable.
SRT replaces each real credential with a sentinel inside the sandbox and
substitutes the real value in its host proxy only for the corresponding Git or
GitHub API host. TLS termination is enabled for that substitution. The worker
restores the parent environment immediately after constructing the sandbox
command; it never writes credentials into the repository, a remote URL, Git
config, or the GitHub CLI credential store.
GitHub's required domains are added to the command egress allowlist only when
authentication is configured. The worker identity, GitHub App key path, token
source variables, and mutation-quarantine record remain denied to sandboxed
Expand Down
15 changes: 5 additions & 10 deletions packages/code/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ import type { NativeProcessSandboxOptions } from './native-process.js';
import type { LocalWorkspaceConfig } from './workspace.js';
import {
GITHUB_ALLOWED_DOMAINS,
GITHUB_CREDENTIAL_ENV_NAME,
GitHubAppCredentialProvider,
gitHubCommandCredentialEnvironment,
gitHubMaskedCredentialVariables,
StaticGitHubCredentialProvider,
gitHubAuthenticationPolicyIdentity,
gitHubCredentialEnvironment,
normalizeGitHubHost,
wrapGitHubCredentialCommand,
} from './github.js';
Expand Down Expand Up @@ -783,16 +783,11 @@ async function run(
...(github.provider
? {
maskedEnvironment: {
variables: [
{
name: GITHUB_CREDENTIAL_ENV_NAME,
extract: '^(.+)$',
injectHosts: [github.host],
},
],
variables: gitHubMaskedCredentialVariables(github.host),
async resolve(signal?: AbortSignal) {
return gitHubCredentialEnvironment(
return gitHubCommandCredentialEnvironment(
await github.provider!.getCredential(signal),
github.host,
);
},
wrapCommand(command: string, platform: NodeJS.Platform) {
Expand Down
73 changes: 73 additions & 0 deletions packages/code/src/github.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import {
GitHubAppCredentialProvider,
StaticGitHubCredentialProvider,
gitHubAuthenticationPolicyIdentity,
gitHubCliTokenEnvironmentName,
gitHubCommandCredentialEnvironment,
gitHubMaskedCredentialVariables,
GITHUB_CREDENTIAL_ENV_NAME,
gitHubCredentialEnvironment,
normalizeGitHubHost,
Expand Down Expand Up @@ -150,6 +153,65 @@ test('builds process-scoped Git HTTPS authorization without embedding credential
assert.ok(!encodedCredential.includes('github_pat_'));
});

test('adds a GitHub CLI token only to the command-sandbox credential bundle', async () => {
const provider = new StaticGitHubCredentialProvider(
'github_pat_abcdefghijklmnopqrstuvwxyz',
);
const credential = await provider.getCredential();
assert.deepEqual(gitHubCommandCredentialEnvironment(credential), {
[GITHUB_CREDENTIAL_ENV_NAME]: Buffer.from(
'x-access-token:github_pat_abcdefghijklmnopqrstuvwxyz',
'utf8',
).toString('base64'),
GH_TOKEN: 'github_pat_abcdefghijklmnopqrstuvwxyz',
});
assert.deepEqual(
gitHubCommandCredentialEnvironment(credential, 'github.example.test'),
{
[GITHUB_CREDENTIAL_ENV_NAME]: Buffer.from(
'x-access-token:github_pat_abcdefghijklmnopqrstuvwxyz',
'utf8',
).toString('base64'),
GH_ENTERPRISE_TOKEN: 'github_pat_abcdefghijklmnopqrstuvwxyz',
},
);
});

test('selects the GitHub CLI token variable for public and enterprise hosts', () => {
assert.equal(gitHubCliTokenEnvironmentName('github.com'), 'GH_TOKEN');
assert.equal(
gitHubCliTokenEnvironmentName('github.example.test'),
'GH_ENTERPRISE_TOKEN',
);
});

test('restricts Git and GitHub CLI credential substitution to their respective hosts', () => {
assert.deepEqual(gitHubMaskedCredentialVariables('github.com'), [
{
name: GITHUB_CREDENTIAL_ENV_NAME,
extract: '^(.+)$',
injectHosts: ['github.com'],
},
{
name: 'GH_TOKEN',
extract: '^(.+)$',
injectHosts: ['api.github.com'],
},
]);
assert.deepEqual(gitHubMaskedCredentialVariables('github.example.test'), [
{
name: GITHUB_CREDENTIAL_ENV_NAME,
extract: '^(.+)$',
injectHosts: ['github.example.test'],
},
{
name: 'GH_ENTERPRISE_TOKEN',
extract: '^(.+)$',
injectHosts: ['github.example.test'],
},
]);
});

test('composes the masked credential with SRT Git configuration inside the sandbox', () => {
const wrapped = wrapGitHubCredentialCommand(
'git push',
Expand All @@ -160,10 +222,21 @@ test('composes the masked credential with SRT Git configuration inside the sandb
assert.match(wrapped, /http\.https:\/\/github\.com\/\.extraheader/);
assert.match(wrapped, /\$\{LIBRECHAT_CODE_GITHUB_AUTHORIZATION\}/);
assert.match(wrapped, /unset LIBRECHAT_CODE_GITHUB_AUTHORIZATION/);
assert.doesNotMatch(wrapped, /unset GH_TOKEN/);
assert.equal(wrapped.match(/Authorization: Basic/g)?.length, 1);
assert.ok(!wrapped.includes('github_pat_'));
});

test('targets GitHub CLI at an enterprise host without exposing its token', () => {
const wrapped = wrapGitHubCredentialCommand(
'gh pr create',
'github.example.test',
'linux',
);
assert.match(wrapped, /GH_HOST=github\.example\.test/);
assert.doesNotMatch(wrapped, /GH_ENTERPRISE_TOKEN=/);
});

test('rejects an insecure GitHub App API endpoint before reading the private key', () => {
assert.throws(
() =>
Expand Down
40 changes: 40 additions & 0 deletions packages/code/src/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,43 @@ export function gitHubCredentialEnvironment(
};
}

export function gitHubCommandCredentialEnvironment(
credential: GitHubCredential,
host = 'github.com',
): Record<string, string> {
return {
...gitHubCredentialEnvironment(credential),
[gitHubCliTokenEnvironmentName(host)]: credential.value,
};
}

export function gitHubCliTokenEnvironmentName(host: string): string {
return host === 'github.com' ? 'GH_TOKEN' : 'GH_ENTERPRISE_TOKEN';
}

export function gitHubApiHost(host: string): string {
return host === 'github.com' ? 'api.github.com' : host;
}

export function gitHubMaskedCredentialVariables(host: string): Array<{
name: string;
injectHosts: string[];
extract: string;
}> {
return [
{
name: GITHUB_CREDENTIAL_ENV_NAME,
extract: '^(.+)$',
injectHosts: [host],
},
{
name: gitHubCliTokenEnvironmentName(host),
extract: '^(.+)$',
injectHosts: [gitHubApiHost(host)],
},
];
}

export function gitHubAuthenticationPolicyIdentity(options: {
mode?: 'app' | 'token';
host: string;
Expand Down Expand Up @@ -249,17 +286,20 @@ export function wrapGitHubCredentialCommand(
platform: NodeJS.Platform = process.platform,
): string {
const key = `http.https://${host}/.extraheader`;
const cliHost = host === 'github.com' ? undefined : host;
if (platform === 'win32') {
return [
'set "GIT_CONFIG_GLOBAL=NUL"',
'set "GIT_CONFIG_NOSYSTEM=1"',
...(cliHost ? [`set "GH_HOST=${cliHost}"`] : []),
`set "GIT_CONFIG_PARAMETERS='http.proxyAuthMethod=basic' '${key}=Authorization: Basic %${GITHUB_CREDENTIAL_ENV_NAME}%'"`,
`set "${GITHUB_CREDENTIAL_ENV_NAME}="`,
command,
].join(' && ');
}
return [
'export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1',
...(cliHost ? [`export GH_HOST=${cliHost}`] : []),
`export GIT_CONFIG_PARAMETERS="'http.proxyAuthMethod=basic' '${key}=Authorization: Basic \${${GITHUB_CREDENTIAL_ENV_NAME}}'"`,
`unset ${GITHUB_CREDENTIAL_ENV_NAME}`,
command,
Expand Down