切换 AgentRun 资源装配到 gitbundle

This commit is contained in:
Codex Agent
2026-06-08 11:25:54 +08:00
parent c52ba0dac2
commit 9d76230bc3
17 changed files with 152 additions and 239 deletions
@@ -50,19 +50,19 @@ Provider profile slug 与配置是 AgentRun profile 管理数据,不是 Cloud
## ResourceBundle
- AgentRun run 必须使用 Git-only `resourceBundleRef`,默认 repo 是 `http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git`
- `commitId` 必须是完整 40 字符小写 SHA,不接受 branch、tag、`HEAD` 或短 SHA。
- `workspaceRef` 只描述目标 repo/branch,默认 branch 是 `v0.2`;实际 checkout 身份以 `resourceBundleRef.repoUrl + commitId` 为准
- 默认 `toolAliases` 会把 `tools/hwpod-cli.ts` 暴露为 `hwpod`,把 `tools/hwpod-ctl.ts` 暴露为 `hwpod-ctl`,并把 `tools/unidesk-ssh.mjs` 暴露为 runner shell 内的 `unidesk-ssh` 命令。HWPOD inspect、workspace、Keil build/download、reset 和 UART/debug-probe 操作必须优先走 `hwpod``unidesk-ssh` 只用于 UniDesk passthrough 任务,不能替代 HWPOD 标准路径
- HWPOD 快速阶段没有独立 lease 控制命令;标准 `hwpod` surface 只提交 HWPOD intent,经 compiler 生成 node-ops。Agent 不得新增 gateway shell 或其他 fallback 来绕过 `hwpod-cli -> hwpod-compiler-cli -> /v1/hwpod-node-ops -> hwpod-node`
- AgentRun run 必须使用 `ResourceBundleRef.kind="gitbundle"`,默认 repo 是 `http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git`
- `commitId` 必须是完整 40 字符小写 SHA,不接受 branch、tag、`HEAD` 或短 SHA`workspaceRef` 只描述目标 repo/branch,默认 branch 是 `v0.2`,实际 checkout 身份以 `resourceBundleRef.repoUrl + commitId` 为准
- 默认 `bundles[]` 只有两项:`{ name: "hwlab-tools", subpath: "tools", target_path: "tools" }``{ name: "hwlab-agent-skills", subpath: "skills", target_path: ".agents/skills" }`。不得再发送 `toolAliases``skillRefs``workspaceFiles``subdir``sparsePaths`
- repo 自身必须提供 extensionless 工具入口,例如 `tools/hwpod``tools/hwpod-ctl``tools/hwpod-compiler``tools/unidesk-ssh``tools/hwlab-code-agent`AgentRun 只把 materialized `tools/` 加入 PATH,不再生成 alias wrapper
- HWPOD 快速阶段没有独立 lease 控制命令;标准 `hwpod` surface 只提交 HWPOD intent,经 compiler 生成 node-ops。Agent 不得新增 gateway shell 或其他 fallback 来绕过 `hwpod -> hwpod-compiler -> /v1/hwpod-node-ops -> hwpod-node`
- 默认 `promptRefs` 指向 `internal/agent/prompts/hwlab-v02-runtime.md``inject=thread-start``required=true`。该 prompt 只在 AgentRun/Codex stdio 新 thread 首轮注入;后续 turn 必须依赖原生 `thread/resume`,不得在 command payload 中拼接历史、旧 skill 列表或长业务 prompt。
- 默认 `skillRefs` 指向 `skills/hwpod-cli/SKILL.md``skills/hwpod-ctl/SKILL.md``skills/hwlab-agent-runtime/SKILL.md`,都为 `required=true`,由 AgentRun 聚合到当前 workspace `.agents/skills/<name>/SKILL.md`。HWLAB 不再依赖 `/app/skills`、hostPath、默认 Codex skill registry、ConfigMap 或用户长 prompt 作为 skill 注入 fallback。
- skills 由 `skills/` subtree 作为整体复制到 `.agents/skills`。HWLAB 不再依赖 `/app/skills`、hostPath、默认 Codex skill registry、ConfigMap、workspace seed files、`workspaceFiles` 或用户长 prompt 作为 AgentRun runner 的 skill 注入 fallback。
- AgentRun runtime image 已预装 `gh`,结合 `tool=github` 注入的 `GH_TOKEN` 即可访问 HWLAB/UniDesk PR 与 issue;不得把 GitHub token 放入 prompt 或 `transientEnv`
## 验收
- 源码合同测试:`node --test internal/agent/agentrun-dispatch.test.mjs`
- 语法检查:`node --check internal/agent/agentrun-dispatch.mjs && node --check internal/agent/agentrun-dispatch.test.mjs`
- 合同必须证明 `UNIDESK_SSH_CLIENT_TOKEN` 不出现在 `transientEnv`GitHub/UniDesk SSH 能力都通过 AgentRun `toolCredentials` SecretRef 装配,且 runner resource bundle 默认暴露 `hwpod``unidesk-ssh``promptRefs``skillRefs`
- 合同必须证明 `UNIDESK_SSH_CLIENT_TOKEN` 不出现在 `transientEnv`GitHub/UniDesk SSH 能力都通过 AgentRun `toolCredentials` SecretRef 装配,且 runner resource bundle 默认使用 `kind="gitbundle"`trace 暴露 `bundles``tools``promptRefs``skillDirs`
- 真实 CLI 验收必须使用目标 profile 显式创建 session,例如 `--provider-profile dsflash-go`,再发送至少一个不传 `--provider-profile` 的 turn,证明 send 继承 session profile;不得因为某个 provider 失败自动切换 profile 并把结果记为原 profile 通过。
- Nested profile 继承验收必须覆盖 runner env:父 AgentRun result/trace 显示目标 `backendProfile`child `spawn` 输出 `resolvedProviderProfile=<profile>``profileSource=env``parentTraceId=<outerTraceId>`child trace/result 使用同一个 profile/model/backend。缺少任意字段时先修可见性,不新增旧 fallback、旧门禁或兼容分支。
+2 -2
View File
@@ -266,7 +266,7 @@ CaseRun 的 case registry repo 与 subject repo 必须分离。标准 registry r
agent-task CaseRun 的 evidence 必须至少记录:subject repo 本地路径、subject commit、隔离 worktree、任务 prompt 来源、Code Agent session/trace/provider/model、agent 产生的 workspace diff、验证动作和验证结果。compile-only smoke 的 evidence 不得伪造或补写 agent provenance。
Agent compile-only 阶段必须走 HWLAB Code Agent / AgentRun 的成熟入口:CaseRun 负责生成 prompt/context、创建或调用显式 Code Agent session,并通过 `case run start/status/result/logs` 轮询终态;prompt 必须要求 agent 在自己的 AgentRun workspace 内使用标准 `hwpod` / `hwpod-ctl` 执行 compile-only 验证,例如 `hwpod-ctl spec validate``hwpod inspect``hwpod build`。CaseRun 可以保留 runner 后置 compile 作为收尾或对照证据,但 evidence 必须把 agent trace 中的 HWPOD commandExecution 与 runner 后置 `hwpod-cli build` 分开记录,不得把 runner 直接编译伪装成 agent 编译。
Agent compile-only 阶段必须走 HWLAB Code Agent / AgentRun 的成熟入口:CaseRun 负责生成 prompt/context、创建或调用显式 Code Agent session,并通过 `case run start/status/result/logs` 轮询终态;prompt 必须要求 agent 在自己的 AgentRun workspace 内使用 gitbundle 装配出的标准 `hwpod` / `hwpod-ctl` 工具执行 compile-only 验证。CaseRun 可以保留 runner 后置 compile 作为收尾或对照证据,但 evidence 必须把 agent trace 中的 HWPOD commandExecution 与 runner 后置 `hwpod-cli build` 分开记录,不得把 runner 直接编译伪装成 agent 编译。
CaseRun 是强化学习 Harness 的最小执行单元,但它本身不是完整 epoch。完整 epoch 还需要样本选择、策略生成、奖励/评价、批量调度、跨 run 归因和自动训练闭环;第一阶段只要求一个 `caseId` 能从 case registry repo 进入真实 HWPOD 编译闭环,并把可审计产物稳定落盘。
@@ -276,7 +276,7 @@ CaseRun 是强化学习 Harness 的最小执行单元,但它本身不是完整
`case run` 在当前版本只负责编排流程和记录原始证据,不做 evidence 自动评价、自动打分或门禁判断。流程是:prepare 先在 subject repo 本地 checkout 下创建 `.worktree/caserun-<runId>` 隔离 worktree并写入 run-local spec,然后生成 Code Agent prompt/context,创建或调用 HWLAB Code Agent session,把 isolated worktree、run-local HWPOD spec、任务目标和约束交给 Code Agent;agent 阶段结束或超时后,CaseRun 记录 agent identity、terminal result 里已有的终态字段、trace lookup hint、subject worktree 的 `git status` / `git diff --stat` / `git diff --binary`,再按 case 定义执行 runner 后置 `hwpod-cli build``cmd.run` Keil 异步 job-status 查询,最后把原始证据和可读归档写入 case registry repo 的 `runs/<caseId>/<runId>/`。固定归档包括 `evidence.json``summary.md``run.json``result.json``agent-prompt.md``agent-diff.patch`、run-local `.hwlab/hwpod-spec.yaml`、worker stdout/stderr、`artifact-manifest.json`,以及 `agent-messages.json``agent-trace.md``agent-transcript.md``final-response.md``agent-trace.md` / `agent-transcript.md` 必须包含 trace lookup hint、renderer 标识和 subject diff 区块;如果归档了完整 rendered rows,其语义必须来自共享 Web/CLI trace renderer,不能由 CaseRun 自行发明。`final-response.md` 在 terminal result 存在权威 finalResponse 时保存正文,不存在时显式记录 `finalResponse=null`、terminal/error rows 和缺失原因。下载不作为 compile-only CaseRun 的隐式步骤;后续需要下载时,应作为独立 action 显式加入 case 定义和流程记录。
CaseRun 下发给 AgentRun 的 workspace 必须带 run-local spec 和当前 source tree 中的 HWPOD 工具种子:`tools/hwpod-cli.ts``tools/hwpod-ctl.ts``tools/hwpod-compiler-cli.ts``tools/src/hwpod-harness-lib.ts`、相关合同/endpoint resolver,以及 `.agents/skills/hwpod-cli|hwpod-ctl|hwlab-agent-runtime/SKILL.md`。这不是把 prompt 写成答案,而是保证 Code Agent 看到的 `hwpod` 能力、help、别名`cmd.run` 组合语义与当前 `v0.2` 源码一致;不得只发送 `.hwlab/hwpod-spec.yaml` 后依赖运行面预装旧工具或旧 skill 缓存。若 agent 报告标准 `hwpod` 命令缺失或 help 明显过时,优先修 CaseRun workspace seed 或 HWPOD CLI/skill 装配,不新增 hwpod-node 专用 op,也不把业务动作旁路到 shell 脚本。
CaseRun 下发给 HWLAB Code Agent 的请求不得再携带 ad hoc `workspaceFiles` 或 source tree 工具种子;当前工具和 skills 必须由 AgentRun `ResourceBundleRef.kind="gitbundle"` 装配,固定把 repo `tools/` 复制到 workspace `tools/`,把 repo `skills/` 复制到 workspace `.agents/skills`。这不是把 prompt 写成答案,而是保证 Code Agent 看到的 `hwpod` 能力、help、wrapper `cmd.run` 组合语义与当前 `v0.2` 源码一致。若 agent 报告标准 `hwpod` 命令缺失或 help 明显过时,优先修 gitbundle runtime assembly 或 HWPOD CLI/skill 装配,不新增 hwpod-node 专用 op,也不把业务动作旁路到 shell 脚本。
CaseRun 的 trace 处理必须遵循 [spec-v02-code-agent-trace.md](spec-v02-code-agent-trace.md) 的组合优先原则:CaseRun 只负责稳定返回和归档 `traceId``sessionId``conversationId``threadId`、AgentRun 执行线索以及 `traceLookup.commands.*`,完整 trace/result/session 读取继续使用已有 `hwlab-cli client agent trace/result/inspect`。CaseRun 不应在自身内部再实现一套 `/v1/agent/chat/trace/{traceId}` 查询器、row renderer、session 反查或 final response 归因逻辑;如果已有 agent CLI 的 trace 可见性不足,先改进 `client agent trace/result/inspect`,再让 CaseRun 输出新的 ID 或 hint。归档中的 `agent-trace.json` 可以是 `lookupOnly=true` 的 identity manifest,表示“用这些 ID 调既有 CLI 查完整 trace”,不能伪装成完整 trace dump。
@@ -7,7 +7,7 @@ Code Agent 第一版 skill 来源分为两类:预装 skill 读取镜像内只
## 在系统中的职责划分
- 证明当前 lane 中 skills bundle 随 source commit 发布,并为 live-build inventory 和 M4/M5 agent-loop 验收提供 health metadata。
-`hwlab-cloud-api` 注入给 AgentRun v0.1 的 `skillRefs` 和 skills commit/version readiness 对齐。
-`hwlab-cloud-api` 注入给 AgentRun v0.1 的 `kind="gitbundle"` skills subtree 和 skills commit/version readiness 对齐。
- 预装 skill 只由镜像内 `/app/skills` 提供,不保存用户数据,不保存 Secret,不执行用户代码。
- 用户上传 skill 由 `hwlab-cloud-api` 管理,普通文件直接落到 PVC `/data/user-skills`,本服务不承载上传、预览或用户数据持久化 API。
@@ -33,10 +33,10 @@ Code Agent 第一版 skill 来源分为两类:预装 skill 读取镜像内只
## Code Agent 发现路径
- HWLAB 旧自有发现路径只作为历史兼容线索;当前 AgentRun 装配以 `ResourceBundleRef.skillRefs` 为权威,详见 [agentrun-code-agent-dispatch.md](agentrun-code-agent-dispatch.md)。
- HWLAB 旧自有发现路径只作为历史线索;当前 AgentRun 装配以 `ResourceBundleRef.kind="gitbundle"``bundles[]` 为权威,详见 [agentrun-code-agent-dispatch.md](agentrun-code-agent-dispatch.md)。
- Codex 原生发现路径:Codex 会从工作区 `.agents/skills` 等标准位置加载 skill;因此 `hwlab-cloud-api` 必须准备 `/workspace/hwlab/.agents/skills` symlink 聚合目录,让 Codex 原生机制也能看到 `/app/skills``/data/user-skills` 中的 skill。
- 两条发现路径都必须保留来源信息;同名 skill 不能因为 HWLAB 侧去重而消失。
- AgentRun 装配发现路径:HWLAB v0.2 通过 AgentRun `ResourceBundleRef.skillRefs` 把 repo 内 `skills/hwpod-cli/SKILL.md``skills/hwpod-ctl/SKILL.md``skills/hwlab-agent-runtime/SKILL.md` 装配到 runner 工作区 `.agents/skills``GET /v1/skills` 必须额外返回 `agentRunAssembly` 摘要,包含 `skillRefs``promptRefs``toolAliases`资源 bundle repo/branch/commit 和 `.agents/skills` 运行时装配目录;Cloud Web skills 面板必须展示该摘要,不能只展示 `/app/skills``/data/user-skills` 的静态列表。
- AgentRun 装配发现路径:HWLAB v0.2 通过 AgentRun `ResourceBundleRef.kind="gitbundle"` 把 repo 内 `skills/` subtree 整体装配到 runner 工作区 `.agents/skills``GET /v1/skills` 必须额外返回 `agentRunAssembly` 摘要,包含 `bundles``promptRefs`、资源 bundle repo/branch/commit 和 `.agents/skills` 运行时装配目录;Cloud Web skills 面板必须展示该摘要,不能只展示 `/app/skills``/data/user-skills` 的静态列表。
- MiniMax-M3 runner 引导必须说明工具调用 JSON、BusyBox/GNU 工具差异和 GitHub CLI 读 issue/PR 的低噪声用法,降低无效 tool-call arguments 与命令兼容性摩擦。
## API 接口说明
+29 -55
View File
@@ -6,26 +6,18 @@ export const DEFAULT_HWLAB_AGENTRUN_REPO_URL = "http://git-mirror-http.devops-in
export const DEFAULT_HWLAB_AGENTRUN_BRANCH = "v0.2";
export const DEFAULT_HWLAB_AGENTRUN_BACKEND_PROFILE = "deepseek";
export const DEFAULT_UNIDESK_MAIN_SERVER_ENV = "UNIDESK_MAIN_SERVER_IP";
export const DEFAULT_HWLAB_AGENTRUN_TOOL_ALIASES = Object.freeze([
{ name: "hwpod", path: "tools/hwpod-cli.ts", kind: "bun-script" },
{ name: "hwpod-ctl", path: "tools/hwpod-ctl.ts", kind: "bun-script" },
{ name: "hwpod-compiler", path: "tools/hwpod-compiler-cli.ts", kind: "bun-script" },
{ name: "unidesk-ssh", path: "tools/unidesk-ssh.mjs", kind: "bun-script" }
export const DEFAULT_HWLAB_AGENTRUN_GIT_BUNDLES = Object.freeze([
{ name: "hwlab-tools", subpath: "tools", target_path: "tools" },
{ name: "hwlab-agent-skills", subpath: "skills", target_path: ".agents/skills" }
]);
export const DEFAULT_HWLAB_AGENTRUN_PROMPT_REFS = Object.freeze([
{ name: "hwlab-v02-runtime", path: "internal/agent/prompts/hwlab-v02-runtime.md", inject: "thread-start", required: true }
]);
export const DEFAULT_HWLAB_AGENTRUN_SKILL_REFS = Object.freeze([
{ name: "hwpod-cli", path: "skills/hwpod-cli/SKILL.md", required: true, aggregateAs: "hwpod-cli" },
{ name: "hwpod-ctl", path: "skills/hwpod-ctl/SKILL.md", required: true, aggregateAs: "hwpod-ctl" },
{ name: "hwlab-agent-runtime", path: "skills/hwlab-agent-runtime/SKILL.md", required: true, aggregateAs: "hwlab-agent-runtime" }
]);
export function createHwlabAgentRunAssemblySummary(options = {}) {
const env = options.env ?? process.env;
const toolAliases = normalizeResourceToolAliases(options.toolAliases ?? DEFAULT_HWLAB_AGENTRUN_TOOL_ALIASES);
const bundles = normalizeResourceGitBundles(options.bundles ?? DEFAULT_HWLAB_AGENTRUN_GIT_BUNDLES);
const promptRefs = normalizeResourcePromptRefs(options.promptRefs ?? DEFAULT_HWLAB_AGENTRUN_PROMPT_REFS);
const skillRefs = normalizeResourceSkillRefs(options.skillRefs ?? DEFAULT_HWLAB_AGENTRUN_SKILL_REFS);
const promptAssembly = createPromptAssemblySummary(promptRefs);
const repoUrl = optionalString(options.repoUrl ?? env.HWLAB_CODE_AGENT_AGENTRUN_REPO_URL ?? env.AGENTRUN_REPO_URL) ?? DEFAULT_HWLAB_AGENTRUN_REPO_URL;
const branch = optionalString(options.branch ?? env.HWLAB_BOOT_REF ?? env.HWLAB_CODE_AGENT_AGENTRUN_BRANCH ?? env.AGENTRUN_BRANCH) ?? DEFAULT_HWLAB_AGENTRUN_BRANCH;
@@ -33,26 +25,24 @@ export function createHwlabAgentRunAssemblySummary(options = {}) {
const namespace = optionalString(options.namespace ?? env.AGENTRUN_RUNTIME_NAMESPACE ?? env.HWLAB_CODE_AGENT_AGENTRUN_NAMESPACE) ?? DEFAULT_AGENTRUN_NAMESPACE;
const managerUrl = optionalString(options.managerUrl ?? env.HWLAB_CODE_AGENT_AGENTRUN_MGR_URL ?? env.AGENTRUN_MANAGER_URL) ?? DEFAULT_AGENTRUN_MANAGER_URL;
return {
status: skillRefs.length > 0 ? "ready" : "degraded",
status: bundles.length > 0 ? "ready" : "degraded",
provider: "agentrun-v01",
namespace,
managerUrl,
resourceBundle: {
kind: "git",
kind: "gitbundle",
repoUrl,
branch,
commitId,
skillsDir: ".agents/skills"
},
counts: {
toolAliases: toolAliases.length,
promptRefs: promptRefs.length,
skillRefs: skillRefs.length
bundles: bundles.length,
promptRefs: promptRefs.length
},
toolAliases: toolAliases.map((item) => ({ ...item })),
bundles: bundles.map((item) => ({ ...item })),
promptRefs: promptRefs.map((item) => ({ ...item })),
promptAssembly,
skillRefs: skillRefs.map((item) => ({ ...item })),
runtimeEnv: {
assembledRuntime: "HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME",
skillsDirs: "HWLAB_CODE_AGENT_SKILLS_DIRS"
@@ -163,9 +153,8 @@ export function createHwlabAgentRunDispatchAssembly(options = {}) {
const branch = nonEmptyString(options.branch ?? DEFAULT_HWLAB_AGENTRUN_BRANCH, "branch");
const timeoutMs = positiveInteger(options.timeoutMs ?? 600000, "timeoutMs");
const unideskMainServerIp = optionalString(options.unideskMainServerIp ?? env.UNIDESK_MAIN_SERVER_IP ?? env.UNIDESK_MAIN_SERVER_HOST);
const toolAliases = normalizeResourceToolAliases(options.toolAliases ?? DEFAULT_HWLAB_AGENTRUN_TOOL_ALIASES);
const bundles = normalizeResourceGitBundles(options.bundles ?? DEFAULT_HWLAB_AGENTRUN_GIT_BUNDLES);
const promptRefs = normalizeResourcePromptRefs(options.promptRefs ?? DEFAULT_HWLAB_AGENTRUN_PROMPT_REFS);
const skillRefs = normalizeResourceSkillRefs(options.skillRefs ?? DEFAULT_HWLAB_AGENTRUN_SKILL_REFS);
if (includeUnideskSshToolCredential && !unideskMainServerIp) {
throw new Error("unideskMainServerIp is required when UniDesk SSH passthrough is enabled");
}
@@ -178,14 +167,11 @@ export function createHwlabAgentRunDispatchAssembly(options = {}) {
const transientEnv = normalizeTransientEnv(transientEnvWithUnideskMainServer(options.transientEnv ?? [], unideskMainServerIp));
const sessionRef = createSessionRef(options);
const resourceBundleRef = {
kind: "git",
kind: "gitbundle",
repoUrl,
commitId,
...(options.subdir ? { subdir: nonEmptyString(options.subdir, "subdir") } : {}),
...(Array.isArray(options.sparsePaths) ? { sparsePaths: options.sparsePaths.map((item, index) => nonEmptyString(item, `sparsePaths[${index}]`)) } : {}),
...(toolAliases.length > 0 ? { toolAliases } : {}),
bundles,
...(promptRefs.length > 0 ? { promptRefs } : {}),
...(skillRefs.length > 0 ? { skillRefs } : {}),
...(options.resourceCredentialRef ? { credentialRef: options.resourceCredentialRef } : {})
};
@@ -246,9 +232,8 @@ export function createHwlabAgentRunDispatchAssembly(options = {}) {
githubCredentialSource: includeGithubToolCredential ? "toolCredentials" : null,
unideskSshCredentialSource: includeUnideskSshToolCredential ? "toolCredentials" : null,
unideskMainServerSource: unideskMainServerIp ? "transientEnv" : null,
toolAliases: toolAliases.map((item) => item.name),
bundles: bundles.map((item) => item.name),
promptRefs: promptRefs.map((item) => item.name),
skillRefs: skillRefs.map((item) => item.name),
transientEnvNames: transientEnv.map((item) => item.name),
forbiddenTransientEnvNames: AGENTRUN_REUSABLE_CREDENTIAL_ENV_NAMES.slice()
}
@@ -325,21 +310,19 @@ function normalizeToolCredentials(items) {
});
}
function normalizeResourceToolAliases(items) {
if (!Array.isArray(items)) throw new Error("toolAliases must be an array");
const allowedKinds = new Set(["node-script", "bun-script", "sh-script", "executable"]);
function normalizeResourceGitBundles(items) {
if (!Array.isArray(items)) throw new Error("bundles must be an array");
const seen = new Set();
return items.map((item, index) => {
if (!item || typeof item !== "object") throw new Error(`toolAliases[${index}] must be an object`);
const name = nonEmptyString(item.name, `toolAliases[${index}].name`);
const aliasPath = nonEmptyString(item.path, `toolAliases[${index}].path`);
const kind = nonEmptyString(item.kind, `toolAliases[${index}].kind`);
if (!/^[a-z][a-z0-9._-]{0,62}$/u.test(name)) throw new Error(`toolAliases[${index}].name must be a lowercase command name`);
if (seen.has(name)) throw new Error(`toolAliases name ${name} is duplicated`);
if (aliasPath.startsWith("/") || aliasPath.includes("..")) throw new Error(`toolAliases[${index}].path must stay within the checkout`);
if (!allowedKinds.has(kind)) throw new Error(`toolAliases[${index}].kind is not supported`);
if (!item || typeof item !== "object") throw new Error(`bundles[${index}] must be an object`);
const name = resourceName(item.name, `bundles[${index}].name`);
if (seen.has(name)) throw new Error(`bundles name ${name} is duplicated`);
seen.add(name);
return { name, path: aliasPath, kind };
return {
name,
subpath: resourcePath(item.subpath, `bundles[${index}].subpath`),
target_path: workspaceTargetPath(item.target_path ?? item.targetPath, `bundles[${index}].target_path`)
};
});
}
@@ -358,21 +341,6 @@ function normalizeResourcePromptRefs(items) {
});
}
function normalizeResourceSkillRefs(items) {
if (!Array.isArray(items)) throw new Error("skillRefs must be an array");
const seen = new Set();
return items.map((item, index) => {
if (!item || typeof item !== "object") throw new Error(`skillRefs[${index}] must be an object`);
const name = resourceName(item.name, `skillRefs[${index}].name`);
if (seen.has(name)) throw new Error(`skillRefs name ${name} is duplicated`);
seen.add(name);
const refPath = resourcePath(item.path, `skillRefs[${index}].path`);
if (!refPath.endsWith("SKILL.md")) throw new Error(`skillRefs[${index}].path must point to SKILL.md`);
const aggregateAs = optionalString(item.aggregateAs);
return { name, path: refPath, required: item.required !== false, ...(aggregateAs ? { aggregateAs: resourceName(aggregateAs, `skillRefs[${index}].aggregateAs`) } : {}) };
});
}
function resourceName(value, fieldName) {
const name = nonEmptyString(value, fieldName);
if (!/^[a-z][a-z0-9._-]{0,62}$/u.test(name)) throw new Error(`${fieldName} must be a lowercase resource name`);
@@ -385,6 +353,12 @@ function resourcePath(value, fieldName) {
return refPath;
}
function workspaceTargetPath(value, fieldName) {
const targetPath = nonEmptyString(value, fieldName);
if (targetPath === "." || targetPath.startsWith("/") || targetPath.includes("..")) throw new Error(`${fieldName} must stay within the workspace`);
return targetPath;
}
async function postJson(fetchImpl, managerUrl, path, payload) {
const response = await fetchImpl(`${managerUrl}${path}`, {
method: "POST",
+19 -25
View File
@@ -25,25 +25,20 @@ test("HWLAB AgentRun assembly grants GitHub and UniDesk SSH through toolCredenti
assert.equal(assembly.runPayload.providerId, "G14");
assert.equal(assembly.runPayload.backendProfile, "deepseek");
assert.equal(assembly.runPayload.workspaceRef.branch, "v0.2");
assert.equal(assembly.runPayload.resourceBundleRef.kind, "gitbundle");
assert.equal(assembly.runPayload.resourceBundleRef.commitId, commitId);
assert.deepEqual(assembly.runPayload.resourceBundleRef.toolAliases.map((item) => item.name), ["hwpod", "hwpod-ctl", "hwpod-compiler", "unidesk-ssh"]);
assert.equal(assembly.runPayload.resourceBundleRef.toolAliases[0].path, "tools/hwpod-cli.ts");
assert.equal(assembly.runPayload.resourceBundleRef.toolAliases[1].path, "tools/hwpod-ctl.ts");
assert.equal(assembly.runPayload.resourceBundleRef.toolAliases[2].path, "tools/hwpod-compiler-cli.ts");
assert.deepEqual(assembly.runPayload.resourceBundleRef.bundles, [
{ name: "hwlab-tools", subpath: "tools", target_path: "tools" },
{ name: "hwlab-agent-skills", subpath: "skills", target_path: ".agents/skills" }
]);
assert.deepEqual(assembly.runPayload.resourceBundleRef.promptRefs, [
{ name: "hwlab-v02-runtime", path: "internal/agent/prompts/hwlab-v02-runtime.md", inject: "thread-start", required: true }
]);
assert.deepEqual(assembly.runPayload.resourceBundleRef.skillRefs, [
{ name: "hwpod-cli", path: "skills/hwpod-cli/SKILL.md", required: true, aggregateAs: "hwpod-cli" },
{ name: "hwpod-ctl", path: "skills/hwpod-ctl/SKILL.md", required: true, aggregateAs: "hwpod-ctl" },
{ name: "hwlab-agent-runtime", path: "skills/hwlab-agent-runtime/SKILL.md", required: true, aggregateAs: "hwlab-agent-runtime" }
]);
assert.equal(Object.hasOwn(assembly.runPayload.resourceBundleRef, "toolAliases"), false);
assert.equal(Object.hasOwn(assembly.runPayload.resourceBundleRef, "skillRefs"), false);
assert.equal(Object.hasOwn(assembly.runPayload.resourceBundleRef, "workspaceFiles"), false);
assert.deepEqual(assembly.boundaries.promptRefs, ["hwlab-v02-runtime"]);
assert.deepEqual(assembly.boundaries.skillRefs, ["hwpod-cli", "hwpod-ctl", "hwlab-agent-runtime"]);
assert.equal(assembly.boundaries.toolAliases.includes("hwpod"), true);
assert.equal(assembly.boundaries.toolAliases.includes("hwpod-ctl"), true);
assert.equal(assembly.boundaries.toolAliases.includes("hwpod-compiler"), true);
assert.equal(assembly.boundaries.toolAliases.includes("unidesk-ssh"), true);
assert.deepEqual(assembly.boundaries.bundles, ["hwlab-tools", "hwlab-agent-skills"]);
assert.equal(assembly.commandPayload.payload.traceId, "trc_hwlab_agentrun_001");
assert.equal(assembly.commandPayload.payload.threadId, "thread_001");
@@ -75,28 +70,27 @@ test("HWLAB AgentRun assembly rejects reusable credentials in transientEnv", ()
);
});
test("HWLAB AgentRun assembly allows explicit resource tool aliases", () => {
test("HWLAB AgentRun assembly allows explicit gitbundle items", () => {
const assembly = createHwlabAgentRunDispatchAssembly({
traceId: "trc_hwlab_agentrun_alias_override",
prompt: "alias override",
traceId: "trc_hwlab_agentrun_bundle_override",
prompt: "bundle override",
commitId,
unideskMainServerIp: "https://unidesk.example.test",
toolAliases: [{ name: "status-tool", path: "tools/status.mjs", kind: "node-script" }]
bundles: [{ name: "status-tools", subpath: "tools/status", target_path: "tools/status" }]
});
assert.deepEqual(assembly.runPayload.resourceBundleRef.toolAliases, [{ name: "status-tool", path: "tools/status.mjs", kind: "node-script" }]);
assert.deepEqual(assembly.runPayload.resourceBundleRef.bundles, [{ name: "status-tools", subpath: "tools/status", target_path: "tools/status" }]);
});
test("HWLAB AgentRun assembly allows explicit resource prompt and skill refs", () => {
test("HWLAB AgentRun assembly allows explicit resource prompt refs", () => {
const assembly = createHwlabAgentRunDispatchAssembly({
traceId: "trc_hwlab_agentrun_prompt_skill_override",
prompt: "prompt skill override",
traceId: "trc_hwlab_agentrun_prompt_override",
prompt: "prompt override",
commitId,
unideskMainServerIp: "https://unidesk.example.test",
promptRefs: [{ name: "custom-prompt", path: "internal/agent/prompts/custom.md", inject: "thread-start", required: true }],
skillRefs: [{ name: "custom-skill", path: "skills/custom-skill/SKILL.md", required: true, aggregateAs: "custom-skill" }]
promptRefs: [{ name: "custom-prompt", path: "internal/agent/prompts/custom.md", inject: "thread-start", required: true }]
});
assert.deepEqual(assembly.runPayload.resourceBundleRef.promptRefs, [{ name: "custom-prompt", path: "internal/agent/prompts/custom.md", inject: "thread-start", required: true }]);
assert.deepEqual(assembly.runPayload.resourceBundleRef.skillRefs, [{ name: "custom-skill", path: "skills/custom-skill/SKILL.md", required: true, aggregateAs: "custom-skill" }]);
assert.equal(Object.hasOwn(assembly.runPayload.resourceBundleRef, "skillRefs"), false);
});
test("HWLAB AgentRun assembly does not duplicate explicit UniDesk main-server env", () => {
+2 -2
View File
@@ -4,7 +4,7 @@ You are running inside the HWLAB v0.2 Code Agent runtime assembled by AgentRun.
Use the repo-local resource bundle as the source of runtime rules:
- Required skills are mounted from `ResourceBundleRef.skillRefs` into `.agents/skills` for this run.
- Required runtime files are mounted from `ResourceBundleRef.kind="gitbundle"`: repo `tools/` is copied to workspace `tools/`, and repo `skills/` is copied to workspace `.agents/skills` for this run.
- The expected HWLAB skills are `hwpod-cli`, `hwpod-ctl`, and `hwlab-agent-runtime`.
- Use `hwpod` from PATH for HWPOD work. It is the standard `hwpod-cli` entry and must read `.hwlab/hwpod-spec.yaml`, call `hwpod-compiler-cli`, submit `/v1/hwpod-node-ops`, and wait for `hwpod-node` results.
- The runner workspace should include a preinstalled `.hwlab/hwpod-spec.yaml`. When the user asks which HWPOD or `hwpod-spec` is available, first query `${HWLAB_RUNTIME_API_URL}/v1/hwpod/specs?probe=1` or validate the preinstalled spec; report the discovered spec count and names before explaining concepts.
@@ -13,7 +13,7 @@ Use the repo-local resource bundle as the source of runtime rules:
Do not use fallback execution paths:
- Do not rely on Codex default system skills as a substitute for HWLAB bundle skills.
- Do not read `/app/skills`, host skill directories, ConfigMaps, or user-provided long prompts as a substitute for `ResourceBundleRef.skillRefs`.
- Do not read `/app/skills`, host skill directories, ConfigMaps, workspace seed files, `workspaceFiles`, or user-provided long prompts as a substitute for `ResourceBundleRef.kind="gitbundle"`.
- Do not manually concatenate prior user/assistant messages. Conversation continuity must come from Codex stdio `thread/resume` only.
When the user asks what skills are visible, mention the HWLAB bundle skills by name and manifest path before any generic model/system skill list.
+15 -43
View File
@@ -32,22 +32,13 @@ const AGENTRUN_BACKEND_PROFILE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/u;
const THREAD_CONTINUITY_POLICY = "hwlab-agentrun-v01-reuse-runner-thread";
const SESSION_POLICY_RUN_LOCAL = "hwlab-agentrun-v01-session-runner-reuse";
const TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "blocked", "cancelled", "canceled"]);
const HWLAB_RESOURCE_TOOL_ALIASES = Object.freeze([
Object.freeze({ name: "hwpod", path: "tools/hwpod-cli.ts", kind: "bun-script" }),
Object.freeze({ name: "hwpod-ctl", path: "tools/hwpod-ctl.ts", kind: "bun-script" }),
Object.freeze({ name: "hwpod-compiler", path: "tools/hwpod-compiler-cli.ts", kind: "bun-script" }),
Object.freeze({ name: "unidesk-ssh", path: "tools/unidesk-ssh.mjs", kind: "bun-script" }),
Object.freeze({ name: "hwlab-code-agent", path: "tools/hwlab-code-agent-cli.ts", kind: "bun-script" })
const HWLAB_RESOURCE_GIT_BUNDLES = Object.freeze([
Object.freeze({ name: "hwlab-tools", subpath: "tools", target_path: "tools" }),
Object.freeze({ name: "hwlab-agent-skills", subpath: "skills", target_path: ".agents/skills" })
]);
const HWLAB_RESOURCE_PROMPT_REFS = Object.freeze([
Object.freeze({ name: "hwlab-v02-runtime", path: "internal/agent/prompts/hwlab-v02-runtime.md", inject: "thread-start", required: true })
]);
const HWLAB_RESOURCE_SKILL_REFS = Object.freeze([
Object.freeze({ name: "hwpod-cli", path: "skills/hwpod-cli/SKILL.md", required: true, aggregateAs: "hwpod-cli" }),
Object.freeze({ name: "hwpod-ctl", path: "skills/hwpod-ctl/SKILL.md", required: true, aggregateAs: "hwpod-ctl" }),
Object.freeze({ name: "hwlab-agent-runtime", path: "skills/hwlab-agent-runtime/SKILL.md", required: true, aggregateAs: "hwlab-agent-runtime" }),
Object.freeze({ name: "hwlab-code-agent", path: "skills/hwlab-code-agent/SKILL.md", required: true, aggregateAs: "hwlab-code-agent" })
]);
export function codeAgentAgentRunAdapterEnabled(env = process.env) {
const value = String(env.HWLAB_CODE_AGENT_ADAPTER ?? env.HWLAB_CODE_AGENT_PROVIDER ?? "").trim().toLowerCase();
@@ -798,18 +789,15 @@ function buildAgentRunCreateRunInput({ params, env, traceId, backendProfile, ses
const commitId = requireAgentRunSourceCommit(env);
const threadId = safeOpaqueId(params.threadId);
const hwlabProjectId = firstNonEmpty(params.projectId);
const toolAliases = filteredResourceToolAliases(toolCapabilities);
const workspaceFiles = normalizeResourceWorkspaceFiles(params.workspaceFiles ?? params.resourceWorkspaceFiles);
rejectRemovedResourceWorkspaceFiles(params.workspaceFiles ?? params.resourceWorkspaceFiles);
const resourceBundleRef = {
kind: "git",
kind: "gitbundle",
repoUrl: resolveAgentRunRepoUrl(env),
commitId,
submodules: false,
lfs: false,
toolAliases,
bundles: HWLAB_RESOURCE_GIT_BUNDLES,
promptRefs: HWLAB_RESOURCE_PROMPT_REFS,
skillRefs: HWLAB_RESOURCE_SKILL_REFS,
...(workspaceFiles.length > 0 ? { workspaceFiles } : {})
};
return {
tenantId: firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_TENANT_ID, DEFAULT_TENANT_ID),
@@ -863,19 +851,9 @@ function buildAgentRunCreateRunInput({ params, env, traceId, backendProfile, ses
};
}
function normalizeResourceWorkspaceFiles(value) {
function rejectRemovedResourceWorkspaceFiles(value) {
if (value == null || value === false || value === "") return [];
if (!Array.isArray(value)) throw adapterError("invalid_workspace_files", "workspaceFiles must be an array when provided");
return value.map((item, index) => {
if (!item || typeof item !== "object" || Array.isArray(item)) throw adapterError("invalid_workspace_file", `workspaceFiles[${index}] must be an object`);
const pathValue = text(item.path);
const content = typeof item.content === "string" ? item.content : "";
const encoding = text(item.encoding) || "utf8";
if (!pathValue || pathValue === "." || pathValue.startsWith("/") || pathValue.includes("..") || pathValue.includes("\\")) throw adapterError("invalid_workspace_file_path", `workspaceFiles[${index}].path must be a safe relative POSIX path`);
if (typeof item.content !== "string") throw adapterError("invalid_workspace_file_content", `workspaceFiles[${index}].content must be a utf8 string`);
if (encoding !== "utf8") throw adapterError("invalid_workspace_file_encoding", `workspaceFiles[${index}].encoding must be utf8`);
return { path: pathValue, content, encoding };
});
throw adapterError("legacy_workspace_files_removed", "workspaceFiles/resourceWorkspaceFiles are removed; use AgentRun ResourceBundleRef kind=gitbundle with bundles[]");
}
function buildAgentRunCommandInput({ params, traceId, backendProfile, sessionId }) {
@@ -1002,16 +980,6 @@ async function resolveToolCapabilities({ params = {}, options = {} } = {}) {
return await accessController.codeAgentToolCapabilitiesForOwner(ownerUserId);
}
function filteredResourceToolAliases(toolCapabilities = null) {
return HWLAB_RESOURCE_TOOL_ALIASES.filter((alias) => {
if (alias.name === "hwpod" || alias.name === "hwpod-ctl" || alias.name === "hwpod-compiler") {
return toolCapabilityAllowed(toolCapabilities, "hwpod");
}
if (alias.name === "unidesk-ssh") return toolCapabilityAllowed(toolCapabilities, "unidesk_ssh") || toolCapabilityAllowed(toolCapabilities, "trans_cmd");
return true;
});
}
function toolCapabilityAllowed(toolCapabilities = null, toolId) {
if (!toolCapabilities || !toolCapabilities.tools) return true;
return toolCapabilities.tools?.[toolId]?.allowed === true;
@@ -1590,9 +1558,10 @@ function agentRunBackendStatusDetails(phase, payload = {}) {
return compactObject({
kind: firstNonEmpty(payload.kind),
commitId: firstNonEmpty(payload.commitId),
toolAliases: agentRunResourceSummary(payload.toolAliases),
bundles: agentRunResourceSummary(payload.bundles),
tools: agentRunResourceSummary(payload.tools),
promptRefs: agentRunResourceSummary(payload.promptRefs),
skillRefs: agentRunResourceSummary(payload.skillRefs),
skillDirs: agentRunResourceSummary(payload.skillDirs),
initialPrompt: agentRunResourceSummary(payload.initialPrompt),
valuesPrinted: false
});
@@ -1608,8 +1577,11 @@ function agentRunResourceSummary(value) {
bytes: Number.isInteger(value.bytes) ? value.bytes : undefined,
sha256: firstNonEmpty(value.sha256),
promptRefCount: Number.isInteger(value.promptRefCount) ? value.promptRefCount : undefined,
skillRefCount: Number.isInteger(value.skillRefCount) ? value.skillRefCount : undefined,
skillCount: Number.isInteger(value.skillCount) ? value.skillCount : undefined,
toolCount: Number.isInteger(value.toolCount) ? value.toolCount : undefined,
names: Array.isArray(value.names) ? value.names.map((item) => firstNonEmpty(item)).filter(Boolean).slice(0, 20) : undefined,
targetPaths: Array.isArray(value.targetPaths) ? value.targetPaths.map((item) => firstNonEmpty(item)).filter(Boolean).slice(0, 20) : undefined,
skillNames: Array.isArray(value.skillNames) ? value.skillNames.map((item) => firstNonEmpty(item)).filter(Boolean).slice(0, 20) : undefined,
requiredMissing: Array.isArray(value.requiredMissing) ? value.requiredMissing.map((item) => firstNonEmpty(item)).filter(Boolean).slice(0, 20) : undefined,
valuesPrinted: false
});
+29 -27
View File
@@ -127,9 +127,14 @@ test("AgentRun adapter filters resource tools and credentials through access cap
assert.equal(result.status, "running");
const createRun = calls.find((call) => call.method === "POST" && call.path === "/api/v1/runs");
assert.deepEqual(createRun.body.resourceBundleRef.toolAliases, [
{ name: "hwlab-code-agent", path: "tools/hwlab-code-agent-cli.ts", kind: "bun-script" }
assert.equal(createRun.body.resourceBundleRef.kind, "gitbundle");
assert.deepEqual(createRun.body.resourceBundleRef.bundles, [
{ name: "hwlab-tools", subpath: "tools", target_path: "tools" },
{ name: "hwlab-agent-skills", subpath: "skills", target_path: ".agents/skills" }
]);
assert.equal(Object.hasOwn(createRun.body.resourceBundleRef, "toolAliases"), false);
assert.equal(Object.hasOwn(createRun.body.resourceBundleRef, "skillRefs"), false);
assert.equal(Object.hasOwn(createRun.body.resourceBundleRef, "workspaceFiles"), false);
assert.deepEqual(createRun.body.executionPolicy.secretScope.toolCredentials, []);
const runnerJob = calls.find((call) => call.method === "POST" && call.path === "/api/v1/runs/run_access_tools/runner-jobs");
assert.equal(runnerJob.body.transientEnv.some((entry) => entry.name === "HWLAB_API_KEY"), false);
@@ -295,25 +300,17 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
assert.equal(body.backendProfile, "deepseek");
assert.equal(body.resourceBundleRef.repoUrl, "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git");
assert.equal(body.resourceBundleRef.commitId, "0123456789abcdef0123456789abcdef01234567");
assert.deepEqual(body.resourceBundleRef.toolAliases, [
{ name: "hwpod", path: "tools/hwpod-cli.ts", kind: "bun-script" },
{ name: "hwpod-ctl", path: "tools/hwpod-ctl.ts", kind: "bun-script" },
{ name: "hwpod-compiler", path: "tools/hwpod-compiler-cli.ts", kind: "bun-script" },
{ name: "unidesk-ssh", path: "tools/unidesk-ssh.mjs", kind: "bun-script" },
{ name: "hwlab-code-agent", path: "tools/hwlab-code-agent-cli.ts", kind: "bun-script" }
assert.equal(body.resourceBundleRef.kind, "gitbundle");
assert.deepEqual(body.resourceBundleRef.bundles, [
{ name: "hwlab-tools", subpath: "tools", target_path: "tools" },
{ name: "hwlab-agent-skills", subpath: "skills", target_path: ".agents/skills" }
]);
assert.deepEqual(body.resourceBundleRef.promptRefs, [
{ name: "hwlab-v02-runtime", path: "internal/agent/prompts/hwlab-v02-runtime.md", inject: "thread-start", required: true }
]);
assert.deepEqual(body.resourceBundleRef.skillRefs, [
{ name: "hwpod-cli", path: "skills/hwpod-cli/SKILL.md", required: true, aggregateAs: "hwpod-cli" },
{ name: "hwpod-ctl", path: "skills/hwpod-ctl/SKILL.md", required: true, aggregateAs: "hwpod-ctl" },
{ name: "hwlab-agent-runtime", path: "skills/hwlab-agent-runtime/SKILL.md", required: true, aggregateAs: "hwlab-agent-runtime" },
{ name: "hwlab-code-agent", path: "skills/hwlab-code-agent/SKILL.md", required: true, aggregateAs: "hwlab-code-agent" }
]);
assert.deepEqual(body.resourceBundleRef.workspaceFiles, [
{ path: ".hwlab/hwpod-spec.yaml", content: "kind: Hwpod\nspec:\n workspace:\n path: F:\\Work\\CASE\n", encoding: "utf8" }
]);
assert.equal(Object.hasOwn(body.resourceBundleRef, "toolAliases"), false);
assert.equal(Object.hasOwn(body.resourceBundleRef, "skillRefs"), false);
assert.equal(Object.hasOwn(body.resourceBundleRef, "workspaceFiles"), false);
const toolCredentials = body.executionPolicy.secretScope.toolCredentials;
assert.equal(toolCredentials.some((item) => item.tool === "github" && item.projection.envName === "GH_TOKEN" && item.secretRef.name === "agentrun-v01-tool-github-pr"), true);
assert.equal(toolCredentials.some((item) => item.tool === "unidesk-ssh" && item.projection.envName === "UNIDESK_SSH_CLIENT_TOKEN" && item.secretRef.name === "agentrun-v01-tool-unidesk-ssh"), true);
@@ -379,7 +376,9 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
"HWLAB_RUNTIME_LANE",
"HWLAB_RUNTIME_ENDPOINT_LOCKED",
"HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME",
"UNIDESK_MAIN_SERVER_IP"
"UNIDESK_MAIN_SERVER_IP",
"HWLAB_CODE_AGENT_PROVIDER_PROFILE",
"HWLAB_CODE_AGENT_PARENT_TRACE_ID"
]);
const transientEnv = Object.fromEntries(body.transientEnv.map((entry) => [entry.name, entry.value]));
assert.equal(transientEnv.HWLAB_RUNTIME_API_URL, "http://hwlab-cloud-api.hwlab-v02.svc.cluster.local:6667");
@@ -389,7 +388,9 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
assert.equal(transientEnv.HWLAB_RUNTIME_ENDPOINT_LOCKED, "1");
assert.equal(transientEnv.HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME, "1");
assert.equal(transientEnv.UNIDESK_MAIN_SERVER_IP, "http://74.48.78.17:18081");
assert.ok(body.transientEnv.length >= 7);
assert.equal(transientEnv.HWLAB_CODE_AGENT_PROVIDER_PROFILE, "deepseek");
assert.equal(transientEnv.HWLAB_CODE_AGENT_PARENT_TRACE_ID, secondRunnerJob ? "trc_server-test-agentrun-adapter-second" : "trc_server-test-agentrun-adapter");
assert.ok(body.transientEnv.length >= 9);
assert.equal(Object.hasOwn(transientEnv, "UNIDESK_SSH_CLIENT_TOKEN"), false);
assert.equal(Object.hasOwn(transientEnv, "GH_TOKEN"), false);
return send({
@@ -410,16 +411,16 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
if (second) return send({ items: [
{ id: "evt_old_tail", runId: "run_hwlab_adapter", seq: 6, type: "assistant_message", payload: { commandId: "cmd_hwlab_adapter", text: "旧 command 尾部不应进入第二轮。" }, createdAt: "2026-06-01T00:00:02.500Z" },
{ id: "evt_7", runId: "run_hwlab_adapter", seq: 7, type: "backend_status", payload: { phase: "turn-started", commandId: "cmd_hwlab_adapter_second", attemptId: "attempt_hwlab_adapter_second", runnerId: "runner_hwlab_adapter_second", jobName: "agentrun-v01-runner-hwlab-adapter-second", namespace: "agentrun-v01" }, createdAt: "2026-06-01T00:00:03.000Z" },
{ id: "evt_7_prompt", runId: "run_hwlab_adapter", seq: 8, type: "backend_status", payload: { phase: "initial-prompt-assembly", commandId: "cmd_hwlab_adapter_second", initialPromptInjected: false, reason: "thread-resume", initialPrompt: { available: true, bytes: 512, sha256: "prompt-sha", promptRefCount: 1, skillRefCount: 2, valuesPrinted: false } }, createdAt: "2026-06-01T00:00:03.500Z" },
{ id: "evt_7_prompt", runId: "run_hwlab_adapter", seq: 8, type: "backend_status", payload: { phase: "initial-prompt-assembly", commandId: "cmd_hwlab_adapter_second", initialPromptInjected: false, reason: "thread-resume", initialPrompt: { available: true, bytes: 512, sha256: "prompt-sha", promptRefCount: 1, skillCount: 3, valuesPrinted: false } }, createdAt: "2026-06-01T00:00:03.500Z" },
{ id: "evt_8", runId: "run_hwlab_adapter", seq: 8, type: "assistant_message", payload: { commandId: "cmd_hwlab_adapter_second", text: "AgentRun adapter 复用已有 runner 完成第二轮。" }, createdAt: "2026-06-01T00:00:04.000Z" },
{ id: "evt_9", runId: "run_hwlab_adapter", seq: 9, type: "terminal_status", payload: { commandId: "cmd_hwlab_adapter_second", terminalStatus: "completed" }, createdAt: "2026-06-01T00:00:05.000Z" }
] });
return send({ items: [
{ id: "evt_1", runId: "run_hwlab_adapter", seq: 1, type: "backend_status", payload: { phase: "runner-job-created", commandId: "cmd_hwlab_adapter", attemptId: "attempt_hwlab_adapter", jobName: "agentrun-v01-runner-hwlab-adapter", namespace: "agentrun-v01" }, createdAt: "2026-06-01T00:00:00.000Z" },
{ id: "evt_bundle", runId: "run_hwlab_adapter", seq: 2, type: "backend_status", payload: { phase: "resource-bundle-materialized", commandId: "cmd_hwlab_adapter", kind: "git", commitId: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", toolAliases: { count: 4, names: ["hwpod", "hwpod-ctl", "hwpod-compiler", "unidesk-ssh"], valuesPrinted: false }, promptRefs: { count: 1, names: ["hwlab-v02-runtime"], valuesPrinted: false }, skillRefs: { count: 3, names: ["hwpod-cli", "hwpod-ctl", "hwlab-agent-runtime"], valuesPrinted: false }, initialPrompt: { available: true, bytes: 512, sha256: "prompt-sha", promptRefCount: 1, skillRefCount: 3, valuesPrinted: false } }, createdAt: "2026-06-01T00:00:00.250Z" },
{ id: "evt_bundle", runId: "run_hwlab_adapter", seq: 2, type: "backend_status", payload: { phase: "resource-bundle-materialized", commandId: "cmd_hwlab_adapter", kind: "gitbundle", commitId: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", bundles: { count: 2, names: ["hwlab-tools", "hwlab-agent-skills"], targetPaths: ["tools", ".agents/skills"], valuesPrinted: false }, tools: { count: 5, names: ["hwpod", "hwpod-ctl", "hwpod-compiler", "unidesk-ssh", "hwlab-code-agent"], valuesPrinted: false }, promptRefs: { count: 1, names: ["hwlab-v02-runtime"], valuesPrinted: false }, skillDirs: { count: 4, names: ["hwpod-cli", "hwpod-ctl", "hwlab-agent-runtime", "hwlab-code-agent"], valuesPrinted: false }, initialPrompt: { available: true, bytes: 512, sha256: "prompt-sha", promptRefCount: 1, skillCount: 4, skillNames: ["hwpod-cli", "hwpod-ctl", "hwlab-agent-runtime", "hwlab-code-agent"], valuesPrinted: false } }, createdAt: "2026-06-01T00:00:00.250Z" },
{ id: "evt_tool", runId: "run_hwlab_adapter", seq: 2, type: "tool_call", payload: { method: "item/completed", type: "commandExecution", toolName: "commandExecution", itemId: "call_agentrun_tool", command: "/bin/sh -lc 'hwpod inspect --dry-run'", status: "completed", exitCode: 0, durationMs: 708, outputSummary: '{"ok":true,"action":"hwpod-cli.plan"}', summary: { outputBytes: 42, outputTruncated: false }, commandId: "cmd_hwlab_adapter", runnerId: "runner_hwlab_adapter", attemptId: "attempt_hwlab_adapter" }, createdAt: "2026-06-01T00:00:00.500Z" },
{ id: "evt_noise", runId: "run_hwlab_adapter", seq: 3, type: "backend_status", payload: { phase: "thread/status/changed", commandId: "cmd_hwlab_adapter" }, createdAt: "2026-06-01T00:00:00.750Z" },
{ id: "evt_prompt", runId: "run_hwlab_adapter", seq: 4, type: "backend_status", payload: { phase: "initial-prompt-assembly", commandId: "cmd_hwlab_adapter", initialPromptInjected: true, reason: "thread-start", initialPrompt: { available: true, bytes: 512, sha256: "prompt-sha", promptRefCount: 1, skillRefCount: 2, valuesPrinted: false } }, createdAt: "2026-06-01T00:00:00.875Z" },
{ id: "evt_prompt", runId: "run_hwlab_adapter", seq: 4, type: "backend_status", payload: { phase: "initial-prompt-assembly", commandId: "cmd_hwlab_adapter", initialPromptInjected: true, reason: "thread-start", initialPrompt: { available: true, bytes: 512, sha256: "prompt-sha", promptRefCount: 1, skillCount: 4, valuesPrinted: false } }, createdAt: "2026-06-01T00:00:00.875Z" },
{ id: "evt_2", runId: "run_hwlab_adapter", seq: 4, type: "assistant_message", payload: { commandId: "cmd_hwlab_adapter", text: "AgentRun adapter 已接管 HWLAB Code Agent。" }, createdAt: "2026-06-01T00:00:01.000Z" },
{ id: "evt_3", runId: "run_hwlab_adapter", seq: 5, type: "terminal_status", payload: { commandId: "cmd_hwlab_adapter", terminalStatus: "completed" }, createdAt: "2026-06-01T00:00:02.000Z" }
] });
@@ -534,8 +535,7 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
ownerRole: "user",
threadId: "019e8078-db67-7750-a5d9-1a99f3abd445",
retryOf: "trc_previous-agentrun-web",
message: "AgentRun adapter smoke",
workspaceFiles: [{ path: ".hwlab/hwpod-spec.yaml", content: "kind: Hwpod\nspec:\n workspace:\n path: F:\\Work\\CASE\n", encoding: "utf8" }]
message: "AgentRun adapter smoke"
})
});
assert.equal(submit.status, 202);
@@ -574,14 +574,16 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
assert.match(payload.reply.content, /AgentRun adapter/u);
assert.ok(payload.runnerTrace.events.some((event) => event.label === "agentrun:backend:runner-job-created"));
const bundleEvent = payload.runnerTrace.events.find((event) => event.label === "agentrun:backend:resource-bundle-materialized");
assert.deepEqual(bundleEvent?.details?.toolAliases?.names, ["hwpod", "hwpod-ctl", "hwpod-compiler", "unidesk-ssh"]);
assert.equal(bundleEvent?.details?.kind, "gitbundle");
assert.deepEqual(bundleEvent?.details?.bundles?.names, ["hwlab-tools", "hwlab-agent-skills"]);
assert.deepEqual(bundleEvent?.details?.tools?.names, ["hwpod", "hwpod-ctl", "hwpod-compiler", "unidesk-ssh", "hwlab-code-agent"]);
assert.deepEqual(bundleEvent?.details?.promptRefs?.names, ["hwlab-v02-runtime"]);
assert.deepEqual(bundleEvent?.details?.skillRefs?.names, ["hwpod-cli", "hwpod-ctl", "hwlab-agent-runtime"]);
assert.deepEqual(bundleEvent?.details?.skillDirs?.names, ["hwpod-cli", "hwpod-ctl", "hwlab-agent-runtime", "hwlab-code-agent"]);
const promptEvent = payload.runnerTrace.events.find((event) => event.label === "agentrun:backend:initial-prompt-assembly");
assert.equal(promptEvent?.details?.initialPromptInjected, true);
assert.equal(promptEvent?.details?.reason, "thread-start");
assert.equal(promptEvent?.details?.initialPrompt?.promptRefCount, 1);
assert.equal(promptEvent?.details?.initialPrompt?.skillRefCount, 2);
assert.equal(promptEvent?.details?.initialPrompt?.skillCount, 4);
assert.ok(calls.some((call) => call.path === "/api/v1/runs/run_hwlab_adapter/runner-jobs"));
const inspectByTrace = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/inspect?traceId=${traceId}`);
+2
View File
@@ -93,6 +93,7 @@ test("cloud api /v1/m3/status clears nested runtime blocker when trusted durable
const server = createCloudApiServer({
runtimeStore: createGreenM3StatusRuntimeStore({ blocker: "runtime_durable_not_green" }),
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"
@@ -207,6 +208,7 @@ test("cloud api /v1/m3/io returns structured blocked payload when gateway is una
const server = createCloudApiServer({
runtimeStore,
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"
+7 -2
View File
@@ -58,8 +58,12 @@ test("cloud api skills upload/list/tree/preview keeps preinstalled and uploaded
assert.equal(list.body.roots.preinstalled, preinstalledDir);
assert.equal(list.body.roots.uploaded, userSkillsDir);
assert.equal(list.body.agentRunAssembly.status, "ready");
assert.equal(list.body.agentRunAssembly.resourceBundle.kind, "gitbundle");
assert.equal(list.body.agentRunAssembly.resourceBundle.skillsDir, ".agents/skills");
assert.deepEqual(list.body.agentRunAssembly.skillRefs.map((item) => item.name), ["hwpod-cli", "hwpod-ctl", "hwlab-agent-runtime"]);
assert.deepEqual(list.body.agentRunAssembly.bundles, [
{ name: "hwlab-tools", subpath: "tools", target_path: "tools" },
{ name: "hwlab-agent-skills", subpath: "skills", target_path: ".agents/skills" }
]);
assert.deepEqual(list.body.agentRunAssembly.promptRefs.map((item) => item.name), ["hwlab-v02-runtime"]);
assert.equal(list.body.agentRunAssembly.promptAssembly.status, "ready");
assert.equal(list.body.agentRunAssembly.promptAssembly.valuesPrinted, false);
@@ -73,7 +77,8 @@ test("cloud api skills upload/list/tree/preview keeps preinstalled and uploaded
required: true,
source: "ResourceBundleRef.promptRefs"
}]);
assert.deepEqual(list.body.agentRunAssembly.toolAliases.map((item) => item.name), ["hwpod", "hwpod-ctl", "hwpod-compiler", "unidesk-ssh"]);
assert.equal(Object.hasOwn(list.body.agentRunAssembly, "toolAliases"), false);
assert.equal(Object.hasOwn(list.body.agentRunAssembly, "skillRefs"), false);
const sameName = list.body.skills.filter((skill) => skill.name === "same-name");
assert.equal(sameName.length, 2);
assert.deepEqual(sameName.map((skill) => skill.source).sort(), ["preinstalled", "uploaded"]);
+19 -33
View File
@@ -37,19 +37,20 @@ test("hwlab-cli case prepare copies a case hwpod-spec into isolated run state",
const runLocalSpec = await readFile(path.join(cwd, ".state", "hwlab-cli", "caserun", "run-test", ".hwlab", "hwpod-spec.yaml"), "utf8");
assert.match(runLocalSpec, /node-d601-f103-v2/u);
assert.match(runLocalSpec, /path: "F:\\\\Work\\\\HWLAB-CASE-F103\\\\\.worktree\\\\caserun-run-test"/u);
assert.equal(requests.length, 1);
assert.equal(requests[0].body.ops.length, 4);
assert.equal(requests[0].body.ops[0].args.workspacePath, SUBJECT_REPO_LOCAL_PATH);
assert.deepEqual(requests[0].body.ops[0].args.argv, ["cat-file", "-e", SUBJECT_COMMIT_ID]);
assert.deepEqual(requests[0].body.ops[1].args.argv, ["worktree", "add", "--detach", "--force", ".worktree\\caserun-run-test", SUBJECT_COMMIT_ID]);
assert.deepEqual(requests[0].body.ops[2].args.argv, ["-C", ".worktree\\caserun-run-test", "rev-parse", "HEAD"]);
assert.equal(requests[0].body.ops[3].opId, "op_04_keil_sidecars");
assert.equal(requests[0].body.ops[3].args.command, "node");
const sidecarInput = JSON.parse(requests[0].body.ops[3].args.argv[2]);
const setupRequest = requests.find((item) => item.body?.ops?.some((op: any) => op.opId === "op_02_subject_worktree_add"));
assert.ok(setupRequest);
assert.equal(setupRequest.body.ops.length, 4);
assert.equal(setupRequest.body.ops[0].args.workspacePath, SUBJECT_REPO_LOCAL_PATH);
assert.deepEqual(setupRequest.body.ops[0].args.argv, ["cat-file", "-e", SUBJECT_COMMIT_ID]);
assert.deepEqual(setupRequest.body.ops[1].args.argv, ["worktree", "add", "--detach", "--force", ".worktree\\caserun-run-test", SUBJECT_COMMIT_ID]);
assert.deepEqual(setupRequest.body.ops[2].args.argv, ["-C", ".worktree\\caserun-run-test", "rev-parse", "HEAD"]);
assert.equal(setupRequest.body.ops[3].opId, "op_04_keil_sidecars");
assert.equal(setupRequest.body.ops[3].args.command, "node");
const sidecarInput = JSON.parse(setupRequest.body.ops[3].args.argv[2]);
assert.equal(sidecarInput.sourceBase, "projects\\01_baseline\\Projects\\MDK-ARM\\atk_f103");
assert.equal(sidecarInput.destinationBase, ".worktree\\caserun-run-test\\projects\\01_baseline\\Projects\\MDK-ARM\\atk_f103");
assert.equal(requests[0].body.ops[3].args.commandBinding.sourceBase, "projects\\01_baseline\\Projects\\MDK-ARM\\atk_f103");
assert.equal(requests[0].body.ops[3].args.commandBinding.destinationBase, ".worktree\\caserun-run-test\\projects\\01_baseline\\Projects\\MDK-ARM\\atk_f103");
assert.equal(setupRequest.body.ops[3].args.commandBinding.sourceBase, "projects\\01_baseline\\Projects\\MDK-ARM\\atk_f103");
assert.equal(setupRequest.body.ops[3].args.commandBinding.destinationBase, ".worktree\\caserun-run-test\\projects\\01_baseline\\Projects\\MDK-ARM\\atk_f103");
assert.equal(result.payload.subject.setup.keilSidecars.copied, 1);
} finally {
await rm(root, { recursive: true, force: true });
@@ -462,41 +463,26 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit
const prompt = await readFile(path.join(cwd, ".state", "hwlab-cli", "caserun", "run-agent-flow", "agent-prompt.md"), "utf8");
assert.match(prompt, /without auto-grading/u);
assert.match(prompt, /Run-local HWPOD spec/u);
assert.match(prompt, /Runtime Assembly/u);
assert.match(prompt, /kind=gitbundle/u);
assert.match(prompt, /verificationMode: compile-only build check/u);
assert.doesNotMatch(prompt, /compile-only subject /u);
assert.doesNotMatch(prompt, /Write exactly this YAML/u);
assert.doesNotMatch(prompt, /```yaml/u);
assert.doesNotMatch(prompt, /apiVersion: hwlab\.pikastech\.com/u);
assert.doesNotMatch(prompt, /path: "F:\\\\Work\\\\HWLAB-CASE-F103/u);
assert.match(prompt, /CaseRun has already placed the run-local spec/u);
assert.match(prompt, /hwpod-ctl spec validate --spec \.hwlab\/hwpod-spec\.yaml/u);
assert.doesNotMatch(prompt, /CaseRun has already placed the run-local spec/u);
assert.doesNotMatch(prompt, /hwpod-ctl spec validate --spec \.hwlab\/hwpod-spec\.yaml/u);
assert.match(prompt, /standard hwpod\/hwpod-ctl commands/u);
assert.match(prompt, /hwpod job status <jobId> --spec \.hwlab\/hwpod-spec\.yaml/u);
assert.match(prompt, /hwpod job status <jobId>/u);
assert.match(prompt, / shell sleep\/&&\/timeout\/watch\/head\/pipe/u);
assert.match(prompt, /separate short `hwpod job status <jobId> --spec \.hwlab\/hwpod-spec\.yaml` commands/u);
assert.match(prompt, /separate short `hwpod job status <jobId>` commands/u);
const diff = await readFile(path.join(cwd, ".state", "hwlab-cli", "caserun", "run-agent-flow", "agent-diff.patch"), "utf8");
assert.match(diff, /printf\("hello"\)/u);
assert.equal(requests.some((item) => item.url === "http://web.test/v1/agent/sessions"), true);
assert.equal(requests.some((item) => item.url === "http://web.test/v1/agent/chat"), true);
const agentChatRequest = requests.find((item) => item.url === "http://web.test/v1/agent/chat");
assert.equal(agentChatRequest.body.workspaceFiles[0].path, ".hwlab/hwpod-spec.yaml");
assert.equal(agentChatRequest.body.workspaceFiles[0].encoding, "utf8");
assert.match(agentChatRequest.body.workspaceFiles[0].content, /HWLAB-CASE-F103.*caserun-run-agent-flow/u);
const workspaceFilePaths = agentChatRequest.body.workspaceFiles.map((item: any) => item.path);
assert.equal(workspaceFilePaths.includes("tools/hwpod-cli.ts"), true);
assert.equal(workspaceFilePaths.includes("tools/hwpod-ctl.ts"), true);
assert.equal(workspaceFilePaths.includes("tools/hwpod-compiler-cli.ts"), true);
assert.equal(workspaceFilePaths.includes("tools/src/hwpod-harness-lib.ts"), true);
assert.equal(workspaceFilePaths.includes("tools/src/hwpod-node-ops-contract.ts"), true);
assert.equal(workspaceFilePaths.includes("tools/src/runtime-endpoint-resolver.ts"), true);
assert.equal(workspaceFilePaths.includes(".agents/skills/hwpod-cli/SKILL.md"), true);
assert.equal(workspaceFilePaths.includes(".agents/skills/hwpod-ctl/SKILL.md"), true);
assert.equal(workspaceFilePaths.includes(".agents/skills/hwlab-agent-runtime/SKILL.md"), true);
const seededHwpodLib = agentChatRequest.body.workspaceFiles.find((item: any) => item.path === "tools/src/hwpod-harness-lib.ts");
assert.match(seededHwpodLib.content, /hwpod-cli\.job\.help/u);
const seededHwpodSkill = agentChatRequest.body.workspaceFiles.find((item: any) => item.path === ".agents/skills/hwpod-cli/SKILL.md");
assert.match(seededHwpodSkill.content, /cmd\.run/u);
assert.equal(Object.hasOwn(agentChatRequest.body, "workspaceFiles"), false);
assert.doesNotMatch(agentChatRequest.body.message, /path: "F:\\Work\\HWLAB-CASE-F103\\\.worktree\\caserun-run-agent-flow"/u);
assert.equal(requests.some((item) => item.url === "http://web.test/v1/agent/chat/result/" + result.payload.agent.traceId), true);
assert.equal(requests.some((item) => item.url === "http://web.test/v1/agent/chat/trace/" + result.payload.agent.traceId), false);
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env sh
exec bun "$(dirname "$0")/hwlab-code-agent-cli.ts" "$@"
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env sh
exec bun "$(dirname "$0")/hwpod-cli.ts" "$@"
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env sh
exec bun "$(dirname "$0")/hwpod-compiler-cli.ts" "$@"
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env sh
exec bun "$(dirname "$0")/hwpod-ctl.ts" "$@"
+8 -40
View File
@@ -3,7 +3,6 @@ import { createHash, randomUUID } from "node:crypto";
import { closeSync, openSync } from "node:fs";
import { copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { traceDisplayRows, traceNoiseEventCount, type TraceEventRow } from "./hwlab-cli/trace-renderer.ts";
import { readHwpodSpec } from "./hwpod-harness-lib.ts";
@@ -20,22 +19,12 @@ const HWLAB_API_KEY_PREFIX = "hwl_live_";
const STANDARD_CASE_REPO_PATH = "/root/hwlab-case-registry";
const REMOVED_CASE_REPO_PATH = "/root/hwpod-cases";
const CASE_TRACE_RENDERER = "tools/src/hwlab-cli/trace-renderer:traceDisplayRows";
const SOURCE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const AGENT_WORKSPACE_SEED_FILES = [
{ source: "tools/hwpod-cli.ts", target: "tools/hwpod-cli.ts" },
{ source: "tools/hwpod-ctl.ts", target: "tools/hwpod-ctl.ts" },
{ source: "tools/hwpod-compiler-cli.ts", target: "tools/hwpod-compiler-cli.ts" },
{ source: "tools/src/hwpod-harness-lib.ts", target: "tools/src/hwpod-harness-lib.ts" },
{ source: "tools/src/hwpod-node-ops-contract.ts", target: "tools/src/hwpod-node-ops-contract.ts" },
{ source: "tools/src/runtime-endpoint-resolver.ts", target: "tools/src/runtime-endpoint-resolver.ts" },
] as const;
type EnvLike = Record<string, string | undefined>;
type ParsedArgs = Record<string, unknown> & { _: string[] };
type FetchLike = typeof fetch;
type RunProcessLike = (command: string[], cwd: string, env: Record<string, string | undefined>) => Promise<{ command: string[]; stdout: string; stderr: string; exitCode: number }>;
type StartProcessLike = (input: { command: string; args: string[]; cwd: string; env: Record<string, string | undefined>; stdoutPath: string; stderrPath: string }) => Promise<{ pid: number }> | { pid: number };
type AgentWorkspaceFile = { path: string; content: string; encoding: "utf8" };
type CaseContext = {
parsed: ParsedArgs;
@@ -720,7 +709,6 @@ async function runAgentTaskStage(context: CaseContext, run: PreparedCaseRun) {
}
const promptPath = path.join(run.runDir, "agent-prompt.md");
const prompt = await renderAgentTaskPrompt(run, run.agentTask);
const workspaceFiles = await agentWorkspaceFilesForRun(run);
await writeFile(promptPath, prompt, "utf8");
const promptSha256 = sha256(prompt);
const baseUrl = webUrlFrom(context, run.definition);
@@ -738,7 +726,7 @@ async function runAgentTaskStage(context: CaseContext, run: PreparedCaseRun) {
const threadId = session.threadId;
const traceId = text(context.parsed.traceId) || `trc_case_${slug(run.caseId)}_${randomUUID().replace(/-/gu, "")}`;
run = await updateRun(context, run, { stage: "agent-submitting", agent: agentFailureStage({ stageStatus: "session_created", baseUrl, projectId, providerProfile, conversationId, sessionId, threadId, traceId, promptPath, promptSha256, sessionResponse: compactObject(session.body) }) });
const accepted = await submitAgentTask(context, { baseUrl, projectId, providerProfile, conversationId, sessionId, threadId, traceId, prompt, workspaceFiles });
const accepted = await submitAgentTask(context, { baseUrl, projectId, providerProfile, conversationId, sessionId, threadId, traceId, prompt });
if (!accepted.ok) {
const agent = agentFailureStage({ stageStatus: "submit_failed", baseUrl, projectId, providerProfile, conversationId, sessionId, threadId, traceId, promptPath, promptSha256, accepted: accepted.body, error: accepted.error, sessionResponse: session.body });
const nextRun = await updateRun(context, run, { agent });
@@ -869,7 +857,7 @@ async function createAgentSession(context: CaseContext, input: { baseUrl: string
};
}
async function submitAgentTask(context: CaseContext, input: { baseUrl: string; projectId: string; providerProfile: string; conversationId: string; sessionId: string; threadId: string; traceId: string; prompt: string; workspaceFiles?: AgentWorkspaceFile[] }) {
async function submitAgentTask(context: CaseContext, input: { baseUrl: string; projectId: string; providerProfile: string; conversationId: string; sessionId: string; threadId: string; traceId: string; prompt: string }) {
return caseFetchJson(context, `${input.baseUrl}/v1/agent/chat`, {
method: "POST",
headers: { ...caseAgentHeaders(context), "x-trace-id": input.traceId, prefer: "respond-async", "x-hwlab-short-connection": "1" },
@@ -881,7 +869,6 @@ async function submitAgentTask(context: CaseContext, input: { baseUrl: string; p
traceId: input.traceId,
providerProfile: input.providerProfile,
shortConnection: true,
workspaceFiles: input.workspaceFiles,
projectId: input.projectId,
updatedByClient: "hwlab-cli.case-run"
}))
@@ -938,21 +925,6 @@ function agentTraceIdentityArtifact(run: PreparedCaseRun, agent: AgentRunStage)
});
}
async function agentWorkspaceFilesForRun(run: PreparedCaseRun): Promise<AgentWorkspaceFile[]> {
const content = await readFile(run.specPath, "utf8");
const files: AgentWorkspaceFile[] = [{ path: ".hwlab/hwpod-spec.yaml", content, encoding: "utf8" }];
for (const entry of AGENT_WORKSPACE_SEED_FILES) {
const sourcePath = path.join(SOURCE_ROOT, entry.source);
try {
files.push({ path: entry.target, content: await readFile(sourcePath, "utf8"), encoding: "utf8" });
} catch (error) {
if (error?.code === "ENOENT") throw cliError("caserun_agent_workspace_seed_missing", "CaseRun agent workspace seed file is missing", { source: entry.source, sourcePath });
throw error;
}
}
return files;
}
async function pollAgentResult(context: CaseContext, input: { baseUrl: string; traceId: string; acceptedBody: any; resultUrl?: string; run?: PreparedCaseRun; agent?: AgentRunStage }) {
const requestedTimeoutMs = effectiveAgentTimeoutMs(context, input.run?.agentTask);
const timeoutMs = Math.max(Math.min(requestedTimeoutMs, MAX_AGENT_TIMEOUT_MS), 1000);
@@ -1085,9 +1057,8 @@ async function renderAgentTaskPrompt(run: PreparedCaseRun, agentTask: PreparedAg
...agentTask.constraints,
"只能修改 isolated subject worktree,不得修改 case registry repo。",
"不得修改原 subject repo checkout;所有源码修改必须落在 subjectWorktreePath。",
"CaseRun 已在 AgentRun workspace 预装 .hwlab/hwpod-spec.yaml;必须使用这个 run-local HWPOD spec,不要从 prompt 重建 spec。",
"CaseRun 已随本次 run 注入当前 v0.2 的 hwpod/hwpod-ctl/hwpod-compiler 工具文件和 HWPOD skill;若标准 hwpod 命令能力缺失,报告 workspace tool seed 问题,不要改走旁路。",
"如果 .hwlab/hwpod-spec.yaml 缺失或内容明显不是本次 case,请报告 CaseRun workspace setup 错误,不要自行编造或迁移 spec。",
"AgentRun 通过 kind=gitbundle 装配当前 v0.2 的 tools/ 与 .agents/skills;若标准 hwpod 命令能力缺失,报告 gitbundle runtime assembly 问题,不要改走旁路。",
"不要依赖 workspaceFiles、seed files、hostPath skill 目录或 ConfigMap 作为工具/skill 注入 fallback。",
"若 case prompt 要求源码修改,必须只改 subjectWorktreePath;若 prompt 未要求源码修改,则保持 subject 源码不变。",
"不要运行 CaseRun 答案执行器;你本人必须通过 hwpod/hwpod-ctl 标准入口触发编译验证。",
"hwpod build/download 是长任务短连接入口;不要再用 shell sleep/&&/timeout/watch/head/pipe 或 shell loop 包住它们。记录返回 JSON 里的 jobId/job_id,再用独立的 hwpod job status <jobId> --spec .hwlab/hwpod-spec.yaml 短命令做有限轮询。"
@@ -1100,11 +1071,10 @@ async function renderAgentTaskPrompt(run: PreparedCaseRun, agentTask: PreparedAg
`subjectRepoLocalPath: ${run.subject.repoLocalPath}`,
`subjectCommitId: ${run.subject.commitId}`,
`subjectWorktreePath: ${run.subject.worktreePath}`,
`runLocalHwpodSpecWorkspacePath: .hwlab/hwpod-spec.yaml`,
`verificationMode: compile-only build check; no download or runtime smoke unless the case explicitly asks for it`,
``,
`## Run-local HWPOD spec`,
`CaseRun has already placed the run-local spec at \`.hwlab/hwpod-spec.yaml\` in this AgentRun workspace before your turn starts, and has seeded the current v0.2 hwpod/hwpod-ctl/hwpod-compiler tool files and HWPOD skills. Treat the run-local spec as the authority for workspace/toolchain/debug/IO bindings. Do not create or overwrite it from this prompt.`,
`## Runtime Assembly`,
`AgentRun materializes HWLAB runtime assets from ResourceBundleRef kind=gitbundle: repo subpath \`tools\` is copied to workspace \`tools\`, and repo subpath \`skills\` is copied to workspace \`.agents/skills\`. CaseRun no longer sends ad hoc workspaceFiles or seed-file payloads.`,
``,
`## Task`,
agentTask.prompt,
@@ -1113,11 +1083,9 @@ async function renderAgentTaskPrompt(run: PreparedCaseRun, agentTask: PreparedAg
...constraints.map((item) => `- ${item}`),
``,
`## Flow`,
`- Confirm \`.hwlab/hwpod-spec.yaml\` exists. If it is missing, report a CaseRun workspace setup error instead of reconstructing it.`,
`- Run \`hwpod-ctl spec validate --spec .hwlab/hwpod-spec.yaml\`.`,
`- Run \`hwpod inspect --spec .hwlab/hwpod-spec.yaml\`.`,
`- Confirm standard \`hwpod\`, \`hwpod-ctl\` and \`hwpod-compiler\` commands are available from the gitbundle tools directory.`,
`- Follow the case task using standard hwpod/hwpod-ctl commands. Run build/download/UART steps only when the case explicitly asks for them, and report returned JSON/job/artifact/serial summaries.`,
`- For hwpod build/download, keep the HWPOD command unwrapped so it can return async JSON; then poll the returned job id with separate short \`hwpod job status <jobId> --spec .hwlab/hwpod-spec.yaml\` commands a bounded number of times. Do not wrap status polling with shell sleep, &&, timeout, watch, head, pipes, or shell loops.`,
`- For hwpod build/download, keep the HWPOD command unwrapped so it can return async JSON; then poll the returned job id with separate short \`hwpod job status <jobId>\` commands a bounded number of times. Do not wrap status polling with shell sleep, &&, timeout, watch, head, pipes, or shell loops.`,
`- CaseRun will inspect git diff under subjectWorktreePath after your turn completes and may run a runner post-check compile as separate evidence.`,
`- CaseRun records trace/session/conversation, agent commandExecution, workspace diff and Keil build evidence without auto-grading them.`
].join("\n");
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env sh
exec bun "$(dirname "$0")/unidesk-ssh.mjs" "$@"