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

// Moved mechanically from scripts/src/agentrun.ts:4220-5176 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 { capture, captureJsonPayload, compactCapture, progressEvent, record, shQuote, sleep, stringOrNull } from "./utils";

export function yamlLaneGitopsPublishPayloadFromProbe(probePayload: Record<string, unknown>): Record<string, unknown> {
  const logsTail = stringOrNull(probePayload.logsTail);
  if (logsTail === null) return {};
  const lines = logsTail.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean);
  for (let index = lines.length - 1; index >= 0; index -= 1) {
    const line = lines[index];
    if (!line.startsWith("{") || !line.endsWith("}")) continue;
    try {
      const parsed = JSON.parse(line) as unknown;
      const payload = record(parsed);
      if (payload.ok === true || payload.ok === false) return payload;
    } catch {
      continue;
    }
  }
  return {};
}

export function yamlLaneGitopsPublishJobManifest(spec: AgentRunLaneSpec, files: readonly { path: string; content: string }[], name: string): Record<string, unknown> {
  return {
    apiVersion: "batch/v1",
    kind: "Job",
    metadata: {
      name,
      namespace: spec.gitMirror.namespace,
      labels: {
        "app.kubernetes.io/name": "gitops-publish",
        "app.kubernetes.io/part-of": "agentrun",
        "agentrun.pikastech.local/lane": spec.version,
        "agentrun.pikastech.local/node": spec.nodeId,
        "agentrun.pikastech.local/component": "gitops-publish",
      },
    },
    spec: {
      backoffLimit: 0,
      activeDeadlineSeconds: 600,
      ttlSecondsAfterFinished: 3600,
      template: {
        metadata: {
          labels: {
            "app.kubernetes.io/name": "gitops-publish",
            "app.kubernetes.io/part-of": "agentrun",
            "agentrun.pikastech.local/lane": spec.version,
            "agentrun.pikastech.local/node": spec.nodeId,
            "agentrun.pikastech.local/component": "gitops-publish",
          },
        },
        spec: {
          restartPolicy: "Never",
          volumes: [yamlLaneGitMirrorCacheVolume(spec)],
          containers: [{
            name: "publish",
            image: spec.gitMirror.toolsImage,
            imagePullPolicy: "IfNotPresent",
            command: ["/bin/sh", "-ec", yamlLaneGitopsPublishShell(spec, files, name)],
            volumeMounts: [{ name: "cache", mountPath: "/cache" }],
          }],
        },
      },
    },
  };
}

export function yamlLaneGitopsPublishShell(spec: AgentRunLaneSpec, files: readonly { path: string; content: string }[], jobName: string): string {
  const filesB64 = Buffer.from(JSON.stringify(files.map((file) => ({
    path: file.path,
    contentBase64: Buffer.from(file.content, "utf8").toString("base64"),
  }))), "utf8").toString("base64");
  return [
    "set -eu",
    `repository=${shQuote(spec.source.repository)}`,
    `gitops_branch=${shQuote(spec.gitops.branch)}`,
    `gitops_root=${shQuote(spec.deployment.gitopsRoot)}`,
    `artifact_catalog=${shQuote(spec.deployment.artifactCatalogPath)}`,
    `files_b64=${shQuote(filesB64)}`,
    `job_name=${shQuote(jobName)}`,
    "remote=\"/cache/${repository}.git\"",
    "remote_mode=git-mirror-cache-volume",
    "publish_worktree=\"/tmp/$job_name\"",
    "write_failed_status() { code=$?; if [ \"$code\" -ne 0 ]; then printf '%s\\n' \"{\\\"ok\\\":false,\\\"status\\\":\\\"failed\\\",\\\"exitCode\\\":$code,\\\"jobName\\\":\\\"$job_name\\\",\\\"publishWorkspace\\\":\\\"$publish_worktree\\\",\\\"gitopsBranch\\\":\\\"$gitops_branch\\\",\\\"publishRemoteMode\\\":\\\"$remote_mode\\\",\\\"valuesPrinted\\\":false}\"; fi; exit \"$code\"; }",
    "trap write_failed_status EXIT",
    "rm -rf \"$publish_worktree\"",
    "git clone --no-checkout \"$remote\" \"$publish_worktree\"",
    "cd \"$publish_worktree\"",
    "git fetch origin \"$gitops_branch\" || true",
    "if git rev-parse --verify \"refs/remotes/origin/$gitops_branch^{commit}\" >/dev/null 2>&1; then",
    "  git checkout -B \"$gitops_branch\" \"refs/remotes/origin/$gitops_branch\"",
    "else",
    "  git checkout --orphan \"$gitops_branch\"",
    "  git rm -rf . >/dev/null 2>&1 || true",
    "fi",
    "git rm -rf --ignore-unmatch \"$gitops_root\" \"$artifact_catalog\" source.json >/dev/null 2>&1 || true",
    "rm -rf \"$gitops_root\" \"$artifact_catalog\" source.json",
    "FILES_B64=\"$files_b64\" node <<'NODE'",
    "const fs = require('node:fs');",
    "const path = require('node:path');",
    "const files = JSON.parse(Buffer.from(process.env.FILES_B64 || '', 'base64').toString('utf8'));",
    "for (const file of files) {",
    "  const target = path.resolve(process.cwd(), file.path);",
    "  if (!target.startsWith(process.cwd() + path.sep)) throw new Error(`refuse path outside workspace: ${file.path}`);",
    "  fs.mkdirSync(path.dirname(target), { recursive: true });",
    "  fs.writeFileSync(target, Buffer.from(file.contentBase64, 'base64'));",
    "}",
    "NODE",
    "git add source.json \"$artifact_catalog\" \"$gitops_root\"",
    "if git diff --quiet --cached; then changed=false; else changed=true; git -c user.email=agentrun@unidesk.local -c user.name='UniDesk AgentRun Ops' commit -m \"deploy: render AgentRun ${gitops_branch} from UniDesk YAML\"; fi",
    "git push -u origin \"$gitops_branch\"",
    "gitops_commit=$(git rev-parse HEAD)",
    "CHANGED=\"$changed\" GITOPS_BRANCH=\"$gitops_branch\" GITOPS_COMMIT=\"$gitops_commit\" FILE_COUNT=\"" + String(files.length) + "\" JOB_NAME=\"$job_name\" PUBLISH_REMOTE_MODE=\"$remote_mode\" node <<'NODE'",
    "console.log(JSON.stringify({ ok: true, status: 'succeeded', jobName: process.env.JOB_NAME, changed: process.env.CHANGED === 'true', gitopsBranch: process.env.GITOPS_BRANCH, gitopsCommit: process.env.GITOPS_COMMIT, publishRemoteMode: process.env.PUBLISH_REMOTE_MODE || null, fileCount: Number(process.env.FILE_COUNT || 0), valuesPrinted: false }));",
    "NODE",
    "trap - EXIT",
  ].join("\n");
}

export const AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS = 5;

export const AGENTRUN_GIT_MIRROR_RETRYABLE_PATTERN =
  /(kex_exchange_identification|connection closed by remote host|connection closed by unknown|could not read from remote repository|ssh\.github\.com|github\.com|fetch-pack|early eof|econnreset|read econnreset|proxy-connect|tunnel socket error)/i;

export function agentRunGitMirrorRetryDelayMs(attempt: number): number {
  return Math.min(15_000, 1_000 * Math.pow(2, Math.max(0, attempt - 1)));
}

export function agentRunGitMirrorFailureText(value: unknown): string {
  try {
    return JSON.stringify(value);
  } catch {
    return String(value);
  }
}

export function agentRunGitMirrorRetryableFailure(
  spec: AgentRunLaneSpec,
  action: "sync" | "flush",
  attempt: number,
  value: unknown,
): Record<string, unknown> | null {
  const text = agentRunGitMirrorFailureText(value);
  if (!AGENTRUN_GIT_MIRROR_RETRYABLE_PATTERN.test(text)) return null;
  const retryExhausted = attempt >= AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS;
  const retryBackoffMs = retryExhausted ? 0 : agentRunGitMirrorRetryDelayMs(attempt);
  return {
    retryable: true,
    retryAttempt: attempt,
    retryMaxAttempts: AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS,
    retryLabel: `${attempt}/${AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS}`,
    retryExhausted,
    retryBackoffMs,
    backoffPolicy: "exponential",
    failureKind: "git-mirror-upstream-transient",
    upstream: "git-mirror-ssh",
    action,
    node: spec.nodeId,
    lane: spec.lane,
    message: retryExhausted
      ? "AgentRun git-mirror upstream transient failure exhausted retries; stopping instead of continuing silently."
      : "AgentRun git-mirror upstream transient failure; retrying with exponential backoff.",
    next: retryExhausted
      ? { stopped: true, reason: "retry-exhausted" }
      : {
          retry: `bun scripts/cli.ts agentrun git-mirror ${action} --node ${spec.nodeId} --lane ${spec.lane} --confirm --wait`,
          delayMs: retryBackoffMs,
        },
    valuesPrinted: false,
  };
}

