Skip to content

Add toolbar pinning for uploaded Chromium extensions#281

Open
rgarcia wants to merge 2 commits into
mainfrom
hypeship/chromium-extension-pinning
Open

Add toolbar pinning for uploaded Chromium extensions#281
rgarcia wants to merge 2 commits into
mainfrom
hypeship/chromium-extension-pinning

Conversation

@rgarcia

@rgarcia rgarcia commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds an optional per-extension pinned field to /chromium/upload-extensions-and-restart and /configure. When set, the extension is force-pinned to the toolbar via the managed ExtensionSettings.toolbar_pin policy.
  • Force-installed (enterprise) extensions get toolbar_pin: force_pinned on their real Chrome extension ID.
  • --load-extension extensions are assigned an ID by Chrome derived from the install path, so a new UnpackedExtensionID helper computes that ID (SHA-256 of the resolved path mapped into the a-p alphabet) and keys toolbar_pin by it. A name-keyed ExtensionSettings entry is ignored by Chrome, so this is required for pinning to actually take effect.
  • ExtensionSettings stays kernel-managed and blocked from raw user policy override; pinning is exposed as a typed flag and merged into the existing managed entries rather than unblocking the whole policy.

This is the browser-VM half. Wiring the public API/SDK/dashboard to send pinned per extension is a separate follow-up.

Testing

  • Unit: ID helper golden + format, ExtensionSetting toolbar_pin JSON marshaling, both multipart parsers, and parseExtensionPinned. go build ./..., go vet, and go test ./lib/... pass locally.
  • E2E (e2e_extension_pin_test.go): uploads an extension with pinned=true and asserts the toolbar_pin policy key equals the ID Chrome reports on chrome://extensions. This settles the two open questions — pin keyed by the real ID, and our computed ID matches Chrome's. Not run locally (requires Docker + prebuilt images); runs in CI.

Test plan

  • CI e2e TestPinnedExtensionInstallation passes on headless + headful
  • Manually confirm a pinned extension shows pinned in the toolbar on a headful session

Note

Medium Risk
Changes extension upload parsing and managed policy.json (ExtensionSettings, install paths); incorrect ID or pin logic could mis-apply toolbar policy, but behavior is covered by unit and e2e tests.

Overview
Adds optional extensions.pinned on extension upload (upload-extensions-and-restart) and /configure, so each extension can be force-pinned to the toolbar via managed ExtensionSettings.toolbar_pin (force_pinned).

Multipart parsing now groups zip_file, name, and optional pinned per extension (field order within a group is flexible; repeating a field starts a new group). policy.AddExtension takes pinned: force-installed extensions get toolbar_pin on the real Chrome ID; --load-extension paths use a new UnpackedExtensionID helper (path-derived ID) so pinning keys match what Chrome applies. Re-uploading unpinned clears stale pin entries for load-extension installs.

OpenAPI/generated types document pinned. Unit tests cover parsers, ID helper, and policy JSON; a new e2e test uploads with pinned=true and checks policy.json and chrome://extensions IDs align.

Reviewed by Cursor Bugbot for commit dad7682. Bugbot is set up for automated code reviews on this repo. Configure here.

Accept an optional per-extension `pinned` flag on the extension upload and
configure endpoints and translate it into ExtensionSettings.toolbar_pin in
the managed enterprise policy.

For force-installed extensions the pin is set on the real Chrome extension
ID. For --load-extension extensions, which Chrome assigns an ID derived from
the install path, compute that ID (SHA-256 of the resolved path mapped into
the a-p alphabet) and key toolbar_pin by it, since a name-keyed entry is
ignored by Chrome.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@rgarcia rgarcia marked this pull request as ready for review June 10, 2026 00:07

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Unpin leaves stale policy entry
    • For --load-extension updates we now clear any existing computed-ID toolbar_pin when pinned=false so stale force_pinned policy does not persist.

Create PR

Or push these changes by commenting:

@cursor push 9b59b1530d
Preview (9b59b1530d)
diff --git a/server/lib/policy/policy.go b/server/lib/policy/policy.go
--- a/server/lib/policy/policy.go
+++ b/server/lib/policy/policy.go
@@ -242,12 +242,16 @@
 
 			// Extensions on this path are loaded via --load-extension, where Chrome
 			// assigns an ID derived from the install path rather than extensionName.
-			// toolbar_pin is keyed by that real ID, so pin under the computed ID.
+			// toolbar_pin is keyed by that real ID.
+			pinnedID := UnpackedExtensionID(extensionPath)
 			if pinned {
-				pinnedID := UnpackedExtensionID(extensionPath)
 				pinnedSetting := current.ExtensionSettings[pinnedID]
 				pinnedSetting.ToolbarPin = forcePinned
 				current.ExtensionSettings[pinnedID] = pinnedSetting
+			} else if pinnedSetting, exists := current.ExtensionSettings[pinnedID]; exists {
+				// Explicitly clear stale force_pinned when callers unpin.
+				pinnedSetting.ToolbarPin = ""
+				current.ExtensionSettings[pinnedID] = pinnedSetting
 			}
 		}

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 49a7abe. Configure here.

Comment thread server/lib/policy/policy.go
…sing

Address review feedback:

- AddExtension reconciles the --load-extension toolbar_pin entry with the
  pinned flag (deletes the computed-ID entry when not pinned) so re-adding an
  extension unpinned cannot leave a stale force_pinned behind.

- Both multipart parsers group extension fields lazily (a repeated field type
  starts a new group) instead of finalizing eagerly and back-patching the last
  finalized item, so the optional extensions.pinned field stays attached to its
  own extension regardless of position in the group.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant