diff --git a/NOTICE b/NOTICE index d2c78f4..cc8dbe4 100644 --- a/NOTICE +++ b/NOTICE @@ -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 @@ -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. diff --git a/docs/internal/cc-switch-reference-notes.md b/docs/internal/cc-switch-reference-notes.md index e1ad140..d2741bb 100644 --- a/docs/internal/cc-switch-reference-notes.md +++ b/docs/internal/cc-switch-reference-notes.md @@ -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 的文字标加高斯模糊滤镜,不是方形图标。 diff --git a/frontend/src/components/icons/agents.test.tsx b/frontend/src/components/icons/agents.test.tsx index c690fda..554d623 100644 --- a/frontend/src/components/icons/agents.test.tsx +++ b/frontend/src/components/icons/agents.test.tsx @@ -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 @@ -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(); - 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(); + 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(""); }); @@ -54,11 +68,13 @@ describe("AgentIcon", () => { for (const size of [18, 20]) { for (const id of ALL) { const { container } = render(); - 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("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"); + } } } }); diff --git a/frontend/src/components/icons/agents.tsx b/frontend/src/components/icons/agents.tsx index 2f3f4e3..956e636 100644 --- a/frontend/src/components/icons/agents.tsx +++ b/frontend/src/components/icons/agents.tsx @@ -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 = { - 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 = { + 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. */ @@ -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