From 0424a98bb07b481cfbe12e1c28825a3747291b77 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:49:29 -0700 Subject: [PATCH 1/3] fix(identity): keep a product agent's curated label that matches its name (TASK-551) The bare displayName===agentName leak rule was written for the shared openclaw runtime. It also matched Scout ('Scout' on agentName 'scout'), so every per-user Scout showed its raw instance id. The rule now applies only to shared runtime names. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: swift Agent-Model: claude/claude-haiku-5-5/default (cherry picked from commit 30719539e6392cd1d69659f91724868d6dfa5310) Co-authored-by: wren Agent-Model: claude/claude-haiku-5-5/max --- ...ntIdentityService.resolveAgentDisplayLabel.test.js | 11 +++++++++++ backend/utils/agentDisplayLabel.ts | 10 +++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/backend/__tests__/unit/services/agentIdentityService.resolveAgentDisplayLabel.test.js b/backend/__tests__/unit/services/agentIdentityService.resolveAgentDisplayLabel.test.js index b4876a77b8..5e10163f72 100644 --- a/backend/__tests__/unit/services/agentIdentityService.resolveAgentDisplayLabel.test.js +++ b/backend/__tests__/unit/services/agentIdentityService.resolveAgentDisplayLabel.test.js @@ -81,6 +81,17 @@ describe('resolveAgentDisplayLabel', () => { expect(resolveAgentDisplayLabel(user)).toBe('aria'); }); + // TASK-551: Scout is a product agent, not a shared runtime. Its curated + // label equals its agentName on a per-user instance, and that label must + // show, not the raw instance id. + it('keeps a product agent\'s curated label that equals its agentName (Scout)', () => { + const user = { + username: 'scout-ubd1004e0d7', + botMetadata: { displayName: 'Scout', agentName: 'scout', instanceId: 'ubd1004e0d7' }, + }; + expect(resolveAgentDisplayLabel(user)).toBe('Scout'); + }); + it('is case-insensitive — "OpenClaw (Pixel)" still rejected', () => { const user = { username: 'openclaw-pixel', diff --git a/backend/utils/agentDisplayLabel.ts b/backend/utils/agentDisplayLabel.ts index d52928a27f..a57cfaabd6 100644 --- a/backend/utils/agentDisplayLabel.ts +++ b/backend/utils/agentDisplayLabel.ts @@ -18,6 +18,12 @@ // runtime name ('openclaw') rather than the agent's identity. Falling // back to it produces "openclaw ↔ openclaw" pod names where what we want // is "Pixel ↔ Strategist (Aria)". +// agentNames that name a shared runtime, not an agent. Only these can leak +// their runtime name as a displayName. A product agent that happens to share +// its name with its curated label ('Scout' on agentName 'scout', TASK-551) +// keeps that label. +const SHARED_RUNTIME_AGENT_NAMES = new Set(['openclaw']); + export function resolveAgentDisplayLabel( user: { username?: string; @@ -44,7 +50,9 @@ export function resolveAgentDisplayLabel( !!display && !!agentName && ( - (!!instanceId && instanceId !== 'default' && display.toLowerCase() === agentName.toLowerCase()) + (!!instanceId && instanceId !== 'default' + && SHARED_RUNTIME_AGENT_NAMES.has(agentName.toLowerCase()) + && display.toLowerCase() === agentName.toLowerCase()) || ( !!instanceId && display.toLowerCase() === `${agentName.toLowerCase()} (${instanceId.toLowerCase()})` From 01e0b4597ac81fd29ffece9cffab6d4c2dbfa314 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Fri, 30 Oct 2026 03:03:54 -0700 Subject: [PATCH 2/3] fix(identity): keep the runtime-name guard for codex and claude-code (TASK-551) The split keeps only openclaw in SHARED_RUNTIME_AGENT_NAMES. A codex seat created without a displayName takes AGENT_TYPES.codex.officialDisplayName ('Codex', agentIdentityService.ts), which now renders as 'Codex' where it rendered the instance id before. codex is a shared runtime, so it stays in the set, and claude-code joins it for the same reason. The constant moves above the resolver's doc comment, so the comment sits on the function. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: wren Agent-Model: claude/claude-haiku-5-5/max --- ...IdentityService.resolveAgentDisplayLabel.test.js | 10 ++++++++++ backend/utils/agentDisplayLabel.ts | 13 +++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/backend/__tests__/unit/services/agentIdentityService.resolveAgentDisplayLabel.test.js b/backend/__tests__/unit/services/agentIdentityService.resolveAgentDisplayLabel.test.js index 5e10163f72..31d50551ec 100644 --- a/backend/__tests__/unit/services/agentIdentityService.resolveAgentDisplayLabel.test.js +++ b/backend/__tests__/unit/services/agentIdentityService.resolveAgentDisplayLabel.test.js @@ -92,6 +92,16 @@ describe('resolveAgentDisplayLabel', () => { expect(resolveAgentDisplayLabel(user)).toBe('Scout'); }); + // The shared-runtime set is wider than openclaw: a codex seat that takes + // its officialDisplayName ('Codex') must still fall back to its instance. + it('still falls back to instanceId when a shared codex runtime name leaks as displayName', () => { + const user = { + username: 'codex-cody', + botMetadata: { displayName: 'Codex', agentName: 'codex', instanceId: 'cody' }, + }; + expect(resolveAgentDisplayLabel(user)).toBe('cody'); + }); + it('is case-insensitive — "OpenClaw (Pixel)" still rejected', () => { const user = { username: 'openclaw-pixel', diff --git a/backend/utils/agentDisplayLabel.ts b/backend/utils/agentDisplayLabel.ts index a57cfaabd6..b30bf80fb8 100644 --- a/backend/utils/agentDisplayLabel.ts +++ b/backend/utils/agentDisplayLabel.ts @@ -1,3 +1,10 @@ +// agentNames that name a shared runtime, not an agent. Only these can leak +// their runtime name as a displayName. A product agent that happens to share +// its name with its curated label ('Scout' on agentName 'scout', TASK-551) +// keeps that label. A codex seat created without a displayName takes the +// officialDisplayName 'Codex' (agentIdentityService.ts), so codex must stay here. +const SHARED_RUNTIME_AGENT_NAMES = new Set(['openclaw', 'codex', 'claude-code']); + // Resolve the human-readable label for an agent User row. The fallback // chain is intentional and load-bearing for any UI / pod-naming surface // that shows agent identity (agent-dm pod names, sidebar member rows): @@ -18,12 +25,6 @@ // runtime name ('openclaw') rather than the agent's identity. Falling // back to it produces "openclaw ↔ openclaw" pod names where what we want // is "Pixel ↔ Strategist (Aria)". -// agentNames that name a shared runtime, not an agent. Only these can leak -// their runtime name as a displayName. A product agent that happens to share -// its name with its curated label ('Scout' on agentName 'scout', TASK-551) -// keeps that label. -const SHARED_RUNTIME_AGENT_NAMES = new Set(['openclaw']); - export function resolveAgentDisplayLabel( user: { username?: string; From b385245889446ca06a2db714f2c62be5a4c7602f Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Fri, 30 Oct 2026 03:35:31 -0700 Subject: [PATCH 3/3] test(identity): guard claude-code and its officialDisplayName (TASK-551) Commit 2 added claude-code to the shared-runtime set with no test that fails if the entry is dropped. This adds that test, plus one for the officialDisplayName 'Claude Code', which the exact name compare missed. - sameName ignores case, spaces and hyphens, so 'Claude Code' is the same leak as the agentName 'claude-code'. - The set comment names commonly-bot and commonly-summarizer as product agents: their displayName shows even when it equals their agentName. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: wren Agent-Model: claude/claude-haiku-5-5/max --- ...ityService.resolveAgentDisplayLabel.test.js | 18 ++++++++++++++++++ backend/utils/agentDisplayLabel.ts | 17 +++++++++++++---- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/backend/__tests__/unit/services/agentIdentityService.resolveAgentDisplayLabel.test.js b/backend/__tests__/unit/services/agentIdentityService.resolveAgentDisplayLabel.test.js index 31d50551ec..30d86636b9 100644 --- a/backend/__tests__/unit/services/agentIdentityService.resolveAgentDisplayLabel.test.js +++ b/backend/__tests__/unit/services/agentIdentityService.resolveAgentDisplayLabel.test.js @@ -102,6 +102,24 @@ describe('resolveAgentDisplayLabel', () => { expect(resolveAgentDisplayLabel(user)).toBe('cody'); }); + // claude-code is a shared runtime too. Its runtime name and its + // officialDisplayName 'Claude Code' both fall back to the instanceId. + it('falls back to instanceId when a shared claude-code runtime name leaks as displayName', () => { + const user = { + username: 'claude-code-ops', + botMetadata: { displayName: 'claude-code', agentName: 'claude-code', instanceId: 'ops' }, + }; + expect(resolveAgentDisplayLabel(user)).toBe('ops'); + }); + + it('falls back to instanceId when the claude-code officialDisplayName leaks as displayName', () => { + const user = { + username: 'claude-code-ops', + botMetadata: { displayName: 'Claude Code', agentName: 'claude-code', instanceId: 'ops' }, + }; + expect(resolveAgentDisplayLabel(user)).toBe('ops'); + }); + it('is case-insensitive — "OpenClaw (Pixel)" still rejected', () => { const user = { username: 'openclaw-pixel', diff --git a/backend/utils/agentDisplayLabel.ts b/backend/utils/agentDisplayLabel.ts index b30bf80fb8..7fc1a9be9f 100644 --- a/backend/utils/agentDisplayLabel.ts +++ b/backend/utils/agentDisplayLabel.ts @@ -1,10 +1,19 @@ // agentNames that name a shared runtime, not an agent. Only these can leak // their runtime name as a displayName. A product agent that happens to share -// its name with its curated label ('Scout' on agentName 'scout', TASK-551) -// keeps that label. A codex seat created without a displayName takes the -// officialDisplayName 'Codex' (agentIdentityService.ts), so codex must stay here. +// its name with its curated label keeps that label: 'Scout' on agentName 'scout' +// (TASK-551). commonly-bot and commonly-summarizer are product agents too, so +// their displayName shows even when it equals their agentName. A codex seat +// created without a displayName takes the officialDisplayName 'Codex' +// (agentIdentityService.ts), and a claude-code seat takes 'Claude Code', so both +// must stay here. const SHARED_RUNTIME_AGENT_NAMES = new Set(['openclaw', 'codex', 'claude-code']); +// Equal names ignoring case, spaces and hyphens, so the officialDisplayName +// 'Claude Code' is the same leak as the agentName 'claude-code'. +const sameName = (a: string, b: string): boolean => ( + a.toLowerCase().replace(/[\s-]/g, '') === b.toLowerCase().replace(/[\s-]/g, '') +); + // Resolve the human-readable label for an agent User row. The fallback // chain is intentional and load-bearing for any UI / pod-naming surface // that shows agent identity (agent-dm pod names, sidebar member rows): @@ -53,7 +62,7 @@ export function resolveAgentDisplayLabel( && ( (!!instanceId && instanceId !== 'default' && SHARED_RUNTIME_AGENT_NAMES.has(agentName.toLowerCase()) - && display.toLowerCase() === agentName.toLowerCase()) + && sameName(display, agentName)) || ( !!instanceId && display.toLowerCase() === `${agentName.toLowerCase()} (${instanceId.toLowerCase()})`