mirror of
https://github.com/dtzp555-max/olp.git
synced 2026-07-19 09:45:07 +00:00
fix(anthropic): stop injecting env.CLAUDE_CODE_OAUTH_TOKEN — let CLI auto-refresh
OLP was injecting accessToken from ~/.claude/.credentials.json into the spawn env as CLAUDE_CODE_OAUTH_TOKEN unconditionally. This defeated claude CLI's built-in OAuth refresh: when the env var is present, the CLI reads it directly and never touches credentials.json, so expired accessTokens were never swapped using the refreshToken sitting right there in the file. Result: hard 401 cascade every ~8h (access token TTL) requiring manual `security find-generic-password → scp → PI231` cycle. Empirical 2026-05-28 spike on PI231 v2.1.104: - expiresAt = 1 min ago (simulated expiry) - spawn claude -p WITHOUT CLAUDE_CODE_OAUTH_TOKEN env - response succeeded, AND credentials.json.expiresAt advanced to ~8h in the future - token refresh handled internally by claude CLI Fix: remove the env injection in _spawnAndStream. buildSpawnEnv still forwards process.env (minus ANTHROPIC_*), so if the operator explicitly sets CLAUDE_CODE_OAUTH_TOKEN at OLP boot, it's still honored — for users on ephemeral CI / no-file-creds setups. The readAuthArtifact() call remains as a preflight check (verify creds exist before spawn) but the read value is no longer forwarded. Tests: 793 → 795 - 41h: regression guard — no CLAUDE_CODE_OAUTH_TOKEN in env when only file/keychain auth is available - 41h-2: operator-set process.env passthrough still works User-facing impact: bot-down-every-8-hours issue resolved. Hermes + OpenClaw clients no longer require periodic keychain → PI231 copy. Authority: - empirical PI231 v2.1.104 spike 2026-05-28 (no public CLI doc cites refresh behavior; verified by simulating expiry + observing file update post-spawn) - ADR 0009 Amendment 1 § "Caveats" — implementation must remain robust to OAuth flow changes (this fix removes a hard-coded assumption that was actively harmful) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -17703,4 +17703,71 @@ describe('Suite 41 — ADR 0009 Amendment 1: stream-json transport (Phase 6)', (
|
||||
// model value must follow immediately after --model
|
||||
assert.equal(args[modelIdx + 1], 'claude-opus-4-7', '--model value must follow the flag');
|
||||
});
|
||||
|
||||
// ── 41h: OAuth env-injection regression guard (2026-05-28) ───────────
|
||||
// Previously _spawnAndStream injected env.CLAUDE_CODE_OAUTH_TOKEN from
|
||||
// readAuthArtifact() unconditionally. That defeated the CLI's built-in
|
||||
// refresh: with env var present, claude never reads credentials.json,
|
||||
// so expired accessTokens are never swapped via refreshToken. Result:
|
||||
// hard 401 cascade every ~8h requiring manual keychain copy.
|
||||
// Fix: don't inject. Spawned process inherits process.env via
|
||||
// buildSpawnEnv; the CLI handles file + refresh natively.
|
||||
|
||||
it('41h: _spawnAndStream does NOT set env.CLAUDE_CODE_OAUTH_TOKEN when only file/keychain auth available', async () => {
|
||||
// Capture the env passed to spawn
|
||||
let capturedEnv = null;
|
||||
const fakeSpawn = (_bin, _args, opts) => {
|
||||
capturedEnv = opts.env;
|
||||
// Minimal valid NDJSON: result/success → stop
|
||||
return makeMockSpawnNDJSON([], { skipEarlySpawn: false })(_bin, _args, opts);
|
||||
};
|
||||
// Ensure the process.env CLAUDE_CODE_OAUTH_TOKEN is unset for this test
|
||||
const sentinel = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
||||
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
||||
__setSpawnImpl(fakeSpawn);
|
||||
try {
|
||||
const ir = makeIR({
|
||||
model: 'claude-sonnet-4-6',
|
||||
messages: [{ role: 'user', content: 'hi' }],
|
||||
});
|
||||
// Provide auth context directly so readAuthArtifact path doesn't matter
|
||||
const it = anthropic.spawn(ir, { accessToken: 'sentinel-should-not-leak-to-env' });
|
||||
try { for await (const _ of it) { /* drain */ } } catch { /* ignored */ }
|
||||
assert.ok(capturedEnv, 'spawn must have been called');
|
||||
assert.equal(
|
||||
capturedEnv.CLAUDE_CODE_OAUTH_TOKEN, undefined,
|
||||
'env.CLAUDE_CODE_OAUTH_TOKEN must NOT be injected — CLI reads credentials.json natively and auto-refreshes',
|
||||
);
|
||||
} finally {
|
||||
__resetSpawnImpl();
|
||||
if (sentinel !== undefined) process.env.CLAUDE_CODE_OAUTH_TOKEN = sentinel;
|
||||
}
|
||||
});
|
||||
|
||||
it('41h-2: _spawnAndStream forwards process.env.CLAUDE_CODE_OAUTH_TOKEN when explicitly set at OLP boot', async () => {
|
||||
let capturedEnv = null;
|
||||
const fakeSpawn = (_bin, _args, opts) => {
|
||||
capturedEnv = opts.env;
|
||||
return makeMockSpawnNDJSON([], { skipEarlySpawn: false })(_bin, _args, opts);
|
||||
};
|
||||
const sentinel = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
||||
process.env.CLAUDE_CODE_OAUTH_TOKEN = 'operator-explicit-token';
|
||||
__setSpawnImpl(fakeSpawn);
|
||||
try {
|
||||
const ir = makeIR({
|
||||
model: 'claude-sonnet-4-6',
|
||||
messages: [{ role: 'user', content: 'hi' }],
|
||||
});
|
||||
const it = anthropic.spawn(ir, { accessToken: 'from-file' });
|
||||
try { for await (const _ of it) { } } catch { }
|
||||
assert.equal(
|
||||
capturedEnv.CLAUDE_CODE_OAUTH_TOKEN, 'operator-explicit-token',
|
||||
'process.env override must propagate through buildSpawnEnv unchanged',
|
||||
);
|
||||
} finally {
|
||||
__resetSpawnImpl();
|
||||
if (sentinel === undefined) delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
||||
else process.env.CLAUDE_CODE_OAUTH_TOKEN = sentinel;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user