From fb257bf8720732f382221baf599b87e0e9a11702 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Mon, 7 Sep 2026 19:43:18 -0400 Subject: [PATCH] Restore narrow service proxies for Indicators clones --- shell/services/PluginShellApi.qml | 4 +- shell/shell.qml | 29 ++- test/shell.d/plugin-indicators-clone-test.sh | 175 +++++++++++++++++++ 3 files changed, 199 insertions(+), 9 deletions(-) create mode 100644 test/shell.d/plugin-indicators-clone-test.sh diff --git a/shell/services/PluginShellApi.qml b/shell/services/PluginShellApi.qml index 0202aa01..9be7009d 100644 --- a/shell/services/PluginShellApi.qml +++ b/shell/services/PluginShellApi.qml @@ -31,8 +31,8 @@ QtObject { return _serviceLookup ? _serviceLookup(String(id || "")) : null } - // Only full-bar facades receive narrow proxies for the specific - // non-authentication services used by the built-in bar widgets. + // Full-bar facades and configured Indicators clones under the trusted bar + // receive narrow proxies for their specific non-authentication services. function firstPartyServiceFor(id) { return _firstPartyServiceLookup ? _firstPartyServiceLookup(String(id || "")) : null diff --git a/shell/shell.qml b/shell/shell.qml index c6b7643a..0cc0575d 100644 --- a/shell/shell.qml +++ b/shell/shell.qml @@ -355,6 +355,12 @@ ShellRoot { return shell.manifestHasKind(manifest, "bar") } + function pluginIsIndicatorsClone(manifest) { + var metadata = manifest && Util.isPlainObject(manifest.omarchy) ? manifest.omarchy : null + return shell.manifestHasKind(manifest, "bar-widget") + && !!metadata && String(metadata.clonedFrom || "") === "omarchy.indicators" + } + function publicIdleConfigFor(manifest) { var metadata = manifest && Util.isPlainObject(manifest.omarchy) ? manifest.omarchy : null if (!metadata || String(metadata.clonedFrom || "") !== "omarchy.idle") return ({}) @@ -519,6 +525,7 @@ ShellRoot { return [ allowOwnService ? "own-service" : "no-own-service", barCapabilities ? "bar" : "no-bar", + allowOwnService && shell.pluginIsIndicatorsClone(manifest) ? "indicators" : "no-indicators", shell.manifestHasKind(manifest, "menu") ? "menu" : "no-menu" ].join("|") } @@ -582,12 +589,13 @@ ShellRoot { // makes QML re-enter the binding and report a loop on the caller's service // property, even though the resulting proxy is otherwise acyclic. var firstPartyServices = ({}) - if (barCapabilities) { - var serviceIds = ["omarchy.idle", "omarchy.media", "omarchy.nightlight", "omarchy.notifications"] - for (var i = 0; i < serviceIds.length; i++) { - var serviceId = serviceIds[i] - firstPartyServices[serviceId] = shell.pluginFirstPartyServiceFor(cacheKey, key, serviceId) - } + var serviceIds = barCapabilities + ? ["omarchy.idle", "omarchy.media", "omarchy.nightlight", "omarchy.notifications"] + : (allowOwnService && shell.pluginIsIndicatorsClone(manifest) + ? ["omarchy.idle", "omarchy.nightlight", "omarchy.notifications"] : []) + for (var i = 0; i < serviceIds.length; i++) { + var serviceId = serviceIds[i] + firstPartyServices[serviceId] = shell.pluginFirstPartyServiceFor(cacheKey, key, serviceId) } var api = pluginShellApiComponent.createObject(null, { @@ -603,7 +611,14 @@ ShellRoot { _firstPartyServiceLookup: function(requestedId) { if (allowOwnService && shell.pluginOwnsTarget(key, requestedId)) return shell.pluginServiceFor(key, requestedId) - return hasCurrentBarCapabilities() ? (firstPartyServices[requestedId] || null) : null + // Indicators clones have no service of their own. Under the trusted + // bar they need only these prebuilt non-authentication proxies, not + // full-bar capabilities or access to another plugin's live service. + var indicatorsAllowed = allowOwnService && shell.pluginIsIndicatorsClone(currentManifest()) + && shell.activeBarManifest && shell.activeBarManifest.__isFirstParty + && shell.pluginRegistry.isEnabled(key) && shell.barEntryConfigured(key) + return hasCurrentBarCapabilities() || indicatorsAllowed + ? (firstPartyServices[requestedId] || null) : null }, _barEntryShellLookup: function(ownerId, moduleName) { return hasCurrentBarCapabilities() diff --git a/test/shell.d/plugin-indicators-clone-test.sh b/test/shell.d/plugin-indicators-clone-test.sh new file mode 100644 index 00000000..db49eb3d --- /dev/null +++ b/test/shell.d/plugin-indicators-clone-test.sh @@ -0,0 +1,175 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +# Run the production facade factories and proxy methods with QObject/binding +# test doubles. Native QML reactivity and graphical clicks need a compositor. +run_node_test <<'JS' +const fs = require('fs') +const vm = require('vm') +const read = file => fs.readFileSync(path.join(root, file), 'utf8') +const shellSource = read('shell/shell.qml') +const barSource = read('shell/plugins/bar/Bar.qml') +const apiSource = read('shell/services/PluginShellApi.qml') +const proxySource = read('shell/services/PluginFirstPartyServiceApi.qml') +const builtin = JSON.parse(read('shell/plugins/bar/widgets/Indicators.manifest.json')) + +function loadFunctions(source, names, context) { + for (const name of names) { + const match = source.match(new RegExp(`^ function ${name}\\([^\\n]*\\) \\{[\\s\\S]*?^ \\}`, 'm')) + if (!match) fail(`production function is present: ${name}`) + vm.runInContext(match[0], context) + } +} + +function component(source, methods, defaults = {}) { + return { + createObject(parent, properties) { + const target = vm.createContext({ + ...defaults, ...properties, parent, destroyed: false, + destroy() { this.destroyed = true } + }) + loadFunctions(source, methods, target) + return new Proxy(target, { + set(object, key, value) { + if (value && value.binding) + Object.defineProperty(object, key, { configurable: true, get: value.binding }) + else object[key] = value + return true + } + }) + } + } +} + +const id = 'local.indicators' +const manifest = { ...builtin, id, omarchy: { clonedFrom: builtin.id } } +const enabled = new Set([id]) +const configured = new Set([id]) +const implementations = {} +const services = { + 'omarchy.idle': { stayAwake: true, setIdleEnabled(value) { this.stayAwake = !value } }, + 'omarchy.nightlight': { enabled: true, setNightlight(value) { this.enabled = value } }, + 'omarchy.notifications': { doNotDisturb: true, setDoNotDisturb(value) { this.doNotDisturb = value } } +} +const context = vm.createContext({ + Util: { isPlainObject: value => !!value && typeof value === 'object' && !Array.isArray(value) }, + Qt: { binding: binding => ({ binding }) }, + _services: services, + _pluginShellApis: {}, _pluginShellApiDescriptors: {}, _pluginBarEntryShellApis: {}, + _pluginAppLibraryApis: {}, _pluginFirstPartyServiceApis: {}, + _pluginRegistryApis: {}, _pluginBarWidgetRegistryApis: {}, _pluginBarStateApis: {}, + activeBarManifest: { id: 'omarchy.bar', __isFirstParty: true }, + pluginRegistry: { + installedPlugins: { [id]: manifest }, + isEnabled: key => enabled.has(key), + resolveEnabledId: key => implementations[key] || key, + findEntryLocation: (config, key) => ({ kind: configured.has(key) ? 'bar' : 'plugin' }) + }, + shellConfig: {}, barConfig: {}, + pluginBarStateFor: () => null, + pluginShellApiComponent: component(apiSource, + ['serviceFor', 'firstPartyServiceFor', 'pluginShellForBarEntry', 'mutateShellConfig'], + { _serviceLookup: null, _firstPartyServiceLookup: null, _barEntryShellLookup: null }), + pluginFirstPartyServiceApiComponent: component(proxySource, + ['setIdleEnabled', 'setNightlight', 'setDoNotDisturb']) +}) +context.shell = context +// Load definitions, not the QML root: imports track production helper changes +// and the same behavioral test can run against the pre-fix shell. +loadFunctions(shellSource, + [...shellSource.matchAll(/^ function (\w+)\(/gm)].map(match => match[1]), context) +context.pluginBarStateFor = () => null + +// Use the actual trusted-bar injection function, including its cached bar API. +const bar = vm.createContext({ shell: context, pluginBarApis: { slot: {} } }) +bar.root = bar +loadFunctions(barSource, ['pluginBarApiFor'], bar) +const api = bar.pluginBarApiFor('slot', id, true).shell +const serviceIds = ['omarchy.idle', 'omarchy.nightlight', 'omarchy.notifications'] +const proxies = serviceIds.map(key => api.firstPartyServiceFor(key)) +assert(proxies.every(Boolean), 'Indicators clone under the trusted bar receives all three service proxies') +assertDeepEqual(Object.keys(context._pluginFirstPartyServiceApis), serviceIds.map(key => `${id}::${key}`), + 'Indicators proxies are eagerly created without the media proxy') +for (let i = 0; i < serviceIds.length; i++) { + const key = serviceIds[i] + assert(proxies[i] !== services[key] && proxies[i].parent === null && api.serviceFor(key) === null, + `${key} exposes a detached proxy, never the live service`) +} +assert(proxies[0].stayAwake && proxies[1].enabled && proxies[2].doNotDisturb, + 'Indicators proxies mirror their service scalar state') +proxies[0].setIdleEnabled(true) +proxies[1].setNightlight(false) +proxies[2].setDoNotDisturb(false) +assert(!services['omarchy.idle'].stayAwake && !services['omarchy.nightlight'].enabled + && !services['omarchy.notifications'].doNotDisturb, + 'Indicators proxy callbacks update exactly their non-authentication service') +const idleClone = { stayAwake: false, setIdleEnabled(value) { this.stayAwake = !value } } +services['local.idle'] = idleClone +implementations['omarchy.idle'] = 'local.idle' +proxies[0].setIdleEnabled(false) +assert(idleClone.stayAwake && proxies[0].stayAwake && !services['omarchy.idle'].stayAwake, + 'an existing proxy follows the enabled service clone') + +for (const key of ['omarchy.lock', 'omarchy.polkit', 'omarchy.media', 'local.idle', 'unrelated.service']) { + assert(api.firstPartyServiceFor(key) === null && api.serviceFor(key) === null, + `Indicators facade denies unrelated service lookup: ${key}`) +} +assert(api.pluginShellForBarEntry('other', id) === null && !api.mutateShellConfig(() => {}), + 'Indicators compatibility does not confer full-bar factories or configuration mutation') + +context.activeBarManifest = { id: 'local.bar', kinds: ['bar'] } +assert(serviceIds.every(key => api.firstPartyServiceFor(key) === null), + 'Indicators service lookups are denied while a replacement bar is active') +const entryApi = context.pluginShellForBarEntry('local.bar:slot', id) +assert(serviceIds.every(key => entryApi.firstPartyServiceFor(key) === null), + 'replacement-bar entry facade remains service-less') +context.activeBarManifest = null +assert(serviceIds.every(key => api.firstPartyServiceFor(key) === null), + 'Indicators service lookups are denied without a trusted active bar') +context.activeBarManifest = { id: 'omarchy.bar', __isFirstParty: true } +configured.delete(id) +assert(serviceIds.every(key => api.firstPartyServiceFor(key) === null), + 'unconfigured Indicators clones cannot look up the services') +configured.add(id) +enabled.delete(id) +assert(serviceIds.every(key => api.firstPartyServiceFor(key) === null), + 'disabled Indicators clones cannot look up the services') +enabled.add(id) +assert(serviceIds.every(key => api.firstPartyServiceFor(key) !== null), + 'restoring the configured enabled clone restores narrow lookups') + +for (const changed of [ + { ...manifest, omarchy: { clonedFrom: 'omarchy.clock' } }, + { ...manifest, kinds: ['panel'] } +]) { + context.pluginRegistry.installedPlugins[id] = changed + assert(serviceIds.every(key => api.firstPartyServiceFor(key) === null), + 'lookups recheck the current clone source and widget kind') +} +context.prunePluginApis() +assert(api.destroyed && proxies.every(proxy => proxy.destroyed) + && !context._pluginShellApis[id] && Object.keys(context._pluginFirstPartyServiceApis).length === 0, + 'a changed Indicators capability revokes the facade and its proxy cache') +const ordinary = context.pluginShellFor(context.pluginRegistry.installedPlugins[id]) +assert(serviceIds.every(key => ordinary.firstPartyServiceFor(key) === null), + 'ordinary plugins do not receive Indicators compatibility') +context.pluginRegistry.installedPlugins[id] = manifest +const restored = context.pluginShellFor(manifest) +assert(restored !== ordinary && ordinary.destroyed + && serviceIds.every(key => restored.firstPartyServiceFor(key) !== null), + 'adding back the Indicators capability recreates the facade and eager proxies') +const hosted = context.createScopedPluginShell(manifest, 'hosted:' + id, false, false) +assert(serviceIds.every(key => hosted.firstPartyServiceFor(key) === null), + 'service-less hosted facades cannot acquire Indicators compatibility') + +const fullBar = { id: 'local.bar', kinds: ['bar'] } +context.pluginRegistry.installedPlugins[fullBar.id] = fullBar +const fullBarApi = context.pluginShellFor(fullBar) +assert(fullBarApi.firstPartyServiceFor('omarchy.media') !== null + && fullBarApi.firstPartyServiceFor('omarchy.lock') === null + && fullBarApi.firstPartyServiceFor('omarchy.polkit') === null, + 'existing full-bar non-authentication proxy scope is unchanged') +JS