Files
pikasTech-unidesk/scripts/src/agentrun-lanes.ts
T

1439 lines
67 KiB
TypeScript

// SPEC: PJ2026-01060305 AgentRun execution policy + PJ2026-01020108 cancel lifecycle draft-2026-06-25-p0.
// Parses AgentRun YAML lane policy, including cancel lifecycle values owned by config/agentrun.yaml.
import { rootPath } from "./config";
import {
asRecord,
arrayField,
booleanField,
integerField,
optionalIntegerField,
optionalStringField,
readYamlRecord,
recordField,
stringArrayField,
stringField,
} from "./platform-infra-ops-library";
import { materializeYamlComposition } from "./yaml-composition";
import { resolveConfigRef } from "./ops/config-refs";
export const AGENTRUN_CONFIG_PATH = "config/agentrun.yaml";
export interface AgentRunGitMirrorRepositorySpec {
readonly key: string;
readonly repository: string;
readonly remote: string;
readonly sourceBranch: string;
readonly gitopsBranch?: string;
}
export interface AgentRunManagedRepositoryReconcilerSpec {
readonly enabled: boolean;
readonly deploymentName: string;
readonly configMapName: string;
readonly serviceAccountName: string;
readonly remoteAuth: {
readonly configRef: string;
readonly capabilitySha256: string;
readonly apiVersion: "unidesk.ai/v1";
readonly kind: "GitFetchCredential";
readonly authMode: "github-https-token";
readonly host: "github.com";
readonly secretRef: {
readonly namespace: string;
readonly name: string;
readonly key: string;
};
};
readonly hostNetwork: boolean;
readonly dnsPolicy: "ClusterFirst" | "ClusterFirstWithHostNet" | "Default";
readonly imagePullPolicy: "Always" | "IfNotPresent" | "Never";
readonly cacheMountPath: string;
readonly stateDirectory: string;
readonly reconcileIntervalMs: number;
readonly fetchTimeoutMs: number;
readonly shutdownGraceMs: number;
readonly maxConcurrentRepositories: number;
readonly retry: {
readonly maxAttempts: number;
readonly initialDelayMs: number;
readonly maxDelayMs: number;
};
readonly freshness: {
readonly maxAgeMs: number;
};
readonly readiness: {
readonly initialDelaySeconds: number;
readonly periodSeconds: number;
readonly timeoutSeconds: number;
readonly failureThreshold: number;
};
readonly status: {
readonly defaultRepositoryLimit: number;
};
readonly lifecycle: {
readonly undeclaredRepositoryPolicy: "retain";
readonly managedRefs: "source-branch-only";
};
readonly resources: AgentRunContainerResources;
}
export interface AgentRunSourceAuthoritySpec {
readonly mode: "gitMirrorSnapshot";
readonly resolver: "k8s-git-mirror";
readonly allowHostGit: false;
readonly allowHostWorkspace: false;
readonly allowGithubDirectInPipeline: false;
}
export interface AgentRunSourceSnapshotSpec {
readonly stageRefPrefix: string;
readonly missingObjectPolicy: "fail-fast";
readonly refreshPolicy: "sync-before-snapshot";
}
export interface AgentRunSecretRef {
readonly namespace: string;
readonly name: string;
readonly key: string;
}
export interface AgentRunLaneSecretSpec {
readonly id: string;
readonly sourceRef: string;
readonly sourceMode: "env" | "file" | "codex-config" | "json";
readonly sourceKey: string | null;
readonly sourceFormat: "env" | "raw-token" | null;
readonly transform: "codex-auth-json-openai-api-key" | null;
readonly codexConfig: AgentRunCodexConfigSpec | null;
readonly jsonValue: unknown;
readonly targetRef: AgentRunSecretRef;
readonly providerCredentialProfile: string | null;
}
export interface AgentRunCodexConfigSpec {
readonly modelProvider: string;
readonly model: string;
readonly reviewModel: string | null;
readonly modelReasoningEffort: string | null;
readonly disableResponseStorage: boolean | null;
readonly networkAccess: string | null;
readonly windowsWslSetupAcknowledged: boolean | null;
readonly serviceTier: string | null;
readonly modelContextWindow: number | null;
readonly modelAutoCompactTokenLimit: number | null;
readonly modelCatalogJson: string | null;
readonly approvalsReviewer: string | null;
readonly noticeHideFullAccessWarning: boolean | null;
readonly modelProviders: readonly AgentRunCodexModelProviderSpec[];
readonly features: Readonly<Record<string, boolean>>;
readonly projects: readonly AgentRunCodexProjectSpec[];
readonly tuiModelAvailabilityNux: Readonly<Record<string, number>>;
}
export interface AgentRunCodexModelProviderSpec {
readonly id: string;
readonly name: string;
readonly baseUrl: string;
readonly wireApi: string;
readonly requiresOpenaiAuth: boolean | null;
}
export interface AgentRunCodexProjectSpec {
readonly path: string;
readonly trustLevel: string;
}
export interface AgentRunProviderCredentialRef {
readonly profile: string;
readonly secretRef: {
readonly namespace: string;
readonly name: string;
readonly keys: readonly string[];
};
}
export interface AgentRunLaneSpec {
readonly lane: string;
readonly nodeId: string;
readonly nodeRoute: string;
readonly nodeKubeRoute: string;
readonly version: string;
readonly source: {
readonly statusMode: "host-worktree" | "k3s-git-mirror";
readonly repository: string;
readonly worktreeRemote: string;
readonly branch: string;
readonly sourceAuthority: AgentRunSourceAuthoritySpec | null;
readonly sourceSnapshot: AgentRunSourceSnapshotSpec | null;
readonly bootstrapFromBranch: string | null;
readonly bootstrapTimeoutSeconds: number;
readonly bootstrapPollSeconds: number;
readonly remote: string;
readonly workspace: string;
};
readonly runtime: {
readonly namespace: string;
readonly managerDeployment: string;
readonly managerService: string;
readonly managerPort: number;
readonly internalBaseUrl: string;
};
readonly ci: {
readonly namespace: string;
readonly pipeline: string;
readonly pipelineRunPrefix: string;
readonly serviceAccountName: string;
readonly registryPrefix: string;
readonly toolsImage: string;
readonly buildkitImage: string | null;
};
readonly gitops: {
readonly branch: string;
readonly path: string;
readonly argoNamespace: string;
readonly argoApplication: string;
readonly repoURL: string;
};
readonly deployment: {
readonly format: "unidesk-yaml-only";
readonly gitopsRoot: string;
readonly runtimeRenderDir: string;
readonly artifactCatalogPath: string;
readonly argocd: {
readonly project: string;
readonly applicationFile: string;
};
readonly manager: {
readonly serviceAccount: string;
readonly apiKeySecretRef: { readonly name: string; readonly key: string };
readonly env: Readonly<Record<string, string>>;
readonly unideskSshEndpointEnv: { readonly name: string; readonly value: string } | null;
readonly bootRepoUrl: string;
readonly imageBuild: AgentRunImageBuildSpec;
readonly resources: AgentRunContainerResources;
};
readonly runner: {
readonly serviceAccount: string;
readonly jobNamePrefix: string;
readonly idleTimeoutMs: number;
readonly backendRetry: {
readonly maxAttempts: number;
readonly initialBackoffMs: number;
readonly maxBackoffMs: number;
};
readonly apiKeySecretRef: { readonly name: string; readonly key: string };
readonly egressProxyUrl: string | null;
readonly noProxyExtra: readonly string[];
readonly retention: AgentRunRunnerRetentionSpec;
readonly cancelLifecycle: AgentRunCancelLifecycleSpec;
};
readonly localPostgres: {
readonly enabled: boolean;
readonly serviceName: string | null;
readonly image: string | null;
readonly storage: string | null;
readonly port: number | null;
readonly database: string | null;
readonly user: string | null;
readonly passwordSourceRef: string | null;
readonly passwordSourceKey: string | null;
};
};
readonly gitMirror: {
readonly namespace: string;
readonly readService: string;
readonly readDeployment: string;
readonly writeService: string;
readonly writeDeployment: string;
readonly resourceBundleBaseUrl: string;
readonly readUrl: string;
readonly writeUrl: string;
readonly cachePvc: string;
readonly cacheHostPath: string | null;
readonly sshSecretName: string;
readonly githubProxy: {
readonly host: string;
readonly port: number;
};
readonly toolsImage: string;
readonly syncJobPrefix: string;
readonly flushJobPrefix: string;
readonly repositoryReconciler: AgentRunManagedRepositoryReconcilerSpec;
readonly repositories: readonly AgentRunGitMirrorRepositorySpec[];
};
readonly database: {
readonly mode: "local-postgres" | "external-postgres";
readonly provider: string | null;
readonly configRef: string | null;
readonly database: string | null;
readonly user: string | null;
readonly sslmode: "require" | null;
readonly secretSourceRef: string | null;
readonly secretRef: { readonly name: string; readonly key: string };
readonly localPostgresExpectedAbsent: boolean;
};
readonly secrets: readonly AgentRunLaneSecretSpec[];
}
export interface AgentRunContainerResources {
readonly requests: {
readonly cpu: string;
readonly memory: string;
};
readonly limits: {
readonly cpu: string;
readonly memory: string;
};
}
export interface AgentRunImageBuildSpec {
readonly context: string;
readonly containerfile: string;
readonly repository: string;
readonly network: string;
readonly buildArgs: Readonly<Record<string, string>>;
readonly httpProxy: string | null;
readonly httpsProxy: string | null;
readonly noProxy: readonly string[];
readonly buildContainerProxy: {
readonly httpProxy: string | null;
readonly httpsProxy: string | null;
readonly noProxy: readonly string[];
};
readonly envIdentityFiles: readonly string[];
readonly timeoutSeconds: number;
readonly pollSeconds: number;
}
export interface AgentRunRunnerRetentionSpec {
readonly maxRunners: number;
readonly cleanupOrder: "oldest-inactive-last-active-first";
readonly activeHeartbeatMaxAgeMs: number;
readonly stalePendingMaxAgeMs: number;
readonly selectors: {
readonly matchLabels: Readonly<Record<string, string>>;
readonly jobNamePrefixes: readonly string[];
};
readonly ageBasedCleanup: {
readonly enabled: boolean;
readonly maxAgeHours: number | null;
};
readonly sessionPvcRetention: {
readonly enabled: boolean;
readonly prefixes: readonly string[];
readonly maxDeletePerRun: number;
};
}
export type AgentRunCancelLifecycleStage = "accepted" | "persisted" | "delivered" | "aborting" | "terminalized" | "fenced" | "late-write-rejected";
export interface AgentRunCancelLifecycleSpec {
readonly deliveryMode: "manager-epoch";
readonly gracefulAbortMs: number;
readonly killEscalationMs: number;
readonly staleHeartbeatFencingMs: number;
readonly lateWriteFencing: {
readonly enabled: boolean;
};
readonly eventStages: readonly AgentRunCancelLifecycleStage[];
}
export interface AgentRunLaneTarget {
readonly configPath: string;
readonly spec: AgentRunLaneSpec;
}
interface AgentRunNodeSpec {
readonly id: string;
readonly route: string;
readonly kubeRoute: string;
}
interface AgentRunControlPlaneConfig {
readonly sourcePath: string;
readonly defaultTarget: {
readonly node: string;
readonly lane: string;
};
readonly nodes: Record<string, AgentRunNodeSpec>;
readonly lanes: Record<string, AgentRunLaneSpec>;
}
export function resolveAgentRunLaneTarget(options: { node?: string | null; lane?: string | null }, env: NodeJS.ProcessEnv = process.env): AgentRunLaneTarget {
const config = readAgentRunControlPlaneConfig(env);
const requestedNode = options.node ?? null;
const requestedLane = options.lane ?? null;
if (requestedLane !== null) {
const spec = config.lanes[requestedLane];
if (spec === undefined) throw new Error(`${config.sourcePath}: controlPlane.lanes.${requestedLane} is not declared`);
if (requestedNode !== null && requestedNode !== spec.nodeId) throw new Error(`--node ${requestedNode} does not match controlPlane.lanes.${requestedLane}.node=${spec.nodeId}`);
return { configPath: config.sourcePath, spec };
}
if (requestedNode !== null) {
const candidates = Object.values(config.lanes).filter((lane) => lane.nodeId === requestedNode);
if (candidates.length === 0) throw new Error(`${config.sourcePath}: no AgentRun lane is declared for node ${requestedNode}`);
if (candidates.length > 1) throw new Error(`--lane is required because node ${requestedNode} has multiple AgentRun lanes: ${candidates.map((lane) => lane.lane).join(", ")}`);
return { configPath: config.sourcePath, spec: candidates[0] };
}
const spec = config.lanes[config.defaultTarget.lane];
if (spec === undefined) throw new Error(`${config.sourcePath}: default controlPlane lane ${config.defaultTarget.lane} is not declared`);
if (spec.nodeId !== config.defaultTarget.node) throw new Error(`${config.sourcePath}: controlPlane.default.node does not match default lane node`);
return { configPath: config.sourcePath, spec };
}
export function resolveAgentRunLaneTargetsForNode(nodeId: string, env: NodeJS.ProcessEnv = process.env): readonly AgentRunLaneTarget[] {
const targets = listAgentRunLaneTargetsForNode(nodeId, env);
if (targets.length === 0) {
const configPath = env.AGENTRUN_CONTROL_PLANE_CONFIG ?? rootPath(AGENTRUN_CONFIG_PATH);
throw new Error(`${configPath}: no AgentRun lane is declared for node ${nodeId}`);
}
return targets;
}
export function listAgentRunLaneTargetsForNode(nodeId: string, env: NodeJS.ProcessEnv = process.env): readonly AgentRunLaneTarget[] {
const config = readAgentRunControlPlaneConfig(env);
return Object.values(config.lanes)
.filter((lane) => lane.nodeId === nodeId)
.sort((left, right) => left.lane.localeCompare(right.lane))
.map((spec) => ({ configPath: config.sourcePath, spec }));
}
export function agentRunLaneSummary(spec: AgentRunLaneSpec): Record<string, unknown> {
return {
node: {
id: spec.nodeId,
route: spec.nodeRoute,
kubeRoute: spec.nodeKubeRoute,
},
lane: spec.lane,
version: spec.version,
source: {
repository: spec.source.repository,
branch: spec.source.branch,
statusMode: spec.source.statusMode,
sourceAuthority: spec.source.sourceAuthority,
sourceSnapshot: spec.source.sourceSnapshot,
bootstrapFromBranch: spec.source.bootstrapFromBranch,
bootstrapTimeoutSeconds: spec.source.bootstrapTimeoutSeconds,
bootstrapPollSeconds: spec.source.bootstrapPollSeconds,
workspace: spec.source.workspace,
},
runtime: {
namespace: spec.runtime.namespace,
managerDeployment: spec.runtime.managerDeployment,
managerService: spec.runtime.managerService,
managerPort: spec.runtime.managerPort,
internalBaseUrl: spec.runtime.internalBaseUrl,
},
ci: {
namespace: spec.ci.namespace,
pipeline: spec.ci.pipeline,
pipelineRunPrefix: spec.ci.pipelineRunPrefix,
serviceAccountName: spec.ci.serviceAccountName,
registryPrefix: spec.ci.registryPrefix,
buildkitImage: spec.ci.buildkitImage,
},
gitops: {
branch: spec.gitops.branch,
path: spec.gitops.path,
argoNamespace: spec.gitops.argoNamespace,
argoApplication: spec.gitops.argoApplication,
repoURL: spec.gitops.repoURL,
},
deployment: {
format: spec.deployment.format,
gitopsRoot: spec.deployment.gitopsRoot,
runtimeRenderDir: spec.deployment.runtimeRenderDir,
artifactCatalogPath: spec.deployment.artifactCatalogPath,
argocd: spec.deployment.argocd,
manager: {
serviceAccount: spec.deployment.manager.serviceAccount,
apiKeySecretRef: spec.deployment.manager.apiKeySecretRef,
envNames: Object.keys(spec.deployment.manager.env).sort(),
unideskSshEndpointEnv: spec.deployment.manager.unideskSshEndpointEnv === null
? null
: { name: spec.deployment.manager.unideskSshEndpointEnv.name, valuesPrinted: false },
bootRepoUrl: spec.deployment.manager.bootRepoUrl,
imageBuild: {
context: spec.deployment.manager.imageBuild.context,
containerfile: spec.deployment.manager.imageBuild.containerfile,
repository: spec.deployment.manager.imageBuild.repository,
network: spec.deployment.manager.imageBuild.network,
buildArgNames: Object.keys(spec.deployment.manager.imageBuild.buildArgs).sort(),
proxyConfigured: spec.deployment.manager.imageBuild.httpProxy !== null || spec.deployment.manager.imageBuild.httpsProxy !== null,
noProxyCount: spec.deployment.manager.imageBuild.noProxy.length,
buildContainerProxyConfigured:
spec.deployment.manager.imageBuild.buildContainerProxy.httpProxy !== null || spec.deployment.manager.imageBuild.buildContainerProxy.httpsProxy !== null,
buildContainerNoProxyCount: spec.deployment.manager.imageBuild.buildContainerProxy.noProxy.length,
envIdentityFileCount: spec.deployment.manager.imageBuild.envIdentityFiles.length,
timeoutSeconds: spec.deployment.manager.imageBuild.timeoutSeconds,
pollSeconds: spec.deployment.manager.imageBuild.pollSeconds,
},
resources: spec.deployment.manager.resources,
},
runner: {
serviceAccount: spec.deployment.runner.serviceAccount,
jobNamePrefix: spec.deployment.runner.jobNamePrefix,
idleTimeoutMs: spec.deployment.runner.idleTimeoutMs,
apiKeySecretRef: spec.deployment.runner.apiKeySecretRef,
egressProxyUrl: spec.deployment.runner.egressProxyUrl,
noProxyExtra: spec.deployment.runner.noProxyExtra,
retention: spec.deployment.runner.retention,
cancelLifecycle: spec.deployment.runner.cancelLifecycle,
},
localPostgres: spec.deployment.localPostgres,
},
gitMirror: {
namespace: spec.gitMirror.namespace,
readService: spec.gitMirror.readService,
readDeployment: spec.gitMirror.readDeployment,
writeService: spec.gitMirror.writeService,
writeDeployment: spec.gitMirror.writeDeployment,
resourceBundleBaseUrl: spec.gitMirror.resourceBundleBaseUrl,
readUrl: spec.gitMirror.readUrl,
writeUrl: spec.gitMirror.writeUrl,
cachePvc: spec.gitMirror.cachePvc,
cacheHostPath: spec.gitMirror.cacheHostPath,
sshSecretName: spec.gitMirror.sshSecretName,
githubProxy: { host: spec.gitMirror.githubProxy.host, port: spec.gitMirror.githubProxy.port },
repositoryReconciler: spec.gitMirror.repositoryReconciler,
repositories: spec.gitMirror.repositories.map((repo) => ({
key: repo.key,
repository: repo.repository,
remoteConfigured: repo.remote.length > 0,
sourceBranch: repo.sourceBranch,
gitopsBranch: repo.gitopsBranch ?? null,
})),
},
database: {
mode: spec.database.mode,
provider: spec.database.provider,
configRef: spec.database.configRef,
database: spec.database.database,
user: spec.database.user,
sslmode: spec.database.sslmode,
secretSourceRef: spec.database.secretSourceRef,
secretRef: spec.database.secretRef,
localPostgresExpectedAbsent: spec.database.localPostgresExpectedAbsent,
valuesPrinted: false,
},
secrets: spec.secrets.map((secret) => ({
id: secret.id,
sourceRef: secret.sourceMode === "codex-config" || secret.sourceMode === "json" ? secret.sourceRef : secret.sourceRef.startsWith("/") ? secret.sourceRef : rootPath(".state", "secrets", secret.sourceRef),
sourceMode: secret.sourceMode,
sourceKey: secret.sourceKey,
sourceFormat: secret.sourceFormat,
transform: secret.transform,
targetRef: secret.targetRef,
providerCredential: secret.providerCredentialProfile === null ? null : { profile: secret.providerCredentialProfile },
codexConfig: secret.codexConfig === null ? null : {
modelProvider: secret.codexConfig.modelProvider,
model: secret.codexConfig.model,
reviewModel: secret.codexConfig.reviewModel,
modelReasoningEffort: secret.codexConfig.modelReasoningEffort,
networkAccess: secret.codexConfig.networkAccess,
serviceTier: secret.codexConfig.serviceTier,
modelContextWindow: secret.codexConfig.modelContextWindow,
modelAutoCompactTokenLimit: secret.codexConfig.modelAutoCompactTokenLimit,
modelCatalogJson: secret.codexConfig.modelCatalogJson,
approvalsReviewer: secret.codexConfig.approvalsReviewer,
noticeHideFullAccessWarning: secret.codexConfig.noticeHideFullAccessWarning,
modelProviderCount: secret.codexConfig.modelProviders.length,
projectCount: secret.codexConfig.projects.length,
},
valuesPrinted: false,
})),
providerCredentials: agentRunProviderCredentialRefs(spec).map((credential) => ({
profile: credential.profile,
secretRef: credential.secretRef,
valuesPrinted: false,
})),
};
}
export function agentRunProviderCredentialRefs(spec: AgentRunLaneSpec, profile?: string | null): readonly AgentRunProviderCredentialRef[] {
const groups = new Map<string, { profile: string; namespace: string; name: string; keys: string[] }>();
for (const secret of spec.secrets) {
const credentialProfile = secret.providerCredentialProfile;
if (credentialProfile === null) continue;
if (profile !== undefined && profile !== null && credentialProfile !== profile) continue;
const groupKey = `${credentialProfile}\0${secret.targetRef.namespace}\0${secret.targetRef.name}`;
const group = groups.get(groupKey) ?? {
profile: credentialProfile,
namespace: secret.targetRef.namespace,
name: secret.targetRef.name,
keys: [],
};
if (!group.keys.includes(secret.targetRef.key)) group.keys.push(secret.targetRef.key);
groups.set(groupKey, group);
}
return Array.from(groups.values()).map((group) => ({
profile: group.profile,
secretRef: {
namespace: group.namespace,
name: group.name,
keys: group.keys,
},
}));
}
export function agentRunPipelineRunName(spec: AgentRunLaneSpec, sourceCommit: string): string {
return `${spec.ci.pipelineRunPrefix}-${sourceCommit.slice(0, 12)}`;
}
export function agentRunSourceSnapshotStageRefPrefix(spec: AgentRunLaneSpec): string {
const snapshot = spec.source.sourceSnapshot;
if (snapshot === null) throw new Error(`config/agentrun.yaml controlPlane.lanes.${spec.lane}.source.sourceSnapshot is required for k8s git-mirror snapshot source authority`);
return snapshot.stageRefPrefix.replaceAll("{branch}", spec.source.branch);
}
export function agentRunSourceSnapshotRef(spec: AgentRunLaneSpec, sourceCommit: string): string {
if (!/^[0-9a-f]{40}$/iu.test(sourceCommit)) throw new Error(`sourceCommit must be a 40-hex git SHA for source snapshot ref: ${sourceCommit}`);
return `${agentRunSourceSnapshotStageRefPrefix(spec).replace(/\/+$/u, "")}/${sourceCommit.toLowerCase()}`;
}
function readAgentRunControlPlaneConfig(env: NodeJS.ProcessEnv): AgentRunControlPlaneConfig {
const configPath = env.AGENTRUN_CONTROL_PLANE_CONFIG ?? rootPath(AGENTRUN_CONFIG_PATH);
const root = materializeYamlComposition(readYamlRecord<Record<string, unknown>>(configPath), { label: configPath }).value;
validateConfigEnvelope(root, configPath);
const controlPlane = recordField(root, "controlPlane", configPath);
const defaultTarget = recordField(controlPlane, "default", `${configPath}.controlPlane`);
const nodes = parseNodes(recordField(controlPlane, "nodes", `${configPath}.controlPlane`), configPath);
const lanes = parseLanes(recordField(controlPlane, "lanes", `${configPath}.controlPlane`), nodes, configPath);
return {
sourcePath: configPath,
defaultTarget: {
node: stringField(defaultTarget, "node", `${configPath}.controlPlane.default`),
lane: stringField(defaultTarget, "lane", `${configPath}.controlPlane.default`),
},
nodes,
lanes,
};
}
function validateConfigEnvelope(root: Record<string, unknown>, configPath: string): void {
if (root.version !== 1) throw new Error(`${configPath}.version must be 1`);
if (root.kind !== "AgentRunConfig") throw new Error(`${configPath}.kind must be AgentRunConfig`);
const metadata = recordField(root, "metadata", configPath);
if (stringField(metadata, "name", `${configPath}.metadata`) !== "agentrun") {
throw new Error(`${configPath}.metadata.name must be agentrun`);
}
}
function parseNodes(input: Record<string, unknown>, configPath: string): Record<string, AgentRunNodeSpec> {
const result: Record<string, AgentRunNodeSpec> = {};
for (const [id, raw] of Object.entries(input)) {
validateSimpleId(id, `${configPath}.controlPlane.nodes`);
const node = asRecord(raw, `${configPath}.controlPlane.nodes.${id}`);
result[id] = {
id,
route: stringField(node, "route", `${configPath}.controlPlane.nodes.${id}`),
kubeRoute: stringField(node, "kubeRoute", `${configPath}.controlPlane.nodes.${id}`),
};
}
if (Object.keys(result).length === 0) throw new Error(`${configPath}.controlPlane.nodes must declare at least one node`);
return result;
}
function parseLanes(input: Record<string, unknown>, nodes: Record<string, AgentRunNodeSpec>, configPath: string): Record<string, AgentRunLaneSpec> {
const result: Record<string, AgentRunLaneSpec> = {};
for (const [lane, raw] of Object.entries(input)) {
validateSimpleId(lane, `${configPath}.controlPlane.lanes`);
const item = asRecord(raw, `${configPath}.controlPlane.lanes.${lane}`);
const nodeId = stringField(item, "node", `${configPath}.controlPlane.lanes.${lane}`);
const node = nodes[nodeId];
if (node === undefined) throw new Error(`${configPath}.controlPlane.lanes.${lane}.node references undeclared node ${nodeId}`);
result[lane] = parseLane(lane, node, item, configPath);
}
if (Object.keys(result).length === 0) throw new Error(`${configPath}.controlPlane.lanes must declare at least one lane`);
return result;
}
function parseLane(lane: string, node: AgentRunNodeSpec, input: Record<string, unknown>, configPath: string): AgentRunLaneSpec {
const path = `${configPath}.controlPlane.lanes.${lane}`;
const source = recordField(input, "source", path);
const runtime = recordField(input, "runtime", path);
const ci = recordField(input, "ci", path);
const gitops = recordField(input, "gitops", path);
const deployment = recordField(input, "deployment", path);
const gitMirror = recordField(input, "gitMirror", path);
const database = recordField(input, "database", path);
const version = stringField(input, "version", path);
const statusMode = sourceStatusModeField(optionalStringField(source, "statusMode", `${path}.source`) ?? (version === "v0.2" ? "k3s-git-mirror" : "host-worktree"), `${path}.source.statusMode`);
const spec: AgentRunLaneSpec = {
lane,
nodeId: node.id,
nodeRoute: node.route,
nodeKubeRoute: node.kubeRoute,
version,
source: {
statusMode,
repository: stringField(source, "repository", `${path}.source`),
worktreeRemote: stringField(source, "worktreeRemote", `${path}.source`),
branch: stringField(source, "branch", `${path}.source`),
sourceAuthority: sourceAuthorityConfig(source.sourceAuthority, `${path}.source.sourceAuthority`),
sourceSnapshot: sourceSnapshotConfig(source.sourceSnapshot, `${path}.source.sourceSnapshot`),
bootstrapFromBranch: optionalStringField(source, "bootstrapFromBranch", `${path}.source`) ?? null,
bootstrapTimeoutSeconds: integerField(source, "bootstrapTimeoutSeconds", `${path}.source`),
bootstrapPollSeconds: integerField(source, "bootstrapPollSeconds", `${path}.source`),
remote: stringField(source, "remote", `${path}.source`),
workspace: absolutePathField(source, "workspace", `${path}.source`),
},
runtime: {
namespace: stringField(runtime, "namespace", `${path}.runtime`),
managerDeployment: stringField(runtime, "managerDeployment", `${path}.runtime`),
managerService: stringField(runtime, "managerService", `${path}.runtime`),
managerPort: integerField(runtime, "managerPort", `${path}.runtime`),
internalBaseUrl: urlField(runtime, "internalBaseUrl", `${path}.runtime`),
},
ci: {
namespace: stringField(ci, "namespace", `${path}.ci`),
pipeline: stringField(ci, "pipeline", `${path}.ci`),
pipelineRunPrefix: stringField(ci, "pipelineRunPrefix", `${path}.ci`),
serviceAccountName: stringField(ci, "serviceAccountName", `${path}.ci`),
registryPrefix: stringField(ci, "registryPrefix", `${path}.ci`),
toolsImage: stringField(ci, "toolsImage", `${path}.ci`),
buildkitImage: optionalStringField(ci, "buildkitImage", `${path}.ci`) ?? null,
},
gitops: {
branch: stringField(gitops, "branch", `${path}.gitops`),
path: relativePathField(gitops, "path", `${path}.gitops`),
argoNamespace: stringField(gitops, "argoNamespace", `${path}.gitops`),
argoApplication: stringField(gitops, "argoApplication", `${path}.gitops`),
repoURL: urlField(gitops, "repoURL", `${path}.gitops`),
},
deployment: parseDeployment(deployment, `${path}.deployment`),
gitMirror: {
namespace: stringField(gitMirror, "namespace", `${path}.gitMirror`),
readService: stringField(gitMirror, "readService", `${path}.gitMirror`),
readDeployment: stringField(gitMirror, "readDeployment", `${path}.gitMirror`),
writeService: stringField(gitMirror, "writeService", `${path}.gitMirror`),
writeDeployment: stringField(gitMirror, "writeDeployment", `${path}.gitMirror`),
resourceBundleBaseUrl: urlField(gitMirror, "resourceBundleBaseUrl", `${path}.gitMirror`),
readUrl: urlField(gitMirror, "readUrl", `${path}.gitMirror`),
writeUrl: urlField(gitMirror, "writeUrl", `${path}.gitMirror`),
cachePvc: stringField(gitMirror, "cachePvc", `${path}.gitMirror`),
cacheHostPath: optionalAbsolutePathField(gitMirror, "cacheHostPath", `${path}.gitMirror`) ?? null,
sshSecretName: stringField(gitMirror, "sshSecretName", `${path}.gitMirror`),
githubProxy: parseGithubProxy(recordField(gitMirror, "githubProxy", `${path}.gitMirror`), `${path}.gitMirror.githubProxy`),
toolsImage: stringField(gitMirror, "toolsImage", `${path}.gitMirror`),
syncJobPrefix: stringField(gitMirror, "syncJobPrefix", `${path}.gitMirror`),
flushJobPrefix: stringField(gitMirror, "flushJobPrefix", `${path}.gitMirror`),
repositoryReconciler: parseManagedRepositoryReconciler(recordField(gitMirror, "repositoryReconciler", `${path}.gitMirror`), `${path}.gitMirror.repositoryReconciler`),
repositories: parseGitMirrorRepositories(arrayField(gitMirror, "repositories", `${path}.gitMirror`), `${path}.gitMirror.repositories`),
},
database: parseDatabase(database, `${path}.database`),
secrets: parseLaneSecrets(input, path),
};
validateAgentRunLaneSourceAuthority(spec, path);
return spec;
}
function sourceStatusModeField(value: string, path: string): "host-worktree" | "k3s-git-mirror" {
if (value !== "host-worktree" && value !== "k3s-git-mirror") throw new Error(`${path} must be host-worktree or k3s-git-mirror`);
return value;
}
function sourceAuthorityConfig(value: unknown, path: string): AgentRunSourceAuthoritySpec | null {
if (value === undefined) return null;
const raw = asRecord(value, path);
return {
mode: enumField(raw, "mode", path, ["gitMirrorSnapshot"]),
resolver: enumField(raw, "resolver", path, ["k8s-git-mirror"]),
allowHostGit: falseBooleanField(raw, "allowHostGit", path),
allowHostWorkspace: falseBooleanField(raw, "allowHostWorkspace", path),
allowGithubDirectInPipeline: falseBooleanField(raw, "allowGithubDirectInPipeline", path),
};
}
function sourceSnapshotConfig(value: unknown, path: string): AgentRunSourceSnapshotSpec | null {
if (value === undefined) return null;
const raw = asRecord(value, path);
const stageRefPrefix = stringField(raw, "stageRefPrefix", path);
if (!stageRefPrefix.startsWith("refs/")) throw new Error(`${path}.stageRefPrefix must start with refs/`);
if (stageRefPrefix.includes("..") || /\s/u.test(stageRefPrefix)) throw new Error(`${path}.stageRefPrefix must not contain whitespace or ..`);
if (!stageRefPrefix.includes("{branch}")) throw new Error(`${path}.stageRefPrefix must include {branch}`);
return {
stageRefPrefix,
missingObjectPolicy: enumField(raw, "missingObjectPolicy", path, ["fail-fast"]),
refreshPolicy: enumField(raw, "refreshPolicy", path, ["sync-before-snapshot"]),
};
}
function falseBooleanField(obj: Record<string, unknown>, key: string, path: string): false {
const value = booleanField(obj, key, path);
if (value !== false) throw new Error(`${path}.${key} must be false`);
return false;
}
function validateAgentRunLaneSourceAuthority(spec: AgentRunLaneSpec, path: string): void {
if (spec.version !== "v0.2") return;
if (spec.source.statusMode !== "k3s-git-mirror") throw new Error(`${path}.source.statusMode must be k3s-git-mirror for AgentRun v0.2`);
if (spec.source.sourceAuthority === null) throw new Error(`${path}.source.sourceAuthority is required for AgentRun v0.2`);
if (spec.source.sourceSnapshot === null) throw new Error(`${path}.source.sourceSnapshot is required for AgentRun v0.2`);
const remoteAuth = spec.gitMirror.repositoryReconciler.remoteAuth;
if (remoteAuth.secretRef.namespace !== spec.gitMirror.namespace) {
throw new Error(`${path}.gitMirror.repositoryReconciler.remoteAuth credential namespace must match gitMirror.namespace`);
}
for (const [index, repository] of spec.gitMirror.repositories.entries()) {
const expected = `https://${remoteAuth.host}/${repository.repository}.git`;
if (repository.remote !== expected) {
throw new Error(`${path}.gitMirror.repositories[${index}].remote must exactly match the declared remoteAuth host and repository path`);
}
}
}
function parseDeployment(input: Record<string, unknown>, path: string): AgentRunLaneSpec["deployment"] {
const argocd = recordField(input, "argocd", path);
const manager = recordField(input, "manager", path);
const runner = recordField(input, "runner", path);
const localPostgres = recordField(input, "localPostgres", path);
return {
format: enumField(input, "format", path, ["unidesk-yaml-only"]),
gitopsRoot: relativePathField(input, "gitopsRoot", path),
runtimeRenderDir: relativePathField(input, "runtimeRenderDir", path),
artifactCatalogPath: relativePathField(input, "artifactCatalogPath", path),
argocd: {
project: stringField(argocd, "project", `${path}.argocd`),
applicationFile: relativePathField(argocd, "applicationFile", `${path}.argocd`),
},
manager: {
serviceAccount: stringField(manager, "serviceAccount", `${path}.manager`),
apiKeySecretRef: parseSecretRef(recordField(manager, "apiKeySecretRef", `${path}.manager`), `${path}.manager.apiKeySecretRef`),
env: optionalEnvRecord(manager, "env", `${path}.manager`),
unideskSshEndpointEnv: optionalEnvPair(manager, "unideskSshEndpointEnv", `${path}.manager`),
bootRepoUrl: urlField(manager, "bootRepoUrl", `${path}.manager`),
imageBuild: parseImageBuild(recordField(manager, "imageBuild", `${path}.manager`), `${path}.manager.imageBuild`),
resources: parseContainerResources(recordField(manager, "resources", `${path}.manager`), `${path}.manager.resources`),
},
runner: {
serviceAccount: stringField(runner, "serviceAccount", `${path}.runner`),
jobNamePrefix: stringField(runner, "jobNamePrefix", `${path}.runner`),
idleTimeoutMs: positiveIntegerField(runner, "idleTimeoutMs", `${path}.runner`),
backendRetry: parseRunnerBackendRetry(recordField(runner, "backendRetry", `${path}.runner`), `${path}.runner.backendRetry`),
apiKeySecretRef: parseSecretRef(recordField(runner, "apiKeySecretRef", `${path}.runner`), `${path}.runner.apiKeySecretRef`),
egressProxyUrl: optionalStringField(runner, "egressProxyUrl", `${path}.runner`) ?? null,
noProxyExtra: optionalStringArrayField(runner, "noProxyExtra", `${path}.runner`),
retention: parseRunnerRetention(recordField(runner, "retention", `${path}.runner`), `${path}.runner.retention`),
cancelLifecycle: parseCancelLifecycle(recordField(runner, "cancelLifecycle", `${path}.runner`), `${path}.runner.cancelLifecycle`),
},
localPostgres: parseLocalPostgres(localPostgres, `${path}.localPostgres`),
};
}
function parseCancelLifecycle(input: Record<string, unknown>, path: string): AgentRunCancelLifecycleSpec {
const lateWriteFencing = recordField(input, "lateWriteFencing", path);
return {
deliveryMode: enumField(input, "deliveryMode", path, ["manager-epoch"]),
gracefulAbortMs: positiveIntegerField(input, "gracefulAbortMs", path),
killEscalationMs: positiveIntegerField(input, "killEscalationMs", path),
staleHeartbeatFencingMs: positiveIntegerField(input, "staleHeartbeatFencingMs", path),
lateWriteFencing: {
enabled: booleanField(lateWriteFencing, "enabled", `${path}.lateWriteFencing`),
},
eventStages: parseCancelLifecycleStages(input.eventStages, `${path}.eventStages`),
};
}
function parseCancelLifecycleStages(input: unknown, path: string): readonly AgentRunCancelLifecycleStage[] {
const values: readonly AgentRunCancelLifecycleStage[] = ["accepted", "persisted", "delivered", "aborting", "terminalized", "fenced", "late-write-rejected"];
if (!Array.isArray(input)) throw new Error(`${path} must be an array`);
if (input.length === 0) throw new Error(`${path} must declare at least one stage`);
const result = input.map((value, index) => {
if (typeof value !== "string" || !values.includes(value as AgentRunCancelLifecycleStage)) throw new Error(`${path}[${index}] must be one of ${values.join(", ")}`);
return value as AgentRunCancelLifecycleStage;
});
const duplicates = result.filter((value, index) => result.indexOf(value) !== index);
if (duplicates.length > 0) throw new Error(`${path} must not contain duplicate stages: ${[...new Set(duplicates)].join(", ")}`);
return result;
}
function parseRunnerRetention(input: Record<string, unknown>, path: string): AgentRunRunnerRetentionSpec {
const selectors = recordField(input, "selectors", path);
const ageBasedCleanup = recordField(input, "ageBasedCleanup", path);
const sessionPvcRetentionRaw = input.sessionPvcRetention;
const sessionPvcRetention = typeof sessionPvcRetentionRaw === "object" && sessionPvcRetentionRaw !== null && !Array.isArray(sessionPvcRetentionRaw)
? sessionPvcRetentionRaw as Record<string, unknown>
: {};
const sessionPvcPrefixes = sessionPvcRetention.prefixes === undefined ? [] : stringArrayField(sessionPvcRetention, "prefixes", `${path}.sessionPvcRetention`);
for (const [index, prefix] of sessionPvcPrefixes.entries()) {
if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9-])?$/u.test(prefix)) throw new Error(`${path}.sessionPvcRetention.prefixes[${index}] must be a lowercase Kubernetes PVC name prefix`);
}
return {
maxRunners: positiveIntegerField(input, "maxRunners", path),
cleanupOrder: enumField(input, "cleanupOrder", path, ["oldest-inactive-last-active-first"]),
activeHeartbeatMaxAgeMs: positiveIntegerField(input, "activeHeartbeatMaxAgeMs", path),
stalePendingMaxAgeMs: positiveIntegerField(input, "stalePendingMaxAgeMs", path),
selectors: {
matchLabels: kubernetesLabelRecordField(recordField(selectors, "matchLabels", `${path}.selectors`), `${path}.selectors.matchLabels`),
jobNamePrefixes: stringArrayField(selectors, "jobNamePrefixes", `${path}.selectors`).map((prefix, index) => {
validateKubernetesNamePrefix(prefix, `${path}.selectors.jobNamePrefixes[${index}]`);
return prefix;
}),
},
ageBasedCleanup: {
enabled: booleanField(ageBasedCleanup, "enabled", `${path}.ageBasedCleanup`),
maxAgeHours: optionalPositiveIntegerField(ageBasedCleanup, "maxAgeHours", `${path}.ageBasedCleanup`) ?? null,
},
sessionPvcRetention: {
enabled: sessionPvcRetention.enabled === undefined ? false : booleanField(sessionPvcRetention, "enabled", `${path}.sessionPvcRetention`),
prefixes: sessionPvcPrefixes,
maxDeletePerRun: optionalPositiveIntegerField(sessionPvcRetention, "maxDeletePerRun", `${path}.sessionPvcRetention`) ?? 100,
},
};
}
function parseRunnerBackendRetry(input: Record<string, unknown>, path: string): AgentRunLaneSpec["deployment"]["runner"]["backendRetry"] {
return {
maxAttempts: positiveIntegerField(input, "maxAttempts", path),
initialBackoffMs: positiveIntegerField(input, "initialBackoffMs", path),
maxBackoffMs: positiveIntegerField(input, "maxBackoffMs", path),
};
}
function positiveIntegerField(input: Record<string, unknown>, key: string, path: string): number {
const value = integerField(input, key, path);
if (value <= 0) throw new Error(`${path}.${key} must be a positive integer`);
return value;
}
function optionalPositiveIntegerField(input: Record<string, unknown>, key: string, path: string): number | undefined {
const value = optionalIntegerField(input, key, path);
if (value === undefined) return undefined;
if (value <= 0) throw new Error(`${path}.${key} must be a positive integer when set`);
return value;
}
function parseLocalPostgres(input: Record<string, unknown>, path: string): AgentRunLaneSpec["deployment"]["localPostgres"] {
const enabled = booleanField(input, "enabled", path);
if (!enabled) {
return {
enabled: false,
serviceName: optionalStringField(input, "serviceName", path) ?? null,
image: optionalStringField(input, "image", path) ?? null,
storage: optionalStringField(input, "storage", path) ?? null,
port: optionalIntegerField(input, "port", path) ?? null,
database: optionalStringField(input, "database", path) ?? null,
user: optionalStringField(input, "user", path) ?? null,
passwordSourceRef: optionalStringField(input, "passwordSourceRef", path) ?? null,
passwordSourceKey: optionalStringField(input, "passwordSourceKey", path) ?? null,
};
}
return {
enabled: true,
serviceName: stringField(input, "serviceName", path),
image: stringField(input, "image", path),
storage: stringField(input, "storage", path),
port: integerField(input, "port", path),
database: stringField(input, "database", path),
user: stringField(input, "user", path),
passwordSourceRef: secretSourceRefField(input, "passwordSourceRef", path),
passwordSourceKey: stringField(input, "passwordSourceKey", path),
};
}
function parseContainerResources(input: Record<string, unknown>, path: string): AgentRunContainerResources {
const requests = recordField(input, "requests", path);
const limits = recordField(input, "limits", path);
return {
requests: {
cpu: stringField(requests, "cpu", `${path}.requests`),
memory: stringField(requests, "memory", `${path}.requests`),
},
limits: {
cpu: stringField(limits, "cpu", `${path}.limits`),
memory: stringField(limits, "memory", `${path}.limits`),
},
};
}
function parseImageBuild(input: Record<string, unknown>, path: string): AgentRunImageBuildSpec {
const buildContainerProxy = recordField(input, "buildContainerProxy", path);
return {
context: relativePathField(input, "context", path),
containerfile: relativePathField(input, "containerfile", path),
repository: stringField(input, "repository", path),
network: stringField(input, "network", path),
buildArgs: stringRecordField(recordField(input, "buildArgs", path), `${path}.buildArgs`),
httpProxy: optionalStringField(input, "httpProxy", path) ?? null,
httpsProxy: optionalStringField(input, "httpsProxy", path) ?? null,
noProxy: stringArrayField(input, "noProxy", path),
buildContainerProxy: {
httpProxy: optionalStringField(buildContainerProxy, "httpProxy", `${path}.buildContainerProxy`) ?? null,
httpsProxy: optionalStringField(buildContainerProxy, "httpsProxy", `${path}.buildContainerProxy`) ?? null,
noProxy: stringArrayField(buildContainerProxy, "noProxy", `${path}.buildContainerProxy`),
},
envIdentityFiles: stringArrayField(input, "envIdentityFiles", path).map((item, index) => {
if (item.startsWith("/") || item.includes("..")) throw new Error(`${path}.envIdentityFiles[${index}] must be a relative path without ..`);
return item;
}),
timeoutSeconds: integerField(input, "timeoutSeconds", path),
pollSeconds: integerField(input, "pollSeconds", path),
};
}
function parseLaneSecret(input: Record<string, unknown>, path: string): AgentRunLaneSecretSpec {
const sourceMode = optionalStringField(input, "sourceMode", path) ?? "env";
if (sourceMode !== "env" && sourceMode !== "file" && sourceMode !== "codex-config" && sourceMode !== "json") throw new Error(`${path}.sourceMode must be env, file, codex-config, or json`);
const sourceKey = optionalStringField(input, "sourceKey", path) ?? null;
if (sourceMode === "env" && sourceKey === null) throw new Error(`${path}.sourceKey is required when sourceMode is env`);
if (sourceMode !== "env" && sourceKey !== null) throw new Error(`${path}.sourceKey is only supported when sourceMode is env`);
const sourceFormat = optionalStringField(input, "sourceFormat", path) ?? null;
if (sourceFormat !== null && sourceFormat !== "env" && sourceFormat !== "raw-token") throw new Error(`${path}.sourceFormat must be env or raw-token`);
if (sourceFormat === "raw-token" && sourceMode !== "env") throw new Error(`${path}.sourceFormat raw-token requires sourceMode env`);
if (sourceMode !== "env" && sourceFormat !== null) throw new Error(`${path}.sourceFormat is only supported when sourceMode is env`);
const transform = optionalStringField(input, "transform", path) ?? null;
if (transform !== null && transform !== "codex-auth-json-openai-api-key") throw new Error(`${path}.transform must be codex-auth-json-openai-api-key when set`);
if (transform !== null && sourceMode !== "env") throw new Error(`${path}.transform ${transform} requires sourceMode env`);
const jsonValue = sourceMode === "json" ? jsonValueField(input, "jsonValue", path) : null;
const providerCredential = parseProviderCredentialBinding(input, `${path}.providerCredential`);
return {
id: stringField(input, "id", path),
sourceRef: secretSourceRefField(input, "sourceRef", path),
sourceMode,
sourceKey,
sourceFormat,
transform,
codexConfig: sourceMode === "codex-config" ? parseCodexConfig(recordField(input, "codexConfig", path), `${path}.codexConfig`) : null,
jsonValue,
targetRef: parseNamespacedSecretRef(recordField(input, "targetRef", path), `${path}.targetRef`),
providerCredentialProfile: providerCredential,
};
}
function parseLaneSecrets(input: Record<string, unknown>, path: string): AgentRunLaneSecretSpec[] {
const secrets = arrayField(input, "secrets", path).map((secret, index) => parseLaneSecret(secret, `${path}.secrets[${index}]`));
const idPaths = new Map<string, string>();
const targetRefPaths = new Map<string, string>();
for (const [index, secret] of secrets.entries()) {
const secretPath = `${path}.secrets[${index}]`;
const previousIdPath = idPaths.get(secret.id);
if (previousIdPath !== undefined) {
throw new Error(`${secretPath}.id duplicates ${previousIdPath}.id (${secret.id})`);
}
idPaths.set(secret.id, secretPath);
const targetRef = `${secret.targetRef.namespace}/${secret.targetRef.name}/${secret.targetRef.key}`;
const previousTargetRefPath = targetRefPaths.get(targetRef);
if (previousTargetRefPath !== undefined) {
throw new Error(`${secretPath}.targetRef duplicates ${previousTargetRefPath}.targetRef (${targetRef})`);
}
targetRefPaths.set(targetRef, secretPath);
}
return secrets;
}
function parseCodexConfig(input: Record<string, unknown>, path: string): AgentRunCodexConfigSpec {
const providersRaw = recordField(input, "modelProviders", path);
const modelProviders = Object.entries(providersRaw).map(([id, raw]) => {
validateSimpleId(id, `${path}.modelProviders`);
const provider = asRecord(raw, `${path}.modelProviders.${id}`);
return {
id,
name: stringField(provider, "name", `${path}.modelProviders.${id}`),
baseUrl: stringField(provider, "baseUrl", `${path}.modelProviders.${id}`),
wireApi: stringField(provider, "wireApi", `${path}.modelProviders.${id}`),
requiresOpenaiAuth: optionalBooleanField(provider, "requiresOpenaiAuth", `${path}.modelProviders.${id}`),
};
});
if (modelProviders.length === 0) throw new Error(`${path}.modelProviders must not be empty`);
const features = booleanRecord(input.features, `${path}.features`);
const projectsRaw = input.projects === undefined || input.projects === null ? {} : asRecord(input.projects, `${path}.projects`);
const projects = Object.entries(projectsRaw).map(([projectPath, raw]) => {
const project = asRecord(raw, `${path}.projects.${projectPath}`);
return { path: projectPath, trustLevel: stringField(project, "trustLevel", `${path}.projects.${projectPath}`) };
});
const tuiModelAvailabilityNux = integerRecord(input.tuiModelAvailabilityNux, `${path}.tuiModelAvailabilityNux`);
return {
modelProvider: stringField(input, "modelProvider", path),
model: stringField(input, "model", path),
reviewModel: optionalStringField(input, "reviewModel", path) ?? null,
modelReasoningEffort: optionalStringField(input, "modelReasoningEffort", path) ?? null,
disableResponseStorage: optionalBooleanField(input, "disableResponseStorage", path),
networkAccess: optionalStringField(input, "networkAccess", path) ?? null,
windowsWslSetupAcknowledged: optionalBooleanField(input, "windowsWslSetupAcknowledged", path),
serviceTier: optionalStringField(input, "serviceTier", path) ?? null,
modelContextWindow: optionalIntegerField(input, "modelContextWindow", path) ?? null,
modelAutoCompactTokenLimit: optionalIntegerField(input, "modelAutoCompactTokenLimit", path) ?? null,
modelCatalogJson: optionalStringField(input, "modelCatalogJson", path) ?? null,
approvalsReviewer: optionalStringField(input, "approvalsReviewer", path) ?? null,
noticeHideFullAccessWarning: optionalBooleanField(input, "noticeHideFullAccessWarning", path),
modelProviders,
features,
projects,
tuiModelAvailabilityNux,
};
}
function jsonValueField(obj: Record<string, unknown>, key: string, path: string): unknown {
if (!(key in obj)) throw new Error(`${path}.${key} is required when sourceMode is json`);
const value = obj[key];
try {
if (JSON.stringify(value) === undefined) throw new Error("not-json");
} catch {
throw new Error(`${path}.${key} must be JSON-serializable`);
}
return value;
}
function optionalBooleanField(obj: Record<string, unknown>, key: string, path: string): boolean | null {
const value = obj[key];
if (value === undefined || value === null) return null;
if (typeof value !== "boolean") throw new Error(`${path}.${key} must be a boolean when set`);
return value;
}
function booleanRecord(value: unknown, path: string): Readonly<Record<string, boolean>> {
if (value === undefined || value === null) return {};
const raw = asRecord(value, path);
const result: Record<string, boolean> = {};
for (const [key, item] of Object.entries(raw)) {
validateSimpleId(key, path);
if (typeof item !== "boolean") throw new Error(`${path}.${key} must be a boolean`);
result[key] = item;
}
return result;
}
function integerRecord(value: unknown, path: string): Readonly<Record<string, number>> {
if (value === undefined || value === null) return {};
const raw = asRecord(value, path);
const result: Record<string, number> = {};
for (const [key, item] of Object.entries(raw)) {
if (!Number.isInteger(item)) throw new Error(`${path}.${key} must be an integer`);
result[key] = item;
}
return result;
}
function parseProviderCredentialBinding(input: Record<string, unknown>, path: string): string | null {
const value = input.providerCredential;
if (value === undefined || value === null) return null;
const record = asRecord(value, path);
const profile = stringField(record, "profile", path);
validateSimpleId(profile, path);
return profile;
}
function parseGitMirrorRepositories(input: readonly Record<string, unknown>[], path: string): readonly AgentRunGitMirrorRepositorySpec[] {
if (input.length === 0) throw new Error(`${path} must declare at least one repository`);
const repositories = input.map((item, index) => parseGitMirrorRepository(item, `${path}[${index}]`));
const keys = new Set<string>();
const names = new Set<string>();
const remotes = new Set<string>();
for (const item of repositories) {
if (keys.has(item.key)) throw new Error(`${path} contains duplicate key ${item.key}`);
if (names.has(item.repository)) throw new Error(`${path} contains duplicate repository ${item.repository}`);
if (remotes.has(item.remote)) throw new Error(`${path} contains duplicate remote ownership for ${item.remote}`);
keys.add(item.key);
names.add(item.repository);
remotes.add(item.remote);
}
return repositories;
}
function parseGitMirrorRepository(input: Record<string, unknown>, path: string): AgentRunGitMirrorRepositorySpec {
const key = stringField(input, "key", path);
const repository = stringField(input, "repository", path);
const remote = stringField(input, "remote", path);
const sourceBranch = stringField(input, "sourceBranch", path);
const gitopsBranch = optionalStringField(input, "gitopsBranch", path);
validateSimpleId(key, `${path}.key`);
validateRepositoryPath(repository, `${path}.repository`);
validateRepositoryRemote(remote, repository, `${path}.remote`);
validateGitRefName(sourceBranch, `${path}.sourceBranch`);
if (gitopsBranch !== undefined) validateGitRefName(gitopsBranch, `${path}.gitopsBranch`);
return {
key,
repository,
remote,
sourceBranch,
...(gitopsBranch === undefined ? {} : { gitopsBranch }),
};
}
function parseManagedRepositoryReconciler(input: Record<string, unknown>, path: string): AgentRunManagedRepositoryReconcilerSpec {
const retry = recordField(input, "retry", path);
const freshness = recordField(input, "freshness", path);
const readiness = recordField(input, "readiness", path);
const status = recordField(input, "status", path);
const lifecycle = recordField(input, "lifecycle", path);
const stateDirectory = relativePathField(input, "stateDirectory", path);
const hostNetwork = booleanField(input, "hostNetwork", path);
const dnsPolicy = enumField(input, "dnsPolicy", path, ["ClusterFirst", "ClusterFirstWithHostNet", "Default"]);
if (stateDirectory === "." || stateDirectory.length === 0) throw new Error(`${path}.stateDirectory must name a dedicated relative directory`);
if (hostNetwork && dnsPolicy !== "ClusterFirstWithHostNet") {
throw new Error(`${path}.dnsPolicy must be ClusterFirstWithHostNet when ${path}.hostNetwork is true`);
}
if (!hostNetwork && dnsPolicy === "ClusterFirstWithHostNet") {
throw new Error(`${path}.dnsPolicy must not be ClusterFirstWithHostNet when ${path}.hostNetwork is false`);
}
return {
enabled: booleanField(input, "enabled", path),
deploymentName: kubernetesNameField(input, "deploymentName", path),
configMapName: kubernetesNameField(input, "configMapName", path),
serviceAccountName: kubernetesNameField(input, "serviceAccountName", path),
remoteAuth: parseManagedRepositoryRemoteAuth(recordField(input, "remoteAuth", path), `${path}.remoteAuth`),
hostNetwork,
dnsPolicy,
imagePullPolicy: enumField(input, "imagePullPolicy", path, ["Always", "IfNotPresent", "Never"]),
cacheMountPath: absolutePathField(input, "cacheMountPath", path),
stateDirectory,
reconcileIntervalMs: positiveIntegerField(input, "reconcileIntervalMs", path),
fetchTimeoutMs: positiveIntegerField(input, "fetchTimeoutMs", path),
shutdownGraceMs: positiveIntegerField(input, "shutdownGraceMs", path),
maxConcurrentRepositories: positiveIntegerField(input, "maxConcurrentRepositories", path),
retry: {
maxAttempts: positiveIntegerField(retry, "maxAttempts", `${path}.retry`),
initialDelayMs: positiveIntegerField(retry, "initialDelayMs", `${path}.retry`),
maxDelayMs: positiveIntegerField(retry, "maxDelayMs", `${path}.retry`),
},
freshness: {
maxAgeMs: positiveIntegerField(freshness, "maxAgeMs", `${path}.freshness`),
},
readiness: {
initialDelaySeconds: positiveIntegerField(readiness, "initialDelaySeconds", `${path}.readiness`),
periodSeconds: positiveIntegerField(readiness, "periodSeconds", `${path}.readiness`),
timeoutSeconds: positiveIntegerField(readiness, "timeoutSeconds", `${path}.readiness`),
failureThreshold: positiveIntegerField(readiness, "failureThreshold", `${path}.readiness`),
},
status: {
defaultRepositoryLimit: positiveIntegerField(status, "defaultRepositoryLimit", `${path}.status`),
},
lifecycle: {
undeclaredRepositoryPolicy: enumField(lifecycle, "undeclaredRepositoryPolicy", `${path}.lifecycle`, ["retain"]),
managedRefs: enumField(lifecycle, "managedRefs", `${path}.lifecycle`, ["source-branch-only"]),
},
resources: parseContainerResources(recordField(input, "resources", path), `${path}.resources`),
};
}
function parseManagedRepositoryRemoteAuth(input: Record<string, unknown>, path: string): AgentRunManagedRepositoryReconcilerSpec["remoteAuth"] {
const configRef = stringField(input, "configRef", path);
const resolved = resolveConfigRef(configRef, `${path}.configRef`);
const capability = parseAgentRunGitFetchCredentialCapability(resolved.value, configRef);
return {
configRef,
capabilitySha256: resolved.sha256,
...capability,
};
}
export function parseAgentRunGitFetchCredentialCapability(value: unknown, path = "gitFetchCredential"): Omit<AgentRunManagedRepositoryReconcilerSpec["remoteAuth"], "configRef" | "capabilitySha256"> {
const capability = asRecord(value, path);
const secretRef = asRecord(capability.secretRef, `${path}.secretRef`);
const apiVersion = enumField(capability, "apiVersion", path, ["unidesk.ai/v1"] as const);
const kind = enumField(capability, "kind", path, ["GitFetchCredential"] as const);
const authMode = enumField(capability, "authMode", path, ["github-https-token"] as const);
const host = stringField(capability, "host", path);
if (host !== "github.com") throw new Error(`${path}.host must be github.com for github-https-token`);
const key = stringField(secretRef, "key", `${path}.secretRef`);
if (!/^[A-Za-z0-9._-]+$/u.test(key)) throw new Error(`${path}.secretRef.key must be a Kubernetes Secret data key`);
return {
apiVersion,
kind,
authMode,
host,
secretRef: {
namespace: kubernetesNameField(secretRef, "namespace", `${path}.secretRef`),
name: kubernetesNameField(secretRef, "name", `${path}.secretRef`),
key,
},
};
}
function validateRepositoryPath(value: string, path: string): void {
if (!/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/u.test(value) || value.includes("..")) {
throw new Error(`${path} must be an owner/name repository path without ..`);
}
}
function validateRepositoryRemote(value: string, repository: string, path: string): void {
try {
const remote = new URL(value);
const expectedPath = `/${repository}.git`;
if (
remote.protocol !== "https:"
|| remote.port.length > 0
|| remote.username.length > 0
|| remote.password.length > 0
|| remote.search.length > 0
|| remote.hash.length > 0
|| remote.pathname !== expectedPath
|| value !== `https://${remote.hostname}${expectedPath}`
) {
throw new Error("unsupported remote");
}
} catch {
throw new Error(`${path} must be a canonical HTTPS URL owned by the same repository entry (${repository}) without userinfo, port, query, or fragment`);
}
}
function validateGitRefName(value: string, path: string): void {
if (
value.length === 0
|| value.startsWith("-")
|| value.startsWith("/")
|| value.endsWith("/")
|| value.endsWith(".")
|| value.includes("..")
|| value.includes("@{")
|| /[\u0000-\u0020~^:?*[\\]/u.test(value)
) {
throw new Error(`${path} must be a safe Git branch name`);
}
}
function parseDatabase(input: Record<string, unknown>, path: string): AgentRunLaneSpec["database"] {
const mode = enumField(input, "mode", path, ["local-postgres", "external-postgres"]);
const sslmode = optionalStringField(input, "sslmode", path);
if (sslmode !== undefined && sslmode !== "require") throw new Error(`${path}.sslmode must be require when set`);
return {
mode,
provider: optionalStringField(input, "provider", path) ?? null,
configRef: optionalStringField(input, "configRef", path) ?? null,
database: optionalStringField(input, "database", path) ?? null,
user: optionalStringField(input, "user", path) ?? null,
sslmode: sslmode === undefined ? null : "require",
secretSourceRef: optionalStringField(input, "secretSourceRef", path) ?? null,
secretRef: parseSecretRef(recordField(input, "secretRef", path), `${path}.secretRef`),
localPostgresExpectedAbsent: booleanField(input, "localPostgresExpectedAbsent", path),
};
}
function parseSecretRef(input: Record<string, unknown>, path: string): { name: string; key: string } {
return {
name: stringField(input, "name", path),
key: stringField(input, "key", path),
};
}
function parseNamespacedSecretRef(input: Record<string, unknown>, path: string): AgentRunSecretRef {
return {
namespace: stringField(input, "namespace", path),
name: stringField(input, "name", path),
key: stringField(input, "key", path),
};
}
function parseGithubProxy(input: Record<string, unknown>, path: string): { host: string; port: number } {
return {
host: stringField(input, "host", path),
port: integerField(input, "port", path),
};
}
function optionalEnvPair(obj: Record<string, unknown>, key: string, path: string): { name: string; value: string } | null {
const value = obj[key];
if (value === undefined || value === null) return null;
const record = asRecord(value, `${path}.${key}`);
return {
name: stringField(record, "name", `${path}.${key}`),
value: stringField(record, "value", `${path}.${key}`),
};
}
function optionalEnvRecord(obj: Record<string, unknown>, key: string, path: string): Readonly<Record<string, string>> {
const value = obj[key];
if (value === undefined || value === null) return {};
return stringRecordField(asRecord(value, `${path}.${key}`), `${path}.${key}`);
}
function optionalStringArrayField(obj: Record<string, unknown>, key: string, path: string): readonly string[] {
const value = obj[key];
if (value === undefined || value === null) return [];
return stringArrayField(obj, key, path);
}
function stringRecordField(obj: Record<string, unknown>, path: string): Readonly<Record<string, string>> {
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(obj)) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) throw new Error(`${path}.${key} must be a valid build arg name`);
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path}.${key} must be a non-empty string`);
result[key] = value.trim();
}
return result;
}
function kubernetesLabelRecordField(obj: Record<string, unknown>, path: string): Readonly<Record<string, string>> {
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(obj)) {
validateKubernetesLabelKey(key, `${path}.${key}`);
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path}.${key} must be a non-empty string`);
const trimmed = value.trim();
validateKubernetesLabelValue(trimmed, `${path}.${key}`);
result[key] = trimmed;
}
if (Object.keys(result).length === 0) throw new Error(`${path} must declare at least one label`);
return result;
}
function enumField<T extends string>(obj: Record<string, unknown>, key: string, path: string, values: readonly T[]): T {
const value = stringField(obj, key, path);
if (!values.includes(value as T)) throw new Error(`${path}.${key} must be one of ${values.join(", ")}`);
return value as T;
}
function absolutePathField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (!value.startsWith("/") || value.includes("..")) throw new Error(`${path}.${key} must be an absolute path without ..`);
return value;
}
function optionalAbsolutePathField(obj: Record<string, unknown>, key: string, path: string): string | undefined {
const value = obj[key];
if (value === undefined || value === null) return undefined;
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path}.${key} must be a non-empty string when set`);
const trimmed = value.trim();
if (!trimmed.startsWith("/") || trimmed.includes("..")) throw new Error(`${path}.${key} must be an absolute path without ..`);
return trimmed;
}
function relativePathField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (value.startsWith("/") || value.includes("..")) throw new Error(`${path}.${key} must be a relative path without ..`);
return value;
}
function secretSourceRefField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (value.includes("..")) throw new Error(`${path}.${key} must not contain ..`);
if (!value.startsWith("/") && value.startsWith(".")) throw new Error(`${path}.${key} must be absolute or relative without a leading dot`);
return value;
}
function urlField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
try {
const parsed = new URL(value);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("unsupported protocol");
} catch {
throw new Error(`${path}.${key} must be an http(s) URL`);
}
return value;
}
function validateSimpleId(value: string, path: string): void {
if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${path}.${value} must use a simple id`);
}
function validateKubernetesNamePrefix(value: string, path: string): void {
if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u.test(value)) throw new Error(`${path} must be a Kubernetes name prefix`);
}
function kubernetesNameField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
validateKubernetesNamePrefix(value, `${path}.${key}`);
if (value.length > 63) throw new Error(`${path}.${key} must be at most 63 characters`);
return value;
}
function validateKubernetesLabelKey(value: string, path: string): void {
const parts = value.split("/");
const name = parts.length === 2 ? parts[1] : parts[0];
if (parts.length > 2 || !name || !/^[A-Za-z0-9]([A-Za-z0-9_.-]{0,61}[A-Za-z0-9])?$/u.test(name)) throw new Error(`${path} must be a Kubernetes label key`);
}
function validateKubernetesLabelValue(value: string, path: string): void {
if (!/^[A-Za-z0-9]([A-Za-z0-9_.-]{0,61}[A-Za-z0-9])?$/u.test(value)) throw new Error(`${path} must be a Kubernetes label value`);
}