/**
 * AiderAdapter -- Aider terminal AI pair-programmer adapter.
 *
 * Aider (https://aider.chat) is a terminal-based AI pair programmer with
 * full project access: it reads, edits and creates files, runs git commands,
 * executes shell commands via `/run`, and can auto-commit. This adapter
 * governs every programmable surface Aider exposes and is purpose-built for
 * the risks that surface brings.
 *
 * Unlike in-IDE adapters, Aider operates on the raw filesystem and git
 * working tree. The governance challenges are therefore different:
 *
 *   - Shell commands (`/run`) are arbitrary -- fork bombs, curl-pipe-sh,
 *     and rm -rf / are live threats, not hypotheticals.
 *   - File context (`/add`, repo-map) sends file contents to an LLM provider.
 *     If the added file contains a .env or private key, the secret is now
 *     transmitted to an external party.
 *   - Auto-commit writes to git history with no human gate unless governance
 *     intervenes.
 *   - Voice input (transcription) may contain PII; CI deployments must never
 *     accept interactive input.
 *   - CI mode (`aider-ci-mode`) removes the human-in-the-loop entirely;
 *     stricter rules apply unconditionally.
 *
 * Deployment modes:
 *   aider-cli-individual  -- single developer; interactive; moderate defaults.
 *   aider-cli-team        -- shared dev environment; secretScanFiles forced on.
 *   aider-ci-mode         -- CI/CD pipeline; no interactive surfaces; shell
 *                            commands always denied; voice always denied;
 *                            auto-commit requires explicit opt-in.
 *
 * Hook surfaces (10):
 *
 *   wrapChat(prompt)
 *     User prompt forwarded to the LLM. Audited; packBinding enforced.
 *
 *   wrapFileEdit(filePath, searchBlock, replaceBlock, affectedFileCount)
 *     Aider-proposed SEARCH/REPLACE edit applied to a tracked file.
 *     Central governance surface. `affectedFileCount` > `multiFileEditCap`
 *     denied with `aider-multi-file-edit-cap-exceeded`.
 *
 *   wrapFileAdd(filePath, content)
 *     Aider adds a file to its context (sent to LLM provider).
 *     Secret scan applied unless `secretScanFiles: false` (individual only).
 *     Denied with `aider-file-add-secret-detected` on finding.
 *     Path allowlist (`allowedFilePaths`) bypasses the scan.
 *
 *   wrapGitCommit(message, stagedFiles)
 *     Aider auto-commit gate. Denied unless `allowAutoCommit: true`.
 *     In `aider-ci-mode`: also requires `allowAutoCommit: true`; without it
 *     denied unconditionally.
 *
 *   wrapGitDiff(args)
 *     Aider runs git diff to understand changes. Audited; low risk. Always
 *     allowed unless packBinding is missing.
 *
 *   wrapShellCommand(command)
 *     Aider's `/run` surface. HIGH RISK.
 *     In `aider-ci-mode`: ALWAYS denied with `aider-shell-not-permitted-ci-mode`.
 *     In non-CI modes: requires `allowShellCommands: true`.
 *     Dangerous patterns always denied regardless of config (see below).
 *     If `allowedShellPrefixes` configured, commands not matching any prefix
 *     denied with `aider-shell-prefix-not-allowed`.
 *
 *   wrapTestRun(testCommand)
 *     Aider runs the project test suite. Audited.
 *
 *   wrapLintRun(lintCommand)
 *     Aider runs the linter. Audited.
 *
 *   wrapVoiceInput(transcribedText)
 *     Aider voice transcription. Denied unless `allowVoiceInput: true`.
 *     In `aider-ci-mode`: ALWAYS denied.
 *
 *   wrapRepomapBuild(repomapContent)
 *     Aider builds a repo-map (file structure + code summaries) sent to LLM.
 *     Enforces `repoMapMaxBytes` cap (default 64 KB).
 *     Scans for embedded sensitive-path references (.env, .aws/, id_rsa,
 *     keystore). Denied with `aider-repomap-sensitive-paths-detected` on find.
 *
 * Dangerous shell patterns (always denied, no bypass):
 *   rm -rf /  (root filesystem destruction)
 *   fork bomb  :(){ :|: & };:
 *   curl|sh, wget|sh  (arbitrary code execution)
 *   sudo rm  (privileged deletion)
 *   > /dev/sda  (disk overwrite)
 *   dd if=  (disk-level write)
 *   chmod 777  (world-write escalation)
 *   git push --force (destructive push)
 *   git reset --hard (destructive reset)
 *
 * CI-mode safeguards (the differentiators):
 *   - `allowShellCommands: true` rejected at construction in CI mode.
 *   - Shell commands denied unconditionally in CI mode.
 *   - Voice input denied unconditionally in CI mode.
 *   - `secretScanFiles: false` rejected at construction in CI/team mode.
 *
 * Secret scanning:
 *   File content scanned for: AWS access keys, GitHub PATs, Anthropic API keys,
 *   OpenAI API keys, private key headers, generic SECRET/PASSWORD/API_KEY env
 *   vars, database URLs with embedded credentials, Bearer tokens.
 *   Secrets are NEVER echoed in audit entries (sanitized before emission).
 *
 * Pen-tests:
 *   PT-ADAPT-AIDER-1: empty packBinding[] denied at every wrap call
 *   PT-ADAPT-AIDER-2: secrets in scanned content never echoed in audit entries
 *   PT-ADAPT-AIDER-3: malformed config throws at construction (invalid mode,
 *                      negative cap, secretScanFiles: false in team/CI)
 *   PT-ADAPT-AIDER-4: CI mode with allowShellCommands: true rejected at construction
 *   PT-ADAPT-AIDER-5: dangerous shell patterns denied with explicit reason codes
 *                      regardless of allowedShellPrefixes setting
 *   PT-ADAPT-AIDER-6: file add with .env-like content denied with
 *                      aider-file-add-secret-detected even if user added it manually
 *
 * Spec: P3-SUB-IDE-12
 *
 * @connexum/ai-governance
 */
