diff --git a/AGENTS.md b/AGENTS.md index 5b1e9adb..a34cd006 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,8 +75,7 @@ HWLAB 是硬件实验室运行面和控制面项目。本文是 agent、指挥 - v0.2 CI/CD 加法 lane、`v0.2-gitops`、`hwlab-v02` 和 `19666/19667` 规格:[docs/reference/spec-v02-cicd.md](docs/reference/spec-v02-cicd.md)。 - v0.2 `hwlab-cloud-api` API 核心服务规格:[docs/reference/spec-v02-hwlab-cloud-api.md](docs/reference/spec-v02-hwlab-cloud-api.md)。 - v0.2 `hwlab-cloud-web` 浏览器工作台规格:[docs/reference/spec-v02-hwlab-cloud-web.md](docs/reference/spec-v02-hwlab-cloud-web.md)。 -- v0.2 `hwlab-agent-mgr` agent 管理服务规格:[docs/reference/spec-v02-hwlab-agent-mgr.md](docs/reference/spec-v02-hwlab-agent-mgr.md)。 -- v0.2 `hwlab-agent-worker` session-scoped Job 模板规格:[docs/reference/spec-v02-hwlab-agent-worker.md](docs/reference/spec-v02-hwlab-agent-worker.md)。 +- v0.2 Code Agent 由 `hwlab-cloud-api` 接入 AgentRun v0.1 共享执行基础设施,不再保留 HWLAB 自有 agent manager/worker 控制面:[docs/reference/agentrun-code-agent-dispatch.md](docs/reference/agentrun-code-agent-dispatch.md)。 - v0.2 `hwlab-agent-skills` 技能包服务规格:[docs/reference/spec-v02-hwlab-agent-skills.md](docs/reference/spec-v02-hwlab-agent-skills.md)。 - v0.2 `hwlab-cli` 固定 repo 短连接 client 规格:[docs/reference/spec-v02-hwlab-cli.md](docs/reference/spec-v02-hwlab-cli.md)。 - v0.2 `hwlab-device-pod` 部署服务规格:[docs/reference/spec-v02-hwlab-device-pod-service.md](docs/reference/spec-v02-hwlab-device-pod-service.md)。 diff --git a/cmd/hwlab-agent-mgr/main.test.ts b/cmd/hwlab-agent-mgr/main.test.ts deleted file mode 100644 index 178b0a81..00000000 --- a/cmd/hwlab-agent-mgr/main.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import assert from "node:assert/strict"; -import { execFile } from "node:child_process"; -import { mkdtemp, rm } from "node:fs/promises"; -import { createServer } from "node:http"; -import os from "node:os"; -import path from "node:path"; -import test from "node:test"; -import { promisify } from "node:util"; - -const execFileAsync = promisify(execFile); -const bunCommand = process.env.HWLAB_TEST_BUN_COMMAND || process.env.HWLAB_BUN_COMMAND || (process.versions.bun ? process.execPath : "bun"); - -test("agent manager CLI reports degraded skills and manages local session lifecycle", async () => { - const stateDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-mgr-")); - try { - const health = await runManagerJson(["health", "--state-dir", stateDir]); - assert.equal(health.serviceId, "hwlab-agent-mgr"); - assert.equal(health.ok, true); - assert.equal(health.status, "degraded"); - assert.deepEqual(health.health.skills.manager.missing, ["skillCommitId", "skillVersion"]); - - const created = await runManagerJson([ - "create", - "--agent-session-id", "ags_test", - "--project-id", "prj_test", - "--goal", "keep behavior", - "--prompt", "run dry", - "--skill-commit-id", "commit-a", - "--skill-version", "1.0.0", - "--state-dir", stateDir - ]); - assert.equal(created.agentSessionId, "ags_test"); - assert.equal(created.status, "ok"); - assert.equal(created.task.status, "queued"); - assert.equal(created.traceCount, 1); - - const status = await runManagerJson(["status", "--agent-session-id", "ags_test", "--state-dir", stateDir]); - assert.equal(status.agentSession.metadata.skills.commitId, "commit-a"); - assert.equal(status.workerSession.metadata.expectedSkills.version, "1.0.0"); - - const trace = await runManagerJson(["trace", "--agent-session-id", "ags_test", "--state-dir", stateDir]); - assert.equal(trace.traceEvents.length, 1); - assert.equal(trace.traceEvents[0].metadata.state, "create"); - - const evidence = await runManagerJson(["evidence", "--agent-session-id", "ags_test", "--state-dir", stateDir]); - assert.equal(evidence.evidenceRecords.length, 0); - - const cleanup = await runManagerJson(["cleanup", "--agent-session-id", "ags_test", "--state-dir", stateDir]); - assert.equal(cleanup.serviceId, "hwlab-agent-mgr"); - assert.equal(cleanup.cleanup.completed, true); - assert.equal(cleanup.workspace.cleaned, true); - } finally { - await rm(stateDir, { recursive: true, force: true }); - } -}); - -test("agent manager server returns health JSON", async () => { - const port = await freePort(); - const server = await startManagerServer(port); - try { - const response = await fetch(`http://127.0.0.1:${port}/health/live`); - assert.equal(response.status, 200); - const health = await response.json(); - assert.equal(health.serviceId, "hwlab-agent-mgr"); - assert.equal(health.ok, true); - assert.equal(health.status, "degraded"); - } finally { - await server.stop(); - } -}); - -async function runManagerJson(args) { - const { stdout } = await execFileAsync(bunCommand, ["run", "cmd/hwlab-agent-mgr/main.ts", ...args], { - cwd: process.cwd(), - maxBuffer: 1024 * 1024 - }); - return JSON.parse(stdout); -} - -async function startManagerServer(port) { - const child = execFile(bunCommand, ["run", "cmd/hwlab-agent-mgr/main.ts", "server"], { - cwd: process.cwd(), - env: { - ...process.env, - PORT: String(port) - }, - maxBuffer: 1024 * 1024 - }); - await new Promise((resolve, reject) => { - let stderr = ""; - const timer = setTimeout(() => reject(new Error(`manager server did not start: ${stderr}`)), 5000); - child.stderr?.on("data", (chunk) => { - stderr += chunk; - }); - child.stdout?.on("data", (chunk) => { - if (chunk.toString().includes("listening")) { - clearTimeout(timer); - resolve(); - } - }); - child.on("exit", (code) => { - clearTimeout(timer); - reject(new Error(`manager server exited early code=${code}: ${stderr}`)); - }); - }); - return { - stop: async () => { - if (child.exitCode !== null) return; - child.kill("SIGTERM"); - await new Promise((resolve) => child.once("exit", resolve)); - } - }; -} - -async function freePort() { - const server = createServer(); - await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); - const port = server.address().port; - await new Promise((resolve) => server.close(resolve)); - return port; -} diff --git a/cmd/hwlab-agent-mgr/main.ts b/cmd/hwlab-agent-mgr/main.ts deleted file mode 100644 index 3d34ff49..00000000 --- a/cmd/hwlab-agent-mgr/main.ts +++ /dev/null @@ -1,234 +0,0 @@ -#!/usr/bin/env node -import { createServer } from "node:http"; - -import { buildMetadataFromEnv } from "../../internal/build-metadata.mjs"; -import { - AGENT_MGR_SERVICE_ID, - createSkillsManifest -} from "../../internal/agent/index.mjs"; -import { - assessSkillsInjection, - cleanupLocalAgentSession, - createLocalAgentSession, - createRuntimeHealth, - getLocalAgentEvidence, - getLocalAgentStatus, - getLocalAgentTrace, - resolveAgentRuntimeStateDir -} from "../../internal/agent/runtime.mjs"; - -const command = process.argv[2] || process.env.HWLAB_AGENT_MGR_DEFAULT_COMMAND || "server"; -const options = parseOptions(process.argv.slice(3)); - -try { - const result = await dispatch(command, options); - printJSON(result); -} catch (error) { - printJSON( - { - serviceId: AGENT_MGR_SERVICE_ID, - ok: false, - error: { - message: error.message - } - }, - { stderr: true } - ); - process.exit(1); -} - -async function dispatch(name, opts) { - if (name === "help" || opts.help) { - return help(); - } - - if (name === "health") { - return createHealthResponse(opts); - } - - if (name === "server") { - return serveHealth(opts); - } - - if (name === "create") { - return { - ok: true, - ...(await createLocalAgentSession({ - stateDir: opts.stateDir, - agentSessionId: requireOption(opts, "agentSessionId", "--agent-session-id"), - projectId: requireOption(opts, "projectId", "--project-id"), - goal: opts.goal || "", - prompt: opts.prompt || "", - requestedBy: opts.requestedBy || "usr_local", - skillCommitId: opts.skillCommitId || null, - skillVersion: opts.skillVersion || null - })) - }; - } - - if (name === "status") { - return { - ok: true, - ...(await getLocalAgentStatus({ - stateDir: opts.stateDir, - agentSessionId: requireOption(opts, "agentSessionId", "--agent-session-id") - })) - }; - } - - if (name === "trace") { - return { - ok: true, - ...(await getLocalAgentTrace({ - stateDir: opts.stateDir, - agentSessionId: requireOption(opts, "agentSessionId", "--agent-session-id") - })) - }; - } - - if (name === "evidence") { - return { - ok: true, - ...(await getLocalAgentEvidence({ - stateDir: opts.stateDir, - agentSessionId: requireOption(opts, "agentSessionId", "--agent-session-id") - })) - }; - } - - if (name === "cleanup") { - return { - ok: true, - ...(await cleanupLocalAgentSession({ - stateDir: opts.stateDir, - agentSessionId: requireOption(opts, "agentSessionId", "--agent-session-id"), - actor: AGENT_MGR_SERVICE_ID - })) - }; - } - - throw new Error(`unknown command ${JSON.stringify(name)}`); -} - -function parseOptions(argv) { - const opts = { - stateDir: process.env.HWLAB_AGENT_RUNTIME_STATE_DIR || "" - }; - - for (let i = 0; i < argv.length; i += 1) { - const arg = argv[i]; - if (arg === "--agent-session-id") opts.agentSessionId = argv[++i] || ""; - else if (arg === "--project-id") opts.projectId = argv[++i] || ""; - else if (arg === "--goal") opts.goal = argv[++i] || ""; - else if (arg === "--prompt") opts.prompt = argv[++i] || ""; - else if (arg === "--requested-by") opts.requestedBy = argv[++i] || ""; - else if (arg === "--skill-commit-id") opts.skillCommitId = argv[++i] || ""; - else if (arg === "--skill-version") opts.skillVersion = argv[++i] || ""; - else if (arg === "--state-dir") opts.stateDir = argv[++i] || ""; - else if (arg === "--help" || arg === "-h") opts.help = true; - else throw new Error(`unknown option ${JSON.stringify(arg)}`); - } - - if (opts.stateDir) { - opts.stateDir = resolveAgentRuntimeStateDir(opts.stateDir); - } - - return opts; -} - -function requireOption(opts, key, flag) { - if (!opts[key]) { - throw new Error(`${flag} is required`); - } - return opts[key]; -} - -function createHealthResponse(opts) { - const managerSkills = assessSkillsInjection({ - skillCommitId: opts.skillCommitId, - skillVersion: opts.skillVersion, - role: "manager" - }); - const health = createRuntimeHealth({ - stateDir: opts.stateDir, - managerSkills - }); - return { - serviceId: AGENT_MGR_SERVICE_ID, - ok: true, - status: health.status, - environment: process.env.HWLAB_ENVIRONMENT || "dev", - ...buildMetadataFromEnv(process.env, { serviceId: AGENT_MGR_SERVICE_ID }), - health, - skillsManifest: managerSkills.status === "ready" - ? createSkillsManifest({ commitId: managerSkills.commitId, version: managerSkills.version }) - : null - }; -} - -async function serveHealth(opts) { - const port = Number.parseInt(process.env.PORT || process.env.HWLAB_PORT || "7410", 10); - if (!Number.isInteger(port) || port < 1 || port > 65535) { - throw new Error(`invalid PORT/HWLAB_PORT ${JSON.stringify(process.env.PORT || process.env.HWLAB_PORT)}`); - } - - const server = createServer((request, response) => { - const url = new URL(request.url || "/", "http://hwlab-agent-mgr.local"); - if (url.pathname === "/health" || url.pathname === "/health/live") { - sendJson(response, 200, createHealthResponse(opts)); - return; - } - if (url.pathname === "/" || url.pathname === "/help") { - sendJson(response, 200, help()); - return; - } - sendJson(response, 404, { serviceId: AGENT_MGR_SERVICE_ID, ok: false, error: "not_found", path: url.pathname }); - }); - - await new Promise((resolve, reject) => { - server.once("error", reject); - server.listen(port, "0.0.0.0", resolve); - }); - printJSON({ serviceId: AGENT_MGR_SERVICE_ID, ok: true, status: "listening", port }); - - return new Promise((resolve, reject) => { - server.once("close", resolve); - server.once("error", reject); - }); -} - -function sendJson(response, statusCode, body) { - const payload = JSON.stringify(body, null, 2); - response.writeHead(statusCode, { - "content-type": "application/json; charset=utf-8", - "content-length": Buffer.byteLength(payload) - }); - response.end(payload); -} - -function help() { - return { - serviceId: AGENT_MGR_SERVICE_ID, - ok: true, - usage: [ - "bun run cmd/hwlab-agent-mgr/main.ts server [--skill-commit-id COMMIT --skill-version VERSION] [--state-dir DIR]", - "bun run cmd/hwlab-agent-mgr/main.ts health [--skill-commit-id COMMIT --skill-version VERSION] [--state-dir DIR]", - "bun run cmd/hwlab-agent-mgr/main.ts create --agent-session-id ID --project-id ID [--goal TEXT --prompt TEXT] [--skill-commit-id COMMIT --skill-version VERSION] [--state-dir DIR]", - "bun run cmd/hwlab-agent-mgr/main.ts status --agent-session-id ID [--state-dir DIR]", - "bun run cmd/hwlab-agent-mgr/main.ts trace --agent-session-id ID [--state-dir DIR]", - "bun run cmd/hwlab-agent-mgr/main.ts evidence --agent-session-id ID [--state-dir DIR]", - "bun run cmd/hwlab-agent-mgr/main.ts cleanup --agent-session-id ID [--state-dir DIR]" - ], - contract: { - stateDir: resolveAgentRuntimeStateDir(), - output: "json", - runtimeMode: "local-dry-run", - prodDisabled: true - } - }; -} - -function printJSON(value, { stderr = false } = {}) { - const target = stderr ? process.stderr : process.stdout; - target.write(`${JSON.stringify(value, null, 2)}\n`); -} diff --git a/cmd/hwlab-agent-worker/main.test.ts b/cmd/hwlab-agent-worker/main.test.ts deleted file mode 100644 index d54d776f..00000000 --- a/cmd/hwlab-agent-worker/main.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import assert from "node:assert/strict"; -import { execFile } from "node:child_process"; -import { access, mkdtemp, rm } from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import test from "node:test"; -import { promisify } from "node:util"; - -const execFileAsync = promisify(execFile); -const bunCommand = process.env.HWLAB_TEST_BUN_COMMAND || process.env.HWLAB_BUN_COMMAND || (process.versions.bun ? process.execPath : "bun"); - -test("agent worker health and dry-run preserve session evidence and cleanup behavior", async () => { - const stateDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-worker-")); - try { - const health = await runWorkerJson(["health", "--state-dir", stateDir]); - assert.equal(health.serviceId, "hwlab-agent-worker"); - assert.equal(health.ok, true); - assert.equal(health.status, "ok"); - assert.equal(health.skills.status, "degraded"); - - await runManagerJson([ - "create", - "--agent-session-id", "ags_worker", - "--project-id", "prj_worker", - "--skill-commit-id", "commit-worker", - "--skill-version", "2.0.0", - "--state-dir", stateDir - ]); - - const dryRun = await runWorkerJson([ - "dry-run", - "--agent-session-id", "ags_worker", - "--skill-commit-id", "commit-worker", - "--skill-version", "2.0.0", - "--state-dir", stateDir - ]); - assert.equal(dryRun.serviceId, "hwlab-agent-worker"); - assert.equal(dryRun.dryRun, true); - assert.equal(dryRun.task.status, "completed"); - assert.equal(dryRun.health.status, "ok"); - assert.equal(dryRun.evidenceRecord.evidenceId, "evi_ags_worker"); - assert.equal(dryRun.cleanup.completed, true); - assert.equal(dryRun.workspace.cleaned, true); - assert.equal(dryRun.workspace.existsAfterCleanup, false); - await assert.rejects(access(dryRun.workspace.path)); - - const evidence = await runManagerJson(["evidence", "--agent-session-id", "ags_worker", "--state-dir", stateDir]); - assert.equal(evidence.evidenceRecords.length, 1); - assert.equal(evidence.evidenceRecords[0].serviceId, "hwlab-agent-worker"); - } finally { - await rm(stateDir, { recursive: true, force: true }); - } -}); - -test("agent worker dry-run requires agent session id", async () => { - const result = await execFileAsync(bunCommand, ["run", "cmd/hwlab-agent-worker/main.ts", "dry-run"], { - cwd: process.cwd(), - maxBuffer: 1024 * 1024 - }).then( - () => ({ code: 0, stderr: "" }), - (error) => ({ code: error.code, stderr: error.stderr }) - ); - assert.equal(result.code, 1); - const payload = JSON.parse(result.stderr); - assert.equal(payload.serviceId, "hwlab-agent-worker"); - assert.match(payload.error.message, /--agent-session-id is required/u); -}); - -async function runManagerJson(args) { - const { stdout } = await execFileAsync(bunCommand, ["run", "cmd/hwlab-agent-mgr/main.ts", ...args], { - cwd: process.cwd(), - maxBuffer: 1024 * 1024 - }); - return JSON.parse(stdout); -} - -async function runWorkerJson(args) { - const { stdout } = await execFileAsync(bunCommand, ["run", "cmd/hwlab-agent-worker/main.ts", ...args], { - cwd: process.cwd(), - maxBuffer: 1024 * 1024 - }); - return JSON.parse(stdout); -} diff --git a/cmd/hwlab-agent-worker/main.ts b/cmd/hwlab-agent-worker/main.ts deleted file mode 100644 index 9fe1629b..00000000 --- a/cmd/hwlab-agent-worker/main.ts +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env node -import { buildMetadataFromEnv } from "../../internal/build-metadata.mjs"; -import { AGENT_WORKER_SERVICE_ID } from "../../internal/agent/index.mjs"; -import { - assessSkillsInjection, - resolveAgentRuntimeStateDir, - runLocalWorkerDryRun -} from "../../internal/agent/runtime.mjs"; - -const command = process.argv[2] || "help"; -const options = parseOptions(process.argv.slice(3)); - -try { - const result = await dispatch(command, options); - printJSON(result); -} catch (error) { - printJSON( - { - serviceId: AGENT_WORKER_SERVICE_ID, - ok: false, - error: { - message: error.message - } - }, - { stderr: true } - ); - process.exit(1); -} - -async function dispatch(name, opts) { - if (name === "help" || opts.help) { - return help(); - } - - if (name === "health") { - return { - serviceId: AGENT_WORKER_SERVICE_ID, - ok: true, - status: "ok", - environment: process.env.HWLAB_ENVIRONMENT || "dev", - ...buildMetadataFromEnv(process.env, { serviceId: AGENT_WORKER_SERVICE_ID }), - runtimeMode: "local-dry-run", - skills: assessSkillsInjection({ - skillCommitId: opts.skillCommitId, - skillVersion: opts.skillVersion, - role: "worker" - }) - }; - } - - if (name === "dry-run" || name === "smoke") { - return { - ok: true, - ...(await runLocalWorkerDryRun({ - stateDir: opts.stateDir, - agentSessionId: requireOption(opts, "agentSessionId", "--agent-session-id"), - skillCommitId: opts.skillCommitId || null, - skillVersion: opts.skillVersion || null - })) - }; - } - - throw new Error(`unknown command ${JSON.stringify(name)}`); -} - -function parseOptions(argv) { - const opts = { - stateDir: process.env.HWLAB_AGENT_RUNTIME_STATE_DIR || "", - agentSessionId: process.env.HWLAB_AGENT_SESSION_ID || "", - skillCommitId: process.env.HWLAB_SKILL_COMMIT_ID || "", - skillVersion: process.env.HWLAB_SKILL_VERSION || "" - }; - - for (let i = 0; i < argv.length; i += 1) { - const arg = argv[i]; - if (arg === "--agent-session-id") opts.agentSessionId = argv[++i] || ""; - else if (arg === "--skill-commit-id") opts.skillCommitId = argv[++i] || ""; - else if (arg === "--skill-version") opts.skillVersion = argv[++i] || ""; - else if (arg === "--state-dir") opts.stateDir = argv[++i] || ""; - else if (arg === "--help" || arg === "-h") opts.help = true; - else throw new Error(`unknown option ${JSON.stringify(arg)}`); - } - - if (opts.stateDir) { - opts.stateDir = resolveAgentRuntimeStateDir(opts.stateDir); - } - - return opts; -} - -function requireOption(opts, key, flag) { - if (!opts[key]) { - throw new Error(`${flag} is required`); - } - return opts[key]; -} - -function help() { - return { - serviceId: AGENT_WORKER_SERVICE_ID, - ok: true, - usage: [ - "bun run cmd/hwlab-agent-worker/main.ts health [--skill-commit-id COMMIT --skill-version VERSION]", - "bun run cmd/hwlab-agent-worker/main.ts dry-run --agent-session-id ID [--skill-commit-id COMMIT --skill-version VERSION] [--state-dir DIR]" - ], - contract: { - output: "json", - runtimeMode: "local-dry-run", - prodDisabled: true, - behavior: "claim queued local session, write trace and evidence, cleanup workspace, exit" - } - }; -} - -function printJSON(value, { stderr = false } = {}) { - const target = stderr ? process.stderr : process.stdout; - target.write(`${JSON.stringify(value, null, 2)}\n`); -} diff --git a/deploy/artifact-catalog.dev.json b/deploy/artifact-catalog.dev.json index 4df8ba07..157433fe 100644 --- a/deploy/artifact-catalog.dev.json +++ b/deploy/artifact-catalog.dev.json @@ -136,76 +136,6 @@ }, "reusedFrom": "fdd27830a0cef173fc07a2cab8cd469d67aaff9da12bb0195f188e323854f93a" }, - { - "serviceId": "hwlab-agent-mgr", - "commitId": "278bbe1", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:278bbe1", - "imageTag": "278bbe1", - "digest": "not_published", - "publishState": "skeleton-only", - "profile": "dev", - "namespace": "hwlab-dev", - "healthPath": "/health/live", - "sourceState": "source-present", - "publishEnabled": true, - "artifactRequired": true, - "artifactScope": "required", - "notPublishedReason": "publish_not_run", - "buildCreatedAt": null, - "buildSource": null, - "componentCommitId": "a56ea7e33ef67040cc9a8748c3759b4f9c87ca46", - "componentInputHash": "ce4aa5c0b74c873d3270a71d9481a8255571b574f024bd5980f18547b4361860", - "dockerfileHash": "8b2595a09276479b8809e70e99516c1539a6a7cc200e73266d247176b56be962", - "baseImageReference": "127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim", - "baseImageDigest": null, - "buildArgsHash": "804a2e2f48cae4f739c3e88bb0d961359fca0a8e1b8cac43517fb065989df72b", - "ciAffected": false, - "ciReason": [ - "component-inputs-unchanged" - ], - "reuse": { - "status": "ready", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:af46386", - "digest": "sha256:5a55d6199b5bf2db4afa55a0be8cc6eae38f2dcac8a7b374ebbec2af2f36d71c", - "reusedFrom": "ce4aa5c0b74c873d3270a71d9481a8255571b574f024bd5980f18547b4361860" - }, - "reusedFrom": "ce4aa5c0b74c873d3270a71d9481a8255571b574f024bd5980f18547b4361860" - }, - { - "serviceId": "hwlab-agent-worker", - "commitId": "278bbe1", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:278bbe1", - "imageTag": "278bbe1", - "digest": "not_published", - "publishState": "skeleton-only", - "profile": "dev", - "namespace": "hwlab-dev", - "healthPath": "/health/live", - "sourceState": "source-present", - "publishEnabled": true, - "artifactRequired": true, - "artifactScope": "required", - "notPublishedReason": "publish_not_run", - "buildCreatedAt": null, - "buildSource": null, - "componentCommitId": "a56ea7e33ef67040cc9a8748c3759b4f9c87ca46", - "componentInputHash": "7b426c226ec6f6d4de516f58055f3bf26d911477a88ebeef320066b1903ef40f", - "dockerfileHash": "8b2595a09276479b8809e70e99516c1539a6a7cc200e73266d247176b56be962", - "baseImageReference": "127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim", - "baseImageDigest": null, - "buildArgsHash": "d5ea384583ca6916b9fdf4469a8c591e33e6e1a0ba9a59b9901fbaa797824cab", - "ciAffected": false, - "ciReason": [ - "component-inputs-unchanged" - ], - "reuse": { - "status": "ready", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:af46386", - "digest": "sha256:03f09333657bb37ad059004aabb4484503f79866ba19272d5af9bf611bbb9c5d", - "reusedFrom": "7b426c226ec6f6d4de516f58055f3bf26d911477a88ebeef320066b1903ef40f" - }, - "reusedFrom": "7b426c226ec6f6d4de516f58055f3bf26d911477a88ebeef320066b1903ef40f" - }, { "serviceId": "hwlab-device-pod", "commitId": "278bbe1", @@ -350,8 +280,6 @@ "requiredServiceIds": [ "hwlab-cloud-api", "hwlab-cloud-web", - "hwlab-agent-mgr", - "hwlab-agent-worker", "hwlab-device-pod", "hwlab-gateway", "hwlab-edge-proxy", @@ -381,28 +309,6 @@ "entrypoint": "web/hwlab-cloud-web/index.html", "disabledReason": null }, - { - "serviceId": "hwlab-agent-mgr", - "publishEnabled": true, - "artifactRequired": true, - "artifactScope": "required", - "runtimeKind": "node-command", - "implementationState": "repo-entrypoint", - "sourceState": "source-present", - "entrypoint": "cmd/hwlab-agent-mgr/main.ts", - "disabledReason": null - }, - { - "serviceId": "hwlab-agent-worker", - "publishEnabled": true, - "artifactRequired": true, - "artifactScope": "required", - "runtimeKind": "node-command", - "implementationState": "repo-entrypoint", - "sourceState": "source-present", - "entrypoint": "cmd/hwlab-agent-worker/main.ts", - "disabledReason": null - }, { "serviceId": "hwlab-device-pod", "publishEnabled": true, diff --git a/deploy/deploy.json b/deploy/deploy.json index 3c8bccd1..6675b904 100644 --- a/deploy/deploy.json +++ b/deploy/deploy.json @@ -82,13 +82,6 @@ "port": 7430, "targetPort": "http" }, - { - "serviceId": "hwlab-agent-mgr", - "name": "hwlab-agent-mgr", - "namespace": "hwlab-dev", - "port": 7410, - "targetPort": "http" - }, { "serviceId": "hwlab-device-pod", "name": "hwlab-device-pod", @@ -138,12 +131,10 @@ "imageTagMode": "full", "envReuseServices": [ "hwlab-cloud-web", - "hwlab-agent-worker", "hwlab-device-pod" ], "bootScripts": { "hwlab-cloud-web": "deploy/runtime/boot/hwlab-cloud-web.sh", - "hwlab-agent-worker": "deploy/runtime/boot/hwlab-agent-worker.sh", "hwlab-device-pod": "deploy/runtime/boot/hwlab-device-pod.sh" }, "services": [ @@ -236,28 +227,6 @@ "HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS": "1260000" } }, - { - "serviceId": "hwlab-agent-mgr", - "namespace": "hwlab-dev", - "healthPath": "/health/live", - "profile": "dev", - "replicas": 1, - "env": { - "HWLAB_AGENT_SESSION_MODE": "control-plane", - "HWLAB_ENVIRONMENT": "dev" - } - }, - { - "serviceId": "hwlab-agent-worker", - "namespace": "hwlab-dev", - "healthPath": "/health/live", - "profile": "dev", - "replicas": 0, - "env": { - "HWLAB_AGENT_SESSION_MODE": "session-scoped", - "HWLAB_ENVIRONMENT": "dev" - } - }, { "serviceId": "hwlab-device-pod", "namespace": "hwlab-dev", diff --git a/deploy/deploy.schema.json b/deploy/deploy.schema.json index d73f94ab..d326de67 100644 --- a/deploy/deploy.schema.json +++ b/deploy/deploy.schema.json @@ -67,8 +67,6 @@ "enum": [ "hwlab-cloud-api", "hwlab-cloud-web", - "hwlab-agent-mgr", - "hwlab-agent-worker", "hwlab-device-pod", "hwlab-gateway", "hwlab-edge-proxy", diff --git a/deploy/k8s/base/services.yaml b/deploy/k8s/base/services.yaml index 1948ed43..1cabbc83 100644 --- a/deploy/k8s/base/services.yaml +++ b/deploy/k8s/base/services.yaml @@ -103,31 +103,6 @@ ] } }, - { - "apiVersion": "v1", - "kind": "Service", - "metadata": { - "name": "hwlab-agent-mgr", - "namespace": "hwlab-dev", - "labels": { - "app.kubernetes.io/name": "hwlab-agent-mgr", - "hwlab.pikastech.local/service-id": "hwlab-agent-mgr" - } - }, - "spec": { - "type": "ClusterIP", - "selector": { - "app.kubernetes.io/name": "hwlab-agent-mgr" - }, - "ports": [ - { - "name": "http", - "port": 7410, - "targetPort": "http" - } - ] - } - }, { "apiVersion": "v1", "kind": "Service", diff --git a/deploy/k8s/base/workloads.yaml b/deploy/k8s/base/workloads.yaml index b94560f2..ccdb8d68 100644 --- a/deploy/k8s/base/workloads.yaml +++ b/deploy/k8s/base/workloads.yaml @@ -498,104 +498,6 @@ } } }, - { - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": { - "name": "hwlab-agent-mgr", - "namespace": "hwlab-dev", - "labels": { - "app.kubernetes.io/name": "hwlab-agent-mgr", - "hwlab.pikastech.local/service-id": "hwlab-agent-mgr" - } - }, - "spec": { - "replicas": 1, - "selector": { - "matchLabels": { - "app.kubernetes.io/name": "hwlab-agent-mgr" - } - }, - "template": { - "metadata": { - "labels": { - "app.kubernetes.io/name": "hwlab-agent-mgr", - "hwlab.pikastech.local/service-id": "hwlab-agent-mgr" - } - }, - "spec": { - "containers": [ - { - "name": "hwlab-agent-mgr", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:af46386", - "ports": [ - { - "name": "http", - "containerPort": 7410 - } - ], - "env": [ - { - "name": "HWLAB_AGENT_SESSION_MODE", - "value": "control-plane" - } - ], - "readinessProbe": { - "httpGet": { - "path": "/health/live", - "port": "http" - } - }, - "livenessProbe": { - "httpGet": { - "path": "/health/live", - "port": "http" - } - } - } - ] - } - } - } - }, - { - "apiVersion": "batch/v1", - "kind": "Job", - "metadata": { - "name": "hwlab-agent-worker-template", - "namespace": "hwlab-dev", - "labels": { - "app.kubernetes.io/name": "hwlab-agent-worker", - "hwlab.pikastech.local/service-id": "hwlab-agent-worker" - } - }, - "spec": { - "suspend": true, - "template": { - "metadata": { - "labels": { - "app.kubernetes.io/name": "hwlab-agent-worker", - "hwlab.pikastech.local/service-id": "hwlab-agent-worker" - } - }, - "spec": { - "restartPolicy": "Never", - "containers": [ - { - "name": "hwlab-agent-worker", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:af46386", - "env": [ - { - "name": "HWLAB_AGENT_SESSION_MODE", - "value": "session-scoped" - } - ] - } - ] - } - } - } - }, { "apiVersion": "apps/v1", "kind": "Deployment", diff --git a/deploy/k8s/dev/health-contract.yaml b/deploy/k8s/dev/health-contract.yaml index bd73b177..bbe5d48e 100644 --- a/deploy/k8s/dev/health-contract.yaml +++ b/deploy/k8s/dev/health-contract.yaml @@ -14,7 +14,7 @@ "cloud-api": "GET /health/live via hwlab-edge-proxy then hwlab-cloud-api:6667", "cloud-api-db": "DEV DB config gate requires HWLAB_CLOUD_DB_URL from Secret hwlab-cloud-api-dev-db/database-url and HWLAB_CLOUD_DB_SSL_MODE=disable. Runtime readiness dials the redacted host parsed from the Secret URL and reports endpointSource=secret-url-host. cloud-api-db is an optional desired alias only until this repo owns Service plus Endpoint or EndpointSlice manifests and rollout/apply contract; health reports env presence, env injection, redacted connection attempt/result, and liveConnected without exposing secret values", "cloud-web": "GET /health/live on hwlab-cloud-web:8080; consumes cloud-api only", - "agent": "hwlab-agent-mgr readiness gates worker template creation; hwlab-agent-worker is suspended until a session is scheduled", + "agent": "hwlab-cloud-api authorizes Code Agent sessions and dispatches execution to AgentRun v0.1; no HWLAB-owned agent manager/worker service is deployed", "edge-proxy": "hwlab-edge-proxy:6667 exposes /health/live and proxies DEV endpoint traffic to hwlab-cloud-api", "runtime-substitute-policy": "Do not replace HWLAB runtime with UniDesk backend, provider-gateway, or microservice proxy", "device-pod": "hwlab-device-pod:7601 exposes /health/live and serves Device Pod profile/job state through cloud-api authority" diff --git a/deploy/master-edge/README.md b/deploy/master-edge/README.md index 033d5b4d..d67cf86f 100644 --- a/deploy/master-edge/README.md +++ b/deploy/master-edge/README.md @@ -12,8 +12,8 @@ Health ownership: - `hwlab-edge-proxy`: accepts public DEV traffic and exposes `/health/live`. - `hwlab-cloud-api`: receives proxied API traffic and exposes `/health/live`. - `hwlab-cloud-web`: reads cloud APIs only; it does not control hardware. -- `hwlab-agent-mgr` and `hwlab-agent-worker`: own agent session and worker - session health boundaries. +- Code Agent sessions are authorized by `hwlab-cloud-api` and dispatched to + AgentRun v0.1; HWLAB no longer owns an agent manager/worker service pair. - `hwlab-device-pod` and `hwlab-gateway`: own the current hardware-facing runtime boundaries; removed simulator/router/tunnel services are not part of the default DEV/v0.2 runtime contract. diff --git a/deploy/master-edge/health-contract.json b/deploy/master-edge/health-contract.json index 1089cb0a..093ed9cf 100644 --- a/deploy/master-edge/health-contract.json +++ b/deploy/master-edge/health-contract.json @@ -24,16 +24,6 @@ "serviceId": "hwlab-cloud-web", "owner": "web frontend boundary", "health": "GET /health/live" - }, - { - "serviceId": "hwlab-agent-mgr", - "owner": "agent session scheduling", - "health": "GET /health/live" - }, - { - "serviceId": "hwlab-agent-worker", - "owner": "session-scoped worker execution", - "health": "job exit status or future GET /health/live sidecar" } ], "forbiddenRuntimeSubstitutes": [ diff --git a/deploy/runtime/boot/hwlab-agent-worker.sh b/deploy/runtime/boot/hwlab-agent-worker.sh deleted file mode 100644 index d208a862..00000000 --- a/deploy/runtime/boot/hwlab-agent-worker.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/sh -set -eu - -export HWLAB_SERVICE_ID="hwlab-agent-worker" -export HWLAB_RUNTIME_MODE="${HWLAB_RUNTIME_MODE:-env-reuse-git-mirror-checkout}" -export HWLAB_COMMIT_ID="${HWLAB_BOOT_COMMIT:?HWLAB_BOOT_COMMIT is required}" -export HWLAB_REVISION="${HWLAB_BOOT_COMMIT}" -export HWLAB_IMAGE="${HWLAB_ENVIRONMENT_IMAGE:-${HWLAB_IMAGE:-}}" -export HWLAB_IMAGE_DIGEST="${HWLAB_ENVIRONMENT_DIGEST:-${HWLAB_IMAGE_DIGEST:-unknown}}" - -bun_bin="${HWLAB_BUN_COMMAND:-}" -if [ -z "$bun_bin" ]; then - if command -v bun >/dev/null 2>&1; then - bun_bin="$(command -v bun)" - elif [ -x /usr/local/bin/bun ]; then - bun_bin="/usr/local/bin/bun" - elif [ -x ./node_modules/.bin/bun ]; then - bun_bin="./node_modules/.bin/bun" - else - echo "hwlab-agent-worker boot failed: bun not found" >&2 - exit 127 - fi -fi - -if [ "$#" -eq 0 ]; then - set -- health -fi - -exec "$bun_bin" cmd/hwlab-agent-worker/main.ts "$@" diff --git a/docs/reference/g14-gitops-cicd.md b/docs/reference/g14-gitops-cicd.md index 279c6553..d5076121 100644 --- a/docs/reference/g14-gitops-cicd.md +++ b/docs/reference/g14-gitops-cicd.md @@ -95,7 +95,7 @@ G14 CI/CD 加速的第一判定标准不是 PipelineRun 总耗时单点变短, - 并发 fan-out:per-service TaskRun 应由 Tekton/k8s scheduler 同时调度,多个 service 的 build/reuse 窗口应接近“最慢单个服务任务耗时”,而不是所有服务串行相加。reuse-only 场景的 fan-out 窗口当前基线约为十几秒;真实组件构建场景仍需按 changed service 单独记录 BuildKit 耗时和 cache hit 情况。 - CD rollout:unchanged service 必须复用 `deploy/artifact-catalog.dev.json` 的 image/digest,pod template 不应因全局 source commit 改变而无意义滚动。手写 manifest(例如 `deepseek-proxy`、device-agent 类辅助 workload)只要复用某个已发布服务镜像,也必须走同一个 catalog image 选择逻辑,不能直接用当前 source commit tag。 - 运行态验证:GitOps promote 成功只说明 `G14-gitops` 分支更新;最终通过必须看 Argo Application revision、sync 状态、目标 namespace Deployment/StatefulSet ready、公网 health。Argo 还停在旧 revision 时,优先做 `argocd.argoproj.io/refresh=hard` 刷新;不要把已经推送的 `G14-gitops` 分支误判成运行面已滚动。 -- Argo health 判定:`spec.suspend=true` 的模板型 Job 会让 Application 总 health 显示 `Suspended`。`hwlab-agent-worker-template` 这类只作为手动启动模板的 Job 必须带 `argocd.argoproj.io/ignore-healthcheck: "true"`,避免 DEV 实际 workload 全部 ready 时仍被误判为 Suspended;`hwlab-cli` 不创建镜像、Service 或 Job template,只在固定 repo 内短连接执行。真正发布验收仍以长驻 workload ready、公网 health 和失败 Pod 清单为准。 +- Argo health 判定:v0.2 不再生成 HWLAB 自有 agent worker Job template;AgentRun v0.1 作为外部共享执行基础设施接入。`hwlab-cli` 不创建镜像、Service 或 Job template,只在固定 repo 内短连接执行。真正发布验收仍以长驻 workload ready、公网 health 和失败 Pod 清单为准。 当前仍然慢的主要位置: diff --git a/docs/reference/spec-user-access.md b/docs/reference/spec-user-access.md index 6ec9d8ca..4459d5b9 100644 --- a/docs/reference/spec-user-access.md +++ b/docs/reference/spec-user-access.md @@ -10,7 +10,7 @@ ## 在系统中的职责划分 -用户和权限管理不是独立微服务,权威实现收敛在 `hwlab-cloud-api`:它消费 [spec-v02-auth.md](spec-v02-auth.md) 产出的 `AuthPrincipal`,负责角色、device pod grant、用户 API key 和 code agent session owner 校验。`hwlab-cloud-web` 只提供浏览器 UI 和同源代理;`hwlab-device-pod` 只执行设备语义;`hwlab-agent-mgr`、`hwlab-agent-worker` 和 Code Agent runtime 只能消费已经由 cloud-api 判断过的 actor/session/device 权限。 +用户和权限管理不是独立微服务,权威实现收敛在 `hwlab-cloud-api`:它消费 [spec-v02-auth.md](spec-v02-auth.md) 产出的 `AuthPrincipal`,负责角色、device pod grant、用户 API key 和 code agent session owner 校验。`hwlab-cloud-web` 只提供浏览器 UI 和同源代理;`hwlab-device-pod` 只执行设备语义;AgentRun v0.1 只消费 cloud-api 传入的 actor/session/device 权限上下文,不成为 HWLAB 用户权限 authority。 Postgres 是该规格的数据持久化边界。Kubernetes namespace、ServiceAccount、Service 直连和 gateway route 都不能替代用户权限模型;普通用户不获得 kubeconfig、内部 Service 直连能力或长期 Secret。 @@ -292,7 +292,7 @@ v0.2 不新增独立用户管理微服务。Keycloak 是独立身份提供方, | --- | --- | | `hwlab-cloud-web` | Keycloak 登录入口、普通用户工作台、API key 管理入口和 admin 用户/授权 UI;浏览器 `/auth/*` 可由 cloud-web 代理到 cloud-api。 | | `hwlab-cloud-api` | 用户映射、Web session/API key 消费、授权、device grant、code agent owner 校验和对 device pod 的受控转发。 | -| `hwlab-agent-mgr` / `hwlab-agent-worker` | 执行 code agent session;接收 owner/session label 或 env 方便观测,但不作为最终权限 authority。 | +| AgentRun v0.1 runner | 执行 code agent session;接收 cloud-api 提供的 owner/session/device 上下文方便观测,但不作为最终权限 authority。 | | `hwlab-device-pod` | 暴露设备语义 API;不保存用户权限,不直接面向浏览器或普通用户 session Pod。 | | `hwlab-edge-proxy` | 公网/FRP 入口和 HTTP 转发;不做业务权限,只转发 cookie/header,不注入伪 actor。 | | Postgres | v0.2 用户、session、授权、device pod 和既有 runtime durable state。 | diff --git a/docs/reference/spec-v02-cicd.md b/docs/reference/spec-v02-cicd.md index 58996f3e..746093a7 100644 --- a/docs/reference/spec-v02-cicd.md +++ b/docs/reference/spec-v02-cicd.md @@ -122,7 +122,7 @@ devops-infra git mirror 仍是 PipelineRun 和 Argo CD 的集群内读写源。` 7. affected service 通过 BuildKit 发布到 G14 本地 registry;reused service 复用 catalog digest。 所有 selected service 的 build TaskRun 都只依赖 `plan-artifacts`,不按 service 串行排队,也不设置 8 并发或其他 Pipeline 级限流。 实际并发由 Tekton controller、G14 节点资源、PVC I/O、BuildKit sidecar 和本地 registry 承载能力决定。 - `hwlab-cloud-web`、`hwlab-agent-worker` 和 `hwlab-device-pod` 必须使用 `env-reuse-git-mirror-checkout`。 + `hwlab-cloud-web` 和 `hwlab-device-pod` 必须使用 `env-reuse-git-mirror-checkout`。 只有 package/runtime/env 输入变化时才构建 `-env` 镜像。 纯前端源码、Device Pod 源码或 boot code 变化只更新三变量和 GitOps desired state, 不再重新构建业务镜像。 @@ -218,7 +218,7 @@ Git 读写都走 `devops-infra` 本地 mirror/relay。读路径用 HTTP mirror c 任何仍需要访问 GitHub canonical remote 的 SSH 操作都必须显式走 G14 proxy。Host SSH config 可以使用 OpenBSD `nc -X connect -x 127.0.0.1:10808 %h %p`;Tekton `GIT_SSH_COMMAND`、git-mirror `sync`/`flush` 和其他容器内 `git@github.com` 或 `ssh://git@ssh.github.com:443` 路径必须使用 repo-owned Node HTTP CONNECT ProxyCommand,并保留短 `ConnectTimeout` 与 `ServerAlive*`。只改 `HTTP_PROXY`/`HTTPS_PROXY` 对 OpenSSH 无效;BusyBox `nc` 不支持 `-X connect`,不得再把 `ssh.github.com:443` 当作等价 proxy 或让 GitHub SSH 直连反复 60s 超时。 -env image 复用把系统依赖和业务代码身份分离。`environmentDigest` 表示可复用运行环境,`HWLAB_BOOT_REPO`、`HWLAB_BOOT_COMMIT` 和 `HWLAB_BOOT_SH` 表示本次代码启动身份。code-only 变更只更新 boot metadata 和 runtime identity,不发布新 env image;只有真实 env 输入变化或缺少可复用 env digest 时才进入 env rebuild。`hwlab-agent-worker` 是 suspended Job template,也必须复用 env image:worker 代码变化只能更新 boot commit 和 Job template identity,不能重新构建 `hwlab-agent-worker` service image。`package.json` 只按 dependency/runtime 字段参与 env/runtime hash,`scripts` 只属于开发入口,不得因为清理旧门禁脚本而重建所有 service 或重建 env image。 +env image 复用把系统依赖和业务代码身份分离。`environmentDigest` 表示可复用运行环境,`HWLAB_BOOT_REPO`、`HWLAB_BOOT_COMMIT` 和 `HWLAB_BOOT_SH` 表示本次代码启动身份。code-only 变更只更新 boot metadata 和 runtime identity,不发布新 env image;只有真实 env 输入变化或缺少可复用 env digest 时才进入 env rebuild。AgentRun v0.1 是 HWLAB 外部共享执行基础设施,`hwlab-agent-worker` 不再作为 v0.2 service image 或 suspended Job template 参与 planner、BuildKit、artifact catalog 或 GitOps render。`package.json` 只按 dependency/runtime 字段参与 env/runtime hash,`scripts` 只属于开发入口,不得因为清理旧门禁脚本而重建所有 service 或重建 env image。 BuildKit publish 采用 service 级全并行 fan-out。 每个 service build task 拥有独立 BuildKit sidecar、独立 service workdir 和独立 report 文件,均从同一 `plan-artifacts` 结果判断是否需要执行。 diff --git a/docs/reference/spec-v02-hwlab-agent-mgr.md b/docs/reference/spec-v02-hwlab-agent-mgr.md deleted file mode 100644 index 4b046384..00000000 --- a/docs/reference/spec-v02-hwlab-agent-mgr.md +++ /dev/null @@ -1,47 +0,0 @@ -# v0.2 hwlab-agent-mgr 服务规格 - -`hwlab-agent-mgr` 是 agent runtime 管理服务,运行在 `hwlab-v02` namespace,内部端口 `7410`。当前实现是本地状态目录驱动的 runtime skeleton,不是 `hwlab-cloud-api` Code Agent 对话的主执行路径。 - -## 在系统中的职责划分 - -- 管理 agent session 生命周期模型:create、start、trace、finish、cleanup。 -- 生成 workspace volume、worker session、trace event 和 evidence record 的结构化状态。 -- 与 `hwlab-agent-worker`、`hwlab-agent-skills` 共同定义未来 session-scoped worker 执行边界。 -- 不直接暴露给普通用户;用户态 Code Agent 请求必须先进入 `hwlab-cloud-api`。 - -## 内部架构 - -- `cmd/hwlab-agent-mgr/main.ts` 同时提供 CLI 和健康 HTTP server。 -- `internal/agent/runtime.mjs` 负责文件系统状态、session lifecycle、trace/evidence 和 cleanup。 -- `internal/agent/index.mjs` 定义 serviceId、session status 和 skills manifest 结构。 -- `HWLAB_AGENT_RUNTIME_STATE_DIR` 可隔离状态目录;默认 `.state/agent-runtime` 只用于本地/测试状态。 - -## API 接口说明 - -| 接口 | 说明 | -| --- | --- | -| `GET /health`、`GET /health/live` | HTTP server 模式下返回 manager health、skills 注入状态和 build metadata。 | -| `GET /`、`GET /help` | 返回 CLI/服务用法。 | -| `bun run cmd/hwlab-agent-mgr/main.ts health` | 本地 JSON health。 | -| `create/status/trace/evidence/cleanup` | 本地 session lifecycle CLI;必须传 `--agent-session-id`,create 还需 `--project-id`。 | - -## 测试规格 - -## T1 - -阅读 docs/reference/spec-v02-hwlab-agent-mgr.md,然后用 cli 手动测试以下内容:访问 v02 ClusterIP 或 pod 内 `/health/live`,确认 `serviceId=hwlab-agent-mgr`,并记录 skills missing/ready 状态。 - -## T2 - -阅读 docs/reference/spec-v02-hwlab-agent-mgr.md,然后用 cli 手动测试以下内容:在隔离 `--state-dir` 下执行 create、status、trace、evidence、cleanup,确认每步输出 JSON 且生命周期顺序稳定。 - -## 规格的实现情况 - -| 规格项 | 状态 | 说明 | -| --- | --- | --- | -| HTTP health server | 已实现 | `server` 模式提供 `/health/live`。 | -| 本地 session lifecycle CLI | 已实现 | create/status/trace/evidence/cleanup 可用。 | -| skills commit/version readiness | 已实现 | health 会报告缺失字段;不静默 fallback。 | -| 调度真实 worker Job | 未完全实现 | 当前是 skeleton,本身不负责创建 Kubernetes worker Job。 | -| 接入 cloud-api 用户会话 | 未完全实现 | 当前 Code Agent 对话主要由 cloud-api 内部 Codex stdio manager 承担。 | - diff --git a/docs/reference/spec-v02-hwlab-agent-skills.md b/docs/reference/spec-v02-hwlab-agent-skills.md index a6e7b87b..482139a8 100644 --- a/docs/reference/spec-v02-hwlab-agent-skills.md +++ b/docs/reference/spec-v02-hwlab-agent-skills.md @@ -7,7 +7,7 @@ Code Agent 第一版 skill 来源分为两类:预装 skill 读取镜像内只 ## 在系统中的职责划分 - 证明当前 lane 中 skills bundle 随 source commit 发布,并为 live-build inventory 和 M4/M5 agent-loop 验收提供 health metadata。 -- 与 `hwlab-agent-mgr` 和 `hwlab-agent-worker` 的 skills commit/version readiness 对齐。 +- 与 `hwlab-cloud-api` 注入给 AgentRun v0.1 的 `skillRefs` 和 skills commit/version readiness 对齐。 - 预装 skill 只由镜像内 `/app/skills` 提供,不保存用户数据,不保存 Secret,不执行用户代码。 - 用户上传 skill 由 `hwlab-cloud-api` 管理,普通文件直接落到 PVC `/data/user-skills`,本服务不承载上传、预览或用户数据持久化 API。 diff --git a/docs/reference/spec-v02-hwlab-agent-worker.md b/docs/reference/spec-v02-hwlab-agent-worker.md deleted file mode 100644 index b12478ed..00000000 --- a/docs/reference/spec-v02-hwlab-agent-worker.md +++ /dev/null @@ -1,46 +0,0 @@ -# v0.2 hwlab-agent-worker 服务规格 - -`hwlab-agent-worker` 是 session-scoped 执行模板,在 `hwlab-v02` 中以 suspended Job template `hwlab-agent-worker-template` 存在。它不是长驻 HTTP 服务。 - -## 在系统中的职责划分 - -- 在未来由 manager 或调度器实例化为单次 worker Job,执行一个 agent session 的受控工作。 -- 负责写入 worker trace、evidence、workspace dry-run 结果,并在完成后退出。 -- 不保存长期用户状态,不直接处理浏览器或公网请求。 - -## 内部架构 - -- `cmd/hwlab-agent-worker/main.ts` 提供 CLI 命令 `health` 和 `dry-run/smoke`。 -- `internal/agent/runtime.mjs` 提供 `runLocalWorkerDryRun`,读取 manager 创建的本地 session 状态并写入 evidence。 -- Job template 通过 env 注入 `HWLAB_AGENT_SESSION_ID`、`HWLAB_SKILL_COMMIT_ID` 和 `HWLAB_SKILL_VERSION`。 -- v0.2 CI/CD 中 worker 使用 `env-reuse-git-mirror-checkout`:环境镜像只承载 runtime、依赖、launcher 和 git client,实际 worker 代码由 `HWLAB_BOOT_REPO`、`HWLAB_BOOT_COMMIT`、`HWLAB_BOOT_SH=deploy/runtime/boot/hwlab-agent-worker.sh` 从 `devops-infra` git mirror checkout 后启动。worker code-only 变更不得触发 `hwlab-agent-worker` service image BuildKit 重建,只允许更新 boot commit 和 Job template identity。 - -## API 接口说明 - -| 接口 | 说明 | -| --- | --- | -| `bun run cmd/hwlab-agent-worker/main.ts health` | 返回 worker health、runtime mode 和 skills 注入状态。 | -| `bun run cmd/hwlab-agent-worker/main.ts dry-run --agent-session-id ID` | 执行本地 dry-run worker 流程,写入 trace/evidence。 | -| Kubernetes Job template | `hwlab-agent-worker-template` 默认 `suspend: true`,只能由受控调度实例化。 | - -## 测试规格 - -## T1 - -阅读 docs/reference/spec-v02-hwlab-agent-worker.md,然后用 cli 手动测试以下内容:从 Job template dry-run 或本地 CLI 执行 `health`,确认输出 JSON,且缺少 skill commit/version 时明确 degraded 或 blocked。 - -## T2 - -阅读 docs/reference/spec-v02-hwlab-agent-worker.md,然后用 cli 手动测试以下内容:先用 agent-mgr 创建 session,再执行 worker `dry-run`,确认 trace 和 evidence 均写入同一 isolated state-dir。 - -## 规格的实现情况 - -| 规格项 | 状态 | 说明 | -| --- | --- | --- | -| CLI health/dry-run | 已实现 | worker 以 JSON 输出。 | -| suspended Job template | 已实现 | runtime-v02 中存在 `hwlab-agent-worker-template`。 | -| v0.2 env-reuse 启动 | 已实现 | `deploy/runtime/boot/hwlab-agent-worker.sh` 通过 git mirror checkout 后执行 worker CLI;code-only 变更复用 env image。 | -| session-scoped evidence 写入 | 已实现 | 本地 dry-run 覆盖 trace/evidence。 | -| 长驻 HTTP API | 不适用 | worker 不是长驻服务。 | -| 生产调度与真实执行 | 未完全实现 | 当前仍是 skeleton/dry-run 模式。 | - diff --git a/docs/reference/spec-v02-services.md b/docs/reference/spec-v02-services.md index 5c0dd542..80a09d34 100644 --- a/docs/reference/spec-v02-services.md +++ b/docs/reference/spec-v02-services.md @@ -20,6 +20,7 @@ - `hwlab-cloud-api` 是 v0.2 应用层 authority:用户身份、`admin/user`、Code Agent session owner、device pod grant、profile authority 和用户态 REST 都在这里判定。 - `hwlab-cloud-web` 只作为用户入口和 API proxy,不拥有业务 authority;CLI 可以旁路 UI,但不能旁路 `cloud-api` 的授权。 - `hwlab-device-pod` 是正式设备业务承载点;用户态请求必须走 `cloud-api -> hwlab-device-pod -> hwlab-gateway -> device-host-cli -> hardware`。 +- Code Agent session 归属、鉴权、trace 和用户态 API 收敛在 `hwlab-cloud-api`;执行调度接入 AgentRun v0.1 共享基础设施。`hwlab-agent-mgr`、`hwlab-agent-worker` 和 repo-owned codex-stdio supervisor 不是 v0.2 runtime service matrix,不再生成 Deployment、Job template、Service、artifact 或 GitOps desired state。 - `hwlab-gateway` 是 transport,不理解用户权限、不保存 profile authority;用户端已经验证稳定,v0.2 第一阶段先不改造它。 - Code Agent provider 通道分为 `codex-api` loopback forwarder 和 `deepseek` bridge/Moon Bridge;自研 bridge/forwarder 属于 HWLAB 常驻服务,Moon Bridge 和 hyueapi/DeepSeek upstream 是稳定外部依赖。 - `hwlab-router`、`hwlab-tunnel-client`、`hwlab-gateway-simu`、`hwlab-box-simu`、`hwlab-patch-panel` 在 v0.2 裁撤;不再为这些裁撤对象保留单独规格文档。 @@ -82,7 +83,7 @@ origin/v0.2 | Device Pod | `/v1/device-pods*`、正式 job/admin API | [spec-device-pod.md](spec-device-pod.md)、[spec-v02-hwlab-device-pod-service.md](spec-v02-hwlab-device-pod-service.md) | | Gateway transport | `cloud-api /v1/gateway/poll`、`/v1/gateway/result`、gateway `/status` | [spec-v02-hwlab-gateway.md](spec-v02-hwlab-gateway.md) | | Code Agent provider | `deepseek` 和 `codex-api` provider profile | [spec-v02-deepseek-proxy.md](spec-v02-deepseek-proxy.md)、[spec-v02-codex-api-forwarder.md](spec-v02-codex-api-forwarder.md) | -| Agent runtime skeleton | manager HTTP/CLI、worker Job template、skills health | [spec-v02-hwlab-agent-mgr.md](spec-v02-hwlab-agent-mgr.md)、[spec-v02-hwlab-agent-worker.md](spec-v02-hwlab-agent-worker.md)、[spec-v02-hwlab-agent-skills.md](spec-v02-hwlab-agent-skills.md) | +| Code Agent AgentRun 调度 | `hwlab-cloud-api` 会话 owner/auth/trace -> AgentRun v0.1 dispatch | [agentrun-code-agent-dispatch.md](agentrun-code-agent-dispatch.md)、[spec-v02-hwlab-agent-skills.md](spec-v02-hwlab-agent-skills.md) | | 短连接 CLI | `G14:/root/hwlab-v02` 内直接运行 `hwlab-cli client ...` | [spec-v02-hwlab-cli.md](spec-v02-hwlab-cli.md) | | Durable runtime store | Postgres TCP `5432` and cloud-api DB readiness | [spec-v02-postgres.md](spec-v02-postgres.md) | | 公网 FRP | master `frps` + `hwlab-v02-frpc` TCP `19666/19667` | [spec-v02-frpc.md](spec-v02-frpc.md) | @@ -98,10 +99,9 @@ origin/v0.2 | `hwlab-cloud-web` runtime wrapper | HWLAB 自研常驻 web/proxy wrapper | 保留 | 是,P0 | [spec-v02-hwlab-cloud-web.md](spec-v02-hwlab-cloud-web.md)、[cloud-workbench.md](cloud-workbench.md) | | `hwlab-edge-proxy` | HWLAB 自研常驻服务 | 保留 | 是,P0 | [spec-v02-hwlab-edge-proxy.md](spec-v02-hwlab-edge-proxy.md) | | `hwlab-device-pod` | HWLAB 自研常驻服务 | 保留并增强 | 是,P0 | [spec-v02-hwlab-device-pod-service.md](spec-v02-hwlab-device-pod-service.md)、[spec-device-pod.md](spec-device-pod.md) | -| `hwlab-agent-mgr` | HWLAB 自研常驻服务 | 保留,但职责需收敛 | 是,P1 | [spec-v02-hwlab-agent-mgr.md](spec-v02-hwlab-agent-mgr.md) | | `hwlab-codex-api-responses-forwarder` | HWLAB 自研常驻 sidecar | 保留 | 是,P1 | [spec-v02-codex-api-forwarder.md](spec-v02-codex-api-forwarder.md) | | `hwlab-deepseek-responses-bridge` / `hwlab-deepseek-proxy` | HWLAB 自研 bridge + Moon Bridge 外部依赖 | 保留 | 是,P1 for bridge | [spec-v02-deepseek-proxy.md](spec-v02-deepseek-proxy.md) | -| `hwlab-agent-worker` | HWLAB 自研 Job/执行入口 | 保留,非第一波 | 建议迁,P2 | [spec-v02-hwlab-agent-worker.md](spec-v02-hwlab-agent-worker.md) | +| AgentRun v0.1 runner | 共享 Agent 执行基础设施 | 作为外部基础设施接入,不进 HWLAB service/artifact matrix | 否 | [agentrun-code-agent-dispatch.md](agentrun-code-agent-dispatch.md) | | `hwlab-agent-skills` wrapper | HWLAB 自研 bundle/health wrapper | 保留 | 仅常驻 wrapper 需要,P2 | [spec-v02-hwlab-agent-skills.md](spec-v02-hwlab-agent-skills.md) | | `hwlab-gateway` | HWLAB 自研用户端/硬件 transport | 保留 | 暂不迁 | [spec-v02-hwlab-gateway.md](spec-v02-hwlab-gateway.md)、[gateway-outbound-demo.md](gateway-outbound-demo.md) | | `hwlab-router` | HWLAB 自研路由占位服务 | 裁撤 | 否 | 本文即裁撤权威,不保留单独 spec | @@ -140,7 +140,6 @@ hwlab-cloud-api hwlab-cloud-web runtime wrapper hwlab-edge-proxy hwlab-device-pod -hwlab-agent-mgr hwlab-codex-api-responses-forwarder hwlab-deepseek-responses-bridge browser-side Cloud Web JS @@ -149,7 +148,6 @@ browser-side Cloud Web JS 后续迁移集合: ```text -hwlab-agent-worker hwlab-agent-skills wrapper ``` @@ -170,6 +168,8 @@ hwlab-tunnel-client hwlab-gateway-simu hwlab-box-simu hwlab-patch-panel +hwlab-agent-mgr +hwlab-agent-worker ``` 裁撤集合不再新增单服务 spec。若历史文档仍提到这些服务作为 v0.2 必需依赖,应删除旧口径或交叉引用本文。 diff --git a/internal/agent/index.mjs b/internal/agent/index.mjs deleted file mode 100644 index 9d75c1c5..00000000 --- a/internal/agent/index.mjs +++ /dev/null @@ -1,361 +0,0 @@ -import crypto from "node:crypto"; -import path from "node:path"; - -import { ENVIRONMENT_DEV } from "../protocol/index.mjs"; - -export const AGENT_MGR_SERVICE_ID = "hwlab-agent-mgr"; -export const AGENT_WORKER_SERVICE_ID = "hwlab-agent-worker"; -export const AGENT_SKILLS_SERVICE_ID = "hwlab-agent-skills"; - -export const AGENT_SESSION_STATUSES = Object.freeze([ - "create", - "start", - "trace", - "finish", - "cleanup" -]); - -export const WORKER_SESSION_STATUSES = Object.freeze([ - "create", - "start", - "trace", - "finish", - "cleanup" -]); - -export function createAgentSession({ - agentSessionId, - projectId, - requestedBy = "usr_local", - goal = "", - environment = ENVIRONMENT_DEV, - now = () => new Date().toISOString(), - metadata = {} -}) { - const timestamp = now(); - return { - agentSessionId, - projectId, - serviceId: AGENT_MGR_SERVICE_ID, - status: "create", - environment, - requestedBy, - goal, - startedAt: timestamp, - updatedAt: timestamp, - metadata: { - ...metadata, - lifecycle: AGENT_SESSION_STATUSES.slice(), - currentState: "create" - } - }; -} - -export function advanceAgentSession(session, nextStatus, { now = () => new Date().toISOString(), extra = {} } = {}) { - assertAgentStatus(nextStatus); - const timestamp = now(); - const completedAt = nextStatus === "finish" ? timestamp : session.completedAt; - return { - ...session, - status: nextStatus, - completedAt, - updatedAt: timestamp, - metadata: { - ...session.metadata, - ...extra, - currentState: nextStatus - } - }; -} - -export function createWorkspaceVolume({ agentSessionId, projectId, now = () => new Date().toISOString() }) { - return { - workspaceVolumeId: `vol_${agentSessionId}`, - agentSessionId, - projectId, - serviceId: AGENT_WORKER_SERVICE_ID, - mountPath: path.posix.join("/workspace", projectId, agentSessionId), - lifecycle: "create", - createdAt: now(), - updatedAt: now() - }; -} - -export function createWorkerSession({ - workerSessionId, - agentSessionId, - projectId, - gatewaySessionId = null, - now = () => new Date().toISOString(), - metadata = {} -}) { - const timestamp = now(); - return { - workerSessionId, - agentSessionId, - projectId, - serviceId: AGENT_WORKER_SERVICE_ID, - gatewaySessionId, - status: "create", - environment: ENVIRONMENT_DEV, - startedAt: timestamp, - updatedAt: timestamp, - metadata: { - ...metadata, - lifecycle: WORKER_SESSION_STATUSES.slice(), - workspaceVolumeId: `vol_${agentSessionId}`, - currentState: "create" - } - }; -} - -export function advanceWorkerSession(session, nextStatus, { now = () => new Date().toISOString(), extra = {} } = {}) { - assertWorkerStatus(nextStatus); - const timestamp = now(); - const completedAt = nextStatus === "finish" ? timestamp : session.completedAt; - return { - ...session, - status: nextStatus, - completedAt, - updatedAt: timestamp, - metadata: { - ...session.metadata, - ...extra, - currentState: nextStatus - } - }; -} - -export function createTraceEvent({ - traceEventId, - traceId, - projectId, - serviceId = AGENT_MGR_SERVICE_ID, - level = "info", - message, - environment = ENVIRONMENT_DEV, - now = () => new Date().toISOString(), - agentSessionId = null, - workerSessionId = null, - operationId = null, - metadata = {} -}) { - return { - traceEventId, - traceId, - projectId, - serviceId, - level, - message, - environment, - occurredAt: now(), - agentSessionId, - workerSessionId, - operationId, - metadata: { - ...metadata - } - }; -} - -export function createEvidenceRecord({ - evidenceId, - projectId, - operationId, - uri, - sha256 = digestHex(uri), - serviceId = AGENT_WORKER_SERVICE_ID, - environment = ENVIRONMENT_DEV, - kind = "artifact", - now = () => new Date().toISOString(), - agentSessionId = null, - workerSessionId = null, - mimeType = "text/plain", - sizeBytes = 0, - metadata = {} -}) { - return { - evidenceId, - projectId, - operationId, - agentSessionId, - workerSessionId, - kind, - uri, - mimeType, - sha256, - sizeBytes, - serviceId, - environment, - metadata: { - ...metadata - }, - createdAt: now() - }; -} - -export function createAgentRunPlan({ - agentSessionId, - projectId, - goal = "", - prompt = "", - skillCommitId = null, - skillVersion = null, - now = () => new Date().toISOString() -}) { - const traceId = `trc_${agentSessionId}`; - const workerSessionId = `wkr_${agentSessionId}`; - const operationId = `op_${agentSessionId}`; - const evidenceId = `evi_${agentSessionId}`; - const traceEventId = `tev_${agentSessionId}`; - const workspaceVolume = createWorkspaceVolume({ agentSessionId, projectId, now }); - const agentSession = createAgentSession({ - agentSessionId, - projectId, - goal, - now - }); - const workerSession = createWorkerSession({ - workerSessionId, - agentSessionId, - projectId, - now - }); - const traceEvent = createTraceEvent({ - traceEventId, - traceId, - projectId, - message: prompt || goal || "agent runtime dry run", - agentSessionId, - workerSessionId, - operationId, - now - }); - const evidenceRecord = createEvidenceRecord({ - evidenceId, - projectId, - operationId, - uri: `file://${path.posix.join(workspaceVolume.mountPath, "evidence.txt")}`, - agentSessionId, - workerSessionId, - metadata: { - skillCommitId, - skillVersion, - prompt, - goal, - workspaceVolumeId: workspaceVolume.workspaceVolumeId - }, - now - }); - - return { - agentSession: advanceAgentSession(agentSession, "start", { - now, - extra: { - workerSessionId, - workspaceVolumeId: workspaceVolume.workspaceVolumeId - } - }), - workerSession: advanceWorkerSession(workerSession, "start", { - now, - extra: { - workspaceVolumeId: workspaceVolume.workspaceVolumeId - } - }), - workspaceVolume, - events: [ - { state: "create", actor: AGENT_MGR_SERVICE_ID, agentSessionId }, - { state: "start", actor: AGENT_WORKER_SERVICE_ID, workerSessionId }, - { state: "trace", actor: AGENT_MGR_SERVICE_ID, traceEvent }, - { state: "finish", actor: AGENT_WORKER_SERVICE_ID, evidenceRecord }, - { state: "cleanup", actor: AGENT_WORKER_SERVICE_ID, workspaceVolumeId: workspaceVolume.workspaceVolumeId } - ], - traceEvent, - evidenceRecord, - skillCommitId, - skillVersion - }; -} - -export function finalizeAgentRun(plan, { now = () => new Date().toISOString() } = {}) { - const finishedAgentSession = advanceAgentSession(plan.agentSession, "finish", { now }); - const finishedWorkerSession = advanceWorkerSession(plan.workerSession, "finish", { now }); - return { - ...plan, - agentSession: advanceAgentSession(finishedAgentSession, "cleanup", { - now, - extra: { cleanedUp: true } - }), - workerSession: advanceWorkerSession(finishedWorkerSession, "cleanup", { - now, - extra: { cleanedUp: true } - }), - cleanup: { - workspaceVolumeId: plan.workspaceVolume.workspaceVolumeId, - removed: true - } - }; -} - -export function createSkillsManifest({ - commitId, - version, - skillName = "hwlab-agent-runtime", - serviceId = AGENT_SKILLS_SERVICE_ID, - environment = ENVIRONMENT_DEV, - skills = [] -}) { - if (!commitId) { - throw new Error("skills manifest requires explicit commitId"); - } - if (!version) { - throw new Error("skills manifest requires explicit version"); - } - return { - manifestVersion: "v1", - environment, - commitId, - version, - namespace: "hwlab", - endpoint: "http://74.48.78.17:16667", - profiles: { - dev: { - name: "dev", - enabled: true, - namespace: "hwlab-dev", - endpoint: "http://74.48.78.17:16667", - notes: "explicit repo-local skills artifact" - } - }, - services: [ - { - serviceId, - image: `${skillName}:${commitId.slice(0, 7)}`, - namespace: "hwlab-dev", - healthPath: "/health", - profile: "dev", - env: { - HWLAB_SKILLS_COMMIT_ID: commitId, - HWLAB_SKILLS_VERSION: version - } - } - ], - skills - }; -} - -function assertAgentStatus(status) { - if (!AGENT_SESSION_STATUSES.includes(status)) { - throw new Error(`unknown agent session status ${JSON.stringify(status)}`); - } -} - -function assertWorkerStatus(status) { - if (!WORKER_SESSION_STATUSES.includes(status)) { - throw new Error(`unknown worker session status ${JSON.stringify(status)}`); - } -} - -function digestHex(value) { - return crypto.createHash("sha256").update(String(value)).digest("hex"); -} diff --git a/internal/agent/index.test.mjs b/internal/agent/index.test.mjs deleted file mode 100644 index d1c6f5b6..00000000 --- a/internal/agent/index.test.mjs +++ /dev/null @@ -1,178 +0,0 @@ -import assert from "node:assert/strict"; -import { mkdtemp, rm } from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import test from "node:test"; - -import { - AGENT_SESSION_STATUSES, - WORKER_SESSION_STATUSES, - advanceAgentSession, - advanceWorkerSession, - createAgentRunPlan, - createSkillsManifest, - finalizeAgentRun -} from "./index.mjs"; -import { - createLocalAgentSession, - getLocalAgentEvidence, - getLocalAgentStatus, - getLocalAgentTrace, - runLocalWorkerDryRun -} from "./runtime.mjs"; - -test("agent runtime run plan exposes create/start/trace/finish/cleanup", () => { - const plan = createAgentRunPlan({ - agentSessionId: "agt_01J00000000000000000000000", - projectId: "prj_01J00000000000000000000000", - goal: "verify runtime skeleton", - prompt: "dry run" - }); - - assert.deepEqual(AGENT_SESSION_STATUSES, ["create", "start", "trace", "finish", "cleanup"]); - assert.deepEqual(WORKER_SESSION_STATUSES, ["create", "start", "trace", "finish", "cleanup"]); - assert.equal(plan.agentSession.status, "start"); - assert.equal(plan.workerSession.status, "start"); - assert.equal(plan.events.map((event) => event.state).join(","), "create,start,trace,finish,cleanup"); - assert.equal(plan.traceEvent.message, "dry run"); - assert.equal(plan.evidenceRecord.projectId, "prj_01J00000000000000000000000"); - assert.equal(plan.evidenceRecord.metadata.workspaceVolumeId, "vol_agt_01J00000000000000000000000"); -}); - -test("finalizeAgentRun marks cleanup and keeps evidence visible", () => { - const plan = createAgentRunPlan({ - agentSessionId: "agt_01J00000000000000000000001", - projectId: "prj_01J00000000000000000000001", - goal: "cleanup coverage" - }); - const finished = finalizeAgentRun(plan); - - assert.equal(finished.agentSession.status, "cleanup"); - assert.equal(finished.workerSession.status, "cleanup"); - assert.equal(finished.cleanup.removed, true); - assert.ok(finished.evidenceRecord.sha256); -}); - -test("skills manifest requires explicit commitId", () => { - assert.throws(() => createSkillsManifest({}), /explicit commitId/); - assert.throws(() => createSkillsManifest({ commitId: "6509a35" }), /explicit version/); - - const manifest = createSkillsManifest({ commitId: "6509a35", version: "v1" }); - assert.equal(manifest.commitId, "6509a35"); - assert.equal(manifest.version, "v1"); - assert.equal(manifest.services[0].serviceId, "hwlab-agent-skills"); - assert.equal(manifest.services[0].env.HWLAB_SKILLS_VERSION, "v1"); -}); - -test("advance helpers preserve completedAt on non-finish states", () => { - const session = { - status: "start", - completedAt: undefined, - metadata: {} - }; - - const agent = advanceAgentSession(session, "trace"); - const worker = advanceWorkerSession(session, "trace"); - - assert.equal(agent.status, "trace"); - assert.equal(worker.status, "trace"); - assert.equal(agent.completedAt, undefined); - assert.equal(worker.completedAt, undefined); -}); - -test("local runtime degrades health and evidence when skills fields are missing", async () => { - const stateDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-runtime-missing-")); - try { - const created = await createLocalAgentSession({ - stateDir, - agentSessionId: "agt_runtime_missing_skills", - projectId: "prj_runtime_missing_skills", - goal: "verify degraded skills evidence", - prompt: "missing skills smoke" - }); - assert.equal(created.health.status, "degraded"); - assert.deepEqual(created.health.skills.manager.missing, ["skillCommitId", "skillVersion"]); - - const worker = await runLocalWorkerDryRun({ - stateDir, - agentSessionId: "agt_runtime_missing_skills" - }); - assert.equal(worker.health.status, "degraded"); - assert.equal(worker.evidenceRecord.metadata.health.status, "degraded"); - assert.deepEqual(worker.evidenceRecord.metadata.health.skills.worker.missing, [ - "skillCommitId", - "skillVersion" - ]); - assert.equal(worker.cleanup.removed, true); - assert.equal(worker.workspace.existsAfterCleanup, false); - - const evidence = await getLocalAgentEvidence({ - stateDir, - agentSessionId: "agt_runtime_missing_skills" - }); - assert.equal(evidence.status, "degraded"); - assert.equal(evidence.evidenceRecords.length, 1); - } finally { - await rm(stateDir, { recursive: true, force: true }); - } -}); - -test("local runtime closes create/status/trace/evidence/cleanup loop", async () => { - const stateDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-runtime-loop-")); - try { - const created = await createLocalAgentSession({ - stateDir, - agentSessionId: "agt_runtime_loop", - projectId: "prj_runtime_loop", - goal: "verify local worker bootstrap", - prompt: "closed loop smoke", - skillCommitId: "abc1234", - skillVersion: "v1" - }); - assert.equal(created.health.status, "ok"); - assert.equal(created.task.status, "queued"); - assert.equal(created.traceCount, 1); - - const worker = await runLocalWorkerDryRun({ - stateDir, - agentSessionId: "agt_runtime_loop", - skillCommitId: "abc1234", - skillVersion: "v1" - }); - assert.equal(worker.health.status, "ok"); - assert.equal(worker.agentSession.status, "cleanup"); - assert.equal(worker.workerSession.status, "cleanup"); - assert.equal(worker.task.status, "completed"); - assert.equal(worker.evidenceRecord.metadata.skills.worker.commitId, "abc1234"); - assert.equal(worker.evidenceRecord.metadata.skills.worker.version, "v1"); - assert.equal(worker.workspace.cleaned, true); - assert.equal(worker.workspace.existsAfterCleanup, false); - - const status = await getLocalAgentStatus({ - stateDir, - agentSessionId: "agt_runtime_loop" - }); - assert.equal(status.health.status, "ok"); - assert.equal(status.evidenceCount, 1); - assert.equal(status.cleanup.removed, true); - - const trace = await getLocalAgentTrace({ - stateDir, - agentSessionId: "agt_runtime_loop" - }); - assert.deepEqual( - trace.traceEvents.map((event) => event.metadata.state), - ["create", "start", "trace", "finish", "cleanup"] - ); - - const evidence = await getLocalAgentEvidence({ - stateDir, - agentSessionId: "agt_runtime_loop" - }); - assert.equal(evidence.status, "ok"); - assert.equal(evidence.evidenceRecords[0].kind, "report"); - assert.equal(evidence.evidenceRecords[0].sha256.length, 64); - } finally { - await rm(stateDir, { recursive: true, force: true }); - } -}); diff --git a/internal/agent/runtime.mjs b/internal/agent/runtime.mjs deleted file mode 100644 index ed48bb67..00000000 --- a/internal/agent/runtime.mjs +++ /dev/null @@ -1,657 +0,0 @@ -import crypto from "node:crypto"; -import { constants as fsConstants } from "node:fs"; -import { access, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises"; -import path from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; - -import { - AGENT_MGR_SERVICE_ID, - AGENT_WORKER_SERVICE_ID, - advanceAgentSession, - advanceWorkerSession, - createAgentSession, - createEvidenceRecord, - createTraceEvent, - createWorkerSession, - createWorkspaceVolume -} from "./index.mjs"; -import { ENVIRONMENT_DEV } from "../protocol/index.mjs"; - -const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); - -export const DEFAULT_AGENT_RUNTIME_STATE_DIR = path.join(repoRoot, ".state", "agent-runtime"); -export const LOCAL_DRY_RUN_MODE = "local-dry-run"; - -export function resolveAgentRuntimeStateDir(stateDir = process.env.HWLAB_AGENT_RUNTIME_STATE_DIR) { - return path.resolve(stateDir || DEFAULT_AGENT_RUNTIME_STATE_DIR); -} - -export function assessSkillsInjection({ skillCommitId = null, skillVersion = null, role = "runtime" } = {}) { - const commitId = explicitValue(skillCommitId); - const version = explicitValue(skillVersion); - const missing = []; - - if (!commitId) missing.push("skillCommitId"); - if (!version) missing.push("skillVersion"); - - return { - role, - status: missing.length === 0 ? "ready" : "degraded", - commitId, - version, - missing, - source: "explicit-field" - }; -} - -export function createRuntimeHealth({ - stateDir, - managerSkills = null, - workerSkills = null, - workspaceCleaned = false -} = {}) { - const missing = []; - const mismatches = []; - - for (const skills of [managerSkills, workerSkills].filter(Boolean)) { - for (const field of skills.missing) { - missing.push(`${skills.role}.${field}`); - } - } - - if (managerSkills && workerSkills) { - if (managerSkills.commitId && workerSkills.commitId && managerSkills.commitId !== workerSkills.commitId) { - mismatches.push("skillCommitId"); - } - if (managerSkills.version && workerSkills.version && managerSkills.version !== workerSkills.version) { - mismatches.push("skillVersion"); - } - } - - const status = missing.length === 0 && mismatches.length === 0 ? "ok" : "degraded"; - - return { - serviceId: AGENT_MGR_SERVICE_ID, - status, - environment: ENVIRONMENT_DEV, - runtimeMode: LOCAL_DRY_RUN_MODE, - stateDir: stateDir ? resolveAgentRuntimeStateDir(stateDir) : undefined, - workspaceCleaned, - skills: { - manager: managerSkills, - worker: workerSkills, - missing, - mismatches - } - }; -} - -export async function createLocalAgentSession({ - stateDir, - agentSessionId, - projectId, - goal = "", - prompt = "", - requestedBy = "usr_local", - skillCommitId = null, - skillVersion = null, - now = () => new Date().toISOString() -}) { - requireId(agentSessionId, "agentSessionId"); - requireId(projectId, "projectId"); - - const root = resolveAgentRuntimeStateDir(stateDir); - const sessionDir = localAgentSessionDir(root, agentSessionId); - if (await pathExists(sessionDir)) { - throw new Error(`agent session already exists: ${agentSessionId}`); - } - - const timestamp = now(); - const traceId = `trc_${agentSessionId}`; - const operationId = `op_${agentSessionId}`; - const workerSessionId = `wkr_${agentSessionId}`; - const taskId = `tsk_${agentSessionId}`; - const workspacePath = path.join(root, "workspaces", agentSessionId); - const managerSkills = assessSkillsInjection({ - skillCommitId, - skillVersion, - role: "manager" - }); - const health = createRuntimeHealth({ stateDir: root, managerSkills }); - const workspaceVolume = { - ...createWorkspaceVolume({ agentSessionId, projectId, now }), - mountPath: workspacePath, - localPath: workspacePath - }; - const agentSession = createAgentSession({ - agentSessionId, - projectId, - requestedBy, - goal, - now, - metadata: { - runtimeMode: LOCAL_DRY_RUN_MODE, - traceId, - operationId, - workerSessionId, - taskId, - prompt, - skills: managerSkills - } - }); - const workerSession = createWorkerSession({ - workerSessionId, - agentSessionId, - projectId, - now, - metadata: { - runtimeMode: LOCAL_DRY_RUN_MODE, - traceId, - operationId, - taskId, - scopedProjectId: projectId, - workspaceVolumeId: workspaceVolume.workspaceVolumeId, - expectedSkills: managerSkills - } - }); - - const state = { - schemaVersion: 1, - environment: ENVIRONMENT_DEV, - runtimeMode: LOCAL_DRY_RUN_MODE, - stateDir: root, - sessionDir, - createdAt: timestamp, - updatedAt: timestamp, - health, - task: { - taskId, - traceId, - operationId, - status: "queued", - goal, - prompt, - createdAt: timestamp, - updatedAt: timestamp - }, - agentSession, - workerSession, - workspaceVolume, - workspace: { - workspaceVolumeId: workspaceVolume.workspaceVolumeId, - path: workspacePath, - cleaned: false, - existsAfterCleanup: null - }, - traceEvents: [], - evidenceRecords: [], - cleanup: null - }; - - state.traceEvents.push( - createRuntimeTraceEvent(state, { - suffix: "create", - serviceId: AGENT_MGR_SERVICE_ID, - message: "Agent manager created local dry-run session", - state: "create", - now, - metadata: { - taskId, - healthStatus: health.status, - skills: managerSkills - } - }) - ); - - await writeLocalAgentState(root, state); - return summarizeLocalAgentSession(state); -} - -export async function readLocalAgentSession({ stateDir, agentSessionId }) { - requireId(agentSessionId, "agentSessionId"); - const root = resolveAgentRuntimeStateDir(stateDir); - return readLocalAgentState(root, agentSessionId); -} - -export async function getLocalAgentStatus({ stateDir, agentSessionId }) { - const state = await readLocalAgentSession({ stateDir, agentSessionId }); - return summarizeLocalAgentSession(state); -} - -export async function getLocalAgentTrace({ stateDir, agentSessionId }) { - const state = await readLocalAgentSession({ stateDir, agentSessionId }); - return { - serviceId: AGENT_MGR_SERVICE_ID, - agentSessionId, - traceId: state.task.traceId, - status: state.health.status, - tracePath: localAgentTracePath(state.sessionDir), - traceEvents: state.traceEvents - }; -} - -export async function getLocalAgentEvidence({ stateDir, agentSessionId }) { - const state = await readLocalAgentSession({ stateDir, agentSessionId }); - const evidenceHealth = state.evidenceRecords.some((record) => record.metadata?.health?.status === "degraded") - ? "degraded" - : state.health.status; - - return { - serviceId: AGENT_MGR_SERVICE_ID, - agentSessionId, - operationId: state.task.operationId, - status: evidenceHealth, - evidenceDir: localAgentEvidenceDir(state.sessionDir), - evidenceRecords: state.evidenceRecords - }; -} - -export async function cleanupLocalAgentSession({ - stateDir, - agentSessionId, - actor = AGENT_MGR_SERVICE_ID, - now = () => new Date().toISOString() -}) { - requireId(agentSessionId, "agentSessionId"); - const root = resolveAgentRuntimeStateDir(stateDir); - const state = await readLocalAgentState(root, agentSessionId); - - if (state.cleanup?.completed === true) { - return { - serviceId: actor, - agentSessionId, - idempotent: true, - cleanup: state.cleanup, - workspace: state.workspace, - status: state.health.status - }; - } - - await cleanupWorkspaceForState(state, { actor, now }); - await writeLocalAgentState(root, state); - - return { - serviceId: actor, - agentSessionId, - idempotent: false, - cleanup: state.cleanup, - workspace: state.workspace, - status: state.health.status - }; -} - -export async function runLocalWorkerDryRun({ - stateDir, - agentSessionId, - skillCommitId = null, - skillVersion = null, - now = () => new Date().toISOString() -}) { - requireId(agentSessionId, "agentSessionId"); - const root = resolveAgentRuntimeStateDir(stateDir); - const state = await readLocalAgentState(root, agentSessionId); - - if (state.cleanup?.completed === true) { - throw new Error(`agent session is already cleaned up: ${agentSessionId}`); - } - - const workerSkills = assessSkillsInjection({ - skillCommitId, - skillVersion, - role: "worker" - }); - const health = createRuntimeHealth({ - stateDir: root, - managerSkills: state.health.skills.manager, - workerSkills - }); - const timestamp = now(); - state.updatedAt = timestamp; - state.health = health; - state.task = { - ...state.task, - status: "claimed", - claimedAt: timestamp, - updatedAt: timestamp - }; - state.agentSession = advanceAgentSession(state.agentSession, "start", { - now, - extra: { - workerSessionId: state.workerSession.workerSessionId, - workspaceVolumeId: state.workspace.workspaceVolumeId, - taskStatus: "claimed", - healthStatus: health.status - } - }); - state.workerSession = advanceWorkerSession(state.workerSession, "start", { - now, - extra: { - workspaceVolumeId: state.workspace.workspaceVolumeId, - workspacePath: state.workspace.path, - taskStatus: "claimed", - healthStatus: health.status, - skills: workerSkills - } - }); - - await mkdir(state.workspace.path, { recursive: true }); - state.traceEvents.push( - createRuntimeTraceEvent(state, { - suffix: "start", - serviceId: AGENT_WORKER_SERVICE_ID, - message: "Agent worker claimed local dry-run task", - state: "start", - now, - metadata: { - taskId: state.task.taskId, - workspacePath: state.workspace.path, - healthStatus: health.status, - skills: workerSkills - } - }) - ); - - const workspaceTracePath = path.join(state.workspace.path, "trace.ndjson"); - const workspaceTrace = createRuntimeTraceEvent(state, { - suffix: "trace", - serviceId: AGENT_WORKER_SERVICE_ID, - message: "Agent worker wrote local dry-run trace", - state: "trace", - now, - metadata: { - taskId: state.task.taskId, - workspaceTracePath, - healthStatus: health.status - } - }); - state.agentSession = advanceAgentSession(state.agentSession, "trace", { - now, - extra: { - taskStatus: "running", - healthStatus: health.status - } - }); - state.workerSession = advanceWorkerSession(state.workerSession, "trace", { - now, - extra: { - taskStatus: "running", - healthStatus: health.status - } - }); - state.traceEvents.push(workspaceTrace); - await writeFile(workspaceTracePath, `${JSON.stringify(workspaceTrace)}\n`, "utf8"); - - const evidencePayload = { - schemaVersion: 1, - environment: ENVIRONMENT_DEV, - runtimeMode: LOCAL_DRY_RUN_MODE, - agentSessionId, - workerSessionId: state.workerSession.workerSessionId, - taskId: state.task.taskId, - traceId: state.task.traceId, - operationId: state.task.operationId, - goal: state.task.goal, - prompt: state.task.prompt, - health, - traceEventIds: state.traceEvents.map((event) => event.traceEventId), - cleanup: { - workspacePath: state.workspace.path, - workspaceWillBeRemoved: true - }, - createdAt: now() - }; - const evidenceContent = `${JSON.stringify(evidencePayload, null, 2)}\n`; - const evidenceDir = localAgentEvidenceDir(state.sessionDir); - const evidenceArtifactPath = path.join(evidenceDir, `worker-dry-run-${agentSessionId}.json`); - await mkdir(evidenceDir, { recursive: true }); - await writeFile(evidenceArtifactPath, evidenceContent, "utf8"); - const evidenceStats = await stat(evidenceArtifactPath); - const evidenceRecord = createEvidenceRecord({ - evidenceId: `evi_${agentSessionId}`, - projectId: state.agentSession.projectId, - operationId: state.task.operationId, - uri: pathToFileURL(evidenceArtifactPath).href, - sha256: sha256Hex(evidenceContent), - serviceId: AGENT_WORKER_SERVICE_ID, - environment: ENVIRONMENT_DEV, - kind: "report", - agentSessionId, - workerSessionId: state.workerSession.workerSessionId, - mimeType: "application/json", - sizeBytes: evidenceStats.size, - metadata: { - runtimeMode: LOCAL_DRY_RUN_MODE, - traceId: state.task.traceId, - taskId: state.task.taskId, - workspaceVolumeId: state.workspace.workspaceVolumeId, - health, - skills: { - manager: state.health.skills.manager, - worker: workerSkills - } - }, - now - }); - state.evidenceRecords.push(evidenceRecord); - state.traceEvents.push( - createRuntimeTraceEvent(state, { - suffix: "finish", - serviceId: AGENT_WORKER_SERVICE_ID, - message: "Agent worker persisted local dry-run evidence", - state: "finish", - now, - metadata: { - taskId: state.task.taskId, - evidenceId: evidenceRecord.evidenceId, - healthStatus: health.status - } - }) - ); - state.task = { - ...state.task, - status: "completed", - completedAt: now(), - updatedAt: now() - }; - state.agentSession = advanceAgentSession(state.agentSession, "finish", { - now, - extra: { - taskStatus: "completed", - healthStatus: health.status - } - }); - state.workerSession = advanceWorkerSession(state.workerSession, "finish", { - now, - extra: { - taskStatus: "completed", - evidenceId: evidenceRecord.evidenceId, - healthStatus: health.status - } - }); - - await cleanupWorkspaceForState(state, { - actor: AGENT_WORKER_SERVICE_ID, - now - }); - await writeLocalAgentState(root, state); - - return { - serviceId: AGENT_WORKER_SERVICE_ID, - dryRun: true, - agentSession: state.agentSession, - workerSession: state.workerSession, - task: state.task, - health: state.health, - traceEvents: state.traceEvents, - evidenceRecord, - evidencePath: evidenceArtifactPath, - cleanup: state.cleanup, - workspace: state.workspace - }; -} - -export function summarizeLocalAgentSession(state) { - return { - serviceId: AGENT_MGR_SERVICE_ID, - agentSessionId: state.agentSession.agentSessionId, - status: state.health.status, - environment: state.environment, - runtimeMode: state.runtimeMode, - stateDir: state.stateDir, - sessionDir: state.sessionDir, - health: state.health, - task: state.task, - agentSession: state.agentSession, - workerSession: state.workerSession, - workspace: state.workspace, - traceCount: state.traceEvents.length, - evidenceCount: state.evidenceRecords.length, - cleanup: state.cleanup - }; -} - -function createRuntimeTraceEvent(state, { suffix, serviceId, message, state: lifecycleState, metadata = {}, now }) { - return createTraceEvent({ - traceEventId: `tev_${state.agentSession.agentSessionId}_${suffix}`, - traceId: state.task.traceId, - projectId: state.agentSession.projectId, - serviceId, - level: metadata.healthStatus === "degraded" ? "warn" : "info", - message, - environment: ENVIRONMENT_DEV, - agentSessionId: state.agentSession.agentSessionId, - workerSessionId: state.workerSession.workerSessionId, - operationId: state.task.operationId, - metadata: { - ...metadata, - state: lifecycleState - }, - now - }); -} - -async function cleanupWorkspaceForState(state, { actor, now }) { - const existedBeforeCleanup = await pathExists(state.workspace.path); - await rm(state.workspace.path, { recursive: true, force: true }); - const existsAfterCleanup = await pathExists(state.workspace.path); - const timestamp = now(); - - state.cleanup = { - cleanupId: `cln_${state.agentSession.agentSessionId}`, - serviceId: actor, - agentSessionId: state.agentSession.agentSessionId, - workerSessionId: state.workerSession.workerSessionId, - workspaceVolumeId: state.workspace.workspaceVolumeId, - workspacePath: state.workspace.path, - completed: true, - existedBeforeCleanup, - removed: !existsAfterCleanup, - existsAfterCleanup, - cleanedAt: timestamp - }; - state.workspace = { - ...state.workspace, - cleaned: true, - existsAfterCleanup - }; - state.health = { - ...state.health, - workspaceCleaned: true - }; - state.agentSession = advanceAgentSession(state.agentSession, "cleanup", { - now, - extra: { - cleanedUp: true, - cleanupId: state.cleanup.cleanupId, - healthStatus: state.health.status - } - }); - state.workerSession = advanceWorkerSession(state.workerSession, "cleanup", { - now, - extra: { - cleanedUp: true, - cleanupId: state.cleanup.cleanupId, - healthStatus: state.health.status - } - }); - state.traceEvents.push( - createRuntimeTraceEvent(state, { - suffix: "cleanup", - serviceId: actor, - message: "Agent worker cleaned local dry-run workspace", - state: "cleanup", - now, - metadata: { - cleanupId: state.cleanup.cleanupId, - workspaceVolumeId: state.workspace.workspaceVolumeId, - workspacePath: state.workspace.path, - removed: state.cleanup.removed, - healthStatus: state.health.status - } - }) - ); - state.updatedAt = timestamp; -} - -async function readLocalAgentState(root, agentSessionId) { - const statePath = localAgentStatePath(localAgentSessionDir(root, agentSessionId)); - try { - const raw = await readFile(statePath, "utf8"); - return JSON.parse(raw); - } catch (error) { - if (error.code === "ENOENT") { - throw new Error(`agent session not found: ${agentSessionId}`); - } - throw error; - } -} - -async function writeLocalAgentState(root, state) { - await mkdir(state.sessionDir, { recursive: true }); - await mkdir(localAgentEvidenceDir(state.sessionDir), { recursive: true }); - await writeFile(localAgentStatePath(state.sessionDir), `${JSON.stringify(state, null, 2)}\n`, "utf8"); - const traceContent = state.traceEvents.map((event) => JSON.stringify(event)).join("\n"); - await writeFile(localAgentTracePath(state.sessionDir), `${traceContent}${traceContent ? "\n" : ""}`, "utf8"); -} - -function localAgentSessionDir(root, agentSessionId) { - return path.join(root, "sessions", agentSessionId); -} - -function localAgentStatePath(sessionDir) { - return path.join(sessionDir, "session.json"); -} - -function localAgentTracePath(sessionDir) { - return path.join(sessionDir, "trace.ndjson"); -} - -function localAgentEvidenceDir(sessionDir) { - return path.join(sessionDir, "evidence"); -} - -async function pathExists(targetPath) { - try { - await access(targetPath, fsConstants.F_OK); - return true; - } catch (error) { - if (error.code === "ENOENT") return false; - throw error; - } -} - -function requireId(value, fieldName) { - if (!explicitValue(value)) { - throw new Error(`${fieldName} is required`); - } -} - -function explicitValue(value) { - if (typeof value !== "string") return null; - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : null; -} - -function sha256Hex(value) { - return crypto.createHash("sha256").update(value).digest("hex"); -} diff --git a/internal/cloud/m3-io-control.test.ts b/internal/cloud/m3-io-control.test.ts index 28817bf2..cf8b6d12 100644 --- a/internal/cloud/m3-io-control.test.ts +++ b/internal/cloud/m3-io-control.test.ts @@ -41,6 +41,7 @@ test("M3 IO live descriptor enables controls only after read-only backend readin const fixture = createM3ControlFixture(); const contract = await describeM3IoControlLive({ env: { + HWLAB_M3_IO_CONTROL_ENABLED: "true", HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1", HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2", HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel" @@ -81,6 +82,7 @@ test("M3 IO live descriptor blocks controls with Chinese reason when readiness i }); const contract = await describeM3IoControlLive({ env: { + HWLAB_M3_IO_CONTROL_ENABLED: "true", HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1", HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2", HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel" @@ -103,6 +105,7 @@ test("M3 IO live descriptor blocks controls with Chinese reason when readiness i test("M3 IO live descriptor classifies gateway unavailable and patch-panel unavailable blockers", async () => { const gatewayBlocked = await describeM3IoControlLive({ env: { + HWLAB_M3_IO_CONTROL_ENABLED: "true", HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1", HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2", HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel" @@ -130,6 +133,7 @@ test("M3 IO live descriptor classifies gateway unavailable and patch-panel unava const fixture = createM3ControlFixture(); const patchPanelBlocked = await describeM3IoControlLive({ env: { + HWLAB_M3_IO_CONTROL_ENABLED: "true", HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1", HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2", HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel" @@ -175,6 +179,7 @@ test("M3 status live descriptor aggregates gateways, boxes, patch-panel, IO valu const status = await describeM3StatusLive({ runtimeStore: createBlockedDurableRuntimeStore({ blocker: "runtime_durable_adapter_auth_blocked" }), env: { + HWLAB_M3_IO_CONTROL_ENABLED: "true", HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1", HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2", HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel" @@ -250,6 +255,7 @@ test("M3 status clears nested runtime blocker when trusted durable evidence is g const status = await describeM3StatusLive({ runtimeStore: createGreenDurableRuntimeStore({ blocker: M3_IO_BLOCKER_CODES.runtimeDurableBlocked }), env: { + HWLAB_M3_IO_CONTROL_ENABLED: "true", HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1", HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2", HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel" @@ -306,6 +312,7 @@ test("M3 status live descriptor keeps SOURCE fallback unverified when patch-pane const status = await describeM3StatusLive({ runtimeStore: createBlockedDurableRuntimeStore(), env: { + HWLAB_M3_IO_CONTROL_ENABLED: "true", HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1", HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2", HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel" @@ -346,6 +353,7 @@ test("M3 DO write dispatches through gateway-simu, ticks patch-panel, reads DI, runtimeStore, now: () => fixedNow, env: { + HWLAB_M3_IO_CONTROL_ENABLED: "true", HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1", HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2", HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel" @@ -450,6 +458,7 @@ test("M3 DO write can prove control reachability while durable trusted persisten runtimeStore: createBlockedDurableRuntimeStore(), now: () => fixedNow, env: { + HWLAB_M3_IO_CONTROL_ENABLED: "true", HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1", HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2", HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel" @@ -503,6 +512,7 @@ test("M3 DI read returns structured value through gateway-simu without patch-pan runtimeStore: createCloudRuntimeStore({ now: () => fixedNow }), now: () => fixedNow, env: { + HWLAB_M3_IO_CONTROL_ENABLED: "true", HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1", HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2", HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel" @@ -547,6 +557,7 @@ test("M3 IO control blocks with Chinese reason when gateway session is unavailab runtimeStore: createCloudRuntimeStore({ now: () => fixedNow }), now: () => fixedNow, env: { + HWLAB_M3_IO_CONTROL_ENABLED: "true", HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1", HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2", HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel" @@ -608,6 +619,7 @@ test("M3 IO control blocks when patch-panel wiring does not deliver DO1 to DI1", runtimeStore: createCloudRuntimeStore({ now: () => fixedNow }), now: () => fixedNow, env: { + HWLAB_M3_IO_CONTROL_ENABLED: "true", HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1", HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2", HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel" @@ -649,6 +661,7 @@ test("M3 IO control blocks when patch-panel reports missing target box", async ( runtimeStore: createCloudRuntimeStore({ now: () => fixedNow }), now: () => fixedNow, env: { + HWLAB_M3_IO_CONTROL_ENABLED: "true", HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1", HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2", HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel" @@ -674,6 +687,7 @@ test("M3 IO JSON-RPC methods route do.write and di.read through the cloud-api ha runtimeStore, now: () => fixedNow, env: { + HWLAB_M3_IO_CONTROL_ENABLED: "true", HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1", HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2", HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel" @@ -698,7 +712,7 @@ test("M3 IO JSON-RPC methods route do.write and di.read through the cloud-api ha meta: { traceId: "trc_m3_rpc_write_true", actorId: "usr_m3_rpc", - serviceId: "hwlab-agent-worker", + serviceId: "hwlab-cloud-api", environment: "dev" } }, @@ -733,7 +747,7 @@ test("M3 IO JSON-RPC methods route do.write and di.read through the cloud-api ha meta: { traceId: "trc_m3_rpc_read_di", actorId: "usr_m3_rpc", - serviceId: "hwlab-agent-worker", + serviceId: "hwlab-cloud-api", environment: "dev" } }, @@ -769,6 +783,7 @@ test("M3 IO fixture success cannot be promoted to DEV-LIVE without durable evide runtimeStore: createCloudRuntimeStore({ now: () => fixedNow }), now: () => fixedNow, env: { + HWLAB_M3_IO_CONTROL_ENABLED: "true", HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1", HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2", HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel" diff --git a/internal/cloud/server-agent-chat.test.ts b/internal/cloud/server-agent-chat.test.ts index d1c2052a..11c8076d 100644 --- a/internal/cloud/server-agent-chat.test.ts +++ b/internal/cloud/server-agent-chat.test.ts @@ -2082,7 +2082,7 @@ test("cloud api /v1/agent/chat does not call OpenAI provider 502/503 fallback", const payload = await response.json(); assert.equal(payload.status, "failed"); assert.equal(payload.traceId, `trc_server-test-agent-chat-provider-${status}`); - assert.match(payload.error.code, /^(codex_cli_binary_missing|stdio_protocol_not_wired)$/u); + assert.match(payload.error.code, /^(codex_cli_binary_missing|codex_cli_native_dependency_missing|stdio_protocol_not_wired)$/u); assert.match(payload.error.layer, /^(runner|api)$/u); assert.equal(typeof payload.error.retryable, "boolean"); assert.equal(payload.error.blocker.traceId, `trc_server-test-agent-chat-provider-${status}`); diff --git a/internal/cloud/server-health.test.ts b/internal/cloud/server-health.test.ts index 6ad00d37..971c6f14 100644 --- a/internal/cloud/server-health.test.ts +++ b/internal/cloud/server-health.test.ts @@ -5,7 +5,7 @@ import os from "node:os"; import path from "node:path"; import { test } from "bun:test"; -import { createCloudApiServer } from "./server.ts"; +import { createCloudApiServer, ensureCodeAgentRuntimeBase } from "./server.ts"; import { createCodexStdioSessionManager } from "./codex-stdio-session.ts"; import { RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED, @@ -845,3 +845,27 @@ test("cloud api REST JSON-RPC bridge exposes unknown serviceId reason for Code A }); } }); + +test("AgentRun adapter does not initialize repo-owned Codex stdio runtime defaults", () => { + const env = { + HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01" + }; + ensureCodeAgentRuntimeBase(env); + assert.equal(env.HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED, undefined); + assert.equal(env.HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR, undefined); + assert.equal(env.HWLAB_CODE_AGENT_CODEX_WORKSPACE, undefined); + assert.equal(env.CODEX_HOME, undefined); + assert.equal(env.HWLAB_PREINSTALLED_SKILLS_DIR, "/app/skills"); + assert.equal(env.HWLAB_USER_SKILLS_DIR, "/data/user-skills"); +}); + +test("explicit Codex stdio mode keeps local runtime defaults available", () => { + const env = { + HWLAB_CODE_AGENT_PROVIDER: "codex-stdio" + }; + ensureCodeAgentRuntimeBase(env); + assert.equal(env.HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED, "1"); + assert.equal(env.HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR, "repo-owned"); + assert.equal(env.HWLAB_CODE_AGENT_CODEX_WORKSPACE, "/workspace/hwlab"); + assert.equal(env.CODEX_HOME, "/codex-home"); +}); diff --git a/internal/cloud/server-live-builds.test.ts b/internal/cloud/server-live-builds.test.ts index 8b0174bf..5b5eb3de 100644 --- a/internal/cloud/server-live-builds.test.ts +++ b/internal/cloud/server-live-builds.test.ts @@ -23,13 +23,13 @@ test("cloud api aggregates live HWLAB build times from health and controlled dep metadataSource: "runtime-env:HWLAB_BUILD_CREATED_AT" } }], - ["/hwlab-agent-mgr/health/live", { - serviceId: "hwlab-agent-mgr", + ["/hwlab-device-pod/health/live", { + serviceId: "hwlab-device-pod", status: "ok", revision: "48dfbf9", commit: { id: "48dfbf9", source: "test" }, image: { - reference: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:48dfbf9", + reference: "127.0.0.1:5000/hwlab/hwlab-device-pod:48dfbf9", tag: "48dfbf9", digest: "sha256:" + "2".repeat(64) } @@ -66,8 +66,8 @@ test("cloud api aggregates live HWLAB build times from health and controlled dep replicas: 1 }, { - serviceId: "hwlab-agent-mgr", - image: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:3df89fe", + serviceId: "hwlab-device-pod", + image: "127.0.0.1:5000/hwlab/hwlab-device-pod:3df89fe", namespace: "hwlab-dev", profile: "dev", healthPath: "/health/live", @@ -88,14 +88,6 @@ test("cloud api aggregates live HWLAB build times from health and controlled dep HWLAB_BUILD_SOURCE: "deploy-env-test" } }, - { - serviceId: "hwlab-agent-worker", - image: "127.0.0.1:5000/hwlab/hwlab-agent-worker:workcat", - namespace: "hwlab-dev", - profile: "dev", - healthPath: "/health/live", - replicas: 0 - }, { serviceId: "hwlab-edge-proxy", image: "127.0.0.1:5000/hwlab/hwlab-edge-proxy:edgecat", @@ -117,9 +109,9 @@ test("cloud api aggregates live HWLAB build times from health and controlled dep artifactCatalog: { services: [ { - serviceId: "hwlab-agent-mgr", + serviceId: "hwlab-device-pod", commitId: "3df89fe", - image: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:3df89fe", + image: "127.0.0.1:5000/hwlab/hwlab-device-pod:3df89fe", imageTag: "3df89fe", digest: "sha256:" + "3".repeat(64), namespace: "hwlab-dev", @@ -167,8 +159,7 @@ test("cloud api aggregates live HWLAB build times from health and controlled dep POD_NAMESPACE: "hwlab-dev", HWLAB_RUNTIME_CONTAINER: "hwlab-cloud-api", HWLAB_CLOUD_WEB_SERVICE_URL: "http://live.test/hwlab-cloud-web", - HWLAB_AGENT_MGR_URL: "http://live.test/hwlab-agent-mgr", - HWLAB_AGENT_WORKER_URL: "http://live.test/missing-agent-worker", + HWLAB_DEVICE_POD_URL: "http://live.test/hwlab-device-pod", HWLAB_GATEWAY_URL: "http://live.test/missing-gateway", HWLAB_GATEWAY_SIMU_URL: "http://live.test/missing-gateway-simu", HWLAB_BOX_SIMU_URL: "http://live.test/missing-box-simu", @@ -223,21 +214,17 @@ test("cloud api aggregates live HWLAB build times from health and controlled dep assert.equal(payload.counts.total, payload.services.length); assert.equal(payload.counts.external, 2); assert.equal(payload.counts.withBuildTime, 3); - const manager = payload.services.find((service) => service.serviceId === "hwlab-agent-mgr"); - assert.equal(manager.build.createdAt, null); - assert.equal(manager.image.tag, "48dfbf9"); - assert.equal(manager.revision, "48dfbf9"); - assert.equal(manager.build.metadataSource, "live-health:controlled-metadata-mismatch"); - assert.equal(manager.build.unavailableReason, "构建时间不可用:live tag 与 catalog metadata 不匹配"); - assert.match(manager.build.unavailableDetail, /live tag 48dfbf9/u); - assert.match(manager.build.unavailableDetail, /metadata tag 3df89fe/u); + const devicePod = payload.services.find((service) => service.serviceId === "hwlab-device-pod"); + assert.equal(devicePod.build.createdAt, null); + assert.equal(devicePod.image.tag, "48dfbf9"); + assert.equal(devicePod.revision, "48dfbf9"); + assert.equal(devicePod.build.metadataSource, "live-health:controlled-metadata-mismatch"); + assert.equal(devicePod.build.unavailableReason, "构建时间不可用:live tag 与 catalog metadata 不匹配"); + assert.match(devicePod.build.unavailableDetail, /live tag 48dfbf9/u); + assert.match(devicePod.build.unavailableDetail, /metadata tag 3df89fe/u); const extra = payload.services.find((service) => service.serviceId === "hwlab-extra-lab"); assert.equal(extra.build.createdAt, null); assert.match(extra.build.unavailableReason, /当前 live health 不可用/u); - const worker = payload.services.find((service) => service.serviceId === "hwlab-agent-worker"); - assert.equal(worker.build.createdAt, null); - assert.match(worker.build.unavailableReason, /构建时间不可用:当前 live health 不可用/u); - assert.match(worker.build.unavailableReason, /hwlab-agent-worker 当前 hwlab-dev desired replicas=0/u); assert.ok(payload.services.some((service) => service.kind === "external" && service.serviceId === "hwlab-edge-proxy" && service.healthUrl.endsWith("/health"))); assert.ok(payload.services.some((service) => service.kind === "external" && service.serviceId === "hwlab-frpc" && service.image.tag === "v0.68.1")); assert.ok(payload.services.some((service) => service.serviceId === "hwlab-device-pod")); @@ -256,8 +243,8 @@ test("cloud api ignores old repository reports and keeps missing buildCreatedAt await writeFile(path.join(deployDir, "deploy.json"), `${JSON.stringify({ services: [ { - serviceId: "hwlab-agent-mgr", - image: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:48dfbf9", + serviceId: "hwlab-device-pod", + image: "127.0.0.1:5000/hwlab/hwlab-device-pod:48dfbf9", namespace: "hwlab-dev", profile: "dev", healthPath: "/health/live", @@ -268,9 +255,9 @@ test("cloud api ignores old repository reports and keeps missing buildCreatedAt await writeFile(path.join(deployDir, "artifact-catalog.dev.json"), `${JSON.stringify({ services: [ { - serviceId: "hwlab-agent-mgr", + serviceId: "hwlab-device-pod", commitId: "48dfbf9abcdef", - image: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:48dfbf9", + image: "127.0.0.1:5000/hwlab/hwlab-device-pod:48dfbf9", imageTag: "48dfbf9", digest: "sha256:" + "6".repeat(64), namespace: "hwlab-dev", @@ -285,9 +272,9 @@ test("cloud api ignores old repository reports and keeps missing buildCreatedAt artifactPublish: { services: [ { - serviceId: "hwlab-agent-mgr", + serviceId: "hwlab-device-pod", sourceCommitId: "48dfbf9abcdef", - image: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:48dfbf9", + image: "127.0.0.1:5000/hwlab/hwlab-device-pod:48dfbf9", imageTag: "48dfbf9", digest: "sha256:" + "6".repeat(64), buildCreatedAt: "2099-01-01T00:00:00.000Z", @@ -301,22 +288,22 @@ test("cloud api ignores old repository reports and keeps missing buildCreatedAt const server = createCloudApiServer({ env: { PATH: "", - HWLAB_AGENT_MGR_URL: "http://live.test/hwlab-agent-mgr" + HWLAB_DEVICE_POD_URL: "http://live.test/hwlab-device-pod" }, repoRoot: tempRoot, fetchImpl: async (url) => { - if (new URL(url).pathname === "/hwlab-agent-mgr/health/live") { + if (new URL(url).pathname === "/hwlab-device-pod/health/live") { return { ok: true, status: 200, async text() { return JSON.stringify({ - serviceId: "hwlab-agent-mgr", + serviceId: "hwlab-device-pod", status: "ok", revision: "48dfbf9abcdef", commit: { id: "48dfbf9abcdef", source: "test" }, image: { - reference: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:48dfbf9", + reference: "127.0.0.1:5000/hwlab/hwlab-device-pod:48dfbf9", tag: "48dfbf9", digest: "sha256:" + "6".repeat(64) } @@ -351,7 +338,7 @@ test("cloud api ignores old repository reports and keeps missing buildCreatedAt await rm(path.join(tempRoot, "reports"), { recursive: true, force: true }); const withoutOldReport = await observe(); const summarize = (payload) => { - const manager = payload.services.find((service) => service.serviceId === "hwlab-agent-mgr"); + const manager = payload.services.find((service) => service.serviceId === "hwlab-device-pod"); return { latest: payload.latest, source: payload.source, @@ -365,7 +352,7 @@ test("cloud api ignores old repository reports and keeps missing buildCreatedAt }; assert.deepEqual(summarize(withOldReport), summarize(withoutOldReport)); - const manager = withOldReport.services.find((service) => service.serviceId === "hwlab-agent-mgr"); + const manager = withOldReport.services.find((service) => service.serviceId === "hwlab-device-pod"); assert.equal(withOldReport.latest, null); assert.equal(manager.build.createdAt, null); assert.equal(Object.hasOwn(withOldReport.source, "artifactReportSource"), false); diff --git a/internal/cloud/server-rest-payloads.ts b/internal/cloud/server-rest-payloads.ts index 3aaeadee..2ea7b65a 100644 --- a/internal/cloud/server-rest-payloads.ts +++ b/internal/cloud/server-rest-payloads.ts @@ -13,8 +13,6 @@ const LIVE_BUILD_DEPLOY_MANIFEST_PATH = "deploy/deploy.json"; const LIVE_BUILD_SERVICE_DEFAULTS = Object.freeze({ "hwlab-cloud-api": Object.freeze({ urlEnv: null, defaultUrl: "http://127.0.0.1:6667" }), "hwlab-cloud-web": Object.freeze({ urlEnv: "HWLAB_CLOUD_WEB_SERVICE_URL" }), - "hwlab-agent-mgr": Object.freeze({ urlEnv: "HWLAB_AGENT_MGR_URL" }), - "hwlab-agent-worker": Object.freeze({ urlEnv: "HWLAB_AGENT_WORKER_URL" }), "hwlab-device-pod": Object.freeze({ urlEnv: "HWLAB_DEVICE_POD_URL" }), "hwlab-gateway": Object.freeze({ urlEnv: "HWLAB_GATEWAY_URL" }), "hwlab-edge-proxy": Object.freeze({ urlEnv: "HWLAB_EDGE_PROXY_URL", healthPath: "/health" }), @@ -446,7 +444,7 @@ function liveBuildRuntimeSummary(service, payload = {}) { } function defaultRuntimeContainer(serviceId) { - if (["hwlab-cloud-api", "hwlab-cloud-web", "hwlab-device-pod", "hwlab-agent-mgr", "hwlab-agent-skills"].includes(serviceId)) return serviceId; + if (["hwlab-cloud-api", "hwlab-cloud-web", "hwlab-device-pod", "hwlab-agent-skills"].includes(serviceId)) return serviceId; return ""; } @@ -884,8 +882,6 @@ function serviceDefaultUrl(serviceId, namespace = null) { const port = { "hwlab-cloud-api": 6667, "hwlab-cloud-web": 8080, - "hwlab-agent-mgr": 7410, - "hwlab-agent-worker": 7411, "hwlab-device-pod": 7601, "hwlab-gateway": 7001, "hwlab-edge-proxy": 6667, diff --git a/internal/cloud/server.ts b/internal/cloud/server.ts index a46478c2..8330bcaf 100644 --- a/internal/cloud/server.ts +++ b/internal/cloud/server.ts @@ -104,6 +104,11 @@ export function createCloudApiServer(options = {}) { } export function ensureCodeAgentRuntimeBase(env = process.env) { + const codeAgentAdapter = String(env.HWLAB_CODE_AGENT_ADAPTER ?? env.HWLAB_CODE_AGENT_PROVIDER ?? "").trim().toLowerCase(); + const useCodexStdioRuntime = !codeAgentAdapter || codeAgentAdapter === "codex-stdio"; + const skillRuntime = configureSkillRuntime(env); + if (!useCodexStdioRuntime) return; + const workspace = env.HWLAB_CODE_AGENT_CODEX_WORKSPACE || env.HWLAB_CODE_AGENT_WORKSPACE || "/workspace/hwlab"; const codexHome = env.CODEX_HOME || env.HWLAB_CODE_AGENT_CODEX_HOME || "/codex-home"; const appCodexCommand = "/app/node_modules/.bin/codex"; @@ -120,7 +125,6 @@ export function ensureCodeAgentRuntimeBase(env = process.env) { env.HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR ||= "repo-owned"; env.CODEX_HOME ||= codexHome; env.HWLAB_CODE_AGENT_CODEX_COMMAND ||= codexCommand; - const skillRuntime = configureSkillRuntime(env); for (const target of [path.dirname(workspace), codexHome]) { try { diff --git a/internal/protocol/index.mjs b/internal/protocol/index.mjs index 2ec2babf..5dba6298 100644 --- a/internal/protocol/index.mjs +++ b/internal/protocol/index.mjs @@ -8,8 +8,6 @@ export const JSON_RPC_VERSION = "2.0"; export const SERVICE_IDS = Object.freeze([ "hwlab-cloud-api", "hwlab-cloud-web", - "hwlab-agent-mgr", - "hwlab-agent-worker", "hwlab-device-pod", "hwlab-gateway", "hwlab-edge-proxy", diff --git a/package.json b/package.json index a6fa370a..19fabae6 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ "check:cloud-web": "node scripts/check-runner.mjs --profile check --group cloud-web", "dev-runtime-base:build": "node scripts/dev-runtime-base-image.mjs", "cloud-api:smoke": "node scripts/run-bun.mjs scripts/cloud-api-runtime-smoke.mjs", - "m1:smoke": "node scripts/run-bun.mjs scripts/m1-contract-smoke.mjs", "l2:smoke": "node scripts/l2-runtime-contract-smoke.mjs", "dev:evidence": "node scripts/dev-evidence-blocker-aggregator.mjs --pretty", "dev:evidence:check": "node scripts/dev-evidence-blocker-aggregator.mjs --check", diff --git a/protocol/examples/evidence-chain/fixtures/agent-automation-loop.json b/protocol/examples/evidence-chain/fixtures/agent-automation-loop.json index e94e6977..16f6fa7a 100644 --- a/protocol/examples/evidence-chain/fixtures/agent-automation-loop.json +++ b/protocol/examples/evidence-chain/fixtures/agent-automation-loop.json @@ -37,66 +37,6 @@ "updatedAt": "2026-05-21T00:20:05.000Z" }, "auditEvents": [ - { - "auditId": "aud_agent-auto-session-started", - "traceId": "trc_agent-auto-loop-001", - "actorType": "user", - "actorId": "usr_alice", - "action": "agent.session.started", - "targetType": "agent_session", - "targetId": "ags_agent-auto-001", - "projectId": "prj_evidence-chain", - "operationId": "op_agent-auto-loop-001", - "serviceId": "hwlab-agent-mgr", - "environment": "dev", - "outcome": "accepted", - "metadata": { - "agentSessionId": "ags_agent-auto-001", - "goal": "run firmware smoke and persist evidence" - }, - "occurredAt": "2026-05-21T00:20:00.000Z" - }, - { - "auditId": "aud_agent-auto-worker-started", - "traceId": "trc_agent-auto-loop-001", - "actorType": "agent", - "actorId": "agt_agent-auto-001", - "action": "worker.session.started", - "targetType": "worker_session", - "targetId": "wks_agent-auto-001", - "projectId": "prj_evidence-chain", - "gatewaySessionId": "gws_evidence-gateway-001", - "workerSessionId": "wks_agent-auto-001", - "operationId": "op_agent-auto-loop-001", - "serviceId": "hwlab-agent-worker", - "environment": "dev", - "outcome": "accepted", - "metadata": { - "agentSessionId": "ags_agent-auto-001" - }, - "occurredAt": "2026-05-21T00:20:01.000Z" - }, - { - "auditId": "aud_agent-auto-operation-requested", - "traceId": "trc_agent-auto-loop-001", - "actorType": "worker", - "actorId": "wkr_agent-auto-001", - "action": "hardware.operation.requested", - "targetType": "hardware_operation", - "targetId": "op_agent-auto-loop-001", - "projectId": "prj_evidence-chain", - "gatewaySessionId": "gws_evidence-gateway-001", - "workerSessionId": "wks_agent-auto-001", - "operationId": "op_agent-auto-loop-001", - "serviceId": "hwlab-agent-worker", - "environment": "dev", - "outcome": "accepted", - "metadata": { - "agentSessionId": "ags_agent-auto-001", - "capabilityId": "cap_firmware-smoke" - }, - "occurredAt": "2026-05-21T00:20:02.000Z" - }, { "auditId": "aud_agent-auto-operation-succeeded", "traceId": "trc_agent-auto-loop-001", @@ -117,63 +57,9 @@ "exitCode": 0 }, "occurredAt": "2026-05-21T00:20:05.000Z" - }, - { - "auditId": "aud_agent-auto-evidence-recorded", - "traceId": "trc_agent-auto-loop-001", - "actorType": "worker", - "actorId": "wkr_agent-auto-001", - "action": "evidence.recorded", - "targetType": "evidence_record", - "targetId": "evd_agent-auto-report", - "projectId": "prj_evidence-chain", - "gatewaySessionId": "gws_evidence-gateway-001", - "workerSessionId": "wks_agent-auto-001", - "operationId": "op_agent-auto-loop-001", - "serviceId": "hwlab-agent-worker", - "environment": "dev", - "outcome": "succeeded", - "metadata": { - "agentSessionId": "ags_agent-auto-001", - "evidenceId": "evd_agent-auto-report", - "evidenceKind": "report" - }, - "occurredAt": "2026-05-21T00:20:06.000Z" } ], "traceEvents": [ - { - "traceEventId": "tev_agent-auto-goal", - "traceId": "trc_agent-auto-loop-001", - "projectId": "prj_evidence-chain", - "agentSessionId": "ags_agent-auto-001", - "workerSessionId": "wks_agent-auto-001", - "operationId": "op_agent-auto-loop-001", - "serviceId": "hwlab-agent-mgr", - "level": "info", - "message": "Agent session accepted automation goal", - "environment": "dev", - "metadata": { - "auditId": "aud_agent-auto-session-started" - }, - "occurredAt": "2026-05-21T00:20:00.000Z" - }, - { - "traceEventId": "tev_agent-auto-worker", - "traceId": "trc_agent-auto-loop-001", - "projectId": "prj_evidence-chain", - "agentSessionId": "ags_agent-auto-001", - "workerSessionId": "wks_agent-auto-001", - "operationId": "op_agent-auto-loop-001", - "serviceId": "hwlab-agent-worker", - "level": "info", - "message": "Worker requested firmware smoke operation", - "environment": "dev", - "metadata": { - "auditId": "aud_agent-auto-operation-requested" - }, - "occurredAt": "2026-05-21T00:20:02.000Z" - }, { "traceEventId": "tev_agent-auto-operation", "traceId": "trc_agent-auto-loop-001", @@ -189,47 +75,7 @@ "auditId": "aud_agent-auto-operation-succeeded" }, "occurredAt": "2026-05-21T00:20:05.000Z" - }, - { - "traceEventId": "tev_agent-auto-evidence", - "traceId": "trc_agent-auto-loop-001", - "projectId": "prj_evidence-chain", - "agentSessionId": "ags_agent-auto-001", - "workerSessionId": "wks_agent-auto-001", - "operationId": "op_agent-auto-loop-001", - "serviceId": "hwlab-agent-worker", - "level": "info", - "message": "Agent report evidence persisted", - "environment": "dev", - "metadata": { - "auditId": "aud_agent-auto-evidence-recorded", - "evidenceId": "evd_agent-auto-report" - }, - "occurredAt": "2026-05-21T00:20:06.000Z" } ], - "evidenceRecords": [ - { - "evidenceId": "evd_agent-auto-report", - "projectId": "prj_evidence-chain", - "operationId": "op_agent-auto-loop-001", - "agentSessionId": "ags_agent-auto-001", - "workerSessionId": "wks_agent-auto-001", - "kind": "report", - "uri": "protocol/examples/evidence-chain/artifacts/agent-automation-report.json", - "mimeType": "application/json", - "sha256": "2fc63687cd3170c9cf34cd650aaedfb84d3fe75683ae969ae286230ce3b425a7", - "sizeBytes": 340, - "serviceId": "hwlab-agent-worker", - "environment": "dev", - "metadata": { - "traceId": "trc_agent-auto-loop-001", - "producedByAuditId": "aud_agent-auto-evidence-recorded", - "agentSessionId": "ags_agent-auto-001", - "workerSessionId": "wks_agent-auto-001", - "producerServiceId": "hwlab-agent-worker" - }, - "createdAt": "2026-05-21T00:20:06.000Z" - } - ] + "evidenceRecords": [] } diff --git a/protocol/examples/m0-contract/audit-event.json b/protocol/examples/m0-contract/audit-event.json index 90d15a0d..74411230 100644 --- a/protocol/examples/m0-contract/audit-event.json +++ b/protocol/examples/m0-contract/audit-event.json @@ -10,7 +10,7 @@ "gatewaySessionId": "gws_01J00000000000000000000010", "workerSessionId": "wks_01J00000000000000000000010", "operationId": "op_01J00000000000000000000010", - "serviceId": "hwlab-agent-worker", + "serviceId": "hwlab-cloud-api", "environment": "dev", "outcome": "succeeded", "reason": "operation_completed", diff --git a/protocol/examples/m0-contract/capability-topology.json b/protocol/examples/m0-contract/capability-topology.json index 8823c14d..88bf063a 100644 --- a/protocol/examples/m0-contract/capability-topology.json +++ b/protocol/examples/m0-contract/capability-topology.json @@ -34,7 +34,9 @@ "state": "available", "environment": "dev", "metadata": { - "ports": ["out0"] + "ports": [ + "out0" + ] }, "createdAt": "2026-05-21T00:00:00.000Z", "updatedAt": "2026-05-21T00:00:00.000Z" @@ -49,7 +51,10 @@ "state": "reserved", "environment": "dev", "metadata": { - "ports": ["vin", "uart0"] + "ports": [ + "vin", + "uart0" + ] }, "createdAt": "2026-05-21T00:00:00.000Z", "updatedAt": "2026-05-21T00:00:00.000Z" @@ -140,28 +145,6 @@ "connectionCount": 1 } }, - "agentSession": { - "agentSessionId": "ags_01J00000000000000000000010", - "projectId": "prj_01J00000000000000000000010", - "serviceId": "hwlab-agent-mgr", - "status": "active", - "environment": "dev", - "requestedBy": "usr_01J00000000000000000000010", - "goal": "Cycle DUT power and persist evidence.", - "startedAt": "2026-05-21T00:00:02.000Z", - "updatedAt": "2026-05-21T00:00:02.000Z" - }, - "workerSession": { - "workerSessionId": "wks_01J00000000000000000000010", - "agentSessionId": "ags_01J00000000000000000000010", - "projectId": "prj_01J00000000000000000000010", - "serviceId": "hwlab-agent-worker", - "gatewaySessionId": "gws_01J00000000000000000000010", - "status": "active", - "environment": "dev", - "startedAt": "2026-05-21T00:00:03.000Z", - "updatedAt": "2026-05-21T00:00:03.000Z" - }, "hardwareOperation": { "operationId": "op_01J00000000000000000000010", "projectId": "prj_01J00000000000000000000010", @@ -185,40 +168,5 @@ "completedAt": "2026-05-21T00:00:05.000Z", "updatedAt": "2026-05-21T00:00:05.000Z" }, - "traceEvents": [ - { - "traceEventId": "tev_01J00000000000000000000010", - "traceId": "trc_01J00000000000000000000010", - "projectId": "prj_01J00000000000000000000010", - "agentSessionId": "ags_01J00000000000000000000010", - "workerSessionId": "wks_01J00000000000000000000010", - "operationId": "op_01J00000000000000000000010", - "serviceId": "hwlab-agent-worker", - "level": "info", - "message": "hardware operation completed", - "environment": "dev", - "metadata": { - "status": "succeeded" - }, - "occurredAt": "2026-05-21T00:00:05.000Z" - } - ], - "evidenceRecord": { - "evidenceId": "evd_01J00000000000000000000010", - "projectId": "prj_01J00000000000000000000010", - "operationId": "op_01J00000000000000000000010", - "agentSessionId": "ags_01J00000000000000000000010", - "workerSessionId": "wks_01J00000000000000000000010", - "kind": "report", - "uri": "protocol/examples/m0-contract/evidence/power-cycle-report.md", - "mimeType": "text/markdown", - "sha256": "0000000000000000000000000000000000000000000000000000000000000000", - "sizeBytes": 0, - "serviceId": "hwlab-agent-worker", - "environment": "dev", - "metadata": { - "source": "m0_contract_example" - }, - "createdAt": "2026-05-21T00:00:05.000Z" - } + "traceEvents": [] } diff --git a/protocol/examples/m0-contract/deploy-manifest.dev.json b/protocol/examples/m0-contract/deploy-manifest.dev.json index 5f6ecbdd..6e241b08 100644 --- a/protocol/examples/m0-contract/deploy-manifest.dev.json +++ b/protocol/examples/m0-contract/deploy-manifest.dev.json @@ -33,22 +33,6 @@ "profile": "dev", "replicas": 1 }, - { - "serviceId": "hwlab-agent-mgr", - "image": "ghcr.io/pikastech/hwlab-agent-mgr:6509a35", - "namespace": "hwlab-dev", - "healthPath": "/healthz", - "profile": "dev", - "replicas": 1 - }, - { - "serviceId": "hwlab-agent-worker", - "image": "ghcr.io/pikastech/hwlab-agent-worker:6509a35", - "namespace": "hwlab-dev", - "healthPath": "/healthz", - "profile": "dev", - "replicas": 1 - }, { "serviceId": "hwlab-device-pod", "image": "ghcr.io/pikastech/hwlab-device-pod:6509a35", diff --git a/protocol/examples/m0-contract/json-rpc-response.json b/protocol/examples/m0-contract/json-rpc-response.json index 1592246a..8a0577ff 100644 --- a/protocol/examples/m0-contract/json-rpc-response.json +++ b/protocol/examples/m0-contract/json-rpc-response.json @@ -8,7 +8,7 @@ }, "meta": { "traceId": "trc_01J00000000000000000000010", - "serviceId": "hwlab-agent-mgr", + "serviceId": "hwlab-cloud-api", "environment": "dev" } } diff --git a/protocol/examples/m0-contract/service-ids.json b/protocol/examples/m0-contract/service-ids.json index 84224ef7..96fa9378 100644 --- a/protocol/examples/m0-contract/service-ids.json +++ b/protocol/examples/m0-contract/service-ids.json @@ -4,8 +4,6 @@ "serviceIds": [ "hwlab-cloud-api", "hwlab-cloud-web", - "hwlab-agent-mgr", - "hwlab-agent-worker", "hwlab-device-pod", "hwlab-gateway", "hwlab-gateway-simu", diff --git a/protocol/examples/runtime-boundary/guard.dev.json b/protocol/examples/runtime-boundary/guard.dev.json index f7c6de30..cc9b084c 100644 --- a/protocol/examples/runtime-boundary/guard.dev.json +++ b/protocol/examples/runtime-boundary/guard.dev.json @@ -9,7 +9,11 @@ "runtimeSubstitution": "forbidden", "prodDeployment": "forbidden", "realDevDeployment": "out_of_scope", - "allowedUniDeskRoles": ["ci", "cd", "code_queue_scheduling"], + "allowedUniDeskRoles": [ + "ci", + "cd", + "code_queue_scheduling" + ], "forbiddenRuntimeSubstitutes": [ "unidesk-backend", "provider-gateway", @@ -24,8 +28,6 @@ "requiredServiceIds": [ "hwlab-cloud-api", "hwlab-cloud-web", - "hwlab-agent-mgr", - "hwlab-agent-worker", "hwlab-gateway", "hwlab-gateway-simu", "hwlab-box-simu", @@ -50,8 +52,6 @@ "hwlab-tunnel-client", "hwlab-cloud-api", "hwlab-cloud-web", - "hwlab-agent-mgr", - "hwlab-agent-worker", "hwlab-gateway-simu", "hwlab-box-simu", "hwlab-patch-panel" @@ -67,8 +67,6 @@ "requiredWorkloadServiceIds": [ "hwlab-cloud-api", "hwlab-cloud-web", - "hwlab-agent-mgr", - "hwlab-agent-worker", "hwlab-gateway", "hwlab-gateway-simu", "hwlab-box-simu", @@ -85,7 +83,6 @@ "hwlab-gateway", "hwlab-cli", "hwlab-agent-skills", - "hwlab-agent-mgr", "hwlab-gateway-simu", "hwlab-box-simu", "hwlab-patch-panel", diff --git a/protocol/json-rpc.md b/protocol/json-rpc.md index 1861d897..ad0e3d78 100644 --- a/protocol/json-rpc.md +++ b/protocol/json-rpc.md @@ -32,7 +32,7 @@ HWLAB command-style APIs use JSON-RPC 2.0 envelopes. }, "meta": { "traceId": "trc_01J00000000000000000000000", - "serviceId": "hwlab-agent-mgr", + "serviceId": "hwlab-cloud-api", "environment": "dev" } } diff --git a/protocol/schemas/agent-session.schema.json b/protocol/schemas/agent-session.schema.json index 852040f1..e821bcd0 100644 --- a/protocol/schemas/agent-session.schema.json +++ b/protocol/schemas/agent-session.schema.json @@ -20,7 +20,7 @@ "$ref": "common.json#/$defs/id" }, "serviceId": { - "const": "hwlab-agent-mgr" + "const": "hwlab-cloud-api" }, "status": { "type": "string", diff --git a/protocol/schemas/common.json b/protocol/schemas/common.json index 68fac14e..a8d417bf 100644 --- a/protocol/schemas/common.json +++ b/protocol/schemas/common.json @@ -23,8 +23,6 @@ "enum": [ "hwlab-cloud-api", "hwlab-cloud-web", - "hwlab-agent-mgr", - "hwlab-agent-worker", "hwlab-device-pod", "hwlab-gateway", "hwlab-edge-proxy", diff --git a/protocol/schemas/worker-session.schema.json b/protocol/schemas/worker-session.schema.json index 967f9e67..efbc4c1d 100644 --- a/protocol/schemas/worker-session.schema.json +++ b/protocol/schemas/worker-session.schema.json @@ -24,7 +24,7 @@ "$ref": "common.json#/$defs/id" }, "serviceId": { - "const": "hwlab-agent-worker" + "const": "hwlab-cloud-api" }, "gatewaySessionId": { "$ref": "common.json#/$defs/id" diff --git a/protocol/topology.md b/protocol/topology.md index 26332a8c..5135a024 100644 --- a/protocol/topology.md +++ b/protocol/topology.md @@ -1,15 +1,14 @@ # Topology Constraints -The MVP topology separates cloud, gateway, worker, simulated box, patch panel, -router, tunnel, and edge proxy responsibilities. +The MVP topology separates cloud, gateway, AgentRun-backed execution, device pod, +hardware transport, and edge proxy responsibilities. ## Constraints - `hwlab-cloud-api` is the cloud API boundary for DEV. - `hwlab-cloud-web` consumes cloud APIs and must not control hardware directly. -- `hwlab-agent-mgr` schedules agent work and creates worker sessions. -- `hwlab-agent-worker` executes scoped work through declared gateway and box - capabilities. +- `hwlab-cloud-api` owns Code Agent session authorization and dispatches execution + to AgentRun v0.1. - `hwlab-gateway` is the hardware control boundary. - `hwlab-gateway-simu` and `hwlab-box-simu` may simulate gateway and box behavior for MVP validation. diff --git a/scripts/artifact-publish.mjs b/scripts/artifact-publish.mjs index d98c3e1f..7ae72596 100644 --- a/scripts/artifact-publish.mjs +++ b/scripts/artifact-publish.mjs @@ -61,8 +61,6 @@ const ciArtifactIdentityEnvNames = [ const servicePorts = new Map([ ["hwlab-cloud-api", 6667], ["hwlab-cloud-web", 8080], - ["hwlab-agent-mgr", 7410], - ["hwlab-agent-worker", 7411], ["hwlab-device-pod", 7601], ["hwlab-gateway", 7001], ["hwlab-edge-proxy", 6667], @@ -72,8 +70,6 @@ const servicePorts = new Map([ const v02RuntimeServiceIds = Object.freeze([ "hwlab-cloud-api", "hwlab-cloud-web", - "hwlab-agent-mgr", - "hwlab-agent-worker", "hwlab-device-pod", "hwlab-gateway", "hwlab-edge-proxy", @@ -784,15 +780,6 @@ function dockerfile(baseImage, port, labels = []) { "WORKDIR /app", ...labels.map(([name, value]) => `LABEL ${name}=${dockerfileQuote(value)}`), "ARG HWLAB_ARTIFACT_KIND", - "ENV CODEX_HOME=/codex-home", - "ENV HWLAB_CODE_AGENT_CODEX_HOME=/codex-home", - "ENV HWLAB_CODE_AGENT_WORKSPACE=/workspace/hwlab", - "ENV HWLAB_CODE_AGENT_CODEX_WORKSPACE=/workspace/hwlab", - "ENV HWLAB_CODE_AGENT_CODEX_SANDBOX=danger-full-access", - "ENV HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED=1", - "ENV HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR=repo-owned", - "ENV HWLAB_CODE_AGENT_CODEX_COMMAND=/app/node_modules/.bin/codex", - "ENV HWLAB_CODE_AGENT_PROVIDER=codex-stdio", "ENV HWLAB_CODE_AGENT_SKILLS_DIRS=/app/skills", "COPY package.json ./package.json", "COPY package-lock.json* ./", @@ -1930,9 +1917,7 @@ function createReport({ args, repo, commitId, shortCommit, mode, services, artif ], localSmoke: { status: "not_run", - commands: [ - "node scripts/m1-contract-smoke.mjs" - ], + commands: [], evidence: [ "This report records artifact build/publish state only; local smoke remains a separate gate." ], diff --git a/scripts/dev-m4-agent-loop-smoke.mjs b/scripts/dev-m4-agent-loop-smoke.mjs deleted file mode 100644 index b555d07d..00000000 --- a/scripts/dev-m4-agent-loop-smoke.mjs +++ /dev/null @@ -1,620 +0,0 @@ -#!/usr/bin/env node -import assert from "node:assert/strict"; -import { execFileSync } from "node:child_process"; -import { mkdtemp, readFile, writeFile, mkdir, rm } from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -import { - AGENT_MGR_SERVICE_ID, - AGENT_SKILLS_SERVICE_ID, - AGENT_WORKER_SERVICE_ID -} from "../internal/agent/index.mjs"; -import { - createLocalAgentSession, - getLocalAgentEvidence, - getLocalAgentStatus, - getLocalAgentTrace, - runLocalWorkerDryRun -} from "../internal/agent/runtime.mjs"; -import { activeReportLifecycle } from "../internal/dev-report-lifecycle.mjs"; -import { DEV_ENDPOINT, DEV_FRONTEND_ENDPOINT, ENVIRONMENT_DEV } from "../internal/protocol/index.mjs"; -import { ensureNotRepoReportsPath, tempReportPath } from "./src/report-paths.mjs"; -import { - collectD601K3sNativeEvidence, - collectPublicEntrypoints, - classifyCloudApiLiveReadiness, - d601K3sReadonlyCommand, - d601Kubeconfig, - devNamespace, - requestJson, - runtimeDurabilityFromHealth, - workerServerDryRunCommand -} from "./src/dev-m4-agent-loop-smoke-lib.mjs"; - -const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const fixedNow = () => "2026-05-22T00:00:00.000Z"; -const requiredDocs = ["docs/reference/MVP-e2e-acceptance.md", "docs/reference/code-agent-chat-readiness.md"]; -const reportPath = tempReportPath("dev-m4-agent-loop.json"); -const preflightCommand = "node scripts/dev-m4-agent-loop-smoke.mjs --live --confirm-dev --confirmed-non-production"; -const dryRunCommand = "node scripts/dev-m4-agent-loop-smoke.mjs --dry-run"; - -function parseArgs(argv) { - const flags = new Set(); - for (const arg of argv) { - if (arg.startsWith("--")) { - flags.add(arg); - } - } - return { - live: flags.has("--live") || flags.has("--allow-live"), - dryRun: flags.has("--dry-run"), - confirmDev: flags.has("--confirm-dev"), - confirmedNonProduction: flags.has("--confirmed-non-production") - }; -} - -async function readJSON(relativePath) { - const raw = await readFile(path.join(repoRoot, relativePath), "utf8"); - return JSON.parse(raw); -} - -async function readText(relativePath) { - return readFile(path.join(repoRoot, relativePath), "utf8"); -} - -async function writeJSON(relativePath, value) { - const absolutePath = ensureNotRepoReportsPath(repoRoot, relativePath, "--report"); - await mkdir(path.dirname(absolutePath), { recursive: true }); - await writeFile(absolutePath, `${JSON.stringify(value, null, 2)}\n`, "utf8"); -} - -function currentOriginMainCommitId() { - return execFileSync("git", ["rev-parse", "--short=7", "origin/main"], { - cwd: repoRoot, - encoding: "utf8" - }).trim(); -} - -function statusForResult(result) { - return result.blocked || (result.additionalBlockers?.length ?? 0) > 0 ? "blocked" : "pass"; -} - -function dbLiveReadyForResult(result) { - return result.dbLiveReady === true || (!result.blocked && result.dbLiveReady !== false); -} - -function runtimeDurableReadyForResult(result) { - return result.runtimeDurableReady === true || (!result.blocked && result.runtimeDurableReady !== false); -} - -function blockerForResult(result) { - const blockers = []; - if (result.blocked && result.blockerType) { - blockers.push({ - type: result.blockerType, - scope: result.blockerScope ?? "devPreconditions", - status: "open", - summary: result.blockerSummary, - classification: result.blockedClassification ?? "other", - ...(result.sourceIssue ? { sourceIssue: result.sourceIssue } : {}) - }); - } - for (const blocker of result.additionalBlockers ?? []) { - blockers.push({ status: "open", ...blocker }); - } - const seen = new Set(); - return blockers.filter((blocker) => { - const key = `${blocker.type}:${blocker.scope}`; - if (seen.has(key)) return false; - seen.add(key); - return true; - }); -} - -function classificationForResult(result) { - return result.blockedClassification ?? result.additionalBlockers?.[0]?.classification ?? "none"; -} - -function buildDryRunEvidence({ status = "not_run", details = [] } = {}) { - return { - status, - commands: [dryRunCommand], - evidence: details.length - ? details - : ["No dry-run output is attached to this live preflight report."], - summary: status === "pass" - ? "Local dry-run covered agent manager, worker, workspace isolation, skills commit, trace, evidence, and cleanup without live DEV mutation." - : "The live preflight report did not attach a dry-run agent run." - }; -} - -function buildComponentEvidence(fixture, dryRun, result) { - const dbLiveReady = dbLiveReadyForResult(result); - const runtimeDurableReady = runtimeDurableReadyForResult(result); - const dbLiveStatus = result.blockedClassification === "DB live" - ? "blocked" - : dbLiveReady - ? "pass" - : "not_run"; - const m3DependencyStatus = result.blockedClassification === "hardware loop" - ? "blocked" - : result.blocked - ? "not_run" - : "pass"; - - return { - agentManagerWorker: { - level: dryRun.status === "pass" ? "DRY-RUN" : "LOCAL", - status: dryRun.status === "pass" ? "pass" : "not_run", - services: [AGENT_MGR_SERVICE_ID, AGENT_WORKER_SERVICE_ID], - summary: dryRun.status === "pass" - ? "Manager created a scoped local session and worker completed the bounded dry-run task." - : "Manager/worker live scheduling was not attempted before preflight blockers were classified." - }, - workspaceIsolation: { - level: dryRun.status === "pass" ? "DRY-RUN" : "LOCAL", - status: "pass", - evidence: [ - `primary=${fixture.projectId}/${fixture.agentSessionId}`, - `isolation=${fixture.workspaceIsolation.projectId}/${fixture.workspaceIsolation.agentSessionId}` - ], - summary: "Workspace identity is per agent session and fixture isolation remains source/local evidence only." - }, - skillsCommit: { - level: dryRun.status === "pass" ? "DRY-RUN" : "LOCAL", - status: "pass", - serviceId: AGENT_SKILLS_SERVICE_ID, - commitId: fixture.skillCommitId, - version: fixture.skillVersion, - summary: "Agent skills are accepted only with explicit commitId and version wiring." - }, - traceEvidenceCleanup: { - level: dryRun.status === "pass" ? "DRY-RUN" : "LOCAL", - status: dryRun.status === "pass" ? "pass" : "not_run", - summary: dryRun.status === "pass" - ? "Dry-run produced trace/evidence records and removed the temporary worker workspace." - : "Trace/evidence/cleanup live closure was not attempted before preflight blockers were classified." - }, - dbLive: { - level: dbLiveStatus === "pass" ? "DEV-LIVE" : "BLOCKED", - status: dbLiveStatus, - summary: dbLiveStatus === "blocked" - ? "Live scheduling is stopped at cloud-api DB readiness; DB health is classified separately from local dry-run evidence." - : dbLiveStatus === "pass" - ? "Current read-only /health/live evidence reports DB ready=true, connected=true, and liveDbEvidence=true; DB live is not the active M4 blocker." - : "Live cloud-api DB readiness did not block before the current M4 classification point." - }, - runtimeDurableAdapter: { - level: runtimeDurableReady ? "DEV-LIVE" : "BLOCKED", - status: result.blockerScope === "runtime-durable-adapter" - ? "blocked" - : runtimeDurableReady - ? "pass" - : "not_run", - summary: result.blockerScope === "runtime-durable-adapter" - ? "Live scheduling is stopped at runtime durable adapter readiness; DB live evidence alone is not runtime durability evidence." - : runtimeDurableReady - ? "Current read-only /health/live evidence reports runtime.adapter=postgres, durable=true, ready=true, and liveRuntimeEvidence=true." - : "Runtime durable adapter readiness did not produce the active M4 blocker." - }, - m3HardwareLoopDependency: { - level: "BLOCKED", - status: m3DependencyStatus, - requiredPath: "DO1 -> hwlab-patch-panel -> DI1", - summary: m3DependencyStatus === "blocked" - ? "M4 reached the hardware dispatch dependency, but the M3 patch-panel trusted loop is not accepted as live." - : "M4 can only support M3 through cloud-api/hardware API and the patch-panel trusted path; no direct box/gateway path is used." - } - }; -} - -function buildDependencyState(result) { - const dbLiveReady = dbLiveReadyForResult(result); - const runtimeDurableReady = runtimeDurableReadyForResult(result); - return { - dbLive: { - status: result.blockedClassification === "DB live" ? "blocked" : dbLiveReady ? "pass" : "not_run", - summary: result.blockedClassification === "DB live" - ? result.blockerSummary - : dbLiveReady - ? "cloud-api /health/live reports DB ready=true, connected=true, and liveDbEvidence=true." - : "DB live readiness did not produce the active M4 blocker." - }, - runtimeDurableAdapter: { - status: result.blockerScope === "runtime-durable-adapter" ? "blocked" : runtimeDurableReady ? "pass" : "not_run", - requiredEvidence: "Postgres runtime adapter schema, migration ledger, and read-query readiness with liveRuntimeEvidence=true.", - summary: result.blockerScope === "runtime-durable-adapter" - ? result.blockerSummary - : runtimeDurableReady - ? "cloud-api /health/live reports runtime.adapter=postgres, durable=true, ready=true, and liveRuntimeEvidence=true." - : "Runtime durable adapter readiness did not produce the active M4 blocker." - }, - m3HardwareLoop: { - status: result.blockedClassification === "hardware loop" ? "blocked" : result.blocked ? "not_run" : "pass", - requiredEvidence: "Traceable DO1 -> hwlab-patch-panel -> DI1 operation with auditId and evidenceId.", - summary: result.blockedClassification === "hardware loop" - ? result.blockerSummary - : "M3 hardware loop was not reached while an earlier M4 precondition remained blocked." - }, - hardwareDispatchBoundary: { - status: "pass", - allowedPath: "cloud-api /rpc hardware.operation.request -> hwlab-patch-panel trusted route", - forbiddenPaths: [ - "direct hwlab-box-simu access", - "direct hwlab-gateway-simu access", - "agent-side fixture promotion to DEV-LIVE" - ] - } - }; -} - -function buildReport( - fixture, - liveEvidence, - result, - { dryRun = buildDryRunEvidence(), d601K3s = null, publicEntrypoints = null } = {} -) { - const sourceContract = { - status: "pass", - documents: requiredDocs, - summary: "The DEV M4 report is anchored to the frozen acceptance matrix and the M4 loop doc." - }; - - const validationCommands = [ - "node --check scripts/validate-dev-gate-report.mjs", - "node scripts/validate-dev-gate-report.mjs", - "node --check scripts/dev-m4-agent-loop-smoke.mjs", - "node --check scripts/src/dev-m4-agent-loop-smoke-lib.mjs", - dryRunCommand, - preflightCommand, - d601K3sReadonlyCommand, - workerServerDryRunCommand - ]; - const blockers = blockerForResult(result); - - return { - $schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json", - $id: "https://hwlab.pikastech.local/dev-m4-agent-loop.json", - reportVersion: "v1", - issue: "pikasTech/HWLAB#37", - taskId: "dev-m4-agent-loop", - commitId: currentOriginMainCommitId(), - acceptanceLevel: "dev_m4_agent_loop", - devOnly: true, - prodDisabled: true, - reportLifecycle: activeReportLifecycle("Current DEV M4 agent-loop preflight report; it does not substitute for M3 DEV-LIVE acceptance."), - sourceContract, - validationCommands, - localSmoke: { - status: "pass", - commands: ["node scripts/m4-agent-loop-smoke.mjs"], - evidence: [ - "Local runtime skeleton smoke confirms create/start/trace/finish/cleanup coverage.", - "Workspace isolation and explicit skills commit wiring are covered by fixture assertions." - ], - summary: "Local contract smoke passes on the repo fixture." - }, - dryRun, - componentEvidence: buildComponentEvidence(fixture, dryRun, result), - dependencyState: buildDependencyState(result), - d601K3sNative: d601K3s, - publicEntrypoints, - safetyEvidence: { - devOnly: true, - namespace: devNamespace, - kubeconfig: d601Kubeconfig, - prodTouched: false, - prodNamespaceTouched: false, - secretMaterialRead: false, - secretResourcesRead: false, - servicesRestarted: false, - longRunningAgentStarted: false, - workerJobPersisted: false, - notes: [ - "kubectl commands were scoped to hwlab-dev and /etc/rancher/k3s/k3s.yaml.", - "Secret RBAC was checked with kubectl auth can-i only; no Secret resource or secret value was read.", - "Worker createability used server-side dry-run with suspend=true and did not persist a Job." - ] - }, - devPreconditions: { - status: statusForResult(result), - classification: classificationForResult(result), - requirements: [ - `DEV route remains frozen at ${DEV_ENDPOINT}`, - `DEV browser route remains frozen at ${DEV_FRONTEND_ENDPOINT}`, - `D601 native k3s access uses KUBECONFIG=${d601Kubeconfig} and namespace ${devNamespace}`, - "No secret reads, real deployment, or long-running agent task is attempted", - "DB live readiness and runtime durable adapter readiness are separate gates; DB live alone cannot promote M4", - "Agent manager, worker, and skills are reachable only through the cloud API boundary", - "M4 hardware assistance must go through cloud-api/hardware API and patch-panel; direct box/gateway access is forbidden" - ], - summary: result.summary - }, - blockers, - livePreflight: { - status: blockers.length > 0 ? "blocked" : "pass", - classification: classificationForResult(result), - commands: [preflightCommand], - evidence: liveEvidence.length ? liveEvidence : ["No live DEV observation was recorded."], - summary: result.summary - }, - notes: "DEV M4 agent loop report. SOURCE/LOCAL/DRY-RUN evidence is not DEV-LIVE, and no secret material is read." - }; -} - -async function loadFixture() { - const fixture = await readJSON("fixtures/mvp/m4-agent-loop/runtime.json"); - const localEvidence = await readText(fixture.evidencePath); - assert.ok(localEvidence.includes("Boundary: local only")); - return fixture; -} - -async function runDryRun(fixture) { - const stateDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-dev-m4-agent-loop-")); - try { - const created = await createLocalAgentSession({ - stateDir, - agentSessionId: fixture.agentSessionId, - projectId: fixture.projectId, - goal: fixture.goal, - prompt: fixture.prompt, - skillCommitId: fixture.skillCommitId, - skillVersion: fixture.skillVersion, - now: fixedNow - }); - assert.equal(created.status, "ok"); - assert.equal(created.environment, ENVIRONMENT_DEV); - - const worker = await runLocalWorkerDryRun({ - stateDir, - agentSessionId: fixture.agentSessionId, - skillCommitId: fixture.skillCommitId, - skillVersion: fixture.skillVersion, - now: fixedNow - }); - assert.equal(worker.dryRun, true); - assert.equal(worker.health.status, "ok"); - assert.equal(worker.agentSession.status, "cleanup"); - assert.equal(worker.workerSession.status, "cleanup"); - assert.equal(worker.workspace.cleaned, true); - assert.equal(worker.workspace.existsAfterCleanup, false); - - const status = await getLocalAgentStatus({ - stateDir, - agentSessionId: fixture.agentSessionId - }); - const trace = await getLocalAgentTrace({ - stateDir, - agentSessionId: fixture.agentSessionId - }); - const evidence = await getLocalAgentEvidence({ - stateDir, - agentSessionId: fixture.agentSessionId - }); - - assert.equal(status.evidenceCount, 1); - assert.equal(trace.traceEvents.length, 5); - assert.equal(evidence.evidenceRecords.length, 1); - - return buildDryRunEvidence({ - status: "pass", - details: [ - `${AGENT_MGR_SERVICE_ID}:session=${fixture.agentSessionId}:created=${created.task.status}:final=${worker.agentSession.status}`, - `${AGENT_WORKER_SERVICE_ID}:session=${worker.workerSession.workerSessionId}:status=${worker.workerSession.status}`, - `workspace:${worker.workspace.workspaceVolumeId}:cleaned=${worker.workspace.cleaned}:existsAfterCleanup=${worker.workspace.existsAfterCleanup}`, - `${AGENT_SKILLS_SERVICE_ID}:commit=${fixture.skillCommitId}:version=${fixture.skillVersion}`, - `trace:${trace.traceId}:events=${trace.traceEvents.length}`, - `evidence:${evidence.evidenceRecords[0].evidenceId}:kind=${evidence.evidenceRecords[0].kind}`, - `cleanup:${worker.cleanup.cleanupId}:removed=${worker.cleanup.removed}` - ] - }); - } finally { - await rm(stateDir, { recursive: true, force: true }); - } -} - -async function runLivePreflight(fixture, dryRun) { - const liveEvidence = []; - const [d601K3s, publicEntrypoints] = await Promise.all([ - collectD601K3sNativeEvidence(fixture), - collectPublicEntrypoints() - ]); - liveEvidence.push(...d601K3s.evidence); - liveEvidence.push(publicEntrypoints.summary); - - let result = { - blocked: true, - blockerType: "network_blocker", - blockerScope: "frp", - blockedClassification: "frp", - blockerSummary: "The fixed DEV endpoint did not accept a live connection from this runner.", - summary: `Blocked before agent scheduling because ${DEV_ENDPOINT} is not reachable from this D601 runner.` - }; - - try { - const health = await requestJson(`${DEV_ENDPOINT}/health`); - if (health.statusCode < 200 || health.statusCode >= 300) { - result = { - blocked: true, - blockerType: "network_blocker", - blockerScope: "frp", - blockedClassification: "frp", - blockerSummary: `DEV health returned HTTP ${health.statusCode}.`, - summary: `Blocked on DEV ingress health because ${DEV_ENDPOINT}/health did not return success.` - }; - } else { - const body = health.body || {}; - liveEvidence.push(`health:${body.serviceId}:${body.environment}:${body.status}`); - - const live = await requestJson(`${DEV_ENDPOINT}/health/live`); - const liveBody = live.body || {}; - if (live.statusCode < 200 || live.statusCode >= 300 || liveBody?.serviceId !== "hwlab-cloud-api") { - result = { - blocked: true, - blockerType: "network_blocker", - blockerScope: "frp", - blockedClassification: "frp", - blockerSummary: "DEV live probe did not present the expected HWLAB cloud API identity.", - summary: "Blocked because the live DEV boundary did not expose the expected cloud API identity." - }; - } else { - const liveReadiness = classifyCloudApiLiveReadiness(liveBody); - if (liveReadiness.blocked) { - liveEvidence.push(`live:${liveBody.serviceId}:${liveBody.status}:${liveReadiness.evidenceSuffix}`); - const runtime = runtimeDurabilityFromHealth(liveBody); - liveEvidence.push(`runtime-durable-adapter:adapter=${runtime.adapter}:blocker=${runtime.blocker}:migration=${runtime.migration.requiredMigrationId}:migrationChecked=${runtime.migration.checked}:migrationReady=${runtime.migration.ready}:queryResult=${runtime.queryResult}`); - result = liveReadiness; - } else { - liveEvidence.push(`live:${liveBody.serviceId}:${liveBody.status}`); - const runtime = runtimeDurabilityFromHealth(liveBody); - liveEvidence.push(`runtime-durable-adapter:adapter=${runtime.adapter}:ready=${runtime.ready}:migration=${runtime.migration.requiredMigrationId}:queryResult=${runtime.queryResult}`); - - const rpcHealth = await requestJson(`${DEV_ENDPOINT}/rpc`, { - method: "POST", - headers: { - "content-type": "application/json" - }, - body: JSON.stringify({ - jsonrpc: "2.0", - id: "req_dev_m4_health", - method: "system.health", - params: {}, - meta: { - traceId: "trc_dev_m4_health", - serviceId: "hwlab-cloud-api", - environment: ENVIRONMENT_DEV - } - }) - }); - const rpcHealthBody = rpcHealth.body || {}; - liveEvidence.push(`rpc-health:${rpcHealthBody.result?.serviceId ?? "unknown"}`); - if (rpcHealth.statusCode < 200 || rpcHealth.statusCode >= 300 || rpcHealthBody.error) { - result = { - blocked: true, - blockerType: "agent_blocker", - blockerScope: "agent-runtime", - blockedClassification: "agent runtime", - blockerSummary: rpcHealthBody.error?.message ?? `system.health returned HTTP ${rpcHealth.statusCode}.`, - summary: "Blocked at agent runtime health before attempting hardware dispatch." - }; - } else { - const rpcHardware = await requestJson(`${DEV_ENDPOINT}/rpc`, { - method: "POST", - headers: { - "content-type": "application/json" - }, - body: JSON.stringify({ - jsonrpc: "2.0", - id: "req_dev_m4_hw", - method: "hardware.operation.request", - params: { - projectId: fixture.projectId, - input: { - requestedPath: "DO1 -> hwlab-patch-panel -> DI1", - directBoxGatewayAccess: false, - source: "dev-m4-agent-loop-preflight" - } - }, - meta: { - traceId: "trc_dev_m4_hw", - serviceId: "hwlab-cloud-api", - environment: ENVIRONMENT_DEV - } - }) - }); - const rpcHardwareBody = rpcHardware.body || {}; - const accepted = rpcHardwareBody.result?.accepted === true; - const status = rpcHardwareBody.result?.status || rpcHardwareBody.error?.message || "unknown"; - liveEvidence.push(`rpc-hardware:${status}`); - - if (!accepted) { - result = { - blocked: true, - blockerType: "agent_blocker", - blockerScope: "hardware-loop", - blockedClassification: "hardware loop", - blockerSummary: "hardware.operation.request did not accept the patch-panel-bounded M3 request.", - summary: "Blocked by M3 readiness because M4 did not observe an accepted DO1 -> patch-panel -> DI1 hardware request." - }; - } else if (!rpcHardwareBody.result?.auditId || !rpcHardwareBody.result?.evidenceId) { - result = { - blocked: true, - blockerType: "observability_blocker", - blockerScope: "evidence-audit", - blockedClassification: "evidence/audit", - blockerSummary: "hardware.operation.request accepted but did not return audit/evidence identifiers.", - summary: "Blocked because accepted DEV agent hardware dispatch lacks audit/evidence closure." - }; - } else { - result = { - blocked: false, - blockerType: null, - blockerScope: null, - blockedClassification: "none", - blockerSummary: "", - summary: "Live DEV preflight observed a non-skeleton agent hardware path." - }; - } - } - } - } - } - } catch (error) { - result = { - blocked: true, - blockerType: "network_blocker", - blockerScope: "frp", - blockedClassification: "frp", - blockerSummary: error instanceof Error ? error.message : String(error), - summary: `Blocked before agent scheduling because ${DEV_ENDPOINT} is unreachable from this runner.` - }; - } - - result.additionalBlockers = d601K3s.blockers; - if (!result.blocked && result.additionalBlockers.length > 0) { - result.summary = "D601 native k3s agent evidence is blocked even though the public preflight path did not produce an earlier blocker."; - } - - const report = buildReport(fixture, liveEvidence, result, { dryRun, d601K3s, publicEntrypoints }); - await writeJSON(reportPath, report); - - process.stdout.write(JSON.stringify({ - reportPath, - status: report.devPreconditions.status, - blocker: report.blockers[0] ?? null, - classification: report.devPreconditions.classification, - evidence: report.livePreflight.evidence, - summary: report.livePreflight.summary, - observedAt: fixedNow() - }, null, 2)); - process.stdout.write("\n"); -} - -async function main() { - const args = parseArgs(process.argv.slice(2)); - const fixture = await loadFixture(); - - if (args.dryRun) { - assert.equal(args.live, false, "--dry-run cannot be combined with --live"); - const dryRun = await runDryRun(fixture); - process.stdout.write(JSON.stringify({ - reportPath, - status: dryRun.status, - evidence: dryRun.evidence, - summary: dryRun.summary, - observedAt: fixedNow() - }, null, 2)); - process.stdout.write("\n"); - return; - } - - assert.equal(args.live, true, "live preflight requires --live"); - assert.equal(args.confirmDev, true, "live preflight requires --confirm-dev"); - assert.equal(args.confirmedNonProduction, true, "live preflight requires --confirmed-non-production"); - - await runLivePreflight(fixture, await runDryRun(fixture)); -} - -await main(); diff --git a/scripts/g14-ci-plan.test.mjs b/scripts/g14-ci-plan.test.mjs index 5c3bd553..4b1a72d6 100644 --- a/scripts/g14-ci-plan.test.mjs +++ b/scripts/g14-ci-plan.test.mjs @@ -231,7 +231,7 @@ test("planner scopes cloud-web runtime changes to cloud-web", async () => { const plan = await createG14CiPlan({ repoRoot: repo, baseRef: "HEAD~1", targetRef: "HEAD" }); assert.deepEqual(plan.affectedServices, ["hwlab-cloud-web"]); - assert.equal(plan.buildServices.includes("hwlab-agent-worker"), false); + assert.equal(plan.services.some((service) => service.serviceId === "hwlab-agent-worker"), false); const cloudWeb = plan.services.find((service) => service.serviceId === "hwlab-cloud-web"); assert.deepEqual(cloudWeb.reason, ["component-path-changed"]); }); @@ -408,96 +408,19 @@ test("v02 planner marks cloud-web code-only change as env reuse rollout", async assert.equal(plan.ciCdPlan.noImageBuildReason, "env-reuse-code-only-rollout"); }); -test("v02 planner marks agent-worker code-only change as env reuse rollout", async () => { - const repo = await createFixtureRepo({ includeDevicePod: true, includeAgentWorker: true }); - const initialSha = (await git(repo, ["rev-parse", "HEAD"])).stdout.trim(); - const initialPlan = await createG14CiPlan({ - repoRoot: repo, - lane: "v02", - baseRef: "HEAD", - targetRef: initialSha, - artifactCatalogPath: "deploy/artifact-catalog.v02.json", - services: ["hwlab-agent-worker"] - }); - const initialWorker = initialPlan.services.find((service) => service.serviceId === "hwlab-agent-worker"); - await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(createCatalogFixture("ready", { - agentWorkerEnvironmentInputHash: initialWorker.environmentInputHash, - agentWorkerCodeInputHash: initialWorker.codeInputHash, - agentWorkerBootCommit: initialPlan.sourceCommitId - }), null, 2)); - await git(repo, ["add", "deploy/artifact-catalog.v02.json"]); - await git(repo, ["commit", "-m", "seed v02 worker catalog"]); - - await writeFile(path.join(repo, "cmd/hwlab-agent-worker/main.ts"), "console.log('worker v2');\n"); - await git(repo, ["add", "."]); - await git(repo, ["commit", "-m", "change agent worker code"]); - - const plan = await createG14CiPlan({ - repoRoot: repo, - lane: "v02", - baseRef: "HEAD~1", - targetRef: "HEAD", - artifactCatalogPath: "deploy/artifact-catalog.v02.json", - services: ["hwlab-agent-worker"] - }); - const worker = plan.services.find((service) => service.serviceId === "hwlab-agent-worker"); - assert.equal(worker.runtimeMode, "env-reuse-git-mirror-checkout"); - assert.equal(worker.envChanged, false); - assert.equal(worker.codeChanged, true); - assert.equal(worker.buildRequired, false); - assert.equal(worker.bootSh, "deploy/runtime/boot/hwlab-agent-worker.sh"); - assert.deepEqual(plan.affectedServices, ["hwlab-agent-worker"]); - assert.deepEqual(plan.rolloutServices, ["hwlab-agent-worker"]); - assert.deepEqual(plan.buildServices, []); - assert.equal(plan.imageBuildRequired, false); - assert.equal(plan.ciCdPlan.noImageBuildReason, "env-reuse-code-only-rollout"); -}); - -test("v02 planner rebuilds agent-worker env image only for runtime dependency changes", async () => { - const repo = await createFixtureRepo({ includeDevicePod: true, includeAgentWorker: true }); - const initialSha = (await git(repo, ["rev-parse", "HEAD"])).stdout.trim(); - const initialPlan = await createG14CiPlan({ - repoRoot: repo, - lane: "v02", - baseRef: "HEAD", - targetRef: initialSha, - artifactCatalogPath: "deploy/artifact-catalog.v02.json", - services: ["hwlab-agent-worker"] - }); - const initialWorker = initialPlan.services.find((service) => service.serviceId === "hwlab-agent-worker"); - await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(createCatalogFixture("ready", { - agentWorkerEnvironmentInputHash: initialWorker.environmentInputHash, - agentWorkerCodeInputHash: initialWorker.codeInputHash, - agentWorkerBootCommit: initialPlan.sourceCommitId - }), null, 2)); - await git(repo, ["add", "deploy/artifact-catalog.v02.json"]); - await git(repo, ["commit", "-m", "seed v02 worker catalog"]); - - await writeFile(path.join(repo, "package.json"), JSON.stringify({ - type: "module", - dependencies: { - "@openai/codex": "^0.129.0" - } - }, null, 2)); - await git(repo, ["add", "package.json"]); - await git(repo, ["commit", "-m", "change worker env dependency"]); - - const plan = await createG14CiPlan({ - repoRoot: repo, - lane: "v02", - baseRef: "HEAD~1", - targetRef: "HEAD", - artifactCatalogPath: "deploy/artifact-catalog.v02.json", - services: ["hwlab-agent-worker"] - }); - const worker = plan.services.find((service) => service.serviceId === "hwlab-agent-worker"); - assert.equal(worker.runtimeMode, "env-reuse-git-mirror-checkout"); - assert.equal(worker.envChanged, true); - assert.equal(worker.buildRequired, true); - assert.deepEqual(plan.affectedServices, ["hwlab-agent-worker"]); - assert.deepEqual(plan.buildServices, ["hwlab-agent-worker"]); - assert.ok(worker.reason.includes("environment-input-changed")); - assert.ok(worker.reason.includes("runtime-deps-changed")); +test("v02 planner rejects removed HWLAB-owned agent worker service selections", async () => { + const repo = await createFixtureRepo({ includeDevicePod: true }); + await assert.rejects( + () => createG14CiPlan({ + repoRoot: repo, + lane: "v02", + baseRef: "HEAD", + targetRef: "HEAD", + artifactCatalogPath: "deploy/artifact-catalog.v02.json", + services: ["hwlab-agent-worker"] + }), + /unknown service IDs for G14 CI plan: hwlab-agent-worker/u + ); }); test("v02 planner keeps hwlab-cli-only changes out of runtime service builds", async () => { @@ -604,11 +527,9 @@ async function createFixtureRepo(options = {}) { const deployServices = options.deployServices !== false; const k3sServiceMappings = options.k3sServiceMappings === true; const includeDevicePod = options.includeDevicePod === true; - const includeAgentWorker = options.includeAgentWorker === true; const repo = await mkdtemp(path.join(os.tmpdir(), "hwlab-g14-ci-plan-")); await mkdir(path.join(repo, "cmd/hwlab-cloud-api"), { recursive: true }); await mkdir(path.join(repo, "cmd/hwlab-deepseek-responses-bridge"), { recursive: true }); - await mkdir(path.join(repo, "cmd/hwlab-agent-worker"), { recursive: true }); await mkdir(path.join(repo, "web/hwlab-cloud-web"), { recursive: true }); await mkdir(path.join(repo, "internal/dev-entrypoint"), { recursive: true }); await mkdir(path.join(repo, "internal/device-pod"), { recursive: true }); @@ -627,7 +548,6 @@ async function createFixtureRepo(options = {}) { await writeFile(path.join(repo, "cmd/hwlab-cloud-api/main.ts"), "console.log('api');\n"); await writeFile(path.join(repo, "cmd/hwlab-deepseek-responses-bridge/main.ts"), "console.log('bridge');\n"); await writeFile(path.join(repo, "cmd/hwlab-deepseek-responses-bridge/main.test.ts"), "console.log('test');\n"); - await writeFile(path.join(repo, "cmd/hwlab-agent-worker/main.ts"), "console.log('worker');\n"); await writeFile(path.join(repo, "web/hwlab-cloud-web/index.html"), "\n"); await writeFile(path.join(repo, "internal/dev-entrypoint/artifact-runtime.mjs"), "export const artifactRuntime = 1;\n"); await writeFile(path.join(repo, "internal/dev-entrypoint/cloud-web-runtime.mjs"), "export const runtime = 1;\n"); @@ -642,7 +562,6 @@ async function createFixtureRepo(options = {}) { await writeFile(path.join(repo, "skills/device-pod-cli/scripts/device-pod-cli.mjs"), "console.log('skill wrapper');\n"); await writeFile(path.join(repo, "skills/device-pod-cli/assets/device-host-cli.mjs"), "console.log('host cli');\n"); await writeFile(path.join(repo, "deploy/runtime/boot/hwlab-cloud-web.sh"), "#!/bin/sh\nbun run --cwd web/hwlab-cloud-web build\nexec bun .hwlab-cloud-web-runtime.mjs\n"); - await writeFile(path.join(repo, "deploy/runtime/boot/hwlab-agent-worker.sh"), "#!/bin/sh\nexec bun cmd/hwlab-agent-worker/main.ts \"$@\"\n"); await writeFile(path.join(repo, "deploy/runtime/boot/hwlab-device-pod.sh"), "#!/bin/sh\nexec bun cmd/hwlab-device-pod/main.ts\n"); await writeFile(path.join(repo, "deploy/runtime/launcher/hwlab-env-reuse-launcher.ts"), "console.log('launcher');\n"); await writeFile(path.join(repo, "skills/hwlab-agent-runtime/SKILL.md"), "agent runtime skill\n"); @@ -656,7 +575,7 @@ async function createFixtureRepo(options = {}) { baseRef: "HEAD", targetRef: "HEAD", artifactCatalogPath: "deploy/nonexistent-artifact-catalog.json", - services: ["hwlab-cloud-api", "hwlab-cloud-web", ...(includeAgentWorker ? ["hwlab-agent-worker"] : []), ...(includeDevicePod ? ["hwlab-device-pod"] : [])] + services: ["hwlab-cloud-api", "hwlab-cloud-web", ...(includeDevicePod ? ["hwlab-device-pod"] : [])] }); const componentInputHashes = Object.fromEntries(initialPlan.services.map((service) => [service.serviceId, service.componentInputHash])); const catalog = createCatalogFixture(catalogMode, { sourceCommitId: initialSha, componentInputHashes }); @@ -691,10 +610,9 @@ function createDeployFixture({ deployServices, k3sServiceMappings, includeDevice lanes: { v02: { sourceRepo: "git@github.com:pikasTech/HWLAB.git", - envReuseServices: ["hwlab-cloud-web", "hwlab-agent-worker", "hwlab-device-pod"], + envReuseServices: ["hwlab-cloud-web", "hwlab-device-pod"], bootScripts: { "hwlab-cloud-web": "deploy/runtime/boot/hwlab-cloud-web.sh", - "hwlab-agent-worker": "deploy/runtime/boot/hwlab-agent-worker.sh", "hwlab-device-pod": "deploy/runtime/boot/hwlab-device-pod.sh" } } @@ -722,7 +640,7 @@ function createDeployFixture({ deployServices, k3sServiceMappings, includeDevice function createCatalogFixture(mode, options = {}) { const digest = mode === "ready" ? `sha256:${"1".repeat(64)}` : "not_published"; return { - services: ["hwlab-cloud-api", "hwlab-cloud-web", "hwlab-agent-worker", "hwlab-device-pod"].map((serviceId) => ({ + services: ["hwlab-cloud-api", "hwlab-cloud-web", "hwlab-device-pod"].map((serviceId) => ({ serviceId, commitId: "abc1234", sourceCommitId: options.sourceCommitId ?? "abc1234", @@ -732,26 +650,20 @@ function createCatalogFixture(mode, options = {}) { ...((options.componentInputHashes?.[serviceId] || (serviceId === "hwlab-cloud-api" && options.cloudApiComponentInputHash)) ? { componentInputHash: options.cloudApiComponentInputHash ?? options.componentInputHashes?.[serviceId] } : {}), - ...((serviceId === "hwlab-cloud-web" || serviceId === "hwlab-agent-worker" || serviceId === "hwlab-device-pod") ? { + ...((serviceId === "hwlab-cloud-web" || serviceId === "hwlab-device-pod") ? { runtimeMode: "env-reuse-git-mirror-checkout", envReuse: true, environmentImage: `127.0.0.1:5000/hwlab/${serviceId}-env:env-abc1234`, environmentDigest: digest, environmentInputHash: serviceId === "hwlab-cloud-web" ? options.cloudWebEnvironmentInputHash ?? "e".repeat(64) - : serviceId === "hwlab-agent-worker" - ? options.agentWorkerEnvironmentInputHash ?? "e".repeat(64) : options.devicePodEnvironmentInputHash ?? "e".repeat(64), codeInputHash: serviceId === "hwlab-cloud-web" ? options.cloudWebCodeInputHash ?? "c".repeat(64) - : serviceId === "hwlab-agent-worker" - ? options.agentWorkerCodeInputHash ?? "c".repeat(64) : options.devicePodCodeInputHash ?? "c".repeat(64), bootRepo: "git@github.com:pikasTech/HWLAB.git", bootCommit: serviceId === "hwlab-cloud-web" ? options.cloudWebBootCommit ?? "abc1234" - : serviceId === "hwlab-agent-worker" - ? options.agentWorkerBootCommit ?? "abc1234" : options.devicePodBootCommit ?? "abc1234", bootSh: `deploy/runtime/boot/${serviceId}.sh` } : {}) diff --git a/scripts/g14-gitops-render.mjs b/scripts/g14-gitops-render.mjs index e7dc719f..bae522d4 100644 --- a/scripts/g14-gitops-render.mjs +++ b/scripts/g14-gitops-render.mjs @@ -33,8 +33,6 @@ const defaultDevBaseImage = process.env.HWLAB_G14_DEV_BASE_IMAGE || "127.0.0.1:5 const defaultServiceIds = Object.freeze([ "hwlab-cloud-api", "hwlab-cloud-web", - "hwlab-agent-mgr", - "hwlab-agent-worker", "hwlab-device-pod", "hwlab-gateway", "hwlab-edge-proxy", @@ -43,14 +41,16 @@ const defaultServiceIds = Object.freeze([ const v02RuntimeServiceIds = Object.freeze([ "hwlab-cloud-api", "hwlab-cloud-web", - "hwlab-agent-mgr", - "hwlab-agent-worker", "hwlab-device-pod", "hwlab-gateway", "hwlab-edge-proxy", "hwlab-agent-skills" ]); const v02RemovedServiceIds = new Set([ + "hwlab-agent-mgr", + "hwlab-agent-worker", + "hwlab-agent-worker-template", + "hwlab-code-agent-workspace", "hwlab-router", "hwlab-tunnel-client", "hwlab-gateway-simu", @@ -58,6 +58,18 @@ const v02RemovedServiceIds = new Set([ "hwlab-patch-panel" ]); const v02RemovedServiceEnvNames = new Set([ + "CODEX_HOME", + "OPENAI_API_KEY", + "HWLAB_CODE_AGENT_PROVIDER", + "HWLAB_CODE_AGENT_MODEL", + "HWLAB_CODE_AGENT_OPENAI_BASE_URL", + "HWLAB_CODE_AGENT_WORKSPACE", + "HWLAB_CODE_AGENT_CODEX_HOME", + "HWLAB_CODE_AGENT_CODEX_WORKSPACE", + "HWLAB_CODE_AGENT_CODEX_SANDBOX", + "HWLAB_CODE_AGENT_CODEX_COMMAND", + "HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED", + "HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR", "HWLAB_M3_GATEWAY_SIMU_1_URL", "HWLAB_M3_GATEWAY_SIMU_2_URL", "HWLAB_M3_PATCH_PANEL_URL", @@ -728,6 +740,21 @@ function removeV02LegacySimulatorEnv(envList) { return envList.filter((entry) => !v02RemovedServiceEnvNames.has(entry?.name)); } +function removeV02RepoOwnedCodeAgentPodConfig(podSpec) { + if (!podSpec) return; + podSpec.initContainers = (podSpec.initContainers ?? []).filter((entry) => entry?.name !== "hwlab-code-agent-workspace-init"); + const removedVolumeNames = new Set([ + "hwlab-code-agent-workspace", + "hwlab-code-agent-codex-home", + "hwlab-code-agent-codex-config", + "hwlab-code-agent-codex-auth" + ]); + podSpec.volumes = (podSpec.volumes ?? []).filter((volume) => !removedVolumeNames.has(volume?.name)); + for (const container of podSpec.containers ?? []) { + container.volumeMounts = (container.volumeMounts ?? []).filter((mount) => !removedVolumeNames.has(mount?.name)); + } +} + function deployServicesForProfile(deploy, profile) { const allowed = new Set(serviceIdsForProfile(profile)); const services = new Map((deploy.services ?? []) @@ -867,8 +894,8 @@ function transformWorkloads({ workloads, deploy, catalog, source, sourceBranch = upsertEnv(container.env, "HWLAB_PUBLIC_ENDPOINT", runtimeEndpoint); } if (serviceId === "hwlab-cloud-api") { - syncCodeAgentWorkspaceInitContainer(podTemplate.spec.initContainers, { image, runtimeCommit, runtimeImageTag }); if (profile === "v02") { + removeV02RepoOwnedCodeAgentPodConfig(podTemplate.spec); upsertEnv(container.env, "HWLAB_M3_IO_CONTROL_ENABLED", "false"); upsertEnv(container.env, "HWLAB_ACCESS_CONTROL_REQUIRED", "1"); upsertEnv(container.env, "HWLAB_BOOTSTRAP_ADMIN_ID", "usr_v02_admin"); @@ -885,6 +912,8 @@ function transformWorkloads({ workloads, deploy, catalog, source, sourceBranch = upsertEnv(container.env, "HWLAB_CODE_AGENT_AGENTRUN_SECRET_NAMESPACE", "agentrun-v01"); upsertEnv(container.env, "HWLAB_CODE_AGENT_AGENTRUN_REPO_URL", "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git"); upsertEnv(container.env, "UNIDESK_MAIN_SERVER_IP", "http://74.48.78.17:18081"); + } else { + syncCodeAgentWorkspaceInitContainer(podTemplate.spec.initContainers, { image, runtimeCommit, runtimeImageTag }); } upsertEnv(container.env, "HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE", "deepseek"); upsertEnv(container.env, "HWLAB_CODE_AGENT_DEEPSEEK_MODEL", deepSeekProfileModel); @@ -4339,7 +4368,8 @@ function v02PostgresManifest({ migrationSql, source }) { } function runtimeKustomization({ profile = "dev", includeDeviceAgent = profile === "dev" } = {}) { - const resources = ["namespace.yaml", "code-agent-codex-config.yaml", "services.yaml", "health-contract.yaml", "workloads.yaml", "deepseek-proxy.yaml"]; + const resources = ["namespace.yaml", "services.yaml", "health-contract.yaml", "workloads.yaml", "deepseek-proxy.yaml"]; + if (profile !== "v02") resources.splice(1, 0, "code-agent-codex-config.yaml"); if (profile === "v02") resources.push("postgres.yaml"); if (includeDeviceAgent) resources.push("device-agent-71-freq.yaml"); resources.push("g14-frpc.yaml"); @@ -4467,7 +4497,7 @@ async function plannedFiles(args) { const endpoints = profileEndpoint(args, profile); putJson(`${runtimePath}/kustomization.yaml`, runtimeKustomization({ profile, includeDeviceAgent })); putJson(`${runtimePath}/namespace.yaml`, runtimeNamespace); - putJson(`${runtimePath}/code-agent-codex-config.yaml`, transformListNamespace(config, namespace, profileLabels, annotations)); + if (profile !== "v02") putJson(`${runtimePath}/code-agent-codex-config.yaml`, transformListNamespace(config, namespace, profileLabels, annotations)); putJson(`${runtimePath}/services.yaml`, transformServices({ services, namespace, labels: profileLabels, annotations, profile })); putJson(`${runtimePath}/health-contract.yaml`, transformHealthContract(health, namespace, profileLabels, annotations, endpoints.runtimeEndpoint, endpoints.webEndpoint, profile)); putJson(`${runtimePath}/workloads.yaml`, transformWorkloads({ workloads, deploy, catalog: artifactCatalog, source, sourceBranch: args.sourceBranch, registryPrefix: args.registryPrefix, runtimeEndpoint: endpoints.runtimeEndpoint, webEndpoint: endpoints.webEndpoint, profile, useDeployImages: args.useDeployImages })); diff --git a/scripts/g14-gitops-render.test.ts b/scripts/g14-gitops-render.test.ts index 68091c3e..060fe95c 100644 --- a/scripts/g14-gitops-render.test.ts +++ b/scripts/g14-gitops-render.test.ts @@ -166,6 +166,10 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots } const removedServiceIds = [ + "hwlab-agent-mgr", + "hwlab-agent-worker", + "hwlab-agent-worker-template", + "hwlab-code-agent-workspace", "hwlab-router", "hwlab-tunnel-client", "hwlab-gateway-simu", @@ -179,8 +183,6 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots const retainedServiceIds = [ "hwlab-cloud-api", "hwlab-cloud-web", - "hwlab-agent-mgr", - "hwlab-agent-worker", "hwlab-device-pod", "hwlab-gateway", "hwlab-edge-proxy", @@ -231,19 +233,18 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots assert.ok(cloudApi, "expected v02 hwlab-cloud-api workload"); const cloudApiEnv = cloudApi.spec.template.spec.containers[0].env; assert.ok(cloudApiEnv.some((entry) => entry.name === "HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT" && entry.value === sourceRevision)); + assert.ok(cloudApiEnv.some((entry) => entry.name === "HWLAB_CODE_AGENT_ADAPTER" && entry.value === "agentrun-v01")); + assert.equal(cloudApi.spec.template.spec.initContainers?.some((entry) => entry.name === "hwlab-code-agent-workspace-init"), false); + assert.equal(cloudApi.spec.template.spec.volumes?.some((entry) => /code-agent-codex|code-agent-workspace/u.test(entry.name)), false); + assert.equal(cloudApi.spec.template.spec.containers[0].volumeMounts?.some((entry) => /code-agent-codex|code-agent-workspace/u.test(entry.name)), false); + assert.equal(cloudApiEnv.some((entry) => entry.name === "HWLAB_CODE_AGENT_PROVIDER" && entry.value === "codex-stdio"), false); + assert.equal(cloudApiEnv.some((entry) => entry.name === "HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR"), false); assert.ok(cloudApiEnv.some((entry) => entry.name === "UNIDESK_MAIN_SERVER_IP" && entry.value === "http://74.48.78.17:18081")); assert.ok(cloudApiEnv.some((entry) => entry.name === "HWLAB_KEYCLOAK_ISSUER" && entry.value === "https://auth.74-48-78-17.nip.io/realms/hwlab")); assert.ok(cloudApiEnv.some((entry) => entry.name === "HWLAB_KEYCLOAK_CLIENT_ID" && entry.value === "hwlab-cloud-web")); assert.ok(cloudApiEnv.some((entry) => entry.name === "HWLAB_KEYCLOAK_CLIENT_SECRET" && entry.valueFrom?.secretKeyRef?.name === "hwlab-cloud-web-client" && entry.valueFrom?.secretKeyRef?.key === "client-secret")); - const agentWorker = workloadsJson.items.find((item) => item.metadata?.labels?.["hwlab.pikastech.local/service-id"] === "hwlab-agent-worker"); - assert.ok(agentWorker, "expected v02 hwlab-agent-worker suspended Job template"); - const agentWorkerEnv = agentWorker?.spec?.template?.spec?.containers?.[0]?.env ?? []; - assert.ok(agentWorkerEnv.some((entry) => entry.name === "HWLAB_RUNTIME_MODE" && entry.value === "env-reuse-git-mirror-checkout")); - assert.ok(agentWorkerEnv.some((entry) => entry.name === "HWLAB_BOOT_REPO" && entry.value === "git@github.com:pikasTech/HWLAB.git")); - assert.ok(agentWorkerEnv.some((entry) => entry.name === "HWLAB_BOOT_COMMIT" && entry.value === sourceRevision)); - assert.ok(agentWorkerEnv.some((entry) => entry.name === "HWLAB_BOOT_SH" && entry.value === "deploy/runtime/boot/hwlab-agent-worker.sh")); - assert.equal(agentWorker?.spec?.template?.metadata?.annotations?.["hwlab.pikastech.local/runtime-mode"], "env-reuse-git-mirror-checkout"); - assert.equal(agentWorker?.spec?.template?.metadata?.annotations?.["hwlab.pikastech.local/boot-commit"], sourceRevision); + assert.equal(workloadsJson.items.some((item) => item.metadata?.labels?.["hwlab.pikastech.local/service-id"] === "hwlab-agent-worker"), false); + assert.doesNotMatch(await readFile(path.join(outDir, "runtime-v02", "kustomization.yaml"), "utf8"), /code-agent-codex-config/u); const workloadTemplates = workloadsJson.items.filter((item) => item.spec?.template?.metadata?.labels?.["hwlab.pikastech.local/source-commit"]); assert.ok(workloadTemplates.length > 0, "expected rendered pod-template source labels"); assert.equal(devicePod.spec.template.metadata.labels["hwlab.pikastech.local/source-commit"], sourceRevision); diff --git a/scripts/m1-contract-smoke.mjs b/scripts/m1-contract-smoke.mjs deleted file mode 100644 index 8d3f7de5..00000000 --- a/scripts/m1-contract-smoke.mjs +++ /dev/null @@ -1,634 +0,0 @@ -#!/usr/bin/env bun -import assert from "node:assert/strict"; -import { spawn } from "node:child_process"; -import { existsSync } from "node:fs"; -import { mkdtemp, readFile, rm } from "node:fs/promises"; -import net from "node:net"; -import os from "node:os"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../internal/protocol/index.mjs"; -import { validateCodeAgentChatSchema } from "../internal/cloud/code-agent-chat.ts"; - -const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const children = []; -const bunCommand = resolveBunCommand(); -const CODEX_STDIO_FEASIBILITY_BLOCKERS = new Set([ - "codex_cli_binary_missing", - "codex_stdio_supervisor_disabled", - "codex_stdio_egress_boundary" -]); - -function logOk(name) { - process.stdout.write(`[m1-smoke] ok ${name}\n`); -} - -function assertCodexStdioFeasibilityBlocker(value, label = "codex stdio blocker") { - assert.equal(CODEX_STDIO_FEASIBILITY_BLOCKERS.has(value), true, `${label}: ${value}`); -} - -function freePort() { - return new Promise((resolve, reject) => { - const server = net.createServer(); - server.on("error", reject); - server.listen(0, "127.0.0.1", () => { - const address = server.address(); - const port = typeof address === "object" && address ? address.port : null; - server.close((error) => { - if (error) { - reject(error); - } else { - resolve(port); - } - }); - }); - }); -} - -function startNode(relativeScript, env = {}) { - const command = commandForScript(relativeScript); - const child = spawn(command.bin, command.args, { - cwd: repoRoot, - env: { - ...process.env, - ...env - }, - stdio: ["ignore", "pipe", "pipe"] - }); - const output = { - stdout: "", - stderr: "" - }; - - child.stdout.setEncoding("utf8"); - child.stderr.setEncoding("utf8"); - child.stdout.on("data", (chunk) => { - output.stdout += chunk; - }); - child.stderr.on("data", (chunk) => { - output.stderr += chunk; - }); - - const record = { child, relativeScript, output }; - children.push(record); - return record; -} - -function resolveBunCommand() { - const candidates = [ - process.env.HWLAB_BUN_COMMAND, - process.env.HOME ? path.join(process.env.HOME, ".bun/bin/bun") : null, - "bun" - ].filter(Boolean); - return candidates.find((candidate) => !candidate.includes("/") || existsSync(candidate)) || "bun"; -} - -function runNodeJson(relativeScript, args = [], env = {}) { - return new Promise((resolve, reject) => { - const command = commandForScript(relativeScript, args); - const child = spawn(command.bin, command.args, { - cwd: repoRoot, - env: { - ...process.env, - ...env - }, - stdio: ["ignore", "pipe", "pipe"] - }); - let stdout = ""; - let stderr = ""; - - child.stdout.setEncoding("utf8"); - child.stderr.setEncoding("utf8"); - child.stdout.on("data", (chunk) => { - stdout += chunk; - }); - child.stderr.on("data", (chunk) => { - stderr += chunk; - }); - child.on("error", reject); - child.on("close", (code) => { - if (code !== 0) { - reject(new Error(`${relativeScript} ${args.join(" ")} exited with ${code}\nstdout:\n${stdout}\nstderr:\n${stderr}`)); - return; - } - try { - resolve(JSON.parse(stdout)); - } catch (error) { - reject(new Error(`${relativeScript} ${args.join(" ")} did not return JSON: ${error.message}\nstdout:\n${stdout}\nstderr:\n${stderr}`)); - } - }); - }); -} - -function commandForScript(relativeScript, args = []) { - if (relativeScript.endsWith(".ts")) { - return { - bin: bunCommand, - args: ["run", relativeScript, ...args] - }; - } - return { - bin: process.execPath, - args: [relativeScript, ...args] - }; -} - -async function stopNode(record) { - const { child } = record; - if (child.exitCode !== null || child.signalCode !== null) { - return; - } - - await new Promise((resolve) => { - const killTimer = setTimeout(() => { - child.kill("SIGKILL"); - }, 1500); - child.once("exit", () => { - clearTimeout(killTimer); - resolve(); - }); - child.kill("SIGTERM"); - }); -} - -async function requestJson(url, options = {}) { - const response = await fetch(url, { - ...options, - headers: { - "content-type": "application/json", - ...(options.headers ?? {}) - } - }); - const text = await response.text(); - const body = text ? JSON.parse(text) : null; - return { response, body }; -} - -async function waitForHttp(url, record) { - const startedAt = Date.now(); - let lastError; - - while (Date.now() - startedAt < 5000) { - if (record.child.exitCode !== null) { - throw new Error( - `${record.relativeScript} exited before becoming ready\nstdout:\n${record.output.stdout}\nstderr:\n${record.output.stderr}` - ); - } - - try { - const { response } = await requestJson(url); - if (response.ok) { - return; - } - lastError = new Error(`HTTP ${response.status}`); - } catch (error) { - lastError = error; - } - await new Promise((resolve) => setTimeout(resolve, 75)); - } - - throw new Error( - `${record.relativeScript} did not become ready at ${url}: ${lastError?.message ?? "timeout"}\nstdout:\n${record.output.stdout}\nstderr:\n${record.output.stderr}` - ); -} - -function rpcEnvelope({ id, method, params = {} }) { - return { - jsonrpc: "2.0", - id, - method, - params, - meta: { - traceId: `trc_${id}`, - serviceId: "hwlab-cloud-api", - environment: ENVIRONMENT_DEV - } - }; -} - -async function smokeCloudApi() { - const port = await freePort(); - const service = startNode("cmd/hwlab-cloud-api/main.ts", { - HWLAB_CLOUD_API_HOST: "127.0.0.1", - HWLAB_CLOUD_API_PORT: String(port), - HWLAB_CLOUD_DB_URL: "", - HWLAB_CLOUD_DB_SSL_MODE: "", - HWLAB_CODE_AGENT_PROVIDER: "codex-cli", - HWLAB_CODE_AGENT_MODEL: "gpt-m1-local", - HWLAB_CODE_AGENT_CODEX_COMMAND: "/tmp/hwlab-missing-codex", - OPENAI_API_KEY: "", - PATH: "" - }); - const baseUrl = `http://127.0.0.1:${port}`; - - await waitForHttp(`${baseUrl}/live`, service); - - const health = await requestJson(`${baseUrl}/health`); - assert.equal(health.response.status, 200); - assert.equal(health.body.serviceId, "hwlab-cloud-api"); - assert.equal(health.body.environment, ENVIRONMENT_DEV); - assert.equal(health.body.status, "degraded"); - assert.equal(health.body.db.connected, false); - assert.equal(health.body.db.liveConnected, false); - assert.equal(health.body.db.liveDbEvidence, false); - assert.equal(health.body.db.connectionAttempted, false); - assert.equal(health.body.db.connectionResult, "not_attempted_missing_env"); - assert.equal(health.body.db.status, "blocked"); - assert.equal(health.body.runtime.durable, false); - assert.deepEqual(health.body.db.missingEnv, ["HWLAB_CLOUD_DB_URL", "HWLAB_CLOUD_DB_SSL_MODE"]); - assert.equal(health.body.db.secretRefs[0].secretName, "hwlab-cloud-api-dev-db"); - assert.equal(health.body.db.secretRefs[0].redacted, true); - assert.equal(health.body.codeAgent.status, "partial"); - assert.match(health.body.codeAgent.blocker, /Codex stdio|long-lived|Codex CLI command/u); - assertCodexStdioFeasibilityBlocker(health.body.codeAgent.reason, "health codeAgent reason"); - assert.equal(health.body.readiness.provider.status, "blocked"); - assert.equal(health.body.readiness.provider.blocker, "provider_config_blocked"); - assert.equal(health.body.readiness.sessionRunner.status, "read_only_long_lived_ready"); - assert.equal(health.body.readiness.sessionRunner.ready, true); - assert.equal(health.body.readiness.codexStdio.status, "blocked"); - assertCodexStdioFeasibilityBlocker(health.body.readiness.codexStdio.blocker, "health readiness codexStdio blocker"); - assert.equal(health.body.readiness.codeAgent.currentBlockers.includes("provider_config_blocked"), true); - assert.equal(health.body.readiness.codeAgent.currentBlockers.includes(health.body.readiness.codexStdio.blocker), true); - assert.ok(health.body.codeAgent.missingEnv.includes("OPENAI_API_KEY")); - assert.equal(health.body.codeAgent.secretRefs[0].secretName, "hwlab-code-agent-provider"); - assert.equal(health.body.codeAgent.secretRefs[0].secretKey, "openai-api-key"); - assert.equal(health.body.codeAgent.secretRefs[0].redacted, true); - assert.equal(JSON.stringify(health.body.codeAgent).includes("sk-"), false); - - const live = await requestJson(`${baseUrl}/live`); - assert.equal(live.response.status, 200); - assert.equal(live.body.status, "live"); - - const adapter = await requestJson(`${baseUrl}/v1`); - assert.equal(adapter.response.status, 200); - assert.equal(adapter.body.status, "degraded"); - assert.ok(adapter.body.methods.includes("system.health")); - assert.ok(adapter.body.methods.includes("gateway.session.register")); - assert.ok(adapter.body.methods.includes("box.capability.report")); - assert.ok(adapter.body.methods.includes("hardware.operation.request")); - assert.ok(adapter.body.methods.includes("audit.event.query")); - assert.ok(adapter.body.methods.includes("evidence.record.query")); - assert.equal(adapter.body.codeAgent.endpoint, "POST /v1/agent/chat"); - assert.equal(adapter.body.codeAgent.status, "partial"); - assert.match(adapter.body.codeAgent.blocker, /Codex stdio|long-lived|Codex CLI command/u); - assert.equal(adapter.body.codeAgent.provider, "codex-cli"); - assert.equal(adapter.body.codeAgent.model, "gpt-m1-local"); - assert.equal(adapter.body.codeAgent.backend, "hwlab-cloud-api/codex-cli"); - assert.equal(adapter.body.codeAgent.ready, false); - assert.equal(adapter.body.readiness.provider.status, "blocked"); - assert.equal(adapter.body.readiness.sessionRunner.status, "read_only_long_lived_ready"); - assert.equal(adapter.body.readiness.codexStdio.status, "blocked"); - assert.ok(adapter.body.codeAgent.missingEnv.includes("OPENAI_API_KEY")); - assert.equal(adapter.body.codeAgent.secretRefs[0].secretName, "hwlab-code-agent-provider"); - assert.equal(adapter.body.codeAgent.secretRefs[0].secretKey, "openai-api-key"); - assert.equal(adapter.body.codeAgent.secretRefs[0].redacted, true); - assert.match(adapter.body.codeAgent.summary, /hwlab-code-agent-provider\/openai-api-key/u); - assert.equal(JSON.stringify(adapter.body.codeAgent).includes("sk-"), false); - - const agentChat = await requestJson(`${baseUrl}/v1/agent/chat`, { - method: "POST", - body: JSON.stringify({ - conversationId: "cnv_m1_local_chat", - traceId: "trc_m1_local_chat", - projectId: "prj_m1_local_contract", - message: "请用一句话说明本地 SOURCE/LOCAL 合同。" - }) - }); - assert.equal(agentChat.response.status, 200); - validateCodeAgentChatSchema(agentChat.body); - assert.equal(agentChat.body.conversationId, "cnv_m1_local_chat"); - assert.equal(agentChat.body.sessionId, "cnv_m1_local_chat"); - assert.match(agentChat.body.messageId, /^msg_/); - assert.equal(agentChat.body.status, "failed"); - assert.equal(agentChat.body.traceId, "trc_m1_local_chat"); - assert.equal(agentChat.body.projectId, "prj_m1_local_contract"); - assert.equal(agentChat.body.provider, "codex-cli"); - assert.equal(agentChat.body.model, "gpt-m1-local"); - assert.equal(agentChat.body.backend, "hwlab-cloud-api/codex-cli"); - assert.equal(agentChat.body.error.code, "codex_cli_binary_missing"); - assert.match(agentChat.body.error.message, /Codex CLI command is not available/); - assert.deepEqual(agentChat.body.error.missingCommands, ["/tmp/hwlab-missing-codex"]); - assert.ok(agentChat.body.error.missingEnv.includes("OPENAI_API_KEY")); - assert.equal(agentChat.body.availability.status, "partial"); - assert.match(agentChat.body.availability.blocker, /Codex stdio|long-lived|Codex CLI command/u); - assertCodexStdioFeasibilityBlocker(agentChat.body.availability.reason, "agent chat availability reason"); - assert.equal(agentChat.body.availability.fallback.status, "blocked"); - assert.equal(agentChat.body.availability.fallback.reason, "provider_unavailable"); - assert.equal(agentChat.body.availability.secretRefs[0].secretName, "hwlab-code-agent-provider"); - assert.equal(agentChat.body.availability.secretRefs[0].secretKey, "openai-api-key"); - assert.equal(agentChat.body.availability.secretRefs[0].redacted, true); - assert.match(agentChat.body.availability.summary, /hwlab-code-agent-provider\/openai-api-key/u); - assert.equal(JSON.stringify(agentChat.body).includes("sk-"), false); - assert.equal(Object.hasOwn(agentChat.body, "reply"), false); - - const rpcHealth = await requestJson(`${baseUrl}/rpc`, { - method: "POST", - body: JSON.stringify(rpcEnvelope({ id: "req_m1_health", method: "system.health" })) - }); - assert.equal(rpcHealth.response.status, 200); - assert.equal(rpcHealth.body.jsonrpc, "2.0"); - assert.equal(rpcHealth.body.result.status, "degraded"); - assert.equal(rpcHealth.body.result.serviceId, "hwlab-cloud-api"); - assert.equal(rpcHealth.body.result.db.status, "blocked"); - assert.equal(rpcHealth.body.meta.serviceId, "hwlab-cloud-api"); - - const projectId = "prj_mvp_topology"; - const gatewaySessionId = "gws_m1_runtime"; - const resourceId = "res_m1_runtime"; - const capabilityId = "cap_m1_runtime"; - - const rpcGateway = await requestJson(`${baseUrl}/rpc`, { - method: "POST", - body: JSON.stringify( - rpcEnvelope({ - id: "req_m1_gateway", - method: "gateway.session.register", - params: { - projectId, - gatewaySessionId, - gatewayId: "gtw_m1_runtime", - serviceId: "hwlab-gateway-simu", - endpoint: `http://127.0.0.1:${port}/gateway` - } - }) - ) - }); - assert.equal(rpcGateway.response.status, 200); - assert.equal(rpcGateway.body.result.registered, true); - - const rpcBox = await requestJson(`${baseUrl}/rpc`, { - method: "POST", - body: JSON.stringify( - rpcEnvelope({ - id: "req_m1_box", - method: "box.resource.register", - params: { - projectId, - gatewaySessionId, - resourceId, - boxId: "box_m1_runtime", - resourceType: "board" - } - }) - ) - }); - assert.equal(rpcBox.response.status, 200); - assert.equal(rpcBox.body.result.registered, true); - - const rpcCapability = await requestJson(`${baseUrl}/rpc`, { - method: "POST", - body: JSON.stringify( - rpcEnvelope({ - id: "req_m1_capability", - method: "box.capability.report", - params: { - capabilityId, - resourceId, - projectId, - name: "shell.exec", - direction: "bidirectional", - valueType: "object", - mutatesState: true - } - }) - ) - }); - assert.equal(rpcCapability.response.status, 200); - assert.equal(rpcCapability.body.result.reported, true); - - const rpcHardware = await requestJson(`${baseUrl}/rpc`, { - method: "POST", - body: JSON.stringify( - rpcEnvelope({ - id: "req_m1_hardware", - method: "hardware.operation.request", - params: { - projectId, - gatewaySessionId, - resourceId, - capabilityId, - input: { - command: "echo m1" - } - } - }) - ) - }); - assert.equal(rpcHardware.response.status, 200); - assert.equal(rpcHardware.body.result.accepted, true); - assert.equal(rpcHardware.body.result.status, "accepted"); - assert.equal(rpcHardware.body.result.auditEvent.serviceId, "hwlab-cloud-api"); - assert.equal(rpcHardware.body.result.persistence.durable, false); - - logOk("cloud-api health/live/rpc runtime"); -} - -async function smokePatchPanelAndSimulators() { - const gatewayPort = await freePort(); - const boxPort = await freePort(); - const patchPort = await freePort(); - - const gateway = startNode("cmd/hwlab-gateway-simu/main.mjs", { - PORT: String(gatewayPort), - HWLAB_GATEWAY_ID: "gwsimu_m1", - HWLAB_BOX_RESOURCES: "res_boxsim_alpha,res_boxsim_beta" - }); - const box = startNode("cmd/hwlab-box-simu/main.mjs", { - PORT: String(boxPort), - HWLAB_BOX_ID: "boxsim_alpha" - }); - const patchPanel = startNode("cmd/hwlab-patch-panel/main.mjs", { - PORT: String(patchPort), - HWLAB_WIRING_CONFIG_PATH: path.join(repoRoot, "fixtures/mvp-topology/topology.json") - }); - - const gatewayBase = `http://127.0.0.1:${gatewayPort}`; - const boxBase = `http://127.0.0.1:${boxPort}`; - const patchBase = `http://127.0.0.1:${patchPort}`; - - await Promise.all([ - waitForHttp(`${gatewayBase}/health/live`, gateway), - waitForHttp(`${boxBase}/health/live`, box), - waitForHttp(`${patchBase}/health/live`, patchPanel) - ]); - - const gatewayStatus = await requestJson(`${gatewayBase}/status`); - assert.equal(gatewayStatus.response.status, 200); - assert.equal(gatewayStatus.body.serviceId, "hwlab-gateway-simu"); - assert.equal(gatewayStatus.body.gatewayId, "gwsimu_m1"); - assert.equal(gatewayStatus.body.live, true); - assert.deepEqual(gatewayStatus.body.boxes, ["res_boxsim_alpha", "res_boxsim_beta"]); - - const boxStatus = await requestJson(`${boxBase}/status`); - assert.equal(boxStatus.response.status, 200); - assert.equal(boxStatus.body.serviceId, "hwlab-box-simu"); - assert.equal(boxStatus.body.boxId, "boxsim_alpha"); - assert.equal(boxStatus.body.live, true); - assert.equal(boxStatus.body.constraints.crossDevicePropagation, "patch-panel-only"); - assert.equal(boxStatus.body.constraints.localLoopbackEnabled, false); - assert.ok(boxStatus.body.ports.uart0); - - const writePort = await requestJson(`${boxBase}/ports/write`, { - method: "POST", - body: JSON.stringify({ - port: "uart0", - value: "m1-smoke", - source: "local" - }) - }); - assert.equal(writePort.response.status, 200); - assert.equal(writePort.body.accepted, true); - assert.equal(writePort.body.crossDevicePropagation, "patch-panel-only"); - - const patchStatus = await requestJson(`${patchBase}/status`); - assert.equal(patchStatus.response.status, 200); - assert.equal(patchStatus.body.serviceId, "hwlab-patch-panel"); - assert.equal(patchStatus.body.state, "active"); - assert.equal(patchStatus.body.metadata.propagation, "patch-panel-only"); - assert.equal(patchStatus.body.activeConnections.length, 2); - - const routed = await requestJson(`${patchBase}/signals/route`, { - method: "POST", - body: JSON.stringify({ - fromResourceId: "res_boxsim_alpha", - fromPort: "uart0", - value: "m1-smoke" - }) - }); - assert.equal(routed.response.status, 200); - assert.equal(routed.body.accepted, true); - assert.equal(routed.body.propagatedBy, "hwlab-patch-panel"); - assert.equal(routed.body.deliveryCount, 1); - assert.deepEqual(routed.body.deliveries[0], { - resourceId: "res_boxsim_beta", - port: "uart0", - value: "m1-smoke", - sourceResourceId: "res_boxsim_alpha", - sourcePort: "uart0", - observedAt: routed.body.deliveries[0].observedAt - }); - - logOk("patch-panel/sim status and route runtime"); -} - -async function smokeAgentMgrLocalContract() { - const stateDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-m1-agent-mgr-")); - const agentSessionId = "agt_m1_local_contract"; - const agentMgrPort = await freePort(); - let agentMgr; - - try { - agentMgr = startNode("cmd/hwlab-agent-mgr/main.ts", { - PORT: String(agentMgrPort), - HWLAB_AGENT_RUNTIME_STATE_DIR: stateDir, - HWLAB_ENVIRONMENT: ENVIRONMENT_DEV - }); - const baseUrl = `http://127.0.0.1:${agentMgrPort}`; - await waitForHttp(`${baseUrl}/health/live`, agentMgr); - - const healthLive = await requestJson(`${baseUrl}/health/live`); - assert.equal(healthLive.response.status, 200); - assert.equal(healthLive.body.serviceId, "hwlab-agent-mgr"); - assert.equal(healthLive.body.ok, true); - assert.equal(healthLive.body.environment, ENVIRONMENT_DEV); - assert.equal(healthLive.body.health.serviceId, "hwlab-agent-mgr"); - assert.equal(healthLive.body.health.status, "degraded"); - assert.equal(healthLive.body.health.runtimeMode, "local-dry-run"); - assert.ok(healthLive.body.health.skills.missing.includes("manager.skillCommitId")); - assert.ok(healthLive.body.health.skills.missing.includes("manager.skillVersion")); - assert.equal(healthLive.body.skillsManifest, null); - - const health = await runNodeJson("cmd/hwlab-agent-mgr/main.ts", [ - "health", - "--state-dir", - stateDir, - "--skill-commit-id", - "local-contract", - "--skill-version", - "m1-local" - ]); - assert.equal(health.serviceId, "hwlab-agent-mgr"); - assert.equal(health.ok, true); - assert.equal(health.health.serviceId, "hwlab-agent-mgr"); - assert.equal(health.health.status, "ok"); - assert.equal(health.health.environment, ENVIRONMENT_DEV); - assert.equal(health.health.runtimeMode, "local-dry-run"); - assert.equal(health.health.workspaceCleaned, false); - assert.equal(health.health.skills.manager.status, "ready"); - assert.equal(health.skillsManifest.services[0].serviceId, "hwlab-agent-skills"); - assert.equal(health.skillsManifest.environment, ENVIRONMENT_DEV); - - const created = await runNodeJson("cmd/hwlab-agent-mgr/main.ts", [ - "create", - "--state-dir", - stateDir, - "--agent-session-id", - agentSessionId, - "--project-id", - "prj_m1_local_contract", - "--goal", - "verify M1 local agent manager contract", - "--prompt", - "local contract sample", - "--skill-commit-id", - "local-contract", - "--skill-version", - "m1-local" - ]); - assert.equal(created.ok, true); - assert.equal(created.serviceId, "hwlab-agent-mgr"); - assert.equal(created.agentSessionId, agentSessionId); - assert.equal(created.status, "ok"); - assert.equal(created.environment, ENVIRONMENT_DEV); - assert.equal(created.runtimeMode, "local-dry-run"); - assert.equal(created.agentSession.serviceId, "hwlab-agent-mgr"); - assert.equal(created.agentSession.projectId, "prj_m1_local_contract"); - assert.equal(created.task.status, "queued"); - assert.equal(created.traceCount, 1); - assert.equal(created.evidenceCount, 0); - - const status = await runNodeJson("cmd/hwlab-agent-mgr/main.ts", [ - "status", - "--state-dir", - stateDir, - "--agent-session-id", - agentSessionId - ]); - assert.equal(status.ok, true); - assert.equal(status.serviceId, "hwlab-agent-mgr"); - assert.equal(status.agentSessionId, agentSessionId); - assert.equal(status.status, "ok"); - assert.equal(status.task.taskId, `tsk_${agentSessionId}`); - - const cleanup = await runNodeJson("cmd/hwlab-agent-mgr/main.ts", [ - "cleanup", - "--state-dir", - stateDir, - "--agent-session-id", - agentSessionId - ]); - assert.equal(cleanup.ok, true); - assert.equal(cleanup.serviceId, "hwlab-agent-mgr"); - assert.equal(cleanup.agentSessionId, agentSessionId); - assert.equal(cleanup.status, "ok"); - assert.equal(cleanup.cleanup.completed, true); - assert.equal(cleanup.workspace.existsAfterCleanup, false); - - logOk("agent-mgr health/sample local contract"); - } finally { - if (agentMgr) { - await stopNode(agentMgr); - } - await rm(stateDir, { recursive: true, force: true }); - } -} - -try { - await smokeCloudApi(); - await smokePatchPanelAndSimulators(); - await smokeAgentMgrLocalContract(); - process.stdout.write("[m1-smoke] passed\n"); -} finally { - await Promise.allSettled(children.map(stopNode)); -} diff --git a/scripts/m4-agent-loop-smoke.mjs b/scripts/m4-agent-loop-smoke.mjs deleted file mode 100644 index c5c88857..00000000 --- a/scripts/m4-agent-loop-smoke.mjs +++ /dev/null @@ -1,299 +0,0 @@ -#!/usr/bin/env node -import assert from "node:assert/strict"; -import { createHash } from "node:crypto"; -import { mkdtemp, readFile, rm } from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -import { - AGENT_MGR_SERVICE_ID, - AGENT_SESSION_STATUSES, - AGENT_SKILLS_SERVICE_ID, - AGENT_WORKER_SERVICE_ID, - WORKER_SESSION_STATUSES, - createAgentRunPlan, - createSkillsManifest, - finalizeAgentRun -} from "../internal/agent/index.mjs"; -import { - createLocalAgentSession, - getLocalAgentEvidence, - getLocalAgentStatus, - getLocalAgentTrace, - runLocalWorkerDryRun -} from "../internal/agent/runtime.mjs"; - -const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const fixedNow = () => "2026-05-21T00:00:00.000Z"; - -async function readJSON(relativePath) { - const raw = await readFile(path.join(repoRoot, relativePath), "utf8"); - return JSON.parse(raw); -} - -async function readText(relativePath) { - return readFile(path.join(repoRoot, relativePath), "utf8"); -} - -function sha256Hex(value) { - return createHash("sha256").update(String(value)).digest("hex"); -} - -function expectLifecycleConstants() { - assert.deepEqual(AGENT_SESSION_STATUSES, ["create", "start", "trace", "finish", "cleanup"]); - assert.deepEqual(WORKER_SESSION_STATUSES, ["create", "start", "trace", "finish", "cleanup"]); -} - -function expectRunPlan(plan, fixture) { - const workerSessionId = `wkr_${fixture.agentSessionId}`; - const traceId = `trc_${fixture.agentSessionId}`; - const operationId = `op_${fixture.agentSessionId}`; - const evidenceId = `evi_${fixture.agentSessionId}`; - const workspaceVolumeId = `vol_${fixture.agentSessionId}`; - const mountPath = `/workspace/${fixture.projectId}/${fixture.agentSessionId}`; - const evidenceUri = `file://${path.posix.join(mountPath, "evidence.txt")}`; - - assert.equal(plan.skillCommitId, fixture.skillCommitId); - assert.equal(plan.skillVersion, fixture.skillVersion); - assert.equal(plan.agentSession.serviceId, AGENT_MGR_SERVICE_ID); - assert.equal(plan.workerSession.serviceId, AGENT_WORKER_SERVICE_ID); - assert.equal(plan.agentSession.agentSessionId, fixture.agentSessionId); - assert.equal(plan.agentSession.projectId, fixture.projectId); - assert.equal(plan.agentSession.environment, fixture.environment); - assert.equal(plan.agentSession.status, "start"); - assert.equal(plan.agentSession.startedAt, fixedNow()); - assert.equal(plan.agentSession.updatedAt, fixedNow()); - assert.equal(plan.agentSession.metadata.currentState, "start"); - assert.equal(plan.agentSession.metadata.workerSessionId, workerSessionId); - assert.equal(plan.agentSession.metadata.workspaceVolumeId, workspaceVolumeId); - assert.equal(plan.workerSession.workerSessionId, workerSessionId); - assert.equal(plan.workerSession.agentSessionId, fixture.agentSessionId); - assert.equal(plan.workerSession.projectId, fixture.projectId); - assert.equal(plan.workerSession.environment, fixture.environment); - assert.equal(plan.workerSession.status, "start"); - assert.equal(plan.workerSession.startedAt, fixedNow()); - assert.equal(plan.workerSession.updatedAt, fixedNow()); - assert.equal(plan.workerSession.metadata.currentState, "start"); - assert.equal(plan.workerSession.metadata.workspaceVolumeId, workspaceVolumeId); - assert.equal(plan.workspaceVolume.workspaceVolumeId, workspaceVolumeId); - assert.equal(plan.workspaceVolume.mountPath, mountPath); - assert.equal(plan.workspaceVolume.serviceId, AGENT_WORKER_SERVICE_ID); - assert.equal(plan.workspaceVolume.lifecycle, "create"); - assert.equal(plan.workspaceVolume.createdAt, fixedNow()); - assert.equal(plan.workspaceVolume.updatedAt, fixedNow()); - assert.deepEqual( - plan.events.map((event) => event.state), - ["create", "start", "trace", "finish", "cleanup"] - ); - assert.equal(plan.events[0].actor, AGENT_MGR_SERVICE_ID); - assert.equal(plan.events[0].agentSessionId, fixture.agentSessionId); - assert.equal(plan.events[1].actor, AGENT_WORKER_SERVICE_ID); - assert.equal(plan.events[1].workerSessionId, workerSessionId); - assert.equal(plan.events[2].actor, AGENT_MGR_SERVICE_ID); - assert.equal(plan.events[2].traceEvent.traceEventId, `tev_${fixture.agentSessionId}`); - assert.equal(plan.events[2].traceEvent.traceId, traceId); - assert.equal(plan.events[2].traceEvent.projectId, fixture.projectId); - assert.equal(plan.events[2].traceEvent.serviceId, AGENT_MGR_SERVICE_ID); - assert.equal(plan.events[2].traceEvent.level, "info"); - assert.equal(plan.events[2].traceEvent.message, fixture.prompt); - assert.equal(plan.events[2].traceEvent.environment, fixture.environment); - assert.equal(plan.events[2].traceEvent.agentSessionId, fixture.agentSessionId); - assert.equal(plan.events[2].traceEvent.workerSessionId, workerSessionId); - assert.equal(plan.events[2].traceEvent.operationId, operationId); - assert.deepEqual(plan.events[2].traceEvent.metadata, {}); - assert.equal(plan.traceEvent.traceId, traceId); - assert.equal(plan.traceEvent.message, fixture.prompt); - assert.equal(plan.traceEvent.occurredAt, fixedNow()); - assert.equal(plan.events[3].actor, AGENT_WORKER_SERVICE_ID); - assert.equal(plan.events[3].evidenceRecord.evidenceId, evidenceId); - assert.equal(plan.events[4].actor, AGENT_WORKER_SERVICE_ID); - assert.equal(plan.events[4].workspaceVolumeId, workspaceVolumeId); - assert.equal(plan.evidenceRecord.evidenceId, evidenceId); - assert.equal(plan.evidenceRecord.projectId, fixture.projectId); - assert.equal(plan.evidenceRecord.operationId, operationId); - assert.equal(plan.evidenceRecord.agentSessionId, fixture.agentSessionId); - assert.equal(plan.evidenceRecord.workerSessionId, workerSessionId); - assert.equal(plan.evidenceRecord.kind, "artifact"); - assert.equal(plan.evidenceRecord.uri, evidenceUri); - assert.equal(plan.evidenceRecord.mimeType, "text/plain"); - assert.equal(plan.evidenceRecord.sha256, sha256Hex(evidenceUri)); - assert.equal(plan.evidenceRecord.sizeBytes, 0); - assert.equal(plan.evidenceRecord.serviceId, AGENT_WORKER_SERVICE_ID); - assert.equal(plan.evidenceRecord.environment, fixture.environment); - assert.equal(plan.evidenceRecord.createdAt, fixedNow()); - assert.equal(plan.evidenceRecord.metadata.skillCommitId, fixture.skillCommitId); - assert.equal(plan.evidenceRecord.metadata.skillVersion, fixture.skillVersion); - assert.equal(plan.evidenceRecord.metadata.prompt, fixture.prompt); - assert.equal(plan.evidenceRecord.metadata.goal, fixture.goal); - assert.equal(plan.evidenceRecord.metadata.workspaceVolumeId, workspaceVolumeId); -} - -function expectFinishedRun(finished, originalPlan) { - assert.equal(finished.agentSession.status, "cleanup"); - assert.equal(finished.workerSession.status, "cleanup"); - assert.equal(finished.agentSession.completedAt, fixedNow()); - assert.equal(finished.workerSession.completedAt, fixedNow()); - assert.equal(finished.agentSession.metadata.cleanedUp, true); - assert.equal(finished.workerSession.metadata.cleanedUp, true); - assert.equal(finished.cleanup.workspaceVolumeId, originalPlan.workspaceVolume.workspaceVolumeId); - assert.equal(finished.cleanup.removed, true); - assert.equal(finished.evidenceRecord.sha256, originalPlan.evidenceRecord.sha256); - assert.equal(originalPlan.agentSession.status, "start"); - assert.equal(originalPlan.workerSession.status, "start"); -} - -function expectSkillsManifest(manifest, fixture) { - assert.equal(manifest.manifestVersion, "v1"); - assert.equal(manifest.environment, fixture.environment); - assert.equal(manifest.commitId, fixture.skillCommitId); - assert.equal(manifest.version, fixture.skillVersion); - assert.equal(manifest.namespace, "hwlab"); - assert.equal(manifest.endpoint, "http://74.48.78.17:16667"); - assert.equal(manifest.profiles.dev.enabled, true); - assert.equal(manifest.profiles.dev.namespace, "hwlab-dev"); - assert.equal(manifest.services[0].serviceId, AGENT_SKILLS_SERVICE_ID); - assert.equal(manifest.services[0].image, `hwlab-agent-runtime:${fixture.skillCommitId.slice(0, 7)}`); - assert.equal(manifest.services[0].env.HWLAB_SKILLS_COMMIT_ID, fixture.skillCommitId); - assert.equal(manifest.services[0].env.HWLAB_SKILLS_VERSION, fixture.skillVersion); - assert.deepEqual(manifest.skills, []); -} - -function expectWorkspaceIsolation(primaryPlan, secondaryPlan, fixture) { - const primaryVolumeId = primaryPlan.workspaceVolume.workspaceVolumeId; - const secondaryVolumeId = secondaryPlan.workspaceVolume.workspaceVolumeId; - const secondaryMountPath = `/workspace/${fixture.workspaceIsolation.projectId}/${fixture.workspaceIsolation.agentSessionId}`; - const secondaryEvidenceUri = `file://${path.posix.join(secondaryMountPath, "evidence.txt")}`; - - assert.notEqual(primaryVolumeId, secondaryVolumeId); - assert.notEqual(primaryPlan.workspaceVolume.mountPath, secondaryPlan.workspaceVolume.mountPath); - assert.equal(secondaryPlan.workspaceVolume.mountPath, secondaryMountPath); - assert.equal(secondaryPlan.evidenceRecord.uri, secondaryEvidenceUri); - assert.equal(secondaryPlan.evidenceRecord.metadata.workspaceVolumeId, secondaryVolumeId); - assert.equal(secondaryPlan.traceEvent.traceId, `trc_${fixture.workspaceIsolation.agentSessionId}`); -} - -function expectEvidenceFixture(text, fixture) { - assert.match(text, /^# M4 Agent Automation Loop Evidence Plan/m); - assert.ok(text.includes(`- Scenario: ${fixture.scenario}`)); - assert.ok(text.includes(`- Environment: ${fixture.environment}`)); - assert.ok(text.includes(`- Project: ${fixture.projectId}`)); - assert.ok(text.includes(`- Skill commitId: ${fixture.skillCommitId}`)); - assert.ok(text.includes(`- Skill version: ${fixture.skillVersion}`)); - assert.ok(text.includes("no secrets")); - assert.ok(text.includes("no live DEV agent run")); -} - -async function expectLocalRuntimeClosedLoop(fixture) { - const stateDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-m4-agent-loop-")); - try { - const created = await createLocalAgentSession({ - stateDir, - agentSessionId: fixture.agentSessionId, - projectId: fixture.projectId, - goal: fixture.goal, - prompt: fixture.prompt, - skillCommitId: fixture.skillCommitId, - skillVersion: fixture.skillVersion, - now: fixedNow - }); - assert.equal(created.health.status, "ok"); - assert.equal(created.task.status, "queued"); - assert.equal(created.traceCount, 1); - - const worker = await runLocalWorkerDryRun({ - stateDir, - agentSessionId: fixture.agentSessionId, - skillCommitId: fixture.skillCommitId, - skillVersion: fixture.skillVersion, - now: fixedNow - }); - assert.equal(worker.health.status, "ok"); - assert.equal(worker.task.status, "completed"); - assert.equal(worker.agentSession.status, "cleanup"); - assert.equal(worker.workerSession.status, "cleanup"); - assert.equal(worker.cleanup.removed, true); - assert.equal(worker.workspace.existsAfterCleanup, false); - assert.equal(worker.evidenceRecord.metadata.health.status, "ok"); - assert.equal(worker.evidenceRecord.metadata.skills.manager.commitId, fixture.skillCommitId); - assert.equal(worker.evidenceRecord.metadata.skills.worker.version, fixture.skillVersion); - - const status = await getLocalAgentStatus({ - stateDir, - agentSessionId: fixture.agentSessionId - }); - assert.equal(status.evidenceCount, 1); - assert.equal(status.cleanup.removed, true); - - const trace = await getLocalAgentTrace({ - stateDir, - agentSessionId: fixture.agentSessionId - }); - assert.deepEqual( - trace.traceEvents.map((event) => event.metadata.state), - ["create", "start", "trace", "finish", "cleanup"] - ); - - const evidence = await getLocalAgentEvidence({ - stateDir, - agentSessionId: fixture.agentSessionId - }); - assert.equal(evidence.status, "ok"); - assert.equal(evidence.evidenceRecords.length, 1); - assert.equal(evidence.evidenceRecords[0].metadata.traceId, `trc_${fixture.agentSessionId}`); - } finally { - await rm(stateDir, { recursive: true, force: true }); - } -} - -const fixture = await readJSON("fixtures/mvp/m4-agent-loop/runtime.json"); -const evidenceFixture = await readText(fixture.evidencePath); - -assert.equal(fixture.environment, "dev"); -assert.equal(fixture.projectId.startsWith("prj_"), true); -assert.equal(fixture.agentSessionId.startsWith("agt_"), true); -assert.equal(fixture.skillCommitId.length >= 7, true); -assert.equal(fixture.skillVersion.length > 0, true); -assert.equal(fixture.workspaceIsolation.projectId.startsWith("prj_"), true); -assert.equal(fixture.workspaceIsolation.agentSessionId.startsWith("agt_"), true); - -expectLifecycleConstants(); -expectEvidenceFixture(evidenceFixture, fixture); - -const plan = createAgentRunPlan({ - agentSessionId: fixture.agentSessionId, - projectId: fixture.projectId, - goal: fixture.goal, - prompt: fixture.prompt, - skillCommitId: fixture.skillCommitId, - skillVersion: fixture.skillVersion, - now: fixedNow -}); - -expectRunPlan(plan, fixture); - -const finished = finalizeAgentRun(plan, { now: fixedNow }); - -expectFinishedRun(finished, plan); - -const manifest = createSkillsManifest({ - commitId: fixture.skillCommitId, - version: fixture.skillVersion -}); - -expectSkillsManifest(manifest, fixture); - -const isolationPlan = createAgentRunPlan({ - agentSessionId: fixture.workspaceIsolation.agentSessionId, - projectId: fixture.workspaceIsolation.projectId, - goal: fixture.goal, - prompt: fixture.prompt, - skillCommitId: fixture.skillCommitId, - skillVersion: fixture.skillVersion, - now: fixedNow -}); - -expectWorkspaceIsolation(plan, isolationPlan, fixture); -await expectLocalRuntimeClosedLoop(fixture); - -console.log(`[m4-smoke] ok ${fixture.scenario}`); diff --git a/scripts/refresh-artifact-catalog.mjs b/scripts/refresh-artifact-catalog.mjs index 4acf0fba..655fd9e5 100644 --- a/scripts/refresh-artifact-catalog.mjs +++ b/scripts/refresh-artifact-catalog.mjs @@ -29,8 +29,6 @@ const v02EnvReuseRuntimeMode = "env-reuse-git-mirror-checkout"; const v02RuntimeServiceIds = Object.freeze([ "hwlab-cloud-api", "hwlab-cloud-web", - "hwlab-agent-mgr", - "hwlab-agent-worker", "hwlab-device-pod", "hwlab-gateway", "hwlab-edge-proxy", diff --git a/scripts/refresh-artifact-catalog.test.mjs b/scripts/refresh-artifact-catalog.test.mjs index 356e80e1..1d46b96e 100644 --- a/scripts/refresh-artifact-catalog.test.mjs +++ b/scripts/refresh-artifact-catalog.test.mjs @@ -13,8 +13,6 @@ const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), " const V02_SERVICE_IDS = Object.freeze([ "hwlab-cloud-api", "hwlab-cloud-web", - "hwlab-agent-mgr", - "hwlab-agent-worker", "hwlab-device-pod", "hwlab-gateway", "hwlab-edge-proxy", diff --git a/scripts/src/artifact-runtime-readiness-guard.mjs b/scripts/src/artifact-runtime-readiness-guard.mjs index 924d13c5..7ed5a78b 100644 --- a/scripts/src/artifact-runtime-readiness-guard.mjs +++ b/scripts/src/artifact-runtime-readiness-guard.mjs @@ -1236,7 +1236,6 @@ function nextCommands({ canPublish, canApply, blockedReasons }) { return [ "node scripts/dev-edge-health-smoke.mjs --live", "node scripts/dev-m3-hardware-loop-smoke.mjs --dry-run", - "node scripts/dev-m4-agent-loop-smoke.mjs --live --confirm-dev --confirmed-non-production", guardCommand ]; } diff --git a/scripts/src/check-plan.mjs b/scripts/src/check-plan.mjs index 82bc276a..1c1979a7 100644 --- a/scripts/src/check-plan.mjs +++ b/scripts/src/check-plan.mjs @@ -30,8 +30,7 @@ export const checkProfiles = Object.freeze({ { id: "check-008-repo-report-paths", group: "repo", command: ["node","--check","scripts/src/report-paths.mjs"] }, { id: "check-009-repo-index", group: "repo", command: ["node","--check","internal/protocol/index.mjs"] }, { id: "check-010-repo-build-metadata", group: "repo", command: ["node","--check","internal/build-metadata.mjs"] }, - { id: "check-011-repo-index", group: "repo", command: ["node","--check","internal/agent/index.mjs"] }, - { id: "check-012-repo-runtime", group: "repo", command: ["node","--check","internal/agent/runtime.mjs"] }, + { id: "check-011-repo-agentrun-dispatch", group: "repo", command: ["node","--check","internal/agent/agentrun-dispatch.mjs"] }, { id: "check-013-repo-index", group: "repo", command: ["node","--check","internal/audit/index.mjs"] }, { id: "check-014-cloud-api-run-bun", group: "cloud-api", command: ["node","scripts/run-bun.mjs","build","internal/db/runtime-store.ts","--target=bun","--packages=external","--outdir=/tmp/hwlab-ts-check"] }, { id: "check-015-cloud-api-run-bun", group: "cloud-api", command: ["node","scripts/run-bun.mjs","build","internal/db/runtime-store.test.ts","--target=bun","--packages=external","--outdir=/tmp/hwlab-ts-check"] }, @@ -69,7 +68,7 @@ export const checkProfiles = Object.freeze({ { id: "check-045-cloud-api-run-bun", group: "cloud-api", command: ["node","scripts/run-bun.mjs","build","cmd/hwlab-cloud-api/main.ts","--target=bun","--packages=external","--outdir=/tmp/hwlab-ts-check"] }, { id: "check-046-cloud-api-run-bun", group: "cloud-api", command: ["node","scripts/run-bun.mjs","build","cmd/hwlab-cloud-api/provision.ts","--target=bun","--packages=external","--outdir=/tmp/hwlab-ts-check"] }, { id: "check-047-cloud-api-run-bun", group: "cloud-api", command: ["node","scripts/run-bun.mjs","build","cmd/hwlab-cloud-api/migrate.ts","--target=bun","--packages=external","--outdir=/tmp/hwlab-ts-check"] }, - { id: "check-048-runtime-services-run-bun", group: "runtime-services", command: ["node","scripts/run-bun.mjs","test","cmd/hwlab-edge-proxy/main.test.ts","cmd/hwlab-device-pod/main.test.ts","cmd/hwlab-gateway/main.test.ts","cmd/hwlab-agent-mgr/main.test.ts","cmd/hwlab-agent-worker/main.test.ts","cmd/hwlab-codex-api-responses-forwarder/main.test.ts","cmd/hwlab-deepseek-responses-bridge/main.test.ts"] }, + { id: "check-048-runtime-services-run-bun", group: "runtime-services", command: ["node","scripts/run-bun.mjs","test","cmd/hwlab-edge-proxy/main.test.ts","cmd/hwlab-device-pod/main.test.ts","cmd/hwlab-gateway/main.test.ts","cmd/hwlab-codex-api-responses-forwarder/main.test.ts","cmd/hwlab-deepseek-responses-bridge/main.test.ts"] }, { id: "check-056-repo-main", group: "repo", command: ["node","scripts/run-bun.mjs","build","cmd/hwlab-gateway/main.ts","cmd/hwlab-gateway/main.test.ts","--target=bun","--packages=external","--outdir=/tmp/hwlab-ts-check"] }, { id: "check-058-tools-hwlab-gateway-shell", group: "tools", command: ["node","--check","tools/hwlab-gateway-shell.mjs"] }, { id: "check-059-tools-hwlab-gateway-tran", group: "tools", command: ["node","--check","tools/hwlab-gateway-tran.mjs"] }, @@ -116,7 +115,6 @@ export const checkProfiles = Object.freeze({ { id: "check-099-smoke-dev-evidence-blocker-aggregator", group: "smoke", command: ["node","--check","scripts/dev-evidence-blocker-aggregator.mjs"] }, { id: "check-100-smoke-dev-evidence-blocker-aggregator", group: "smoke", command: ["node","--check","scripts/src/dev-evidence-blocker-aggregator.mjs"] }, { id: "check-101-smoke-dev-evidence-blocker-aggregator-test", group: "smoke", command: ["node","--check","scripts/src/dev-evidence-blocker-aggregator.test.mjs"] }, - { id: "check-102-smoke-dev-m4-agent-loop-smoke-lib-test", group: "smoke", command: ["node","--check","scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs"] }, { id: "check-103-repo-d601-k3s-readonly-observability", group: "repo", command: ["node","--check","scripts/d601-k3s-readonly-observability.mjs"] }, { id: "check-104-repo-d601-k3s-readonly-observability", group: "repo", command: ["node","--check","scripts/src/d601-k3s-readonly-observability.mjs"] }, { id: "check-105-dev-runtime-dev-runtime-provisioning", group: "dev-runtime", command: ["node","--check","scripts/dev-runtime-provisioning.mjs"] }, diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index c97b90b7..581afbfa 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -3103,7 +3103,7 @@ async function inspectLiveDom(url, options = {}) { dom.rootAfterScrollAttempt.bodyScrollTop === 0 && dom.liveBuildDefaultVisible && /最新镜像构建时间:\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} 北京时间/u.test(dom.liveBuildLatest) && - /hwlab-agent-mgr/u.test(dom.liveBuildLatest) && + /hwlab-agent-skills/u.test(dom.liveBuildLatest) && /tag f09ad05/u.test(dom.liveBuildLatest) && /commit f09ad05/u.test(dom.liveBuildLatest) && /revision f09ad05/u.test(dom.liveBuildLatest) && @@ -3118,7 +3118,7 @@ async function inspectLiveDom(url, options = {}) { dom.liveBuildDetailWhenOpen.overflowX <= 1 && /构建版本明细/u.test(dom.liveBuildDetailWhenOpen.title) && /hwlab-cloud-api/u.test(dom.liveBuildDetailWhenOpen.text) && - /hwlab-agent-worker/u.test(dom.liveBuildDetailWhenOpen.text) && + /hwlab-device-pod/u.test(dom.liveBuildDetailWhenOpen.text) && /构建时间不可用/u.test(dom.liveBuildDetailWhenOpen.text) && /外部镜像或非 HWLAB 构建产物/u.test(dom.liveBuildDetailWhenOpen.text) && /来源 live health/u.test(dom.liveBuildDetailWhenOpen.text) && @@ -5516,8 +5516,8 @@ function liveBuildsFixturePayload() { healthPath: "/health/live" }, latest: { - serviceId: "hwlab-agent-mgr", - name: "hwlab-agent-mgr", + serviceId: "hwlab-agent-skills", + name: "hwlab-agent-skills", kind: "hwlab", status: "ok", build: { @@ -5526,7 +5526,7 @@ function liveBuildsFixturePayload() { liveHealthMissingReason: "health payload 缺少 build.createdAt / image.createdAt / buildCreatedAt,已使用与 live tag/revision 匹配的 catalog metadata" }, image: { - reference: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:f09ad05", + reference: "127.0.0.1:5000/hwlab/hwlab-agent-skills:f09ad05", tag: "f09ad05", digest: "sha256:" + "1".repeat(64) }, @@ -5565,8 +5565,8 @@ function liveBuildsFixturePayload() { revision: "f09ad05dec5f2bbca12ae661b140a87dfcbf54a4" }, { - serviceId: "hwlab-agent-mgr", - name: "hwlab-agent-mgr", + serviceId: "hwlab-agent-skills", + name: "hwlab-agent-skills", kind: "hwlab", status: "ok", build: { @@ -5574,7 +5574,7 @@ function liveBuildsFixturePayload() { metadataSource: "runtime-env:HWLAB_BUILD_CREATED_AT" }, image: { - reference: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:f09ad05", + reference: "127.0.0.1:5000/hwlab/hwlab-agent-skills:f09ad05", tag: "f09ad05", digest: "sha256:" + "1".repeat(64) }, @@ -5606,14 +5606,14 @@ function liveBuildsFixturePayload() { revision: "unknown" }, { - serviceId: "hwlab-agent-worker", - name: "hwlab-agent-worker", + serviceId: "hwlab-device-pod", + name: "hwlab-device-pod", kind: "hwlab", status: "unavailable", build: { createdAt: null, metadataSource: "unavailable", - unavailableReason: "构建时间不可用:hwlab-agent-worker 是 suspended Job template,当前 live desired replicas=0" + unavailableReason: "构建时间不可用:当前 live health 不可用" }, image: { reference: "unknown", diff --git a/scripts/src/dev-edge-health-smoke-lib.mjs b/scripts/src/dev-edge-health-smoke-lib.mjs index 5b6f18b7..d52e2592 100644 --- a/scripts/src/dev-edge-health-smoke-lib.mjs +++ b/scripts/src/dev-edge-health-smoke-lib.mjs @@ -183,9 +183,7 @@ async function createDevGateReport(edgeHealth) { ], localSmoke: { status: "pass", - commands: [ - "node scripts/m1-contract-smoke.mjs" - ], + commands: [], evidence: [ "npm run check includes internal/cloud/server-health.test.ts and validates /health plus /health/live." ], diff --git a/scripts/src/dev-evidence-blocker-aggregator.mjs b/scripts/src/dev-evidence-blocker-aggregator.mjs index 3936e785..1ebd6e02 100644 --- a/scripts/src/dev-evidence-blocker-aggregator.mjs +++ b/scripts/src/dev-evidence-blocker-aggregator.mjs @@ -317,9 +317,9 @@ function buildEvidence() { category: "local-smoke", level: "LOCAL", status: "pass", - sources: ["scripts/m1-contract-smoke.mjs"], - commands: ["node scripts/m1-contract-smoke.mjs"], - summary: "Local skeleton smoke remains local-only evidence." + sources: ["scripts/validate-contract.mjs"], + commands: ["node scripts/validate-contract.mjs"], + summary: "Contract validation remains local-only source evidence." }), sourceEntry({ milestone: "M5", diff --git a/scripts/src/dev-m4-agent-loop-smoke-lib.mjs b/scripts/src/dev-m4-agent-loop-smoke-lib.mjs deleted file mode 100644 index dd273f1e..00000000 --- a/scripts/src/dev-m4-agent-loop-smoke-lib.mjs +++ /dev/null @@ -1,747 +0,0 @@ -import { execFileSync } from "node:child_process"; -import { request as httpRequest } from "node:http"; -import { request as httpsRequest } from "node:https"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -import { - AGENT_MGR_SERVICE_ID, - AGENT_SKILLS_SERVICE_ID, - AGENT_WORKER_SERVICE_ID -} from "../../internal/agent/index.mjs"; -import { - RUNTIME_DURABLE_ADAPTER_MISSING, - RUNTIME_DURABILITY_REQUIRED_EVIDENCE, - RUNTIME_STORE_KIND_POSTGRES -} from "../../internal/db/runtime-store.ts"; -import { CLOUD_CORE_MIGRATION_ID } from "../../internal/db/schema.ts"; -import { DEV_ENDPOINT, DEV_FRONTEND_ENDPOINT, ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs"; - -const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); - -export const d601Kubeconfig = "/etc/rancher/k3s/k3s.yaml"; -export const devNamespace = "hwlab-dev"; -export const d601K3sReadonlyCommand = - "KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n hwlab-dev get pods,svc,deploy,job,cm -o wide"; -export const workerServerDryRunCommand = - "KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n hwlab-dev create -f - --dry-run=server -o json"; - -export function isDbLiveReady(body) { - const db = body?.db; - return Boolean(db?.ready && db?.connected && db?.liveConnected !== false && db?.liveDbEvidence); -} - -export function isDbLiveBlocked(body) { - const db = body?.db; - if (!db || typeof db !== "object") { - return false; - } - if (isDbLiveReady(body)) { - return false; - } - return db.connected === false || - db.ready === false || - db.liveConnected === false || - db.liveDbEvidence === false || - ["blocked", "degraded", "failed"].includes(db.status); -} - -export function summarizeDbBlocker(body) { - const db = body?.db ?? {}; - const missing = Array.isArray(db.missingEnv) && db.missingEnv.length > 0 - ? ` missing ${db.missingEnv.join(", ")}` - : ""; - return `cloud-api /health/live reports DB ${db.status ?? "not ready"}; connected=${db.connected ?? "unknown"}; ready=${db.ready ?? "unknown"}; liveDbEvidence=${db.liveDbEvidence ?? "unknown"}.${missing}`; -} - -export function runtimeDurabilityFromHealth(body) { - const runtime = body?.runtime ?? {}; - const durability = body?.readiness?.durability ?? {}; - const migration = runtime.migration ?? {}; - const migrationGate = runtime.gates?.migration ?? durability.gates?.migration ?? {}; - const blocker = runtime.blocker ?? - durability.blocker ?? - (Array.isArray(body?.blockerCodes) - ? body.blockerCodes.find((code) => String(code).startsWith("runtime_durable_adapter_")) - : null) ?? - RUNTIME_DURABLE_ADAPTER_MISSING; - const durableRequested = Boolean( - runtime.durableRequested || - durability.durableRequested || - runtime.adapter === RUNTIME_STORE_KIND_POSTGRES || - durability.adapter === RUNTIME_STORE_KIND_POSTGRES - ); - const ready = Boolean( - runtime.durable === true && - runtime.ready === true && - runtime.liveRuntimeEvidence === true && - durability.ready !== false - ); - - return { - adapter: runtime.adapter ?? durability.adapter ?? "unknown", - durable: Boolean(runtime.durable), - durableRequested, - ready, - runtimeStatus: runtime.status ?? durability.runtimeStatus ?? "unknown", - blocker, - blockedLayer: runtime.durabilityContract?.blockedLayer ?? durability.blockedLayer ?? null, - requiredEvidence: runtime.durabilityContract?.requiredEvidence ?? - durability.requiredEvidence ?? - RUNTIME_DURABILITY_REQUIRED_EVIDENCE, - migration: { - checked: migration.checked ?? migrationGate.checked ?? false, - ready: migration.ready ?? migrationGate.ready ?? false, - missing: migration.missing ?? (migration.ready === false ? true : null), - requiredMigrationId: migration.requiredMigrationId ?? CLOUD_CORE_MIGRATION_ID, - appliedMigrationId: migration.appliedMigrationId ?? null - }, - queryAttempted: Boolean(runtime.connection?.queryAttempted ?? durability.queryAttempted), - queryResult: runtime.connection?.queryResult ?? durability.queryResult ?? "unknown" - }; -} - -export function isRuntimeDurableAdapterBlocked(body) { - const runtime = runtimeDurabilityFromHealth(body); - if (!runtime.durableRequested) { - return false; - } - return !runtime.ready || - ["blocked", "degraded", "failed"].includes(runtime.runtimeStatus) || - String(runtime.blocker).startsWith("runtime_durable_adapter_"); -} - -export function summarizeRuntimeDurableBlocker(body) { - const runtime = runtimeDurabilityFromHealth(body); - const migration = runtime.migration; - return `cloud-api durable runtime adapter is blocked: ${runtime.blocker}; adapter=${runtime.adapter}; durable=${runtime.durable}; ready=${runtime.ready}; migration=${migration.requiredMigrationId} checked=${migration.checked} ready=${migration.ready} missing=${migration.missing ?? "unknown"}; queryResult=${runtime.queryResult}.`; -} - -export function publicDbLiveSummary(body) { - const db = body?.db ?? null; - if (!db || typeof db !== "object") { - return null; - } - return { - status: db.status ?? null, - connected: db.connected ?? null, - liveConnected: db.liveConnected ?? null, - ready: db.ready ?? null, - configReady: db.configReady ?? null, - connectionChecked: db.connectionChecked ?? null, - connectionAttempted: db.connectionAttempted ?? null, - connectionResult: db.connectionResult ?? null, - endpointSource: db.endpointSource ?? db.connection?.endpointSource ?? null, - liveDbEvidence: db.liveDbEvidence ?? null, - evidence: db.evidence ?? null - }; -} - -export function publicRuntimeDurabilitySummary(body) { - if (!body?.runtime && !body?.readiness?.durability) { - return null; - } - const runtime = runtimeDurabilityFromHealth(body); - return { - adapter: runtime.adapter, - durable: runtime.durable, - durableRequested: runtime.durableRequested, - ready: runtime.ready, - status: runtime.runtimeStatus, - blocker: runtime.blocker, - blockedLayer: runtime.blockedLayer, - requiredEvidence: runtime.requiredEvidence, - liveRuntimeEvidence: Boolean(runtime.ready), - migration: runtime.migration, - queryAttempted: runtime.queryAttempted, - queryResult: runtime.queryResult - }; -} - -export function classifyCloudApiLiveReadiness(body) { - if (isDbLiveBlocked(body)) { - return { - blocked: true, - blockerType: "runtime_blocker", - blockerScope: "db-live", - sourceIssue: "pikasTech/HWLAB#49", - blockedClassification: "DB live", - blockerSummary: summarizeDbBlocker(body), - summary: "Blocked at DB live readiness before scheduling a DEV agent task.", - evidenceSuffix: "db-blocked", - dbLiveReady: false, - runtimeDurableReady: false - }; - } - - if (isRuntimeDurableAdapterBlocked(body)) { - const runtime = runtimeDurabilityFromHealth(body); - return { - blocked: true, - blockerType: "runtime_blocker", - blockerScope: "runtime-durable-adapter", - sourceIssue: "pikasTech/HWLAB#164", - blockedClassification: runtime.blocker, - blockerSummary: summarizeRuntimeDurableBlocker(body), - summary: "Blocked at runtime durable adapter readiness before scheduling a DEV agent task.", - evidenceSuffix: `runtime-durable-adapter-blocked:${runtime.blocker}`, - dbLiveReady: isDbLiveReady(body), - runtimeDurableReady: false - }; - } - - return { - blocked: false, - blockerType: null, - blockerScope: null, - blockedClassification: "none", - blockerSummary: "", - summary: "Live DEV preflight observed DB live and durable runtime adapter readiness.", - evidenceSuffix: null, - dbLiveReady: isDbLiveReady(body), - runtimeDurableReady: true - }; -} - -export function requestJson(urlString, { method = "GET", headers = {}, body, timeoutMs = 5000 } = {}) { - const url = new URL(urlString); - const client = url.protocol === "https:" ? httpsRequest : httpRequest; - const payload = body === undefined ? null : Buffer.from(body); - - return new Promise((resolve, reject) => { - const request = client( - { - protocol: url.protocol, - hostname: url.hostname, - port: url.port, - path: `${url.pathname}${url.search}`, - method, - headers - }, - (response) => { - let raw = ""; - response.setEncoding("utf8"); - response.on("data", (chunk) => { - raw += chunk; - }); - response.on("end", () => { - let bodyValue = null; - if (raw.length > 0) { - try { - bodyValue = JSON.parse(raw); - } catch (error) { - reject(new Error(`invalid JSON response from ${urlString}: ${error.message}`)); - return; - } - } - resolve({ - statusCode: response.statusCode ?? 0, - headers: response.headers, - body: bodyValue, - raw - }); - }); - } - ); - - request.on("error", reject); - request.setTimeout(timeoutMs, () => { - request.destroy(new Error(`request to ${urlString} timed out after ${timeoutMs}ms`)); - }); - - if (payload) request.write(payload); - request.end(); - }); -} - -function requestHead(urlString, { timeoutMs = 5000 } = {}) { - const url = new URL(urlString); - const client = url.protocol === "https:" ? httpsRequest : httpRequest; - - return new Promise((resolve, reject) => { - const request = client( - { - protocol: url.protocol, - hostname: url.hostname, - port: url.port, - path: `${url.pathname}${url.search}`, - method: "HEAD" - }, - (response) => { - response.resume(); - response.on("end", () => { - resolve({ - statusCode: response.statusCode ?? 0, - headers: response.headers - }); - }); - } - ); - request.on("error", reject); - request.setTimeout(timeoutMs, () => { - request.destroy(new Error(`request to ${urlString} timed out after ${timeoutMs}ms`)); - }); - request.end(); - }); -} - -function runKubectl(args, { input, timeoutMs = 5000 } = {}) { - try { - return { - ok: true, - command: `KUBECONFIG=${d601Kubeconfig} kubectl ${args.join(" ")}`, - exitCode: 0, - stdout: execFileSync("kubectl", args, { - cwd: repoRoot, - encoding: "utf8", - env: { ...process.env, KUBECONFIG: d601Kubeconfig }, - input, - stdio: ["pipe", "pipe", "pipe"], - timeout: timeoutMs - }) - }; - } catch (error) { - return { - ok: false, - command: `KUBECONFIG=${d601Kubeconfig} kubectl ${args.join(" ")}`, - exitCode: typeof error.status === "number" ? error.status : 1, - stdout: String(error.stdout ?? ""), - stderr: String(error.stderr ?? ""), - error: error instanceof Error ? error.message : String(error) - }; - } -} - -function parseProbeJson(probe) { - if (!probe.ok || !probe.stdout.trim()) return null; - try { - return JSON.parse(probe.stdout); - } catch { - return null; - } -} - -function compactError(probe) { - return (probe.stderr || probe.error || probe.stdout || "unknown error") - .replace(/\s+/g, " ") - .trim() - .slice(0, 400); -} - -function items(document) { - if (!document) return []; - return Array.isArray(document.items) ? document.items : [document]; -} - -function named(document, name) { - return items(document).find((item) => item?.metadata?.name === name) ?? null; -} - -function firstContainer(workload) { - return workload?.spec?.template?.spec?.containers?.[0] ?? null; -} - -function envMap(container) { - const values = {}; - const valueFrom = []; - for (const entry of container?.env ?? []) { - if (Object.hasOwn(entry, "value")) values[entry.name] = entry.value; - else if (entry.valueFrom) valueFrom.push(entry.name); - } - return { values, valueFrom }; -} - -function condition(workload, type) { - return workload?.status?.conditions?.find((entry) => entry.type === type)?.status ?? "Unknown"; -} - -function deploymentSummary(deployment) { - const container = firstContainer(deployment); - const replicas = deployment?.status?.replicas ?? deployment?.spec?.replicas ?? 0; - const readyReplicas = deployment?.status?.readyReplicas ?? 0; - return { - name: deployment?.metadata?.name ?? null, - image: container?.image ?? null, - readyReplicas, - replicas, - available: condition(deployment, "Available"), - ready: replicas > 0 && readyReplicas >= replicas && condition(deployment, "Available") === "True", - env: envMap(container) - }; -} - -function workerTemplateSummary(job) { - const container = firstContainer(job); - return { - name: job?.metadata?.name ?? null, - suspend: job?.spec?.suspend ?? null, - image: container?.image ?? null, - restartPolicy: job?.spec?.template?.spec?.restartPolicy ?? null, - env: envMap(container) - }; -} - -function serviceUrl(service, fallbackPort) { - const clusterIP = service?.spec?.clusterIP; - if (!clusterIP || clusterIP === "None") return null; - return `http://${clusterIP}:${service?.spec?.ports?.[0]?.port ?? fallbackPort}`; -} - -function endpointCount(endpoint) { - return (endpoint?.subsets ?? []).reduce((total, subset) => total + (subset.addresses?.length ?? 0), 0); -} - -function buildWorkerDryRunJob(templateJob, fixture, liveSkillsEnv) { - const templateContainer = firstContainer(templateJob); - const labels = { - "app.kubernetes.io/name": AGENT_WORKER_SERVICE_ID, - "hwlab.pikastech.local/environment": ENVIRONMENT_DEV, - "hwlab.pikastech.local/profile": ENVIRONMENT_DEV, - "hwlab.pikastech.local/service-id": AGENT_WORKER_SERVICE_ID, - "hwlab.pikastech.local/smoke": "dev-m4-agent-loop" - }; - const reservedEnv = new Set([ - "HWLAB_AGENT_SESSION_ID", - "HWLAB_PROJECT_ID", - "HWLAB_SKILL_COMMIT_ID", - "HWLAB_SKILL_VERSION", - "HWLAB_WORKSPACE_ROOT" - ]); - return { - apiVersion: "batch/v1", - kind: "Job", - metadata: { - name: "hwlab-agent-worker-smoke-dryrun-20260522", - namespace: devNamespace, - labels - }, - spec: { - suspend: true, - backoffLimit: 0, - template: { - metadata: { labels }, - spec: { - restartPolicy: templateJob?.spec?.template?.spec?.restartPolicy ?? "Never", - containers: [ - { - name: templateContainer?.name ?? AGENT_WORKER_SERVICE_ID, - image: templateContainer?.image ?? "unknown", - env: [ - ...(templateContainer?.env ?? []).filter((entry) => !reservedEnv.has(entry.name)), - { name: "HWLAB_AGENT_SESSION_ID", value: fixture.agentSessionId }, - { name: "HWLAB_PROJECT_ID", value: fixture.projectId }, - { name: "HWLAB_SKILL_COMMIT_ID", value: liveSkillsEnv.HWLAB_SKILLS_COMMIT_ID ?? "" }, - { name: "HWLAB_SKILL_VERSION", value: liveSkillsEnv.HWLAB_SKILLS_VERSION ?? "" }, - { name: "HWLAB_WORKSPACE_ROOT", value: `/workspace/${fixture.projectId}/${fixture.agentSessionId}` } - ] - } - ] - } - } - } - }; -} - -function k3sBlockers(report) { - const blockers = []; - const push = (type, scope, summary, classification, nextFix) => { - blockers.push({ type, scope, status: "open", summary, classification, nextFix }); - }; - if (!report.clusterReadable) { - push( - "environment_blocker", - "d601-k3s-readonly", - "D601 native k3s hwlab-dev resources were not readable with /etc/rancher/k3s/k3s.yaml.", - "k3s readonly", - "Mount or authorize read-only hwlab-dev access through the D601 native k3s kubeconfig." - ); - return blockers; - } - if (report.agentManager.health.status !== "ok") { - push( - "agent_blocker", - "agent-mgr-health", - `hwlab-agent-mgr /health/live is ${report.agentManager.health.status}.`, - "agent runtime", - "Inject explicit skill commit and version into agent-mgr health configuration, then redeploy DEV." - ); - } - if (report.skillsInjection.status !== "pass") { - push( - "agent_blocker", - "skills-commit-version-injection", - `DEV skills injection is incomplete: missing ${report.skillsInjection.missing.join(", ")}.`, - "skills injection", - "Add HWLAB_SKILLS_VERSION to hwlab-agent-skills and ensure scheduled worker jobs receive HWLAB_SKILL_COMMIT_ID plus HWLAB_SKILL_VERSION." - ); - } - if (report.workerJob.serverDryRun.status !== "pass") { - push( - "agent_blocker", - "worker-job-admission", - report.workerJob.serverDryRun.summary, - "worker job", - "Fix the hwlab-agent-worker-template image/env/job spec until server-side dry-run admission succeeds." - ); - } - return blockers; -} - -export async function collectPublicEntrypoints() { - const apiHealth = await requestJson(`${DEV_ENDPOINT}/health`).catch((error) => ({ - statusCode: 0, - body: null, - error: error instanceof Error ? error.message : String(error) - })); - const apiLive = await requestJson(`${DEV_ENDPOINT}/health/live`).catch((error) => ({ - statusCode: 0, - body: null, - error: error instanceof Error ? error.message : String(error) - })); - const frontend = await requestHead(`${DEV_FRONTEND_ENDPOINT}/`).catch((error) => ({ - statusCode: 0, - headers: {}, - error: error instanceof Error ? error.message : String(error) - })); - return { - status: apiHealth.statusCode >= 200 && - apiHealth.statusCode < 300 && - apiLive.statusCode >= 200 && - apiLive.statusCode < 300 && - frontend.statusCode >= 200 && - frontend.statusCode < 400 - ? "pass" - : "blocked", - commands: [ - `curl -fsS --max-time 10 ${DEV_ENDPOINT}/health`, - `curl -fsS --max-time 10 ${DEV_ENDPOINT}/health/live`, - `curl -fsS -I --max-time 10 ${DEV_FRONTEND_ENDPOINT}/` - ], - api: { - endpoint: DEV_ENDPOINT, - health: { - statusCode: apiHealth.statusCode, - serviceId: apiHealth.body?.serviceId ?? null, - environment: apiHealth.body?.environment ?? null, - status: apiHealth.body?.status ?? null, - error: apiHealth.error ?? null - }, - live: { - statusCode: apiLive.statusCode, - serviceId: apiLive.body?.serviceId ?? null, - environment: apiLive.body?.environment ?? null, - status: apiLive.body?.status ?? null, - db: publicDbLiveSummary(apiLive.body), - runtime: publicRuntimeDurabilitySummary(apiLive.body), - blockerCodes: Array.isArray(apiLive.body?.blockerCodes) ? apiLive.body.blockerCodes : [], - error: apiLive.error ?? null - } - }, - frontend: { - endpoint: DEV_FRONTEND_ENDPOINT, - statusCode: frontend.statusCode, - contentType: frontend.headers?.["content-type"] ?? null, - contentLength: frontend.headers?.["content-length"] ?? null, - error: frontend.error ?? null - }, - summary: `Public DEV API ${DEV_ENDPOINT} health=${apiHealth.statusCode} live=${apiLive.statusCode}; frontend ${DEV_FRONTEND_ENDPOINT}/=${frontend.statusCode}.` - }; -} - -export async function collectD601K3sNativeEvidence(fixture) { - const namespaceProbe = runKubectl(["get", "namespace", devNamespace, "-o", "json"]); - const deploymentsProbe = runKubectl([ - "-n", - devNamespace, - "get", - "deploy", - AGENT_MGR_SERVICE_ID, - AGENT_SKILLS_SERVICE_ID, - "-o", - "json" - ]); - const workerTemplateProbe = runKubectl([ - "-n", - devNamespace, - "get", - "job", - "hwlab-agent-worker-template", - "-o", - "json" - ]); - const servicesProbe = runKubectl([ - "-n", - devNamespace, - "get", - "svc", - AGENT_MGR_SERVICE_ID, - AGENT_SKILLS_SERVICE_ID, - "-o", - "json" - ]); - const endpointsProbe = runKubectl([ - "-n", - devNamespace, - "get", - "endpoints", - AGENT_MGR_SERVICE_ID, - AGENT_SKILLS_SERVICE_ID, - "-o", - "json" - ]); - const canCreateJobs = runKubectl(["auth", "can-i", "create", "jobs", "-n", devNamespace]); - const canDeleteJobs = runKubectl(["auth", "can-i", "delete", "jobs", "-n", devNamespace]); - const canGetSecrets = runKubectl(["auth", "can-i", "get", "secrets", "-n", devNamespace]); - - const deployments = parseProbeJson(deploymentsProbe); - const services = parseProbeJson(servicesProbe); - const endpoints = parseProbeJson(endpointsProbe); - const workerTemplate = parseProbeJson(workerTemplateProbe); - const agentMgrDeployment = named(deployments, AGENT_MGR_SERVICE_ID); - const skillsDeployment = named(deployments, AGENT_SKILLS_SERVICE_ID); - const agentMgrService = named(services, AGENT_MGR_SERVICE_ID); - const skillsService = named(services, AGENT_SKILLS_SERVICE_ID); - const agentMgrEndpoint = named(endpoints, AGENT_MGR_SERVICE_ID); - const skillsEndpoint = named(endpoints, AGENT_SKILLS_SERVICE_ID); - const agentMgrSummary = deploymentSummary(agentMgrDeployment); - const skillsSummary = deploymentSummary(skillsDeployment); - const workerSummary = workerTemplateSummary(workerTemplate); - const liveSkillsEnv = skillsSummary.env.values; - const agentMgrUrl = serviceUrl(agentMgrService, 7410); - const skillsUrl = serviceUrl(skillsService, 7430); - const agentMgrHealth = agentMgrUrl ? await requestJson(`${agentMgrUrl}/health/live`).catch((error) => ({ - statusCode: 0, - body: null, - error: error instanceof Error ? error.message : String(error) - })) : null; - const skillsHealth = skillsUrl ? await requestJson(`${skillsUrl}/health/live`).catch((error) => ({ - statusCode: 0, - body: null, - error: error instanceof Error ? error.message : String(error) - })) : null; - const workerDryRunManifest = workerTemplate ? buildWorkerDryRunJob(workerTemplate, fixture, liveSkillsEnv) : null; - const workerDryRunProbe = workerDryRunManifest - ? runKubectl(["-n", devNamespace, "create", "-f", "-", "--dry-run=server", "-o", "json"], { - input: `${JSON.stringify(workerDryRunManifest)}\n` - }) - : { ok: false, stdout: "", stderr: "worker template was not readable" }; - const workerDryRun = parseProbeJson(workerDryRunProbe); - const workerInjectedEnv = envMap(workerDryRunManifest?.spec?.template?.spec?.containers?.[0] ?? null).values; - const missingSkills = []; - if (!liveSkillsEnv.HWLAB_SKILLS_COMMIT_ID) missingSkills.push("hwlab-agent-skills.HWLAB_SKILLS_COMMIT_ID"); - if (!liveSkillsEnv.HWLAB_SKILLS_VERSION) missingSkills.push("hwlab-agent-skills.HWLAB_SKILLS_VERSION"); - if (!workerInjectedEnv.HWLAB_SKILL_COMMIT_ID) missingSkills.push("worker-dry-run.HWLAB_SKILL_COMMIT_ID"); - if (!workerInjectedEnv.HWLAB_SKILL_VERSION) missingSkills.push("worker-dry-run.HWLAB_SKILL_VERSION_FROM_DEV"); - const clusterReadable = namespaceProbe.ok && deploymentsProbe.ok && workerTemplateProbe.ok && servicesProbe.ok; - const evidence = [ - `k3s:namespace=${devNamespace}:read=${namespaceProbe.ok}`, - `k3s:deploy/${AGENT_MGR_SERVICE_ID}:ready=${agentMgrSummary.readyReplicas}/${agentMgrSummary.replicas}:available=${agentMgrSummary.available}`, - `k3s:deploy/${AGENT_SKILLS_SERVICE_ID}:ready=${skillsSummary.readyReplicas}/${skillsSummary.replicas}:available=${skillsSummary.available}`, - `k3s:svc/${AGENT_MGR_SERVICE_ID}:endpoints=${endpointCount(agentMgrEndpoint)}`, - `k3s:svc/${AGENT_SKILLS_SERVICE_ID}:endpoints=${endpointCount(skillsEndpoint)}`, - `agent-mgr:/health/live:${agentMgrHealth?.body?.health?.status ?? agentMgrHealth?.body?.status ?? "unreachable"}`, - `agent-skills:/health/live:${skillsHealth?.body?.status ?? "unreachable"}:${skillsHealth?.body?.revision ?? "unknown"}`, - `worker-template:suspend=${workerSummary.suspend}:image=${workerSummary.image ?? "unknown"}`, - `worker-job:server-dry-run=${workerDryRunProbe.ok}:persisted=false`, - `skills-injection:commit=${liveSkillsEnv.HWLAB_SKILLS_COMMIT_ID ?? "missing"}:version=${liveSkillsEnv.HWLAB_SKILLS_VERSION ?? "missing"}`, - `secret-rbac:get=${canGetSecrets.stdout.trim() || "unknown"}:secretResourcesRead=false` - ]; - const report = { - status: "pass", - mode: "dev-read-only-plus-server-dry-run", - generatedAt: new Date().toISOString(), - commands: [ - `KUBECONFIG=${d601Kubeconfig} kubectl get namespace ${devNamespace} -o json`, - d601K3sReadonlyCommand, - `KUBECONFIG=${d601Kubeconfig} kubectl -n ${devNamespace} get deploy ${AGENT_MGR_SERVICE_ID} ${AGENT_SKILLS_SERVICE_ID} -o json`, - `KUBECONFIG=${d601Kubeconfig} kubectl -n ${devNamespace} get job hwlab-agent-worker-template -o json`, - `KUBECONFIG=${d601Kubeconfig} kubectl -n ${devNamespace} get svc ${AGENT_MGR_SERVICE_ID} ${AGENT_SKILLS_SERVICE_ID} -o json`, - `KUBECONFIG=${d601Kubeconfig} kubectl -n ${devNamespace} get endpoints ${AGENT_MGR_SERVICE_ID} ${AGENT_SKILLS_SERVICE_ID} -o json`, - `KUBECONFIG=${d601Kubeconfig} kubectl auth can-i create jobs -n ${devNamespace}`, - `KUBECONFIG=${d601Kubeconfig} kubectl auth can-i delete jobs -n ${devNamespace}`, - `KUBECONFIG=${d601Kubeconfig} kubectl auth can-i get secrets -n ${devNamespace}`, - workerServerDryRunCommand - ], - safety: { - namespace: devNamespace, - kubeconfig: d601Kubeconfig, - readOnly: true, - prodTouched: false, - secretResourcesRead: false, - secretMaterialPrinted: false, - servicesRestarted: false, - workerJobPersisted: false, - longRunningAgentStarted: false, - note: "The smoke checks Secret RBAC only with kubectl auth can-i; it never reads Secret resources or kubeconfig contents." - }, - clusterReadable, - namespace: { - name: devNamespace, - readable: namespaceProbe.ok, - error: namespaceProbe.ok ? null : compactError(namespaceProbe) - }, - rbac: { - createJobs: canCreateJobs.stdout.trim() === "yes", - deleteJobs: canDeleteJobs.stdout.trim() === "yes", - getSecrets: canGetSecrets.stdout.trim() === "yes", - secretReadNotPerformed: true - }, - agentManager: { - deployment: agentMgrSummary, - endpointCount: endpointCount(agentMgrEndpoint), - health: { - statusCode: agentMgrHealth?.statusCode ?? 0, - status: agentMgrHealth?.body?.health?.status ?? agentMgrHealth?.body?.status ?? "unreachable", - serviceId: agentMgrHealth?.body?.serviceId ?? null, - missingSkills: agentMgrHealth?.body?.health?.skills?.missing ?? [], - error: agentMgrHealth?.error ?? null - } - }, - agentSkills: { - deployment: skillsSummary, - endpointCount: endpointCount(skillsEndpoint), - health: { - statusCode: skillsHealth?.statusCode ?? 0, - status: skillsHealth?.body?.status ?? "unreachable", - serviceId: skillsHealth?.body?.serviceId ?? null, - revision: skillsHealth?.body?.revision ?? null, - error: skillsHealth?.error ?? null - } - }, - workerJob: { - template: workerSummary, - serverDryRun: { - status: workerDryRunProbe.ok ? "pass" : "blocked", - accepted: workerDryRunProbe.ok, - command: workerServerDryRunCommand, - persisted: false, - generatedName: workerDryRunManifest?.metadata?.name ?? null, - injectedEnvNames: Object.keys(workerInjectedEnv).sort(), - dryRunKind: workerDryRun?.kind ?? null, - dryRunSuspend: workerDryRun?.spec?.suspend ?? null, - summary: workerDryRunProbe.ok - ? "Server-side admission accepted a suspended worker Job generated from the template with session/workspace/skills env injected; no Job was persisted." - : compactError(workerDryRunProbe) - } - }, - skillsInjection: { - status: missingSkills.length === 0 ? "pass" : "blocked", - source: "k3s deployment env plus generated worker dry-run manifest", - serviceCommitId: liveSkillsEnv.HWLAB_SKILLS_COMMIT_ID ?? null, - serviceVersion: liveSkillsEnv.HWLAB_SKILLS_VERSION ?? null, - workerDryRunCommitId: workerInjectedEnv.HWLAB_SKILL_COMMIT_ID ?? null, - workerDryRunVersion: workerInjectedEnv.HWLAB_SKILL_VERSION ?? null, - workerDryRunSource: "derived-from-live-dev-skills-env", - missing: missingSkills - }, - traceEvidenceCleanup: { - status: "dry-run-only", - summary: "Live worker execution was not started. Trace/evidence/cleanup closure is covered by the bounded local dry-run and remains explicitly non-M3 DEV-LIVE." - }, - evidence - }; - report.blockers = k3sBlockers(report); - report.status = report.blockers.length === 0 ? "pass" : "blocked"; - report.minimumFixRecommendations = report.blockers.map((blocker) => blocker.nextFix).filter(Boolean); - return report; -} diff --git a/scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs b/scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs deleted file mode 100644 index 37980690..00000000 --- a/scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs +++ /dev/null @@ -1,157 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { - classifyCloudApiLiveReadiness, - publicDbLiveSummary, - publicRuntimeDurabilitySummary, - runtimeDurabilityFromHealth -} from "./dev-m4-agent-loop-smoke-lib.mjs"; - -test("M4 live readiness classifies durable adapter query blocker after DB live is ready", () => { - const payload = { - db: { - ready: true, - connected: true, - liveConnected: true, - liveDbEvidence: true, - status: "ok" - }, - runtime: { - adapter: "postgres", - durable: false, - durableRequested: true, - ready: false, - status: "blocked", - blocker: "runtime_durable_adapter_query_blocked", - reason: "durable runtime read query is blocked", - liveRuntimeEvidence: false, - connection: { - queryAttempted: true, - queryResult: "query_blocked" - }, - migration: { - checked: true, - ready: false, - missing: true, - requiredMigrationId: "0001_cloud_core_skeleton" - } - }, - readiness: { - durability: { - blockedLayer: "durability_query", - requiredEvidence: "runtime_adapter_schema_migration_read_query" - } - } - }; - - const classification = classifyCloudApiLiveReadiness(payload); - assert.equal(classification.blocked, true); - assert.equal(classification.blockerScope, "runtime-durable-adapter"); - assert.equal(classification.blockedClassification, "runtime_durable_adapter_query_blocked"); - assert.match(classification.blockerSummary, /0001_cloud_core_skeleton/u); - assert.match(classification.evidenceSuffix, /runtime_durable_adapter_query_blocked/u); - - const runtime = runtimeDurabilityFromHealth(payload); - assert.equal(runtime.blocker, "runtime_durable_adapter_query_blocked"); - assert.equal(runtime.migration.checked, true); - assert.equal(runtime.migration.ready, false); - assert.equal(runtime.queryResult, "query_blocked"); -}); - -test("M4 live readiness keeps DB live blocker when DB is not connected", () => { - const classification = classifyCloudApiLiveReadiness({ - db: { - ready: false, - connected: false, - liveDbEvidence: false, - status: "degraded" - }, - runtime: { - adapter: "postgres", - durableRequested: true, - blocker: "runtime_durable_adapter_query_blocked" - } - }); - - assert.equal(classification.blockerScope, "db-live"); - assert.equal(classification.blockedClassification, "DB live"); -}); - -test("public DB live summary preserves DB pass fields apart from durable adapter blocker", () => { - const summary = publicDbLiveSummary({ - db: { - status: "ok", - connected: true, - liveConnected: true, - ready: true, - configReady: true, - connectionChecked: true, - endpointSource: "secret-url-host", - liveDbEvidence: true, - evidence: "live_db_tcp_connection_ready" - }, - runtime: { - adapter: "postgres", - durable: false, - ready: false, - blocker: "runtime_durable_adapter_query_blocked" - } - }); - - assert.equal(summary.status, "ok"); - assert.equal(summary.connected, true); - assert.equal(summary.liveConnected, true); - assert.equal(summary.ready, true); - assert.equal(summary.configReady, true); - assert.equal(summary.connectionChecked, true); - assert.equal(summary.endpointSource, "secret-url-host"); - assert.equal(summary.liveDbEvidence, true); - assert.equal(summary.evidence, "live_db_tcp_connection_ready"); -}); - -test("public runtime durability summary preserves durable adapter fields separately from DB live", () => { - const summary = publicRuntimeDurabilitySummary({ - db: { - ready: true, - connected: true, - liveConnected: true, - liveDbEvidence: true - }, - runtime: { - adapter: "postgres", - durable: false, - durableRequested: true, - ready: false, - status: "blocked", - blocker: "runtime_durable_adapter_query_blocked", - liveRuntimeEvidence: false, - connection: { - queryAttempted: true, - queryResult: "query_blocked" - }, - migration: { - checked: false, - ready: false, - missing: true, - requiredMigrationId: "0001_cloud_core_skeleton" - }, - durabilityContract: { - blockedLayer: "durability_query", - requiredEvidence: "runtime_adapter_schema_migration_read_query" - } - } - }); - - assert.equal(summary.adapter, "postgres"); - assert.equal(summary.durableRequested, true); - assert.equal(summary.durable, false); - assert.equal(summary.ready, false); - assert.equal(summary.liveRuntimeEvidence, false); - assert.equal(summary.blocker, "runtime_durable_adapter_query_blocked"); - assert.equal(summary.blockedLayer, "durability_query"); - assert.equal(summary.requiredEvidence, "runtime_adapter_schema_migration_read_query"); - assert.equal(summary.queryAttempted, true); - assert.equal(summary.queryResult, "query_blocked"); - assert.equal(summary.migration.requiredMigrationId, "0001_cloud_core_skeleton"); -}); diff --git a/scripts/src/g14-ci-plan-lib.mjs b/scripts/src/g14-ci-plan-lib.mjs index 03c2f34a..f2ce2e01 100644 --- a/scripts/src/g14-ci-plan-lib.mjs +++ b/scripts/src/g14-ci-plan-lib.mjs @@ -81,12 +81,6 @@ const serviceSpecificPaths = Object.freeze({ "internal/dev-entrypoint/cloud-web-proxy.mjs", "internal/dev-entrypoint/cloud-web-routes.mjs" ], - "hwlab-agent-mgr": ["cmd/hwlab-agent-mgr/", "internal/agent/", "internal/db/", "internal/audit/"], - "hwlab-agent-worker": [ - "cmd/hwlab-agent-worker/", - "internal/agent/", - "skills/hwlab-agent-runtime/" - ], "hwlab-device-pod": ["cmd/hwlab-device-pod/", "internal/device-pod/"], "hwlab-gateway": ["cmd/hwlab-gateway/"], "hwlab-edge-proxy": ["cmd/hwlab-edge-proxy/", "internal/dev-entrypoint/http.mjs"], @@ -94,8 +88,6 @@ const serviceSpecificPaths = Object.freeze({ }); const bunCommandServices = new Set([ - "hwlab-agent-mgr", - "hwlab-agent-worker", "hwlab-cloud-api", "hwlab-codex-api-responses-forwarder", "hwlab-deepseek-responses-bridge", @@ -485,7 +477,13 @@ export async function lastCommitForPaths(repoRoot, targetRef, paths) { function resolveServiceIds({ deployJson, options }) { if (Array.isArray(options.services) && options.services.length > 0) { - return { source: "cli-services", serviceIds: uniqueSorted(options.services.map(String)) }; + const serviceIds = uniqueSorted(options.services.map(String)); + const allowedServiceIds = knownServiceIds(deployJson); + const unknownServiceIds = serviceIds.filter((serviceId) => !allowedServiceIds.has(serviceId)); + if (unknownServiceIds.length > 0) { + throw new Error(`unknown service IDs for G14 CI plan: ${unknownServiceIds.join(", ")}`); + } + return { source: "cli-services", serviceIds }; } const deployServices = Array.isArray(deployJson?.services) ? deployJson.services.map((service) => service?.serviceId).filter(Boolean) : []; if (deployServices.length > 0) return { source: "deploy.services", serviceIds: uniquePreserveOrder(deployServices) }; @@ -496,6 +494,17 @@ function resolveServiceIds({ deployJson, options }) { return { source: "internal.protocol.SERVICE_IDS", serviceIds: [...SERVICE_IDS] }; } +function knownServiceIds(deployJson) { + const values = new Set(SERVICE_IDS); + for (const service of deployJson?.services ?? []) { + if (service?.serviceId) values.add(service.serviceId); + } + for (const service of deployJson?.k3s?.serviceMappings ?? []) { + if (service?.serviceId) values.add(service.serviceId); + } + return values; +} + async function readJsonIfPresent(repoRoot, relativePath, fallback) { try { const filePath = path.isAbsolute(relativePath) ? relativePath : path.join(repoRoot, relativePath); diff --git a/scripts/src/rpt004-mvp-e2e-harness.test.mjs b/scripts/src/rpt004-mvp-e2e-harness.test.mjs index cfb265b5..c02c98b4 100644 --- a/scripts/src/rpt004-mvp-e2e-harness.test.mjs +++ b/scripts/src/rpt004-mvp-e2e-harness.test.mjs @@ -543,8 +543,6 @@ function fixtureArtifactJson() { const serviceIds = [ "hwlab-cloud-api", "hwlab-cloud-web", - "hwlab-agent-mgr", - "hwlab-agent-worker", "hwlab-gateway", "hwlab-gateway-simu", "hwlab-box-simu", diff --git a/scripts/validate-contract.mjs b/scripts/validate-contract.mjs index 50d8fe33..58a6a4d8 100644 --- a/scripts/validate-contract.mjs +++ b/scripts/validate-contract.mjs @@ -127,7 +127,7 @@ function assertEnvelopeValidation() { }, meta: { traceId: "trc_01J00000000000000000000000", - serviceId: "hwlab-agent-mgr", + serviceId: "hwlab-cloud-api", environment: "dev" } }); @@ -141,7 +141,7 @@ function assertEnvelopeValidation() { error: { code: -32000, message: "bad" }, meta: { traceId: "trc_01J00000000000000000000000", - serviceId: "hwlab-agent-mgr", + serviceId: "hwlab-cloud-api", environment: "dev" } }), @@ -164,10 +164,8 @@ assertCommonSchema(docsByName.get("common.json")); assertDeploySchema(docsByName.get("deploy.schema.json")); assertEnvelopeValidation(); const deployServicesById = new Map(deployManifest.services.map((service) => [service.serviceId, service])); -assert.equal(deployServicesById.has("hwlab-agent-mgr"), true); -assert.equal(deployServicesById.has("hwlab-agent-worker"), true); assert.equal(deployServicesById.has("hwlab-agent-skills"), true); -for (const removedServiceId of ["hwlab-gateway-simu", "hwlab-box-simu", "hwlab-patch-panel", "hwlab-router", "hwlab-tunnel-client"]) { +for (const removedServiceId of ["hwlab-agent-mgr", "hwlab-agent-worker", "hwlab-gateway-simu", "hwlab-box-simu", "hwlab-patch-panel", "hwlab-router", "hwlab-tunnel-client"]) { assert.equal(deployServicesById.has(removedServiceId), false, `${removedServiceId} is removed from default deploy services`); assert.equal(getWorkload(k8sWorkloads, removedServiceId), null, `${removedServiceId} is removed from default workload manifests`); assert.equal(listItems(k8sServices).some((item) => item?.metadata?.labels?.["hwlab.pikastech.local/service-id"] === removedServiceId), false, `${removedServiceId} is removed from default service manifests`); diff --git a/scripts/validate-m0-contract.mjs b/scripts/validate-m0-contract.mjs deleted file mode 100644 index 5e43193f..00000000 --- a/scripts/validate-m0-contract.mjs +++ /dev/null @@ -1,262 +0,0 @@ -#!/usr/bin/env node -import assert from "node:assert/strict"; -import { readdir, readFile } from "node:fs/promises"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { - DEV_ENDPOINT, - ENVIRONMENT_DEV, - ERROR_CODES, - SERVICE_IDS, - validateRequest, - validateResponse -} from "../internal/protocol/index.mjs"; - -const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const examplesDir = "protocol/examples/m0-contract"; -const auditDocPath = "docs/reference/spec-v02-documentation-governance.md"; - -const timestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; -const idPattern = /^[a-z][a-z0-9]*_[A-Za-z0-9._:-]+$/; -const actionPattern = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/; - -async function readJSON(relativePath) { - const raw = await readFile(path.join(repoRoot, relativePath), "utf8"); - return JSON.parse(raw); -} - -async function loadExamples() { - const entries = await readdir(path.join(repoRoot, examplesDir), { withFileTypes: true }); - const examples = new Map(); - for (const entry of entries) { - if (!entry.isFile() || !entry.name.endsWith(".json")) { - continue; - } - const relativePath = path.join(examplesDir, entry.name); - examples.set(entry.name, await readJSON(relativePath)); - } - return examples; -} - -function assertUnique(name, values) { - assert.equal(new Set(values).size, values.length, `${name} must be unique`); -} - -function assertServiceId(serviceId, context) { - assert.ok(SERVICE_IDS.includes(serviceId), `${context} uses unknown serviceId ${serviceId}`); -} - -function assertDevEnvironment(value, context) { - assert.equal(value, ENVIRONMENT_DEV, `${context} must be dev`); -} - -function assertId(value, context) { - assert.equal(typeof value, "string", `${context} must be a string id`); - assert.match(value, idPattern, `${context} must match HWLAB id pattern`); -} - -function assertTimestamp(value, context) { - assert.equal(typeof value, "string", `${context} must be a timestamp`); - assert.match(value, timestampPattern, `${context} must be an RFC 3339 UTC timestamp`); -} - -function assertServiceIdsExample(doc) { - assert.equal(doc.contract, "m0-service-ids"); - assertDevEnvironment(doc.environment, "service-ids environment"); - assert.deepEqual(doc.serviceIds, SERVICE_IDS, "example service ids must match frozen runtime service ids"); - assertUnique("example service ids", doc.serviceIds); -} - -function assertJsonRpcExamples(request, response, errorEnvelope) { - validateRequest(request); - validateResponse(response); - validateResponse(errorEnvelope); - - assert.equal(response.id, request.id, "JSON-RPC response must echo request id"); - assert.equal(errorEnvelope.id, request.id, "JSON-RPC error must echo request id"); - assert.equal(response.meta.traceId, request.meta.traceId, "response must keep request traceId"); - assert.equal(errorEnvelope.meta.traceId, request.meta.traceId, "error must keep request traceId"); - assertServiceId(request.meta.serviceId, "request meta"); - assertServiceId(response.meta.serviceId, "response meta"); - assertServiceId(errorEnvelope.meta.serviceId, "error meta"); - assert.ok( - Object.values(ERROR_CODES).includes(errorEnvelope.error.code), - "error example must use a reserved JSON-RPC or HWLAB error code" - ); - for (const key of ["projectId", "gatewaySessionId", "resourceId", "capabilityId", "reason"]) { - assert.ok(Object.hasOwn(errorEnvelope.error.data, key), `error.data must include ${key}`); - } -} - -function assertAuditEvent(doc) { - for (const key of [ - "auditId", - "traceId", - "actorType", - "actorId", - "action", - "targetType", - "targetId", - "serviceId", - "environment", - "occurredAt" - ]) { - assert.ok(Object.hasOwn(doc, key), `audit event missing ${key}`); - } - for (const key of ["auditId", "traceId", "actorId", "targetId", "projectId", "gatewaySessionId", "workerSessionId", "operationId"]) { - assertId(doc[key], `audit event ${key}`); - } - assert.match(doc.action, actionPattern, "audit action must be dotted lower-case"); - assert.equal(doc.targetId, doc.operationId, "operation audit targetId must equal operationId"); - assertServiceId(doc.serviceId, "audit event"); - assertDevEnvironment(doc.environment, "audit event environment"); - assertTimestamp(doc.occurredAt, "audit occurredAt"); -} - -function assertCapabilityTopology(doc) { - assert.equal(doc.contract, "m0-capability-topology"); - assertDevEnvironment(doc.project.environment, "project environment"); - assertId(doc.project.projectId, "projectId"); - assert.equal(doc.gatewaySession.projectId, doc.project.projectId, "gateway session projectId must match project"); - assert.ok(["hwlab-gateway", "hwlab-gateway-simu"].includes(doc.gatewaySession.serviceId), "gateway service must be gateway"); - assertDevEnvironment(doc.gatewaySession.environment, "gateway session environment"); - - const resourceIds = new Set(); - for (const resource of doc.resources) { - assertId(resource.resourceId, "resourceId"); - assert.equal(resource.projectId, doc.project.projectId, "resource projectId must match project"); - assert.equal(resource.gatewaySessionId, doc.gatewaySession.gatewaySessionId, "resource gatewaySessionId must match gateway"); - assertDevEnvironment(resource.environment, `resource ${resource.resourceId} environment`); - resourceIds.add(resource.resourceId); - } - assert.ok(resourceIds.size >= 2, "topology example must include at least two resources"); - - const capabilityIds = new Set(); - for (const capability of doc.capabilities) { - assertId(capability.capabilityId, "capabilityId"); - assert.ok(resourceIds.has(capability.resourceId), `capability ${capability.capabilityId} must reference a known resource`); - assert.equal(capability.projectId, doc.project.projectId, "capability projectId must match project"); - assert.match(capability.name, actionPattern, "capability name must be dotted lower-case"); - assert.equal(typeof capability.mutatesState, "boolean", "capability mutatesState must be boolean"); - capabilityIds.add(capability.capabilityId); - } - assert.ok(capabilityIds.size >= 2, "topology example must include multiple capabilities"); - - const wiring = doc.wiringConfig; - assert.equal(wiring.projectId, doc.project.projectId, "wiring projectId must match project"); - assert.equal(wiring.gatewaySessionId, doc.gatewaySession.gatewaySessionId, "wiring gatewaySessionId must match gateway"); - assert.equal(wiring.status, "active", "wiring example should show an active config"); - for (const connection of wiring.connections) { - assert.ok(resourceIds.has(connection.from.resourceId), "wiring from.resourceId must exist"); - assert.ok(resourceIds.has(connection.to.resourceId), "wiring to.resourceId must exist"); - if (connection.from.capabilityId) { - assert.ok(capabilityIds.has(connection.from.capabilityId), "wiring from.capabilityId must exist"); - } - if (connection.to.capabilityId) { - assert.ok(capabilityIds.has(connection.to.capabilityId), "wiring to.capabilityId must exist"); - } - } - - const patch = doc.patchPanelStatus; - assert.equal(patch.serviceId, "hwlab-patch-panel"); - assert.equal(patch.projectId, doc.project.projectId, "patch panel projectId must match project"); - assert.equal(patch.gatewaySessionId, doc.gatewaySession.gatewaySessionId, "patch panel gatewaySessionId must match gateway"); - assert.equal(patch.wiringConfigId, wiring.wiringConfigId, "patch panel wiringConfigId must match wiring"); - assertDevEnvironment(patch.environment, "patch panel environment"); - for (const active of patch.activeConnections) { - assert.ok(resourceIds.has(active.fromResourceId), "active fromResourceId must exist"); - assert.ok(resourceIds.has(active.toResourceId), "active toResourceId must exist"); - } - - assert.equal(doc.agentSession.projectId, doc.project.projectId, "agent session projectId must match project"); - assert.equal(doc.agentSession.serviceId, "hwlab-agent-mgr"); - assertDevEnvironment(doc.agentSession.environment, "agent session environment"); - - assert.equal(doc.workerSession.projectId, doc.project.projectId, "worker session projectId must match project"); - assert.equal(doc.workerSession.agentSessionId, doc.agentSession.agentSessionId, "worker must reference agent session"); - assert.equal(doc.workerSession.gatewaySessionId, doc.gatewaySession.gatewaySessionId, "worker must reference gateway session"); - assert.equal(doc.workerSession.serviceId, "hwlab-agent-worker"); - assertDevEnvironment(doc.workerSession.environment, "worker session environment"); - - const operation = doc.hardwareOperation; - assert.equal(operation.projectId, doc.project.projectId, "operation projectId must match project"); - assert.equal(operation.gatewaySessionId, doc.gatewaySession.gatewaySessionId, "operation gatewaySessionId must match gateway"); - assert.equal(operation.agentSessionId, doc.agentSession.agentSessionId, "operation must reference agent session"); - assert.equal(operation.workerSessionId, doc.workerSession.workerSessionId, "operation must reference worker session"); - assert.ok(resourceIds.has(operation.resourceId), "operation resourceId must exist"); - assert.ok(capabilityIds.has(operation.capabilityId), "operation capabilityId must exist"); - assertDevEnvironment(operation.environment, "operation environment"); - - for (const traceEvent of doc.traceEvents) { - assert.equal(traceEvent.projectId, doc.project.projectId, "trace projectId must match project"); - assert.equal(traceEvent.operationId, operation.operationId, "trace operationId must match operation"); - assertServiceId(traceEvent.serviceId, "trace event"); - assertDevEnvironment(traceEvent.environment, "trace environment"); - } - - const evidence = doc.evidenceRecord; - assert.equal(evidence.projectId, doc.project.projectId, "evidence projectId must match project"); - assert.equal(evidence.operationId, operation.operationId, "evidence operationId must match operation"); - assert.equal(evidence.agentSessionId, doc.agentSession.agentSessionId, "evidence must reference agent session"); - assert.equal(evidence.workerSessionId, doc.workerSession.workerSessionId, "evidence must reference worker session"); - assert.match(evidence.sha256, /^[a-f0-9]{64}$/, "evidence sha256 must be lowercase hex"); - assertServiceId(evidence.serviceId, "evidence"); - assertDevEnvironment(evidence.environment, "evidence environment"); -} - -function assertDeployManifest(doc) { - assert.equal(doc.manifestVersion, "v1"); - assertDevEnvironment(doc.environment, "deploy manifest environment"); - assert.equal(doc.endpoint, DEV_ENDPOINT, "deploy manifest endpoint must be the frozen DEV endpoint"); - assert.ok(doc.profiles.dev.enabled, "deploy manifest dev profile must be enabled"); - assert.equal(doc.profiles.dev.endpoint, DEV_ENDPOINT, "deploy manifest dev profile endpoint must be frozen"); - assert.ok(!doc.profiles.prod, "M0 example must not include a PROD profile"); - - const serviceIds = doc.services.map((service) => service.serviceId); - assert.deepEqual(serviceIds, SERVICE_IDS, "deploy manifest example must cover every frozen service id in order"); - assertUnique("deploy service ids", serviceIds); - for (const service of doc.services) { - assertServiceId(service.serviceId, "deploy service"); - assert.equal(service.profile, "dev", `${service.serviceId} must use dev profile in M0 example`); - assert.equal(service.namespace, doc.namespace, `${service.serviceId} namespace must match manifest namespace`); - assert.ok(service.healthPath.startsWith("/"), `${service.serviceId} healthPath must be absolute`); - } -} - -async function assertAuditMarkdown() { - const doc = await readFile(path.join(repoRoot, auditDocPath), "utf8"); - for (const needle of [ - "# v0.2 文档治理规格", - "docs/m0-*", - "旧里程碑", - "#532" - ]) { - assert.ok(doc.includes(needle), `${auditDocPath} missing ${needle}`); - } -} - -const examples = await loadExamples(); -for (const required of [ - "service-ids.json", - "json-rpc-request.json", - "json-rpc-response.json", - "json-rpc-error.json", - "audit-event.json", - "capability-topology.json", - "deploy-manifest.dev.json" -]) { - assert.ok(examples.has(required), `${examplesDir} missing ${required}`); -} - -assertServiceIdsExample(examples.get("service-ids.json")); -assertJsonRpcExamples( - examples.get("json-rpc-request.json"), - examples.get("json-rpc-response.json"), - examples.get("json-rpc-error.json") -); -assertAuditEvent(examples.get("audit-event.json")); -assertCapabilityTopology(examples.get("capability-topology.json")); -assertDeployManifest(examples.get("deploy-manifest.dev.json")); -await assertAuditMarkdown(); - -console.log(`validated M0 contract examples: ${examples.size} JSON files, ${SERVICE_IDS.length} service ids`); diff --git a/scripts/validate-runtime-boundary.mjs b/scripts/validate-runtime-boundary.mjs index e6d613a5..688e7e67 100644 --- a/scripts/validate-runtime-boundary.mjs +++ b/scripts/validate-runtime-boundary.mjs @@ -156,10 +156,6 @@ function assertHwlabImage(image, serviceId, label) { } function assertHealthProbe(container, serviceId, label) { - if (serviceId === "hwlab-agent-worker") { - return; - } - for (const probeName of ["readinessProbe", "livenessProbe"]) { assert.equal( container[probeName]?.httpGet?.path, diff --git a/tools/src/hwlab-cli-lib.ts b/tools/src/hwlab-cli-lib.ts index 791d3a89..b6da0439 100644 --- a/tools/src/hwlab-cli-lib.ts +++ b/tools/src/hwlab-cli-lib.ts @@ -389,7 +389,7 @@ function runtimeRouteSummary(value: any) { } function defaultRuntimeContainer(serviceId: string) { - if (["hwlab-cloud-api", "hwlab-cloud-web", "hwlab-device-pod", "hwlab-agent-mgr", "hwlab-agent-skills"].includes(serviceId)) return serviceId; + if (["hwlab-cloud-api", "hwlab-cloud-web", "hwlab-device-pod", "hwlab-agent-skills"].includes(serviceId)) return serviceId; return ""; }