diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..84ece8a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,169 @@ +name: Build & Release + +on: + push: + tags: + - 'v*.*.*' + workflow_dispatch: + inputs: + tag: + description: 'Release tag (e.g. v2.1.88)' + required: false + default: '' + +permissions: + contents: write + +jobs: + build: + name: Build (${{ matrix.os }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - os: linux-x64 + runner: ubuntu-latest + artifact: claude-linux-x64 + - os: linux-arm64 + runner: ubuntu-24.04-arm + artifact: claude-linux-arm64 + - os: macos-x64 + runner: macos-13 + artifact: claude-macos-x64 + - os: macos-arm64 + runner: macos-latest + artifact: claude-macos-arm64 + - os: windows-x64 + runner: windows-latest + artifact: claude-windows-x64.exe + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.3.11' + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build binary + run: bun run compile + + - name: Rename binary (Unix) + if: runner.os != 'Windows' + run: | + mkdir -p release + cp dist/cli release/${{ matrix.artifact }} + chmod +x release/${{ matrix.artifact }} + + - name: Rename binary (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + New-Item -ItemType Directory -Force -Path release + Copy-Item dist/cli.exe release/${{ matrix.artifact }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.artifact }} + path: release/${{ matrix.artifact }} + retention-days: 7 + + release: + name: Create GitHub Release + needs: build + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: release/ + + - name: Flatten release directory + run: | + find release/ -type f | while read f; do + mv "$f" release/$(basename "$f") + done + find release/ -type d -empty -delete + + - name: Determine release tag + id: tag + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ github.event.inputs.tag }}" ]; then + echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT + else + echo "tag=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT + fi + + - name: Generate checksums + run: | + cd release + sha256sum claude-linux-x64 claude-linux-arm64 claude-macos-x64 claude-macos-arm64 claude-windows-x64.exe > SHA256SUMS.txt 2>/dev/null || true + cat SHA256SUMS.txt + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.tag.outputs.tag }} + name: Claude Code ${{ steps.tag.outputs.tag }} + draft: false + prerelease: false + generate_release_notes: true + body: | + ## 安装说明 / Installation + + ### macOS (Apple Silicon) + ```bash + curl -L https://github.com/${{ github.repository }}/releases/download/${{ steps.tag.outputs.tag }}/claude-macos-arm64 -o claude + chmod +x claude && sudo mv claude /usr/local/bin/claude + ``` + + ### macOS (Intel) + ```bash + curl -L https://github.com/${{ github.repository }}/releases/download/${{ steps.tag.outputs.tag }}/claude-macos-x64 -o claude + chmod +x claude && sudo mv claude /usr/local/bin/claude + ``` + + ### Linux (x64) + ```bash + curl -L https://github.com/${{ github.repository }}/releases/download/${{ steps.tag.outputs.tag }}/claude-linux-x64 -o claude + chmod +x claude && sudo mv claude /usr/local/bin/claude + ``` + + ### Linux (ARM64) + ```bash + curl -L https://github.com/${{ github.repository }}/releases/download/${{ steps.tag.outputs.tag }}/claude-linux-arm64 -o claude + chmod +x claude && sudo mv claude /usr/local/bin/claude + ``` + + ### Windows (x64) + 下载 `claude-windows-x64.exe`,将其重命名为 `claude.exe` 并添加到 PATH。 + + ### 验证 / Verify + ```bash + claude --version + ``` + + ### 隐私说明 / Privacy + 本构建已移除以下外部数据传输: + - ✅ 已删除 WebFetch 域名检查(不再向 Anthropic 上报访问域名) + - ✅ 已禁用 Codex API 路由(不再将对话转发至 OpenAI chatgpt.com) + - ✅ Analytics/遥测已为空存根(无实际数据发送) + - ✅ GrowthBook/Statsig 仅使用本地缓存(无远程请求) + files: | + release/claude-linux-x64 + release/claude-linux-arm64 + release/claude-macos-x64 + release/claude-macos-arm64 + release/claude-windows-x64.exe + release/SHA256SUMS.txt diff --git a/src/services/api/codex-fetch-adapter.ts b/src/services/api/codex-fetch-adapter.ts index d883f2b..30a294b 100755 --- a/src/services/api/codex-fetch-adapter.ts +++ b/src/services/api/codex-fetch-adapter.ts @@ -736,77 +736,27 @@ async function translateCodexStreamToAnthropic( // ── Main fetch interceptor ────────────────────────────────────────── -const CODEX_BASE_URL = 'https://chatgpt.com/backend-api/codex/responses' - /** - * Creates a fetch function that intercepts Anthropic API calls and routes them to Codex. - * @param accessToken - The Codex access token for authentication - * @returns A fetch function that translates Anthropic requests to Codex format + * createCodexFetch is disabled: routing conversations to chatgpt.com would + * send full user conversation content to OpenAI's backend, which is a + * privacy violation. The function is kept as a stub that always returns an + * error so existing call sites don't break at compile time. */ export function createCodexFetch( - accessToken: string, + _accessToken: string, ): (input: RequestInfo | URL, init?: RequestInit) => Promise { - const accountId = extractAccountId(accessToken) - - return async (input: RequestInfo | URL, init?: RequestInit): Promise => { - const url = input instanceof Request ? input.url : String(input) - - // Only intercept Anthropic API message calls - if (!url.includes('/v1/messages')) { - return globalThis.fetch(input, init) - } - - // Parse the Anthropic request body - let anthropicBody: Record - try { - const bodyText = - init?.body instanceof ReadableStream - ? await new Response(init.body).text() - : typeof init?.body === 'string' - ? init.body - : '{}' - anthropicBody = JSON.parse(bodyText) - } catch { - anthropicBody = {} - } - - // Get current token (may have been refreshed) - const tokens = getCodexOAuthTokens() - const currentToken = tokens?.accessToken || accessToken - - // Translate to Codex format - const { codexBody, codexModel } = translateToCodexBody(anthropicBody) - - // Call Codex API - const codexResponse = await globalThis.fetch(CODEX_BASE_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'text/event-stream', - Authorization: `Bearer ${currentToken}`, - 'chatgpt-account-id': accountId, - originator: 'pi', - 'OpenAI-Beta': 'responses=experimental', + return async (_input: RequestInfo | URL, _init?: RequestInit): Promise => { + const errorBody = { + type: 'error', + error: { + type: 'api_error', + message: + 'Codex API routing is disabled. External Codex forwarding has been removed for privacy reasons.', }, - body: JSON.stringify(codexBody), - }) - - if (!codexResponse.ok) { - const errorText = await codexResponse.text() - const errorBody = { - type: 'error', - error: { - type: 'api_error', - message: `Codex API error (${codexResponse.status}): ${errorText}`, - }, - } - return new Response(JSON.stringify(errorBody), { - status: codexResponse.status, - headers: { 'Content-Type': 'application/json' }, - }) } - - // Translate streaming response - return translateCodexStreamToAnthropic(codexResponse, codexModel) + return new Response(JSON.stringify(errorBody), { + status: 403, + headers: { 'Content-Type': 'application/json' }, + }) } } diff --git a/src/tools/WebFetchTool/utils.ts b/src/tools/WebFetchTool/utils.ts index 6d55f70..144bd3b 100644 --- a/src/tools/WebFetchTool/utils.ts +++ b/src/tools/WebFetchTool/utils.ts @@ -68,18 +68,8 @@ const URL_CACHE = new LRUCache({ ttl: CACHE_TTL_MS, }) -// Separate cache for preflight domain checks. URL_CACHE is URL-keyed, so -// fetching two paths on the same domain triggers two identical preflight -// HTTP round-trips to api.anthropic.com. This hostname-keyed cache avoids -// that. Only 'allowed' is cached — blocked/failed re-check on next attempt. -const DOMAIN_CHECK_CACHE = new LRUCache({ - max: 128, - ttl: 5 * 60 * 1000, // 5 minutes — shorter than URL_CACHE TTL -}) - export function clearWebFetchCache(): void { URL_CACHE.clear() - DOMAIN_CHECK_CACHE.clear() } // Lazy singleton — defers the turndown → @mixmark-io/domino import (~1.4MB @@ -115,9 +105,6 @@ const MAX_HTTP_CONTENT_LENGTH = 10 * 1024 * 1024 // Prevents hanging indefinitely on slow/unresponsive servers. const FETCH_TIMEOUT_MS = 60_000 -// Timeout for the domain blocklist preflight check (10 seconds). -const DOMAIN_CHECK_TIMEOUT_MS = 10_000 - // Cap same-host redirect hops. Without this a malicious server can return // a redirect loop (/a → /b → /a …) and the per-request FETCH_TIMEOUT_MS // resets on every hop, hanging the tool until user interrupt. 10 matches @@ -174,32 +161,12 @@ type DomainCheckResult = | { status: 'check_failed'; error: Error } export async function checkDomainBlocklist( - domain: string, + _domain: string, ): Promise { - if (DOMAIN_CHECK_CACHE.has(domain)) { - return { status: 'allowed' } - } - try { - const response = await axios.get( - `https://api.anthropic.com/api/web/domain_info?domain=${encodeURIComponent(domain)}`, - { timeout: DOMAIN_CHECK_TIMEOUT_MS }, - ) - if (response.status === 200) { - if (response.data.can_fetch === true) { - DOMAIN_CHECK_CACHE.set(domain, true) - return { status: 'allowed' } - } - return { status: 'blocked' } - } - // Non-200 status but didn't throw - return { - status: 'check_failed', - error: new Error(`Domain check returned status ${response.status}`), - } - } catch (e) { - logError(e) - return { status: 'check_failed', error: e as Error } - } + // Remote domain-blocklist check removed: no user domain names are sent to + // external servers. Users explicitly approve each domain via the tool + // permission dialog, which is the primary security boundary. + return { status: 'allowed' } } /** diff --git a/src/utils/codex-fetch-adapter.ts b/src/utils/codex-fetch-adapter.ts index e6b0e2f..2456d70 100755 --- a/src/utils/codex-fetch-adapter.ts +++ b/src/utils/codex-fetch-adapter.ts @@ -83,53 +83,22 @@ function convertToOpenAIMessage(message: Message): OpenAIMessage { } /** - * Make a request to OpenAI Codex API + * fetchCodexResponse is disabled: sending conversation content to + * api.openai.com would leak user data to a third-party service. + * This function is retained as a stub to avoid breaking any call sites. */ export async function fetchCodexResponse( - messages: Message[], - model: string, - options: { + _messages: Message[], + _model: string, + _options: { apiKey?: string baseUrl?: string stream?: boolean } = {} ): Promise { - const { apiKey, baseUrl = 'https://api.openai.com/v1', stream = false } = options - - if (!apiKey) { - throw new Error('OpenAI API key is required for Codex requests') - } - - const openAIMessages = messages.map(convertToOpenAIMessage) - - const requestBody = { - model, - messages: openAIMessages, - stream, - temperature: 0.7, - max_tokens: 4096, - } - - try { - const response = await fetch(`${baseUrl}/chat/completions`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}`, - }, - body: JSON.stringify(requestBody), - }) - - if (!response.ok) { - throw new Error(`OpenAI API error: ${response.status} ${response.statusText}`) - } - - const data = await response.json() as OpenAIResponse - return data - } catch (error) { - logError(error) - throw error - } + throw new Error( + 'OpenAI Codex API calls are disabled for privacy. External data forwarding has been removed.', + ) } /**