Skip to content

Commit b03582a

Browse files
feat: Allow Installation-Scoped GitHub Tokens for Trusted Workers (#263)
* feat(code): allow installation-scoped GitHub tokens on trusted workers * fix(code): revalidate GitHub installation token grants --------- Co-authored-by: Lia <lia@librechat.ai>
1 parent cf0e668 commit b03582a

6 files changed

Lines changed: 459 additions & 38 deletions

File tree

‎docs/remote-bridge/worker-runbook.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,15 @@ filesystem root, but anyone able to alter a checkout's remote can select any
338338
repository where the App is installed; keep the App's installation scope narrow.
339339
Pass the checkout as the command working directory; changing directories only
340340
inside the shell cannot change the token chosen before command launch.
341+
For a trusted VM that needs to switch among repositories in the same installed
342+
account or organization inside one command, set
343+
`LIBRECHAT_CODE_GITHUB_TOKEN_SCOPE=installation`. The resolved installation
344+
token covers only repositories and permissions GitHub granted to that App
345+
installation. It refreshes after two minutes so newly approved permissions
346+
become available without a worker restart. The default is `repository`.
347+
Commands spanning different accounts or organizations must start in a checkout
348+
from the target account or organization; a shell `cd` cannot switch the
349+
installation chosen at command launch.
341350
Set `LIBRECHAT_CODE_GITHUB_INSTALLATION_ID` only as a legacy
342351
fixed-installation fallback; it cannot be combined with checkout routing.
343352

‎packages/code/README.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,17 @@ installed**. Use this mode only where the machine operator trusts the VM and
308308
the App's installation scope; the default `admitted` mode keeps the startup
309309
binding. Checkout routing requires an App without a fixed installation ID.
310310

311+
On a trusted VM, `--github-token-scope installation` (or
312+
`LIBRECHAT_CODE_GITHUB_TOKEN_SCOPE=installation`) mints one token for all
313+
repositories GitHub grants to the resolved App installation. This lets a
314+
command started in one checkout push to another repository in the same account
315+
or organization, including through `cd` or `git -C`, and use organization
316+
Projects. GitHub still enforces the installation's selected repositories and
317+
permissions. Tokens are shared by installation, refreshed after two minutes,
318+
and kept out of the sandbox's readable environment. The default remains
319+
`repository`. A command crossing to another account or organization still
320+
needs to start in a checkout belonging to that account or organization.
321+
311322
For compatibility with deployments that intentionally bind a worker to one
312323
installation, set the optional legacy
313324
`LIBRECHAT_CODE_GITHUB_INSTALLATION_ID` fallback.

‎packages/code/src/cli.test.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,35 @@ test('CLI rejects checkout routing outside a trusted VM or without repository-sc
358358
assert.match(invalid.stderr, /must be admitted or checkout/);
359359
});
360360

