"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.PermissionEngine = void 0;
const path = __importStar(require("path"));
const registry_1 = require("../tools/registry");
const pathResolution_1 = require("../tools/pathResolution");
const EDIT_TOOLS = new Set(['file_write', 'file_edit']);
const FILE_MUTATION_TOOLS = new Set(['file_write', 'file_edit', 'file_move', 'file_delete']);
const READ_TOOLS = new Set(registry_1.BUILTIN_TOOL_ENTRIES.filter((entry) => entry.tool.isReadOnly).map((entry) => entry.tool.name));
const PROTECTED_EXTERNAL_FILENAMES = new Set([
    '.env',
    'dockerfile',
    'journal.py',
    'package.json',
    'pyproject.toml',
    'setup.py',
    'telegram_sender.py',
]);
const PROTECTED_EXTERNAL_EXTENSIONS = new Set(['.bat', '.cmd', '.cjs', '.js', '.mjs', '.ps1', '.py', '.sh', '.ts']);
/**
 * META_TOOLS are harness-internal learning/memory tools that mutate only
 * `.harness/` scratch space — never user code, never the shell. They are
 * harmless to auto-approve under `acceptEdits`, and blocking them wastes
 * autonomous turns when an agent tries to record reflections or consolidate
 * memory between substantive tool calls.
 */
