Sitelet https://github.com/openclaw/openclaw/commit/a59ac5cf6f948b3277511698069d4dacb6c85b34
Skip to content

Commit a59ac5c

Browse files
committed
feat: add agent identity avatars (#1329) (thanks @dlauer)
1 parent a2bea8e commit a59ac5c

26 files changed

Lines changed: 477 additions & 22 deletions

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Docs: https://docs.clawd.bot
66

77
### Changes
88
- Highlight: Lobster optional plugin tool for typed workflows + approval gates. https://docs.clawd.bot/tools/lobster
9+
- Agents: add identity avatar config support and Control UI avatar rendering. (#1329) Thanks @dlauer.
910
- Memory: prevent CLI hangs by deferring vector probes, adding sqlite-vec/embedding timeouts, and showing sync progress early.
1011
- Docs: add troubleshooting entry for gateway.mode blocking gateway start. https://docs.clawd.bot/gateway/troubleshooting
1112
- Docs: add per-message Gmail search example for gog. (#1220) Thanks @mbelinky.

‎docs/cli/agents.md‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,51 @@ clawdbot agents set-identity --workspace ~/clawd --from-identity
2121
clawdbot agents set-identity --agent main --avatar avatars/clawd.png
2222
clawdbot agents delete work
2323
```
24+
25+
## Identity files
26+
27+
Each agent workspace can include an `IDENTITY.md` at the workspace root:
28+
- Example path: `~/clawd/IDENTITY.md`
29+
- `set-identity --from-identity` reads from the workspace root (or an explicit `--identity-file`)
30+
31+
Avatar paths resolve relative to the workspace root.
32+
33+
## Set identity
34+
35+
`set-identity` writes fields into `agents.list[].identity`:
36+
- `name`
37+
- `theme`
38+
- `emoji`
39+
- `avatar` (workspace-relative path, http(s) URL, or data URI)
40+
41+
Load from `IDENTITY.md`:
42+
43+
```bash
44+
clawdbot agents set-identity --workspace ~/clawd --from-identity
45+
```
46+
47+
Override fields explicitly:
48+
49+
```bash
50+
clawdbot agents set-identity --agent main --name "Clawd" --emoji "🦞" --avatar avatars/clawd.png
51+
```
52+
53+
Config sample:
54+
55+
```json5
56+
{
57+
agents: {
58+
list: [
59+
{
60+
id: "main",
61+
identity: {
62+
name: "Clawd",
63+
theme: "space lobster",
64+
emoji: "🦞",
65+
avatar: "avatars/clawd.png"
66+
}
67+
}
68+
]
69+
}
70+
}
71+
```

‎docs/gateway/configuration.md‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,13 +402,23 @@ If set, Clawdbot derives defaults (only when you haven’t set them explicitly):
402402
- `agents.list[].groupChat.mentionPatterns` from the agent’s `identity.name`/`identity.emoji` (so “@Samantha” works in groups across Telegram/Slack/Discord/iMessage/WhatsApp)
403403
- `identity.avatar` accepts a workspace-relative image path or a remote URL/data URL. Local files must live inside the agent workspace.
404404

405+
`identity.avatar` accepts:
406+
- Workspace-relative path (must stay within the agent workspace)
407+
- `http(s)` URL
408+
- `data:` URI
409+
405410
```json5
406411
{
407412
agents: {
408413
list: [
409414
{
410415
id: "main",
411-
identity: { name: "Samantha", theme: "helpful sloth", emoji: "🦥", avatar: "avatars/sam.png" }
416+
identity: {
417+
name: "Samantha",
418+
theme: "helpful sloth",
419+
emoji: "🦥",
420+
avatar: "avatars/samantha.png"
421+
}
412422
}
413423
]
414424
}

‎docs/reference/templates/IDENTITY.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ read_when:
1111
- **Creature:** *(AI? robot? familiar? ghost in the machine? something weirder?)*
1212
- **Vibe:** *(how do you come across? sharp? warm? chaotic? calm?)*
1313
- **Emoji:** *(your signature — pick one that feels right)*
14-
- **Avatar:** *(workspace-relative path, or a URL/data URL)*
14+
- **Avatar:** *(workspace-relative path, http(s) URL, or data URI)*
1515

1616
---
1717

1818
This isn't just metadata. It's the start of figuring out who you are.
19+
20+
Notes:
21+
- Save this file at the workspace root as `IDENTITY.md`.
22+
- For avatars, use a workspace-relative path like `avatars/clawd.png`.

‎src/cli/program/register.agent.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.clawd.bot/cli/agent
147147
.option("--name <name>", "Identity name")
148148
.option("--theme <theme>", "Identity theme")
149149
.option("--emoji <emoji>", "Identity emoji")
150-
.option("--avatar <path>", "Identity avatar (workspace-relative, URL, or data: URL)")
150+
.option("--avatar <value>", "Identity avatar (workspace path, http(s) URL, or data URI)")
151151
.option("--json", "Output JSON summary", false)
152152
.addHelpText(
153153
"after",
@@ -156,6 +156,7 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.clawd.bot/cli/agent
156156
${theme.heading("Examples:")}
157157
${formatHelpExamples([
158158
['clawdbot agents set-identity --agent main --name "Clawd" --emoji "🦞"', "Set name + emoji."],
159+
["clawdbot agents set-identity --agent main --avatar avatars/clawd.png", "Set avatar path."],
159160
["clawdbot agents set-identity --workspace ~/clawd --from-identity", "Load from IDENTITY.md."],
160161
[
161162
"clawdbot agents set-identity --identity-file ~/clawd/IDENTITY.md --agent main",

‎src/commands/agents.identity.test.ts‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ describe("agents set-identity command", () => {
126126
"- Name: Clawd",
127127
"- Theme: space lobster",
128128
"- Emoji: :)",
129-
"- Avatar: avatars/base.png",
129+
"- Avatar: avatars/clawd.png",
130130
"",
131131
].join("\n"),
132132
"utf-8",
@@ -138,7 +138,13 @@ describe("agents set-identity command", () => {
138138
});
139139

140140
await agentsSetIdentityCommand(
141-
{ workspace, fromIdentity: true, name: "Nova", emoji: "🦞", avatar: "avatars/custom.png" },
141+
{
142+
workspace,
143+
fromIdentity: true,
144+
name: "Nova",
145+
emoji: "🦞",
146+
avatar: "https://example.com/override.png",
147+
},
142148
runtime,
143149
);
144150

@@ -150,7 +156,7 @@ describe("agents set-identity command", () => {
150156
name: "Nova",
151157
theme: "space lobster",
152158
emoji: "🦞",
153-
avatar: "avatars/custom.png",
159+
avatar: "https://example.com/override.png",
154160
});
155161
});
156162

@@ -216,6 +222,26 @@ describe("agents set-identity command", () => {
216222
});
217223
});
218224

225+
it("accepts avatar-only updates via flags", async () => {
226+
configMocks.readConfigFileSnapshot.mockResolvedValue({
227+
...baseSnapshot,
228+
config: { agents: { list: [{ id: "main" }] } },
229+
});
230+
231+
await agentsSetIdentityCommand(
232+
{ agent: "main", avatar: "https://example.com/avatar.png" },
233+
runtime,
234+
);
235+
236+
const written = configMocks.writeConfigFile.mock.calls[0]?.[0] as {
237+
agents?: { list?: Array<{ id: string; identity?: Record<string, string> }> };
238+
};
239+
const main = written.agents?.list?.find((entry) => entry.id === "main");
240+
expect(main?.identity).toEqual({
241+
avatar: "https://example.com/avatar.png",
242+
});
243+
});
244+
219245
it("errors when identity data is missing", async () => {
220246
const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-identity-"));
221247
const workspace = path.join(root, "work");
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import path from "node:path";
2+
3+
import { describe, expect, it } from "vitest";
4+
5+
import { validateConfigObject } from "./config.js";
6+
import { withTempHome } from "./test-helpers.js";
7+
8+
describe("identity avatar validation", () => {
9+
it("accepts workspace-relative avatar paths", async () => {
10+
await withTempHome(async (home) => {
11+
const workspace = path.join(home, "clawd");
12+
const res = validateConfigObject({
13+
agents: {
14+
list: [{ id: "main", workspace, identity: { avatar: "avatars/clawd.png" } }],
15+
},
16+
});
17+
expect(res.ok).toBe(true);
18+
});
19+
});
20+
21+
it("accepts http(s) and data avatars", async () => {
22+
await withTempHome(async (home) => {
23+
const workspace = path.join(home, "clawd");
24+
const httpRes = validateConfigObject({
25+
agents: {
26+
list: [{ id: "main", workspace, identity: { avatar: "https://example.com/avatar.png" } }],
27+
},
28+
});
29+
expect(httpRes.ok).toBe(true);
30+
31+
const dataRes = validateConfigObject({
32+
agents: {
33+
list: [{ id: "main", workspace, identity: { avatar: "data:image/png;base64,AAA" } }],
34+
},
35+
});
36+
expect(dataRes.ok).toBe(true);
37+
});
38+
});
39+
40+
it("rejects avatar paths outside workspace", async () => {
41+
await withTempHome(async (home) => {
42+
const workspace = path.join(home, "clawd");
43+
const res = validateConfigObject({
44+
agents: {
45+
list: [{ id: "main", workspace, identity: { avatar: "../oops.png" } }],
46+
},
47+
});
48+
expect(res.ok).toBe(false);
49+
if (!res.ok) {
50+
expect(res.issues[0]?.path).toBe("agents.list.0.identity.avatar");
51+
}
52+
});
53+
});
54+
});

‎src/config/schema.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ const FIELD_LABELS: Record<string, string> = {
119119
"diagnostics.cacheTrace.includeMessages": "Cache Trace Include Messages",
120120
"diagnostics.cacheTrace.includePrompt": "Cache Trace Include Prompt",
121121
"diagnostics.cacheTrace.includeSystem": "Cache Trace Include System",
122+
"agents.list.*.identity.avatar": "Identity Avatar",
122123
"gateway.remote.url": "Remote Gateway URL",
123124
"gateway.remote.sshTarget": "Remote Gateway SSH Target",
124125
"gateway.remote.sshIdentity": "Remote Gateway SSH Identity",
@@ -511,6 +512,8 @@ const FIELD_HELP: Record<string, string> = {
511512
"Resolved install directory (usually ~/.clawdbot/extensions/<id>).",
512513
"plugins.installs.*.version": "Version recorded at install time (if available).",
513514
"plugins.installs.*.installedAt": "ISO timestamp of last install/update.",
515+
"agents.list.*.identity.avatar":
516+
"Agent avatar (workspace-relative path, http(s) URL, or data URI).",
514517
"agents.defaults.model.primary": "Primary model (provider/model).",
515518
"agents.defaults.model.fallbacks":
516519
"Ordered fallback models (provider/model). Used when the primary model fails.",
@@ -616,7 +619,7 @@ const FIELD_PLACEHOLDERS: Record<string, string> = {
616619
"gateway.remote.tlsFingerprint": "sha256:ab12cd34…",
617620
"gateway.remote.sshTarget": "user@host",
618621
"gateway.controlUi.basePath": "/clawdbot",
619-
"agents.list[].identity.avatar": "avatars/assistant.png",
622+
"agents.list[].identity.avatar": "avatars/clawd.png",
620623
};
621624

622625
const SENSITIVE_PATTERNS = [/token/i, /password/i, /secret/i, /api.?key/i];

‎src/config/types.base.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,6 @@ export type IdentityConfig = {
154154
name?: string;
155155
theme?: string;
156156
emoji?: string;
157-
/** Avatar image path (workspace-relative) or a URL/data URL. Local files must live in the workspace. */
157+
/** Avatar image: workspace-relative path, http(s) URL, or data URI. */
158158
avatar?: string;
159159
};

‎src/config/validation.ts‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import path from "node:path";
2+
13
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
24
import { CHANNEL_IDS } from "../channels/registry.js";
35
import {
@@ -13,6 +15,60 @@ import { findLegacyConfigIssues } from "./legacy.js";
1315
import type { ClawdbotConfig, ConfigValidationIssue } from "./types.js";
1416
import { ClawdbotSchema } from "./zod-schema.js";
1517

18+
const AVATAR_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
19+
const AVATAR_DATA_RE = /^data:/i;
20+
const AVATAR_HTTP_RE = /^https?:\/\//i;
21+
const WINDOWS_ABS_RE = /^[a-zA-Z]:[\\/]/;
22+
23+
function isWorkspaceAvatarPath(value: string, workspaceDir: string): boolean {
24+
const workspaceRoot = path.resolve(workspaceDir);
25+
const resolved = path.resolve(workspaceRoot, value);
26+
const relative = path.relative(workspaceRoot, resolved);
27+
if (relative === "") return true;
28+
if (relative.startsWith("..")) return false;
29+
return !path.isAbsolute(relative);
30+
}
31+
32+
function validateIdentityAvatar(config: ClawdbotConfig): ConfigValidationIssue[] {
33+
const agents = config.agents?.list;
34+
if (!Array.isArray(agents) || agents.length === 0) return [];
35+
const issues: ConfigValidationIssue[] = [];
36+
for (const [index, entry] of agents.entries()) {
37+
if (!entry || typeof entry !== "object") continue;
38+
const avatarRaw = entry.identity?.avatar;
39+
if (typeof avatarRaw !== "string") continue;
40+
const avatar = avatarRaw.trim();
41+
if (!avatar) continue;
42+
if (AVATAR_DATA_RE.test(avatar) || AVATAR_HTTP_RE.test(avatar)) continue;
43+
if (avatar.startsWith("~")) {
44+
issues.push({
45+
path: `agents.list.${index}.identity.avatar`,
46+
message: "identity.avatar must be a workspace-relative path, http(s) URL, or data URI.",
47+
});
48+
continue;
49+
}
50+
const hasScheme = AVATAR_SCHEME_RE.test(avatar);
51+
if (hasScheme && !WINDOWS_ABS_RE.test(avatar)) {
52+
issues.push({
53+
path: `agents.list.${index}.identity.avatar`,
54+
message: "identity.avatar must be a workspace-relative path, http(s) URL, or data URI.",
55+
});
56+
continue;
57+
}
58+
const workspaceDir = resolveAgentWorkspaceDir(
59+
config,
60+
entry.id ?? resolveDefaultAgentId(config),
61+
);
62+
if (!isWorkspaceAvatarPath(avatar, workspaceDir)) {
63+
issues.push({
64+
path: `agents.list.${index}.identity.avatar`,
65+
message: "identity.avatar must stay within the agent workspace.",
66+
});
67+
}
68+
}
69+
return issues;
70+
}
71+
1672
export function validateConfigObject(
1773
raw: unknown,
1874
): { ok: true; config: ClawdbotConfig } | { ok: false; issues: ConfigValidationIssue[] } {
@@ -48,6 +104,10 @@ export function validateConfigObject(
48104
],
49105
};
50106
}
107+
const avatarIssues = validateIdentityAvatar(validated.data as ClawdbotConfig);
108+
if (avatarIssues.length > 0) {
109+
return { ok: false, issues: avatarIssues };
110+
}
51111
return {
52112
ok: true,
53113
config: applyModelDefaults(

0 commit comments

Comments
 (0)