@baize-ai/core 0.3.16 → 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.
- package/.dockerignore +1 -0
- package/CHANGELOG.md +15 -0
- package/Dockerfile +5 -5
- package/cli/commands/doctor.js +60 -0
- package/cli/commands/init.js +56 -37
- package/cli/lib/__tests__/runtime-launch.test.js +4 -2
- package/cli/lib/__tests__/tmux-env.test.js +19 -5
- package/cli/lib/claude-eval.js +2 -1
- package/cli/lib/codex-hooks.js +12 -0
- package/cli/lib/path-bins.js +54 -0
- package/cli/lib/runtime/claude.js +4 -2
- package/cli/lib/runtime/codex.js +2 -0
- package/cli/lib/runtime/tmux-env.js +5 -4
- package/cli/lib/runtime-setup.js +4 -1
- package/docker/entrypoint.sh +4 -2
- package/docs/ops-runbook.md +161 -0
- package/package.json +1 -1
- package/scripts/docker-publish.sh +11 -1
- package/skills/activity-monitor/scripts/__tests__/guardian.test.js +51 -0
- package/skills/activity-monitor/scripts/adapters/runtime-components.js +27 -0
- package/skills/activity-monitor/scripts/guardian.js +45 -1
- package/skills/activity-monitor/scripts/monitor-orchestrator.js +8 -1
- package/skills/activity-monitor/scripts/upgrade-check.js +3 -1
- package/templates/pm2/ecosystem.config.cjs +5 -4
package/.dockerignore
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,21 @@ 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.17] - 2026-09-14
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- 启动链路 PATH 完整性加固(D54):共享 `cli/lib/path-bins.js`(`completeBinPath`/`knownBinDirs`——静态 `~/.npm-global/bin` + 动态 npm prefix);所有 runtime 二进制 spawn(codex/claude/baize)子进程 env 补完整 PATH——登录 shell PATH 污染(09-13 事故根因:缺 `~/.npm-global/bin`)不再导致 ENOENT
|
|
12
|
+
- `init.js ensureCompletePathProfile`:每次 init 修复 shell profile 完整 bin PATH(npm-global/.local/claude/baize/node),幂等 + 存量缺失自动补;`saveSystemPath` 写显式完整集
|
|
13
|
+
- `ecosystem.config.cjs` `ENHANCED_PATH` 显式含 `~/.npm-global/bin`(PM2 服务不依赖 SYSTEM_PATH/进程 PATH)
|
|
14
|
+
- Guardian 连续失败告警(`ALERT_AFTER_RESTARTS=3` → `~/baize/activity-monitor/guardian-alert.json` + 日志,恢复自动清除);ENOENT 启动失败打修复提示
|
|
15
|
+
- `baize doctor` 新增 PATH 完整性检查(`path_incomplete` issue + path 组)
|
|
16
|
+
- Dockerfile 本地源码构建层序修复:`COPY core.tgz` 移到 npm install RUN 前(tarball 变更正确失效 npm 层缓存)
|
|
17
|
+
- 运维手册 `docs/ops-runbook.md`:PATH 事故排查 + D54 加固说明 + 巡检命令
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- `docker/entrypoint.sh` 不再写 `SYSTEM_PATH`(init.js `saveSystemPath` 唯一写者,写完整集)
|
|
21
|
+
- `templates/pm2/ecosystem.config.cjs` 与 `docker-publish.sh`:发布构建临时 context + 空 `core.tgz` 占位(可选 COPY 不破坏 registry 构建)
|
|
22
|
+
|
|
8
23
|
## [0.3.16] - 2026-09-13
|
|
9
24
|
|
|
10
25
|
### Added
|
package/Dockerfile
CHANGED
|
@@ -86,6 +86,11 @@ ARG BAZE_CORE_VERSION=latest
|
|
|
86
86
|
# build context) to install the workspace package instead of the registry
|
|
87
87
|
# version — lets us verify un-released fixes (e.g. D51) in the image.
|
|
88
88
|
ARG BAZE_CORE_TARBALL=
|
|
89
|
+
# Local-source build tarball — COPY'd BEFORE the install RUN so the file is
|
|
90
|
+
# present in that layer AND a tarball change invalidates the install layer.
|
|
91
|
+
# Registry builds provide an empty placeholder via docker-publish.sh's temp
|
|
92
|
+
# context (or omit BAZE_CORE_TARBALL → the COPY still needs a file present).
|
|
93
|
+
COPY core.tgz /tmp/core.tgz
|
|
89
94
|
WORKDIR /home/baize
|
|
90
95
|
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 \
|
|
91
96
|
&& baize --version \
|
|
@@ -138,11 +143,6 @@ RUN mkdir -p \
|
|
|
138
143
|
# ── Copy PM2 ecosystem config ─────────────────────────────────────────────────
|
|
139
144
|
COPY --chown=baize:baize templates/pm2/ecosystem.config.cjs /home/baize/baize/pm2/ecosystem.config.cjs
|
|
140
145
|
|
|
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
|
-
|
|
146
146
|
# ── Copy entrypoint ───────────────────────────────────────────────────────────
|
|
147
147
|
COPY --chown=baize:baize docker/entrypoint.sh /entrypoint.sh
|
|
148
148
|
RUN chmod +x /entrypoint.sh
|
package/cli/commands/doctor.js
CHANGED
|
@@ -20,6 +20,7 @@ import { loadComponents } from '../lib/components.js';
|
|
|
20
20
|
import { fetchLatestTagAsync, compareSemverDesc } from '../lib/github.js';
|
|
21
21
|
import { getCurrentVersion } from '../lib/self-upgrade.js';
|
|
22
22
|
import { commandExists } from '../lib/shell-utils.js';
|
|
23
|
+
import { completeBinPath } from '../lib/path-bins.js';
|
|
23
24
|
import { parseSkillMd } from '../lib/skill.js';
|
|
24
25
|
import { bold, dim, green, red, yellow, heading } from '../lib/colors.js';
|
|
25
26
|
import { getActiveAdapter } from '../lib/runtime/index.js';
|
|
@@ -280,6 +281,30 @@ function checkTmuxSession() {
|
|
|
280
281
|
}
|
|
281
282
|
}
|
|
282
283
|
|
|
284
|
+
/**
|
|
285
|
+
* D54: verify the runtime binaries are resolvable from the CURRENT process
|
|
286
|
+
* PATH (i.e. PM2 services spawned from here can find codex/claude). A
|
|
287
|
+
* login-shell-polluted PATH (missing ~/.npm-global/bin) shows up here as
|
|
288
|
+
* missing binaries even though the files exist.
|
|
289
|
+
*/
|
|
290
|
+
function checkBinResolvable(bin) {
|
|
291
|
+
try {
|
|
292
|
+
execFileSync(bin, ['--version'], {
|
|
293
|
+
stdio: 'pipe', encoding: 'utf8', timeout: 10000,
|
|
294
|
+
});
|
|
295
|
+
return true;
|
|
296
|
+
} catch {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function checkPathComplete() {
|
|
301
|
+
const bins = ['baize'];
|
|
302
|
+
if (ACTIVE_RUNTIME === 'codex') bins.push('codex');
|
|
303
|
+
else bins.push('claude');
|
|
304
|
+
const missing = bins.filter((b) => !checkBinResolvable(b));
|
|
305
|
+
return { complete: missing.length === 0, missing };
|
|
306
|
+
}
|
|
307
|
+
|
|
283
308
|
// ── Diagnostics collection ───────────────────────────────────────
|
|
284
309
|
|
|
285
310
|
async function collectDiagnostics(env) {
|
|
@@ -308,11 +333,13 @@ async function collectDiagnostics(env) {
|
|
|
308
333
|
? checkPm2Services()
|
|
309
334
|
: { running: false, total: 0, online: 0, activityMonitor: false, procs: [] };
|
|
310
335
|
const session = tmux.installed ? checkTmuxSession() : false;
|
|
336
|
+
const pathCheck = checkPathComplete();
|
|
311
337
|
|
|
312
338
|
return {
|
|
313
339
|
system: { tmux, pm2, network: net },
|
|
314
340
|
ai: { cli, auth, authStatus, autonomous, networkSkipped: !net.reachable },
|
|
315
341
|
services: { ...services, session },
|
|
342
|
+
path: pathCheck,
|
|
316
343
|
};
|
|
317
344
|
}
|
|
318
345
|
|
|
@@ -366,6 +393,19 @@ function buildDiagnosticJson(diag, coreVersion) {
|
|
|
366
393
|
}
|
|
367
394
|
}
|
|
368
395
|
}
|
|
396
|
+
// D54: PATH completeness — runtime binaries must resolve from the current
|
|
397
|
+
// process PATH (services spawned from here inherit it). A polluted login
|
|
398
|
+
// shell PATH missing ~/.npm-global/bin surfaces here as ENOENT risk.
|
|
399
|
+
// Only relevant when the CLI is actually installed (else cli_missing covers
|
|
400
|
+
// it and a PATH repair hint would be misleading).
|
|
401
|
+
const pathSkipped = !diag.ai.cli.installed;
|
|
402
|
+
if (!pathSkipped && !diag.path.complete) {
|
|
403
|
+
issues.push({
|
|
404
|
+
id: 'path_incomplete',
|
|
405
|
+
label: `PATH 缺失运行时二进制: ${diag.path.missing.join(', ')}`,
|
|
406
|
+
hint: 'Run: baize init(修复 ~/.npm-global/bin 等完整 PATH)',
|
|
407
|
+
});
|
|
408
|
+
}
|
|
369
409
|
|
|
370
410
|
return {
|
|
371
411
|
version: coreVersion.success ? coreVersion.version : null,
|
|
@@ -395,6 +435,10 @@ function buildDiagnosticJson(diag, coreVersion) {
|
|
|
395
435
|
procs: diag.services.procs.map(p => ({ name: p.name, status: p.pm2_env?.status || 'unknown' })),
|
|
396
436
|
session: { active: diag.services.session },
|
|
397
437
|
},
|
|
438
|
+
path: {
|
|
439
|
+
passed: !diag.ai.cli.installed ? null : diag.path.complete,
|
|
440
|
+
checks: { missing: diag.path.missing },
|
|
441
|
+
},
|
|
398
442
|
},
|
|
399
443
|
issues,
|
|
400
444
|
};
|
|
@@ -512,6 +556,18 @@ function displayServiceGroup(diag, jsonGroup) {
|
|
|
512
556
|
logToFile(`check: services — ${jsonGroup.passed ? 'passed' : 'failed'}`);
|
|
513
557
|
}
|
|
514
558
|
|
|
559
|
+
// D54: PATH completeness group — runtime binaries must resolve from the
|
|
560
|
+
// current process PATH (services spawned from here inherit it).
|
|
561
|
+
function displayPathGroup(diag, jsonGroup) {
|
|
562
|
+
if (!diag.path.complete) {
|
|
563
|
+
const checks = diag.path.missing.map((b) => red(`${b} 不可解析(ENOENT 风险)`));
|
|
564
|
+
displayCheckGroup('PATH', 'fail', checks);
|
|
565
|
+
} else {
|
|
566
|
+
displayCheckGroup('PATH', 'pass', [green('运行时二进制可解析(npm-global/.local/claude/baize)')]);
|
|
567
|
+
}
|
|
568
|
+
logToFile(`check: path — ${jsonGroup.passed ? 'passed' : 'failed'}`);
|
|
569
|
+
}
|
|
570
|
+
|
|
515
571
|
// ── Channel discovery ────────────────────────────────────────────
|
|
516
572
|
|
|
517
573
|
function getNetworkIP() {
|
|
@@ -651,6 +707,7 @@ function runClaudeFix(diagnosticJson) {
|
|
|
651
707
|
const proc = spawn('claude', ['-p', prompt, ...permArgs], {
|
|
652
708
|
cwd: BAIZE_DIR,
|
|
653
709
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
710
|
+
env: { ...process.env, PATH: completeBinPath() },
|
|
654
711
|
});
|
|
655
712
|
|
|
656
713
|
let stdout = '';
|
|
@@ -772,6 +829,9 @@ export async function doctorCommand(args) {
|
|
|
772
829
|
displaySystemGroup(diag, diagnostic.groups.system);
|
|
773
830
|
displayAiGroup(diag, diagnostic.groups.ai_service);
|
|
774
831
|
displayServiceGroup(diag, diagnostic.groups.services);
|
|
832
|
+
if (diagnostic.groups.path.passed !== null) {
|
|
833
|
+
displayPathGroup(diag, diagnostic.groups.path);
|
|
834
|
+
}
|
|
775
835
|
|
|
776
836
|
// ── Phase 4: Channels ─────────────────────────────────────────
|
|
777
837
|
|
package/cli/commands/init.js
CHANGED
|
@@ -17,6 +17,7 @@ import { generateManifest, saveMergeBaseline } from '../lib/manifest.js';
|
|
|
17
17
|
import { prompt, promptYesNo, promptChoice, promptSecret } from '../lib/prompts.js';
|
|
18
18
|
import { bold, dim, green, red, yellow, cyan, bgGreen, success, error, warn, heading } from '../lib/colors.js';
|
|
19
19
|
import { commandExists } from '../lib/shell-utils.js';
|
|
20
|
+
import { knownBinDirs, completeBinPath } from '../lib/path-bins.js';
|
|
20
21
|
import { getActiveAdapter } from '../lib/runtime/index.js';
|
|
21
22
|
import {
|
|
22
23
|
activateFreshSplitInstructions,
|
|
@@ -114,54 +115,68 @@ function installSystemPackage(pkg) {
|
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
118
|
+
* D54: ensure the user's shell profile carries the COMPLETE bin PATH —
|
|
119
|
+
* every directory where baize/claude/codex binaries actually live:
|
|
120
|
+
* ~/.npm-global/bin (static — codex/baize default install),
|
|
121
|
+
* npm global prefix bin (dynamic), ~/.local/bin (claude default),
|
|
122
|
+
* ~/.claude/bin, ~/baize/bin (components), ~/.local/node/bin.
|
|
123
|
+
* Idempotent (marker-based) and repairs existing profiles that miss any bin
|
|
124
|
+
* (the 09-13 incident root: ~/.npm-global/bin was never added, so login-shell
|
|
125
|
+
* PATH was always incomplete).
|
|
126
|
+
* NOTE: rc files are NOT chattr/chflags-locked — content-marker idempotency
|
|
127
|
+
* already prevents duplicate writes, and a lock would make `baize
|
|
128
|
+
* self-uninstall` / install.sh re-runs abort with EPERM (D54 review P1-3).
|
|
119
129
|
* Returns the profile path if modified, null otherwise.
|
|
120
130
|
*/
|
|
121
|
-
function
|
|
131
|
+
function ensureCompletePathProfile() {
|
|
122
132
|
const homedir = os.homedir();
|
|
123
133
|
const shell = (process.env.SHELL || '').split('/').pop();
|
|
124
|
-
const pathLine = 'export PATH="$HOME/.local/bin:$PATH"';
|
|
125
|
-
|
|
126
|
-
// Map shell to profile file
|
|
127
134
|
const profileMap = {
|
|
128
135
|
zsh: '.zshrc',
|
|
129
136
|
bash: '.bashrc',
|
|
130
|
-
fish: null, // fish uses different
|
|
137
|
+
fish: null, // fish uses a different config mechanism (handled elsewhere)
|
|
131
138
|
sh: '.profile',
|
|
132
139
|
};
|
|
133
|
-
|
|
134
140
|
const profileName = profileMap[shell] || '.profile';
|
|
135
141
|
if (!profileName) return null; // unsupported shell (fish)
|
|
136
|
-
|
|
137
142
|
const profilePath = path.join(homedir, profileName);
|
|
138
143
|
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
const content = fs.readFileSync(profilePath, 'utf8');
|
|
142
|
-
if (content.includes('.local/bin')) return null; // already there
|
|
143
|
-
} catch {
|
|
144
|
-
// File doesn't exist — we'll create it
|
|
145
|
-
}
|
|
144
|
+
// ── Complete bin set (single source of truth: cli/lib/path-bins.js) ──────
|
|
145
|
+
const bins = new Set(knownBinDirs());
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
147
|
+
// Read existing profile content (missing file → create)
|
|
148
|
+
let content = '';
|
|
149
|
+
try { content = fs.readFileSync(profilePath, 'utf8'); } catch { /* absent */ }
|
|
150
|
+
|
|
151
|
+
// ── Idempotent + repair: add any bin not already present ─────────────────
|
|
152
|
+
const toAdd = [];
|
|
153
|
+
for (const bin of bins) {
|
|
154
|
+
// match both absolute (~/...) and $HOME-relative forms already in profile
|
|
155
|
+
const rel = bin.startsWith(homedir) ? bin.slice(homedir.length) : bin;
|
|
156
|
+
if (content.includes(bin) || content.includes(`$HOME${rel}`)) continue;
|
|
157
|
+
const portable = bin.startsWith(homedir) ? `$HOME${rel}` : bin;
|
|
158
|
+
toAdd.push(`export PATH="${portable}:$PATH"`);
|
|
152
159
|
}
|
|
160
|
+
if (toAdd.length === 0) return null; // already complete
|
|
161
|
+
|
|
162
|
+
fs.appendFileSync(profilePath, `\n# Added by baize init (D54 complete PATH)\n${toAdd.join('\n')}\n`);
|
|
163
|
+
return `~/${profileName}`;
|
|
153
164
|
}
|
|
154
165
|
|
|
155
166
|
/**
|
|
156
|
-
* Save the
|
|
157
|
-
*
|
|
167
|
+
* Save the COMPLETE bin PATH to .env so PM2 services can use it.
|
|
168
|
+
* D54: no longer captures process.env.PATH (which may be polluted by a login
|
|
169
|
+
* shell) — builds the full known-good set explicitly: npm global prefix bin,
|
|
170
|
+
* ~/.local/bin, ~/.claude/bin, ~/baize/bin, ~/.local/node/bin.
|
|
158
171
|
*/
|
|
159
172
|
function saveSystemPath(envPath) {
|
|
160
|
-
const
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
// the
|
|
164
|
-
const
|
|
173
|
+
const homedir = os.homedir();
|
|
174
|
+
// D54: build the full known-good set via path-bins (single source of truth)
|
|
175
|
+
const bins = new Set(knownBinDirs());
|
|
176
|
+
// keep the current PATH too (it may carry user-added entries worth keeping)
|
|
177
|
+
for (const entry of (process.env.PATH || '').split(':').filter(Boolean)) bins.add(entry);
|
|
178
|
+
const fullPath = [...bins].join(':');
|
|
179
|
+
|
|
165
180
|
let content = '';
|
|
166
181
|
try {
|
|
167
182
|
content = fs.readFileSync(envPath, 'utf8');
|
|
@@ -169,7 +184,7 @@ function saveSystemPath(envPath) {
|
|
|
169
184
|
return; // .env doesn't exist yet
|
|
170
185
|
}
|
|
171
186
|
|
|
172
|
-
const line = `SYSTEM_PATH=${
|
|
187
|
+
const line = `SYSTEM_PATH=${fullPath}`;
|
|
173
188
|
if (content.includes('SYSTEM_PATH=')) {
|
|
174
189
|
content = content.replace(/^SYSTEM_PATH=.*$/m, line);
|
|
175
190
|
} else {
|
|
@@ -477,7 +492,7 @@ function verifySetupToken() {
|
|
|
477
492
|
try {
|
|
478
493
|
const result = spawnSync('claude', ['-p', 'hi', '--max-turns', '1'], {
|
|
479
494
|
timeout: 30000,
|
|
480
|
-
env: { ...process.env },
|
|
495
|
+
env: { ...process.env, PATH: completeBinPath() },
|
|
481
496
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
482
497
|
});
|
|
483
498
|
|
|
@@ -2178,7 +2193,7 @@ export async function initCommand(args) {
|
|
|
2178
2193
|
console.log(`\n ${cyan('Starting Codex device auth...')}`);
|
|
2179
2194
|
console.log(` ${dim('Follow the instructions to authenticate. Press Ctrl+C when done.')}\n`);
|
|
2180
2195
|
try {
|
|
2181
|
-
spawnSync('codex', ['login', '--device-auth'], { stdio: 'inherit' });
|
|
2196
|
+
spawnSync('codex', ['login', '--device-auth'], { stdio: 'inherit', env: { ...process.env, PATH: completeBinPath() } });
|
|
2182
2197
|
} catch { /* user may Ctrl+C */ }
|
|
2183
2198
|
codexAuthenticated = isCodexAuthenticated();
|
|
2184
2199
|
if (codexAuthenticated) {
|
|
@@ -2192,7 +2207,7 @@ export async function initCommand(args) {
|
|
|
2192
2207
|
console.log(`\n ${cyan('Starting Codex browser login...')}`);
|
|
2193
2208
|
console.log(` ${dim('After login completes, press Ctrl+C to return.')}\n`);
|
|
2194
2209
|
try {
|
|
2195
|
-
spawnSync('codex', ['login'], { stdio: 'inherit' });
|
|
2210
|
+
spawnSync('codex', ['login'], { stdio: 'inherit', env: { ...process.env, PATH: completeBinPath() } });
|
|
2196
2211
|
} catch { /* user may Ctrl+C */ }
|
|
2197
2212
|
codexAuthenticated = isCodexAuthenticated();
|
|
2198
2213
|
if (codexAuthenticated) {
|
|
@@ -2309,9 +2324,9 @@ export async function initCommand(args) {
|
|
|
2309
2324
|
// /dev/tty redirects. Use `script` to allocate a fresh pseudo-terminal
|
|
2310
2325
|
// that gives Claude full terminal control.
|
|
2311
2326
|
if (process.platform === 'darwin') {
|
|
2312
|
-
spawnSync('script', ['-q', '/dev/null', 'claude'], { stdio: 'inherit' });
|
|
2327
|
+
spawnSync('script', ['-q', '/dev/null', 'claude'], { stdio: 'inherit', env: { ...process.env, PATH: completeBinPath() } });
|
|
2313
2328
|
} else {
|
|
2314
|
-
spawnSync('claude', [], { stdio: 'inherit' });
|
|
2329
|
+
spawnSync('claude', [], { stdio: 'inherit', env: { ...process.env, PATH: completeBinPath() } });
|
|
2315
2330
|
}
|
|
2316
2331
|
} catch { /* user may Ctrl+C */ }
|
|
2317
2332
|
process.removeAllListeners('SIGINT');
|
|
@@ -2623,9 +2638,13 @@ export async function initCommand(args) {
|
|
|
2623
2638
|
|
|
2624
2639
|
printWebConsoleInfo();
|
|
2625
2640
|
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2641
|
+
// D54: EVERY init run repairs the complete bin PATH in the shell profile
|
|
2642
|
+
// (npm-global/.local/claude/baize/node bins) — idempotent, no-op when
|
|
2643
|
+
// complete. Runs unconditionally: a claude-runtime deployment re-running
|
|
2644
|
+
// init with --yes/--quiet must also get the missing bins repaired.
|
|
2645
|
+
const profile = ensureCompletePathProfile();
|
|
2646
|
+
if (profile && !quiet) {
|
|
2647
|
+
console.log(`${green(` ✔ PATH 已写入 ${profile}(含 npm-global/.local/claude/baize/node bin)`)}`);
|
|
2629
2648
|
}
|
|
2630
2649
|
|
|
2631
2650
|
if (!quiet) {
|
|
@@ -278,12 +278,14 @@ describe('Claude launch — compat mode PATH dedupe', () => {
|
|
|
278
278
|
await makeAdapter(ClaudeAdapter).launch({ bypassPermissions: false });
|
|
279
279
|
const env = readSpecEnv();
|
|
280
280
|
assert.ok(env, 'spec should be written');
|
|
281
|
-
|
|
281
|
+
// D54: known bin dirs first + deduplicated (npm-global static first)
|
|
282
|
+
const expected = `${fakeHome}/.npm-global/bin:${fakeHome}/.local/bin:${fakeHome}/.claude/bin:${fakeHome}/baize/bin:${fakeHome}/.local/node/bin:/a:/b:/c`;
|
|
283
|
+
assert.equal(env.PATH, expected, 'PATH must carry known bins first and be deduplicated in compat mode');
|
|
282
284
|
|
|
283
285
|
const tmux = findTmuxNewSession();
|
|
284
286
|
const pathArg = tmux.args.find(a => a.startsWith('PATH='));
|
|
285
287
|
assert.ok(pathArg, 'tmux args should contain PATH= env');
|
|
286
|
-
assert.equal(pathArg,
|
|
288
|
+
assert.equal(pathArg, `PATH=${expected}`, 'tmux -e PATH must also carry known bins + dedupe');
|
|
287
289
|
} finally {
|
|
288
290
|
process.env.PATH = origPath;
|
|
289
291
|
// Restore default (clean env is now the default)
|
|
@@ -464,11 +464,20 @@ describe('buildCleanEnv', () => {
|
|
|
464
464
|
// ── buildCompatEnv ─────────────────────────────────────────────────────────
|
|
465
465
|
|
|
466
466
|
describe('buildCompatEnv', () => {
|
|
467
|
-
it('passes through full processEnv', () => {
|
|
467
|
+
it('passes through full processEnv and prepends known bin dirs (D54)', () => {
|
|
468
468
|
const processEnv = { PATH: '/usr/bin', HOME: '/home/test', SECRET: 'abc123' };
|
|
469
469
|
const { env } = buildCompatEnv({ processEnv, dotenvVars: {} });
|
|
470
470
|
assert.equal(env.SECRET, 'abc123');
|
|
471
|
-
|
|
471
|
+
const parts = env.PATH.split(':');
|
|
472
|
+
// D54: known bin dirs are ALWAYS prepended (npm-global static first),
|
|
473
|
+
// original PATH entries preserved, no duplicates.
|
|
474
|
+
assert.equal(parts[0], '/home/test/.npm-global/bin');
|
|
475
|
+
assert.ok(parts.includes('/home/test/.local/bin'));
|
|
476
|
+
assert.ok(parts.includes('/home/test/.claude/bin'));
|
|
477
|
+
assert.ok(parts.includes('/home/test/baize/bin'));
|
|
478
|
+
assert.ok(parts.includes('/home/test/.local/node/bin'));
|
|
479
|
+
assert.ok(parts.includes('/usr/bin'));
|
|
480
|
+
assert.equal(new Set(parts).size, parts.length, 'PATH must be deduplicated');
|
|
472
481
|
});
|
|
473
482
|
|
|
474
483
|
it('overrides with BAIZE_TMUX_ENV manifest vars', () => {
|
|
@@ -478,10 +487,15 @@ describe('buildCompatEnv', () => {
|
|
|
478
487
|
assert.equal(env.MY_VAR, 'new_value');
|
|
479
488
|
});
|
|
480
489
|
|
|
481
|
-
it('deduplicates PATH preserving first-occurrence order', () => {
|
|
490
|
+
it('deduplicates PATH preserving first-occurrence order with known bins first (D54)', () => {
|
|
482
491
|
const processEnv = { PATH: '/a:/b:/a:/c:/b', HOME: '/home/test' };
|
|
483
492
|
const { env } = buildCompatEnv({ processEnv, dotenvVars: {} });
|
|
484
|
-
|
|
493
|
+
const parts = env.PATH.split(':');
|
|
494
|
+
assert.equal(parts[0], '/home/test/.npm-global/bin');
|
|
495
|
+
// original entries deduped, first-occurrence order preserved after known bins
|
|
496
|
+
const tail = parts.slice(parts.indexOf('/a'));
|
|
497
|
+
assert.deepEqual(tail, ['/a', '/b', '/c']);
|
|
498
|
+
assert.equal(new Set(parts).size, parts.length);
|
|
485
499
|
});
|
|
486
500
|
|
|
487
501
|
it('does not inject GH_PROMPT_DISABLED, Homebrew paths, or PATH manifest', () => {
|
|
@@ -492,7 +506,7 @@ describe('buildCompatEnv', () => {
|
|
|
492
506
|
};
|
|
493
507
|
const { env } = buildCompatEnv({ processEnv, dotenvVars });
|
|
494
508
|
assert.equal(env.GH_PROMPT_DISABLED, undefined);
|
|
495
|
-
|
|
509
|
+
// compat mode does NOT apply PATH manifest PREPEND/APPEND (clean-env only)
|
|
496
510
|
assert.ok(!env.PATH.includes('/custom/pre'), 'compat mode should not apply PREPEND');
|
|
497
511
|
assert.ok(!env.PATH.includes('/custom/post'), 'compat mode should not apply APPEND');
|
|
498
512
|
});
|
package/cli/lib/claude-eval.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { spawn } from 'node:child_process';
|
|
11
|
+
import { completeBinPath } from './path-bins.js';
|
|
11
12
|
import { parseSkillMd } from './skill.js';
|
|
12
13
|
|
|
13
14
|
const MAX_FILE_LINES = 500;
|
|
@@ -133,7 +134,7 @@ function callClaude(prompt) {
|
|
|
133
134
|
|
|
134
135
|
const child = spawn('claude', ['--print', '--output-format', 'text'], {
|
|
135
136
|
stdio: ['pipe', 'pipe', 'ignore'],
|
|
136
|
-
env: { ...process.env },
|
|
137
|
+
env: { ...process.env, PATH: completeBinPath() },
|
|
137
138
|
});
|
|
138
139
|
|
|
139
140
|
child.stdin.on('error', () => {}); // Ignore EPIPE if child exits early
|
package/cli/lib/codex-hooks.js
CHANGED
|
@@ -289,12 +289,23 @@ function writeTrustMarker(markerPath, marker) {
|
|
|
289
289
|
fs.writeFileSync(markerPath, JSON.stringify(stable(marker), null, 2) + '\n', { mode: 0o600 });
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
/**
|
|
293
|
+
* D54 PATH hardening: build a PATH that always includes the known bin dirs
|
|
294
|
+
* (npm global prefix bin, ~/.local/bin, ~/.claude/bin, ~/baize/bin) on top of
|
|
295
|
+
* the current PATH — a login-shell-polluted PATH (missing ~/.npm-global/bin)
|
|
296
|
+
* must not make a bare `codex` spawn fail with ENOENT (09-13 incident).
|
|
297
|
+
* Single source of truth: cli/lib/path-bins.js.
|
|
298
|
+
*/
|
|
299
|
+
import { completeBinPath } from './path-bins.js';
|
|
300
|
+
export { completeBinPath };
|
|
301
|
+
|
|
292
302
|
export function getCodexVersion({ codexBin = process.env.CODEX_BIN || 'codex', execFileSyncImpl } = {}) {
|
|
293
303
|
const execImpl = execFileSyncImpl || execFileSync;
|
|
294
304
|
return String(execImpl(codexBin, ['--version'], {
|
|
295
305
|
encoding: 'utf8',
|
|
296
306
|
timeout: 10_000,
|
|
297
307
|
stdio: 'pipe',
|
|
308
|
+
env: { ...process.env, PATH: completeBinPath() },
|
|
298
309
|
})).trim();
|
|
299
310
|
}
|
|
300
311
|
|
|
@@ -470,6 +481,7 @@ send('initialize', {
|
|
|
470
481
|
cwd: baizeDir,
|
|
471
482
|
env: {
|
|
472
483
|
...process.env,
|
|
484
|
+
PATH: completeBinPath(),
|
|
473
485
|
BAIZE_CODEX_TRUST_CWD: baizeDir,
|
|
474
486
|
BAIZE_CODEX_BIN: codexBin,
|
|
475
487
|
},
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D54: complete known-good bin PATH — the single source of truth for the bin
|
|
3
|
+
* directories where baize/claude/codex binaries actually live. Every spawn
|
|
4
|
+
* of a runtime binary must pass PATH built from this so a login-shell-polluted
|
|
5
|
+
* PATH (missing ~/.npm-global/bin — 09-13 incident) can never cause ENOENT.
|
|
6
|
+
*
|
|
7
|
+
* Consumers: init.js (profile repair + SYSTEM_PATH), codex-hooks.js
|
|
8
|
+
* (getCodexVersion), runtime-setup.js (auth checks), claude-eval.js,
|
|
9
|
+
* runtime/codex.js + runtime/claude.js (launch auth checks).
|
|
10
|
+
*/
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { execFileSync } from 'node:child_process';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {object} [opts]
|
|
17
|
+
* @param {string} [opts.currentPath] base PATH to extend (default process.env.PATH)
|
|
18
|
+
* @returns {string} deduplicated PATH with all known bin dirs first
|
|
19
|
+
*/
|
|
20
|
+
export function completeBinPath({ currentPath = process.env.PATH || '', home = os.homedir() } = {}) {
|
|
21
|
+
const bins = [];
|
|
22
|
+
// ~/.npm-global/bin — STATIC first (the 09-13 incident dir): codex/baize
|
|
23
|
+
// install there in the standard (Dockerfile NPM_CONFIG_PREFIX + install.sh
|
|
24
|
+
// user prefix) layouts; must never depend on `npm` being resolvable from
|
|
25
|
+
// the calling process (PM2 services, containers with replaced PATH).
|
|
26
|
+
bins.push(path.join(home, '.npm-global', 'bin'));
|
|
27
|
+
// npm global prefix bin — dynamic (npm config get prefix); covers custom
|
|
28
|
+
// prefixes (e.g. /opt/homebrew) in addition to the default.
|
|
29
|
+
try {
|
|
30
|
+
const prefix = execFileSync('npm', ['config', 'get', 'prefix'], { encoding: 'utf8', timeout: 10_000 }).trim();
|
|
31
|
+
if (prefix && prefix !== 'undefined' && prefix !== 'null') bins.push(path.join(prefix, 'bin'));
|
|
32
|
+
} catch { /* npm unavailable */ }
|
|
33
|
+
bins.push(path.join(home, '.local', 'bin'));
|
|
34
|
+
bins.push(path.join(home, '.claude', 'bin'));
|
|
35
|
+
bins.push(path.join(home, 'baize', 'bin'));
|
|
36
|
+
bins.push(path.join(home, '.local', 'node', 'bin'));
|
|
37
|
+
for (const entry of currentPath.split(':').filter(Boolean)) bins.push(entry);
|
|
38
|
+
return [...new Set(bins)].join(':');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Known bin dirs (without the current PATH tail) — for profile writes. */
|
|
42
|
+
export function knownBinDirs(home = os.homedir()) {
|
|
43
|
+
const bins = [];
|
|
44
|
+
bins.push(path.join(home, '.npm-global', 'bin'));
|
|
45
|
+
try {
|
|
46
|
+
const prefix = execFileSync('npm', ['config', 'get', 'prefix'], { encoding: 'utf8', timeout: 10_000 }).trim();
|
|
47
|
+
if (prefix && prefix !== 'undefined' && prefix !== 'null') bins.push(path.join(prefix, 'bin'));
|
|
48
|
+
} catch { /* npm unavailable */ }
|
|
49
|
+
bins.push(path.join(home, '.local', 'bin'));
|
|
50
|
+
bins.push(path.join(home, '.claude', 'bin'));
|
|
51
|
+
bins.push(path.join(home, 'baize', 'bin'));
|
|
52
|
+
bins.push(path.join(home, '.local', 'node', 'bin'));
|
|
53
|
+
return [...new Set(bins)];
|
|
54
|
+
}
|
|
@@ -16,6 +16,7 @@ import fs from 'node:fs';
|
|
|
16
16
|
import os from 'node:os';
|
|
17
17
|
import path from 'node:path';
|
|
18
18
|
import { execFileSync, execFile } from 'node:child_process';
|
|
19
|
+
import { completeBinPath } from '../path-bins.js';
|
|
19
20
|
import { promisify } from 'node:util';
|
|
20
21
|
|
|
21
22
|
const execFileAsync = promisify(execFile);
|
|
@@ -104,7 +105,7 @@ export class ClaudeAdapter extends RuntimeAdapter {
|
|
|
104
105
|
*/
|
|
105
106
|
async checkAuth() {
|
|
106
107
|
// Build subprocess env: inherit current env, inject .env API keys (same as launch()).
|
|
107
|
-
const injectedEnv = { ...process.env };
|
|
108
|
+
const injectedEnv = { ...process.env, PATH: completeBinPath() };
|
|
108
109
|
let envApiKey = '';
|
|
109
110
|
let envOauthToken = '';
|
|
110
111
|
let envBaseUrl = '';
|
|
@@ -257,6 +258,7 @@ export class ClaudeAdapter extends RuntimeAdapter {
|
|
|
257
258
|
try {
|
|
258
259
|
const out = execFileSync(CLAUDE_BIN, ['auth', 'status'], {
|
|
259
260
|
encoding: 'utf8', timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'],
|
|
261
|
+
env: { ...process.env, PATH: completeBinPath() },
|
|
260
262
|
});
|
|
261
263
|
const status = JSON.parse(out);
|
|
262
264
|
if (status?.loggedIn === true && status?.authMethod === 'claude.ai') {
|
|
@@ -435,7 +437,7 @@ function _ensureOnboardingComplete(projectDir) {
|
|
|
435
437
|
config.hasCompletedOnboarding = true;
|
|
436
438
|
try {
|
|
437
439
|
config.lastOnboardingVersion = execFileSync(
|
|
438
|
-
CLAUDE_BIN, ['--version'], { encoding: 'utf8', timeout: 5000 }
|
|
440
|
+
CLAUDE_BIN, ['--version'], { encoding: 'utf8', timeout: 5000, env: { ...process.env, PATH: completeBinPath() } }
|
|
439
441
|
).trim();
|
|
440
442
|
} catch {
|
|
441
443
|
config.lastOnboardingVersion = '2.1.59';
|
package/cli/lib/runtime/codex.js
CHANGED
|
@@ -18,6 +18,7 @@ import os from 'node:os';
|
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import { execFileSync, execFile } from 'node:child_process';
|
|
20
20
|
import { promisify } from 'node:util';
|
|
21
|
+
import { completeBinPath } from '../path-bins.js';
|
|
21
22
|
|
|
22
23
|
const execFileAsync = promisify(execFile);
|
|
23
24
|
import { RuntimeAdapter } from './base.js';
|
|
@@ -147,6 +148,7 @@ export class CodexAdapter extends RuntimeAdapter {
|
|
|
147
148
|
try {
|
|
148
149
|
const { stdout, stderr } = await execFileAsync(CODEX_BIN, ['login', 'status'], {
|
|
149
150
|
stdio: 'pipe', encoding: 'utf8', timeout: 10_000,
|
|
151
|
+
env: { ...process.env, PATH: completeBinPath() },
|
|
150
152
|
});
|
|
151
153
|
const status = classifyCodexLoginStatus((stdout || '') + (stderr || ''));
|
|
152
154
|
if (status === 'success') return { status: 'success', reason: 'codex_login_status' };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { completeBinPath } from '../path-bins.js';
|
|
4
5
|
|
|
5
6
|
const VALID_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
6
7
|
|
|
@@ -280,10 +281,10 @@ export function buildCleanEnv({ processEnv, dotenvVars, manifest, platform, uid,
|
|
|
280
281
|
export function buildCompatEnv({ processEnv, dotenvVars }) {
|
|
281
282
|
const env = { ...processEnv };
|
|
282
283
|
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
284
|
+
// D54: PATH built from the shared single source of truth (path-bins) —
|
|
285
|
+
// always prepends npm-global/.local/.claude/baize/node bins and dedupes;
|
|
286
|
+
// a polluted login-shell PATH can never hide the runtime binaries.
|
|
287
|
+
env.PATH = completeBinPath({ currentPath: env.PATH, home: processEnv.HOME });
|
|
287
288
|
|
|
288
289
|
// Override with BAIZE_TMUX_ENV manifest vars from dotenvVars
|
|
289
290
|
const warnings = [];
|
package/cli/lib/runtime-setup.js
CHANGED
|
@@ -13,6 +13,7 @@ import { execSync, execFileSync, spawnSync } from 'node:child_process';
|
|
|
13
13
|
import { parse, stringify } from 'smol-toml';
|
|
14
14
|
import crypto from 'node:crypto';
|
|
15
15
|
import { BAIZE_DIR } from './config.js';
|
|
16
|
+
import { completeBinPath } from './path-bins.js';
|
|
16
17
|
import { commandExists } from './shell-utils.js';
|
|
17
18
|
import { parseClaudeAuthStatus, parseCodexLoginStatus } from './auth-parsers.js';
|
|
18
19
|
import { installCoreCodexHook } from './codex-hooks.js';
|
|
@@ -90,6 +91,7 @@ export function isClaudeAuthenticated() {
|
|
|
90
91
|
stdio: 'pipe',
|
|
91
92
|
encoding: 'utf8',
|
|
92
93
|
timeout: 10000,
|
|
94
|
+
env: { ...process.env, PATH: completeBinPath() },
|
|
93
95
|
});
|
|
94
96
|
return parseClaudeAuthStatus(result.stdout);
|
|
95
97
|
} catch {
|
|
@@ -129,6 +131,7 @@ export function isCodexAuthenticated() {
|
|
|
129
131
|
try {
|
|
130
132
|
const result = spawnSync('codex', ['login', 'status'], {
|
|
131
133
|
stdio: 'pipe', encoding: 'utf8', timeout: 10000,
|
|
134
|
+
env: { ...process.env, PATH: completeBinPath() },
|
|
132
135
|
});
|
|
133
136
|
return parseCodexLoginStatus((result.stdout || '') + (result.stderr || ''));
|
|
134
137
|
} catch {
|
|
@@ -158,7 +161,7 @@ export function approveApiKey(keyOrToken) {
|
|
|
158
161
|
if (!config.hasCompletedOnboarding) {
|
|
159
162
|
config.hasCompletedOnboarding = true;
|
|
160
163
|
try {
|
|
161
|
-
const ver = execSync('claude --version 2>/dev/null', { encoding: 'utf8' }).trim();
|
|
164
|
+
const ver = execSync('claude --version 2>/dev/null', { encoding: 'utf8', env: { ...process.env, PATH: completeBinPath() } }).trim();
|
|
162
165
|
config.lastOnboardingVersion = ver;
|
|
163
166
|
} catch { /* omit if claude binary not yet available */ }
|
|
164
167
|
}
|
package/docker/entrypoint.sh
CHANGED
|
@@ -124,8 +124,10 @@ upsert_env "CODEX_BYPASS_PERMISSIONS" "${CODEX_BYPASS_PERMISSIONS:-true}"
|
|
|
124
124
|
upsert_env "OPENAI_API_KEY" "${OPENAI_API_KEY:-}"
|
|
125
125
|
upsert_env "CODEX_API_KEY" "${CODEX_API_KEY:-}"
|
|
126
126
|
|
|
127
|
-
#
|
|
128
|
-
|
|
127
|
+
# D54: SYSTEM_PATH is owned exclusively by baize init (saveSystemPath writes
|
|
128
|
+
# the complete known-good bin PATH). entrypoint no longer captures the
|
|
129
|
+
# container PATH here — ecosystem.config.cjs carries an explicit
|
|
130
|
+
# ~/.npm-global/bin fallback, so a polluted shell can never degrade services.
|
|
129
131
|
|
|
130
132
|
# ── Graceful shutdown ─────────────────────────────────────────────────────────
|
|
131
133
|
# docker stop sends SIGTERM to PID 1 (this script). Clean up tmux + PM2.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Baize Agent 容器运维排查手册
|
|
2
|
+
|
|
3
|
+
> 适用:docker 部署的 baize agent(`baize-core` 镜像)。
|
|
4
|
+
> 原则:先只读诊断定位,再修改;生产环境每步执行前确认。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. 症状:AI main 会话起不来
|
|
9
|
+
|
|
10
|
+
**表现**:agent 不再响应任务;`web-console` 可能还活着,但调度/消息链路断了。
|
|
11
|
+
|
|
12
|
+
**"main 会话"是什么**:AI 主循环(Claude Code / Codex)跑在持久 **tmux 会话**里
|
|
13
|
+
(`claude-main` / `codex-main`),由 `activity-monitor`(Guardian 看门狗)负责拉起,
|
|
14
|
+
`scheduler` 依赖它收发消息。会话没了 = 看门狗没拉起或拉起失败。
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 2. 快速排查流程(只读,从外到内)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# ① 容器状态(是否在跑 / 重启次数)
|
|
22
|
+
docker ps -a | grep -i baize
|
|
23
|
+
|
|
24
|
+
# ② pm2 服务(web-console / scheduler / c4-dispatcher / activity-monitor)
|
|
25
|
+
docker exec <容器名> pm2 list
|
|
26
|
+
|
|
27
|
+
# ③ tmux 会话(main 会话在不在)
|
|
28
|
+
docker exec <容器名> bash -lc "tmux ls"
|
|
29
|
+
|
|
30
|
+
# ④ 看门狗日志(核心——Guardian 为什么没拉起)
|
|
31
|
+
docker exec <容器名> bash -lc "pm2 logs activity-monitor --lines 30 --nostream"
|
|
32
|
+
|
|
33
|
+
# ⑤ 调度器日志
|
|
34
|
+
docker exec <容器名> bash -lc "pm2 logs scheduler --lines 20 --nostream"
|
|
35
|
+
|
|
36
|
+
# ⑥ 初始化状态(runtime 装没装完)
|
|
37
|
+
docker exec <容器名> bash -lc "cat ~/baize/init-state.json 2>/dev/null"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**日志判读**:
|
|
41
|
+
|
|
42
|
+
| 日志 | 含义 | 下一步 |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `spawnSync codex ENOENT` / `spawnSync claude ENOENT` | **PATH 问题**(可执行文件不在 PATH)→ 见 §3 | §3 |
|
|
45
|
+
| `tmux ls` 无会话 + pm2 正常 | 看门狗没触发或拉起失败 | ④ 看 Guardian 日志 |
|
|
46
|
+
| `init-state.json` 失败 | 初始化未完成(runtime/模型未配置) | web 控制台模型设置重新安装 |
|
|
47
|
+
| tmux 会话在但卡住 | Claude/Codex 等输入 / 认证过期 / API 错误 | `tmux attach -t claude-main` 看输出 |
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 3. 本次实战:PATH 污染导致 `spawnSync codex ENOENT`
|
|
52
|
+
|
|
53
|
+
### 现象
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
activity-monitor: Guardian: Failed to start Codex: spawnSync codex ENOENT
|
|
57
|
+
scheduler: Waiting for agent runtime (offline or stopped)...
|
|
58
|
+
tmux ls: no server running on /tmp/tmux-1001/default
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 诊断证据链
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# ① codex symlink 在不在(npm 全局 bin 通常是 symlink)
|
|
65
|
+
docker exec baize-2 bash -lc "ls -la ~/.npm-global/bin/codex; readlink -f ~/.npm-global/bin/codex"
|
|
66
|
+
# → lrwxrwxrwx ... codex -> ../lib/node_modules/@openai/codex/bin/codex.js (symlink 完好)
|
|
67
|
+
|
|
68
|
+
# ② codex 二进制本身能不能跑(关键——区分"二进制坏了" vs "PATH 找不到")
|
|
69
|
+
docker exec baize-2 bash -lc "~/.npm-global/bin/codex --version; echo EXIT=\$?"
|
|
70
|
+
# → codex-cli 0.153.2 / EXIT=0 (二进制完好!)
|
|
71
|
+
|
|
72
|
+
# ③ pm2 进程实际 PATH(Guardian 用它 spawn codex)
|
|
73
|
+
docker exec baize-2 bash -lc "cat /proc/\$(pgrep -f 'activity-monitor' | head -1)/environ | tr '\0' '\n' | grep '^PATH='"
|
|
74
|
+
# → PATH=...codex-path:/home/baize/baize/bin:...(★ 没有 ~/.npm-global/bin)
|
|
75
|
+
|
|
76
|
+
# ④ 谁污染了 PATH(login shell 的 profile)
|
|
77
|
+
docker exec baize-2 bash -lc "grep -n 'PATH' ~/.bashrc ~/.profile ~/.bash_profile 2>/dev/null"
|
|
78
|
+
# → .bashrc/.profile 里有 export PATH="/home/baize/baize/bin:$PATH"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**判定**:`codex` 二进制完好但 **pm2 进程 PATH 缺 `~/.npm-global/bin`**(codex symlink
|
|
82
|
+
所在目录)→ `spawnSync codex` 找不到 → ENOENT。
|
|
83
|
+
|
|
84
|
+
### 根因
|
|
85
|
+
|
|
86
|
+
1. **PATH 被污染**:`~/.bashrc`/`~/.profile` 前置 `baize/bin` + codex npm 包
|
|
87
|
+
postinstall 前插 vendor 路径,把 `~/.npm-global/bin` 挤掉。
|
|
88
|
+
2. **坏环境被固化**:在污染 PATH 的 shell 里执行过 `pm2 start`/`pm2 save`,
|
|
89
|
+
pm2 dump 保存了坏环境,之后 pm2 进程一直继承(`docker restart` 若走 pm2
|
|
90
|
+
resurrect 也不会恢复)。
|
|
91
|
+
3. **谁干的**:使用人在容器内让 AI 执行环境类操作(改 profile / 重装 runtime /
|
|
92
|
+
重跑 pm2)时污染了环境并固化。
|
|
93
|
+
|
|
94
|
+
### 修复(干净 PATH + --update-env 覆盖 pm2 固化环境)
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# 用干净 PATH 重启看门狗;--update-env 强制用当前 shell 环境覆盖 pm2 dump
|
|
98
|
+
docker exec -e PATH=/home/baize/.npm-global/bin:/home/baize/.local/bin:/usr/local/bin:/usr/bin:/bin \
|
|
99
|
+
<容器名> pm2 restart activity-monitor --update-env
|
|
100
|
+
|
|
101
|
+
# 验证:15s 后会话出现、日志不再 ENOENT
|
|
102
|
+
sleep 15
|
|
103
|
+
docker exec <容器名> bash -lc "tmux ls"
|
|
104
|
+
docker exec <容器名> bash -lc "pm2 logs activity-monitor --lines 6 --nostream"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
> `docker exec -e PATH=...` 用 `-e` 注入,避免嵌套引号。
|
|
108
|
+
> 其他 pm2 服务(scheduler 等)若同样 PATH 坏,按同法 `--update-env` 重启。
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 4. 预防(如何避免再发生)
|
|
113
|
+
|
|
114
|
+
### 操作纪律(人和 AI 都要遵守)
|
|
115
|
+
|
|
116
|
+
1. **容器内操作统一走封装入口**:优先用 web 控制台 / `baize` CLI,避免裸
|
|
117
|
+
`docker exec ... bash -lc`(login shell 会加载 profile,触发 PATH 污染)。
|
|
118
|
+
2. **禁止手工改 `~/.bashrc` / `~/.profile` 的 PATH**:本次污染源头。若必须加
|
|
119
|
+
路径,用 `export PATH="$PATH:<新路径>"` **追加**,且绝不动
|
|
120
|
+
`~/.npm-global/bin` / `~/.local/bin` 两项。
|
|
121
|
+
3. **不在容器内手工 `pm2 save`**:pm2 dump 会固化当时的 PATH 环境;容器重启
|
|
122
|
+
由 entrypoint 统一注入环境(它会重存 `SYSTEM_PATH`)。
|
|
123
|
+
4. **AI 执行环境修改类操作前先备份**:`cp ~/.bashrc ~/.bashrc.bak`,
|
|
124
|
+
记录改动 diff,改完验证 `which codex claude baize node` 全部可解析。
|
|
125
|
+
|
|
126
|
+
### 工程加固(D54 已落地,0.3.17 起)
|
|
127
|
+
|
|
128
|
+
- **PATH 完整性收敛到 init.js**:`baize init` 每次运行修复 shell profile 完整 bin PATH
|
|
129
|
+
(`~/.npm-global/bin` + `.local/bin` + `.claude/bin` + `baize/bin` + `.local/node/bin`),
|
|
130
|
+
幂等 + 存量缺失自动补。裸机/docker/存量升级全部覆盖(docker entrypoint 每次启动跑 init)。
|
|
131
|
+
- **共享 `path-bins.js`**:所有 runtime 二进制 spawn(codex/claude/baize)统一走
|
|
132
|
+
`completeBinPath()`(子进程 env 补完整 PATH)——登录 shell PATH 污染不再导致 ENOENT。
|
|
133
|
+
- **ecosystem `ENHANCED_PATH` 显式含 `~/.npm-global/bin`**:PM2 服务即使继承残缺 PATH 也能
|
|
134
|
+
找到 runtime。
|
|
135
|
+
- **Guardian 告警**:连续 3 次启动失败 → 写 `~/baize/activity-monitor/guardian-alert.json`
|
|
136
|
+
+ 日志;恢复后自动清除。静默宕机不再发生。
|
|
137
|
+
- **doctor 检查**:`baize doctor` 新增 PATH 完整性组(`path_incomplete` issue)。
|
|
138
|
+
- install.sh / Dockerfile **不改**(收敛到 init.js);`--update-env` 从根源安全化。
|
|
139
|
+
|
|
140
|
+
### 巡检(可选定时)
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# 检查 main 会话与 PATH 健康度
|
|
144
|
+
docker exec <容器名> bash -lc "tmux ls"
|
|
145
|
+
docker exec <容器名> bash -lc "which codex claude baize node"
|
|
146
|
+
# 告警文件存在 = 有未恢复的连续失败(无文件=健康)
|
|
147
|
+
docker exec <容器名> bash -lc "cat ~/baize/activity-monitor/guardian-alert.json 2>/dev/null"
|
|
148
|
+
# doctor PATH 组
|
|
149
|
+
docker exec <容器名> bash -lc "baize doctor --json" | python3 -c "import json,sys;print(json.load(sys.stdin)['groups']['path'])"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 5. 其他常见问题速查
|
|
155
|
+
|
|
156
|
+
| 问题 | 快速处理 |
|
|
157
|
+
|---|---|
|
|
158
|
+
| web 控制台打不开 | `pm2 logs web-console`;D51 起检查 `~/.claude/cli/lib` 是否存在(init 部署缺陷已修复,见 0.3.16) |
|
|
159
|
+
| 会话起了但任务不响应 | `tmux attach -t <claude-main\|codex-main>` 看 AI 输出;`pm2 logs scheduler` |
|
|
160
|
+
| 容器一直 restart | `docker logs <容器名>`(entrypoint 崩溃);检查 `init-state.json` |
|
|
161
|
+
| A2A 端口不通 | `docker exec <容器名> pm2 list` 看 baize-a2a;`ss -tlnp | grep 8443` |
|
package/package.json
CHANGED
|
@@ -30,11 +30,21 @@ ACR_NAMESPACE="${ACR_NAMESPACE:-baize01}"
|
|
|
30
30
|
|
|
31
31
|
# ── Build ────────────────────────────────────────────────────────────────────
|
|
32
32
|
echo "==> Building baize-core:${VERSION} (from npm @baize-ai/core@${VERSION})"
|
|
33
|
+
# Temp build context: .dockerignore is a whitelist (source stays out of the
|
|
34
|
+
# image), and the optional COPY core.tgz needs a placeholder file present —
|
|
35
|
+
# a temp dir keeps the repo worktree clean.
|
|
36
|
+
BUILD_CTX="$(mktemp -d)"
|
|
37
|
+
trap 'rm -rf "$BUILD_CTX"' EXIT
|
|
38
|
+
cp Dockerfile .dockerignore "$BUILD_CTX"/
|
|
39
|
+
mkdir -p "$BUILD_CTX/templates/pm2" "$BUILD_CTX/docker"
|
|
40
|
+
cp templates/pm2/ecosystem.config.cjs "$BUILD_CTX/templates/pm2/"
|
|
41
|
+
cp docker/entrypoint.sh "$BUILD_CTX/docker/"
|
|
42
|
+
touch "$BUILD_CTX/core.tgz" # placeholder — registry install ignores it (BAZE_CORE_TARBALL empty)
|
|
33
43
|
docker build \
|
|
34
44
|
--build-arg BAZE_CORE_VERSION="${VERSION}" \
|
|
35
45
|
-t "${GHCR_IMAGE}:${VERSION}" \
|
|
36
46
|
-t "${GHCR_IMAGE}:latest" \
|
|
37
|
-
|
|
47
|
+
"$BUILD_CTX"
|
|
38
48
|
|
|
39
49
|
if [ -n "${ACR_REGISTRY}" ] && [ -n "${ACR_NAMESPACE}" ]; then
|
|
40
50
|
ACR_IMAGE="${ACR_REGISTRY}/${ACR_NAMESPACE}/baize-core"
|
|
@@ -48,6 +48,7 @@ function createDeps(overrides = {}) {
|
|
|
48
48
|
nowMs: overrides.nowMs ?? (() => 100_000),
|
|
49
49
|
initialRuntimeLaunchAtMs: overrides.initialRuntimeLaunchAtMs ?? 0,
|
|
50
50
|
};
|
|
51
|
+
if (overrides.alert) deps.alert = overrides.alert;
|
|
51
52
|
|
|
52
53
|
return { deps, calls };
|
|
53
54
|
}
|
|
@@ -174,4 +175,54 @@ describe('Guardian', () => {
|
|
|
174
175
|
await Promise.resolve();
|
|
175
176
|
assert.deepEqual(order, ['prepare', 'launch', 'prompt']);
|
|
176
177
|
});
|
|
178
|
+
|
|
179
|
+
it('raises alert after ALERT_AFTER_RESTARTS consecutive ENOENT launch failures (D54 P2)', async () => {
|
|
180
|
+
const alerts = [];
|
|
181
|
+
const adapter = {
|
|
182
|
+
sessionName: 'test-main',
|
|
183
|
+
displayName: 'TestRuntime',
|
|
184
|
+
runtimeId: 'codex',
|
|
185
|
+
isRunning: async () => false,
|
|
186
|
+
launch: async () => { throw Object.assign(new Error('spawnSync codex ENOENT'), { code: 'ENOENT' }); },
|
|
187
|
+
clearStaleState: () => {},
|
|
188
|
+
enqueueStartupPrompt: () => {},
|
|
189
|
+
};
|
|
190
|
+
const { deps } = createDeps({ alert: (info) => alerts.push(info) });
|
|
191
|
+
const guardian = new Guardian(adapter, deps);
|
|
192
|
+
guardian._maybeAlert('spawnSync codex ENOENT'); // below threshold → no alert
|
|
193
|
+
assert.equal(alerts.length, 0);
|
|
194
|
+
|
|
195
|
+
// simulate 3 consecutive failed launches crossing the threshold
|
|
196
|
+
guardian.consecutiveRestarts = 3;
|
|
197
|
+
guardian._maybeAlert('spawnSync codex ENOENT');
|
|
198
|
+
guardian._maybeAlert('spawnSync codex ENOENT'); // no duplicate
|
|
199
|
+
assert.equal(alerts.length, 1, 'alert must fire exactly once per episode');
|
|
200
|
+
assert.equal(alerts[0].runtime, 'codex');
|
|
201
|
+
assert.equal(alerts[0].consecutive_restarts, 3);
|
|
202
|
+
assert.ok(/ENOENT/.test(alerts[0].reason));
|
|
203
|
+
|
|
204
|
+
// recovery resets alerted → next episode may alert again
|
|
205
|
+
guardian.alerted = false;
|
|
206
|
+
guardian._maybeAlert('again');
|
|
207
|
+
assert.equal(alerts.length, 2, 'alert must fire again after recovery');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('logs a repair hint on ENOENT launch failure (D54 P2)', async () => {
|
|
211
|
+
const adapter = {
|
|
212
|
+
sessionName: 'test-main',
|
|
213
|
+
displayName: 'TestRuntime',
|
|
214
|
+
runtimeId: 'codex',
|
|
215
|
+
isRunning: async () => false,
|
|
216
|
+
launch: async () => { throw Object.assign(new Error('spawnSync codex ENOENT'), { code: 'ENOENT' }); },
|
|
217
|
+
clearStaleState: () => {},
|
|
218
|
+
enqueueStartupPrompt: () => {},
|
|
219
|
+
};
|
|
220
|
+
const { deps, calls } = createDeps({ alert: () => {} });
|
|
221
|
+
const guardian = new Guardian(adapter, deps);
|
|
222
|
+
guardian.consecutiveRestarts = 0;
|
|
223
|
+
await guardian.startAgent();
|
|
224
|
+
await Promise.resolve();
|
|
225
|
+
await Promise.resolve();
|
|
226
|
+
assert.ok(calls.log.some(m => m.includes('ENOENT') && m.includes('baize init')), 'must log the repair hint');
|
|
227
|
+
});
|
|
177
228
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync, execSync } from 'child_process';
|
|
2
2
|
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
3
4
|
import { Guardian } from '../guardian.js';
|
|
4
5
|
import { HealthEngine } from '../health-engine.js';
|
|
5
6
|
import { ProcSampler } from '../proc-sampler.js';
|
|
@@ -62,6 +63,7 @@ export function createGuardian(activeAdapter, activeToolPipeline, initialRuntime
|
|
|
62
63
|
hookStateFile,
|
|
63
64
|
log,
|
|
64
65
|
}) {
|
|
66
|
+
const monitorDir = apiActivityFile ? path.dirname(apiActivityFile) : undefined;
|
|
65
67
|
return new Guardian(activeAdapter, {
|
|
66
68
|
log,
|
|
67
69
|
initialRuntimeLaunchAtMs,
|
|
@@ -76,6 +78,31 @@ export function createGuardian(activeAdapter, activeToolPipeline, initialRuntime
|
|
|
76
78
|
}));
|
|
77
79
|
fs.writeFileSync(hookStateFile, JSON.stringify({ active_tools: 0 }));
|
|
78
80
|
},
|
|
81
|
+
// D54 P2: consecutive-failure alert — write a visible file in the monitor
|
|
82
|
+
// dir (alongside agent-status.json) so ops/AI can discover an outage that
|
|
83
|
+
// Guardian keeps retrying. Cleared on recovery via onRecovered.
|
|
84
|
+
alert: (info) => {
|
|
85
|
+
if (!monitorDir) return;
|
|
86
|
+
const alertFile = path.join(monitorDir, 'guardian-alert.json');
|
|
87
|
+
try {
|
|
88
|
+
const tmp = `${alertFile}.tmp.${process.pid}`;
|
|
89
|
+
fs.writeFileSync(tmp, `${JSON.stringify(info, null, 2)}\n`);
|
|
90
|
+
fs.renameSync(tmp, alertFile);
|
|
91
|
+
log(`Guardian: ⚠ ALERT after ${info.consecutive_restarts} consecutive failures — ${info.reason}`);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
log(`Guardian: alert write failed: ${e.message}`);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
onRecovered: () => {
|
|
97
|
+
if (!monitorDir) return;
|
|
98
|
+
const alertFile = path.join(monitorDir, 'guardian-alert.json');
|
|
99
|
+
try {
|
|
100
|
+
if (fs.existsSync(alertFile)) fs.unlinkSync(alertFile);
|
|
101
|
+
log('Guardian: runtime recovered — alert cleared');
|
|
102
|
+
} catch (e) {
|
|
103
|
+
log(`Guardian: alert clear failed: ${e.message}`);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
79
106
|
});
|
|
80
107
|
}
|
|
81
108
|
|
|
@@ -5,6 +5,9 @@ export const MAX_RESTART_DELAY = 60;
|
|
|
5
5
|
export const BACKOFF_RESET_THRESHOLD = 60;
|
|
6
6
|
export const STARTUP_GRACE_TICKS = 30;
|
|
7
7
|
export const MAINTENANCE_WAIT_TIMEOUT = 300;
|
|
8
|
+
// D54 P2: after this many consecutive launch failures, raise an alert —
|
|
9
|
+
// the 09-13 incident stayed silent for 44 attempts / 48 minutes.
|
|
10
|
+
export const ALERT_AFTER_RESTARTS = 3;
|
|
8
11
|
|
|
9
12
|
export function getRunningMaintenance({ execSyncImpl = defaultExecSync } = {}) {
|
|
10
13
|
try {
|
|
@@ -76,6 +79,8 @@ export class Guardian {
|
|
|
76
79
|
execSyncImpl: defaultExecSync,
|
|
77
80
|
nowMs: () => Date.now(),
|
|
78
81
|
initialRuntimeLaunchAtMs: 0,
|
|
82
|
+
alert: () => {}, // D54 P2: consecutive-failure alert sink (default no-op)
|
|
83
|
+
onRecovered: () => {}, // D54 P2: clears the alert once the episode ends
|
|
79
84
|
...deps,
|
|
80
85
|
};
|
|
81
86
|
this.notRunningCount = 0;
|
|
@@ -84,6 +89,7 @@ export class Guardian {
|
|
|
84
89
|
this.startupGrace = 0;
|
|
85
90
|
this.startAgentInProgress = false;
|
|
86
91
|
this.runtimeLaunchAtMs = this.deps.initialRuntimeLaunchAtMs;
|
|
92
|
+
this.alerted = false; // D54 P2: raise alert at most once per outage episode
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
getState() {
|
|
@@ -135,6 +141,8 @@ export class Guardian {
|
|
|
135
141
|
} else if (currentTime - this.stableRunningSince >= BACKOFF_RESET_THRESHOLD) {
|
|
136
142
|
this.consecutiveRestarts = 0;
|
|
137
143
|
this.stableRunningSince = 0;
|
|
144
|
+
this.alerted = false; // D54 P2: outage episode over — next one may alert again
|
|
145
|
+
try { this.deps.onRecovered(); } catch (e) { this.deps.log(`Guardian: recovery callback failed: ${e.message}`); }
|
|
138
146
|
}
|
|
139
147
|
}
|
|
140
148
|
|
|
@@ -148,6 +156,29 @@ export class Guardian {
|
|
|
148
156
|
};
|
|
149
157
|
}
|
|
150
158
|
|
|
159
|
+
/**
|
|
160
|
+
* D54 P2: raise the consecutive-failure alert exactly once per outage
|
|
161
|
+
* episode (threshold ALERT_AFTER_RESTARTS). Sink is injectable — the
|
|
162
|
+
* activity-monitor wires it to a visible status/alert file.
|
|
163
|
+
*/
|
|
164
|
+
_maybeAlert(reason) {
|
|
165
|
+
if (this.alerted) return;
|
|
166
|
+
if (this.consecutiveRestarts < ALERT_AFTER_RESTARTS) return;
|
|
167
|
+
this.alerted = true;
|
|
168
|
+
try {
|
|
169
|
+
this.deps.alert({
|
|
170
|
+
agent: this.adapter.displayName,
|
|
171
|
+
runtime: this.adapter.runtimeId,
|
|
172
|
+
consecutive_restarts: this.consecutiveRestarts,
|
|
173
|
+
reason: String(reason || '').slice(0, 500),
|
|
174
|
+
at: this.deps.nowMs(),
|
|
175
|
+
atIso: new Date(this.deps.nowMs()).toISOString(),
|
|
176
|
+
});
|
|
177
|
+
} catch (e) {
|
|
178
|
+
this.deps.log(`Guardian: alert sink failed: ${e.message}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
151
182
|
_handleNotRunning({ state, message, restartLog }) {
|
|
152
183
|
if (this.startupGrace > 0) {
|
|
153
184
|
this.startupGrace -= 1;
|
|
@@ -204,6 +235,11 @@ export class Guardian {
|
|
|
204
235
|
|
|
205
236
|
this.deps.log(`Guardian: Starting ${this.adapter.displayName}...`);
|
|
206
237
|
|
|
238
|
+
// D54 P2: alert on consecutive startAgent attempts regardless of WHERE
|
|
239
|
+
// the failure lands (launch throw, or silent tmux-session-with-dead-runtime).
|
|
240
|
+
// Raised here (threshold checked inside) so every retry episode counts.
|
|
241
|
+
this._maybeAlert(`startAgent attempt #${this.consecutiveRestarts}`);
|
|
242
|
+
|
|
207
243
|
try {
|
|
208
244
|
this.adapter.clearStaleState?.();
|
|
209
245
|
} catch { }
|
|
@@ -213,7 +249,15 @@ export class Guardian {
|
|
|
213
249
|
} catch { }
|
|
214
250
|
|
|
215
251
|
const reportFailure = (err) => {
|
|
216
|
-
|
|
252
|
+
const msg = err?.message || String(err);
|
|
253
|
+
this.deps.log(`Guardian: Failed to start ${this.adapter.displayName}: ${msg}`);
|
|
254
|
+
// D54 P2: an ENOENT on launch means the runtime binary is missing from
|
|
255
|
+
// PATH (or uninstalled) — surface the repair hint instead of silently
|
|
256
|
+
// retrying; then raise an alert once the failure count crosses the bar.
|
|
257
|
+
if (err?.code === 'ENOENT' || /ENOENT/.test(msg)) {
|
|
258
|
+
this.deps.log(`Guardian: runtime binary not found (ENOENT) — run "baize init" to repair PATH/binaries, or reinstall the ${this.adapter.displayName} runtime`);
|
|
259
|
+
}
|
|
260
|
+
this._maybeAlert(msg);
|
|
217
261
|
};
|
|
218
262
|
const launchPrepared = () => {
|
|
219
263
|
try {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
2
3
|
import {
|
|
3
4
|
WATCHDOG_INTERRUPT_AVAILABLE_IN_SEC,
|
|
4
5
|
evaluateToolWatchdogTransition,
|
|
@@ -51,7 +52,13 @@ export class MonitorOrchestrator {
|
|
|
51
52
|
const runtimeLaunchAtMs = Number(initialStatus.runtime_launch_at) || nowMs();
|
|
52
53
|
|
|
53
54
|
const engine = createHealthEngine(adapter, initialStatus);
|
|
54
|
-
|
|
55
|
+
// D54 P2: pass the alert sink wiring (apiActivityFile/hookStateFile/log)
|
|
56
|
+
// so Guardian's consecutive-failure alert writes guardian-alert.json.
|
|
57
|
+
const guardian = createGuardian(adapter, toolPipeline, runtimeLaunchAtMs, {
|
|
58
|
+
apiActivityFile: path.join(monitorDir, 'api-activity.json'),
|
|
59
|
+
hookStateFile: path.join(monitorDir, 'hook-state.json'),
|
|
60
|
+
log,
|
|
61
|
+
});
|
|
55
62
|
|
|
56
63
|
if (initialHealth === 'rate_limited' && initialStatus.cooldown_until) {
|
|
57
64
|
engine.enterRateLimited(initialStatus.cooldown_until, initialStatus.rate_limit_reset || '');
|
|
@@ -10,6 +10,7 @@ import { execFileSync } from 'child_process';
|
|
|
10
10
|
import fs from 'fs';
|
|
11
11
|
import path from 'path';
|
|
12
12
|
import os from 'os';
|
|
13
|
+
import { completeBinPath } from '../../../cli/lib/path-bins.js';
|
|
13
14
|
|
|
14
15
|
const BAIZE_DIR = process.env.BAIZE_DIR || path.join(os.homedir(), 'baize');
|
|
15
16
|
const MONITOR_DIR = path.join(BAIZE_DIR, 'activity-monitor');
|
|
@@ -93,7 +94,8 @@ function main() {
|
|
|
93
94
|
// Check baize-core
|
|
94
95
|
try {
|
|
95
96
|
const coreVersion = execFileSync('baize', ['--version'], {
|
|
96
|
-
encoding: 'utf8', stdio: 'pipe', timeout: 5000
|
|
97
|
+
encoding: 'utf8', stdio: 'pipe', timeout: 5000,
|
|
98
|
+
env: { ...process.env, PATH: completeBinPath() },
|
|
97
99
|
}).trim();
|
|
98
100
|
const result = getLatestTag('baize-ai/baize-core');
|
|
99
101
|
if (result.error) {
|
|
@@ -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 +
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
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),
|