mirror of
https://github.com/Ryder-Sun/Meldwork.git
synced 2026-11-07 06:56:03 -10:00
Support OpenClaw native state directories
Use selected or shell-configured state roots for native discovery while retaining guarded path and isolated execution checks. Verified readiness, security and frontend tests, frontend builds and desktop packaging.
This commit is contained in:
1 parent
1c8eb35ad5
commit
00a0353f6d
12 files changed
+68
-6
No files matched your search
@@ -105,6 +105,8 @@ An explicit ACP provider or model in the native profile or DSH home patch takes
|
||||
|
||||
WorkBuddy also supports selecting its existing native configuration directory in Settings → Agents. Its model list, execution environment, and local and plugin Skills use the selected directory.
|
||||
|
||||
OpenClaw also supports selecting its native state directory in Settings → Agents. Meldwork reads the selected Agent's model configuration from that directory and runs it in its guarded local runtime.
|
||||
|
||||
When switching an Agent's native configuration directory, Meldwork keeps separate local conversation sessions for each selected directory and restores them when switching back. Configuration changes are blocked while a run is active or awaiting recovery.
|
||||
|
||||
For Zcode, choose an existing `.zcode/cli` directory containing `config.json`. Zcode runs with that directory's parent user home, which also affects its home-relative session and other file paths; project configuration may still override user configuration.
|
||||
|
||||
@@ -105,6 +105,8 @@ DeepSeek Harness(`dsh`)通过 ACP 支持私聊与群聊,各对话使用独
|
||||
|
||||
WorkBuddy 也可以在「设置 → Agent」中选择已有的原生配置目录;模型列表、执行环境以及本地和插件 Skill 都会使用所选目录。
|
||||
|
||||
OpenClaw 也可以在「设置 → Agent」中选择原生状态目录。Meldwork 会从该目录读取所选 Agent 的模型配置,并在受保护的本地运行环境中执行。
|
||||
|
||||
切换 Agent 的原生配置目录时,Meldwork 会按目录分别保存本地会话,切回原目录后恢复对应会话。任务运行中或等待恢复时,暂时不能切换配置。
|
||||
|
||||
Zcode 请选择包含 `config.json` 的 `.zcode/cli` 目录。运行时 Zcode 会使用该目录所属的用户主目录,这也会影响其会话及其他文件中的主目录相对路径;项目配置仍可能覆盖用户配置。
|
||||
|
||||
@@ -42,7 +42,7 @@ const RUNTIME_ENV_KEYS = Object.freeze({
|
||||
'AWS_CONTAINER_AUTHORIZATION_TOKEN', 'AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE', ...AWS_SDK_ENV_KEYS,
|
||||
'VERTEX_CREDENTIALS_PATH', 'VERTEX_PROJECT_ID', 'VERTEX_REGION', ...GOOGLE_CLOUD_ENV_KEYS,
|
||||
],
|
||||
openclaw: ['OPENAI_API_KEY', 'OPENROUTER_API_KEY'],
|
||||
openclaw: ['OPENAI_API_KEY', 'OPENROUTER_API_KEY', 'OPENCLAW_STATE_DIR'],
|
||||
workbuddy: [
|
||||
'CODEBUDDY_API_KEY', 'CODEBUDDY_AUTH_TOKEN', 'CODEBUDDY_BASE_URL', 'CODEBUDDY_MODEL',
|
||||
'CODEBUDDY_CONFIG_DIR',
|
||||
|
||||
@@ -1083,7 +1083,9 @@ function parseOpenClawStatus(output, options = {}) {
|
||||
return { credentialState: { state: 'unknown', source: 'native-runtime-unavailable' } }
|
||||
}
|
||||
const resolvedHome = fs.realpathSync(path.resolve(options.home || os.homedir()))
|
||||
const nativeRoot = path.join(resolvedHome, '.openclaw')
|
||||
const configuredRoot = options.env?.OPENCLAW_STATE_DIR
|
||||
if (configuredRoot && !path.isAbsolute(configuredRoot)) return null
|
||||
const nativeRoot = configuredRoot ? path.resolve(configuredRoot) : path.join(resolvedHome, '.openclaw')
|
||||
const nativeRootIdentity = openClawDirectoryIdentity(nativeRoot)
|
||||
const agentDir = fs.realpathSync(requestedAgentDir)
|
||||
if (!fs.statSync(agentDir).isDirectory() || !pathInside(nativeRoot, agentDir)) {
|
||||
|
||||
@@ -154,6 +154,7 @@ function nativeAuthIdentityFiles(kind, env, home, platform = process.platform) {
|
||||
|
||||
const NATIVE_CONFIG_DIRECTORIES = Object.freeze({
|
||||
hermes: { key: 'HERMES_HOME', files: ['config.yaml', 'auth.json', '.env'], identityFiles: ['config.json'] },
|
||||
openclaw: { key: 'OPENCLAW_STATE_DIR', files: ['openclaw.json'], directories: ['agents'] },
|
||||
codex: { key: 'CODEX_HOME', files: ['config.toml', 'auth.json'], identityFiles: ['config.json', '.env'] },
|
||||
claude: { key: 'CLAUDE_CONFIG_DIR', files: ['settings.json', '.credentials.json'], identityFiles: ['settings.local.json', '.env'] },
|
||||
pi: { key: 'PI_CODING_AGENT_DIR', files: ['settings.json', 'auth.json', 'models.json'] },
|
||||
@@ -193,7 +194,7 @@ function nativeIdentityDirectory(kind, env, home, platform = process.platform) {
|
||||
hermes: platform === 'win32'
|
||||
? path.join(env?.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), `hermes${env?.HERMES_DATA_DIR_SUFFIX || ''}`)
|
||||
: path.join(home, `.hermes${env?.HERMES_DATA_DIR_SUFFIX || ''}`),
|
||||
codex: path.join(home, '.codex'), claude: path.join(home, '.claude'),
|
||||
codex: path.join(home, '.codex'), claude: path.join(home, '.claude'), openclaw: path.join(home, '.openclaw'),
|
||||
pi: path.join(home, '.pi', 'agent'), kimi: path.join(home, '.kimi-code'),
|
||||
mimo: path.join(env?.XDG_CONFIG_HOME || path.join(home, '.config'), 'mimocode'),
|
||||
opencode: path.join(env?.XDG_CONFIG_HOME || path.join(home, '.config'), 'opencode'),
|
||||
|
||||
@@ -406,7 +406,7 @@ async function resolveProjectEnvironmentIdentities(workdir, kinds) {
|
||||
if (!agent?.executable) continue
|
||||
try {
|
||||
const runtime = await resolveNativeOpenClawRuntime({ executable: agent.executable,
|
||||
env: captured.env, home: captured.home })
|
||||
env: nativeConfigEnvironment(kind, workspace?.state.agentPreferences, captured.env), home: captured.home })
|
||||
result[kind] = nativeEnvironmentIdentity({ runtime: JSON.stringify(runtime),
|
||||
...networkEnvironment(captured.env) })
|
||||
} catch { /* Fail closed for this Agent without blocking other kinds. */ }
|
||||
@@ -546,7 +546,7 @@ async function runCoreAgent(agent, prompt, workdir, options = {}) {
|
||||
const nativeRuntime = !connectorCredentialIsolation && agent.kind === 'openclaw'
|
||||
? await resolveNativeOpenClawRuntime({
|
||||
executable: agent.executable,
|
||||
env: shellEnvironment.env,
|
||||
env: selectedNativeEnv,
|
||||
home: shellEnvironment.home,
|
||||
})
|
||||
: null
|
||||
|
||||
@@ -1538,6 +1538,26 @@ test('OpenClaw native runtime rejects Env SecretRefs without reading unrelated p
|
||||
assert.equal(calls[0].env.GITHUB_TOKEN, undefined)
|
||||
})
|
||||
|
||||
test('OpenClaw native runtime accepts a selected state root but rejects agents outside it', async t => {
|
||||
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'meldwork-openclaw-selected-root-'))
|
||||
t.after(() => fs.rmSync(home, { recursive: true, force: true }))
|
||||
const selected = fs.realpathSync(home)
|
||||
const agentDir = path.join(selected, 'agents', 'main', 'agent')
|
||||
fs.mkdirSync(agentDir, { recursive: true })
|
||||
fs.writeFileSync(path.join(agentDir, 'models.json'), openClawModelsCatalog('synthetic-key'))
|
||||
const status = directory => JSON.stringify({ resolvedDefault: 'provider/model', agentDir: directory,
|
||||
auth: { missingProvidersInUse: [] } })
|
||||
const options = { home, env: { OPENCLAW_STATE_DIR: selected }, executable: '/tmp/openclaw',
|
||||
execFileFn: async (_command, _args, child) => {
|
||||
assert.equal(child.env.OPENCLAW_STATE_DIR, selected)
|
||||
return { stdout: status(agentDir) }
|
||||
} }
|
||||
assert.equal((await resolveNativeOpenClawRuntime(options)).provider.apiKey, 'synthetic-key')
|
||||
await assert.rejects(resolveNativeOpenClawRuntime({ ...options,
|
||||
execFileFn: async () => ({ stdout: status(path.dirname(selected)) }) }),
|
||||
{ message: 'OPENCLAW_NATIVE_RUNTIME_UNAVAILABLE' })
|
||||
})
|
||||
|
||||
test('OpenClaw native runtime rejects every structured and string SecretRef marker', async (t) => {
|
||||
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'meldwork-openclaw-secretrefs-'))
|
||||
const agentDir = path.join(home, '.openclaw', 'agents', 'main', 'agent')
|
||||
|
||||
@@ -5,6 +5,18 @@ const os = require('node:os')
|
||||
const path = require('node:path')
|
||||
const { nativeAuthIdentityFiles, nativeConfigEnvironment, nativeIdentityDirectory, nativeProjectConfigPaths, validateNativeConfigDirectory } = require('../../src/agents/native-config-directory.cjs')
|
||||
|
||||
test('OpenClaw selects its native state directory without changing other Agents', t => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'meldwork-openclaw-config-'))
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }))
|
||||
assert.throws(() => validateNativeConfigDirectory('openclaw', root), { message: 'LOCAL_NATIVE_CONFIG_INVALID' })
|
||||
fs.mkdirSync(path.join(root, 'agents'))
|
||||
const selected = validateNativeConfigDirectory('openclaw', root)
|
||||
const preferences = { openclaw: { nativeConfigDirectory: selected } }
|
||||
assert.equal(nativeConfigEnvironment('openclaw', preferences, {}).OPENCLAW_STATE_DIR, selected)
|
||||
assert.equal(nativeIdentityDirectory('openclaw', { OPENCLAW_STATE_DIR: selected }, root), selected)
|
||||
assert.equal(nativeConfigEnvironment('hermes', preferences, {}).OPENCLAW_STATE_DIR, undefined)
|
||||
})
|
||||
|
||||
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 }))
|
||||
|
||||
@@ -991,6 +991,27 @@ test('OpenClaw project identity tracks its guarded native runtime without exposi
|
||||
assert.equal(failed.codex, second.codex)
|
||||
})
|
||||
|
||||
test('OpenClaw selected state directory reaches runtime discovery and isolated execution', async t => {
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'meldwork-openclaw-selected-main-'))
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }))
|
||||
const selected = path.join(directory, 'native-state')
|
||||
fs.mkdirSync(path.join(selected, 'agents'), { recursive: true })
|
||||
const calls = []
|
||||
const { harness } = loadMain(directory, {
|
||||
nativeOpenClawRuntime: input => {
|
||||
calls.push(input.env.OPENCLAW_STATE_DIR)
|
||||
return { model: 'native/model', provider: { id: 'native', baseUrl: 'https://example.test', apiKey: 'synthetic' } }
|
||||
},
|
||||
})
|
||||
await harness.ready()
|
||||
const workspace = harness.workspaceInstances[0]
|
||||
workspace.state.agentPreferences = { openclaw: { nativeConfigDirectory: selected } }
|
||||
workspace.detectedAgents = [{ kind: 'openclaw', executable: '/native/openclaw' }]
|
||||
await workspace.input.resolveProjectEnvironmentIdentities(directory, ['openclaw'])
|
||||
await workspace.input.runAgent({ kind: 'openclaw', executable: '/native/openclaw' }, 'hello', directory, {})
|
||||
assert.deepEqual(calls, [selected, selected])
|
||||
})
|
||||
|
||||
test('MiMo captures the recent model provider credential for a project', async t => {
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'meldwork-mimo-recent-main-'))
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }))
|
||||
|
||||
@@ -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', 'opencodereview']
|
||||
const nativeConfigKinds = ['hermes', 'openclaw', 'codex', 'claude', 'pi', 'kimi', 'mimo', 'deepseek', 'workbuddy', 'opencode', 'gemini', 'zcode', 'qwen', 'opencodereview']
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@@ -11,6 +11,7 @@ export default {
|
||||
'nativeConfig.invalid': 'The selected directory does not contain this Agent’s native configuration.',
|
||||
'nativeConfig.sessionProfilesInvalid': 'Cannot safely switch configurations because saved session data is invalid or at its limit.',
|
||||
'nativeConfig.expected.hermes': 'Expected config.yaml, auth.json, or .env.',
|
||||
'nativeConfig.expected.openclaw': 'Expected openclaw.json or an agents directory.',
|
||||
'nativeConfig.expected.codex': 'Expected config.toml or auth.json.',
|
||||
'nativeConfig.expected.claude': 'Expected settings.json or .credentials.json.',
|
||||
'nativeConfig.expected.pi': 'Expected settings.json, auth.json or models.json.',
|
||||
|
||||
@@ -11,6 +11,7 @@ export default {
|
||||
'nativeConfig.invalid': '所选目录没有找到此 Agent 的原生配置文件。',
|
||||
'nativeConfig.sessionProfilesInvalid': '无法安全切换原生配置:保存的会话数据无效或已达到容量上限。',
|
||||
'nativeConfig.expected.hermes': '需要 config.yaml、auth.json 或 .env。',
|
||||
'nativeConfig.expected.openclaw': '需要 openclaw.json 或 agents 目录。',
|
||||
'nativeConfig.expected.codex': '需要 config.toml 或 auth.json。',
|
||||
'nativeConfig.expected.claude': '需要 settings.json 或 .credentials.json。',
|
||||
'nativeConfig.expected.pi': '需要 settings.json、auth.json 或 models.json。',
|
||||
|
||||
Reference in new issue
Block a user