Merge pull request #9618 from acrogenesis/security/plugin-auth-boundary

Restrict third-party plugin access to authentication services

(cherry picked from commit e78d89ee2a)
This commit is contained in:
Ryan Hughes committed 2026-09-07 18:36:02 -04:00
1 parent af6f64fa3a
commit b1cdec9e47
27 files changed
+2020 -57

No files matched your search

@@ -0,0 +1,20 @@
import QtQuick
import "services/AuthServiceStore.js" as AuthServiceStore
QtObject {
function retain(id, service) {
AuthServiceStore.put(id, service)
}
function has(id) {
return AuthServiceStore.has(id)
}
function isTrusted(id) {
return AuthServiceStore.isTrusted(id)
}
function updateManifest(id, manifest) {
AuthServiceStore.updateManifest(id, manifest)
}
}
@@ -0,0 +1,8 @@
import QtQuick
import "services/AuthServiceStore.js" as AuthServiceStore
QtObject {
function has(id) {
return AuthServiceStore.has(id)
}
}
@@ -0,0 +1,92 @@
import QtQuick
import Quickshell
import Quickshell.Io
import "services"
ShellRoot {
id: root
property var calls: []
property QtObject ownService: QtObject {
property string marker: "own"
property var manifest: null
}
AuthStoreOwner { id: authStoreOwner }
AuthStoreReader { id: authStoreReader }
Component {
id: apiComponent
PluginShellApi { }
}
FileView {
id: resultFile
path: Quickshell.env("OMARCHY_QML_TEST_RESULT")
atomicWrites: true
}
Component.onCompleted: {
var caller = "example.safe"
authStoreOwner.retain("omarchy.lock", root.ownService)
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
},
_summon: function(requestedId) {
if (requestedId !== caller) return false
root.calls = root.calls.concat(["summon"])
return true
},
_hide: function(requestedId) {
if (requestedId !== caller) return false
root.calls = root.calls.concat(["hide"])
return true
},
_toggle: function(requestedId) {
if (requestedId !== caller) return false
root.calls = root.calls.concat(["toggle"])
return true
},
_isOpen: function(requestedId) { return requestedId === caller },
_updateSettings: function(requestedId) {
if (requestedId !== caller) return false
root.calls = root.calls.concat(["settings"])
return true
}
})
var own = api.serviceFor(caller)
var result = {
detached: api.parent === undefined || api.parent === null,
ownService: own && own.marker === "own",
foreignService: api.serviceFor("omarchy.lock") === null,
firstPartyService: api.firstPartyServiceFor("omarchy.polkit") === null,
ownSummon: api.summon(caller, "{}") === true,
foreignSummon: api.summon("omarchy.lock", "{}") === false,
ownHide: api.hide(caller) === true,
foreignHide: api.hide("omarchy.lock") === false,
ownToggle: api.toggle(caller, "{}") === true,
foreignToggle: api.toggle("omarchy.lock", "{}") === false,
ownOpen: api.isPluginOpen(caller) === true,
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
&& root.ownService.manifest.version === "kept",
authStoreImportIsolated: authStoreReader.has("omarchy.lock") === false,
noGenericPluginShellFactory: typeof api.pluginShellForId !== "function",
calls: root.calls
}
result.ok = Object.keys(result).every(function(key) {
return key === "ok" || key === "calls" || result[key] === true
}) && JSON.stringify(result.calls) === JSON.stringify(["summon", "hide", "toggle", "settings"])
resultFile.setText(JSON.stringify(result))
}
}
@@ -83,6 +83,9 @@ ShellRoot {
scan += block("firstparty", "/first/bar", manifest("omarchy.bar", ["bar"], { bar: "Bar.qml" }))
scan += block("firstparty", "/first/panels/grouped", manifest("omarchy.grouped-panel", ["panel"], { panel: "Panel.qml" }))
scan += block("firstparty", "/first/hybrid", manifest("omarchy.hybrid", ["menu", "bar-widget"], { menu: "Menu.qml", barWidget: "Widget.qml" }))
var futureAuth = manifest("omarchy.future-auth", ["service"], { service: "Service.qml" })
futureAuth.omarchy = { capabilities: ["authentication"] }
scan += block("firstparty", "/first/future-auth", futureAuth)
scan += block("thirdparty", "/third/panel", manifest("third.panel", ["panel"], { panel: "Panel.qml" }))
scan += block("thirdparty", "/third/widget", manifest("third.widget", ["bar-widget"], { barWidget: "Widget.qml" }, { defaultSection: "left" }))
scan += block("thirdparty", "/third/center-widget", manifest("third.center-widget", ["bar-widget"], { barWidget: "Widget.qml" }))
@@ -103,6 +106,12 @@ ShellRoot {
localBar.omarchy = { clonedFrom: "omarchy.bar" }
scan += block("thirdparty", "/third/local-bar", localBar)
scan += block("thirdparty", "/third/bar", manifest("third.bar", ["bar"], { bar: "Bar.qml" }))
var localFutureAuth = manifest("local.future-auth", ["service"], { service: "Service.qml" })
localFutureAuth.omarchy = { clonedFrom: "omarchy.future-auth" }
scan += block("thirdparty", "/third/local-future-auth", localFutureAuth)
var spoofedAuth = manifest("third.spoofed-auth", ["service"], { service: "Service.qml" })
spoofedAuth.omarchy = { capabilities: ["authentication"] }
scan += block("thirdparty", "/third/spoofed-auth", spoofedAuth)
scan += block("thirdparty", "/third/shadow", manifest("omarchy.first-widget", ["panel"], { panel: "Panel.qml" }))
scan += block("thirdparty", "/third/reserved", manifest("omarchy.reserved", ["panel"], { panel: "Panel.qml" }))
scan += block("thirdparty", "/third/unsafe", manifest("third.unsafe", ["panel"], { panel: "../Panel.qml" }))
@@ -116,22 +125,28 @@ ShellRoot {
root.assertDeepEqual(pluginIds(), [
"local.bar",
"local.first-widget",
"local.future-auth",
"local.grouped-panel",
"local.hybrid",
"local.weather",
"omarchy.bar",
"omarchy.first-widget",
"omarchy.future-auth",
"omarchy.grouped-panel",
"omarchy.hybrid",
"third.bar",
"third.center-widget",
"third.panel",
"third.right-widget",
"third.spoofed-auth",
"third.widget"
], "registry merges valid first-party and third-party manifests")
root.assertTrue(registry.installedPlugins["omarchy.first-widget"].__isFirstParty === true, "first-party manifests are stamped")
root.assertTrue(registry.installedPlugins["third.panel"].__isFirstParty === false, "third-party manifests are stamped")
root.assertDeepEqual(registry.installedPlugins["omarchy.future-auth"].__hostCapabilities, ["authentication"], "trusted manifests stamp authentication capability")
root.assertDeepEqual(registry.installedPlugins["local.future-auth"].__hostCapabilities, ["authentication"], "clones inherit trusted host capabilities")
root.assertDeepEqual(registry.installedPlugins["third.spoofed-auth"].__hostCapabilities, [], "third-party manifests cannot self-grant host capabilities")
root.assertEqual(registry.installedPlugins["omarchy.grouped-panel"].__sourceDir, "/first/panels/grouped", "grouped plugin source paths are preserved")
root.assertEqual(registry.entryPointUrl(registry.installedPlugins["third.panel"], "panel"), "file:///third/panel/Panel.qml", "entryPointUrl resolves plugin-relative paths")
root.assertEqual(registry.entryPointUrl(registry.installedPlugins["third.widget"], "barWidget"), "file:///third/widget/Widget.qml", "entryPointUrl resolves bar widget paths")
+201
View File
@@ -0,0 +1,201 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
TMPDIR=""
QS_PID=""
cleanup() {
if [[ -n $QS_PID ]] && kill -0 "$QS_PID" 2>/dev/null; then
kill "$QS_PID" 2>/dev/null || true
wait "$QS_PID" 2>/dev/null || true
fi
if [[ -n $TMPDIR && -d $TMPDIR ]]; then
rm -rf "$TMPDIR"
fi
}
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
# PluginShellApi and AuthServiceStore; these checks remain the guard for their
# integration through shell.qml and Bar.qml, including without a compositor.
qml_matches() {
local file=$1
local pattern=$2
tr '\n\r\t' ' ' < "$file" | grep -Eq "$pattern"
}
qml_matches "$shell_qml" 'comp\.createObject\( *manifest\.__isFirstParty *&& *!authenticationService *\? *serviceHost *: *null *\)' ||
fail "third-party and authentication services are detached from the host object tree"
qml_matches "$shell_qml" 'AuthServiceStore\.put\( *key, *inst *\)' ||
fail "authentication services are retained outside the host service map"
qml_matches "$shell_qml" 'AuthServiceStore\.isTrusted\( *key *\)' ||
fail "live authentication classification survives public manifest mutation"
qml_matches "$shell_qml" 'AuthServiceStore\.updateManifest\( *id, *shell\.publicPluginManifest\( *m *\) *\)' ||
fail "kept authentication services receive only a public manifest snapshot"
qml_matches "$shell_qml" 'if *\( *!serviceKeepLoaded\( *authenticationId *\) *\) *AuthServiceStore\.destroy\( *authenticationId *\)' ||
fail "keepLoaded authentication services survive plugin rescans"
pass "third-party and authentication services are detached from the host object tree"
run_node_test <<'JS'
const fs = require('fs')
const vm = require('vm')
const store = {}
vm.createContext(store)
vm.runInContext(
fs.readFileSync(path.join(root, 'shell/services/AuthServiceStore.js'), 'utf8'),
store
)
const service = { destroy() {} }
store.put('omarchy.lock', service)
store.destroy('omarchy.lock')
assert(
!store.has('omarchy.lock') && store.isTrusted('omarchy.lock'),
'authentication classification survives service teardown'
)
JS
qml_matches "$shell_qml" 'inst\.shell *= *shell\.pluginShellFor\( *manifest *\)' ||
fail "service plugins receive a scoped shell facade"
qml_matches "$shell_qml" 'item\.shell *= *shell\.pluginShellFor\( *panelEntry\.manifest *\)' ||
fail "panel plugins receive a scoped shell facade"
qml_matches "$shell_qml" 'target\.shell *= *shell\.pluginShellFor\( *manifest *\)' ||
fail "full-bar plugins receive a scoped shell facade"
pass "third-party entry points receive scoped shell facades"
if qml_matches "$plugin_shell_api" 'function +pluginShellForId\('; then
fail "replacement-bar facade exposes a generic plugin-shell factory"
fi
qml_matches "$bar_qml" 'else if *\( *root\.shell *&& *typeof root\.shell\.pluginShellForBarEntry *=== *"function" *\) *\{[^}]*pluginShell *= *root\.shell\.pluginShellForBarEntry\( *key, *moduleName *\)' ||
fail "replacement bars do not fall back to a service-less entry facade"
pass "replacement bars cannot manufacture another plugin's service facade"
qml_matches "$shell_qml" 'target\.barConfig *= *shell\.barConfigFor\( *manifest *\)' ||
fail "initial replacement-bar configuration is not detached"
qml_matches "$shell_qml" 'bar\.barConfig *= *shell\.barConfigFor\( *shell\.activeBarManifest *\)' ||
fail "replacement-bar configuration updates are not detached"
pass "replacement bars receive detached configuration snapshots"
qml_matches "$bar_qml" 'target\.bar *= *firstParty *\? *root *: *root\.pluginBarApiFor\( *pluginApiId, *moduleName, *registered *\)' ||
fail "third-party widgets receive a bar facade instead of the host bar"
qml_matches "$bar_qml" 'api\.clickTargets *= *root\.pluginClickTargets\( *api\.pluginId *\)' ||
fail "third-party bar facades exclude other widgets from their object graph"
pass "third-party widgets receive a bar facade instead of the host bar"
qml_matches "$shell_qml" 'widgets: *shell\.publicBarWidgetSnapshot\( *\)' ||
fail "third-party widget registries receive detached snapshots"
qml_matches "$bar_qml" 'root\.markPluginObject\( *pluginId, *target, *"clickTarget" *\)' ||
fail "third-party bar-object ownership is stamped by the host callback"
qml_matches "$bar_qml" 'root\.markPluginObject\( *pluginId, *owner, *"popout" *\)' ||
fail "owner-less popouts receive trusted ownership before activation"
qml_matches "$shell_qml" 'manifest\.__hostCapabilities\.indexOf\( *"authentication" *\)' ||
fail "authentication isolation follows host-stamped capabilities"
pass "registry mutation and ownership boundaries are host-controlled"
qml_matches "$bar_qml" 'root\.moduleWidgets\( *moduleName *\)' ||
fail "custom bar module widget lookups use their real module name"
qml_matches "$shell_qml" 'shell\.pluginShellForBarEntry\( *cacheKey *\+ *":" *\+ *ownerId, *moduleName *\)' ||
fail "full-bar plugins receive a scoped settings facade for custom modules"
pass "custom bar modules retain settings and popout identity"
if qml_matches "$bar_qml" 'on(Foreground|BarForeground|Background|Urgent|FontFamily|Vertical|BarSize|Transparent)Changed: *sync'; then
fail "animated scalar properties still trigger full facade resyncs"
fi
qml_matches "$bar_qml" 'api\.foreground *= *Qt\.binding\( *function\( *\) *\{ *return root\.foreground *\} *\)' ||
fail "third-party bar scalar mirrors use bindings"
qml_matches "$shell_qml" 'shell\.prunePluginApis\( *\)' ||
fail "disabled plugin facade caches are pruned"
pass "plugin facade synchronization is bounded"
qml_matches "$shell_qml" 'descriptor\.profile *!== *expectedProfile[^}]*shell\.revokePluginShellApi\( *shellKey *\)' ||
fail "manifest capability changes do not revoke cached plugin facades"
qml_matches "$shell_qml" 'shell\.barPluginMayControl\( *currentManifest\( *\), *requestedId *\)' ||
fail "bar lifecycle callbacks do not validate the current manifest"
qml_matches "$shell_qml" 'return hasCurrentBarCapabilities\( *\) *\? *shell\.mutatePluginBarConfig\( *mutator *\) *: *false' ||
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"
bar_entry_shell=$(sed -n '/^ function pluginShellForBarEntry(/,/^ function pluginShellFor(/p' "$shell_qml")
tr '\n\r\t' ' ' <<<"$bar_entry_shell" |
grep -Eq 'var id *= *shell\.pluginRegistry\.resolveEnabledId\( *target *\)[^}]*return shell\.pluginRegistry\.installedPlugins\[id\] *\|\| *null' ||
fail "replacement-bar clone authorization does not follow the enabled implementation"
tr '\n\r\t' ' ' <<<"$bar_entry_shell" |
grep -Eq 'shell\.pluginCloneMaySummon\( *currentManifest\( *\), *requestedId *\)' ||
fail "built-in clones in replacement bars cannot summon their existing auxiliary UI"
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 *\) *\)' ||
fail "trusted first-party service lookups do not resolve enabled clones"
qml_matches "$shell_qml" 'allowOwnService *&& *shell\.pluginOwnsTarget\( *key, *requestedId *\)[^}]*return shell\.pluginServiceFor\( *key, *requestedId *\)' ||
fail "cloned widgets cannot use a source id to reach their own service"
pass "service facades resolve enabled clones without widening replacement-bar access"
require_compositor "plugin authentication boundary runtime test"
if ! command -v quickshell >/dev/null 2>&1; then
pass "quickshell not installed; skipping plugin authentication boundary runtime test"
exit 0
fi
require_command jq
TMPDIR=$(mktemp -d)
result="$TMPDIR/result.json"
log="$TMPDIR/quickshell.log"
config_dir="$TMPDIR/plugin-auth-boundary"
mkdir -p "$config_dir" "$TMPDIR/home"
cp "$SHELL_TEST_DIR/fixtures/plugin-auth-boundary/"*.qml "$config_dir/"
ln -s "$ROOT/shell/services" "$config_dir/services"
OMARCHY_QML_TEST_RESULT="$result" \
HOME="$TMPDIR/home" \
XDG_CONFIG_HOME="$TMPDIR/home/.config" \
XDG_CACHE_HOME="$TMPDIR/home/.cache" \
XDG_STATE_HOME="$TMPDIR/home/.local/state" \
quickshell -p "$config_dir" --no-color >"$log" 2>&1 &
QS_PID=$!
for _ in {1..80}; do
[[ -s $result ]] && break
if ! kill -0 "$QS_PID" 2>/dev/null; then
sed -n '1,220p' "$log" >&2
fail "plugin authentication boundary fixture exited before writing result"
fi
sleep 0.1
done
[[ -s $result ]] || {
sed -n '1,220p' "$log" >&2
fail "plugin authentication boundary runtime test timed out"
}
if ! jq -e '.ok == true' "$result" >/dev/null; then
jq . "$result" >&2
sed -n '1,220p' "$log" >&2
fail "plugin authentication boundary runtime behavior"
fi
pass "plugin authentication boundary runtime behavior"
+313
View File
@@ -112,6 +112,200 @@ Item {
}
QML
# A replacement bar must not receive a generic factory for another plugin's
# live service, and its barConfig must be a detached snapshot on both initial
# injection and later host-config updates.
victim_service_id="acme.victim-service"
victim_service_dir="$test_home/.config/omarchy/plugins/$victim_service_id"
mkdir -p "$victim_service_dir"
cat >"$victim_service_dir/manifest.json" <<JSON
{
"schemaVersion": 1,
"id": "$victim_service_id",
"name": "Victim Service",
"version": "1.0.0",
"kinds": ["service"],
"entryPoints": {"service": "Service.qml"}
}
JSON
cat >"$victim_service_dir/Service.qml" <<'QML'
import QtQuick
Item {
property string privateValue: "victim-secret"
}
QML
# A clone of the built-in media service exercises both supported service paths:
# its own widget receives the raw companion service under the trusted bar, while
# a replacement bar receives only the narrow media proxy resolved to the clone.
media_clone_id="acme.media-clone"
media_clone_dir="$test_home/.config/omarchy/plugins/$media_clone_id"
mkdir -p "$media_clone_dir"
cat >"$media_clone_dir/manifest.json" <<JSON
{
"schemaVersion": 1,
"id": "$media_clone_id",
"name": "Media Clone",
"version": "1.0.0",
"kinds": ["service", "bar-widget"],
"entryPoints": {"service": "Service.qml", "barWidget": "BarWidget.qml"},
"barWidget": {"defaultSection": "center"},
"omarchy": {"clonedFrom": "omarchy.media"}
}
JSON
cat >"$media_clone_dir/Service.qml" <<'QML'
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 "" }
function selectPlayer(playerKey) {}
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
cat >"$media_clone_dir/BarWidget.qml" <<'QML'
import QtQuick
import Quickshell.Io
Item {
id: root
property var bar: null
IpcHandler {
target: "acme-media-clone-widget"
function probeOwnService(): string {
var service = root.bar && root.bar.shell
? root.bar.shell.firstPartyServiceFor("omarchy.media") : null
return JSON.stringify({
reachable: !!service,
marker: service ? String(service.marker || "") : ""
})
}
}
}
QML
review_bar_id="acme.review-bar"
review_bar_dir="$test_home/.config/omarchy/plugins/$review_bar_id"
mkdir -p "$review_bar_dir"
cat >"$review_bar_dir/manifest.json" <<JSON
{
"schemaVersion": 1,
"id": "$review_bar_id",
"name": "Review Bar",
"version": "1.0.0",
"kinds": ["bar", "service"],
"keepLoaded": true,
"entryPoints": {"bar": "Bar.qml", "service": "Service.qml"}
}
JSON
cat >"$review_bar_dir/Bar.qml" <<'QML'
import QtQuick
import Quickshell.Io
Item {
id: root
property var shell: null
property var barConfig: ({})
IpcHandler {
target: "acme-review-bar"
function probeVictim(): string {
var genericFactory = root.shell
&& typeof root.shell.pluginShellForId === "function"
var entryFacade = root.shell
&& typeof root.shell.pluginShellForBarEntry === "function"
? root.shell.pluginShellForBarEntry("probe", "acme.victim-service") : null
var victim = entryFacade && typeof entryFacade.serviceFor === "function"
? entryFacade.serviceFor("acme.victim-service") : null
return JSON.stringify({
genericFactory: !!genericFactory,
entryFacade: !!entryFacade,
victimServiceReachable: !!victim
})
}
function snapshot(): string {
return JSON.stringify(root.barConfig || {})
}
function probeMediaProxy(): string {
var service = root.shell
? root.shell.firstPartyServiceFor("omarchy.media") : null
return JSON.stringify({ reachable: !!service, enabled: service ? service.enabled === true : false })
}
function probeMediaWidgetSummon(): string {
var entryFacade = root.shell
&& typeof root.shell.pluginShellForBarEntry === "function"
? root.shell.pluginShellForBarEntry("probe-media", "acme.media-clone") : null
return JSON.stringify({
entryFacade: !!entryFacade,
osdSummoned: entryFacade ? entryFacade.summon("omarchy.osd", "{}") : false,
foreignSummoned: entryFacade ? entryFacade.summon("omarchy.lock", "{}") : false
})
}
function mutateSnapshot(): string {
if (root.barConfig && root.barConfig.layout
&& root.barConfig.layout.left && root.barConfig.layout.left.length > 0)
root.barConfig.layout.left[0].id = "tampered.by.review-bar"
return snapshot()
}
}
}
QML
cat >"$review_bar_dir/Service.qml" <<'QML'
import QtQuick
import Quickshell.Io
Item {
id: root
property var shell: null
property var retainedShell: null
onShellChanged: if (!retainedShell && shell) retainedShell = shell
function mutationAllowed(candidate) {
if (!candidate) return false
try {
return typeof candidate.mutateShellConfig === "function"
&& candidate.mutateShellConfig(function(config) {}) === true
} catch (e) {
return false
}
}
IpcHandler {
target: "acme-review-capability"
function probe(): string {
return JSON.stringify({
currentAllowed: root.mutationAllowed(root.shell),
retainedAllowed: root.mutationAllowed(root.retainedShell)
})
}
}
}
QML
cat >"$stub_bin/omarchy-update-available" <<'SH'
#!/bin/bash
echo "Omarchy update available (test)"
@@ -434,3 +628,122 @@ jq -e 'all(.bar.layout.right[]; (.id // .) != "omarchy.keyboard-layout")' \
<<<"$(shell_ipc shell listShellConfig)" >/dev/null ||
fail_with_log "bar put added a second copy of a widget already on the bar"
pass "bar put leaves a widget already on the bar alone"
# Run the replacement-bar probes last: switching bar loaders can transiently
# leave bar-aware panels without a visual host, which should not add noise to
# the default-bar assertions above.
[[ $(shell_ipc shell setPluginEnabled "$media_clone_id" true) == "ok" ]] ||
fail_with_log "media clone fixture could not be enabled"
clone_widget_probe=""
for _ in {1..80}; do
clone_widget_probe=$(shell_ipc acme-media-clone-widget probeOwnService 2>/dev/null || true)
if jq -e '.reachable == true and .marker == "clone-service"' \
<<<"$clone_widget_probe" >/dev/null 2>&1; then
break
fi
sleep 0.1
done
jq -e '.reachable == true and .marker == "clone-service"' \
<<<"$clone_widget_probe" >/dev/null || {
printf 'Clone own-service probe: %s\n' "$clone_widget_probe" >&2
fail_with_log "a cloned widget resolves its source id to its own companion 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" ]] ||
fail_with_log "replacement-bar fixture could not be enabled"
review_probe=""
for _ in {1..80}; do
review_probe=$(shell_ipc acme-review-bar probeVictim 2>/dev/null || true)
if jq -e '.genericFactory == false and .entryFacade == false and .victimServiceReachable == false' \
<<<"$review_probe" >/dev/null 2>&1; then
break
fi
if ! kill -0 "$QS_PID" 2>/dev/null; then
fail_with_log "test shell exited while loading the replacement-bar fixture"
fi
sleep 0.1
done
jq -e '.genericFactory == false and .entryFacade == false and .victimServiceReachable == false' \
<<<"$review_probe" >/dev/null || {
printf 'Replacement-bar service probe: %s\n' "$review_probe" >&2
fail_with_log "replacement bar cannot recover another plugin's live service"
}
media_proxy_probe=$(shell_ipc acme-review-bar probeMediaProxy)
jq -e '.reachable == true and .enabled == true' <<<"$media_proxy_probe" >/dev/null || {
printf 'Replacement-bar media proxy probe: %s\n' "$media_proxy_probe" >&2
fail_with_log "replacement-bar service proxies resolve enabled clones"
}
media_summon_probe=$(shell_ipc acme-review-bar probeMediaWidgetSummon)
jq -e '.entryFacade == true and .osdSummoned == true and .foreignSummoned == false' \
<<<"$media_summon_probe" >/dev/null || {
printf 'Replacement-bar media summon probe: %s\n' "$media_summon_probe" >&2
fail_with_log "replacement-bar clone facades retain only their auxiliary UI integration"
}
bar_config_before=$(shell_ipc shell listShellConfig | jq -c '.bar')
shell_ipc acme-review-bar mutateSnapshot >/dev/null
bar_config_after=$(shell_ipc shell listShellConfig | jq -c '.bar')
[[ $bar_config_after == "$bar_config_before" ]] ||
fail_with_log "replacement bar mutated the initially injected host configuration"
[[ $(shell_ipc shell setBarWidget omarchy.clock format '"HH:mm:ss"' '{}') == "ok" ]] ||
fail_with_log "host bar configuration could not be updated for snapshot testing"
updated_snapshot=""
for _ in {1..80}; do
updated_snapshot=$(shell_ipc acme-review-bar snapshot 2>/dev/null || true)
if jq -e 'any(.layout.center[]; (.id // .) == "omarchy.clock" and .format == "HH:mm:ss")' \
<<<"$updated_snapshot" >/dev/null 2>&1; then
break
fi
sleep 0.1
done
jq -e 'any(.layout.center[]; (.id // .) == "omarchy.clock" and .format == "HH:mm:ss")' \
<<<"$updated_snapshot" >/dev/null ||
fail_with_log "replacement bar did not receive the refreshed configuration snapshot"
bar_config_before=$(shell_ipc shell listShellConfig | jq -c '.bar')
shell_ipc acme-review-bar mutateSnapshot >/dev/null
bar_config_after=$(shell_ipc shell listShellConfig | jq -c '.bar')
[[ $bar_config_after == "$bar_config_before" ]] ||
fail_with_log "replacement bar mutated a refreshed host configuration"
pass "replacement-bar service and configuration boundaries hold at runtime"
capability_before=$(shell_ipc acme-review-capability probe)
jq -e '.currentAllowed == true and .retainedAllowed == true' \
<<<"$capability_before" >/dev/null ||
fail_with_log "bar service fixture did not initially receive bar capabilities"
# Keep the same enabled plugin ID and service instance while dropping the bar
# kind. Both the currently injected facade and a reference retained by the
# plugin must lose the old configuration capability after the manifest rescan.
jq '.kinds = ["service"] | .entryPoints = {"service": "Service.qml"}' \
"$review_bar_dir/manifest.json" >"$review_bar_dir/manifest.json.tmp"
mv "$review_bar_dir/manifest.json.tmp" "$review_bar_dir/manifest.json"
capability_after=""
for _ in {1..80}; do
capability_after=$(shell_ipc acme-review-capability probe 2>/dev/null || true)
if jq -e '.currentAllowed == false and .retainedAllowed == false' \
<<<"$capability_after" >/dev/null 2>&1; then
break
fi
if ! kill -0 "$QS_PID" 2>/dev/null; then
fail_with_log "test shell exited while revoking changed manifest capabilities"
fi
sleep 0.1
done
jq -e '.currentAllowed == false and .retainedAllowed == false' \
<<<"$capability_after" >/dev/null || {
printf 'Capability revocation probe: %s\n' "$capability_after" >&2
fail_with_log "cached plugin facades revoke capabilities removed from the manifest"
}
pass "manifest reload revokes cached facade capabilities"