Skip to content

Commit de4ac6a

Browse files
committed
fix(openclaw): redact secrets and harden session usage
1 parent b9e6da2 commit de4ac6a

6 files changed

Lines changed: 68 additions & 18 deletions

File tree

cli.js

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2820,13 +2820,16 @@ function normalizeOpenclawWorkspaceFileName(input) {
28202820

28212821
function readOpenclawConfigFile() {
28222822
const filePath = OPENCLAW_CONFIG_FILE;
2823+
const authProfilesByProvider = sanitizeOpenclawAuthProfilesForClient(
2824+
readOpenclawAuthProfilesSummary().providers
2825+
);
28232826
if (!fs.existsSync(filePath)) {
28242827
return {
28252828
exists: false,
28262829
path: filePath,
28272830
content: '',
28282831
lineEnding: os.EOL === '\r\n' ? '\r\n' : '\n',
2829-
authProfilesByProvider: readOpenclawAuthProfilesSummary().providers
2832+
authProfilesByProvider
28302833
};
28312834
}
28322835

@@ -2837,7 +2840,7 @@ function readOpenclawConfigFile() {
28372840
path: filePath,
28382841
content: stripUtf8Bom(raw),
28392842
lineEnding: detectLineEnding(raw),
2840-
authProfilesByProvider: readOpenclawAuthProfilesSummary().providers
2843+
authProfilesByProvider
28412844
};
28422845
} catch (e) {
28432846
return { error: `读取 OpenClaw 配置失败: ${e.message}` };
@@ -3049,6 +3052,22 @@ function readOpenclawAuthProfilesSummary() {
30493052
};
30503053
}
30513054

3055+
function sanitizeOpenclawAuthProfilesForClient(providers) {
3056+
if (!isPlainObject(providers)) {
3057+
return {};
3058+
}
3059+
const sanitized = {};
3060+
for (const [providerKey, summary] of Object.entries(providers)) {
3061+
if (!isPlainObject(summary)) {
3062+
continue;
3063+
}
3064+
const normalized = { ...summary };
3065+
delete normalized.resolvedValue;
3066+
sanitized[providerKey] = normalized;
3067+
}
3068+
return sanitized;
3069+
}
3070+
30523071
function normalizeOpenclawAuthProfileUpdate(entry) {
30533072
if (!isPlainObject(entry)) return null;
30543073
const profileId = typeof entry.profileId === 'string' ? entry.profileId.trim() : '';
@@ -5756,19 +5775,11 @@ async function listSessionUsage(params = {}) {
57565775
const limit = Number.isFinite(rawLimit)
57575776
? Math.max(1, Math.min(rawLimit, MAX_SESSION_LIST_SIZE))
57585777
: 200;
5759-
const sessions = await listAllSessions({
5778+
return await listAllSessionsData({
57605779
source,
57615780
limit,
57625781
forceRefresh: !!params.forceRefresh
57635782
});
5764-
return sessions.map((item) => {
5765-
if (!item || typeof item !== 'object' || Array.isArray(item)) {
5766-
return item;
5767-
}
5768-
const normalized = { ...item };
5769-
delete normalized.__messageCountExact;
5770-
return normalized;
5771-
});
57725783
}
57735784

57745785
function listSessionPaths(params = {}) {
@@ -11036,12 +11047,16 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
1103611047
break;
1103711048
case 'list-sessions-usage':
1103811049
{
11039-
const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
11050+
const usageParams = isPlainObject(params) ? params : {};
11051+
const source = typeof usageParams.source === 'string' ? usageParams.source.trim().toLowerCase() : '';
1104011052
if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
1104111053
result = { error: 'Invalid source. Must be codex, claude, or all' };
1104211054
} else {
1104311055
result = {
11044-
sessions: await listSessionUsage(params || {}),
11056+
sessions: await listSessionUsage({
11057+
...usageParams,
11058+
source: source || 'all'
11059+
}),
1104511060
source: source || 'all'
1104611061
};
1104711062
}

tests/e2e/test-openclaw.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,33 @@
1+
const fs = require('fs');
2+
const path = require('path');
13
const { assert } = require('./helpers');
24

35
module.exports = async function testOpenclaw(ctx) {
4-
const { api } = ctx;
6+
const { api, tmpHome } = ctx;
7+
const authAgentDir = path.join(tmpHome, '.openclaw', 'agents', 'main', 'agent');
8+
fs.mkdirSync(authAgentDir, { recursive: true });
9+
fs.writeFileSync(path.join(authAgentDir, 'auth-profiles.json'), JSON.stringify({
10+
version: 1,
11+
profiles: {
12+
'openai-codex:default': {
13+
provider: 'openai-codex',
14+
type: 'oauth',
15+
access: 'super-secret-token',
16+
displayName: 'OpenAI Codex'
17+
}
18+
}
19+
}, null, 2), 'utf-8');
20+
fs.writeFileSync(path.join(authAgentDir, 'auth-state.json'), JSON.stringify({}, null, 2), 'utf-8');
521

622
// ========== Get OpenClaw Config Tests ==========
723
const openclawReadEmpty = await api('get-openclaw-config');
824
assert(openclawReadEmpty.exists === false, 'openclaw config should not exist initially');
925
assert(typeof openclawReadEmpty.path === 'string', 'get-openclaw-config missing path');
1026
assert('lineEnding' in openclawReadEmpty, 'get-openclaw-config missing lineEnding');
27+
assert(openclawReadEmpty.authProfilesByProvider && openclawReadEmpty.authProfilesByProvider['openai-codex'], 'get-openclaw-config missing auth profile summary');
28+
assert(openclawReadEmpty.authProfilesByProvider['openai-codex'].resolvedValue === undefined, 'get-openclaw-config should not expose resolved auth profile secrets');
29+
assert(openclawReadEmpty.authProfilesByProvider['openai-codex'].resolvedField === 'access', 'get-openclaw-config missing auth profile write field');
30+
assert(openclawReadEmpty.authProfilesByProvider['openai-codex'].editable === true, 'get-openclaw-config missing editable auth profile flag');
1131

1232
// ========== Apply OpenClaw Config Tests ==========
1333
const openclawInvalid = await api('apply-openclaw-config', { content: '', lineEnding: '\n' });

tests/e2e/test-sessions.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ module.exports = async function testSessions(ctx) {
4040
assert(usageSessions.sessions.some((item) => item.sessionId === sessionId), 'list-sessions-usage missing codex entry');
4141
assert(usageSessions.sessions.some((item) => item.sessionId === claudeSessionId), 'list-sessions-usage missing claude entry');
4242
assert(usageSessions.sessions.every((item) => !Object.prototype.hasOwnProperty.call(item, '__messageCountExact')), 'list-sessions-usage should not expose exact hydration markers');
43+
const defaultUsageSessions = await api('list-sessions-usage');
44+
assert(Array.isArray(defaultUsageSessions.sessions), 'list-sessions-usage without params should still return sessions');
45+
assert(defaultUsageSessions.source === 'all', 'list-sessions-usage without params should default source to all');
4346

4447
const usageSessionsInvalid = await api('list-sessions-usage', { source: 'invalid', limit: 50 });
4548
assert(usageSessionsInvalid.error, 'list-sessions-usage should fail for invalid source');

tests/unit/openclaw-core.test.mjs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,6 @@ test('fillOpenclawQuickFromConfig uses builtin openai-codex defaults and editabl
298298
profileId: 'openai-codex:default',
299299
type: 'oauth',
300300
display: 'AuthProfile(oauth:openai-codex:default)',
301-
resolvedValue: 'access-token-value',
302301
resolvedField: 'access',
303302
editable: true,
304303
valueKind: 'oauth-access'
@@ -328,13 +327,13 @@ test('fillOpenclawQuickFromConfig uses builtin openai-codex defaults and editabl
328327
assert.strictEqual(context.openclawQuick.baseUrl, 'https://chatgpt.com/backend-api');
329328
assert.strictEqual(context.openclawQuick.baseUrlDisplayKind, 'builtin-default');
330329
assert.strictEqual(context.openclawQuick.apiType, 'openai-codex-responses');
331-
assert.strictEqual(context.openclawQuick.apiKey, 'access-token-value');
330+
assert.strictEqual(context.openclawQuick.apiKey, '');
332331
assert.strictEqual(context.openclawQuick.apiKeyReadOnly, false);
333332
assert.strictEqual(context.openclawQuick.apiKeyDisplayKind, 'auth-profile-value');
334333
assert.strictEqual(context.openclawQuick.apiKeySourceKind, 'auth-profile');
335334
assert.strictEqual(context.openclawQuick.apiKeySourceProfileId, 'openai-codex:default');
336335
assert.strictEqual(context.openclawQuick.apiKeySourceWriteField, 'access');
337-
assert.strictEqual(context.openclawQuick.apiKeySourceOriginalValue, 'access-token-value');
336+
assert.strictEqual(context.openclawQuick.apiKeySourceOriginalValue, '');
338337
assert.strictEqual(context.openclawQuick.apiKeySourceCredentialType, 'oauth');
339338
});
340339

tests/unit/openclaw-editing.test.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,6 @@ test('applyOpenclawQuickToText queues auth profile updates instead of inlining e
164164
profileId: 'openai-codex:default',
165165
type: 'oauth',
166166
display: 'AuthProfile(oauth:openai-codex:default)',
167-
resolvedValue: 'old-access-token',
168167
resolvedField: 'access',
169168
editable: true,
170169
valueKind: 'oauth-access'
@@ -193,6 +192,7 @@ test('applyOpenclawQuickToText queues auth profile updates instead of inlining e
193192
context.fillOpenclawQuickFromConfig(config, {
194193
authProfilesByProvider: context.openclawAuthProfilesByProvider
195194
});
195+
assert.strictEqual(context.openclawQuick.apiKey, '');
196196
context.openclawQuick.apiKey = 'new-access-token';
197197
context.openclawQuick.overrideProvider = true;
198198

web-ui/modules/app.methods.openclaw-core.mjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,19 @@ function readOpenclawAuthProfileDisplayValue(authProfilesByProvider, providerNam
160160
sourceCredentialType: typeof summary.type === 'string' ? summary.type.trim() : ''
161161
};
162162
}
163+
const resolvedField = typeof summary.resolvedField === 'string' ? summary.resolvedField.trim() : '';
164+
if (summary.editable === true && resolvedField) {
165+
return {
166+
value: '',
167+
readOnly: false,
168+
kind: 'auth-profile-value',
169+
sourceKind: 'auth-profile',
170+
sourceProfileId: typeof summary.profileId === 'string' ? summary.profileId.trim() : '',
171+
sourceWriteField: resolvedField,
172+
sourceOriginalValue: '',
173+
sourceCredentialType: typeof summary.type === 'string' ? summary.type.trim() : ''
174+
};
175+
}
163176
if (typeof summary.display !== 'string' || !summary.display.trim()) {
164177
return {
165178
value: '',

0 commit comments

Comments
 (0)