// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. config module for scripts/src/agentrun.ts.

// Moved mechanically from scripts/src/agentrun.ts:6916-7650 for #903.

// SPEC: PJ2026-01020108 cancel lifecycle + PJ2026-01020205 AipodSpec binding + PJ2026-01020302 session policy + PJ2026-01020305 cancel control + PJ2026-01060305/06 YAML execution policy and bounded output draft-2026-06-25-p0.
// Exposes AgentRun lane-scoped policy, AipodSpec SecretRef binding, cancel lifecycle, and bounded default output in the UniDesk CLI.
import { chmodSync, copyFileSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
import { rootPath, type UniDeskConfig } from "../config";
import type { RenderedCliResult } from "../output";
import { applyLocalCaddyManagedSite } from "../pk01-caddy";
import { runSshCommandCapture, type SshCaptureResult } from "../ssh";
import { runRemoteSshCommandCapture } from "../remote";
import { startJob } from "../jobs";
import {
  AGENTRUN_CONFIG_PATH,
  agentRunLaneSummary,
  agentRunPipelineRunName,
  agentRunProviderCredentialRefs,
  resolveAgentRunLaneTarget,
  type AgentRunCancelLifecycleSpec,
  type AgentRunLaneSpec,
} from "../agentrun-lanes";
import {
  agentRunImageArtifact,
  placeholderAgentRunImage,
  renderedFilesDigest,
  renderedObjectsDigest,
  renderAgentRunControlPlaneManifests,
  renderAgentRunGitopsFiles,
  type AgentRunArtifactService,
} from "../agentrun-manifests";
import { sha256Fingerprint } from "../platform-infra-ops-library";

import type { AgentRunFailureKind, AgentRunHttpMethod, AgentRunResolvedAuth, AgentRunRestTarget, AgentRunRestTargetOptions, AgentRunSessionPolicyTarget } from "./utils";
import { isRecord, pickCompact } from "./options";
import { pathValue } from "./render";
import { AgentRunRestError, activeAgentRunRestTarget } from "./rest-bridge";
import { record, stringOrNull } from "./utils";

export function readAgentRunClientConfig(env: NodeJS.ProcessEnv = process.env): AgentRunClientConfig {
  const configPath = env.AGENTRUN_CLIENT_CONFIG ?? rootPath("config", "agentrun.yaml");
  if (!existsSync(configPath)) throw new AgentRunRestError("validation-failed", `AgentRun client config not found: ${configPath}`);
  try {
    const parsed = parseAgentRunClientConfigYaml(readFileSync(configPath, "utf8"), configPath);
    return { ...parsed, sourcePath: configPath };
  } catch (error) {
    if (error instanceof AgentRunRestError) throw error;
    throw new AgentRunRestError("validation-failed", `AgentRun client config invalid: ${error instanceof Error ? error.message : String(error)}`, {
      bridge: { mode: "direct-http", clientRole: "render-only", configPath },
    });
  }
}

export function parseAgentRunClientConfigYaml(raw: string, sourcePath = "config/agentrun.yaml"): AgentRunClientConfig {
  const input = record(Bun.YAML.parse(raw) as unknown);
  validateAgentRunConfigEnvelope(input, sourcePath);
  const manager = record(input.manager);
  const auth = record(input.auth);
  const client = record(input.client);
  const baseUrl = stringFieldFromRecord(manager, "baseUrl", "manager");
  try {
    const parsed = new URL(baseUrl);
    if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("invalid protocol");
  } catch {
    throw new Error(`${sourcePath}: manager.baseUrl must be an http(s) URL`);
  }
  const timeoutMs = numberFieldFromRecord(manager, "timeoutMs", "manager", { min: 1, max: 60_000 });
  const header = stringFieldFromRecord(auth, "header", "auth");
  const scheme = stringFieldFromRecord(auth, "scheme", "auth");
  if (header.toLowerCase() !== "authorization") throw new Error(`${sourcePath}: auth.header must be Authorization`);
  if (scheme !== "Bearer") throw new Error(`${sourcePath}: auth.scheme must be Bearer`);
  const role = stringFieldFromRecord(client, "role", "client");
  const transport = stringFieldFromRecord(client, "transport", "client");
  if (role !== "render-only") throw new Error(`${sourcePath}: client.role must be render-only`);
  if (transport !== "direct-http") throw new Error(`${sourcePath}: client.transport must be direct-http`);
  const sessionPolicy = readAgentRunSessionPolicyConfig(client, sourcePath);
  const publicExposure = readAgentRunPublicExposureConfig(input.publicExposure, baseUrl.replace(/\/+$/u, "/"), sourcePath);
  return {
    sourcePath,
    manager: {
      baseUrl: baseUrl.replace(/\/+$/u, "/"),
      timeoutMs,
    },
    auth: {
      env: stringFieldFromRecord(auth, "env", "auth"),
      file: stringFieldFromRecord(auth, "file", "auth"),
      header,
      scheme,
    },
    client: {
      role,
      transport,
      sessionPolicy,
    },
    publicExposure,
  };
}

export function readAgentRunSessionPolicyConfig(client: Record<string, unknown>, sourcePath: string): AgentRunSessionPolicyConfig {
  const pathValue = "client.sessionPolicy";
  const sessionPolicy = record(client.sessionPolicy);
  const workspaceRef = record(sessionPolicy.workspaceRef);
  const executionPolicy = record(sessionPolicy.executionPolicy);
  const secretScope = record(executionPolicy.secretScope);
  stringFieldFromRecord(workspaceRef, "kind", `${pathValue}.workspaceRef`);
  stringFieldFromRecord(executionPolicy, "sandbox", `${pathValue}.executionPolicy`);
  stringFieldFromRecord(executionPolicy, "approval", `${pathValue}.executionPolicy`);
  positiveIntegerFieldFromRecord(executionPolicy, "timeoutMs", `${pathValue}.executionPolicy`);
  stringFieldFromRecord(executionPolicy, "network", `${pathValue}.executionPolicy`);
  booleanFieldFromRecord(secretScope, "allowCredentialEcho", `${pathValue}.executionPolicy.secretScope`);
  return {
    sourcePath,
    tenantId: stringFieldFromRecord(sessionPolicy, "tenantId", pathValue),
    projectId: stringFieldFromRecord(sessionPolicy, "projectId", pathValue),
    providerId: stringFieldFromRecord(sessionPolicy, "providerId", pathValue),
    backendProfile: stringFieldFromRecord(sessionPolicy, "backendProfile", pathValue),
    workspaceRef: cloneJsonRecord(workspaceRef),
    executionPolicy: cloneJsonRecord(executionPolicy),
  };
}

export function validateAgentRunConfigEnvelope(input: Record<string, unknown>, sourcePath: string): void {
  if (input.version !== 1) throw new Error(`${sourcePath}: version must be 1`);
  if (input.kind !== "AgentRunConfig") throw new Error(`${sourcePath}: kind must be AgentRunConfig`);
  const metadata = record(input.metadata);
  if (stringFieldFromRecord(metadata, "name", "metadata") !== "agentrun") throw new Error(`${sourcePath}: metadata.name must be agentrun`);
}

export function readAgentRunPublicExposureConfig(value: unknown, managerBaseUrl: string, sourcePath: string): AgentRunPublicExposure | null {
  if (value === undefined || value === null) return null;
  const exposure = record(value);
  if (exposure.enabled !== true) return null;
  const proxyName = stringFieldFromRecord(exposure, "proxyName", "publicExposure");
  if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/u.test(proxyName)) throw new Error(`${sourcePath}: publicExposure.proxyName must be a DNS label`);
  const remotePort = numberFieldFromRecord(exposure, "remotePort", "publicExposure", { min: 1, max: 65_535 });
  const publicBaseUrl = normalizedHttpUrl(stringFieldFromRecord(exposure, "publicBaseUrl", "publicExposure"), `${sourcePath}: publicExposure.publicBaseUrl`);
  const masterBaseUrl = normalizedHttpUrl(stringFieldFromRecord(exposure, "masterBaseUrl", "publicExposure"), `${sourcePath}: publicExposure.masterBaseUrl`);
  if (!publicBaseUrl.startsWith("https://")) throw new Error(`${sourcePath}: publicExposure.publicBaseUrl must use https://`);
  if (publicBaseUrl !== managerBaseUrl) throw new Error(`${sourcePath}: manager.baseUrl must match publicExposure.publicBaseUrl`);
  if (!masterBaseUrl.startsWith(`http://127.0.0.1:${remotePort}`)) throw new Error(`${sourcePath}: publicExposure.masterBaseUrl must point to http://127.0.0.1:${remotePort}`);
  const masterFrps = record(exposure.masterFrps);
  const masterCaddy = record(exposure.masterCaddy);
  const caddyEnabled = masterCaddy.enabled === true;
  const domain = stringFieldFromRecord(masterCaddy, "domain", "publicExposure.masterCaddy");
  if (!/^[a-z0-9.-]+$/u.test(domain) || !domain.endsWith(".nip.io")) throw new Error(`${sourcePath}: publicExposure.masterCaddy.domain must be a nip.io hostname`);
  const caddyUpstreamBaseUrl = normalizedHttpUrl(stringFieldFromRecord(masterCaddy, "upstreamBaseUrl", "publicExposure.masterCaddy"), `${sourcePath}: publicExposure.masterCaddy.upstreamBaseUrl`);
  if (caddyUpstreamBaseUrl !== masterBaseUrl) throw new Error(`${sourcePath}: publicExposure.masterCaddy.upstreamBaseUrl must match publicExposure.masterBaseUrl`);
  return {
    enabled: true,
    proxyName,
    remotePort,
    publicBaseUrl,
    masterBaseUrl,
    masterFrps: {
      configPath: absolutePathField(masterFrps, "configPath", "publicExposure.masterFrps", sourcePath),
      containerName: stringFieldFromRecord(masterFrps, "containerName", "publicExposure.masterFrps"),
    },
    masterCaddy: {
      enabled: caddyEnabled,
      domain,
      configPath: absolutePathField(masterCaddy, "configPath", "publicExposure.masterCaddy", sourcePath),
      serviceName: stringFieldFromRecord(masterCaddy, "serviceName", "publicExposure.masterCaddy"),
      upstreamBaseUrl: caddyUpstreamBaseUrl,
      responseHeaderTimeoutSeconds: numberFieldFromRecord(masterCaddy, "responseHeaderTimeoutSeconds", "publicExposure.masterCaddy", { min: 1, max: 600 }),
    },
  };
}

