Merge pull request #1821 from pikasTech/fix/1819-agentrun-task-input
fix: 增加 AgentRun task 输入的有界下钻
This commit is contained in:
@@ -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 和脱敏 provenance,credential 只保留 SecretRef 并声明 `valuesPrinted=false`;
|
||||
- `--input` 与 `--full`、`--raw` 互斥,非法组合必须在请求服务端前返回带修正路径的 `validation-failed`;
|
||||
- 完整服务端资源继续使用显式 `--full -o json` 或 `--raw`,禁止提高全局 stdout 阈值来掩盖大输出。
|
||||
|
||||
retry 必须遵守以下边界:
|
||||
|
||||
- task identity 不变,每次执行使用新的 attempt、run、command 和 runner Job identity;
|
||||
|
||||
@@ -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":');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -64,6 +64,7 @@ export interface AgentRunResourceOptions {
|
||||
full: boolean;
|
||||
raw: boolean;
|
||||
debug: boolean;
|
||||
taskInput: boolean;
|
||||
limit: number;
|
||||
cursor: number | null;
|
||||
queue: string | null;
|
||||
|
||||
@@ -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>",
|
||||
|
||||
Reference in New Issue
Block a user