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
31 changes: 14 additions & 17 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ Licensed under the Apache License, Version 2.0. See LICENSE for the full text.
Third-party software redistributed in OneAgent binaries
================================================================================

The target-specific dependency inventory is generated from the production Go
build graph and the frozen frontend production dependency graph. The complete
inventory is distributed as THIRD_PARTY_NOTICES.md, with the corresponding
license and notice texts under licenses/ in every binary artifact.
The target-specific dependency and reviewed static-asset inventory is generated
from the production Go build graph, the frozen frontend production dependency
graph, and frontend/src/components/icons/asset-rights.json. The complete inventory
is distributed as THIRD_PARTY_NOTICES.md, with the corresponding license and
notice texts under licenses/ in every binary artifact.

The tracked source of that generated material is third_party/. Run
`scripts/generate_third_party_licenses.py --check` to verify that it still
Expand All @@ -39,17 +40,13 @@ explicit request.
Third-party trademarks
================================================================================

The application displays each supported Agent's own published mark so a user
can tell which tool a row refers to. This is nominative use: the marks
identify the referenced products and do not imply endorsement, affiliation or
sponsorship. They are not OneAgent product artwork, are not recoloured or
restyled, and remain the property of their respective owners. Provenance for
each file is recorded inline in
frontend/src/components/icons/agents.tsx.
The application identifies supported Agents in a row using either a generic
OneAgent UI symbol or a separately audited image asset. Generic Lucide symbols
are used for Claude Code, Cursor, Hermes, Kilo CLI, and Aider so their website
favicons are not copied into the release. The three retained image assets are
listed with source, license, owner, and SHA-256 in
frontend/src/components/icons/asset-rights.json.

Marks included: Claude Code (Anthropic), Codex (OpenAI), Cursor (Anysphere),
Kilo CLI (Kilo Code), Aider, OpenCode, OpenClaw, Hermes (Nous Research).

