mirror of
https://github.com/Ryder-Sun/Meldwork.git
synced 2026-11-07 06:56:03 -10:00
Support OpenCodeReview native configuration directories
Use the selected native home consistently for OCR detection, readiness, execution and session isolation, and explain the home-relative behavior in settings. Checks: desktop native configuration and security tests, frontend tests and builds, macOS pack, real OCR CLI with an isolated native home.
This commit is contained in:
1 parent
4a9fa31812
commit
90e43a3011
13 files changed
+98
-15
No files matched your search
@@ -111,6 +111,8 @@ For Zcode, choose an existing `.zcode/cli` directory containing `config.json`. Z
|
||||
|
||||
For Qwen Code, choose an existing `.qwen` directory containing settings or credentials. Qwen uses its parent user home for native sessions, Skills and other home-relative files; project and system settings retain their native precedence.
|
||||
|
||||
For OpenCodeReview, choose an existing `.opencodereview` directory containing `config.json`. OCR runs with that directory's parent user home, including its native session and other home-relative paths.
|
||||
|
||||
Hermes uses its native `config.yaml` and Provider settings. When its model, fallback model, or provider entries declare credential variables with `key_env` or `api_key_env`, Meldwork reads those variables from the launch environment or login shell for Hermes; unrelated variables and unsafe process controls are excluded. Hermes may prepare its own dependencies when started with a new native data directory.
|
||||
|
||||
## Context and local boundaries
|
||||
|
||||
@@ -122,6 +122,7 @@ const RUNTIME_ENV_KEYS = Object.freeze({
|
||||
...GOOGLE_CLOUD_ENV_KEYS,
|
||||
],
|
||||
opencodereview: [
|
||||
'HOME', 'USERPROFILE',
|
||||
'OCR_LLM_TOKEN', 'OCR_LLM_URL', 'OCR_LLM_MODEL', 'OCR_USE_ANTHROPIC',
|
||||
'OCR_LLM_PROTOCOL', 'OCR_LLM_AUTH_HEADER', 'OCR_LLM_TIMEOUT',
|
||||
'OPENAI_API_KEY', 'OPENAI_BASE_URL', 'OPENAI_MODEL',
|
||||
|
||||
@@ -705,8 +705,8 @@ function nativeCredentialState(kind, options = {}) {
|
||||
}
|
||||
const home = options.home || os.homedir()
|
||||
const env = options.env || process.env
|
||||
const configuredQwenHome = (options.platform || process.platform) === 'win32' ? env.USERPROFILE : env.HOME
|
||||
const qwenHome = typeof configuredQwenHome === 'string' && configuredQwenHome.trim() ? configuredQwenHome.trim() : home
|
||||
const configuredUserHome = (options.platform || process.platform) === 'win32' ? env.USERPROFILE : env.HOME
|
||||
const effectiveUserHome = typeof configuredUserHome === 'string' && configuredUserHome.trim() ? configuredUserHome.trim() : home
|
||||
const codexHome = env.CODEX_HOME || path.join(home, '.codex')
|
||||
const claudeHome = env.CLAUDE_CONFIG_DIR || path.join(home, '.claude')
|
||||
const geminiHome = env.GEMINI_CLI_HOME
|
||||
@@ -738,7 +738,7 @@ function nativeCredentialState(kind, options = {}) {
|
||||
const [key] = nativeSecretEnvKeys(options.nativeCredentialKeys)
|
||||
const inherited = key && Object.hasOwn(env, key)
|
||||
const envFile = !inherited && key
|
||||
? [path.join(qwenHome, '.qwen', '.env'), path.join(qwenHome, '.env')].find(file => fs.existsSync(file)) : ''
|
||||
? [path.join(effectiveUserHome, '.qwen', '.env'), path.join(effectiveUserHome, '.env')].find(file => fs.existsSync(file)) : ''
|
||||
const configured = inherited ? typeof env[key] === 'string' && env[key].trim()
|
||||
: envFile && dotenvContainsCredential(envFile, key)
|
||||
return configured
|
||||
@@ -787,10 +787,10 @@ function nativeCredentialState(kind, options = {}) {
|
||||
path.join(claudeHome, 'settings.json'),
|
||||
path.join(claudeHome, 'settings.local.json'),
|
||||
],
|
||||
qwen: [path.join(qwenHome, '.qwen', 'oauth_creds.json'), path.join(qwenHome, '.qwen', 'settings.json')],
|
||||
qwen: [path.join(effectiveUserHome, '.qwen', 'oauth_creds.json'), path.join(effectiveUserHome, '.qwen', 'settings.json')],
|
||||
gemini: geminiHome ? [path.join(geminiHome, 'oauth_creds.json'), path.join(geminiHome, 'settings.json')] : [],
|
||||
opencode: [openCodeAuth],
|
||||
opencodereview: [path.join(home, '.opencodereview', 'config.json')],
|
||||
opencodereview: [path.join(effectiveUserHome, '.opencodereview', 'config.json')],
|
||||
}[kind] || []
|
||||
if (jsonFiles.some(filename => kind === 'pi' ? !piCloudflare && piAuthContainsCredential(filename, piProvider)
|
||||
: hermesSelection ? path.basename(filename) === 'auth.json'
|
||||
@@ -820,13 +820,13 @@ function nativeCredentialState(kind, options = {}) {
|
||||
],
|
||||
mimo: [path.join(home, '.mimo', '.env'), path.join(home, '.mimocode', '.env')],
|
||||
claude: [path.join(claudeHome, '.env')],
|
||||
qwen: [path.join(qwenHome, '.qwen', '.env')],
|
||||
qwen: [path.join(effectiveUserHome, '.qwen', '.env')],
|
||||
gemini: geminiHome ? [path.join(geminiHome, '.env')] : [],
|
||||
opencode: [
|
||||
path.join(home, '.opencode', '.env'),
|
||||
path.join(home, '.config', 'opencode', '.env'),
|
||||
],
|
||||
opencodereview: [path.join(home, '.opencodereview', '.env')],
|
||||
opencodereview: [path.join(effectiveUserHome, '.opencodereview', '.env')],
|
||||
}[kind] || []
|
||||
if (textFiles.some(filename => hermesSelection && filename === path.join(hermesHome, 'config.yaml')
|
||||
? hermesConfigContainsCredential(hermesSelection)
|
||||
|
||||
@@ -164,12 +164,13 @@ const NATIVE_CONFIG_DIRECTORIES = Object.freeze({
|
||||
workbuddy: { key: 'CODEBUDDY_CONFIG_DIR', files: ['models.json', 'settings.json', '.credentials.json'] },
|
||||
zcode: { key: 'HOME', files: ['config.json'] },
|
||||
qwen: { key: 'HOME', files: ['settings.json', 'oauth_creds.json', '.env'], identityFiles: ['trustedFolders.json'] },
|
||||
opencodereview: { key: 'HOME', files: ['config.json'], identityFiles: ['.env'] },
|
||||
})
|
||||
function nativeIdentityDirectory(kind, env, home, platform = process.platform) {
|
||||
if (kind === 'qwen') {
|
||||
if (kind === 'qwen' || kind === 'opencodereview') {
|
||||
const paths = platform === 'win32' ? path.win32 : path.posix
|
||||
const base = (platform === 'win32' ? env?.USERPROFILE : env?.HOME) || home
|
||||
return typeof base === 'string' && paths.isAbsolute(base) ? paths.join(base, '.qwen') : ''
|
||||
return typeof base === 'string' && paths.isAbsolute(base) ? paths.join(base, kind === 'qwen' ? '.qwen' : '.opencodereview') : ''
|
||||
}
|
||||
if (kind === 'zcode') {
|
||||
const paths = platform === 'win32' ? path.win32 : path.posix
|
||||
@@ -207,10 +208,10 @@ function nativeConfigEnvironment(kind, preferences, env = {}, platform = process
|
||||
if (typeof directory !== 'string' || directory.length > 4096
|
||||
|| directory.includes('\u0000') || !path.isAbsolute(directory)) throw new Error('LOCAL_NATIVE_CONFIG_INVALID')
|
||||
if (kind === 'gemini' && path.basename(path.resolve(directory)) !== '.gemini') throw new Error('LOCAL_NATIVE_CONFIG_INVALID')
|
||||
if (kind === 'qwen') {
|
||||
if (kind === 'qwen' || kind === 'opencodereview') {
|
||||
const resolved = path.resolve(directory)
|
||||
const name = path.basename(resolved)
|
||||
if ((platform === 'win32' ? name.toLowerCase() : name) !== '.qwen') throw new Error('LOCAL_NATIVE_CONFIG_INVALID')
|
||||
if ((platform === 'win32' ? name.toLowerCase() : name) !== (kind === 'qwen' ? '.qwen' : '.opencodereview')) throw new Error('LOCAL_NATIVE_CONFIG_INVALID')
|
||||
const home = path.dirname(resolved)
|
||||
return platform === 'win32' ? { ...env, HOME: home, USERPROFILE: home } : { ...env, HOME: home }
|
||||
}
|
||||
@@ -233,6 +234,7 @@ function validateNativeConfigDirectory(kind, directory) {
|
||||
if (kind === 'gemini' && path.basename(path.resolve(directory)) !== '.gemini') throw new Error('LOCAL_NATIVE_CONFIG_INVALID')
|
||||
const caseName = value => process.platform === 'win32' ? value.toLowerCase() : value
|
||||
if (kind === 'qwen' && caseName(path.basename(path.resolve(directory))) !== '.qwen') throw new Error('LOCAL_NATIVE_CONFIG_INVALID')
|
||||
if (kind === 'opencodereview' && caseName(path.basename(path.resolve(directory))) !== '.opencodereview') throw new Error('LOCAL_NATIVE_CONFIG_INVALID')
|
||||
if (kind === 'zcode' && (caseName(path.basename(path.resolve(directory))) !== 'cli'
|
||||
|| caseName(path.basename(path.dirname(path.resolve(directory)))) !== '.zcode')) throw new Error('LOCAL_NATIVE_CONFIG_INVALID')
|
||||
let resolved
|
||||
@@ -245,7 +247,7 @@ function validateNativeConfigDirectory(kind, directory) {
|
||||
try { return fs.statSync(path.join(resolved, name)).isDirectory() } catch { return false }
|
||||
}))) throw new Error('invalid directory')
|
||||
} catch { throw new Error('LOCAL_NATIVE_CONFIG_INVALID') }
|
||||
return ['gemini', 'zcode', 'qwen'].includes(kind) ? path.resolve(directory) : resolved
|
||||
return ['gemini', 'zcode', 'qwen', 'opencodereview'].includes(kind) ? path.resolve(directory) : resolved
|
||||
}
|
||||
|
||||
module.exports = { NATIVE_CONFIG_DIRECTORIES, NATIVE_CONFIG_IDENTITY_DIRECTORY, NATIVE_ENVIRONMENT_IDENTITY, NATIVE_AUTH_IDENTITY_FILES, NATIVE_PROJECT_CONFIG_PATHS,
|
||||
|
||||
@@ -316,7 +316,7 @@ async function nativeShellEnvironment(options = {}) {
|
||||
for (const key of resolved.unsetKeys || []) delete env[key]
|
||||
opencodeCredentialKeys = opencodeCredentialKeys.filter(key => typeof env[key] === 'string' && env[key].length > 0)
|
||||
for (const [kind, { key }] of Object.entries(NATIVE_CONFIG_DIRECTORIES)) {
|
||||
if (kind === 'zcode') continue
|
||||
if (kind === 'zcode' || kind === 'opencodereview') continue
|
||||
if (kind === 'qwen') {
|
||||
const directory = (process.platform === 'win32' ? env.USERPROFILE : env.HOME) || ''
|
||||
if (Object.hasOwn(nativeSkillDirectories, key) && nativeSkillDirectories[key] !== directory) skillCatalog?.invalidate(kind)
|
||||
|
||||
@@ -42,6 +42,16 @@ test('OpenCodeReview receives its native Anthropic configuration without exposin
|
||||
assert.equal(redactChildSecrets('synthetic-token synthetic-key', child), '[redacted] [redacted]')
|
||||
})
|
||||
|
||||
test('OpenCodeReview readiness reads the selected native home', t => {
|
||||
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'meldwork-ocr-readiness-'))
|
||||
t.after(() => fs.rmSync(home, { recursive: true, force: true }))
|
||||
const selected = path.join(home, 'selected')
|
||||
fs.mkdirSync(path.join(selected, '.opencodereview'), { recursive: true })
|
||||
fs.writeFileSync(path.join(selected, '.opencodereview', 'config.json'), JSON.stringify({ api_key: 'synthetic-key' }))
|
||||
assert.equal(nativeCredentialState('opencodereview', { home, env: { HOME: selected } }).state, 'ready')
|
||||
assert.equal(nativeCredentialState('opencodereview', { home, env: {} }).state, 'unknown')
|
||||
})
|
||||
|
||||
test('Hermes receives native Bedrock and Vertex environment for discovery and execution', async () => {
|
||||
const env = { AWS_PROFILE: 'research', AWS_REGION: 'us-east-1', AWS_ACCESS_KEY_ID: 'native-access',
|
||||
AWS_SECRET_ACCESS_KEY: 'native-secret', GOOGLE_APPLICATION_CREDENTIALS: '/native/google.json',
|
||||
|
||||
@@ -5,6 +5,36 @@ const os = require('node:os')
|
||||
const path = require('node:path')
|
||||
const { nativeAuthIdentityFiles, nativeConfigEnvironment, nativeIdentityDirectory, nativeProjectConfigPaths, validateNativeConfigDirectory } = require('../../src/agents/native-config-directory.cjs')
|
||||
|
||||
test('OpenCodeReview selects its native configuration directory and parent home', t => {
|
||||
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'meldwork-ocr-config-home-'))
|
||||
t.after(() => fs.rmSync(home, { recursive: true, force: true }))
|
||||
const directory = path.join(home, '.opencodereview')
|
||||
fs.mkdirSync(directory)
|
||||
assert.throws(() => validateNativeConfigDirectory('opencodereview', directory), { message: 'LOCAL_NATIVE_CONFIG_INVALID' })
|
||||
fs.writeFileSync(path.join(directory, 'config.json'), '{}')
|
||||
assert.equal(validateNativeConfigDirectory('opencodereview', directory), directory)
|
||||
const preferences = { opencodereview: { nativeConfigDirectory: directory } }
|
||||
assert.equal(nativeConfigEnvironment('opencodereview', preferences, { HOME: '/old' }).HOME, home)
|
||||
const { childEnvironment } = require('../../src/agents/cli/cli-process-support.cjs')
|
||||
const selected = nativeConfigEnvironment('opencodereview', preferences, { HOME: '/old' })
|
||||
assert.equal(childEnvironment({ kind: 'opencodereview' }, home, { env: selected }, process.platform).HOME, home)
|
||||
assert.deepEqual(nativeConfigEnvironment('opencodereview', preferences, {}, 'win32'), { HOME: home, USERPROFILE: home })
|
||||
const windows = nativeConfigEnvironment('opencodereview', preferences, {}, 'win32')
|
||||
assert.equal(childEnvironment({ kind: 'opencodereview' }, home, { env: windows }, 'win32').USERPROFILE, home)
|
||||
assert.equal(nativeIdentityDirectory('opencodereview', { HOME: home }, '/other', 'linux'), directory)
|
||||
assert.throws(() => nativeConfigEnvironment('opencodereview', { opencodereview: { nativeConfigDirectory: home } }),
|
||||
{ message: 'LOCAL_NATIVE_CONFIG_INVALID' })
|
||||
if (process.platform !== 'win32') {
|
||||
assert.throws(() => nativeConfigEnvironment('opencodereview', { opencodereview: { nativeConfigDirectory: path.join(home, '.OPENCODEREVIEW') } }),
|
||||
{ message: 'LOCAL_NATIVE_CONFIG_INVALID' })
|
||||
const alias = path.join(home, 'alias', '.opencodereview')
|
||||
fs.mkdirSync(path.dirname(alias))
|
||||
fs.symlinkSync(directory, alias, 'dir')
|
||||
assert.equal(validateNativeConfigDirectory('opencodereview', alias), alias)
|
||||
assert.equal(nativeConfigEnvironment('opencodereview', { opencodereview: { nativeConfigDirectory: alias } }).HOME, path.dirname(alias))
|
||||
}
|
||||
})
|
||||
|
||||
test('OpenCodeReview verification changes when native credentials or configuration change', t => {
|
||||
const { NATIVE_AUTH_IDENTITY_FILES } = require('../../src/agents/native-config-directory.cjs')
|
||||
const { agentVerificationIdentity } = require('../../src/workspace/agent-verification-identity.cjs')
|
||||
|
||||
@@ -680,6 +680,34 @@ test('Qwen selected native configuration controls credentials, execution and Ski
|
||||
assert.equal(harness.skillCatalogInstances[0].input.configDirectoryForKind('qwen'), path.resolve(home))
|
||||
})
|
||||
|
||||
test('OpenCodeReview selected native directory reaches detection and execution', async t => {
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'meldwork-ocr-config-main-'))
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }))
|
||||
const home = path.join(directory, 'selected-home')
|
||||
const selected = path.join(home, '.opencodereview')
|
||||
fs.mkdirSync(selected, { recursive: true })
|
||||
fs.writeFileSync(path.join(selected, 'config.json'), '{}')
|
||||
const readiness = require('../../src/agents/local-agent-readiness.cjs')
|
||||
let detected
|
||||
const { harness } = loadMain(directory, {
|
||||
dialogResult: { canceled: false, filePaths: [selected] },
|
||||
nativeEnvironment: readiness.nativeCredentialEnvironment,
|
||||
detectAgents: input => { detected = input.nativeEnvironmentForKind('opencodereview'); return [] },
|
||||
})
|
||||
await harness.ready()
|
||||
const workspace = harness.workspaceInstances[0]
|
||||
workspace.state.agentPreferences = {}
|
||||
workspace.state.agentRuntime = {}
|
||||
workspace.activeRuns = new Map()
|
||||
workspace.save = () => {}
|
||||
const choice = await harness.ipcHandlers.get('local-agent-native-config:choose')(harness.event(), 'opencodereview')
|
||||
assert.equal(choice.directory, selected)
|
||||
await harness.installerInstances[0].input.detectAgents()
|
||||
assert.equal(detected.HOME, home)
|
||||
await workspace.input.runAgent({ kind: 'opencodereview', executable: '/tmp/ocr' }, 'hello', directory, {})
|
||||
assert.equal(harness.runAgentCalls.at(-1)[3].env.HOME, home)
|
||||
})
|
||||
|
||||
test('Qwen verification follows the selected native home environment file', async t => {
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'meldwork-qwen-selected-identity-'))
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }))
|
||||
|
||||
@@ -49,6 +49,14 @@ it('explains the Qwen home directory used by the selected configuration', async
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('explains the OpenCodeReview native directory', async () => {
|
||||
window.meldworkDesktop = { localAgentNativeConfig: { get: vi.fn().mockResolvedValue({ directory: '' }) } }
|
||||
const wrapper = mount(AgentNativeConfig, { props: { kind: 'opencodereview' } })
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain(t('nativeConfig.expected.opencodereview'))
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('does not claim automatic discovery when the saved directory cannot be read', async () => {
|
||||
window.meldworkDesktop = { localAgentNativeConfig: {
|
||||
get: vi.fn().mockRejectedValue(new Error('LOCAL_WORKSPACE_UNAVAILABLE')),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<section class="agent-detail-section">
|
||||
<h3>{{ t('nativeConfig.title') }}</h3>
|
||||
<p v-if="directory !== null">{{ directory || t('nativeConfig.automatic') }}</p>
|
||||
<p v-if="['zcode', 'qwen'].includes(kind)">{{ t(`nativeConfig.expected.${kind}`) }}</p>
|
||||
<p v-if="['zcode', 'qwen', 'opencodereview'].includes(kind)">{{ t(`nativeConfig.expected.${kind}`) }}</p>
|
||||
<p v-if="directoryMissing" class="form-error" role="alert">{{ t('nativeConfig.directoryMissing') }} {{ t(`nativeConfig.expected.${kind}`) }}</p>
|
||||
<p v-if="kind === 'hermes' && directory !== null">{{ t('nativeConfig.configuredProvider') }}: {{ provider || t('nativeConfig.providerUnknown') }}</p>
|
||||
<p v-if="['codex', 'zcode'].includes(kind) && directory !== null">{{ t('nativeConfig.userConfiguredProvider') }}: {{ provider || t('nativeConfig.providerUnknown') }}</p>
|
||||
|
||||
@@ -353,7 +353,7 @@ import AgentNativeConfig from './AgentNativeConfig.vue'
|
||||
import { desktopApi } from '../desktop.js'
|
||||
|
||||
const nativeConfigAvailable = Boolean(desktopApi()?.localAgentNativeConfig)
|
||||
const nativeConfigKinds = ['hermes', 'codex', 'claude', 'pi', 'kimi', 'mimo', 'deepseek', 'workbuddy', 'opencode', 'gemini', 'zcode', 'qwen']
|
||||
const nativeConfigKinds = ['hermes', 'codex', 'claude', 'pi', 'kimi', 'mimo', 'deepseek', 'workbuddy', 'opencode', 'gemini', 'zcode', 'qwen', 'opencodereview']
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@@ -22,6 +22,7 @@ export default {
|
||||
'nativeConfig.expected.gemini': 'Choose a .gemini directory containing settings or credentials.',
|
||||
'nativeConfig.expected.zcode': 'Choose a .zcode/cli directory containing config.json. Zcode uses its parent user home.',
|
||||
'nativeConfig.expected.qwen': 'Choose a .qwen directory containing settings or credentials. Qwen uses its parent user home.',
|
||||
'nativeConfig.expected.opencodereview': 'Choose a .opencodereview directory containing config.json. OpenCodeReview uses its parent user home.',
|
||||
'skillTrust.title': 'Skill trust',
|
||||
'skillTrust.hint': 'Review trust granted to an Agent and exact Skill content. Changed content requires approval again.',
|
||||
'skillTrust.refresh': 'Refresh',
|
||||
|
||||
@@ -22,6 +22,7 @@ export default {
|
||||
'nativeConfig.expected.gemini': '请选择包含设置或认证文件的 .gemini 目录。',
|
||||
'nativeConfig.expected.zcode': '请选择包含 config.json 的 .zcode/cli 目录;Zcode 会使用其所属的用户主目录。',
|
||||
'nativeConfig.expected.qwen': '请选择包含设置或认证文件的 .qwen 目录;Qwen 会使用其所属的用户主目录。',
|
||||
'nativeConfig.expected.opencodereview': '请选择包含 config.json 的 .opencodereview 目录;OpenCodeReview 会使用其所属的用户主目录。',
|
||||
'skillTrust.title': 'Skill 信任',
|
||||
'skillTrust.hint': '查看按 Agent 和内容绑定的信任授权。内容变化后需要重新授权。',
|
||||
'skillTrust.refresh': '刷新',
|
||||
|
||||
Reference in new issue
Block a user