// Standing audit for the native-CC admin PIN identity gate (Task 619).
//
// The no-event-failure detector. The gate hook, the validate endpoint, and the
// auth tool each emit a log line WHEN they run — but a silent bypass (the gate
// hook not wired into a session's settings, or a regression that strips it)
// produces no log line at all. This audit reconciles, independently of the hook,
// the set of live native-operator sessions that reached real tool use against the
// set that completed PIN auth (one auth-state file per authenticated session_id).
// Any live native-operator session that used a real tool with no auth-state file
// is an orphan: the gate did not bind an identity, yet the session acted.
//
// Native-operator classification: a live watcher row whose `role` is null. Native
// `claude rc` operator sessions are spawned by the rc-daemon, not via the
// manager's /spawn path, so they carry no sidecar and `role` resolves to null.
// pty-spawned sessions (recorder, specialists, public, WhatsApp/Telegram)
// normally carry a sidecar with a non-null role and are excluded. The narrow
// exception is a pty spawn whose sidecar is briefly absent or unreadable
// (`applySidecarToRow` defaults role to null) AND has already produced a real
// tool_use — that would be flagged spuriously. This only ever ADDS an orphan
// signal (false positive), never hides a real bypass, so the audit errs safe.
//
// "Real tool" excludes admin-identity-authenticate itself: a session that only
// ever called the auth tool either matched (state file written) or missed
// (terminated), so it never reaches this branch.
import { existsSync, readFileSync, statSync } from 'node:fs';
import { join } from 'node:path';
import { jsonlPathForSessionId } from './jsonl-path.js';
const TAG = '[admin-identity]';
const AUTH_TOOL = 'admin-identity-authenticate';
/** Skip JSONLs larger than this in the tool-use scan — a bounded read keeps the
 *  per-tick cost predictable. A session this large has unquestionably used real
 *  tools, so treat an oversized file as "used a real tool" (conservative: it can
 *  only ever ADD an orphan signal, never hide one). */
const MAX_SCAN_BYTES = 8 * 1024 * 1024;
/** Pure reconciliation: emit one audit-orphan line per live native-operator
 *  session that used a real tool but has no auth-state file. */
export function auditOnce(deps) {
    let orphans = 0;
    for (const s of deps.sessions) {
        if (s.state !== 'live')
            continue;
        if (s.role !== null)
            continue; // pty-spawned (sidecar present) — not a native operator
        if (!s.usedRealTool)
            continue; // pre-tool-use session — gate has not been exercised yet
        if (deps.hasState(s.sessionId))
            continue; // authenticated — the gate bound an identity
        deps.logger(`${TAG} op=audit-orphan sessionId=${s.sessionId.slice(0, 8)} reason=loopback-session-without-authenticated-userId`);
        orphans++;
    }
    return orphans;
}
/** True when the session JSONL contains an assistant tool_use whose name is not
 *  the auth tool. Best-effort: unreadable / unparseable lines are skipped. */
function jsonlUsedRealTool(jsonlPath) {
    if (!existsSync(jsonlPath))
        return false;
    let raw;
    try {
        if (statSync(jsonlPath).size > MAX_SCAN_BYTES)
            return true;
        raw = readFileSync(jsonlPath, 'utf8');
    }
    catch {
        return false;
    }
    for (const line of raw.split('\n')) {
        if (!line.includes('"tool_use"'))
            continue;
        let obj;
        try {
            obj = JSON.parse(line);
        }
        catch {
            continue;
        }
        const content = obj?.message?.content;
        if (!Array.isArray(content))
            continue;
        for (const block of content) {
            const b = block;
            if (b?.type === 'tool_use' && typeof b.name === 'string' && !b.name.endsWith(AUTH_TOOL)) {
                return true;
            }
        }
    }
    return false;
}
export function createIdentityAudit(deps) {
    let timer = null;
    function tickOnce() {
        const rows = deps.watcher.all();
        const candidates = rows.filter((r) => r.state === 'live' && r.role === null);
        const sessions = candidates.map((r) => ({
            sessionId: r.sessionId,
            role: r.role,
            state: r.state,
            usedRealTool: jsonlUsedRealTool(jsonlPathForSessionId(deps.spawnCwd, r.sessionId)),
        }));
        const orphans = auditOnce({
            sessions,
            hasState: (sid) => existsSync(join(deps.stateDir, `${sid}.json`)),
            logger: deps.logger,
        });
        return { scanned: candidates.length, orphans };
    }
    return {
        tickOnce,
        start() {
            if (timer)
                return;
            timer = setInterval(() => {
                try {
                    tickOnce();
                }
                catch (err) {
                    deps.logger(`${TAG} op=audit-tick-failed message=${err instanceof Error ? err.message : String(err)}`);
                }
            }, deps.intervalMs);
            // Don't keep the event loop alive for the audit alone.
            if (typeof timer.unref === 'function')
                timer.unref();
            deps.logger(`${TAG} op=audit-started intervalMs=${deps.intervalMs} stateDir=${deps.stateDir}`);
        },
        stop() {
            if (timer) {
                clearInterval(timer);
                timer = null;
            }
        },
    };
}
//# sourceMappingURL=admin-identity-audit.js.map