diff --git a/docs/reference/agentrun-code-agent-dispatch.md b/docs/reference/agentrun-code-agent-dispatch.md index 95087015..95823ecc 100644 --- a/docs/reference/agentrun-code-agent-dispatch.md +++ b/docs/reference/agentrun-code-agent-dispatch.md @@ -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//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=`、`profileSource=env`、`parentTraceId=`,child trace/result 使用同一个 profile/model/backend。缺少任意字段时先修可见性,不新增旧 fallback、旧门禁或兼容分支。 diff --git a/docs/reference/spec-hwpod-harness.md b/docs/reference/spec-hwpod-harness.md index 111e92ba..f5ce9ba4 100644 --- a/docs/reference/spec-hwpod-harness.md +++ b/docs/reference/spec-hwpod-harness.md @@ -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-` 隔离 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///`。固定归档包括 `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。 diff --git a/docs/reference/spec-v02-hwlab-agent-skills.md b/docs/reference/spec-v02-hwlab-agent-skills.md index 24612609..9dfef8e7 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-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 接口说明 diff --git a/internal/agent/agentrun-dispatch.mjs b/internal/agent/agentrun-dispatch.mjs index c5bec88f..fa759f8d 100644 --- a/internal/agent/agentrun-dispatch.mjs +++ b/internal/agent/agentrun-dispatch.mjs @@ -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", diff --git a/internal/agent/agentrun-dispatch.test.mjs b/internal/agent/agentrun-dispatch.test.mjs index 4f9cf41d..c4667ea9 100644 --- a/internal/agent/agentrun-dispatch.test.mjs +++ b/internal/agent/agentrun-dispatch.test.mjs @@ -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", () => { diff --git a/internal/agent/prompts/hwlab-v02-runtime.md b/internal/agent/prompts/hwlab-v02-runtime.md index e61781e9..ea9b00e2 100644 --- a/internal/agent/prompts/hwlab-v02-runtime.md +++ b/internal/agent/prompts/hwlab-v02-runtime.md @@ -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. diff --git a/internal/cloud/code-agent-agentrun-adapter.ts b/internal/cloud/code-agent-agentrun-adapter.ts index 1c0ed961..948ca6ee 100644 --- a/internal/cloud/code-agent-agentrun-adapter.ts +++ b/internal/cloud/code-agent-agentrun-adapter.ts @@ -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 }); diff --git a/internal/cloud/server-agent-chat.test.ts b/internal/cloud/server-agent-chat.test.ts index 55666204..af3558d7 100644 --- a/internal/cloud/server-agent-chat.test.ts +++ b/internal/cloud/server-agent-chat.test.ts @@ -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}`); diff --git a/internal/cloud/server-m3-http.test.ts b/internal/cloud/server-m3-http.test.ts index ab95507a..592a664d 100644 --- a/internal/cloud/server-m3-http.test.ts +++ b/internal/cloud/server-m3-http.test.ts @@ -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" diff --git a/internal/cloud/server-skills.test.ts b/internal/cloud/server-skills.test.ts index f975df5d..bcc9d982 100644 --- a/internal/cloud/server-skills.test.ts +++ b/internal/cloud/server-skills.test.ts @@ -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"]); diff --git a/tools/hwlab-cli/caserun.test.ts b/tools/hwlab-cli/caserun.test.ts index 851c54c8..52d0f0c1 100644 --- a/tools/hwlab-cli/caserun.test.ts +++ b/tools/hwlab-cli/caserun.test.ts @@ -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 --spec \.hwlab\/hwpod-spec\.yaml/u); + assert.match(prompt, /hwpod job status /u); assert.match(prompt, /不要再用 shell sleep\/&&\/timeout\/watch\/head\/pipe/u); - assert.match(prompt, /separate short `hwpod job status --spec \.hwlab\/hwpod-spec\.yaml` commands/u); + assert.match(prompt, /separate short `hwpod job status ` 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); diff --git a/tools/hwlab-code-agent b/tools/hwlab-code-agent new file mode 100644 index 00000000..292172de --- /dev/null +++ b/tools/hwlab-code-agent @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +exec bun "$(dirname "$0")/hwlab-code-agent-cli.ts" "$@" diff --git a/tools/hwpod b/tools/hwpod new file mode 100644 index 00000000..d11a9ba5 --- /dev/null +++ b/tools/hwpod @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +exec bun "$(dirname "$0")/hwpod-cli.ts" "$@" diff --git a/tools/hwpod-compiler b/tools/hwpod-compiler new file mode 100644 index 00000000..c844d9e7 --- /dev/null +++ b/tools/hwpod-compiler @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +exec bun "$(dirname "$0")/hwpod-compiler-cli.ts" "$@" diff --git a/tools/hwpod-ctl b/tools/hwpod-ctl new file mode 100644 index 00000000..2da51f6f --- /dev/null +++ b/tools/hwpod-ctl @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +exec bun "$(dirname "$0")/hwpod-ctl.ts" "$@" diff --git a/tools/src/hwlab-caserun-lib.ts b/tools/src/hwlab-caserun-lib.ts index 29d2c37d..8398bbd7 100644 --- a/tools/src/hwlab-caserun-lib.ts +++ b/tools/src/hwlab-caserun-lib.ts @@ -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; type ParsedArgs = Record & { _: string[] }; type FetchLike = typeof fetch; type RunProcessLike = (command: string[], cwd: string, env: Record) => Promise<{ command: string[]; stdout: string; stderr: string; exitCode: number }>; type StartProcessLike = (input: { command: string; args: string[]; cwd: string; env: Record; 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 { - 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 --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 --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 \` 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"); diff --git a/tools/unidesk-ssh b/tools/unidesk-ssh new file mode 100644 index 00000000..179469f1 --- /dev/null +++ b/tools/unidesk-ssh @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +exec bun "$(dirname "$0")/unidesk-ssh.mjs" "$@"