@baize-ai/core 0.3.15 → 0.3.17

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.
Files changed (34) hide show
  1. package/.dockerignore +1 -0
  2. package/CHANGELOG.md +29 -0
  3. package/Dockerfile +10 -1
  4. package/cli/commands/doctor.js +60 -0
  5. package/cli/commands/init.js +104 -42
  6. package/cli/commands/runtime.js +9 -1
  7. package/cli/lib/__tests__/init-base-url.test.js +55 -13
  8. package/cli/lib/__tests__/runtime-base-url.test.js +3 -1
  9. package/cli/lib/__tests__/runtime-launch.test.js +4 -2
  10. package/cli/lib/__tests__/runtime-setup.test.js +53 -6
  11. package/cli/lib/__tests__/tmux-env.test.js +19 -5
  12. package/cli/lib/claude-eval.js +2 -1
  13. package/cli/lib/codex-hooks.js +12 -0
  14. package/cli/lib/path-bins.js +54 -0
  15. package/cli/lib/runtime/claude.js +4 -2
  16. package/cli/lib/runtime/codex.js +19 -0
  17. package/cli/lib/runtime/tmux-env.js +5 -4
  18. package/cli/lib/runtime-setup.js +157 -13
  19. package/docker/entrypoint.sh +4 -2
  20. package/docs/ops-runbook.md +161 -0
  21. package/package.json +2 -2
  22. package/scripts/docker-publish.sh +11 -5
  23. package/scripts/pack-release.sh +34 -26
  24. package/skills/activity-monitor/scripts/__tests__/guardian.test.js +51 -0
  25. package/skills/activity-monitor/scripts/adapters/runtime-components.js +27 -0
  26. package/skills/activity-monitor/scripts/guardian.js +45 -1
  27. package/skills/activity-monitor/scripts/monitor-orchestrator.js +8 -1
  28. package/skills/activity-monitor/scripts/upgrade-check.js +3 -1
  29. package/skills/web-console/public/app.js +20 -75
  30. package/skills/web-console/scripts/model-provider.js +97 -59
  31. package/skills/web-console/scripts/server.js +6 -24
  32. package/templates/pm2/ecosystem.config.cjs +5 -4
  33. package/test/model-provider.test.js +150 -40
  34. package/test/web-console-routes.test.js +11 -13
@@ -1170,7 +1170,18 @@ async function loadProviders(basePath) {
1170
1170
  const names = { official: 'Anthropic 官方', 'official-openai': 'OpenAI 官方' };
1171
1171
  const prov = (body.providers || []).find((x) => x.id === body.active);
1172
1172
  const label = prov?.name || names[body.active] || body.active || 'official';
1173
- activeEl.textContent = `当前激活:${label}`;
1173
+ const codexSlug = body.codexProviderKey || prov?.codex?.providerKey || null;
1174
+ const codexKind = prov?.codex?.kind || null;
1175
+ // D51 review P3: surface the kind so openai-official (no block) vs
1176
+ // responses-compatible (provider block) are distinguishable at a glance.
1177
+ const codexTag = codexKind === 'responses-compatible' && codexSlug
1178
+ ? `(codex: ${codexSlug} · responses)`
1179
+ : codexKind === 'openai-official'
1180
+ ? '(codex: OpenAI 官方)'
1181
+ : codexSlug
1182
+ ? `(codex: ${codexSlug})`
1183
+ : '';
1184
+ activeEl.textContent = `当前激活:${label}${codexTag}`;
1174
1185
  }
1175
1186
  const list = document.getElementById('provider-list');
1176
1187
  if (!list) return;
@@ -1249,7 +1260,8 @@ async function activateProviderFromAdmin(basePath, id) {
1249
1260
  const restartNote = body.restart?.restarted ? ',会话已重启' : (body.restart?.reason === 'no_session' ? '(无运行中会话)' : '');
1250
1261
  const warnNote = body.warnings?.length ? `
1251
1262
  警告:${body.warnings.join('\n')}` : '';
1252
- setAdminMsg(`已激活 ${body.active}${restartNote}${warnNote}`);
1263
+ const slugNote = body.codexProviderKey ? `(codex: ${body.codexProviderKey})` : '';
1264
+ setAdminMsg(`已激活 ${body.active}${slugNote}${restartNote}${warnNote}`);
1253
1265
  } else {
1254
1266
  setAdminMsg(`激活失败:${body.error || '未知错误'}`, true);
1255
1267
  }
