import { spawn } from "node:child_process";
import { randomBytes } from "node:crypto";
import { mkdtemp, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { parseLocalUiHandoffFragment, } from "../launcher/LocalUiHandoffLauncher.js";
import { isRecord } from "../utils/json.js";
import { seedPermissionActivitySmokeData, startBridge, } from "./opencodeRuntimeSmoke.js";
import { delay } from "./smokeTiming.js";
const DEFAULT_TIMEOUT_MS = 30_000;
const LOCAL_UI_ORIGIN = "http://127.0.0.1:41731";
const LOCAL_UI_DEVICE_ID = "permission-surface-smoke-device";
const LOCAL_UI_USER_SESSION_ID = "permission-surface-smoke-user-session";
const LOCAL_UI_SCOPES = [
    "bridge:read",
    "permissions:reply",
    "permissions:manage",
    "permissions:evaluate",
];
const PROOFED_LOCAL_UI_ROUTES = [
    "POST /local-ui/session",
    "GET /local-ui/session",
    "POST /permissions/evaluate",
    "POST /permissions/requests/:id/reply",
    "POST /workspaces/:workspaceId/permissions/decisions/:decisionId/revoke",
    "GET /workspaces/:workspaceId/permissions/activity",
    "GET /workspaces/:workspaceId/permissions/activity?runId=...",
    "GET /workspaces/:workspaceId/permissions/decisions",
    "GET /permissions/requests",
];
const REQUIRED_NO_LEAK_SURFACES = [
    "unauthenticated GET /state/status",
    "unauthenticated GET /permissions/requests",
    "POST /local-ui/session",
    "GET /local-ui/session",
    "POST /permissions/evaluate external mutation",
    "POST invalid deny-only permission reply",
    "POST permission decision revoke",
    "GET workspace permission activity",
    "GET run-filtered permission activity",
    "GET revoked permission decisions",
    "GET pending permission requests",
];
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
export async function main(argv = process.argv.slice(2)) {
    try {
        const options = parsePermissionSurfaceSmokeArgs(argv);
        if (options.help) {
            process.stdout.write(helpText());
            return;
        }
        const summary = await executePermissionSurfaceSmoke(options, process.env);
        if (options.json) {
            process.stdout.write(`${JSON.stringify(summary)}\n`);
            return;
        }
        process.stdout.write(`Permission surface smoke passed: workspace=${summary.workspaceId} run=${summary.runId} activityEvents=${summary.permissionActivityEvents} retryEvents=${summary.retryHistoryEvents.join(",")} revokedDecisions=${summary.revokedDecisionCount} denyOnlyRequest=${summary.denyOnlyRequestId}\n`);
        if (summary.tempRoot) {
            process.stdout.write(`Temp fixture retained at ${summary.tempRoot}\n`);
        }
    }
    catch (error) {
        process.stderr.write(`${error instanceof Error ? error.message : "Permission surface smoke failed."}\n`);
        process.exitCode = 1;
    }
}
export async function runPermissionSurfaceSmoke(argv = [], env = process.env) {
    const options = parsePermissionSurfaceSmokeArgs(argv);
    if (options.help) {
        throw new Error("--help is only supported by the permission smoke CLI.");
    }
    return executePermissionSurfaceSmoke(options, env);
}
export function parsePermissionSurfaceSmokeArgs(argv) {
    const options = {
        keepTemp: false,
        timeoutMs: DEFAULT_TIMEOUT_MS,
        json: false,
        help: false,
    };
    for (let index = 0; index < argv.length; index += 1) {
        const arg = argv[index];
        if (!arg || arg === "--")
            continue;
        if (arg === "--help" || arg === "-h") {
            options.help = true;
            continue;
        }
        if (arg === "--keep-temp") {
            options.keepTemp = true;
            continue;
        }
        if (arg === "--json") {
            options.json = true;
            continue;
        }
        if (arg === "--timeout-ms") {
            options.timeoutMs = parseTimeout(requireArgValue(argv, ++index, arg));
            continue;
        }
        throw new Error(`Unknown permission surface smoke argument: ${arg}`);
    }
    return options;
}
export function createPermissionSurfaceSmokeEnvironment(input) {
    const smokeEnv = { ...input.env };
    for (const key of [
        "OPENAI_API_KEY",
        "ANTHROPIC_API_KEY",
        "GITHUB_TOKEN",
        "DATABASE_URL",
        "PATCHTRAIL_SMOKE_OPENAI_API_KEY",
        "PATCHTRAIL_CREDENTIAL_FALLBACK_KEY",
        "PATCHTRAIL_OPENCODE_METADATA_DIR",
        "PATCHTRAIL_OPENCODE_ARTIFACT_ROOT",
        "PATCHTRAIL_OPENCODE_TRUSTED_ROOT_KEY_IDS",
    ]) {
        delete smokeEnv[key];
    }
    smokeEnv.NODE_ENV = "test";
    smokeEnv.PATCHTRAIL_CREDENTIAL_STORE = "in-memory";
    smokeEnv.PATCHTRAIL_BRIDGE_HOST = "127.0.0.1";
    smokeEnv.PATCHTRAIL_BRIDGE_PORT = "0";
    smokeEnv.PATCHTRAIL_BRIDGE_CAPABILITY_TOKEN = input.bridgeToken;
    smokeEnv.PATCHTRAIL_OPENCODE_SERVER_PASSWORD = input.opencodeServerPassword;
    smokeEnv.PATCHTRAIL_APP_DATA_DIR = input.appDataDir;
    smokeEnv.PATCHTRAIL_LOCAL_DB_PATH = input.localDbPath;
    return smokeEnv;
}
async function executePermissionSurfaceSmoke(options, env) {
    let tempRoot;
    let bridge;
    const forbidden = [];
    try {
        tempRoot = await mkdtemp(path.join(os.tmpdir(), "patchtrail-permission-surface-smoke-"));
        const appDataDir = path.join(tempRoot, "app-data");
        const localDbPath = path.join(tempRoot, "patchtrail.sqlite");
        const workspaceRoot = path.join(tempRoot, "workspace");
        const bridgeToken = `pt_perm_smoke_${randomBytes(24).toString("base64url")}`;
        const opencodeServerPassword = randomBytes(32).toString("base64url");
        forbidden.push({ label: "bridge token", value: bridgeToken }, { label: "OpenCode server password", value: opencodeServerPassword });
        const seeded = seedPermissionActivitySmokeData({
            localDbPath,
            workspaceRoot,
        });
        forbidden.push(...seeded.forbidden);
        const smokeEnv = createPermissionSurfaceSmokeEnvironment({
            env,
            bridgeToken,
            opencodeServerPassword,
            appDataDir,
            localDbPath,
        });
        bridge = await startBridge(smokeEnv, options.timeoutMs, forbidden);
        const summary = await runPermissionSurfaceHttpFlow({
            bridge,
            bridgeToken,
            seeded,
            forbidden,
            timeoutMs: options.timeoutMs,
            env,
        });
        await assertNoLeaks("bridge stdout", bridge.stdout.text(), forbidden);
        await assertNoLeaks("bridge stderr", bridge.stderr.text(), forbidden);
        return {
            ...summary,
            ...(options.keepTemp && tempRoot ? { tempRoot } : {}),
        };
    }
    catch (error) {
        throw new Error(sanitizePermissionSurfaceSmokeError(error, forbidden, [tempRoot]));
    }
    finally {
        if (bridge)
            await shutdownBridge(bridge, forbidden);
        if (tempRoot && !options.keepTemp) {
            await rm(tempRoot, { recursive: true, force: true });
        }
    }
}
async function runPermissionSurfaceHttpFlow(input) {
    const request = (method, requestPath, body, options = {}) => requestJson(input.bridge.baseUrl, method, requestPath, body, options);
    const noLeakCheckedSurfaces = [];
    const checkNoLeaks = async (surface, value) => {
        await assertNoLeaks(surface, value, input.forbidden);
        noLeakCheckedSurfaces.push(surface);
    };
    const health = await request("GET", "/health");
    assertStatus(health, 200, "GET /health");
    await checkNoLeaks("GET /health", health.body);
    const unauthorized = await request("GET", "/state/status");
    assertStatus(unauthorized, 401, "unauthenticated GET /state/status");
    await checkNoLeaks("unauthenticated GET /state/status", unauthorized.body);
    const unauthorizedPermissionRequests = await request("GET", "/permissions/requests");
    assertStatus(unauthorizedPermissionRequests, 401, "unauthenticated GET /permissions/requests");
    await checkNoLeaks("unauthenticated GET /permissions/requests", unauthorizedPermissionRequests.body);
    const state = await request("GET", "/state/status", undefined, {
        bridgeToken: input.bridgeToken,
    });
    assertStatus(state, 200, "GET /state/status");
    assertNestedEqual(state.body, ["counts", "workspaces"], 1);
    assertNestedEqual(state.body, ["counts", "runs"], 2);
    await checkNoLeaks("GET /state/status", state.body);
    const handoff = await mintLocalUiHandoffWithCompiledCli({
        bridgeUrl: input.bridge.baseUrl,
        bridgeToken: input.bridgeToken,
        workspaceId: input.seeded.workspaceId,
        timeoutMs: input.timeoutMs,
        forbidden: input.forbidden,
        env: input.env,
    });
    assertRequiredLocalUiScopes(handoff.scopes, "local UI handoff CLI scopes");
    const handoffPayload = readHandoffPayload(handoff.handoffUrl);
    input.forbidden.push({
        label: "local UI capability token",
        value: handoffPayload.capabilityToken,
    });
    const sessionExchange = await request("POST", "/local-ui/session", {
        capabilityToken: handoffPayload.capabilityToken,
        workspaceId: handoffPayload.workspaceId,
        deviceId: handoffPayload.deviceId,
        userSessionId: handoffPayload.userSessionId,
    }, { origin: handoffPayload.origin });
    assertStatus(sessionExchange, 201, "POST /local-ui/session");
    await checkNoLeaks("POST /local-ui/session", sessionExchange.body);
    const sessionToken = readNestedString(sessionExchange.body, ["sessionToken"]);
    input.forbidden.push({
        label: "local UI session token",
        value: sessionToken,
    });
    const localUiAuth = {
        bearerToken: sessionToken,
        origin: handoffPayload.origin,
    };
    const localSession = await request("GET", "/local-ui/session", undefined, localUiAuth);
    assertStatus(localSession, 200, "GET /local-ui/session");
    assertNestedEqual(localSession.body, ["session", "workspaceId"], input.seeded.workspaceId);
    assertRequiredLocalUiScopes(readNestedStringArray(localSession.body, ["session", "scopes"]), "local UI session scopes");
    await checkNoLeaks("GET /local-ui/session", localSession.body);
    const evaluatedRawCommand = "curl https://permission-smoke-evaluate.example/deploy --api-key evaluated-command-secret";
    input.forbidden.push({ label: "evaluated raw command", value: evaluatedRawCommand }, { label: "evaluated command secret", value: "evaluated-command-secret" });
    const externalMutationEvaluation = await request("POST", "/permissions/evaluate", {
        toolCall: {
            id: "call_smoke_evaluate_external_mutation",
            name: "shell.exec",
            title: "Smoke evaluated external mutation",
            risk: "network",
            inputPreview: "Sanitized evaluated external mutation preview",
            command: evaluatedRawCommand,
        },
        context: {
            workspaceId: input.seeded.workspaceId,
            sessionId: input.seeded.runId,
        },
    }, localUiAuth);
    assertStatus(externalMutationEvaluation, 200, "POST /permissions/evaluate external mutation");
    await checkNoLeaks("POST /permissions/evaluate external mutation", externalMutationEvaluation.body);
    const evaluatedExternalMutationOptions = assertDenyOnlyEvaluation(externalMutationEvaluation.body);
    const invalidDenyOnlyReply = await request("POST", "/permissions/requests/prq_smoke_activity_deny_only/reply", { option: "approve_once" }, localUiAuth);
    assertStatus(invalidDenyOnlyReply, 400, "POST invalid deny-only permission reply");
    await checkNoLeaks("POST invalid deny-only permission reply", invalidDenyOnlyReply.body);
    const revokedViaRoute = await request("POST", `/workspaces/${encodeURIComponent(input.seeded.workspaceId)}/permissions/decisions/${encodeURIComponent(input.seeded.revocableDecisionId)}/revoke`, { reason: "Permission smoke route revoke proof" }, localUiAuth);
    assertStatus(revokedViaRoute, 200, "POST permission decision revoke");
    await checkNoLeaks("POST permission decision revoke", revokedViaRoute.body);
    const revokedDecisionId = assertRevokedDecisionResponse(revokedViaRoute.body, input.seeded.revocableDecisionId);
    const activity = await request("GET", `/workspaces/${encodeURIComponent(input.seeded.workspaceId)}/permissions/activity?limit=20`, undefined, localUiAuth);
    assertStatus(activity, 200, "GET workspace permission activity");
    await checkNoLeaks("GET workspace permission activity", activity.body);
    const activitySummary = assertPermissionActivity(activity.body, {
        retryToolTitle: input.seeded.retryToolTitle,
        retryInputPreview: input.seeded.retryInputPreview,
    });
    const runActivity = await request("GET", `/workspaces/${encodeURIComponent(input.seeded.workspaceId)}/permissions/activity?runId=${encodeURIComponent(input.seeded.runId)}&limit=20`, undefined, localUiAuth);
    assertStatus(runActivity, 200, "GET run-filtered permission activity");
    await checkNoLeaks("GET run-filtered permission activity", runActivity.body);
    const runFilteredEvents = assertRunFilteredActivity(runActivity.body, input.seeded.otherRunMarker);
    const revokedDecisions = await request("GET", `/workspaces/${encodeURIComponent(input.seeded.workspaceId)}/permissions/decisions?status=revoked&limit=20`, undefined, localUiAuth);
    assertStatus(revokedDecisions, 200, "GET revoked permission decisions");
    await checkNoLeaks("GET revoked permission decisions", revokedDecisions.body);
    const revokedDecisionCount = assertRevokedSessionApproval(revokedDecisions.body);
    const pendingRequests = await request("GET", `/permissions/requests?workspaceId=${encodeURIComponent(input.seeded.workspaceId)}&status=pending&limit=20`, undefined, localUiAuth);
    assertStatus(pendingRequests, 200, "GET pending permission requests");
    await checkNoLeaks("GET pending permission requests", pendingRequests.body);
    const pendingRequestSummary = assertPendingDenyOnlyRequest(pendingRequests.body);
    assertRequiredNoLeakSurfaces(noLeakCheckedSurfaces);
    return {
        status: "passed",
        workspaceId: input.seeded.workspaceId,
        runId: input.seeded.runId,
        permissionActivityEvents: activitySummary.events.length,
        runFilteredActivityEvents: runFilteredEvents.length,
        revokedDecisionCount,
        pendingRequestCount: pendingRequestSummary.pendingCount,
        denyOnlyRequestId: pendingRequestSummary.denyOnlyRequestId,
        denyOnlyRequestOptions: pendingRequestSummary.denyOnlyOptions,
        retryToolTitle: input.seeded.retryToolTitle,
        retryHistoryEvents: activitySummary.retryHistoryEvents,
        revokedDecisionId,
        evaluatedExternalMutationOptions,
        localUiScopes: handoff.scopes,
        proofedLocalUiRoutes: [...PROOFED_LOCAL_UI_ROUTES],
        unauthenticatedRejected: true,
        runFilteredActivityExcludedOtherRun: true,
        noLeakCheckedSurfaces,
    };
}
async function mintLocalUiHandoffWithCompiledCli(input) {
    const launcherPath = path.join(PACKAGE_ROOT, "dist", "launcher", "localUiHandoffCli.js");
    const childEnv = {
        ...input.env,
        PATCHTRAIL_BRIDGE_CAPABILITY_TOKEN: input.bridgeToken,
    };
    const result = await runChild(process.execPath, [
        launcherPath,
        "--json",
        "--print-url",
        "--bridge-url",
        input.bridgeUrl,
        "--ui-url",
        `${LOCAL_UI_ORIGIN}/app/settings`,
        "--workspace-id",
        input.workspaceId,
        "--device-id",
        LOCAL_UI_DEVICE_ID,
        "--user-session-id",
        LOCAL_UI_USER_SESSION_ID,
        "--scopes",
        LOCAL_UI_SCOPES.join(","),
    ], {
        cwd: PACKAGE_ROOT,
        env: childEnv,
        timeoutMs: input.timeoutMs,
    });
    await assertNoLeaks("local UI handoff CLI stdout", result.stdout, input.forbidden.filter((item) => item.label !== "local UI capability token"));
    await assertNoLeaks("local UI handoff CLI stderr", result.stderr, input.forbidden);
    if (result.exitCode !== 0) {
        throw new Error(`local-ui handoff CLI exited ${result.exitCode}: ${result.stderr.trim() || result.stdout.trim() || "no output"}`);
    }
    const parsed = JSON.parse(result.stdout);
    if (!isRecord(parsed)) {
        throw new Error("local-ui handoff CLI did not return a JSON object.");
    }
    const handoffUrl = readString(parsed.handoffUrl, "handoffUrl");
    const origin = readString(parsed.origin, "origin");
    const scopes = readStringArray(parsed.scopes, "scopes");
    return { handoffUrl, origin, scopes };
}
function readHandoffPayload(handoffUrl) {
    const payload = parseLocalUiHandoffFragment(handoffUrl);
    if (!payload) {
        throw new Error("local-ui handoff CLI output did not include a handoff fragment.");
    }
    if (payload.origin !== LOCAL_UI_ORIGIN) {
        throw new Error("local-ui handoff origin did not match the smoke origin.");
    }
    return payload;
}
async function runChild(command, args, options) {
    return new Promise((resolve, reject) => {
        const child = spawn(command, args, {
            cwd: options.cwd,
            env: options.env,
            stdio: ["ignore", "pipe", "pipe"],
        });
        const stdout = new BoundedString();
        const stderr = new BoundedString();
        const timeout = setTimeout(() => {
            child.kill("SIGTERM");
            reject(new Error(`Timed out running ${path.basename(args[0] ?? command)}.`));
        }, options.timeoutMs);
        child.stdout?.on("data", (chunk) => stdout.append(chunk));
        child.stderr?.on("data", (chunk) => stderr.append(chunk));
        child.once("error", (error) => {
            clearTimeout(timeout);
            reject(error);
        });
        child.once("close", (exitCode) => {
            clearTimeout(timeout);
            resolve({ exitCode, stdout: stdout.text(), stderr: stderr.text() });
        });
    });
}
async function requestJson(baseUrl, method, requestPath, body, options = {}) {
    const headers = { accept: "application/json" };
    if (options.bridgeToken)
        headers["x-patchtrail-bridge-token"] = options.bridgeToken;
    if (options.bearerToken)
        headers.authorization = `Bearer ${options.bearerToken}`;
    if (options.origin)
        headers.origin = options.origin;
    let requestBody;
    if (body !== undefined) {
        headers["content-type"] = "application/json";
        requestBody = JSON.stringify(body);
    }
    const response = await fetch(new URL(requestPath, baseUrl), {
        method,
        headers,
        ...(requestBody ? { body: requestBody } : {}),
        signal: AbortSignal.timeout(10_000),
    });
    const text = await response.text();
    let parsed = text;
    if (text) {
        try {
            parsed = JSON.parse(text);
        }
        catch {
            parsed = text;
        }
    }
    return { status: response.status, body: parsed, text };
}
function assertPermissionActivity(body, retry) {
    const events = readNestedArray(body, ["events"]);
    const kinds = new Set(events
        .map((event) => (isRecord(event) ? event.kind : undefined))
        .filter((kind) => typeof kind === "string"));
    for (const kind of [
        "request_created",
        "request_answered",
        "request_reply_failed",
        "session_decision_recorded",
        "session_decision_revoked",
    ]) {
        if (!kinds.has(kind)) {
            throw new Error(`Permission activity did not include ${kind}.`);
        }
    }
    const denyOnlyEvent = events.find((event) => isRecord(event) &&
        event.externalServiceMutation === true &&
        isRecord(event.denyOnlyDiagnostics));
    if (!denyOnlyEvent) {
        throw new Error("Permission activity did not include deny-only diagnostics.");
    }
    const retryHistoryEvents = assertRetryHistory(events, retry);
    return { events, retryHistoryEvents };
}
function assertRetryHistory(events, retry) {
    const retryEvents = events.filter((event) => {
        if (!isRecord(event) || !isRecord(event.toolCall))
            return false;
        return (event.toolCall.title === retry.retryToolTitle &&
            event.toolCall.inputPreview === retry.retryInputPreview);
    });
    const history = retryEvents
        .map((event) => (isRecord(event) ? event.kind : undefined))
        .filter((kind) => typeof kind === "string");
    const expected = [
        "request_created",
        "request_reply_failed",
        "request_answered",
    ];
    let searchFrom = 0;
    for (const kind of expected) {
        const foundAt = history.indexOf(kind, searchFrom);
        if (foundAt < 0) {
            throw new Error(`Retry permission activity did not preserve ${kind} for the same safe tool preview.`);
        }
        searchFrom = foundAt + 1;
    }
    return history;
}
function assertRunFilteredActivity(body, excludedMarker) {
    const events = readNestedArray(body, ["events"]);
    if (JSON.stringify(events).includes(excludedMarker)) {
        throw new Error("Run-filtered permission activity included another run marker.");
    }
    return events;
}
function assertRevokedSessionApproval(body) {
    const decisions = readNestedArray(body, ["decisions"]);
    const revoked = decisions.filter((decision) => isRecord(decision) &&
        decision.status === "revoked" &&
        decision.option === "allow_for_session" &&
        decision.revokeSource === "local_ui");
    if (revoked.length < 2) {
        throw new Error("Revoked session approval history did not expose seeded and route-revoked approvals.");
    }
    return revoked.length;
}
function assertPendingDenyOnlyRequest(body) {
    const requests = readNestedArray(body, ["requests"]);
    const denyOnly = requests.find((candidate) => {
        if (!isRecord(candidate) || !isRecord(candidate.request))
            return false;
        const request = candidate.request;
        return (request.externalServiceMutation === true &&
            isRecord(request.denyOnlyDiagnostics) &&
            request.denyOnlyDiagnostics.policyClass === "external_service_mutation" &&
            Array.isArray(request.options) &&
            request.options.length === 1 &&
            request.options[0] === "deny");
    });
    if (!denyOnly || !isRecord(denyOnly)) {
        throw new Error("Pending permission requests did not include deny-only external-service data.");
    }
    const denyOnlyRequest = denyOnly.request;
    if (!isRecord(denyOnlyRequest)) {
        throw new Error("Deny-only permission request was malformed.");
    }
    const denyOnlyOptions = readStringArray(denyOnlyRequest.options, "deny-only request options");
    if (denyOnlyOptions.length !== 1 || denyOnlyOptions[0] !== "deny") {
        throw new Error("Pending deny-only request options were not deny-only.");
    }
    return {
        pendingCount: requests.length,
        denyOnlyRequestId: readString(denyOnly.id, "deny-only request id"),
        denyOnlyOptions,
    };
}
function assertDenyOnlyEvaluation(body) {
    const request = readNested(body, ["request"]);
    if (!isRecord(request)) {
        throw new Error("Permission evaluation did not return a request.");
    }
    if (request.externalServiceMutation !== true) {
        throw new Error("Permission evaluation did not flag external mutation.");
    }
    if (!isRecord(request.denyOnlyDiagnostics)) {
        throw new Error("Permission evaluation missed deny-only diagnostics.");
    }
    const options = readStringArray(request.options, "evaluation request options");
    if (options.length !== 1 || options[0] !== "deny") {
        throw new Error("Permission evaluation was not deny-only.");
    }
    return options;
}
function assertRevokedDecisionResponse(body, expectedDecisionId) {
    const decision = readNested(body, ["decision"]);
    if (!isRecord(decision)) {
        throw new Error("Permission revoke did not return a decision.");
    }
    const id = readString(decision.id, "revoked decision id");
    if (id !== expectedDecisionId) {
        throw new Error("Permission revoke returned the wrong decision id.");
    }
    if (decision.status !== "revoked") {
        throw new Error("Permission revoke did not mark the decision revoked.");
    }
    if (decision.revokeSource !== "local_ui") {
        throw new Error("Permission revoke did not record local UI source.");
    }
    return id;
}
function assertRequiredNoLeakSurfaces(surfaces) {
    for (const surface of REQUIRED_NO_LEAK_SURFACES) {
        if (!surfaces.includes(surface)) {
            throw new Error(`Permission smoke skipped no-leak check for ${surface}.`);
        }
    }
}
function assertRequiredLocalUiScopes(scopes, label) {
    for (const scope of LOCAL_UI_SCOPES) {
        if (!scopes.includes(scope)) {
            throw new Error(`${label} did not include ${scope}.`);
        }
    }
}
function assertStatus(result, expected, label) {
    if (result.status !== expected) {
        throw new Error(`${label} returned HTTP ${result.status}; expected ${expected}.`);
    }
}
function assertNestedEqual(value, pathSegments, expected) {
    const actual = readNested(value, pathSegments);
    if (actual !== expected) {
        throw new Error(`Expected ${pathSegments.join(".")} to equal ${String(expected)}.`);
    }
}
function readNestedArray(value, pathSegments) {
    const nested = readNested(value, pathSegments);
    if (!Array.isArray(nested)) {
        throw new Error(`Expected ${pathSegments.join(".")} to be an array.`);
    }
    return nested;
}
function readNestedString(value, pathSegments) {
    const nested = readNested(value, pathSegments);
    return readString(nested, pathSegments.join("."));
}
function readNestedStringArray(value, pathSegments) {
    const nested = readNested(value, pathSegments);
    return readStringArray(nested, pathSegments.join("."));
}
function readNested(value, pathSegments) {
    let current = value;
    for (const segment of pathSegments) {
        if (!isRecord(current))
            return undefined;
        current = current[segment];
    }
    return current;
}
function readString(value, label) {
    if (typeof value === "string" && value.length > 0)
        return value;
    throw new Error(`Expected ${label} to be a non-empty string.`);
}
function readStringArray(value, label) {
    if (!Array.isArray(value)) {
        throw new Error(`Expected ${label} to be an array.`);
    }
    return value.filter((item) => typeof item === "string");
}
export async function assertNoLeaks(surface, value, forbidden) {
    const serialized = typeof value === "string" ? value : (JSON.stringify(value) ?? "");
    for (const item of forbidden) {
        if (!item.value)
            continue;
        if (serialized.includes(item.value)) {
            throw new Error(`${item.label} leaked through ${surface}.`);
        }
    }
}
async function shutdownBridge(bridge, forbidden) {
    await terminateChild(bridge.child);
    await assertNoLeaks("bridge stdout", bridge.stdout.text(), forbidden);
    await assertNoLeaks("bridge stderr", bridge.stderr.text(), forbidden);
}
async function terminateChild(child) {
    if (child.exitCode !== null || child.signalCode !== null)
        return;
    child.kill("SIGTERM");
    const deadline = Date.now() + 5_000;
    while (Date.now() < deadline) {
        if (child.exitCode !== null || child.signalCode !== null)
            return;
        await delay(50);
    }
    if (child.exitCode === null && child.signalCode === null)
        child.kill("SIGKILL");
}
export function sanitizePermissionSurfaceSmokeError(error, forbidden, pathHints) {
    let message = error instanceof Error ? error.message : "Permission surface smoke failed.";
    message = message.replace(/patchTrailLocalHandoff=[^\s"']+/g, "patchTrailLocalHandoff=[redacted]");
    for (const item of forbidden) {
        if (!item.value)
            continue;
        message = message.split(item.value).join(`[redacted:${item.label}]`);
    }
    for (const hint of [...pathHints, PACKAGE_ROOT, process.env.HOME]) {
        if (!hint)
            continue;
        message = message.split(hint).join("[redacted:path]");
    }
    message = message.replace(/(?:[A-Za-z]:)?\/(?:private\/)?(?:tmp|var\/folders|Users)\/[^\s"']+/g, "[redacted:path]");
    return message.slice(0, 2_000);
}
function parseTimeout(value) {
    if (!/^\d+$/.test(value)) {
        throw new Error("--timeout-ms must be an integer >= 1000.");
    }
    const parsed = Number(value);
    if (!Number.isInteger(parsed) || parsed < 1_000) {
        throw new Error("--timeout-ms must be an integer >= 1000.");
    }
    return parsed;
}
function requireArgValue(argv, index, flag) {
    const value = argv[index];
    if (!value)
        throw new Error(`Expected value after ${flag}.`);
    return value;
}
function helpText() {
    return `Usage: pnpm smoke:permissions -- [options]\n\nStarts a compiled loopback bridge with a deterministic permission-surface fixture, verifies scoped local UI reads/replies/evaluate/manage routes, no-leak boundaries, retry history, revoked decisions, and deny-only external mutations, then shuts the bridge down.\n\nOptions:\n  --timeout-ms <ms>   Bridge and launcher readiness timeout. Default: ${DEFAULT_TIMEOUT_MS}.\n  --keep-temp         Keep the isolated temp SQLite/app-data directory for local inspection.\n  --json              Print a machine-readable non-secret proof packet.\n  --help              Show this help.\n`;
}
class BoundedString {
    #value = "";
    append(chunk) {
        this.#value = `${this.#value}${chunk.toString("utf8")}`.slice(-16_384);
    }
    text() {
        return this.#value;
    }
}