export function agentRunGitMirrorRetrySummary(attempts: Record<string, unknown>[]): Record<string, unknown> {
  const last = attempts.length > 0 ? record(attempts[attempts.length - 1]) : {};
  return {
    policy: "exponential",
    maxAttempts: AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS,
    attempts,
    exhausted: last.retryExhausted === true,
    stopped: last.retryExhausted === true,
    valuesPrinted: false,
  };
}

export async function runYamlLaneGitMirrorSyncJob(config: UniDeskConfig, spec: AgentRunLaneSpec): Promise<Record<string, unknown>> {
  return await runYamlLaneGitMirrorJob(config, spec, "sync");
}

export async function runYamlLaneGitMirrorFlushJob(config: UniDeskConfig, spec: AgentRunLaneSpec): Promise<Record<string, unknown>> {
  return await runYamlLaneGitMirrorJob(config, spec, "flush");
}

export async function runYamlLaneGitMirrorJob(config: UniDeskConfig, spec: AgentRunLaneSpec, action: "sync" | "flush"): Promise<Record<string, unknown>> {
  const attempts: Record<string, unknown>[] = [];
  for (let attempt = 1; attempt <= AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS; attempt += 1) {
    const jobPrefix = action === "sync" ? spec.gitMirror.syncJobPrefix : spec.gitMirror.flushJobPrefix;
    const jobName = `${jobPrefix}-${Date.now().toString(36)}-${attempt}`.slice(0, 63);
    const manifest = yamlLaneGitMirrorJobManifest(spec, action, jobName);
    const created = await capture(config, spec.nodeKubeRoute, ["sh", "--", createYamlLaneJobScript(spec.gitMirror.namespace, jobName, manifest)]);
    if (created.exitCode !== 0) {
      const result = { ok: false, phase: "create-job", jobName, capture: compactCapture(created, { full: true, stdoutTailChars: 4000, stderrTailChars: 4000 }), valuesPrinted: false };
      const retryableFailure = agentRunGitMirrorRetryableFailure(spec, action, attempt, result);
      attempts.push({ attempt, retryLabel: `${attempt}/${AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS}`, jobName, ok: false, phase: "create-job", retryableFailure, retryExhausted: retryableFailure !== null && record(retryableFailure).retryExhausted === true });
      if (retryableFailure !== null && record(retryableFailure).retryExhausted !== true) {
        progressEvent("agentrun.yaml-lane.git-mirror.retry", retryableFailure);
        await sleep(agentRunGitMirrorRetryDelayMs(attempt));
        continue;
      }
      return { ...result, retryableFailure, retry: agentRunGitMirrorRetrySummary(attempts) };
    }
    const startedAt = Date.now();
    let polls = 0;
    let lastProbe: SshCaptureResult | null = null;
    while (Date.now() - startedAt < 300_000) {
      polls += 1;
      lastProbe = await capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneJobProbeScript(spec.gitMirror.namespace, jobName)]);
      const payload = captureJsonPayload(lastProbe);
      progressEvent("agentrun.yaml-lane.git-mirror.progress", {
        node: spec.nodeId,
        lane: spec.lane,
        action,
        jobName,
        polls,
        retryAttempt: attempt,
        retryMaxAttempts: AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS,
        retryLabel: `${attempt}/${AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS}`,
        succeeded: payload.succeeded === true,
        failed: payload.failed === true,
        elapsedMs: Date.now() - startedAt,
      });
      if (payload.succeeded === true) {
        attempts.push({ attempt, retryLabel: `${attempt}/${AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS}`, jobName, ok: true, polls, elapsedMs: Date.now() - startedAt });
        return { ok: true, jobName, polls, elapsedMs: Date.now() - startedAt, result: payload, retry: agentRunGitMirrorRetrySummary(attempts), valuesPrinted: false };
      }
      if (payload.failed === true) {
        const result = { ok: false, jobName, polls, elapsedMs: Date.now() - startedAt, result: payload, capture: compactCapture(lastProbe, { full: true, stdoutTailChars: 5000, stderrTailChars: 3000 }), valuesPrinted: false };
        const retryableFailure = agentRunGitMirrorRetryableFailure(spec, action, attempt, result);
        attempts.push({ attempt, retryLabel: `${attempt}/${AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS}`, jobName, ok: false, polls, elapsedMs: Date.now() - startedAt, retryableFailure, retryExhausted: retryableFailure !== null && record(retryableFailure).retryExhausted === true });
        if (retryableFailure !== null && record(retryableFailure).retryExhausted !== true) {
          progressEvent("agentrun.yaml-lane.git-mirror.retry", retryableFailure);
          await sleep(agentRunGitMirrorRetryDelayMs(attempt));
          break;
        }
        return { ...result, retryableFailure, retry: agentRunGitMirrorRetrySummary(attempts) };
      }
      await sleep(5_000);
    }
    if (attempts.length > 0 && record(attempts[attempts.length - 1]).jobName === jobName) continue;
    const degradedReason = `git-mirror-${action}-job-timeout`;
    const result = { ok: false, jobName, polls, elapsedMs: Date.now() - startedAt, degradedReason, capture: lastProbe === null ? null : compactCapture(lastProbe, { full: true, stdoutTailChars: 5000, stderrTailChars: 3000 }), valuesPrinted: false };
    const retryableFailure = agentRunGitMirrorRetryableFailure(spec, action, attempt, result);
    attempts.push({ attempt, retryLabel: `${attempt}/${AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS}`, jobName, ok: false, polls, elapsedMs: Date.now() - startedAt, degradedReason, retryableFailure, retryExhausted: retryableFailure !== null && record(retryableFailure).retryExhausted === true });
    if (retryableFailure !== null && record(retryableFailure).retryExhausted !== true) {
      progressEvent("agentrun.yaml-lane.git-mirror.retry", retryableFailure);
      await sleep(agentRunGitMirrorRetryDelayMs(attempt));
      continue;
    }
    return { ...result, retryableFailure, retry: agentRunGitMirrorRetrySummary(attempts) };
  }
  return { ok: false, degradedReason: `git-mirror-${action}-retry-loop-exhausted`, retry: agentRunGitMirrorRetrySummary(attempts), valuesPrinted: false };
}

export function yamlLanePipelineRunCreateScript(spec: AgentRunLaneSpec, sourceCommit: string, pipelineRun: string): string {
  const manifest = {
    apiVersion: "tekton.dev/v1",
    kind: "PipelineRun",
    metadata: {
      name: pipelineRun,
      namespace: spec.ci.namespace,
      labels: {
        "app.kubernetes.io/part-of": "agentrun",
        "agentrun.pikastech.local/lane": spec.version,
        "agentrun.pikastech.local/node": spec.nodeId,
        "agentrun.pikastech.local/source-commit": sourceCommit,
        "agentrun.pikastech.local/trigger": "unidesk-yaml-only",
      },
    },
    spec: {
      pipelineRef: { name: spec.ci.pipeline },
      taskRunTemplate: {
        serviceAccountName: spec.ci.serviceAccountName,
        podTemplate: { hostNetwork: true, dnsPolicy: "ClusterFirstWithHostNet", securityContext: { fsGroup: 1000 } },
      },
      params: [
        { name: "git-url", value: spec.source.remote },
        { name: "git-read-url", value: spec.gitMirror.readUrl },
        { name: "git-write-url", value: spec.gitMirror.writeUrl },
        { name: "source-branch", value: spec.source.branch },
        { name: "gitops-branch", value: spec.gitops.branch },
        { name: "revision", value: sourceCommit },
        { name: "registry-prefix", value: spec.ci.registryPrefix },
        { name: "tools-image", value: spec.ci.toolsImage },
      ],
      workspaces: [
        { name: "source", volumeClaimTemplate: { spec: { accessModes: ["ReadWriteOnce"], resources: { requests: { storage: "4Gi" } } } } },
        { name: "git-ssh", secret: { secretName: spec.gitMirror.sshSecretName } },
      ],
    },
  };
  const manifestB64 = Buffer.from(JSON.stringify(manifest), "utf8").toString("base64");
  return [
    "set -eu",
    `namespace=${shQuote(spec.ci.namespace)}`,
    `pipeline_run=${shQuote(pipelineRun)}`,
    `manifest_b64=${shQuote(manifestB64)}`,
    "tmp=$(mktemp)",
    "trap 'rm -f \"$tmp\"' EXIT",
    "printf '%s' \"$manifest_b64\" | base64 -d > \"$tmp\"",
    "existing_status=$(kubectl -n \"$namespace\" get pipelinerun \"$pipeline_run\" -o 'jsonpath={.status.conditions[0].status}' 2>/dev/null || true)",
    "if [ \"$existing_status\" = False ]; then kubectl -n \"$namespace\" delete pipelinerun \"$pipeline_run\" --ignore-not-found=true --wait=false >/dev/null 2>&1 || true; fi",
    "if kubectl -n \"$namespace\" get pipelinerun \"$pipeline_run\" >/dev/null 2>&1; then created=false; else kubectl create -f \"$tmp\"; created=true; fi",
    "status=$(kubectl -n \"$namespace\" get pipelinerun \"$pipeline_run\" -o 'jsonpath={.status.conditions[0].status}' 2>/dev/null || true)",
    "CREATED=\"$created\" STATUS=\"$status\" PIPELINE_RUN=\"$pipeline_run\" node <<'NODE'",
    "console.log(JSON.stringify({ ok: true, pipelineRun: process.env.PIPELINE_RUN, created: process.env.CREATED === 'true', status: process.env.STATUS || null, valuesPrinted: false }));",
    "NODE",
  ].join("\n");
}

