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
16 changes: 10 additions & 6 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,18 @@ Third-party trademarks
================================================================================

The application identifies supported Agents in a row using either a generic
OneAgent UI symbol or a separately audited image asset. A generic Lucide symbol
is used for Aider, which has no mark in lobe-icons, so no website favicon is
copied into the release. The five retained image assets are listed with source,
license, owner, and SHA-256 in
OneAgent UI symbol or a separately audited image asset. Generic Lucide symbols
are used for Aider and WorkBuddy, neither of which has a mark in lobe-icons or a
published vector under a license that permits redistribution, so no website
favicon is copied into the release. The six retained image assets are listed
with source, license, owner, and SHA-256 in
frontend/src/components/icons/asset-rights.json.

Four of those five -- for Codex, Claude Code, OpenCode, and Kilo CLI -- come
from lobe-icons and are redistributed unmodified. The fifth, for OpenClaw, is
Five of those six -- for Codex, Claude Code, OpenCode, Kilo CLI, and Hermes
Agent -- come from lobe-icons and are redistributed unmodified. The Hermes Agent
mark is the drawing lobe-icons also publishes as nousresearch.svg, identical but
for its <title>: Hermes Agent is Nous Research's product, so the company mark
identifies it. The sixth, for OpenClaw, is
not the vendor's own artwork: no official vector is published, so the mark is
the lobster drawn by the cc-switch project (MIT, Copyright (c) 2025 Jason
Young), and OneAgent has modified it. The red gradient, the coloured antenna
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/components/DesktopAppSection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,9 @@ describe("DesktopAppSection", () => {
expect(workbuddy).not.toBe(chatgpt);
// ChatGPT Desktop is OpenAI's own app, so a licensed asset is correct there.
expect(chatgpt).toContain('data-mark-kind="asset"');
// WorkBuddy has no licensed mark, so the generic fallback is the right answer.
expect(workbuddy).toContain('data-mark-kind="fallback"');
// WorkBuddy has no licensed mark, so a generic symbol is the right answer --
// a registered one now, so "no redistributable vector exists" is
// distinguishable from an Agent nobody assigned a mark to.
expect(workbuddy).toContain('data-mark-kind="generic"');
});
});
46 changes: 40 additions & 6 deletions frontend/src/components/icons/agents.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { render } from "@testing-library/react";
import { describe, expect, it } from "vitest";

import { AGENT_ICON_IDS, AgentIcon, agentMarkKind, agentMarkRights, agentMarkSource, agentTagline } from "./agents";
import assetRights from "./asset-rights.json";