@@ -1863,13 +1875,11 @@ async function openAgentCardEditor(basePath) {
1863
1875
  let catalogue;
1864
1876
  let card;
1865
1877
  let authz;
1866
- let peerIds;
1867
1878
  try {
1868
- const [catRes, cardRes, authzRes, peersRes] = await Promise.all([
1879
+ const [catRes, cardRes, authzRes] = await Promise.all([
1869
1880
  adminFetch(basePath, '/api/admin/a2a/card/skills'),
1870
1881
  adminFetch(basePath, '/api/admin/a2a/card'),
1871
1882
  adminFetch(basePath, '/api/admin/a2a/authz'),
1872
- adminFetch(basePath, '/api/admin/a2a/peers'),
1873
1883
  ]);
1874
1884
  if (catRes.status !== 200) throw new Error(catRes.body.error || 'skills 列表加载失败');
1875
1885
  if (cardRes.status !== 200) throw new Error(cardRes.body.error || '名片加载失败');
@@ -1877,9 +1887,6 @@ async function openAgentCardEditor(basePath) {
1877
1887
  catalogue = catRes.body.skills || [];
1878
1888
  card = cardRes.body;
1879
1889
  authz = authzRes.body;
1880
- peerIds = peersRes.status === 200 && Array.isArray(peersRes.body.peers)
1881
- ? peersRes.body.peers.map((p) => p.agentId).filter(Boolean)
1882
- : [];
1883
1890
  } catch (err) {
1884
1891
  setAdminMsg(`❌ 无法打开名片编辑器:${err.message}`, true);
1885
1892
  return;
@@ -1917,14 +1924,7 @@ async function openAgentCardEditor(basePath) {
1917
1924
  <label class="authz-mode-option"><input type="radio" name="authz-mode" value="allowlist"> 白名单</label>
1918
1925
  </div>
1919
1926
  <p class="conn-block-desc" id="authz-mode-desc"></p>
1920
- <div class="agent-card-field-label"><span id="authz-list-title">阻止列表</span> <span class="agent-card-selected-count" id="authz-count"></span></div>
1921
- <div class="authz-tags" id="authz-tags" aria-live="polite"></div>
1922
- <div class="authz-add-row">
1923
- <input type="text" id="authz-peer-input" class="agent-card-input" placeholder="输入 peer agent_id 后添加" autocomplete="off" spellcheck="false">
1924
- <button type="button" class="btn btn-outline" id="authz-peer-add">添加</button>
1925
- </div>
1926
- <div class="agent-card-field-label">从 peer 列表选择</div>
1927
- <select id="authz-peer-select" class="agent-card-input" aria-label="从 peer 列表选择"></select>
1927
+ <p class="conn-block-desc" style="color:#8c8c8c">允许 / 阻止名单由 admin 控制台统一管控(组织架构 · 调用白名单),此处仅切换本 agent 的授权模式。</p>
1928
1928
  </div>
1929
1929
  </div>
1930
1930
  <div class="agent-card-modal-foot">
@@ -1943,18 +1943,10 @@ async function openAgentCardEditor(basePath) {
1943
1943
  nameInput.value = card.name || '';
1944
1944
  descInput.value = card.description || '';
1945
1945
 
1946
- // ── 调用授权 tab state (D22 单元 C) ──
1947
- const allowSet = new Set(Array.isArray(authz.allow) ? authz.allow : []);
1948
- const blockSet = new Set(Array.isArray(authz.block) ? authz.block : []);
1946
+ // ── 调用授权 tab state (D52: local mode only; lists are admin-managed) ──
1949
1947
  let authzMode = authz.mode === 'allowlist' ? 'allowlist' : 'open';
1950
- const activeAuthzList = () => (authzMode === 'allowlist' ? allowSet : blockSet);
1951
1948
 
1952
1949
  const authzModeDesc = overlay.querySelector('#authz-mode-desc');
1953
- const authzListTitle = overlay.querySelector('#authz-list-title');
1954
- const authzCount = overlay.querySelector('#authz-count');
1955
- const authzTagsEl = overlay.querySelector('#authz-tags');
1956
- const authzPeerInput = overlay.querySelector('#authz-peer-input');
1957
- const authzPeerSelect = overlay.querySelector('#authz-peer-select');
1958
1950
 
1959
1951
  const renderAuthz = () => {
1960
1952
  overlay.querySelectorAll('input[name="authz-mode"]').forEach((r) => {
@@ -1962,19 +1954,8 @@ async function openAgentCardEditor(basePath) {
1962
1954
  });
1963
1955
  const isAllowlist = authzMode === 'allowlist';
1964
1956
  authzModeDesc.textContent = isAllowlist
1965
- ? '仅允许列表中的 agent 可以调用本 agent(空列表 = 无人可调用)。'
1966
- : '接受所有已批准 agent,除阻止列表中的 agent。';
1967
- authzListTitle.textContent = isAllowlist ? '允许列表' : '阻止列表';
1968
- const list = [...activeAuthzList()];
1969
- authzCount.textContent = `${list.length} 个`;
1970
- authzTagsEl.innerHTML = list.length
1971
- ? list.map((id) => `<span class="authz-tag">${escapeHtml(id)}<button type="button" class="authz-tag-x" aria-label="移除 ${escapeHtml(id)}" data-id="${escapeHtml(id)}">×</button></span>`).join('')
1972
- : '<span class="cred-state">(空)</span>';
1973
- const current = new Set(activeAuthzList());
1974
- const available = peerIds.filter((id) => !current.has(id));
1975
- authzPeerSelect.innerHTML = `<option value="">${available.length ? '选择 peer 添加到列表…' : '列表已包含所有已知 peer'}</option>`
1976
- + available.map((id) => `<option value="${escapeHtml(id)}">${escapeHtml(id)}</option>`).join('');
1977
- authzPeerSelect.disabled = available.length === 0;
1957
+ ? '仅允许白名单中的 agent 调用本 agent(白名单由 admin 控制台配置;空名单 = 无人可调用)。'
1958
+ : '接受所有已批准 agent(除 admin 控制台配置的阻止名单)。';
1978
1959
  };
1979
1960
 
1980
1961
  overlay.querySelectorAll('input[name="authz-mode"]').forEach((r) => {
@@ -1986,42 +1967,6 @@ async function openAgentCardEditor(basePath) {
1986
1967
  });
1987
1968
  });
1988
1969
 
1989
- const addAuthzPeer = () => {
1990
- const value = authzPeerInput.value.trim();
1991
- if (!value) {
1992
- setAdminMsg('请输入 peer agent_id', true);
1993
- return;
1994
- }
1995
- const list = activeAuthzList();
1996
- if (list.has(value)) {
1997
- setAdminMsg(`⚠️ ${value} 已在${authzMode === 'allowlist' ? '允许' : '阻止'}列表中`, true);
1998
- return;
1999
- }
2000
- list.add(value);
2001
- authzPeerInput.value = '';
2002
- renderAuthz();
2003
- };
2004
- authzPeerInput.addEventListener('keydown', (e) => {
2005
- if (e.key === 'Enter') {
2006
- e.preventDefault();
2007
- addAuthzPeer();
2008
- }
2009
- });
2010
- overlay.querySelector('#authz-peer-add').addEventListener('click', addAuthzPeer);
2011
- authzPeerSelect.addEventListener('change', () => {
2012
- const value = authzPeerSelect.value;
2013
- if (!value) return;
2014
- activeAuthzList().add(value);
2015
- renderAuthz();
2016
- });
2017
- authzTagsEl.addEventListener('click', (e) => {
2018
- const btn = e.target.closest('.authz-tag-x');
2019
- if (btn) {
2020
- activeAuthzList().delete(btn.dataset.id);
2021
- renderAuthz();
2022
- }
2023
- });
2024
-
2025
1970
  // ── Tab switching ──
2026
1971
  const tabButtons = overlay.querySelectorAll('.agent-card-tab');
2027
1972
  const cardTabEl = overlay.querySelector('#agent-card-tab-card');
@@ -2123,7 +2068,7 @@ async function openAgentCardEditor(basePath) {
2123
2068
  adminFetch(basePath, '/api/admin/a2a/authz', {
2124
2069
  method: 'PUT',
2125
2070
  headers: { 'Content-Type': 'application/json' },
2126
- body: JSON.stringify({ mode: authzMode, allow: [...allowSet], block: [...blockSet] }),
2071
+ body: JSON.stringify({ mode: authzMode }),
2127
2072
  }),
2128
2073
  ]);
2129
2074
  if (cardRes.status === 401 || authzRes.status === 401) {
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * A provider entry carries per-runtime endpoints/models:
6
6
  * cc: { baseUrl, authToken, model, smallModel } → ANTHROPIC_* env
7
- * codex: { baseUrl, apiKey, model } OPENAI_BASE_URL + auth.json + config.toml
7
+ * codex: { kind, providerKey, baseUrl, apiKey, model } dedicated provider block in config.toml
8
8
  *
9
9
  * Activation writes the same storage surfaces init uses (~/baize/.env +
10
10
  * ~/.claude/settings.json env + ~/.codex/auth.json + ~/.codex/config.toml)
@@ -31,6 +31,13 @@ import {
31
31
  restartAgentSession,
32
32
  } from './admin-auth.js';
33
33
 
34
+ // D51: Codex provider-block writers shared with the CLI (same package tree).
35
+ import {
36
+ applyCodexProviderBlock,
37
+ codexProviderKeyForId,
38
+ restoreCodexOfficialBlock,
39
+ } from '../../../cli/lib/runtime-setup.js';
40
+
34
41
  // ── Config store (~/.baize/providers.json) ───────────────────────────────────
35
42
 
36
43
  function providersPath() {
@@ -53,13 +60,49 @@ function projectCodexConfigPath() {
53
60
  return path.join(process.env.BAIZE_DIR || path.join(process.env.HOME || '', 'baize'), '.codex', 'config.toml');
54
61
  }
55
62
 
63
+ // Codex-side kind enum (D51 — DeepSeek remote-compaction fix):
64
+ // openai-official use the built-in OpenAI provider (no model_provider block)
65
+ // responses-compatible generic /responses endpoint → dedicated [model_providers.<slug>] block
66
+ // chat-only legacy /chat wire only — Codex removed the Chat wire
67
+ const CODEX_KINDS = new Set(['openai-official', 'responses-compatible', 'chat-only']);
68
+
69
+ // Home/project roots, resolved from the same env vars every other path in
70
+ // this module uses. Passed explicitly to the runtime-setup writers because
71
+ // their os.homedir()/BAIZE_DIR fallbacks don't follow test env overrides
72
+ // (jest sandboxes os.homedir()).
73
+ function homeRoot() {
74
+ return process.env.HOME || '';
75
+ }
76
+ function baizeRoot() {
77
+ return process.env.BAIZE_DIR || path.join(process.env.HOME || '', 'baize');
78
+ }
79
+
56
80
  function loadStore() {
57
81
  const data = readJson(providersPath()) || {};
58
- return {
82
+ const store = {
59
83
  active: typeof data.active === 'string' ? data.active : 'official',
60
84
  providers: Array.isArray(data.providers) ? data.providers : [],
61
85
  stash: data.stash && typeof data.stash === 'object' ? data.stash : null,
62
86
  };
87
+ // Lazy D51 migration: entries saved before kind/providerKey existed get
88
+ // backfilled from the entry shape (baseUrl → responses-compatible) and the
89
+ // store is re-persisted so slugs stay stable from the first read onward.
90
+ let migrated = false;
91
+ for (const p of store.providers) {
92
+ const codex = p.codex && typeof p.codex === 'object' ? p.codex : {};
93
+ if (!codex.kind) {
94
+ codex.kind = codex.baseUrl ? 'responses-compatible' : 'openai-official';
95
+ p.codex = codex;
96
+ migrated = true;
97
+ }
98
+ if (!codex.providerKey && p.id) {
99
+ codex.providerKey = codexProviderKeyForId(p.id);
100
+ p.codex = codex;
101
+ migrated = true;
102
+ }
103
+ }
104
+ if (migrated) saveStore(store);
105
+ return store;
63
106
  }
64
107
 
65
108
  function saveStore(store) {
@@ -91,6 +134,7 @@ function validProviderInput(data) {
91
134
  const urlRe = /^https?:\/\/.+/;
92
135
  if (cc.baseUrl && !urlRe.test(String(cc.baseUrl))) return 'cc.baseUrl must start with http(s)://';
93
136
  if (codex.baseUrl && !urlRe.test(String(codex.baseUrl))) return 'codex.baseUrl must start with http(s)://';
137
+ if (codex.kind && !CODEX_KINDS.has(String(codex.kind))) return `codex.kind must be one of: ${[...CODEX_KINDS].join(', ')}`;
94
138
  return null;
95
139
  }
96
140
 
@@ -106,6 +150,8 @@ function sanitizeProvider(p) {
106
150
  authConfigured: Boolean(p.cc?.authToken),
107
151
  },
108
152
  codex: {
153
+ kind: p.codex?.kind || null,
154
+ providerKey: p.codex?.providerKey || null,
109
155
  baseUrl: p.codex?.baseUrl || null,
110
156
  model: p.codex?.model || null,
111
157
  apiConfigured: Boolean(p.codex?.apiKey),
@@ -131,8 +177,15 @@ export async function saveProvider(input) {
131
177
  const cc = input.cc || {};
132
178
  const codex = input.codex || {};
133
179
  const exists = input.id && store.providers.some((p) => p.id === input.id);
180
+ // D51: keep the pre-existing kind/providerKey on updates (slug is generated
181
+ // once and never regenerated — a rename must not orphan the TOML block).
182
+ const prevCodex = exists ? store.providers.find((p) => p.id === input.id).codex || {} : {};
183
+ const kind = codex.kind
184
+ ? String(codex.kind)
185
+ : (prevCodex.kind || (codex.baseUrl ? 'responses-compatible' : 'openai-official'));
186
+ const id = exists ? input.id : genId(name, store);
134
187
  const entry = {
135
- id: exists ? input.id : genId(name, store),
188
+ id,
136
189
  name,
137
190
  cc: {
138
191
  baseUrl: cc.baseUrl ? String(cc.baseUrl).trim() : undefined,
@@ -141,6 +194,8 @@ export async function saveProvider(input) {
141
194
  smallModel: cc.smallModel ? String(cc.smallModel).trim() : undefined,
142
195
  },
143
196
  codex: {
197
+ kind,
198
+ providerKey: prevCodex.providerKey || codexProviderKeyForId(id),
144
199
  baseUrl: codex.baseUrl ? String(codex.baseUrl).trim() : undefined,
145
200
  apiKey: codex.apiKey ? String(codex.apiKey).trim() : undefined,
146
201
  model: codex.model ? String(codex.model).trim() : undefined,
@@ -165,9 +220,12 @@ function readRuntime() {
165
220
 
166
221
  function genId(name, store) {
167
222
  const base = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'provider';
168
- // 'official' / 'official-openai' are built-in restore targets — reserve them
169
- // so a user-named provider can never shadow or be shadowed by them.
170
- let id = (base === 'official' || base === 'official-openai') ? `${base}-custom` : base;
223
+ // Reserved ids never land in the store: the built-in restore targets
224
+ // ('official' / 'official-openai') plus Codex built-in provider keys that a
225
+ // user-named provider must not shadow (D51 'openai' would make the
226
+ // generated provider block collide with the built-in OpenAI entry).
227
+ const reserved = new Set(['official', 'official-openai', 'openai', 'azure', 'amazon-bedrock', 'amazon-bedrock-runtime', 'ollama', 'lmstudio', 'gpt-oss']);
228
+ let id = reserved.has(base) ? `${base}-custom` : base;
171
229
  let n = 2;
172
230
  while (store.providers.some((p) => p.id === id)) id = `${base}-${n++}`;
173
231
  return id;
@@ -189,7 +247,8 @@ const CC_EXTERNAL_KEYS = ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL', 'ANTHROP
189
247
 
190
248
  /**
191
249
  * Activate a provider or built-in official restore target.
192
- * @returns {Promise<{success: boolean, active: string, warnings: string[], restart?: object, error?: string}>}
250
+ * @returns {Promise<{success: boolean, active: string, warnings: string[],
251
+ * restart?: object, codexProviderKey?: string|null, error?: string}>}
193
252
  */
194
253
  export async function activateProvider(id, deps = {}) {
195
254
  const store = loadStore();
@@ -206,6 +265,14 @@ export async function activateProvider(id, deps = {}) {
206
265
  // current runtime's side is meaningless (e.g. a cc-only provider while the
207
266
  // Codex runtime is active). Official restores and dual-side providers pass.
208
267
  if (provider) {
268
+ // D51: chat-only endpoints have no Codex wiring at all (the Chat wire was
269
+ // removed from Codex) — block activation before anything is written.
270
+ if (provider.codex?.kind === 'chat-only') {
271
+ return {
272
+ success: false,
273
+ error: '该供应商仅支持 Chat 接口,Codex 已移除 Chat wire,请改用 Claude 运行时',
274
+ };
275
+ }
209
276
  const runtime = readRuntime();
210
277
  const hasCc = Boolean(provider.cc?.authToken || provider.cc?.baseUrl || provider.cc?.model || provider.cc?.smallModel);
211
278
  const hasCodex = Boolean(provider.codex?.baseUrl || provider.codex?.apiKey || provider.codex?.model);
@@ -233,10 +300,11 @@ export async function activateProvider(id, deps = {}) {
233
300
  applyClaudeExternal(cc, store);
234
301
  }
235
302
 
303
+ let codexProviderKey = null;
236
304
  if (isOfficialOpenai) {
237
305
  applyCodexOfficial();
238
306
  } else if (provider?.codex) {
239
- applyCodexExternal(provider.codex);
307
+ codexProviderKey = applyCodexExternal(provider.codex) || null;
240
308
  }
241
309
 
242
310
  // Non-blocking reachability probe for configured base URLs (informational).
@@ -251,7 +319,7 @@ export async function activateProvider(id, deps = {}) {
251
319
  store.active = id;
252
320
  saveStore(store);
253
321
  const restart = await (deps.restart ?? restartAgentSession)();
254
- return { success: true, active: id, warnings, restart };
322
+ return { success: true, active: id, warnings, codexProviderKey, restart };
255
323
  }
256
324
 
257
325
  // Claude Code: external provider application
@@ -308,67 +376,37 @@ function applyOfficialClaude(store) {
308
376
  updateSettingsEnv(updates, CC_EXTERNAL_KEYS);
309
377
  }
310
378
 
311
- // Codex: external provider application
379
+ // Codex: external provider application (D51 — dedicated provider block).
380
+ // Returns the active codex provider slug, or null when nothing was written.
312
381
  function applyCodexExternal(codex) {
313
382
  let env = readEnv(envFile());
383
+ // Legacy override kept for health-probe/status consumers that still read it.
314
384
  if (codex.baseUrl) env = upsertEnv(env, 'OPENAI_BASE_URL', codex.baseUrl, 'OpenAI-compatible endpoint (web console)');
315
385
  writeEnvFile(env);
316
386
  if (codex.apiKey) writeCodexAuth(codex.apiKey);
317
- editCodexConfigToml({ model: codex.model, openaiBaseUrl: codex.baseUrl });
387
+ if (codex.kind === 'openai-official') {
388
+ // Official endpoint: keep the built-in OpenAI provider (remote compaction
389
+ // works there) — just drop any leftover custom block/override.
390
+ restoreCodexOfficialBlock({ homeDir: homeRoot(), projectDir: baizeRoot() });
391
+ return null;
392
+ }
393
+ applyCodexProviderBlock({
394
+ providerKey: codex.providerKey || codexProviderKeyForId('provider'),
395
+ baseUrl: codex.baseUrl,
396
+ token: codex.apiKey,
397
+ model: codex.model,
398
+ homeDir: homeRoot(),
399
+ projectDir: baizeRoot(),
400
+ });
401
+ return codex.providerKey || null;
318
402
  }
319
403
 
320
- // Codex: restore official (clear external base_url, default model; auth untouched)
404
+ // Codex: restore official (drop provider block + overrides, reset model; auth untouched)
321
405
  function applyCodexOfficial() {
322
406
  let env = readEnv(envFile());
323
407
  env = removeEnvKey(env, 'OPENAI_BASE_URL');
324
408
  writeEnvFile(env);
325
- editCodexConfigToml({ model: 'gpt-5.5', openaiBaseUrl: null });
326
- }
327
-
328
- /**
329
- * Conservative top-level edit of a Codex config.toml: set/replace the
330
- * `model` and `openai_base_url` keys, preserving every other line (comments,
331
- * sections, providers). Only the TOP-LEVEL prefix (before the first section
332
- * header) is touched — keys inside [table] sections are also unindented, so
333
- * regex edits must never run past the first `[` header.
334
- */
335
- function writeCodexTomlTopLevel(targetPath, { model, openaiBaseUrl }) {
336
- const dir = path.dirname(targetPath);
337
- fs.mkdirSync(dir, { recursive: true });
338
- let content = '';
339
- try { content = fs.readFileSync(targetPath, 'utf8'); } catch { /* new file */ }
340
-
341
- const sectionAt = content.search(/^\[/m);
342
- const head = sectionAt === -1 ? content : content.slice(0, sectionAt);
343
- const tail = sectionAt === -1 ? '' : content.slice(sectionAt);
344
-
345
- const setTopLevel = (text, key, value) => {
346
- const lineRe = new RegExp(`^${key}\\s*=.*$`, 'm');
347
- const line = `${key} = ${JSON.stringify(value)}`;
348
- if (lineRe.test(text)) return text.replace(lineRe, line);
349
- return `${text.trimEnd()}\n${line}\n`;
350
- };
351
- const removeTopLevel = (text, key) => text.replace(new RegExp(`^${key}\\s*=.*\\n?`, 'm'), '');
352
-
353
- let out = head;
354
- if (model) out = setTopLevel(out, 'model', model);
355
- if (openaiBaseUrl) out = setTopLevel(out, 'openai_base_url', openaiBaseUrl);
356
- if (openaiBaseUrl === null) out = removeTopLevel(out, 'openai_base_url');
357
- const joined = tail && !out.endsWith('\n') ? `${out}\n${tail}` : `${out}${tail}`;
358
- fs.writeFileSync(targetPath, joined, 'utf8');
359
- return targetPath;
360
- }
361
-
362
- /**
363
- * Apply a provider's Codex model/base_url to BOTH the global config.toml and
364
- * the baize project-level config.toml. codex prefers project-level keys, so
365
- * writing only the global file let baize init's backfilled defaults (model =
366
- * "gpt-5.5") shadow the provider config (D31).
367
- */
368
- export function editCodexConfigToml({ model, openaiBaseUrl }) {
369
- writeCodexTomlTopLevel(codexConfigPath(), { model, openaiBaseUrl });
370
- writeCodexTomlTopLevel(projectCodexConfigPath(), { model, openaiBaseUrl });
371
- return codexConfigPath();
409
+ restoreCodexOfficialBlock({ homeDir: homeRoot(), projectDir: baizeRoot() });
372
410
  }
373
411
 
374
412
  // Reachability probe: any <500 status means the endpoint answers.
@@ -1558,23 +1558,6 @@ function readAuthz() {
1558
1558
  }
1559
1559
  }
1560
1560
 
1561
- // Validates an allow/block list; returns { list } or { error }.
1562
- function validateAuthzList(value, name) {
1563
- if (!Array.isArray(value)) return { error: `${name} 必须是数组` };
1564
- const seen = new Set();
1565
- const list = [];
1566
- for (const item of value) {
1567
- if (typeof item !== 'string' || item.trim() === '') {
1568
- return { error: `${name} 包含空项(必须是非空字符串)` };
1569
- }
1570
- const v = item.trim();
1571
- if (seen.has(v)) return { error: `${name} 包含重复的 agent_id:${v}` };
1572
- seen.add(v);
1573
- list.push(v);
1574
- }
1575
- return { list };
1576
- }
1577
-
1578
1561
  // Current authz policy (config.json authz; contract defaults when unconfigured)
1579
1562
  app.get('/api/admin/a2a/authz', (req, res) => {
1580
1563
  try {
@@ -1584,8 +1567,11 @@ app.get('/api/admin/a2a/authz', (req, res) => {
1584
1567
  }
1585
1568
  });
1586
1569
 
1587
- // Save the authz policy — validates, then writes config.json (preserving other
1588
- // fields) and returns the persisted policy
1570
+ // Save the authz policy — D52 governance: the LOCAL agent may only flip the
1571
+ // mode (open/allowlist); allow/block lists are admin-managed (admin-workspace
1572
+ // pushes the cluster policy; local lists here would be dead weight / bypass
1573
+ // confusion). Any allow/block sent is ignored and forced to [] — the UI hides
1574
+ // list editing, and a direct API call cannot reintroduce local lists.
1589
1575
  app.put('/api/admin/a2a/authz', (req, res) => {
1590
1576
  try {
1591
1577
  const body = req.body || {};
@@ -1593,11 +1579,7 @@ app.put('/api/admin/a2a/authz', (req, res) => {
1593
1579
  if (mode !== 'open' && mode !== 'allowlist') {
1594
1580
  return res.status(400).json({ success: false, error: 'mode 必须是 open 或 allowlist' });
1595
1581
  }
1596
- const allow = body.allow === undefined ? { list: [] } : validateAuthzList(body.allow, 'allow');
1597
- if (allow.error) return res.status(400).json({ success: false, error: allow.error });
1598
- const block = body.block === undefined ? { list: [] } : validateAuthzList(body.block, 'block');
1599
- if (block.error) return res.status(400).json({ success: false, error: block.error });
1600
- const authz = { mode, allow: allow.list, block: block.list };
1582
+ const authz = { mode, allow: [], block: [] };
1601
1583
  const file = a2aConfigPath();
1602
1584
  let cfg = {};
1603
1585
  try {
@@ -28,11 +28,12 @@ function readEnvValue(key, defaultValue = '') {
28
28
  return defaultValue;
29
29
  }
30
30
 
31
- // Build PATH: Claude locations + user's full shell PATH + PM2's own PATH
32
- // Deduplicate to prevent PATH bloat across PM2 restarts each restart
33
- // re-evaluates this file with process.env.PATH already containing the
34
- // previous ENHANCED_PATH, which would otherwise compound indefinitely.
31
+ // Build PATH: npm global bin + Claude locations + user's full shell PATH +
32
+ // PM2's own PATH. D54: ~/.npm-global/bin is added EXPLICITLY (independent of
33
+ // SYSTEM_PATH / process.env.PATH) codex/baize live there; a polluted login
34
+ // shell PATH must never hide them from PM2 services.
35
35
  const ENHANCED_PATH = [...new Set([
36
+ path.join(HOME, '.npm-global', 'bin'),
36
37
  path.join(HOME, '.local', 'bin'),
37
38
  path.join(HOME, '.claude', 'bin'),
38
39
  ...(readEnvValue('SYSTEM_PATH') || '').split(':').filter(Boolean),