fix: connect L1 Workbench directly to AgentRun

This commit is contained in:
pikastech
2026-07-18 04:42:06 +02:00
parent 5fed7d33a3
commit bc82a55323
4 changed files with 113 additions and 33 deletions
+10 -5
View File
@@ -4,13 +4,18 @@ import { hwlabNativeDevelopmentEnvironment, hwlabNativeDevelopmentHelp } from ".
import { hwlabRuntimeLaneSpecForNode } from "./hwlab-node-lanes";
test("L1 Workbench exposure uses YAML public IP and separate bind/probe addresses", () => {
const native = hwlabRuntimeLaneSpecForNode("v03", "NC01").nativeDevelopment?.workbench;
const spec = hwlabRuntimeLaneSpecForNode("v03", "NC01");
const native = spec.nativeDevelopment?.workbench;
expect(native).toBeDefined();
const env = hwlabNativeDevelopmentEnvironment(native!);
const env = hwlabNativeDevelopmentEnvironment(native!, spec, "0123456789abcdef0123456789abcdef01234567");
expect(env.WORKBENCH_NATIVE_SERVICE_STATE_DIR).toBe(native!.stateDir);
expect(env.WORKBENCH_MODE).toBe("temporal");
expect(env.WORKBENCH_CLOUD_API_URL).toBe(native!.cloudApiUrl);
expect(env.WORKBENCH_CLOUD_API_AUTHORIZATION).toMatch(/^Bearer hwl_live_/u);
expect(env.WORKBENCH_MODE).toBe("agentrun-native");
expect(env.WORKBENCH_CLOUD_API_URL).toBeUndefined();
expect(env.WORKBENCH_CLOUD_API_AUTHORIZATION).toBeUndefined();
expect(env.AGENTRUN_MGR_URL).toBe(native!.agentRun.managerUrl);
expect(env.AGENTRUN_API_KEY).toMatch(/^hwl_live_/u);
expect(env.HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE).toBe("gpt.pika");
expect(env.HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT).toBe("0123456789abcdef0123456789abcdef01234567");
expect(env.WORKBENCH_API_BIND_HOST).toBe(native!.api.bindHost);
expect(env.WORKBENCH_API_PROBE_HOST).toBe(native!.api.probeHost);
expect(env.WORKBENCH_API_PUBLIC_HOST).toBe(native!.publicHost);
+36 -9
View File
@@ -1,6 +1,6 @@
import { runCommand } from "./command";
import { readEnvSourceFile, requiredEnvValue } from "./secrets";
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneSpecForNode, isHwlabRuntimeLane, type HwlabRuntimeNativeDevelopmentSpec } from "./hwlab-node-lanes";
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneSpecForNode, isHwlabRuntimeLane, type HwlabRuntimeLaneSpec, type HwlabRuntimeNativeDevelopmentSpec } from "./hwlab-node-lanes";
type Service = "api" | "worker" | "web";
type Action = "start" | "stop" | "restart" | "status" | "logs";
@@ -29,7 +29,11 @@ export function runHwlabNativeDevelopmentCommand(args: string[]): Record<string,
const spec = hwlabRuntimeLaneSpecForNode(lane, node);
const native = spec.nativeDevelopment?.workbench;
if (native === undefined) throw new Error(`${hwlabRuntimeLaneConfigPath()}#lanes.${lane}.targets.${node}.nativeDevelopment.workbench is required`);
const env = hwlabNativeDevelopmentEnvironment(native);
const sourceCommitResult = runCommand(["git", "rev-parse", "HEAD"], spec.workspace, { timeoutMs: 5_000 });
if (sourceCommitResult.exitCode !== 0 || !/^[0-9a-f]{40}$/iu.test(sourceCommitResult.stdout.trim())) {
throw new Error(`cannot resolve current HWLAB source commit from ${spec.workspace}`);
}
const env = hwlabNativeDevelopmentEnvironment(native, spec, sourceCommitResult.stdout.trim());
const command = ["bun", "tools/hwlab-cli/bin/hwlab-cli.ts", "workbench", "service", service, action];
const result = runCommand(command, spec.workspace, { timeoutMs: 15_000, env: { ...process.env, ...env } });
const payload = parsePayload(result.stdout);
@@ -53,18 +57,41 @@ export function runHwlabNativeDevelopmentCommand(args: string[]): Record<string,
};
}
export function hwlabNativeDevelopmentEnvironment(native: HwlabRuntimeNativeDevelopmentSpec["workbench"]): NodeJS.ProcessEnv {
export function hwlabNativeDevelopmentEnvironment(native: HwlabRuntimeNativeDevelopmentSpec["workbench"], spec: HwlabRuntimeLaneSpec, sourceCommit: string): NodeJS.ProcessEnv {
const codeAgent = spec.codeAgentRuntime;
if (codeAgent === undefined) throw new Error(`${hwlabRuntimeLaneConfigPath()}#lanes.${spec.lane}.targets.${spec.nodeId}.codeAgentRuntime is required`);
if (!/^[0-9a-f]{40}$/iu.test(sourceCommit)) throw new Error("sourceCommit must be the current HWLAB workspace 40-hex git SHA");
const authorizationSource = readEnvSourceFile({
root: "/root/.unidesk/.state/secrets",
sourceRef: native.cloudApiAuthorization.sourceRef,
sourceRef: codeAgent.apiKeySourceRef,
});
const cloudApiKey = requiredEnvValue(authorizationSource.values, native.cloudApiAuthorization.sourceKey, native.cloudApiAuthorization.sourceRef);
const cloudApiAuthorization = `Bearer ${cloudApiKey}`;
const agentRunApiKey = requiredEnvValue(authorizationSource.values, codeAgent.apiKeySourceKey, codeAgent.apiKeySourceRef);
return {
WORKBENCH_MODE: native.mode,
WORKBENCH_CLOUD_API_URL: native.cloudApiUrl,
WORKBENCH_CLOUD_API_AUTHORIZATION: cloudApiAuthorization,
WORKBENCH_API_AUTHORIZATION: cloudApiAuthorization,
WORKBENCH_TEMPORAL_ADDRESS: native.temporal.address,
WORKBENCH_TEMPORAL_NAMESPACE: native.temporal.namespace,
WORKBENCH_TEMPORAL_TASK_QUEUE: native.temporal.taskQueue,
WORKBENCH_ACTIVITY_START_TO_CLOSE_TIMEOUT_MS: String(native.temporal.activity.startToCloseTimeoutMs),
WORKBENCH_ACTIVITY_RETRY_INITIAL_INTERVAL_MS: String(native.temporal.activity.retryInitialIntervalMs),
WORKBENCH_ACTIVITY_RETRY_MAXIMUM_INTERVAL_MS: String(native.temporal.activity.retryMaximumIntervalMs),
WORKBENCH_ACTIVITY_RETRY_MAXIMUM_ATTEMPTS: String(native.temporal.activity.retryMaximumAttempts),
WORKBENCH_AGENTRUN_TERMINAL_TIMEOUT_MS: String(native.agentRun.terminalTimeoutMs),
WORKBENCH_AGENTRUN_POLL_INTERVAL_MS: String(native.agentRun.pollIntervalMs),
AGENTRUN_MGR_URL: native.agentRun.managerUrl,
AGENTRUN_API_KEY: agentRunApiKey,
HWLAB_CODE_AGENT_ADAPTER: codeAgent.adapter,
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_PRIVATE_URL: "1",
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: spec.nodeId,
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: codeAgent.defaultProviderProfile,
HWLAB_CODE_AGENT_AGENTRUN_RUNNER_NAMESPACE: codeAgent.runnerNamespace,
HWLAB_CODE_AGENT_AGENTRUN_REPO_URL: spec.gitReadUrl,
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: sourceCommit,
HWLAB_RUNTIME_API_URL: native.agentRun.runtimeApiUrl,
HWLAB_RUNTIME_WEB_URL: native.agentRun.runtimeWebUrl,
HWLAB_RUNTIME_NAMESPACE: spec.runtimeNamespace,
HWLAB_RUNTIME_LANE: spec.lane,
HWLAB_RUNTIME_ENDPOINT_LOCKED: "1",
HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME: "1",
WORKBENCH_NATIVE_SERVICE_STATE_DIR: native.stateDir,
WORKBENCH_API_BIND_HOST: native.api.bindHost,
WORKBENCH_API_PROBE_HOST: native.api.probeHost,
+51 -14
View File
@@ -670,10 +670,25 @@ export interface HwlabRuntimeNativeDevelopmentSpec {
readonly publicHostRef: string;
readonly publicHost: string;
readonly stateDir: string;
readonly mode: "temporal";
readonly cloudApiUrlRef: string;
readonly cloudApiUrl: string;
readonly cloudApiAuthorization: { readonly sourceRef: string; readonly sourceKey: string };
readonly mode: "agentrun-native";
readonly temporal: {
readonly address: string;
readonly namespace: string;
readonly taskQueue: string;
readonly activity: {
readonly startToCloseTimeoutMs: number;
readonly retryInitialIntervalMs: number;
readonly retryMaximumIntervalMs: number;
readonly retryMaximumAttempts: number;
};
};
readonly agentRun: {
readonly managerUrl: string;
readonly runtimeApiUrl: string;
readonly runtimeWebUrl: string;
readonly terminalTimeoutMs: number;
readonly pollIntervalMs: number;
};
readonly api: { readonly bindHost: string; readonly probeHost: string; readonly port: number };
readonly worker: { readonly bindHost: string; readonly probeHost: string; readonly healthPort: number };
readonly web: {
@@ -1440,11 +1455,10 @@ function nativeDevelopmentConfig(value: unknown, path: string): HwlabRuntimeNati
const publicHostRef = stringField(workbench, "publicHostRef", `${path}.workbench`);
const publicHost = resolveConfigRefString(publicHostRef, `${path}.workbench.publicHostRef`);
if (isIP(publicHost) === 0) throw new Error(`${path}.workbench.publicHostRef must resolve to a public IP address`);
const mode = enumStringField(workbench, "mode", `${path}.workbench`, ["temporal"]);
const cloudApiUrlRef = stringField(workbench, "cloudApiUrlRef", `${path}.workbench`);
const cloudApiUrl = resolveConfigRefString(cloudApiUrlRef, `${path}.workbench.cloudApiUrlRef`);
if (!/^https?:\/\//u.test(cloudApiUrl)) throw new Error(`${path}.workbench.cloudApiUrlRef must resolve to an http(s) URL`);
const cloudApiAuthorization = asRecord(workbench.cloudApiAuthorization, `${path}.workbench.cloudApiAuthorization`);
const mode = enumStringField(workbench, "mode", `${path}.workbench`, ["agentrun-native"]);
const temporal = asRecord(workbench.temporal, `${path}.workbench.temporal`);
const temporalActivity = asRecord(temporal.activity, `${path}.workbench.temporal.activity`);
const agentRun = asRecord(workbench.agentRun, `${path}.workbench.agentRun`);
const api = asRecord(workbench.api, `${path}.workbench.api`);
const worker = asRecord(workbench.worker, `${path}.workbench.worker`);
const web = asRecord(workbench.web, `${path}.workbench.web`);
@@ -1461,11 +1475,23 @@ function nativeDevelopmentConfig(value: unknown, path: string): HwlabRuntimeNati
publicHost,
stateDir: relativeWorkspacePathField(stringField(workbench, "stateDir", `${path}.workbench`), `${path}.workbench.stateDir`),
mode,
cloudApiUrlRef,
cloudApiUrl,
cloudApiAuthorization: {
sourceRef: sourceRefField(cloudApiAuthorization, "sourceRef", `${path}.workbench.cloudApiAuthorization`),
sourceKey: secretKeyField(cloudApiAuthorization, "sourceKey", `${path}.workbench.cloudApiAuthorization`),
temporal: {
address: stringField(temporal, "address", `${path}.workbench.temporal`),
namespace: stringField(temporal, "namespace", `${path}.workbench.temporal`),
taskQueue: stringField(temporal, "taskQueue", `${path}.workbench.temporal`),
activity: {
startToCloseTimeoutMs: numberField(temporalActivity, "startToCloseTimeoutMs", `${path}.workbench.temporal.activity`),
retryInitialIntervalMs: numberField(temporalActivity, "retryInitialIntervalMs", `${path}.workbench.temporal.activity`),
retryMaximumIntervalMs: numberField(temporalActivity, "retryMaximumIntervalMs", `${path}.workbench.temporal.activity`),
retryMaximumAttempts: numberField(temporalActivity, "retryMaximumAttempts", `${path}.workbench.temporal.activity`),
},
},
agentRun: {
managerUrl: httpUrlField(agentRun, "managerUrl", `${path}.workbench.agentRun`),
runtimeApiUrl: httpUrlField(agentRun, "runtimeApiUrl", `${path}.workbench.agentRun`),
runtimeWebUrl: httpUrlField(agentRun, "runtimeWebUrl", `${path}.workbench.agentRun`),
terminalTimeoutMs: numberField(agentRun, "terminalTimeoutMs", `${path}.workbench.agentRun`),
pollIntervalMs: numberField(agentRun, "pollIntervalMs", `${path}.workbench.agentRun`),
},
api: { bindHost: stringField(api, "bindHost", `${path}.workbench.api`), probeHost: stringField(api, "probeHost", `${path}.workbench.api`), port: numberField(api, "port", `${path}.workbench.api`) },
worker: { bindHost: stringField(worker, "bindHost", `${path}.workbench.worker`), probeHost: stringField(worker, "probeHost", `${path}.workbench.worker`), healthPort: numberField(worker, "healthPort", `${path}.workbench.worker`) },
@@ -1504,6 +1530,17 @@ function nativeDevelopmentConfig(value: unknown, path: string): HwlabRuntimeNati
};
}
function httpUrlField(raw: Record<string, unknown>, key: string, path: string): string {
const value = stringField(raw, key, path);
try {
const parsed = new URL(value);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("unsupported protocol");
} catch {
throw new Error(`${path}.${key} must be an http(s) URL`);
}
return value;
}
function sourceWorkspaceHostDependenciesConfig(value: unknown, path: string): HwlabRuntimeSourceWorkspaceHostDependenciesSpec | undefined {
if (value === undefined) return undefined;
const raw = asRecord(value, path);