export function createYamlLaneJobScript(namespace: string, jobName: string, manifest: Record<string, unknown>): string {
  const manifestB64 = Buffer.from(JSON.stringify(manifest), "utf8").toString("base64");
  return [
    "set -eu",
    `namespace=${shQuote(namespace)}`,
    `job=${shQuote(jobName)}`,
    `manifest_b64=${shQuote(manifestB64)}`,
    "tmp=$(mktemp)",
    "trap 'rm -f \"$tmp\"' EXIT",
    "printf '%s' \"$manifest_b64\" | base64 -d > \"$tmp\"",
    "kubectl -n \"$namespace\" delete job \"$job\" --ignore-not-found=true --wait=false >/dev/null 2>&1 || true",
    "kubectl create -f \"$tmp\"",
    "JOB=\"$job\" node <<'NODE'",
    "console.log(JSON.stringify({ ok: true, jobName: process.env.JOB, valuesPrinted: false }));",
    "NODE",
  ].join("\n");
}

export function yamlLaneJobProbeScript(namespace: string, jobName: string): string {
  return [
    "set +e",
    `namespace=${shQuote(namespace)}`,
    `job=${shQuote(jobName)}`,
    "kubectl -n \"$namespace\" get job \"$job\" -o json > /tmp/agentrun-job.json 2>/dev/null",
    "job_exit=$?",
    "kubectl -n \"$namespace\" logs \"job/$job\" --tail=120 > /tmp/agentrun-job.log 2>/dev/null",
    "JOB_EXIT=\"$job_exit\" JOB=\"$job\" node <<'NODE'",
    "const fs = require('node:fs');",
    "let job = null; try { job = JSON.parse(fs.readFileSync('/tmp/agentrun-job.json', 'utf8')); } catch {}",
    "let log = ''; try { log = fs.readFileSync('/tmp/agentrun-job.log', 'utf8'); } catch {}",
    "const succeeded = Number(job?.status?.succeeded || 0) > 0;",
    "const failed = Number(job?.status?.failed || 0) > 0;",
    "console.log(JSON.stringify({ ok: process.env.JOB_EXIT === '0', jobName: process.env.JOB, succeeded, failed, active: job?.status?.active || 0, logsTail: log.slice(-4000), valuesPrinted: false }));",
    "NODE",
  ].join("\n");
}

export function yamlLaneGitMirrorCacheVolume(spec: AgentRunLaneSpec): Record<string, unknown> {
  if (spec.gitMirror.cacheHostPath !== null) {
    return { name: "cache", hostPath: { path: spec.gitMirror.cacheHostPath, type: "DirectoryOrCreate" } };
  }
  return { name: "cache", persistentVolumeClaim: { claimName: spec.gitMirror.cachePvc } };
}

export function yamlLaneGitMirrorJobManifest(spec: AgentRunLaneSpec, action: "sync" | "flush", name: string): Record<string, unknown> {
  const proxyHostNeedsHostNetwork = spec.gitMirror.githubProxy.host === "127.0.0.1" || spec.gitMirror.githubProxy.host === "localhost";
  return {
    apiVersion: "batch/v1",
    kind: "Job",
    metadata: {
      name,
      namespace: spec.gitMirror.namespace,
      labels: {
        "app.kubernetes.io/name": "git-mirror",
        "app.kubernetes.io/part-of": "agentrun",
        "agentrun.pikastech.local/lane": spec.version,
        "agentrun.pikastech.local/node": spec.nodeId,
        "agentrun.pikastech.local/component": action,
      },
    },
    spec: {
      backoffLimit: 0,
      activeDeadlineSeconds: 600,
      ttlSecondsAfterFinished: 3600,
      template: {
        metadata: {
          labels: {
            "app.kubernetes.io/name": "git-mirror",
            "app.kubernetes.io/part-of": "agentrun",
            "agentrun.pikastech.local/lane": spec.version,
            "agentrun.pikastech.local/node": spec.nodeId,
            "agentrun.pikastech.local/component": action,
          },
        },
        spec: {
          restartPolicy: "Never",
          ...(proxyHostNeedsHostNetwork ? { hostNetwork: true, dnsPolicy: "ClusterFirstWithHostNet" } : {}),
          volumes: [
            yamlLaneGitMirrorCacheVolume(spec),
            { name: "git-ssh", secret: { secretName: spec.gitMirror.sshSecretName, defaultMode: 0o400 } },
          ],
          containers: [{
            name: action,
            image: spec.gitMirror.toolsImage,
            imagePullPolicy: "IfNotPresent",
            command: ["/bin/sh", "-ec", action === "sync" ? yamlLaneGitMirrorSyncShell(spec) : yamlLaneGitMirrorFlushShell(spec)],
            volumeMounts: [
              { name: "cache", mountPath: "/cache" },
              { name: "git-ssh", mountPath: "/git-ssh", readOnly: true },
            ],
          }],
        },
      },
    },
  };
}

