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:
Ryder Sun committed 2026-11-07 01:33:24 +08:00
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)
+29 -1
View File
@@ -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
View File
@@ -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 || '' },