#!/usr/bin/env node
/**
 * audit-chain-append — Node CLI wrapping pack-evaluation + SHA-256 chain.
 *
 * Thomas directive 2026-05-17 evening: "It must start up and report findings
 * to a local database on the compliance and company packs, including what
 * they are allowed and not allowed to do. It must have SHA-256 installed."
 *
 * Pre-fix audit (Explore agent 2026-05-17): audit-logger.sh writes plain
 * JSONL to .claude/governance/audit/audit-YYYY-MM-DD.jsonl with NO hash
 * chain. A regulator reviewing the file cannot detect tampering — anyone
 * can edit any line and the audit trail still parses cleanly. Separately,
 * each entry lacks the pack-evaluation verdict so compliance reasoning
 * is silent on every tool call.
 *
 * This bridge consolidates both fixes. A shell hook calls:
 *
 *   node dist/cli/audit-chain-append.js \\
 *     --tool=Bash \\
 *     --input='{"command":"ls"}' \\
 *     --base-entry='{"timestamp":"...","session":"...","user":"...","outcome":{...}}' \\
 *     --audit-file=.claude/governance/audit/audit-2026-05-17.jsonl
 *
 * The CLI:
 *   1. Runs pack-enforcement-bridge logic to get verdict + triggeringPackIds.
 *   2. Reads the last line of --audit-file to get the previousHash (or
 *      "0".repeat(64) if the file is empty / missing).
 *   3. Merges the base entry + verdict fields into a single record.
 *   4. Computes currentHash = sha256(previousHash + record_json + timestamp).
 *   5. Appends the chained record to --audit-file.
 *   6. Prints the chained record to stdout for the caller.
 *
 * Exit codes:
 *   0  ok (verdict ALLOW or LOG or WARN — record appended)
 *   1  verdict DENY (record appended, hook should refuse the tool call)
 *   2  verdict REQUIRE_APPROVAL (record appended, hook should pause)
 *   3  internal error (no record appended)
 *
 * Invariant 2 (advisory, never authoritative): a chain-append failure or
 * a bridge throw must NOT block the hook. The hook is expected to swallow
 * non-zero exit codes for verdict != DENY and proceed. The DENY path is
 * the only place the customer's tool call should be refused.
 *
 * @connexum/ai-governance — runtime
 */
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as crypto from 'node:crypto';
import { BUILT_IN_PACKS } from '../governed-agent.js';
import { PackRegistry } from '../packs/index.js';
import { PackEvaluator } from '../governance/pack-evaluator.js';
function parseArgs(argv) {
    const args = { cwd: process.cwd() };
    for (const a of argv) {
        const m = a.match(/^--([^=]+)=(.*)$/);
        if (!m)
            continue;
        const key = m[1].replace(/-([a-z])/g, (_, c) => c.toUpperCase());
        if (key === 'tool' ||
            key === 'input' ||
            key === 'baseEntry' ||
            key === 'auditFile' ||
            key === 'cwd') {
            args[key] = m[2];
        }
    }
    if (!args.tool || !args.auditFile)
        return null;
    if (typeof args.input !== 'string')
        args.input = '{}';
    if (typeof args.baseEntry !== 'string')
        args.baseEntry = '{}';
    return args;
}
function readGovernanceConfig(cwd) {
    const p = path.join(cwd, '.governance.json');
    if (!fs.existsSync(p))
        return null;
    try {
        return JSON.parse(fs.readFileSync(p, 'utf8'));
    }
    catch {
        return null;
    }
}
function readLastChainHash(auditFile) {
    if (!fs.existsSync(auditFile))
        return '0'.repeat(64);
    try {
        const raw = fs.readFileSync(auditFile, 'utf8');
        const lines = raw.trimEnd().split('\n').filter((l) => l.length > 0);
        if (lines.length === 0)
            return '0'.repeat(64);
        const last = JSON.parse(lines[lines.length - 1]);
        return last.currentHash ?? '0'.repeat(64);
    }
    catch {
        return '0'.repeat(64);
    }
}
function computeHash(previousHash, recordJson, timestamp) {
    return crypto.createHash('sha256').update(previousHash + recordJson + timestamp).digest('hex');
}
function evaluatePacks(cwd, tool, inputJson) {
    const config = readGovernanceConfig(cwd);
    if (!config || !Array.isArray(config.packs) || config.packs.length === 0) {
        return {
            verdict: 'ALLOW',
            triggeringPackIds: [],
            triggeringClassifierIds: [],
            reason: 'No packs bound in .governance.json',
            boundPacks: [],
        };
    }
    const registry = new PackRegistry();
    const resolved = [];
    for (const rawId of config.packs) {
        const normalizedId = rawId.toLowerCase().replace(/[\s-_]/g, '');
        const pack = BUILT_IN_PACKS[rawId] ?? BUILT_IN_PACKS[normalizedId];
        if (pack) {
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            registry.register(pack);
            resolved.push(pack.id);
        }
    }
    if (resolved.length === 0) {
        return {
            verdict: 'ALLOW',
            triggeringPackIds: [],
            triggeringClassifierIds: [],
            reason: `No bound pack id matched a built-in pack (configured: ${config.packs.join(',')})`,
            boundPacks: config.packs,
        };
    }
    let inputObj = {};
    try {
        inputObj = JSON.parse(inputJson);
    }
    catch {
        inputObj = { raw: inputJson };
    }
    const tenantId = config.runtime?.orgId ?? 'local';
    const agentId = config.runtime?.agentId ?? 'local-agent';
    const actionContext = {
        toolName: tool,
        input: inputObj,
        agent: { name: agentId },
        timestamp: new Date().toISOString(),
    };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const noopEventBus = { on: () => undefined };
    const evaluator = new PackEvaluator(registry, noopEventBus);
    const result = evaluator.evaluate(tenantId, agentId, actionContext);
    return {
        verdict: result.verdict,
        triggeringPackIds: result.triggeringPackIds,
        triggeringClassifierIds: result.triggeringClassifierIds,
        reason: result.reason,
        boundPacks: resolved,
    };
}
function main() {
    const args = parseArgs(process.argv.slice(2));
    if (!args) {
        process.stderr.write('audit-chain-append: --tool=<name> --audit-file=<path> required\n');
        process.exit(3);
    }
    // 1. Pack evaluation
    let verdictPayload;
    try {
        verdictPayload = evaluatePacks(args.cwd, args.tool, args.input);
    }
    catch (err) {
        verdictPayload = {
            verdict: 'ALLOW',
            triggeringPackIds: [],
            triggeringClassifierIds: [],
            reason: `pack-eval internal error: ${err.message}`,
            boundPacks: [],
        };
    }
    // 2. Parse base entry from hook
    let base = {};
    try {
        base = JSON.parse(args.baseEntry);
    }
    catch {
        base = { raw_base_entry: args.baseEntry };
    }
    const timestamp = base.timestamp ?? new Date().toISOString();
    // 3. Build merged record (verdict + bound packs + base hook fields)
    const merged = {
        ...base,
        tool: args.tool,
        pack: {
            verdict: verdictPayload.verdict,
            triggeringPackIds: verdictPayload.triggeringPackIds,
            triggeringClassifierIds: verdictPayload.triggeringClassifierIds,
            reason: verdictPayload.reason,
            boundPacks: verdictPayload.boundPacks,
        },
    };
    // 4. Read previous chain hash + compute current
    let previousHash = '0'.repeat(64);
    try {
        previousHash = readLastChainHash(args.auditFile);
    }
    catch {
        // best-effort — fall through to zero hash
    }
    const recordJson = JSON.stringify(merged);
    const currentHash = computeHash(previousHash, recordJson, timestamp);
    const chained = {
        ...merged,
        previousHash,
        currentHash,
    };
    // 5. Append to audit file (best-effort — Invariant 2)
    try {
        const dir = path.dirname(args.auditFile);
        if (!fs.existsSync(dir))
            fs.mkdirSync(dir, { recursive: true });
        fs.appendFileSync(args.auditFile, JSON.stringify(chained) + '\n', { mode: 0o600 });
    }
    catch (err) {
        process.stderr.write(`audit-chain-append: append failed: ${err.message}\n`);
        // continue — print to stdout so caller still sees the verdict
    }
    // 6. Print chained record to stdout
    process.stdout.write(JSON.stringify(chained) + '\n');
    // 7. Exit code reflects verdict
    const exitCode = verdictPayload.verdict === 'DENY' ? 1 : verdictPayload.verdict === 'PENDING' ? 2 : 0;
    process.exit(exitCode);
}
try {
    main();
}
catch (err) {
    process.stderr.write(`audit-chain-append: ${err.message}\n`);
    process.exit(3);
}
//# sourceMappingURL=audit-chain-append.js.map