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:
KRATOS
2026-04-04 11:52:33 +05:30
committed by GitHub
parent cdbe016e6f
commit 27e6505bfd
18 changed files with 367 additions and 59 deletions

View 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')
})

File diff suppressed because one or more lines are too long

View File

@@ -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 ?? ''
}

View File

@@ -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

View File

@@ -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'

View 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)
})
})

View File

@@ -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' }
}

View File

@@ -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()

View File

@@ -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 = {

File diff suppressed because one or more lines are too long

90
src/utils/user.test.ts Normal file
View 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')
})
})

View File

@@ -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()
}