export function yamlLaneGitMirrorSshSetupShellLines(spec: AgentRunLaneSpec): string[] {
  const proxyHost = spec.gitMirror.githubProxy.host;
  const proxyPort = spec.gitMirror.githubProxy.port;
  const proxyUrl = `http://${proxyHost}:${proxyPort}`;
  const proxySummary = `agentrun git-mirror-egress-proxy node=${spec.nodeId} lane=${spec.lane} host=${proxyHost} port=${proxyPort} ssh=GIT_SSH-wrapper source=yaml`;
  const proxyCommand = `ProxyCommand=node /tmp/agentrun-github-proxy-connect.cjs ${proxyHost} ${proxyPort} %h %p`;
  return [
    "mkdir -p /root/.ssh",
    "cp /git-ssh/ssh-privatekey /root/.ssh/id_rsa",
    "chmod 0400 /root/.ssh/id_rsa",
    `printf '%s\\n' ${shQuote(proxySummary)} >&2`,
    "cat > /tmp/agentrun-github-proxy-connect.cjs <<'NODE_PROXY'",
    "#!/usr/bin/env node",
    "const net = require('node:net');",
    "const [proxyHost, proxyPortRaw, targetHost, targetPortRaw] = process.argv.slice(2);",
    "const proxyPort = Number.parseInt(proxyPortRaw || '', 10);",
    "const targetPort = Number.parseInt(targetPortRaw || '', 10);",
    "if (!proxyHost || !Number.isInteger(proxyPort) || !targetHost || !Number.isInteger(targetPort)) {",
    "  console.error('agentrun git-mirror proxy-connect: invalid ProxyCommand arguments');",
    "  process.exit(64);",
    "}",
    "let settled = false;",
    "let tunnelEstablished = false;",
    "function finish(code, message) {",
    "  if (settled) return;",
    "  settled = true;",
    "  if (message) console.error('agentrun git-mirror proxy-connect: ' + message);",
    "  process.exit(code);",
    "}",
    "const socket = net.createConnection({ host: proxyHost, port: proxyPort });",
    "let buffer = Buffer.alloc(0);",
    "socket.setTimeout(15000, () => { socket.destroy(); finish(65, 'timeout connecting via ' + proxyHost + ':' + proxyPort + ' to ' + targetHost + ':' + targetPort); });",
    "socket.on('connect', () => socket.write('CONNECT ' + targetHost + ':' + targetPort + ' HTTP/1.1\\r\\nHost: ' + targetHost + ':' + targetPort + '\\r\\nProxy-Connection: Keep-Alive\\r\\n\\r\\n'));",
    "socket.on('error', (error) => finish(tunnelEstablished ? 69 : 66, (tunnelEstablished ? 'tunnel socket error: ' : 'tcp error connecting to proxy: ') + (error && error.message ? error.message : String(error))));",
    "socket.on('close', () => { if (!tunnelEstablished) finish(68, 'proxy closed before CONNECT completed via ' + proxyHost + ':' + proxyPort + ' to ' + targetHost + ':' + targetPort); else finish(0); });",
    "function onData(chunk) {",
    "  buffer = Buffer.concat([buffer, chunk]);",
    "  const headerEnd = buffer.indexOf('\\r\\n\\r\\n');",
    "  if (headerEnd === -1 && buffer.length < 8192) return;",
    "  if (headerEnd === -1) { socket.destroy(); finish(68, 'proxy response header exceeded 8192 bytes before CONNECT status via ' + proxyHost + ':' + proxyPort + ' to ' + targetHost + ':' + targetPort); return; }",
    "  const head = buffer.slice(0, headerEnd + 4).toString('latin1');",
    "  const statusLine = head.split('\\r\\n', 1)[0] || '';",
    "  const statusCode = Number.parseInt(statusLine.split(' ')[1] || '', 10);",
    "  if (!statusLine.startsWith('HTTP/1.') || !Number.isInteger(statusCode) || statusCode < 200 || statusCode > 299) {",
    "    const safeStatus = statusLine.replace(/[^\\x20-\\x7e]/g, '?').slice(0, 160);",
    "    socket.destroy();",
    "    finish(67, 'proxy CONNECT failed via ' + proxyHost + ':' + proxyPort + ' to ' + targetHost + ':' + targetPort + ': ' + safeStatus);",
    "    return;",
    "  }",
    "  socket.off('data', onData);",
    "  socket.setTimeout(0);",
    "  tunnelEstablished = true;",
    "  const rest = buffer.slice(headerEnd + 4);",
    "  if (rest.length) process.stdout.write(rest);",
    "  process.stdin.on('error', () => {});",
    "  process.stdout.on('error', () => {});",
    "  process.stdin.pipe(socket);",
    "  socket.pipe(process.stdout);",
    "}",
    "socket.on('data', onData);",
    "NODE_PROXY",
    "chmod 0700 /tmp/agentrun-github-proxy-connect.cjs",
    "cat > /tmp/agentrun-git-ssh-proxy.sh <<'SH_PROXY'",
    "#!/bin/sh",
    `exec ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.ssh/known_hosts -o ConnectTimeout=15 -o ServerAliveInterval=5 -o ServerAliveCountMax=1 -o ${shQuote(proxyCommand)} "$@"`,
    "SH_PROXY",
    "chmod 0700 /tmp/agentrun-git-ssh-proxy.sh",
    `export HTTP_PROXY=${shQuote(proxyUrl)}`,
    `export HTTPS_PROXY=${shQuote(proxyUrl)}`,
    `export ALL_PROXY=${shQuote(proxyUrl)}`,
    `export http_proxy=${shQuote(proxyUrl)}`,
    `export https_proxy=${shQuote(proxyUrl)}`,
    `export all_proxy=${shQuote(proxyUrl)}`,
    "export GIT_SSH=/tmp/agentrun-git-ssh-proxy.sh",
    "unset GIT_SSH_COMMAND",
  ];
}

export function yamlLaneGitMirrorSyncShell(spec: AgentRunLaneSpec): string {
  return [
    "set -eu",
    ...yamlLaneGitMirrorSshSetupShellLines(spec),
    `repository=${shQuote(spec.source.repository)}`,
    `source_branch=${shQuote(spec.source.branch)}`,
    `gitops_branch=${shQuote(spec.gitops.branch)}`,
    "repo=\"/cache/${repository}.git\"",
    "remote=\"ssh://git@ssh.github.com:443/${repository}.git\"",
    "mkdir -p \"$(dirname \"$repo\")\"",
    "if [ -d \"$repo/objects\" ] && [ -f \"$repo/HEAD\" ]; then",
    "  git --git-dir=\"$repo\" remote set-url origin \"$remote\" || git --git-dir=\"$repo\" remote add origin \"$remote\"",
    "else",
    "  rm -rf \"$repo\"",
    "  git init --bare \"$repo\"",
    "  git --git-dir=\"$repo\" remote add origin \"$remote\"",
    "fi",
    "git --git-dir=\"$repo\" config uploadpack.allowReachableSHA1InWant true",
    "git --git-dir=\"$repo\" config uploadpack.allowAnySHA1InWant true",
    "git --git-dir=\"$repo\" config http.uploadpack true",
    "git --git-dir=\"$repo\" config http.receivepack true",
    "timeout 240 git --git-dir=\"$repo\" fetch origin \"+refs/heads/${source_branch}:refs/mirror-stage/heads/${source_branch}\"",
    "source_sha=$(git --git-dir=\"$repo\" rev-parse --verify \"refs/mirror-stage/heads/${source_branch}^{commit}\")",
    "git --git-dir=\"$repo\" update-ref \"refs/heads/${source_branch}\" \"$source_sha\"",
    "gitops_fetch_err=$(mktemp)",
    "gitops_remote_missing=false",
    "gitops_sha=\"\"",
    "if timeout 240 git --git-dir=\"$repo\" fetch origin \"+refs/heads/${gitops_branch}:refs/mirror-stage/heads/${gitops_branch}\" 2>\"$gitops_fetch_err\"; then",
    "  gitops_sha=$(git --git-dir=\"$repo\" rev-parse --verify \"refs/mirror-stage/heads/${gitops_branch}^{commit}\")",
    "  git --git-dir=\"$repo\" update-ref \"refs/heads/${gitops_branch}\" \"$gitops_sha\"",
    "else",
    "  gitops_fetch_exit=$?",
    "  if grep -qi \"couldn't find remote ref\" \"$gitops_fetch_err\"; then",
    "    gitops_remote_missing=true",
    "    gitops_sha=$(git --git-dir=\"$repo\" rev-parse --verify \"refs/heads/${gitops_branch}^{commit}\" 2>/dev/null || true)",
    "  else",
    "    cat \"$gitops_fetch_err\" >&2",
    "    exit \"$gitops_fetch_exit\"",
    "  fi",
    "fi",
    "git --git-dir=\"$repo\" update-server-info",
    "SOURCE_SHA=\"$source_sha\" GITOPS_SHA=\"$gitops_sha\" GITOPS_REMOTE_MISSING=\"$gitops_remote_missing\" node <<'NODE'",
    "console.log(JSON.stringify({ ok: true, localSource: process.env.SOURCE_SHA, localGitops: process.env.GITOPS_SHA || null, gitopsRemoteMissing: process.env.GITOPS_REMOTE_MISSING === 'true', valuesPrinted: false }));",
    "NODE",
  ].join("\n");
}

export function yamlLaneGitMirrorFlushShell(spec: AgentRunLaneSpec): string {
  return [
    "set -eu",
    ...yamlLaneGitMirrorSshSetupShellLines(spec),
    `repository=${shQuote(spec.source.repository)}`,
    `gitops_branch=${shQuote(spec.gitops.branch)}`,
    "repo=\"/cache/${repository}.git\"",
    "remote=\"ssh://git@ssh.github.com:443/${repository}.git\"",
    "test -d \"$repo\"",
    "git --git-dir=\"$repo\" remote set-url origin \"$remote\" || git --git-dir=\"$repo\" remote add origin \"$remote\"",
    "local_gitops=$(git --git-dir=\"$repo\" rev-parse --verify \"refs/heads/${gitops_branch}^{commit}\" 2>/dev/null || true)",
    "if [ -n \"$local_gitops\" ]; then",
    "  git --git-dir=\"$repo\" -c remote.origin.mirror=false push origin \"refs/heads/${gitops_branch}:refs/heads/${gitops_branch}\"",
    "  git --git-dir=\"$repo\" fetch origin \"+refs/heads/${gitops_branch}:refs/mirror-stage/heads/${gitops_branch}\"",
    "fi",
    "github_gitops=$(git --git-dir=\"$repo\" rev-parse --verify \"refs/mirror-stage/heads/${gitops_branch}^{commit}\" 2>/dev/null || true)",
    "pending=false; if [ -n \"$local_gitops\" ] && { [ -z \"$github_gitops\" ] || [ \"$local_gitops\" != \"$github_gitops\" ]; }; then pending=true; fi",
    "LOCAL_GITOPS=\"$local_gitops\" GITHUB_GITOPS=\"$github_gitops\" PENDING=\"$pending\" node <<'NODE'",
    "console.log(JSON.stringify({ ok: process.env.PENDING !== 'true', localGitops: process.env.LOCAL_GITOPS || null, githubGitops: process.env.GITHUB_GITOPS || null, pendingFlush: process.env.PENDING === 'true', valuesPrinted: false }));",
    "NODE",
  ].join("\n");
}