// Every Agent in agents.lock.json, all of which reach the first screen. Keep in
// step with the catalog: an Agent with no mark falls back to a shared symbol and
Expand Down Expand Up @@ -30,7 +31,7 @@ describe("AgentIcon", () => {
const assetIds = AGENT_ICON_IDS.filter((id) => agentMarkKind(id) === "asset");
// chatgpt-desktop is a desktop Agent rather than a CLI, and it reuses the
// OpenAI mark because it is OpenAI's own product sharing Codex's config.
expect(assetIds.sort()).toEqual(["chatgpt-desktop", "claude-code", "codex", "kilo-cli", "openclaw", "opencode"]);
expect(assetIds.sort()).toEqual(["chatgpt-desktop", "claude-code", "codex", "hermes", "kilo-cli", "openclaw", "opencode"]);
for (const id of assetIds) {
const rights = agentMarkRights(id);
expect(agentMarkKind(id)).toBe("asset");
Expand Down Expand Up @@ -106,7 +107,7 @@ describe("AgentIcon", () => {
// viewBox is the check: inlining must not rescale or crop the artwork.
//
// Each mark is checked against its own source coordinate system rather than
// one shared value. The four vendor marks come from lobe-icons at 24x24;
// one shared value. The five vendor marks come from lobe-icons at 24x24;
// OpenClaw's is the cc-switch drawing at 120x120, and normalising it to 24
// would be the re-drawing this test exists to prevent. The recolouring
// recorded in asset-rights.json does not touch geometry.
Expand All @@ -116,6 +117,7 @@ describe("AgentIcon", () => {
opencode: "0 0 24 24",
"claude-code": "0 0 24 24",
"kilo-cli": "0 0 24 24",
hermes: "0 0 24 24",
openclaw: "0 0 120 120",
};
const assetIds = AGENT_ICON_IDS.filter((value) => agentMarkKind(value) === "asset");
Expand Down Expand Up @@ -159,18 +161,50 @@ describe("AgentIcon", () => {
const { container: codex } = render(<AgentIcon agentId="codex" />);
expect(chatgpt.innerHTML).toBe(codex.innerHTML);

// WorkBuddy has no licensed mark, so it must fall back to the generic symbol
// and must not borrow one that belongs to somebody else.
// WorkBuddy has no licensed mark, so it must use a generic symbol and must
// not borrow one that belongs to somebody else. It is now a registered
// `generic` rather than an unregistered `fallback`, which is the difference
// between "evaluated, no redistributable vector exists" and "never looked
// at" -- the two rendered the same glyph and were indistinguishable.
const { container: workbuddy } = render(<AgentIcon agentId="workbuddy" />);
expect(agentMarkKind("workbuddy")).toBe("fallback");
expect(workbuddy.querySelector('[data-mark-kind="fallback"]')).not.toBeNull();
expect(agentMarkKind("workbuddy")).toBe("generic");
expect(workbuddy.querySelector('[data-mark-kind="generic"]')).not.toBeNull();
expect(workbuddy.innerHTML).not.toBe(codex.innerHTML);
for (const id of AGENT_ICON_IDS.filter((value) => agentMarkKind(value) === "asset")) {
const { container } = render(<AgentIcon agentId={id} />);
expect(workbuddy.innerHTML, `workbuddy must not reuse the ${id} mark`).not.toBe(container.innerHTML);
}
});

// The recorded sha256 is the claim that what ships is byte-for-byte what the
// source published. Nothing verifies it at build time, so a hand-edit to an
// asset -- reducing its coordinate precision, say -- would otherwise leave the
// manifest asserting a hash that no longer matches.
it("ships each asset byte-identical to its recorded hash", async () => {
const { createHash } = await import("node:crypto");
const { readFileSync } = await import("node:fs");
const { join } = await import("node:path");
// path.join off a literal directory, not new URL(..., import.meta.url):
// Vite statically rewrites that form into an asset reference, so the
// interpolated filename was replaced by "undefined" at transform time.
const here = join(process.cwd(), "src/components/icons");
// Iterated over the manifest, not over Agent ids: chatgpt-desktop reuses
// codex's rights object rather than being a manifest key of its own, so
// walking ids would check one file twice and reach a key with no `file`.
for (const [key, rights] of Object.entries(assetRights.assets)) {
const digest = createHash("sha256").update(readFileSync(join(here, rights.file))).digest("hex");
expect(digest, `${key} (${rights.file}) does not match its recorded sha256`).toBe(rights.sha256);
}
});

// Every Agent the app can show needs a mark it was actually given, so a new one
// does not quietly land on the unknown-Agent fallback.
it("registers a mark for every Agent and desktop Agent", () => {
for (const id of [...ALL, "chatgpt-desktop", "workbuddy"]) {
expect(agentMarkKind(id), `${id} has no registered mark`).not.toBe("fallback");
}
});

it("offers a tagline for hover, distinct from the name", () => {
for (const id of ALL) {
const tagline = agentTagline(id);
Expand Down
20 changes: 17 additions & 3 deletions frontend/src/components/icons/agents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
*/
import {
Bot,
Briefcase,
GitBranch,
type LucideIcon
} from "lucide-react";
Expand All @@ -31,6 +32,7 @@ import { sourceTranslate, type Translate, type TranslationKey } from "../../i18n
import assetRightsManifest from "./asset-rights.json";
import claudeCodeMark from "./assets/claude-code.svg?raw";
import codexMark from "./assets/codex.svg?raw";
import hermesMark from "./assets/hermes.svg?raw";
import kiloCliMark from "./assets/kilo-cli.svg?raw";
import openclawMark from "./assets/openclaw.svg?raw";
import opencodeMark from "./assets/opencode.svg?raw";
Expand Down Expand Up @@ -87,10 +89,22 @@ const MARKS: Record<string, Mark> = {
source: assetRightsManifest.assets.codex.source,
rights: assetRightsManifest.assets.codex,
},
// Aider has no mark in lobe-icons, so it keeps a generic symbol rather than a
// vendor favicon copied in without an auditable redistribution basis.
// The same drawing lobe-icons publishes as nousresearch.svg -- byte-identical
// but for the <title> -- which is right: Hermes Agent is Nous Research's
// product, so the company mark identifies it.
hermes: {
kind: "asset",
markup: hermesMark,
source: assetRightsManifest.assets.hermes.source,
rights: assetRightsManifest.assets.hermes,
},
// Aider and WorkBuddy have no mark in lobe-icons and no published vector under
// a licence that permits redistribution, so they keep a generic symbol rather
// than a vendor favicon copied in without an auditable basis. WorkBuddy is
// registered rather than left to AgentIcon's fallback so "evaluated, using a
// generic mark" is distinguishable from "never looked at".
aider: { kind: "generic", Icon: GitBranch, source: GENERIC_SOURCE },
hermes: { kind: "generic", Icon: Bot, source: GENERIC_SOURCE },
workbuddy: { kind: "generic", Icon: Briefcase, source: GENERIC_SOURCE },
// Unlike the four above, this mark is not the vendor's own artwork: it is the
// lobster the cc-switch project drew, MIT, and OneAgent recoloured it to a
// single currentColor glyph so it adapts to the theme like the rest of the set.
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/components/icons/asset-rights.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@
"copyrightOwner": "LobeHub contributors",
"sha256": "a2aef0e0bb992ebe768bfb8c3a3ab6ff2c2638448b09dd88db27877de1c07a22"
},
"hermes": {
"file": "assets/hermes.svg",
"source": "https://github.com/lobehub/lobe-icons/blob/master/packages/static-svg/icons/hermesagent.svg",
"license": "MIT",
"licenseSource": "licenses/lobehub/LICENSE",
"copyrightOwner": "LobeHub contributors",
"sha256": "02d43c0a91fc2ee41f2684fc39613ff6687de29a4b079665bace5303671dd63b"
},
"openclaw": {
"file": "assets/openclaw.svg",
"source": "https://github.com/farion1231/cc-switch/blob/main/src/icons/extracted/claw.svg",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/icons/assets/hermes.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions third_party/THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ the embedded frontend. Full license and notice texts are included under
| --- | --- | --- | --- | --- | --- |
| asset | `claude-code` | `365a70a7eb3956d9b9a96086058ebe04e1dbd8e291a756ad964e8a283fbd6d38` | frontend | MIT | `licenses/assets/claude-code/LICENSE` |
| asset | `codex` | `a595df6b423920c67a7f8f73c063e4bfb72d415948097b6cac063a2366bb5186` | frontend | MIT | `licenses/assets/codex/LICENSE` |
| asset | `hermes` | `02d43c0a91fc2ee41f2684fc39613ff6687de29a4b079665bace5303671dd63b` | frontend | MIT | `licenses/assets/hermes/LICENSE` |
| asset | `kilo-cli` | `a2aef0e0bb992ebe768bfb8c3a3ab6ff2c2638448b09dd88db27877de1c07a22` | frontend | MIT | `licenses/assets/kilo-cli/LICENSE` |
| asset | `openclaw` | `9eac78bcfa8e106ed3267293235281743e161b5accf2ff5c676700b64cfcdbbc` | frontend | MIT | `licenses/assets/openclaw/LICENSE` |
| asset | `opencode` | `7cfa6e9d6726f7c9fa26c7d9aef0dfec52d20a137380454340f30f12ccbfd302` | frontend | MIT | `licenses/assets/opencode/LICENSE` |
Expand Down
21 changes: 21 additions & 0 deletions third_party/licenses/assets/hermes/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 LobeHub

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
13 changes: 13 additions & 0 deletions third_party/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@
],
"version": "a595df6b423920c67a7f8f73c063e4bfb72d415948097b6cac063a2366bb5186"
},
{
"ecosystem": "asset",
"license": "MIT",
"license_files": [
"licenses/assets/hermes/LICENSE"
],
"modification": "",
"name": "hermes",
"platforms": [
"frontend"
],
"version": "02d43c0a91fc2ee41f2684fc39613ff6687de29a4b079665bace5303671dd63b"
},
{
"ecosystem": "asset",
"license": "MIT",
Expand Down
Loading