Preserve built-in clone integrations

Co-Authored-By: GPT-5.6-Sol <noreply@openai.com>
This commit is contained in:
Ryan HughesandGPT-5.6-Sol committed 2026-09-06 22:04:48 -04:00
1 parent f2b4731bb3
commit 3292c19fef
10 files changed
+64 -8

No files matched your search

+1 -1
View File
@@ -20,7 +20,7 @@ Run `omarchy-restart-shell` after making changes to QML files.
[`docs/omarchy-shell.md`](../../docs/omarchy-shell.md) and
`shell/services/PluginRegistry.qml` for the current contract; fields such as
`activation` are optional.
- Entry-point QML files are `Item`s (not `ShellRoot`), and accept the shell-injected properties `omarchyPath`, `shell`, `manifest`, and `pluginRegistry` / `barWidgetRegistry` as appropriate. First-party plugins receive the host objects. Third-party plugins receive capability-scoped facades: ordinary plugins may look up and control only their own service and lifecycle, menu plugins receive an application-library facade, and plugins can read detached scalar bar state; full-bar plugins additionally receive detached bar configuration and widget-catalog snapshots, narrow proxies for the non-authentication services used by built-in bar widgets, and lifecycle control over configured non-authentication UI plugins. Authentication capabilities must be stamped from trusted first-party manifests, and third-party registry views and bar configuration must be detached snapshots rather than shared objects. These facades reduce accidental authority but are not a same-process QML sandbox: a visual bar widget can walk its parent hierarchy to ordinary host objects. Authentication services must therefore remain outside both `ShellRoot._services` and the host QObject tree. Do not expose authentication services through new third-party-facing properties.
- Entry-point QML files are `Item`s (not `ShellRoot`), and accept the shell-injected properties `omarchyPath`, `shell`, `manifest`, and `pluginRegistry` / `barWidgetRegistry` as appropriate. First-party plugins receive the host objects. Third-party plugins receive capability-scoped facades: ordinary plugins may look up and control only their own service and lifecycle, built-in clones retain narrow source-specific configuration and UI compatibility, menu plugins receive an application-library facade, and plugins can read detached scalar bar state; full-bar plugins additionally receive detached bar configuration and widget-catalog snapshots, narrow proxies for the non-authentication services used by built-in bar widgets, and lifecycle control over configured non-authentication UI plugins. Authentication capabilities must be stamped from trusted first-party manifests, and third-party registry views and bar configuration must be detached snapshots rather than shared objects. These facades reduce accidental authority but are not a same-process QML sandbox: a visual bar widget can walk its parent hierarchy to ordinary host objects. Authentication services must therefore remain outside both `ShellRoot._services` and the host QObject tree. Do not expose authentication services through new third-party-facing properties.
- Panel / overlay / menu plugins must expose `open(payloadJson)` and
`close()` lifecycle methods for `shell summon` and `shell hide`.
+1 -1
View File
@@ -39,7 +39,7 @@ Only one full bar option is active at a time. The built-in `omarchy.bar` is
used when `bar.id` is omitted or when a selected third-party bar cannot load.
Panels, overlays, and menus are loaded when summoned. Plugins can set the top-level manifest key `keepLoaded: true` to survive between summons, and to keep a service mounted across plugin hot-reload (so `omarchy.lock` is not destroyed while Hyprland still holds the session lock). First-party services are loaded at startup.
Entry points are QML `Item`s. Panel, overlay, and menu entry points expose `open(payloadJson)` and `close()` for summon/hide; on load the host injects `omarchyPath`, `shell`, `manifest`, and the registries (`pluginRegistry` / `barWidgetRegistry`) as properties. Built-in plugins receive the trusted host objects. Third-party plugins receive capability-scoped facades instead: ordinary plugins may look up and control only their own service and lifecycle, menu plugins receive an application-library facade, and plugins can read detached scalar bar state. A full-bar plugin additionally receives detached bar configuration and widget-catalog snapshots, narrow proxies for the non-authentication services used by built-in bar widgets, and lifecycle control over configured non-authentication UI plugins. Authentication capabilities are stamped from trusted first-party manifests, authentication services are kept out of the host's public service map and QML object tree, and third-party registry/configuration snapshots can be changed only locally without mutating host state. The facades are API boundaries, not same-process QML sandboxes: a visual widget shares the host bar's scene and can walk its parent hierarchy to ordinary host objects. Sensitive state must not rely on the facade alone for isolation.
Entry points are QML `Item`s. Panel, overlay, and menu entry points expose `open(payloadJson)` and `close()` for summon/hide; on load the host injects `omarchyPath`, `shell`, `manifest`, and the registries (`pluginRegistry` / `barWidgetRegistry`) as properties. Built-in plugins receive the trusted host objects. Third-party plugins receive capability-scoped facades instead: ordinary plugins may look up and control only their own service and lifecycle, built-in clones retain narrow source-specific configuration and UI compatibility, menu plugins receive an application-library facade, and plugins can read detached scalar bar state. A full-bar plugin additionally receives detached bar configuration and widget-catalog snapshots, narrow proxies for the non-authentication services used by built-in bar widgets, and lifecycle control over configured non-authentication UI plugins. Authentication capabilities are stamped from trusted first-party manifests, authentication services are kept out of the host's public service map and QML object tree, and third-party registry/configuration snapshots can be changed only locally without mutating host state. The facades are API boundaries, not same-process QML sandboxes: a visual widget shares the host bar's scene and can walk its parent hierarchy to ordinary host objects. Sensitive state must not rely on the facade alone for isolation.
A third-party replacement bar can render registered widget components, but widgets it hosts receive a service-less entry facade. Allowing the bar to manufacture an own-service facade for an arbitrary widget would also let it retrieve that plugin's live service object. Service-backed third-party widgets therefore retain their full integration only under the trusted built-in bar; a replacement bar may still provide their target-scoped lifecycle and settings operations.
+1 -1
View File
@@ -4,7 +4,7 @@ The Omarchy desktop runs as a single long-lived Quickshell process called `omarc
That's not just an implementation detail. It means you can turn pieces of the desktop off, swap them out, or write your own without touching a line of Omarchy's source.
The first-party plugins ship with Omarchy and live in `$OMARCHY_PATH/shell/plugins/`. Anything you add yourself — your own experiments, or something you found on GitHub — lives in `~/.config/omarchy/plugins/`. Both are discovered the same way at startup, but built-ins receive trusted shell interfaces while third-party plugins receive a limited interface scoped to their own service and lifecycle.
The first-party plugins ship with Omarchy and live in `$OMARCHY_PATH/shell/plugins/`. Anything you add yourself — your own experiments, or something you found on GitHub — lives in `~/.config/omarchy/plugins/`. Both are discovered the same way at startup, but built-ins receive trusted shell interfaces while third-party plugins receive a limited interface scoped to their own service and lifecycle. Clones of built-ins keep only the source-specific configuration and UI calls needed for the original behavior.
## Seeing what you have
+1 -1
View File
@@ -93,7 +93,7 @@ holds the session lock. The kept instance is not replaced, so code
changes to a `keepLoaded` service itself only take effect on a shell
restart. First-party services are loaded at startup.
Entry points may declare `omarchyPath`, `shell`, `manifest`, `pluginRegistry`, and `barWidgetRegistry` properties for host injection. Built-in plugins receive the trusted host objects. Third-party plugins receive capability-scoped facades: ordinary plugins can look up and control only their own service and lifecycle, menu plugins receive an application-library facade, and plugins can read detached scalar bar state. A full-bar plugin additionally receives detached bar configuration and widget-catalog snapshots, narrow proxies for the non-authentication services used by built-in bar widgets, and lifecycle control over configured non-authentication UI plugins. Authentication capabilities are stamped from trusted first-party manifests, authentication services are retained outside the host's public service map and QML object tree, and changing a third-party registry or configuration snapshot cannot mutate host state. Facades do not isolate visual widgets from the parent hierarchy of the shared QML scene, so sensitive state must remain outside that reachable graph.
Entry points may declare `omarchyPath`, `shell`, `manifest`, `pluginRegistry`, and `barWidgetRegistry` properties for host injection. Built-in plugins receive the trusted host objects. Third-party plugins receive capability-scoped facades: ordinary plugins can look up and control only their own service and lifecycle, built-in clones retain narrow source-specific configuration and UI compatibility, menu plugins receive an application-library facade, and plugins can read detached scalar bar state. A full-bar plugin additionally receives detached bar configuration and widget-catalog snapshots, narrow proxies for the non-authentication services used by built-in bar widgets, and lifecycle control over configured non-authentication UI plugins. Authentication capabilities are stamped from trusted first-party manifests, authentication services are retained outside the host's public service map and QML object tree, and changing a third-party registry or configuration snapshot cannot mutate host state. Facades do not isolate visual widgets from the parent hierarchy of the shared QML scene, so sensitive state must remain outside that reachable graph.
Widgets rendered by a third-party replacement bar receive a service-less entry facade with target-scoped lifecycle and settings operations. Their live service objects are available only when the trusted built-in bar hosts them; otherwise the replacement bar could request and retain any configured widget's service.
+2 -1
View File
@@ -16,7 +16,8 @@ Item {
readonly property string stayAwakeStatePath: stayAwakeStateDir + "/stay-awake"
readonly property int defaultScreensaverSeconds: 150
readonly property int defaultLockSeconds: 300
readonly property var idleConfig: shell && shell.shellConfig && shell.shellConfig.idle ? shell.shellConfig.idle : ({})
readonly property var idleConfig: shell && shell.shellConfig && shell.shellConfig.idle
? shell.shellConfig.idle : (shell && shell.idleConfig ? shell.idleConfig : ({}))
readonly property int screensaverTimeoutSeconds: secondsFromConfig(idleConfig.screensaver, defaultScreensaverSeconds)
readonly property int lockTimeoutSeconds: secondsFromConfig(idleConfig.lock, defaultLockSeconds)
readonly property int firstIdleTimeoutSeconds: Math.min(screensaverTimeoutSeconds, lockTimeoutSeconds)
+1
View File
@@ -15,6 +15,7 @@ QtObject {
property var appLibrary: null
property var bar: null
property var barConfig: ({})
property var idleConfig: ({})
property var _serviceLookup: null
property var _firstPartyServiceLookup: null
+31 -3
View File
@@ -355,6 +355,27 @@ ShellRoot {
return shell.manifestHasKind(manifest, "bar")
}
function publicIdleConfigFor(manifest) {
var metadata = manifest && Util.isPlainObject(manifest.omarchy) ? manifest.omarchy : null
if (!metadata || String(metadata.clonedFrom || "") !== "omarchy.idle") return ({})
var idle = shell.shellConfig && Util.isPlainObject(shell.shellConfig.idle)
? shell.shellConfig.idle : ({})
return JSON.parse(JSON.stringify(idle))
}
function pluginCloneMaySummon(manifest, requestedId) {
var metadata = manifest && Util.isPlainObject(manifest.omarchy) ? manifest.omarchy : null
var sourceId = metadata ? String(metadata.clonedFrom || "") : ""
var allowed = {
"omarchy.audio": ["omarchy.osd"],
"omarchy.media": ["omarchy.osd"],
"omarchy.monitor": ["omarchy.osd"],
"omarchy.network": ["omarchy.speedtest", "omarchy.wifiqr"]
}
var targets = allowed[sourceId] || []
return targets.indexOf(String(requestedId || "")) !== -1
}
function pluginOwnsTarget(pluginId, requestedId) {
var caller = String(pluginId || "")
if (!caller) return false
@@ -575,6 +596,7 @@ ShellRoot {
? shell.pluginAppLibraryFor(cacheKey, key) : null,
bar: shell.pluginBarStateFor(cacheKey, key),
barConfig: shell.publicBarConfig(),
idleConfig: shell.publicIdleConfigFor(manifest),
_serviceLookup: function(requestedId) {
return allowOwnService ? shell.pluginServiceFor(key, requestedId) : null
},
@@ -589,7 +611,8 @@ ShellRoot {
},
_summon: function(requestedId, payloadJson) {
if (!shell.pluginOwnsTarget(key, requestedId)
&& !shell.barPluginMayControl(currentManifest(), requestedId)) return false
&& !shell.barPluginMayControl(currentManifest(), requestedId)
&& !shell.pluginCloneMaySummon(currentManifest(), requestedId)) return false
return shell.summon(shell.pluginRegistry.resolveEnabledId(requestedId), payloadJson)
},
_hide: function(requestedId) {
@@ -828,8 +851,13 @@ ShellRoot {
widgetApi.widgets = shell.publicBarWidgetSnapshot()
widgetApi.revision = shell.barWidgetRegistry.revision
}
for (var shellKey in _pluginShellApis)
_pluginShellApis[shellKey].barConfig = shell.publicBarConfig()
for (var shellKey in _pluginShellApis) {
var shellApi = _pluginShellApis[shellKey]
var descriptor = _pluginShellApiDescriptors[shellKey]
var shellManifest = descriptor ? plugins[descriptor.pluginId] : null
shellApi.barConfig = shell.publicBarConfig()
shellApi.idleConfig = shell.publicIdleConfigFor(shellManifest)
}
for (var entryKey in _pluginBarEntryShellApis)
_pluginBarEntryShellApis[entryKey].barConfig = shell.publicBarConfig()
}
@@ -32,6 +32,7 @@ ShellRoot {
authStoreOwner.updateManifest("omarchy.lock", { version: "kept" })
var api = apiComponent.createObject(null, {
pluginId: caller,
idleConfig: { screensaver: 60, lock: 120 },
_serviceLookup: function(requestedId) {
return requestedId === caller ? root.ownService : null
},
@@ -74,6 +75,7 @@ ShellRoot {
foreignOpen: api.isPluginOpen("omarchy.lock") === false,
ownSettings: api.updateEntryInline(caller, {}) === true,
foreignSettings: api.updateEntryInline("omarchy.lock", {}) === false,
detachedIdleConfig: api.idleConfig.screensaver === 60 && api.idleConfig.lock === 120,
authStoreOwnerRetains: authStoreOwner.has("omarchy.lock") === true,
authStoreOwnerRemembersTrust: authStoreOwner.isTrusted("omarchy.lock") === true,
authStoreOwnerUpdatesManifest: root.ownService.manifest
+15
View File
@@ -21,6 +21,7 @@ trap cleanup EXIT
shell_qml="$ROOT/shell/shell.qml"
bar_qml="$ROOT/shell/plugins/bar/Bar.qml"
plugin_shell_api="$ROOT/shell/services/PluginShellApi.qml"
idle_service="$ROOT/shell/plugins/services/idle/Service.qml"
# Normalize horizontal and vertical whitespace so the wiring assertions survive
# harmless QML reflow. The runtime fixture below behaviorally covers
@@ -123,6 +124,20 @@ qml_matches "$shell_qml" 'return hasCurrentBarCapabilities\( *\) *\? *shell\.mut
fail "bar configuration mutation does not validate the current manifest"
pass "manifest changes revoke cached facade capabilities"
qml_matches "$shell_qml" 'idleConfig: *shell\.publicIdleConfigFor\( *manifest *\)' ||
fail "cloned idle services do not receive their configured timeouts"
qml_matches "$shell_qml" 'shellApi\.idleConfig *= *shell\.publicIdleConfigFor\( *shellManifest *\)' ||
fail "cloned idle service configuration does not refresh"
qml_matches "$idle_service" 'shell *&& *shell\.idleConfig *\? *shell\.idleConfig *: *\(\{\}\)' ||
fail "the idle service does not consume its scoped configuration"
qml_matches "$shell_qml" 'shell\.pluginCloneMaySummon\( *currentManifest\( *\), *requestedId *\)' ||
fail "built-in clones cannot summon their existing auxiliary UI"
qml_matches "$shell_qml" '"omarchy\.media": *\["omarchy\.osd"\]' ||
fail "media clones cannot summon their existing OSD target"
qml_matches "$shell_qml" '"omarchy\.network": *\["omarchy\.speedtest", *"omarchy\.wifiqr"\]' ||
fail "network clones cannot summon their existing auxiliary panels"
pass "built-in service and widget clones retain narrow configuration and UI integration"
qml_matches "$shell_qml" 'shell\.serviceFor\( *shell\.pluginRegistry\.resolveEnabledId\( *id *\) *\)' ||
fail "narrow first-party service proxies do not resolve enabled clones"
qml_matches "$shell_qml" 'return serviceFor\( *shell\.pluginRegistry\.resolveEnabledId\( *pluginId *\) *\)' ||
+9
View File
@@ -159,10 +159,12 @@ import QtQuick
import Quickshell.Io
Item {
id: root
property string marker: "clone-service"
property bool enabled: true
property var activePlayer: null
property var sourcePlayers: []
property var shell: null
function runAction(action, showFeedback, targetKey) {}
function playerKey(player) { return "" }
@@ -171,6 +173,9 @@ Item {
IpcHandler {
target: "acme-media-clone-service"
function ping(): string { return marker }
function summonOsd(): string {
return root.shell && root.shell.summon("omarchy.osd", "{}") ? "true" : "false"
}
}
}
QML
@@ -634,6 +639,10 @@ jq -e '.reachable == true and .marker == "clone-service"' \
}
pass "trusted bar gives a cloned widget its own companion service"
[[ $(shell_ipc acme-media-clone-service summonOsd) == "true" ]] ||
fail_with_log "a cloned media service cannot summon its existing OSD target"
pass "a cloned built-in service retains its auxiliary UI integration"
[[ $(shell_ipc shell setPluginEnabled "$victim_service_id" true) == "ok" ]] ||
fail_with_log "victim service fixture could not be enabled"
[[ $(shell_ipc shell enablePlugin "$review_bar_id" '{}') == "ok" ]] ||