diff --git a/desktop/src/agents/cli/deepseek-native-config.cjs b/desktop/src/agents/cli/deepseek-native-config.cjs index 5ffc720..2a49c3e 100644 --- a/desktop/src/agents/cli/deepseek-native-config.cjs +++ b/desktop/src/agents/cli/deepseek-native-config.cjs @@ -88,7 +88,9 @@ function deepseekDefaultCredentialConfigured(options = {}) { if (!path.isAbsolute(root)) return false const mapping = value => value && typeof value === 'object' && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null) + let remaining = 8192 const jsonValue = (value, seen = new Set(), depth = 0) => { + if (--remaining < 0) return false if (value === null || typeof value === 'string' || typeof value === 'boolean') return true if (typeof value === 'number') return Number.isFinite(value) if (depth > 32 || (!Array.isArray(value) && !mapping(value)) || seen.has(value)) return false diff --git a/desktop/src/agents/cli/native-secret-environment.cjs b/desktop/src/agents/cli/native-secret-environment.cjs index d7a0f1c..352b22b 100644 --- a/desktop/src/agents/cli/native-secret-environment.cjs +++ b/desktop/src/agents/cli/native-secret-environment.cjs @@ -20,9 +20,12 @@ function credentialLiteral(value) { function configSecrets(configs) { const secrets = new Set() const pending = configs.map(value => [value, 0]) + const visited = new WeakMap() while (pending.length && secrets.size < 128) { const [value, depth] = pending.pop() if (!value || typeof value !== 'object' || depth > 32) continue + if (visited.has(value) && visited.get(value) <= depth) continue + visited.set(value, depth) for (const [key, item] of Object.entries(value)) { if ((/(?:api[_-]?key|access[_-]?key|token|secret|password|authorization)$/i.test(key) || (key === 'key' && value.type === 'api_key')) diff --git a/desktop/test/agents/cli/cli-process-support.test.cjs b/desktop/test/agents/cli/cli-process-support.test.cjs index 9b0d937..fb07fe2 100644 --- a/desktop/test/agents/cli/cli-process-support.test.cjs +++ b/desktop/test/agents/cli/cli-process-support.test.cjs @@ -4,6 +4,7 @@ const fs = require('node:fs') const os = require('node:os') const path = require('node:path') const { EventEmitter } = require('node:events') +const { spawnSync } = require('node:child_process') const { childEnvironment, failedAgentProcessError, terminateWindowsProcessTree } = require('../../../src/agents/cli/cli-process-support.cjs') const { systemChildEnvironment } = require('../../../src/agents/cli/cli-discovery.cjs') @@ -11,6 +12,17 @@ const { redactChildSecrets, createRuntimeEventEmitter } = require('../../../src/ const { nativeOpenClawOptions } = require('../../../src/agents/cli/openclaw-runtime.cjs') const { workBuddyInstall } = require('../../support/cli-adapters-test-helpers.cjs') +test('native credential extraction handles cyclic and shared configuration references', () => { + const modulePath = require.resolve('../../../src/agents/cli/native-secret-environment.cjs') + const script = `const { configSecrets } = require(${JSON.stringify(modulePath)}) +const root = { apiKey: 'synthetic-private-value' } +root.first = root; root.second = root +process.stdout.write(JSON.stringify(configSecrets([root])))` + const result = spawnSync(process.execPath, ['-e', script], { encoding: 'utf8', timeout: 3000 }) + assert.equal(result.status, 0) + assert.deepEqual(JSON.parse(result.stdout), ['synthetic-private-value']) +}) + test('Claude project credentials are redacted from final and split streaming output', t => { const workdir = fs.mkdtempSync(path.join(os.tmpdir(), 'meldwork-claude-project-secret-')) t.after(() => fs.rmSync(workdir, { recursive: true, force: true })) diff --git a/desktop/test/agents/local-agent-readiness.test.cjs b/desktop/test/agents/local-agent-readiness.test.cjs index 809e082..25837dd 100644 --- a/desktop/test/agents/local-agent-readiness.test.cjs +++ b/desktop/test/agents/local-agent-readiness.test.cjs @@ -30,6 +30,12 @@ test('DeepSeek recognizes its native default-route credential without exposing i assert.equal(state().state, 'unknown') write('version: 1\nrefs:\n ZGCI_API_KEY: synthetic-key\nrecords:\n invalid: {}\n') assert.equal(state().state, 'unknown') + const aliases = [' n0: &n0 [x, x]'] + for (let index = 1; index < 16; index += 1) { + aliases.push(` n${index}: &n${index} [*n${index - 1}, *n${index - 1}]`) + } + write(`version: 1\nrefs:\n ZGCI_API_KEY: synthetic-key\nrecords:\n other/provider:\n kind: grant\n payload:\n${aliases.join('\n')}\n`) + assert.equal(state().state, 'unknown') write('version: 1\nrefs:\n ZGCI_API_KEY: synthetic-key\n') if (process.platform !== 'win32') { fs.chmodSync(file, 0o644)