Tighten replacement bar plugin boundaries

Reported-by: Roger Piñol <rogerpicar@gmail.com>
This commit is contained in:
acrogenesis committed 2026-09-02 11:03:03 -06:00
1 parent 19985314c2
commit 094c913065
12 files changed
+206 -24

No files matched your search

@@ -78,6 +78,7 @@ ShellRoot {
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) {
+14
View File
@@ -20,6 +20,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"
# Normalize horizontal and vertical whitespace so the wiring assertions survive
# harmless QML reflow. The runtime fixture below behaviorally covers
@@ -50,6 +51,19 @@ 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 *\)' ||
+133
View File
@@ -112,6 +112,85 @@ 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
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"],
"entryPoints": {"bar": "Bar.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 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 >"$stub_bin/omarchy-update-available" <<'SH'
#!/bin/bash
echo "Omarchy update available (test)"
@@ -434,3 +513,57 @@ 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 "$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"
}
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"