Merge pull request #143 from pikasTech/fix/queue-stdin-disclosure

fix: Queue dry-run 默认提示 stdin 输入
This commit is contained in:
Lyon
2026-06-10 13:02:10 +08:00
committed by GitHub
2 changed files with 100 additions and 16 deletions
+49 -15
View File
@@ -648,7 +648,7 @@ function summarizeQueueTaskMutationResult(action: "queue-read" | "queue-cancel"
};
}
function queueMutationDryRunPlan(action: string, taskId: string | null, pathValue: string, body: JsonRecord, method: "POST", confirmCommand: string, task?: JsonValue): JsonRecord {
function queueMutationDryRunPlan(action: string, taskId: string | null, pathValue: string, body: JsonRecord, method: "POST", confirmCommand: string, task?: JsonValue, jsonInput?: JsonRecord): JsonRecord {
return {
action: `${action}-plan`,
dryRun: true,
@@ -661,15 +661,53 @@ function queueMutationDryRunPlan(action: string, taskId: string | null, pathValu
bodyBytes: jsonByteLength(body),
valuesPrinted: false,
},
...(jsonInput ? { jsonInput } : {}),
...(task === undefined ? {} : { task: summarizeQueueTaskWithAttempt(jsonRecordValue(task), taskId ?? stringValue(jsonRecordValue(task)?.id) ?? "unknown") }),
next: {
confirm: confirmCommand,
note: "Remove --dry-run to perform the mutation.",
note: "Remove --dry-run to perform the mutation. Prefer --json-stdin with a quoted heredoc for one-shot JSON; use --json-file only for reusable files.",
},
valuesPrinted: false,
};
}
function jsonInputDisclosure(args: ParsedArgs, filePlaceholder: string, options: { required?: boolean } = {}): JsonRecord {
const source = args.flags.get("json-stdin") === true ? "stdin" : optionalFlag(args, "json-file") ? "file" : "none";
return {
source,
required: options.required ?? true,
preferred: "--json-stdin",
fileFallback: `--json-file ${filePlaceholder}`,
note: "Use a quoted heredoc, for example: ./scripts/agentrun ... --json-stdin <<'JSON'. Do not create temporary dump files for one-shot Queue bodies.",
valuesPrinted: false,
};
}
function queueSubmitConfirmCommand(args: ParsedArgs): string {
const parts = ["./scripts/agentrun queue submit --json-stdin"];
if (optionalFlag(args, "idempotency-key")) parts.push("--idempotency-key <idempotency-key>");
return parts.join(" ");
}
function queueDispatchConfirmCommand(args: ParsedArgs, taskId: string): string {
const parts = [`./scripts/agentrun queue dispatch ${taskId}`];
if (args.flags.get("json-stdin") === true || optionalFlag(args, "json-file")) parts.push("--json-stdin");
const flagPlaceholders: Record<string, string> = {
"idempotency-key": "<idempotency-key>",
image: "<image>",
namespace: "<namespace>",
"attempt-id": "<attempt-id>",
"runner-id": "<runner-id>",
"source-commit": "<source-commit>",
"runner-manager-url": "<url|auto>",
"service-account-name": "<service-account-name>",
};
for (const [flagName, placeholder] of Object.entries(flagPlaceholders)) {
if (optionalFlag(args, flagName)) parts.push(`--${flagName} ${placeholder}`);
}
return parts.join(" ");
}
function summarizeMutationBody(body: JsonRecord): JsonRecord {
return {
...compactRecord(body, { keys: ["idempotencyKey", "image", "namespace", "attemptId", "runnerId", "sourceCommit", "managerUrl", "serviceAccountName", "readerId", "reason"] }),
@@ -1049,7 +1087,7 @@ async function submitQueueTask(args: ParsedArgs): Promise<JsonValue> {
const idempotencyKey = optionalFlag(args, "idempotency-key");
if (idempotencyKey) body.idempotencyKey = idempotencyKey;
if (args.flags.get("dry-run") === true) {
return queueMutationDryRunPlan("queue-submit", null, "/api/v1/queue/tasks", body, "POST", `./scripts/agentrun queue submit ${jsonInputHelp(args, "<task.json>")}`);
return queueMutationDryRunPlan("queue-submit", null, "/api/v1/queue/tasks", body, "POST", queueSubmitConfirmCommand(args), undefined, jsonInputDisclosure(args, "<task.json>"));
}
return client(args).post("/api/v1/queue/tasks", body);
}
@@ -1100,7 +1138,7 @@ async function dispatchQueueTask(args: ParsedArgs, taskId: string): Promise<Json
const body = await queueDispatchBody(args);
if (args.flags.get("dry-run") === true) {
const task = await client(args).get(`/api/v1/queue/tasks/${encodeURIComponent(taskId)}`);
return queueMutationDryRunPlan("queue-dispatch", taskId, `/api/v1/queue/tasks/${encodeURIComponent(taskId)}/dispatch`, body, "POST", `./scripts/agentrun queue dispatch ${taskId} ${jsonInputHelp(args, "<dispatch.json>")}`, task);
return queueMutationDryRunPlan("queue-dispatch", taskId, `/api/v1/queue/tasks/${encodeURIComponent(taskId)}/dispatch`, body, "POST", queueDispatchConfirmCommand(args, taskId), task, jsonInputDisclosure(args, "<dispatch.json>", { required: false }));
}
const result = await client(args).post(`/api/v1/queue/tasks/${encodeURIComponent(taskId)}/dispatch`, body);
if (wantsExpandedOutput(args)) return result;
@@ -1500,7 +1538,7 @@ async function sleep(ms: number): Promise<void> {
async function jsonFile(args: ParsedArgs): Promise<JsonRecord> {
if (args.flags.get("json-stdin") === true) return parseJsonObject(await readStdinText(), "stdin json");
const file = optionalFlag(args, "json-file");
if (!file) throw new AgentRunError("schema-invalid", "JSON input is required; use --json-file <file> or --json-stdin", { httpStatus: 2 });
if (!file) throw new AgentRunError("schema-invalid", "JSON input is required; prefer --json-stdin with a quoted heredoc, or use --json-file <file> for reusable files", { httpStatus: 2 });
return parseJsonObject(await readFile(file, "utf8"), "json file");
}
@@ -1524,10 +1562,6 @@ function parseJsonObject(text: string, source: string): JsonRecord {
throw new AgentRunError("schema-invalid", `${source} must contain an object`, { httpStatus: 2 });
}
function jsonInputHelp(args: ParsedArgs, filePlaceholder: string): string {
return args.flags.get("json-stdin") === true ? "--json-stdin" : `--json-file ${filePlaceholder}`;
}
async function readPrompt(args: ParsedArgs): Promise<string> {
const promptFlag = optionalFlag(args, "prompt");
if (promptFlag) return promptFlag;
@@ -1643,7 +1677,7 @@ function cancelBody(args: ParsedArgs): JsonRecord {
function help(args: ParsedArgs, group?: string): JsonRecord {
const commands = [
"runs create --json-file <run.json>|--json-stdin",
"runs create --json-stdin|--json-file <run.json>",
"runs show <runId>",
"runs events <runId> --after-seq <n> --limit <n> [--summary|--tail-summary] [--tail <n>] [--summary-chars <n>] [--format json|tsv]",
"runs result <runId> [--command-id <commandId>]",
@@ -1653,13 +1687,13 @@ function help(args: ParsedArgs, group?: string): JsonRecord {
"sessions storage <sessionId>",
"sessions storage <sessionId> --delete",
"sessions show <sessionId> [--reader-id <reader>]",
"sessions turn [sessionId] [--json-file <run-base.json>|--json-stdin] [--prompt-file <file>|--prompt-stdin|--prompt <text>] [--profile codex|deepseek|minimax-m3|dsflash-go|<dynamic-profile>|M3] [--runner-json-file <job.json>|--runner-json-stdin]",
"sessions steer <sessionId> [--prompt-file <file>|--prompt-stdin|--prompt <text>]",
"sessions turn [sessionId] [--json-stdin|--json-file <run-base.json>] [--prompt-stdin|--prompt-file <file>|--prompt <text>] [--profile codex|deepseek|minimax-m3|dsflash-go|<dynamic-profile>|M3] [--runner-json-stdin|--runner-json-file <job.json>]",
"sessions steer <sessionId> [--prompt-stdin|--prompt-file <file>|--prompt <text>]",
"sessions cancel <sessionId> [--reason <text>] [--full|--raw]",
"sessions trace <sessionId> [--after-seq <n>] [--limit <n>] [--run-id <runId>] [--summary-chars <n>] [--include-output] [--seq <n>|--event-id <id>|--item-id <id>] [--detail-scan-pages <n>] [--full|--raw]",
"sessions output <sessionId> [--after-seq <n>] [--limit <n>] [--run-id <runId>] [--summary-chars <n>] [--include-output] [--seq <n>|--event-id <id>|--item-id <id>] [--detail-scan-pages <n>] [--full|--raw]",
"sessions read <sessionId> [--reader-id <reader>] [--full|--raw]",
"commands create <runId> --type turn|steer|interrupt --json-file <payload.json>|--json-stdin",
"commands create <runId> --type turn|steer|interrupt --json-stdin|--json-file <payload.json>",
"commands show <commandId> --run-id <runId>",
"commands result <commandId> --run-id <runId>",
"commands cancel <commandId> [--reason <text>]",
@@ -1668,14 +1702,14 @@ function help(args: ParsedArgs, group?: string): JsonRecord {
"runner job --dry-run --run-id <runId> --command-id <commandId> --image <image>",
"runner jobs --run-id <runId> [--command-id <commandId>]",
"runner job-status [runnerJobId] --run-id <runId>",
"queue submit --json-file <task.json>|--json-stdin [--idempotency-key <key>] [--dry-run]",
"queue submit --json-stdin|--json-file <task.json> [--idempotency-key <key>] [--dry-run]",
"queue list [--queue <queue>] [--state <state>] [--cursor <cursor>] [--limit <limit>] [--updated-after <version>] [--full|--raw]",
"queue show <taskId> [--full|--raw]",
"queue stats [--queue <queue>]",
"queue commander [--queue <queue>] [--reader-id <reader>] [--limit <display-limit>] [--full|--raw]",
"queue read <taskId> [--reader-id <reader>] [--dry-run] [--full|--raw]",
"queue cancel <taskId> [--reason <text>] [--dry-run] [--full|--raw]",
"queue dispatch <taskId> [--json-file <dispatch.json>|--json-stdin] [--idempotency-key <key>] [--image <image>] [--namespace <namespace>] [--dry-run] [--full|--raw]",
"queue dispatch <taskId> [--json-stdin|--json-file <dispatch.json>] [--idempotency-key <key>] [--image <image>] [--namespace <namespace>] [--dry-run] [--full|--raw]",
"queue refresh <taskId> [--dry-run] [--full|--raw]",
"secrets codex render --dry-run [--profile codex|deepseek|minimax-m3|dsflash-go|<dynamic-profile>] [--codex-home <dir>] [--model-catalog-file <file>] [--namespace agentrun-v01] [--secret-name <name>]",
"provider-profiles list",
+51 -1
View File
@@ -1,4 +1,5 @@
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { chmod, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { startManagerServer } from "../../mgr/server.js";
@@ -53,6 +54,25 @@ process.exit(1);
});
try {
const client = new ManagerClient(server.baseUrl);
const stdinSubmitPlan = await runCliJson(context, server.baseUrl, ["queue", "submit", "--json-stdin", "--dry-run", "--idempotency-key", "queue-q2-cli-stdin-dry-run"], {
tenantId: "unidesk",
projectId: "pikasTech/unidesk",
queue: "dev",
title: "stdin queue dry-run task",
payload: { prompt: "stdin queue dry-run" },
});
assert.equal(stdinSubmitPlan.ok, true);
assert.equal(((stdinSubmitPlan.data as JsonRecord).action), "queue-submit-plan");
assert.equal((((stdinSubmitPlan.data as JsonRecord).jsonInput as JsonRecord).preferred), "--json-stdin");
assert.equal(String(((stdinSubmitPlan.data as JsonRecord).next as JsonRecord).confirm).includes("--json-stdin"), true);
assert.equal(String(((stdinSubmitPlan.data as JsonRecord).next as JsonRecord).confirm).includes("--idempotency-key <idempotency-key>"), true);
assert.equal(String(((stdinSubmitPlan.data as JsonRecord).next as JsonRecord).confirm).includes("--json-file"), false);
const help = await runCliJson(context, server.baseUrl, ["help"]);
const commands = ((help.data as JsonRecord).commands as string[]) ?? [];
assert.equal(commands.some((item) => item.includes("queue submit --json-stdin|--json-file")), true);
assert.equal(commands.some((item) => item.includes("queue submit --json-file <task.json>|--json-stdin")), false);
const created = await client.post("/api/v1/queue/tasks", {
tenantId: "unidesk",
projectId: "pikasTech/unidesk",
@@ -77,6 +97,17 @@ process.exit(1);
metadata: { source: "queue-q2-self-test" },
idempotencyKey: "queue-q2-dispatch-self-test",
}) as QueueTaskRecord;
const dispatchPlan = await runCliJson(context, server.baseUrl, ["queue", "dispatch", String(created.id), "--dry-run", "--attempt-id", "attempt_queue_q2_cli_dryrun"]);
assert.equal(dispatchPlan.ok, true);
assert.equal(((dispatchPlan.data as JsonRecord).action), "queue-dispatch-plan");
assert.equal(String(((dispatchPlan.data as JsonRecord).next as JsonRecord).confirm).includes("--json-file"), false);
assert.equal(String(((dispatchPlan.data as JsonRecord).next as JsonRecord).confirm).includes("--attempt-id <attempt-id>"), true);
const dispatchStdinPlan = await runCliJson(context, server.baseUrl, ["queue", "dispatch", String(created.id), "--json-stdin", "--dry-run"], { attemptId: "attempt_queue_q2_cli_stdin_dryrun" });
assert.equal(dispatchStdinPlan.ok, true);
assert.equal(String(((dispatchStdinPlan.data as JsonRecord).next as JsonRecord).confirm).includes("--json-stdin"), true);
assert.equal(String(((dispatchStdinPlan.data as JsonRecord).next as JsonRecord).confirm).includes("--json-file"), false);
const dispatched = await client.post(`/api/v1/queue/tasks/${created.id}/dispatch`, { attemptId: "attempt_queue_q2_selftest" }) as QueueDispatchResult;
assert.equal(dispatched.action, "queue-dispatch");
assert.equal(dispatched.mutation, true);
@@ -242,7 +273,7 @@ process.exit(1);
assert.ok(JSON.stringify(cancelManifest).includes(cancelDispatched.run.id));
assertNoSecretLeak(dispatched);
assertNoSecretLeak(cancelled);
return { name: "queue-q2-dispatch", tests: ["queue-dispatch-run-command-runner-job", "queue-read-views-refresh-terminal-state", "queue-refresh-from-core-status", "queue-dispatch-no-repeat", "queue-unidesk-ssh-endpoint-auto-env", "queue-blocked-run-state-wins-over-command-failed", "queue-cancel-propagates-to-run-command-session"] };
return { name: "queue-q2-dispatch", tests: ["queue-cli-json-stdin-dry-run", "queue-dispatch-run-command-runner-job", "queue-read-views-refresh-terminal-state", "queue-refresh-from-core-status", "queue-dispatch-no-repeat", "queue-unidesk-ssh-endpoint-auto-env", "queue-blocked-run-state-wins-over-command-failed", "queue-cancel-propagates-to-run-command-session"] };
} finally {
await new Promise<void>((resolve) => server.server.close(() => resolve()));
}
@@ -250,6 +281,25 @@ process.exit(1);
export default selfTest;
async function runCliJson(context: { root: string }, managerUrl: string, args: string[], stdinJson?: JsonRecord): Promise<JsonRecord> {
const proc = spawn(process.execPath, [`${context.root}/scripts/agentrun-cli.ts`, "--manager-url", managerUrl, ...args], { stdio: ["pipe", "pipe", "pipe"] });
if (stdinJson !== undefined) proc.stdin.write(JSON.stringify(stdinJson));
proc.stdin.end();
const [stdout, stderr, code] = await Promise.all([readStream(proc.stdout), readStream(proc.stderr), new Promise<number | null>((resolve) => proc.on("close", resolve))]);
assert.equal(code, 0, stderr || stdout);
return JSON.parse(stdout) as JsonRecord;
}
async function readStream(stream: NodeJS.ReadableStream): Promise<string> {
const chunks: Buffer[] = [];
stream.on("data", (chunk: Buffer | string) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
await new Promise<void>((resolve, reject) => {
stream.on("end", resolve);
stream.on("error", reject);
});
return Buffer.concat(chunks).toString("utf8");
}
function runnerEnvValue(manifest: JsonRecord, name: string): unknown {
const spec = manifest.spec as JsonRecord;
const template = spec.template as JsonRecord;