takt/src/core/piece/permission-profile-resolution.ts
Tomohisa Takaoka 17232f9940
feat: add GitHub Copilot CLI as a new provider (#425)
* feat: add GitHub Copilot CLI as a new provider

Add support for GitHub Copilot CLI (@github/copilot) as a takt provider,
enabling the 'copilot' command to be used for AI-driven task execution.

New files:
- src/infra/copilot/client.ts: CLI client with streaming, session ID
  extraction via --share, and permission mode mapping
- src/infra/copilot/types.ts: CopilotCallOptions type definitions
- src/infra/copilot/index.ts: barrel exports
- src/infra/providers/copilot.ts: CopilotProvider implementing Provider
- src/__tests__/copilot-client.test.ts: 20 unit tests for client
- src/__tests__/copilot-provider.test.ts: 8 unit tests for provider

Key features:
- Spawns 'copilot -p' in non-interactive mode with --silent --no-color
- Permission modes: full (--yolo), edit (--allow-all-tools --no-ask-user),
  readonly (no permission flags)
- Session ID extraction from --share transcript files
- Real-time stdout streaming via onStream callbacks
- Configurable via COPILOT_CLI_PATH and COPILOT_GITHUB_TOKEN env vars

* fix: remove unused COPILOT_DEFAULT_MAX_AUTOPILOT_CONTINUES constant

* fix: address review feedback for copilot provider

- Remove excess maxAutopilotContinues property from test (#1 High)
- Extract cleanupTmpDir() helper to eliminate DRY violation (#2 Medium)
- Deduplicate chunk string conversion in stdout handler (#3 Medium)
- Remove 5 what/how comments that restate code (#4 Medium)
- Log readFile failure instead of silently swallowing (#5 Medium)
- Add credential scrubbing (ghp_/ghs_/gho_/github_pat_) for stderr (#6 Medium)
- Add buffer overflow tests for stdout and stderr (#7 Medium)
- Add pre-aborted AbortSignal test (#8 Low)
- Add mkdtemp failure fallback test (#9 Low)
- Add rm cleanup verification to fallback test (#10 Low)
- Log mkdtemp failure with debug level (#11 Persist)
- Add createLogger('copilot-client') for structured logging
2026-02-28 20:28:56 +09:00

83 lines
2.8 KiB
TypeScript

import type { PermissionMode } from '../models/types.js';
import type { ProviderPermissionProfiles, ProviderProfileName } from '../models/provider-profiles.js';
export interface ResolvePermissionModeInput {
movementName: string;
requiredPermissionMode?: PermissionMode;
provider?: ProviderProfileName;
projectProviderProfiles?: ProviderPermissionProfiles;
globalProviderProfiles?: ProviderPermissionProfiles;
}
export const DEFAULT_PROVIDER_PERMISSION_PROFILES: ProviderPermissionProfiles = {
claude: { defaultPermissionMode: 'edit' },
codex: { defaultPermissionMode: 'edit' },
opencode: { defaultPermissionMode: 'edit' },
cursor: { defaultPermissionMode: 'edit' },
copilot: { defaultPermissionMode: 'edit' },
mock: { defaultPermissionMode: 'edit' },
};
/**
* Resolve movement permission mode using provider profiles.
*
* Priority:
* 1. project provider_profiles.<provider>.movement_permission_overrides.<movement>
* 2. global provider_profiles.<provider>.movement_permission_overrides.<movement>
* 3. project provider_profiles.<provider>.default_permission_mode
* 4. global provider_profiles.<provider>.default_permission_mode
* 5. apply movement.required_permission_mode as minimum floor
*
* Throws when unresolved.
*/
export function resolveMovementPermissionMode(input: ResolvePermissionModeInput): PermissionMode {
if (!input.provider) {
return input.requiredPermissionMode ?? 'readonly';
}
const projectProfile = input.projectProviderProfiles?.[input.provider];
const globalProfile = input.globalProviderProfiles?.[input.provider];
const projectOverride = projectProfile?.movementPermissionOverrides?.[input.movementName];
if (projectOverride) {
return applyRequiredPermissionFloor(projectOverride, input.requiredPermissionMode);
}
const globalOverride = globalProfile?.movementPermissionOverrides?.[input.movementName];
if (globalOverride) {
return applyRequiredPermissionFloor(globalOverride, input.requiredPermissionMode);
}
if (projectProfile?.defaultPermissionMode) {
return applyRequiredPermissionFloor(projectProfile.defaultPermissionMode, input.requiredPermissionMode);
}
if (globalProfile?.defaultPermissionMode) {
return applyRequiredPermissionFloor(globalProfile.defaultPermissionMode, input.requiredPermissionMode);
}
if (input.requiredPermissionMode) {
return input.requiredPermissionMode;
}
return 'readonly';
}
const PERMISSION_MODE_RANK: Record<PermissionMode, number> = {
readonly: 0,
edit: 1,
full: 2,
};
function applyRequiredPermissionFloor(
resolvedMode: PermissionMode,
requiredMode?: PermissionMode,
): PermissionMode {
if (!requiredMode) {
return resolvedMode;
}
return PERMISSION_MODE_RANK[requiredMode] > PERMISSION_MODE_RANK[resolvedMode]
? requiredMode
: resolvedMode;
}