Merge pull request #1821 from pikasTech/fix/1819-agentrun-task-input
Pipelines as Code CI / hwlab-web-probe-sentinel-nc01- Success
Pipelines as Code CI / platform-infra-gitea-nc01- Success
Pipelines as Code CI / unidesk-host- Success

fix: 增加 AgentRun task 输入的有界下钻
This commit is contained in:
Lyon
2026-07-12 12:32:51 +08:00
committed by GitHub
8 changed files with 355 additions and 7 deletions
@@ -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/<taskId>
bun scripts/cli.ts agentrun describe task/<taskId> --input -o json
bun scripts/cli.ts agentrun events run/<runId> --limit 20
bun scripts/cli.ts agentrun logs session/<sessionId> --tail 120
bun scripts/cli.ts agentrun result command/<commandId> --run <runId>
@@ -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/<taskId> --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/<taskId>`:保持同一 task,新增不可变 attempt;先用 `--dry-run` 完成服务端全校验,再以同一幂等键执行真实重试。
- retry 只接受 Queue 已处于 `failed``blocked``completed``cancelled` 和其他状态必须 fail closed。不得复制 task、重置 task、引入本地 scheduler/backoff/judge 或直接创建 runner Job 来模拟重试。
@@ -4,6 +4,7 @@
- `agentrun create|apply`:创建 task 或应用 AipodSpec manifest
- `agentrun get|describe`:有界读取列表和详情;
- `agentrun describe task/<taskId> --input -o json`:读取可重建 task 输入、AipodSpec identity、资源引用和脱敏 provenance
- `agentrun events|logs|result`:渐进披露运行证据;
- `agentrun get attempts --task <taskId>`:读取同一 task 的不可变 attempt 历史;
- `agentrun retry task/<taskId> --idempotency-key <key> --reason <text> [--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/<taskId>``Next` 必须给出 `--input -o json` 语义化命令;
- `--input` 只读取现有 task durable fact,不创建、复制、retry 或 dispatch task
- 输出固定为 task create 字段、AipodSpec identity、bundle/SecretRef 和脱敏 provenancecredential 只保留 SecretRef 并声明 `valuesPrinted=false`
- `--input``--full``--raw` 互斥,非法组合必须在请求服务端前返回带修正路径的 `validation-failed`
- 完整服务端资源继续使用显式 `--full -o json``--raw`,禁止提高全局 stdout 阈值来掩盖大输出。
retry 必须遵守以下边界:
- task identity 不变,每次执行使用新的 attempt、run、command 和 runner Job identity
+167 -1
View File
@@ -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<string, any>;
aipodSpec?: Record<string, any>;
provenance?: Record<string, any>;
redaction?: Record<string, any>;
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/<taskId> --idempotency-key <key> --reason <text>");
expect("renderedText" in attemptHelp ? attemptHelp.renderedText : "").toContain("agentrun get attempts --task <taskId>");
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":');
}
});
+4 -1
View File
@@ -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/<taskId>",
"bun scripts/cli.ts agentrun describe task/<taskId> --input -o json",
"bun scripts/cli.ts agentrun events run/<runId> --after-seq 0 --limit 100",
"bun scripts/cli.ts agentrun logs session/<sessionId> --tail 100",
"bun scripts/cli.ts agentrun result run/<runId> --command <commandId>",
@@ -272,11 +273,12 @@ export function agentRunHelpText(args: string[]): string {
}
if (verb === "describe") {
return [
"Usage: bun scripts/cli.ts agentrun describe <kind/name> [--node <node> --lane <lane>] [--full] [-o json|yaml] [--raw]",
"Usage: bun scripts/cli.ts agentrun describe <kind/name> [--node <node> --lane <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/<runId> --node NC01 --lane nc01-v02",
" bun scripts/cli.ts agentrun describe command/cmd_... --run <runId>",
" bun scripts/cli.ts agentrun describe session/<sessionId> --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/<taskId>",
" bun scripts/cli.ts agentrun describe task/<taskId> --input -o json",
" bun scripts/cli.ts agentrun describe run/<runId> --node NC01 --lane nc01-v02",
" bun scripts/cli.ts agentrun events run/<runId> --after-seq 0 --limit 100",
" bun scripts/cli.ts agentrun logs session/<sessionId> --tail 100",
+137 -1
View File
@@ -1335,12 +1335,13 @@ export function renderTaskDescription(task: Record<string, unknown>, 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<string, unknown>): Record<string, unknown> {
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<string, unknown> | 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<string, unknown> | null {
if (!isRecord(value)) return null;
return {
...pickCompact(value, ["namespace", "name", "keys", "mountPath"]),
valuesPrinted: false,
};
}
function taskInputResourceBundleRef(value: unknown): Record<string, unknown> | 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<string, unknown> | 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<string, unknown> | null {
if (!isRecord(value)) return null;
return pickCompact(value, ["sessionId", "conversationId", "threadId", "expiresAt"]);
}
function taskInputMetadata(value: unknown): Record<string, unknown> {
if (!isRecord(value)) return {};
return pickCompact(value, ["aipod", "source", "aipodSpecHash", "aipodImageRef"]);
}
function taskInputImageRef(value: unknown): Record<string, unknown> | 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 "<invalid-or-redacted-url>";
}
}
export function unwrapTaskDetail(data: Record<string, unknown>): Record<string, unknown> {
const task = record(data.task);
if (Object.keys(task).length > 0) return task;
+30 -4
View File
@@ -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<RenderedCliResult> {
const ref = parseResourceRef(action, args);
if (options.taskInput && ref.kind !== "task") throw new AgentRunRestError("validation-failed", "describe --input supports task/<taskId> 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") {
+1
View File
@@ -64,6 +64,7 @@ export interface AgentRunResourceOptions {
full: boolean;
raw: boolean;
debug: boolean;
taskInput: boolean;
limit: number;
cursor: number | null;
queue: string | null;
+1
View File
@@ -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/<taskId>",
"bun scripts/cli.ts agentrun describe task/<taskId> --input -o json",
"bun scripts/cli.ts agentrun events run/<runId> --after-seq 0 --limit 100",
"bun scripts/cli.ts agentrun logs session/<sessionId> --tail 100",
"bun scripts/cli.ts agentrun result run/<runId> --command <commandId>",