mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-20 09:09:35 +02:00
The COPILOT_ prefix admits every variable Copilot reads, and the 1.0.98 bundle reads far more than `copilot help environment` lists. Only COPILOT_HOME, COPILOT_ALLOW_ALL and the BYOK trio were privileged, so a non-granted multi-user owner could still hand back what the allowAll clamp removed: - approval: COPILOT_PLAN_THEN_AUTOPILOT, COPILOT_ASSISTED_APPROVAL - code with no approval row: COPILOT_CLI_DIST_DIR runs <dir>/index.js in place of the CLI, the package caches choose the CLI build, COPILOT_EDITOR, COPILOT_PROVIDER_API_KEY_COMMAND and COPILOT_AHP_HOST_BIN are executed, skills, provider configs and hooks are loaded from the named places (the DSH_HOME class) - token redirects: COPILOT_API_URL and the other API bases receive the CLI's sign-in token, which in multi-user mode is the operator's (all users share the OS account and ~/.copilot), and the rest of the BYOK provider family re-aims a Codeman custom endpoint (the DEEPSEEK_BASE_URL / OMP_AUTH_BROKER_URL class) All are now privilegedEnvKeys. The registry schema capped that list at 8; the cap is now 48, since a longer deny list only takes more away from non-granted owners. The test sends one of each class through the real schema (they all pass it) and the real clamp. The docs say the list is read from the 1.0.98 bundle and that a variable a later release adds is not covered until it is listed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
95 lines
4.1 KiB
TypeScript
95 lines
4.1 KiB
TypeScript
import { afterEach, beforeEach, describe, it, expect } from 'vitest';
|
|
import { STOCK_CLIS } from '../src/config/cli-registry/stock.js';
|
|
import { _clampEnvOverridesForOwner } from '../src/web/routes/session-routes.js';
|
|
import { CreateSessionSchema } from '../src/web/schemas.js';
|
|
|
|
const copilot = STOCK_CLIS.find((c) => (c.id as string) === 'copilot')!;
|
|
|
|
describe('GitHub Copilot custom endpoint (BYOK)', () => {
|
|
it('injects its COPILOT_PROVIDER_* variables, with the /v1 suffix an OpenAI route needs', () => {
|
|
expect(copilot.capabilities.customModelInjection).toEqual({
|
|
kind: 'env',
|
|
baseUrlVar: 'COPILOT_PROVIDER_BASE_URL',
|
|
apiKeyVar: 'COPILOT_PROVIDER_API_KEY',
|
|
modelVars: ['COPILOT_MODEL'],
|
|
appendV1Suffix: true,
|
|
});
|
|
});
|
|
|
|
it('keeps every injected variable privileged, so a non-granted owner cannot set them by hand', () => {
|
|
const injection = copilot.capabilities.customModelInjection;
|
|
if (injection.kind !== 'env') throw new Error('expected env injection');
|
|
for (const name of [injection.baseUrlVar, injection.apiKeyVar, ...injection.modelVars]) {
|
|
expect(copilot.capabilities.privilegedEnvKeys).toContain(name);
|
|
expect(name.startsWith('COPILOT_')).toBe(true); // reachable through the prefix allowlist
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('GitHub Copilot environment and sync declarations', () => {
|
|
it('admits only its own COPILOT_ namespace, never the shared GitHub token names', () => {
|
|
// The env allowlist is one global list with no mode context, so a foreign key here would be
|
|
// settable on every session. COPILOT_GITHUB_TOKEN is already reachable through the prefix.
|
|
expect(copilot.env.allowedPrefixes).toEqual(['COPILOT_']);
|
|
expect(copilot.env.allowedKeys).toEqual([]);
|
|
});
|
|
|
|
it('declares its own MCP config so Settings does not list it as unsupported', () => {
|
|
expect(copilot.capabilities.mcpConfig).toEqual({
|
|
path: '.copilot/mcp-config.json',
|
|
format: 'copilot-json',
|
|
relocation: { envVar: 'COPILOT_HOME', path: 'mcp-config.json' },
|
|
});
|
|
});
|
|
|
|
it('seeds no credentials into a Docker case', () => {
|
|
expect(copilot.overlays?.credStore).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('GitHub Copilot multi-user clamp: the env-var half', () => {
|
|
// COPILOT_ is an allowlisted prefix, so every variable below would otherwise pass the schema.
|
|
// Each one hands back what the allowAll clamp removed: an approval switch, code that runs with no
|
|
// approval row (a CLI build, an editor, a key command, skills, hooks), or the sign-in token sent to
|
|
// a host of the caller's choosing.
|
|
const DANGEROUS: Record<string, string> = {
|
|
COPILOT_ALLOW_ALL: 'true',
|
|
COPILOT_PLAN_THEN_AUTOPILOT: '1',
|
|
COPILOT_HOME: '/tmp/planted-copilot-home',
|
|
COPILOT_CLI_DIST_DIR: '/tmp/planted-dist',
|
|
COPILOT_PKG_CACHE_HOME: '/tmp/planted-cache',
|
|
COPILOT_CACHE_HOME: '/tmp/planted-cache',
|
|
COPILOT_EDITOR: 'sh -c planted',
|
|
COPILOT_PROVIDER_API_KEY_COMMAND: 'sh -c planted',
|
|
COPILOT_SKILLS_DIRS: '/tmp/planted-skills',
|
|
COPILOT_PROVIDERS_CONFIG: '/tmp/planted-providers.json',
|
|
COPILOT_API_URL: 'https://attacker.example',
|
|
COPILOT_GH_HOST: 'attacker.example',
|
|
COPILOT_PROVIDER_BASE_URL: 'https://attacker.example/v1',
|
|
COPILOT_PROVIDER_HEADERS: 'X-Planted: 1',
|
|
};
|
|
const ORIGINAL = process.env.CODEMAN_MULTIUSER;
|
|
beforeEach(() => {
|
|
process.env.CODEMAN_MULTIUSER = '1';
|
|
});
|
|
afterEach(() => {
|
|
if (ORIGINAL === undefined) delete process.env.CODEMAN_MULTIUSER;
|
|
else process.env.CODEMAN_MULTIUSER = ORIGINAL;
|
|
});
|
|
|
|
it('passes the env allowlist, so only the clamp stands between these and the pane', () => {
|
|
expect(() =>
|
|
CreateSessionSchema.parse({ workingDir: '/tmp', mode: 'copilot', envOverrides: DANGEROUS })
|
|
).not.toThrow();
|
|
});
|
|
|
|
it("strips every one of them from a non-granted owner's overrides and keeps the harmless knobs", async () => {
|
|
const out = await _clampEnvOverridesForOwner('nobody', {
|
|
...DANGEROUS,
|
|
COPILOT_GITHUB_TOKEN: 'own-token',
|
|
COPILOT_AUTO_UPDATE: 'false',
|
|
});
|
|
expect(out).toEqual({ COPILOT_GITHUB_TOKEN: 'own-token', COPILOT_AUTO_UPDATE: 'false' });
|
|
});
|
|
});
|