hardening: isolate third-party paths and clean external-build metadata (#311)
* hardening: isolate third-party paths and clean external-build metadata * fix: restore external feedback flow and make privacy check portable
This commit is contained in:
42
LICENSE
42
LICENSE
@@ -1,21 +1,29 @@
|
||||
MIT License
|
||||
NOTICE
|
||||
|
||||
Copyright (c) 2026 OpenClaude contributors
|
||||
This repository contains code derived from Anthropic's Claude Code CLI.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
The original Claude Code source is proprietary software:
|
||||
Copyright (c) Anthropic PBC. All rights reserved.
|
||||
Subject to Anthropic's Commercial Terms of Service.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
Modifications and additions by OpenClaude contributors are offered under
|
||||
the MIT License where legally permissible:
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
MIT License
|
||||
Copyright (c) 2026 OpenClaude contributors (modifications only)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the modifications made by OpenClaude contributors, to deal
|
||||
in those modifications without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the modifications.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
|
||||
|
||||
The underlying derived code remains subject to Anthropic's copyright.
|
||||
This project does not have Anthropic's authorization to distribute
|
||||
their proprietary source. Users and contributors should evaluate their
|
||||
own legal position.
|
||||
|
||||
16
README.md
16
README.md
@@ -11,6 +11,22 @@ Use OpenAI-compatible APIs, Gemini, GitHub Models, Codex, Ollama, Atomic Chat, a
|
||||
- Run locally with Ollama or Atomic Chat
|
||||
- Keep core coding-agent workflows: bash, file tools, grep, glob, agents, tasks, MCP, and web tools
|
||||
|
||||
## Provenance & Legal Notice
|
||||
|
||||
OpenClaude is derived from Anthropic's Claude Code CLI source code, which was
|
||||
inadvertently exposed in March 2026 through a packaging error in npm. The
|
||||
original Claude Code source is proprietary software owned by Anthropic PBC.
|
||||
|
||||
This project adds multi-provider support, strips telemetry, and adapts the
|
||||
codebase for open use. It is not an authorized fork or open-source release
|
||||
by Anthropic.
|
||||
|
||||
**"Claude" and "Claude Code" are trademarks of Anthropic PBC.**
|
||||
|
||||
Contributors should be aware that the legal status of distributing code
|
||||
derived from Anthropic's proprietary source is unresolved. See the LICENSE
|
||||
file for details.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
"test:provider-recommendation": "bun test src/utils/providerRecommendation.test.ts src/utils/providerProfile.test.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"smoke": "bun run build && node dist/cli.mjs --version",
|
||||
"verify:privacy": "bun run scripts/verify-no-phone-home.ts",
|
||||
"build:verified": "bun run build && bun run verify:privacy",
|
||||
"test:provider": "bun test src/services/api/*.test.ts src/utils/context.test.ts",
|
||||
"doctor:runtime": "bun run scripts/system-check.ts",
|
||||
"doctor:runtime:json": "bun run scripts/system-check.ts --json",
|
||||
@@ -140,7 +142,7 @@
|
||||
"ollama",
|
||||
"gemini"
|
||||
],
|
||||
"license": "MIT",
|
||||
"license": "SEE LICENSE FILE",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
|
||||
@@ -196,6 +196,13 @@ export function classifyFetchError() { return 'disabled'; }
|
||||
|
||||
'components/FeedbackSurvey/submitTranscriptShare': `
|
||||
export async function submitTranscriptShare() { return { success: false }; }
|
||||
`,
|
||||
|
||||
// ─── Internal employee logging (not needed in the external build) ─────
|
||||
|
||||
'services/internalLogging': `
|
||||
export async function logPermissionContextForAnts() {}
|
||||
export const getContainerId = async () => null;
|
||||
`,
|
||||
}
|
||||
|
||||
|
||||
46
scripts/verify-no-phone-home.sh
Normal file
46
scripts/verify-no-phone-home.sh
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DIST="dist/cli.mjs"
|
||||
|
||||
if [ ! -f "$DIST" ]; then
|
||||
echo "ERROR: $DIST not found. Run 'bun run build' first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
EXIT=0
|
||||
|
||||
BANNED=(
|
||||
"datadoghq.com"
|
||||
"api/event_logging/batch"
|
||||
"api/claude_code/metrics"
|
||||
"getKubernetesNamespace"
|
||||
"/var/run/secrets/kubernetes"
|
||||
"/proc/self/mountinfo"
|
||||
"tengu_internal_record_permission_context"
|
||||
)
|
||||
|
||||
echo "Checking $DIST for banned patterns..."
|
||||
echo ""
|
||||
|
||||
for pattern in "${BANNED[@]}"; do
|
||||
COUNT=$(grep -F -c "$pattern" "$DIST" 2>/dev/null || true)
|
||||
COUNT=${COUNT:-0}
|
||||
if [ "$COUNT" -gt 0 ]; then
|
||||
echo " FAIL: '$pattern' found ($COUNT occurrences)"
|
||||
EXIT=1
|
||||
else
|
||||
echo " PASS: '$pattern' not found"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
|
||||
if [ "$EXIT" -eq 0 ]; then
|
||||
echo "✓ All checks passed — no banned patterns in build output"
|
||||
else
|
||||
echo "✗ FAILED — banned patterns found in build output"
|
||||
fi
|
||||
|
||||
exit $EXIT
|
||||
43
scripts/verify-no-phone-home.ts
Normal file
43
scripts/verify-no-phone-home.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
|
||||
const DIST = 'dist/cli.mjs'
|
||||
const BANNED_PATTERNS = [
|
||||
'datadoghq.com',
|
||||
'api/event_logging/batch',
|
||||
'api/claude_code/metrics',
|
||||
'getKubernetesNamespace',
|
||||
'/var/run/secrets/kubernetes',
|
||||
'/proc/self/mountinfo',
|
||||
'tengu_internal_record_permission_context',
|
||||
] as const
|
||||
|
||||
if (!existsSync(DIST)) {
|
||||
console.error(`ERROR: ${DIST} not found. Run 'bun run build' first.`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const contents = readFileSync(DIST, 'utf8')
|
||||
let exitCode = 0
|
||||
|
||||
console.log(`Checking ${DIST} for banned patterns...`)
|
||||
console.log('')
|
||||
|
||||
for (const pattern of BANNED_PATTERNS) {
|
||||
const count = contents.split(pattern).length - 1
|
||||
if (count > 0) {
|
||||
console.log(` FAIL: '${pattern}' found (${count} occurrences)`)
|
||||
exitCode = 1
|
||||
} else {
|
||||
console.log(` PASS: '${pattern}' not found`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('')
|
||||
|
||||
if (exitCode === 0) {
|
||||
console.log('✓ All checks passed — no banned patterns in build output')
|
||||
} else {
|
||||
console.log('✗ FAILED — banned patterns found in build output')
|
||||
}
|
||||
|
||||
process.exit(exitCode)
|
||||
23
src/components/Feedback.test.ts
Normal file
23
src/components/Feedback.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { expect, test } from 'bun:test'
|
||||
|
||||
import { createGitHubIssueUrl } from './Feedback.tsx'
|
||||
|
||||
(globalThis as { MACRO?: { VERSION?: string } }).MACRO = { VERSION: '0.1.7' }
|
||||
|
||||
test('createGitHubIssueUrl omits empty feedback IDs', () => {
|
||||
const url = decodeURIComponent(
|
||||
createGitHubIssueUrl('', 'Bug title', 'Bug description', []),
|
||||
)
|
||||
|
||||
expect(url).not.toContain('Feedback ID:')
|
||||
expect(url).toContain('Bug Description')
|
||||
expect(url).toContain('Errors')
|
||||
})
|
||||
|
||||
test('createGitHubIssueUrl includes feedback IDs when present', () => {
|
||||
const url = decodeURIComponent(
|
||||
createGitHubIssueUrl('fb-123', 'Bug title', 'Bug description', []),
|
||||
)
|
||||
|
||||
expect(url).toContain('Feedback ID: fb-123')
|
||||
})
|
||||
@@ -20,6 +20,7 @@ import { env } from '../utils/env.js';
|
||||
import { type GitRepoState, getGitState, getIsGit } from '../utils/git.js';
|
||||
import { getAuthHeaders, getUserAgent } from '../utils/http.js';
|
||||
import { getInMemoryErrors, logError } from '../utils/log.js';
|
||||
import { getAPIProvider } from '../utils/model/providers.js';
|
||||
import { isEssentialTrafficOnly } from '../utils/privacyLevel.js';
|
||||
import { extractTeammateTranscriptsFromTasks, getTranscriptPath, loadAllSubagentTranscriptsFromDisk, MAX_TRANSCRIPT_READ_BYTES } from '../utils/sessionStorage.js';
|
||||
import { jsonStringify } from '../utils/slowOperations.js';
|
||||
@@ -32,7 +33,7 @@ import TextInput from './TextInput.js';
|
||||
|
||||
// This value was determined experimentally by testing the URL length limit
|
||||
const GITHUB_URL_LIMIT = 7250;
|
||||
const GITHUB_ISSUES_REPO_URL = "external" === 'ant' ? 'https://github.com/anthropics/claude-cli-internal/issues' : 'https://github.com/anthropics/claude-code/issues';
|
||||
const GITHUB_ISSUES_REPO_URL = 'https://github.com/Gitlawb/openclaude/issues';
|
||||
type Props = {
|
||||
abortSignal: AbortSignal;
|
||||
messages: Message[];
|
||||
@@ -51,6 +52,7 @@ type Props = {
|
||||
};
|
||||
};
|
||||
type Step = 'userInput' | 'consent' | 'submitting' | 'done';
|
||||
type CompletionMode = 'submitted' | 'issue-draft';
|
||||
type FeedbackData = {
|
||||
// latestAssistantMessageId is the message ID from the latest main model call
|
||||
latestAssistantMessageId: string | null;
|
||||
@@ -162,6 +164,7 @@ export function Feedback({
|
||||
const [cursorOffset, setCursorOffset] = useState(0);
|
||||
const [description, setDescription] = useState(initialDescription ?? '');
|
||||
const [feedbackId, setFeedbackId] = useState<string | null>(null);
|
||||
const [completionMode, setCompletionMode] = useState<CompletionMode>('submitted');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [envInfo, setEnvInfo] = useState<{
|
||||
isGit: boolean;
|
||||
@@ -190,6 +193,7 @@ export function Feedback({
|
||||
setStep('submitting');
|
||||
setError(null);
|
||||
setFeedbackId(null);
|
||||
setCompletionMode('submitted');
|
||||
|
||||
// Get sanitized errors for the report
|
||||
const sanitizedErrors = getSanitizedErrorLogs();
|
||||
@@ -225,6 +229,7 @@ export function Feedback({
|
||||
const [result, t] = await Promise.all([submitFeedback(reportData, abortSignal), generateTitle(description, abortSignal)]);
|
||||
setTitle(t);
|
||||
if (result.success) {
|
||||
setCompletionMode(result.issueDraftOnly ? 'issue-draft' : 'submitted');
|
||||
if (result.feedbackId) {
|
||||
setFeedbackId(result.feedbackId);
|
||||
logEvent('tengu_bug_report_submitted', {
|
||||
@@ -258,7 +263,7 @@ export function Feedback({
|
||||
display: 'system'
|
||||
});
|
||||
} else {
|
||||
onDone('Feedback / bug report submitted', {
|
||||
onDone(completionMode === 'issue-draft' ? 'GitHub issue draft ready' : 'Feedback / bug report submitted', {
|
||||
display: 'system'
|
||||
});
|
||||
}
|
||||
@@ -267,7 +272,7 @@ export function Feedback({
|
||||
onDone('Feedback / bug report cancelled', {
|
||||
display: 'system'
|
||||
});
|
||||
}, [step, error, onDone]);
|
||||
}, [step, error, completionMode, onDone]);
|
||||
|
||||
// During text input, use Settings context where only Escape (not 'n') triggers confirm:no.
|
||||
// This allows typing 'n' in the text field while still supporting Escape to cancel.
|
||||
@@ -288,7 +293,7 @@ export function Feedback({
|
||||
display: 'system'
|
||||
});
|
||||
} else {
|
||||
onDone('Feedback / bug report submitted', {
|
||||
onDone(completionMode === 'issue-draft' ? 'GitHub issue draft ready' : 'Feedback / bug report submitted', {
|
||||
display: 'system'
|
||||
});
|
||||
}
|
||||
@@ -377,7 +382,7 @@ export function Feedback({
|
||||
</Box>}
|
||||
|
||||
{step === 'done' && <Box flexDirection="column">
|
||||
{error ? <Text color="error">{error}</Text> : <Text color="success">Thank you for your report!</Text>}
|
||||
{error ? <Text color="error">{error}</Text> : <Text color="success">{completionMode === 'issue-draft' ? 'Your GitHub issue draft is ready.' : 'Thank you for your report!'}</Text>}
|
||||
{feedbackId && <Text dimColor>Feedback ID: {feedbackId}</Text>}
|
||||
<Box marginTop={1}>
|
||||
<Text>Press </Text>
|
||||
@@ -396,7 +401,8 @@ export function createGitHubIssueUrl(feedbackId: string, title: string, descript
|
||||
}>): string {
|
||||
const sanitizedTitle = redactSensitiveInfo(title);
|
||||
const sanitizedDescription = redactSensitiveInfo(description);
|
||||
const bodyPrefix = `**Bug Description**\n${sanitizedDescription}\n\n` + `**Environment Info**\n` + `- Platform: ${env.platform}\n` + `- Terminal: ${env.terminal}\n` + `- Version: ${MACRO.VERSION || 'unknown'}\n` + `- Feedback ID: ${feedbackId}\n` + `\n**Errors**\n\`\`\`json\n`;
|
||||
const feedbackIdLine = feedbackId ? `- Feedback ID: ${feedbackId}\n` : '';
|
||||
const bodyPrefix = `**Bug Description**\n${sanitizedDescription}\n\n` + `**Environment Info**\n` + `- Platform: ${env.platform}\n` + `- Terminal: ${env.terminal}\n` + `- Version: ${MACRO.VERSION || 'unknown'}\n` + feedbackIdLine + `\n**Errors**\n\`\`\`json\n`;
|
||||
const errorSuffix = `\n\`\`\`\n`;
|
||||
const errorsJson = jsonStringify(errors);
|
||||
const baseUrl = `${GITHUB_ISSUES_REPO_URL}/new?title=${encodeURIComponent(sanitizedTitle)}&labels=user-reported,bug&body=`;
|
||||
@@ -519,6 +525,7 @@ async function submitFeedback(data: FeedbackData, signal?: AbortSignal): Promise
|
||||
success: boolean;
|
||||
feedbackId?: string;
|
||||
isZdrOrg?: boolean;
|
||||
issueDraftOnly?: boolean;
|
||||
}> {
|
||||
if (isEssentialTrafficOnly()) {
|
||||
return {
|
||||
@@ -526,6 +533,15 @@ async function submitFeedback(data: FeedbackData, signal?: AbortSignal): Promise
|
||||
};
|
||||
}
|
||||
try {
|
||||
// Third-party providers should not post feedback to Anthropic, but they
|
||||
// should still reach the done state so users can open a GitHub issue draft.
|
||||
if (getAPIProvider() !== 'firstParty') {
|
||||
return {
|
||||
success: true,
|
||||
issueDraftOnly: true
|
||||
};
|
||||
}
|
||||
|
||||
// Ensure OAuth token is fresh before getting auth headers
|
||||
// This prevents 401 errors from stale cached tokens
|
||||
await checkAndRefreshOAuthTokenIfNeeded();
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
import { isEnvTruthy } from '../utils/envUtils.js'
|
||||
|
||||
// Lazy read so ENABLE_GROWTHBOOK_DEV from globalSettings.env (applied after
|
||||
// module load) is picked up. USER_TYPE is a build-time define so it's safe.
|
||||
export function getGrowthBookClientKey(): string {
|
||||
return process.env.USER_TYPE === 'ant'
|
||||
? isEnvTruthy(process.env.ENABLE_GROWTHBOOK_DEV)
|
||||
? 'sdk-yZQvlplybuXjYh6L'
|
||||
: 'sdk-xRVcrliHIlrg4og4'
|
||||
: 'sdk-zAZezfDKGoZuXXKe'
|
||||
return process.env.GROWTHBOOK_CLIENT_KEY ?? ''
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { getEventMetadata } from './metadata.js'
|
||||
|
||||
const DATADOG_LOGS_ENDPOINT =
|
||||
'https://http-intake.logs.us5.datadoghq.com/api/v2/logs'
|
||||
const DATADOG_CLIENT_TOKEN = 'pubbbf48e6d78dae54bceaa4acf463299bf'
|
||||
const DATADOG_CLIENT_TOKEN = process.env.DATADOG_CLIENT_TOKEN ?? ''
|
||||
const DEFAULT_FLUSH_INTERVAL_MS = 15000
|
||||
const MAX_BATCH_SIZE = 100
|
||||
const NETWORK_TIMEOUT_MS = 5000
|
||||
|
||||
@@ -1198,9 +1198,14 @@ export function getErrorMessageIfRefusal(
|
||||
|
||||
logEvent('tengu_refusal_api_response', {})
|
||||
|
||||
const usagePolicyUrl =
|
||||
getAPIProvider() === 'firstParty'
|
||||
? 'https://www.anthropic.com/legal/aup'
|
||||
: "your provider's acceptable use policy"
|
||||
|
||||
const baseMessage = getIsNonInteractiveSession()
|
||||
? `${API_ERROR_MESSAGE_PREFIX}: Claude Code is unable to respond to this request, which appears to violate our Usage Policy (https://www.anthropic.com/legal/aup). Try rephrasing the request or attempting a different approach.`
|
||||
: `${API_ERROR_MESSAGE_PREFIX}: Claude Code is unable to respond to this request, which appears to violate our Usage Policy (https://www.anthropic.com/legal/aup). Please double press esc to edit your last message or start a new session for Claude Code to assist with a different task.`
|
||||
? `${API_ERROR_MESSAGE_PREFIX}: Claude Code is unable to respond to this request, which appears to violate our Usage Policy (${usagePolicyUrl}). Try rephrasing the request or attempting a different approach.`
|
||||
: `${API_ERROR_MESSAGE_PREFIX}: Claude Code is unable to respond to this request, which appears to violate our Usage Policy (${usagePolicyUrl}). Please double press esc to edit your last message or start a new session for Claude Code to assist with a different task.`
|
||||
|
||||
const modelSuggestion =
|
||||
model !== 'claude-sonnet-4-20250514'
|
||||
|
||||
63
src/tools/WebFetchTool/domainCheck.test.ts
Normal file
63
src/tools/WebFetchTool/domainCheck.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import axios from 'axios'
|
||||
|
||||
const originalEnv = { ...process.env }
|
||||
|
||||
async function importFreshModule() {
|
||||
return import(`./utils.ts?ts=${Date.now()}-${Math.random()}`)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnv }
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv }
|
||||
})
|
||||
|
||||
describe('checkDomainBlocklist', () => {
|
||||
test('returns allowed without API call in OpenAI mode', async () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
const getSpy = mock(() =>
|
||||
Promise.resolve({ status: 200, data: { can_fetch: true } }),
|
||||
)
|
||||
axios.get = getSpy as typeof axios.get
|
||||
|
||||
const { checkDomainBlocklist } = await importFreshModule()
|
||||
const result = await checkDomainBlocklist('example.com')
|
||||
|
||||
expect(result.status).toBe('allowed')
|
||||
expect(getSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('returns allowed without API call in Gemini mode', async () => {
|
||||
process.env.CLAUDE_CODE_USE_GEMINI = '1'
|
||||
const getSpy = mock(() =>
|
||||
Promise.resolve({ status: 200, data: { can_fetch: true } }),
|
||||
)
|
||||
axios.get = getSpy as typeof axios.get
|
||||
|
||||
const { checkDomainBlocklist } = await importFreshModule()
|
||||
const result = await checkDomainBlocklist('example.com')
|
||||
|
||||
expect(result.status).toBe('allowed')
|
||||
expect(getSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('calls Anthropic domain check in first-party mode', async () => {
|
||||
delete process.env.CLAUDE_CODE_USE_OPENAI
|
||||
delete process.env.CLAUDE_CODE_USE_GEMINI
|
||||
delete process.env.CLAUDE_CODE_USE_GITHUB
|
||||
|
||||
const getSpy = mock(() =>
|
||||
Promise.resolve({ status: 200, data: { can_fetch: true } }),
|
||||
)
|
||||
axios.get = getSpy as typeof axios.get
|
||||
|
||||
const { checkDomainBlocklist } = await importFreshModule()
|
||||
const result = await checkDomainBlocklist('example.com')
|
||||
|
||||
expect(result.status).toBe('allowed')
|
||||
expect(getSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -8,6 +8,7 @@ import { queryHaiku } from '../../services/api/claude.js'
|
||||
import { AbortError } from '../../utils/errors.js'
|
||||
import { getWebFetchUserAgent } from '../../utils/http.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import { getAPIProvider } from '../../utils/model/providers.js'
|
||||
import {
|
||||
isBinaryContentType,
|
||||
persistBinaryContent,
|
||||
@@ -176,6 +177,11 @@ type DomainCheckResult =
|
||||
export async function checkDomainBlocklist(
|
||||
domain: string,
|
||||
): Promise<DomainCheckResult> {
|
||||
// Third-party providers should not consult Anthropic's domain policy.
|
||||
if (getAPIProvider() !== 'firstParty') {
|
||||
return { status: 'allowed' }
|
||||
}
|
||||
|
||||
if (DOMAIN_CHECK_CACHE.has(domain)) {
|
||||
return { status: 'allowed' }
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { logForDebugging } from './debug.js'
|
||||
import { parseJSONL } from './json.js'
|
||||
import { logError } from './log.js'
|
||||
import { getAPIProvider } from './model/providers.js'
|
||||
import {
|
||||
getCanonicalName,
|
||||
getMainLoopModel,
|
||||
@@ -75,11 +76,13 @@ export function getAttributionTexts(): AttributionTexts {
|
||||
: 'Claude Opus 4.6'
|
||||
const defaultAttribution =
|
||||
'🤖 Generated with [OpenClaude](https://github.com/Gitlawb/openclaude)'
|
||||
const coAuthorDomain =
|
||||
getAPIProvider() === 'firstParty' ? 'anthropic.com' : 'openclaude.dev'
|
||||
const defaultCommit = isEnvTruthy(
|
||||
process.env.OPENCLAUDE_DISABLE_CO_AUTHORED_BY,
|
||||
)
|
||||
? ''
|
||||
: `Co-Authored-By: ${modelName} <noreply@anthropic.com>`
|
||||
: `Co-Authored-By: ${modelName} <noreply@${coAuthorDomain}>`
|
||||
|
||||
const settings = getInitialSettings()
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
handleOAuth401Error,
|
||||
isClaudeAISubscriber,
|
||||
} from './auth.js'
|
||||
import { getAPIProvider } from './model/providers.js'
|
||||
import { getClaudeCodeUserAgent } from './userAgent.js'
|
||||
import { getWorkload } from './workloadContext.js'
|
||||
|
||||
@@ -54,7 +55,11 @@ export function getMCPUserAgent(): string {
|
||||
// operators match in robots.txt); the claude-code suffix lets them distinguish
|
||||
// local CLI traffic from claude.ai server-side fetches.
|
||||
export function getWebFetchUserAgent(): string {
|
||||
return `Claude-User (${getClaudeCodeUserAgent()}; +https://support.anthropic.com/)`
|
||||
const supportUrl =
|
||||
getAPIProvider() === 'firstParty'
|
||||
? 'https://support.anthropic.com/'
|
||||
: 'https://github.com/Gitlawb/openclaude'
|
||||
return `Claude-User (${getClaudeCodeUserAgent()}; +${supportUrl})`
|
||||
}
|
||||
|
||||
export type AuthHeaders = {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Box, Text } from '../ink.js';
|
||||
import { getSSLErrorHint } from '../services/api/errorUtils.js';
|
||||
import { getUserAgent } from './http.js';
|
||||
import { logError } from './log.js';
|
||||
import { getAPIProvider } from './model/providers.js';
|
||||
export interface PreflightCheckResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
@@ -127,7 +128,7 @@ export function PreflightStep(t0) {
|
||||
useEffect(t3, t4);
|
||||
let t5;
|
||||
if ($[6] !== isChecking || $[7] !== result || $[8] !== showSpinner) {
|
||||
t5 = isChecking && showSpinner ? <Box paddingLeft={1}><Spinner /><Text>Checking connectivity...</Text></Box> : !result?.success && !isChecking && <Box flexDirection="column" gap={1}><Text color="error">Unable to connect to Anthropic services</Text><Text color="error">{result?.error}</Text>{result?.sslHint ? <Box flexDirection="column" gap={1}><Text>{result.sslHint}</Text><Text color="suggestion">See https://code.claude.com/docs/en/network-config</Text></Box> : <Box flexDirection="column" gap={1}><Text>Please check your internet connection and network settings.</Text><Text>Note: Claude Code might not be available in your country. Check supported countries at{" "}<Text color="suggestion">https://anthropic.com/supported-countries</Text></Text></Box>}</Box>;
|
||||
t5 = isChecking && showSpinner ? <Box paddingLeft={1}><Spinner /><Text>Checking connectivity...</Text></Box> : !result?.success && !isChecking && <Box flexDirection="column" gap={1}><Text color="error">Unable to connect to Anthropic services</Text><Text color="error">{result?.error}</Text>{result?.sslHint ? <Box flexDirection="column" gap={1}><Text>{result.sslHint}</Text><Text color="suggestion">See https://code.claude.com/docs/en/network-config</Text></Box> : <Box flexDirection="column" gap={1}><Text>Please check your internet connection and network settings.</Text>{getAPIProvider() === 'firstParty' && <Text>Note: Claude Code might not be available in your country. Check supported countries at{" "}<Text color="suggestion">https://anthropic.com/supported-countries</Text></Text>}</Box>}</Box>;
|
||||
$[6] = isChecking;
|
||||
$[7] = result;
|
||||
$[8] = showSpinner;
|
||||
|
||||
90
src/utils/user.test.ts
Normal file
90
src/utils/user.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { afterEach, describe, expect, mock, test } from 'bun:test'
|
||||
|
||||
const originalEnv = { ...process.env }
|
||||
|
||||
async function importFreshUserModule() {
|
||||
return import(`./user.ts?ts=${Date.now()}-${Math.random()}`)
|
||||
}
|
||||
|
||||
function installCommonMocks(options?: {
|
||||
oauthEmail?: string
|
||||
gitEmail?: string
|
||||
}) {
|
||||
mock.module('../bootstrap/state.js', () => ({
|
||||
getSessionId: () => 'session-test',
|
||||
}))
|
||||
|
||||
mock.module('./auth.js', () => ({
|
||||
getOauthAccountInfo: () =>
|
||||
options?.oauthEmail
|
||||
? {
|
||||
emailAddress: options.oauthEmail,
|
||||
organizationUuid: 'org-test',
|
||||
accountUuid: 'acct-test',
|
||||
}
|
||||
: undefined,
|
||||
getRateLimitTier: () => null,
|
||||
getSubscriptionType: () => null,
|
||||
}))
|
||||
|
||||
mock.module('./config.js', () => ({
|
||||
getGlobalConfig: () => ({}),
|
||||
getOrCreateUserID: () => 'device-test',
|
||||
}))
|
||||
|
||||
mock.module('./cwd.js', () => ({
|
||||
getCwd: () => 'C:\\repo',
|
||||
}))
|
||||
|
||||
mock.module('./env.js', () => ({
|
||||
env: { platform: 'windows' },
|
||||
getHostPlatformForAnalytics: () => 'windows',
|
||||
}))
|
||||
|
||||
mock.module('./envUtils.js', () => ({
|
||||
isEnvTruthy: (value: string | undefined) =>
|
||||
!!value && value !== '0' && value.toLowerCase() !== 'false',
|
||||
}))
|
||||
|
||||
mock.module('execa', () => ({
|
||||
execa: async () => ({
|
||||
exitCode: options?.gitEmail ? 0 : 1,
|
||||
stdout: options?.gitEmail ?? '',
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
process.env = { ...originalEnv }
|
||||
delete (globalThis as Record<string, unknown>).MACRO
|
||||
})
|
||||
|
||||
describe('user email fallbacks', () => {
|
||||
test('getCoreUserData does not synthesize Anthropic email from COO_CREATOR', async () => {
|
||||
process.env.USER_TYPE = 'ant'
|
||||
process.env.COO_CREATOR = 'alice'
|
||||
;(globalThis as Record<string, unknown>).MACRO = { VERSION: '0.0.0' }
|
||||
|
||||
installCommonMocks()
|
||||
|
||||
const { getCoreUserData } = await importFreshUserModule()
|
||||
const result = getCoreUserData()
|
||||
|
||||
expect(result.email).toBeUndefined()
|
||||
})
|
||||
|
||||
test('initUser falls back to git email when oauth email is missing', async () => {
|
||||
process.env.USER_TYPE = 'ant'
|
||||
process.env.COO_CREATOR = 'alice'
|
||||
;(globalThis as Record<string, unknown>).MACRO = { VERSION: '0.0.0' }
|
||||
|
||||
installCommonMocks({ gitEmail: 'git@example.com' })
|
||||
|
||||
const { initUser, getCoreUserData } = await importFreshUserModule()
|
||||
await initUser()
|
||||
|
||||
const result = getCoreUserData()
|
||||
expect(result.email).toBe('git@example.com')
|
||||
})
|
||||
})
|
||||
@@ -146,15 +146,6 @@ function getEmail(): string | undefined {
|
||||
return oauthAccount.emailAddress
|
||||
}
|
||||
|
||||
// Ant-only fallbacks below (no execSync)
|
||||
if (process.env.USER_TYPE !== 'ant') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (process.env.COO_CREATOR) {
|
||||
return `${process.env.COO_CREATOR}@anthropic.com`
|
||||
}
|
||||
|
||||
// If initUser() wasn't called, we return undefined instead of blocking
|
||||
return undefined
|
||||
}
|
||||
@@ -166,15 +157,6 @@ async function getEmailAsync(): Promise<string | undefined> {
|
||||
return oauthAccount.emailAddress
|
||||
}
|
||||
|
||||
// Ant-only fallbacks below
|
||||
if (process.env.USER_TYPE !== 'ant') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (process.env.COO_CREATOR) {
|
||||
return `${process.env.COO_CREATOR}@anthropic.com`
|
||||
}
|
||||
|
||||
return getGitEmail()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user