// Standing audit for public-agent reachability (Task 623). The no-event-failure
// detector: an agent dir created `status=active` but unreachable (its slug is
// shadowed by a static route, or it is neither the default nor route-resolvable)
// emits nothing on its own and does not reproduce on a visitor request. Every
// 60s this audit walks `agents/<slug>/` and emits one verdict line per active
// agent, naming the exact slug and cause without any HTTP request.
//
// MANAGER_SHADOWED_SLUGS MUST equal the ui canonical SYSTEM_PATH_SLUGS
// (platform/ui/app/lib/system-path-slugs.ts); the manager builds with
// rootDir=src and cannot import ui modules, so a cross-package test
// (__tests__/shadowed-slugs-drift.test.ts) asserts equality.
import { existsSync, readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
import { resolveAgentIdentityDir, publicIdentityFilesPresent } from './agent-identity-locator.js';
export const MANAGER_SHADOWED_SLUGS = ['public', 'chat', 'graph', 'sessions', 'data'];
const SLUG_PATTERN = /^[a-z][a-z0-9-]{2,49}$/;
const TAG = '[public-agent-reachability]';
/** Pure verdict. Default-first: a slug that is the default is reachable at the
 *  bare root regardless of shadowing. Otherwise a non-shadowed pattern-valid
 *  slug is reachable via `/{slug}`; a shadowed slug is not; a slug that fails
 *  the route pattern is unrouted. */
export function reachabilityVerdict(slug, defaultAgent, shadowedSlugs) {
    if (slug === defaultAgent)
        return { reachable: true, reason: 'ok' };
    if (shadowedSlugs.includes(slug))
        return { reachable: false, reason: 'shadowed' };
    if (!SLUG_PATTERN.test(slug))
        return { reachable: false, reason: 'no-default-and-unrouted' };
    return { reachable: true, reason: 'ok' };
}
/** Routing verdict layered with identity-file presence. A slug that routes
 *  (default-first, then non-shadowed pattern-valid) but whose spawn dir is
 *  missing a required identity file is NOT reachable — `reachable=true` here
 *  means the spawn would find SOUL/KNOWLEDGE/IDENTITY (Task 638). The file
 *  verdict is supplied by the caller so this function stays pure and fs-free;
 *  the audit computes it from `publicIdentityFilesPresent` against the same
 *  `resolveAgentIdentityDir` the spawn composer uses. */
export function reachabilityWithFiles(slug, defaultAgent, shadowedSlugs, files) {
    const routing = reachabilityVerdict(slug, defaultAgent, shadowedSlugs);
    if (!routing.reachable)
        return routing;
    if (!files.present)
        return { reachable: false, reason: 'files-missing', detail: files.detail };
    return { reachable: true, reason: 'ok' };
}
function readDefaultAgent(accountDir) {
    try {
        const cfg = JSON.parse(readFileSync(join(accountDir, 'account.json'), 'utf8'));
        return cfg.defaultAgent && cfg.defaultAgent.length > 0 ? cfg.defaultAgent : null;
    }
    catch {
        return null;
    }
}
function isActiveAgent(agentDir) {
    const cfgPath = join(agentDir, 'config.json');
    if (!existsSync(cfgPath))
        return false;
    try {
        const cfg = JSON.parse(readFileSync(cfgPath, 'utf8'));
        return cfg.status === 'active';
    }
    catch {
        return false;
    }
}
export function createReachabilityAudit(deps) {
    let timer = null;
    function tickOnce() {
        const agentsRoot = join(deps.accountDir, 'agents');
        if (!existsSync(agentsRoot))
            return { scanned: 0, unreachable: 0 };
        const defaultAgent = readDefaultAgent(deps.accountDir);
        let scanned = 0;
        let unreachable = 0;
        for (const entry of readdirSync(agentsRoot, { withFileTypes: true })) {
            if (!entry.isDirectory() || entry.name === 'admin')
                continue;
            const agentDir = join(agentsRoot, entry.name);
            if (!isActiveAgent(agentDir))
                continue;
            scanned++;
            // Task 638 — resolve the file-check dir through the SAME shared resolver
            // the spawn composer uses, and verify the identity files the spawn
            // requires are present. A routing-reachable slug whose SOUL/KNOWLEDGE/
            // IDENTITY is missing is reachable=false, so `reachable=true` implies the
            // spawn will not refuse on a missing file — the audit can no longer
            // certify an agent it has never proven can spawn.
            const identityDir = resolveAgentIdentityDir(deps.accountDir, 'public', entry.name);
            const v = reachabilityWithFiles(entry.name, defaultAgent, MANAGER_SHADOWED_SLUGS, publicIdentityFilesPresent(identityDir));
            if (!v.reachable)
                unreachable++;
            const fileField = v.detail ? ` file=${join(identityDir, v.detail)}` : '';
            deps.logger(`${TAG} op=audit slug=${entry.name} reachable=${v.reachable} reason=${v.reason}${fileField}`);
        }
        return { scanned, unreachable };
    }
    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);
            if (typeof timer.unref === 'function')
                timer.unref();
            deps.logger(`${TAG} op=audit-started intervalMs=${deps.intervalMs} accountDir=${deps.accountDir}`);
        },
        stop() {
            if (timer) {
                clearInterval(timer);
                timer = null;
            }
        },
    };
}
//# sourceMappingURL=public-agent-reachability.js.map