From f87884185714ff3ad0614f8709e7d70e9a171d38 Mon Sep 17 00:00:00 2001 From: unidesk-code-queue-runner Date: Sat, 23 May 2026 08:27:23 +0000 Subject: [PATCH] fix: expose codex submit execution mode mapping --- AGENTS.md | 2 +- docs/reference/cli.md | 2 +- docs/reference/code-queue-supervision.md | 2 +- ...eue-submit-execution-mode-contract-test.ts | 152 ++++++++++++++++++ scripts/src/check.ts | 3 + scripts/src/code-queue.ts | 86 +++++++++- scripts/src/help.ts | 5 + .../code-queue/src/code-agent/common.ts | 13 ++ .../microservices/code-queue/src/index.ts | 11 +- .../microservices/code-queue/src/queue-api.ts | 12 +- .../microservices/code-queue/src/task-view.ts | 4 + .../microservices/code-queue/src/types.ts | 2 + 12 files changed, 287 insertions(+), 7 deletions(-) create mode 100644 scripts/code-queue-submit-execution-mode-contract-test.ts diff --git a/AGENTS.md b/AGENTS.md index 64eaec7f..87b0748d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,7 +49,7 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文 - `bun scripts/cli.ts commander contract|plan --dry-run|smoke --dry-run|approval request --dry-run`:查看 host Codex 指挥官直管微服务 skeleton 的 source/contract、无 daemon smoke 验证计划、.state/commander/ 状态模型、trace summary 聚合和 ClaudeQQ 高风险请示草案;当前只返回 dry-run 计划,不接 live bridge、不接管人工指挥官,不发送消息,规则见 `docs/reference/host-codex-commander.md`。 - `bun scripts/cli.ts ci install/status/run/publish-backend-core/publish-user-service/run-dev-e2e/logs`:在 D601 原生 k3s 上安装和运行 Tekton CI,支持每 commit 检查、Code Queue 只读性能门禁、`CI.json` catalog 驱动的 backend-core 与 user-service commit-pinned 镜像发布和手动触发的 `origin/master:deploy.json#environments.dev` 临时 namespace e2e;catalog/producer/consumer 分工见 `docs/reference/cicd-standardization.md`,`run-dev-e2e` 的 Git 控制 runner、短 launcher 和 no-CD 边界见 `docs/reference/dev-ci-runner.md`,Tekton 规则见 `docs/reference/ci.md`。 - `bun scripts/cli.ts codex deploy `:旧 Code Queue 兼容部署入口已禁用,原因是它会绕过受控部署边界直连 D601 部署 Code Queue;规则见 `docs/reference/codex-deploy.md`。 -- `bun scripts/cli.ts codex prompt-lint [prompt|--prompt-file path|--prompt-stdin]` / `codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue ]` / `codex pr-preflight [--remote]`:`prompt-lint` 在派发/steer 前 dry-run 检查 runner prompt 的 DEV 测试授权分级(`read-only`/`live-read`/`live-mutating`)且不回显 prompt;`submit --dry-run` 同时给出 MiniMax/GPT/人工路由建议和该 lint 结果但不改写 payload,真实提交成功只返回写入确认、task id 和后续查看命令,不回显 prompt;`pr-preflight` 只读检查 D601 scheduler/runner 的 GitHub token、egress 和 PR 能力,PR 型派单前必须使用,规则见 `docs/reference/cli.md` 和 `docs/reference/code-queue-supervision.md`。 +- `bun scripts/cli.ts codex prompt-lint [prompt|--prompt-file path|--prompt-stdin]` / `codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue ]` / `codex pr-preflight [--remote]`:`prompt-lint` 在派发/steer 前 dry-run 检查 runner prompt 的 DEV 测试授权分级(`read-only`/`live-read`/`live-mutating`)且不回显 prompt;`submit --dry-run` 同时给出 MiniMax/GPT/人工路由建议、该 lint 结果和 requested/effective execution mode;真实提交成功只返回写入确认、task id、服务级 runnerPermissions 和后续查看命令,不回显 prompt;`pr-preflight` 只读检查 D601 scheduler/runner 的 GitHub token、egress 和 PR 能力,PR 型派单前必须使用,规则见 `docs/reference/cli.md` 和 `docs/reference/code-queue-supervision.md`。 - `bun scripts/cli.ts codex task `:按 Code Queue 任务 ID 查询默认审阅摘要,只返回原始 prompt、最终 response、最后错误和渐进披露命令;`--detail`、`codex output` 和 supervisor 大 `--limit` 仍默认有界,完整内容需显式 `--full`/`--full-text`/分页展开;`codex queues [--full] [--limit N] [--page N|--offset N]` 默认分页低噪声输出队列摘要,完整 upstream 只通过 raw command 显式获取。 - `bun scripts/cli.ts codex unread [--repo owner/name] [--issue N] [--limit N]`:只读汇总完成未读积压并给出 repo/issue/status/queue 计数和 drill-down/read 命令;批量已读必须显式 `codex unread mark-read ... --confirm`,规则见 `docs/reference/cli.md`。 - `bun scripts/cli.ts codex judge --attempt [--dry-run]`:按指定 task/attempt 用与队列 worker 相同的上下文构建和 MiniMax judge 调用路径单步复现完成判定;`--dry-run` 只输出 prompt/payload 诊断。 diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 0d7b84e7..45985a7f 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -45,7 +45,7 @@ CLI 可以从 `master` 快速演进,但必须兼容 `deploy.json` 固定的 CI - `ci install|status|run|publish-backend-core|publish-user-service|run-dev-e2e|logs` 管理 D601 原生 k3s 上的 Tekton CI。`run` 手动创建每 commit 检查和 Code Queue 只读性能门禁;`publish-backend-core` 与 `publish-user-service` 从 pushed Git commit 构建并发布 `127.0.0.1:5000/unidesk/:` commit-pinned artifacts,输出 `artifactSummary`(含 `serviceId`、`sourceCommit`、`sourceRepo`、`dockerfile`、`imageRef`、`tag`、`digest`、`digestRef`),但不部署生产;`run-dev-e2e` 的 Git 控制 runner、短 launcher、host fetch 边界、临时 smoke namespace 和 no-CD 规则只在 `docs/reference/dev-ci-runner.md` 定义;Tekton CI 通用规则见 `docs/reference/ci.md`。 - `schedule list|get|runs|run|retry-run|delete|upsert-pgdata-backup` 管理 backend-core 定时任务和运行历史。`schedule list`、`schedule get`、`schedule runs --limit N` 和 `schedule runs --limit N` 是只读观察入口;`schedule run`、`schedule retry-run`、`schedule delete` 和 `schedule upsert-pgdata-backup` 会触发运行或写入配置,生产恢复时必须有明确授权。`schedule runs --limit N` 是全局历史视图,返回 `scope=global` 和 `scheduleId=null`;`schedule runs --limit N` 是指定 schedule 历史视图,返回 `scope=schedule` 和对应 `scheduleId`。CLI 必须拒绝 `schedule runs 50` 这类纯数字位置参数,并提示使用 `schedule runs --limit 50`,避免把空数组误判成“没有历史 run”。`schedule run --wait-ms N` 触发同一 schedule,并且即使 wait 超时也必须返回 `newRunId` 和 `observeCommand`;`schedule retry-run ` 只接受 failed run,从原 run 反查 `scheduleId` 后重触发同一 schedule,并输出 `originalRunId`、`scheduleId`、`newRunId` 和 `observeCommand`。当 backend-core 目标容器缺失或只观察到 verify-only 容器时,schedule/microservice 命令必须以非零退出并返回 `failureKind=target-stack-not-running`、`runnerDisposition=infra-blocked`、`readOnlyCommands` 和 `authorizationRequiredForRecovery`,不得把 Docker 的 `No such container` 当成成功的空历史。 - `codex deploy ` 是旧 Code Queue 兼容部署入口,已禁用以防止维护通道直连 D601 部署 Code Queue;当前 dev 自动化只做 `ci run-dev-e2e` smoke,不提供 Code Queue CD,详细规则见 `docs/reference/codex-deploy.md`。 -- `codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue queueId] [--provider-id id] [--cwd path] [--model model] [--reasoning-effort effort] [--execution-mode mode] [--max-attempts N] [--reference-task-id id] [--dry-run]` 通过 backend-core 私有代理向稳定 `code-queue` 用户服务路径提交任务;prompt 必须且只能来自位置参数、文件或 stdin 之一,`--dry-run` 只返回结构化请求且不实际入队。长 prompt、多行 prompt、含引号/反引号/Markdown 表格/JSON/反斜杠的 prompt 必须优先用 `--prompt-stdin` 或 `--prompt-file`,不要拼进 shell 单个参数;位置参数只适合短单行 smoke prompt。stdin 推荐用 quoted heredoc:`cat <<'PROMPT' | bun scripts/cli.ts codex submit --prompt-stdin --queue --dry-run`,文件路径推荐 `bun scripts/cli.ts codex submit --prompt-file /tmp/code-queue-prompt.md --queue --dry-run`,确认 dry-run 后移除 `--dry-run` 提交同一 payload。dry-run 会额外输出 `routingRecommendation`,包含推荐 route、runner、model、风险信号、prompt 自包含/issue 非唯一来源/prod-secret-DB 禁止/运行态或 release 禁止/证据要求/中等复杂度候选等 guard 状态;同时输出 `policyContract`,固定暴露 GPT-5.5、DeepSeek、MiniMax 的风险分层、并发上限和外部 provider 429 退避处置。该建议只用于指挥官 preflight,不会改写 payload,不改变 runtime admission,也不假设生产 MiniMax 或 DeepSeek 可用。`--dry-run` 必须返回完整 prompt、字符数和 `truncated=false` 用于人工验收;真实提交是写入操作,默认只返回 `accepted=true`、task id、队列、写入保护摘要和后续查看命令,必须标记 `promptOmitted=true` 且不得回显 prompt 或 promptPreview。真实提交会经过本机本地串行化保护和短节流,避免同一指挥端并发 submit 把低内存主机或 `code-queue-mgr` 控制面打抖;返回值会附带低噪声 `submitConcurrencyGuard` 说明本次提交的锁与等待信息。真实提交的 `queue` 摘要保持低噪声:`submittedTaskIds`、`queuedTaskIds`、`activeTaskIds` 和 `databaseActiveTaskIds` 是带 `items/count/returned/omitted/truncated/source` 的有界预览对象,`queuedTaskIds.items` 必须包含本次新入队的 queued/retry_wait 任务,`countContext` 与 `counts` 是权威计数;当预览被省略或截断时,`listPreviewPolicy` 必须写明 omitted counts 和 raw 查看命令。backend-core 默认把提交、队列 CRUD、已读状态、历史摘要和轻量 Trace 读取分流到主 server `code-queue-mgr`,由它写入主 PostgreSQL;D601 scheduler 只轮询并执行已入库任务。 +- `codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue queueId] [--provider-id id] [--cwd path] [--model model] [--reasoning-effort effort] [--execution-mode mode] [--max-attempts N] [--reference-task-id id] [--dry-run]` 通过 backend-core 私有代理向稳定 `code-queue` 用户服务路径提交任务;prompt 必须且只能来自位置参数、文件或 stdin 之一,`--dry-run` 只返回结构化请求且不实际入队。长 prompt、多行 prompt、含引号/反引号/Markdown 表格/JSON/反斜杠的 prompt 必须优先用 `--prompt-stdin` 或 `--prompt-file`,不要拼进 shell 单个参数;位置参数只适合短单行 smoke prompt。stdin 推荐用 quoted heredoc:`cat <<'PROMPT' | bun scripts/cli.ts codex submit --prompt-stdin --queue --dry-run`,文件路径推荐 `bun scripts/cli.ts codex submit --prompt-file /tmp/code-queue-prompt.md --queue --dry-run`,确认 dry-run 后移除 `--dry-run` 提交同一 payload。dry-run 会额外输出 `routingRecommendation`,包含推荐 route、runner、model、风险信号、prompt 自包含/issue 非唯一来源/prod-secret-DB 禁止/运行态或 release 禁止/证据要求/中等复杂度候选等 guard 状态;同时输出 `policyContract`,固定暴露 GPT-5.5、DeepSeek、MiniMax 的风险分层、并发上限和外部 provider 429 退避处置。该建议只用于指挥官 preflight,不会改写 payload,不改变 runtime admission,也不假设生产 MiniMax 或 DeepSeek 可用。`--dry-run` 必须返回完整 prompt、字符数和 `truncated=false` 用于人工验收;真实提交是写入操作,默认只返回 `accepted=true`、task id、队列、写入保护摘要和后续查看命令,必须标记 `promptOmitted=true` 且不得回显 prompt 或 promptPreview。真实提交会经过本机本地串行化保护和短节流,避免同一指挥端并发 submit 把低内存主机或 `code-queue-mgr` 控制面打抖;返回值会附带 `executionMode`、`runnerPermissions` 和低噪声 `submitConcurrencyGuard`,显式说明 requested/effective mode、服务级 runner sandbox/approvalPolicy、锁与等待信息。`--execution-mode` 是 Code Queue runtime placement,不是 Codex sandbox 权限;有效模式是 `default` 和 `windows-native`,`--execution-mode full-access` 等 sandbox-like 值会保留 requested 值并显示 effective `default`,同时提示当前不支持每任务 sandbox override。真实提交的 `queue` 摘要保持低噪声:`submittedTaskIds`、`queuedTaskIds`、`activeTaskIds` 和 `databaseActiveTaskIds` 是带 `items/count/returned/omitted/truncated/source` 的有界预览对象,`queuedTaskIds.items` 必须包含本次新入队的 queued/retry_wait 任务,`countContext` 与 `counts` 是权威计数;当预览被省略或截断时,`listPreviewPolicy` 必须写明 omitted counts 和 raw 查看命令。backend-core 默认把提交、队列 CRUD、已读状态、历史摘要和轻量 Trace 读取分流到主 server `code-queue-mgr`,由它写入主 PostgreSQL;D601 scheduler 只轮询并执行已入库任务。 - `codex pr-preflight [--remote] [--push-dry-run --push-dry-run-ref refs/heads/probe/] [--pr-create-dry-run --pr-create-dry-run-head ] [--issue N] [--full|--raw]` 通过稳定 `code-queue` proxy 请求 D601 scheduler `/api/runtime-preflight`,用于 PR 型派单 admission。默认输出是紧凑 commander 视图,显式分出 `schedulerPreflight` 与 `activeRunnerPrCapability`,并附带 `commands` 和 `disclosure`,方便先看 scheduler auth 缺口、再看当前 runner/dev container 的 `gh auth status` 与 `gh pr create --dry-run` 能力;`--full` 或 `--raw` 才展开完整 `preflight`、工具、agent port、Git worktree、GitHub egress、repo/issue/PR 只读探测和观测原文。只报告 `GH_TOKEN`/`GITHUB_TOKEN` 是否存在和来源 key,不打印值。当 auth-broker 配置存在时,`tokenCoverage.source="auth-broker"`、`credentialSource="broker-issued-token"` 且 runner env token 不是成功前提;当仅 env token 存在时,`credentialSource="env-token"` 且 `authBroker.nextAction="use-env-token-until-auth-broker-live"`;两者都缺失时顶层 `ok=false`、`runnerDisposition=infra-blocked`、`degradedReason=auth-broker-needed`,`tokenCoverage.missing` 同时列出 `GH_TOKEN` 与 `GITHUB_TOKEN`,并输出 `authBroker.source="broker/auth-broker-needed"`、`capability.source="missing-token"`。该 `auth-missing` 的 scope 是 `scheduler-runner-env`,不能简化成“当前 active runner/dev container 不能创建 PR”;默认视图必须带 `scopeBoundary` 和 `activeRunnerPrCapability`。GitHub DNS/API 连接失败应归类为 `failureKind=github-transient`、`degradedReason=github-dns-api-transient`,并带 `retryable=true`、`commanderAction=retry-backoff-or-keep-running-if-heartbeat-fresh` 和有界 `githubTransient.failedProbes`;调用方应重试/退避,且在任务 heartbeat/trace 新鲜时继续监督,不把它当成 auth 缺失或 PR 语义失败。`prCapability` 是 runner-facing 合同摘要,必须包含目标分支、token/auth 来源、`systemGhBinaryRequiredForWrites=false`、UniDesk REST `bun scripts/cli.ts gh` 可用性、push dry-run/PR create dry-run 的 `writesRemote=false`、expected PR handoff、真实 PR 创建需要 commander 授权和 `gh pr merge` 的 `unsupported-command` 边界;系统 `gh` binary 缺失只进入 `tools.systemGhBinary`,不得误判为 UniDesk REST `gh` CLI 不可用。`--remote` 在 runner-like 环境里不再依赖本地 `unidesk-backend-core`、`unidesk-database`、`baidu-netdisk-backend` 容器存在;这些缺失只作为本地观测证据。若远程控制面可达,则继续走远程控制面结果;若远程控制面不可达,则结构化返回 `failureKind=control-plane-missing` / `degradedReason=remote-control-plane-unreachable`,而不是把本地 `backend-core-container-missing` 当作最终阻塞。`--pr-create-dry-run` 不 POST GitHub,只证明 runner 内 PR body 生成、`scripts/cli.ts gh pr create --dry-run` 和 branch 参数形态可用;服务端创建权限仍以 token/auth broker、repo/issue/PR read、push dry-run 和最终授权后的真实 PR 创建结果为准。 - `codex task ` 通过 Code Queue 私有代理按任务 ID 查询结构化审阅摘要;默认只返回任务身份、执行 Provider、工作目录、attempt 计数、原始 prompt、最终 response、最后错误和渐进披露命令,适合指挥官审阅完成未读任务且避免上下文爆炸。`--detail` 仍是有界详细摘要:默认只返回少量 attempt/tool 行、短 prompt/response/stderr/feedback 预览和 omitted/truncated 元数据;需要完整 prompt/response 文本或更多 tool/attempt 细节时再显式加 `--full`、`--tool-limit N`、`--trace` 或 `codex output`。该摘要读取默认由主 server `code-queue-mgr` 从 PostgreSQL 返回,不依赖 D601 `code-queue-read` Service 可用。 - `codex tasks [--view supervisor|full] [--queue id] [--status succeeded|running|queued|failed|canceled|judging|retry_wait[,..]] [--unread|--unread-only] [--limit N] [--before-id id]` 通过同一私有代理输出渐进式披露视图。默认 `supervisor` 是低噪声指挥官视图,只返回 `activeRunning`、`running`、`completedUnread`、`recentCompleted`、`queued`、`activity`、`commanderConcurrency` 和 `executionDiagnostics` 的紧凑行;`activeRunning.count` 是 running+judging 的状态计数,`exact=true` 时来自 queue summary counts,`running.returned` 和 `activeRunning.rowPage.returned` 只是本次返回的紧凑行数。`commanderConcurrency.activeRunnerCount` 是并发策略应使用的 active/running 计数,等于 `activity.effectiveActiveTaskCount`;15 并发策略按 `15 - activeRunnerCount` 计算剩余窗口。`commanderConcurrency.splitBrainDisposition=live-count-as-active` 表示 split-brain 有 fresh heartbeat 证据,应继续监督并计入 active;`interventionRequired=true` 才提示介入。prompt/body 只给短预览和原始字符数,`running`/`completedUnread`/`queued` 默认只返回一个有界小页并通过 section `commands.next` 继续分页,`recentCompleted` 默认限量且不重复 `completedUnread` 未读终态,不嵌入完整 Trace、final response 或全量 overview。`--limit` 在 supervisor 中主要是扫描/分页预算,不是返回几十条肥行的开关;CLI 安全上限是 100,输出会在 `filters.requestedLimit`、`filters.effectiveLimit`、`filters.limitCapped` 和 `disclosure.limitPolicy` 说明显式请求是否被 capped;底层 overview 拉取预算独立显示在 `source.requestedLimit` / `source.effectiveLimit`,所以 `--limit 260` 应显示 requested=260、effective=100、source requested/effective=200,而不是只露出一个含糊的 `limit`。`--unread` 是 `--unread-only` 的别名,必须只保留未读终态;`--status` 必须真实过滤支持的状态,未知参数或未知状态必须结构化失败。需要更详细当前页任务行时显式使用 `--view full` 或 `--full`,仍受 `--limit` 和 `--before-id` 分页约束。 diff --git a/docs/reference/code-queue-supervision.md b/docs/reference/code-queue-supervision.md index c05ff979..b512a378 100644 --- a/docs/reference/code-queue-supervision.md +++ b/docs/reference/code-queue-supervision.md @@ -91,7 +91,7 @@ HWLAB M3 口径使用同一分级:只读报告、fixture、LOCAL/DRY-RUN 和 d Code Queue 派单模型按成本、可信度和 blast radius 分层:GPT-5.5/Codex 处理高风险和复杂任务,DeepSeek/OpenCode 处理中等复杂度且边界清晰的任务,MiniMax/OpenCode 处理简单、低权限、可复核任务,生产重启、密钥、数据库手工写入和运行中任务控制保留给指挥官或人工。 -当前提交合同由 `bun scripts/cli.ts codex submit` 暴露:prompt 必须来自位置参数、`--prompt-file` 或 `--prompt-stdin`;可选字段包括 `--queue/--queue-id`、`--provider-id/--provider`、`--cwd/--workdir`、`--model`、`--reasoning-effort`、`--execution-mode/--mode`、`--max-attempts` 和 `--reference-task-id/--reference/--ref`。长 prompt、多行 prompt、含引号/反引号/Markdown 表格/JSON/反斜杠的 prompt 应使用 `--prompt-stdin` 或 `--prompt-file`,例如 `cat <<'PROMPT' | bun scripts/cli.ts codex submit --prompt-stdin --queue --dry-run` 或 `bun scripts/cli.ts codex submit --prompt-file /tmp/code-queue-prompt.md --queue --dry-run`;位置参数只适合短单行 smoke prompt。提交前先用 `--dry-run` 检查完整 payload,确认后移除 `--dry-run`。真实提交成功只返回低噪声写入确认、task id、队列和后续查看命令,必须标记 `promptOmitted=true` 且不得回显 prompt;需要复核正文时用返回的 `codex task ` 渐进展开。这些字段写入任务 payload 后由 `code-queue-mgr` 入 PostgreSQL,核心任务字段包括 `queue_id`、`provider_id`、`execution_mode`、`model`、`cwd`、`prompt/base_prompt`、`reference_task_ids`、`reasoning_effort`、`max_attempts` 和 `task_json`;队列记录至少有 `id/name/created_at/updated_at`。模型治理应优先看任务 payload 和数据库字段,不靠 worker final response 自报。 +当前提交合同由 `bun scripts/cli.ts codex submit` 暴露:prompt 必须来自位置参数、`--prompt-file` 或 `--prompt-stdin`;可选字段包括 `--queue/--queue-id`、`--provider-id/--provider`、`--cwd/--workdir`、`--model`、`--reasoning-effort`、`--execution-mode/--mode`、`--max-attempts` 和 `--reference-task-id/--reference/--ref`。长 prompt、多行 prompt、含引号/反引号/Markdown 表格/JSON/反斜杠的 prompt 应使用 `--prompt-stdin` 或 `--prompt-file`,例如 `cat <<'PROMPT' | bun scripts/cli.ts codex submit --prompt-stdin --queue --dry-run` 或 `bun scripts/cli.ts codex submit --prompt-file /tmp/code-queue-prompt.md --queue --dry-run`;位置参数只适合短单行 smoke prompt。提交前先用 `--dry-run` 检查完整 payload,确认后移除 `--dry-run`。`--execution-mode` 只表示 Code Queue runtime placement,有效值是 `default` 与 `windows-native`;像 `full-access` 这类 sandbox-like 值必须在 response 中显示 requested/effective mapping,并提示真实权限看服务级 `runnerPermissions.sandbox` / `approvalPolicy`,当前不支持每任务 sandbox override。真实提交成功只返回低噪声写入确认、task id、队列和后续查看命令,必须标记 `promptOmitted=true` 且不得回显 prompt;需要复核正文时用返回的 `codex task ` 渐进展开。这些字段写入任务 payload 后由 `code-queue-mgr` 入 PostgreSQL,核心任务字段包括 `queue_id`、`provider_id`、`execution_mode`、`model`、`cwd`、`prompt/base_prompt`、`reference_task_ids`、`reasoning_effort`、`max_attempts` 和 `task_json`;`task_json` 还保留 `requestedExecutionMode` 以便审计 requested/effective 差异;队列记录至少有 `id/name/created_at/updated_at`。模型治理应优先看任务 payload 和数据库字段,不靠 worker final response 自报。 真实 `codex submit` 确认输出的 `queue` 是低噪声监督摘要:`queuedTaskIds.items` 必须强制包含本次新建且仍为 queued/retry_wait 的任务 ID;`activeTaskIds` 在主 server 控制面 `activeTaskIds=[]` 但 `counts.running/judging>0` 时必须回退到 PostgreSQL `databaseActiveTaskIds` 或执行诊断中的 active IDs;这些 ID 列表都只能作为带 `count/returned/omitted/truncated/source` 的有界预览,权威并发口径来自 `counts` 和 `countContext`。当预览没有展开所有 ID 时,`listPreviewPolicy` 必须明确说明 omitted counts 和 raw 查看命令,避免指挥侧误判 15-runner 目标。 运行态默认模型仍是 `gpt-5.5`。`CODE_QUEUE_MODELS` 当前长期合同至少包含 GPT-5.5、GPT-5.4、GPT-5.4 Mini、DeepSeek Chat 和 MiniMax M2.7;`deepseek`/`deepseek-chat` 与 `minimax-m2.7` 会走 OpenCode port,其余模型走 Codex port。只有当执行面 `/health` 或等价配置已经显示 DeepSeek 模型可用、并完成轻量 runner smoke 后,才允许真实提交 `--model deepseek-chat`。 diff --git a/scripts/code-queue-submit-execution-mode-contract-test.ts b/scripts/code-queue-submit-execution-mode-contract-test.ts new file mode 100644 index 00000000..4ffe01fa --- /dev/null +++ b/scripts/code-queue-submit-execution-mode-contract-test.ts @@ -0,0 +1,152 @@ +import { spawnSync } from "node:child_process"; +import { + normalizeCodeExecutionMode, + normalizeRequestedCodeExecutionMode, + requestedCodeExecutionModeIsRecognized, +} from "../src/components/microservices/code-queue/src/code-agent/common"; +import { compactSubmitSuccessResponseForTest } from "./src/code-queue"; + +type JsonRecord = Record; + +function assertCondition(condition: unknown, message: string, detail: unknown = {}): void { + if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`); +} + +function runCli(args: string[]): { status: number | null; stdout: string; stderr: string; json: JsonRecord | null } { + const result = spawnSync("bun", ["scripts/cli.ts", ...args], { + cwd: process.cwd(), + encoding: "utf8", + }); + const stdout = String(result.stdout || ""); + let json: JsonRecord | null = null; + try { + json = JSON.parse(stdout) as JsonRecord; + } catch { + json = null; + } + return { + status: result.status, + stdout, + stderr: String(result.stderr || ""), + json, + }; +} + +function nestedRecord(value: unknown, path: string[]): JsonRecord { + let current: unknown = value; + for (const key of path) { + assertCondition(current !== null && typeof current === "object" && !Array.isArray(current), "expected object while traversing JSON", { path, key, current }); + current = (current as JsonRecord)[key]; + } + assertCondition(current !== null && typeof current === "object" && !Array.isArray(current), "expected nested object", { path, current }); + return current as JsonRecord; +} + +function asArray(value: unknown): unknown[] { + assertCondition(Array.isArray(value), "expected JSON array", { value }); + return value as unknown[]; +} + +function assertSecretFree(output: string): void { + const forbidden = ["GH_TOKEN=", "GITHUB_TOKEN=", "OPENAI_API_KEY=", "CRS_OAI_KEY=", "DEEPSEEK_API_KEY=", "MINIMAX_API_KEY="]; + for (const needle of forbidden) { + assertCondition(!output.includes(needle), "submit execution-mode contract must not print credential assignments", { needle }); + } +} + +export function runCodeQueueSubmitExecutionModeContract(): JsonRecord { + assertCondition(normalizeRequestedCodeExecutionMode("full-access") === "full-access", "shared parser should preserve short requested mode ids"); + assertCondition(normalizeCodeExecutionMode("full-access") === "default", "shared execution-mode normalizer should keep full-access on effective default"); + assertCondition(requestedCodeExecutionModeIsRecognized("full-access") === false, "shared recognition helper should reject full-access as a runtime mode"); + assertCondition(requestedCodeExecutionModeIsRecognized("default") === true, "shared recognition helper should accept default mode"); + + const defaultMode = runCli(["codex", "submit", "execution mode default smoke", "--dry-run"]); + assertCondition(defaultMode.status === 0 && defaultMode.json?.ok === true, "default submit dry-run should succeed", defaultMode.json ?? { stdout: defaultMode.stdout, stderr: defaultMode.stderr }); + assertSecretFree(defaultMode.stdout); + const defaultData = nestedRecord(defaultMode.json?.data, []); + const defaultRequest = nestedRecord(defaultData, ["request"]); + const defaultExecutionMode = nestedRecord(defaultData, ["executionMode"]); + const defaultPermissions = nestedRecord(defaultData, ["runnerPermissions"]); + assertCondition(defaultRequest.executionMode === undefined, "default payload should omit executionMode so service default is authoritative", defaultRequest); + assertCondition(defaultExecutionMode.requested === null, "default mode should show no explicit requested mode", defaultExecutionMode); + assertCondition(defaultExecutionMode.effective === "default", "default mode should expose effective default", defaultExecutionMode); + assertCondition(defaultExecutionMode.normalized === false, "default mode should not be reported as normalized", defaultExecutionMode); + assertCondition(defaultExecutionMode.recognized === true, "default mode should be recognized", defaultExecutionMode); + assertCondition(defaultPermissions.observed === false && defaultPermissions.perTaskOverrideSupported === false, "dry-run should mark runner permissions unobserved and non per-task", defaultPermissions); + + const fullAccess = runCli(["codex", "submit", "execution mode full access smoke", "--execution-mode", "full-access", "--dry-run"]); + assertCondition(fullAccess.status === 0 && fullAccess.json?.ok === true, "full-access submit dry-run should succeed", fullAccess.json ?? { stdout: fullAccess.stdout, stderr: fullAccess.stderr }); + assertSecretFree(fullAccess.stdout); + const fullData = nestedRecord(fullAccess.json?.data, []); + const fullRequest = nestedRecord(fullData, ["request"]); + const fullExecutionMode = nestedRecord(fullData, ["executionMode"]); + assertCondition(fullRequest.executionMode === "full-access", "payload should preserve the requested executionMode value for backend visibility", fullRequest); + assertCondition(fullExecutionMode.requested === "full-access", "full-access request should be visible", fullExecutionMode); + assertCondition(fullExecutionMode.effective === "default", "full-access should normalize to the effective default runtime mode", fullExecutionMode); + assertCondition(fullExecutionMode.recognized === false, "full-access should not be treated as a recognized Code Queue execution mode", fullExecutionMode); + assertCondition(fullExecutionMode.normalized === true, "full-access should explicitly show normalization", fullExecutionMode); + assertCondition(fullExecutionMode.requestedLooksLikeSandbox === true, "full-access should be classified as a sandbox-like request", fullExecutionMode); + assertCondition(String(fullExecutionMode.permissionBoundary || "").includes("runnerPermissions.sandbox"), "permission boundary should point at runnerPermissions.sandbox", fullExecutionMode); + assertCondition(String(fullExecutionMode.warning || "").includes("not applied"), "full-access warning should say it is not a per-task sandbox override", fullExecutionMode); + + const promptText = "submitted full-access prompt body must stay omitted"; + const submitted = compactSubmitSuccessResponseForTest({ + tasks: [{ + id: "codex_exec_mode_contract", + queueId: "commander-efficiency", + status: "queued", + providerId: "D601", + model: "gpt-5.5", + cwd: "/workspace", + prompt: promptText, + executionMode: "default", + requestedExecutionMode: "full-access", + maxAttempts: 99, + createdAt: "2026-05-23T00:00:00.000Z", + updatedAt: "2026-05-23T00:00:00.000Z", + }], + queue: { + total: 1, + queueCount: 1, + counts: { queued: 1 }, + queuedTaskIds: ["codex_exec_mode_contract"], + runnerPermissions: { + observed: true, + scope: "code-queue-service-config", + sandbox: "danger-full-access", + approvalPolicy: "never", + perTaskOverrideSupported: false, + secretsPrinted: false, + }, + }, + }, { ok: true, status: 200 }, { mode: "local-atomic-directory-submit-serialization", acquiredAfterMs: 1, heldMs: 2, throttleMs: 2000 }); + const submittedExecutionMode = nestedRecord(submitted, ["executionMode"]); + const submittedPermissions = nestedRecord(submitted, ["runnerPermissions"]); + const firstTask = nestedRecord(asArray(nestedRecord(submitted, ["submitted"]).tasks)[0], []); + const taskExecutionMode = nestedRecord(firstTask, ["executionModeRequest"]); + const queuePermissions = nestedRecord(submitted, ["queue", "runnerPermissions"]); + const submittedJson = JSON.stringify(submitted); + assertCondition(submittedExecutionMode.requested === "full-access" && submittedExecutionMode.effective === "default", "real submit summary should show requested/effective mode", submittedExecutionMode); + assertCondition(submittedPermissions.observed === true && submittedPermissions.sandbox === "danger-full-access" && submittedPermissions.approvalPolicy === "never", "real submit summary should expose observed service-level runner permissions", submittedPermissions); + assertCondition(submittedPermissions.perTaskOverrideSupported === false, "real submit summary should not imply per-task sandbox override", submittedPermissions); + assertCondition(firstTask.requestedExecutionMode === "full-access" && firstTask.executionMode === "default", "submitted task should carry requested and effective mode", firstTask); + assertCondition(taskExecutionMode.warning === submittedExecutionMode.warning, "task-level execution mode summary should match top-level warning", { taskExecutionMode, submittedExecutionMode }); + assertCondition(queuePermissions.sandbox === "danger-full-access", "queue summary should keep runner permissions visible", queuePermissions); + assertCondition(!submittedJson.includes(promptText), "real submit summary must keep prompt text omitted", submitted); + assertCondition(!submittedJson.includes("promptPreview"), "real submit summary must not reintroduce promptPreview", submitted); + + return { + ok: true, + checks: [ + "default codex submit dry-run omits executionMode, reports effective default, and marks runner permissions unobserved", + "--execution-mode full-access preserves requested mode, reports effective default, and warns that sandbox permissions are service-level", + "real submit summary fixture exposes requested/effective mode plus observed runnerPermissions without prompt echo", + "shared execution-mode helpers preserve requested full-access while normalizing effective runtime to default", + "execution-mode dry-run output does not print credential assignments", + ], + }; +} + +if (import.meta.main) { + process.stdout.write(`${JSON.stringify(runCodeQueueSubmitExecutionModeContract(), null, 2)}\n`); +} diff --git a/scripts/src/check.ts b/scripts/src/check.ts index b666c1fb..a45b3078 100644 --- a/scripts/src/check.ts +++ b/scripts/src/check.ts @@ -34,6 +34,7 @@ const syntaxFiles = [ "scripts/code-queue-prompt-lint-contract-test.ts", "scripts/code-queue-cli-steer-test.ts", "scripts/code-queue-cli-submit-prompt-contract-test.ts", + "scripts/code-queue-submit-execution-mode-contract-test.ts", "scripts/code-queue-submit-summary-contract-test.ts", "scripts/code-queue-cli-read-terminal-contract-test.ts", "scripts/code-queue-gh-auth-redaction-contract-test.ts", @@ -362,6 +363,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default items.push(commandItem("code-queue:cli-steer-contract", ["bun", "scripts/code-queue-cli-steer-test.ts"], 30_000)); items.push(commandItem("code-queue:read-terminal-contract", ["bun", "scripts/code-queue-cli-read-terminal-contract-test.ts"], 30_000)); items.push(commandItem("code-queue:submit-prompt-contract", ["bun", "scripts/code-queue-cli-submit-prompt-contract-test.ts"], 30_000)); + items.push(commandItem("code-queue:submit-execution-mode-contract", ["bun", "scripts/code-queue-submit-execution-mode-contract-test.ts"], 30_000)); items.push(commandItem("code-queue:submit-summary-contract", ["bun", "scripts/code-queue-submit-summary-contract-test.ts"], 30_000)); items.push(commandItem("code-queue:submit-routing-contract", ["bun", "scripts/code-queue-submit-routing-contract-test.ts"], 30_000)); items.push(commandItem("code-queue:gh-auth-redaction-contract", ["bun", "scripts/code-queue-gh-auth-redaction-contract-test.ts"], 30_000)); @@ -396,6 +398,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default items.push(skippedItem("code-queue:cli-steer-contract", "Code Queue steer CLI contract is opt-in with script checks", "--scripts-typecheck or --full")); items.push(skippedItem("code-queue:read-terminal-contract", "Code Queue terminal read contract is opt-in with script checks", "--scripts-typecheck or --full")); items.push(skippedItem("code-queue:submit-prompt-contract", "Code Queue submit prompt contract is opt-in with script checks", "--scripts-typecheck or --full")); + items.push(skippedItem("code-queue:submit-execution-mode-contract", "Code Queue submit execution-mode contract is opt-in with script checks", "--scripts-typecheck or --full")); items.push(skippedItem("code-queue:submit-summary-contract", "Code Queue submit summary contract is opt-in with script checks", "--scripts-typecheck or --full")); items.push(skippedItem("code-queue:submit-routing-contract", "Code Queue submit routing contract is opt-in with script checks", "--scripts-typecheck or --full")); items.push(skippedItem("code-queue:gh-auth-redaction-contract", "Code Queue GitHub auth output redaction contract is opt-in with script checks", "--scripts-typecheck or --full")); diff --git a/scripts/src/code-queue.ts b/scripts/src/code-queue.ts index 273ef6ce..2ddfdfad 100644 --- a/scripts/src/code-queue.ts +++ b/scripts/src/code-queue.ts @@ -3,7 +3,16 @@ import { runCommand } from "./command"; import { type UniDeskConfig, repoRoot, rootPath } from "./config"; import { coreInternalFetch } from "./microservices"; import { previewJson } from "./preview"; -import { codeAgentPortForModel, codeModelPorts as sharedCodeModelPorts, defaultCodeModels as sharedDefaultCodeModels, opencodeModels as sharedOpencodeModels } from "../../src/components/microservices/code-queue/src/code-agent/common"; +import { + codeAgentPortForModel, + codeExecutionModes, + codeModelPorts as sharedCodeModelPorts, + defaultCodeModels as sharedDefaultCodeModels, + normalizeCodeExecutionMode, + normalizeRequestedCodeExecutionMode, + opencodeModels as sharedOpencodeModels, + requestedCodeExecutionModeIsRecognized, +} from "../../src/components/microservices/code-queue/src/code-agent/common"; const defaultToolLimit = 3; const defaultTraceLimit = 80; @@ -25,6 +34,7 @@ const diagnosticsIdPreviewLimit = 3; const diagnosticsReasonPreviewLimit = 2; const mutationQueueIdPreviewLimit = 15; const steerPromptPreviewChars = 320; +const sandboxLikeExecutionModes = new Set(["full-access", "danger-full-access", "workspace-write", "read-only"]); const detailAttemptReturnedLimit = 3; const detailInitialPromptPreviewChars = 1200; const detailBasePromptPreviewChars = 800; @@ -3921,6 +3931,61 @@ function referenceTaskIdsFromOptions(args: string[]): string[] { return ids; } +function parseRequestedExecutionMode(value: string | undefined): string | undefined { + if (value === undefined) return undefined; + const requested = normalizeRequestedCodeExecutionMode(value); + if (requested === null) throw new Error("--execution-mode must be a short mode identifier"); + return requested; +} + +function executionModeSummary(requestedValue: string | null | undefined, effectiveValue?: unknown): Record { + const requested = normalizeRequestedCodeExecutionMode(requestedValue); + const effective = typeof effectiveValue === "string" && effectiveValue.trim().length > 0 + ? normalizeCodeExecutionMode(effectiveValue) + : normalizeCodeExecutionMode(requested); + const requestedLooksLikeSandbox = requested !== null && sandboxLikeExecutionModes.has(requested); + const recognized = requestedCodeExecutionModeIsRecognized(requested); + return { + requested, + effective, + recognized, + normalized: requested !== null && requested !== effective, + availableModes: codeExecutionModes, + requestedLooksLikeSandbox, + permissionBoundary: "--execution-mode selects the Code Queue runtime mode; Codex sandbox permissions come from runnerPermissions.sandbox.", + ...(requestedLooksLikeSandbox ? { warning: `${requested} is not a Code Queue execution mode and is not applied as a per-task sandbox override.` } : {}), + }; +} + +function dryRunRunnerPermissionsSummary(): Record { + return { + observed: false, + scope: "code-queue-service-config", + sandbox: null, + approvalPolicy: null, + perTaskOverrideSupported: false, + note: "Dry-run does not contact Code Queue; real submit responses include observed runnerPermissions from the service.", + secretsPrinted: false, + }; +} + +function compactRunnerPermissions(value: unknown): Record | null { + const record = asRecord(value); + if (record === null) return null; + return { + observed: record.observed ?? true, + scope: record.scope ?? "code-queue-service-config", + sandbox: record.sandbox ?? null, + approvalPolicy: record.approvalPolicy ?? null, + perTaskOverrideSupported: record.perTaskOverrideSupported ?? false, + secretsPrinted: false, + }; +} + +function compactTaskExecutionModeRequest(record: Record): Record { + return executionModeSummary(asString(record.requestedExecutionMode) || null, record.executionMode); +} + function parseSubmitOptions(args: string[]): CodexSubmitOptions { assertKnownOptions(args, { flags: ["--prompt-stdin", "--stdin", "--dry-run"], @@ -3953,7 +4018,7 @@ function parseSubmitOptions(args: string[]): CodexSubmitOptions { cwd: optionValue(args, ["--cwd", "--workdir"]), model: optionValue(args, ["--model"]), reasoningEffort: optionValue(args, ["--reasoning-effort"]), - executionMode: optionValue(args, ["--execution-mode", "--mode"]), + executionMode: parseRequestedExecutionMode(optionValue(args, ["--execution-mode", "--mode"])), maxAttempts, referenceTaskIds: referenceTaskIdsFromOptions(args), dryRun: hasFlag(args, "--dry-run"), @@ -4014,6 +4079,8 @@ function compactTaskMutationResponse(task: unknown, options: CompactTaskMutation reasoningEffort: record.reasoningEffort ?? null, cwd: record.cwd ?? null, executionMode: record.executionMode ?? null, + requestedExecutionMode: record.requestedExecutionMode ?? null, + executionModeRequest: compactTaskExecutionModeRequest(record), maxAttempts: record.maxAttempts ?? null, currentAttempt: record.currentAttempt ?? null, cancelRequested: record.cancelRequested ?? null, @@ -4048,6 +4115,8 @@ function compactSubmitTaskConfirmation(task: unknown): Record { reasoningEffort: record.reasoningEffort ?? null, cwd: record.cwd ?? null, executionMode: record.executionMode ?? null, + requestedExecutionMode: record.requestedExecutionMode ?? null, + executionModeRequest: compactTaskExecutionModeRequest(record), maxAttempts: record.maxAttempts ?? null, createdAt: record.createdAt ?? null, updatedAt: record.updatedAt ?? null, @@ -4220,6 +4289,7 @@ function compactSubmitQueueConfirmation(value: unknown, options: CompactSubmitQu databaseActiveTaskIds: databaseActivePreview, queuedTaskIds: queuedPreview, executionDiagnostics: compactQueueExecutionDiagnostics(record.executionDiagnostics), + runnerPermissions: compactRunnerPermissions(record.runnerPermissions), ...(submittedTasks.length === 0 ? {} : { submittedTaskIds: submittedPreview }), countContext: { queued: countForStatus(counts, "queued"), @@ -4265,9 +4335,14 @@ function compactSubmitSuccessResponse(body: Record, upstream: R const queueIds = Array.from(new Set(tasks.map((task) => asString(task.queueId)).filter(Boolean))).sort(); const firstTaskId = taskIds[0] ?? null; const firstQueueId = queueIds[0] ?? null; + const firstSubmittedTask = submittedTasks[0] ?? {}; + const queueRecord = asRecord(body.queue); + const runnerPermissions = compactRunnerPermissions(queueRecord?.runnerPermissions); return { ok: true, upstream, + executionMode: executionModeSummary(asString(firstSubmittedTask.requestedExecutionMode) || null, firstSubmittedTask.executionMode), + runnerPermissions, submitted: { accepted: true, taskCount: allTasks.length, @@ -4393,6 +4468,11 @@ function compactTerminalReadTask(summary: unknown, readTask: unknown, taskId: st readAt, providerId: summaryRecord.providerId ?? readRecord.providerId ?? null, executionMode: summaryRecord.executionMode ?? readRecord.executionMode ?? null, + requestedExecutionMode: summaryRecord.requestedExecutionMode ?? readRecord.requestedExecutionMode ?? null, + executionModeRequest: compactTaskExecutionModeRequest({ + requestedExecutionMode: summaryRecord.requestedExecutionMode ?? readRecord.requestedExecutionMode ?? null, + executionMode: summaryRecord.executionMode ?? readRecord.executionMode ?? null, + }), executionModeInfo: summaryRecord.executionModeInfo ?? readRecord.executionModeInfo ?? null, model: summaryRecord.model ?? readRecord.model ?? null, agentPort: summaryRecord.agentPort ?? readRecord.agentPort ?? null, @@ -5848,6 +5928,8 @@ function codexSubmitTask(args: string[]): unknown { ok: true, dryRun: true, promptLint, + executionMode: executionModeSummary(options.executionMode), + runnerPermissions: dryRunRunnerPermissionsSummary(), routingRecommendation: submitRoutingRecommendation(options), modelRegistry: submitModelRegistry(), request: { diff --git a/scripts/src/help.ts b/scripts/src/help.ts index 8b758622..c4ddaae1 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -276,6 +276,11 @@ function codexHelp(): unknown { positional: "Keep positional prompt usage to short one-line smoke prompts only.", sourceRule: "Exactly one prompt source is accepted: positional prompt, --prompt-file, or --prompt-stdin.", }, + executionMode: { + validModes: ["default", "windows-native"], + boundary: "--execution-mode selects Code Queue runtime placement, not Codex sandbox permissions.", + permissionVisibility: "Submit dry-runs show requested/effective mode; real submit responses include service runnerPermissions.sandbox and approvalPolicy.", + }, readOutput: { default: "codex read marks a terminal task read and returns terminal metadata, final response, last error/judge, counts, and drill-down commands.", disclosure: "Full prompt, tool logs, and feedback prompts are not printed by codex read; use codex task/detail/trace/output for progressive disclosure.", diff --git a/src/components/microservices/code-queue/src/code-agent/common.ts b/src/components/microservices/code-queue/src/code-agent/common.ts index 01acfb9a..6c62ce4a 100644 --- a/src/components/microservices/code-queue/src/code-agent/common.ts +++ b/src/components/microservices/code-queue/src/code-agent/common.ts @@ -38,6 +38,7 @@ export const defaultCodeModels = ["gpt-5.5", "gpt-5.4-mini", "gpt-5.4", deepseek export const opencodeNpmPackage = "opencode-ai@1.14.48"; export const defaultCodeExecutionMode: CodeExecutionMode = "default"; export const codeExecutionModes: CodeExecutionMode[] = ["default", "windows-native"]; +const codeExecutionModeAliases = new Set(["default", "windows-native", "windows", "win32", "native-windows"]); export interface CodeModelProviderSourceConfig { codeModels: string[]; @@ -104,6 +105,18 @@ export function normalizeCodeExecutionMode(value: unknown): CodeExecutionMode { return defaultCodeExecutionMode; } +export function normalizeRequestedCodeExecutionMode(value: unknown): string | null { + const raw = typeof value === "string" ? value.trim().toLowerCase() : ""; + if (raw.length === 0) return null; + if (!/^[a-z0-9][a-z0-9-]{0,63}$/u.test(raw)) return null; + return raw; +} + +export function requestedCodeExecutionModeIsRecognized(value: unknown): boolean { + const requested = normalizeRequestedCodeExecutionMode(value); + return requested === null || codeExecutionModeAliases.has(requested); +} + export function codeExecutionModeInfo(mode: CodeExecutionMode): Record { if (mode === "windows-native") { return { diff --git a/src/components/microservices/code-queue/src/index.ts b/src/components/microservices/code-queue/src/index.ts index 6fdadb47..72d865a0 100644 --- a/src/components/microservices/code-queue/src/index.ts +++ b/src/components/microservices/code-queue/src/index.ts @@ -37,6 +37,7 @@ import { codeModelProviderSourceContract, normalizeCodeExecutionMode, normalizeCodeModel, + normalizeRequestedCodeExecutionMode, terminalStatus, } from "./code-agent/common"; import type { ActiveRun, ActiveRunSlotWaiter } from "./code-agent/common"; @@ -1024,6 +1025,7 @@ function normalizeTask(task: QueueTask): QueueTask { task.providerId = normalizeTaskProviderId(task.providerId); task.model ||= config.defaultModel; task.executionMode = normalizeCodeExecutionMode(task.executionMode); + task.requestedExecutionMode = normalizeRequestedCodeExecutionMode(task.requestedExecutionMode); task.cwd = resolveTaskCwd(task.providerId, task.cwd); task.reasoningEffort = resolveReasoningEffort(task.model, task.reasoningEffort); task.maxAttempts = clampTaskAttempts(task.maxAttempts || config.defaultMaxAttempts); @@ -2573,7 +2575,12 @@ function normalizeRequest(value: unknown): QueueTaskRequest { if (typeof record.cwd === "string" && record.cwd.length > 0) request.cwd = record.cwd; if (typeof record.model === "string" && record.model.length > 0) request.model = record.model; if (typeof record.reasoningEffort === "string" && record.reasoningEffort.length > 0) request.reasoningEffort = record.reasoningEffort; - if (typeof record.executionMode === "string" && record.executionMode.length > 0) request.executionMode = normalizeCodeExecutionMode(record.executionMode); + if (typeof record.executionMode === "string" && record.executionMode.length > 0) { + const requestedExecutionMode = normalizeRequestedCodeExecutionMode(record.executionMode); + if (requestedExecutionMode === null) throw new Error("executionMode must be a short mode identifier"); + request.requestedExecutionMode = requestedExecutionMode; + request.executionMode = normalizeCodeExecutionMode(requestedExecutionMode); + } if (typeof record.maxAttempts === "number" && Number.isInteger(record.maxAttempts) && record.maxAttempts > 0) request.maxAttempts = clampTaskAttempts(record.maxAttempts); const referenceTaskIds = collectReferenceTaskIds(record, record.prompt); if (referenceTaskIds.length > 0) request.referenceTaskIds = referenceTaskIds; @@ -2594,6 +2601,7 @@ function createTask(request: QueueTaskRequest): QueueTask { const providerId = normalizeTaskProviderId(request.providerId); const model = normalizeCodeModel(request.model ?? config.defaultModel); const executionMode = normalizeCodeExecutionMode(request.executionMode); + const requestedExecutionMode = normalizeRequestedCodeExecutionMode(request.requestedExecutionMode); const cwd = resolveTaskCwd(providerId, request.cwd); validateExecutionModeForTask(providerId, cwd, model, executionMode); rememberWorkdir(providerId, executionMode, cwd, at); @@ -2612,6 +2620,7 @@ function createTask(request: QueueTaskRequest): QueueTask { model, reasoningEffort: resolveReasoningEffort(model, request.reasoningEffort), executionMode, + requestedExecutionMode, maxAttempts: request.maxAttempts ?? config.defaultMaxAttempts, status: "queued", createdAt: at, diff --git a/src/components/microservices/code-queue/src/queue-api.ts b/src/components/microservices/code-queue/src/queue-api.ts index 8fa0255d..04dc6086 100644 --- a/src/components/microservices/code-queue/src/queue-api.ts +++ b/src/components/microservices/code-queue/src/queue-api.ts @@ -14,7 +14,7 @@ import type { ActiveRun, ActiveRunSlotWaiter } from "./code-agent/common"; import type { JsonValue, QueueRecord, QueuedStatusReason, QueueTask, RuntimeConfig, TaskStatus, TranscriptLine } from "./types"; export interface QueueApiContext { - config: Pick; + config: Pick; activeRunSlotQueueIds: () => string[]; activeRunSlotWaiterSummaries: () => JsonValue[]; activeRuns: Map; @@ -268,6 +268,7 @@ function taskForListResponse(task: QueueTask, lite = false, queueTasks?: QueueTa }, providerId: task.providerId, executionMode: task.executionMode, + requestedExecutionMode: task.requestedExecutionMode ?? null, executionModeInfo: codeExecutionModeInfo(task.executionMode), cwd: task.cwd, model: task.model, @@ -325,6 +326,7 @@ function taskForListResponse(task: QueueTask, lite = false, queueTasks?: QueueTa referenceInjection: task.referenceInjection, providerId: task.providerId, executionMode: task.executionMode, + requestedExecutionMode: task.requestedExecutionMode ?? null, executionModeInfo: codeExecutionModeInfo(task.executionMode), cwd: task.cwd, model: task.model, @@ -483,6 +485,14 @@ function queueSummary(includeDevReady = true, tasks: QueueTask[] = ctx().tasks() modelProviderConfig: codeModelProviderSourceContract(ctx().config) as unknown as JsonValue, executionModes: executionModeOptions(), executionModeInfo: Object.fromEntries(codeExecutionModes.map((mode) => [mode, codeExecutionModeInfo(mode)])) as unknown as JsonValue, + runnerPermissions: { + observed: true, + scope: "code-queue-service-config", + sandbox: ctx().config.sandbox, + approvalPolicy: ctx().config.approvalPolicy, + perTaskOverrideSupported: false, + secretsPrinted: false, + } as unknown as JsonValue, agentPorts: { codex: codeAgentPortInfo("codex"), opencode: codeAgentPortInfo("opencode"), diff --git a/src/components/microservices/code-queue/src/task-view.ts b/src/components/microservices/code-queue/src/task-view.ts index 21e354a4..d04e82be 100644 --- a/src/components/microservices/code-queue/src/task-view.ts +++ b/src/components/microservices/code-queue/src/task-view.ts @@ -1447,6 +1447,7 @@ function taskForMetaResponse(task: QueueTask): JsonValue { referenceInjection: task.referenceInjection, providerId: task.providerId, executionMode: task.executionMode, + requestedExecutionMode: task.requestedExecutionMode ?? null, executionModeInfo: codeExecutionModeInfo(task.executionMode), cwd: task.cwd, model: task.model, @@ -1518,6 +1519,7 @@ function taskForCompactMetaResponse(task: QueueTask): JsonValue { }, providerId: task.providerId, executionMode: task.executionMode, + requestedExecutionMode: task.requestedExecutionMode ?? null, executionModeInfo: codeExecutionModeInfo(task.executionMode), cwd: task.cwd, model: task.model, @@ -2391,6 +2393,7 @@ function taskTraceSummaryResponse(task: QueueTask, oaTraceStats: JsonValue | nul status: task.status, providerId: task.providerId, executionMode: task.executionMode, + requestedExecutionMode: task.requestedExecutionMode ?? null, executionModeInfo: codeExecutionModeInfo(task.executionMode), model: task.model, agentPort: codeAgentPortForModel(task.model), @@ -2552,6 +2555,7 @@ function taskSummaryResponse(task: QueueTask, url: URL): JsonValue { status: task.status, providerId: task.providerId, executionMode: task.executionMode, + requestedExecutionMode: task.requestedExecutionMode ?? null, executionModeInfo: codeExecutionModeInfo(task.executionMode), model: task.model, agentPort: codeAgentPortForModel(task.model), diff --git a/src/components/microservices/code-queue/src/types.ts b/src/components/microservices/code-queue/src/types.ts index 7bb72877..2780f81a 100644 --- a/src/components/microservices/code-queue/src/types.ts +++ b/src/components/microservices/code-queue/src/types.ts @@ -191,6 +191,7 @@ export interface QueueTaskRequest { model?: string; reasoningEffort?: string; executionMode?: CodeExecutionMode; + requestedExecutionMode?: string | null; maxAttempts?: number; referenceTaskIds?: string[]; basePrompt?: string; @@ -394,6 +395,7 @@ export interface QueueTask { model: string; reasoningEffort: string | null; executionMode: CodeExecutionMode; + requestedExecutionMode?: string | null; maxAttempts: number; status: TaskStatus; createdAt: string;