diff --git a/src/web/public/i18n.js b/src/web/public/i18n.js index b47da9f3..34c7a1fb 100644 --- a/src/web/public/i18n.js +++ b/src/web/public/i18n.js @@ -55,6 +55,9 @@ '应用于新本地会话;可用强度取决于模型和 Codex 版本。自定义端点、Docker 和远程会话保留自己的设置。', 'Default Codex model may only contain letters, digits, ".", "_", "-" and "/"': 'Codex 默认模型只能包含字母、数字、"."、"_"、"-" 和 "/"', + 'Allow network access': '允许网络访问', + "Lets commands in Codex's workspace-write sandbox use the network, which the Codeman skill needs to reach this server. Those commands can then call Codeman's API, which can start a session outside the sandbox with no approval prompt, even when a password is set. So with this on, Codex's sandbox does not contain a prompt-injected command: treat it as close to Bypass. Applies to every new local Codex session, not scheduled jobs. It does not change Codex's sandbox mode: a folder Codex runs read-only (one it does not trust) keeps the network closed and asks per command.": + '允许 Codex 的 workspace-write 沙箱中的命令使用网络,Codeman 技能需要网络才能连接到此服务器。这些命令随后可以调用 Codeman 的 API,而该 API 可以在沙箱之外启动会话且不弹出审批提示,即使设置了密码也是如此。因此开启后,Codex 的沙箱无法约束被提示注入的命令:请把它视为接近“绕过审批与沙箱”。应用于所有新的本地 Codex 会话,不包括定时任务。它不会改变 Codex 的沙箱模式:Codex 以只读方式运行的文件夹(即它不信任的文件夹)仍会保持网络关闭,并逐条命令询问。', 'Skip to terminal': '跳转到终端', 'Go to main page': '返回主页', 'Session tabs': '会话标签页', diff --git a/test/codex-network-access-i18n.test.ts b/test/codex-network-access-i18n.test.ts new file mode 100644 index 00000000..c7e30599 --- /dev/null +++ b/test/codex-network-access-i18n.test.ts @@ -0,0 +1,68 @@ +/** + * @fileoverview The Codex "Allow network access" row (App Settings → Codex) in Chinese (zh-CN). + * That description is the only warning a user gets that the switch weakens Codex's sandbox, so + * it must read in Chinese, through the real translator over the real index.html in JSDOM, with + * no English left beyond product names and the `workspace-write` mode name, and each key must be + * in the dictionary exactly once (a repeated key in the object literal silently replaces the + * first). + * + * Port: N/A. + */ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import vm from 'node:vm'; +import { JSDOM } from 'jsdom'; +import { afterAll, describe, expect, it } from 'vitest'; + +const PUBLIC = resolve(import.meta.dirname, '../src/web/public'); +const I18N = readFileSync(resolve(PUBLIC, 'i18n.js'), 'utf8'); +const INDEX = readFileSync(resolve(PUBLIC, 'index.html'), 'utf8'); +const ROW_INPUT = 'appSettingsCodexNetworkAccess'; + +type Api = { configure(o: Record): void; start(): void }; + +/** What may stay Latin: the product names, the API acronym and codex's own sandbox mode name. */ +const leftover = (text: string) => + text.replace(/workspace-write|\b(Codex|Codeman|API)\b/g, '').match(/[A-Za-z]+/g) ?? []; + +function rowStrings(doc: Document): { label: string; description: string } { + const row = doc.getElementById(ROW_INPUT)!.closest('.set-row')!; + return { + label: row.querySelector('.set-row-label')!.textContent!.trim(), + description: row.querySelector('.set-row-desc')!.textContent!.trim(), + }; +} + +const source = new JSDOM(INDEX); +const dom = new JSDOM(INDEX, { runScripts: 'outside-only', url: 'http://localhost/' }); +vm.runInContext(I18N, dom.getInternalVMContext(), { filename: 'i18n.js' }); +const api = (dom.window as unknown as { CodemanI18n: Api }).CodemanI18n; +api.start(); +api.configure({ language: 'zh-CN' }); + +afterAll(() => { + source.window.close(); + dom.window.close(); +}); + +describe('the Allow network access row in zh-CN', () => { + const english = rowStrings(source.window.document); + const chinese = rowStrings(dom.window.document); + + it.each(['label', 'description'] as const)('translates the %s, with no English left', (key) => { + expect(english[key].length).toBeGreaterThan(0); + expect(chinese[key]).not.toBe(english[key]); + expect(leftover(chinese[key])).toEqual([]); + }); + + it('keeps the warning that the setting is close to Bypass', () => { + expect(chinese.description).toContain('绕过审批与沙箱'); + }); + + it('adds each key to the dictionary once', () => { + for (const text of Object.values(english)) { + const quoted = text.includes("'") ? JSON.stringify(text) : `'${text}'`; + expect(I18N.split(`${quoted}:`).length - 1).toBe(1); + } + }); +});