diff --git a/scripts/src/agentrun-lanes.ts b/scripts/src/agentrun-lanes.ts index f931eb2c..92d64f76 100644 --- a/scripts/src/agentrun-lanes.ts +++ b/scripts/src/agentrun-lanes.ts @@ -1,5 +1,16 @@ -import { readFileSync } from "node:fs"; import { rootPath } from "./config"; +import { + asRecord, + arrayField, + booleanField, + integerField, + optionalIntegerField, + optionalStringField, + readYamlRecord, + recordField, + stringArrayField, + stringField, +} from "./platform-infra-ops-library"; export const AGENTRUN_CONFIG_PATH = "config/agentrun.yaml"; @@ -355,7 +366,7 @@ export function agentRunPipelineRunName(spec: AgentRunLaneSpec, sourceCommit: st function readAgentRunControlPlaneConfig(env: NodeJS.ProcessEnv): AgentRunControlPlaneConfig { const configPath = env.AGENTRUN_CONTROL_PLANE_CONFIG ?? rootPath(AGENTRUN_CONFIG_PATH); - const root = asRecord(Bun.YAML.parse(readFileSync(configPath, "utf8")) as unknown, configPath); + const root = readYamlRecord>(configPath); validateConfigEnvelope(root, configPath); const controlPlane = recordField(root, "controlPlane", configPath); const defaultTarget = recordField(controlPlane, "default", `${configPath}.controlPlane`); @@ -648,62 +659,6 @@ function optionalEnvPair(obj: Record, key: string, path: string }; } -function asRecord(value: unknown, path: string): Record { - if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be a YAML object`); - return value as Record; -} - -function recordField(obj: Record, key: string, path: string): Record { - return asRecord(obj[key], `${path}.${key}`); -} - -function stringField(obj: Record, key: string, path: string): string { - const value = obj[key]; - if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path}.${key} must be a non-empty string`); - return value.trim(); -} - -function optionalStringField(obj: Record, 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`); - return value.trim(); -} - -function booleanField(obj: Record, key: string, path: string): boolean { - const value = obj[key]; - if (typeof value !== "boolean") throw new Error(`${path}.${key} must be a boolean`); - return value; -} - -function integerField(obj: Record, key: string, path: string): number { - const value = obj[key]; - if (!Number.isInteger(value)) throw new Error(`${path}.${key} must be an integer`); - return Number(value); -} - -function optionalIntegerField(obj: Record, key: string, path: string): number | undefined { - const value = obj[key]; - if (value === undefined || value === null) return undefined; - if (!Number.isInteger(value)) throw new Error(`${path}.${key} must be an integer when set`); - return Number(value); -} - -function arrayField(obj: Record, key: string, path: string): Record[] { - const value = obj[key]; - if (!Array.isArray(value)) throw new Error(`${path}.${key} must be a YAML array`); - return value.map((item, index) => asRecord(item, `${path}.${key}[${index}]`)); -} - -function stringArrayField(obj: Record, key: string, path: string): string[] { - const value = obj[key]; - if (!Array.isArray(value)) throw new Error(`${path}.${key} must be a YAML array`); - return value.map((item, index) => { - if (typeof item !== "string" || item.trim().length === 0) throw new Error(`${path}.${key}[${index}] must be a non-empty string`); - return item.trim(); - }); -} - function stringRecordField(obj: Record, path: string): Readonly> { const result: Record = {}; for (const [key, value] of Object.entries(obj)) { diff --git a/scripts/src/agentrun.ts b/scripts/src/agentrun.ts index 79bd8392..2e7f584f 100644 --- a/scripts/src/agentrun.ts +++ b/scripts/src/agentrun.ts @@ -1,4 +1,3 @@ -import { createHash } from "node:crypto"; import { chmodSync, copyFileSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { spawnSync } from "node:child_process"; @@ -25,6 +24,7 @@ import { renderAgentRunGitopsFiles, type AgentRunArtifactService, } from "./agentrun-manifests"; +import { sha256Fingerprint } from "./platform-infra-ops-library"; export function agentRunHelp(): unknown { return { @@ -3669,7 +3669,7 @@ function readSecretSourceValue(spec: AgentRunLaneSpec, source: LaneSecretSource) redactedPath: sourceRef.startsWith("/") ? redactAbsoluteSecretPath(sourceRef) : `.state/secrets/${sourceRef}`, value, valueBytes: Buffer.byteLength(value, "utf8"), - fingerprint: `sha256:${createHash("sha256").update(value).digest("hex")}`, + fingerprint: sha256Fingerprint(value), }; } diff --git a/scripts/src/platform-infra-ops-library.ts b/scripts/src/platform-infra-ops-library.ts index 2cea0822..9aa738c1 100644 --- a/scripts/src/platform-infra-ops-library.ts +++ b/scripts/src/platform-infra-ops-library.ts @@ -72,6 +72,18 @@ export function fingerprintValues(values: Record, keys: string[] return `sha256:${hash.digest("hex")}`; } +export function sha256Hex(value: string): string { + return createHash("sha256").update(value).digest("hex"); +} + +export function sha256Fingerprint(value: string): string { + return `sha256:${sha256Hex(value)}`; +} + +export function shortSha256Fingerprint(value: string, chars = 12): string { + return sha256Hex(value).slice(0, chars); +} + export function fingerprintEnvValues(values: Record, keys: string[]): string { const material = keys .slice() @@ -178,14 +190,14 @@ export function recordField(obj: Record, key: string, path: str export function stringField(obj: Record, key: string, path: string): string { const value = obj[key]; if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path}.${key} must be a non-empty string`); - return value; + return value.trim(); } export function optionalStringField(obj: Record, 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`); - return value; + return value.trim(); } export function booleanField(obj: Record, key: string, path: string): boolean { @@ -200,6 +212,19 @@ export function numberField(obj: Record, key: string, path: str return value; } +export function integerField(obj: Record, key: string, path: string): number { + const value = obj[key]; + if (!Number.isInteger(value)) throw new Error(`${path}.${key} must be an integer`); + return Number(value); +} + +export function optionalIntegerField(obj: Record, key: string, path: string): number | undefined { + const value = obj[key]; + if (value === undefined || value === null) return undefined; + if (!Number.isInteger(value)) throw new Error(`${path}.${key} must be an integer when set`); + return Number(value); +} + export function arrayField(obj: Record, key: string, path: string): Record[] { const value = obj[key]; if (!Array.isArray(value)) throw new Error(`${path}.${key} must be an array`); @@ -212,6 +237,15 @@ export function stringListField(obj: Record, key: string, path: return value as string[]; } +export function stringArrayField(obj: Record, key: string, path: string): string[] { + const value = obj[key]; + if (!Array.isArray(value)) throw new Error(`${path}.${key} must be an array`); + return value.map((item, index) => { + if (typeof item !== "string" || item.trim().length === 0) throw new Error(`${path}.${key}[${index}] must be a non-empty string`); + return item.trim(); + }); +} + export function yamlFieldLabel(configLabel: string, path: string, key: string): string { const prefix = path.length > 0 ? `${path}.` : ""; return `${configLabel}.${prefix}${key}`; diff --git a/scripts/src/platform-infra-sub2api-codex.ts b/scripts/src/platform-infra-sub2api-codex.ts index a874207c..4a2fe034 100644 --- a/scripts/src/platform-infra-sub2api-codex.ts +++ b/scripts/src/platform-infra-sub2api-codex.ts @@ -1,4 +1,3 @@ -import { createHash } from "node:crypto"; import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; @@ -6,6 +5,7 @@ import type { UniDeskConfig } from "./config"; import { rootPath } from "./config"; import type { RenderedCliResult } from "./output"; import { applyPk01CaddyBlock, prepareFrpcSecret, renderFrpcManifest, type PublicServiceExposure, type PublicServiceTarget } from "./platform-infra-public-service"; +import { shortSha256Fingerprint } from "./platform-infra-ops-library"; import { codexPoolSentinelSummary, codexPoolSentinelRuntimeImage, @@ -3793,7 +3793,7 @@ function isRecord(value: unknown): value is Record { } function fingerprint(value: string): string { - return createHash("sha256").update(value).digest("hex").slice(0, 12); + return shortSha256Fingerprint(value); } export function codexPoolSentinelProbeConfigFingerprint(input: {