"use strict";
// Promotion gate for learning candidates.
//
// The harness already extracts learning candidates from session traces
// and lets a human (or auto-curator) promote them into the persistent
// patterns library. This module adds an evidence-led gate in front of
// promotion: a candidate may only graduate when (a) it has accumulated
// enough successful eval runs against the frozen dataset, AND (b) it
// shows no safety violations in its recent traces.
//
// Mirrors the Claw-Eval blueprint: completion is weighted, robustness
// adds a tail, but safety acts as a multiplicative gate that suppresses
// promotion regardless of completion score. Pure & dependency-free; the
// HTTP layer wires it into the existing review endpoint.
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.DEFAULT_SAFETY_RULES = void 0;
exports.scanSafety = scanSafety;
exports.scanSafetyText = scanSafetyText;
exports.countPassingRuns = countPassingRuns;
exports.evaluatePromotionGate = evaluatePromotionGate;
exports.compileSafetyRule = compileSafetyRule;
exports.loadSafetyRules = loadSafetyRules;
const silentFailureSink_1 = require("../observability/silentFailureSink");
/** Built-in rule set. Conservative on purpose — extend in dedicated files later. */
exports.DEFAULT_SAFETY_RULES = [
    {
        id: 'secret.aws-key',
        label: 'AWS access key id',
        severity: 'high',
        pattern: /\bAKIA[0-9A-Z]{16}\b/,
        scopes: ['outcome', 'prompt'],
    },
    {
        id: 'secret.aws-session-token',
        label: 'AWS session token',
        severity: 'high',
        // Session tokens start with ASIA followed by 16 base32 chars.
        pattern: /\bASIA[0-9A-Z]{16}\b/,
        scopes: ['outcome', 'prompt'],
    },
    {
        id: 'secret.gcp-service-account',
        label: 'GCP service account JSON',
        severity: 'high',
        pattern: /"type"\s*:\s*"service_account"[\s\S]{0,200}"private_key"/,
        scopes: ['outcome'],
    },
    {
        id: 'secret.github-token',
        label: 'GitHub fine-grained / classic personal access token',
        severity: 'high',
        pattern: /\bgh[ps]_[A-Za-z0-9]{36,}\b/,
        scopes: ['outcome', 'prompt'],
    },
    {
        id: 'secret.slack-token',
        label: 'Slack bot/user token',
        severity: 'high',
        pattern: /\bxox[abp]-[A-Za-z0-9-]{10,}\b/,
        scopes: ['outcome', 'prompt'],
    },
    {
        id: 'secret.private-key',
        label: 'PEM private key block',
        severity: 'high',
        pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
        scopes: ['outcome', 'prompt'],
    },
    {
        id: 'secret.ssh-private-key',
        label: 'OpenSSH private key block',
        severity: 'high',
        pattern: /-----BEGIN OPENSSH PRIVATE KEY-----/,
        scopes: ['outcome', 'prompt'],
    },
    {
        id: 'secret.bearer-token',
        label: 'Bearer token in plaintext',
        severity: 'medium',
        pattern: /Bearer\s+[A-Za-z0-9_\-\.]{16,}/,
        scopes: ['outcome'],
    },
    {
        id: 'secret.jwt',
        label: 'Bare JWT-shaped token',
        severity: 'medium',
        // header.payload.signature with base64url segments. Conservative
        // length floor avoids matching short test fixtures.
        pattern: /\beyJ[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\b/,
        scopes: ['outcome'],
    },
    {
        id: 'injection.role-override',
        label: 'Prompt-injection role override',
        severity: 'medium',
        pattern: /(?:ignore|disregard|forget)\s+(?:the\s+)?(?:above|previous|earlier|prior|all)\s+(?:instructions?|rules?|prompts?)/i,
        scopes: ['prompt', 'outcome'],
    },
    {
        id: 'injection.system-prompt-leak',
        label: 'Asks the model to reveal its system prompt',
        severity: 'medium',
        pattern: /reveal\s+(?:your|the)\s+(?:system\s+)?(?:prompt|instructions)/i,
        scopes: ['prompt'],
    },
    {
        id: 'shell.rm-rf-root',
        label: 'Destructive recursive delete from root',
        severity: 'high',
        // Match `rm -rf /` (with optional flag order) but not `rm -rf /tmp/foo`.
        pattern: /\brm\s+-[a-z]*r[a-z]*f?[a-z]*\s+\/\s*(?:[\s;&|]|$)/,
        scopes: ['outcome', 'prompt'],
    },
    {
        id: 'shell.curl-pipe-bash',
        label: 'Pipe-curl-to-shell pattern',
        severity: 'high',
        pattern: /\bcurl\s+[^\n|]+\|\s*(?:sudo\s+)?(?:bash|sh|zsh)\b/,
        scopes: ['outcome', 'prompt'],
    },
    {
        id: 'sensitive.dotenv-read',
        label: 'Reads or writes a .env-like file',
        severity: 'low',
        pattern: /(?:^|[\\\/])\.env(?:\.[\w-]+)?\b/,
        scopes: ['outcome'],
    },
    {
        id: 'sensitive.aws-credentials-path',
        label: 'Reads ~/.aws/credentials',
        severity: 'medium',
        pattern: /\.aws[\\\/]credentials\b/,
        scopes: ['outcome', 'prompt'],
    },
    {
        id: 'sensitive.ssh-private-path',
        label: 'Reads ~/.ssh/id_* private key path',
        severity: 'medium',
        pattern: /\.ssh[\\\/]id_(?:rsa|ed25519|ecdsa|dsa)\b(?!\.pub)/,
        scopes: ['outcome', 'prompt'],
    },
];
const SEVERITY_RANK = { low: 1, medium: 2, high: 3 };
/**
 * Run all safety rules against the candidate + extra content. Returns
 * every match (sorted high → low so the UI can show worst first).
 */
function scanSafety(candidate, extraScannedContent = [], rules = exports.DEFAULT_SAFETY_RULES) {
    const haystacks = [
        { scope: 'prompt', text: candidate.prompt ?? '' },
        { scope: 'outcome', text: candidate.outcome ?? '' },
        ...candidate.toolNames.map((name) => ({ scope: 'tool_name', text: name })),
        ...extraScannedContent.map((entry) => ({ scope: entry.source, text: entry.text ?? '' })),
    ];
    return scanHaystacks(haystacks, rules);
}
/**
 * Same as `scanSafety` but accepts arbitrary text snippets so callers
 * outside the learning-candidate flow (curator, heartbeat skill checks,
 * agent definition validators) can reuse the rule engine without
 * fabricating a fake candidate.
 */
function scanSafetyText(text, scope = 'outcome', rules = exports.DEFAULT_SAFETY_RULES) {
    return scanHaystacks([{ scope, text }], rules);
}
function scanHaystacks(haystacks, rules) {
    const violations = [];
    for (const rule of rules) {
        for (const haystack of haystacks) {
            if (!rule.scopes.includes(haystack.scope))
                continue;
            if (!haystack.text)
                continue;
            const match = rule.pattern.exec(haystack.text);
            if (!match)
                continue;
            const matchStart = match.index;
            const excerpt = haystack.text.slice(Math.max(0, matchStart - 12), matchStart + match[0].length + 12).replace(/\s+/g, ' ').slice(0, 80);
            violations.push({
                ruleId: rule.id,
                ruleLabel: rule.label,
                severity: rule.severity,
                matchedIn: haystack.scope,
                excerpt,
            });
        }
    }
    return violations.sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]);
}
/**
 * Count how many of the most-recent N runs passed every result. A run
 * "passes" iff every result inside it is `pass` (no failures), so the
 * counter mirrors the Claw-Eval Pass@k semantics with a threshold of 1.
 */
