diff --git a/.agents/skills/unidesk-code-queue/SKILL.md b/.agents/skills/unidesk-code-queue/SKILL.md index 794db9c9..394e3a1c 100644 --- a/.agents/skills/unidesk-code-queue/SKILL.md +++ b/.agents/skills/unidesk-code-queue/SKILL.md @@ -12,6 +12,7 @@ description: UniDesk AgentRun-backed Code Queue CLI — Skill(cli-spec)。legacy ```bash bun scripts/cli.ts agentrun get task --limit 20 bun scripts/cli.ts agentrun describe task/ +bun scripts/cli.ts agentrun describe task/ --input -o json bun scripts/cli.ts agentrun events run/ --limit 20 bun scripts/cli.ts agentrun logs session/ --tail 120 bun scripts/cli.ts agentrun result command/ --run @@ -28,6 +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; + - `--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 7e1dfdd2..1a505506 100644 --- a/.agents/skills/unidesk-code-queue/references/resources.md +++ b/.agents/skills/unidesk-code-queue/references/resources.md @@ -4,6 +4,7 @@ - `agentrun create|apply`:创建 task 或应用 AipodSpec manifest; - `agentrun get|describe`:有界读取列表和详情; +- `agentrun describe task/ --input -o json`:读取可重建 task 输入、AipodSpec identity、资源引用和脱敏 provenance; - `agentrun events|logs|result`:渐进披露运行证据; - `agentrun get attempts --task `:读取同一 task 的不可变 attempt 历史; - `agentrun retry task/ --idempotency-key --reason [--dry-run]`:对终态失败 task 创建下一次 attempt; @@ -17,6 +18,14 @@ - 完整事件使用显式 `--full`、`--raw`,或按 `item.identity` 和 `afterSeq=item.seq-1 --limit 1 --full` 下钻; - 该 CLI 只渲染 AgentRun durable facts,不删除事件、不按正文去重,也不改变 Kafka/SSE 事件链。 +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 必须遵守以下边界: - task identity 不变,每次执行使用新的 attempt、run、command 和 runner Job identity; diff --git a/scripts/src/agentrun.test.ts b/scripts/src/agentrun.test.ts index d3060e50..a9f1d472 100644 --- a/scripts/src/agentrun.test.ts +++ b/scripts/src/agentrun.test.ts @@ -126,6 +126,7 @@ describe("AgentRun resource bridge argv", () => { "--output=yaml", "--raw", "--full", + "--input", "--full-text", "--dry-run", ])).toEqual([ @@ -214,6 +215,165 @@ describe("AgentRun default transport contract", () => { } }); + test("task input drill-down is bounded, reconstructable, and SecretRef-only", async () => { + const taskId = "qt_artificer_input"; + const hiddenCredential = "HIDDEN_CREDENTIAL_VALUE_MUST_NOT_BE_PRINTED"; + const task = { + id: taskId, + tenantId: "unidesk", + projectId: "pikasTech/unidesk", + queue: "commander", + lane: "v0.1", + title: "Artificer input fixture", + priority: 50, + state: "completed", + version: 7, + backendProfile: "sub2api", + providerId: "G14", + workspaceRef: { kind: "opaque", path: ".", repo: "pikasTech/unidesk", branch: "master" }, + sessionRef: { sessionId: "ses_fixture", conversationId: "conv_fixture", metadata: { credential: hiddenCredential } }, + executionPolicy: { + sandbox: "workspace-write", + approval: "never", + timeoutMs: 1_800_000, + network: "enabled", + secretScope: { + allowCredentialEcho: false, + providerCredentials: [{ + profile: "sub2api", + secretRef: { name: "agentrun-provider", keys: ["auth.json"], value: hiddenCredential }, + value: hiddenCredential, + }], + toolCredentials: [{ + tool: "github", + purpose: "github-pr", + secretRef: { name: "agentrun-github", keys: ["GH_TOKEN"], token: hiddenCredential }, + projection: { kind: "env", envName: "GH_TOKEN", secretKey: "GH_TOKEN", value: hiddenCredential }, + }], + }, + }, + resourceBundleRef: { + kind: "gitbundle", + repoUrl: `https://fixture:${hiddenCredential}@github.com/pikasTech/unidesk.git`, + ref: "master", + bundles: [{ name: "unidesk-skills", subpath: ".agents/skills", targetPath: ".agents/skills" }], + requiredSkills: [{ name: "unidesk-gh" }], + credentialRef: { name: "agentrun-resource-git", keys: ["username", "password"], value: hiddenCredential }, + submodules: false, + lfs: false, + }, + payload: { prompt: "Inspect the existing sentinel configuration.", model: "gpt-5.5" }, + payloadHash: "sha256:fixture", + references: [{ kind: "github-issue", url: "https://github.com/pikasTech/unidesk/issues/1819" }], + metadata: { + aipod: "Artificer", + source: "config/aipods/artificer.yaml", + aipodSpecHash: "sha256:aipod-fixture", + aipodImageRef: { + kind: "env-image-dockerfile", + repoUrl: `https://fixture:${hiddenCredential}@github.com/pikasTech/agentrun.git`, + commitId: "a".repeat(40), + dockerfilePath: "deploy/container/Containerfile", + }, + credential: hiddenCredential, + }, + idempotencyKey: "artificer-input-fixture", + latestAttempt: { attemptId: "qat_fixture", state: "completed", runId: "run_fixture", commandId: "cmd_fixture", runnerJobId: "rjob_fixture", sessionId: "ses_fixture" }, + supervisor: { verboseDiagnostic: "x".repeat(20_000), credential: hiddenCredential }, + valuesPrinted: false, + }; + let requests = 0; + const server = Bun.serve({ + port: 0, + fetch(request) { + const url = new URL(request.url); + expect(request.headers.get("authorization")).toBe("Bearer secret-value"); + expect(url.pathname).toBe(`/api/v1/queue/tasks/${taskId}`); + requests += 1; + return Response.json({ ok: true, data: task }); + }, + }); + const previousConfig = process.env.AGENTRUN_CLIENT_CONFIG; + const previousKey = process.env.HWLAB_API_KEY; + process.env.HWLAB_API_KEY = "secret-value"; + const tempConfigPath = `/tmp/unidesk-agentrun-input-test-${process.pid}-${Date.now()}.yaml`; + try { + 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("cannot be combined with complete resource disclosure"); + 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); + + const compact = JSON.parse(renderedTextOf(await runAgentRunCommand(null, ["describe", `task/${taskId}`, "-o", "json"]))) as { next?: { input?: string } }; + expect(compact.next?.input).toContain("--input -o json"); + + const inputText = renderedTextOf(await runAgentRunCommand(null, ["describe", `task/${taskId}`, "--input"])); + const input = JSON.parse(inputText) as { + kind?: string; + spec?: Record; + aipodSpec?: Record; + provenance?: Record; + redaction?: Record; + valuesPrinted?: boolean; + }; + expect(input.kind).toBe("TaskInput"); + expect(input.spec?.payload?.prompt).toBe(task.payload.prompt); + expect(input.spec?.idempotencyKey).toBe(task.idempotencyKey); + expect(input.spec?.metadata?.aipod).toBe("Artificer"); + expect(input.aipodSpec?.specHash).toBe("sha256:aipod-fixture"); + expect(input.provenance?.sourceTaskId).toBe(taskId); + expect(input.spec?.executionPolicy?.secretScope?.providerCredentials?.[0]?.secretRef).toEqual({ + name: "agentrun-provider", + keys: ["auth.json"], + valuesPrinted: false, + }); + expect(input.spec?.resourceBundleRef?.credentialRef).toEqual({ + name: "agentrun-resource-git", + keys: ["username", "password"], + valuesPrinted: false, + }); + expect(input.spec?.resourceBundleRef?.repoUrl).toBe("https://github.com/pikasTech/unidesk.git"); + expect(input.aipodSpec?.imageRef?.repoUrl).toBe("https://github.com/pikasTech/agentrun.git"); + expect(input.redaction?.credentialValues).toBe("omitted"); + expect(input.valuesPrinted).toBe(false); + expect(inputText).not.toContain(hiddenCredential); + expect(Buffer.byteLength(inputText, "utf8")).toBeLessThan(10_240); + + const full = JSON.parse(renderedTextOf(await runAgentRunCommand(null, ["describe", `task/${taskId}`, "--full", "-o", "json"]))) as { resource?: typeof task }; + expect(full.resource?.executionPolicy.secretScope.providerCredentials[0]?.secretRef.value).toBe(hiddenCredential); + const raw = JSON.parse(renderedTextOf(await runAgentRunCommand(null, ["describe", `task/${taskId}`, "--raw"]))) as { data?: typeof task }; + expect(raw.data?.resourceBundleRef.credentialRef.value).toBe(hiddenCredential); + expect(requests).toBe(5); + } finally { + server.stop(true); + if (previousConfig === undefined) delete process.env.AGENTRUN_CLIENT_CONFIG; + else process.env.AGENTRUN_CLIENT_CONFIG = previousConfig; + if (previousKey === undefined) delete process.env.HWLAB_API_KEY; + else process.env.HWLAB_API_KEY = previousKey; + rmSync(tempConfigPath, { force: true }); + } + }); + test("retry dry-run is server-validated and attempts use the formal resource API", async () => { const taskId = "qt_failed_1"; const attempt = { @@ -325,8 +485,10 @@ describe("AgentRun default transport contract", () => { test("retry and attempt resource help stays scoped to the controlled commands", async () => { const retryHelp = await runAgentRunCommand(null, ["retry", "--help"]); const attemptHelp = await runAgentRunCommand(null, ["get", "attempts", "--help"]); + const describeHelp = await runAgentRunCommand(null, ["describe", "--help"]); expect("renderedText" in retryHelp ? retryHelp.renderedText : "").toContain("agentrun retry task/ --idempotency-key --reason "); expect("renderedText" in attemptHelp ? attemptHelp.renderedText : "").toContain("agentrun get attempts --task "); + expect("renderedText" in describeHelp ? describeHelp.renderedText : "").toContain("agentrun describe task/qt_... --input -o json"); const rootHelp = Bun.spawnSync(["bun", "scripts/cli.ts", "agentrun", "--help"], { cwd: new URL("../..", import.meta.url).pathname, @@ -342,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"], @@ -389,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, @@ -400,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/entry.ts b/scripts/src/agentrun/entry.ts index fa3b5bb1..6913220c 100644 --- a/scripts/src/agentrun/entry.ts +++ b/scripts/src/agentrun/entry.ts @@ -57,6 +57,7 @@ export function agentRunHelp(): unknown { "bun scripts/cli.ts agentrun get tasks -o wide", "bun scripts/cli.ts agentrun get tasks -o json", "bun scripts/cli.ts agentrun describe task/", + "bun scripts/cli.ts agentrun describe task/ --input -o json", "bun scripts/cli.ts agentrun events run/ --after-seq 0 --limit 100", "bun scripts/cli.ts agentrun logs session/ --tail 100", "bun scripts/cli.ts agentrun result run/ --command ", @@ -272,11 +273,12 @@ export function agentRunHelpText(args: string[]): string { } if (verb === "describe") { return [ - "Usage: bun scripts/cli.ts agentrun describe [--node --lane ] [--full] [-o json|yaml] [--raw]", + "Usage: bun scripts/cli.ts agentrun describe [--node --lane ] [--input] [--full] [-o json|yaml] [--raw]", "", "Kinds: task|run|command|runnerjob|session|aipodspec", "Examples:", " bun scripts/cli.ts agentrun describe task/qt_...", + " bun scripts/cli.ts agentrun describe task/qt_... --input -o json", " bun scripts/cli.ts agentrun describe run/ --node NC01 --lane nc01-v02", " bun scripts/cli.ts agentrun describe command/cmd_... --run ", " bun scripts/cli.ts agentrun describe session/ --full", @@ -391,6 +393,7 @@ export function agentRunHelpText(args: string[]): string { "Common:", " bun scripts/cli.ts agentrun get tasks --queue commander --limit 20", " bun scripts/cli.ts agentrun describe task/", + " bun scripts/cli.ts agentrun describe task/ --input -o json", " bun scripts/cli.ts agentrun describe run/ --node NC01 --lane nc01-v02", " bun scripts/cli.ts agentrun events run/ --after-seq 0 --limit 100", " bun scripts/cli.ts agentrun logs session/ --tail 100", diff --git a/scripts/src/agentrun/render.ts b/scripts/src/agentrun/render.ts index abd50be8..03008f94 100644 --- a/scripts/src/agentrun/render.ts +++ b/scripts/src/agentrun/render.ts @@ -1335,12 +1335,13 @@ export function renderTaskDescription(task: Record, options: Ag "Next:", ]; const next = [ + `bun scripts/cli.ts agentrun describe task/${taskId} --input -o json`, runId === null ? null : `bun scripts/cli.ts agentrun events run/${runId} --after-seq 0 --limit 100`, sessionId === null ? null : `bun scripts/cli.ts agentrun logs session/${sessionId} --tail 100`, runId === null || commandId === null ? null : `bun scripts/cli.ts agentrun result run/${runId} --command ${commandId}`, `bun scripts/cli.ts agentrun ack task/${taskId}`, options.full ? null : `bun scripts/cli.ts agentrun describe task/${taskId} --full`, - ].filter((item): item is string => item !== null).slice(0, 5); + ].filter((item): item is string => item !== null).slice(0, 6); lines.push(...next.map((item) => ` ${item}`)); return lines.join("\n"); } @@ -1379,12 +1380,147 @@ export function compactTaskDescriptionPayload(ref: AgentRunResourceRef, task: Re logs: sessionId === null ? null : `bun scripts/cli.ts agentrun logs session/${sessionId} --tail 100`, result: runId === null || commandId === null ? null : `bun scripts/cli.ts agentrun result run/${runId} --command ${commandId}`, ack: `bun scripts/cli.ts agentrun ack task/${taskId}`, + input: `bun scripts/cli.ts agentrun describe task/${taskId} --input -o json`, full: `bun scripts/cli.ts agentrun describe task/${taskId} --full -o json`, }, valuesPrinted: false, }; } +export function taskInputDescriptionPayload(ref: AgentRunResourceRef, task: Record): Record { + const metadata = taskInputMetadata(task.metadata); + const aipodImageRef = taskInputImageRef(metadata.aipodImageRef); + if (aipodImageRef !== null) metadata.aipodImageRef = aipodImageRef; + const spec = pickCompact(task, [ + "tenantId", + "projectId", + "queue", + "lane", + "title", + "priority", + "backendProfile", + "providerId", + "payload", + "references", + "idempotencyKey", + ]); + spec.workspaceRef = taskInputWorkspaceRef(task.workspaceRef); + spec.sessionRef = taskInputSessionRef(task.sessionRef); + spec.executionPolicy = taskInputExecutionPolicy(task.executionPolicy); + spec.resourceBundleRef = taskInputResourceBundleRef(task.resourceBundleRef); + spec.metadata = metadata; + return { + kind: "TaskInput", + name: ref.name, + spec, + aipodSpec: { + name: metadata.aipod ?? null, + specHash: metadata.aipodSpecHash ?? null, + imageRef: aipodImageRef, + valuesPrinted: false, + }, + provenance: { + sourceTaskId: stringOrNull(task.id) ?? ref.name, + source: metadata.source ?? null, + payloadHash: task.payloadHash ?? null, + valuesPrinted: false, + }, + redaction: { + credentialValues: "omitted", + secretRefsOnly: true, + valuesPrinted: false, + }, + valuesPrinted: false, + }; +} + +function taskInputExecutionPolicy(value: unknown): Record | null { + if (!isRecord(value)) return null; + const policy = pickCompact(value, ["sandbox", "approval", "timeoutMs", "network"]); + const secretScope = record(value.secretScope); + policy.secretScope = { + allowCredentialEcho: false, + providerCredentials: arrayRecords(secretScope.providerCredentials).map((credential) => ({ + ...pickCompact(credential, ["profile"]), + secretRef: taskInputSecretRef(credential.secretRef), + valuesPrinted: false, + })), + toolCredentials: arrayRecords(secretScope.toolCredentials).map((credential) => ({ + ...pickCompact(credential, ["tool", "purpose"]), + secretRef: taskInputSecretRef(credential.secretRef), + projection: pickCompact(record(credential.projection), ["kind", "envName", "secretKey", "mountPath"]), + valuesPrinted: false, + })), + valuesPrinted: false, + }; + return policy; +} + +function taskInputSecretRef(value: unknown): Record | null { + if (!isRecord(value)) return null; + return { + ...pickCompact(value, ["namespace", "name", "keys", "mountPath"]), + valuesPrinted: false, + }; +} + +function taskInputResourceBundleRef(value: unknown): Record | null { + if (!isRecord(value)) return null; + const result = pickCompact(value, ["kind", "commitId", "ref", "submodules", "lfs"]); + result.repoUrl = taskInputRepoUrl(value.repoUrl); + result.bundles = arrayRecords(value.bundles).map((bundle) => ({ + ...pickCompact(bundle, ["name", "commitId", "ref", "subpath", "targetPath"]), + ...(bundle.repoUrl === undefined ? {} : { repoUrl: taskInputRepoUrl(bundle.repoUrl) }), + })); + if (Array.isArray(value.promptRefs)) { + result.promptRefs = arrayRecords(value.promptRefs).map((item) => pickCompact(item, ["name", "path", "inject", "required"])); + } + if (Array.isArray(value.requiredSkills)) { + result.requiredSkills = arrayRecords(value.requiredSkills).map((item) => pickCompact(item, ["name"])); + } + if (value.credentialRef !== undefined) result.credentialRef = taskInputSecretRef(value.credentialRef); + result.valuesPrinted = false; + return result; +} + +function taskInputWorkspaceRef(value: unknown): Record | null { + if (!isRecord(value)) return null; + const result = pickCompact(value, ["kind", "path", "branch"]); + if (value.repo !== undefined) result.repo = taskInputRepoUrl(value.repo); + return result; +} + +function taskInputSessionRef(value: unknown): Record | null { + if (!isRecord(value)) return null; + return pickCompact(value, ["sessionId", "conversationId", "threadId", "expiresAt"]); +} + +function taskInputMetadata(value: unknown): Record { + if (!isRecord(value)) return {}; + return pickCompact(value, ["aipod", "source", "aipodSpecHash", "aipodImageRef"]); +} + +function taskInputImageRef(value: unknown): Record | null { + if (!isRecord(value)) return null; + const result = pickCompact(value, ["kind", "commitId", "dockerfilePath", "sourceIdentity"]); + if (value.repoUrl !== undefined) result.repoUrl = taskInputRepoUrl(value.repoUrl); + result.valuesPrinted = false; + return result; +} + +function taskInputRepoUrl(value: unknown): unknown { + if (typeof value !== "string") return value ?? null; + if (!value.includes("://")) return value; + try { + const parsed = new URL(value); + parsed.username = ""; + parsed.password = ""; + return parsed.toString(); + } catch { + return ""; + } +} + export function unwrapTaskDetail(data: Record): Record { const task = record(data.task); if (Object.keys(task).length > 0) return task; diff --git a/scripts/src/agentrun/resource-actions.ts b/scripts/src/agentrun/resource-actions.ts index 10e67d4b..984559eb 100644 --- a/scripts/src/agentrun/resource-actions.ts +++ b/scripts/src/agentrun/resource-actions.ts @@ -37,7 +37,7 @@ import type { AgentRunResourceOptions, AgentRunResourceRef, AgentRunResourceVerb import { agentRunDryRunPlan } from "./config"; import { isHelpArg, renderAgentRunHelp } from "./entry"; import { agentRunExplain, arrayRecords, shortId } from "./options"; -import { agentRunCancelAuthorityDisclosure, agentRunCancelCascadeScope, agentRunCancelFencingDisclosure, agentRunCancelRunnerAbortDisclosure, compactTaskDescriptionPayload, innerData, normalizeAipodSpecItems, normalizeAttemptResources, normalizeEventItems, normalizeLogItems, normalizeQueueAttemptItems, normalizeRunnerJobItems, normalizeSessionItems, normalizeSingleCommand, normalizeTaskItems, parseResourceKind, parseResourceRef, renderDescribe, renderEventLike, renderGenericDescription, renderMachine, renderMutationSummary, renderQueueAttemptList, renderQueueRetrySummary, renderResourceResult, renderResultSummary, renderTaskDescription, renderedCliResult, requiredContext, rerunWithoutDryRun, resolveAgentRunCancelPolicyTarget, resourceApply, resourceCreate, resourceDispatch, resourceSessionPromptCommand, taskListState, unwrapTaskDetail } from "./render"; +import { agentRunCancelAuthorityDisclosure, agentRunCancelCascadeScope, agentRunCancelFencingDisclosure, agentRunCancelRunnerAbortDisclosure, compactTaskDescriptionPayload, innerData, normalizeAipodSpecItems, normalizeAttemptResources, normalizeEventItems, normalizeLogItems, normalizeQueueAttemptItems, normalizeRunnerJobItems, normalizeSessionItems, normalizeSingleCommand, normalizeTaskItems, parseResourceKind, parseResourceRef, renderDescribe, renderEventLike, renderGenericDescription, renderMachine, renderMutationSummary, renderQueueAttemptList, renderQueueRetrySummary, renderResourceResult, renderResultSummary, renderTaskDescription, renderedCliResult, requiredContext, rerunWithoutDryRun, resolveAgentRunCancelPolicyTarget, resourceApply, resourceCreate, resourceDispatch, resourceSessionPromptCommand, taskInputDescriptionPayload, taskListState, unwrapTaskDetail } from "./render"; import { AgentRunRestError, renderAgentRunRestError, resolveAgentRunRestTarget, runAgentRunRestCommand, withAgentRunRestTarget } from "./rest-bridge"; import { runnerJobObservationScript, type RunnerJobObservationIdentity } from "./runner-observation"; import { capture, captureJsonPayload, compactCapture, nonNegativeIntegerOrNull, record, stringOrNull } from "./utils"; @@ -121,7 +121,7 @@ export function stripAgentRunResourceWrapperArgs(args: string[]): string[] { } if (arg.startsWith("--output=") || arg.startsWith("-o=")) continue; if (arg.startsWith("--node=") || arg.startsWith("--lane=")) continue; - if (arg === "--full" || arg === "--raw" || arg === "--debug" || arg === "--full-text") continue; + if (arg === "--full" || arg === "--raw" || arg === "--debug" || arg === "--full-text" || arg === "--input") continue; result.push(arg); } return result; @@ -133,6 +133,7 @@ export function parseResourceOptions(args: string[]): AgentRunResourceOptions { full: false, raw: false, debug: false, + taskInput: false, limit: 20, cursor: null, queue: null, @@ -157,7 +158,7 @@ export function parseResourceOptions(args: string[]): AgentRunResourceOptions { passthroughArgs: [], }; const valueFlags = new Set(["-o", "--output", "--limit", "--cursor", "--queue", "--state", "--reader-id", "--task", "--task-id", "--run", "--run-id", "--command", "--command-id", "--session", "--session-id", "--after-seq", "--tail", "--reason", "-f", "--file", "--filename", "--aipod", "--idempotency-key", "--node", "--lane"]); - const booleanFlags = new Set(["--full", "--raw", "--debug", "--unread", "--dry-run", "--full-text", "--prompt-stdin", "--stdin"]); + const booleanFlags = new Set(["--full", "--raw", "--debug", "--input", "--unread", "--dry-run", "--full-text", "--prompt-stdin", "--stdin"]); for (let index = 0; index < args.length; index += 1) { const arg = args[index] ?? ""; if (!arg.startsWith("-")) continue; @@ -178,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; } @@ -190,6 +209,7 @@ export function applyResourceOption(options: AgentRunResourceOptions, flag: stri if (flag === "--full") options.full = true; else if (flag === "--raw") options.raw = true; else if (flag === "--debug") options.debug = true; + else if (flag === "--input") options.taskInput = true; else if (flag === "--unread") options.unread = true; else if (flag === "--dry-run") options.dryRun = true; else if (flag === "--full-text") options.fullText = true; @@ -303,15 +323,21 @@ export async function resourceRetry(config: UniDeskConfig | null, command: strin export async function resourceDescribe(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise { const ref = parseResourceRef(action, args); + if (options.taskInput && ref.kind !== "task") throw new AgentRunRestError("validation-failed", "describe --input supports task/ only"); if (ref.kind === "task") { const result = await runAgentRunRestCommand(config, "queue", ["show", ref.name, ...(options.full ? ["--full"] : [])]); const data = record(innerData(result)); const task = unwrapTaskDetail(data); if (options.raw) return renderMachine(command, result, "json", result.ok !== false); if (options.output === "json" || options.output === "yaml") { - const payload = options.full ? { kind: ref.kind, name: ref.name, resource: task } : compactTaskDescriptionPayload(ref, task); + const payload = options.full + ? { kind: ref.kind, name: ref.name, resource: task } + : options.taskInput + ? taskInputDescriptionPayload(ref, task) + : compactTaskDescriptionPayload(ref, task); return renderMachine(command, payload, options.output, result.ok !== false); } + if (options.taskInput && !options.full) return renderMachine(command, taskInputDescriptionPayload(ref, task), "json", result.ok !== false); return renderedCliResult(result.ok !== false, command, renderTaskDescription(task, options)); } if (ref.kind === "run") { diff --git a/scripts/src/agentrun/utils.ts b/scripts/src/agentrun/utils.ts index 3395b39b..8dec3890 100644 --- a/scripts/src/agentrun/utils.ts +++ b/scripts/src/agentrun/utils.ts @@ -64,6 +64,7 @@ export interface AgentRunResourceOptions { full: boolean; raw: boolean; debug: boolean; + taskInput: boolean; limit: number; cursor: number | null; queue: string | null; diff --git a/scripts/src/help.ts b/scripts/src/help.ts index 7fb7ca40..1cc9fcda 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -831,6 +831,7 @@ function agentRunHelpSummary(): unknown { usage: [ "bun scripts/cli.ts agentrun get tasks --queue commander --limit 20", "bun scripts/cli.ts agentrun describe task/", + "bun scripts/cli.ts agentrun describe task/ --input -o json", "bun scripts/cli.ts agentrun events run/ --after-seq 0 --limit 100", "bun scripts/cli.ts agentrun logs session/ --tail 100", "bun scripts/cli.ts agentrun result run/ --command ",