diff --git a/docs/reference/g14-gitops-cicd.md b/docs/reference/g14-gitops-cicd.md index 903571cd..279c6553 100644 --- a/docs/reference/g14-gitops-cicd.md +++ b/docs/reference/g14-gitops-cicd.md @@ -119,7 +119,7 @@ G14 Code Agent 通过同一个 repo-owned Codex app-server stdio runner 承载 | `codex-api` | 否 | `HWLAB_CODE_AGENT_CODEX_API_MODEL`,默认 `gpt-5.5` | `HWLAB_CODE_AGENT_CODEX_API_BASE_URL`,G14 默认应指向同 Pod `127.0.0.1` loopback forwarder,forwarder upstream 为 hyueapi | 独立 Codex/OpenAI-compatible Responses API 通道;不得依赖 DeepSeek bridge。 | | `minimax-m3` | 否 | `HWLAB_CODE_AGENT_MINIMAX_M3_MODEL`,默认 `MiniMax-M3` | 不由 HWLAB 配置;由 AgentRun `backendProfile=minimax-m3` 的 Secret/profile 承接 | HWLAB 只把该 profile 委托给 AgentRun v0.1,不在 HWLAB 新增 MiniMax API key、base URL、proxy 或 provider 实现。 | -`hwlab-cloud-api` 仍以 `HWLAB_CODE_AGENT_PROVIDER=codex-stdio` 运行。`HWLAB_CODE_AGENT_MODEL` 与 `HWLAB_CODE_AGENT_OPENAI_BASE_URL` 可作为 runtime-default 兜底,但前端默认 profile 是 `deepseek`,用户可以切到 `codex-api` 或 `minimax-m3`。Codex app-server 启动参数必须同时设置 provider base URL、provider `name`、`model` 和 `review_model`,否则 Codex/DeepSeek 链路可能在模型目录或 `/v1/responses` 阶段退化成 `model=None`。`minimax-m3` 是 AgentRun 委托 profile,HWLAB 只负责把用户选择透传为 AgentRun `backendProfile=minimax-m3`,不得为了该 profile 在 HWLAB 里重新读取 MiniMax Secret 或维护第二套 provider bridge。G14 cloud-api 的 `NO_PROXY/no_proxy` 必须包含 `hyueapi.com` 和 `.hyueapi.com`;`codex-api` 的 hyueapi upstream 由同 Pod loopback forwarder 直连,不能被 proxy 注入污染。 +`hwlab-cloud-api` 仍以 `HWLAB_CODE_AGENT_PROVIDER=codex-stdio` 运行。`HWLAB_CODE_AGENT_MODEL` 与 `HWLAB_CODE_AGENT_OPENAI_BASE_URL` 可作为 runtime-default 兜底,但前端默认 profile 是 `deepseek`,用户可以切到 `codex-api` 或 `minimax-m3`。Codex app-server 启动参数必须同时设置 provider base URL、provider `name`、`model` 和 `review_model`,否则 Codex/DeepSeek 链路可能在模型目录或 `/v1/responses` 阶段退化成 `model=None`。`minimax-m3` 是 AgentRun 委托 profile,HWLAB 只负责把用户选择透传为 AgentRun `backendProfile=minimax-m3`,不得为了该 profile 在 HWLAB 里重新读取 MiniMax Secret 或维护第二套 provider bridge。HWLAB 对外的 conversation/session/thread 合同不随 AgentRun backend 切换而改变;adapter 内部传给 AgentRun 的 `sessionRef.sessionId` 必须按 `backendProfile` 分域,`accessController` 持久化记录仍以 HWLAB 原始 session 归属为准,嵌套 `agentRun.sessionId` 保存 AgentRun scoped sessionRef。G14 cloud-api 的 `NO_PROXY/no_proxy` 必须包含 `hyueapi.com` 和 `.hyueapi.com`;`codex-api` 的 hyueapi upstream 由同 Pod loopback forwarder 直连,不能被 proxy 注入污染。 G14 `codex-api` 不得把 `http://172.26.26.227:17680/v1/responses` 作为默认 base URL;该地址只保留为 D601 legacy Code Queue runner 或历史 egress 对照线索。G14 上的 `codex-api` 应先进入同 Pod `127.0.0.1` loopback forwarder,再由 forwarder 直连 `hyueapi.com` / `.hyueapi.com`;这两个域名必须同时进入 `NO_PROXY` 和 `no_proxy`。DeepSeek profile 可以通过集群内 bridge/Moon Bridge 转换,`codex-api` 不能用 DeepSeek bridge 伪装通过,也不能因为默认 profile 切到 DeepSeek 而删除或覆盖 `codex-api` 的独立模型、base URL、auth 和最小闭环验证。 diff --git a/internal/cloud/code-agent-agentrun-adapter.ts b/internal/cloud/code-agent-agentrun-adapter.ts index 9c03d14f..289e3421 100644 --- a/internal/cloud/code-agent-agentrun-adapter.ts +++ b/internal/cloud/code-agent-agentrun-adapter.ts @@ -151,7 +151,7 @@ export async function submitAgentRunChatTurn({ params = {}, options = {}, traceI } } - const sessionId = agentRunSessionIdForParams(params, traceId); + const sessionId = scopedAgentRunSessionIdForParams(params, traceId, backendProfile); const runInput = buildAgentRunCreateRunInput({ params, env, traceId, backendProfile, sessionId }); const run = await agentRunJson(fetchImpl, managerUrl, "/api/v1/runs", { method: "POST", @@ -365,7 +365,7 @@ export function agentRunSessionEvidence(payload = {}) { lastSeq: payload.agentRun.lastSeq ?? 0, providerId: payload.agentRun.providerId ?? DEFAULT_PROVIDER_ID, reuseEligible: payload.agentRun.reuseEligible ?? false, - sessionId: payload.sessionId ?? payload.agentRun.sessionId ?? null, + sessionId: payload.agentRun.sessionId ?? payload.sessionId ?? null, conversationId: payload.conversationId ?? payload.agentRun.conversationId ?? null, threadId: payload.threadId ?? payload.agentRun.threadId ?? null, providerTrace: payload.providerTrace ?? payload.agentRun.providerTrace ?? null, @@ -398,7 +398,7 @@ function buildAgentRunCreateRunInput({ params, env, traceId, backendProfile, ses valuesPrinted: false }, sessionRef: { - sessionId: sessionId ?? agentRunSessionIdForParams(params, traceId), + sessionId: sessionId ?? scopedAgentRunSessionIdForParams(params, traceId, backendProfile), ...(safeConversationId(params.conversationId) ? { conversationId: safeConversationId(params.conversationId) } : {}), metadata: { adapter: ADAPTER_ID, @@ -408,6 +408,8 @@ function buildAgentRunCreateRunInput({ params, env, traceId, backendProfile, ses requestedThreadId: safeOpaqueId(params.threadId) || null, threadContinuityPolicy: THREAD_CONTINUITY_POLICY, sessionPolicy: SESSION_POLICY_RUN_LOCAL, + agentRunSessionProfile: backendProfile, + agentRunSessionPolicy: "backend-profile-scoped", valuesPrinted: false } }, @@ -443,7 +445,7 @@ function buildAgentRunCommandInput({ params, traceId, backendProfile, sessionId message: prompt, traceId, conversationId: safeConversationId(params.conversationId) || null, - sessionId: sessionId ?? agentRunSessionIdForParams(params, traceId), + sessionId: sessionId ?? scopedAgentRunSessionIdForParams(params, traceId, backendProfile), hwlabSessionId: safeSessionId(params.sessionId) || null, threadId: null, requestedThreadId: safeOpaqueId(params.threadId) || null, @@ -491,18 +493,18 @@ function buildAgentRunTransientEnv(env = process.env) { } async function resolveReusableAgentRun({ params = {}, options = {}, env = process.env, managerUrl, fetchImpl, timeoutMs, backendProfile, traceId, traceStore }) { - const sessionId = agentRunSessionIdForParams(params, traceId); - if (!safeSessionId(sessionId) || sessionId === agentRunSessionId(traceId) || typeof options.accessController?.getAgentSession !== "function") { + const hwlabSessionId = hwlabSessionIdForParams(params, traceId); + if (!safeSessionId(hwlabSessionId) || hwlabSessionId === agentRunSessionId(traceId) || typeof options.accessController?.getAgentSession !== "function") { return null; } - const session = await options.accessController.getAgentSession(sessionId); + const session = await options.accessController.getAgentSession(hwlabSessionId); if (!canReuseAgentRunSessionForOwner(session, params, options)) { traceStore.append(traceId, { type: "backend", status: "running", label: "agentrun:run:reuse-owner-skipped", message: "Stored AgentRun session is not visible to the current actor; a new runner Job will be created for this request.", - sessionId, + sessionId: hwlabSessionId, waitingFor: "agentrun-run-create", valuesPrinted: false }); @@ -515,7 +517,7 @@ async function resolveReusableAgentRun({ params = {}, options = {}, env = proces status: "running", label: "agentrun:run:reuse-skipped", message: "No reusable AgentRun run was found for this HWLAB session; a new runner Job will be created.", - sessionId, + sessionId: hwlabSessionId, waitingFor: "agentrun-run-create", valuesPrinted: false }); @@ -535,7 +537,7 @@ async function resolveReusableAgentRun({ params = {}, options = {}, env = proces message: `Stored AgentRun run ${mapping.runId} could not be read; a new runner Job will be created.`, errorCode: error?.code ?? "agentrun_run_lookup_failed", runId: mapping.runId, - sessionId, + sessionId: hwlabSessionId, waitingFor: "agentrun-run-create", valuesPrinted: false }); @@ -549,7 +551,7 @@ async function resolveReusableAgentRun({ params = {}, options = {}, env = proces label: "agentrun:run:reuse-blocked", message: `Stored AgentRun run ${mapping.runId} cannot be reused: ${reason}; a new runner Job will be created.`, runId: mapping.runId, - sessionId, + sessionId: hwlabSessionId, waitingFor: "agentrun-run-create", valuesPrinted: false }); @@ -559,17 +561,17 @@ async function resolveReusableAgentRun({ params = {}, options = {}, env = proces type: "backend", status: "running", label: "agentrun:run:reused", - message: `AgentRun run ${mapping.runId} is reused for HWLAB session ${sessionId}; runner lease is active and no new bundle will be requested for this turn.`, + message: `AgentRun run ${mapping.runId} is reused for HWLAB session ${hwlabSessionId}; runner lease is active and no new bundle will be requested for this turn.`, runId: mapping.runId, commandId: mapping.commandId ?? null, runnerId: mapping.runnerId ?? null, jobName: mapping.jobName ?? null, namespace: mapping.namespace ?? null, - sessionId, + sessionId: hwlabSessionId, waitingFor: "agentrun-command-create", valuesPrinted: false }); - return { sessionId, session, mapping, run }; + return { sessionId: mapping.sessionId ?? scopedAgentRunSessionIdForParams(params, traceId, backendProfile), hwlabSessionId, session, mapping, run }; } function isReusableAgentRunMapping(mapping, backendProfile) { @@ -1014,10 +1016,21 @@ function agentRunResultRefs(result = {}) { return refs; } -function agentRunSessionIdForParams(params = {}, traceId) { +function hwlabSessionIdForParams(params = {}, traceId) { return safeSessionId(params.sessionId) || agentRunSessionId(traceId); } +function scopedAgentRunSessionIdForParams(params = {}, traceId, backendProfile) { + const baseSessionId = hwlabSessionIdForParams(params, traceId); + const profile = agentRunSessionProfileToken(backendProfile); + const base = String(baseSessionId).replace(/^ses_/u, "").replace(/[^A-Za-z0-9_]+/gu, "_").replace(/^_+|_+$/gu, "") || "session"; + return `ses_agentrun_${profile}_${base}`; +} + +function agentRunSessionProfileToken(backendProfile) { + return String(backendProfile ?? "deepseek").trim().toLowerCase().replace(/[^a-z0-9]+/gu, "_").replace(/^_+|_+$/gu, "") || "default"; +} + function agentRunSessionId(traceId) { return `ses_agentrun_${String(safeTraceId(traceId) || `trc_${randomUUID()}`).slice(4)}`; } diff --git a/internal/cloud/server-agent-chat.test.ts b/internal/cloud/server-agent-chat.test.ts index a88cca21..5692ce10 100644 --- a/internal/cloud/server-agent-chat.test.ts +++ b/internal/cloud/server-agent-chat.test.ts @@ -159,7 +159,7 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte leaseExpiresAt: "2999-01-01T00:00:00.000Z", backendProfile: "deepseek", sessionRef: { - sessionId: "ses_server-test-agentrun", + sessionId: "ses_agentrun_deepseek_server_test_agentrun", conversationId: "cnv_server-test-agentrun", threadId: "019e8078-db67-7750-a5d9-1a99f3abd445", metadata: {} @@ -175,8 +175,10 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte { name: "hwpod", path: "tools/device-pod-cli.mjs", kind: "node-script" } ]); assert.equal(Object.hasOwn(body.sessionRef, "threadId"), false); - assert.equal(body.sessionRef.sessionId, "ses_server-test-agentrun"); + assert.equal(body.sessionRef.sessionId, "ses_agentrun_deepseek_server_test_agentrun"); assert.equal(body.sessionRef.metadata.hwlabSessionId, "ses_server-test-agentrun"); + assert.equal(body.sessionRef.metadata.agentRunSessionProfile, "deepseek"); + assert.equal(body.sessionRef.metadata.agentRunSessionPolicy, "backend-profile-scoped"); assert.equal(body.sessionRef.metadata.requestedThreadId, "019e8078-db67-7750-a5d9-1a99f3abd445"); assert.equal(body.sessionRef.metadata.threadContinuityPolicy, "hwlab-agentrun-v01-reuse-runner-thread"); assert.equal(body.sessionRef.metadata.sessionPolicy, "hwlab-agentrun-v01-session-runner-reuse"); @@ -185,7 +187,7 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte if (request.method === "POST" && url.pathname === "/api/v1/runs/run_hwlab_adapter/commands") { assert.equal(body.type, "turn"); assert.match(body.payload.prompt, /AgentRun adapter/u); - assert.equal(body.payload.sessionId, "ses_server-test-agentrun"); + assert.equal(body.payload.sessionId, "ses_agentrun_deepseek_server_test_agentrun"); assert.equal(body.payload.hwlabSessionId, "ses_server-test-agentrun"); assert.equal(body.payload.threadId, null); assert.equal(body.payload.requestedThreadId, "019e8078-db67-7750-a5d9-1a99f3abd445"); @@ -256,7 +258,7 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte reply: "AgentRun adapter 已接管 HWLAB Code Agent。", lastSeq: 5, eventCount: 5, - sessionRef: { sessionId: "ses_server-test-agentrun", conversationId: "cnv_server-test-agentrun", threadId: "019e8078-db67-7750-a5d9-1a99f3abd445" } + sessionRef: { sessionId: "ses_agentrun_deepseek_server_test_agentrun", conversationId: "cnv_server-test-agentrun", threadId: "019e8078-db67-7750-a5d9-1a99f3abd445" } }); } if (request.method === "GET" && url.pathname === "/api/v1/runs/run_hwlab_adapter/commands/cmd_hwlab_adapter_second/result") { @@ -275,7 +277,7 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte reply: "AgentRun adapter 复用已有 runner 完成第二轮。", lastSeq: 6, eventCount: 6, - sessionRef: { sessionId: "ses_server-test-agentrun", conversationId: "cnv_server-test-agentrun", threadId: "019e8078-db67-7750-a5d9-1a99f3abd445" } + sessionRef: { sessionId: "ses_agentrun_deepseek_server_test_agentrun", conversationId: "cnv_server-test-agentrun", threadId: "019e8078-db67-7750-a5d9-1a99f3abd445" } }); } response.writeHead(404, { "content-type": "application/json" }); @@ -429,6 +431,10 @@ test("cloud api AgentRun adapter maps minimax-m3 provider profile to AgentRun ba }; if (request.method === "POST" && url.pathname === "/api/v1/runs") { assert.equal(body.backendProfile, "minimax-m3"); + assert.equal(body.sessionRef.sessionId, "ses_agentrun_minimax_m3_server_test_minimax_m3"); + assert.equal(body.sessionRef.metadata.hwlabSessionId, "ses_server-test-minimax-m3"); + assert.equal(body.sessionRef.metadata.agentRunSessionProfile, "minimax-m3"); + assert.equal(body.sessionRef.metadata.agentRunSessionPolicy, "backend-profile-scoped"); assert.equal(body.executionPolicy.secretScope.providerCredentials[0].profile, "minimax-m3"); assert.equal(body.executionPolicy.secretScope.providerCredentials[0].secretRef.name, "agentrun-v01-provider-minimax-m3"); assert.equal(body.executionPolicy.secretScope.providerCredentials[0].secretRef.namespace, "agentrun-v01"); @@ -436,6 +442,8 @@ test("cloud api AgentRun adapter maps minimax-m3 provider profile to AgentRun ba } if (request.method === "POST" && url.pathname === "/api/v1/runs/run_hwlab_minimax_m3/commands") { assert.equal(body.payload.providerProfile, "minimax-m3"); + assert.equal(body.payload.sessionId, "ses_agentrun_minimax_m3_server_test_minimax_m3"); + assert.equal(body.payload.hwlabSessionId, "ses_server-test-minimax-m3"); assert.match(body.payload.prompt, /MiniMax-M3/u); return send({ id: "cmd_hwlab_minimax_m3", runId: "run_hwlab_minimax_m3", state: "pending", type: "turn", seq: 1 }); } @@ -476,7 +484,7 @@ test("cloud api AgentRun adapter maps minimax-m3 provider profile to AgentRun ba reply: "AGENTRUN_MINIMAX_M3_OK", lastSeq: 3, eventCount: 3, - sessionRef: { sessionId: "ses_server-test-minimax-m3", conversationId: "cnv_server-test-minimax-m3", threadId: null } + sessionRef: { sessionId: "ses_agentrun_minimax_m3_server_test_minimax_m3", conversationId: "cnv_server-test-minimax-m3", threadId: null } }); } response.writeHead(404, { "content-type": "application/json" }); @@ -526,6 +534,8 @@ test("cloud api AgentRun adapter maps minimax-m3 provider profile to AgentRun ba assert.equal(payload.status, "completed"); assert.equal(payload.infrastructureBackend, "agentrun-v01/minimax-m3"); assert.equal(payload.agentRun.backendProfile, "minimax-m3"); + assert.equal(payload.sessionId, "ses_server-test-minimax-m3"); + assert.equal(payload.agentRun.sessionId, "ses_agentrun_minimax_m3_server_test_minimax_m3"); assert.equal(payload.providerTrace.backendProfile, "minimax-m3"); assert.equal(payload.agentRun.jobName, "agentrun-v01-runner-hwlab-minimax-m3"); assert.equal(payload.reply.content, "AGENTRUN_MINIMAX_M3_OK"); @@ -536,6 +546,172 @@ test("cloud api AgentRun adapter maps minimax-m3 provider profile to AgentRun ba } }); +test("cloud api AgentRun adapter scopes AgentRun sessions by backend profile", async () => { + const calls = []; + const hwlabSessionId = "ses_server-test-profile-switch"; + const minimaxSessionId = "ses_agentrun_minimax_m3_server_test_profile_switch"; + const ownerSessions = new Map([[hwlabSessionId, { + ownerUserId: "usr_agent_owner", + ownerRole: "user", + sessionId: hwlabSessionId, + conversationId: "cnv_server-test-profile-switch", + threadId: "019e8078-db67-7750-a5d9-1a99f3abd445", + traceId: "trc_existing_deepseek", + status: "active", + session: { + agentRun: { + adapter: "agentrun-v01", + backendProfile: "deepseek", + runId: "run_existing_deepseek", + commandId: "cmd_existing_deepseek", + jobName: "agentrun-v01-runner-existing-deepseek", + namespace: "agentrun-v01", + sessionId: "ses_agentrun_deepseek_server_test_profile_switch", + reuseEligible: true, + managerUrl: "http://127.0.0.1:1", + status: "runner-job-created" + } + } + }]]); + + const agentRunServer = createHttpServer(async (request, response) => { + const url = new URL(request.url || "/", "http://127.0.0.1"); + const chunks = []; + for await (const chunk of request) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + const body = chunks.length ? JSON.parse(Buffer.concat(chunks).toString("utf8")) : null; + calls.push({ method: request.method, path: url.pathname, body }); + const send = (data) => { + response.writeHead(200, { "content-type": "application/json" }); + response.end(`${JSON.stringify({ ok: true, data, traceId: "trc_fake_profile_switch" })}\n`); + }; + if (request.method === "GET" && url.pathname === "/api/v1/runs/run_existing_deepseek") { + response.writeHead(500, { "content-type": "application/json" }); + response.end(`${JSON.stringify({ ok: false, failureKind: "unexpected-reuse", message: "deepseek run must not be reused for minimax-m3" })}\n`); + return; + } + if (request.method === "POST" && url.pathname === "/api/v1/runs") { + assert.equal(body.backendProfile, "minimax-m3"); + assert.equal(body.sessionRef.sessionId, minimaxSessionId); + assert.equal(body.sessionRef.metadata.hwlabSessionId, hwlabSessionId); + assert.equal(body.sessionRef.metadata.agentRunSessionProfile, "minimax-m3"); + return send({ id: "run_hwlab_profile_switch_m3", status: "pending", backendProfile: "minimax-m3", sessionRef: body.sessionRef, resourceBundleRef: body.resourceBundleRef }); + } + if (request.method === "POST" && url.pathname === "/api/v1/runs/run_hwlab_profile_switch_m3/commands") { + assert.equal(body.payload.providerProfile, "minimax-m3"); + assert.equal(body.payload.sessionId, minimaxSessionId); + assert.equal(body.payload.hwlabSessionId, hwlabSessionId); + return send({ id: "cmd_hwlab_profile_switch_m3", runId: "run_hwlab_profile_switch_m3", state: "pending", type: "turn", seq: 1 }); + } + if (request.method === "POST" && url.pathname === "/api/v1/runs/run_hwlab_profile_switch_m3/runner-jobs") { + assert.equal(body.commandId, "cmd_hwlab_profile_switch_m3"); + return send({ + action: "create-kubernetes-job", + runId: "run_hwlab_profile_switch_m3", + commandId: "cmd_hwlab_profile_switch_m3", + attemptId: "attempt_hwlab_profile_switch_m3", + runnerId: "runner_hwlab_profile_switch_m3", + namespace: "agentrun-v01", + jobName: "agentrun-v01-runner-profile-switch-m3", + jobIdentity: { namespace: "agentrun-v01", name: "agentrun-v01-runner-profile-switch-m3" }, + runner: { attemptId: "attempt_hwlab_profile_switch_m3", runnerId: "runner_hwlab_profile_switch_m3" } + }); + } + if (request.method === "GET" && url.pathname === "/api/v1/runs/run_hwlab_profile_switch_m3/events") { + return send({ items: [ + { id: "evt_profile_switch_1", runId: "run_hwlab_profile_switch_m3", seq: 1, type: "backend_status", payload: { phase: "runner-job-created", commandId: "cmd_hwlab_profile_switch_m3", attemptId: "attempt_hwlab_profile_switch_m3", jobName: "agentrun-v01-runner-profile-switch-m3", namespace: "agentrun-v01" }, createdAt: "2026-06-02T00:00:00.000Z" }, + { id: "evt_profile_switch_2", runId: "run_hwlab_profile_switch_m3", seq: 2, type: "assistant_message", payload: { commandId: "cmd_hwlab_profile_switch_m3", text: "AGENTRUN_HWLAB_MINIMAX_M3_OK" }, createdAt: "2026-06-02T00:00:01.000Z" }, + { id: "evt_profile_switch_3", runId: "run_hwlab_profile_switch_m3", seq: 3, type: "terminal_status", payload: { commandId: "cmd_hwlab_profile_switch_m3", terminalStatus: "completed" }, createdAt: "2026-06-02T00:00:02.000Z" } + ] }); + } + if (request.method === "GET" && url.pathname === "/api/v1/runs/run_hwlab_profile_switch_m3/commands/cmd_hwlab_profile_switch_m3/result") { + return send({ + runId: "run_hwlab_profile_switch_m3", + commandId: "cmd_hwlab_profile_switch_m3", + attemptId: "attempt_hwlab_profile_switch_m3", + runnerId: "runner_hwlab_profile_switch_m3", + jobName: "agentrun-v01-runner-profile-switch-m3", + namespace: "agentrun-v01", + status: "completed", + runStatus: "completed", + commandState: "completed", + terminalStatus: "completed", + completed: true, + reply: "AGENTRUN_HWLAB_MINIMAX_M3_OK", + lastSeq: 3, + eventCount: 3, + sessionRef: { sessionId: minimaxSessionId, conversationId: "cnv_server-test-profile-switch", threadId: "019e8078-db67-7750-a5d9-1a99f3abd445" } + }); + } + response.writeHead(404, { "content-type": "application/json" }); + response.end(`${JSON.stringify({ ok: false, failureKind: "schema-invalid", message: `unexpected ${request.method} ${url.pathname}`, traceId: "trc_fake_profile_switch" })}\n`); + }); + await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve)); + + const agentRunPort = agentRunServer.address().port; + const server = createCloudApiServer({ + env: { + HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01", + AGENTRUN_MGR_URL: `http://127.0.0.1:${agentRunPort}`, + HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1", + HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567", + HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek", + HWLAB_ENVIRONMENT: "v02", + HWLAB_GITOPS_PROFILE: "v02" + }, + accessController: { + required: false, + async authenticate() { + return { ok: true, actor: null, session: null }; + }, + async recordAgentSessionOwner(input) { + ownerSessions.set(input.sessionId, input); + return input; + }, + async getAgentSession(sessionId) { + return ownerSessions.get(sessionId) ?? null; + } + } + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + + try { + const { port } = server.address(); + const traceId = "trc_server-test-agentrun-profile-switch"; + const submit = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, { + method: "POST", + headers: { "content-type": "application/json", "x-trace-id": traceId }, + body: JSON.stringify({ + conversationId: "cnv_server-test-profile-switch", + sessionId: hwlabSessionId, + ownerUserId: "usr_agent_owner", + ownerRole: "user", + threadId: "019e8078-db67-7750-a5d9-1a99f3abd445", + providerProfile: "minimax-m3", + message: "Switch this HWLAB session to MiniMax-M3 through AgentRun" + }) + }); + assert.equal(submit.status, 202); + + const payload = await pollAgentResult(port, traceId); + validateCodeAgentChatSchema(payload); + assert.equal(payload.status, "completed"); + assert.equal(payload.sessionId, hwlabSessionId); + assert.equal(payload.infrastructureBackend, "agentrun-v01/minimax-m3"); + assert.equal(payload.agentRun.backendProfile, "minimax-m3"); + assert.equal(payload.agentRun.runId, "run_hwlab_profile_switch_m3"); + assert.equal(payload.agentRun.sessionId, minimaxSessionId); + assert.equal(payload.reply.content, "AGENTRUN_HWLAB_MINIMAX_M3_OK"); + assert.equal(calls.some((call) => call.method === "GET" && call.path === "/api/v1/runs/run_existing_deepseek"), false); + assert.equal(calls.filter((call) => call.method === "POST" && call.path === "/api/v1/runs").length, 1); + const stored = ownerSessions.get(hwlabSessionId); + assert.equal(stored.session.agentRun.backendProfile, "minimax-m3"); + assert.equal(stored.session.agentRun.sessionId, minimaxSessionId); + } finally { + await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))); + await new Promise((resolve, reject) => agentRunServer.close((error) => (error ? reject(error) : resolve()))); + } +}); + test("cloud api AgentRun adapter rejects non-internal manager URLs by default", async () => { const server = createCloudApiServer({ env: {