function countPassingRuns(runs, window) {
    const trimmed = runs.slice(-window);
    let passing = 0;
    for (const run of trimmed) {
        if (run.results.length === 0)
            continue;
        if (run.results.every((result) => result.status === 'pass')) {
            passing += 1;
        }
    }
    return { considered: trimmed.length, passing };
}
/**
 * Evaluate the promotion gate. Pure — returns a structured verdict that
 * the HTTP layer decides whether to honour.
 */
function evaluatePromotionGate(input) {
    const config = input.config ?? {};
    const requiredPasses = Math.max(1, config.requiredPasses ?? 3);
    const trialsWindow = Math.max(requiredPasses, config.trialsWindow ?? 5);
    const maxAllowed = config.maxAllowedSeverity ?? 'low';
    const violations = scanSafety(input.candidate, input.extraScannedContent ?? [], config.safetyRules ?? exports.DEFAULT_SAFETY_RULES);
    const { considered, passing } = countPassingRuns(input.recentEvalRuns ?? [], trialsWindow);
    const passAtAll = considered > 0 && passing === considered;
    // Safety acts as a multiplicative gate: any violation above the
    // allowed-severity ceiling blocks promotion regardless of completion.
    const blockingViolations = violations.filter((violation) => SEVERITY_RANK[violation.severity] > SEVERITY_RANK[maxAllowed]);
    if (blockingViolations.length > 0) {
        const top = blockingViolations[0];
        return {
            allowed: false,
            reason: `Safety violation (${top.severity}): ${top.ruleLabel}`,
            passCount: passing,
            consideredRuns: considered,
            requiredPasses,
            safetyViolations: violations,
            passAtAll,
        };
    }
    if (!input.candidate.accepted) {
        return {
            allowed: false,
            reason: 'Candidate is not accepted (extractor rejected it).',
            passCount: passing,
            consideredRuns: considered,
            requiredPasses,
            safetyViolations: violations,
            passAtAll,
        };
    }
    if (passing < requiredPasses) {
        return {
            allowed: false,
            reason: `Need ${requiredPasses} successful eval run(s) within the last ${trialsWindow}; have ${passing} of ${considered}.`,
            passCount: passing,
            consideredRuns: considered,
            requiredPasses,
            safetyViolations: violations,
            passAtAll,
        };
    }
    return {
        allowed: true,
        reason: `Eligible: ${passing}/${considered} recent runs passed; no blocking safety violations.`,
        passCount: passing,
        consideredRuns: considered,
        requiredPasses,
        safetyViolations: violations,
        passAtAll,
    };
}
/**
 * Compile a serialized rule into a SafetyRule. Returns null when the
 * pattern is invalid; callers can choose whether to log + skip.
 */
