// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. config module for scripts/src/agentrun.ts. // Moved mechanically from scripts/src/agentrun.ts:6916-7650 for #903. // SPEC: PJ2026-01020108 cancel lifecycle + PJ2026-01020205 AipodSpec binding + PJ2026-01020302 session policy + PJ2026-01020305 cancel control + PJ2026-01060305/06 YAML execution policy and bounded output draft-2026-06-25-p0. // Exposes AgentRun lane-scoped policy, AipodSpec SecretRef binding, cancel lifecycle, and bounded default output in the UniDesk CLI. import { chmodSync, copyFileSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { spawnSync } from "node:child_process"; import { rootPath, type UniDeskConfig } from "../config"; import type { RenderedCliResult } from "../output"; import { applyLocalCaddyManagedSite } from "../pk01-caddy"; import { runSshCommandCapture, type SshCaptureResult } from "../ssh"; import { runRemoteSshCommandCapture } from "../remote"; import { startJob } from "../jobs"; import { AGENTRUN_CONFIG_PATH, agentRunLaneSummary, agentRunPipelineRunName, agentRunProviderCredentialRefs, resolveAgentRunLaneTarget, type AgentRunCancelLifecycleSpec, type AgentRunLaneSpec, } from "../agentrun-lanes"; import { agentRunImageArtifact, placeholderAgentRunImage, renderedFilesDigest, renderedObjectsDigest, renderAgentRunControlPlaneManifests, renderAgentRunGitopsFiles, type AgentRunArtifactService, } from "../agentrun-manifests"; import { sha256Fingerprint } from "../platform-infra-ops-library"; import type { AgentRunFailureKind, AgentRunHttpMethod, AgentRunResolvedAuth, AgentRunRestTarget, AgentRunRestTargetOptions, AgentRunSessionPolicyTarget } from "./utils"; import { isRecord, pickCompact } from "./options"; import { pathValue } from "./render"; import { AgentRunRestError, activeAgentRunRestTarget } from "./rest-bridge"; import { record, stringOrNull } from "./utils"; export function readAgentRunClientConfig(env: NodeJS.ProcessEnv = process.env): AgentRunClientConfig { const configPath = env.AGENTRUN_CLIENT_CONFIG ?? rootPath("config", "agentrun.yaml"); if (!existsSync(configPath)) throw new AgentRunRestError("validation-failed", `AgentRun client config not found: ${configPath}`); try { const parsed = parseAgentRunClientConfigYaml(readFileSync(configPath, "utf8"), configPath); return { ...parsed, sourcePath: configPath }; } catch (error) { if (error instanceof AgentRunRestError) throw error; throw new AgentRunRestError("validation-failed", `AgentRun client config invalid: ${error instanceof Error ? error.message : String(error)}`, { bridge: { mode: "direct-http", clientRole: "render-only", configPath }, }); } } export function parseAgentRunClientConfigYaml(raw: string, sourcePath = "config/agentrun.yaml"): AgentRunClientConfig { const input = record(Bun.YAML.parse(raw) as unknown); validateAgentRunConfigEnvelope(input, sourcePath); const manager = record(input.manager); const auth = record(input.auth); const client = record(input.client); const baseUrl = stringFieldFromRecord(manager, "baseUrl", "manager"); try { const parsed = new URL(baseUrl); if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("invalid protocol"); } catch { throw new Error(`${sourcePath}: manager.baseUrl must be an http(s) URL`); } const timeoutMs = numberFieldFromRecord(manager, "timeoutMs", "manager", { min: 1, max: 60_000 }); const header = stringFieldFromRecord(auth, "header", "auth"); const scheme = stringFieldFromRecord(auth, "scheme", "auth"); if (header.toLowerCase() !== "authorization") throw new Error(`${sourcePath}: auth.header must be Authorization`); if (scheme !== "Bearer") throw new Error(`${sourcePath}: auth.scheme must be Bearer`); const role = stringFieldFromRecord(client, "role", "client"); const transport = stringFieldFromRecord(client, "transport", "client"); if (role !== "render-only") throw new Error(`${sourcePath}: client.role must be render-only`); if (transport !== "direct-http") throw new Error(`${sourcePath}: client.transport must be direct-http`); const sessionPolicy = readAgentRunSessionPolicyConfig(client, sourcePath); const publicExposure = readAgentRunPublicExposureConfig(input.publicExposure, baseUrl.replace(/\/+$/u, "/"), sourcePath); return { sourcePath, manager: { baseUrl: baseUrl.replace(/\/+$/u, "/"), timeoutMs, }, auth: { env: stringFieldFromRecord(auth, "env", "auth"), file: stringFieldFromRecord(auth, "file", "auth"), header, scheme, }, client: { role, transport, sessionPolicy, }, publicExposure, }; } export function readAgentRunSessionPolicyConfig(client: Record, sourcePath: string): AgentRunSessionPolicyConfig { const pathValue = "client.sessionPolicy"; const sessionPolicy = record(client.sessionPolicy); const workspaceRef = record(sessionPolicy.workspaceRef); const executionPolicy = record(sessionPolicy.executionPolicy); const secretScope = record(executionPolicy.secretScope); stringFieldFromRecord(workspaceRef, "kind", `${pathValue}.workspaceRef`); stringFieldFromRecord(executionPolicy, "sandbox", `${pathValue}.executionPolicy`); stringFieldFromRecord(executionPolicy, "approval", `${pathValue}.executionPolicy`); positiveIntegerFieldFromRecord(executionPolicy, "timeoutMs", `${pathValue}.executionPolicy`); stringFieldFromRecord(executionPolicy, "network", `${pathValue}.executionPolicy`); booleanFieldFromRecord(secretScope, "allowCredentialEcho", `${pathValue}.executionPolicy.secretScope`); return { sourcePath, tenantId: stringFieldFromRecord(sessionPolicy, "tenantId", pathValue), projectId: stringFieldFromRecord(sessionPolicy, "projectId", pathValue), providerId: stringFieldFromRecord(sessionPolicy, "providerId", pathValue), backendProfile: stringFieldFromRecord(sessionPolicy, "backendProfile", pathValue), workspaceRef: cloneJsonRecord(workspaceRef), executionPolicy: cloneJsonRecord(executionPolicy), }; } export function validateAgentRunConfigEnvelope(input: Record, sourcePath: string): void { if (input.version !== 1) throw new Error(`${sourcePath}: version must be 1`); if (input.kind !== "AgentRunConfig") throw new Error(`${sourcePath}: kind must be AgentRunConfig`); const metadata = record(input.metadata); if (stringFieldFromRecord(metadata, "name", "metadata") !== "agentrun") throw new Error(`${sourcePath}: metadata.name must be agentrun`); } export function readAgentRunPublicExposureConfig(value: unknown, managerBaseUrl: string, sourcePath: string): AgentRunPublicExposure | null { if (value === undefined || value === null) return null; const exposure = record(value); if (exposure.enabled !== true) return null; const proxyName = stringFieldFromRecord(exposure, "proxyName", "publicExposure"); if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/u.test(proxyName)) throw new Error(`${sourcePath}: publicExposure.proxyName must be a DNS label`); const remotePort = numberFieldFromRecord(exposure, "remotePort", "publicExposure", { min: 1, max: 65_535 }); const publicBaseUrl = normalizedHttpUrl(stringFieldFromRecord(exposure, "publicBaseUrl", "publicExposure"), `${sourcePath}: publicExposure.publicBaseUrl`); const masterBaseUrl = normalizedHttpUrl(stringFieldFromRecord(exposure, "masterBaseUrl", "publicExposure"), `${sourcePath}: publicExposure.masterBaseUrl`); if (!publicBaseUrl.startsWith("https://")) throw new Error(`${sourcePath}: publicExposure.publicBaseUrl must use https://`); if (publicBaseUrl !== managerBaseUrl) throw new Error(`${sourcePath}: manager.baseUrl must match publicExposure.publicBaseUrl`); if (!masterBaseUrl.startsWith(`http://127.0.0.1:${remotePort}`)) throw new Error(`${sourcePath}: publicExposure.masterBaseUrl must point to http://127.0.0.1:${remotePort}`); const masterFrps = record(exposure.masterFrps); const masterCaddy = record(exposure.masterCaddy); const caddyEnabled = masterCaddy.enabled === true; const domain = stringFieldFromRecord(masterCaddy, "domain", "publicExposure.masterCaddy"); if (!/^[a-z0-9.-]+$/u.test(domain) || !domain.endsWith(".nip.io")) throw new Error(`${sourcePath}: publicExposure.masterCaddy.domain must be a nip.io hostname`); const caddyUpstreamBaseUrl = normalizedHttpUrl(stringFieldFromRecord(masterCaddy, "upstreamBaseUrl", "publicExposure.masterCaddy"), `${sourcePath}: publicExposure.masterCaddy.upstreamBaseUrl`); if (caddyUpstreamBaseUrl !== masterBaseUrl) throw new Error(`${sourcePath}: publicExposure.masterCaddy.upstreamBaseUrl must match publicExposure.masterBaseUrl`); return { enabled: true, proxyName, remotePort, publicBaseUrl, masterBaseUrl, masterFrps: { configPath: absolutePathField(masterFrps, "configPath", "publicExposure.masterFrps", sourcePath), containerName: stringFieldFromRecord(masterFrps, "containerName", "publicExposure.masterFrps"), }, masterCaddy: { enabled: caddyEnabled, domain, configPath: absolutePathField(masterCaddy, "configPath", "publicExposure.masterCaddy", sourcePath), serviceName: stringFieldFromRecord(masterCaddy, "serviceName", "publicExposure.masterCaddy"), upstreamBaseUrl: caddyUpstreamBaseUrl, responseHeaderTimeoutSeconds: numberFieldFromRecord(masterCaddy, "responseHeaderTimeoutSeconds", "publicExposure.masterCaddy", { min: 1, max: 600 }), }, }; } export function normalizedHttpUrl(value: string, label: string): string { let url: URL; try { url = new URL(value); } catch { throw new Error(`${label} must be an http(s) URL`); } if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`${label} must be an http(s) URL`); return value.replace(/\/+$/u, "/"); } export function absolutePathField(obj: Record, key: string, pathValue: string, sourcePath: string): string { const value = stringFieldFromRecord(obj, key, pathValue); if (!value.startsWith("/")) throw new Error(`${sourcePath}: ${pathValue}.${key} must be an absolute path`); return value; } export function resolveAgentRunAuth(config: AgentRunClientConfig, env: NodeJS.ProcessEnv = process.env): AgentRunResolvedAuth { const envValue = env[config.auth.env]?.trim(); if (envValue) return { source: "env", value: envValue }; if (existsSync(config.auth.file)) { const fromFile = readEnvValueFromFile(config.auth.file, config.auth.env); if (fromFile !== null) return { source: "file", value: fromFile }; } throw new AgentRunRestError("auth-missing", `${config.auth.env} is required for direct AgentRun REST access`, { bridge: { mode: "direct-http", clientRole: "render-only", configPath: config.sourcePath, auth: { env: config.auth.env, file: config.auth.file, source: "missing", header: config.auth.header, scheme: config.auth.scheme, valuesPrinted: false }, }, }); } export function readEnvValueFromFile(pathValue: string, envName: string): string | null { const text = readFileSync(pathValue, "utf8"); for (const line of text.split(/\r?\n/u)) { const trimmed = line.trim(); if (trimmed.length === 0 || trimmed.startsWith("#")) continue; const normalized = trimmed.startsWith("export ") ? trimmed.slice("export ".length).trim() : trimmed; const index = normalized.indexOf("="); if (index <= 0) continue; if (normalized.slice(0, index).trim() !== envName) continue; const raw = normalized.slice(index + 1).trim(); const value = raw.replace(/^['"]|['"]$/gu, ""); return value.length > 0 ? value : null; } return null; } export function agentRunRestBridgeMetadata(config: AgentRunClientConfig, auth: AgentRunResolvedAuth, method: AgentRunHttpMethod, pathValue: string): Record { return { mode: "direct-http", clientRole: "render-only", compatibility: "no-hwlab-runtime-proxy-no-ssh-official-cli", configPath: config.sourcePath, baseUrl: config.manager.baseUrl, request: { method, path: pathValue, timeoutMs: config.manager.timeoutMs }, auth: { env: config.auth.env, file: config.auth.file, source: auth.source, header: config.auth.header, scheme: config.auth.scheme, valuesPrinted: false }, }; } export function agentRunLaneRestBridgeMetadata(config: AgentRunClientConfig, target: AgentRunRestTarget, method: AgentRunHttpMethod, pathValue: string): Record { return { mode: "lane-k8s-service-proxy", clientRole: "render-only", configPath: config.sourcePath, laneConfigPath: target.configPath, node: target.spec.nodeId, lane: target.spec.lane, kubeRoute: target.spec.nodeKubeRoute, namespace: target.spec.runtime.namespace, managerDeployment: target.spec.runtime.managerDeployment, baseUrl: target.spec.runtime.internalBaseUrl, request: { method, path: pathValue, timeoutMs: config.manager.timeoutMs }, auth: { source: "manager-pod-env", env: config.auth.env, fallbackEnv: "AGENTRUN_API_KEY", header: config.auth.header, scheme: config.auth.scheme, valuesPrinted: false }, valuesPrinted: false, }; } export function addAgentRunNotFoundLookupHint(details: Record, config: AgentRunClientConfig, method: AgentRunHttpMethod, pathValue: string, target: AgentRunRestTarget | null): Record { const resource = agentRunResourceFromPath(pathValue); const laneConfig = readAgentRunLaneLookupConfig(config.sourcePath); const candidateLanes = laneConfig?.lanes ?? []; const nextCommands = candidateLanes .filter((lane) => typeof lane.node === "string" && lane.node.length > 0 && typeof lane.lane === "string" && lane.lane.length > 0) .slice(0, 4) .map((lane) => `bun scripts/cli.ts agentrun control-plane status --node ${lane.node} --lane ${lane.lane}`); return { ...details, laneAwareLookup: { kind: "agentrun-resource-not-found-on-current-endpoint", summary: resource === null ? "The request returned 404 on the currently configured AgentRun manager endpoint; a resource from another lane will not be visible through this baseUrl." : `${resource.kind}/${resource.id} returned 404 on the currently configured AgentRun manager endpoint; if it was created on another lane, inspect that lane before treating the resource as lost.`, request: { method, path: pathValue, ...(resource === null ? {} : { resource }), }, currentEndpoint: { transport: target === null ? "direct-http" : "lane-k8s-service-proxy", baseUrl: target === null ? config.manager.baseUrl : target.spec.runtime.internalBaseUrl, configPath: config.sourcePath, defaultNode: laneConfig?.defaultNode ?? null, defaultLane: laneConfig?.defaultLane ?? null, selectedNode: target?.spec.nodeId ?? null, selectedLane: target?.spec.lane ?? null, }, candidateLanes, nextCommands, note: target === null ? "Pass --node --lane to query a YAML-declared non-default AgentRun lane." : "The resource was not found on the selected AgentRun lane; verify the run/session id and the HWLAB runtime lane that created it.", valuesPrinted: false, }, }; } export function agentRunResourceFromPath(pathValue: string): Record | null { const run = pathValue.match(/\/runs\/([^/?#]+)/u); if (run?.[1] !== undefined) return { kind: "run", id: decodeURIComponent(run[1]) }; const session = pathValue.match(/\/sessions\/([^/?#]+)/u); if (session?.[1] !== undefined) return { kind: "session", id: decodeURIComponent(session[1]) }; const command = pathValue.match(/\/commands\/([^/?#]+)/u); if (command?.[1] !== undefined) return { kind: "command", id: decodeURIComponent(command[1]) }; const task = pathValue.match(/\/queue\/tasks\/([^/?#]+)/u); if (task?.[1] !== undefined) return { kind: "task", id: decodeURIComponent(task[1]) }; return null; } export function readAgentRunLaneLookupConfig(configPath: string): { defaultNode: string | null; defaultLane: string | null; lanes: Record[] } | null { try { const raw = readFileSync(configPath, "utf8"); const parsed = record(Bun.YAML.parse(raw) as unknown); const controlPlane = record(parsed.controlPlane); const defaultTarget = record(controlPlane.default); const lanes = record(controlPlane.lanes); return { defaultNode: stringOrNull(defaultTarget.node), defaultLane: stringOrNull(defaultTarget.lane), lanes: Object.entries(lanes).map(([laneName, rawLane]) => { const lane = record(rawLane); const runtime = record(lane.runtime); return { lane: laneName, node: stringOrNull(lane.node) ?? stringOrNull(lane.nodeId), version: stringOrNull(lane.version), namespace: stringOrNull(runtime.namespace), internalBaseUrl: stringOrNull(runtime.internalBaseUrl), isDefault: laneName === stringOrNull(defaultTarget.lane), }; }), }; } catch { return null; } } export function agentRunDryRunPlan(action: string, pathValue: string, body: Record, confirmCommand: string, method: AgentRunHttpMethod = "POST", extra: Record = {}): Record { return { ok: true, action, dryRun: true, mutation: false, transport: "direct-http", clientRole: "render-only", request: { method, path: pathValue, bodyKeys: Object.keys(body), valuesPrinted: false, }, ...extra, next: { confirm: confirmCommand }, valuesPrinted: false, }; } export function agentRunQuery(args: string[], names: string[]): string { const params = new URLSearchParams(); const keyMap: Record = { "after-seq": "afterSeq", "backend-profile": "backendProfile", "command": "commandId", "command-id": "commandId", "reader-id": "readerId", "run-id": "runId", "updated-after": "updatedAfter", }; for (const name of names) { const value = agentRunOption(args, name); if (value !== null) params.set(keyMap[name] ?? name, value); } const query = params.toString(); return query.length > 0 ? `?${query}` : ""; } export function agentRunOption(args: string[], name: string): string | null { const flag = `--${name}`; for (let index = 0; index < args.length; index += 1) { const arg = args[index] ?? ""; if (arg === flag) { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new AgentRunRestError("validation-failed", `${flag} requires a value`); return value; } if (arg.startsWith(`${flag}=`)) return arg.slice(flag.length + 1); } return null; } export function requiredAgentRunOption(args: string[], names: string[], message: string): string { for (const name of names) { const value = agentRunOption(args, name); if (value !== null) return value; } throw new AgentRunRestError("validation-failed", message); } export function agentRunHasFlag(args: string[], name: string): boolean { const flag = `--${name}`; return args.includes(flag) || args.some((arg) => arg.startsWith(`${flag}=`)); } export async function requiredJsonBody(args: string[], context: string): Promise> { const body = await optionalJsonBody(args); if (Object.keys(body).length === 0) throw new AgentRunRestError("validation-failed", `${context} requires --json-stdin or --json-file `); return body; } export async function optionalJsonBody(args: string[]): Promise> { const raw = readJsonTextFromArgs(args, "json"); if (raw === null) return {}; return record(JSON.parse(raw) as unknown); } export async function optionalRunnerJsonBody(args: string[]): Promise> { const raw = readJsonTextFromArgs(args, "runner-json"); if (raw === null) return {}; return record(JSON.parse(raw) as unknown); } export function readJsonTextFromArgs(args: string[], prefix: "json" | "runner-json"): string | null { if (agentRunHasFlag(args, `${prefix}-stdin`)) return readFileSync(0, "utf8"); const file = agentRunOption(args, `${prefix}-file`); if (file !== null) return file === "-" ? readFileSync(0, "utf8") : readFileSync(file, "utf8"); return null; } export function readYamlInputFromArgs(args: string[]): string { if (agentRunHasFlag(args, "yaml-stdin")) return readFileSync(0, "utf8"); const file = agentRunOption(args, "yaml-file"); if (file !== null) return file === "-" ? readFileSync(0, "utf8") : readFileSync(file, "utf8"); throw new AgentRunRestError("validation-failed", "aipod-spec YAML input is required; use --yaml-stdin or --yaml-file "); } export async function aipodRenderInputFromArgs(args: string[], trailingPromptStart: number, overrides: Record = {}): Promise> { const input = await optionalJsonBody(args); const prompt = optionalPromptFromArgs(args, trailingPromptStart); if (prompt !== null) input.prompt = prompt; copyAgentRunOptions(args, input, ["tenant-id", "project-id", "queue", "node", "lane", "title", "provider-id", "idempotency-key", "session-id"]); const sessionPolicy = readAgentRunClientConfig().client.sessionPolicy; const policyTarget = resolveAgentRunSessionPolicyTarget({ node: stringOrNull(input.node), lane: stringOrNull(input.lane) }); if (input.node === undefined) input.node = policyTarget.spec.nodeId; if (input.lane === undefined) input.lane = policyTarget.spec.lane; if (input.providerId === undefined) input.providerId = defaultAgentRunProviderId(sessionPolicy, policyTarget); const profile = agentRunOption(args, "profile") ?? agentRunOption(args, "backend-profile"); const backendProfile = profile ?? stringOrNull(input.backendProfile) ?? sessionPolicy.backendProfile; input.backendProfile = backendProfile; if (!isRecord(input.workspaceRef)) input.workspaceRef = cloneJsonRecord(sessionPolicy.workspaceRef); if (!isRecord(input.executionPolicy)) { input.executionPolicy = defaultAgentRunExecutionPolicy(backendProfile, sessionPolicy, policyTarget, { includeToolCredentials: true }); } const priority = agentRunOption(args, "priority"); if (priority) input.priority = Number(priority); const workspaceRef = jsonObjectOption(args, "workspace-json"); if (workspaceRef !== null) input.workspaceRef = workspaceRef; return { ...input, ...overrides }; } export function readPromptFromArgs(args: string[], trailingStart: number): string { const prompt = optionalPromptFromArgs(args, trailingStart); if (prompt === null) throw new AgentRunRestError("validation-failed", "prompt is required; use --prompt-stdin, --prompt-file, --prompt, or a trailing prompt"); return prompt; } export function optionalPromptFromArgs(args: string[], trailingStart: number): string | null { if (agentRunHasFlag(args, "prompt-stdin") || agentRunHasFlag(args, "stdin")) return readFileSync(0, "utf8"); const promptFile = agentRunOption(args, "prompt-file"); if (promptFile !== null) return promptFile === "-" ? readFileSync(0, "utf8") : readFileSync(promptFile, "utf8"); const prompt = agentRunOption(args, "prompt"); if (prompt !== null) return prompt; const trailing = args.slice(trailingStart).filter((arg) => !arg.startsWith("-")); return trailing.length > 0 ? trailing.join(" ") : null; } export function queueDispatchBodyFromArgs(args: string[]): Record { const body = recordFromMaybeJson(args); copyAgentRunOptions(args, body, ["idempotency-key", "image", "namespace", "attempt-id", "runner-id", "source-commit", "service-account-name"]); const managerUrl = agentRunOption(args, "runner-manager-url"); if (managerUrl) body.managerUrl = managerUrl; return body; } export function recordFromMaybeJson(args: string[]): Record { const raw = readJsonTextFromArgs(args, "json"); if (raw === null) return {}; return record(JSON.parse(raw) as unknown); } export function cancelBodyFromArgs(args: string[]): Record { const reason = agentRunOption(args, "reason"); return reason === null ? {} : { reason }; } export function copyAgentRunOptions(args: string[], target: Record, flagNames: string[]): void { for (const flagName of flagNames) { const value = agentRunOption(args, flagName); if (value === null) continue; target[camelCaseFlag(flagName)] = value; } } export function camelCaseFlag(flagName: string): string { return flagName.replace(/-([a-z])/gu, (_, letter: string) => String(letter).toUpperCase()); } export function jsonObjectOption(args: string[], flagName: string): Record | null { const value = agentRunOption(args, flagName); if (value === null) return null; return record(JSON.parse(value) as unknown); } export function queueSubmitConfirmCommand(args: string[], aipod?: string): string { const parts: string[] = []; for (let index = 0; index < args.length; index += 1) { const arg = args[index] ?? ""; if (arg === "submit" || arg === "--dry-run") continue; if (arg === "--aipod" || arg === "--aipod-spec") { index += 1; continue; } if (arg.startsWith("--aipod=") || arg.startsWith("--aipod-spec=")) continue; parts.push(arg); } const dryRunless = parts.join(" "); return `bun scripts/cli.ts agentrun queue submit${aipod ? ` --aipod ${aipod}` : ""}${dryRunless.length > 0 ? ` ${dryRunless}` : ""}`.trim(); } export function jsonInputDisclosureFromArgs(args: string[]): Record { return { source: agentRunHasFlag(args, "json-stdin") ? "stdin" : agentRunOption(args, "json-file") ?? "", valuesPrinted: false, }; } export function resolveAgentRunSessionPolicyTarget(options: AgentRunRestTargetOptions = { node: null, lane: null }): AgentRunSessionPolicyTarget { if (activeAgentRunRestTarget !== null) { return { configPath: activeAgentRunRestTarget.configPath, spec: activeAgentRunRestTarget.spec, source: "selected-lane", transport: "lane-k8s-service-proxy", }; } const node = options.node ?? null; const lane = options.lane ?? null; const { configPath, spec } = resolveAgentRunLaneTarget({ node, lane }); return { configPath, spec, source: node !== null || lane !== null ? "selected-lane" : "default-lane", transport: "direct-http", }; } export function defaultAgentRunProviderId(sessionPolicy: AgentRunSessionPolicyConfig, target: AgentRunSessionPolicyTarget): string { return target.source === "selected-lane" ? target.spec.nodeId : sessionPolicy.providerId; } export function defaultAgentRunExecutionPolicy( profile: string, sessionPolicy: AgentRunSessionPolicyConfig = readAgentRunClientConfig().client.sessionPolicy, target: AgentRunSessionPolicyTarget = resolveAgentRunSessionPolicyTarget(), options: { includeToolCredentials?: boolean; basePolicy?: Record } = {}, ): Record { const basePolicy = Object.keys(record(options.basePolicy)).length > 0 ? cloneJsonRecord(record(options.basePolicy)) : cloneJsonRecord(sessionPolicy.executionPolicy); return agentRunExecutionPolicyWithLaneCredentials(basePolicy, profile, target, options.includeToolCredentials === true); } export function agentRunExecutionPolicyWithLaneCredentials(basePolicy: Record, profile: string, target: AgentRunSessionPolicyTarget, includeToolCredentials: boolean): Record { const spec = target.spec; const credentials = agentRunProviderCredentialRefs(spec, profile); if (credentials.length === 0) { throw new AgentRunRestError("validation-failed", `${target.configPath} has no providerCredential Secret binding for backendProfile=${profile} on ${target.source} ${spec.nodeId}/${spec.lane}`); } const providerCredentials = credentials.map((credential) => { if (credential.secretRef.namespace !== spec.runtime.namespace) { throw new AgentRunRestError("validation-failed", `providerCredential ${profile} Secret ${credential.secretRef.name} is in namespace ${credential.secretRef.namespace}, expected target lane namespace ${spec.runtime.namespace}`); } return { profile: credential.profile, secretRef: { name: credential.secretRef.name, keys: credential.secretRef.keys, }, }; }); const laneToolCredentialRefs = includeToolCredentials ? agentRunToolCredentialRefs(spec) : []; const toolCredentialRefs = includeToolCredentials ? selectBasePolicyToolCredentials(basePolicy, laneToolCredentialRefs, target) : []; const toolCredentials = toolCredentialRefs.map((credential) => { if (credential.secretRef.namespace !== spec.runtime.namespace) { throw new AgentRunRestError("validation-failed", `toolCredential ${credential.tool} Secret ${credential.secretRef.name} is in namespace ${credential.secretRef.namespace}, expected target lane namespace ${spec.runtime.namespace}`); } return { tool: credential.tool, purpose: credential.purpose, secretRef: { name: credential.secretRef.name, keys: credential.secretRef.keys, }, projection: credential.projection, }; }); const secretScope = record(basePolicy.secretScope); return { ...basePolicy, secretScope: { ...secretScope, providerCredentials, ...(toolCredentials.length === 0 ? {} : { toolCredentials }), }, }; } export interface AgentRunToolCredentialRef { readonly tool: string; readonly purpose: string; readonly projection: Record; readonly sourceId: string; readonly secretRef: { readonly namespace: string; readonly name: string; readonly keys: readonly string[] }; readonly valuesPrinted: false; } export function agentRunToolCredentialRefs(spec: AgentRunLaneSpec): AgentRunToolCredentialRef[] { return spec.toolCredentials.map((credential) => ({ tool: credential.tool, purpose: credential.purpose, projection: credential.projection, sourceId: credential.id, secretRef: credential.secretRef, valuesPrinted: false, })); } function selectBasePolicyToolCredentials(basePolicy: Record, laneCredentials: readonly AgentRunToolCredentialRef[], target: AgentRunSessionPolicyTarget): AgentRunToolCredentialRef[] { const secretScope = record(basePolicy.secretScope); if (!Object.hasOwn(secretScope, "toolCredentials")) return [...laneCredentials]; const rawCredentials = secretScope.toolCredentials; if (!Array.isArray(rawCredentials)) { throw new AgentRunRestError("validation-failed", "executionPolicy.secretScope.toolCredentials must be an array when lane tool credentials are enabled"); } const laneByIdentity = new Map(laneCredentials.map((credential) => [`${credential.tool}\0${credential.purpose}`, credential])); const seen = new Set(); const selected: AgentRunToolCredentialRef[] = []; for (const [index, rawCredential] of rawCredentials.entries()) { const credential = record(rawCredential); const tool = stringOrNull(credential.tool); const purpose = stringOrNull(credential.purpose); if (tool === null || purpose === null) { throw new AgentRunRestError("validation-failed", `executionPolicy.secretScope.toolCredentials[${index}] must declare tool and purpose`); } const identity = `${tool}\0${purpose}`; if (seen.has(identity)) { throw new AgentRunRestError("validation-failed", `executionPolicy.secretScope.toolCredentials contains duplicate ${tool}/${purpose} identity`); } seen.add(identity); const laneCredential = laneByIdentity.get(identity); if (laneCredential === undefined) { throw new AgentRunRestError("validation-failed", `executionPolicy toolCredential ${tool}/${purpose} is not declared by YAML lane ${target.spec.nodeId}/${target.spec.lane}`); } const secretRef = record(credential.secretRef); const secretName = stringOrNull(secretRef.name); const namespace = stringOrNull(secretRef.namespace) ?? target.spec.runtime.namespace; if (secretName !== laneCredential.secretRef.name || namespace !== laneCredential.secretRef.namespace) { throw new AgentRunRestError("validation-failed", `executionPolicy toolCredential ${tool}/${purpose} SecretRef conflicts with YAML lane ${target.spec.nodeId}/${target.spec.lane}`); } const keys = normalizedCredentialKeys(secretRef.keys); const laneKeys = normalizedCredentialKeys(laneCredential.secretRef.keys); if (keys === null || laneKeys === null || keys.length !== laneKeys.length || keys.some((key, keyIndex) => key !== laneKeys[keyIndex])) { throw new AgentRunRestError("validation-failed", `executionPolicy toolCredential ${tool}/${purpose} keys conflict with YAML lane ${target.spec.nodeId}/${target.spec.lane}`); } if (!toolCredentialProjectionMatches(record(credential.projection), laneCredential.projection)) { throw new AgentRunRestError("validation-failed", `executionPolicy toolCredential ${tool}/${purpose} projection conflicts with YAML lane ${target.spec.nodeId}/${target.spec.lane}`); } selected.push(laneCredential); } return selected; } function normalizedCredentialKeys(value: unknown): string[] | null { if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim().length === 0)) return null; const keys = value.map((item) => String(item)).sort(); return new Set(keys).size === keys.length ? keys : null; } function toolCredentialProjectionMatches(actual: Record, expected: Record): boolean { if (actual.kind !== expected.kind) return false; if (expected.kind === "env") { return actual.envName === expected.envName && actual.secretKey === expected.secretKey; } if (expected.kind === "volume") return actual.mountPath === expected.mountPath; return false; } export function renderAgentRunSessionPolicyExplanation(args: string[] = [], options: AgentRunRestTargetOptions = { node: null, lane: null }): string { const config = readAgentRunClientConfig(); const target = resolveAgentRunSessionPolicyTarget(options); const spec = target.spec; const sessionPolicy = config.client.sessionPolicy; const backendProfile = agentRunOption(args, "profile") ?? agentRunOption(args, "backend-profile") ?? sessionPolicy.backendProfile; const providerId = agentRunOption(args, "provider-id") ?? defaultAgentRunProviderId(sessionPolicy, target); const workspaceRef = jsonObjectOption(args, "workspace-json") ?? cloneJsonRecord(sessionPolicy.workspaceRef); const credentials = agentRunProviderCredentialRefs(spec, backendProfile); const execution = defaultAgentRunExecutionPolicy(backendProfile, sessionPolicy, target); const credentialSources = credentials.map((credential) => ({ profile: credential.profile, secretRef: credential.secretRef, valuesPrinted: false, })); const toolCredentialSources = agentRunToolCredentialRefs(spec); return [ "KIND: session-policy", `CONFIG: ${config.sourcePath}`, `LANE CONFIG: ${target.configPath}`, `TARGET LANE: ${spec.nodeId}/${spec.lane}`, `POLICY SOURCE: ${target.source}`, `TRANSPORT: ${target.transport}`, `DEFAULTS: tenantId=${sessionPolicy.tenantId} projectId=${sessionPolicy.projectId} providerId=${providerId} backendProfile=${backendProfile}`, `WORKSPACE: ${JSON.stringify(workspaceRef)}`, `EXECUTION: ${JSON.stringify(execution)}`, `PROVIDER CREDENTIAL SOURCES: ${JSON.stringify(credentialSources)}`, `TOOL CREDENTIAL SOURCES: ${JSON.stringify(toolCredentialSources)}`, "VALUES: secret payloads are not printed; valuesPrinted=false", ].join("\n"); } export function safeAgentRunEnvelope(envelope: Record): Record { return pickCompact(envelope, ["ok", "failureKind", "message", "code", "details", "valuesPrinted"]); } export function normalizeAgentRunFailureKind(raw: string | null, httpStatus: number): AgentRunFailureKind { if (raw === "agentrun-unreachable") return "agentrun-connect-failed"; if (raw === "auth-missing" || raw === "auth-failed" || raw === "agentrun-connect-failed" || raw === "agentrun-proxy-exec-failed" || raw === "agentrun-manager-fetch-failed" || raw === "agentrun-timeout" || raw === "schema-mismatch" || raw === "unsupported-version" || raw === "validation-failed" || raw === "not-found") return raw; if (httpStatus === 401 || httpStatus === 403) return "auth-failed"; if (httpStatus === 404) return "not-found"; return raw === "schema-invalid" ? "validation-failed" : "validation-failed"; } export function classifyAgentRunProxyFailureKind(raw: string | null): AgentRunFailureKind { if (raw === "manager-pod-fetch-timeout" || raw === "agentrun-timeout") return "agentrun-timeout"; if (raw === "manager-pod-fetch-failed") return "agentrun-manager-fetch-failed"; const normalized = normalizeAgentRunFailureKind(raw, 0); return normalized === "validation-failed" ? "agentrun-proxy-exec-failed" : normalized; } export function stringFieldFromRecord(obj: Record, key: string, pathValue: string): string { const value = obj[key]; if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${pathValue}.${key} must be a non-empty string`); return value.trim(); } export function numberFieldFromRecord(obj: Record, key: string, pathValue: string, bounds: { min: number; max: number }): number { const value = obj[key]; if (typeof value !== "number" || !Number.isFinite(value) || value < bounds.min || value > bounds.max) throw new Error(`${pathValue}.${key} must be a number between ${bounds.min} and ${bounds.max}`); return value; } export function positiveIntegerFieldFromRecord(obj: Record, key: string, pathValue: string): number { const value = obj[key]; if (!Number.isInteger(value) || Number(value) <= 0) throw new Error(`${pathValue}.${key} must be a positive integer`); return Number(value); } export function booleanFieldFromRecord(obj: Record, key: string, pathValue: string): boolean { const value = obj[key]; if (typeof value !== "boolean") throw new Error(`${pathValue}.${key} must be a boolean`); return value; } export function cloneJsonRecord(value: Record): Record { return JSON.parse(JSON.stringify(value)) as Record; } export interface AgentRunClientConfig { sourcePath: string; manager: { baseUrl: string; timeoutMs: number; }; auth: { env: string; file: string; header: string; scheme: string; }; client: { role: string; transport: string; sessionPolicy: AgentRunSessionPolicyConfig; }; publicExposure: AgentRunPublicExposure | null; } export interface AgentRunSessionPolicyConfig { sourcePath: string; tenantId: string; projectId: string; providerId: string; backendProfile: string; workspaceRef: Record; executionPolicy: Record; } export interface AgentRunPublicExposure { enabled: true; proxyName: string; remotePort: number; publicBaseUrl: string; masterBaseUrl: string; masterFrps: { configPath: string; containerName: string; }; masterCaddy: { enabled: boolean; domain: string; configPath: string; serviceName: string; upstreamBaseUrl: string; responseHeaderTimeoutSeconds: number; }; }