chore: read agentrun hwlab defaults from yaml
This commit is contained in:
@@ -1,3 +1,8 @@
|
|||||||
|
version: 1
|
||||||
|
kind: AgentRunConfig
|
||||||
|
metadata:
|
||||||
|
name: agentrun
|
||||||
|
|
||||||
manager:
|
manager:
|
||||||
baseUrl: https://agentrun.74-48-78-17.nip.io/
|
baseUrl: https://agentrun.74-48-78-17.nip.io/
|
||||||
timeoutMs: 15000
|
timeoutMs: 15000
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
version: 1
|
||||||
|
kind: HwlabNodeLaneConfig
|
||||||
|
metadata:
|
||||||
|
name: hwlab-node-lanes
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
node: G14
|
||||||
|
lane: v03
|
||||||
|
|
||||||
requiredNoProxy:
|
requiredNoProxy:
|
||||||
- hyueapi.com
|
- hyueapi.com
|
||||||
- .hyueapi.com
|
- .hyueapi.com
|
||||||
|
|||||||
@@ -193,6 +193,10 @@ export function resolveAgentRunLaneTarget(options: { node?: string | null; lane?
|
|||||||
return { configPath: config.sourcePath, spec };
|
return { configPath: config.sourcePath, spec };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function agentRunDefaultProviderId(env: NodeJS.ProcessEnv = process.env): string {
|
||||||
|
return resolveAgentRunLaneTarget({}, env).spec.nodeId;
|
||||||
|
}
|
||||||
|
|
||||||
export function agentRunLaneSummary(spec: AgentRunLaneSpec): Record<string, unknown> {
|
export function agentRunLaneSummary(spec: AgentRunLaneSpec): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
node: {
|
node: {
|
||||||
@@ -314,6 +318,7 @@ export function agentRunPipelineRunName(spec: AgentRunLaneSpec, sourceCommit: st
|
|||||||
function readAgentRunControlPlaneConfig(env: NodeJS.ProcessEnv): AgentRunControlPlaneConfig {
|
function readAgentRunControlPlaneConfig(env: NodeJS.ProcessEnv): AgentRunControlPlaneConfig {
|
||||||
const configPath = env.AGENTRUN_CONTROL_PLANE_CONFIG ?? rootPath(AGENTRUN_CONFIG_PATH);
|
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 = asRecord(Bun.YAML.parse(readFileSync(configPath, "utf8")) as unknown, configPath);
|
||||||
|
validateConfigEnvelope(root, configPath);
|
||||||
const controlPlane = recordField(root, "controlPlane", configPath);
|
const controlPlane = recordField(root, "controlPlane", configPath);
|
||||||
const defaultTarget = recordField(controlPlane, "default", `${configPath}.controlPlane`);
|
const defaultTarget = recordField(controlPlane, "default", `${configPath}.controlPlane`);
|
||||||
const nodes = parseNodes(recordField(controlPlane, "nodes", `${configPath}.controlPlane`), configPath);
|
const nodes = parseNodes(recordField(controlPlane, "nodes", `${configPath}.controlPlane`), configPath);
|
||||||
@@ -329,6 +334,15 @@ function readAgentRunControlPlaneConfig(env: NodeJS.ProcessEnv): AgentRunControl
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
function parseNodes(input: Record<string, unknown>, configPath: string): Record<string, AgentRunNodeSpec> {
|
||||||
const result: Record<string, AgentRunNodeSpec> = {};
|
const result: Record<string, AgentRunNodeSpec> = {};
|
||||||
for (const [id, raw] of Object.entries(input)) {
|
for (const [id, raw] of Object.entries(input)) {
|
||||||
|
|||||||
+13
-2
@@ -10,6 +10,7 @@ import { runRemoteSshCommandCapture } from "./remote";
|
|||||||
import { startJob } from "./jobs";
|
import { startJob } from "./jobs";
|
||||||
import {
|
import {
|
||||||
AGENTRUN_CONFIG_PATH,
|
AGENTRUN_CONFIG_PATH,
|
||||||
|
agentRunDefaultProviderId,
|
||||||
agentRunLaneSummary,
|
agentRunLaneSummary,
|
||||||
agentRunPipelineRunName,
|
agentRunPipelineRunName,
|
||||||
resolveAgentRunLaneTarget,
|
resolveAgentRunLaneTarget,
|
||||||
@@ -5302,6 +5303,7 @@ function isExplicitSessionSendBody(input: Record<string, unknown>): boolean {
|
|||||||
|
|
||||||
async function sessionRunBodyFromArgs(sessionId: string, args: string[], input: Record<string, unknown>): Promise<Record<string, unknown>> {
|
async function sessionRunBodyFromArgs(sessionId: string, args: string[], input: Record<string, unknown>): Promise<Record<string, unknown>> {
|
||||||
const existing = await fetchAgentRunSessionOrNull(sessionId, args);
|
const existing = await fetchAgentRunSessionOrNull(sessionId, args);
|
||||||
|
const defaultProviderId = agentRunDefaultProviderId();
|
||||||
const profile = agentRunOption(args, "profile")
|
const profile = agentRunOption(args, "profile")
|
||||||
?? agentRunOption(args, "backend-profile")
|
?? agentRunOption(args, "backend-profile")
|
||||||
?? stringOrNull(input.backendProfile)
|
?? stringOrNull(input.backendProfile)
|
||||||
@@ -5310,7 +5312,7 @@ async function sessionRunBodyFromArgs(sessionId: string, args: string[], input:
|
|||||||
const body: Record<string, unknown> = { ...input };
|
const body: Record<string, unknown> = { ...input };
|
||||||
body.tenantId = agentRunOption(args, "tenant-id") ?? stringOrNull(body.tenantId) ?? stringOrNull(existing?.tenantId) ?? "unidesk";
|
body.tenantId = agentRunOption(args, "tenant-id") ?? stringOrNull(body.tenantId) ?? stringOrNull(existing?.tenantId) ?? "unidesk";
|
||||||
body.projectId = agentRunOption(args, "project-id") ?? stringOrNull(body.projectId) ?? stringOrNull(existing?.projectId) ?? "default";
|
body.projectId = agentRunOption(args, "project-id") ?? stringOrNull(body.projectId) ?? stringOrNull(existing?.projectId) ?? "default";
|
||||||
body.providerId = agentRunOption(args, "provider-id") ?? stringOrNull(body.providerId) ?? "G14";
|
body.providerId = agentRunOption(args, "provider-id") ?? stringOrNull(body.providerId) ?? stringOrNull(existing?.providerId) ?? defaultProviderId;
|
||||||
body.backendProfile = profile;
|
body.backendProfile = profile;
|
||||||
body.workspaceRef = jsonObjectOption(args, "workspace-json") ?? record(body.workspaceRef);
|
body.workspaceRef = jsonObjectOption(args, "workspace-json") ?? record(body.workspaceRef);
|
||||||
if (Object.keys(record(body.workspaceRef)).length === 0) body.workspaceRef = { kind: "opaque", path: "." };
|
if (Object.keys(record(body.workspaceRef)).length === 0) body.workspaceRef = { kind: "opaque", path: "." };
|
||||||
@@ -5338,10 +5340,11 @@ async function sessionSendWithAipodRest(sessionId: string, aipod: string, args:
|
|||||||
const metadata = record(sessionRef.metadata);
|
const metadata = record(sessionRef.metadata);
|
||||||
const title = agentRunOption(args, "title") ?? stringOrNull(task.title);
|
const title = agentRunOption(args, "title") ?? stringOrNull(task.title);
|
||||||
if (title) metadata.title = title;
|
if (title) metadata.title = title;
|
||||||
|
const defaultProviderId = agentRunDefaultProviderId();
|
||||||
const runBody: Record<string, unknown> = {
|
const runBody: Record<string, unknown> = {
|
||||||
tenantId: task.tenantId,
|
tenantId: task.tenantId,
|
||||||
projectId: task.projectId,
|
projectId: task.projectId,
|
||||||
providerId: task.providerId ?? "G14",
|
providerId: task.providerId ?? defaultProviderId,
|
||||||
backendProfile: task.backendProfile,
|
backendProfile: task.backendProfile,
|
||||||
workspaceRef: task.workspaceRef ?? { kind: "opaque", path: "." },
|
workspaceRef: task.workspaceRef ?? { kind: "opaque", path: "." },
|
||||||
sessionRef: { ...sessionRef, sessionId, metadata },
|
sessionRef: { ...sessionRef, sessionId, metadata },
|
||||||
@@ -5489,6 +5492,7 @@ function readAgentRunClientConfig(env: NodeJS.ProcessEnv = process.env): AgentRu
|
|||||||
|
|
||||||
export function parseAgentRunClientConfigYaml(raw: string, sourcePath = "config/agentrun.yaml"): AgentRunClientConfig {
|
export function parseAgentRunClientConfigYaml(raw: string, sourcePath = "config/agentrun.yaml"): AgentRunClientConfig {
|
||||||
const input = record(Bun.YAML.parse(raw) as unknown);
|
const input = record(Bun.YAML.parse(raw) as unknown);
|
||||||
|
validateAgentRunConfigEnvelope(input, sourcePath);
|
||||||
const manager = record(input.manager);
|
const manager = record(input.manager);
|
||||||
const auth = record(input.auth);
|
const auth = record(input.auth);
|
||||||
const client = record(input.client);
|
const client = record(input.client);
|
||||||
@@ -5529,6 +5533,13 @@ export function parseAgentRunClientConfigYaml(raw: string, sourcePath = "config/
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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`);
|
||||||
|
}
|
||||||
|
|
||||||
function readAgentRunPublicExposureConfig(value: unknown, managerBaseUrl: string, sourcePath: string): AgentRunPublicExposure | null {
|
function readAgentRunPublicExposureConfig(value: unknown, managerBaseUrl: string, sourcePath: string): AgentRunPublicExposure | null {
|
||||||
if (value === undefined || value === null) return null;
|
if (value === undefined || value === null) return null;
|
||||||
const exposure = record(value);
|
const exposure = record(value);
|
||||||
|
|||||||
@@ -216,6 +216,10 @@ interface HwlabLaneConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface HwlabNodeLaneConfig {
|
interface HwlabNodeLaneConfig {
|
||||||
|
readonly defaultTarget: {
|
||||||
|
readonly node: string;
|
||||||
|
readonly lane: HwlabRuntimeLane;
|
||||||
|
};
|
||||||
readonly requiredNoProxy: readonly string[];
|
readonly requiredNoProxy: readonly string[];
|
||||||
readonly nodes: Record<string, HwlabRuntimeNodeSpec>;
|
readonly nodes: Record<string, HwlabRuntimeNodeSpec>;
|
||||||
readonly lanes: Record<HwlabRuntimeLane, HwlabLaneConfig>;
|
readonly lanes: Record<HwlabRuntimeLane, HwlabLaneConfig>;
|
||||||
@@ -572,6 +576,8 @@ function readHwlabNodeLaneConfig(): HwlabNodeLaneConfig {
|
|||||||
const path = rootPath(HWLAB_NODE_LANE_CONFIG_PATH);
|
const path = rootPath(HWLAB_NODE_LANE_CONFIG_PATH);
|
||||||
const raw = readFileSync(path, "utf8");
|
const raw = readFileSync(path, "utf8");
|
||||||
const parsed = asRecord(Bun.YAML.parse(raw) as unknown, HWLAB_NODE_LANE_CONFIG_PATH);
|
const parsed = asRecord(Bun.YAML.parse(raw) as unknown, HWLAB_NODE_LANE_CONFIG_PATH);
|
||||||
|
validateConfigEnvelope(parsed);
|
||||||
|
const defaultTarget = parseDefaultTarget(asRecord(parsed.defaults, `${HWLAB_NODE_LANE_CONFIG_PATH}.defaults`));
|
||||||
const requiredNoProxy = stringArrayField(parsed, "requiredNoProxy", HWLAB_NODE_LANE_CONFIG_PATH);
|
const requiredNoProxy = stringArrayField(parsed, "requiredNoProxy", HWLAB_NODE_LANE_CONFIG_PATH);
|
||||||
for (const required of ["hyueapi.com", ".hyueapi.com"]) {
|
for (const required of ["hyueapi.com", ".hyueapi.com"]) {
|
||||||
if (!requiredNoProxy.includes(required)) throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.requiredNoProxy must include ${required}`);
|
if (!requiredNoProxy.includes(required)) throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.requiredNoProxy must include ${required}`);
|
||||||
@@ -603,7 +609,30 @@ function readHwlabNodeLaneConfig(): HwlabNodeLaneConfig {
|
|||||||
for (const lane of [...Object.values(lanes), ...Object.values(laneTargets).flatMap((targets) => Object.values(targets))]) {
|
for (const lane of [...Object.values(lanes), ...Object.values(laneTargets).flatMap((targets) => Object.values(targets))]) {
|
||||||
if (nodes[lane.node] === undefined) throw new Error(`lanes.${lane.id}.node references missing node ${lane.node}`);
|
if (nodes[lane.node] === undefined) throw new Error(`lanes.${lane.id}.node references missing node ${lane.node}`);
|
||||||
}
|
}
|
||||||
return { requiredNoProxy, nodes, lanes, laneTargets, networkProfiles, downloadProfiles };
|
if (nodes[defaultTarget.node] === undefined) throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.defaults.node references missing node ${defaultTarget.node}`);
|
||||||
|
const defaultLane = laneTargets[defaultTarget.lane]?.[defaultTarget.node] ?? lanes[defaultTarget.lane];
|
||||||
|
if (defaultLane === undefined || defaultLane.node !== defaultTarget.node) {
|
||||||
|
throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.defaults must reference a declared lane target`);
|
||||||
|
}
|
||||||
|
return { defaultTarget, requiredNoProxy, nodes, lanes, laneTargets, networkProfiles, downloadProfiles };
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateConfigEnvelope(parsed: Record<string, unknown>): void {
|
||||||
|
if (parsed.version !== 1) throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.version must be 1`);
|
||||||
|
if (parsed.kind !== "HwlabNodeLaneConfig") throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.kind must be HwlabNodeLaneConfig`);
|
||||||
|
const metadata = asRecord(parsed.metadata, `${HWLAB_NODE_LANE_CONFIG_PATH}.metadata`);
|
||||||
|
if (stringField(metadata, "name", `${HWLAB_NODE_LANE_CONFIG_PATH}.metadata`) !== "hwlab-node-lanes") {
|
||||||
|
throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.metadata.name must be hwlab-node-lanes`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDefaultTarget(raw: Record<string, unknown>): { readonly node: string; readonly lane: HwlabRuntimeLane } {
|
||||||
|
const lane = stringField(raw, "lane", `${HWLAB_NODE_LANE_CONFIG_PATH}.defaults`);
|
||||||
|
if (!isSupportedLaneId(lane)) throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.defaults.lane is not supported by this CLI build`);
|
||||||
|
return {
|
||||||
|
node: stringField(raw, "node", `${HWLAB_NODE_LANE_CONFIG_PATH}.defaults`),
|
||||||
|
lane,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const HWLAB_NODE_LANE_CONFIG = readHwlabNodeLaneConfig();
|
const HWLAB_NODE_LANE_CONFIG = readHwlabNodeLaneConfig();
|
||||||
@@ -689,6 +718,10 @@ export function hwlabRuntimeNodeIds(): string[] {
|
|||||||
return Object.keys(HWLAB_NODE_LANE_CONFIG.nodes);
|
return Object.keys(HWLAB_NODE_LANE_CONFIG.nodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hwlabDefaultRuntimeTarget(): { readonly node: string; readonly lane: HwlabRuntimeLane } {
|
||||||
|
return { ...HWLAB_NODE_LANE_CONFIG.defaultTarget };
|
||||||
|
}
|
||||||
|
|
||||||
export function hwlabRuntimeLaneConfigPath(): string {
|
export function hwlabRuntimeLaneConfigPath(): string {
|
||||||
return HWLAB_NODE_LANE_CONFIG_PATH;
|
return HWLAB_NODE_LANE_CONFIG_PATH;
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-1
@@ -3,6 +3,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSy
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { repoRoot, rootPath } from "./config";
|
import { repoRoot, rootPath } from "./config";
|
||||||
import { runCommandToFiles, tailFile } from "./command";
|
import { runCommandToFiles, tailFile } from "./command";
|
||||||
|
import { hwlabDefaultRuntimeTarget } from "./hwlab-node-lanes";
|
||||||
|
|
||||||
export type JobStatus = "queued" | "running" | "succeeded" | "failed" | "canceled";
|
export type JobStatus = "queued" | "running" | "succeeded" | "failed" | "canceled";
|
||||||
|
|
||||||
@@ -438,6 +439,8 @@ function summarizeRuntimeLaneTriggerJobProgress(job: JobRecord, stdoutTail: stri
|
|||||||
stageElapsedSeconds,
|
stageElapsedSeconds,
|
||||||
lastEventAgeSeconds,
|
lastEventAgeSeconds,
|
||||||
});
|
});
|
||||||
|
const nextStatusCommand = hwlabRuntimeLaneStatusNextCommand(pipelineRun, lastEvent);
|
||||||
|
if (nextStatusCommand.warning !== null) warnings.push(nextStatusCommand.warning);
|
||||||
const tcpPoolDiagnostics = job.status === "succeeded" ? null : sshTcpPoolDiagnosticsFromJobText(`${stderrTail}\n${stdoutTail}`);
|
const tcpPoolDiagnostics = job.status === "succeeded" ? null : sshTcpPoolDiagnosticsFromJobText(`${stderrTail}\n${stdoutTail}`);
|
||||||
const slow = warnings.length > 0;
|
const slow = warnings.length > 0;
|
||||||
return {
|
return {
|
||||||
@@ -469,13 +472,32 @@ function summarizeRuntimeLaneTriggerJobProgress(job: JobRecord, stdoutTail: stri
|
|||||||
slow ? "visibility-warning" : null,
|
slow ? "visibility-warning" : null,
|
||||||
].filter(Boolean).join(" "),
|
].filter(Boolean).join(" "),
|
||||||
nextCommand: pipelineRun
|
nextCommand: pipelineRun
|
||||||
? `bun scripts/cli.ts hwlab nodes control-plane status --node ${stringField(lastEvent.node) ?? "G14"} --lane ${stringField(lastEvent.lane) ?? "v03"} --pipeline-run ${pipelineRun}`
|
? nextStatusCommand.command
|
||||||
: job.status === "running"
|
: job.status === "running"
|
||||||
? `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`
|
? `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`
|
||||||
: null,
|
: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hwlabRuntimeLaneStatusNextCommand(pipelineRun: string | null, lastEvent: Record<string, unknown>): { command: string | null; warning: string | null } {
|
||||||
|
if (pipelineRun === null) return { command: null, warning: null };
|
||||||
|
const eventNode = stringField(lastEvent.node);
|
||||||
|
const eventLane = stringField(lastEvent.lane);
|
||||||
|
const defaults = hwlabDefaultRuntimeTarget();
|
||||||
|
const node = eventNode ?? defaults.node;
|
||||||
|
const lane = eventLane ?? defaults.lane;
|
||||||
|
const missing = [
|
||||||
|
eventNode === null ? "node" : null,
|
||||||
|
eventLane === null ? "lane" : null,
|
||||||
|
].filter((value): value is string => value !== null);
|
||||||
|
return {
|
||||||
|
command: `bun scripts/cli.ts hwlab nodes control-plane status --node ${node} --lane ${lane} --pipeline-run ${pipelineRun}`,
|
||||||
|
warning: missing.length === 0
|
||||||
|
? null
|
||||||
|
: `runtime lane progress event missing ${missing.join("/")} for nextCommand; used YAML default ${defaults.node}/${defaults.lane} from config/hwlab-node-lanes.yaml`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function sshTcpPoolDiagnosticsFromJobText(text: string): Record<string, unknown> | null {
|
function sshTcpPoolDiagnosticsFromJobText(text: string): Record<string, unknown> | null {
|
||||||
const hint = lastJsonLinePayload(text, "UNIDESK_SSH_TCP_POOL_HINT");
|
const hint = lastJsonLinePayload(text, "UNIDESK_SSH_TCP_POOL_HINT");
|
||||||
if (hint !== null) return hint;
|
if (hint !== null) return hint;
|
||||||
|
|||||||
Reference in New Issue
Block a user