Two marks are redistributed under an open-source license rather than as
favicons: the OpenAI and OpenCode glyphs come from lobehub/icons-static-svg
(MIT), and the OpenClaw glyph from openclaw/openclaw (MIT).
This is nominative use: the labels identify the referenced products and do not
imply endorsement, affiliation or sponsorship. The image assets are not
recoloured or restyled and remain the property of their respective owners.
2 changes: 1 addition & 1 deletion docs/internal/cc-switch-reference-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ CC Switch 依赖 **`@lobehub/icons-static-svg`**(MIT,723 个 AI 品牌 SVG
| Kilo CLI | `kilocode.ai/favicon/favicon.svg` |
| Aider | `aider.chat/assets/icons/favicon-32x32.png` |

因此不再有任何自绘图标。**结论:判断某个品牌「没有官方图标」之前,要查该项目自己的站点,而不是只查一个图标集。**
这些来源只能证明「站点上存在一个图像」,不能单独证明 OneAgent 有权复制并随二进制再分发。当前实现只保留有完整权利清单的 Codex、OpenCode、OpenClaw 资产;Claude Code、Cursor、Hermes、Kilo CLI 和 Aider 使用通用 Lucide 符号。**结论:判断某个品牌「没有官方图标」之前,要查该项目自己的站点;决定能否随产品再分发之前,还要取得许可证或书面授权。**

两个资产不能直接用,需换等价版本:`openai.com/favicon.svg` 与 `opencode.ai/favicon.svg` 都带 `:root` CSS 变量和 `prefers-color-scheme` 媒体查询,内联进宿主文档会污染全局样式;`aider.chat/assets/logo.svg` 是 200×60 的文字标加高斯模糊滤镜,不是方形图标。

Expand Down
46 changes: 31 additions & 15 deletions frontend/src/components/icons/agents.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { render } from "@testing-library/react";
import { describe, expect, it } from "vitest";

import { AGENT_ICON_IDS, AgentIcon, agentMarkSource, agentTagline } from "./agents";
import { AGENT_ICON_IDS, AgentIcon, agentMarkKind, agentMarkRights, agentMarkSource, agentTagline } from "./agents";

const AUTO_AGENTS = ["codex", "claude-code", "opencode", "kilo-cli", "aider"];
// Shown on the first screen alongside them, so they need marks of their own even
Expand All @@ -16,19 +16,33 @@ describe("AgentIcon", () => {
for (const id of ALL) {
expect(AGENT_ICON_IDS).toContain(id);
}
const sources = ALL.map((id) => {
const rendered = ALL.map((id) => {
const { container } = render(<AgentIcon agentId={id} />);
return container.querySelector("img")?.getAttribute("src") ?? "";
return container.innerHTML;
});
expect(new Set(sources).size).toBe(ALL.length);
expect(sources.every(Boolean)).toBe(true);
expect(new Set(rendered).size).toBe(ALL.length);
expect(rendered.every(Boolean)).toBe(true);
});

it("records where every mark came from", () => {
// Provenance is the thing that makes these safe to ship: each is the
// project's own published artwork, not something drawn to look like it.
for (const id of ALL) {
expect(agentMarkSource(id)).toMatch(/\.(svg|png)|MIT/);
it("uses generic Lucide marks for assets without redistribution evidence", () => {
for (const id of ["claude-code", "cursor", "hermes", "kilo-cli", "aider"]) {
const { container } = render(<AgentIcon agentId={id} />);
expect(agentMarkKind(id)).toBe("generic");
expect(container.querySelector("svg[data-mark-kind=generic]")).not.toBeNull();
expect(container.querySelector("img")).toBeNull();
expect(agentMarkRights(id)).toBeNull();
}
});

it("records auditable rights for every redistributed image asset", () => {
for (const id of ["codex", "opencode", "openclaw"]) {
const rights = agentMarkRights(id);
expect(agentMarkKind(id)).toBe("asset");
expect(rights?.license).toBe("MIT");
expect(rights?.source).toMatch(/^https:\/\//);
expect(rights?.sha256).toMatch(/^[a-f0-9]{64}$/);
expect(rights?.licenseSource).toMatch(/^licenses\//);
expect(agentMarkSource(id)).toBe(rights?.source);
}
expect(agentMarkSource("brand-new-agent")).toBe("");
});
Expand All @@ -54,11 +68,13 @@ describe("AgentIcon", () => {
for (const size of [18, 20]) {
for (const id of ALL) {
const { container } = render(<AgentIcon agentId={id} size={size} />);
const img = container.querySelector("img");
expect(img?.getAttribute("width")).toBe(String(size));
expect(img?.getAttribute("height")).toBe(String(size));
// contain, so a mark with tighter padding cannot overflow the box.
expect(img?.style.objectFit).toBe("contain");
const mark = container.querySelector<HTMLElement>("img, svg");
expect(mark?.getAttribute("width")).toBe(String(size));
expect(mark?.getAttribute("height")).toBe(String(size));
if (mark?.tagName === "IMG") {
// contain, so a mark with tighter padding cannot overflow the box.
expect((mark as HTMLImageElement).style.objectFit).toBe("contain");
}
}
}
});
Expand Down
120 changes: 86 additions & 34 deletions frontend/src/components/icons/agents.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,64 @@
/**
* Agent marks, taken from each project's own published artwork.
* Agent marks used to identify rows in OneAgent.
*
* Earlier revisions drew these by hand to keep one monochrome style. That
* inverted the priority: a glyph has to be recognised before it is tidy, and a
* hand-drawn "claw" read as a cup. Every Agent here has an official mark, so
* every mark is the official one, in the brand's own colours.
* Three image assets remain because their exact source, MIT license text,
* copyright owner, and SHA-256 are tracked in asset-rights.json. Agents whose
* published artwork does not have an auditable redistribution basis use generic
* Lucide marks instead; those marks identify a row without copying a vendor
* favicon into the release.
*
* Uniformity now comes from the container rather than from redrawing: one square
* box, one size per context, consistent padding. That is how a folder of
* unrelated app icons still reads as a set.
*
* Trademark note: these identify which Agent a row refers to — nominative use.
* They are not OneAgent product artwork and are not recoloured or restyled.
* Sources are recorded per entry; assets live beside this file and are inlined
* at build time, since the release policy forbids external references.
* Trademark note: the labels identify which Agent a row refers to. The generic
* marks are OneAgent UI symbols, not vendor artwork. The three licensed image
* marks are not recoloured or restyled.
*/
import { Bot } from "lucide-react";
import {
Blocks,
Bot,
Braces,
GitBranch,
MousePointer2,
Sparkles,
type LucideIcon,
} from "lucide-react";

import { sourceTranslate, type Translate, type TranslationKey } from "../../i18n";

import aiderMark from "./assets/aider.png";
import claudeMark from "./assets/claude-code.svg";
import assetRightsManifest from "./asset-rights.json";
import codexMark from "./assets/codex.svg";
import cursorMark from "./assets/cursor.svg";
import hermesMark from "./assets/hermes.png";
import kiloMark from "./assets/kilo-cli.svg";
import openclawMark from "./assets/openclaw.svg";
import opencodeMark from "./assets/opencode.svg";

/** Where each mark came from, so the next person can re-check it. */
const MARKS: Record<string, { src: string; source: string }> = {
codex: { src: codexMark, source: "lobehub/icons-static-svg openai (MIT)" },
"claude-code": { src: claudeMark, source: "claude.ai/favicon.svg" },
cursor: { src: cursorMark, source: "cursor.com/favicon.svg" },
opencode: { src: opencodeMark, source: "lobehub/icons-static-svg opencode (MIT)" },
openclaw: { src: openclawMark, source: "openclaw/openclaw docs/assets/pixel-lobster.svg (MIT)" },
hermes: { src: hermesMark, source: "hermes-agent.nousresearch.com/icon.png" },
"kilo-cli": { src: kiloMark, source: "kilocode.ai/favicon/favicon.svg" },
aider: { src: aiderMark, source: "aider.chat/assets/icons/favicon-32x32.png" },
type AssetRights = (typeof assetRightsManifest.assets)[keyof typeof assetRightsManifest.assets];
type AssetMark = { kind: "asset"; src: string; source: string; rights: AssetRights };
type GenericMark = { kind: "generic"; Icon: LucideIcon; source: string };
type Mark = AssetMark | GenericMark;

const GENERIC_SOURCE = "lucide-react@1.25.0 (ISC)";

const MARKS: Record<string, Mark> = {
codex: {
kind: "asset",
src: codexMark,
source: assetRightsManifest.assets.codex.source,
rights: assetRightsManifest.assets.codex,
},
opencode: {
kind: "asset",
src: opencodeMark,
source: assetRightsManifest.assets.opencode.source,
rights: assetRightsManifest.assets.opencode,
},
openclaw: {
kind: "asset",
src: openclawMark,
source: assetRightsManifest.assets.openclaw.source,
rights: assetRightsManifest.assets.openclaw,
},
"claude-code": { kind: "generic", Icon: Braces, source: GENERIC_SOURCE },
cursor: { kind: "generic", Icon: MousePointer2, source: GENERIC_SOURCE },
hermes: { kind: "generic", Icon: Sparkles, source: GENERIC_SOURCE },
"kilo-cli": { kind: "generic", Icon: Blocks, source: GENERIC_SOURCE },
aider: { kind: "generic", Icon: GitBranch, source: GENERIC_SOURCE },
};

/** One-line positioning shown on hover; never a restatement of the name. */
Expand All @@ -59,17 +80,48 @@ export function agentTagline(agentId: string, t: Translate = sourceTranslate): s
return tagline ? t(tagline) : "";
}

/** The provenance of an Agent's mark, for the reference notes and tests. */
/** The source URL or package provenance displayed in reference notes/tests. */
export function agentMarkSource(agentId: string): string {
return MARKS[agentId]?.source ?? "";
}

/** Whether an Agent uses a licensed image asset, a generic mark, or fallback. */
export function agentMarkKind(agentId: string): "asset" | "generic" | "fallback" {
return MARKS[agentId]?.kind ?? "fallback";
}

/** Auditable rights for an image asset; generic marks intentionally return null. */
export function agentMarkRights(agentId: string): AssetRights | null {
const mark = MARKS[agentId];
return mark?.kind === "asset" ? mark.rights : null;
}

export function AgentIcon({ agentId, size = 18 }: { agentId: string; size?: number }) {
const mark = MARKS[agentId];
if (!mark) {
// An Agent added to agents.lock.json before its mark is fetched still
// An Agent added to agents.lock.json before its mark is assigned still
// renders rather than leaving a blank square.
return <Bot size={size} strokeWidth={1.8} aria-hidden="true" />;
return (
<Bot
width={size}
height={size}
strokeWidth={1.8}
data-mark-kind="fallback"
aria-hidden="true"
/>
);
}
if (mark.kind === "generic") {
const Icon = mark.Icon;
return (
<Icon
width={size}
height={size}
strokeWidth={1.8}
data-mark-kind="generic"
aria-hidden="true"
/>
);
}
return (
<img
Expand All @@ -79,8 +131,8 @@ export function AgentIcon({ agentId, size = 18 }: { agentId: string; size?: numb
height={size}
alt=""
aria-hidden="true"
// Marks are square but not identically padded; contain keeps the tallest
// and widest ones from overflowing the box they share.
data-mark-kind="asset"
// contain keeps the three licensed assets inside the same square box.
style={{ objectFit: "contain" }}
draggable={false}
/>
Expand Down
29 changes: 29 additions & 0 deletions frontend/src/components/icons/asset-rights.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"schemaVersion": 1,
"assets": {
"codex": {
"file": "assets/codex.svg",
"source": "https://github.com/lobehub/lobe-icons/blob/master/packages/static-svg/icons/openai.svg",
"license": "MIT",
"licenseSource": "licenses/lobehub/LICENSE",
"copyrightOwner": "LobeHub contributors",
"sha256": "a595df6b423920c67a7f8f73c063e4bfb72d415948097b6cac063a2366bb5186"
},
"opencode": {
"file": "assets/opencode.svg",
"source": "https://github.com/lobehub/lobe-icons/blob/master/packages/static-svg/icons/opencode.svg",
"license": "MIT",
"licenseSource": "licenses/lobehub/LICENSE",
"copyrightOwner": "LobeHub contributors",
"sha256": "7cfa6e9d6726f7c9fa26c7d9aef0dfec52d20a137380454340f30f12ccbfd302"
},
"openclaw": {
"file": "assets/openclaw.svg",
"source": "https://github.com/openclaw/openclaw/blob/main/docs/assets/pixel-lobster.svg",
"license": "MIT",
"licenseSource": "licenses/openclaw/LICENSE",
"copyrightOwner": "OpenClaw contributors",
"sha256": "1ee82ff331b7295074bf6f60a0812c5f64daad65b2395d5d5e47a6c6c5180a7c"
}
}
}
Binary file removed frontend/src/components/icons/assets/aider.png
Binary file not shown.
3 changes: 0 additions & 3 deletions frontend/src/components/icons/assets/claude-code.svg

This file was deleted.

Loading
Loading