561 lines
24 KiB
TypeScript
561 lines
24 KiB
TypeScript
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 SecretRef;runner 将按 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";
|
||
}
|