mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
fix(server): repair provider proxy and cron permissions
Fix #845 by starting a lightweight standalone provider proxy for CLI-only OpenAI-compatible desktop providers, avoiding a fixed localhost:3456 proxy when the desktop server is not running. Fix #847 by passing explicit --permission-mode bypassPermissions for scheduled task subprocesses alongside the dangerous-skip flag. Tested: bun test src/utils/managedEnv.test.ts src/server/__tests__/provider-runtime-env.test.ts src/server/__tests__/cron-scheduler-launcher.test.ts Tested: bun run check:server Confidence: high Scope-risk: moderate
This commit is contained in:
parent
cd081116cf
commit
a090070352
@ -474,7 +474,10 @@ describe('cron scheduler launcher resolution', () => {
|
||||
.trim()
|
||||
.split('\n')
|
||||
expect(sidecarArgs).toContain('--dangerously-skip-permissions')
|
||||
expect(sidecarArgs).not.toContain('--permission-mode')
|
||||
expect(sidecarArgs).toContain('--permission-mode')
|
||||
expect(sidecarArgs[sidecarArgs.indexOf('--permission-mode') + 1]).toBe(
|
||||
'bypassPermissions',
|
||||
)
|
||||
})
|
||||
|
||||
unixOnly('executeTask inherits exported terminal shell variables', async () => {
|
||||
|
||||
31
src/server/proxy/standaloneProviderProxy.ts
Normal file
31
src/server/proxy/standaloneProviderProxy.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { handleProxyRequest } from './handler.js'
|
||||
|
||||
let standaloneProviderProxy: ReturnType<typeof Bun.serve> | null = null
|
||||
|
||||
export function ensureStandaloneProviderProxy(): number {
|
||||
if (standaloneProviderProxy) {
|
||||
return standaloneProviderProxy.port
|
||||
}
|
||||
|
||||
standaloneProviderProxy = Bun.serve({
|
||||
port: 0,
|
||||
hostname: '127.0.0.1',
|
||||
async fetch(req) {
|
||||
const url = new URL(req.url)
|
||||
if (url.pathname === '/health') {
|
||||
return Response.json({ status: 'ok' })
|
||||
}
|
||||
if (url.pathname.startsWith('/proxy/')) {
|
||||
return handleProxyRequest(req, url)
|
||||
}
|
||||
return Response.json({ error: 'Not Found' }, { status: 404 })
|
||||
},
|
||||
})
|
||||
|
||||
return standaloneProviderProxy.port
|
||||
}
|
||||
|
||||
export function stopStandaloneProviderProxyForTests(): void {
|
||||
standaloneProviderProxy?.stop(true)
|
||||
standaloneProviderProxy = null
|
||||
}
|
||||
@ -690,6 +690,8 @@ export class CronScheduler {
|
||||
return [
|
||||
...(model ? ['--model', model] : []),
|
||||
'--dangerously-skip-permissions',
|
||||
'--permission-mode',
|
||||
'bypassPermissions',
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@ -379,6 +379,23 @@ export function readActiveProviderManagedEnv(
|
||||
}
|
||||
}
|
||||
|
||||
export function activeProviderNeedsProxy(configDir: string): boolean {
|
||||
try {
|
||||
const raw = fs.readFileSync(path.join(configDir, 'cc-haha', 'providers.json'), 'utf-8')
|
||||
const index = normalizeProvidersIndex(JSON.parse(raw))
|
||||
if (!index?.activeId || isOpenAIOfficialProviderId(index.activeId)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const provider = index.providers.find((entry) => entry.id === index.activeId)
|
||||
if (!provider) return false
|
||||
|
||||
return (provider.apiFormat ?? 'anthropic') !== 'anthropic'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeActiveProviderManagedEnv(
|
||||
settingsEnv: Record<string, string>,
|
||||
configDir: string,
|
||||
|
||||
88
src/utils/managedEnv.test.ts
Normal file
88
src/utils/managedEnv.test.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||
import * as fs from 'fs/promises'
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
|
||||
import { applySafeConfigEnvironmentVariables } from './managedEnv.js'
|
||||
|
||||
let tmpDir: string
|
||||
const originalEnv = {
|
||||
CLAUDE_CONFIG_DIR: process.env.CLAUDE_CONFIG_DIR,
|
||||
CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST: process.env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST,
|
||||
ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL,
|
||||
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
|
||||
ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN,
|
||||
ANTHROPIC_MODEL: process.env.ANTHROPIC_MODEL,
|
||||
}
|
||||
|
||||
function restoreEnv(key: keyof typeof originalEnv): void {
|
||||
const value = originalEnv[key]
|
||||
if (value === undefined) {
|
||||
delete process.env[key]
|
||||
} else {
|
||||
process.env[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
async function writeJson(filePath: string, value: unknown): Promise<void> {
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
||||
await fs.writeFile(filePath, JSON.stringify(value, null, 2), 'utf-8')
|
||||
}
|
||||
|
||||
describe('managedEnv', () => {
|
||||
beforeEach(async () => {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'managed-env-'))
|
||||
process.env.CLAUDE_CONFIG_DIR = tmpDir
|
||||
delete process.env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST
|
||||
delete process.env.ANTHROPIC_BASE_URL
|
||||
delete process.env.ANTHROPIC_API_KEY
|
||||
delete process.env.ANTHROPIC_AUTH_TOKEN
|
||||
delete process.env.ANTHROPIC_MODEL
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await import('../server/proxy/standaloneProviderProxy.js')
|
||||
.then((mod) => mod.stopStandaloneProviderProxyForTests?.())
|
||||
.catch(() => {})
|
||||
await fs.rm(tmpDir, { recursive: true, force: true })
|
||||
restoreEnv('CLAUDE_CONFIG_DIR')
|
||||
restoreEnv('CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST')
|
||||
restoreEnv('ANTHROPIC_BASE_URL')
|
||||
restoreEnv('ANTHROPIC_API_KEY')
|
||||
restoreEnv('ANTHROPIC_AUTH_TOKEN')
|
||||
restoreEnv('ANTHROPIC_MODEL')
|
||||
})
|
||||
|
||||
test('starts a standalone provider proxy for CLI-only OpenAI-compatible providers', async () => {
|
||||
await writeJson(path.join(tmpDir, 'cc-haha', 'providers.json'), {
|
||||
activeId: 'agnes-provider',
|
||||
providers: [
|
||||
{
|
||||
id: 'agnes-provider',
|
||||
presetId: 'custom',
|
||||
name: 'Agnes',
|
||||
apiKey: 'sk-agnes',
|
||||
authStrategy: 'api_key',
|
||||
baseUrl: 'https://apihub.agnes-ai.com',
|
||||
apiFormat: 'openai_chat',
|
||||
models: {
|
||||
main: 'agnes-2.0-flash',
|
||||
haiku: 'agnes-2.0-flash',
|
||||
sonnet: 'agnes-2.0-flash',
|
||||
opus: 'agnes-2.0-flash',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
applySafeConfigEnvironmentVariables()
|
||||
|
||||
const baseUrl = new URL(process.env.ANTHROPIC_BASE_URL!)
|
||||
expect(baseUrl.hostname).toBe('127.0.0.1')
|
||||
expect(baseUrl.port).not.toBe('3456')
|
||||
expect(baseUrl.pathname).toBe('/proxy')
|
||||
|
||||
const health = await fetch(new URL('/health', baseUrl.origin))
|
||||
expect(health.status).toBe(200)
|
||||
})
|
||||
})
|
||||
@ -1,7 +1,11 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { isRemoteManagedSettingsEligible } from '../services/remoteManagedSettings/syncCache.js'
|
||||
import { mergeActiveProviderManagedEnv } from '../server/services/providerRuntimeEnv.js'
|
||||
import {
|
||||
activeProviderNeedsProxy,
|
||||
mergeActiveProviderManagedEnv,
|
||||
} from '../server/services/providerRuntimeEnv.js'
|
||||
import { ensureStandaloneProviderProxy } from '../server/proxy/standaloneProviderProxy.js'
|
||||
import { clearCACertsCache } from './caCerts.js'
|
||||
import { getGlobalConfig } from './config.js'
|
||||
import { getClaudeConfigHomeDir, isEnvTruthy } from './envUtils.js'
|
||||
@ -101,14 +105,20 @@ function filterSettingsEnv(
|
||||
* Returns an empty object if the file doesn't exist or is invalid.
|
||||
*/
|
||||
function getCcHahaSettingsEnv(): Record<string, string> {
|
||||
const configDir = getClaudeConfigHomeDir()
|
||||
const serverPort =
|
||||
!isEnvTruthy(process.env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST) &&
|
||||
activeProviderNeedsProxy(configDir)
|
||||
? ensureStandaloneProviderProxy()
|
||||
: undefined
|
||||
try {
|
||||
const ccHahaSettings = join(getClaudeConfigHomeDir(), 'cc-haha', 'settings.json')
|
||||
const ccHahaSettings = join(configDir, 'cc-haha', 'settings.json')
|
||||
const raw = readFileSync(ccHahaSettings, 'utf-8')
|
||||
const parsed = JSON.parse(raw) as { env?: Record<string, string> }
|
||||
const settingsEnv = normalizeLegacyDeepSeekManagedEnv(parsed.env ?? {}).env
|
||||
return mergeActiveProviderManagedEnv(settingsEnv, getClaudeConfigHomeDir())
|
||||
return mergeActiveProviderManagedEnv(settingsEnv, configDir, { serverPort })
|
||||
} catch {
|
||||
return mergeActiveProviderManagedEnv({}, getClaudeConfigHomeDir())
|
||||
return mergeActiveProviderManagedEnv({}, configDir, { serverPort })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user