From aeefb7660bd40b999c7370a081f14a1eef2a6c84 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Fri, 5 Jun 2026 02:06:09 +0800 Subject: [PATCH] fix(v02): report AgentRun code agent health --- internal/cloud/code-agent-agentrun-adapter.ts | 185 ++++++++++++++++++ internal/cloud/code-agent-chat.ts | 8 + internal/cloud/health-contract.ts | 94 ++++++++- internal/cloud/server-health.test.ts | 77 ++++++++ 4 files changed, 359 insertions(+), 5 deletions(-) diff --git a/internal/cloud/code-agent-agentrun-adapter.ts b/internal/cloud/code-agent-agentrun-adapter.ts index 7c0b7a0c..8380499c 100644 --- a/internal/cloud/code-agent-agentrun-adapter.ts +++ b/internal/cloud/code-agent-agentrun-adapter.ts @@ -31,6 +31,9 @@ const DEFAULT_PROVIDER_ID = "G14"; const DEFAULT_REPO_URL = "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git"; const DEFAULT_RUNNER_NAMESPACE = "agentrun-v01"; const DEFAULT_TIMEOUT_MS = 1_200_000; +const AGENTRUN_RUNNER_KIND = "agentrun-v01-shared-runner"; +const AGENTRUN_SESSION_MODE = "agentrun-v01-durable-session"; +const AGENTRUN_IMPLEMENTATION_TYPE = "agentrun-v01-shared-execution-infra"; const AGENTRUN_BACKENDS = Object.freeze(["codex", "deepseek", "minimax-m3"]); const THREAD_CONTINUITY_POLICY = "hwlab-agentrun-v01-reuse-runner-thread"; const SESSION_POLICY_RUN_LOCAL = "hwlab-agentrun-v01-session-runner-reuse"; @@ -52,6 +55,160 @@ export function codeAgentAgentRunAdapterEnabled(env = process.env) { return value === ADAPTER_ID || value === "agentrun" || value === "agentrun-v0.1"; } +export function describeAgentRunAdapterAvailability(env = process.env, options = {}) { + const params = options.params ?? {}; + const backendProfile = resolveAgentRunBackendProfile(env, params); + const provider = providerForBackendProfile(backendProfile); + const backend = `${ADAPTER_ID}/${backendProfile}`; + const model = modelForBackendProfile(backendProfile, env); + const blockers = []; + let managerUrl = null; + let repoUrl = null; + + try { + managerUrl = resolveAgentRunManagerUrl(env); + } catch (error) { + blockers.push(agentRunAvailabilityBlocker(error, "manager-url")); + } + try { + repoUrl = resolveAgentRunRepoUrl(env); + } catch (error) { + blockers.push(agentRunAvailabilityBlocker(error, "repo-url")); + } + + const providerId = firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID, env.AGENTRUN_PROVIDER_ID, DEFAULT_PROVIDER_ID); + const runnerNamespace = firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_RUNNER_NAMESPACE, env.AGENTRUN_RUNTIME_NAMESPACE, DEFAULT_RUNNER_NAMESPACE); + const ready = blockers.length === 0 && Boolean(providerId && runnerNamespace && backendProfile && managerUrl && repoUrl); + const managerHost = hostForUrl(managerUrl); + const repoHost = hostForUrl(repoUrl); + const status = ready ? "agentrun-ready" : "blocked"; + const capabilityLevel = ready ? CODEX_STDIO_CAPABILITY_LEVEL : "blocked"; + + return { + endpoint: "POST /v1/agent/chat", + adapter: ADAPTER_ID, + provider, + model, + backend, + infrastructureBackend: backend, + mode: ADAPTER_ID, + schema: [ + "conversationId", + "sessionId", + "messageId", + "status", + "createdAt", + "updatedAt", + "traceId", + "provider", + "model", + "backend", + "infrastructureBackend", + "agentRun", + "runner", + "runnerTrace", + "session", + "sessionMode", + "sessionReuse", + "threadContinuityPolicy", + "error.message", + "error.code" + ], + runner: { + kind: AGENTRUN_RUNNER_KIND, + adapter: ADAPTER_ID, + backend, + provider, + model, + namespace: runnerNamespace, + managerHost, + repoHost, + mode: ADAPTER_ID, + sessionMode: AGENTRUN_SESSION_MODE, + implementationType: AGENTRUN_IMPLEMENTATION_TYPE, + status: ready ? "available" : "blocked", + ready, + capabilityLevel, + longLivedSession: ready, + durableSession: ready, + durable: ready, + codexStdio: false, + delegatedToAgentRun: true, + writeCapable: ready, + readOnly: false, + runnerLimitations: ready ? [] : ["agentrun-adapter-config-blocked"], + secretMaterialRead: false, + valuesRedacted: true + }, + agentRun: { + adapter: ADAPTER_ID, + backendProfile, + providerId, + runnerNamespace, + managerUrl, + managerHost, + repoUrl, + repoHost, + managerConfigured: Boolean(managerUrl), + repoConfigured: Boolean(repoUrl), + internalServiceDns: managerHost === "agentrun-mgr.agentrun-v01.svc.cluster.local", + ready, + valuesPrinted: false + }, + status, + readinessStatus: status, + agentKind: "agentrun-v01-adapter", + capabilityStatus: status, + blocker: ready ? null : blockers[0]?.summary ?? "AgentRun adapter runtime configuration is blocked.", + reason: ready ? null : blockers[0]?.code ?? "agentrun_adapter_blocked", + summary: ready + ? "HWLAB Code Agent delegates execution to AgentRun v0.1 shared infrastructure; hwlab-cloud-api keeps session/trace/API ownership and no longer manages a repo-owned runner control plane." + : "HWLAB Code Agent is configured for AgentRun v0.1, but the adapter runtime configuration is incomplete or invalid.", + missingEnv: [], + secretRefs: [], + egress: { + ready, + mode: "agentrun-managed-provider-egress", + directPublicOpenAi: false, + secretMaterialRead: false, + valuesRedacted: true + }, + safety: { + secretsRead: false, + secretValuesPrinted: false, + kubeconfigRead: false, + providerCredentialsDelegatedToAgentRun: true, + repoOwnedRunnerControlPlane: false, + valuesRedacted: true + }, + capabilityLevel, + workspace: repoUrl, + sandbox: firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_SANDBOX, env.HWLAB_CODE_AGENT_CODEX_SANDBOX, "danger-full-access"), + session: null, + sessionStatus: null, + sessionMode: AGENTRUN_SESSION_MODE, + implementationType: AGENTRUN_IMPLEMENTATION_TYPE, + runnerLimitations: ready ? [] : ["agentrun-adapter-config-blocked"], + longLivedSessionGate: { + status: ready ? "pass" : "blocked", + provider, + runnerKind: AGENTRUN_RUNNER_KIND, + sessionMode: AGENTRUN_SESSION_MODE, + implementationType: AGENTRUN_IMPLEMENTATION_TYPE, + adapter: ADAPTER_ID, + codexStdio: false, + delegatedToAgentRun: true, + blockers, + secretMaterialRead: false, + valuesRedacted: true + }, + blockers, + blockerCodes: blockers.map((blocker) => blocker.code).filter(Boolean), + ready, + partialReady: false + }; +} + export function initialAgentRunChatResult({ params = {}, options = {}, traceId }) { const env = options.env ?? process.env; const timestamp = nowIso(options.now); @@ -1540,6 +1697,34 @@ function modelForBackendProfile(profile, env = process.env) { return firstNonEmpty(env.HWLAB_CODE_AGENT_DEEPSEEK_MODEL, "deepseek-chat"); } +function providerForBackendProfile(profile) { + if (profile === "codex") return "codex-api"; + if (profile === "minimax-m3") return "minimax-m3"; + return "deepseek"; +} + +function agentRunAvailabilityBlocker(error, scope) { + return { + code: error?.code ?? `agentrun_${String(scope).replace(/[^a-z0-9]+/giu, "_")}_blocked`, + type: "agent_blocker", + scope: `agentrun-${scope}`, + status: "open", + sourceIssue: "pikasTech/HWLAB#879", + summary: error?.message ?? `AgentRun ${scope} is not configured correctly.` , + secretMaterialRead: false, + valuesRedacted: true + }; +} + +function hostForUrl(value) { + if (!value) return null; + try { + return new URL(value).hostname.toLowerCase(); + } catch { + return null; + } +} + function waitingForForPhase(phase) { if (phase.includes("runner-job")) return "agentrun-runner"; if (phase.includes("resource-bundle")) return "agentrun-resource-bundle"; diff --git a/internal/cloud/code-agent-chat.ts b/internal/cloud/code-agent-chat.ts index 9dba7276..1d31e187 100644 --- a/internal/cloud/code-agent-chat.ts +++ b/internal/cloud/code-agent-chat.ts @@ -31,6 +31,10 @@ import { codeAgentSessionLifecycleSummary, decorateCodeAgentSession } from "./code-agent-session-lifecycle.ts"; +import { + codeAgentAgentRunAdapterEnabled, + describeAgentRunAdapterAvailability +} from "./code-agent-agentrun-adapter.ts"; const DEFAULT_MODEL = DEV_CODE_AGENT_PROVIDER_CONTRACT.model; const DEFAULT_PROJECT_ID = "prj_hwlab-cloud-workbench"; const READONLY_RUNNER_PROVIDER = "codex-readonly-runner"; @@ -459,6 +463,10 @@ function decorateChatSessionLifecycle(payload) { } export async function describeCodeAgentAvailability(env = process.env, options = {}) { + if (codeAgentAgentRunAdapterEnabled(env)) { + return describeAgentRunAdapterAvailability(env, options); + } + const providerPlan = resolveProviderPlan(env, options); const providerContract = inspectCodeAgentProviderEnv(env); const missingEnv = providerPlan.mode === "openai" diff --git a/internal/cloud/health-contract.ts b/internal/cloud/health-contract.ts index 54d359ff..89215beb 100644 --- a/internal/cloud/health-contract.ts +++ b/internal/cloud/health-contract.ts @@ -78,6 +78,9 @@ function isDbReady(db) { } function isCodeAgentReady(codeAgent) { + if (isAgentRunCodeAgent(codeAgent)) { + return Boolean(codeAgent?.ready === true && codeAgent?.agentRun?.managerConfigured !== false && codeAgent?.agentRun?.ready !== false); + } return Boolean( codeAgent?.ready === true && codeAgent?.longLivedSessionGate?.status === "pass" && @@ -116,18 +119,21 @@ function dbBlocker(db) { function codeAgentBlocker(codeAgent) { const primaryBlocker = Array.isArray(codeAgent?.blockers) ? codeAgent.blockers[0] : null; + const agentRun = isAgentRunCodeAgent(codeAgent); return { code: codeAgent?.reason ?? primaryBlocker?.code ?? "code_agent_long_lived_session_blocked", type: "agent_blocker", - scope: "code-agent-long-lived-session", + scope: agentRun ? "code-agent-agentrun-adapter" : "code-agent-long-lived-session", status: "open", - sourceIssue: primaryBlocker?.sourceIssue ?? "pikasTech/HWLAB#275", + sourceIssue: primaryBlocker?.sourceIssue ?? (agentRun ? "pikasTech/HWLAB#879" : "pikasTech/HWLAB#275"), summary: codeAgent?.summary ?? "Code Agent provider readiness is not established", evidence: { + adapter: codeAgent?.adapter ?? codeAgent?.agentRun?.adapter ?? null, provider: codeAgent?.provider ?? "unknown", backend: codeAgent?.backend ?? "unknown", agentKind: codeAgent?.agentKind ?? "unknown", capabilityStatus: codeAgent?.capabilityStatus ?? "unknown", + agentRun: codeAgent?.agentRun ?? null, longLivedSessionGate: codeAgent?.longLivedSessionGate ?? null, codexStdioFeasibility: codeAgent?.codexStdioFeasibility ?? null, partialReady: codeAgent?.partialReady === true, @@ -205,6 +211,27 @@ function buildRuntimeDurabilityReadiness({ db, runtime, runtimeReady, runtimeBlo } function buildProviderReadiness(codeAgent = {}) { + if (isAgentRunCodeAgent(codeAgent)) { + const ready = codeAgent?.ready === true && codeAgent?.agentRun?.ready !== false; + return { + status: ready ? "ready" : "blocked", + ready, + configured: ready, + adapter: codeAgent.adapter ?? codeAgent.agentRun?.adapter ?? "agentrun-v01", + provider: codeAgent?.provider ?? "unknown", + model: codeAgent?.model ?? "unknown", + backend: codeAgent?.backend ?? "unknown", + mode: codeAgent?.mode ?? "agentrun-v01", + backendProfile: codeAgent?.agentRun?.backendProfile ?? null, + evidence: "agentrun_runtime_adapter_config", + egressReady: codeAgent?.egress?.ready !== false, + missingEnv: [], + blocker: ready ? null : firstCode(codeAgent?.reason, codeAgent?.blockers?.[0]?.code, "agentrun_adapter_blocked"), + secretMaterialRead: false, + valuesRedacted: true + }; + } + const fallback = codeAgent?.fallback ?? {}; const fallbackReady = fallback.ready === true || fallback.status === "available"; const tokenBoundary = codeAgent?.codexStdioFeasibility?.tokenBoundary ?? codeAgent?.codexStdio?.tokenBoundary ?? null; @@ -230,6 +257,32 @@ function buildProviderReadiness(codeAgent = {}) { } function buildSessionRunnerReadiness(codeAgent = {}) { + if (isAgentRunCodeAgent(codeAgent)) { + const runner = codeAgent?.runner ?? {}; + const ready = codeAgent?.ready === true && runner.ready !== false; + return { + status: ready ? "agentrun_ready" : "blocked", + ready, + kind: runner.kind ?? "agentrun-v01-shared-runner", + adapter: codeAgent.adapter ?? codeAgent.agentRun?.adapter ?? "agentrun-v01", + provider: codeAgent?.provider ?? runner.provider ?? "unknown", + backend: codeAgent?.backend ?? runner.backend ?? "unknown", + mode: runner.sessionMode ?? runner.mode ?? codeAgent?.sessionMode ?? "agentrun-v01-durable-session", + capabilityLevel: runner.capabilityLevel ?? codeAgent?.capabilityLevel ?? "unknown", + longLivedSession: runner.longLivedSession !== false, + durableSession: runner.durableSession !== false, + codexStdio: false, + delegatedToAgentRun: true, + writeCapable: runner.writeCapable !== false, + readOnly: false, + namespace: runner.namespace ?? codeAgent?.agentRun?.runnerNamespace ?? null, + managerHost: runner.managerHost ?? codeAgent?.agentRun?.managerHost ?? null, + blocker: ready ? null : firstCode(runner.blocker, codeAgent?.reason, codeAgent?.blockers?.[0]?.code, "agentrun_session_runner_blocked"), + runnerLimitations: Array.isArray(codeAgent?.runnerLimitations) ? [...codeAgent.runnerLimitations] : [], + secretMaterialRead: false + }; + } + const stdio = codeAgent?.codexStdioFeasibility ?? codeAgent?.codexStdio ?? {}; const stdioCommandReady = stdio.commandProbe?.ready === true; const stdioReady = (stdio.ready === true || stdio.canStartLongLivedCodexStdio === true) && stdioCommandReady; @@ -263,6 +316,29 @@ function buildSessionRunnerReadiness(codeAgent = {}) { } function buildCodexStdioReadiness(codeAgent = {}) { + if (isAgentRunCodeAgent(codeAgent)) { + return { + status: "not_applicable", + ready: false, + feasible: false, + adapter: codeAgent.adapter ?? codeAgent.agentRun?.adapter ?? "agentrun-v01", + delegatedToAgentRun: true, + blocker: null, + blockerCodes: [], + sourceIssue: "pikasTech/HWLAB#879", + commandPresent: false, + commandExecutable: false, + nativeDependencyPresent: false, + commandProbeReady: false, + commandProbe: null, + supervisorConfigured: false, + workspaceReady: false, + tokenBoundaryPresent: false, + egressReady: true, + secretMaterialRead: false + }; + } + const stdio = codeAgent?.codexStdioFeasibility ?? codeAgent?.codexStdio ?? {}; const commandReady = stdio.commandProbe?.ready === true; const ready = (stdio.ready === true || stdio.canStartLongLivedCodexStdio === true) && commandReady; @@ -297,24 +373,27 @@ function buildCodexStdioReadiness(codeAgent = {}) { } function buildCodeAgentStructuredReadiness({ provider, dbDurable, sessionRunner, codexStdio, codeAgent } = {}) { + const agentRun = isAgentRunCodeAgent(codeAgent); const currentBlockers = [ provider?.ready === false ? provider.blocker ?? "provider_config_blocked" : null, dbDurable?.ready === false ? dbDurable.blocker ?? "runtime_durable_adapter_not_ready" : null, sessionRunner?.ready === false ? sessionRunner.blocker ?? "session_runner_blocked" : null, - codexStdio?.ready === false ? codexStdio.blocker ?? "codex_stdio_feasibility_blocked" : null + !agentRun && codexStdio?.ready === false ? codexStdio.blocker ?? "codex_stdio_feasibility_blocked" : null ].filter(Boolean); const ready = provider?.ready === true && dbDurable?.ready === true && sessionRunner?.ready === true && - codexStdio?.ready === true && + (agentRun || codexStdio?.ready === true) && codeAgent?.ready === true; return { status: ready ? "ready" : currentBlockers.length > 0 ? "blocked" : "degraded", ready, + adapter: codeAgent?.adapter ?? codeAgent?.agentRun?.adapter ?? null, + agentRunReady: agentRun ? codeAgent?.agentRun?.ready === true : null, providerReady: provider?.ready === true, durableDbReady: dbDurable?.ready === true, sessionRunnerReady: sessionRunner?.ready === true, - codexStdioFeasible: codexStdio?.ready === true, + codexStdioFeasible: agentRun ? false : codexStdio?.ready === true, currentBlocker: currentBlockers[0] ?? null, currentBlockers, provider: { @@ -349,6 +428,7 @@ function buildCodeAgentStructuredReadiness({ provider, dbDurable, sessionRunner, status: codexStdio?.status ?? "unknown", ready: codexStdio?.ready === true, feasible: codexStdio?.feasible === true, + delegatedToAgentRun: codexStdio?.delegatedToAgentRun === true, blocker: codexStdio?.blocker ?? null, blockerCodes: Array.isArray(codexStdio?.blockerCodes) ? [...codexStdio.blockerCodes] : [] }, @@ -357,6 +437,10 @@ function buildCodeAgentStructuredReadiness({ provider, dbDurable, sessionRunner, }; } +function isAgentRunCodeAgent(codeAgent = {}) { + return codeAgent?.adapter === "agentrun-v01" || codeAgent?.agentRun?.adapter === "agentrun-v01" || codeAgent?.mode === "agentrun-v01"; +} + function providerBlockerCode(codeAgent = {}, fallback = {}) { if (Array.isArray(codeAgent?.missingEnv) && codeAgent.missingEnv.length > 0) return "provider_config_blocked"; if (fallback?.egress?.directPublicOpenAi === true || codeAgent?.egress?.directPublicOpenAi === true) return "provider_egress_blocked"; diff --git a/internal/cloud/server-health.test.ts b/internal/cloud/server-health.test.ts index 971c6f14..7c24fb99 100644 --- a/internal/cloud/server-health.test.ts +++ b/internal/cloud/server-health.test.ts @@ -718,6 +718,83 @@ test("cloud api health reports provider, durable DB, and codex stdio ready when } }); +test("cloud api health reports AgentRun adapter readiness without repo-owned codex stdio control plane", async () => { + const server = createCloudApiServer({ + env: { + HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01", + AGENTRUN_MGR_URL: "http://agentrun-mgr.agentrun-v01.svc.cluster.local:8080", + HWLAB_CODE_AGENT_AGENTRUN_RUNNER_NAMESPACE: "agentrun-v01", + HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "G14", + HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek", + HWLAB_CODE_AGENT_DEEPSEEK_MODEL: "deepseek-chat", + HWLAB_BOOT_READ_URL: "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git", + HWLAB_CLOUD_DB_URL: "postgres://hwlab_test:password@db.internal.local:5432/hwlab", + HWLAB_CLOUD_DB_SSL_MODE: "disable" + }, + dbProbe: { + probe: async ({ endpointSource, timeoutMs }) => ({ + attempted: true, + networkAttempted: true, + endpointSource, + probeType: "tcp-connect", + endpointRedacted: true, + valueRedacted: true, + timeoutMs, + durationMs: 1, + result: "connected", + classification: "tcp_connected", + errorCode: null, + missingEnv: [] + }) + }, + runtimeStore: { + async readiness() { + return durableReadyRuntime(); + } + } + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + + try { + const { port } = server.address(); + const response = await fetch(`http://127.0.0.1:${port}/health/live`); + assert.equal(response.status, 200); + const payload = await response.json(); + assert.equal(payload.status, "ok"); + assert.equal(payload.ready, true); + assert.equal(payload.codeAgent.adapter, "agentrun-v01"); + assert.equal(payload.codeAgent.status, "agentrun-ready"); + assert.equal(payload.codeAgent.provider, "deepseek"); + assert.equal(payload.codeAgent.backend, "agentrun-v01/deepseek"); + assert.equal(payload.codeAgent.agentRun.managerHost, "agentrun-mgr.agentrun-v01.svc.cluster.local"); + assert.equal(payload.codeAgent.agentRun.runnerNamespace, "agentrun-v01"); + assert.equal(payload.codeAgent.runner.kind, "agentrun-v01-shared-runner"); + assert.equal(payload.codeAgent.runner.delegatedToAgentRun, true); + assert.equal(payload.codeAgent.runner.codexStdio, false); + assert.equal(payload.readiness.components.codeAgent, "ready"); + assert.equal(payload.readiness.provider.status, "ready"); + assert.equal(payload.readiness.sessionRunner.status, "agentrun_ready"); + assert.equal(payload.readiness.sessionRunner.delegatedToAgentRun, true); + assert.equal(payload.readiness.codexStdio.status, "not_applicable"); + assert.equal(payload.readiness.codeAgent.ready, true); + assert.equal(payload.readiness.codeAgent.adapter, "agentrun-v01"); + assert.equal(payload.readiness.codeAgent.agentRunReady, true); + assert.equal(payload.readiness.codeAgent.codexStdioFeasible, false); + assert.deepEqual(payload.readiness.codeAgent.currentBlockers, []); + const serialized = JSON.stringify(payload); + assert.equal(serialized.includes("repo-owned-codex"), false); + assert.equal(serialized.includes("codex-app-server-stdio"), false); + assert.equal(serialized.includes("CODEX_HOME"), false); + assert.equal(serialized.includes("/codex-home"), false); + assert.equal(serialized.includes("test-openai-key-material"), false); + assert.equal(serialized.includes("password"), false); + } finally { + await new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }); + } +}); + test("cloud api health blocks full Code Agent readiness when Codex stdio command probe fails", async () => { const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-health-codex-stdio-probe-fail-")); const codexHome = path.join(workspace, "codex-home");