diff --git a/.agents/skills/unidesk-code-queue/SKILL.md b/.agents/skills/unidesk-code-queue/SKILL.md index 3824a1b6..394e3a1c 100644 --- a/.agents/skills/unidesk-code-queue/SKILL.md +++ b/.agents/skills/unidesk-code-queue/SKILL.md @@ -29,7 +29,11 @@ AipodSpec/Artificer、task manifest 和 queue 渐进披露见 [references/resour - 新写入口不要用 `codex submit/steer/resume`;这些 legacy mutation 已冻结。 - 默认输出保持低噪声表格/摘要;机器消费显式 `-o json|yaml` 或 `--raw`。 -- 读取 task 的可重建输入使用 `agentrun describe task/ --input -o json`;该入口只投影 task create 字段、AipodSpec identity、资源引用和脱敏 provenance,不打印 Secret value。 +- 读取 task 的可重建输入: + - 使用 `agentrun describe task/ --input -o json`; + - 该入口只投影 task create 字段、AipodSpec identity、资源引用和脱敏 provenance; + - 不打印 Secret value; + - `--input` 与 `--full`、`--raw` 互斥,非法组合必须在请求服务端前返回 `validation-failed`。 - Session follow-up 使用 `agentrun send`,由服务端决定内部 steer vs turn。 - 终态失败重试只使用 `agentrun retry task/`:保持同一 task,新增不可变 attempt;先用 `--dry-run` 完成服务端全校验,再以同一幂等键执行真实重试。 - retry 只接受 Queue 已处于 `failed` 或 `blocked`;`completed`、`cancelled` 和其他状态必须 fail closed。不得复制 task、重置 task、引入本地 scheduler/backoff/judge 或直接创建 runner Job 来模拟重试。 diff --git a/.agents/skills/unidesk-code-queue/references/resources.md b/.agents/skills/unidesk-code-queue/references/resources.md index 89d911f8..1a505506 100644 --- a/.agents/skills/unidesk-code-queue/references/resources.md +++ b/.agents/skills/unidesk-code-queue/references/resources.md @@ -23,6 +23,7 @@ task 输入下钻必须遵守以下边界: - 默认 `describe task/` 的 `Next` 必须给出 `--input -o json` 语义化命令; - `--input` 只读取现有 task durable fact,不创建、复制、retry 或 dispatch task; - 输出固定为 task create 字段、AipodSpec identity、bundle/SecretRef 和脱敏 provenance,credential 只保留 SecretRef 并声明 `valuesPrinted=false`; +- `--input` 与 `--full`、`--raw` 互斥,非法组合必须在请求服务端前返回带修正路径的 `validation-failed`; - 完整服务端资源继续使用显式 `--full -o json` 或 `--raw`,禁止提高全局 stdout 阈值来掩盖大输出。 retry 必须遵守以下边界: diff --git a/scripts/src/agentrun.test.ts b/scripts/src/agentrun.test.ts index a9750081..077967ba 100644 --- a/scripts/src/agentrun.test.ts +++ b/scripts/src/agentrun.test.ts @@ -301,6 +301,25 @@ describe("AgentRun default transport contract", () => { process.env.AGENTRUN_CLIENT_CONFIG = tempConfigPath; await Bun.write(tempConfigPath, agentRunMinimalClientYaml.replace("http://agentrun.example.local:8080", server.url.href.replace(/\/$/u, ""))); + const invalidCombinations = [ + { args: ["--input", "--full"], contentType: "text/plain" }, + { args: ["--full", "--input", "-o", "json"], contentType: "application/json" }, + { args: ["--input", "--full", "-o", "yaml"], contentType: "application/yaml" }, + { args: ["--input", "--raw"], contentType: "application/json" }, + ]; + for (const invalid of invalidCombinations) { + const rejected = await runAgentRunCommand(null, ["describe", `task/${taskId}`, ...invalid.args]); + const rejectedText = renderedTextOf(rejected); + expect(rejected.ok).toBe(false); + expect(renderedContentTypeOf(rejected)).toBe(invalid.contentType); + expect(rejectedText).toContain("validation-failed"); + expect(rejectedText).toContain("Use --input without --full or --raw"); + expect(rejectedText).not.toContain(hiddenCredential); + expect(rejectedText).not.toContain("supervisor"); + expect(Buffer.byteLength(rejectedText, "utf8")).toBeLessThan(2400); + } + expect(requests).toBe(0); + const summaryText = renderedTextOf(await runAgentRunCommand(null, ["describe", `task/${taskId}`])); expect(summaryText).toContain(`agentrun describe task/${taskId} --input -o json`); expect(summaryText).not.toContain(hiddenCredential); @@ -485,7 +504,7 @@ describe("AgentRun default transport contract", () => { expect(rootHelpText.split("\n").length).toBeLessThan(40); }); - test("local retry and attempt validation preserves bounded human and machine errors", async () => { + test("local resource validation preserves bounded human and machine errors", async () => { const cases = [ { args: ["retry", "task/qt_failed_1", "--reason", "dependency repaired"], @@ -532,6 +551,8 @@ describe("AgentRun default transport contract", () => { for (const cliArgs of [ ["retry", "run/run_invalid", "--idempotency-key", "retry-key-1", "--reason", "dependency repaired", "-o", "json"], ["get", "attempts", "--raw"], + ["describe", "task/qt_sensitive", "--input", "--full", "-o", "json"], + ["describe", "task/qt_sensitive", "--input", "--raw"], ]) { const cli = Bun.spawnSync(["bun", "scripts/cli.ts", "agentrun", ...cliArgs], { cwd: new URL("../..", import.meta.url).pathname, @@ -543,6 +564,8 @@ describe("AgentRun default transport contract", () => { expect(payload.ok).toBe(false); expect(payload.failureKind).toBe("validation-failed"); expect(cli.stdout.toString().length).toBeLessThan(2400); + expect(cli.stdout.toString()).not.toContain('"resource":'); + expect(cli.stdout.toString()).not.toContain('"data":'); } }); diff --git a/scripts/src/agentrun/resource-actions.ts b/scripts/src/agentrun/resource-actions.ts index 0c0644b5..984559eb 100644 --- a/scripts/src/agentrun/resource-actions.ts +++ b/scripts/src/agentrun/resource-actions.ts @@ -179,6 +179,24 @@ export function parseResourceOptions(args: string[]): AgentRunResourceOptions { continue; } } + if (options.taskInput && (options.full || options.raw)) { + const conflictingOptions = ["--input", options.full ? "--full" : null, options.raw ? "--raw" : null] + .filter((value): value is string => value !== null); + throw new AgentRunRestError( + "validation-failed", + `${conflictingOptions.join(" with ")} is not allowed: --input is the bounded SecretRef-only TaskInput projection and cannot be combined with complete resource disclosure.`, + { + details: { + conflictingOptions, + recoveryActions: [ + "Use --input without --full or --raw for the bounded SecretRef-only TaskInput projection.", + "Remove --input when explicitly requesting the complete resource with --full or --raw.", + ], + valuesPrinted: false, + }, + }, + ); + } return options; } diff --git a/scripts/src/agentrun/rest-bridge.ts b/scripts/src/agentrun/rest-bridge.ts index 8a7b2129..73da1aed 100644 --- a/scripts/src/agentrun/rest-bridge.ts +++ b/scripts/src/agentrun/rest-bridge.ts @@ -1068,6 +1068,10 @@ export function renderAgentRunRestError(command: string, error: AgentRunRestErro const nextCommands = Array.isArray(laneHint.nextCommands) ? laneHint.nextCommands.filter((value): value is string => typeof value === "string" && value.length > 0).slice(0, 4) : []; + const validationDetails = record(payload.agentrun); + const recoveryActions = Array.isArray(validationDetails.recoveryActions) + ? validationDetails.recoveryActions.filter((value): value is string => typeof value === "string" && value.length > 0).slice(0, 4) + : []; const hintLines = Object.keys(laneHint).length === 0 ? [] : [ @@ -1080,15 +1084,17 @@ export function renderAgentRunRestError(command: string, error: AgentRunRestErro const validationHelp = `bun scripts/cli.ts agentrun ${command.split(/\s+/u)[1] ?? "--help"} --help`; const nextLines = nextCommands.length > 0 ? nextCommands - : error.failureKind === "validation-failed" - ? [ - "Review the AgentRun validation message and correct the request; do not bypass the formal resource API.", - validationHelp, - ] - : [ - "Check config/agentrun.yaml manager.baseUrl and the AgentRun API route.", - "Verify HWLAB_API_KEY is present in env or in the configured auth.file without printing the key.", - ]; + : recoveryActions.length > 0 + ? recoveryActions + : error.failureKind === "validation-failed" + ? [ + "Review the AgentRun validation message and correct the request; do not bypass the formal resource API.", + validationHelp, + ] + : [ + "Check config/agentrun.yaml manager.baseUrl and the AgentRun API route.", + "Verify HWLAB_API_KEY is present in env or in the configured auth.file without printing the key.", + ]; return renderedCliResult(false, command, [ `Error: ${payload.failureKind}`, String(payload.message ?? ""),