Files
pikasTech-unidesk/scripts/src/agentrun/config.ts
T
2026-06-25 16:16:25 +00:00

776 lines
36 KiB
TypeScript

// 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<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, unknown> {
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<string, unknown> {
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<string, unknown>, config: AgentRunClientConfig, method: AgentRunHttpMethod, pathValue: string, target: AgentRunRestTarget | null): Record<string, unknown> {
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 <node> --lane <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<string, string> | 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<string, unknown>[] } | 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<string, unknown>, confirmCommand: string, method: AgentRunHttpMethod = "POST", extra: Record<string, unknown> = {}): Record<string, unknown> {
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<string, string> = {
"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<Record<string, unknown>> {
const body = await optionalJsonBody(args);
if (Object.keys(body).length === 0) throw new AgentRunRestError("validation-failed", `${context} requires --json-stdin or --json-file <path>`);
return body;
}
export async function optionalJsonBody(args: string[]): Promise<Record<string, unknown>> {
const raw = readJsonTextFromArgs(args, "json");
if (raw === null) return {};
return record(JSON.parse(raw) as unknown);
}
export async function optionalRunnerJsonBody(args: string[]): Promise<Record<string, unknown>> {
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 <file>");
}
export async function aipodRenderInputFromArgs(args: string[], trailingPromptStart: number, overrides: Record<string, unknown> = {}): Promise<Record<string, unknown>> {
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<string, unknown> {
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<string, unknown> {
const raw = readJsonTextFromArgs(args, "json");
if (raw === null) return {};
return record(JSON.parse(raw) as unknown);
}
export function cancelBodyFromArgs(args: string[]): Record<string, unknown> {
const reason = agentRunOption(args, "reason");
return reason === null ? {} : { reason };
}
export function copyAgentRunOptions(args: string[], target: Record<string, unknown>, 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<string, unknown> | 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<string, unknown> {
return {
source: agentRunHasFlag(args, "json-stdin") ? "stdin" : agentRunOption(args, "json-file") ?? "<task.json>",
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<string, unknown> } = {},
): Record<string, unknown> {
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<string, unknown>, profile: string, target: AgentRunSessionPolicyTarget, includeToolCredentials: boolean): Record<string, unknown> {
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 toolCredentials = includeToolCredentials ? agentRunToolCredentialRefs(spec).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.key],
},
projection: credential.projection,
};
}) : [];
const secretScope = record(basePolicy.secretScope);
return {
...basePolicy,
secretScope: {
...secretScope,
providerCredentials,
...(toolCredentials.length === 0 ? {} : { toolCredentials }),
},
};
}
export const AGENTRUN_DEFAULT_TOOL_CREDENTIAL_IDS = new Set(["tool-github-pr-token", "tool-unidesk-ssh-token"]);
export function agentRunToolCredentialRefs(spec: AgentRunLaneSpec, options: { includeUnsupported?: boolean } = {}): Array<{ tool: string; purpose: string; projection: Record<string, unknown>; sourceId: string; secretRef: { namespace: string; name: string; key: string }; valuesPrinted: false }> {
return spec.secrets
.filter((secret) => secret.providerCredentialProfile === null && secret.id.startsWith("tool-"))
.map((secret) => {
const binding = agentRunToolCredentialBinding(secret.id, secret.targetRef.key);
return {
tool: binding.tool,
purpose: binding.purpose,
projection: binding.projection,
sourceId: secret.id,
secretRef: secret.targetRef,
valuesPrinted: false as const,
};
})
.filter((credential) => options.includeUnsupported === true || AGENTRUN_DEFAULT_TOOL_CREDENTIAL_IDS.has(credential.sourceId));
}
export function agentRunToolCredentialBinding(sourceId: string, key: string): { tool: string; purpose: string; projection: Record<string, unknown> } {
if (sourceId === "tool-github-pr-token") {
return {
tool: "github",
purpose: "github-pr",
projection: { kind: "env", envName: key, secretKey: key },
};
}
if (sourceId === "tool-unidesk-ssh-token") {
return {
tool: "unidesk-ssh",
purpose: "ssh-passthrough",
projection: { kind: "env", envName: key, secretKey: key },
};
}
return {
tool: sourceId.replace(/^tool-/u, "").replace(/-token$/u, ""),
purpose: sourceId.replace(/^tool-/u, ""),
projection: {},
};
}
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, { includeUnsupported: true });
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<string, unknown>): Record<string, unknown> {
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<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, unknown>): Record<string, unknown> {
return JSON.parse(JSON.stringify(value)) as Record<string, unknown>;
}
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<string, unknown>;
executionPolicy: Record<string, unknown>;
}
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;
};
}