@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
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* K5: step8 restart hook. A nohup-started baize-a2a daemon (Docker entrypoint)
|
|
3
|
+
* is invisible to pm2, so the pm2-only restart never refreshed its code (C6).
|
|
4
|
+
* For service name 'baize-a2a' step8 must prefer `node <a2a-cli> restart`
|
|
5
|
+
* (daemon-ctl covers nohup pid + pm2 + waitReady), falling back to the
|
|
6
|
+
* existing pm2 path when the CLI is missing or fails.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, test } from '@jest/globals';
|
|
12
|
+
import { step8_startService } from '../cli/lib/upgrade.js';
|
|
13
|
+
|
|
14
|
+
let tmp;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'restart-hook-'));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function makeCtx({ component = 'a2a', serviceWasRunning = true, skillMd = null } = {}) {
|
|
25
|
+
const skillDir = path.join(tmp, component);
|
|
26
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
27
|
+
if (skillMd) fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillMd);
|
|
28
|
+
return {
|
|
29
|
+
component,
|
|
30
|
+
skillDir,
|
|
31
|
+
serviceWasRunning,
|
|
32
|
+
steps: [],
|
|
33
|
+
mergeConflicts: [],
|
|
34
|
+
mergedFiles: [],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const throwingPm2 = {
|
|
39
|
+
restartManagedProcess: () => { throw new Error('pm2 restart must not be called'); },
|
|
40
|
+
restartFromEcosystem: () => { throw new Error('pm2 ecosystem restart must not be called'); },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe('step8 a2a cli restart hook (K5)', () => {
|
|
44
|
+
test('service name baize-a2a prefers the a2a cli restart and skips pm2', () => {
|
|
45
|
+
const ctx = makeCtx();
|
|
46
|
+
const calls = [];
|
|
47
|
+
const spawnSyncFn = (cmd, args, opts) => {
|
|
48
|
+
calls.push({ cmd, args, opts });
|
|
49
|
+
return { status: 0, stdout: '{"ok":true}' };
|
|
50
|
+
};
|
|
51
|
+
const step = step8_startService(ctx, {
|
|
52
|
+
...throwingPm2,
|
|
53
|
+
spawnSyncFn,
|
|
54
|
+
resolveA2aCli: () => '/fake/a2a/scripts/cli.js',
|
|
55
|
+
});
|
|
56
|
+
expect(step.status).toBe('done');
|
|
57
|
+
expect(step.message).toBe('baize-a2a (a2a cli restart)');
|
|
58
|
+
expect(calls).toHaveLength(1);
|
|
59
|
+
expect(calls[0].args[0]).toBe('/fake/a2a/scripts/cli.js');
|
|
60
|
+
expect(calls[0].args[1]).toBe('restart');
|
|
61
|
+
// JSON contract on stdout + stdin closed immediately (a2a cli merges stdin)
|
|
62
|
+
expect(calls[0].args).toContain('--json');
|
|
63
|
+
expect(calls[0].opts).toMatchObject({ timeout: 120000 });
|
|
64
|
+
expect(calls[0].opts.input).toBe('');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('hook fires even when pm2 never managed the service (nohup daemon)', () => {
|
|
68
|
+
// Docker nohup daemon: pm2 jlist has no entry → serviceWasRunning=false.
|
|
69
|
+
// The hook must still run, otherwise the daemon keeps the old code (C6).
|
|
70
|
+
const ctx = makeCtx({ serviceWasRunning: false });
|
|
71
|
+
const step = step8_startService(ctx, {
|
|
72
|
+
...throwingPm2,
|
|
73
|
+
spawnSyncFn: () => ({ status: 0, stdout: '{"ok":true}' }),
|
|
74
|
+
resolveA2aCli: () => '/fake/a2a/scripts/cli.js',
|
|
75
|
+
});
|
|
76
|
+
expect(step.status).toBe('done');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('falls back to the pm2 path when the cli restart fails', () => {
|
|
80
|
+
const ctx = makeCtx();
|
|
81
|
+
let restartCalls = 0;
|
|
82
|
+
const step = step8_startService(ctx, {
|
|
83
|
+
spawnSyncFn: () => ({ status: 1, stdout: '{"ok":false,"error":"boom"}' }),
|
|
84
|
+
resolveA2aCli: () => '/fake/a2a/scripts/cli.js',
|
|
85
|
+
restartManagedProcess: () => { restartCalls += 1; },
|
|
86
|
+
restartFromEcosystem: () => { throw new Error('unexpected'); },
|
|
87
|
+
});
|
|
88
|
+
expect(step.status).toBe('done');
|
|
89
|
+
expect(step.message).toBe('baize-a2a');
|
|
90
|
+
expect(restartCalls).toBe(1);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('falls back to the pm2 path when no a2a cli can be resolved', () => {
|
|
94
|
+
const ctx = makeCtx();
|
|
95
|
+
let spawnCalls = 0;
|
|
96
|
+
const step = step8_startService(ctx, {
|
|
97
|
+
spawnSyncFn: () => { spawnCalls += 1; return { status: 0 }; },
|
|
98
|
+
resolveA2aCli: () => null,
|
|
99
|
+
restartManagedProcess: () => {},
|
|
100
|
+
restartFromEcosystem: () => { throw new Error('unexpected'); },
|
|
101
|
+
});
|
|
102
|
+
expect(spawnCalls).toBe(0);
|
|
103
|
+
expect(step.status).toBe('done');
|
|
104
|
+
expect(step.message).toBe('baize-a2a');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('non-a2a components never trigger the hook (pm2 path directly)', () => {
|
|
108
|
+
const ctx = makeCtx({
|
|
109
|
+
component: 'telegram',
|
|
110
|
+
skillMd: '---\nname: telegram\nlifecycle:\n service:\n name: baize-telegram\n---\n# T\n',
|
|
111
|
+
});
|
|
112
|
+
let spawnCalls = 0;
|
|
113
|
+
const step = step8_startService(ctx, {
|
|
114
|
+
spawnSyncFn: () => { spawnCalls += 1; return { status: 0 }; },
|
|
115
|
+
restartManagedProcess: () => {},
|
|
116
|
+
restartFromEcosystem: () => { throw new Error('unexpected'); },
|
|
117
|
+
});
|
|
118
|
+
expect(spawnCalls).toBe(0);
|
|
119
|
+
expect(step.status).toBe('done');
|
|
120
|
+
expect(step.message).toBe('baize-telegram');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('non-a2a component that was not running stays skipped', () => {
|
|
124
|
+
const ctx = makeCtx({ component: 'telegram', serviceWasRunning: false });
|
|
125
|
+
const step = step8_startService(ctx, { ...throwingPm2, spawnSyncFn: () => ({ status: 0 }) });
|
|
126
|
+
expect(step.status).toBe('skipped');
|
|
127
|
+
expect(step.message).toBe('was not running');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -744,7 +744,7 @@ describe('web-console a2a authz routes (D22 单元 C)', () => {
|
|
|
744
744
|
expect(await res.json()).toEqual({ success: true, mode: 'open', allow: [], block: [] });
|
|
745
745
|
});
|
|
746
746
|
|
|
747
|
-
test('PUT authz validates mode enum and
|
|
747
|
+
test('PUT authz validates mode enum; allow/block ignored and forced empty (D52 governance)', async () => {
|
|
748
748
|
ctx = await startServer({ extraEnv: a2aEnv() });
|
|
749
749
|
const put = async (body) => {
|
|
750
750
|
const res = await fetch(`${ctx.baseUrl}/api/admin/a2a/authz`, {
|
|
@@ -754,17 +754,15 @@ describe('web-console a2a authz routes (D22 单元 C)', () => {
|
|
|
754
754
|
});
|
|
755
755
|
return { status: res.status, body: await res.json() };
|
|
756
756
|
};
|
|
757
|
-
expect((await put({ mode: 'bogus' })).status).toBe(400);
|
|
758
|
-
expect((await put({
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
expect((await put({ mode: 'open', block: ['a', 'a'], allow: [] })).status).toBe(400); // duplicate
|
|
762
|
-
const ok = await put({ mode: 'allowlist', allow: ['peer_1', 'peer_2'], block: [] });
|
|
757
|
+
expect((await put({ mode: 'bogus' })).status).toBe(400); // invalid mode rejected
|
|
758
|
+
expect((await put({})).status).toBe(400); // missing mode rejected
|
|
759
|
+
// D52: lists are admin-managed — any allow/block payload is ignored → []
|
|
760
|
+
const ok = await put({ mode: 'allowlist', allow: ['peer_1', 'peer_2'], block: ['x'] });
|
|
763
761
|
expect(ok.status).toBe(200);
|
|
764
|
-
expect(ok.body).toEqual({ success: true, mode: 'allowlist', allow: [
|
|
762
|
+
expect(ok.body).toEqual({ success: true, mode: 'allowlist', allow: [], block: [] });
|
|
765
763
|
});
|
|
766
764
|
|
|
767
|
-
test('PUT authz writes config.json preserving other fields
|
|
765
|
+
test('PUT authz writes config.json preserving other fields; lists forced empty', async () => {
|
|
768
766
|
ctx = await startServer({ extraEnv: a2aEnv() });
|
|
769
767
|
const res = await fetch(`${ctx.baseUrl}/api/admin/a2a/authz`, {
|
|
770
768
|
method: 'PUT',
|
|
@@ -772,16 +770,16 @@ describe('web-console a2a authz routes (D22 单元 C)', () => {
|
|
|
772
770
|
body: JSON.stringify({ mode: 'open', allow: [], block: [' peer_9 ', 'peer_10'] }),
|
|
773
771
|
});
|
|
774
772
|
expect(res.status).toBe(200);
|
|
775
|
-
expect(await res.json()).toEqual({ success: true, mode: 'open', allow: [], block: [
|
|
773
|
+
expect(await res.json()).toEqual({ success: true, mode: 'open', allow: [], block: [] });
|
|
776
774
|
const cfg = JSON.parse(fs.readFileSync(configFile(), 'utf8'));
|
|
777
|
-
expect(cfg.authz).toEqual({ mode: 'open', allow: [], block: [
|
|
775
|
+
expect(cfg.authz).toEqual({ mode: 'open', allow: [], block: [] });
|
|
778
776
|
expect(cfg.enabled).toBe(true);
|
|
779
777
|
expect(cfg.agentId).toBe('agent_test');
|
|
780
778
|
expect(cfg.cert).toEqual({ keyPath: '', certPath: '' });
|
|
781
779
|
expect(cfg.name).toBeUndefined();
|
|
782
780
|
});
|
|
783
781
|
|
|
784
|
-
test('PUT authz creates config.json when missing', async () => {
|
|
782
|
+
test('PUT authz creates config.json when missing (mode only)', async () => {
|
|
785
783
|
fs.rmSync(configFile());
|
|
786
784
|
ctx = await startServer({ extraEnv: a2aEnv() });
|
|
787
785
|
const res = await fetch(`${ctx.baseUrl}/api/admin/a2a/authz`, {
|
|
@@ -791,7 +789,7 @@ describe('web-console a2a authz routes (D22 单元 C)', () => {
|
|
|
791
789
|
});
|
|
792
790
|
expect(res.status).toBe(200);
|
|
793
791
|
const cfg = JSON.parse(fs.readFileSync(configFile(), 'utf8'));
|
|
794
|
-
expect(cfg.authz).toEqual({ mode: 'allowlist', allow: [
|
|
792
|
+
expect(cfg.authz).toEqual({ mode: 'allowlist', allow: [], block: [] });
|
|
795
793
|
});
|
|
796
794
|
|
|
797
795
|
test('authz routes require session when a password is set', async () => {
|
|
@@ -827,6 +825,11 @@ switch (args[0]) {
|
|
|
827
825
|
break;
|
|
828
826
|
case 'enable': out({ ok: true, ...(mode ? { mode } : {}) }); break;
|
|
829
827
|
case 'disable': out({ ok: true, ...(mode ? { mode } : {}) }); break;
|
|
828
|
+
case 'restart':
|
|
829
|
+
// D49 K3: upgrade pipeline must restart the daemon via the a2a cli.
|
|
830
|
+
require('fs').writeFileSync(process.env.BAIZE_A2A_PATH + '/restart-called', '1');
|
|
831
|
+
out({ ok: true });
|
|
832
|
+
break;
|
|
830
833
|
default: console.error('unknown: ' + args.join(' ')); process.exit(1);
|
|
831
834
|
}
|
|
832
835
|
`);
|
|
@@ -1030,21 +1033,95 @@ switch (args[0]) {
|
|
|
1030
1033
|
});
|
|
1031
1034
|
});
|
|
1032
1035
|
|
|
1033
|
-
test('
|
|
1036
|
+
test('second upload with the same version is rejected as version_not_higher (409)', async () => {
|
|
1034
1037
|
ctx = await startServer({ extraEnv: a2aEnv() });
|
|
1035
1038
|
const tgz = buildTarball({ 'package.json': validManifest });
|
|
1036
1039
|
try {
|
|
1037
1040
|
const first = await installUpload(ctx, tgz);
|
|
1038
1041
|
expect(first.status).toBe(200);
|
|
1039
1042
|
const second = await installUpload(ctx, tgz);
|
|
1040
|
-
expect(second.status).toBe(
|
|
1041
|
-
expect(second.body).toMatchObject({
|
|
1042
|
-
|
|
1043
|
+
expect(second.status).toBe(409);
|
|
1044
|
+
expect(second.body).toMatchObject({
|
|
1045
|
+
ok: false,
|
|
1046
|
+
error: 'version_not_higher',
|
|
1047
|
+
local: '9.9.9',
|
|
1048
|
+
incoming: '9.9.9',
|
|
1049
|
+
});
|
|
1050
|
+
// Installation untouched by the rejected upload
|
|
1051
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(ctx.skillsDir, 'a2a', 'package.json'), 'utf8'));
|
|
1052
|
+
expect(pkg.version).toBe('9.9.9');
|
|
1043
1053
|
} finally {
|
|
1044
1054
|
fs.rmSync(tgz, { force: true });
|
|
1045
1055
|
}
|
|
1046
1056
|
});
|
|
1047
1057
|
|
|
1058
|
+
test('uploading a lower version than installed is rejected with local/incoming detail', async () => {
|
|
1059
|
+
ctx = await startServer({ extraEnv: a2aEnv() });
|
|
1060
|
+
const v1 = buildTarball({ 'package.json': validManifest });
|
|
1061
|
+
const v0 = buildTarball({ 'package.json': JSON.stringify({ name: '@baize-ai/baize-a2a', version: '1.0.0' }) });
|
|
1062
|
+
try {
|
|
1063
|
+
expect((await installUpload(ctx, v1)).status).toBe(200);
|
|
1064
|
+
const downgrade = await installUpload(ctx, v0);
|
|
1065
|
+
expect(downgrade.status).toBe(409);
|
|
1066
|
+
expect(downgrade.body).toMatchObject({ ok: false, error: 'version_not_higher', local: '9.9.9', incoming: '1.0.0' });
|
|
1067
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(ctx.skillsDir, 'a2a', 'package.json'), 'utf8'));
|
|
1068
|
+
expect(pkg.version).toBe('9.9.9');
|
|
1069
|
+
} finally {
|
|
1070
|
+
fs.rmSync(v1, { force: true });
|
|
1071
|
+
fs.rmSync(v0, { force: true });
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
test('uploading a higher version upgrades in place: files replaced, components.json bumped, a2a cli restart called', async () => {
|
|
1076
|
+
ctx = await startServer({ extraEnv: a2aEnv() });
|
|
1077
|
+
const v1 = buildTarball({
|
|
1078
|
+
'package.json': validManifest,
|
|
1079
|
+
'SKILL.md': '---\nname: a2a\n---\n# A2A v9\n',
|
|
1080
|
+
});
|
|
1081
|
+
const v2 = buildTarball({
|
|
1082
|
+
'package.json': JSON.stringify({ name: '@baize-ai/baize-a2a', version: '10.0.0' }),
|
|
1083
|
+
'SKILL.md': '---\nname: a2a\nversion: 10.0.0\n---\n# A2A v10\n',
|
|
1084
|
+
'new-module.js': 'export const fresh = true;\n',
|
|
1085
|
+
});
|
|
1086
|
+
try {
|
|
1087
|
+
expect((await installUpload(ctx, v1)).status).toBe(200);
|
|
1088
|
+
const second = await installUpload(ctx, v2);
|
|
1089
|
+
expect(second.status).toBe(200);
|
|
1090
|
+
expect(second.body).toMatchObject({
|
|
1091
|
+
ok: true,
|
|
1092
|
+
version: '10.0.0',
|
|
1093
|
+
previousVersion: '9.9.9',
|
|
1094
|
+
upgraded: true,
|
|
1095
|
+
restarted: true,
|
|
1096
|
+
});
|
|
1097
|
+
// K3/K5: the upgrade restarted the daemon through the a2a cli
|
|
1098
|
+
expect(fs.existsSync(path.join(fakeA2aDir, 'restart-called'))).toBe(true);
|
|
1099
|
+
// Files replaced in place
|
|
1100
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(ctx.skillsDir, 'a2a', 'package.json'), 'utf8'));
|
|
1101
|
+
expect(pkg.version).toBe('10.0.0');
|
|
1102
|
+
expect(fs.readFileSync(path.join(ctx.skillsDir, 'a2a', 'new-module.js'), 'utf8')).toContain('fresh');
|
|
1103
|
+
// Registration preserved, version bumped, both timestamps present
|
|
1104
|
+
const components = JSON.parse(fs.readFileSync(path.join(ctx.root, '.baize', 'components.json'), 'utf8'));
|
|
1105
|
+
expect(components.a2a).toMatchObject({
|
|
1106
|
+
version: '10.0.0',
|
|
1107
|
+
repo: 'baize-ai/baize-a2a',
|
|
1108
|
+
skillDir: path.join(ctx.skillsDir, 'a2a'),
|
|
1109
|
+
});
|
|
1110
|
+
expect(components.a2a.installedAt).toBeTruthy();
|
|
1111
|
+
expect(components.a2a.upgradedAt).toBeTruthy();
|
|
1112
|
+
// Backup taken before any write (upgrade.js .backup convention)
|
|
1113
|
+
const backupRoot = path.join(ctx.skillsDir, 'a2a', '.backup');
|
|
1114
|
+
expect(fs.readdirSync(backupRoot)).toHaveLength(1);
|
|
1115
|
+
// Backup holds the pre-upgrade code
|
|
1116
|
+
const backupTs = fs.readdirSync(backupRoot)[0];
|
|
1117
|
+
const backupPkg = JSON.parse(fs.readFileSync(path.join(backupRoot, backupTs, 'package.json'), 'utf8'));
|
|
1118
|
+
expect(backupPkg.version).toBe('9.9.9');
|
|
1119
|
+
} finally {
|
|
1120
|
+
fs.rmSync(v1, { force: true });
|
|
1121
|
+
fs.rmSync(v2, { force: true });
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1048
1125
|
test('install requires a session when a password is set', async () => {
|
|
1049
1126
|
ctx = await startServer({ envContent: 'BAIZE_WEB_PASSWORD=secret123\n', extraEnv: a2aEnv() });
|
|
1050
1127
|
const res = await fetch(`${ctx.baseUrl}/api/admin/a2a/install`, { method: 'POST' });
|