Preserve built-in clone integrations
Co-Authored-By: GPT-5.6-Sol <noreply@openai.com>
This commit is contained in:
1 parent
f2b4731bb3
commit
3292c19fef
10 files changed
+64
-8
No files matched your search
@@ -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`.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 *\) *\)' ||
|
||||
|
||||
@@ -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" ]] ||
|
||||
|
||||
Reference in new issue
Block a user