export function normalizedHttpUrl(value: string, label: string): string {
  let url: URL;
  try {
    url = new URL(value);
  } catch {
    throw new Error(`${label} must be an http(s) URL`);
  }
  if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`${label} must be an http(s) URL`);
  return value.replace(/\/+$/u, "/");
}

export function absolutePathField(obj: Record<string, unknown>, key: string, pathValue: string, sourcePath: string): string {
  const value = stringFieldFromRecord(obj, key, pathValue);
  if (!value.startsWith("/")) throw new Error(`${sourcePath}: ${pathValue}.${key} must be an absolute path`);
  return value;
}

export function resolveAgentRunAuth(config: AgentRunClientConfig, env: NodeJS.ProcessEnv = process.env): AgentRunResolvedAuth {
  const envValue = env[config.auth.env]?.trim();
  if (envValue) return { source: "env", value: envValue };
  if (existsSync(config.auth.file)) {
    const fromFile = readEnvValueFromFile(config.auth.file, config.auth.env);
    if (fromFile !== null) return { source: "file", value: fromFile };
  }
  throw new AgentRunRestError("auth-missing", `${config.auth.env} is required for direct AgentRun REST access`, {
    bridge: {
      mode: "direct-http",
      clientRole: "render-only",
      configPath: config.sourcePath,
      auth: { env: config.auth.env, file: config.auth.file, source: "missing", header: config.auth.header, scheme: config.auth.scheme, valuesPrinted: false },
    },
  });
}

