fix(worker): env 收敛改黑名单 + 不 detached —— 修 worker 'Not logged in'
cutover 实测:worker 起的 claude 报 Not logged in。根因:① 严格 env allowlist 把 macOS keychain/Security 访问所需的会话变量(USER/LOGNAME/__CF_USER_TEXT_ENCODING/TMPDIR/XPC_* 等, 均非密钥)也滤掉了;本机鉴权走 keychain(无 API_KEY/凭证文件) → 读不到。 ② detached(setsid)另起会话也不利于沿用 daemon 鉴权上下文。 修:workerEnv 改黑名单(会话变量透传保 keychain,仅剥 *_SECRET/*_TOKEN/*_KEY/AWS_/CF_ 等密钥, ANTHROPIC_/CLAUDE_ 始终留);spawn 不 detached(同会话,unref 后孤儿仍被 init 收养存活)。 实测:smoke 任务 worker 真建文件+提交、双复审 approve、到 exec_review。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -31,26 +31,36 @@ export function workerEntry(): string[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* spawn worker 时传给子进程的最小 env:只保留 PATH/HOME/LANG 与 ANTHROPIC_ / CLAUDE_ 前缀变量,
|
||||
* 不把 daemon 全量 env(含 MAESTRO_ 控制变量、无关密钥)漏给独立 worker 进程。
|
||||
* MAESTRO_DATA_DIR 例外透传——worker 必须和 daemon 看同一个 runs/ 目录(job.json/outbox 路径一致)。
|
||||
* spawn worker 时传给子进程的 env:透传完整会话/系统环境,仅【剥离敏感密钥类变量】。
|
||||
* 为什么不用严格 allowlist:本机鉴权走 macOS keychain(无 API_KEY/凭证文件),而 Security/keychain
|
||||
* 访问依赖一批会话变量(USER/LOGNAME/__CF_USER_TEXT_ENCODING/TMPDIR/XPC_* 等,均非密钥);只放
|
||||
* PATH/HOME/LANG 会让 keychain 读不到 → "Not logged in"。故改为黑名单:会话变量照常透传、保 keychain 可用,
|
||||
* 同时剥掉 *_SECRET/*_TOKEN/*_KEY/AWS_/CF_ 等真正的密钥(鉴权用的 ANTHROPIC_/CLAUDE_ 始终保留)。
|
||||
*/
|
||||
const KEEP_AUTH = (k: string): boolean => k.startsWith('ANTHROPIC_') || k.startsWith('CLAUDE_') || k === 'CLAUDECODE';
|
||||
const SECRET_KEY_RE = /SECRET|PASSWORD|PASSWD|PRIVATE_KEY|CREDENTIAL|_TOKEN|API[_-]?KEY|ACCESS_KEY|^AWS_|^CF_|^GITHUB|^GH_|^NPM_/i;
|
||||
export function workerEnv(src: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
||||
const out: NodeJS.ProcessEnv = {};
|
||||
for (const k of ['PATH', 'HOME', 'LANG', 'MAESTRO_DATA_DIR', 'MAESTRO_WORKER_CMD'] as const) {
|
||||
if (src[k] !== undefined) out[k] = src[k];
|
||||
}
|
||||
for (const [k, v] of Object.entries(src)) {
|
||||
if (v !== undefined && (k.startsWith('ANTHROPIC_') || k.startsWith('CLAUDE_'))) out[k] = v;
|
||||
if (v === undefined) continue;
|
||||
if (KEEP_AUTH(k)) { out[k] = v; continue; } // 鉴权变量始终保留(即便名含 TOKEN/KEY)
|
||||
if (SECRET_KEY_RE.test(k)) continue; // 真正的密钥类:剥离
|
||||
out[k] = v; // 其余(会话/系统变量)透传 → keychain 可用
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 默认 spawnWorker:detached + 脱离 stdio + 收敛 env,unref 后自存活(daemon 退出不杀 worker)。返回子进程 pid。 */
|
||||
/**
|
||||
* 默认 spawnWorker:unref + 脱离 stdio + 收敛 env,返回子进程 pid。
|
||||
* 关键:【不 detached(不 setsid)】——保持与 daemon 同会话,worker 内起的 `claude` 才能沿用
|
||||
* daemon 的鉴权上下文(本机无 API_KEY/凭证文件,鉴权走 macOS keychain;detached 后新会话读不到 → Not logged in)。
|
||||
* 存活性不依赖 detached:unref 后 daemon 退出,worker 作为孤儿被 init/launchd 收养、继续运行(daemon 无控制终端,
|
||||
* 不会有进程组 SIGHUP;重启用单 pid kill 不波及 worker)。
|
||||
*/
|
||||
function defaultSpawnWorker(runId: string): number {
|
||||
const [cmd, ...args] = workerEntry();
|
||||
const child = spawn(cmd, [...args, runId], {
|
||||
detached: true,
|
||||
detached: false,
|
||||
stdio: 'ignore',
|
||||
env: workerEnv(),
|
||||
});
|
||||
|
||||
+17
-10
@@ -68,22 +68,29 @@ test('workerEntry:MAESTRO_WORKER_CMD 覆盖整条命令(空格分隔)', ()
|
||||
else process.env.MAESTRO_WORKER_CMD = prev;
|
||||
});
|
||||
|
||||
test('workerEnv:只透传 PATH/HOME/LANG + ANTHROPIC_/CLAUDE_ 前缀;丢弃无关与 MAESTRO_ 控制变量', () => {
|
||||
test('workerEnv:透传会话/系统变量(keychain 可用),剥离密钥,保留鉴权变量', () => {
|
||||
const env = workerEnv({
|
||||
PATH: '/usr/bin', HOME: '/home/u', LANG: 'en_US.UTF-8',
|
||||
ANTHROPIC_API_KEY: 'sk-x', CLAUDE_CODE_FOO: 'y',
|
||||
USER: 'wj', LOGNAME: 'wj', __CF_USER_TEXT_ENCODING: '0x1F5:0:0', TMPDIR: '/tmp', XPC_SERVICE_NAME: '0',
|
||||
ANTHROPIC_API_KEY: 'sk-x', CLAUDE_CODE_OAUTH_TOKEN: 'oauth-x', CLAUDE_CODE_FOO: 'y',
|
||||
MAESTRO_ORCH_INTERVAL: '15', MAESTRO_DATA_DIR: '/data',
|
||||
SOME_SECRET: 'leak', AWS_SECRET_ACCESS_KEY: 'nope',
|
||||
SOME_SECRET: 'leak', AWS_SECRET_ACCESS_KEY: 'nope', GITHUB_TOKEN: 'gh', CF_API_TOKEN: 'cf',
|
||||
});
|
||||
assert.equal(env.PATH, '/usr/bin');
|
||||
assert.equal(env.HOME, '/home/u');
|
||||
assert.equal(env.LANG, 'en_US.UTF-8');
|
||||
// 会话/系统变量透传(macOS keychain/Security 访问需要它们)
|
||||
for (const k of ['PATH', 'HOME', 'LANG', 'USER', 'LOGNAME', '__CF_USER_TEXT_ENCODING', 'TMPDIR', 'XPC_SERVICE_NAME']) {
|
||||
assert.ok(env[k] !== undefined, `会话变量 ${k} 应透传`);
|
||||
}
|
||||
// 鉴权变量始终保留(即便名含 TOKEN/KEY)
|
||||
assert.equal(env.ANTHROPIC_API_KEY, 'sk-x');
|
||||
assert.equal(env.CLAUDE_CODE_OAUTH_TOKEN, 'oauth-x');
|
||||
assert.equal(env.CLAUDE_CODE_FOO, 'y');
|
||||
assert.equal(env.MAESTRO_DATA_DIR, '/data'); // 例外:worker 须看同一 runs/ 目录
|
||||
assert.equal(env.MAESTRO_ORCH_INTERVAL, undefined, 'daemon 控制变量不下传');
|
||||
assert.equal(env.SOME_SECRET, undefined, '无关变量不下传');
|
||||
assert.equal(env.AWS_SECRET_ACCESS_KEY, undefined, '无关密钥不下传');
|
||||
assert.equal(env.MAESTRO_DATA_DIR, '/data'); // worker 须看同一 runs/ 目录
|
||||
assert.equal(env.MAESTRO_ORCH_INTERVAL, '15'); // 非密钥 → 透传(黑名单语义)
|
||||
// 真正的密钥类被剥离
|
||||
assert.equal(env.SOME_SECRET, undefined, '*_SECRET 剥离');
|
||||
assert.equal(env.AWS_SECRET_ACCESS_KEY, undefined, 'AWS 密钥剥离');
|
||||
assert.equal(env.GITHUB_TOKEN, undefined, 'GITHUB_TOKEN 剥离');
|
||||
assert.equal(env.CF_API_TOKEN, undefined, 'CF token 剥离');
|
||||
});
|
||||
|
||||
// ───────────────────────── tick 顺序:ingest + reaper 先于领取 ─────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user