From 6df574cbb472340ba55b7c283e1df43466124159 Mon Sep 17 00:00:00 2001 From: Joseph Winke Date: Tue, 15 Sep 2026 06:37:33 -0400 Subject: [PATCH 1/3] fix(shell): keep __sourceDir on third-party plugin manifests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit publicPluginManifest() stripped __sourceDir from every non-first-party manifest, but service and panel plugin instances receive only that stripped manifest and no separate sourceDir (unlike barWidgets, which get meta.sourceDir). That left third-party service/panel plugins with no way to locate their own bundled scripts and assets: any plugin that launches a bundled script resolves it against an empty base path and fails silently (e.g. Omarchy-Spotify hangs on "Checking playback support…", its keyring/credential scripts never run). __sourceDir is the plugin's own install directory, not a privilege signal — the plugin's code already runs from there. Keep withholding the real privilege markers (__isFirstParty, __hostCapabilities) but stop stripping __sourceDir. The plugin auth-boundary test suite still passes. Co-Authored-By: Claude Opus 4.8 --- shell/shell.qml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/shell/shell.qml b/shell/shell.qml index 82080624..8b4b82dc 100644 --- a/shell/shell.qml +++ b/shell/shell.qml @@ -317,7 +317,13 @@ ShellRoot { if (!manifest) return null if (manifest.__isFirstParty) return manifest var copy = JSON.parse(JSON.stringify(manifest)) - delete copy.__sourceDir + // __sourceDir is the plugin's own install directory, not a privilege + // signal: a third-party service/panel plugin needs it to locate its + // bundled scripts and assets (barWidgets already receive it via + // meta.sourceDir, but service/panel instances get only this manifest). + // Stripping it left those plugins with no way to find their own files, + // silently breaking every plugin that shells out to a bundled script. + // Only the true privilege markers below are withheld from third parties. delete copy.__isFirstParty delete copy.__hostCapabilities return copy From 146fe5d368966560affe3a7e79f64c9c3c366d14 Mon Sep 17 00:00:00 2001 From: Omarchybot <317366263+omarchybot@users.noreply.github.com> Date: Thu, 1 Oct 2026 04:49:25 +0200 Subject: [PATCH 2/3] Say only what is true in the __sourceDir comment Bar widget instances are not handed sourceDir (ModuleSlot.injectProps sets only bar, moduleName and settings), and a plugin could reach its directory through Qt.resolvedUrl, so the comment's rationale was wrong on both counts. Two lines carry what the code cannot say. Co-Authored-By: Claude Opus 5.5 --- shell/shell.qml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/shell/shell.qml b/shell/shell.qml index 8b4b82dc..e2ab310e 100644 --- a/shell/shell.qml +++ b/shell/shell.qml @@ -317,13 +317,8 @@ ShellRoot { if (!manifest) return null if (manifest.__isFirstParty) return manifest var copy = JSON.parse(JSON.stringify(manifest)) - // __sourceDir is the plugin's own install directory, not a privilege - // signal: a third-party service/panel plugin needs it to locate its - // bundled scripts and assets (barWidgets already receive it via - // meta.sourceDir, but service/panel instances get only this manifest). - // Stripping it left those plugins with no way to find their own files, - // silently breaking every plugin that shells out to a bundled script. - // Only the true privilege markers below are withheld from third parties. + // Keep __sourceDir: third-party plugins find their bundled scripts through it. + // Only the host trust markers are withheld. delete copy.__isFirstParty delete copy.__hostCapabilities return copy From 84e88b4a8e44c33692e3561d3a94f2b2306daa8b Mon Sep 17 00:00:00 2001 From: Omarchybot <317366263+omarchybot@users.noreply.github.com> Date: Thu, 1 Oct 2026 04:49:25 +0200 Subject: [PATCH 3/3] Guard third-party manifests keeping __sourceDir No suite failed with the strip restored, so nothing kept the 4.0.3 regression from coming back. Co-Authored-By: Claude Opus 5.5 --- test/shell.d/plugins-test.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/shell.d/plugins-test.sh b/test/shell.d/plugins-test.sh index 5d634dc9..c10509a7 100644 --- a/test/shell.d/plugins-test.sh +++ b/test/shell.d/plugins-test.sh @@ -210,5 +210,18 @@ check( '_syncServices still drops disabled or removed services' ) +const publicManifestMatch = shellSource.match(/function publicPluginManifest\(manifest\) \{[\s\S]*?\n \}/) +check(!!publicManifestMatch, 'publicPluginManifest is defined') +if (publicManifestMatch) { + const host = {} + require('vm').runInNewContext(`${publicManifestMatch[0]}\nthis.publicPluginManifest = publicPluginManifest`, host) + const raw = { id: 'acme.demo', __sourceDir: '/plugins/acme.demo', __isFirstParty: false, __hostCapabilities: ['authentication'] } + const copy = host.publicPluginManifest(raw) + check(copy.__sourceDir === '/plugins/acme.demo', 'third-party manifests keep their own source directory') + check(!('__isFirstParty' in copy) && !('__hostCapabilities' in copy), 'third-party manifests drop host trust markers') + copy.__sourceDir = '/elsewhere' + check(raw.__sourceDir === '/plugins/acme.demo', 'a plugin editing its manifest copy cannot move the registry entry') +} + assert(errors.length === 0, 'plugin manifests match shell registry contract', errors.join('\n')) JS