pipeline executor improvements, proxy refresh, task runner step list

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 07:25:11 +00:00
co-authored by Claude Opus 4.6
parent 0f1e1f35f1
commit 92da03fcff
6 changed files with 280 additions and 86 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ import { buildSandboxPrefix, buildRunuserSuffix, SANDBOX_HOME } from '../sandbox
import { setClaudeSession, clearClaudeSession, getClaudeSession } from './state';
import { parseStream } from './stream-parser';
const SEND_TIMEOUT_MS = 5 * 60 * 1000;
const SEND_TIMEOUT_MS = 30 * 60 * 1000;
// Use /usr/local/bin/claude so it's visible inside bwrap sandbox (which ro-binds /usr).
// The actual binary lives at ~/.local/bin/claude, symlinked from /usr/local/bin/claude.
+152 -9
View File
@@ -5,24 +5,119 @@ import { getState, updateState } from './state';
const PROXY_PORT = Number(process.env.ANTHROPIC_PROXY_PORT ?? '5051');
const ANTHROPIC_API_BASE = 'https://api.anthropic.com';
const CREDENTIALS_PATH = join(homedir(), '.claude', '.credentials.json');
const TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
const CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
type CredentialsFile = {
claudeAiOauth?: {
accessToken?: string;
};
// Buffer: refresh 5 minutes before expiry
const EXPIRY_BUFFER_MS = 5 * 60 * 1000;
type OAuthCredentials = {
accessToken?: string;
refreshToken?: string;
expiresAt?: number;
};
async function readOAuthToken(): Promise<string | null> {
type CredentialsFile = {
claudeAiOauth?: OAuthCredentials;
};
async function readCredentials(): Promise<CredentialsFile | null> {
try {
const file = Bun.file(CREDENTIALS_PATH);
if (!(await file.exists())) return null;
const data = (await file.json()) as CredentialsFile;
return data.claudeAiOauth?.accessToken?.trim() || null;
return (await file.json()) as CredentialsFile;
} catch {
return null;
}
}
async function writeCredentials(creds: CredentialsFile): Promise<void> {
try {
await Bun.write(CREDENTIALS_PATH, JSON.stringify(creds, null, 2));
} catch (err) {
console.error('[claude:proxy] failed to write credentials:', err);
}
}
async function refreshOAuthToken(refreshToken: string): Promise<OAuthCredentials | null> {
try {
console.log('[claude:proxy] refreshing OAuth token...');
const res = await fetch(TOKEN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: CLIENT_ID,
}),
});
if (!res.ok) {
const text = await res.text();
console.error(`[claude:proxy] token refresh failed (${res.status}): ${text}`);
return null;
}
const data = (await res.json()) as { access_token?: string; refresh_token?: string; expires_in?: number };
if (!data.access_token) {
console.error('[claude:proxy] token refresh response missing access_token');
return null;
}
const expiresAt = data.expires_in ? Date.now() + data.expires_in * 1000 : undefined;
console.log(`[claude:proxy] token refreshed, expires in ${data.expires_in ?? '?'}s`);
return {
accessToken: data.access_token,
refreshToken: data.refresh_token ?? refreshToken,
expiresAt,
};
} catch (err) {
console.error('[claude:proxy] token refresh error:', err);
return null;
}
}
async function getValidToken(): Promise<string | null> {
const creds = await readCredentials();
if (!creds?.claudeAiOauth) return null;
const oauth = creds.claudeAiOauth;
const now = Date.now();
// Check if token is still valid (with buffer)
if (oauth.accessToken && oauth.expiresAt && oauth.expiresAt - now > EXPIRY_BUFFER_MS) {
return oauth.accessToken.trim();
}
// Token expired or about to expire — try refresh
if (oauth.refreshToken) {
const refreshed = await refreshOAuthToken(oauth.refreshToken);
if (refreshed?.accessToken) {
creds.claudeAiOauth = { ...oauth, ...refreshed };
await writeCredentials(creds);
return refreshed.accessToken.trim();
}
}
// Fallback: return current token even if potentially expired (let upstream decide)
return oauth.accessToken?.trim() || null;
}
async function forceRefresh(): Promise<string | null> {
const creds = await readCredentials();
const refreshToken = creds?.claudeAiOauth?.refreshToken;
if (!refreshToken) return null;
const refreshed = await refreshOAuthToken(refreshToken);
if (refreshed?.accessToken && creds?.claudeAiOauth) {
creds.claudeAiOauth = { ...creds.claudeAiOauth, ...refreshed };
await writeCredentials(creds);
return refreshed.accessToken;
}
return null;
}
export function getProxySecret(): string {
return getState().proxySecret;
}
@@ -48,6 +143,18 @@ export function startAnthropicProxy() {
async fetch(req) {
const url = new URL(req.url);
// Internal refresh endpoint (no auth needed — localhost only)
if (url.pathname === '/refresh') {
const token = await forceRefresh();
if (token) {
return new Response(JSON.stringify({ ok: true }), { headers: { 'Content-Type': 'application/json' } });
}
return new Response(JSON.stringify({ error: 'Refresh failed' }), {
status: 502,
headers: { 'Content-Type': 'application/json' },
});
}
// Validate proxy secret
const incomingKey = req.headers.get('x-api-key');
if (incomingKey !== secret) {
@@ -57,8 +164,8 @@ export function startAnthropicProxy() {
});
}
// Read fresh OAuth token
const token = await readOAuthToken();
// Get valid token (refreshes if needed)
const token = await getValidToken();
if (!token) {
return new Response(JSON.stringify({ error: 'No OAuth token available' }), {
status: 502,
@@ -84,6 +191,42 @@ export function startAnthropicProxy() {
body,
});
// If upstream returns 401, try one refresh and retry
if (upstreamRes.status === 401) {
const creds = await readCredentials();
const refreshToken = creds?.claudeAiOauth?.refreshToken;
if (refreshToken) {
const refreshed = await refreshOAuthToken(refreshToken);
if (refreshed?.accessToken) {
if (creds?.claudeAiOauth) {
creds.claudeAiOauth = { ...creds.claudeAiOauth, ...refreshed };
await writeCredentials(creds);
}
headers.set('x-api-key', refreshed.accessToken.trim());
const retryBody = body ? new Uint8Array(body) : null;
const retryRes = await fetch(upstream, {
method: req.method,
headers,
body: retryBody,
});
const retryHeaders = new Headers();
for (const [key, value] of retryRes.headers) {
const lower = key.toLowerCase();
if (lower === 'content-encoding' || lower === 'content-length' || lower === 'transfer-encoding') continue;
retryHeaders.set(key, value);
}
return new Response(retryRes.body, {
status: retryRes.status,
statusText: retryRes.statusText,
headers: retryHeaders,
});
}
}
}
// Build clean response headers
const resHeaders = new Headers();
for (const [key, value] of upstreamRes.headers) {