mirror of
https://github.com/Ryder-Sun/Meldwork.git
synced 2026-11-07 06:56:03 -10:00
Honor configured Zcode provider credentials
Capture only credentials declared by native model providers to preserve CLI behavior without exposing unrelated shell secrets. Verified agent and security tests, native CLI direct/group execution, and desktop packaging.
This commit is contained in:
1 parent
b271e7976b
commit
a21f6bb598
7 files changed
+97
-11
No files matched your search
@@ -190,7 +190,7 @@ function childEnvironment(agent, workdir, options, platform) {
|
||||
openCodeSafetyEnv.MIMOCODE_PERMISSION = JSON.stringify(headlessPermissions)
|
||||
}
|
||||
const source = { ...process.env, ...options.env }
|
||||
const nativeKeys = agent.kind === 'hermes' ? nativeSecretEnvKeys(options.nativeCredentialKeys) : []
|
||||
const nativeKeys = ['hermes', 'zcode'].includes(agent.kind) ? nativeSecretEnvKeys(options.nativeCredentialKeys) : []
|
||||
const controlled = Object.fromEntries(Object.entries(options.env || {}).filter(([key, value]) => (
|
||||
typeof value === 'string'
|
||||
&& (CONTROLLED_RUNTIME_ENV.test(key) || nativeKeys.includes(key) || NETWORK_ENV_KEYS.includes(key)
|
||||
|
||||
@@ -241,11 +241,38 @@ function hermesDeclaredCredentialKeys(options = {}) {
|
||||
} catch { return [] }
|
||||
}
|
||||
|
||||
function zcodeDeclaredCredentialKeys(options = {}) {
|
||||
const home = options.home || os.homedir()
|
||||
try {
|
||||
const filename = path.join(home, '.zcode', 'cli', 'config.json')
|
||||
const stat = fs.statSync(filename)
|
||||
if (!stat.isFile() || stat.size > 1024 * 1024) return []
|
||||
const config = JSON.parse(fs.readFileSync(filename, 'utf8'))
|
||||
const model = config?.model && typeof config.model === 'object' && !Array.isArray(config.model) ? config.model : {}
|
||||
const models = [model.main, model.lite, ...(Array.isArray(model.available) ? model.available.slice(0, 32) : [])]
|
||||
const providers = config?.provider && typeof config.provider === 'object' && !Array.isArray(config.provider) ? config.provider : {}
|
||||
const names = []
|
||||
for (const item of models) {
|
||||
const provider = typeof item === 'string' ? item.split('/')[0]
|
||||
: item && typeof item === 'object' ? item.provider : ''
|
||||
if (typeof provider !== 'string' || !provider.trim()) continue
|
||||
const id = provider.trim()
|
||||
const entry = providers[id] && typeof providers[id] === 'object' ? providers[id] : {}
|
||||
names.push(id, id.replace(/^default[-_]/, ''), entry.name)
|
||||
if (entry.kind === 'openai' || id === 'openai') names.push('openai')
|
||||
}
|
||||
return nativeSecretEnvKeys(names.filter(value => typeof value === 'string').map(value => {
|
||||
const name = value.trim().replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_+|_+$/g, '').toUpperCase()
|
||||
return name ? `${name}_API_KEY` : ''
|
||||
}))
|
||||
} catch { return [] }
|
||||
}
|
||||
|
||||
function nativeCredentialEnvironment(kind, env = process.env, declaredKeys = []) {
|
||||
const result = {}
|
||||
const keys = [...(RUNTIME_ENV_KEYS[kind] || []), ...(kind === 'claude'
|
||||
? Object.keys(env).filter(key => VERTEX_MODEL_REGION_KEY.test(key)) : []),
|
||||
...(kind === 'hermes' ? nativeSecretEnvKeys(declaredKeys) : [])]
|
||||
...(['hermes', 'zcode'].includes(kind) ? nativeSecretEnvKeys(declaredKeys) : [])]
|
||||
for (const key of keys) {
|
||||
if (typeof env[key] === 'string') result[key] = env[key]
|
||||
}
|
||||
@@ -885,6 +912,7 @@ async function resolveNativeCredentialState(kind, options = {}) {
|
||||
|
||||
module.exports = {
|
||||
hermesDeclaredCredentialKeys,
|
||||
zcodeDeclaredCredentialKeys,
|
||||
nativeCredentialEnvironment,
|
||||
nativeCredentialState,
|
||||
resolveNativeOpenClawRuntime,
|
||||
|
||||
+16
-9
@@ -48,6 +48,7 @@ const { CustomAgentStore } = require('./agents/custom-agent-store.cjs')
|
||||
const { configDirectoryEnvironment } = require('./agents/cli/cli-config-directories.cjs')
|
||||
const {
|
||||
hermesDeclaredCredentialKeys,
|
||||
zcodeDeclaredCredentialKeys,
|
||||
nativeCredentialEnvironment,
|
||||
resolveNativeOpenClawRuntime,
|
||||
resolveNativeCredentialState,
|
||||
@@ -249,9 +250,11 @@ async function nativeShellEnvironment(options = {}) {
|
||||
let resolved = await resolveNativeShellEnvironment({ home, refresh: options.refresh === true })
|
||||
const hermesEnv = nativeConfigEnvironment('hermes', workspace?.state.agentPreferences, { ...process.env, ...resolved.env })
|
||||
const hermesCredentialKeys = hermesDeclaredCredentialKeys({ home, env: hermesEnv })
|
||||
if (hermesCredentialKeys.length) {
|
||||
const extended = await resolveNativeShellEnvironment({ home, refresh: options.refresh === true, extraEnvKeys: hermesCredentialKeys })
|
||||
const extra = Object.fromEntries(hermesCredentialKeys.filter(key => Object.hasOwn(extended.env, key))
|
||||
const zcodeCredentialKeys = zcodeDeclaredCredentialKeys({ home })
|
||||
const declaredKeys = [...new Set([...hermesCredentialKeys, ...zcodeCredentialKeys])]
|
||||
if (declaredKeys.length) {
|
||||
const extended = await resolveNativeShellEnvironment({ home, refresh: options.refresh === true, extraEnvKeys: declaredKeys })
|
||||
const extra = Object.fromEntries(declaredKeys.filter(key => Object.hasOwn(extended.env, key))
|
||||
.map(key => [key, extended.env[key]]))
|
||||
resolved = { ...resolved, env: { ...resolved.env, ...extra } }
|
||||
}
|
||||
@@ -269,6 +272,7 @@ async function nativeShellEnvironment(options = {}) {
|
||||
env,
|
||||
home,
|
||||
hermesCredentialKeys,
|
||||
zcodeCredentialKeys,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,9 +281,11 @@ async function detectLocalAgents() {
|
||||
return detectAgents({
|
||||
env: shellEnvironment.env,
|
||||
home: shellEnvironment.home,
|
||||
nativeCredentialKeysForKind: kind => kind === 'hermes' ? shellEnvironment.hermesCredentialKeys : [],
|
||||
nativeCredentialKeysForKind: kind => kind === 'hermes' ? shellEnvironment.hermesCredentialKeys
|
||||
: kind === 'zcode' ? shellEnvironment.zcodeCredentialKeys : [],
|
||||
nativeEnvironmentForKind: kind => kind === 'openclaw'
|
||||
? {} : nativeCredentialEnvironment(kind, nativeConfigEnvironment(kind, workspace?.state.agentPreferences, shellEnvironment.env), shellEnvironment.hermesCredentialKeys),
|
||||
? {} : nativeCredentialEnvironment(kind, nativeConfigEnvironment(kind, workspace?.state.agentPreferences, shellEnvironment.env),
|
||||
kind === 'hermes' ? shellEnvironment.hermesCredentialKeys : kind === 'zcode' ? shellEnvironment.zcodeCredentialKeys : []),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -399,7 +405,8 @@ async function runCoreAgent(agent, prompt, workdir, options = {}) {
|
||||
? {}
|
||||
: {
|
||||
...configDirectoryEnvironment(selectedNativeEnv),
|
||||
...nativeCredentialEnvironment(agent.kind, selectedNativeEnv, shellEnvironment.hermesCredentialKeys),
|
||||
...nativeCredentialEnvironment(agent.kind, selectedNativeEnv,
|
||||
agent.kind === 'hermes' ? shellEnvironment.hermesCredentialKeys : agent.kind === 'zcode' ? shellEnvironment.zcodeCredentialKeys : []),
|
||||
...(agent.kind === 'workbuddy' ? workBuddyEnvironment({
|
||||
env: selectedNativeEnv, home: shellEnvironment.home,
|
||||
modelId: workspace?.state.agentPreferences.workbuddy?.modelId,
|
||||
@@ -411,8 +418,8 @@ async function runCoreAgent(agent, prompt, workdir, options = {}) {
|
||||
: {}
|
||||
return runAgent(agent, prompt, workdir, {
|
||||
...runOptions,
|
||||
...(agent.kind === 'hermes' && !connectorCredentialIsolation
|
||||
? { nativeCredentialKeys: shellEnvironment.hermesCredentialKeys } : {}),
|
||||
...(!connectorCredentialIsolation && ['hermes', 'zcode'].includes(agent.kind)
|
||||
? { nativeCredentialKeys: agent.kind === 'hermes' ? shellEnvironment.hermesCredentialKeys : shellEnvironment.zcodeCredentialKeys } : {}),
|
||||
...injected,
|
||||
env: {
|
||||
...networkEnvironment(shellEnvironment?.env),
|
||||
@@ -592,7 +599,7 @@ function createWorkspace() {
|
||||
env: nativeConfigEnvironment(kind, workspace?.state.agentPreferences, shellEnvironment.env),
|
||||
home: shellEnvironment.home,
|
||||
credentialSource: shellEnvironment.source,
|
||||
nativeCredentialKeys: kind === 'hermes' ? shellEnvironment.hermesCredentialKeys : [],
|
||||
nativeCredentialKeys: kind === 'hermes' ? shellEnvironment.hermesCredentialKeys : kind === 'zcode' ? shellEnvironment.zcodeCredentialKeys : [],
|
||||
...(kind === 'workbuddy' ? { modelId: workspace?.state.agentPreferences.workbuddy?.modelId } : {}),
|
||||
})
|
||||
},
|
||||
|
||||
@@ -43,6 +43,14 @@ test('Zcode native provider environment reaches its child without leaking to oth
|
||||
assert.equal(nativeCredentialEnvironment('codex', env).ZCODE_API_KEY, undefined)
|
||||
})
|
||||
|
||||
test('Zcode declared provider credentials are isolated and redacted', () => {
|
||||
const options = { env: { ACME_API_KEY: 'synthetic-secret' }, nativeCredentialKeys: ['ACME_API_KEY'] }
|
||||
const zcode = childEnvironment({ kind: 'zcode', executable: '/native/zcode' }, '/workspace', options, 'darwin')
|
||||
assert.equal(zcode.ACME_API_KEY, 'synthetic-secret')
|
||||
assert.equal(redactChildSecrets('value=synthetic-secret', zcode), 'value=[redacted]')
|
||||
assert.equal(childEnvironment({ kind: 'codex', executable: '/native/codex' }, '/workspace', options, 'darwin').ACME_API_KEY, undefined)
|
||||
})
|
||||
|
||||
test('Hermes native endpoint variables reach its child without leaking to other Agents', () => {
|
||||
const env = { CUSTOM_BASE_URL: 'https://custom.example', OPENROUTER_BASE_URL: 'https://router.example',
|
||||
OLLAMA_HOST: 'http://localhost:11434', OLLAMA_API_KEY: 'test-secret', NODE_OPTIONS: '--require /untrusted.js' }
|
||||
|
||||
@@ -6,6 +6,7 @@ const test = require('node:test')
|
||||
|
||||
const {
|
||||
hermesDeclaredCredentialKeys,
|
||||
zcodeDeclaredCredentialKeys,
|
||||
nativeCredentialEnvironment,
|
||||
nativeCredentialState,
|
||||
resolveNativeOpenClawRuntime,
|
||||
@@ -13,6 +14,25 @@ const {
|
||||
resolveNativeShellEnvironment,
|
||||
} = require('../../src/agents/local-agent-readiness.cjs')
|
||||
|
||||
test('Zcode captures credentials only for configured native models', async t => {
|
||||
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'meldwork-zcode-declared-'))
|
||||
t.after(() => fs.rmSync(home, { recursive: true, force: true }))
|
||||
const dir = path.join(home, '.zcode', 'cli'); fs.mkdirSync(dir, { recursive: true })
|
||||
fs.writeFileSync(path.join(dir, 'config.json'), JSON.stringify({
|
||||
provider: { acme: { name: 'Acme Service', kind: 'openai' }, 'default-beta': {}, unused: {} },
|
||||
model: { main: 'acme/model', lite: 'default-beta/model' },
|
||||
}))
|
||||
const keys = zcodeDeclaredCredentialKeys({ home })
|
||||
assert.deepEqual(keys, ['ACME_API_KEY', 'ACME_SERVICE_API_KEY', 'BETA_API_KEY', 'DEFAULT_BETA_API_KEY', 'OPENAI_API_KEY'])
|
||||
const result = await resolveNativeShellEnvironment({ cache: false, platform: 'win32', extraEnvKeys: keys,
|
||||
env: { ACME_API_KEY: 'test-private', UNUSED_API_KEY: 'not-shared' } })
|
||||
assert.deepEqual(nativeCredentialEnvironment('zcode', result.env, keys), { ACME_API_KEY: 'test-private' })
|
||||
assert.deepEqual(nativeCredentialEnvironment('codex', result.env, keys), {})
|
||||
assert.equal(result.env.UNUSED_API_KEY, undefined)
|
||||
fs.writeFileSync(path.join(dir, 'config.json'), '{')
|
||||
assert.deepEqual(zcodeDeclaredCredentialKeys({ home }), [])
|
||||
})
|
||||
|
||||
test('Hermes captures only safe credential names declared by its selected YAML config', async t => {
|
||||
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'meldwork-hermes-declared-'))
|
||||
t.after(() => fs.rmSync(home, { recursive: true, force: true }))
|
||||
|
||||
@@ -227,6 +227,27 @@ test('Hermes selected config captures only its declared native credential for ex
|
||||
assert.equal(harness.runAgentCalls.at(-1)[3].env.CUSTOM_CREDENTIAL, undefined)
|
||||
})
|
||||
|
||||
test('Zcode receives only its native provider credential', async t => {
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'meldwork-zcode-env-main-'))
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }))
|
||||
const readiness = require('../../src/agents/local-agent-readiness.cjs')
|
||||
const { harness } = loadMain(directory, {
|
||||
zcodeDeclaredCredentialKeys: () => ['ACME_API_KEY'],
|
||||
nativeEnvironment: readiness.nativeCredentialEnvironment,
|
||||
nativeShellEnvironment: input => ({ source: 'native-shell', env: input.extraEnvKeys?.includes('ACME_API_KEY')
|
||||
? { ACME_API_KEY: 'synthetic-secret', UNRELATED_SECRET: 'private' } : {} }),
|
||||
})
|
||||
await harness.ready()
|
||||
const workspace = harness.workspaceInstances[0]
|
||||
await workspace.input.runAgent({ kind: 'zcode', executable: '/tmp/zcode' }, 'hello', directory, {})
|
||||
const options = harness.runAgentCalls.at(-1)[3]
|
||||
assert.equal(options.env.ACME_API_KEY, 'synthetic-secret')
|
||||
assert.equal(options.env.UNRELATED_SECRET, undefined)
|
||||
assert.deepEqual(options.nativeCredentialKeys, ['ACME_API_KEY'])
|
||||
await workspace.input.runAgent({ kind: 'codex', executable: '/tmp/codex' }, 'hello', directory, {})
|
||||
assert.equal(harness.runAgentCalls.at(-1)[3].env.ACME_API_KEY, undefined)
|
||||
})
|
||||
|
||||
test('Skill discovery follows native shell directories and explicit selections take precedence', async t => {
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'meldwork-native-skill-env-'))
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }))
|
||||
@@ -2225,6 +2246,7 @@ test('OpenClaw native auth is routed through the app-owned isolated runtime', as
|
||||
moduleMocks: {
|
||||
'./agents/local-agent-readiness.cjs': {
|
||||
hermesDeclaredCredentialKeys: () => [],
|
||||
zcodeDeclaredCredentialKeys: () => [],
|
||||
nativeCredentialEnvironment: kind => kind === 'openclaw'
|
||||
? {
|
||||
OPENCLAW_NATIVE_CONFIG: 'available',
|
||||
|
||||
@@ -756,6 +756,7 @@ function loadMain(userData, options = {}) {
|
||||
'./attachments/attachment-store.cjs': { AttachmentStore: TestAttachmentStore },
|
||||
'./agents/local-agent-readiness.cjs': {
|
||||
hermesDeclaredCredentialKeys: input => options.hermesDeclaredCredentialKeys?.(input) || [],
|
||||
zcodeDeclaredCredentialKeys: input => options.zcodeDeclaredCredentialKeys?.(input) || [],
|
||||
nativeCredentialEnvironment: (...args) => options.nativeEnvironment?.(...args) || {},
|
||||
resolveNativeShellEnvironment: async input => options.nativeShellEnvironment?.(input) || ({
|
||||
env: { PATH: process.env.PATH || '' },
|
||||
|
||||
Reference in new issue
Block a user