export function yamlLaneGitMirrorStatusScript(spec: AgentRunLaneSpec): string {
  return [
    "set +e",
    `namespace=${shQuote(spec.gitMirror.namespace)}`,
    `read_deployment=${shQuote(spec.gitMirror.readDeployment)}`,
    `read_service=${shQuote(spec.gitMirror.readService)}`,
    `write_service=${shQuote(spec.gitMirror.writeService)}`,
    `cache_pvc=${shQuote(spec.gitMirror.cachePvc)}`,
    `cache_host_path=${spec.gitMirror.cacheHostPath === null ? "''" : shQuote(spec.gitMirror.cacheHostPath)}`,
    `repository=${shQuote(spec.source.repository)}`,
    `source_branch=${shQuote(spec.source.branch)}`,
    `gitops_branch=${shQuote(spec.gitops.branch)}`,
    `repositories_json=${shQuote(JSON.stringify(spec.gitMirror.repositories))}`,
    "kubectl -n \"$namespace\" get svc \"$read_service\" -o json >/tmp/agentrun-gitmirror-read.json 2>/dev/null",
    "read_exit=$?",
    "kubectl -n \"$namespace\" get svc \"$write_service\" -o json >/tmp/agentrun-gitmirror-write.json 2>/dev/null",
    "write_exit=$?",
    "kubectl -n \"$namespace\" get pvc \"$cache_pvc\" -o json >/tmp/agentrun-gitmirror-cache.json 2>/dev/null",
    "cache_exit=$?",
    "cache_mode=pvc",
    "if [ -n \"$cache_host_path\" ]; then cache_mode=hostPath; fi",
    "kubectl -n \"$namespace\" get deploy,svc,pvc -o name > /tmp/agentrun-gitmirror-names.txt 2>/dev/null",
    "repo_path=\"/cache/${repository}.git\"",
    "rm -f /tmp/agentrun-gitmirror-refs.txt",
    "if [ \"$read_exit\" -eq 0 ]; then",
    "  kubectl -n \"$namespace\" exec deploy/\"$read_deployment\" -- sh -lc 'repo_path=\"$1\"; source_branch=\"$2\"; gitops_branch=\"$3\"; printf \"sourceCommit=\"; git --git-dir=\"$repo_path\" rev-parse \"refs/heads/$source_branch\" 2>/dev/null || true; printf \"gitopsCommit=\"; git --git-dir=\"$repo_path\" rev-parse \"refs/heads/$gitops_branch\" 2>/dev/null || true' sh \"$repo_path\" \"$source_branch\" \"$gitops_branch\" > /tmp/agentrun-gitmirror-refs.txt 2>/dev/null",
    "fi",
    "NAMESPACE=\"$namespace\" READ_EXIT=\"$read_exit\" WRITE_EXIT=\"$write_exit\" CACHE_EXIT=\"$cache_exit\" CACHE_MODE=\"$cache_mode\" CACHE_HOST_PATH=\"$cache_host_path\" REPOSITORY=\"$repository\" SOURCE_BRANCH=\"$source_branch\" GITOPS_BRANCH=\"$gitops_branch\" REPOSITORIES_JSON=\"$repositories_json\" node <<'NODE'",
    "const fs = require('node:fs');",
    "const repositories = JSON.parse(process.env.REPOSITORIES_JSON || '[]');",
    "let names = ''; try { names = fs.readFileSync('/tmp/agentrun-gitmirror-names.txt', 'utf8'); } catch {}",
    "let refs = ''; try { refs = fs.readFileSync('/tmp/agentrun-gitmirror-refs.txt', 'utf8'); } catch {}",
    "const refValue = (key) => refs.split(/\\r?\\n/).find((line) => line.startsWith(`${key}=`))?.slice(key.length + 1).trim() || null;",
    "const readReady = process.env.READ_EXIT === '0';",
    "const writeReady = process.env.WRITE_EXIT === '0';",
    "const cachePvcExists = process.env.CACHE_EXIT === '0';",
    "const cacheMode = process.env.CACHE_MODE || 'pvc';",
    "const cacheReady = cacheMode === 'hostPath' ? true : cachePvcExists;",
    "console.log(JSON.stringify({",
    "  ok: readReady && writeReady && cacheReady,",
    "  namespace: process.env.NAMESPACE,",
    "  readReady,",
    "  writeReady,",
    "  cacheMode,",
    "  cacheReady,",
    "  cachePvcExists,",
    "  cacheHostPath: process.env.CACHE_HOST_PATH || null,",
    "  resources: names.split(/\\r?\\n/).filter(Boolean).slice(0, 40),",
    "  repository: process.env.REPOSITORY,",
    "  sourceBranch: process.env.SOURCE_BRANCH,",
    "  gitopsBranch: process.env.GITOPS_BRANCH,",
    "  sourceCommit: refValue('sourceCommit'),",
    "  gitopsCommit: refValue('gitopsCommit'),",
    "  repositories,",
    "  valuesPrinted: false",
    "}));",
    "NODE",
  ].join("\n");
}

export type LaneSecretSource = {
  id: string;
  sourceRef: string;
  sourceMode: "env" | "file";
  sourceKey: string | null;
  targetRef: { namespace: string; name: string; key: string };
};

export function readSecretSourceValue(spec: AgentRunLaneSpec, source: LaneSecretSource): { redactedPath: string; value: string; valueBytes: number; fingerprint: string } {
  const { sourceRef } = source;
  if (sourceRef.includes("..")) throw new Error(`secret sourceRef must not contain ..: ${sourceRef}`);
  const sourcePath = sourceRef.startsWith("/")
    ? sourceRef
    : join(resolveSecretSourceRoot(spec), ...sourceRef.split("/"));
  if (!existsSync(sourcePath)) throw new Error(`secret source ${sourceRef} is missing`);
  let value: string;
  if (source.sourceMode === "file") {
    value = readFileSync(sourcePath, "utf8");
  } else {
    if (source.sourceKey === null) throw new Error(`secret source ${sourceRef} is missing sourceKey`);
    const values = parseEnvFile(readFileSync(sourcePath, "utf8"));
    const envValue = values.get(source.sourceKey);
    if (envValue === undefined || envValue.length === 0) throw new Error(`secret source ${sourceRef} is missing required key ${source.sourceKey}`);
    value = envValue;
  }
  if (value.length === 0) throw new Error(`secret source ${sourceRef} is empty`);
  return {
    redactedPath: sourceRef.startsWith("/") ? redactAbsoluteSecretPath(sourceRef) : `.state/secrets/${sourceRef}`,
    value,
    valueBytes: Buffer.byteLength(value, "utf8"),
    fingerprint: sha256Fingerprint(value),
  };
}

export function redactAbsoluteSecretPath(sourceRef: string): string {
  const parts = sourceRef.split("/").filter(Boolean);
  return parts.length === 0 ? "/" : `/${parts.slice(0, -1).join("/")}/<redacted>`;
}

export function resolveSecretSourceRoot(spec: AgentRunLaneSpec): string {
  if (spec.database.configRef === null) return rootPath(".state", "secrets");
  const configPath = spec.database.configRef.startsWith("/") ? spec.database.configRef : rootPath(spec.database.configRef);
  const configRoot = record(Bun.YAML.parse(readFileSync(configPath, "utf8")) as unknown);
  const secrets = record(configRoot.secrets);
  const root = stringOrNull(secrets.root);
  if (root === null || !root.startsWith("/") || root.includes("..")) throw new Error(`${spec.database.configRef}.secrets.root must be an absolute path without ..`);
  return root;
}

