@baize-ai/core 0.3.14 → 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 +26 -0
- package/Dockerfile +10 -1
- package/cli/commands/add.js +3 -0
- package/cli/commands/component.js +289 -40
- package/cli/commands/init.js +91 -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/cli/lib/upgrade.js +55 -15
- package/docker/entrypoint.sh +0 -20
- package/docker-compose.yml +20 -2
- 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 +44 -82
- package/skills/web-console/public/index.html +1 -1
- package/skills/web-console/scripts/a2a-admin.js +225 -26
- package/skills/web-console/scripts/model-provider.js +97 -59
- package/skills/web-console/scripts/server.js +22 -32
- package/templates/pm2/ecosystem.config.cjs +6 -0
- package/test/channel-admin.test.js +7 -5
- package/test/helpers/run-upgrade-file-driver.mjs +15 -0
- package/test/model-provider.test.js +150 -40
- package/test/upgrade-file.test.js +229 -0
- package/test/upgrade-local-version.test.js +70 -0
- package/test/upgrade-restart-hook.test.js +129 -0
- package/test/web-console-routes.test.js +94 -17
|
@@ -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 }
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
169
|
-
//
|
|
170
|
-
|
|
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[],
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
@@ -71,8 +71,9 @@ import {
|
|
|
71
71
|
cancelA2aTask,
|
|
72
72
|
getA2aTaskRuns,
|
|
73
73
|
getA2aMessages,
|
|
74
|
-
|
|
74
|
+
installOrUpgradeA2aTarball,
|
|
75
75
|
probeA2aDaemonHealthy,
|
|
76
|
+
getA2aBootstrapState,
|
|
76
77
|
resolveA2aMode,
|
|
77
78
|
getSchedulerTasks,
|
|
78
79
|
} from './a2a-admin.js';
|
|
@@ -1266,6 +1267,7 @@ app.get('/api/admin/a2a/status', async (req, res) => {
|
|
|
1266
1267
|
const payload = { ...(result.json || {}) };
|
|
1267
1268
|
payload.mode = resolveA2aMode(payload.mode);
|
|
1268
1269
|
payload.daemonHealthy = await probeA2aDaemonHealthy();
|
|
1270
|
+
payload.bootstrap = getA2aBootstrapState(); // D49: init bootstrap result (launched/ok/failed)
|
|
1269
1271
|
if (result.success) {
|
|
1270
1272
|
res.json({ success: true, ...payload });
|
|
1271
1273
|
} else {
|
|
@@ -1385,11 +1387,14 @@ app.get('/api/admin/a2a/messages', (req, res) => {
|
|
|
1385
1387
|
}
|
|
1386
1388
|
});
|
|
1387
1389
|
|
|
1388
|
-
// Install @baize-ai/baize-a2a from an uploaded .tar.gz (D25
|
|
1389
|
-
// multipart field `file`; ≤100MB (413 over); entries vetted
|
|
1390
|
-
// absolute paths / links before extraction
|
|
1391
|
-
//
|
|
1392
|
-
// components.json
|
|
1390
|
+
// Install/upgrade @baize-ai/baize-a2a from an uploaded .tar.gz (D25 私有交付,
|
|
1391
|
+
// D49 K3 双语义). multipart field `file`; ≤100MB (413 over); entries vetted
|
|
1392
|
+
// against `..` / absolute paths / links before extraction. Dispatch (in
|
|
1393
|
+
// a2a-admin.js): 未安装 = 安装 (extract to SKILLS_DIR/a2a → npm install →
|
|
1394
|
+
// components.json); 已安装 = 升级 (version gate new>local else 409 → backup →
|
|
1395
|
+
// extract → npm install+rebuild → components.json bump → a2a cli restart).
|
|
1396
|
+
// Response: {ok:true, version, upgraded?} | {ok:false, error} | 409
|
|
1397
|
+
// {ok:false, error:'version_not_higher', local, incoming}.
|
|
1393
1398
|
app.post('/api/admin/a2a/install', (req, res) => {
|
|
1394
1399
|
a2aInstallUpload.single('file')(req, res, async (err) => {
|
|
1395
1400
|
if (err) {
|
|
@@ -1403,10 +1408,13 @@ app.post('/api/admin/a2a/install', (req, res) => {
|
|
|
1403
1408
|
return res.status(400).json({ ok: false, error: '缺少 file 字段(multipart 字段名必须为 file)' });
|
|
1404
1409
|
}
|
|
1405
1410
|
try {
|
|
1406
|
-
const result = await
|
|
1411
|
+
const result = await installOrUpgradeA2aTarball(file.path, { skillsDir: SKILLS_DIR, originalName: file.originalname });
|
|
1412
|
+
if (result.error === 'version_not_higher') {
|
|
1413
|
+
return res.status(409).json(result);
|
|
1414
|
+
}
|
|
1407
1415
|
res.status(result.ok ? 200 : 400).json(result);
|
|
1408
1416
|
} catch (e) {
|
|
1409
|
-
res.status(500).json({ ok: false, error:
|
|
1417
|
+
res.status(500).json({ ok: false, error: `安装/升级失败: ${e.message}` });
|
|
1410
1418
|
} finally {
|
|
1411
1419
|
fs.promises.rm(file.path, { force: true }).catch(() => {});
|
|
1412
1420
|
}
|
|
@@ -1550,23 +1558,6 @@ function readAuthz() {
|
|
|
1550
1558
|
}
|
|
1551
1559
|
}
|
|
1552
1560
|
|
|
1553
|
-
// Validates an allow/block list; returns { list } or { error }.
|
|
1554
|
-
function validateAuthzList(value, name) {
|
|
1555
|
-
if (!Array.isArray(value)) return { error: `${name} 必须是数组` };
|
|
1556
|
-
const seen = new Set();
|
|
1557
|
-
const list = [];
|
|
1558
|
-
for (const item of value) {
|
|
1559
|
-
if (typeof item !== 'string' || item.trim() === '') {
|
|
1560
|
-
return { error: `${name} 包含空项(必须是非空字符串)` };
|
|
1561
|
-
}
|
|
1562
|
-
const v = item.trim();
|
|
1563
|
-
if (seen.has(v)) return { error: `${name} 包含重复的 agent_id:${v}` };
|
|
1564
|
-
seen.add(v);
|
|
1565
|
-
list.push(v);
|
|
1566
|
-
}
|
|
1567
|
-
return { list };
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
1561
|
// Current authz policy (config.json authz; contract defaults when unconfigured)
|
|
1571
1562
|
app.get('/api/admin/a2a/authz', (req, res) => {
|
|
1572
1563
|
try {
|
|
@@ -1576,8 +1567,11 @@ app.get('/api/admin/a2a/authz', (req, res) => {
|
|
|
1576
1567
|
}
|
|
1577
1568
|
});
|
|
1578
1569
|
|
|
1579
|
-
// Save the authz policy —
|
|
1580
|
-
//
|
|
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.
|
|
1581
1575
|
app.put('/api/admin/a2a/authz', (req, res) => {
|
|
1582
1576
|
try {
|
|
1583
1577
|
const body = req.body || {};
|
|
@@ -1585,11 +1579,7 @@ app.put('/api/admin/a2a/authz', (req, res) => {
|
|
|
1585
1579
|
if (mode !== 'open' && mode !== 'allowlist') {
|
|
1586
1580
|
return res.status(400).json({ success: false, error: 'mode 必须是 open 或 allowlist' });
|
|
1587
1581
|
}
|
|
1588
|
-
const
|
|
1589
|
-
if (allow.error) return res.status(400).json({ success: false, error: allow.error });
|
|
1590
|
-
const block = body.block === undefined ? { list: [] } : validateAuthzList(body.block, 'block');
|
|
1591
|
-
if (block.error) return res.status(400).json({ success: false, error: block.error });
|
|
1592
|
-
const authz = { mode, allow: allow.list, block: block.list };
|
|
1582
|
+
const authz = { mode, allow: [], block: [] };
|
|
1593
1583
|
const file = a2aConfigPath();
|
|
1594
1584
|
let cfg = {};
|
|
1595
1585
|
try {
|
|
@@ -127,6 +127,12 @@ function loadComponentServices() {
|
|
|
127
127
|
// Skip components that haven't finished setup (AI-mode install in progress)
|
|
128
128
|
if (meta && meta.setupComplete === false) continue;
|
|
129
129
|
|
|
130
|
+
// D49: the baize-a2a daemon is owned by its own lifecycle manager
|
|
131
|
+
// (daemon-ctl: pm2 + readiness probe + nohup fallback). Registering it
|
|
132
|
+
// here too created a second start path that raced the dedicated one
|
|
133
|
+
// for port 8443 (double-instance) — a2a is intentionally excluded.
|
|
134
|
+
if (name === 'a2a' || (meta && meta.npmPkg === '@baize-ai/baize-a2a')) continue;
|
|
135
|
+
|
|
130
136
|
const skillDir = (meta && meta.skillDir) || path.join(SKILLS_DIR, name);
|
|
131
137
|
|
|
132
138
|
// Try loading the component's own ecosystem.config.cjs
|
|
@@ -237,14 +237,16 @@ describe('installChannel / uninstallChannel', () => {
|
|
|
237
237
|
describe('a2a builtin channel (D19 单元 C)', () => {
|
|
238
238
|
const a2aSchemaKeys = ['enabled', 'adminUrl', 'agentId', 'advertiseUrl', 'listenPort', 'certKeyPath', 'certCertPath'];
|
|
239
239
|
|
|
240
|
-
test('
|
|
240
|
+
test('a2a keeps the contract config schema but is hidden from the catalogue (D33)', async () => {
|
|
241
241
|
const channels = await ca.discoverChannels({ fetch: noResultsFetch });
|
|
242
|
-
|
|
243
|
-
expect(a2a).
|
|
242
|
+
// Commercial project: a2a must not appear in any channel/component listing.
|
|
243
|
+
expect(channels.find((c) => c.name === 'a2a')).toBeUndefined();
|
|
244
|
+
// The builtin definition (used by configure/status) keeps the D19 schema.
|
|
245
|
+
const a2a = ca.BUILTIN_CHANNELS.a2a;
|
|
244
246
|
expect(a2a).toMatchObject({
|
|
247
|
+
name: 'a2a',
|
|
245
248
|
npmPkg: '@baize-ai/baize-a2a',
|
|
246
249
|
repo: 'baize-ai/baize-a2a',
|
|
247
|
-
installed: false,
|
|
248
250
|
});
|
|
249
251
|
expect(a2a.configSchema.map((f) => f.key)).toEqual(a2aSchemaKeys);
|
|
250
252
|
expect(a2a.configSchema.every((f) => f.target === 'config')).toBe(true);
|
|
@@ -286,7 +288,7 @@ describe('a2a builtin channel (D19 单元 C)', () => {
|
|
|
286
288
|
});
|
|
287
289
|
|
|
288
290
|
test('channelStatus for installed a2a: configured only when enabled + both urls', async () => {
|
|
289
|
-
const entry =
|
|
291
|
+
const entry = ca.BUILTIN_CHANNELS.a2a;
|
|
290
292
|
fs.writeFileSync(path.join(baizeDir, '.baize', 'components.json'), JSON.stringify({
|
|
291
293
|
a2a: { version: '0.1.0', source: { type: 'npm' } },
|
|
292
294
|
}));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Child-process driver for the K4 --file upgrade e2e tests: runs the REAL
|
|
3
|
+
* upgradeComponent() flow in a clean process so BAIZE_DIR resolves the fixture
|
|
4
|
+
* root (cli/lib/config.js reads env at import time) — Jest's sandboxed
|
|
5
|
+
* process.env is not inherited by grandchildren, so the flow must run outside
|
|
6
|
+
* the Jest process.
|
|
7
|
+
*
|
|
8
|
+
* argv: <component> <tgzPath> [--check]
|
|
9
|
+
* stdout: the command output (JSON — --json is always passed)
|
|
10
|
+
*/
|
|
11
|
+
const [component, tgzPath, checkFlag] = process.argv.slice(2);
|
|
12
|
+
const { upgradeComponent } = await import('../../cli/commands/component.js');
|
|
13
|
+
const args = [component, '--file', tgzPath, '--yes', '--json'];
|
|
14
|
+
if (checkFlag === '--check') args.push('--check');
|
|
15
|
+
await upgradeComponent(args);
|