@baize-ai/core 0.3.15 → 0.3.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/Dockerfile +10 -1
- package/cli/commands/init.js +48 -5
- package/cli/commands/runtime.js +9 -1
- package/cli/lib/__tests__/init-base-url.test.js +55 -13
- package/cli/lib/__tests__/runtime-base-url.test.js +3 -1
- package/cli/lib/__tests__/runtime-setup.test.js +53 -6
- package/cli/lib/runtime/codex.js +17 -0
- package/cli/lib/runtime-setup.js +153 -12
- package/package.json +2 -2
- package/scripts/docker-publish.sh +0 -4
- package/scripts/pack-release.sh +34 -26
- package/skills/web-console/public/app.js +20 -75
- package/skills/web-console/scripts/model-provider.js +97 -59
- package/skills/web-console/scripts/server.js +6 -24
- package/test/model-provider.test.js +150 -40
- package/test/web-console-routes.test.js +11 -13
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to baize-core will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.16] - 2026-09-13
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Codex 供应商配置改造(D51):独立 provider 块写入器 + kind/slug store + init/runtime 遗留路径转换 + web-console 只读激活卡;`/models` 探测读取 provider 块 base_url
|
|
12
|
+
- Dockerfile 本地源码构建支持(D51):`BAZE_CORE_TARBALL=/core.tgz` 安装工作区包而非 registry 版本——验证未发布修复(如 D51)进镜像
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- D51 部署缺陷(init):web-console `import ../../../cli/lib/runtime-setup.js` 运行时 `~/.claude/cli/lib/` 不存在 → ERR_MODULE_NOT_FOUND 崩溃——`syncCoreSkills` 补拷 `package/cli` 至 `SKILLS_DIR 上级/cli`,并从包根复制零依赖 `node_modules/smol-toml`
|
|
16
|
+
- D51 配置污染事故根因(4a37368):`globalCodexConfigPath` 优先读 `HOME` env 而非 `os.homedir()`——jest ESM sandbox 曾写穿真实 `~/.codex/config.toml`
|
|
17
|
+
- D51 review 修复:restore 只删 baize-written 块(`name=key && wire_api=responses`);写盘直设 `0600` + 去重用例;激活卡展示 kind
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- A2A 授权治理收敛(D52 2b11171):agent 本地 authz 仅保留 mode 切换(open/allowlist)——allow/block 名单由 admin 控制台统一管控(PUT 忽略名单强制清空 + UI 隐藏名单编辑并提示管控来源;移除死代码 `validateAuthzList`/peerIds 请求)
|
|
21
|
+
|
|
8
22
|
## [0.3.15] - 2026-08-31
|
|
9
23
|
|
|
10
24
|
### Fixed
|
package/Dockerfile
CHANGED
|
@@ -82,8 +82,12 @@ ENV WEB_CONSOLE_BIND=0.0.0.0
|
|
|
82
82
|
# build context) — source stays private, the image matches the npm version
|
|
83
83
|
# exactly. Build with: docker build --build-arg BAZE_CORE_VERSION=0.2.0 .
|
|
84
84
|
ARG BAZE_CORE_VERSION=latest
|
|
85
|
+
# Local-source build: pass BAZE_CORE_TARBALL=/core.tgz (COPY'd into the
|
|
86
|
+
# build context) to install the workspace package instead of the registry
|
|
87
|
+
# version — lets us verify un-released fixes (e.g. D51) in the image.
|
|
88
|
+
ARG BAZE_CORE_TARBALL=
|
|
85
89
|
WORKDIR /home/baize
|
|
86
|
-
RUN npm install -g @baize-ai/core@${BAZE_CORE_VERSION} \
|
|
90
|
+
RUN if [ -n "${BAZE_CORE_TARBALL}" ]; then npm install -g /tmp/${BAZE_CORE_TARBALL}; else npm install -g @baize-ai/core@${BAZE_CORE_VERSION}; fi \
|
|
87
91
|
&& baize --version \
|
|
88
92
|
# ── Build-time skill dependency preinstall (reproducibility) ─────────
|
|
89
93
|
# `baize init` reinstalls skill deps at runtime; doing it here bakes the
|
|
@@ -134,6 +138,11 @@ RUN mkdir -p \
|
|
|
134
138
|
# ── Copy PM2 ecosystem config ─────────────────────────────────────────────────
|
|
135
139
|
COPY --chown=baize:baize templates/pm2/ecosystem.config.cjs /home/baize/baize/pm2/ecosystem.config.cjs
|
|
136
140
|
|
|
141
|
+
# ── Local-source build context (optional) ────────────────────────────────────
|
|
142
|
+
# When BAZE_CORE_TARBALL is set, the tarball must live at ./core.tgz in the
|
|
143
|
+
# build context; it is staged at /tmp so the install RUN above can consume it.
|
|
144
|
+
COPY core.tgz /tmp/core.tgz
|
|
145
|
+
|
|
137
146
|
# ── Copy entrypoint ───────────────────────────────────────────────────────────
|
|
138
147
|
COPY --chown=baize:baize docker/entrypoint.sh /entrypoint.sh
|
|
139
148
|
RUN chmod +x /entrypoint.sh
|
package/cli/commands/init.js
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
saveCodexApiKeyToEnv,
|
|
42
42
|
saveCodexBaseUrlToEnv,
|
|
43
43
|
writeCodexConfig,
|
|
44
|
+
applyCodexProviderBlock,
|
|
44
45
|
} from '../lib/runtime-setup.js';
|
|
45
46
|
|
|
46
47
|
// Source directories (shipped with baize package)
|
|
@@ -839,6 +840,30 @@ function syncCoreSkills() {
|
|
|
839
840
|
const installed = [];
|
|
840
841
|
const updated = [];
|
|
841
842
|
|
|
843
|
+
// D51: web-console imports `../../../cli/lib/runtime-setup.js`, which from
|
|
844
|
+
// `~/.claude/skills/web-console/scripts/` resolves to `~/.claude/cli/lib/`.
|
|
845
|
+
// The npm package ships `package/cli`, but init never deployed it to the
|
|
846
|
+
// runtime tree — web-console crashed with ERR_MODULE_NOT_FOUND in the image.
|
|
847
|
+
// Deploy the shared CLI lib beside SKILLS_DIR so the import path mirrors the
|
|
848
|
+
// package layout (package/cli ↔ <SKILLS_DIR>/../cli).
|
|
849
|
+
const cliSrc = path.join(PACKAGE_ROOT, 'cli');
|
|
850
|
+
const cliDest = path.join(path.dirname(SKILLS_DIR), 'cli');
|
|
851
|
+
if (fs.existsSync(cliSrc)) {
|
|
852
|
+
try {
|
|
853
|
+
copyTree(cliSrc, cliDest);
|
|
854
|
+
// runtime-setup.js imports smol-toml (D51). The cli/ tree is deployed
|
|
855
|
+
// outside any skill's node_modules, so the dependency must ride along —
|
|
856
|
+
// mirror the package layout by copying the package's smol-toml into
|
|
857
|
+
// <cliDest>/node_modules/.
|
|
858
|
+
const tomlSrc = path.join(PACKAGE_ROOT, 'node_modules', 'smol-toml');
|
|
859
|
+
if (fs.existsSync(tomlSrc)) {
|
|
860
|
+
copyTree(tomlSrc, path.join(cliDest, 'node_modules', 'smol-toml'));
|
|
861
|
+
}
|
|
862
|
+
} catch {
|
|
863
|
+
console.log(` ${warn(`Failed to sync cli/ to ${cliDest}`)}`);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
842
867
|
const entries = fs.readdirSync(CORE_SKILLS_SRC, { withFileTypes: true });
|
|
843
868
|
for (const entry of entries) {
|
|
844
869
|
if (!entry.isDirectory()) continue;
|
|
@@ -2182,9 +2207,17 @@ export async function initCommand(args) {
|
|
|
2182
2207
|
}
|
|
2183
2208
|
}
|
|
2184
2209
|
}
|
|
2185
|
-
|
|
2186
|
-
//
|
|
2187
|
-
|
|
2210
|
+
// Write ~/.codex/config.toml to suppress interactive prompts on first
|
|
2211
|
+
// launch. A custom --codex-base-url becomes a dedicated provider block
|
|
2212
|
+
// (D51) — never the built-in-OpenAI openai_base_url override.
|
|
2213
|
+
if (pendingCodexBaseUrl) {
|
|
2214
|
+
applyCodexProviderBlock({
|
|
2215
|
+
providerKey: 'baize-init',
|
|
2216
|
+
baseUrl: pendingCodexBaseUrl,
|
|
2217
|
+
token: opts.codexApiKey || undefined,
|
|
2218
|
+
});
|
|
2219
|
+
if (!quiet) console.log(` ${success('Codex startup config written (provider block)')}`);
|
|
2220
|
+
} else if (writeCodexConfig(BAIZE_DIR)) {
|
|
2188
2221
|
if (!quiet) console.log(` ${success('Codex startup config written')}`);
|
|
2189
2222
|
}
|
|
2190
2223
|
}
|
|
@@ -2409,7 +2442,12 @@ export async function initCommand(args) {
|
|
|
2409
2442
|
}
|
|
2410
2443
|
if (pendingCodexBaseUrl) {
|
|
2411
2444
|
saveCodexBaseUrlToEnv(pendingCodexBaseUrl);
|
|
2412
|
-
|
|
2445
|
+
// D51: custom endpoint → dedicated provider block, not the built-in-OpenAI override.
|
|
2446
|
+
applyCodexProviderBlock({
|
|
2447
|
+
providerKey: 'baize-init',
|
|
2448
|
+
baseUrl: pendingCodexBaseUrl,
|
|
2449
|
+
token: opts.codexApiKey || undefined,
|
|
2450
|
+
});
|
|
2413
2451
|
}
|
|
2414
2452
|
|
|
2415
2453
|
// Timezone: use resolved value or show current
|
|
@@ -2518,7 +2556,12 @@ export async function initCommand(args) {
|
|
|
2518
2556
|
}
|
|
2519
2557
|
if (pendingCodexBaseUrl) {
|
|
2520
2558
|
saveCodexBaseUrlToEnv(pendingCodexBaseUrl);
|
|
2521
|
-
|
|
2559
|
+
// D51: custom endpoint → dedicated provider block, not the built-in-OpenAI override.
|
|
2560
|
+
applyCodexProviderBlock({
|
|
2561
|
+
providerKey: 'baize-init',
|
|
2562
|
+
baseUrl: pendingCodexBaseUrl,
|
|
2563
|
+
token: opts.codexApiKey || undefined,
|
|
2564
|
+
});
|
|
2522
2565
|
}
|
|
2523
2566
|
// Step 8: Configure timezone
|
|
2524
2567
|
if (!quiet) console.log(`\n${heading('Timezone configuration...')}`);
|
package/cli/commands/runtime.js
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
saveSetupTokenToEnv,
|
|
30
30
|
saveCodexApiKey,
|
|
31
31
|
saveCodexBaseUrlToEnv,
|
|
32
|
+
applyCodexProviderBlock,
|
|
32
33
|
saveCodexApiKeyToEnv,
|
|
33
34
|
writeCodexConfig,
|
|
34
35
|
} from '../lib/runtime-setup.js';
|
|
@@ -123,7 +124,14 @@ export function applyBaseUrl(target, baseUrl) {
|
|
|
123
124
|
}
|
|
124
125
|
if (target === 'codex') {
|
|
125
126
|
if (!saveCodexBaseUrlToEnv(baseUrl)) return false;
|
|
126
|
-
|
|
127
|
+
// D51: external endpoints get a dedicated provider block (DeepSeek-safe,
|
|
128
|
+
// local compaction) — never the built-in-openai openai_base_url override.
|
|
129
|
+
try {
|
|
130
|
+
applyCodexProviderBlock({ providerKey: 'baize-runtime', baseUrl, projectDir: BAIZE_DIR });
|
|
131
|
+
return true;
|
|
132
|
+
} catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
127
135
|
}
|
|
128
136
|
return false;
|
|
129
137
|
}
|
|
@@ -72,7 +72,7 @@ describe('base URL support', () => {
|
|
|
72
72
|
}
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
test('writeCodexConfig writes openai_base_url
|
|
75
|
+
test('writeCodexConfig never writes openai_base_url — env override is dropped (D51)', async () => {
|
|
76
76
|
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'baize-base-url-'));
|
|
77
77
|
const originalHome = process.env.HOME;
|
|
78
78
|
const originalOpenAiBaseUrl = process.env.OPENAI_BASE_URL;
|
|
@@ -87,7 +87,7 @@ describe('base URL support', () => {
|
|
|
87
87
|
|
|
88
88
|
const configPath = path.join(tmpRoot, '.codex', 'config.toml');
|
|
89
89
|
const config = fs.readFileSync(configPath, 'utf8');
|
|
90
|
-
assert.
|
|
90
|
+
assert.doesNotMatch(config, /openai_base_url/);
|
|
91
91
|
} finally {
|
|
92
92
|
if (originalHome === undefined) delete process.env.HOME;
|
|
93
93
|
else process.env.HOME = originalHome;
|
|
@@ -99,31 +99,73 @@ describe('base URL support', () => {
|
|
|
99
99
|
}
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
-
test('writeCodexConfig
|
|
102
|
+
test('writeCodexConfig strips a stale legacy openai_base_url from existing config (D51)', async () => {
|
|
103
103
|
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'baize-base-url-opt-'));
|
|
104
104
|
const originalHome = process.env.HOME;
|
|
105
|
-
const originalOpenAiBaseUrl = process.env.OPENAI_BASE_URL;
|
|
106
105
|
|
|
107
106
|
process.env.HOME = tmpRoot;
|
|
108
|
-
delete process.env.OPENAI_BASE_URL;
|
|
109
107
|
|
|
110
108
|
try {
|
|
111
109
|
const { writeCodexConfig } = await import('../runtime-setup.js');
|
|
112
110
|
|
|
113
|
-
assert.equal(
|
|
114
|
-
writeCodexConfig(path.join(tmpRoot, 'baize-project'), { openaiBaseUrl: 'https://explicit-proxy.example.com/v1' }),
|
|
115
|
-
true
|
|
116
|
-
);
|
|
117
|
-
|
|
118
111
|
const configPath = path.join(tmpRoot, '.codex', 'config.toml');
|
|
112
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
113
|
+
fs.writeFileSync(configPath, 'openai_base_url = "https://explicit-proxy.example.com/v1"\nmodel = "old"\n');
|
|
114
|
+
|
|
115
|
+
assert.equal(writeCodexConfig(path.join(tmpRoot, 'baize-project'), { openaiBaseUrl: 'https://explicit-proxy.example.com/v1' }), true);
|
|
116
|
+
|
|
119
117
|
const config = fs.readFileSync(configPath, 'utf8');
|
|
120
|
-
assert.
|
|
118
|
+
assert.doesNotMatch(config, /openai_base_url/);
|
|
119
|
+
assert.match(config, /^model = "old"$/m, 'unrelated keys preserved');
|
|
121
120
|
} finally {
|
|
122
121
|
if (originalHome === undefined) delete process.env.HOME;
|
|
123
122
|
else process.env.HOME = originalHome;
|
|
124
123
|
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('--codex-base-url path writes a baize-init provider block (D51)', async () => {
|
|
129
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'baize-init-provider-'));
|
|
130
|
+
const originalHome = process.env.HOME;
|
|
131
|
+
const originalBaizeDir = process.env.BAIZE_DIR;
|
|
132
|
+
|
|
133
|
+
process.env.HOME = tmpRoot;
|
|
134
|
+
process.env.BAIZE_DIR = path.join(tmpRoot, 'baize');
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const { applyCodexProviderBlock } = await import('../runtime-setup.js');
|
|
138
|
+
|
|
139
|
+
// This mirrors exactly what init.js does when --codex-base-url is given:
|
|
140
|
+
// pendingCodexBaseUrl → applyCodexProviderBlock (providerKey 'baize-init').
|
|
141
|
+
// projectDir passed explicitly: the module-level BAIZE_DIR fallback is
|
|
142
|
+
// captured at first import and may not match this test's env override.
|
|
143
|
+
applyCodexProviderBlock({
|
|
144
|
+
providerKey: 'baize-init',
|
|
145
|
+
baseUrl: 'https://proxy.example.com/v1',
|
|
146
|
+
token: 'sk-init-key',
|
|
147
|
+
projectDir: process.env.BAIZE_DIR,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const config = fs.readFileSync(path.join(tmpRoot, '.codex', 'config.toml'), 'utf8');
|
|
151
|
+
assert.match(config, /\[model_providers\.baize-init\]/);
|
|
152
|
+
assert.match(config, /^model_provider = "baize-init"$/m);
|
|
153
|
+
assert.match(config, /^name = "baize-init"$/m);
|
|
154
|
+
assert.match(config, /^base_url = "https:\/\/proxy\.example\.com\/v1"$/m);
|
|
155
|
+
assert.match(config, /^wire_api = "responses"$/m);
|
|
156
|
+
assert.match(config, /^experimental_bearer_token = "sk-init-key"$/m);
|
|
157
|
+
assert.doesNotMatch(config, /openai_base_url/);
|
|
158
|
+
|
|
159
|
+
// Project-level: no provider block, no override.
|
|
160
|
+
const projectConfig = fs.readFileSync(path.join(tmpRoot, 'baize', '.codex', 'config.toml'), 'utf8');
|
|
161
|
+
assert.doesNotMatch(projectConfig, /model_provider/);
|
|
162
|
+
assert.doesNotMatch(projectConfig, /\[model_providers\./);
|
|
163
|
+
assert.doesNotMatch(projectConfig, /openai_base_url/);
|
|
164
|
+
} finally {
|
|
165
|
+
if (originalHome === undefined) delete process.env.HOME;
|
|
166
|
+
else process.env.HOME = originalHome;
|
|
167
|
+
if (originalBaizeDir === undefined) delete process.env.BAIZE_DIR;
|
|
168
|
+
else process.env.BAIZE_DIR = originalBaizeDir;
|
|
127
169
|
|
|
128
170
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
129
171
|
}
|
|
@@ -126,6 +126,8 @@ describe('runtime base URL support', () => {
|
|
|
126
126
|
assert.equal(claudeSettings.env.ANTHROPIC_BASE_URL, 'https://claude-proxy.example.com');
|
|
127
127
|
|
|
128
128
|
const codexConfig = fs.readFileSync(path.join(tmpRoot, '.codex', 'config.toml'), 'utf8');
|
|
129
|
-
assert.match(codexConfig, /
|
|
129
|
+
assert.match(codexConfig, /model_provider = "baize-runtime"/);
|
|
130
|
+
assert.match(codexConfig, /\[model_providers\.baize-runtime\][\s\S]*?base_url = "https:\/\/codex-proxy\.example\.com\/v1"/);
|
|
131
|
+
assert.doesNotMatch(codexConfig, /openai_base_url/);
|
|
130
132
|
});
|
|
131
133
|
});
|
|
@@ -162,9 +162,55 @@ describe('renderCodexGlobalConfig', () => {
|
|
|
162
162
|
assert.doesNotMatch(content, /check_for_update_on_startup/);
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
-
it('
|
|
165
|
+
it('never writes openai_base_url — legacy opt only cleans the override (D51)', () => {
|
|
166
166
|
const content = renderCodexGlobalConfig('/home/user/baize', '', { openaiBaseUrl: 'https://proxy.example.com/v1' });
|
|
167
|
-
assert.
|
|
167
|
+
assert.doesNotMatch(content, /openai_base_url/);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('renders a dedicated provider block from opts.codex (D51)', () => {
|
|
171
|
+
const content = renderCodexGlobalConfig('/home/user/baize', '', {
|
|
172
|
+
codex: {
|
|
173
|
+
providerKey: 'deepseek',
|
|
174
|
+
baseUrl: 'https://api.deepseek.com/',
|
|
175
|
+
token: 'sk-test',
|
|
176
|
+
model: 'deepseek-v4-flash',
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
assert.match(content, /^model = "deepseek-v4-flash"$/m);
|
|
180
|
+
assert.match(content, /^model_provider = "deepseek"$/m);
|
|
181
|
+
assert.match(content, /\[model_providers\.deepseek\]/);
|
|
182
|
+
assert.match(content, /name = "deepseek"/);
|
|
183
|
+
assert.match(content, /base_url = "https:\/\/api\.deepseek\.com\/"/);
|
|
184
|
+
assert.match(content, /wire_api = "responses"/);
|
|
185
|
+
assert.match(content, /experimental_bearer_token = "sk-test"/);
|
|
186
|
+
assert.doesNotMatch(content, /openai_base_url/);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('provider block omits the token key when no token is given (D51)', () => {
|
|
190
|
+
const content = renderCodexGlobalConfig('/home/user/baize', '', {
|
|
191
|
+
codex: { providerKey: 'deepseek', baseUrl: 'https://api.deepseek.com/', model: 'deepseek-v4-flash' },
|
|
192
|
+
});
|
|
193
|
+
assert.match(content, /\[model_providers\.deepseek\]/);
|
|
194
|
+
assert.doesNotMatch(content, /experimental_bearer_token/);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('provider block replaces the legacy override and keeps other provider tables (D51)', () => {
|
|
198
|
+
const existing = [
|
|
199
|
+
'openai_base_url = "https://old.example.com/v1"',
|
|
200
|
+
'model = "old"',
|
|
201
|
+
'',
|
|
202
|
+
'[model_providers.userown]',
|
|
203
|
+
'name = "userown"',
|
|
204
|
+
'base_url = "https://u.example"',
|
|
205
|
+
'',
|
|
206
|
+
].join('\n');
|
|
207
|
+
const content = renderCodexGlobalConfig('/home/user/baize', existing, {
|
|
208
|
+
codex: { providerKey: 'deepseek', baseUrl: 'https://api.deepseek.com/' },
|
|
209
|
+
});
|
|
210
|
+
assert.doesNotMatch(content, /openai_base_url/);
|
|
211
|
+
assert.match(content, /^model = "old"$/m, 'model kept when opts.codex.model absent');
|
|
212
|
+
assert.match(content, /\[model_providers\.userown\]/, 'user tables survive');
|
|
213
|
+
assert.match(content, /\[model_providers\.deepseek\]/);
|
|
168
214
|
});
|
|
169
215
|
|
|
170
216
|
it('preserves unknown global top-level keys, sections, and unrelated projects', () => {
|
|
@@ -189,22 +235,23 @@ describe('renderCodexGlobalConfig', () => {
|
|
|
189
235
|
assert.match(content, /\[projects\."\/home\/user\/baize"\]\ntrust_level = "trusted"/);
|
|
190
236
|
});
|
|
191
237
|
|
|
192
|
-
it('
|
|
238
|
+
it('strips a stale legacy openai_base_url even without opts (D51 migration)', () => {
|
|
193
239
|
const existing = 'openai_base_url = "https://user-proxy.example.com/v1"\n';
|
|
194
240
|
const content = renderCodexGlobalConfig('/home/user/baize', existing);
|
|
195
|
-
assert.
|
|
241
|
+
assert.doesNotMatch(content, /openai_base_url/);
|
|
196
242
|
});
|
|
197
243
|
|
|
198
|
-
it('
|
|
244
|
+
it('cleans the legacy openai_base_url when the legacy opt is passed (D51)', () => {
|
|
199
245
|
const existing = 'openai_base_url = "https://old-proxy.example.com/v1"\n';
|
|
200
246
|
const content = renderCodexGlobalConfig('/home/user/baize', existing, {
|
|
201
247
|
openaiBaseUrl: 'https://new-proxy.example.com/v1',
|
|
202
248
|
});
|
|
203
|
-
assert.
|
|
249
|
+
assert.doesNotMatch(content, /openai_base_url/);
|
|
204
250
|
assert.doesNotMatch(content, /old-proxy/);
|
|
205
251
|
});
|
|
206
252
|
});
|
|
207
253
|
|
|
254
|
+
|
|
208
255
|
describe('writeCodexConfig', () => {
|
|
209
256
|
it('writes project-level config and global config to separate locations', () => {
|
|
210
257
|
const globalConfigPath = path.join(fakeHome, '.codex', 'config.toml');
|
package/cli/lib/runtime/codex.js
CHANGED
|
@@ -55,10 +55,27 @@ function getCodexApiBaseUrl() {
|
|
|
55
55
|
try {
|
|
56
56
|
const configPath = path.join(os.homedir(), '.codex', 'config.toml');
|
|
57
57
|
const config = fs.readFileSync(configPath, 'utf8');
|
|
58
|
+
// legacy override first (pre-D51 configs)
|
|
58
59
|
const match = config.match(/^\s*openai_base_url\s*=\s*"([^"]+)"\s*$/m);
|
|
59
60
|
if (match?.[1]) {
|
|
60
61
|
return match[1].replace(/\/+$/, '');
|
|
61
62
|
}
|
|
63
|
+
// D51: the active provider block carries the endpoint — resolve the
|
|
64
|
+
// top-level model_provider slug and read its base_url, so the /models
|
|
65
|
+
// probe stays in sync with what codex actually calls (otherwise an
|
|
66
|
+
// external endpoint activation reports 401 against api.openai.com).
|
|
67
|
+
const providerKey = config.match(/^\s*model_provider\s*=\s*"([^"]+)"\s*$/m)?.[1];
|
|
68
|
+
if (providerKey) {
|
|
69
|
+
const escaped = providerKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
70
|
+
const blockRe = new RegExp(
|
|
71
|
+
`\\[model_providers\\.${escaped}\\][^[]*?\\bbase_url\\s*=\\s*"([^"]+)"`,
|
|
72
|
+
'm'
|
|
73
|
+
);
|
|
74
|
+
const blockMatch = config.match(blockRe);
|
|
75
|
+
if (blockMatch?.[1]) {
|
|
76
|
+
return blockMatch[1].replace(/\/+$/, '');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
62
79
|
} catch { /* ignore missing config */ }
|
|
63
80
|
|
|
64
81
|
if (process.env.OPENAI_BASE_URL) {
|
package/cli/lib/runtime-setup.js
CHANGED
|
@@ -11,6 +11,7 @@ import os from 'node:os';
|
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import { execSync, execFileSync, spawnSync } from 'node:child_process';
|
|
13
13
|
import { parse, stringify } from 'smol-toml';
|
|
14
|
+
import crypto from 'node:crypto';
|
|
14
15
|
import { BAIZE_DIR } from './config.js';
|
|
15
16
|
import { commandExists } from './shell-utils.js';
|
|
16
17
|
import { parseClaudeAuthStatus, parseCodexLoginStatus } from './auth-parsers.js';
|
|
@@ -410,21 +411,30 @@ export function renderCodexProjectConfig(existingContent = '', opts = {}) {
|
|
|
410
411
|
/**
|
|
411
412
|
* Render global ~/.codex/config.toml with user/environment-level settings.
|
|
412
413
|
*
|
|
413
|
-
* Contains only trust declarations and optional
|
|
414
|
-
* Existing [projects.*] trust entries are preserved; the baize project trust
|
|
415
|
-
* entry is always regenerated.
|
|
416
|
-
*
|
|
417
|
-
* @param {string} projectDir - The baize working directory to pre-trust
|
|
418
|
-
* @param {string} existingContent - Existing global config.toml contents (optional)
|
|
419
|
-
* @param {{ openaiBaseUrl?: string }} opts - Optional Codex config overrides
|
|
414
|
+
* Contains only trust declarations and the optional D51 provider block.
|
|
420
415
|
* @returns {string}
|
|
421
416
|
*/
|
|
422
417
|
export function renderCodexGlobalConfig(projectDir, existingContent = '', opts = {}) {
|
|
423
418
|
const absProject = path.resolve(projectDir);
|
|
424
|
-
const openaiBaseUrl = opts.openaiBaseUrl || process.env.OPENAI_BASE_URL || '';
|
|
425
419
|
const obj = parseCodexToml(existingContent);
|
|
426
|
-
|
|
427
|
-
|
|
420
|
+
// D51: external Codex endpoints are configured as a dedicated provider
|
|
421
|
+
// block (model_providers.<key> + top-level model_provider) instead of
|
|
422
|
+
// overriding the built-in OpenAI provider via openai_base_url — the override
|
|
423
|
+
// keeps provider.name === "OpenAI", which makes Codex enable remote
|
|
424
|
+
// compaction v2 against endpoints that cannot answer it (DeepSeek → fatal
|
|
425
|
+
// "expected exactly one compaction output item").
|
|
426
|
+
const codex = opts.codex || null;
|
|
427
|
+
if (codex && codex.providerKey && codex.baseUrl) {
|
|
428
|
+
obj.model = codex.model || obj.model;
|
|
429
|
+
obj.model_provider = codex.providerKey;
|
|
430
|
+
obj.model_providers = isTomlSectionValue(obj.model_providers) ? obj.model_providers : {};
|
|
431
|
+
const block = { name: codex.providerKey, base_url: codex.baseUrl, wire_api: 'responses' };
|
|
432
|
+
if (codex.token) block.experimental_bearer_token = codex.token;
|
|
433
|
+
obj.model_providers[codex.providerKey] = block;
|
|
434
|
+
delete obj.openai_base_url; // migration cleanup — never mix the two mechanisms
|
|
435
|
+
} else {
|
|
436
|
+
// D51: no provider block requested → never leave a stale override behind
|
|
437
|
+
delete obj.openai_base_url;
|
|
428
438
|
}
|
|
429
439
|
obj.features = isTomlSectionValue(obj.features) ? obj.features : {};
|
|
430
440
|
obj.features.hooks = true;
|
|
@@ -438,8 +448,9 @@ export function renderCodexGlobalConfig(projectDir, existingContent = '', opts =
|
|
|
438
448
|
*
|
|
439
449
|
* - Project config (<projectDir>/.codex/config.toml): headless settings,
|
|
440
450
|
* features, notice suppression — required for baize unattended operation.
|
|
441
|
-
* - Global config (~/.codex/config.toml): trust declarations
|
|
442
|
-
*
|
|
451
|
+
* - Global config (~/.codex/config.toml): trust declarations + optional D51
|
|
452
|
+
* provider block (opts.codex). Legacy opts.openaiBaseUrl is still accepted
|
|
453
|
+
* for compatibility but only cleans the override key (D51 migration).
|
|
443
454
|
*
|
|
444
455
|
* Called by both `baize init` (Codex runtime) and `baize runtime codex` so the
|
|
445
456
|
* config is always present when switching to Codex.
|
|
@@ -485,6 +496,136 @@ export function writeCodexConfig(projectDir, opts = {}) {
|
|
|
485
496
|
}
|
|
486
497
|
}
|
|
487
498
|
|
|
499
|
+
// ── D51: dedicated provider block writers (DeepSeek remote-compaction fix) ──
|
|
500
|
+
|
|
501
|
+
// Keys Codex treats as built-in model_providers entries — a custom provider
|
|
502
|
+
// must never shadow these (merge_configured_model_providers silently drops
|
|
503
|
+
// colliding keys). Also covers the web-console restore targets.
|
|
504
|
+
export const CODEX_RESERVED_PROVIDER_KEYS = new Set([
|
|
505
|
+
'openai', 'azure', 'amazon-bedrock', 'amazon-bedrock-runtime',
|
|
506
|
+
'ollama', 'lmstudio', 'gpt-oss', 'official', 'official-openai',
|
|
507
|
+
]);
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Deterministic provider slug for a provider id: ASCII slug, or
|
|
511
|
+
* `codex-<id-hash8>` when empty or reserved. Pure — no store access.
|
|
512
|
+
*/
|
|
513
|
+
export function codexProviderKeyForId(id) {
|
|
514
|
+
const base = String(id || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
515
|
+
if (base && !CODEX_RESERVED_PROVIDER_KEYS.has(base)) return base;
|
|
516
|
+
const hash = crypto.createHash('sha256').update(String(id || '')).digest('hex').slice(0, 8);
|
|
517
|
+
return `codex-${hash}`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function globalCodexConfigPath(homeDir) {
|
|
521
|
+
// D51 incident hardening: jest's ESM sandbox resolves os.homedir() to the
|
|
522
|
+
// REAL home regardless of process.env.HOME — relying on the default once
|
|
523
|
+
// wrote test fixtures into the user's live ~/.codex. Callers must pass
|
|
524
|
+
// homeDir explicitly; when they don't, honor HOME env first and only then
|
|
525
|
+
// fall back to os.homedir().
|
|
526
|
+
const base = homeDir || process.env.HOME || os.homedir();
|
|
527
|
+
return path.join(base, '.codex', 'config.toml');
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function projectCodexConfigPathFor(projectDir) {
|
|
531
|
+
// D51: resolve the fallback lazily — BAIZE_DIR is a module-load constant in
|
|
532
|
+
// config.js, so under test runners that mutate HOME/BAIZE_DIR per case the
|
|
533
|
+
// cached value would point at the first case's temp dir. env wins here.
|
|
534
|
+
const base = projectDir
|
|
535
|
+
? path.resolve(projectDir)
|
|
536
|
+
: (process.env.BAIZE_DIR || BAIZE_DIR);
|
|
537
|
+
return path.join(base, '.codex', 'config.toml');
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** Parse + stringify round-trip preserving everything smol-toml supports. */
|
|
541
|
+
function rewriteCodexToml(targetPath, mutate, { mode } = {}) {
|
|
542
|
+
const dir = path.dirname(targetPath);
|
|
543
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
544
|
+
let content = '';
|
|
545
|
+
try { content = fs.readFileSync(targetPath, 'utf8'); } catch { /* new file */ }
|
|
546
|
+
const obj = parseCodexToml(content);
|
|
547
|
+
mutate(obj);
|
|
548
|
+
const out = stringify(obj);
|
|
549
|
+
fs.writeFileSync(targetPath, out.endsWith('\n') ? out : `${out}\n`, mode ? { mode } : 'utf8');
|
|
550
|
+
return targetPath;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* D51: activate an external Codex provider as a dedicated [model_providers.<key>]
|
|
555
|
+
* block in the GLOBAL config + top-level model_provider, and ensure both files
|
|
556
|
+
* are free of the legacy openai_base_url override. The block's `name` always
|
|
557
|
+
* equals the slug (never "OpenAI") so Codex treats the endpoint as a generic
|
|
558
|
+
* responses-compatible provider → local compaction (DeepSeek-safe).
|
|
559
|
+
*
|
|
560
|
+
* @param {{ providerKey: string, baseUrl: string, token?: string, model?: string,
|
|
561
|
+
* homeDir?: string, projectDir?: string }} opts
|
|
562
|
+
* @returns {{ globalPath: string, projectPath: string }} written file paths
|
|
563
|
+
*/
|
|
564
|
+
export function applyCodexProviderBlock(opts) {
|
|
565
|
+
const { providerKey, baseUrl, token, model } = opts || {};
|
|
566
|
+
if (!providerKey || !baseUrl) {
|
|
567
|
+
throw new Error('applyCodexProviderBlock requires providerKey and baseUrl');
|
|
568
|
+
}
|
|
569
|
+
const key = CODEX_RESERVED_PROVIDER_KEYS.has(providerKey)
|
|
570
|
+
? codexProviderKeyForId(providerKey)
|
|
571
|
+
: providerKey;
|
|
572
|
+
const globalPath = rewriteCodexToml(globalCodexConfigPath(opts.homeDir), (obj) => {
|
|
573
|
+
if (model) obj.model = model;
|
|
574
|
+
obj.model_provider = key;
|
|
575
|
+
obj.model_providers = isTomlSectionValue(obj.model_providers) ? obj.model_providers : {};
|
|
576
|
+
const block = { name: key, base_url: baseUrl, wire_api: 'responses' };
|
|
577
|
+
if (token) block.experimental_bearer_token = token;
|
|
578
|
+
obj.model_providers[key] = block;
|
|
579
|
+
delete obj.openai_base_url;
|
|
580
|
+
}, { mode: 0o600 }); // token may ride the global file — write user-only from the start
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
// D31 semantics: codex prefers project-level values, so the provider model
|
|
584
|
+
// must be mirrored here — otherwise a stale project model (e.g. gpt-5.5
|
|
585
|
+
// backfilled by init) would shadow the just-activated provider model.
|
|
586
|
+
const projectPath = rewriteCodexToml(projectCodexConfigPathFor(opts.projectDir), (obj) => {
|
|
587
|
+
if (model) obj.model = model;
|
|
588
|
+
delete obj.openai_base_url; // project-level never carries endpoint/auth
|
|
589
|
+
});
|
|
590
|
+
return { globalPath, projectPath };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* D51: restore the official OpenAI configuration — remove the top-level
|
|
595
|
+
* model_provider and every baize-written custom provider block, drop the
|
|
596
|
+
* legacy openai_base_url override, reset model to gpt-5.5. Project-level
|
|
597
|
+
* model resets too (mirrors the pre-D51 applyCodexOfficial behavior).
|
|
598
|
+
*
|
|
599
|
+
* @param {{ homeDir?: string, projectDir?: string }} [opts]
|
|
600
|
+
* @returns {{ globalPath: string, projectPath: string }}
|
|
601
|
+
*/
|
|
602
|
+
export function restoreCodexOfficialBlock(opts = {}) {
|
|
603
|
+
const globalPath = rewriteCodexToml(globalCodexConfigPath(opts.homeDir), (obj) => {
|
|
604
|
+
delete obj.model_provider;
|
|
605
|
+
if (isTomlSectionValue(obj.model_providers)) {
|
|
606
|
+
for (const key of Object.keys(obj.model_providers)) {
|
|
607
|
+
const block = obj.model_providers[key];
|
|
608
|
+
// D51 review P2: only remove blocks baize itself wrote — the writer
|
|
609
|
+
// always stamps name === key && wire_api === 'responses'. Hand-edited
|
|
610
|
+
// user providers (any other shape) must survive an official restore.
|
|
611
|
+
const isBaizeWritten = isTomlSectionValue(block)
|
|
612
|
+
&& block.name === key
|
|
613
|
+
&& block.wire_api === 'responses';
|
|
614
|
+
if (isBaizeWritten) delete obj.model_providers[key];
|
|
615
|
+
}
|
|
616
|
+
if (Object.keys(obj.model_providers).length === 0) delete obj.model_providers;
|
|
617
|
+
}
|
|
618
|
+
delete obj.openai_base_url;
|
|
619
|
+
obj.model = 'gpt-5.5';
|
|
620
|
+
});
|
|
621
|
+
const projectPath = rewriteCodexToml(projectCodexConfigPathFor(opts.projectDir), (obj) => {
|
|
622
|
+
delete obj.openai_base_url;
|
|
623
|
+
obj.model = 'gpt-5.5';
|
|
624
|
+
});
|
|
625
|
+
return { globalPath, projectPath };
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
|
|
488
629
|
/**
|
|
489
630
|
* Persist an OpenAI API key to ~/.codex/auth.json (Codex's native credential store).
|
|
490
631
|
* Also sets OPENAI_API_KEY in process.env for the current init process so that
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@baize-ai/core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Baize (白泽) — autonomous AI agent infrastructure",
|
|
6
6
|
"main": "cli/baize.js",
|
|
@@ -46,4 +46,4 @@
|
|
|
46
46
|
"publishConfig": {
|
|
47
47
|
"access": "public"
|
|
48
48
|
}
|
|
49
|
-
}
|
|
49
|
+
}
|
|
@@ -35,10 +35,6 @@ docker build \
|
|
|
35
35
|
-t "${GHCR_IMAGE}:${VERSION}" \
|
|
36
36
|
-t "${GHCR_IMAGE}:latest" \
|
|
37
37
|
.
|
|
38
|
-
"${BUILD_ARGS[@]}" \
|
|
39
|
-
-t "${GHCR_IMAGE}:${VERSION}" \
|
|
40
|
-
-t "${GHCR_IMAGE}:latest" \
|
|
41
|
-
.
|
|
42
38
|
|
|
43
39
|
if [ -n "${ACR_REGISTRY}" ] && [ -n "${ACR_NAMESPACE}" ]; then
|
|
44
40
|
ACR_IMAGE="${ACR_REGISTRY}/${ACR_NAMESPACE}/baize-core"
|