Files
pikasTech-agentrun/src/runner/k8s-job.ts
T
2026-06-20 15:32:46 +08:00

561 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { newId, stableHash } from "../common/validation.js";
import type { BackendProfile, ExecutionPolicy, JsonRecord, JsonValue, RunRecord, SecretRef } from "../common/types.js";
import { backendProfileSpec } from "../common/backend-profiles.js";
import { staticWorkReadyCapabilitySummary } from "../common/work-ready.js";
import { gitTransportSummary, runnerGitTransportEnvVars } from "../common/git-transport.js";
const defaultBootRepoUrl = "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/agentrun.git";
const defaultResourceBinPath = "/usr/local/bin";
const defaultCodexShellSandbox = "danger-full-access";
const defaultRunnerIdleTimeoutMs = 600_000;
const fallbackRunnerEgressProxyUrl = "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789";
const defaultRunnerNoProxyItems = [
"localhost",
"127.0.0.1",
"::1",
"agentrun-mgr",
"agentrun-mgr.agentrun-v01",
"agentrun-mgr.agentrun-v01.svc",
"agentrun-mgr.agentrun-v01.svc.cluster.local",
"agentrun-v01-postgres",
"agentrun-v01-postgres.agentrun-v01",
"agentrun-v01-postgres.agentrun-v01.svc",
"agentrun-v01-postgres.agentrun-v01.svc.cluster.local",
"g14-provider-egress-proxy",
"g14-provider-egress-proxy.unidesk",
"g14-provider-egress-proxy.unidesk.svc",
"g14-provider-egress-proxy.unidesk.svc.cluster.local",
"registry.npmjs.org",
"registry.npmmirror.com",
"g14-tcp-egress-gateway",
"g14-tcp-egress-gateway.unidesk",
"g14-tcp-egress-gateway.unidesk.svc",
"g14-tcp-egress-gateway.unidesk.svc.cluster.local",
"hyueapi.com",
".hyueapi.com",
"10.42.0.0/16",
"10.43.0.0/16",
".svc",
".cluster.local",
];
export interface RunnerJobRenderOptions {
run: RunRecord;
commandId: string;
runnerJobId?: string;
managerUrl: string;
image: string;
bootRepoUrl?: string;
namespace?: string;
attemptId?: string;
runnerId?: string;
sourceCommit?: string;
serviceAccountName?: string;
jobNamePrefix?: string;
lane?: string;
imagePullPolicy?: string;
backoffLimit?: number;
ttlSecondsAfterFinished?: number;
runnerIdleTimeoutMs?: number;
transientEnv?: RunnerTransientEnv[];
sessionPvc?: RunnerSessionPvcOptions;
dryRun?: boolean;
}
export interface RunnerSessionPvcOptions {
pvcName: string;
namespace: string;
mountPath: string;
codexRolloutSubdir: string;
}
export interface RunnerTransientEnv {
name: string;
value: string;
sensitive?: boolean;
secretRef?: RunnerTransientEnvSecretRef;
}
export interface RunnerTransientEnvSecretRef {
name: string;
key: string;
}
interface CredentialProjection {
profile: BackendProfile | string;
secretRef: SecretRef;
volumeName: string;
runtimeMountPath: string;
projectionMountPath: string;
}
type ToolCredentialProjection = ToolCredentialEnvProjection | ToolCredentialVolumeProjection;
interface ToolCredentialBaseProjection {
tool: string;
purpose: string | null;
secretRef: SecretRef;
}
interface ToolCredentialEnvProjection extends ToolCredentialBaseProjection {
kind: "env";
envName: string;
secretKey: string;
}
interface ToolCredentialVolumeProjection extends ToolCredentialBaseProjection {
kind: "volume";
volumeName: string;
mountPath: 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 })),
toolCredentials: summarizeToolCredentials(render.toolCredentials, render.namespace),
gitTransport: gitTransportSummary(),
transientEnv: summarizeTransientEnv(options.transientEnv ?? []),
workReady: staticWorkReadyCapabilitySummary(),
retention: {
ttlSecondsAfterFinished: render.ttlSecondsAfterFinished,
runnerIdleTimeoutMs: render.runnerIdleTimeoutMs,
},
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; runnerJobId: string; runnerId: string; attemptId: string; sourceCommit: string; serviceAccountName: string; secretRefs: CredentialProjection[]; toolCredentials: ToolCredentialProjection[]; warnings: string[]; ttlSecondsAfterFinished: number; runnerIdleTimeoutMs: 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 runnerJobId = options.runnerJobId ?? newId("rjob");
const sourceCommit = options.sourceCommit ?? process.env.AGENTRUN_SOURCE_COMMIT ?? "unknown";
const serviceAccountName = options.serviceAccountName ?? "agentrun-v01-runner";
const jobNamePrefix = normalizeJobNamePrefix(options.jobNamePrefix ?? serviceAccountName);
const lane = options.lane ?? process.env.AGENTRUN_LANE ?? "v0.1";
const ttlSecondsAfterFinished = options.ttlSecondsAfterFinished ?? 86_400;
const runnerIdleTimeoutMs = normalizeRunnerIdleTimeoutMs(options.runnerIdleTimeoutMs);
const jobName = `${jobNamePrefix}-${shortDnsHash(options.run.id, attemptId)}`;
const secretRefs = credentialProjections(options.run, namespace);
const toolCredentials = toolCredentialProjections(options.run, namespace);
const sessionPvc = options.sessionPvc;
const warnings: string[] = [];
if (secretRefs.length === 0) warnings.push("run executionPolicy.secretScope 未声明 provider SecretRefrunner 将按 secret-unavailable 上报,而不会降级直连外部凭据");
const env = runnerEnv(options, { namespace, jobName, runnerJobId, runnerId, attemptId, sourceCommit, secretRefs, toolCredentials, sessionPvc, runnerIdleTimeoutMs });
const manifest: JsonRecord = {
apiVersion: "batch/v1",
kind: "Job",
metadata: {
name: jobName,
namespace,
labels: labels(options.run, jobName, lane),
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, lane),
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: ["/opt/agentrun/deploy/runtime/boot/agentrun-runner.sh"],
env,
volumeMounts: [
{ name: "runner-home", mountPath: "/home/agentrun" },
...secretRefs.map((item) => ({ name: item.volumeName, mountPath: item.projectionMountPath, readOnly: true })),
...toolCredentialVolumeMounts(toolCredentials),
...(sessionPvc ? [{ name: "agentrun-sessions", mountPath: sessionPvc.mountPath, readOnly: false }] : []),
],
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),
...toolCredentialVolumes(toolCredentials),
...(sessionPvc ? [{ name: "agentrun-sessions", persistentVolumeClaim: { claimName: sessionPvc.pvcName } }] : []),
],
},
},
},
};
return { manifest, namespace, jobName, runnerJobId, runnerId, attemptId, sourceCommit, serviceAccountName, secretRefs, toolCredentials, warnings, ttlSecondsAfterFinished, runnerIdleTimeoutMs };
}
function runnerEnv(options: RunnerJobRenderOptions, context: { namespace: string; jobName: string; runnerJobId: string; runnerId: string; attemptId: string; sourceCommit: string; secretRefs: CredentialProjection[]; toolCredentials: ToolCredentialProjection[]; sessionPvc: RunnerSessionPvcOptions | undefined; runnerIdleTimeoutMs: number }): JsonRecord[] {
const selectedSecret = context.secretRefs.find((item) => item.profile === options.run.backendProfile);
const codexHome = selectedSecret?.runtimeMountPath ?? defaultRuntimeHome(options.run.backendProfile);
const bootRepoUrl = optionalString(options.bootRepoUrl) ?? defaultBootRepoUrl;
return dedupeEnvVars([
{ name: "AGENTRUN_MGR_URL", value: options.managerUrl },
{ name: "AGENTRUN_API_KEY", valueFrom: { secretKeyRef: { name: "agentrun-v01-api-key", key: "HWLAB_API_KEY" } } },
{ name: "AGENTRUN_RUN_ID", value: options.run.id },
{ name: "AGENTRUN_COMMAND_ID", value: options.commandId },
{ name: "AGENTRUN_RUNNER_JOB_ID", value: context.runnerJobId },
{ 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_CODEX_SHELL_SANDBOX", value: codexShellSandbox(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_RESOURCE_BIN_PATH", value: defaultResourceBinPath },
{ name: "AGENTRUN_SOURCE_COMMIT", value: context.sourceCommit },
{ name: "AGENTRUN_BOOT_COMMIT", value: context.sourceCommit },
{ name: "AGENTRUN_BOOT_REPO_URL", value: bootRepoUrl },
{ name: "AGENTRUN_BOOT_MODE", value: "runner" },
{ name: "AGENTRUN_APP_ROOT", value: "/home/agentrun/agentrun-source" },
{ 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_WORK_READY_VERSION", value: String(staticWorkReadyCapabilitySummary().version) },
{ name: "AGENTRUN_PROJECT_DEPENDENCY_POLICY", value: "explicit-cache-or-derived-image-only" },
{ name: "AGENTRUN_RUNNER_IDLE_TIMEOUT_MS", value: String(context.runnerIdleTimeoutMs) },
{ name: "AGENTRUN_RUNNER_POLL_INTERVAL_MS", value: "250" },
{ name: "HOME", value: "/home/agentrun" },
{ name: "CODEX_HOME", value: codexHome },
...runnerOtelEnvVars(process.env),
...(selectedSecret ? [{ name: "AGENTRUN_CODEX_SECRET_HOME", value: selectedSecret.projectionMountPath }] : []),
...(context.sessionPvc ? [
{ name: "AGENTRUN_SESSION_PVC_NAME", value: context.sessionPvc.pvcName },
{ name: "AGENTRUN_SESSION_PVC_NAMESPACE", value: context.sessionPvc.namespace },
{ name: "AGENTRUN_SESSION_PVC_MOUNT_PATH", value: context.sessionPvc.mountPath },
{ name: "AGENTRUN_CODEX_ROLLOUT_SUBDIR", value: context.sessionPvc.codexRolloutSubdir },
] : []),
...runnerEgressProxyEnvVars(),
...runnerGitTransportEnvVars(),
...toolCredentialEnvVars(context.toolCredentials),
...transientEnvVars(options.transientEnv ?? []),
]);
}
function normalizeRunnerIdleTimeoutMs(value: number | undefined): number {
if (value === undefined) return defaultRunnerIdleTimeoutMs;
if (!Number.isInteger(value) || value <= 0) throw new Error("runnerIdleTimeoutMs must be a positive integer");
return value;
}
function codexShellSandbox(policy: ExecutionPolicy): string {
if (policy.sandbox === "workspace-write") return defaultCodexShellSandbox;
return policy.sandbox;
}
function toolCredentialEnvVars(items: ToolCredentialProjection[]): JsonRecord[] {
return [
...items.filter((item): item is ToolCredentialEnvProjection => item.kind === "env").map((item) => ({
name: item.envName,
valueFrom: {
secretKeyRef: {
name: item.secretRef.name,
key: item.secretKey,
},
},
})),
...githubSshCommandEnvVars(items),
];
}
function githubSshCommandEnvVars(items: ToolCredentialProjection[]): JsonRecord[] {
const credential = items.find((item): item is ToolCredentialVolumeProjection => item.kind === "volume" && item.tool === "github" && item.purpose === "github-ssh");
if (!credential) return [];
const mountPath = credential.mountPath.replace(/\/+$/u, "");
return [{
name: "GIT_SSH_COMMAND",
value: [
"ssh",
"-F", `${mountPath}/config`,
"-i", `${mountPath}/id_ed25519`,
"-o", "IdentitiesOnly=yes",
"-o", "StrictHostKeyChecking=yes",
"-o", `UserKnownHostsFile=${mountPath}/known_hosts`,
].join(" "),
}];
}
function toolCredentialVolumeMounts(items: ToolCredentialProjection[]): JsonRecord[] {
return items.filter((item): item is ToolCredentialVolumeProjection => item.kind === "volume").map((item) => ({ name: item.volumeName, mountPath: item.mountPath, readOnly: true }));
}
function toolCredentialVolumes(items: ToolCredentialProjection[]): JsonRecord[] {
return items.filter((item): item is ToolCredentialVolumeProjection => item.kind === "volume").map((item) => secretVolume({ profile: item.tool, secretRef: item.secretRef, volumeName: item.volumeName, runtimeMountPath: item.mountPath, projectionMountPath: item.mountPath }));
}
function transientEnvVars(items: RunnerTransientEnv[]): JsonRecord[] {
return items.map((item) => {
if (item.secretRef) {
return {
name: item.name,
valueFrom: {
secretKeyRef: {
name: item.secretRef.name,
key: item.secretRef.key,
},
},
};
}
return { name: item.name, value: item.value };
});
}
function runnerEgressProxyEnvVars(): JsonRecord[] {
const proxyUrl = runnerEgressProxyUrl(process.env);
const noProxy = runnerNoProxy(process.env, proxyUrl);
return [
{ name: "HTTP_PROXY", value: proxyUrl },
{ name: "HTTPS_PROXY", value: proxyUrl },
{ name: "ALL_PROXY", value: proxyUrl },
{ name: "NO_PROXY", value: noProxy },
{ name: "http_proxy", value: proxyUrl },
{ name: "https_proxy", value: proxyUrl },
{ name: "all_proxy", value: proxyUrl },
{ name: "no_proxy", value: noProxy },
];
}
function runnerOtelEnvVars(env: NodeJS.ProcessEnv): JsonRecord[] {
const tracesEndpoint = firstNonEmpty(env.AGENTRUN_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT);
const baseEndpoint = firstNonEmpty(env.AGENTRUN_OTEL_EXPORTER_OTLP_ENDPOINT, env.OTEL_EXPORTER_OTLP_ENDPOINT);
return [
...(tracesEndpoint ? [
{ name: "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", value: tracesEndpoint },
{ name: "AGENTRUN_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", value: tracesEndpoint },
] : []),
...(!tracesEndpoint && baseEndpoint ? [
{ name: "OTEL_EXPORTER_OTLP_ENDPOINT", value: baseEndpoint },
{ name: "AGENTRUN_OTEL_EXPORTER_OTLP_ENDPOINT", value: baseEndpoint },
] : []),
{ name: "OTEL_SERVICE_NAME", value: "agentrun-runner" },
...optionalEnvVar("AGENTRUN_LANE", env.AGENTRUN_LANE),
...optionalEnvVar("UNIDESK_NODE_ID", env.UNIDESK_NODE_ID ?? env.AGENTRUN_NODE_ID),
...optionalEnvVar("AGENTRUN_NODE_ID", env.AGENTRUN_NODE_ID ?? env.UNIDESK_NODE_ID),
...optionalEnvVar("HWLAB_RUNTIME_LANE", env.HWLAB_RUNTIME_LANE),
];
}
function optionalEnvVar(name: string, value: string | undefined): JsonRecord[] {
const normalized = value?.trim();
return normalized ? [{ name, value: normalized }] : [];
}
function firstNonEmpty(...values: Array<string | null | undefined>): string | null {
for (const value of values) {
const text = value?.trim();
if (text) return text;
}
return null;
}
function runnerEgressProxyUrl(env: NodeJS.ProcessEnv): string {
const value = env.AGENTRUN_RUNNER_EGRESS_PROXY_URL?.trim();
return value && value.length > 0 ? value : fallbackRunnerEgressProxyUrl;
}
function runnerNoProxy(env: NodeJS.ProcessEnv, proxyUrl: string): string {
const items = new Set(defaultRunnerNoProxyItems);
const proxyHost = hostFromUrl(proxyUrl);
if (proxyHost) items.add(proxyHost);
for (const item of (env.AGENTRUN_RUNNER_NO_PROXY_EXTRA ?? "").split(",")) {
const value = item.trim();
if (value.length > 0) items.add(value);
}
return [...items].join(",");
}
function hostFromUrl(value: string): string | null {
try {
return new URL(value).hostname || null;
} catch {
return null;
}
}
function dedupeEnvVars(items: JsonRecord[]): JsonRecord[] {
const order: string[] = [];
const byName = new Map<string, JsonRecord>();
for (const item of items) {
const name = typeof item.name === "string" ? item.name : "";
if (!name) continue;
if (!byName.has(name)) order.push(name);
byName.set(name, item);
}
return order.map((name) => byName.get(name)).filter((item): item is JsonRecord => item !== undefined);
}
function summarizeTransientEnv(items: RunnerTransientEnv[]): JsonRecord {
return {
count: items.length,
names: items.map((item) => item.name),
valuesPrinted: false,
};
}
function summarizeToolCredentials(items: ToolCredentialProjection[], namespace: string): JsonRecord {
return {
count: items.length,
items: items.map((item) => ({
tool: item.tool,
purpose: item.purpose,
name: item.secretRef.name,
namespace: item.secretRef.namespace ?? namespace,
keys: item.secretRef.keys ?? [],
projection: item.kind === "env" ? { kind: "env", envName: item.envName, secretKey: item.secretKey } : { kind: "volume", mountPath: item.mountPath },
valuesPrinted: false,
})),
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 !== undefined) 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: credentialSecretRef(item.profile, 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 credentialSecretRef(profile: string, secretRef: SecretRef, namespace: string): SecretRef {
const spec = backendProfileSpec(profile);
const keys = [...new Set([...(secretRef.keys ?? []), ...(spec?.requiredSecretKeys ?? [])])];
return { ...secretRef, namespace: secretRef.namespace ?? namespace, ...(keys.length > 0 ? { keys } : {}) };
}
function toolCredentialProjections(run: RunRecord, namespace: string): ToolCredentialProjection[] {
const policy: ExecutionPolicy = run.executionPolicy;
const credentials = policy.secretScope.toolCredentials ?? [];
return credentials.map((item, index) => {
const secretRef = item.secretRef.namespace ? item.secretRef : { ...item.secretRef, namespace };
const base = { tool: item.tool, purpose: item.purpose ?? null, secretRef };
if (item.projection.kind === "env") return { ...base, kind: "env" as const, envName: item.projection.envName, secretKey: item.projection.secretKey ?? item.secretRef.keys?.[0] ?? item.projection.envName };
return { ...base, kind: "volume" as const, volumeName: sanitizeVolumeName(`tool-${item.tool}-${index}`), mountPath: item.projection.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, 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, lane: string): JsonRecord {
return {
"app.kubernetes.io/name": "agentrun-runner",
"app.kubernetes.io/component": "runner",
"app.kubernetes.io/part-of": "agentrun",
"agentrun.pikastech.local/lane": lane,
"agentrun.pikastech.local/run-hash": shortHash(run.id),
"job-name": jobName,
};
}
function normalizeJobNamePrefix(value: string): string {
const normalized = value.toLowerCase().replace(/[^a-z0-9-]+/gu, "-").replace(/^-+|-+$/gu, "");
if (!normalized) throw new Error("jobNamePrefix must contain at least one DNS label character");
if (normalized.length > 46) return normalized.slice(0, 46).replace(/-+$/u, "") || "agentrun-runner";
return normalized;
}
function shortDnsHash(...parts: string[]): string {
return shortHash(parts.join(":"));
}
function shortHash(value: JsonValue): string {
return stableHash(value).slice(0, 12);
}
function optionalString(value: unknown): string | undefined {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
}
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";
}