privacy: remove external data transmissions & add GitHub release workflow
Remove three active external data transmission paths:
1. WebFetch domain blocklist (api.anthropic.com/api/web/domain_info)
- src/tools/WebFetchTool/utils.ts
- Was sending every domain a user tried to fetch to Anthropic
- Replaced with always-allowed stub; tool permission dialog is
the primary security boundary
2. Codex API router (chatgpt.com/backend-api/codex/responses)
- src/services/api/codex-fetch-adapter.ts
- Would have forwarded full conversation content to OpenAI
- createCodexFetch now returns HTTP 403 stub
3. OpenAI API adapter (api.openai.com/v1/chat/completions)
- src/utils/codex-fetch-adapter.ts
- Would have forwarded messages to OpenAI
- fetchCodexResponse now throws immediately
Already-disabled paths (no changes needed):
- Analytics logEvent/logEventAsync: empty stubs in services/analytics/index.ts
- GrowthBook/Statsig: local cache only, no outbound requests
- Auto-updater GCS: already guarded by CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC
- MCP registry: already guarded by CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC
- Release notes GitHub: already guarded by isEssentialTrafficOnly()
Add .github/workflows/release.yml:
- Builds self-contained binaries for macOS (x64+arm64), Linux (x64+arm64),
Windows (x64) using bun compile on each native runner
- Triggers on version tags (v*.*.*) or manual workflow_dispatch
- Publishes binaries + SHA256SUMS.txt as a GitHub Release with
per-platform install instructions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
169
.github/workflows/release.yml
vendored
Normal file
169
.github/workflows/release.yml
vendored
Normal file
@@ -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
|
||||||
@@ -736,77 +736,27 @@ async function translateCodexStreamToAnthropic(
|
|||||||
|
|
||||||
// ── Main fetch interceptor ──────────────────────────────────────────
|
// ── 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.
|
* createCodexFetch is disabled: routing conversations to chatgpt.com would
|
||||||
* @param accessToken - The Codex access token for authentication
|
* send full user conversation content to OpenAI's backend, which is a
|
||||||
* @returns A fetch function that translates Anthropic requests to Codex format
|
* 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(
|
export function createCodexFetch(
|
||||||
accessToken: string,
|
_accessToken: string,
|
||||||
): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> {
|
): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> {
|
||||||
const accountId = extractAccountId(accessToken)
|
return async (_input: RequestInfo | URL, _init?: RequestInit): Promise<Response> => {
|
||||||
|
const errorBody = {
|
||||||
return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
type: 'error',
|
||||||
const url = input instanceof Request ? input.url : String(input)
|
error: {
|
||||||
|
type: 'api_error',
|
||||||
// Only intercept Anthropic API message calls
|
message:
|
||||||
if (!url.includes('/v1/messages')) {
|
'Codex API routing is disabled. External Codex forwarding has been removed for privacy reasons.',
|
||||||
return globalThis.fetch(input, init)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the Anthropic request body
|
|
||||||
let anthropicBody: Record<string, unknown>
|
|
||||||
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',
|
|
||||||
},
|
},
|
||||||
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' },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
return new Response(JSON.stringify(errorBody), {
|
||||||
// Translate streaming response
|
status: 403,
|
||||||
return translateCodexStreamToAnthropic(codexResponse, codexModel)
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,18 +68,8 @@ const URL_CACHE = new LRUCache<string, CacheEntry>({
|
|||||||
ttl: CACHE_TTL_MS,
|
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<string, true>({
|
|
||||||
max: 128,
|
|
||||||
ttl: 5 * 60 * 1000, // 5 minutes — shorter than URL_CACHE TTL
|
|
||||||
})
|
|
||||||
|
|
||||||
export function clearWebFetchCache(): void {
|
export function clearWebFetchCache(): void {
|
||||||
URL_CACHE.clear()
|
URL_CACHE.clear()
|
||||||
DOMAIN_CHECK_CACHE.clear()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lazy singleton — defers the turndown → @mixmark-io/domino import (~1.4MB
|
// 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.
|
// Prevents hanging indefinitely on slow/unresponsive servers.
|
||||||
const FETCH_TIMEOUT_MS = 60_000
|
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
|
// 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
|
// 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
|
// resets on every hop, hanging the tool until user interrupt. 10 matches
|
||||||
@@ -174,32 +161,12 @@ type DomainCheckResult =
|
|||||||
| { status: 'check_failed'; error: Error }
|
| { status: 'check_failed'; error: Error }
|
||||||
|
|
||||||
export async function checkDomainBlocklist(
|
export async function checkDomainBlocklist(
|
||||||
domain: string,
|
_domain: string,
|
||||||
): Promise<DomainCheckResult> {
|
): Promise<DomainCheckResult> {
|
||||||
if (DOMAIN_CHECK_CACHE.has(domain)) {
|
// Remote domain-blocklist check removed: no user domain names are sent to
|
||||||
return { status: 'allowed' }
|
// external servers. Users explicitly approve each domain via the tool
|
||||||
}
|
// permission dialog, which is the primary security boundary.
|
||||||
try {
|
return { status: 'allowed' }
|
||||||
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 }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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(
|
export async function fetchCodexResponse(
|
||||||
messages: Message[],
|
_messages: Message[],
|
||||||
model: string,
|
_model: string,
|
||||||
options: {
|
_options: {
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
baseUrl?: string
|
baseUrl?: string
|
||||||
stream?: boolean
|
stream?: boolean
|
||||||
} = {}
|
} = {}
|
||||||
): Promise<OpenAIResponse> {
|
): Promise<OpenAIResponse> {
|
||||||
const { apiKey, baseUrl = 'https://api.openai.com/v1', stream = false } = options
|
throw new Error(
|
||||||
|
'OpenAI Codex API calls are disabled for privacy. External data forwarding has been removed.',
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user