export function readEnvValueFromFile(pathValue: string, envName: string): string | null {
  const text = readFileSync(pathValue, "utf8");
  for (const line of text.split(/\r?\n/u)) {
    const trimmed = line.trim();
    if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
    const normalized = trimmed.startsWith("export ") ? trimmed.slice("export ".length).trim() : trimmed;
    const index = normalized.indexOf("=");
    if (index <= 0) continue;
    if (normalized.slice(0, index).trim() !== envName) continue;
    const raw = normalized.slice(index + 1).trim();
    const value = raw.replace(/^['"]|['"]$/gu, "");
    return value.length > 0 ? value : null;
  }
  return null;
}

export function agentRunRestBridgeMetadata(config: AgentRunClientConfig, auth: AgentRunResolvedAuth, method: AgentRunHttpMethod, pathValue: string): Record<string, unknown> {
  return {
    mode: "direct-http",
    clientRole: "render-only",
    compatibility: "no-hwlab-runtime-proxy-no-ssh-official-cli",
    configPath: config.sourcePath,
    baseUrl: config.manager.baseUrl,
    request: { method, path: pathValue, timeoutMs: config.manager.timeoutMs },
    auth: { env: config.auth.env, file: config.auth.file, source: auth.source, header: config.auth.header, scheme: config.auth.scheme, valuesPrinted: false },
  };
}

export function agentRunLaneRestBridgeMetadata(config: AgentRunClientConfig, target: AgentRunRestTarget, method: AgentRunHttpMethod, pathValue: string): Record<string, unknown> {
  return {
    mode: "lane-k8s-service-proxy",
    clientRole: "render-only",
    configPath: config.sourcePath,
    laneConfigPath: target.configPath,
    node: target.spec.nodeId,
    lane: target.spec.lane,
    kubeRoute: target.spec.nodeKubeRoute,
    namespace: target.spec.runtime.namespace,
    managerDeployment: target.spec.runtime.managerDeployment,
    baseUrl: target.spec.runtime.internalBaseUrl,
    request: { method, path: pathValue, timeoutMs: config.manager.timeoutMs },
    auth: { source: "manager-pod-env", env: config.auth.env, fallbackEnv: "AGENTRUN_API_KEY", header: config.auth.header, scheme: config.auth.scheme, valuesPrinted: false },
    valuesPrinted: false,
  };
}

export function addAgentRunNotFoundLookupHint(details: Record<string, unknown>, config: AgentRunClientConfig, method: AgentRunHttpMethod, pathValue: string, target: AgentRunRestTarget | null): Record<string, unknown> {
  const resource = agentRunResourceFromPath(pathValue);
  const laneConfig = readAgentRunLaneLookupConfig(config.sourcePath);
  const candidateLanes = laneConfig?.lanes ?? [];
  const nextCommands = candidateLanes
    .filter((lane) => typeof lane.node === "string" && lane.node.length > 0 && typeof lane.lane === "string" && lane.lane.length > 0)
    .slice(0, 4)
    .map((lane) => `bun scripts/cli.ts agentrun control-plane status --node ${lane.node} --lane ${lane.lane}`);
  return {
    ...details,
    laneAwareLookup: {
      kind: "agentrun-resource-not-found-on-current-endpoint",
      summary: resource === null
        ? "The request returned 404 on the currently configured AgentRun manager endpoint; a resource from another lane will not be visible through this baseUrl."
        : `${resource.kind}/${resource.id} returned 404 on the currently configured AgentRun manager endpoint; if it was created on another lane, inspect that lane before treating the resource as lost.`,
      request: {
        method,
        path: pathValue,
        ...(resource === null ? {} : { resource }),
      },
      currentEndpoint: {
        transport: target === null ? "direct-http" : "lane-k8s-service-proxy",
        baseUrl: target === null ? config.manager.baseUrl : target.spec.runtime.internalBaseUrl,
        configPath: config.sourcePath,
        defaultNode: laneConfig?.defaultNode ?? null,
        defaultLane: laneConfig?.defaultLane ?? null,
        selectedNode: target?.spec.nodeId ?? null,
        selectedLane: target?.spec.lane ?? null,
      },
      candidateLanes,
      nextCommands,
      note: target === null
        ? "Pass --node <node> --lane <lane> to query a YAML-declared non-default AgentRun lane."
        : "The resource was not found on the selected AgentRun lane; verify the run/session id and the HWLAB runtime lane that created it.",
      valuesPrinted: false,
    },
  };
}

export function agentRunResourceFromPath(pathValue: string): Record<string, string> | null {
  const run = pathValue.match(/\/runs\/([^/?#]+)/u);
  if (run?.[1] !== undefined) return { kind: "run", id: decodeURIComponent(run[1]) };
  const session = pathValue.match(/\/sessions\/([^/?#]+)/u);
  if (session?.[1] !== undefined) return { kind: "session", id: decodeURIComponent(session[1]) };
  const command = pathValue.match(/\/commands\/([^/?#]+)/u);
  if (command?.[1] !== undefined) return { kind: "command", id: decodeURIComponent(command[1]) };
  const task = pathValue.match(/\/queue\/tasks\/([^/?#]+)/u);
  if (task?.[1] !== undefined) return { kind: "task", id: decodeURIComponent(task[1]) };
  return null;
}

export function readAgentRunLaneLookupConfig(configPath: string): { defaultNode: string | null; defaultLane: string | null; lanes: Record<string, unknown>[] } | null {
  try {
    const raw = readFileSync(configPath, "utf8");
    const parsed = record(Bun.YAML.parse(raw) as unknown);
    const controlPlane = record(parsed.controlPlane);
    const defaultTarget = record(controlPlane.default);
    const lanes = record(controlPlane.lanes);
    return {
      defaultNode: stringOrNull(defaultTarget.node),
      defaultLane: stringOrNull(defaultTarget.lane),
      lanes: Object.entries(lanes).map(([laneName, rawLane]) => {
        const lane = record(rawLane);
        const runtime = record(lane.runtime);
        return {
          lane: laneName,
          node: stringOrNull(lane.node) ?? stringOrNull(lane.nodeId),
          version: stringOrNull(lane.version),
          namespace: stringOrNull(runtime.namespace),
          internalBaseUrl: stringOrNull(runtime.internalBaseUrl),
          isDefault: laneName === stringOrNull(defaultTarget.lane),
        };
      }),
    };
  } catch {
    return null;
  }
}

export function agentRunDryRunPlan(action: string, pathValue: string, body: Record<string, unknown>, confirmCommand: string, method: AgentRunHttpMethod = "POST", extra: Record<string, unknown> = {}): Record<string, unknown> {
  return {
    ok: true,
    action,
    dryRun: true,
    mutation: false,
    transport: "direct-http",
    clientRole: "render-only",
    request: {
      method,
      path: pathValue,
      bodyKeys: Object.keys(body),
      valuesPrinted: false,
    },
    ...extra,
    next: { confirm: confirmCommand },
    valuesPrinted: false,
  };
}

export function agentRunQuery(args: string[], names: string[]): string {
  const params = new URLSearchParams();
  const keyMap: Record<string, string> = {
    "after-seq": "afterSeq",
    "backend-profile": "backendProfile",
    "command": "commandId",
    "command-id": "commandId",
    "reader-id": "readerId",
    "run-id": "runId",
    "updated-after": "updatedAfter",
  };
  for (const name of names) {
    const value = agentRunOption(args, name);
    if (value !== null) params.set(keyMap[name] ?? name, value);
  }
  const query = params.toString();
  return query.length > 0 ? `?${query}` : "";
}

export function agentRunOption(args: string[], name: string): string | null {
  const flag = `--${name}`;
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index] ?? "";
    if (arg === flag) {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new AgentRunRestError("validation-failed", `${flag} requires a value`);
      return value;
    }
    if (arg.startsWith(`${flag}=`)) return arg.slice(flag.length + 1);
  }
  return null;
}

export function requiredAgentRunOption(args: string[], names: string[], message: string): string {
  for (const name of names) {
    const value = agentRunOption(args, name);
    if (value !== null) return value;
  }
  throw new AgentRunRestError("validation-failed", message);
}

export function agentRunHasFlag(args: string[], name: string): boolean {
  const flag = `--${name}`;
  return args.includes(flag) || args.some((arg) => arg.startsWith(`${flag}=`));
}

export async function requiredJsonBody(args: string[], context: string): Promise<Record<string, unknown>> {
  const body = await optionalJsonBody(args);
  if (Object.keys(body).length === 0) throw new AgentRunRestError("validation-failed", `${context} requires --json-stdin or --json-file <path>`);
  return body;
}

export async function optionalJsonBody(args: string[]): Promise<Record<string, unknown>> {
  const raw = readJsonTextFromArgs(args, "json");
  if (raw === null) return {};
  return record(JSON.parse(raw) as unknown);
}

export async function optionalRunnerJsonBody(args: string[]): Promise<Record<string, unknown>> {
  const raw = readJsonTextFromArgs(args, "runner-json");
  if (raw === null) return {};
  return record(JSON.parse(raw) as unknown);
}

export function readJsonTextFromArgs(args: string[], prefix: "json" | "runner-json"): string | null {
  if (agentRunHasFlag(args, `${prefix}-stdin`)) return readFileSync(0, "utf8");
  const file = agentRunOption(args, `${prefix}-file`);
  if (file !== null) return file === "-" ? readFileSync(0, "utf8") : readFileSync(file, "utf8");
  return null;
}

export function readYamlInputFromArgs(args: string[]): string {
  if (agentRunHasFlag(args, "yaml-stdin")) return readFileSync(0, "utf8");
  const file = agentRunOption(args, "yaml-file");
  if (file !== null) return file === "-" ? readFileSync(0, "utf8") : readFileSync(file, "utf8");
  throw new AgentRunRestError("validation-failed", "aipod-spec YAML input is required; use --yaml-stdin or --yaml-file <file>");
}

export async function aipodRenderInputFromArgs(args: string[], trailingPromptStart: number, overrides: Record<string, unknown> = {}): Promise<Record<string, unknown>> {
  const input = await optionalJsonBody(args);
  const prompt = optionalPromptFromArgs(args, trailingPromptStart);
  if (prompt !== null) input.prompt = prompt;
  copyAgentRunOptions(args, input, ["tenant-id", "project-id", "queue", "node", "lane", "title", "provider-id", "idempotency-key", "session-id"]);
  const sessionPolicy = readAgentRunClientConfig().client.sessionPolicy;
  const policyTarget = resolveAgentRunSessionPolicyTarget({ node: stringOrNull(input.node), lane: stringOrNull(input.lane) });
  if (input.node === undefined) input.node = policyTarget.spec.nodeId;
  if (input.lane === undefined) input.lane = policyTarget.spec.lane;
  if (input.providerId === undefined) input.providerId = defaultAgentRunProviderId(sessionPolicy, policyTarget);
  const profile = agentRunOption(args, "profile") ?? agentRunOption(args, "backend-profile");
  const backendProfile = profile ?? stringOrNull(input.backendProfile) ?? sessionPolicy.backendProfile;
  input.backendProfile = backendProfile;
  if (!isRecord(input.workspaceRef)) input.workspaceRef = cloneJsonRecord(sessionPolicy.workspaceRef);
  if (!isRecord(input.executionPolicy)) {
    input.executionPolicy = defaultAgentRunExecutionPolicy(backendProfile, sessionPolicy, policyTarget, { includeToolCredentials: true });
  }
  const priority = agentRunOption(args, "priority");
  if (priority) input.priority = Number(priority);
  const workspaceRef = jsonObjectOption(args, "workspace-json");
  if (workspaceRef !== null) input.workspaceRef = workspaceRef;
  return { ...input, ...overrides };
}

export function readPromptFromArgs(args: string[], trailingStart: number): string {
  const prompt = optionalPromptFromArgs(args, trailingStart);
  if (prompt === null) throw new AgentRunRestError("validation-failed", "prompt is required; use --prompt-stdin, --prompt-file, --prompt, or a trailing prompt");
  return prompt;
}

export function optionalPromptFromArgs(args: string[], trailingStart: number): string | null {
  if (agentRunHasFlag(args, "prompt-stdin") || agentRunHasFlag(args, "stdin")) return readFileSync(0, "utf8");
  const promptFile = agentRunOption(args, "prompt-file");
  if (promptFile !== null) return promptFile === "-" ? readFileSync(0, "utf8") : readFileSync(promptFile, "utf8");
  const prompt = agentRunOption(args, "prompt");
  if (prompt !== null) return prompt;
  const trailing = args.slice(trailingStart).filter((arg) => !arg.startsWith("-"));
  return trailing.length > 0 ? trailing.join(" ") : null;
}

export function queueDispatchBodyFromArgs(args: string[]): Record<string, unknown> {
  const body = recordFromMaybeJson(args);
  copyAgentRunOptions(args, body, ["idempotency-key", "image", "namespace", "attempt-id", "runner-id", "source-commit", "service-account-name"]);
  const managerUrl = agentRunOption(args, "runner-manager-url");
  if (managerUrl) body.managerUrl = managerUrl;
  return body;
}

export function recordFromMaybeJson(args: string[]): Record<string, unknown> {
  const raw = readJsonTextFromArgs(args, "json");
  if (raw === null) return {};
  return record(JSON.parse(raw) as unknown);
}

export function cancelBodyFromArgs(args: string[]): Record<string, unknown> {
  const reason = agentRunOption(args, "reason");
  return reason === null ? {} : { reason };
}

export function copyAgentRunOptions(args: string[], target: Record<string, unknown>, flagNames: string[]): void {
  for (const flagName of flagNames) {
    const value = agentRunOption(args, flagName);
    if (value === null) continue;
    target[camelCaseFlag(flagName)] = value;
  }
}

export function camelCaseFlag(flagName: string): string {
  return flagName.replace(/-([a-z])/gu, (_, letter: string) => String(letter).toUpperCase());
}

export function jsonObjectOption(args: string[], flagName: string): Record<string, unknown> | null {
  const value = agentRunOption(args, flagName);
  if (value === null) return null;
  return record(JSON.parse(value) as unknown);
}

export function queueSubmitConfirmCommand(args: string[], aipod?: string): string {
  const parts: string[] = [];
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index] ?? "";
    if (arg === "submit" || arg === "--dry-run") continue;
    if (arg === "--aipod" || arg === "--aipod-spec") {
      index += 1;
      continue;
    }
    if (arg.startsWith("--aipod=") || arg.startsWith("--aipod-spec=")) continue;
    parts.push(arg);
  }
  const dryRunless = parts.join(" ");
  return `bun scripts/cli.ts agentrun queue submit${aipod ? ` --aipod ${aipod}` : ""}${dryRunless.length > 0 ? ` ${dryRunless}` : ""}`.trim();
}

export function jsonInputDisclosureFromArgs(args: string[]): Record<string, unknown> {
  return {
    source: agentRunHasFlag(args, "json-stdin") ? "stdin" : agentRunOption(args, "json-file") ?? "<task.json>",
    valuesPrinted: false,
  };
}

export function resolveAgentRunSessionPolicyTarget(options: AgentRunRestTargetOptions = { node: null, lane: null }): AgentRunSessionPolicyTarget {
  if (activeAgentRunRestTarget !== null) {
    return {
      configPath: activeAgentRunRestTarget.configPath,
      spec: activeAgentRunRestTarget.spec,
      source: "selected-lane",
      transport: "lane-k8s-service-proxy",
    };
  }
  const node = options.node ?? null;
  const lane = options.lane ?? null;
  const { configPath, spec } = resolveAgentRunLaneTarget({ node, lane });
  return {
    configPath,
    spec,
    source: node !== null || lane !== null ? "selected-lane" : "default-lane",
    transport: "direct-http",
  };
}

export function defaultAgentRunProviderId(sessionPolicy: AgentRunSessionPolicyConfig, target: AgentRunSessionPolicyTarget): string {
  return target.source === "selected-lane" ? target.spec.nodeId : sessionPolicy.providerId;
}

export function defaultAgentRunExecutionPolicy(
  profile: string,
  sessionPolicy: AgentRunSessionPolicyConfig = readAgentRunClientConfig().client.sessionPolicy,
  target: AgentRunSessionPolicyTarget = resolveAgentRunSessionPolicyTarget(),
  options: { includeToolCredentials?: boolean; basePolicy?: Record<string, unknown> } = {},
): Record<string, unknown> {
  const basePolicy = Object.keys(record(options.basePolicy)).length > 0
    ? cloneJsonRecord(record(options.basePolicy))
    : cloneJsonRecord(sessionPolicy.executionPolicy);
  return agentRunExecutionPolicyWithLaneCredentials(basePolicy, profile, target, options.includeToolCredentials === true);
}

export function agentRunExecutionPolicyWithLaneCredentials(basePolicy: Record<string, unknown>, profile: string, target: AgentRunSessionPolicyTarget, includeToolCredentials: boolean): Record<string, unknown> {
  const spec = target.spec;
  const credentials = agentRunProviderCredentialRefs(spec, profile);
  if (credentials.length === 0) {
    throw new AgentRunRestError("validation-failed", `${target.configPath} has no providerCredential Secret binding for backendProfile=${profile} on ${target.source} ${spec.nodeId}/${spec.lane}`);
  }
  const providerCredentials = credentials.map((credential) => {
    if (credential.secretRef.namespace !== spec.runtime.namespace) {
      throw new AgentRunRestError("validation-failed", `providerCredential ${profile} Secret ${credential.secretRef.name} is in namespace ${credential.secretRef.namespace}, expected target lane namespace ${spec.runtime.namespace}`);
    }
    return {
      profile: credential.profile,
      secretRef: {
        name: credential.secretRef.name,
        keys: credential.secretRef.keys,
      },
    };
  });
  const toolCredentials = includeToolCredentials ? agentRunToolCredentialRefs(spec).map((credential) => {
    if (credential.secretRef.namespace !== spec.runtime.namespace) {
      throw new AgentRunRestError("validation-failed", `toolCredential ${credential.tool} Secret ${credential.secretRef.name} is in namespace ${credential.secretRef.namespace}, expected target lane namespace ${spec.runtime.namespace}`);
    }
    return {
      tool: credential.tool,
      purpose: credential.purpose,
      secretRef: {
        name: credential.secretRef.name,
        keys: [credential.secretRef.key],
      },
      projection: credential.projection,
    };
  }) : [];
  const secretScope = record(basePolicy.secretScope);
  return {
    ...basePolicy,
    secretScope: {
      ...secretScope,
      providerCredentials,
      ...(toolCredentials.length === 0 ? {} : { toolCredentials }),
    },
  };
}

export const AGENTRUN_DEFAULT_TOOL_CREDENTIAL_IDS = new Set(["tool-github-pr-token", "tool-unidesk-ssh-token"]);

export function agentRunToolCredentialRefs(spec: AgentRunLaneSpec, options: { includeUnsupported?: boolean } = {}): Array<{ tool: string; purpose: string; projection: Record<string, unknown>; sourceId: string; secretRef: { namespace: string; name: string; key: string }; valuesPrinted: false }> {
  return spec.secrets
    .filter((secret) => secret.providerCredentialProfile === null && secret.id.startsWith("tool-"))
    .map((secret) => {
      const binding = agentRunToolCredentialBinding(secret.id, secret.targetRef.key);
      return {
        tool: binding.tool,
        purpose: binding.purpose,
        projection: binding.projection,
        sourceId: secret.id,
        secretRef: secret.targetRef,
        valuesPrinted: false as const,
      };
    })
    .filter((credential) => options.includeUnsupported === true || AGENTRUN_DEFAULT_TOOL_CREDENTIAL_IDS.has(credential.sourceId));
}

export function agentRunToolCredentialBinding(sourceId: string, key: string): { tool: string; purpose: string; projection: Record<string, unknown> } {
  if (sourceId === "tool-github-pr-token") {
    return {
      tool: "github",
      purpose: "github-pr",
      projection: { kind: "env", envName: key, secretKey: key },
    };
  }
  if (sourceId === "tool-unidesk-ssh-token") {
    return {
      tool: "unidesk-ssh",
      purpose: "ssh-passthrough",
      projection: { kind: "env", envName: key, secretKey: key },
    };
  }
  return {
    tool: sourceId.replace(/^tool-/u, "").replace(/-token$/u, ""),
    purpose: sourceId.replace(/^tool-/u, ""),
    projection: {},
  };
}

export function renderAgentRunSessionPolicyExplanation(args: string[] = [], options: AgentRunRestTargetOptions = { node: null, lane: null }): string {
  const config = readAgentRunClientConfig();
  const target = resolveAgentRunSessionPolicyTarget(options);
  const spec = target.spec;
  const sessionPolicy = config.client.sessionPolicy;
  const backendProfile = agentRunOption(args, "profile") ?? agentRunOption(args, "backend-profile") ?? sessionPolicy.backendProfile;
  const providerId = agentRunOption(args, "provider-id") ?? defaultAgentRunProviderId(sessionPolicy, target);
  const workspaceRef = jsonObjectOption(args, "workspace-json") ?? cloneJsonRecord(sessionPolicy.workspaceRef);
  const credentials = agentRunProviderCredentialRefs(spec, backendProfile);
  const execution = defaultAgentRunExecutionPolicy(backendProfile, sessionPolicy, target);
  const credentialSources = credentials.map((credential) => ({
    profile: credential.profile,
    secretRef: credential.secretRef,
    valuesPrinted: false,
  }));
  const toolCredentialSources = agentRunToolCredentialRefs(spec, { includeUnsupported: true });
  return [
    "KIND: session-policy",
    `CONFIG: ${config.sourcePath}`,
    `LANE CONFIG: ${target.configPath}`,
    `TARGET LANE: ${spec.nodeId}/${spec.lane}`,
    `POLICY SOURCE: ${target.source}`,
    `TRANSPORT: ${target.transport}`,
    `DEFAULTS: tenantId=${sessionPolicy.tenantId} projectId=${sessionPolicy.projectId} providerId=${providerId} backendProfile=${backendProfile}`,
    `WORKSPACE: ${JSON.stringify(workspaceRef)}`,
    `EXECUTION: ${JSON.stringify(execution)}`,
    `PROVIDER CREDENTIAL SOURCES: ${JSON.stringify(credentialSources)}`,
    `TOOL CREDENTIAL SOURCES: ${JSON.stringify(toolCredentialSources)}`,
    "VALUES: secret payloads are not printed; valuesPrinted=false",
  ].join("\n");
}

export function safeAgentRunEnvelope(envelope: Record<string, unknown>): Record<string, unknown> {
  return pickCompact(envelope, ["ok", "failureKind", "message", "code", "details", "valuesPrinted"]);
}

export function normalizeAgentRunFailureKind(raw: string | null, httpStatus: number): AgentRunFailureKind {
  if (raw === "agentrun-unreachable") return "agentrun-connect-failed";
  if (raw === "auth-missing" || raw === "auth-failed" || raw === "agentrun-connect-failed" || raw === "agentrun-proxy-exec-failed" || raw === "agentrun-manager-fetch-failed" || raw === "agentrun-timeout" || raw === "schema-mismatch" || raw === "unsupported-version" || raw === "validation-failed" || raw === "not-found") return raw;
  if (httpStatus === 401 || httpStatus === 403) return "auth-failed";
  if (httpStatus === 404) return "not-found";
  return raw === "schema-invalid" ? "validation-failed" : "validation-failed";
}

export function classifyAgentRunProxyFailureKind(raw: string | null): AgentRunFailureKind {
  if (raw === "manager-pod-fetch-timeout" || raw === "agentrun-timeout") return "agentrun-timeout";
  if (raw === "manager-pod-fetch-failed") return "agentrun-manager-fetch-failed";
  const normalized = normalizeAgentRunFailureKind(raw, 0);
  return normalized === "validation-failed" ? "agentrun-proxy-exec-failed" : normalized;
}

export function stringFieldFromRecord(obj: Record<string, unknown>, key: string, pathValue: string): string {
  const value = obj[key];
  if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${pathValue}.${key} must be a non-empty string`);
  return value.trim();
}

export function numberFieldFromRecord(obj: Record<string, unknown>, key: string, pathValue: string, bounds: { min: number; max: number }): number {
  const value = obj[key];
  if (typeof value !== "number" || !Number.isFinite(value) || value < bounds.min || value > bounds.max) throw new Error(`${pathValue}.${key} must be a number between ${bounds.min} and ${bounds.max}`);
  return value;
}

export function positiveIntegerFieldFromRecord(obj: Record<string, unknown>, key: string, pathValue: string): number {
  const value = obj[key];
  if (!Number.isInteger(value) || Number(value) <= 0) throw new Error(`${pathValue}.${key} must be a positive integer`);
  return Number(value);
}

export function booleanFieldFromRecord(obj: Record<string, unknown>, key: string, pathValue: string): boolean {
  const value = obj[key];
  if (typeof value !== "boolean") throw new Error(`${pathValue}.${key} must be a boolean`);
  return value;
}

export function cloneJsonRecord(value: Record<string, unknown>): Record<string, unknown> {
  return JSON.parse(JSON.stringify(value)) as Record<string, unknown>;
}

export interface AgentRunClientConfig {
  sourcePath: string;
  manager: {
    baseUrl: string;
    timeoutMs: number;
  };
  auth: {
    env: string;
    file: string;
    header: string;
    scheme: string;
  };
  client: {
    role: string;
    transport: string;
    sessionPolicy: AgentRunSessionPolicyConfig;
  };
  publicExposure: AgentRunPublicExposure | null;
}

export interface AgentRunSessionPolicyConfig {
  sourcePath: string;
  tenantId: string;
  projectId: string;
  providerId: string;
  backendProfile: string;
  workspaceRef: Record<string, unknown>;
  executionPolicy: Record<string, unknown>;
}

export interface AgentRunPublicExposure {
  enabled: true;
  proxyName: string;
  remotePort: number;
  publicBaseUrl: string;
  masterBaseUrl: string;
  masterFrps: {
    configPath: string;
    containerName: string;
  };
  masterCaddy: {
    enabled: boolean;
    domain: string;
    configPath: string;
    serviceName: string;
    upstreamBaseUrl: string;
    responseHeaderTimeoutSeconds: number;
  };
}