function compileSafetyRule(serialized) {
    if (!serialized || typeof serialized.pattern !== 'string' || !serialized.id)
        return null;
    if (!Array.isArray(serialized.scopes) || serialized.scopes.length === 0)
        return null;
    if (!['low', 'medium', 'high'].includes(serialized.severity))
        return null;
    let regex;
    try {
        regex = new RegExp(serialized.pattern, serialized.flags ?? '');
    }
    catch (err) {
        (0, silentFailureSink_1.recordSwallowed)('promotionGate.compileSafetyRule', err, { id: serialized.id, pattern: serialized.pattern });
        return null;
    }
    return {
        id: serialized.id,
        label: serialized.label || serialized.id,
        severity: serialized.severity,
        pattern: regex,
        scopes: serialized.scopes.filter((scope) => scope === 'prompt' || scope === 'outcome' || scope === 'tool_name'),
    };
}
/**
 * Load `.harness/safety-rules.json` from `projectDir`. Returns the
 * default rule set merged with any valid custom rules. Custom rules
 * with the same id as a built-in REPLACE the built-in (last-write-wins
 * by id) so users can tighten or relax shipping rules.
 *
 * Pure I/O wrapper: returns DEFAULT_SAFETY_RULES when the file is
 * missing or unreadable so callers never have to handle the empty case.
 */
async function loadSafetyRules(projectDir) {
    // Lazy-import fs/path so the pure scanner stays usable in environments
    // without a real filesystem (e.g. browser bundlers, in-memory tests).
    const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
    const path = await Promise.resolve().then(() => __importStar(require('path')));
    const fp = path.join(projectDir, '.harness', 'safety-rules.json');
    let raw;
    try {
        raw = await fs.readFile(fp, 'utf-8');
    }
    catch {
        return exports.DEFAULT_SAFETY_RULES;
    }
    let parsed;
    try {
        parsed = JSON.parse(raw);
    }
    catch {
        return exports.DEFAULT_SAFETY_RULES;
    }
    if (!Array.isArray(parsed))
        return exports.DEFAULT_SAFETY_RULES;
    const compiled = [];
    for (const entry of parsed) {
        const rule = compileSafetyRule(entry);
        if (rule)
            compiled.push(rule);
    }
    if (compiled.length === 0)
        return exports.DEFAULT_SAFETY_RULES;
    // Merge: custom rules override built-ins by id; remaining built-ins keep their slot.
    const overrideIds = new Set(compiled.map((rule) => rule.id));
    const merged = [
        ...exports.DEFAULT_SAFETY_RULES.filter((rule) => !overrideIds.has(rule.id)),
        ...compiled,
    ];
    return merged;
}
//# sourceMappingURL=promotionGate.js.map