import { createHash, randomUUID } from "node:crypto";
export class PermissionPolicyEngine {
    audit;
    #decisions = [];
    constructor(audit) {
        this.audit = audit;
    }
    evaluate(toolCall, context) {
        const denyOnlyDiagnostics = getExternalServiceMutationDenyOnlyDiagnostics(toolCall);
        const externalServiceMutation = Boolean(denyOnlyDiagnostics);
        const hardDenyReason = getHardDenyReason(toolCall);
        if (hardDenyReason) {
            return {
                verdict: "blocked",
                reason: hardDenyReason,
                externalServiceMutation,
                ...(denyOnlyDiagnostics ? { denyOnlyDiagnostics } : {}),
            };
        }
        const matching = this.#findMatchingDecision(toolCall, context, externalServiceMutation);
        if (matching) {
            if (matching.effect === "deny") {
                return {
                    verdict: "blocked",
                    reason: "A remembered permission denial matches this tool call.",
                    externalServiceMutation,
                    ...(denyOnlyDiagnostics ? { denyOnlyDiagnostics } : {}),
                };
            }
            return {
                verdict: "allowed",
                source: "existing_scope",
                decision: matching,
            };
        }
        return {
            verdict: "requires_approval",
            request: createPermissionRequest(toolCall, externalServiceMutation, denyOnlyDiagnostics),
        };
    }
    recordDecision(toolCall, option, context, capabilities = ["local_tools"]) {
        const scope = optionToScope(option);
        const pattern = patternForToolCall(toolCall, option);
        const event = this.audit.append({
            type: "permission.decision.recorded",
            severity: option.includes("escape") || option.includes("always")
                ? "warning"
                : "info",
            message: `Permission decision ${option} recorded for ${toolCall.name}.`,
            metadata: {
                option,
                scope,
                patternKind: patternKind(pattern),
                capabilities,
                workspaceId: context.workspaceId,
                sessionId: context.sessionId,
                tool: toolCall.name,
                risk: toolCall.risk,
            },
        });
        const decision = {
            option,
            effect: option === "deny_and_remember" ? "deny" : "allow",
            scope,
            pattern,
            capabilities,
            workspaceId: context.workspaceId,
            sessionId: context.sessionId,
            ...(context.userId ? { userId: context.userId } : {}),
            createdAt: new Date().toISOString(),
            auditId: event.id,
        };
        this.rememberDecision(decision);
        return decision;
    }
    rememberDecision(decision) {
        this.#decisions.push(decision);
    }
    forgetDecision(auditId) {
        const index = this.#decisions.findIndex((decision) => decision.auditId === auditId);
        if (index < 0)
            return false;
        this.#decisions.splice(index, 1);
        return true;
    }
    revoke(auditId) {
        const revoked = this.forgetDecision(auditId);
        if (!revoked)
            return false;
        this.audit.append({
            type: "permission.decision.revoked",
            severity: "warning",
            message: "Permission decision revoked.",
            metadata: { auditId },
        });
        return true;
    }
    listDecisions() {
        return [...this.#decisions];
    }
    #findMatchingDecision(toolCall, context, externalServiceMutation) {
        return this.#decisions.find((decision) => {
            if (decision.expiresAt && new Date(decision.expiresAt) <= new Date())
                return false;
            if (externalServiceMutation &&
                !decision.capabilities.includes("external_service_mutations")) {
                return false;
            }
            if (!scopeMatches(decision, context))
                return false;
            return patternMatches(decision, toolCall);
        });
    }
}
export function createPermissionRequest(toolCall, externalServiceMutation = isExternalServiceMutation(toolCall), denyOnlyDiagnostics = externalServiceMutation
    ? getExternalServiceMutationDenyOnlyDiagnostics(toolCall)
    : undefined) {
    const request = {
        id: randomUUID(),
        toolCall,
        reason: externalServiceMutation
            ? "External-service mutation requests are deny-only in this approval surface."
            : "No prior scoped permission decision matches this tool call.",
        options: externalServiceMutation
            ? ["deny"]
            : ["approve_once", "allow_for_session", "deny"],
        policyRecommendation: externalServiceMutation ? "deny" : "ask",
        suggestedScope: "once",
        capabilities: externalServiceMutation
            ? ["local_tools", "external_service_mutations"]
            : ["local_tools"],
        externalServiceMutation,
        ...(denyOnlyDiagnostics ? { denyOnlyDiagnostics } : {}),
    };
    if (externalServiceMutation) {
        request.externalServiceCapabilityDefault = "unchecked";
    }
    return request;
}
export function isExternalServiceMutation(toolCall) {
    return Boolean(getExternalServiceMutationDenyOnlyDiagnostics(toolCall));
}
export function hasExternalServiceMutationCommand(command) {
    return collectExternalServiceMutationCommandEvidence(command).length > 0;
}
export function getExternalServiceMutationDenyOnlyDiagnostics(toolCall) {
    const evidence = collectExternalServiceMutationEvidence(toolCall);
    if (evidence.length === 0)
        return undefined;
    return {
        policyClass: "external_service_mutation",
        riskReason: riskReasonForExternalMutationEvidence(evidence),
        evidence,
    };
}
const externalServiceCommandPatterns = [
    { pattern: /^git\s+push\b/, evidence: "git push" },
    {
        pattern: /^gh\s+pr\s+(create|merge|close|reopen|edit|comment|review|ready)\b/,
        evidence: "gh pr mutation",
    },
    {
        pattern: /^gh\s+issue\s+(create|close|reopen|comment|edit|delete|lock|unlock)\b/,
        evidence: "gh issue mutation",
    },
    {
        pattern: /^gh\s+release\s+(create|delete|edit|upload)\b/,
        evidence: "gh release mutation",
    },
    {
        pattern: /^gh\s+workflow\s+(run|enable|disable)\b/,
        evidence: "gh workflow mutation",
    },
    { pattern: /^gh\s+api\b/, evidence: "gh api" },
    {
        pattern: /^glab\s+mr\s+(create|merge|close|reopen|update|edit|note|approve|revoke|delete)\b/,
        evidence: "glab mr mutation",
    },
    {
        pattern: /^glab\s+issue\s+(create|close|reopen|update|edit|note|delete|lock|unlock)\b/,
        evidence: "glab issue mutation",
    },
    {
        pattern: /^glab\s+release\s+(create|delete|edit|upload)\b/,
        evidence: "glab release mutation",
    },
    { pattern: /^glab\s+api\b/, evidence: "glab api" },
    {
        pattern: /^az\s+repos\s+pr\s+(create|update|set-vote)\b/,
        evidence: "azure repos pr mutation",
    },
    {
        pattern: /^az\s+repos\s+pr\s+reviewer\s+(add|remove)\b/,
        evidence: "azure repos pr mutation",
    },
    {
        pattern: /^az\s+repos\s+pr\s+work-item\s+(add|remove)\b/,
        evidence: "azure repos pr mutation",
    },
    {
        pattern: /^az\s+repos\s+pr\s+comment\s+(create|update|delete)\b/,
        evidence: "azure repos pr mutation",
    },
    {
        pattern: /^az\s+repos\s+pr\s+policy\s+queue\b/,
        evidence: "azure repos pr mutation",
    },
    {
        pattern: /^az\s+repos\s+pr\s+iteration\s+status\s+(create|update|delete)\b/,
        evidence: "azure repos pr mutation",
    },
    { pattern: /^curl\b/, evidence: "curl" },
    { pattern: /^wget\b/, evidence: "wget" },
    { pattern: /\b(npm|pnpm|yarn)\s+publish\b/, evidence: "package publish" },
    { pattern: /\b(vercel|railway)\s+deploy\b/, evidence: "deployment CLI" },
    { pattern: /\bkubectl\s+apply\b/, evidence: "kubectl apply" },
    { pattern: /\bterraform\s+apply\b/, evidence: "terraform apply" },
    {
        pattern: /^aws(?:\s+\S+){0,8}\s+(create|update|delete|put|apply)[\w-]*\b/,
        evidence: "aws mutation",
    },
    {
        pattern: /^gcloud(?:\s+\S+){0,8}\s+(create|update|delete|deploy)[\w-]*\b/,
        evidence: "gcloud mutation",
    },
];
function collectExternalServiceMutationEvidence(toolCall) {
    const evidence = new Set();
    if (toolCall.risk === "external")
        evidence.add("tool risk: external");
    if (toolCall.risk === "network")
        evidence.add("tool risk: network");
    for (const commandEvidence of collectExternalServiceMutationCommandEvidence(toolCall.command ?? "")) {
        evidence.add(commandEvidence);
    }
    return [...evidence].slice(0, 6);
}
function collectExternalServiceMutationCommandEvidence(command) {
    const normalized = command.trim().toLowerCase();
    if (!normalized)
        return [];
    const evidence = new Set();
    for (const segment of splitShellCommandSegments(normalized)) {
        const executableSegment = stripShellEnvironmentPrefix(segment);
        for (const { pattern, evidence: label } of externalServiceCommandPatterns) {
            if (pattern.test(executableSegment))
                evidence.add(label);
        }
    }
    return [...evidence].slice(0, 6);
}
function riskReasonForExternalMutationEvidence(evidence) {
    const commandEvidence = evidence.filter((item) => !item.startsWith("tool risk:"));
    if (commandEvidence.length > 0) {
        return `Detected external-service mutation indicator${commandEvidence.length === 1 ? "" : "s"}: ${commandEvidence.join(", ")}.`;
    }
    if (evidence.includes("tool risk: external")) {
        return "Tool risk is external, so this request is deny-only in the current approval surface.";
    }
    return "Tool risk is network, so this request is deny-only in the current approval surface.";
}
function stripShellEnvironmentPrefix(segment) {
    let current = segment.trim();
    if (/^env(?:\s|$)/.test(current))
        current = current.replace(/^env\s*/, "");
    let changed = true;
    while (changed) {
        changed = false;
        const next = current
            .replace(/^(?:-i|--ignore-environment)\s+/, "")
            .replace(/^-u\s+\S+\s+/, "")
            .replace(/^--unset(?:=\S+|\s+\S+)\s+/, "")
            .replace(/^[A-Za-z_][A-Za-z0-9_]*=(?:'[^']*'|"[^"]*"|\S+)\s+/, "");
        if (next !== current) {
            current = next.trimStart();
            changed = true;
        }
    }
    return current;
}
function splitShellCommandSegments(command) {
    return command
        .split(/&&|\|\||;|\n|\|/g)
        .map((segment) => segment.trim())
        .filter(Boolean);
}
export function getHardDenyReason(toolCall) {
    const paths = toolCall.affectedPaths ?? [];
    const secretPath = paths.find((path) => /(^|\/)(\.env|\.env\..*|\.ssh|\.aws|\.npmrc|\.pypirc|\.netrc)(\/|$)/.test(path));
    if (secretPath) {
        return `Secret-like path is protected: ${secretPath}`;
    }
    const command = toolCall.command?.trim().toLowerCase() ?? "";
    const segments = splitShellCommandSegments(command);
    if (segments.some((segment) => /^rm\s+-rf\s+(\/|~|\.\.?($|\s))/.test(segment))) {
        return "Destructive filesystem removal is blocked by hard-deny guardrail.";
    }
    if (segments.some((segment) => /^sudo\b/.test(segment))) {
        return "sudo is blocked by hard-deny guardrail.";
    }
    if (/\b(curl|wget)\b.*\|\s*(sh|bash)\b/.test(command)) {
        return "Piping remote script into a shell is blocked by hard-deny guardrail.";
    }
    return undefined;
}
function optionToScope(option) {
    switch (option) {
        case "approve_once":
            return "once";
        case "allow_for_session":
            return "session";
        case "allow_for_project":
            return "project";
        case "always_allow":
            return "always";
        case "always_allow_tool_class":
            return "tool_class";
        case "full_escape_hatch_run":
            return "escape_hatch_run";
        case "full_escape_hatch_project":
            return "escape_hatch_project";
        case "full_escape_hatch_global":
            return "escape_hatch_global";
        case "deny":
        case "deny_and_remember":
            return "once";
    }
}
function patternForToolCall(toolCall, option) {
    if (option === "approve_once" || option === "deny")
        return `call:${toolCall.id}`;
    if (option === "deny_and_remember")
        return `tool:${toolCall.name}`;
    if (option === "always_allow_tool_class")
        return `tool:${toolCall.name}`;
    if (option.startsWith("full_escape_hatch"))
        return "escape-hatch:*";
    if (option === "allow_for_session") {
        if (toolCall.commandMatchKey) {
            return `command-key:${toolCall.commandMatchKey}`;
        }
        if (toolCall.command) {
            return `command-hash:${commandHashFor(toolCall.command)}`;
        }
        return `call:${toolCall.id}`;
    }
    if (toolCall.command)
        return `command:${normalizeCommandPattern(toolCall.command)}`;
    return `tool:${toolCall.name}`;
}
function commandHashFor(command) {
    return createHash("sha256")
        .update(normalizeExactCommand(command))
        .digest("base64url");
}
function normalizeExactCommand(command) {
    return command.trim().replace(/\s+/g, " ");
}
function normalizeCommandPattern(command) {
    const trimmed = command.trim();
    const firstToken = trimmed.split(/\s+/).slice(0, 2).join(" ");
    return `${firstToken}*`;
}
function scopeMatches(decision, context) {
    switch (decision.scope) {
        case "once":
            return decision.sessionId === context.sessionId;
        case "session":
        case "escape_hatch_run":
            return decision.sessionId === context.sessionId;
        case "project":
        case "escape_hatch_project":
            return decision.workspaceId === context.workspaceId;
        case "always":
        case "tool_class":
        case "escape_hatch_global":
            return true;
    }
}
function patternKind(pattern) {
    const separator = pattern.indexOf(":");
    return separator > 0 ? pattern.slice(0, separator) : pattern;
}
function patternMatches(decision, toolCall) {
    if (decision.pattern === "escape-hatch:*")
        return true;
    if (decision.pattern === `call:${toolCall.id}`)
        return true;
    if (decision.pattern === `tool:${toolCall.name}`)
        return true;
    if (decision.pattern.startsWith("command-key:") && toolCall.commandMatchKey) {
        const pattern = decision.pattern.slice("command-key:".length);
        return toolCall.commandMatchKey === pattern;
    }
    if (decision.pattern.startsWith("command-hash:") && toolCall.command) {
        const pattern = decision.pattern.slice("command-hash:".length);
        return commandHashFor(toolCall.command) === pattern;
    }
    if (decision.pattern.startsWith("command-exact:") && toolCall.command) {
        const pattern = decision.pattern.slice("command-exact:".length);
        return normalizeExactCommand(toolCall.command) === pattern;
    }
    if (decision.pattern.startsWith("command:") && toolCall.command) {
        const pattern = decision.pattern.slice("command:".length);
        if (pattern.endsWith("*")) {
            return toolCall.command.trim().startsWith(pattern.slice(0, -1));
        }
        return toolCall.command.trim() === pattern;
    }
    return false;
}