export function parseEnvFile(text: string): Map<string, string> {
  const result = new Map<string, string>();
  for (const rawLine of text.split(/\r?\n/u)) {
    let line = rawLine.trim();
    if (line.length === 0 || line.startsWith("#")) continue;
    if (line.startsWith("export ")) line = line.slice("export ".length).trim();
    const index = line.indexOf("=");
    if (index <= 0) continue;
    const key = line.slice(0, index).trim();
    let value = line.slice(index + 1).trim();
    if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
      value = value.slice(1, -1);
    }
    result.set(key, value);
  }
  return result;
}

export function collectLaneSecretSources(spec: AgentRunLaneSpec): LaneSecretSource[] {
  const result: LaneSecretSource[] = [];
  if (spec.database.secretSourceRef !== null) {
    result.push({
      id: "database",
      sourceRef: spec.database.secretSourceRef,
      sourceMode: "env",
      sourceKey: spec.database.secretRef.key,
      targetRef: { namespace: spec.runtime.namespace, name: spec.database.secretRef.name, key: spec.database.secretRef.key },
    });
  }
  for (const secret of spec.secrets) {
    result.push({
      id: secret.id,
      sourceRef: secret.sourceRef,
      sourceMode: secret.sourceMode,
      sourceKey: secret.sourceKey,
      targetRef: secret.targetRef,
    });
  }
  const seen = new Set<string>();
  return result.filter((item) => {
    const key = `${item.targetRef.namespace}/${item.targetRef.name}/${item.targetRef.key}`;
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}

export function secretSyncScript(_spec: AgentRunLaneSpec, values: Array<{ targetRef: { namespace: string; name: string; key: string }; value: string }>): string {
  const encoded = Buffer.from(JSON.stringify(values.map((item) => ({
    targetRef: item.targetRef,
    valueBase64: Buffer.from(item.value, "utf8").toString("base64"),
  }))), "utf8").toString("base64");
  return [
    "set -eu",
    `payload_b64=${shQuote(encoded)}`,
    "tmp_dir=$(mktemp -d)",
    "trap 'rm -rf \"$tmp_dir\"' EXIT",
    "payload_json=\"$tmp_dir/payload.json\"",
    "printf '%s' \"$payload_b64\" | base64 -d > \"$payload_json\"",
    "PAYLOAD_JSON=\"$payload_json\" TMP_DIR=\"$tmp_dir\" node <<'NODE'",
    "const fs = require('node:fs');",
    "const cp = require('node:child_process');",
    "const crypto = require('node:crypto');",
    "const items = JSON.parse(fs.readFileSync(process.env.PAYLOAD_JSON, 'utf8'));",
    "const results = [];",
    "const groups = new Map();",
    "function run(argv, input) {",
    "  const out = cp.spawnSync(argv[0], argv.slice(1), { input, encoding: 'utf8' });",
    "  if (out.status !== 0) throw new Error(`${argv.join(' ')} failed: ${out.stderr || out.stdout}`);",
    "  return out;",
    "}",
    "function tryRun(argv) {",
    "  return cp.spawnSync(argv[0], argv.slice(1), { encoding: 'utf8' });",
    "}",
    "for (let index = 0; index < items.length; index += 1) {",
    "  const item = items[index];",
    "  const ref = item.targetRef;",
    "  const groupKey = `${ref.namespace}\\u0000${ref.name}`;",
    "  if (!groups.has(groupKey)) groups.set(groupKey, { namespace: ref.namespace, name: ref.name, items: [] });",
    "  groups.get(groupKey).items.push({ key: ref.key, valueBase64: item.valueBase64 });",
    "}",
    "for (const group of groups.values()) {",
    "  const ns = run(['kubectl', 'create', 'namespace', group.namespace, '--dry-run=client', '-o', 'yaml']).stdout;",
    "  run(['kubectl', 'apply', '--server-side', '--field-manager=unidesk-agentrun-secret-sync', '-f', '-'], ns);",
    "  const existing = tryRun(['kubectl', '-n', group.namespace, 'get', 'secret', group.name]);",
    "  if (existing.status !== 0) run(['kubectl', '-n', group.namespace, 'create', 'secret', 'generic', group.name]);",
    "  const patch = { data: {} };",
    "  for (const entry of group.items) patch.data[entry.key] = entry.valueBase64;",
    "  const patchFile = `${process.env.TMP_DIR}/${group.namespace}-${group.name}-patch.json`;",
    "  fs.writeFileSync(patchFile, JSON.stringify(patch));",
    "  run(['kubectl', '-n', group.namespace, 'patch', 'secret', group.name, '--type=merge', `--patch-file=${patchFile}`]);",
    "  const fetched = JSON.parse(run(['kubectl', '-n', group.namespace, 'get', 'secret', group.name, '-o', 'json']).stdout);",
    "  for (const entry of group.items) {",
    "    const raw = fetched.data?.[entry.key] || '';",
    "    const decoded = raw ? Buffer.from(raw, 'base64') : Buffer.alloc(0);",
    "    results.push({ namespace: group.namespace, secret: group.name, key: entry.key, ok: raw.length > 0, valueBytes: decoded.length, fingerprint: raw ? 'sha256:' + crypto.createHash('sha256').update(decoded).digest('hex') : null, valuesPrinted: false });",
    "  }",
    "}",
    "console.log(JSON.stringify({ ok: results.every((item) => item.ok), secretCount: results.length, items: results, valuesPrinted: false }));",
    "NODE",
  ].join("\n");
}

export function restartYamlLaneScript(spec: AgentRunLaneSpec): string {
  return [
    "set +e",
    `namespace=${shQuote(spec.runtime.namespace)}`,
    `deployment=${shQuote(spec.runtime.managerDeployment)}`,
    "ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)",
    "kubectl -n \"$namespace\" patch deployment \"$deployment\" --type='merge' -p \"{\\\"spec\\\":{\\\"template\\\":{\\\"metadata\\\":{\\\"annotations\\\":{\\\"kubectl.kubernetes.io/restartedAt\\\":\\\"$ts\\\"}}}}}\" >/tmp/agentrun-restart-patch.out 2>/tmp/agentrun-restart-patch.err",
    "patch_rc=$?",
    "if [ \"$patch_rc\" -eq 0 ]; then",
    "  kubectl -n \"$namespace\" rollout status deployment/\"$deployment\" --timeout=180s >/tmp/agentrun-rollout-status.out 2>/tmp/agentrun-rollout-status.err",
    "  rollout_rc=$?",
    "else",
    "  rollout_rc=$patch_rc",
    "fi",
    "pods_json=$(kubectl -n \"$namespace\" get pod -l app.kubernetes.io/name=agentrun-mgr -o json 2>/dev/null || printf '{}')",
    "deployment_json=$(kubectl -n \"$namespace\" get deployment \"$deployment\" -o json 2>/dev/null || printf '{}')",
    "patch_err=$(cat /tmp/agentrun-restart-patch.err 2>/dev/null || true)",
    "rollout_err=$(cat /tmp/agentrun-rollout-status.err 2>/dev/null || true)",
    "PODS_JSON=\"$pods_json\" DEPLOYMENT_JSON=\"$deployment_json\" PATCH_RC=\"$patch_rc\" ROLLOUT_RC=\"$rollout_rc\" TS=\"$ts\" PATCH_ERR=\"$patch_err\" ROLLOUT_ERR=\"$rollout_err\" node <<'NODE'",
    "const pods = JSON.parse(process.env.PODS_JSON || '{}');",
    "const deployment = JSON.parse(process.env.DEPLOYMENT_JSON || '{}');",
    "const conditions = Array.isArray(deployment.status?.conditions) ? deployment.status.conditions : [];",
    "const items = Array.isArray(pods.items) ? pods.items : [];",
    "const compactError = (value) => String(value || '').replace(/\\s+/g, ' ').trim().slice(0, 500) || null;",
    "console.log(JSON.stringify({",
    "  ok: process.env.PATCH_RC === '0' && process.env.ROLLOUT_RC === '0',",
    "  restartedAt: process.env.TS,",
    "  patch: { exitCode: Number(process.env.PATCH_RC || '1'), error: compactError(process.env.PATCH_ERR) },",
    "  rollout: { exitCode: Number(process.env.ROLLOUT_RC || '1'), error: compactError(process.env.ROLLOUT_ERR) },",
    "  readyReplicas: deployment.status?.readyReplicas ?? null,",
    "  replicas: deployment.status?.replicas ?? null,",
    "  conditions: conditions.map((item) => ({ type: item.type, status: item.status, reason: item.reason ?? null })),",
    "  pods: items.map((pod) => ({",
    "    name: pod.metadata?.name ?? null,",
    "    phase: pod.status?.phase ?? null,",
    "    ready: (pod.status?.containerStatuses ?? []).map((item) => item.ready),",
    "    restarts: (pod.status?.containerStatuses ?? []).map((item) => item.restartCount),",
    "    waiting: (pod.status?.containerStatuses ?? []).map((item) => item.state?.waiting?.reason ?? null),",
    "  })),",
    "  valuesPrinted: false",
    "}));",
    "NODE",
    "exit \"$rollout_rc\"",
  ].join("\n");
}

export function applyYamlScript(yaml: string, fieldManager: string, dryRun: boolean): string {
  const encoded = Buffer.from(yaml, "utf8").toString("base64");
  return [
    "set -eu",
    `manifest_b64=${shQuote(encoded)}`,
    `field_manager=${shQuote(fieldManager)}`,
    `dry_run=${dryRun ? "true" : "false"}`,
    "tmp_dir=$(mktemp -d)",
    "trap 'rm -rf \"$tmp_dir\"' EXIT",
    "manifest=\"$tmp_dir/manifest.yaml\"",
    "printf '%s' \"$manifest_b64\" | base64 -d > \"$manifest\"",
    "args=\"--server-side --force-conflicts --field-manager=$field_manager\"",
    "if [ \"$dry_run\" = true ]; then args=\"$args --dry-run=server\"; fi",
    "set +e",
    "kubectl apply $args -f \"$manifest\" > \"$tmp_dir/apply.out\" 2> \"$tmp_dir/apply.err\"",
    "apply_exit=$?",
    "set -e",
    "APPLY_EXIT=\"$apply_exit\" APPLY_OUT=\"$tmp_dir/apply.out\" APPLY_ERR=\"$tmp_dir/apply.err\" MANIFEST=\"$manifest\" node <<'NODE'",
    "const fs = require('node:fs');",
    "const crypto = require('node:crypto');",
    "const out = fs.readFileSync(process.env.APPLY_OUT, 'utf8');",
    "const err = fs.readFileSync(process.env.APPLY_ERR, 'utf8');",
    "const manifest = fs.readFileSync(process.env.MANIFEST, 'utf8');",
    "const resources = out.split(/\\r?\\n/).map((line) => line.trim()).filter(Boolean).slice(0, 80);",
    "console.log(JSON.stringify({ ok: process.env.APPLY_EXIT === '0', exitCode: Number(process.env.APPLY_EXIT), resourceCount: resources.length, resources, manifestBytes: Buffer.byteLength(manifest, 'utf8'), manifestDigest: 'sha256:' + crypto.createHash('sha256').update(manifest).digest('hex'), stderrTail: err.slice(-3000), valuesPrinted: false }));",
    "NODE",
    "exit \"$apply_exit\"",
  ].join("\n");
}

export function manifestObjectRef(object: Record<string, unknown>): Record<string, unknown> {
  const metadata = record(object.metadata);
  return {
    apiVersion: stringOrNull(object.apiVersion),
    kind: stringOrNull(object.kind),
    namespace: stringOrNull(metadata.namespace),
    name: stringOrNull(metadata.name),
  };
}

export function cleanupRunnersFactsNodeScript(): string {
  return String.raw`
function iso(value) {
  if (value instanceof Date) return value.toISOString();
  if (typeof value === "string" && value.length > 0) {
    const ms = Date.parse(value);
    return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
  }
  return null;
}

try {
  const { Pool } = await import("pg");
  const namespace = process.env.RETENTION_NAMESPACE || process.env.AGENTRUN_RUNTIME_NAMESPACE || "";
  const pool = new Pool({ connectionString: process.env.DATABASE_URL });
  const sql = [
    "SELECT",
    "  j.id, j.run_id, j.command_id, j.attempt_id, j.runner_id, j.namespace, j.job_name, j.created_at, j.updated_at,",
    "  r.registered_at AS runner_registered_at, r.heartbeat_at AS runner_heartbeat_at,",
    "  rr.status AS run_status, rr.terminal_status AS run_terminal_status, rr.failure_kind AS run_failure_kind, rr.updated_at AS run_updated_at",
    "FROM agentrun_runner_jobs j",
    "LEFT JOIN agentrun_runners r ON r.id = j.runner_id",
    "LEFT JOIN agentrun_runs rr ON rr.id = j.run_id",
    "WHERE j.namespace = $1",
    "ORDER BY COALESCE(r.heartbeat_at, j.updated_at, j.created_at) ASC",
  ].join("\n");
  const result = await pool.query(sql, [namespace]);
  await pool.end();
  console.log(JSON.stringify({
    ok: true,
    itemCount: result.rows.length,
    items: result.rows.map((row) => ({
      id: row.id,
      runId: row.run_id,
      commandId: row.command_id,
      attemptId: row.attempt_id,
      runnerId: row.runner_id,
      namespace: row.namespace,
      jobName: row.job_name,
      runnerJobCreatedAt: iso(row.created_at),
      runnerJobUpdatedAt: iso(row.updated_at),
      runnerRegisteredAt: iso(row.runner_registered_at),
      runnerHeartbeatAt: iso(row.runner_heartbeat_at),
      runStatus: row.run_status || null,
      runTerminalStatus: row.run_terminal_status || null,
      runFailureKind: row.run_failure_kind || null,
      runUpdatedAt: iso(row.run_updated_at),
      valuesPrinted: false,
    })),
    valuesPrinted: false,
  }));
} catch (error) {
  console.log(JSON.stringify({
    ok: false,
    itemCount: 0,
    items: [],
    failureKind: "manager-runner-facts-query-failed",
    message: error instanceof Error ? error.message : String(error),
    valuesPrinted: false,
  }));
}
`;
}

export function cleanupRunnersPlanNodeScript(): string {
  return String.raw`
const fs = require("node:fs");
const path = require("node:path");
const tmp = process.env.TMP_DIR;
const namespace = process.env.NAMESPACE;
const maxRunners = Number(process.env.MAX_RUNNERS || 0);
const cleanupOrder = process.env.CLEANUP_ORDER || "";
const activeHeartbeatMaxAgeMs = Number(process.env.ACTIVE_HEARTBEAT_MAX_AGE_MS || 0);
const ageBasedCleanupEnabled = process.env.AGE_BASED_CLEANUP_ENABLED === "true";
const ageBasedMaxAgeHours = process.env.AGE_BASED_MAX_AGE_HOURS ? Number(process.env.AGE_BASED_MAX_AGE_HOURS) : null;
const forceActive = process.env.FORCE_ACTIVE === "true";
const matchLabels = JSON.parse(process.env.MATCH_LABELS_JSON || "{}");
const jobNamePrefixes = JSON.parse(process.env.JOB_NAME_PREFIXES_JSON || "[]");
const now = Date.now();

function readJson(name) {
  return JSON.parse(fs.readFileSync(path.join(tmp, name), "utf8"));
}

function dateMs(value) {
  const ms = Date.parse(value || "");
  return Number.isFinite(ms) ? ms : null;
}

function ageMs(value) {
  const ms = dateMs(value);
  return ms === null ? null : Math.max(0, now - ms);
}

function labelsOf(item) {
  return item?.metadata?.labels && typeof item.metadata.labels === "object" ? item.metadata.labels : {};
}

function annotationsOf(item) {
  return item?.metadata?.annotations && typeof item.metadata.annotations === "object" ? item.metadata.annotations : {};
}

function matchesLabels(labels) {
  return Object.entries(matchLabels).every(([key, value]) => labels?.[key] === value);
}

function matchesPrefix(name) {
  return jobNamePrefixes.length === 0 || jobNamePrefixes.some((prefix) => name === prefix || name.startsWith(prefix + "-"));
}

function terminalRunStatus(value) {
  return value === "completed" || value === "failed" || value === "blocked" || value === "cancelled";
}

function jobCondition(job, type) {
  const conditions = Array.isArray(job?.status?.conditions) ? job.status.conditions : [];
  return conditions.find((entry) => entry?.type === type && entry?.status === "True") || null;
}

function isTerminalJob(job) {
  return jobCondition(job, "Complete") !== null || jobCondition(job, "Failed") !== null || Number(job?.status?.succeeded || 0) > 0 || Number(job?.status?.failed || 0) > 0;
}

function isTerminalPod(pod) {
  const phase = pod?.status?.phase || "";
  return phase === "Succeeded" || phase === "Failed";
}

function jobNameForPod(pod) {
  const labels = labelsOf(pod);
  if (typeof labels["job-name"] === "string") return labels["job-name"];
  const owner = (Array.isArray(pod?.metadata?.ownerReferences) ? pod.metadata.ownerReferences : []).find((entry) => entry?.kind === "Job" && typeof entry?.name === "string");
  return owner?.name || null;
}

function preferredLastActiveAt(fact, job) {
  return fact?.runnerHeartbeatAt
    || fact?.runUpdatedAt
    || fact?.runnerJobUpdatedAt
    || job?.status?.completionTime
    || job?.status?.startTime
    || job?.metadata?.creationTimestamp
    || null;
}

function sortOldestLastActive(left, right) {
  const leftMs = dateMs(left.lastActiveAt) ?? dateMs(left.createdAt) ?? Number.MAX_SAFE_INTEGER;
  const rightMs = dateMs(right.lastActiveAt) ?? dateMs(right.createdAt) ?? Number.MAX_SAFE_INTEGER;
  if (leftMs !== rightMs) return leftMs - rightMs;
  return String(left.name).localeCompare(String(right.name));
}

function countNonTerminalPods(pods) {
  return pods.filter((pod) => !isTerminalPod(pod)).length;
}

const jobs = readJson("jobs.json");
const pods = readJson("pods.json");
const facts = readJson("runner-facts.json");
const factItems = Array.isArray(facts.items) ? facts.items : [];
const factByJob = new Map(factItems.map((item) => [item.jobName, item]));
const allPods = Array.isArray(pods.items) ? pods.items : [];
const podsByJob = new Map();
for (const pod of allPods) {
  const jobName = jobNameForPod(pod);
  if (!jobName) continue;
  const entry = podsByJob.get(jobName) || [];
  entry.push(pod);
  podsByJob.set(jobName, entry);
}

const matchedJobs = (Array.isArray(jobs.items) ? jobs.items : [])
  .map((job) => {
    const name = job?.metadata?.name || "";
    return { job, name, labels: labelsOf(job), annotations: annotationsOf(job) };
  })
  .filter((item) => item.name && matchesLabels(item.labels) && matchesPrefix(item.name));

const runnerJobs = matchedJobs.map(({ job, name, labels, annotations }) => {
  const fact = factByJob.get(name) || null;
  const jobPods = podsByJob.get(name) || [];
  const nonTerminalPods = jobPods.filter((pod) => !isTerminalPod(pod));
  const terminatingPods = nonTerminalPods.filter((pod) => typeof pod?.metadata?.deletionTimestamp === "string");
  const terminal = isTerminalJob(job) || terminalRunStatus(fact?.runStatus) || terminalRunStatus(fact?.runTerminalStatus);
  const heartbeatAgeMs = ageMs(fact?.runnerHeartbeatAt);
  const heartbeatFresh = heartbeatAgeMs !== null && heartbeatAgeMs <= activeHeartbeatMaxAgeMs;
  const hasActivePod = Number(job?.status?.active || 0) > 0 || nonTerminalPods.length > 0;
  const lastActiveAt = preferredLastActiveAt(fact, job);
  let inactive = false;
  let protectedActive = false;
  let classification = "unknown";
  if (terminal) {
    inactive = true;
    classification = "terminal";
  } else if (heartbeatFresh) {
    protectedActive = true;
    classification = "active-fresh-heartbeat";
  } else if (heartbeatAgeMs !== null) {
    inactive = true;
    classification = "inactive-stale-heartbeat";
  } else if (terminatingPods.length > 0 && nonTerminalPods.length === terminatingPods.length) {
    inactive = true;
    classification = "inactive-terminating";
  } else if (hasActivePod) {
    protectedActive = true;
    classification = facts.ok === true ? "active-no-heartbeat-row" : "active-unverified-manager-facts";
  } else {
    inactive = true;
    classification = "inactive-no-active-pod";
  }
  return {
    name,
    namespace,
    createdAt: job?.metadata?.creationTimestamp || null,
    lastActiveAt,
    lastActiveAgeMs: ageMs(lastActiveAt),
    lastActiveSource: fact?.runnerHeartbeatAt ? "runner-heartbeat" : fact?.runUpdatedAt ? "run-updated" : fact?.runnerJobUpdatedAt ? "runner-job-updated" : job?.status?.completionTime ? "job-completion" : job?.status?.startTime ? "job-start" : "job-created",
    runId: annotations["agentrun.pikastech.local/run-id"] || fact?.runId || null,
    commandId: annotations["agentrun.pikastech.local/command-id"] || fact?.commandId || null,
    runnerJobId: fact?.id || null,
    runnerId: fact?.runnerId || null,
    runStatus: fact?.runStatus || null,
    runTerminalStatus: fact?.runTerminalStatus || null,
    jobStatus: {
      active: Number(job?.status?.active || 0),
      succeeded: Number(job?.status?.succeeded || 0),
      failed: Number(job?.status?.failed || 0),
      ready: Number(job?.status?.ready || 0),
      terminating: Number(job?.status?.terminating || 0),
    },
    podCount: jobPods.length,
    nonTerminalPodCount: nonTerminalPods.length,
    terminatingPodCount: terminatingPods.length,
    inactive,
    protectedActive,
    classification,
    labels,
    valuesPrinted: false,
  };
});

const runnerJobCount = runnerJobs.length;
const inactiveCandidates = runnerJobs.filter((item) => item.inactive).sort(sortOldestLastActive);
const overLimitCount = Math.max(0, runnerJobCount - maxRunners);
const selectedByName = new Map();
const selectionReasons = new Map();
for (const item of inactiveCandidates.slice(0, overLimitCount)) {
  selectedByName.set(item.name, item);
  selectionReasons.set(item.name, "over-max-runners");
}
if (ageBasedCleanupEnabled && ageBasedMaxAgeHours !== null) {
  const maxAgeMs = ageBasedMaxAgeHours * 3600 * 1000;
  for (const item of inactiveCandidates) {
    const itemAgeMs = item.lastActiveAgeMs;
    if (itemAgeMs !== null && itemAgeMs >= maxAgeMs) {
      selectedByName.set(item.name, item);
      if (!selectionReasons.has(item.name)) selectionReasons.set(item.name, "age-based-cleanup");
    }
  }
}
if (forceActive) {
  for (const item of runnerJobs) {
    selectedByName.set(item.name, item);
    selectionReasons.set(item.name, item.protectedActive ? "force-active-runner-cleanup" : "force-runner-cleanup");
  }
}

const selectionPool = forceActive ? runnerJobs : inactiveCandidates;
const selected = selectionPool
  .filter((item) => selectedByName.has(item.name))
  .map((item) => ({ ...item, selectionReason: selectionReasons.get(item.name) || "selected" }));
const matchedPodNames = new Set(runnerJobs.map((item) => item.name));
const matchedPods = allPods.filter((pod) => {
  const jobName = jobNameForPod(pod);
  return jobName !== null && matchedPodNames.has(jobName);
});
const remainingAfterSelection = runnerJobCount - selected.length;
console.log(JSON.stringify({
  ok: true,
  planKind: "agentrun-runtime-runner-retention",
  generatedAt: new Date(now).toISOString(),
  namespace,
  criteria: {
    maxRunners,
    cleanupOrder,
    activeHeartbeatMaxAgeMs,
    forceActive,
    selectors: { matchLabels, jobNamePrefixes },
    ageBasedCleanup: { enabled: ageBasedCleanupEnabled, maxAgeHours: ageBasedMaxAgeHours },
  },
  managerFacts: {
    ok: facts.ok === true,
    factCount: factItems.length,
    factsExit: Number(process.env.FACTS_EXIT || 0),
    failureKind: facts.failureKind || null,
    message: facts.ok === true ? null : facts.message || null,
    valuesPrinted: false,
  },
  runnerJobCount,
  nonTerminalRunnerPodCount: countNonTerminalPods(matchedPods),
  overLimitCount,
  inactiveCandidateCount: inactiveCandidates.length,
  protectedActiveRunnerCount: runnerJobs.filter((item) => item.protectedActive).length,
  activeRunRisk: remainingAfterSelection > maxRunners,
  remainingRunnerJobCountAfterSelection: remainingAfterSelection,
  runnerJobs: runnerJobs.sort(sortOldestLastActive),
  candidates: inactiveCandidates,
  selected,
  selectedRunnerJobs: selected.map((item) => item.name),
  selectedRunnerJobCount: selected.length,
  valuesPrinted: false,
}));
`;
}