import { BaseIdeAdapter } from './base.js';
import { IdeSessionError, IdeAttributionError, } from './index.js';
export const AIDER_VALID_DEPLOYMENT_MODES = [
    'aider-cli-individual',
    'aider-cli-team',
    'aider-ci-mode',
];
// ---------------------------------------------------------------------------
// Dangerous shell patterns (always denied, no bypass path)
// ---------------------------------------------------------------------------
const AIDER_DANGEROUS_SHELL_PATTERNS = [
    { name: 'rm-rf-root', re: /\brm\s+(-[rR][fF]?|-[fF][rR]?|--recursive\s+--force|--force\s+--recursive)\s+\/\s*$/ },
    { name: 'rm-rf-root', re: /\brm\s+.*--recursive.*\/\s*$/ },
    { name: 'rm-rf-slash', re: /\brm\s+-[rRfF]{2}\s+\/[^/]/ },
    { name: 'fork-bomb', re: /:\s*\(\s*\)\s*\{\s*:\s*\|/ },
    { name: 'curl-pipe-sh', re: /\bcurl\b[^|]*\|\s*(bash|sh)\b/ },
    { name: 'wget-pipe-sh', re: /\bwget\b[^|]*\|\s*(bash|sh)\b/ },
    { name: 'sudo-rm', re: /\bsudo\s+rm\b/ },
    { name: 'dev-sda', re: />\s*\/dev\/(sda|hda|nvme|disk)/ },
    { name: 'dd-if', re: /\bdd\s+if=/ },
    { name: 'chmod-777', re: /\bchmod\s+(0?777|a\+rwx)\b/ },
    { name: 'git-force-push', re: /\bgit\s+push\s+.*--force/ },
    { name: 'git-reset-hard', re: /\bgit\s+reset\s+--hard/ },
    { name: 'kill-nine-1', re: /\bkill\s+-9\s+1\b/ },
    { name: 'rm-rf-home', re: /\brm\s+-[rRfF]{2}\s+~\s*$/ },
    { name: 'rm-rf-home', re: /\brm\s+-[rRfF]{2}\s+\$HOME\s*$/ },
];
// ---------------------------------------------------------------------------
// Secret scan patterns
// ---------------------------------------------------------------------------
const AIDER_SECRET_PATTERNS = [
    { name: 'AWS access key', re: /AKIA[0-9A-Z]{16}/ },
    { name: 'AWS secret key', re: /aws_secret_access_key\s*=\s*\S{20,}/i },
    { name: 'GitHub PAT', re: /ghp_[0-9A-Za-z]{36}|github_pat_[0-9A-Za-z_]{82}/ },
    { name: 'Anthropic API key', re: /sk-ant-[0-9A-Za-z\-_]{20,}/ },
    { name: 'OpenAI API key', re: /sk-[0-9A-Za-z]{20,}/ },
    { name: 'Private key header', re: /-----BEGIN (RSA|EC|OPENSSH|DSA|ED25519) PRIVATE KEY-----/ },
    { name: 'Generic secret env var', re: /(SECRET|PASSWORD|PASSWD|API_KEY|PRIVATE_KEY|ACCESS_TOKEN)\s*=\s*["']?\S{8,}["']?/i },
    { name: 'Bearer token', re: /Authorization:\s*Bearer\s+[0-9A-Za-z\-._~+/]{20,}/i },
    { name: 'Database URL with creds', re: /(postgres|mysql|mongodb|redis):\/\/[^:]+:[^@]+@/ },
    { name: 'env file marker', re: /^(export\s+)?(SECRET|PASSWORD|PASSWD|API_KEY|TOKEN|KEY)\w*=\S/im },
];
// ---------------------------------------------------------------------------
// Sensitive repo-map path markers
// ---------------------------------------------------------------------------
const AIDER_SENSITIVE_REPOMAP_PATTERNS = [
    { name: 'dotenv-file', re: /(^|\s|\/|\\)\.env(\s|$|\.)/ },
    { name: 'aws-creds', re: /\.aws[/\\](credentials|config)/ },
    { name: 'ssh-private-key', re: /(^|\s|\/)(id_rsa|id_ed25519|id_ecdsa|id_dsa)(\s|$)/ },
    { name: 'keystore', re: /\.(jks|p12|pfx|keystore)(\s|$)/ },
    { name: 'private-key-pem', re: /-----BEGIN.*PRIVATE KEY-----/ },
    { name: 'gcloud-creds', re: /application_default_credentials\.json/ },
    { name: 'kubeconfig', re: /(^|\s|\/)\.?kubeconfig(\s|$)/ },
];
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Scan text for known secret patterns. Returns first match or null. */
function scanForSecrets(text) {
    for (const pattern of AIDER_SECRET_PATTERNS) {
        if (pattern.re.test(text))
            return pattern;
    }
    return null;
}
/** Sanitize text so that any matched secret pattern value is redacted. */
function sanitizeSecretFromAudit(text) {
    // Replace any token that looks like a secret with [REDACTED]
    let out = text;
    for (const pattern of AIDER_SECRET_PATTERNS) {
        out = out.replace(pattern.re, '[REDACTED]');
    }
    return out;
}
/** Split a shell command on separators so sub-commands are checked independently. */
function splitShellSeparators(cmd) {
    return cmd
        .split(/;|&&|\|\||\$\(|`/)
        .map((s) => s.replace(/\)\s*$/, '').trim())
        .filter((s) => s.length > 0);
}
/** Check command for dangerous patterns. Returns finding or null. */
function checkDangerousShell(command) {
    const segments = splitShellSeparators(command);
    for (const segment of segments) {
        for (const p of AIDER_DANGEROUS_SHELL_PATTERNS) {
            if (p.re.test(segment))
                return p;
        }
    }
    return null;
}
/** Check whether a file path is on the allowlist (exact or prefix match). */
function isPathAllowed(filePath, allowedPaths) {
    if (allowedPaths.length === 0)
        return false;
    for (const allowed of allowedPaths) {
        if (filePath === allowed || filePath.startsWith(allowed + '/'))
            return true;
    }
    return false;
}
// ---------------------------------------------------------------------------
// AiderAdapter
// ---------------------------------------------------------------------------
export class AiderAdapter extends BaseIdeAdapter {
    adapterId = 'aider';
    ideId = 'aider';
    deploymentMode = 'aider-cli-individual';
    allowShellCommands = false;
    allowedShellPrefixes = [];
    allowAutoCommit = false;
    allowVoiceInput = false;
    secretScanFiles = true;
    allowedFilePaths = [];
    repoMapMaxBytes = 65536;
    multiFileEditCap = 20;
    // ---------------------------------------------------------------------------
    // Lifecycle
    // ---------------------------------------------------------------------------
    initialize(config) {
        // Base performs packId guard
        super.initialize(config);
        const mode = config.deploymentMode ?? 'aider-cli-individual';
        // Validate mode
        if (!AIDER_VALID_DEPLOYMENT_MODES.includes(mode)) {
            throw new IdeSessionError({
                message: `AiderAdapter: invalid deploymentMode '${mode}'. ` +
                    `Valid modes: ${AIDER_VALID_DEPLOYMENT_MODES.join(', ')}.`,
                ideId: this.ideId,
                adapterId: this.adapterId,
            });
        }
        // PT-ADAPT-AIDER-4: CI mode must never allow shell commands
        if (mode === 'aider-ci-mode' && config.allowShellCommands === true) {
            throw new IdeSessionError({
                message: 'AiderAdapter: allowShellCommands: true is rejected in aider-ci-mode. ' +
                    'CI pipelines must be deterministic. Shell command execution is not permitted ' +
                    'in CI mode regardless of configuration. Remove allowShellCommands from the ' +
                    'aider-ci-mode config or switch to aider-cli-individual / aider-cli-team.',
                ideId: this.ideId,
                adapterId: this.adapterId,
            });
        }
        // PT-ADAPT-AIDER-3: secretScanFiles cannot be disabled in team or CI mode
        if (config.secretScanFiles === false &&
            (mode === 'aider-cli-team' || mode === 'aider-ci-mode')) {
            throw new IdeSessionError({
                message: `AiderAdapter: secretScanFiles: false is not permitted in '${mode}'. ` +
                    'Secret scanning is mandatory in team and CI deployment modes to prevent ' +
                    'accidental transmission of secrets to LLM providers. ' +
                    'Remove secretScanFiles: false from the config.',
                ideId: this.ideId,
                adapterId: this.adapterId,
            });
        }
        // Validate numeric caps
        const repoMapMaxBytes = config.repoMapMaxBytes ?? 65536;
        if (typeof repoMapMaxBytes !== 'number' || repoMapMaxBytes < 1) {
            throw new IdeSessionError({
                message: `AiderAdapter: repoMapMaxBytes must be a positive number, got: ${String(repoMapMaxBytes)}.`,
                ideId: this.ideId,
                adapterId: this.adapterId,
            });
        }
        const multiFileEditCap = config.multiFileEditCap ?? 20;
        if (typeof multiFileEditCap !== 'number' || multiFileEditCap < 1) {
            throw new IdeSessionError({
                message: `AiderAdapter: multiFileEditCap must be a positive number, got: ${String(multiFileEditCap)}.`,
                ideId: this.ideId,
                adapterId: this.adapterId,
            });
        }
        this.deploymentMode = mode;
        this.allowShellCommands = config.allowShellCommands ?? false;
        this.allowedShellPrefixes = config.allowedShellPrefixes ?? [];
        this.allowAutoCommit = config.allowAutoCommit ?? false;
        this.allowVoiceInput = config.allowVoiceInput ?? false;
        this.secretScanFiles = config.secretScanFiles ?? true;
        this.allowedFilePaths = config.allowedFilePaths ?? [];
        this.repoMapMaxBytes = repoMapMaxBytes;
        this.multiFileEditCap = multiFileEditCap;
    }
    // ---------------------------------------------------------------------------
    // IdeAdapter: resolveUserIdentity
    //
    // Aider is a CLI tool. Identity comes from env var AIDER_SESSION_USER,
    // the session context userId, or the CLAUDE_SESSION_USER env var (reuse
    // the same convention for compatibility).
    // ---------------------------------------------------------------------------
    async resolveUserIdentity(session) {
        this.ensureInitialized();
        const envUser = process.env['AIDER_SESSION_USER']?.trim() ||
            process.env['CLAUDE_SESSION_USER']?.trim();
        if (envUser) {
            const identity = {
                userId: envUser,
                attributionMethod: 'session-env',
            };
            this.emitIdentityResolved(envUser, session.clientSessionId);
            return identity;
        }
        const ctxUser = session.identity?.userId?.trim() ||
            session.metadata?.['aiderId']?.trim() ||
            session.clientSessionId?.trim();
        if (ctxUser) {
            const identity = {
                userId: ctxUser,
                attributionMethod: 'session-env',
            };
            if (session.identity?.email)
                identity.email = session.identity.email;
            this.emitIdentityResolved(ctxUser, session.clientSessionId);
            return identity;
        }
        if (this.config?.allowAnonymousIdeSessions) {
            const identity = {
                userId: 'anonymous',
                attributionMethod: 'session-env',
            };
            this.emitIdentityResolved('anonymous', session.clientSessionId);
            return identity;
        }
        this.emitIdentityUnresolved(session.clientSessionId);
        throw new IdeAttributionError({
            message: 'AiderAdapter: cannot resolve user identity. Set the AIDER_SESSION_USER ' +
                'or CLAUDE_SESSION_USER environment variable, or provide a userId in the ' +
                'session context. To permit anonymous sessions, set ' +
                'allowAnonymousIdeSessions: true in adapter config.',
            ideId: this.ideId,
            adapterId: this.adapterId,
        });
    }
    // ---------------------------------------------------------------------------
    // IdeAdapter: getIdeCapabilities
    // ---------------------------------------------------------------------------
    getIdeCapabilities() {
        return {
            runtimeHooks: true,
            prBoundary: false,
            userIdentity: true,
            reviewerRouting: true,
            integrationMode: this.deploymentMode,
        };
    }
    // ---------------------------------------------------------------------------
    // IdeAdapter: interceptToolInvocation -- dispatcher
    //
    // Routes by ctx.tool to the typed wrap methods below.
    // ---------------------------------------------------------------------------
    async interceptToolInvocation(ctx) {
        this.ensureInitialized();
        const packBinding = this.config?.packId;
        if (!packBinding || packBinding.trim() === '') {
            const decision = {
                decision: 'DENY',
                reason: 'AiderAdapter: tool invocation denied -- packBinding is empty. ' +
                    'Pack-binding primacy: every invocation MUST carry a pack binding.',
            };
            return this.routeToolInvocation(ctx, decision);
        }
        const { tool, input } = ctx;
        switch (tool) {
            case 'chatRequested':
                return this.wrapChat(ctx, String(input['prompt'] ?? ''));
            case 'fileEditRequested':
                return this.wrapFileEdit(ctx, String(input['filePath'] ?? ''), String(input['searchBlock'] ?? ''), String(input['replaceBlock'] ?? ''), typeof input['affectedFileCount'] === 'number' ? input['affectedFileCount'] : 1);
            case 'fileAddRequested':
                return this.wrapFileAdd(ctx, String(input['filePath'] ?? ''), String(input['content'] ?? ''));
            case 'gitCommitRequested':
                return this.wrapGitCommit(ctx, String(input['message'] ?? ''), input['stagedFiles']);
            case 'gitDiffRequested':
                return this.wrapGitDiff(ctx, String(input['args'] ?? ''));
            case 'shellCommandRequested':
                return this.wrapShellCommand(ctx, String(input['command'] ?? ''));
            case 'testRunRequested':
                return this.wrapTestRun(ctx, String(input['testCommand'] ?? ''));
            case 'lintRunRequested':
                return this.wrapLintRun(ctx, String(input['lintCommand'] ?? ''));
            case 'voiceInputRequested':
                return this.wrapVoiceInput(ctx, String(input['transcribedText'] ?? ''));
            case 'repomapBuildRequested':
                return this.wrapRepomapBuild(ctx, String(input['repomapContent'] ?? ''));
            default:
                // Unknown surface: allow with audit (future Aider surfaces)
                return this.routeToolInvocation(ctx, {
                    decision: 'ALLOW',
                    reason: `AiderAdapter: unknown surface '${tool}' -- allowed with audit.`,
                });
        }
    }
    // ---------------------------------------------------------------------------
    // Surface: wrapChat
    // ---------------------------------------------------------------------------
    async wrapChat(ctx, prompt) {
        this.ensureInitialized();
        const deny = this.checkPackBinding(ctx);
        if (deny)
            return this.routeToolInvocation(ctx, deny);
        return this.routeToolInvocation(ctx, {
            decision: 'ALLOW',
            reason: `AiderAdapter: chat request audited (mode: ${this.deploymentMode}).`,
            metadata: { mode: this.deploymentMode, promptLength: prompt.length },
        });
    }
    // ---------------------------------------------------------------------------
    // Surface: wrapFileEdit
    // ---------------------------------------------------------------------------
    async wrapFileEdit(ctx, filePath, searchBlock, replaceBlock, affectedFileCount) {
        this.ensureInitialized();
        const deny = this.checkPackBinding(ctx);
        if (deny)
            return this.routeToolInvocation(ctx, deny);
        if (affectedFileCount > this.multiFileEditCap) {
            return this.routeToolInvocation(ctx, {
                decision: 'DENY',
                reason: `AiderAdapter: file edit denied -- affectedFileCount (${affectedFileCount}) ` +
                    `exceeds multiFileEditCap (${this.multiFileEditCap}). ` +
                    'Reason code: aider-multi-file-edit-cap-exceeded. ' +
                    'Reduce the number of files in a single edit or raise multiFileEditCap.',
                metadata: { affectedFileCount, multiFileEditCap: this.multiFileEditCap },
            });
        }
        return this.routeToolInvocation(ctx, {
            decision: 'ALLOW',
            reason: `AiderAdapter: file edit allowed for '${filePath}'.`,
            metadata: { filePath, affectedFileCount },
        });
    }
    // ---------------------------------------------------------------------------
    // Surface: wrapFileAdd
    // ---------------------------------------------------------------------------
    async wrapFileAdd(ctx, filePath, content) {
        this.ensureInitialized();
        const deny = this.checkPackBinding(ctx);
        if (deny)
            return this.routeToolInvocation(ctx, deny);
        // Path allowlist bypasses secret scan
        if (isPathAllowed(filePath, this.allowedFilePaths)) {
            return this.routeToolInvocation(ctx, {
                decision: 'ALLOW',
                reason: `AiderAdapter: file add allowed -- path '${filePath}' is on the allowlist.`,
                metadata: { filePath, allowlisted: true },
            });
        }
        // Secret scan
        if (this.secretScanFiles) {
            const finding = scanForSecrets(content);
            if (finding) {
                // PT-ADAPT-AIDER-2: never echo the secret value in the audit entry
                const sanitizedPath = sanitizeSecretFromAudit(filePath);
                return this.routeToolInvocation(ctx, {
                    decision: 'DENY',
                    reason: `AiderAdapter: file add blocked -- secret pattern '${finding.name}' detected in ` +
                        `file content of '${sanitizedPath}'. ` +
                        'Reason code: aider-file-add-secret-detected. ' +
                        'Adding this file to LLM context would transmit secrets to an external LLM provider. ' +
                        'Remove the secret from the file or add the path to allowedFilePaths if the file is intentionally public.',
                    metadata: { filePath: sanitizedPath, secretPattern: finding.name },
                });
            }
        }
        return this.routeToolInvocation(ctx, {
            decision: 'ALLOW',
            reason: `AiderAdapter: file add allowed for '${filePath}'.`,
            metadata: { filePath, secretScanned: this.secretScanFiles },
        });
    }
    // ---------------------------------------------------------------------------
    // Surface: wrapGitCommit
    // ---------------------------------------------------------------------------
    async wrapGitCommit(ctx, message, stagedFiles) {
        this.ensureInitialized();
        const deny = this.checkPackBinding(ctx);
        if (deny)
            return this.routeToolInvocation(ctx, deny);
        if (!this.allowAutoCommit) {
            return this.routeToolInvocation(ctx, {
                decision: 'DENY',
                reason: 'AiderAdapter: auto-commit denied -- allowAutoCommit is false. ' +
                    'Reason code: aider-auto-commit-not-permitted. ' +
                    'Set allowAutoCommit: true in AiderAdapterConfig to enable Aider auto-commits.',
                metadata: { mode: this.deploymentMode },
            });
        }
        return this.routeToolInvocation(ctx, {
            decision: 'ALLOW',
            reason: `AiderAdapter: git commit allowed (auto-commit enabled, mode: ${this.deploymentMode}).`,
            metadata: { stagedFileCount: stagedFiles?.length ?? 0 },
        });
    }
    // ---------------------------------------------------------------------------
    // Surface: wrapGitDiff
    // ---------------------------------------------------------------------------
    async wrapGitDiff(ctx, args) {
        this.ensureInitialized();
        const deny = this.checkPackBinding(ctx);
        if (deny)
            return this.routeToolInvocation(ctx, deny);
        return this.routeToolInvocation(ctx, {
            decision: 'ALLOW',
            reason: `AiderAdapter: git diff allowed (read-only operation).`,
            metadata: { args },
        });
    }
    // ---------------------------------------------------------------------------
    // Surface: wrapShellCommand
    //
    // HIGH RISK. Multiple deny layers applied in order.
    // ---------------------------------------------------------------------------
    async wrapShellCommand(ctx, command) {
        this.ensureInitialized();
        const deny = this.checkPackBinding(ctx);
        if (deny)
            return this.routeToolInvocation(ctx, deny);
        // Layer 1: CI mode -- unconditional deny regardless of config
        if (this.deploymentMode === 'aider-ci-mode') {
            return this.routeToolInvocation(ctx, {
                decision: 'DENY',
                reason: 'AiderAdapter: shell command denied in aider-ci-mode. ' +
                    'Reason code: aider-shell-not-permitted-ci-mode. ' +
                    'CI pipelines must be deterministic. Interactive shell commands are ' +
                    'not permitted in CI mode regardless of allowShellCommands configuration.',
                metadata: { mode: this.deploymentMode },
            });
        }
        // Layer 2: opt-in required
        if (!this.allowShellCommands) {
            return this.routeToolInvocation(ctx, {
                decision: 'DENY',
                reason: 'AiderAdapter: shell command denied -- allowShellCommands is false. ' +
                    'Reason code: aider-shell-not-permitted. ' +
                    'Set allowShellCommands: true in AiderAdapterConfig to enable shell execution.',
                metadata: { mode: this.deploymentMode },
            });
        }
        // Layer 3: dangerous patterns -- unconditional deny (no bypass via prefixes)
        // PT-ADAPT-AIDER-5
        const dangerousMatch = checkDangerousShell(command);
        if (dangerousMatch) {
            return this.routeToolInvocation(ctx, {
                decision: 'DENY',
                reason: `AiderAdapter: shell command denied -- dangerous pattern '${dangerousMatch.name}' detected. ` +
                    'Reason code: aider-shell-dangerous-pattern. ' +
                    'This pattern is unconditionally blocked regardless of allowedShellPrefixes. ' +
                    `Pattern matched: ${dangerousMatch.name}.`,
                metadata: { dangerousPattern: dangerousMatch.name },
            });
        }
        // Layer 4: prefix allowlist
        if (this.allowedShellPrefixes.length > 0) {
            const trimmedCmd = command.trim();
            const prefixMatch = this.allowedShellPrefixes.some((prefix) => trimmedCmd.startsWith(prefix));
            if (!prefixMatch) {
                return this.routeToolInvocation(ctx, {
                    decision: 'DENY',
                    reason: `AiderAdapter: shell command denied -- no allowed prefix matched for '${trimmedCmd.slice(0, 80)}'. ` +
                        'Reason code: aider-shell-prefix-not-allowed. ' +
                        `Allowed prefixes: [${this.allowedShellPrefixes.map((p) => `'${p}'`).join(', ')}].`,
                    metadata: { allowedPrefixes: this.allowedShellPrefixes },
                });
            }
        }
        return this.routeToolInvocation(ctx, {
            decision: 'ALLOW',
            reason: `AiderAdapter: shell command allowed.`,
            metadata: { commandLength: command.length },
        });
    }
    // ---------------------------------------------------------------------------
    // Surface: wrapTestRun
    // ---------------------------------------------------------------------------
    async wrapTestRun(ctx, testCommand) {
        this.ensureInitialized();
        const deny = this.checkPackBinding(ctx);
        if (deny)
            return this.routeToolInvocation(ctx, deny);
        return this.routeToolInvocation(ctx, {
            decision: 'ALLOW',
            reason: `AiderAdapter: test run allowed (mode: ${this.deploymentMode}).`,
            metadata: { testCommand },
        });
    }
    // ---------------------------------------------------------------------------
    // Surface: wrapLintRun
    // ---------------------------------------------------------------------------
    async wrapLintRun(ctx, lintCommand) {
        this.ensureInitialized();
        const deny = this.checkPackBinding(ctx);
        if (deny)
            return this.routeToolInvocation(ctx, deny);
        return this.routeToolInvocation(ctx, {
            decision: 'ALLOW',
            reason: `AiderAdapter: lint run allowed (mode: ${this.deploymentMode}).`,
            metadata: { lintCommand },
        });
    }
    // ---------------------------------------------------------------------------
    // Surface: wrapVoiceInput
    // ---------------------------------------------------------------------------
    async wrapVoiceInput(ctx, transcribedText) {
        this.ensureInitialized();
        const deny = this.checkPackBinding(ctx);
        if (deny)
            return this.routeToolInvocation(ctx, deny);
        // CI mode: always deny (non-interactive)
        if (this.deploymentMode === 'aider-ci-mode') {
            return this.routeToolInvocation(ctx, {
                decision: 'DENY',
                reason: 'AiderAdapter: voice input denied in aider-ci-mode. ' +
                    'Reason code: aider-voice-not-permitted-ci-mode. ' +
                    'CI pipelines are non-interactive. Voice input is not permitted in CI mode.',
                metadata: { mode: this.deploymentMode },
            });
        }
        if (!this.allowVoiceInput) {
            return this.routeToolInvocation(ctx, {
                decision: 'DENY',
                reason: 'AiderAdapter: voice input denied -- allowVoiceInput is false. ' +
                    'Reason code: aider-voice-not-permitted. ' +
                    'Voice transcription may contain PII. Set allowVoiceInput: true to enable.',
                metadata: { mode: this.deploymentMode },
            });
        }
        return this.routeToolInvocation(ctx, {
            decision: 'ALLOW',
            reason: 'AiderAdapter: voice input allowed.',
            metadata: { transcribedLength: transcribedText.length },
        });
    }
    // ---------------------------------------------------------------------------
    // Surface: wrapRepomapBuild
    // ---------------------------------------------------------------------------
    async wrapRepomapBuild(ctx, repomapContent) {
        this.ensureInitialized();
        const deny = this.checkPackBinding(ctx);
        if (deny)
            return this.routeToolInvocation(ctx, deny);
        // Byte cap
        const byteSize = Buffer.byteLength(repomapContent, 'utf8');
        if (byteSize > this.repoMapMaxBytes) {
            return this.routeToolInvocation(ctx, {
                decision: 'DENY',
                reason: `AiderAdapter: repo-map build denied -- content size (${byteSize} bytes) exceeds ` +
                    `repoMapMaxBytes cap (${this.repoMapMaxBytes} bytes). ` +
                    'Reason code: aider-repomap-too-large. ' +
                    'Large repo-maps transmit significant project data to LLM providers. ' +
                    'Raise repoMapMaxBytes or reduce the repo-map scope.',
                metadata: { byteSize, repoMapMaxBytes: this.repoMapMaxBytes },
            });
        }
        // Sensitive path scan
        for (const pattern of AIDER_SENSITIVE_REPOMAP_PATTERNS) {
            if (pattern.re.test(repomapContent)) {
                return this.routeToolInvocation(ctx, {
                    decision: 'DENY',
                    reason: `AiderAdapter: repo-map build denied -- sensitive path pattern '${pattern.name}' ` +
                        'detected in repo-map content. ' +
                        'Reason code: aider-repomap-sensitive-paths-detected. ' +
                        'Transmitting the repo-map would reveal sensitive file locations to the LLM provider. ' +
                        'Exclude the sensitive path from the Aider context.',
                    metadata: { sensitivePattern: pattern.name },
                });
            }
        }
        return this.routeToolInvocation(ctx, {
            decision: 'ALLOW',
            reason: `AiderAdapter: repo-map build allowed (${byteSize} bytes, within cap).`,
            metadata: { byteSize, repoMapMaxBytes: this.repoMapMaxBytes },
        });
    }
    // ---------------------------------------------------------------------------
    // Private helpers
    // ---------------------------------------------------------------------------
    checkPackBinding(ctx) {
        const packId = this.config?.packId;
        if (!packId || packId.trim() === '') {
            return {
                decision: 'DENY',
                reason: 'AiderAdapter: tool invocation denied -- packBinding is empty. ' +
                    'Pack-binding primacy: every invocation MUST carry a pack binding. ' +
                    `Tool: ${ctx.tool}.`,
            };
        }
        return null;
    }
}
//# sourceMappingURL=aider.js.map