import { stableHash } from "../common/validation.js"; import type { BackendProfile, ExecutionPolicy, JsonRecord, JsonValue, RunRecord, SecretRef } from "../common/types.js"; export interface RunnerJobRenderOptions { run: RunRecord; commandId: string; managerUrl: string; image: string; namespace?: string; attemptId?: string; runnerId?: string; sourceCommit?: string; serviceAccountName?: string; imagePullPolicy?: string; backoffLimit?: number; ttlSecondsAfterFinished?: number; } interface CredentialProjection { profile: BackendProfile | string; secretRef: SecretRef; volumeName: string; mountPath: string; } export function renderRunnerJobDryRun(options: RunnerJobRenderOptions): JsonRecord { const render = renderRunnerJobManifest(options); return { dryRun: true, mutation: false, action: "render-kubernetes-job", jobIdentity: { kind: "Job", namespace: render.namespace, name: render.jobName, serviceAccountName: render.serviceAccountName, }, runner: { runId: options.run.id, commandId: options.commandId, attemptId: render.attemptId, runnerId: render.runnerId, backendProfile: options.run.backendProfile, managerUrl: options.managerUrl, sourceCommit: render.sourceCommit, }, secretRefs: render.secretRefs.map((item) => ({ profile: item.profile, name: item.secretRef.name, namespace: item.secretRef.namespace ?? render.namespace, keys: item.secretRef.keys ?? [], mountPath: item.mountPath, valuesPrinted: false })), pollCommands: { run: `bun scripts/agentrun-cli.ts runs show ${options.run.id} --manager-url ${options.managerUrl}`, events: `bun scripts/agentrun-cli.ts runs events ${options.run.id} --manager-url ${options.managerUrl} --after-seq 0 --limit 100`, }, warnings: render.warnings, manifest: render.manifest, }; } export function renderRunnerJobManifest(options: RunnerJobRenderOptions): { manifest: JsonRecord; namespace: string; jobName: string; runnerId: string; attemptId: string; sourceCommit: string; serviceAccountName: string; secretRefs: CredentialProjection[]; warnings: string[] } { const namespace = options.namespace ?? "agentrun-v01"; const attemptId = options.attemptId ?? `attempt_${Date.now().toString(36)}`; const runnerId = options.runnerId ?? `runner_${shortHash(`${options.run.id}:${attemptId}:${options.commandId}`)}`; const sourceCommit = options.sourceCommit ?? process.env.AGENTRUN_SOURCE_COMMIT ?? "unknown"; const serviceAccountName = options.serviceAccountName ?? "agentrun-v01-runner"; const jobName = `agentrun-v01-runner-${shortDnsHash(options.run.id, attemptId)}`; const secretRefs = credentialProjections(options.run, namespace); const warnings: string[] = []; if (secretRefs.length === 0) warnings.push("run executionPolicy.secretScope 未声明 provider SecretRef;runner 将按 secret-unavailable 上报,而不会降级直连外部凭据"); const env = runnerEnv(options, { namespace, jobName, runnerId, attemptId, sourceCommit, secretRefs }); const manifest: JsonRecord = { apiVersion: "batch/v1", kind: "Job", metadata: { name: jobName, namespace, labels: labels(options.run, jobName), annotations: { "agentrun.pikastech.local/run-id": options.run.id, "agentrun.pikastech.local/command-id": options.commandId, "agentrun.pikastech.local/dry-run-render": "true", }, }, spec: { backoffLimit: options.backoffLimit ?? 0, ttlSecondsAfterFinished: options.ttlSecondsAfterFinished ?? 86_400, template: { metadata: { labels: labels(options.run, jobName), annotations: { "agentrun.pikastech.local/run-id": options.run.id, "agentrun.pikastech.local/command-id": options.commandId, }, }, spec: { serviceAccountName, automountServiceAccountToken: false, restartPolicy: "Never", containers: [ { name: "runner", image: options.image, imagePullPolicy: options.imagePullPolicy ?? "IfNotPresent", command: ["bun", "src/runner/main.ts"], env, volumeMounts: secretRefs.map((item) => ({ name: item.volumeName, mountPath: item.mountPath, readOnly: true })), resources: { requests: { cpu: "250m", memory: "512Mi" }, limits: { cpu: "2", memory: "4Gi" }, }, securityContext: { allowPrivilegeEscalation: false, readOnlyRootFilesystem: false, capabilities: { drop: ["ALL"] }, }, }, ], volumes: secretRefs.map(secretVolume), }, }, }, }; return { manifest, namespace, jobName, runnerId, attemptId, sourceCommit, serviceAccountName, secretRefs, warnings }; } function runnerEnv(options: RunnerJobRenderOptions, context: { namespace: string; jobName: string; runnerId: string; attemptId: string; sourceCommit: string; secretRefs: CredentialProjection[] }): JsonRecord[] { const codexMount = context.secretRefs.find((item) => item.profile === "codex")?.mountPath ?? "/home/agentrun/.codex"; return [ { name: "AGENTRUN_MGR_URL", value: options.managerUrl }, { name: "AGENTRUN_RUN_ID", value: options.run.id }, { name: "AGENTRUN_COMMAND_ID", value: options.commandId }, { name: "AGENTRUN_ATTEMPT_ID", value: context.attemptId }, { name: "AGENTRUN_RUNNER_ID", value: context.runnerId }, { name: "AGENTRUN_BACKEND_PROFILE", value: options.run.backendProfile }, { name: "AGENTRUN_EXECUTION_POLICY_JSON", value: JSON.stringify(options.run.executionPolicy) }, { name: "AGENTRUN_SOURCE_COMMIT", value: context.sourceCommit }, { name: "AGENTRUN_RUNTIME_NAMESPACE", value: context.namespace }, { name: "AGENTRUN_K8S_JOB_NAME", value: context.jobName }, { name: "AGENTRUN_LOG_PATH", value: "/tmp/agentrun-runner.jsonl" }, { name: "HOME", value: "/home/agentrun" }, { name: "CODEX_HOME", value: codexMount }, ]; } function credentialProjections(run: RunRecord, namespace: string): CredentialProjection[] { const policy: ExecutionPolicy = run.executionPolicy; const credentials = policy.secretScope.providerCredentials ?? []; return credentials.map((item, index) => ({ profile: item.profile, secretRef: item.secretRef.namespace ? item.secretRef : { ...item.secretRef, namespace }, volumeName: sanitizeVolumeName(`${String(item.profile)}-${index}`), mountPath: normalizeMountPath(item.secretRef.mountPath), })); } function secretVolume(item: CredentialProjection): JsonRecord { const secret: JsonRecord = { secretName: item.secretRef.name, defaultMode: 256, }; const keys = item.secretRef.keys ?? []; if (keys.length > 0) secret.items = keys.map((key) => ({ key, path: key, mode: 256 })); return { name: item.volumeName, secret }; } function normalizeMountPath(value: string | undefined): string { if (!value || value === "~/.codex") return "/home/agentrun/.codex"; if (value.startsWith("~/")) return `/home/agentrun/${value.slice(2)}`; return value; } function labels(run: RunRecord, jobName: string): JsonRecord { return { "app.kubernetes.io/name": "agentrun-runner", "app.kubernetes.io/component": "runner", "app.kubernetes.io/part-of": "agentrun", "agentrun.pikastech.local/lane": "v0.1", "agentrun.pikastech.local/run-hash": shortHash(run.id), "job-name": jobName, }; } function shortDnsHash(...parts: string[]): string { return shortHash(parts.join(":")); } function shortHash(value: JsonValue): string { return stableHash(value).slice(0, 12); } function sanitizeVolumeName(value: string): string { const sanitized = value.toLowerCase().replace(/[^a-z0-9-]+/gu, "-").replace(/^-+|-+$/gu, ""); return sanitized.length > 0 ? sanitized.slice(0, 40) : "credential"; }