From b7a44ee30284d95a0d11d9bbd0f74599417474c4 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 9 Jun 2026 07:18:35 +0000 Subject: [PATCH] chore: update codex pool git rules --- AGENTS.md | 4 +- config/platform-infra/sub2api-codex-pool.yaml | 2 + docs/reference/platform-infra.md | 2 +- docs/reference/repo-tree.md | 1 + ...ub2api-codex-local-config-contract-test.ts | 58 ++++++++++++++ scripts/src/check.ts | 3 + scripts/src/platform-infra-sub2api-codex.ts | 75 ++++++++++++++++++- 7 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 scripts/platform-infra-sub2api-codex-local-config-contract-test.ts diff --git a/AGENTS.md b/AGENTS.md index e021c041..dd5b58f1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,7 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文 - P0: 单纯文档、AGENTS.md、docs/reference、SPEC、短连接 CLI、trans/tran/helper 这类轻量变更,禁止为了“隔离开发”新建 git worktree;必须在当前项目固定主 worktree 直接修改、提交并 push 到对应 remote 分支。 - P0: 只有业务代码、运行面、发布链路、CI/CD、Secret、权限、数据迁移、PROD 或明确高风险改动,才按项目规则创建隔离 worktree 或走 PR;不要把重型开发流程套到文档/CLI 轻量变更上。 -- P0: 若固定主 worktree 因并行 dirty 文件无法快进,先保留并行变更,只提交当前文档/CLI相关文件;必要时使用非破坏性 rebase/autostash 或等价方式对齐 remote 后直接 push,禁止绕到新 worktree 掩盖主 worktree 状态。 +- P0: 若固定主 worktree 已有非本次任务的未提交修改或未推送提交,默认视为可共同交付状态,一律可以一起提交并直接 push;必要时使用非破坏性 rebase/autostash 或等价方式对齐 remote 后直接 push,禁止绕到新 worktree 掩盖主 worktree 状态。 ## P0 最高优先级:可见性先修 @@ -162,7 +162,7 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文 - UniDesk 同时存在 main server、D601 `~/cq-deploy` 和其他 provider worktree 等多个开发/部署实例;Git remote 是长期 source of truth,本地部署实例只能视为运行副本或缓存。 - 任何开发、文档或部署配置变更开始前,必须先在当前 worktree 执行 `git status` 并从主线拉取最新源码:`git pull --ff-only origin master`;若本地并行变更或远端推进导致不能快进,必须当即分清来源并解决冲突后再继续。 - 任何需要保留的代码、文档或配置变更,在完成必要自测/部署验证后必须立刻按 `git-spec` 提交并 push 到 remote;禁止让未推送的本地修改成为部署真相或后续任务依赖。 -- 提交前必须用 `git status` 和 `git diff` 区分并只提交当前任务相关文件,保留并避开并行任务产生的无关修改;所有 UniDesk agent 变更只允许在 `master` 上开发并 `git push origin master`,禁止新建、切换到或推送其他分支;长期规则见 `docs/reference/arch.md`。 +- 提交前必须用 `git status` 和 `git diff` 确认工作区状态;遇到非本次任务的未推送提交一律可以直接推,遇到未提交修改一律可以一起提交并直接推;所有 UniDesk agent 变更只允许在 `master` 上开发并 `git push origin master`,禁止新建、切换到或推送其他分支;长期规则见 `docs/reference/arch.md`。 - P0: 单纯文档或 UniDesk CLI/trans/tran/helper 变更默认直接提交并推送到 `origin master`,不开 PR、不建临时分支;若涉及外部仓库、发布线、运行面部署或服务高风险行为,按对应 reference 的显式规则执行。 - `release/v1` 是规划中的稳定维护线,不是普通 feature/fix 分支;创建、更新或启用必须作为显式 release operation,先满足 `docs/reference/release-governance.md` 和 GitHub issue #6 的 CLI/CI/CD/文档条件。当前常规 agent 任务仍按 master-only 规则执行。 - `frontend`、`scripts/cli.ts`、`trans`/`tran` 和分布式 SSH 透传能力跟随 `master`;`release/v1` 仅用于明确批准的 backend-core / Code Queue 稳定维护,不作为 frontend 或 CLI/trans/tran 修复的 backport 目标。 diff --git a/config/platform-infra/sub2api-codex-pool.yaml b/config/platform-infra/sub2api-codex-pool.yaml index ec9af3f1..85357e7a 100644 --- a/config/platform-infra/sub2api-codex-pool.yaml +++ b/config/platform-infra/sub2api-codex-pool.yaml @@ -61,3 +61,5 @@ localCodex: backupSuffix: pre-sub2api providerName: OpenAI wireApi: responses + supportsWebSockets: true + responsesWebSocketsV2: true diff --git a/docs/reference/platform-infra.md b/docs/reference/platform-infra.md index f420a6ea..00f1904a 100644 --- a/docs/reference/platform-infra.md +++ b/docs/reference/platform-infra.md @@ -29,7 +29,7 @@ - `profiles.entries[].openaiResponsesWebSocketsV2Mode` is the account-level Responses WebSocket v2 switch for OpenAI-compatible upstreams that require WebSocket transport. Allowed values are `off`, `ctx_pool`, and `passthrough`; omit the field unless that upstream needs it. - `profiles.entries[].upstreamUserAgent` is an optional account-level upstream request User-Agent override. Use it only for upstreams that require a Codex CLI compatible User-Agent; keep the value YAML-controlled and newline-free. - `publicExposure` controls the optional FRP bridge from master server to the G14 ClusterIP service. -- `localCodex` controls how the master server's current `~/.codex` consumer files are backed up and rewritten. +- `localCodex` controls how the master server's current `~/.codex` consumer files are backed up and rewritten. Codex consumers using Sub2API must keep `supportsWebSockets` and `responsesWebSocketsV2` enabled so compacted long sessions can continue through the Responses WebSocket v2 response chain instead of falling back to HTTP-only summary context. The request path is: diff --git a/docs/reference/repo-tree.md b/docs/reference/repo-tree.md index 14725239..5b46b57c 100644 --- a/docs/reference/repo-tree.md +++ b/docs/reference/repo-tree.md @@ -5,6 +5,7 @@ - docker-compose.yml (Main server orchestration for database, backend-core, frontend, dev-frontend-proxy, provider-gateway, and managed main-server user services such as Todo Note) - package.json / bun.lock (Root Bun tooling for CLI checks) - .gitignore + - .agents/skills/ (Repo-owned UniDesk agent skills. Skill directories named `unidesk-*` live here as the Git-backed source of truth; `~/.agents/skills` is reserved for non-UniDesk shared/external skills or local runtime copies.) - reference -> docs/reference (Compatibility symlink for older references) - scripts/ (Unified CLI and implementation modules) - cli.ts (Single Bun CLI entry) diff --git a/scripts/platform-infra-sub2api-codex-local-config-contract-test.ts b/scripts/platform-infra-sub2api-codex-local-config-contract-test.ts new file mode 100644 index 00000000..dcd93570 --- /dev/null +++ b/scripts/platform-infra-sub2api-codex-local-config-contract-test.ts @@ -0,0 +1,58 @@ +import { renderCodexLocalConsumerToml } from "./src/platform-infra-sub2api-codex"; + +function assertCondition(condition: unknown, message: string, detail: unknown = {}): void { + if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`); +} + +const baseOptions = { + providerName: "OpenAI", + baseUrl: "http://127.0.0.1:21880", + wireApi: "responses", + supportsWebSockets: true, + responsesWebSocketsV2: true, +}; + +const existing = [ + 'model_provider = "Legacy"', + "", + "[model_providers.OpenAI]", + 'name = "OpenAI"', + 'base_url = "https://old.example"', + 'wire_api = "responses"', + "requires_openai_auth = false", + "supports_websockets = false", + 'env_key = "OLD_OPENAI_API_KEY"', + 'extra_setting = "kept"', + "", + "[features]", + "goals = true", + "responses_websockets_v2 = false", + "", +].join("\n"); + +const updated = renderCodexLocalConsumerToml(existing, baseOptions); + +assertCondition(updated.includes('model_provider = "OpenAI"'), "model_provider must point at the configured provider", updated); +assertCondition(updated.includes('base_url = "http://127.0.0.1:21880"'), "provider base_url must use the Sub2API consumer endpoint", updated); +assertCondition(updated.includes("requires_openai_auth = true"), "provider must require OpenAI auth", updated); +assertCondition(updated.includes("supports_websockets = true"), "provider must enable WebSocket transport", updated); +assertCondition(updated.includes("responses_websockets_v2 = true"), "features must enable Responses WebSocket v2", updated); +assertCondition(updated.includes("goals = true"), "existing feature flags must be preserved", updated); +assertCondition(updated.includes('extra_setting = "kept"'), "unknown provider settings must be preserved", updated); +assertCondition(!updated.includes("env_key"), "stale provider env_key must be removed", updated); +assertCondition(updated.endsWith("\n"), "rendered TOML must end with newline", updated); + +const fresh = renderCodexLocalConsumerToml("", baseOptions); + +assertCondition(fresh.includes("[model_providers.OpenAI]"), "fresh TOML must create the provider section", fresh); +assertCondition(fresh.includes("[features]"), "fresh TOML must create the features section", fresh); +assertCondition(fresh.includes("supports_websockets = true"), "fresh TOML must enable WebSocket transport", fresh); +assertCondition(fresh.includes("responses_websockets_v2 = true"), "fresh TOML must enable Responses WebSocket v2", fresh); + +console.log(JSON.stringify({ + ok: true, + checks: [ + "existing Codex TOML is upgraded to the Sub2API WSv2 consumer settings", + "fresh Codex TOML creates provider and feature sections with WSv2 enabled", + ], +})); diff --git a/scripts/src/check.ts b/scripts/src/check.ts index 2bb34525..6e5db9f1 100644 --- a/scripts/src/check.ts +++ b/scripts/src/check.ts @@ -15,6 +15,7 @@ const syntaxFiles = [ "scripts/cli.ts", "scripts/playwright-cli.ts", "scripts/playwright-cli-contract-test.ts", + "scripts/platform-infra-sub2api-codex-local-config-contract-test.ts", "scripts/src/playwright-cli.ts", "scripts/src/check.ts", "scripts/src/artifact-registry.ts", @@ -382,6 +383,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default fileItem("scripts/gh-cli-pr-files-contract-test.ts"), fileItem("scripts/gh-cli-pr-contract-test.ts"), fileItem("scripts/playwright-cli-contract-test.ts"), + fileItem("scripts/platform-infra-sub2api-codex-local-config-contract-test.ts"), fileItem("scripts/code-queue-pr-preflight-example.ts"), fileItem("scripts/schedule-cli-contract-test.ts"), fileItem("scripts/server-cleanup-plan-contract-test.ts"), @@ -446,6 +448,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default items.push(commandItem("gh:pr-files-contract", ["bun", "scripts/gh-cli-pr-files-contract-test.ts"], 30_000)); items.push(commandItem("gh:pr-contract", ["bun", "scripts/gh-cli-pr-contract-test.ts"], 30_000)); items.push(commandItem("playwright:cli-wrapper-contract", ["bun", "scripts/playwright-cli-contract-test.ts"], 30_000)); + items.push(commandItem("platform-infra:sub2api-codex-local-config-contract", ["bun", "scripts/platform-infra-sub2api-codex-local-config-contract-test.ts"], 30_000)); items.push(commandItem("auth-broker:p0-contract", ["bun", "scripts/auth-broker-contract-test.ts"], 30_000)); items.push(commandItem("d601:recovery-guardrails-contract", ["bun", "scripts/d601-recovery-guardrails-contract-test.ts"], 30_000)); items.push(commandItem("hwlab:cd-wrapper-contract", ["bun", "scripts/hwlab-cd-wrapper-contract-test.ts"], 30_000)); diff --git a/scripts/src/platform-infra-sub2api-codex.ts b/scripts/src/platform-infra-sub2api-codex.ts index 04e31afb..5f49fbff 100644 --- a/scripts/src/platform-infra-sub2api-codex.ts +++ b/scripts/src/platform-infra-sub2api-codex.ts @@ -103,6 +103,16 @@ interface CodexPoolLocalCodexConfig { backupSuffix: string; providerName: string; wireApi: string; + supportsWebSockets: boolean; + responsesWebSocketsV2: boolean; +} + +interface CodexLocalConsumerTomlOptions { + providerName: string; + baseUrl: string; + wireApi: string; + supportsWebSockets: boolean; + responsesWebSocketsV2: boolean; } export function codexPoolHelp(): unknown { @@ -367,6 +377,8 @@ async function codexPoolConfigureLocal(config: UniDeskConfig, options: ConfirmOp baseUrl: pool.publicExposure.masterBaseUrl, providerName: pool.localCodex.providerName, wireApi: pool.localCodex.wireApi, + supportsWebSockets: pool.localCodex.supportsWebSockets, + responsesWebSocketsV2: pool.localCodex.responsesWebSocketsV2, valuesPrinted: false, }, next: { @@ -570,6 +582,8 @@ function defaultCodexPoolConfig(): CodexPoolConfig { backupSuffix: "pre-sub2api", providerName: "OpenAI", wireApi: "responses", + supportsWebSockets: true, + responsesWebSocketsV2: true, }, }; } @@ -647,6 +661,13 @@ function readAccountCapacity(value: unknown, key: string): number { return capacity; } +function readBooleanConfig(value: unknown, key: string, fallback: boolean): boolean { + if (value === undefined || value === null) return fallback; + const parsed = booleanValue(value); + if (parsed === null) throw new Error(`${codexPoolConfigPath}.${key} must be a boolean`); + return parsed; +} + function readPublicExposureConfig(value: unknown, defaults: CodexPoolPublicExposureConfig): CodexPoolPublicExposureConfig { if (!isRecord(value)) return defaults; const masterFrpsValue = isRecord(value.masterFrps) ? value.masterFrps : {}; @@ -688,6 +709,8 @@ function readLocalCodexConfig(value: unknown, defaults: CodexPoolLocalCodexConfi backupSuffix: stringValue(value.backupSuffix) ?? defaults.backupSuffix, providerName: stringValue(value.providerName) ?? defaults.providerName, wireApi: stringValue(value.wireApi) ?? defaults.wireApi, + supportsWebSockets: readBooleanConfig(value.supportsWebSockets, "localCodex.supportsWebSockets", defaults.supportsWebSockets), + responsesWebSocketsV2: readBooleanConfig(value.responsesWebSocketsV2, "localCodex.responsesWebSocketsV2", defaults.responsesWebSocketsV2), }; if (!/^[A-Za-z0-9._-]+$/u.test(config.backupSuffix)) throw new Error(`${codexPoolConfigPath}.localCodex.backupSuffix has an unsupported format`); validateProxyName(config.providerName, "localCodex.providerName"); @@ -1093,6 +1116,8 @@ function writeLocalCodexConfig(pool: CodexPoolConfig, apiKey: string): Record line.trim() === header); @@ -1134,6 +1171,7 @@ function upsertProviderSection(text: string, providerName: string, baseUrl: stri `base_url = ${tomlString(baseUrl)}`, `wire_api = ${tomlString(wireApi)}`, "requires_openai_auth = true", + `supports_websockets = ${tomlBoolean(supportsWebSockets)}`, ]; if (start === -1) { const needsBlank = lines.length > 0 && lines[lines.length - 1].trim() !== ""; @@ -1147,12 +1185,37 @@ function upsertProviderSection(text: string, providerName: string, baseUrl: stri } } const preserved = lines.slice(start + 1, end).filter((line) => { - return !/^\s*(name|base_url|wire_api|requires_openai_auth|env_key)\s*=/u.test(line); + return !/^\s*(name|base_url|wire_api|requires_openai_auth|supports_websockets|env_key)\s*=/u.test(line); }); const replacement = [header, ...canonical, ...preserved.filter((line) => line.trim() !== "")]; return [...lines.slice(0, start), ...replacement, ...lines.slice(end)].join("\n"); } +function upsertTomlSectionBoolean(text: string, sectionName: string, key: string, value: boolean): string { + const header = `[${sectionName}]`; + const lines = text.split(/\r?\n/u); + const start = lines.findIndex((line) => line.trim() === header); + const assignment = `${key} = ${tomlBoolean(value)}`; + if (start === -1) { + const needsBlank = lines.length > 0 && lines[lines.length - 1].trim() !== ""; + return [...lines, ...(needsBlank ? [""] : []), header, assignment].join("\n"); + } + let end = lines.length; + for (let index = start + 1; index < lines.length; index += 1) { + if (/^\s*\[/.test(lines[index])) { + end = index; + break; + } + } + for (let index = start + 1; index < end; index += 1) { + if (new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(lines[index])) { + lines[index] = assignment; + return lines.join("\n"); + } + } + return [...lines.slice(0, start + 1), assignment, ...lines.slice(start + 1)].join("\n"); +} + async function validatePublicGatewayWithKey(pool: CodexPoolConfig, apiKey: string): Promise> { const probe = await probePublicModels(pool, "with-api-key", apiKey); return { @@ -1216,6 +1279,10 @@ function tomlString(value: string): string { return JSON.stringify(value); } +function tomlBoolean(value: boolean): string { + return value ? "true" : "false"; +} + function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); }