const META_TOOLS = new Set([
    'reflect',
    'analyze_patterns',
    'promote_pattern',
    'consolidate',
    'evolve',
    'improve_skill',
    'memory_write',
    'memory_read',
]);
class PermissionEngine {
    rules;
    mode;
    /**
     * Local kill-switch state used when no shared `KillSwitch` is wired. Tests
     * and standalone callers fall back to this; the server passes its shared
     * `KillSwitch` so per-session engines see live state.
     */
    killSwitchActive = false;
    killSwitchReason = '';
    killSwitch;
    trustLadder;
    constructor(rules = [], mode = 'default', trustLadder, killSwitch) {
        this.rules = rules;
        this.mode = mode;
        this.trustLadder = trustLadder;
        this.killSwitch = killSwitch;
    }
    /** Replace or clear the trust-ladder provider at runtime. */
    setTrustLadder(provider) {
        this.trustLadder = provider;
    }
    /**
     * Attach (or detach) a shared `KillSwitch` after construction. Any future
     * kill-switch read/write goes through the shared instance.
     */
    setKillSwitch(killSwitch) {
        this.killSwitch = killSwitch;
    }
    /** Engage the global kill switch. While active, every tool call is denied. */
    engageKillSwitch(reason = 'Kill switch engaged.') {
        if (this.killSwitch) {
            this.killSwitch.engage(reason);
            return;
        }
        this.killSwitchActive = true;
        this.killSwitchReason = reason;
    }
    /** Release the kill switch and resume normal evaluation. */
    releaseKillSwitch() {
        if (this.killSwitch) {
            this.killSwitch.release();
            return;
        }
        this.killSwitchActive = false;
        this.killSwitchReason = '';
    }
    isKillSwitchActive() {
        return this.killSwitch ? this.killSwitch.isActive() : this.killSwitchActive;
    }
    getKillSwitchReason() {
        return this.killSwitch ? this.killSwitch.getReason() : this.killSwitchReason;
    }
    evaluate(call) {
        if (this.isKillSwitchActive()) {
            return { decision: 'deny', reason: this.getKillSwitchReason() || 'Kill switch active.' };
        }
        // Trust-ladder pre-check (no-op when not provided).
        if (this.trustLadder) {
            const cap = this.trustLadder.capabilityOf(call);
            if (cap) {
                const rung = this.trustLadder.rungOf(cap);
                if (rung === 0)
                    return { decision: 'deny', reason: `Trust ladder rung 0 (shadow) for capability \`${cap}\` — observe only.` };
                if (rung === 1)
                    return { decision: 'deny', reason: `Trust ladder rung 1 (suggest) for capability \`${cap}\` — surface a card, do not execute.` };
                if (rung === 3)
                    return { decision: 'ask', reason: `Trust ladder rung 3 (confirm) for capability \`${cap}\` — typed confirmation required.` };
                // rungs 2 and 4 fall through to standard evaluation
            }
        }
        // Phase 1: Deny rules always evaluated first — deny overrides everything
        for (const rule of this.rules) {
            if (rule.type === 'deny' && this.matchesRule(rule, call)) {
                return { decision: 'deny', reason: `Denied by rule: ${rule.tool}`, rule };
            }
        }
        if (this.requiresProtectedExternalFileConfirmation(call)) {
            return { decision: 'ask', reason: 'Protected external program file requires confirmation.' };
        }
        // Phase 2: Allow rules
        for (const rule of this.rules) {
            if (rule.type === 'allow' && this.matchesRule(rule, call)) {
                return { decision: 'allow', reason: `Allowed by rule: ${rule.tool}`, rule };
            }
        }
        // Phase 3: Mode-based default
        return this.modeDefault(call);
    }
    async evaluateAsync(call) {
        const result = this.evaluate(call);
        return {
            allowed: result.decision === 'allow',
            reason: result.reason,
        };
    }
    matchesRule(rule, call) {
        // Wildcard match
        if (rule.tool === '*')
            return true;
        // Exact tool name match
        if (rule.tool === call.name) {
            // If rule has a pattern, check it against the serialized input
            if (rule.pattern) {
                const inputStr = JSON.stringify(call.input);
                return inputStr.includes(rule.pattern);
            }
            return true;
        }
        return false;
    }
    modeDefault(call) {
        switch (this.mode) {
            case 'dontAsk':
                // Allow everything not explicitly denied
                return { decision: 'allow', reason: 'Mode: dontAsk — no matching deny rule' };
            case 'acceptEdits':
                // Auto-approve reads, file edits, and harness-internal meta tools;
                // ask for everything else (notably bash and arbitrary executors).
                if (READ_TOOLS.has(call.name) || EDIT_TOOLS.has(call.name) || META_TOOLS.has(call.name)) {
                    return { decision: 'allow', reason: 'Mode: acceptEdits — read/edit/meta auto-approved' };
                }
                return { decision: 'ask', reason: 'Mode: acceptEdits — requires approval' };
            case 'default':
            default:
                // Read-only tools auto-approved; everything else asks
                if (READ_TOOLS.has(call.name)) {
                    return { decision: 'allow', reason: 'Mode: default — read-only auto-approved' };
                }
                return { decision: 'ask', reason: 'Mode: default — requires approval' };
        }
    }
    addRule(rule) {
        this.rules.push(rule);
    }
    setMode(mode) {
        this.mode = mode;
    }
    getMode() {
        return this.mode;
    }
    getRules() {
        return this.rules;
    }
    requiresProtectedExternalFileConfirmation(call) {
        if (!FILE_MUTATION_TOOLS.has(call.name))
            return false;
        return mutationPaths(call).some((targetPath) => isProtectedExternalProgramPath(targetPath));
    }
}
exports.PermissionEngine = PermissionEngine;
function mutationPaths(call) {
    const input = call.input ?? {};
    return [input.path, input.from, input.to, input.source, input.destination]
        .filter((value) => typeof value === 'string' && value.trim().length > 0);
}
function isProtectedExternalProgramPath(rawPath) {
    const target = path.resolve(rawPath);
    if (isInsideOrEqualPath(target, process.cwd()))
        return false;
    const externalRoot = (0, pathResolution_1.getAllowedExternalPaths)().find((allowedPath) => isInsideOrEqualPath(target, allowedPath));
    if (!externalRoot)
        return false;
    const basename = path.basename(target).toLowerCase();
    if (PROTECTED_EXTERNAL_FILENAMES.has(basename))
        return true;
    // Check every dotted suffix so that 'malware.bat.txt' or 'evil.tar.sh' is
    // still treated as protected — a trailing-extension-only check (`.txt`)
    // would let attackers chain extensions to bypass.
    const segments = basename.split('.');
    for (let i = 1; i < segments.length; i++) {
        const ext = '.' + segments.slice(i).join('.');
        const lastExt = '.' + segments[i];
        if (PROTECTED_EXTERNAL_EXTENSIONS.has(ext))
            return true;
        if (PROTECTED_EXTERNAL_EXTENSIONS.has(lastExt))
            return true;
    }
    return false;
}
function isInsideOrEqualPath(child, parent) {
    const rel = path.relative(parent, child);
    return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
}
//# sourceMappingURL=engine.js.map