fix: preserve native cloud auth and selected provider routing

Forward Agent-scoped cloud configuration and explicit empty overrides. Keep configured Provider credentials isolated and use Claude's native key helper to prevent settings from overriding the selected endpoint or exposing secrets in argv. Treat cloud/API-key status as configuration evidence so refresh does not erase recent auth failures.

Validation: desktop suite 1525/1525 before final credential-evidence classification; final readiness/catalog regressions 53/53; native Claude helper and endpoint tests 3/3; isolated Electron Provider save/refresh/chat passed; git diff --check passed. See progress document for platform and cloud-account limits.
This commit is contained in:
Ryder Sun committed 2026-09-09 19:11:50 +08:00
1 parent f08fe96529
commit f3794a9732
15 files changed
+343 -18

No files matched your search

+1
View File
@@ -123,6 +123,7 @@ async function runAgent(agent, prompt, workdir, options = {}) {
? options.hermesAcpAvailable
: agent.acpAvailable
const spec = invocation(agent.kind, agent.executable, workdir, sessionRef, {
platform,
sandbox: options.sandbox,
provider: options.provider,
attachments: options.attachments,
@@ -216,6 +216,24 @@ function invocation(kind, executable, workdir, sessionRef = '', options = {}) {
'--include-partial-messages',
'--verbose',
'--permission-mode', options.sandbox === 'workspace-write' ? 'acceptEdits' : 'plan',
...(options.provider?.id === 'anthropic' ? [
'--settings', JSON.stringify({
// Native settings.env overrides process env. The helper keeps the key out of argv.
apiKeyHelper: (options.platform || process.platform) === 'win32'
? 'powershell.exe -NoLogo -NoProfile -NonInteractive -Command "[Console]::Write([Environment]::GetEnvironmentVariable(\'MELDWORK_PROVIDER_API_KEY\'))"'
: 'printf \'%s\' "$MELDWORK_PROVIDER_API_KEY"',
env: {
CLAUDE_CODE_USE_BEDROCK: '0', CLAUDE_CODE_USE_VERTEX: '0',
CLAUDE_CODE_USE_FOUNDRY: '0', CLAUDE_CODE_USE_MANTLE: '0',
ANTHROPIC_BASE_URL: options.provider.baseUrl,
ANTHROPIC_API_KEY: '', ANTHROPIC_AUTH_TOKEN: '', CLAUDE_CODE_OAUTH_TOKEN: '',
ANTHROPIC_CUSTOM_HEADERS: '',
ANTHROPIC_DEFAULT_OPUS_MODEL: '', ANTHROPIC_DEFAULT_SONNET_MODEL: '',
ANTHROPIC_DEFAULT_HAIKU_MODEL: '',
},
}),
] : []),
...(options.provider?.model ? ['--model', options.provider.model] : []),
...(sessionRef ? ['--resume', sessionRef] : []),
],
promptArg: true,
@@ -38,7 +38,7 @@ function childSecrets(env) {
const secrets = new Set()
for (const [name, value] of Object.entries(env || {})) {
if (typeof value !== 'string' || !value) continue
if (/(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD|AUTHORIZATION)/i.test(name)
if (/(?:API[_-]?KEY|ACCESS[_-]?KEY|TOKEN|SECRET|PASSWORD|AUTHORIZATION)/i.test(name)
&& utf8ByteLength(value) >= 8) secrets.add(value)
if (!/^(?:https?|all)_proxy$/i.test(name)) continue
if (value.includes('@')) secrets.add(value)
+36 -10
View File
@@ -43,6 +43,11 @@ const CREDENTIAL_ENV_KEYS = Object.freeze({
opencode: ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'OPENROUTER_API_KEY'],
opencodereview: ['OCR_LLM_TOKEN', 'OPENAI_API_KEY'],
})
const GOOGLE_CLOUD_ENV_KEYS = [
'GOOGLE_APPLICATION_CREDENTIALS', 'GOOGLE_CLOUD_PROJECT', 'GCLOUD_PROJECT',
'GOOGLE_CLOUD_LOCATION', 'GOOGLE_CLOUD_QUOTA_PROJECT', 'CLOUDSDK_CONFIG',
]
const NATIVE_CONFIG_ROOT_KEYS = new Set(['CODEX_HOME', 'CLAUDE_CONFIG_DIR', 'PI_CODING_AGENT_DIR'])
const RUNTIME_ENV_KEYS = Object.freeze({
codex: ['OPENAI_API_KEY', 'OPENAI_BASE_URL', 'OPENAI_MODEL', 'CODEX_HOME'],
hermes: [
@@ -65,9 +70,25 @@ const RUNTIME_ENV_KEYS = Object.freeze({
'OPENAI_API_KEY', 'OPENAI_BASE_URL', 'OPENAI_MODEL',
],
mimo: ['MIMO_API_KEY', 'MIMO_BASE_URL', 'MIMO_MODEL'],
claude: ['ANTHROPIC_API_KEY', 'ANTHROPIC_BASE_URL', 'ANTHROPIC_MODEL', 'CLAUDE_CONFIG_DIR'],
claude: [
'ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'CLAUDE_CODE_OAUTH_TOKEN',
'ANTHROPIC_BASE_URL', 'ANTHROPIC_MODEL', 'CLAUDE_CONFIG_DIR',
'ANTHROPIC_DEFAULT_OPUS_MODEL', 'ANTHROPIC_DEFAULT_SONNET_MODEL', 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX', 'CLAUDE_CODE_USE_FOUNDRY', 'CLAUDE_CODE_USE_MANTLE',
'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN', 'AWS_BEARER_TOKEN_BEDROCK',
'AWS_PROFILE', 'AWS_REGION', 'AWS_DEFAULT_REGION', 'AWS_CONFIG_FILE', 'AWS_SHARED_CREDENTIALS_FILE',
'AWS_ROLE_ARN', 'AWS_ROLE_SESSION_NAME', 'AWS_WEB_IDENTITY_TOKEN_FILE',
'ANTHROPIC_BEDROCK_BASE_URL', 'ANTHROPIC_BEDROCK_MANTLE_BASE_URL', 'ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION',
...GOOGLE_CLOUD_ENV_KEYS,
'CLOUD_ML_REGION', 'ANTHROPIC_VERTEX_PROJECT_ID', 'ANTHROPIC_VERTEX_BASE_URL',
'ANTHROPIC_FOUNDRY_RESOURCE', 'ANTHROPIC_FOUNDRY_BASE_URL',
'ANTHROPIC_FOUNDRY_API_KEY', 'ANTHROPIC_FOUNDRY_AUTH_TOKEN',
'AZURE_CLIENT_ID', 'AZURE_TENANT_ID', 'AZURE_CLIENT_SECRET', 'AZURE_AUTHORITY_HOST',
'AZURE_CLIENT_CERTIFICATE_PATH', 'AZURE_CLIENT_CERTIFICATE_PASSWORD', 'AZURE_FEDERATED_TOKEN_FILE',
'AZURE_CONFIG_DIR',
],
qwen: ['DASHSCOPE_API_KEY', 'OPENAI_API_KEY', 'OPENAI_BASE_URL', 'OPENAI_MODEL'],
gemini: ['GEMINI_API_KEY', 'GOOGLE_API_KEY', 'GEMINI_MODEL'],
gemini: ['GEMINI_API_KEY', 'GOOGLE_API_KEY', 'GEMINI_MODEL', ...GOOGLE_CLOUD_ENV_KEYS],
opencode: [
'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'OPENROUTER_API_KEY',
'OPENAI_BASE_URL', 'OPENAI_MODEL',
@@ -136,10 +157,11 @@ function textContainsCredential(filename) {
}
}
function nativeCredentialEnvironment(kind, env = process.env) {
function nativeCredentialEnvironment(kind, env = process.env, options = {}) {
const result = {}
for (const key of RUNTIME_ENV_KEYS[kind] || []) {
if (typeof env[key] === 'string' && env[key].trim()) result[key] = env[key]
if (options.providerConfigured && !NATIVE_CONFIG_ROOT_KEYS.has(key)) continue
if (typeof env[key] === 'string') result[key] = env[key]
}
return result
}
@@ -157,8 +179,7 @@ function allowedShellEnvironment(env = process.env, platform = process.platform)
const result = {}
for (const key of SHELL_ENV_KEYS) {
const value = source[key]
if (typeof value !== 'string' || (!value && !NETWORK_ENV_KEYS.includes(key))
|| value.length > MAX_SHELL_ENV_BYTES) continue
if (typeof value !== 'string' || value.length > MAX_SHELL_ENV_BYTES) continue
result[key] = value
}
return result
@@ -190,8 +211,7 @@ function parseNativeShellEnvironment(output) {
if (separator <= 0) continue
const key = entry.slice(0, separator)
const value = entry.slice(separator + 1)
if (!allowed.has(key) || (!value && !NETWORK_ENV_KEYS.includes(key))
|| value.length > MAX_SHELL_ENV_BYTES) continue
if (!allowed.has(key) || value.length > MAX_SHELL_ENV_BYTES) continue
result[key] = value
}
return result
@@ -357,6 +377,13 @@ function probeEnvironment(kind, options = {}) {
function claudeAuthState(output) {
try {
const status = JSON.parse(String(output || '').trim())
// Cloud status recognizes a provider selection without checking SDK credentials.
if (status?.loggedIn === true && status.authMethod === 'third_party') {
return { state: 'unknown', source: 'unverified' }
}
if (status?.loggedIn === true && status.authMethod === 'api_key') {
return { state: 'ready', source: 'native-credential' }
}
if (status?.loggedIn === true) return { state: 'ready', source: 'native-auth-status' }
if (status?.loggedIn === false) return { state: 'missing', source: 'native-auth-status' }
} catch { /* keep the file-based result */ }
@@ -681,8 +708,7 @@ async function resolveNativeCredentialState(kind, options = {}) {
? status.credentialState
: { state: 'unknown', source: 'native-runtime-unavailable' }
}
if (kind !== 'claude' || !options.executable
|| Object.keys(nativeCredentialKeyEnvironment(kind, options.env)).length) return current
if (kind !== 'claude' || !options.executable) return current
const platform = options.platform || process.platform
const prepareCommandFn = options.prepareCommandFn || prepareCommand
+3 -1
View File
@@ -398,7 +398,9 @@ async function runCoreAgent(agent, prompt, workdir, options = {}) {
const nativeEnv = connectorCredentialIsolation || agent.kind === 'openclaw'
? {}
: {
...nativeCredentialEnvironment(agent.kind, shellEnvironment.env),
...nativeCredentialEnvironment(agent.kind, shellEnvironment.env, {
providerConfigured: status.configured,
}),
...(shellEnvironment.env.PATH ? { PATH: shellEnvironment.env.PATH } : {}),
}
const callerEnv = connectorCredentialIsolation || agent.kind !== 'openclaw'
@@ -102,9 +102,11 @@ function providerOptionsFor(kind, generic, context = {}, status = {}) {
}
if (kind === 'claude') {
return {
provider: { id: 'anthropic', model: generic.OPENAI_MODEL, baseUrl: generic.OPENAI_BASE_URL },
env: {
...generic,
ANTHROPIC_API_KEY: generic.OPENAI_API_KEY,
MELDWORK_PROVIDER_API_KEY: generic.OPENAI_API_KEY,
ANTHROPIC_BASE_URL: generic.OPENAI_BASE_URL,
ANTHROPIC_MODEL: generic.OPENAI_MODEL,
},
@@ -1033,6 +1033,26 @@ test('Claude uses partial stream JSON and resumes its native session', () => {
assert.equal(spec.promptArg, true)
})
test('Claude selected Provider overrides native cloud selection without credentials in argv', () => {
const spec = invocation('claude', '/tmp/claude', '/tmp/work', 'native-session', {
provider: { id: 'anthropic', model: 'selected-model', baseUrl: 'https://selected.example' },
env: { ANTHROPIC_API_KEY: 'selected-secret-key' },
})
const settings = JSON.parse(spec.args[spec.args.indexOf('--settings') + 1])
assert.equal(settings.env.ANTHROPIC_BASE_URL, 'https://selected.example')
assert.equal(settings.env.ANTHROPIC_API_KEY, '')
assert.equal(settings.env.ANTHROPIC_AUTH_TOKEN, '')
assert.equal(settings.env.CLAUDE_CODE_OAUTH_TOKEN, '')
assert.equal(settings.env.ANTHROPIC_CUSTOM_HEADERS, '')
assert.match(settings.apiKeyHelper, /MELDWORK_PROVIDER_API_KEY/)
for (const flag of ['BEDROCK', 'VERTEX', 'FOUNDRY', 'MANTLE']) {
assert.equal(settings.env[`CLAUDE_CODE_USE_${flag}`], '0')
}
assert.equal(spec.args[spec.args.indexOf('--model') + 1], 'selected-model')
assert.equal(spec.args[spec.args.indexOf('--resume') + 1], 'native-session')
assert.equal(JSON.stringify(spec.args).includes('selected-secret-key'), false)
})
test('MiMo uses ACP by default and declares its JSON setup fallback', () => {
const spec = invocation('mimo', '/tmp/mimo', '/tmp/work', 'mimo-session', {
sandbox: 'workspace-write',
@@ -0,0 +1,102 @@
const assert = require('node:assert/strict')
const fs = require('node:fs')
const http = require('node:http')
const os = require('node:os')
const path = require('node:path')
const { execFile } = require('node:child_process')
const { promisify } = require('node:util')
const test = require('node:test')
const { invocation, runAgent } = require('../../../src/agents/cli/cli-adapters.cjs')
const { providerOptionsFor } = require('../../../src/providers/provider-options.cjs')
test('Claude credential helper reads a secret as data without shell expansion', {
skip: process.platform === 'win32',
}, async () => {
const secret = 'test-$(exit 37)-`exit 38`-"quoted"-$HOME-&-secret'
const spec = invocation('claude', '/tmp/claude', '/tmp', '', {
provider: { id: 'anthropic', model: 'test-model', baseUrl: 'https://example.com' },
platform: 'linux',
})
const settings = JSON.parse(spec.args[spec.args.indexOf('--settings') + 1])
const result = await promisify(execFile)('/bin/sh', ['-c', settings.apiKeyHelper], {
env: { MELDWORK_PROVIDER_API_KEY: secret }, timeout: 5000,
})
assert.equal(result.stdout, secret)
assert.equal(JSON.stringify(spec.args).includes(secret), false)
})
test('Claude Windows credential helper does not interpolate the key into cmd syntax', () => {
const spec = invocation('claude', 'C:\\claude.exe', 'C:\\work', '', {
provider: { id: 'anthropic', model: 'test-model' }, platform: 'win32',
})
const settings = JSON.parse(spec.args[spec.args.indexOf('--settings') + 1])
assert.match(settings.apiKeyHelper, /GetEnvironmentVariable\('MELDWORK_PROVIDER_API_KEY'\)/)
assert.doesNotMatch(settings.apiKeyHelper, /%MELDWORK_PROVIDER_API_KEY%/)
})
test('native Claude uses the selected Provider despite conflicting native settings', {
skip: !process.env.MELDWORK_TEST_CLAUDE_EXECUTABLE,
timeout: 40000,
}, async (t) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'meldwork-claude-provider-'))
t.after(() => fs.rmSync(root, { recursive: true, force: true }))
const requests = []
const selectedKey = 'selected-validation-key'
async function endpoint(label) {
const server = http.createServer((req, res) => {
req.resume()
if (req.method === 'HEAD') { res.writeHead(200); res.end(); return }
requests.push({ label, selectedKey: req.headers['x-api-key'] === selectedKey
|| req.headers.authorization === `Bearer ${selectedKey}` })
res.writeHead(200, { 'Content-Type': 'text/event-stream' })
for (const event of [
{ type: 'message_start', message: {
id: 'msg_local', type: 'message', role: 'assistant', model: 'claude-sonnet-4-6',
content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 1, output_tokens: 0 },
} },
{ type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } },
{ type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'LOCAL_PROVIDER_OK' } },
{ type: 'content_block_stop', index: 0 },
{ type: 'message_delta', delta: { stop_reason: 'end_turn', stop_sequence: null }, usage: { output_tokens: 1 } },
{ type: 'message_stop' },
]) res.write(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`)
res.end()
})
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
t.after(() => new Promise(resolve => { server.close(resolve); server.closeAllConnections() }))
return `http://127.0.0.1:${server.address().port}`
}
const selected = await endpoint('selected')
const native = await endpoint('native')
const config = path.join(root, '.claude')
fs.mkdirSync(config)
const filename = path.join(config, 'settings.json')
const settings = JSON.stringify({
apiKeyHelper: 'exit 39', permissions: { deny: ['Bash(*)'] },
env: {
CLAUDE_CODE_USE_BEDROCK: '1', ANTHROPIC_BASE_URL: native,
ANTHROPIC_API_KEY: 'native-validation-key', ANTHROPIC_AUTH_TOKEN: 'native-validation-token',
ANTHROPIC_CUSTOM_HEADERS: 'Authorization: Bearer native-header-token',
},
})
fs.writeFileSync(filename, settings)
const options = providerOptionsFor('claude', {
OPENAI_API_KEY: selectedKey, OPENAI_BASE_URL: selected, OPENAI_MODEL: 'claude-sonnet-4-6',
})
const events = []
const result = await runAgent({ kind: 'claude', executable: process.env.MELDWORK_TEST_CLAUDE_EXECUTABLE },
'Reply with OK. Do not use tools.', root, {
...options, signal: AbortSignal.timeout(25000), onEvent: event => events.push(event),
env: {
...options.env, HOME: root, USERPROFILE: root, CLAUDE_CONFIG_DIR: config,
XDG_CONFIG_HOME: root, XDG_DATA_HOME: root, XDG_STATE_HOME: root, XDG_CACHE_HOME: root,
HTTP_PROXY: '', HTTPS_PROXY: '', ALL_PROXY: '', http_proxy: '', https_proxy: '', all_proxy: '',
NO_PROXY: '*', no_proxy: '*', CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
},
})
assert.equal(result.text, 'LOCAL_PROVIDER_OK')
assert.ok(requests.length > 0)
assert.ok(requests.every(request => request.label === 'selected' && request.selectedKey))
assert.equal(fs.readFileSync(filename, 'utf8'), settings)
assert.equal(JSON.stringify(events).includes(selectedKey), false)
})
@@ -527,6 +527,22 @@ test('terminal reconciliation never reconstructs an exact 8-byte child secret',
}
})
test('AWS access keys are redacted across every streaming boundary', () => {
const secret = 'AKIATESTACCESSKEY1234'
for (let split = 1; split < secret.length; split += 1) {
const emitted = []
const runtimeEvents = createRuntimeEventEmitter(
{ onEvent: event => emitted.push(event) }, { AWS_ACCESS_KEY_ID: secret },
)
runtimeEvents.emit({ type: 'answer_delta', status: 'running', delta: secret.slice(0, split) })
runtimeEvents.emit({ type: 'answer_delta', status: 'running', delta: secret.slice(split) })
runtimeEvents.emitFinalAnswer(secret)
assert.equal(JSON.stringify(emitted).includes(secret), false)
assert.equal(emitted.filter(event => event.type === 'answer_delta')
.map(event => event.delta).join('').includes(secret), false)
}
})
test('runtime answer delivery reconciles empty and incomplete deltas with the terminal answer', () => {
for (const deltas of [[], [''], ['authoritative ']]) {
const emitted = []
@@ -768,6 +768,104 @@ test('Claude auth status overrides a stale credential file', async () => {
}
})
test('Claude probes native auth with cloud configuration even when a direct API key exists', async () => {
let calls = 0
const env = {
ANTHROPIC_API_KEY: 'unused-direct-key', CLAUDE_CODE_USE_BEDROCK: '1',
AWS_PROFILE: 'native-profile', AWS_REGION: 'us-east-1',
AWS_SESSION_TOKEN: 'native-session-token', GITHUB_TOKEN: 'unrelated-secret',
}
const result = await resolveNativeCredentialState('claude', {
env, executable: '/tmp/claude',
execFileFn: async (command, args, options) => {
calls += 1
assert.deepEqual(args, ['auth', 'status', '--json'])
assert.equal(options.env.AWS_PROFILE, env.AWS_PROFILE)
assert.equal(options.env.AWS_SESSION_TOKEN, env.AWS_SESSION_TOKEN)
assert.equal(options.env.CLAUDE_CODE_USE_BEDROCK, '1')
assert.equal(options.env.GITHUB_TOKEN, undefined)
return { stdout: JSON.stringify({ loggedIn: false }) }
},
})
assert.equal(calls, 1)
assert.deepEqual(result, { state: 'missing', source: 'native-auth-status' })
})
test('Claude API key status remains configuration evidence rather than verified authentication', async () => {
const result = await resolveNativeCredentialState('claude', {
env: { ANTHROPIC_API_KEY: 'unverified-api-key' }, executable: '/tmp/claude',
execFileFn: async () => ({ stdout: JSON.stringify({ loggedIn: true, authMethod: 'api_key' }) }),
})
assert.deepEqual(result, { state: 'ready', source: 'native-credential' })
})
test('Claude cloud provider recognition alone does not verify SDK credentials', async () => {
for (const apiProvider of ['bedrock', 'vertex', 'foundry']) {
const result = await resolveNativeCredentialState('claude', {
env: { ANTHROPIC_API_KEY: 'unused-direct-key' }, executable: '/tmp/claude',
execFileFn: async () => ({ stdout: JSON.stringify({
loggedIn: true, authMethod: 'third_party', apiProvider,
}) }),
})
assert.deepEqual(result, { state: 'unknown', source: 'unverified' })
}
})
test('real shell extraction preserves explicit empty cloud and credential overrides', {
skip: process.platform === 'win32',
}, async () => {
const execFile = require('node:util').promisify(require('node:child_process').execFile)
const result = await resolveNativeShellEnvironment({
cache: false, platform: 'linux', shell: '/bin/sh',
env: { AWS_PROFILE: 'stale-profile', ANTHROPIC_API_KEY: 'stale-key', PATH: '/usr/bin:/bin' },
execFileFn: (command, args, options) => execFile(command, ['-c',
"export AWS_PROFILE='' ANTHROPIC_API_KEY='' CLAUDE_CODE_USE_BEDROCK='0'\n" + args[1],
], options),
})
assert.equal(result.source, 'native-shell')
assert.equal(result.env.AWS_PROFILE, '')
assert.equal(result.env.ANTHROPIC_API_KEY, '')
assert.equal(result.env.CLAUDE_CODE_USE_BEDROCK, '0')
assert.deepEqual(nativeCredentialEnvironment('claude', result.env), {
AWS_PROFILE: '', ANTHROPIC_API_KEY: '', CLAUDE_CODE_USE_BEDROCK: '0',
})
const fallback = await resolveNativeShellEnvironment({
cache: false, platform: 'win32', env: { AWS_PROFILE: '', ANTHROPIC_API_KEY: '' },
})
assert.equal(fallback.env.AWS_PROFILE, '')
assert.equal(fallback.env.ANTHROPIC_API_KEY, '')
})
test('native cloud configuration is forwarded only to compatible Agents', () => {
const cloud = {
CLAUDE_CODE_USE_VERTEX: '1', AWS_PROFILE: 'native', AWS_ACCESS_KEY_ID: 'access-key',
AWS_SECRET_ACCESS_KEY: 'secret-key', AZURE_CLIENT_SECRET: 'azure-secret',
GOOGLE_APPLICATION_CREDENTIALS: '/tmp/adc.json', GOOGLE_CLOUD_PROJECT: 'native-project',
CLOUD_ML_REGION: 'us-east5', ANTHROPIC_FOUNDRY_RESOURCE: 'native-resource',
}
assert.deepEqual(nativeCredentialEnvironment('claude', cloud), cloud)
assert.deepEqual(nativeCredentialEnvironment('gemini', cloud), {
GOOGLE_APPLICATION_CREDENTIALS: '/tmp/adc.json', GOOGLE_CLOUD_PROJECT: 'native-project',
})
assert.deepEqual(nativeCredentialEnvironment('codex', cloud), {})
})
test('selected Provider retains native configuration roots without native credentials or selectors', () => {
const env = {
CLAUDE_CONFIG_DIR: '/tmp/claude-config', CODEX_HOME: '/tmp/codex-config',
PI_CODING_AGENT_DIR: '/tmp/pi-config', AWS_PROFILE: 'native',
CLAUDE_CODE_USE_BEDROCK: '1', ANTHROPIC_API_KEY: 'native-key',
ANTHROPIC_BASE_URL: 'https://native.example', OPENAI_API_KEY: 'other-key',
}
for (const [kind, root] of [
['claude', 'CLAUDE_CONFIG_DIR'], ['codex', 'CODEX_HOME'], ['pi', 'PI_CODING_AGENT_DIR'],
]) {
assert.deepEqual(nativeCredentialEnvironment(kind, env, { providerConfigured: true }), {
[root]: env[root],
})
}
})
test('an installed Agent without credential evidence remains unverified', () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'meldwork-missing-readiness-'))
try {
@@ -59,6 +59,7 @@ test('Provider options map native Agent credential names without dropping generi
},
claude: {
ANTHROPIC_API_KEY: 'provider-key',
MELDWORK_PROVIDER_API_KEY: 'provider-key',
ANTHROPIC_BASE_URL: 'https://api.example.com/v1',
ANTHROPIC_MODEL: 'example-model',
},
@@ -71,6 +72,7 @@ test('Provider options map native Agent credential names without dropping generi
for (const [kind, values] of Object.entries(expected)) {
assert.deepEqual(providerOptionsFor(kind, GENERIC), {
...(kind === 'claude' ? { provider: { id: 'anthropic', model: 'example-model', baseUrl: GENERIC.OPENAI_BASE_URL } } : {}),
env: { ...GENERIC, ...values },
})
}
+4 -5
View File
@@ -1996,12 +1996,11 @@ test('configured Provider is injected only through local Agent execution options
const { harness } = loadMain(directory, {
providerConfigured: true,
nativeShellEnvironment: () => ({
env: { PATH: '/opt/native-agent/bin:/usr/bin', https_proxy: 'http://proxy.example:3128', NO_PROXY: 'localhost,127.0.0.1' },
env: { PATH: '/opt/native-agent/bin:/usr/bin', https_proxy: 'http://proxy.example:3128', NO_PROXY: 'localhost,127.0.0.1', ANTHROPIC_API_KEY: 'native-hermes-key' },
source: 'native-shell',
}),
nativeEnvironment: kind => kind === 'hermes'
? { ANTHROPIC_API_KEY: 'native-hermes-key' }
: {},
nativeEnvironment: (...args) => require('../../src/agents/local-agent-readiness.cjs')
.nativeCredentialEnvironment(...args),
})
await harness.ready()
@@ -2017,7 +2016,7 @@ test('configured Provider is injected only through local Agent execution options
assert.equal(options.env.OPENAI_MODEL, PROVIDER_METADATA.model)
assert.equal(options.env.HERMES_INFERENCE_PROVIDER, 'openai-api')
assert.equal(options.env.HERMES_INFERENCE_MODEL, PROVIDER_METADATA.model)
assert.equal(options.env.ANTHROPIC_API_KEY, 'native-hermes-key')
assert.equal(options.env.ANTHROPIC_API_KEY, undefined)
assert.equal(options.env.CURRENT_RUN, '1')
assert.equal(options.env.PATH, '/opt/native-agent/bin:/usr/bin')
assert.equal(options.env.https_proxy, 'http://proxy.example:3128')
@@ -825,7 +825,7 @@ function loadMain(userData, options = {}) {
'./agents/custom-agent-store.cjs': { CustomAgentStore: TestCustomAgentStore },
'./attachments/attachment-store.cjs': { AttachmentStore: TestAttachmentStore },
'./agents/local-agent-readiness.cjs': {
nativeCredentialEnvironment: kind => options.nativeEnvironment?.(kind) || {},
nativeCredentialEnvironment: (...args) => options.nativeEnvironment?.(...args) || {},
resolveNativeShellEnvironment: async input => options.nativeShellEnvironment?.(input) || ({
env: { PATH: process.env.PATH || '' },
source: 'process',
@@ -312,6 +312,26 @@ test('recent runtime authentication failures stay blocked during the retry coold
assert.equal(agents()[0].availabilitySource, 'runtime-auth-failure')
})
test('Claude API key and cloud status cannot erase a recent runtime authentication failure', async () => {
const { resolveNativeCredentialState } = require('../../src/agents/local-agent-readiness.cjs')
for (const authMethod of ['api_key', 'third_party']) {
const { agents, catalog, state } = fixture({
detectAgents: async () => [{ kind: 'claude', compatibilityState: 'compatible' }],
credentialState: () => resolveNativeCredentialState('claude', {
executable: '/tmp/claude', env: { ANTHROPIC_API_KEY: 'unverified-key' },
execFileFn: async () => ({ stdout: JSON.stringify({ loggedIn: true, authMethod }) }),
}),
})
state.agentRuntime.claude = {
credentialState: 'missing', checkedAt: '2026-08-02T23:59:45.000Z',
}
await catalog.refresh()
assert.equal(agents()[0].invocable, false)
assert.equal(agents()[0].availabilitySource, 'runtime-auth-failure')
assert.equal(state.agentRuntime.claude.credentialState, 'missing')
}
})
test('configured credentials do not make an Agent invocable when runtime prerequisites fail', async () => {
const { agents, catalog } = fixture({
detectAgents: async () => [{
+19
View File
@@ -127,3 +127,22 @@
- 最终再次执行 `npm --prefix desktop test`:1515/1515,退出码 0,约 308 秒;日志 `/tmp/meldwork-105-desktop-final.log`。之前未确认全量通过的 Gate/ACP 重启路径也在本次全套中通过。`git diff --check` 通过。本检查点未更新包版本或重新打包。
此检查点仍未实现自然讨论按原生 Session 的确认状态去重发送,也未完成可恢复 `needs-human`、剩余 CLI 环境问题或最终版本验收。
## 原生云认证环境与显式 Provider 检查点
2026-09-09:原生调用现在按 Agent 协议白名单传递 Claude 的 Bedrock、Vertex、Foundry、OAuth/API 认证与模型配置,以及 Gemini 的 Google Cloud 项目和 ADC 配置路径。登录 shell 采集及回退均保留显式空值,避免用户清空的配置又被旧进程环境覆盖。云 SDK 配置的存在不直接证明认证成功,新增 AWS access key 也进入完整结果与流式分片脱敏。
选择 Meldwork Provider 后,原生环境只保留当前 Agent 的配置根,排除其他原生凭据和 Provider 选择;网络与 PATH 继续独立传递。真实 Claude CLI 进一步证明 `settings.json.env` 优先于进程环境:仅注入云开关为 0,仍选择原生云 Provider;原生 API 地址和密钥也会覆盖所选值。Claude 协议适配器因此使用高优先级 `--settings` 指定非秘密地址、清除冲突认证选择,并通过原生 `apiKeyHelper` 从子进程环境读取所选密钥。密钥不写入 argv 或临时配置文件,不改写用户原生配置,也不关闭全部设置或 Skills。此处属于必要协议适配,没有新增品牌或 Skill 驱动的业务调度。
认证探测区分证据强度:真实 CLI 在没有实际云凭据时也会返回 `loggedIn: true / third_party`,现在仅标记尚未验证;`api_key` 的肯定状态仍是凭据配置证据,不当作一次实际认证成功。两者均不能在刷新时立即抹去近期运行鉴权失败;没有近期失败时仍允许用户尝试原生运行。
验证与边界:
- 首批 readiness、Provider、CLI 协议和 Main 安全测试 189/189;完善云状态和空值后 Agent 套件 454/454。加入实际 Provider 密钥 helper 后 Agent 套件再次 454/454,Main/Provider 60/60。
- 新增 `cli-claude-provider.test.cjs`:通过 `MELDWORK_TEST_CLAUDE_EXECUTABLE` 指向实际 Claude CLI 的可选集成测试,与两项 helper 测试合计 3/3。使用本机 HTTP/SSE 模拟服务、合成凭据和隔离 HOME,实际 `runAgent` 请求在冲突原生配置下到达所选地址、携带所选密钥并返回 `LOCAL_PROVIDER_OK`;原生设置字节不变。默认未指定真实 CLI 时该集成项明确跳过。
- shell helper 对包含命令替换、引号和控制运算符的合成密钥按数据输出,不执行其中的 shell 文本。Windows helper 使用系统 PowerShell 读取环境变量,已有参数断言,但未在 Windows 主机实测。
- `/tmp/meldwork-105-provider-electron.cjs` 在独立配置 `/tmp/meldwork-105-provider-ui-DBsHOV` 完成真实 Electron 的保存 Provider、刷新 Agent、创建直聊及收取回复。会话 `946a2fac-8177-4471-bc5c-b0ec64eba92c` 显示 `LOCAL_PROVIDER_UI_OK`;两次模拟服务请求均使用所选地址与密钥,原生配置未变。已查看 `/tmp/meldwork-105-provider-ui.png`,脚本退出码 0,Electron 正常关闭。
- 最后补充 API key 配置证据不能清除近期运行失败的修正后,readiness 与 Agent catalog 回归 53/53。完整桌面测试结果另记于下方,不能把不同批次相加为一次全量结果。
- `MELDWORK_TEST_CLAUDE_EXECUTABLE=... npm --prefix desktop test`:1525/1525,跳过 0,退出码 0,约 290 秒;日志 `/tmp/meldwork-105-cloud-desktop.log`。该批次启动后新增了上述 API key 证据分类及两项回归,因此全量数字对应分类修正前的状态;最终分类行为由随后 53/53 的针对性测试覆盖。`git diff --check` 通过,尚未在最终分类修正后重跑整个桌面套件。
未使用真实 AWS、Azure、Vertex 凭据进行外部模型调用;云状态实测证明配置优先级和证据边界,不证明账户权限、配额或模型可用性。动态 `VERTEX_REGION_<MODEL>` 尚未纳入固定环境白名单,其他 Agent 的云 SDK 环境支持仍需核查;也未证明所有 Agent 原生配置均无法覆盖显式 Provider。本检查点未更新版本、重新打包或替换日常应用,完整 V1.0.5 目标仍在开发中。