Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,45 @@ 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');
});

// 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');
});

// 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',
Expand Down
20 changes: 19 additions & 1 deletion backend/utils/agentDisplayLabel.ts
Original file line number Diff line number Diff line change
@@ -1,3 +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 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):
Expand Down Expand Up @@ -44,7 +60,9 @@ export function resolveAgentDisplayLabel(
!!display
&& !!agentName
&& (
(!!instanceId && instanceId !== 'default' && display.toLowerCase() === agentName.toLowerCase())
(!!instanceId && instanceId !== 'default'
&& SHARED_RUNTIME_AGENT_NAMES.has(agentName.toLowerCase())
&& sameName(display, agentName))
|| (
!!instanceId
&& display.toLowerCase() === `${agentName.toLowerCase()} (${instanceId.toLowerCase()})`
Expand Down
Loading