import { stableHash } from "../common/validation.js"; import type { BackendProfile, ExecutionPolicy, JsonRecord, JsonValue, RunRecord, SecretRef } from "../common/types.js"; import { backendProfileSpec } from "../common/backend-profiles.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; transientEnv?: RunnerTransientEnv[]; dryRun?: boolean; } export interface RunnerTransientEnv { name: string; value: string; sensitive?: boolean; } interface CredentialProjection { profile: BackendProfile | string; secretRef: SecretRef; volumeName: string; runtimeMountPath: string; projectionMountPath: string; } export function renderRunnerJobDryRun(options: RunnerJobRenderOptions): JsonRecord { const render = renderRunnerJobManifest({ ...options, dryRun: true }); const manifest = redactTransientEnvInManifest(render.manifest, options.transientEnv ?? []); 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.runtimeMountPath, projectionPath: item.projectionMountPath, writableCopy: true, valuesPrinted: false })), transientEnv: summarizeTransientEnv(options.transientEnv ?? []), retention: { ttlSecondsAfterFinished: render.ttlSecondsAfterFinished, }, pollCommands: { run: `./scripts/agentrun runs show ${options.run.id} --manager-url ${options.managerUrl}`, events: `./scripts/agentrun runs events ${options.run.id} --manager-url ${options.managerUrl} --after-seq 0 --limit 100`, }, warnings: render.warnings, manifest, }; } export function renderRunnerJobManifest(options: RunnerJobRenderOptions): { manifest: JsonRecord; namespace: string; jobName: string; runnerId: string; attemptId: string; sourceCommit: string; serviceAccountName: string; secretRefs: CredentialProjection[]; warnings: string[]; ttlSecondsAfterFinished: number } { 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 ttlSecondsAfterFinished = options.ttlSecondsAfterFinished ?? 86_400; 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": String(options.dryRun === true), }, }, spec: { backoffLimit: options.backoffLimit ?? 0, ttlSecondsAfterFinished, 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: [ { name: "runner-home", mountPath: "/home/agentrun" }, ...secretRefs.map((item) => ({ name: item.volumeName, mountPath: item.projectionMountPath, readOnly: true })), ], resources: { requests: { cpu: "250m", memory: "512Mi" }, limits: { cpu: "2", memory: "4Gi" }, }, securityContext: { allowPrivilegeEscalation: false, readOnlyRootFilesystem: false, capabilities: { drop: ["ALL"] }, }, }, ], volumes: [{ name: "runner-home", emptyDir: {} }, ...secretRefs.map(secretVolume)], }, }, }, }; return { manifest, namespace, jobName, runnerId, attemptId, sourceCommit, serviceAccountName, secretRefs, warnings, ttlSecondsAfterFinished }; } function runnerEnv(options: RunnerJobRenderOptions, context: { namespace: string; jobName: string; runnerId: string; attemptId: string; sourceCommit: string; secretRefs: CredentialProjection[] }): JsonRecord[] { const selectedSecret = context.secretRefs.find((item) => item.profile === options.run.backendProfile); const codexHome = selectedSecret?.runtimeMountPath ?? defaultRuntimeHome(options.run.backendProfile); 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_SESSION_REF_JSON", value: JSON.stringify(options.run.sessionRef ?? null) }, { name: "AGENTRUN_RESOURCE_BUNDLE_JSON", value: JSON.stringify(options.run.resourceBundleRef ?? null) }, { name: "AGENTRUN_WORKSPACE_ROOT", value: "/home/agentrun/workspaces" }, { 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: "AGENTRUN_RUNNER_IDLE_TIMEOUT_MS", value: "600000" }, { name: "HOME", value: "/home/agentrun" }, { name: "CODEX_HOME", value: codexHome }, ...(selectedSecret ? [{ name: "AGENTRUN_CODEX_SECRET_HOME", value: selectedSecret.projectionMountPath }] : []), ...transientEnvVars(options.transientEnv ?? []), ]; } function transientEnvVars(items: RunnerTransientEnv[]): JsonRecord[] { return items.map((item) => ({ name: item.name, value: item.value })); } function summarizeTransientEnv(items: RunnerTransientEnv[]): JsonRecord { return { count: items.length, names: items.map((item) => item.name), valuesPrinted: false, }; } function redactTransientEnvInManifest(manifest: JsonRecord, items: RunnerTransientEnv[]): JsonRecord { if (items.length === 0) return manifest; const names = new Set(items.map((item) => item.name)); const copy = JSON.parse(JSON.stringify(manifest)) as JsonRecord; const spec = copy.spec as JsonRecord | undefined; const template = spec?.template as JsonRecord | undefined; const podSpec = template?.spec as JsonRecord | undefined; const containers = Array.isArray(podSpec?.containers) ? podSpec.containers as JsonRecord[] : []; for (const container of containers) { const env = Array.isArray(container.env) ? container.env as JsonRecord[] : []; for (const entry of env) { if (typeof entry.name === "string" && names.has(entry.name)) entry.value = "REDACTED"; } } return copy; } function credentialProjections(run: RunRecord, namespace: string): CredentialProjection[] { const policy: ExecutionPolicy = run.executionPolicy; const credentials = (policy.secretScope.providerCredentials ?? []).filter((item) => item.profile === run.backendProfile); return credentials.map((item, index) => ({ profile: item.profile, secretRef: item.secretRef.namespace ? item.secretRef : { ...item.secretRef, namespace }, volumeName: sanitizeVolumeName(`${String(item.profile)}-${index}`), runtimeMountPath: normalizeMountPath(item.secretRef.mountPath, String(item.profile)), projectionMountPath: `/var/run/agentrun/secrets/${sanitizeVolumeName(`${String(item.profile)}-${index}`)}`, })); } 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, profile: string): string { const spec = backendProfileSpec(profile); const suffix = spec ? spec.profile : sanitizeVolumeName(profile); if (!value || value === "~/.codex") return defaultRuntimeHome(suffix); if (value.startsWith("~/")) return `/home/agentrun/${value.slice(2)}-${suffix}`; return value; } function defaultRuntimeHome(profile: string): string { return `/home/agentrun/.codex-${sanitizeVolumeName(profile)}`; } 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"; }