Skip to content

Allow user plugins to declare a monochrome SVG icon - #4608

Merged
steipete merged 4 commits into
steipete:mainfrom
shivanathd:feat/plugin-svg-icon
Oct 19, 2026
Merged

steipete merged 4 commits into
steipete:mainfrom
shivanathd:feat/plugin-svg-icon

Conversation

@shivanathd

@shivanathd shivanathd commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Allow user plugins to declare a monochrome SVG icon

Summary

User plugins can only show a 1–3 character monogram on a tint today (docs/plugins.md: "File/SVG icons are not
supported"), so a plugin can't look like the service it reports on the way built-in providers do. This adds an
optional inline icon.svg that the host draws as a template image, exactly like built-in provider icons, in the
provider switcher and on the plugin's menu card. The monogram stays the fallback.

defineProvider({
  id: "acme-meter",
  name: "Acme",
  icon: { monogram: "AM", tint: "#336699", svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M2 2h12v12H2z"/></svg>' },
  // ...
});

Safety

The SVG is untrusted plugin input, so it is validated with an allowlist (ProviderPluginIconSVG) before it reaches
NSImage:

  • at most 8 KB, root must be <svg>;
  • elements: svg, g, path, circle, ellipse, rect, line, polyline, polygon, title, desc only;
  • attributes: plain geometry/fill/stroke/transform only;
  • attribute values: only letters, digits, spaces and .,#%+-(), so no CSS escapes, comments, declarations, quotes,
    colons or slashes, and no url in any spelling; fill/stroke must be a colour keyword, #hex or numeric
    rgb()/rgba(); xmlns must be the SVG namespace;
  • no scripts, <style>, <image>/raster, <foreignObject>, links (href), event handlers, CDATA,
    processing instructions, DOCTYPE or entities (external entities are never resolved).

An SVG that fails validation is dropped and the monogram is shown; the plugin itself still loads (an invalid icon
is cosmetic, so it doesn't fail the manifest).

Independently of the sanitizer, I checked the native decoder: on macOS 27, NSImage(data:) decoding and drawing SVGs
with url() paint, an escaped u\72l(), <image href>, <use xlink:href> and <style>@import pointing at a local
HTTP server made no requests at all (server log empty). The sanitizer doesn't rely on that.

Changes

  • CodexBarCore/Plugins/ProviderPluginIconSVG.swift (new): the allowlist validator (FoundationXML on Linux).
  • ProviderPluginManifest.swift: ProviderPluginIcon.svg, parsed from icon.svg, validated, else nil.
  • codexbar-plugin.d.ts: icon?: { monogram?; tint?; svg? } with the limits documented.
  • StatusItemController+UserPlugins.swift: switcher icon uses the SVG as a template image; the card header draws
    it in white on the tint; monogram unchanged when there's no SVG.
  • StatusItemController+MenuLocalization.swift: the SVG joins the menu signature so editing it refreshes the menu.
  • docs/plugins.md, CHANGELOG.md.

Tests / commands run

  • swift build --build-tests ✔
  • swift test --filter UserProviderPluginTests ✔ (28 tests), including the two new ones:
    • plugin svg icon is kept and rendered as a template image
    • unsafe or oversized svg icons fall back to the monogram (script, style, foreignObject, image href, url(),
      onload, DOCTYPE/entity, non-svg root, >8 KB, empty), plus the escaped u\72l(...) bypass from review and red-team
      cases: case and full-width variants, character references, image-set/paint/var, style, use/a/
      xlink:href/xml:base, foreign namespaces, a stylesheet PI and NUL. Verified red: before the value grammar the
      escaped and injection cases were accepted.
    • accepted paints: none, currentColor, names, #hex, rgb()/rgba(), with transform.
  • make check ✔ (0 SwiftLint violations).
  • Not run: the full sharded suite (left to PR CI per AGENTS.md).
  • Note: locally, MenuBarLayoutThroughputTests.swift:82 hits the Swift 6.3.3 type-checker time limit; I used an
    uncommitted local rewrite of that expression to build tests. It is not part of this patch.

Screenshots

Rendered from the production menu views in a test (synthetic "Acme Meter" plugin, no real account data):

icon.svg set No icon.svg (monogram fallback)
plugin svg icon monogram fallback

The images live on a separate pr-proof branch of my fork, not in this PR. The switcher tab and the card header draw the SVG as a template image; without it the existing monogram is unchanged.

🤖 Generated with Claude Code

Plugins can set icon.svg (inline, at most 8 KB). Only plain vector shapes
are accepted; scripts, styles, links, url() references, raster images,
foreignObject and DOCTYPE/entities are rejected, and a rejected SVG falls
back to the monogram. The SVG renders as a template image in the switcher
and on the menu card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@clawsweeper

clawsweeper Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

ClawSweeper review complete

ClawSweeper finished reviewing this revision. The review result is being finalized.

View the workflow run.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-19T07:26:28.356596Z 06b3190 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fc94052594

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/CodexBarCore/Plugins/ProviderPluginIconSVG.swift Outdated
@clawsweeper clawsweeper Bot added clawsweeper:intake:feature Preliminary submission classification; full source review pending. P2 Normal priority bug or improvement with limited blast radius. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. and removed clawsweeper:intake:feature Preliminary submission classification; full source review pending. labels Oct 17, 2026
@clawsweeper

clawsweeper Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codex review: needs real behavior proof before merge.

What this changes

This PR adds optional validated inline SVG icons to user-plugin switcher tabs and menu cards.

Example: Acme Meter declares monogram AM, tint #336699, and an inline gauge SVG.

  • Before: Its switcher tab and menu card show AM.
  • After: Both show the SVG, with AM retained when the SVG is absent or rejected.

Review scores

Measure Result What it means
Overall readiness 🦪 silver shellfish (2/6) The focused patch addresses the earlier defect and has useful rendering evidence, but shipped-app and existing-state validation remain incomplete and the API direction awaits its owner.
Proof confidence 🦪 silver shellfish (2/6) Real behavior proof is necessary before merge. See Before merge.
Patch quality 🐚 platinum hermit (4/6) No actionable review findings were identified.

Product

Kind: Feature · Worth it: Needs a maintainer decision · Fix scope: Complete
User problem: User plugins cannot show recognizable service artwork alongside built-in provider icons.
Reason: The feature has clear visual value, but accepting a new public plugin manifest field belongs to the plugin owner.

Merge readiness

⛔ Blocked before merge - 3 items remain

Keep this PR open for the plugin owner's decision on the public icon contract; the earlier escaped-URL defect is fixed, and no additional actionable code defect was found.

Priority: P2
Reviewed head: 2732769ae05bd2d5b343b83d2d9d336c097787a6
Owner decision: Required. See Decision needed.

Decision needed

  • Question: Should local user plugins gain optional inline SVG branding through the public icon.svg contract?
  • Recommendation: Approve bounded inline SVG support: Accept the optional presentation field while preserving monogram fallback and the prohibition on external resources.
  • Why: The existing documented contract deliberately supports monogram and tint, and no maintainer has approved this additive API direction in the inspected discussion.

Before merge

  • Add real behavior proof - Inspected screenshots exercise the production switcher and card views through synthetic in-process rendering; the separate macOS 27 decoder probe covers hostile-resource inputs, but neither exercises installed-plugin loading through the shipped app. Provide installed-app screenshots or a recording showing SVG and fallback behavior, redacting credentials and non-public endpoints. After adding proof, update the PR body, then request a new review with @clawsweeper re-review. The PR author or someone with repository write access can request it.
  • Resolve maintainer decision - Resolve the maintainer decision shown above before merge.
  • Add data-model compatibility proof - The review found that existing stored data may not work after upgrade. Show that existing data still loads and works with this change.

Findings

None.

Tests

  • Missing end-to-end proof: Show installed-plugin SVG and rejected/absent-SVG fallback behavior, including an upgrade from v0.77.0 with existing settings and approvals intact; the author reports the escaped-reference regression failing before the grammar repair.
Agent review details

How this fits together

Local JavaScript or TypeScript plugin manifests enter the shared parser, which validates optional SVG markup before the app converts it into template images for plugin menu views.

flowchart LR
  A[Local plugin file] --> B[Manifest parser]
  B --> C[SVG allowlist validation]
  C --> D[Validated SVG or monogram fallback]
  D --> E[Native template image]
  E --> F[Switcher and menu card]
Loading

Technical review

Best possible solution:

If the plugin owner approves SVG support, retain this bounded shared validation and existing fallback within the current rendering owners.

Do we have a high-confidence way to reproduce the issue?

This is a feature request: current main intentionally exposes monogram and tint, and source inspection confirms no existing user-plugin SVG path.

Is this the best way to solve the issue?

The implementation fits the existing shared manifest parser and menu renderers, bounds untrusted markup, and preserves fallback without adding a competing rendering owner.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning medium; reviewed against 6480977b47f6.

Provenance checked

Testing

Proof path: in-process harness.

Security

None.

Evidence

What I checked:

Likely related people:

  • Peter Steinberger: Raw commit a82f509 adds Sources/CodexBarCore/Plugins/ProviderPluginManifest.swift:318 relative to its recorded parents. This identifies author metadata, not feature responsibility or a PR merger. (role: source-line author; confidence: high; commits: a82f509ea8e7; files: Sources/CodexBarCore/Plugins/ProviderPluginManifest.swift)
  • Harjoth Khara: Raw commit e9da079 adds Sources/CodexBar/StatusItemController+UserPlugins.swift:145 relative to its recorded parents. This identifies author metadata, not feature responsibility or a PR merger. (role: source-line author; confidence: high; commits: e9da07961168; files: Sources/CodexBar/StatusItemController+UserPlugins.swift)

Labels

Label changes:

No label changes.

Label justifications:

  • P2: This is a limited user-plugin presentation improvement with no established urgent runtime failure.
  • merge-risk: 🚨 compatibility: The PR extends the canonical third-party plugin manifest API; the plugin owner's contract decision remains outstanding.
  • rating: 🦪 silver shellfish: Overall readiness is 🦪 silver shellfish; proof is 🦪 silver shellfish and patch quality is 🐚 platinum hermit.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask.
  • proof: 📸 screenshot: Contributor real behavior proof includes screenshot evidence.

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Supply the installed-plugin and v0.77.0 upgrade evidence described in the proof review.

Rating scale

6/6 🦀 challenger crab · 5/6 🦞 diamond lobster · 4/6 🐚 platinum hermit · 3/6 🦐 gold shrimp · 2/6 🦪 silver shellfish · 1/6 🧂 unranked krab. Overall follows the weaker of proof and patch quality; ✨ marks media proof (a screenshot, video, or linked artifact) that directly shows the changed behavior.

Workflow

ClawSweeper edits this one comment on every review. Comment @clawsweeper re-review for a fresh review only. Repair and merge execution lanes are retired.

History

Review history (2 earlier review cycles)
  • reviewed 2026-10-17T21:45:57.230Z sha fc94052 :: needs real behavior proof before merge. :: [P2] Reject CSS-escaped URL functions in paint attributes
  • reviewed 2026-10-18T06:31:21.537Z sha 2732769 :: needs real behavior proof before merge. :: none

Reviewed October 18, 2026, 2:36 AM ET / 06:36 UTC (Revision 3).

Rejecting a literal url( was not enough: fill="u\72l(...)" passes it and CSS
decodes the escape into a URL function. Attribute values now allow only
letters, digits, spaces and .,#%+-() (no CSS escapes, comments, declarations,
quotes, colons or slashes), fill/stroke must be a colour keyword, #hex or
numeric rgb()/rgba(), and xmlns must be the SVG namespace. Adds the reported
bypass plus red-team cases (case and full-width variants, character
references, image-set/paint/var, style, use/a/xlink/xml:base, foreign
namespaces, stylesheet PI, NUL) and accepted-paint cases.
@shivanathd

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review. Going through the blockers:

Escaped URL functions (P2): fixed in 2732769. Attribute values are now limited to letters, digits, spaces and .,#%+-(), so CSS escapes, comments, declarations, quotes, colons and slashes are all rejected, and url in any spelling is refused. fill/stroke must be a colour keyword, #hex or numeric rgb()/rgba(), which also closes image-set()/paint()/var(). The reported u\72l(...) input is in the rejection test with the other red-team cases (case and full-width variants, character references, style, use/a/xlink:href/xml:base, foreign namespaces, stylesheet PI, NUL). I confirmed the bypass and injection cases were accepted before the change and are rejected after it. UserProviderPluginTests passes (28), and make check reports 0 violations.

Native resource boundary: two layers now.

  1. After validation, no attribute value can express a reference: no href, no url, no <style>, no entities.
  2. I tested the decoder on its own, with no sanitizer in front of it. On macOS 27, NSImage(data:) decoded and drew SVGs using url() paint, the escaped form, <image href>, <use xlink:href> and <style>@import url(...), all pointing at a local http.server. The server logged no requests.
probe
// For each hostile SVG (BASE = http://127.0.0.1:8765):
let image = NSImage(data: Data(svg.utf8)); image?.isTemplate = true
// draw into a 32×32 NSBitmapImageRep, then keep the run loop alive 3 s for any async loader

Output: all five decoded (16×16); python3 -m http.server log: no GET.

So icon rendering never reaches the plugin's network path, with or without approval.

Real behavior proof: the screenshots render the production menu views in-process with a synthetic plugin. I haven't attached an installed-app capture yet, because a dev build here would load my personal plugin config. If you want that for merge, I'm happy to record one against a clean profile.

Maintainer decision: @steipete, this adds an optional icon.svg to the public plugin contract; the monogram stays the default and the fallback. If you'd rather keep plugins monogram-only, I'm fine closing this.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

🦞👀
Exact review queued.

Re-review progress:

@clawsweeper clawsweeper Bot added clawsweeper:intake:feature Preliminary submission classification; full source review pending. and removed merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. clawsweeper:intake:feature Preliminary submission classification; full source review pending. labels Oct 18, 2026
Rasterize validated inline icons into fixed template bitmaps and reject unbounded canvas dimensions before native decoding. Keep validation in the icon constructor so every caller retains the monogram fallback.

Co-authored-by: Shivanath Devinarayanan <1937549+shivanathd@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4fe7af967b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +65 to +66
svg.range(of: "<!DOCTYPE", options: .caseInsensitive) == nil,
svg.range(of: "<!ENTITY", options: .caseInsensitive) == nil,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject encoded DTD declarations during SVG validation

When an icon declares encoding="UTF-7", < can be written as +ADw-, so an external-DTD payload contains neither literal checked substring; Foundation's XMLParser then decodes it and ProviderPluginIconSVG.isValid returns true for inputs such as +ADw-!DOCTYPE svg SYSTEM .... Because userPluginSVGImage later hands the original bytes to NSImage, this defeats the promised DTD/entity boundary and exposes the native decoder to external-entity/network processing; reject non-UTF-8 declarations or reject DTD/entity declarations from the parser itself.

Useful? React with 👍 / 👎.

steipete added a commit that referenced this pull request Oct 19, 2026
@steipete
steipete merged commit ac30c44 into steipete:main Oct 19, 2026
1 check passed
@steipete

Copy link
Copy Markdown
Owner

Merged in ac30c44, thanks @shivanathd! User plugins can now ship a monochrome SVG icon. We kept your allowlist and added a few hardening steps for untrusted SVGs: canvas bounds are validated before decoding and capped, and the icon is rasterized once into a template image with the monogram as fallback. It will ship in the next release.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 06b3190728

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if name == "fill" || name == "stroke" {
let paint = value.trimmingCharacters(in: .whitespaces)
return paint.range(of: self.paintPattern, options: .regularExpression) != nil
&& paint.range(of: "url", options: .caseInsensitive) == nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow color keywords that contain url

When a plugin uses the standard SVG color keyword burlywood for fill or stroke, the paint regex accepts it but this substring check rejects it because the name contains url, so the otherwise valid SVG is discarded and the UI shows the monogram instead. The exact paint grammar already prevents url(...), so URL syntax should be rejected without banning substrings inside valid color names.

Useful? React with 👍 / 👎.

steipete pushed a commit that referenced this pull request Oct 20, 2026
…kage (#4653)

The 0.78.0 glibc Linux CLI linked libxml2.so.2 again (regression of #1046) because the plugin SVG icon validation from #4608 pulled FoundationXML into Linux builds, so it exited 127 on systems without that library such as Ubuntu 26.04. SVG parsing is now macOS-only (Linux uses the existing monogram fallback), and a new `Scripts/check-linux-cli-linkage.sh` inspects the ELF NEEDED entries and rejects any libxml2 SONAME; Linux CI and both glibc release builds run it. Fixes #4652.

Co-authored-by: harjoth <harjoth.khara@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. P2 Normal priority bug or improvement with limited blast radius. proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants