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

+8 -8
View File
@@ -93,6 +93,10 @@ 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, 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.
The full schema lives in `services/PluginRegistry.qml`.
## Installing a third-party plugin
@@ -108,15 +112,11 @@ omarchy plugin update # updates every git-managed plugin
omarchy plugin remove acme.weather
```
> ⚠️ **Plugins run as unsandboxed code inside `omarchy-shell`.** Adding warns
> you before cloning, plugins land disabled so you can review the code before
> enabling, and updates show a diff of the changes before touching anything.
> Only add repos whose code you are willing to run.
> ⚠️ **Plugins run as unsandboxed code inside `omarchy-shell`.** Adding warns you before cloning, plugins land disabled so you can review the code before enabling, and updates show a diff of the changes before touching anything. The scoped QML interfaces remove direct authentication-service and generic replacement-bar service lookups, but visual plugins still share and can traverse the ordinary host scene. Only add repos whose code you are willing to run.
Each command is **interactive** when run bare in a terminal (gum pickers,
confirmation, a diff to review) and fully **non-interactive** when given
arguments. Pass `--yes` to skip every prompt — this is the path for scripts and
AI agents:
Add, update, and remove commands confirm in a terminal even when given
arguments; without a terminal they refuse rather than guess. Pass `--yes` to
skip every prompt — this is the path for scripts and AI agents:
```bash
omarchy plugin add https://github.com/acme/omarchy-weather.git --enable --yes
+87
View File
@@ -0,0 +1,87 @@
import QtQuick
// Bar surface exposed to an installed third-party widget. Scalar presentation
// state is mirrored by Bar.qml and operations are delegated through scoped
// callbacks. The facade avoids direct host-Bar injection; it cannot isolate a
// visual child from the parent hierarchy of the QML scene that renders it.
QtObject {
id: api
required property string pluginId
required property string moduleName
property var shell: null
property color foreground: "transparent"
property color barForeground: "transparent"
property color background: "transparent"
property color urgent: "transparent"
property string fontFamily: ""
property string position: "top"
property bool vertical: false
property int barSize: 0
property bool transparent: false
property bool foregroundAnimationEnabled: true
property bool centerSectionRevealHeld: false
property bool _centerHoverRevealSuppressed: false
readonly property bool centerHoverRevealSuppressed: _centerHoverRevealSuppressed
property var activePopout: null
property var clickTargets: []
property var layoutConfig: ({})
readonly property var foreignPopoutMarker: ({ foreign: true })
property var _showTooltip: null
property var _hideTooltip: null
property var _registerClickTarget: null
property var _unregisterClickTarget: null
property var _requestPopout: null
property var _releasePopout: null
property var _switchPanelFrom: null
property var _targetBelongsToWindow: null
property var _moduleWidgets: null
property var _run: null
property var _setCenterHoverRevealSuppressed: null
function setCenterHoverRevealSuppressed(value) {
if (_setCenterHoverRevealSuppressed) _setCenterHoverRevealSuppressed(!!value)
}
function showTooltip(target, text) {
if (_showTooltip) _showTooltip(target, String(text || ""))
}
function hideTooltip(target) {
if (_hideTooltip) _hideTooltip(target)
}
function registerClickTarget(target) {
if (_registerClickTarget) _registerClickTarget(target)
}
function unregisterClickTarget(target) {
if (_unregisterClickTarget) _unregisterClickTarget(target)
}
function requestPopout(owner) {
if (_requestPopout) _requestPopout(owner)
}
function releasePopout(owner) {
if (_releasePopout) _releasePopout(owner)
}
function switchPanelFrom(owner, direction) {
return _switchPanelFrom ? _switchPanelFrom(owner, direction) : false
}
function targetBelongsToWindow(target, window) {
return _targetBelongsToWindow ? _targetBelongsToWindow(target, window) : false
}
function moduleWidgets(id) {
return _moduleWidgets ? _moduleWidgets(String(id || "")) : []
}
function run(command) {
if (_run) _run(String(command || ""))
}
}
+1
View File
@@ -23,6 +23,7 @@ PanelSectionHeader 1.0 PanelSectionHeader.qml
PanelSeparator 1.0 PanelSeparator.qml
PanelSlider 1.0 PanelSlider.qml
PanelToolTip 1.0 PanelToolTip.qml
PluginBarApi 1.0 PluginBarApi.qml
PointerMoveGate 1.0 PointerMoveGate.qml
ScreenMoveRemap 1.0 ScreenMoveRemap.qml
PopupCard 1.0 PopupCard.qml
+238 -4
View File
@@ -12,20 +12,29 @@ Item {
id: root
// The omarchy-shell host injects omarchyPath from OMARCHY_PATH.
required property string omarchyPath
property string omarchyPath: Quickshell.env("OMARCHY_PATH")
// Injected by the host shell so bar slots can resolve enabled widgets.
required property var barWidgetRegistry
property var barWidgetRegistry: fallbackBarWidgetRegistry
// Read-only registry view for third-party full bars; the built-in bar does
// not otherwise need it, but declaring it keeps clone construction atomic.
property var pluginRegistry: null
// Injected by the host shell every time shell.json is reloaded. Holds the
// `bar:` subtree: position, centerAnchor, layout. The host owns file IO;
// the bar just renders whatever it's handed. The bar font follows the
// OS-level fontconfig monospace binding — it is not stored in shell.json.
required property var barConfig
property var barConfig: ({})
// Injected by the host shell. Used for shell-wide actions such as opening
// settings and persisting inline widget state.
property var shell: null
// Manifest for the active bar option. Present for custom bars and useful for
// diagnostics; the built-in bar does not otherwise need it.
property var manifest: null
QtObject {
id: fallbackBarWidgetRegistry
property var widgets: ({})
property int revision: 0
function metadataFor(id) { return null }
}
// Mirrors the on-disk `bar-off` flag so the user can hide the bar without
// killing the entire shell. Hidden panels stay mapped but park off-screen
// without an exclusion zone; updated by the FileView watcher further down.
@@ -100,6 +109,223 @@ Item {
property var barMoveScreen: null
property var clickTargets: []
property var moduleSlots: []
property var pluginBarApis: ({})
property var pluginObjectOwners: []
Component {
id: pluginBarApiComponent
PluginBarApi { }
}
function publicLayoutConfig() {
return JSON.parse(JSON.stringify(root.layoutConfig || {}))
}
function bindPluginBarApi(api) {
if (!api) return
api.foreground = Qt.binding(function() { return root.foreground })
api.barForeground = Qt.binding(function() { return root.barForeground })
api.background = Qt.binding(function() { return root.background })
api.urgent = Qt.binding(function() { return root.urgent })
api.fontFamily = Qt.binding(function() { return root.fontFamily })
api.position = Qt.binding(function() { return root.position })
api.vertical = Qt.binding(function() { return root.vertical })
api.barSize = Qt.binding(function() { return root.barSize })
api.transparent = Qt.binding(function() { return root.transparent })
api.foregroundAnimationEnabled = Qt.binding(function() { return root.foregroundAnimationEnabled })
api.centerSectionRevealHeld = Qt.binding(function() { return root.centerSectionRevealHeld })
api._centerHoverRevealSuppressed = Qt.binding(function() { return root.centerHoverRevealSuppressed })
root.syncPluginBarApiObjects(api)
}
function syncPluginBarApiObjects(api) {
if (!api) return
api.activePopout = root.pluginOwnsBarObject(api.pluginId, root.activePopout)
? root.activePopout : (root.activePopout ? api.foreignPopoutMarker : null)
api.clickTargets = root.pluginClickTargets(api.pluginId)
api.layoutConfig = root.publicLayoutConfig()
}
function pluginObjectRecord(target) {
for (var i = 0; i < pluginObjectOwners.length; i++) {
var record = pluginObjectOwners[i]
if (record && record.target === target) return record
}
return null
}
function markPluginObject(pluginId, target, role) {
var key = String(pluginId || "")
if (!key || !target) return false
var record = root.pluginObjectRecord(target)
if (record && record.pluginId !== key) return false
var next = []
for (var i = 0; i < pluginObjectOwners.length; i++) {
var existing = pluginObjectOwners[i]
if (!existing || existing.target !== target) next.push(existing)
}
var updated = record || { target: target, pluginId: key, clickTarget: false, popout: false }
updated[role] = true
next.push(updated)
pluginObjectOwners = next
return true
}
function unmarkPluginObject(pluginId, target, role) {
var key = String(pluginId || "")
var next = []
for (var i = 0; i < pluginObjectOwners.length; i++) {
var record = pluginObjectOwners[i]
if (!record || record.target !== target || record.pluginId !== key) {
next.push(record)
continue
}
record[role] = false
if (record.clickTarget || record.popout) next.push(record)
}
pluginObjectOwners = next
}
function pluginOwnsBarObject(pluginId, target) {
var record = target ? root.pluginObjectRecord(target) : null
return !!record && record.pluginId === String(pluginId || "")
}
function pluginClickTargets(pluginId) {
var out = []
for (var i = 0; i < root.clickTargets.length; i++) {
var target = root.clickTargets[i]
if (root.pluginOwnsBarObject(pluginId, target)) out.push(target)
}
return out
}
function syncAllPluginBarApiObjects() {
for (var id in pluginBarApis) root.syncPluginBarApiObjects(pluginBarApis[id])
}
function registerPluginClickTarget(pluginId, target) {
if (!root.markPluginObject(pluginId, target, "clickTarget")) return
root.registerClickTarget(target)
}
function unregisterPluginClickTarget(pluginId, target) {
if (!root.pluginOwnsBarObject(pluginId, target)) return
root.unregisterClickTarget(target)
root.unmarkPluginObject(pluginId, target, "clickTarget")
}
function requestPluginPopout(pluginId, owner) {
if (!root.markPluginObject(pluginId, owner, "popout")) return
root.requestPopout(owner)
}
function releasePluginPopout(pluginId, owner) {
if (!root.pluginOwnsBarObject(pluginId, owner)) return
root.releasePopout(owner)
root.unmarkPluginObject(pluginId, owner, "popout")
}
function pluginBarApiFor(pluginId, moduleName, registered) {
var key = String(pluginId || "")
if (!key) return null
var pluginShell = null
if (registered && root.shell && typeof root.shell.pluginShellForId === "function") {
// Only the trusted built-in bar receives ShellRoot and can request a
// service-capable facade for the widget it is instantiating.
pluginShell = root.shell.pluginShellForId(moduleName)
} else if (root.shell && typeof root.shell.pluginShellForBarEntry === "function") {
// Replacement bars receive a service-less entry facade. Giving an
// untrusted bar a generic facade factory would let it retrieve another
// third-party plugin's live service object.
pluginShell = root.shell.pluginShellForBarEntry(key, moduleName)
}
if (pluginBarApis[key]) {
pluginBarApis[key].shell = pluginShell
return pluginBarApis[key]
}
var api = pluginBarApiComponent.createObject(null, {
pluginId: key,
moduleName: String(moduleName || ""),
shell: pluginShell,
_showTooltip: function(target, text) { root.showTooltip(target, text) },
_hideTooltip: function(target) { root.hideTooltip(target) },
_registerClickTarget: function(target) { root.registerPluginClickTarget(key, target) },
_unregisterClickTarget: function(target) { root.unregisterPluginClickTarget(key, target) },
_requestPopout: function(owner) { root.requestPluginPopout(key, owner) },
_releasePopout: function(owner) { root.releasePluginPopout(key, owner) },
_switchPanelFrom: function(owner, direction) { return root.switchPanelFrom(owner, direction) },
_targetBelongsToWindow: function(target, window) { return root.targetBelongsToWindow(target, window) },
_moduleWidgets: function(requestedId) {
return String(requestedId || "") === String(moduleName || "")
? root.moduleWidgets(moduleName) : []
},
_run: function(command) { root.run(command) },
_setCenterHoverRevealSuppressed: function(value) {
root.centerHoverRevealSuppressed = !!value
}
})
if (!api) return null
root.bindPluginBarApi(api)
var next = ({})
for (var id in pluginBarApis) next[id] = pluginBarApis[id]
next[key] = api
pluginBarApis = next
return api
}
function pluginBarApiUsed(pluginId) {
for (var i = 0; i < moduleSlots.length; i++) {
var slot = moduleSlots[i]
if (slot && slot.pluginApiId === pluginId) return true
}
return false
}
function releasePluginObjects(pluginId) {
var owned = pluginObjectOwners.slice()
for (var i = 0; i < owned.length; i++) {
var record = owned[i]
if (!record || record.pluginId !== pluginId) continue
if (record.clickTarget) root.unregisterClickTarget(record.target)
if (record.popout && root.activePopout === record.target) root.releasePopout(record.target)
}
pluginObjectOwners = pluginObjectOwners.filter(function(record) {
return record && record.pluginId !== pluginId
})
}
function prunePluginBarApis() {
var next = ({})
for (var id in pluginBarApis) {
var api = pluginBarApis[id]
if (root.pluginBarApiUsed(id)) {
next[id] = api
continue
}
root.releasePluginObjects(id)
if (api && typeof api.destroy === "function") api.destroy()
}
pluginBarApis = next
}
onActivePopoutChanged: syncAllPluginBarApiObjects()
onClickTargetsChanged: syncAllPluginBarApiObjects()
onLayoutConfigChanged: syncAllPluginBarApiObjects()
onModuleSlotsChanged: Qt.callLater(prunePluginBarApis)
Component.onDestruction: {
for (var id in pluginBarApis) {
root.releasePluginObjects(id)
if (pluginBarApis[id] && typeof pluginBarApis[id].destroy === "function")
pluginBarApis[id].destroy()
}
pluginBarApis = ({})
}
function registerClickTarget(target) {
if (!target || clickTargets.indexOf(target) !== -1) return
@@ -599,6 +825,10 @@ Item {
if (barHoverCount === 0) centerSectionRevealTimer.restart()
}
function setCenterHoverRevealSuppressed(value) {
centerHoverRevealSuppressed = !!value
}
Timer {
id: centerSectionRevealTimer
interval: 120
@@ -1548,6 +1778,9 @@ Item {
readonly property string moduleName: root.entryId(entry)
readonly property var moduleSettings: root.entrySettings(entry)
readonly property string customType: root.customModuleType(entry)
readonly property var registryMetadata: root.barWidgetRegistry.metadataFor(root.canonicalWidgetId(moduleName))
readonly property bool firstParty: registryMetadata && registryMetadata.firstParty === true
readonly property string pluginApiId: registered ? root.canonicalWidgetId(moduleName) : "bar-entry:" + moduleName
// Re-evaluate when the registry mutates (Component reference changes,
// plugin enabled/disabled, etc.). Reading the `widgets` property creates
// the binding dependency — the wrapped function call alone wouldn't.
@@ -1766,7 +1999,8 @@ Item {
function injectProps() {
var target = activeItem
if (!target) return
if ("bar" in target) target.bar = root
if ("bar" in target) target.bar = firstParty
? root : root.pluginBarApiFor(pluginApiId, moduleName, registered)
if ("moduleName" in target) target.moduleName = moduleName
if ("settings" in target) target.settings = moduleSettings
}
+5
View File
@@ -5,6 +5,11 @@
"version": "1.0.0",
"author": "Omarchy",
"description": "Quickshell session lock with separate password and fingerprint PAM flows.",
"omarchy": {
"capabilities": [
"authentication"
]
},
"kinds": [
"service"
],
+3 -1
View File
@@ -119,7 +119,9 @@ Panel {
// Summoning by hotkey moves no pointer, so a hover the bar was still
// holding must not keep the center indicators revealed behind the panel.
function setCenterHoverRevealSuppressed(value) {
if (root.bar && "centerHoverRevealSuppressed" in root.bar)
if (root.bar && typeof root.bar.setCenterHoverRevealSuppressed === "function")
root.bar.setCenterHoverRevealSuppressed(value)
else if (root.bar && "centerHoverRevealSuppressed" in root.bar)
root.bar.centerHoverRevealSuppressed = value
}
+3 -1
View File
@@ -63,7 +63,9 @@ Panel {
}
function setCenterHoverRevealSuppressed(value) {
if (root.bar && "centerHoverRevealSuppressed" in root.bar)
if (root.bar && typeof root.bar.setCenterHoverRevealSuppressed === "function")
root.bar.setCenterHoverRevealSuppressed(value)
else if (root.bar && "centerHoverRevealSuppressed" in root.bar)
root.bar.centerHoverRevealSuppressed = value
}
+5
View File
@@ -5,6 +5,11 @@
"version": "1.0.0",
"author": "Omarchy",
"description": "Theme-aware authentication dialog for privileged actions.",
"omarchy": {
"capabilities": [
"authentication"
]
},
"kinds": [
"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)
+45
View File
@@ -0,0 +1,45 @@
// Intentionally not `.pragma library`: QML JavaScript imports get a private
// module instance per importing component. shell.qml's instance retains the
// authentication services; a third-party plugin importing this file receives
// a separate empty store rather than a shared path to credential-bearing QML.
var services = ({})
var trustedIds = ({})
function has(id) {
return services[String(id || "")] !== undefined
}
function put(id, service) {
var key = String(id || "")
if (!key || !service) return
trustedIds[key] = true
if (services[key] && services[key] !== service && typeof services[key].destroy === "function")
services[key].destroy()
services[key] = service
}
function isTrusted(id) {
return trustedIds[String(id || "")] === true
}
function ids() {
return Object.keys(services)
}
function updateManifest(id, manifest) {
var service = services[String(id || "")]
if (service && "manifest" in service) service.manifest = manifest
}
function destroy(id) {
var key = String(id || "")
var service = services[key]
if (service && typeof service.destroy === "function") service.destroy()
delete services[key]
}
function destroyAll() {
var keys = ids()
for (var i = 0; i < keys.length; i++) destroy(keys[i])
}
+46
View File
@@ -0,0 +1,46 @@
import QtQuick
// Detached application-library capability for third-party menus. Callbacks
// expose the supported app-list operations without retaining AppLibrary or its
// ShellRoot parent in the plugin-visible object graph.
QtObject {
required property string ownerPluginId
signal appsChanged()
property var _entryName: null
property var _entrySubtext: null
property var _sortedEntries: null
property var _iconSource: null
property var _refreshIcons: null
property var _launch: null
property var _remove: null
function entryName(entry) {
return _entryName ? _entryName(entry) : ""
}
function entrySubtext(entry) {
return _entrySubtext ? _entrySubtext(entry) : ""
}
function sortedEntries(query) {
return _sortedEntries ? _sortedEntries(String(query || "")) : []
}
function iconSource(icon) {
return _iconSource ? _iconSource(icon) : ""
}
function refreshIcons() {
if (_refreshIcons) _refreshIcons()
}
function launch(desktopId, name) {
if (_launch) _launch(String(desktopId || ""), String(name || ""))
}
function remove(desktopId, name) {
if (_remove) _remove(String(desktopId || ""), String(name || ""))
}
}
+12
View File
@@ -0,0 +1,12 @@
import QtQuick
// Scalar-only view of the active bar for plugins that position independent
// windows. The active Bar QObject is never retained here.
QtObject {
required property string ownerPluginId
property bool barHidden: false
property int barSize: 0
property string fontFamily: ""
property string position: "top"
}
@@ -0,0 +1,24 @@
import QtQuick
// Detached widget-catalogue snapshot for third-party full-bar implementations.
// Plugins can render the referenced components, but mutating this local view
// cannot replace a registration in the host registry.
QtObject {
id: api
property var widgets: ({})
property int revision: 0
function metadataFor(id) {
var entry = widgets[String(id || "")]
return entry ? entry.metadata : null
}
function availableIds() {
return Object.keys(widgets)
}
function has(id) {
return widgets[String(id || "")] !== undefined
}
}
@@ -0,0 +1,46 @@
import QtQuick
// Narrow proxy for the non-authentication first-party services used by the
// built-in bar. It intentionally has no generic property or method forwarding.
QtObject {
required property string ownerPluginId
required property string serviceId
property bool stayAwake: false
property bool enabled: false
property bool doNotDisturb: false
property var activePlayer: null
property var sourcePlayers: []
property var _setIdleEnabled: null
property var _setNightlight: null
property var _setDoNotDisturb: null
property var _runAction: null
property var _playerKey: null
property var _selectPlayer: null
function setIdleEnabled(value) {
if (serviceId === "omarchy.idle" && _setIdleEnabled) _setIdleEnabled(!!value)
}
function setNightlight(value) {
if (serviceId === "omarchy.nightlight" && _setNightlight) _setNightlight(!!value)
}
function setDoNotDisturb(value) {
if (serviceId === "omarchy.notifications" && _setDoNotDisturb) _setDoNotDisturb(!!value)
}
function runAction(action, showFeedback, playerId) {
if (serviceId === "omarchy.media" && _runAction)
_runAction(String(action || ""), !!showFeedback, String(playerId || ""))
}
function playerKey(player) {
return serviceId === "omarchy.media" && _playerKey ? _playerKey(player) : ""
}
function selectPlayer(playerId) {
if (serviceId === "omarchy.media" && _selectPlayer) _selectPlayer(String(playerId || ""))
}
}
+30 -3
View File
@@ -20,7 +20,7 @@ QtObject {
property var shellConfigProvider: null
property var shellConfigMutator: null
// { pluginId: manifest } — manifests have __sourceDir and __isFirstParty stamped in.
// { pluginId: manifest } — manifests have source/trust metadata stamped in.
property var installedPlugins: ({})
property int registryRevision: 0
property bool scanning: false
@@ -78,8 +78,7 @@ QtObject {
}
}
// Every entry point must be a relative path inside the plugin's source
// directory. Reject the whole manifest if anything looks like an attempt
// to escape the plugin's sandbox.
// directory. Reject the whole manifest if an entry point escapes it.
for (var key in manifest.entryPoints) {
if (!isSafeEntryPoint(manifest.entryPoints[key])) {
console.warn("PluginRegistry: unsafe entryPoint '" + key + "'='"
@@ -90,6 +89,32 @@ QtObject {
return manifest
}
function trustedCapabilities(manifest) {
if (!manifest || !manifest.__isFirstParty) return []
var metadata = Util.isPlainObject(manifest.omarchy) ? manifest.omarchy : null
var declared = metadata && Array.isArray(metadata.capabilities) ? metadata.capabilities : []
var out = []
for (var i = 0; i < declared.length; i++) {
var capability = String(declared[i] || "")
if (capability && out.indexOf(capability) === -1) out.push(capability)
}
return out
}
function stampHostCapabilities(firstParty, thirdParty) {
for (var firstPartyId in firstParty)
firstParty[firstPartyId].__hostCapabilities = trustedCapabilities(firstParty[firstPartyId])
for (var thirdPartyId in thirdParty) {
var manifest = thirdParty[thirdPartyId]
var metadata = manifest && Util.isPlainObject(manifest.omarchy) ? manifest.omarchy : null
var clonedFrom = metadata ? String(metadata.clonedFrom || "") : ""
var source = clonedFrom ? firstParty[clonedFrom] : null
manifest.__hostCapabilities = source && Array.isArray(source.__hostCapabilities)
? source.__hostCapabilities.slice() : []
}
}
function entryPointUrl(manifest, kind) {
if (!Util.isPlainObject(manifest)) return ""
var ep = manifest.entryPoints ? manifest.entryPoints[kind] : null
@@ -594,6 +619,8 @@ QtObject {
}
flush()
stampHostCapabilities(firstParty, thirdParty)
var merged = {}
for (var fk in firstParty) merged[fk] = firstParty[fk]
// Third-party plugins never shadow first-party ids. The whole
+32
View File
@@ -0,0 +1,32 @@
import QtQuick
// Read-only, self-scoped registry view for an installed third-party plugin.
// The host updates manifest/enabled when it rescans; no host registry object is
// retained here, so `parent` and property traversal cannot reach ShellRoot.
QtObject {
id: api
required property string pluginId
property var manifest: null
property bool enabled: false
property var _entryPointUrl: null
readonly property var installedPlugins: {
var out = ({})
if (manifest) out[pluginId] = manifest
return out
}
function isEnabled(id) {
return String(id || "") === pluginId && enabled
}
function resolveEnabledId(id) {
return String(id || "") === pluginId ? pluginId : ""
}
function entryPointUrl(candidate, kind) {
if (!candidate || String(candidate.id || "") !== pluginId) return ""
return _entryPointUrl ? _entryPointUrl(String(kind || "")) : ""
}
}
+70
View File
@@ -0,0 +1,70 @@
import QtQuick
// Capability-scoped shell surface for installed third-party plugins.
//
// The callbacks are closed over one plugin id by shell.qml. A plugin can call
// them directly, but it cannot widen their scope: ordinary plugins are limited
// to their own id, and full-bar callbacks independently enforce their explicit
// non-authentication UI scope. This object avoids directly injecting the host
// shell, but it is not a QML sandbox: visual plugins share the host object tree.
QtObject {
id: api
required property string pluginId
property var appLibrary: null
property var bar: null
property var barConfig: ({})
property var idleConfig: ({})
property var _serviceLookup: null
property var _firstPartyServiceLookup: null
property var _barEntryShellLookup: null
property var _summon: null
property var _hide: null
property var _toggle: null
property var _isOpen: null
property var _updateSettings: null
property var _mutateBarConfig: null
function serviceFor(id) {
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.
function firstPartyServiceFor(id) {
return _firstPartyServiceLookup
? _firstPartyServiceLookup(String(id || "")) : null
}
function pluginShellForBarEntry(ownerId, moduleName) {
return _barEntryShellLookup
? _barEntryShellLookup(String(ownerId || ""), String(moduleName || "")) : null
}
function summon(id, payloadJson) {
return _summon ? _summon(String(id || ""), String(payloadJson || "")) : false
}
function hide(id) {
return _hide ? _hide(String(id || "")) : false
}
function toggle(id, payloadJson) {
return _toggle ? _toggle(String(id || ""), String(payloadJson || "")) : false
}
function isPluginOpen(id) {
return _isOpen ? _isOpen(String(id || "")) : false
}
function updateEntryInline(id, settings) {
return _updateSettings ? _updateSettings(String(id || ""), settings) : false
}
function mutateShellConfig(mutator) {
return _mutateBarConfig && typeof mutator === "function"
? _mutateBarConfig(mutator) : false
}
}
+704 -29
View File
@@ -7,6 +7,7 @@ import qs.Commons
import "plugins/bar"
import "services"
import "services/AuthServiceStore.js" as AuthServiceStore
ShellRoot {
id: shell
@@ -113,7 +114,10 @@ ShellRoot {
}
readonly property var barConfig: shellConfig && Util.isPlainObject(shellConfig.bar) ? shellConfig.bar : builtinShellConfig.bar
onBarConfigChanged: if (bar && "barConfig" in bar) bar.barConfig = shell.barConfig
onBarConfigChanged: {
if (bar && "barConfig" in bar)
bar.barConfig = shell.barConfigFor(shell.activeBarManifest)
}
FileView {
id: defaultsFile
path: shell.defaultsPath
@@ -214,11 +218,11 @@ ShellRoot {
function configureBar(target, manifest) {
if (!target) return
if ("omarchyPath" in target) target.omarchyPath = shell.omarchyPath
if ("shell" in target) target.shell = shell
if ("manifest" in target) target.manifest = manifest
if ("barWidgetRegistry" in target) target.barWidgetRegistry = shell.barWidgetRegistry
if ("pluginRegistry" in target) target.pluginRegistry = shell.pluginRegistry
if ("barConfig" in target) target.barConfig = shell.barConfig
if ("shell" in target) target.shell = shell.pluginShellFor(manifest)
if ("manifest" in target) target.manifest = shell.publicPluginManifest(manifest)
if ("barWidgetRegistry" in target) target.barWidgetRegistry = shell.pluginBarWidgetRegistryFor(manifest)
if ("pluginRegistry" in target) target.pluginRegistry = shell.pluginRegistryFor(manifest)
if ("barConfig" in target) target.barConfig = shell.barConfigFor(manifest)
shell.bar = target
}
@@ -253,8 +257,7 @@ ShellRoot {
onActiveChanged: if (!active) shell.bar = null
onStatusChanged: {
if (status === Loader.Error) {
var detail = errorString && errorString() ? errorString() : ""
console.warn("bar option " + shell.activeBarId + " failed to load, falling back to " + shell.defaultBarId + ":", detail)
console.warn("bar option " + shell.activeBarId + " failed to load, falling back to " + shell.defaultBarId)
shell.failedBarId = shell.activeBarId
}
}
@@ -271,13 +274,613 @@ ShellRoot {
}
property var _services: ({})
property var _pluginShellApis: ({})
property var _pluginShellApiDescriptors: ({})
property var _pluginBarEntryShellApis: ({})
property var _pluginRegistryApis: ({})
property var _pluginBarWidgetRegistryApis: ({})
property var _pluginAppLibraryApis: ({})
property var _pluginBarStateApis: ({})
property var _pluginFirstPartyServiceApis: ({})
Component {
id: pluginShellApiComponent
PluginShellApi { }
}
Component {
id: pluginRegistryApiComponent
PluginRegistryApi { }
}
Component {
id: pluginBarWidgetRegistryApiComponent
PluginBarWidgetRegistryApi { }
}
Component {
id: pluginAppLibraryApiComponent
PluginAppLibraryApi { }
}
Component {
id: pluginBarStateApiComponent
PluginBarStateApi { }
}
Component {
id: pluginFirstPartyServiceApiComponent
PluginFirstPartyServiceApi { }
}
function publicPluginManifest(manifest) {
if (!manifest) return null
if (manifest.__isFirstParty) return manifest
var copy = JSON.parse(JSON.stringify(manifest))
delete copy.__sourceDir
delete copy.__isFirstParty
delete copy.__hostCapabilities
return copy
}
function publicBarConfig() {
return JSON.parse(JSON.stringify(shell.barConfig || {}))
}
function barConfigFor(manifest) {
return !manifest || manifest.__isFirstParty
? shell.barConfig : shell.publicBarConfig()
}
function publicBarWidgetSnapshot() {
var source = shell.barWidgetRegistry.widgets || {}
var snapshot = {}
for (var id in source) {
var entry = source[id]
if (!entry) continue
snapshot[id] = {
component: entry.component,
metadata: JSON.parse(JSON.stringify(entry.metadata || {}))
}
}
return snapshot
}
function manifestHasKind(manifest, kind) {
return !!manifest && Array.isArray(manifest.kinds)
&& manifest.kinds.indexOf(kind) !== -1
}
function pluginHasBarCapabilities(manifest) {
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
return shell.pluginRegistry.resolveEnabledId(String(requestedId || "")) === caller
}
function pluginServiceFor(pluginId, requestedId) {
if (!shell.pluginOwnsTarget(pluginId, requestedId)) return null
return shell.serviceFor(shell.pluginRegistry.resolveEnabledId(requestedId))
}
function barEntryConfigured(pluginId) {
var location = shell.pluginRegistry.findEntryLocation(shell.shellConfig, pluginId)
return location && location.kind === "bar"
}
function barPluginMayControl(manifest, requestedId) {
if (!shell.pluginHasBarCapabilities(manifest)) return false
var id = shell.pluginRegistry.resolveEnabledId(String(requestedId || ""))
var target = shell.pluginRegistry.installedPlugins[id]
if (!target || shell.isAuthenticationService(target, id)) return false
if (shell.barEntryConfigured(id)) return true
var uiKinds = ["bar-widget", "panel", "overlay", "menu"]
for (var i = 0; i < uiKinds.length; i++)
if (shell.manifestHasKind(target, uiKinds[i])) return true
return false
}
function mutatePluginBarConfig(mutator) {
if (typeof mutator !== "function") return false
shell.mutateShellConfig(function(config) {
var scoped = { bar: JSON.parse(JSON.stringify(config.bar || {})) }
mutator(scoped)
if (Util.isPlainObject(scoped.bar)) config.bar = JSON.parse(JSON.stringify(scoped.bar))
})
return true
}
function pluginAppLibraryFor(cacheKey, pluginId) {
if (_pluginAppLibraryApis[cacheKey]) return _pluginAppLibraryApis[cacheKey]
var api = pluginAppLibraryApiComponent.createObject(null, {
ownerPluginId: pluginId,
_entryName: function(entry) { return shell.appLibrary.entryName(entry) },
_entrySubtext: function(entry) { return shell.appLibrary.entrySubtext(entry) },
_sortedEntries: function(query) { return shell.appLibrary.sortedEntries(query) },
_iconSource: function(icon) { return shell.appLibrary.iconSource(icon) },
_refreshIcons: function() { shell.appLibrary.refreshIcons() },
_launch: function(desktopId, name) { shell.appLibrary.launch(desktopId, name) },
_remove: function(desktopId, name) { shell.appLibrary.remove(desktopId, name) }
})
if (!api) return null
var next = ({})
for (var id in _pluginAppLibraryApis) next[id] = _pluginAppLibraryApis[id]
next[cacheKey] = api
_pluginAppLibraryApis = next
return api
}
function pluginBarStateFor(cacheKey, pluginId) {
if (_pluginBarStateApis[cacheKey]) return _pluginBarStateApis[cacheKey]
var api = pluginBarStateApiComponent.createObject(null, { ownerPluginId: pluginId })
if (!api) return null
api.barHidden = Qt.binding(function() { return shell.bar ? shell.bar.barHidden === true : false })
api.barSize = Qt.binding(function() { return shell.bar ? Math.max(0, shell.bar.barSize || 0) : 0 })
api.fontFamily = Qt.binding(function() { return shell.bar ? String(shell.bar.fontFamily || "") : "" })
api.position = Qt.binding(function() { return shell.bar ? String(shell.bar.position || "top") : "top" })
var next = ({})
for (var id in _pluginBarStateApis) next[id] = _pluginBarStateApis[id]
next[cacheKey] = api
_pluginBarStateApis = next
return api
}
function pluginFirstPartyServiceFor(cacheKey, pluginId, requestedId) {
var id = String(requestedId || "")
var allowed = ["omarchy.idle", "omarchy.media", "omarchy.nightlight", "omarchy.notifications"]
if (allowed.indexOf(id) === -1) return null
var proxyKey = cacheKey + "::" + id
if (_pluginFirstPartyServiceApis[proxyKey]) return _pluginFirstPartyServiceApis[proxyKey]
function service() {
return shell.serviceFor(shell.pluginRegistry.resolveEnabledId(id))
}
var api = pluginFirstPartyServiceApiComponent.createObject(null, {
ownerPluginId: pluginId,
serviceId: id,
_setIdleEnabled: function(value) {
var target = service()
if (target && typeof target.setIdleEnabled === "function") target.setIdleEnabled(value)
},
_setNightlight: function(value) {
var target = service()
if (target && typeof target.setNightlight === "function") target.setNightlight(value)
},
_setDoNotDisturb: function(value) {
var target = service()
if (target && typeof target.setDoNotDisturb === "function") target.setDoNotDisturb(value)
},
_runAction: function(action, showFeedback, targetKey) {
var target = service()
if (target && typeof target.runAction === "function") target.runAction(action, showFeedback, targetKey)
},
_playerKey: function(player) {
var target = service()
return target && typeof target.playerKey === "function" ? target.playerKey(player) : ""
},
_selectPlayer: function(playerKey) {
var target = service()
if (target && typeof target.selectPlayer === "function") target.selectPlayer(playerKey)
}
})
if (!api) return null
api.stayAwake = Qt.binding(function() {
var target = service()
return target ? target.stayAwake === true : false
})
api.enabled = Qt.binding(function() {
var target = service()
return target ? target.enabled === true : false
})
api.doNotDisturb = Qt.binding(function() {
var target = service()
return target ? target.doNotDisturb === true : false
})
api.activePlayer = Qt.binding(function() {
var target = service()
return target ? target.activePlayer : null
})
api.sourcePlayers = Qt.binding(function() {
var target = service()
return target && Array.isArray(target.sourcePlayers) ? target.sourcePlayers : []
})
var next = ({})
for (var existing in _pluginFirstPartyServiceApis) next[existing] = _pluginFirstPartyServiceApis[existing]
next[proxyKey] = api
_pluginFirstPartyServiceApis = next
return api
}
function pluginShellCapabilityProfile(manifest, allowOwnService, barCapabilities) {
return [
allowOwnService ? "own-service" : "no-own-service",
barCapabilities ? "bar" : "no-bar",
shell.manifestHasKind(manifest, "menu") ? "menu" : "no-menu"
].join("|")
}
function cacheWithoutKey(cache, key, destroyValue) {
var next = ({})
for (var existing in cache) {
if (existing === key) {
var value = cache[existing]
if (destroyValue && value && typeof value.destroy === "function") value.destroy()
} else {
next[existing] = cache[existing]
}
}
return next
}
function cacheWithoutPrefix(cache, prefix) {
var next = ({})
for (var existing in cache) {
if (existing.indexOf(prefix) === 0) {
var value = cache[existing]
if (value && typeof value.destroy === "function") value.destroy()
} else {
next[existing] = cache[existing]
}
}
return next
}
function revokePluginShellApi(cacheKey) {
var key = String(cacheKey || "")
if (!key) return
_pluginAppLibraryApis = shell.cacheWithoutKey(_pluginAppLibraryApis, key, true)
_pluginFirstPartyServiceApis = shell.cacheWithoutPrefix(_pluginFirstPartyServiceApis, key + "::")
_pluginBarEntryShellApis = shell.cacheWithoutPrefix(_pluginBarEntryShellApis, key + ":")
_pluginShellApis = shell.cacheWithoutKey(_pluginShellApis, key, true)
_pluginShellApiDescriptors = shell.cacheWithoutKey(_pluginShellApiDescriptors, key, false)
}
function createScopedPluginShell(manifest, cacheKey, allowOwnService, barCapabilities) {
var key = String(manifest && manifest.id || "")
if (!key) return null
var profile = shell.pluginShellCapabilityProfile(manifest, allowOwnService, barCapabilities)
var cached = _pluginShellApis[cacheKey]
var descriptor = _pluginShellApiDescriptors[cacheKey]
if (cached && descriptor && descriptor.pluginId === key
&& descriptor.profile === profile) return cached
if (cached || descriptor) shell.revokePluginShellApi(cacheKey)
function currentManifest() {
return shell.pluginRegistry.installedPlugins[key] || null
}
function hasCurrentBarCapabilities() {
return barCapabilities && shell.pluginHasBarCapabilities(currentManifest())
}
// Construct the narrow service proxies before any plugin binding can call
// firstPartyServiceFor(). Creating a QObject while evaluating that binding
// 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 api = pluginShellApiComponent.createObject(null, {
pluginId: key,
appLibrary: shell.manifestHasKind(manifest, "menu")
? 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
},
_firstPartyServiceLookup: function(requestedId) {
if (allowOwnService && shell.pluginOwnsTarget(key, requestedId))
return shell.pluginServiceFor(key, requestedId)
return hasCurrentBarCapabilities() ? (firstPartyServices[requestedId] || null) : null
},
_barEntryShellLookup: function(ownerId, moduleName) {
return hasCurrentBarCapabilities()
? shell.pluginShellForBarEntry(cacheKey + ":" + ownerId, moduleName) : null
},
_summon: function(requestedId, payloadJson) {
if (!shell.pluginOwnsTarget(key, requestedId)
&& !shell.barPluginMayControl(currentManifest(), requestedId)
&& !shell.pluginCloneMaySummon(currentManifest(), requestedId)) return false
return shell.summon(shell.pluginRegistry.resolveEnabledId(requestedId), payloadJson)
},
_hide: function(requestedId) {
if (!shell.pluginOwnsTarget(key, requestedId)
&& !shell.barPluginMayControl(currentManifest(), requestedId)) return false
return shell.hide(shell.pluginRegistry.resolveEnabledId(requestedId))
},
_toggle: function(requestedId, payloadJson) {
if (!shell.pluginOwnsTarget(key, requestedId)
&& !shell.barPluginMayControl(currentManifest(), requestedId)) return false
return shell.toggle(shell.pluginRegistry.resolveEnabledId(requestedId), payloadJson)
},
_isOpen: function(requestedId) {
if (!shell.pluginOwnsTarget(key, requestedId)
&& !shell.barPluginMayControl(currentManifest(), requestedId)) return false
return shell.isPluginOpen(shell.pluginRegistry.resolveEnabledId(requestedId))
},
_updateSettings: function(requestedId, settings) {
if (shell.pluginOwnsTarget(key, requestedId)) return shell.updateEntryInline(key, settings)
if (hasCurrentBarCapabilities() && shell.barEntryConfigured(requestedId))
return shell.updateEntryInline(requestedId, settings)
return false
},
_mutateBarConfig: function(mutator) {
return hasCurrentBarCapabilities() ? shell.mutatePluginBarConfig(mutator) : false
}
})
if (!api) return null
var next = ({})
for (var id in _pluginShellApis) next[id] = _pluginShellApis[id]
next[cacheKey] = api
_pluginShellApis = next
var descriptorNext = ({})
for (var descriptorKey in _pluginShellApiDescriptors)
descriptorNext[descriptorKey] = _pluginShellApiDescriptors[descriptorKey]
descriptorNext[cacheKey] = {
pluginId: key,
allowOwnService: allowOwnService === true,
profile: profile
}
_pluginShellApiDescriptors = descriptorNext
return api
}
function scopedPluginShellForId(pluginId) {
var key = String(pluginId || "")
var manifest = shell.pluginRegistry.installedPlugins[key]
if (!manifest) return null
if (!manifest.__isFirstParty) return shell.pluginShellFor(manifest)
return shell.createScopedPluginShell(manifest, "hosted:" + key, false, false)
}
function pluginShellForId(pluginId) {
return shell.scopedPluginShellForId(pluginId)
}
function pluginShellForBarEntry(ownerId, moduleName) {
var owner = String(ownerId || "")
var target = String(moduleName || "")
if (!owner || !target) return null
if (!shell.barEntryConfigured(target)) return null
var cacheKey = owner + "::" + target
if (_pluginBarEntryShellApis[cacheKey]) return _pluginBarEntryShellApis[cacheKey]
function owns(requestedId) {
return shell.pluginRegistry.resolveEnabledId(String(requestedId || ""))
=== shell.pluginRegistry.resolveEnabledId(target)
}
function currentManifest() {
var id = shell.pluginRegistry.resolveEnabledId(target)
return shell.pluginRegistry.installedPlugins[id] || null
}
var api = pluginShellApiComponent.createObject(null, {
pluginId: target,
barConfig: shell.publicBarConfig(),
_summon: function(requestedId, payloadJson) {
if (!owns(requestedId)
&& !shell.pluginCloneMaySummon(currentManifest(), requestedId)) return false
return shell.summon(shell.pluginRegistry.resolveEnabledId(requestedId), payloadJson)
},
_hide: function(requestedId) {
return owns(requestedId)
? shell.hide(shell.pluginRegistry.resolveEnabledId(target)) : false
},
_toggle: function(requestedId, payloadJson) {
return owns(requestedId)
? shell.toggle(shell.pluginRegistry.resolveEnabledId(target), payloadJson) : false
},
_isOpen: function(requestedId) {
return owns(requestedId)
? shell.isPluginOpen(shell.pluginRegistry.resolveEnabledId(target)) : false
},
_updateSettings: function(requestedId, settings) {
return String(requestedId || "") === target
? shell.updateEntryInline(target, settings) : false
}
})
if (!api) return null
var next = ({})
for (var id in _pluginBarEntryShellApis) next[id] = _pluginBarEntryShellApis[id]
next[cacheKey] = api
_pluginBarEntryShellApis = next
return api
}
function pluginShellFor(manifest) {
if (!manifest || manifest.__isFirstParty) return shell
var key = String(manifest.id || "")
if (!key) return null
return shell.createScopedPluginShell(manifest, key, true, shell.pluginHasBarCapabilities(manifest))
}
function pluginRegistryFor(manifest) {
if (!manifest || manifest.__isFirstParty) return shell.pluginRegistry
var key = String(manifest.id || "")
if (!key) return null
if (_pluginRegistryApis[key]) return _pluginRegistryApis[key]
var api = pluginRegistryApiComponent.createObject(null, {
pluginId: key,
manifest: shell.publicPluginManifest(manifest),
enabled: shell.pluginRegistry.isEnabled(key),
_entryPointUrl: function(kind) {
var current = shell.pluginRegistry.installedPlugins[key]
return current ? shell.pluginRegistry.entryPointUrl(current, kind) : ""
}
})
if (!api) return null
var next = ({})
for (var id in _pluginRegistryApis) next[id] = _pluginRegistryApis[id]
next[key] = api
_pluginRegistryApis = next
return api
}
function pluginBarWidgetRegistryFor(manifest) {
if (!manifest || manifest.__isFirstParty) return shell.barWidgetRegistry
var key = String(manifest.id || "")
if (!key) return null
if (_pluginBarWidgetRegistryApis[key]) return _pluginBarWidgetRegistryApis[key]
var api = pluginBarWidgetRegistryApiComponent.createObject(null, {
widgets: shell.publicBarWidgetSnapshot(),
revision: shell.barWidgetRegistry.revision
})
if (!api) return null
var next = ({})
for (var id in _pluginBarWidgetRegistryApis) next[id] = _pluginBarWidgetRegistryApis[id]
next[key] = api
_pluginBarWidgetRegistryApis = next
return api
}
function pluginApiActive(api, plugins) {
var id = api ? String(api.pluginId || api.ownerPluginId || "") : ""
var manifest = id ? plugins[id] : null
return !!manifest && shell.pluginRegistry.isEnabled(id)
}
function prunePluginApis() {
var plugins = shell.pluginRegistry.installedPlugins
var shellKeys = Object.keys(_pluginShellApis)
for (var si = 0; si < shellKeys.length; si++) {
var shellKey = shellKeys[si]
var shellApi = _pluginShellApis[shellKey]
var descriptor = _pluginShellApiDescriptors[shellKey]
var manifest = descriptor ? plugins[descriptor.pluginId] : null
var barCapabilities = descriptor && descriptor.allowOwnService
&& shell.pluginHasBarCapabilities(manifest)
var expectedProfile = descriptor
? shell.pluginShellCapabilityProfile(manifest, descriptor.allowOwnService, barCapabilities) : ""
var active = descriptor && manifest && shell.pluginRegistry.isEnabled(descriptor.pluginId)
if (!active || descriptor.profile !== expectedProfile)
shell.revokePluginShellApi(shellKey)
}
var registryNext = ({})
for (var registryKey in _pluginRegistryApis) {
var registryApi = _pluginRegistryApis[registryKey]
if (shell.pluginApiActive(registryApi, plugins)) registryNext[registryKey] = registryApi
else if (registryApi && typeof registryApi.destroy === "function") registryApi.destroy()
}
_pluginRegistryApis = registryNext
var widgetNext = ({})
for (var widgetKey in _pluginBarWidgetRegistryApis) {
var widgetApi = _pluginBarWidgetRegistryApis[widgetKey]
if (plugins[widgetKey] && shell.pluginRegistry.isEnabled(widgetKey)) widgetNext[widgetKey] = widgetApi
else if (widgetApi && typeof widgetApi.destroy === "function") widgetApi.destroy()
}
_pluginBarWidgetRegistryApis = widgetNext
var appNext = ({})
for (var appKey in _pluginAppLibraryApis) {
var appApi = _pluginAppLibraryApis[appKey]
if (shell.pluginApiActive(appApi, plugins)) appNext[appKey] = appApi
else if (appApi && typeof appApi.destroy === "function") appApi.destroy()
}
_pluginAppLibraryApis = appNext
var barStateNext = ({})
for (var barStateKey in _pluginBarStateApis) {
var barStateApi = _pluginBarStateApis[barStateKey]
if (shell.pluginApiActive(barStateApi, plugins)) barStateNext[barStateKey] = barStateApi
else if (barStateApi && typeof barStateApi.destroy === "function") barStateApi.destroy()
}
_pluginBarStateApis = barStateNext
var serviceNext = ({})
for (var serviceKey in _pluginFirstPartyServiceApis) {
var serviceApi = _pluginFirstPartyServiceApis[serviceKey]
if (shell.pluginApiActive(serviceApi, plugins)) serviceNext[serviceKey] = serviceApi
else if (serviceApi && typeof serviceApi.destroy === "function") serviceApi.destroy()
}
_pluginFirstPartyServiceApis = serviceNext
var entryNext = ({})
for (var entryKey in _pluginBarEntryShellApis) {
var entryApi = _pluginBarEntryShellApis[entryKey]
if (entryApi && shell.barEntryConfigured(entryApi.pluginId)) entryNext[entryKey] = entryApi
else if (entryApi && typeof entryApi.destroy === "function") entryApi.destroy()
}
_pluginBarEntryShellApis = entryNext
}
function syncPluginApis() {
shell.prunePluginApis()
var plugins = shell.pluginRegistry.installedPlugins
for (var id in _pluginRegistryApis) {
var registryApi = _pluginRegistryApis[id]
var manifest = plugins[id]
registryApi.manifest = shell.publicPluginManifest(manifest)
registryApi.enabled = !!manifest && shell.pluginRegistry.isEnabled(id)
}
for (var widgetId in _pluginBarWidgetRegistryApis) {
var widgetApi = _pluginBarWidgetRegistryApis[widgetId]
widgetApi.widgets = shell.publicBarWidgetSnapshot()
widgetApi.revision = shell.barWidgetRegistry.revision
}
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()
}
function serviceFor(pluginId) {
return _services[String(pluginId)] || null
}
function firstPartyServiceFor(pluginId) {
return serviceFor(pluginId)
return serviceFor(shell.pluginRegistry.resolveEnabledId(pluginId))
}
function isAuthenticationService(manifest, pluginId) {
var key = String(pluginId || (manifest && manifest.id) || "")
return AuthServiceStore.isTrusted(key)
|| (!!manifest && Array.isArray(manifest.__hostCapabilities)
&& manifest.__hostCapabilities.indexOf("authentication") !== -1)
}
function ensureService(pluginId) {
@@ -290,6 +893,8 @@ ShellRoot {
if (!manifest.entryPoints || !manifest.entryPoints.service) return null
var url = pluginRegistry.entryPointUrl(manifest, "service")
if (!url) return null
var authenticationService = shell.isAuthenticationService(manifest, key)
if (authenticationService && AuthServiceStore.has(key)) return null
var comp = Qt.createComponent(url, Component.PreferSynchronous)
function finalize() {
@@ -297,27 +902,37 @@ ShellRoot {
console.warn("service plugin load failed for " + key + ": " + comp.errorString())
return
}
var inst = comp.createObject(serviceHost)
// Authentication services and third-party services have no visual
// parent. Parenting either to serviceHost would let a plugin's object
// traversal walk between the host and credential-bearing QML.
var inst = comp.createObject(manifest.__isFirstParty && !authenticationService ? serviceHost : null)
if (!inst) {
console.warn("service plugin createObject returned null for", key)
return
}
if ("omarchyPath" in inst) inst.omarchyPath = shell.omarchyPath
if ("shell" in inst) inst.shell = shell
if ("manifest" in inst) inst.manifest = manifest
if ("barWidgetRegistry" in inst) inst.barWidgetRegistry = shell.barWidgetRegistry
if ("pluginRegistry" in inst) inst.pluginRegistry = shell.pluginRegistry
var snext = ({})
for (var sk in _services) snext[sk] = _services[sk]
snext[key] = inst
_services = snext
if ("shell" in inst) inst.shell = shell.pluginShellFor(manifest)
if ("manifest" in inst) inst.manifest = shell.publicPluginManifest(manifest)
if ("barWidgetRegistry" in inst) inst.barWidgetRegistry = shell.pluginBarWidgetRegistryFor(manifest)
if ("pluginRegistry" in inst) inst.pluginRegistry = shell.pluginRegistryFor(manifest)
if (authenticationService) {
// Never publish lock/polkit through ShellRoot._services. The private JS
// import retains their lifetime without adding a traversable property
// or QObject parent back to the host shell.
AuthServiceStore.put(key, inst)
} else {
var snext = ({})
for (var sk in _services) snext[sk] = _services[sk]
snext[key] = inst
_services = snext
}
}
if (comp.status === Component.Loading) {
comp.statusChanged.connect(finalize)
return null
}
finalize()
return _services[key] || null
return authenticationService ? null : (_services[key] || null)
}
function _syncServices() {
@@ -329,11 +944,33 @@ ShellRoot {
if (!Array.isArray(m.kinds) || m.kinds.indexOf("service") === -1) continue
if (!m.entryPoints || !m.entryPoints.service) continue
if (!pluginRegistry.isEnabled(id)) continue
var authenticationService = shell.isAuthenticationService(m, id)
if (_services[id]) {
// A kept instance outlives the rescan; hand it the fresh manifest.
var kept = _services[id]
if (kept && "manifest" in kept) kept.manifest = m
continue
if (authenticationService) {
// A service that gains a trusted authentication capability must move
// out of the host's public service map before it is recreated.
var published = _services[id]
if (published && typeof published.destroy === "function") published.destroy()
var withoutPublished = ({})
for (var publishedId in _services)
if (publishedId !== id) withoutPublished[publishedId] = _services[publishedId]
_services = withoutPublished
} else {
// A kept instance outlives the rescan; hand it the fresh manifest.
var kept = _services[id]
if (kept && "shell" in kept) kept.shell = shell.pluginShellFor(m)
if (kept && "manifest" in kept) kept.manifest = shell.publicPluginManifest(m)
continue
}
}
if (AuthServiceStore.has(id)) {
if (authenticationService) {
AuthServiceStore.updateManifest(id, shell.publicPluginManifest(m))
continue
}
// A service that loses its trusted authentication capability can move
// back to the ordinary service map only after the isolated copy dies.
AuthServiceStore.destroy(id)
}
ensureService(id)
}
@@ -352,6 +989,21 @@ ShellRoot {
for (var k in _services) if (k !== existingId) next[k] = _services[k]
_services = next
}
// Authentication services are retained outside the root object graph, so
// reconcile their disable/remove lifecycle separately from _services.
var authenticationIds = AuthServiceStore.ids()
for (var ai = 0; ai < authenticationIds.length; ai++) {
var authenticationId = authenticationIds[ai]
var authenticationManifest = plugins[authenticationId]
var stillAuthenticationService = authenticationManifest
&& Array.isArray(authenticationManifest.kinds)
&& authenticationManifest.kinds.indexOf("service") !== -1
&& authenticationManifest.entryPoints
&& authenticationManifest.entryPoints.service
if (stillAuthenticationService && pluginRegistry.isEnabled(authenticationId)
&& shell.isAuthenticationService(authenticationManifest, authenticationId)) continue
AuthServiceStore.destroy(authenticationId)
}
}
function serviceKeepLoaded(pluginId) {
@@ -374,11 +1026,33 @@ ShellRoot {
if (inst && typeof inst.destroy === "function") inst.destroy()
}
_services = next
var authenticationIds = AuthServiceStore.ids()
for (var ai = 0; ai < authenticationIds.length; ai++) {
var authenticationId = authenticationIds[ai]
if (!serviceKeepLoaded(authenticationId))
AuthServiceStore.destroy(authenticationId)
}
}
Connections {
target: shell.pluginRegistry
function onPluginsChanged() { if (!shell.pluginReloading) shell._syncServices() }
function onPluginsChanged() {
shell.syncPluginApis()
if (!shell.pluginReloading) shell._syncServices()
}
}
Connections {
target: shell.barWidgetRegistry
function onChanged() { shell.syncPluginApis() }
}
Connections {
target: shell.appLibrary
function onAppsChanged() {
for (var id in shell._pluginAppLibraryApis)
shell._pluginAppLibraryApis[id].appsChanged()
}
}
// Writes inline settings to a bar layout entry or top-level plugin entry in
@@ -650,10 +1324,10 @@ ShellRoot {
onLoaded: {
if (!item) return
if ("omarchyPath" in item) item.omarchyPath = shell.omarchyPath
if ("shell" in item) item.shell = shell
if ("manifest" in item) item.manifest = panelEntry.manifest
if ("barWidgetRegistry" in item) item.barWidgetRegistry = shell.barWidgetRegistry
if ("pluginRegistry" in item) item.pluginRegistry = shell.pluginRegistry
if ("shell" in item) item.shell = shell.pluginShellFor(panelEntry.manifest)
if ("manifest" in item) item.manifest = shell.publicPluginManifest(panelEntry.manifest)
if ("barWidgetRegistry" in item) item.barWidgetRegistry = shell.pluginBarWidgetRegistryFor(panelEntry.manifest)
if ("pluginRegistry" in item) item.pluginRegistry = shell.pluginRegistryFor(panelEntry.manifest)
// Plugins that pair a panel UI with a service entry read shared
// state off `service`. Hand them the matching singleton if one was
// loaded.
@@ -719,7 +1393,8 @@ ShellRoot {
schema: meta.schema || [],
pluginId: manifest.id,
sourceDir: manifest.__sourceDir || "",
source: "plugin"
source: "plugin",
firstParty: !!manifest.__isFirstParty
}
// A load already in flight for this URL registers itself when it