361+
test('CLI permits installation-scoped GitHub tokens only for a trusted VM with routed App auth', () => {
362+
const cli = fileURLToPath(new URL('./cli.js', import.meta.url));
363+
const base = {
364+
...process.env,
365+
LIBRECHAT_CODE_URL: 'http://127.0.0.1:1/v1',
366+
LIBRECHAT_CODE_WORKER_TOKEN: 'worker-secret',
367+
LIBRECHAT_CODE_WORKER_ID: 'engineering-vm',
368+
LIBRECHAT_CODE_WORKER_DIR: process.cwd(),
369+
LIBRECHAT_CODE_ALLOW_WORKSPACE_COMMANDS: 'true',
370+
LIBRECHAT_CODE_GITHUB_TOKEN: undefined,
371+
LIBRECHAT_CODE_GITHUB_APP_ID: '123',
372+
LIBRECHAT_CODE_GITHUB_PRIVATE_KEY_FILE: '/does/not/matter',
373+
LIBRECHAT_CODE_GITHUB_INSTALLATION_ID: undefined,
374+
LIBRECHAT_CODE_GITHUB_TOKEN_SCOPE: 'installation',
375+
};
376+
const restricted = spawnSync(process.execPath, [cli], { encoding: 'utf8', env: base });
377+
assert.match(restricted.stderr, /Installation-scoped GitHub tokens require the trusted-vm/);
378+
const trusted = spawnSync(process.execPath, [cli], {
379+
encoding: 'utf8',
380+
env: { ...base, LIBRECHAT_CODE_COMMAND_POLICY_PRESET: 'trusted-vm' },
381+
});
382+
assert.doesNotMatch(trusted.stderr, /Installation-scoped GitHub tokens require/);
383+
const fixed = spawnSync(process.execPath, [cli], {
384+
encoding: 'utf8',
385+
env: { ...base, LIBRECHAT_CODE_GITHUB_INSTALLATION_ID: '456' },
386+
});
387+
assert.match(fixed.stderr, /without a fixed installation ID/);
388+
});
389+
361390
test('CLI requires a runtime image for Docker supervision', () => {
362391
const result = spawnSync(
363392
process.execPath,

‎packages/code/src/cli.ts‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ function githubCredentials(args: string[]): {
157157
mode?: 'app' | 'token';
158158
repositoryRouting?: boolean;
159159
checkoutRouting?: boolean;
160+
installationTokenScope?: boolean;
160161
policyIdentity: string;
161162
} {
162163
const token = nonEmpty(process.env.LIBRECHAT_CODE_GITHUB_TOKEN);
@@ -191,6 +192,18 @@ function githubCredentials(args: string[]): {
191192
'Checkout GitHub repository routing requires a GitHub App without a fixed installation ID',
192193
);
193194
}
195+
const tokenScope =
196+
option(args, '--github-token-scope')?.trim().toLowerCase() ??
197+
process.env.LIBRECHAT_CODE_GITHUB_TOKEN_SCOPE?.trim().toLowerCase() ??
198+
'repository';
199+
if (tokenScope !== 'repository' && tokenScope !== 'installation') {
200+
throw new Error('GitHub token scope must be repository or installation');
201+
}
202+
if (tokenScope === 'installation' && (!hasApp || installationId)) {
203+
throw new Error(
204+
'Installation-scoped GitHub tokens require a GitHub App without a fixed installation ID',
205+
);
206+
}
194207
const configuredHostValue = nonEmpty(
195208
process.env.LIBRECHAT_CODE_GITHUB_HOST,
196209
);
@@ -225,16 +238,19 @@ function githubCredentials(args: string[]): {
225238
mode: 'app',
226239
repositoryRouting: !installationId,
227240
checkoutRouting: routing === 'checkout',
241+
installationTokenScope: tokenScope === 'installation',
228242
policyIdentity: gitHubAuthenticationPolicyIdentity({
229243
mode: 'app',
230244
host,
231245
appId,
232246
installationId,
233-
}) + (routing === 'checkout' ? ':routing:checkout' : ''),
247+
}) + (routing === 'checkout' ? ':routing:checkout' : '') +
248+
(tokenScope === 'installation' ? ':scope:installation' : ''),
234249
privateKeyPath,
235250
provider: new GitHubAppCredentialProvider({
236251
appId: appId!,
237252
installationId,
253+
tokenScope,
238254
privateKeyPath: privateKeyPath!,
239255
host,
240256
apiUrl,
@@ -568,6 +584,11 @@ async function run(
568584
'Checkout GitHub repository routing requires the trusted-vm command policy',
569585
);
570586
}
587+
if (github.installationTokenScope && commandPolicy.preset !== 'trusted-vm') {
588+
throw new Error(
589+
'Installation-scoped GitHub tokens require the trusted-vm command policy',
590+
);
591+
}
571592
const githubDomains = github.provider
572593
? github.host === 'github.com'
573594
? [...GITHUB_ALLOWED_DOMAINS]

0 commit comments

Comments
 (0)