Skip to content

Security model

What the host enforces around a plugin, and what it does not. Use it to decide whether to install someone else’s plugin, or what your own plugin can be trusted with. Anything declared but not enforced, or shipped but not implemented, is stated as such.

Party Trusted for Not trusted for, or the limit
Host Everything: it owns state, credentials, secrets and install decisions -
Desktop app, over the private loopback port Admin, with a per-launch secret instead of a token A process running as the same user can read that secret - see loopback trust
Plugin process Scope plugin: the plugin protocol surface, and nothing else Not sandboxed: it runs with the user’s full privileges. Declared permissions are not enforced, except host:adb
Deck clients (web client, companion) Scope client: the viewer-safe endpoints Plugin endpoints refuse them
LAN callers and browsers The public listener’s API and web client Every plugin endpoint refuses them
Creator Portal and Store Signing Store plugins after checking the publishing workflow’s provenance; publishing the signed registry and its revoked keys Revocation refuses new installs and updates, not a plugin that is already installed

Your plugin’s own listener binds 127.0.0.1:0 by default, and the host’s plugin endpoints answer only from loopback. Exposing custom endpoints of your plugin to the network is a separate, explicit security decision.

Listener Reachable from Notes
Public listener The LAN Serves the API and the web client. HTTPS is optional and configurable - see ADR 0040. While its plain-HTTP port is serving, the host announces its instance name, version and that port on the LAN over mDNS (_macrodeck._tcp), unless this is turned off in Settings > Network - see ADR 0082.
Private loopback listener The local machine only Never gets TLS: the desktop shell and the plugin SDK reach it over plain HTTP on 127.0.0.1.
Rule Consequence
Plugin endpoints are served on both listeners but accept only a loopback remote address A LAN caller is refused whichever port it used. A self-registering plugin must run on the same machine as the host (ADR 0028)
A browser-shaped request (cross-site Origin or Sec-Fetch-Site) is refused Plugin paths are also excluded from the app-wide CORS policy
Enabling HTTPS on the public listener does not widen this The local-only gate grants no principal of its own: reaching it from another listener adds reachability, not authority
/_macrodeck/* on the plugin’s listener is reserved (/_macrodeckery is not) Middleware added with Configure runs after the SDK’s and cannot answer there; a constant path there is analyzer error MDP2005. See reserved routes

A request on the private loopback port, from a loopback address, with a loopback Host header, is authenticated as admin only when it also presents a secret the desktop app generates for every launch: the app itself sends it as a header, and its window holds a session cookie derived from it. That is how the desktop UI works without handling a token.

Reaching the port is therefore not enough: another account on the same computer, a local tool that fetches URLs for someone else, a sandboxed process or a browser tab gets no admin rights there. ADR 0098 states the limit that remains: a hostile process running as the same user can read the secret, as can anything with that user’s file access, such as WSL on Windows, and is outside what this boundary defends against. The Host header check is a DNS-rebinding guard, and a request a browser marks as cross-site is refused even with the cookie.

Plain HTTP on the LAN leaves tokens visible to on-path attackers, so the public listener can be configured for TLS. The shipped posture is a self-signed certificate: it “exists to encrypt a link the user already trusts, not to prove identity to strangers”.

Authorisation is deny-by-default: an endpoint with no authorisation metadata is admin-only.

Scope Grants
admin Everything, including the plugin installation surface
client The viewer-safe endpoints a deck client needs
plugin The plugin protocol surface, and nothing else
plugin:enroll Developer (enrollment) tokens - the only Developer-token scope defined today

plugin is a peer scope, not a subset. A plugin session token does not grant client access to the deck-viewing endpoints, and neither an admin token nor a client token is accepted where a plugin token is required.

A plugin holds one credential to buy a session, then sends the session token on every request. The mechanics are in the authentication guide; this is how they are stored, revoked, and must be handled.

# Right: the session token in the Authorization header, the secret only on the session exchange
POST /api/plugins/sessions HTTP/1.1
X-MacroDeck-Plugin-Id: com.example.ref-probe
X-MacroDeck-Plugin-Secret: <plugin-secret>
GET /plugins/ws HTTP/1.1
Authorization: Bearer <session-token>
# Wrong: a token in the query string lands in logs
GET /plugins/ws?access_token=<session-token> HTTP/1.1
Credential Where it is kept Rules
Launch bootstrap token (managed) Host: memory only, keyed by hash. Minted per launch, discarded on every exit path A process that outlives its own termination sequence can never present a token the host still recognises. The managed credential store refuses to save it
Developer token Host: SQLite, as a SHA-256 hash. Plaintext shown exactly once Never persist the plaintext; a caller that displays it should discard it
Per-plugin secret Host: SQLite, as a SHA-256 hash, plaintext shown once. Plugin: credentials.json in its state directory, not encrypted at rest Sent only to POST /api/plugins/sessions - never on the WebSocket upgrade, in a query string or in a log line
Session token Plugin memory; 15-minute JWT Authorization header only - never a cookie or a URL
PKCE codeVerifier Plugin memory Sent only to the redemption call
  • Hashing. Verification is constant-time. A plain SHA-256 hash is used deliberately: Developer tokens and per-plugin secrets are high-entropy random values, unlike user passwords, which use PBKDF2.
  • The plugin’s copy of its secret is written owner-only, to a temporary file made owner-only before anything is written to it, then moved into place. On Windows the per-user profile ACL is the protection.
  • Revocation cascades. Revoking a Developer token revokes every registration derived from it and terminates those sessions immediately. Revoking one registration terminates that plugin’s session. The host re-checks the live session registry on every request, so a terminated session stops working at once even though its 15-minute JWT is still cryptographically valid.
  • Identity is never taken from a claim. The host checks the credential against its own record. MACRO_DECK_PLUGIN_LAUNCH_ID is yours to log and nothing more.
  • Client sessions (web client, companion) hold an access token - 15 minutes for the admin surface, 60 days for a deck device - and a refresh token that rotates on every use and lives 365 days. Signing a device out in the devices list, removing it, or changing the account password or username is checked on every request from then on, so a long device token ends the moment you end the session rather than when it expires - every device at once for a credential change. A client session that is not tied to a device keeps the 15 minute token, because there is nothing to revoke it by. A rotated refresh token presented again within 30 days of its rotation revokes that token’s rotation chain - the session it belongs to - and leaves every other session of the account signed in. One exception: the token rotated out most recently is accepted once more, from the same device and scope, so a client whose rotation response was lost in transit can retry - within a minute of the rotation, or, when the host restarted in between, within ten minutes of it starting up - and then only for the rotation that restart interrupted, because a restart is exactly when a rotation response goes missing. The companion pairs with a six-digit, single-use code from the desktop app’s network panel: one code at a time, minted only on the loopback listener, cleared after five failed guesses from any caller, and throttled globally. See ADR 0083.

By default a self-registering plugin gets its per-plugin secret by pairing, not from a Developer token: it creates a request, the user approves it in the desktop app, and the plugin redeems it. Mechanics: the authentication guide.

Guarantee Detail
Loopback only Pairing endpoints are ordinary plugin endpoints: served on both listeners, reachable only from a loopback remote address
Gated by Developer Mode, continuously Off by default. Turning it off invalidates redemption of an already-approved request, so no stale approval survives it
Approved only in the trusted desktop app The prompt is rendered over the private loopback transport (ADR 0003). An admin bearer token over the network listener is not sufficient to approve: approval is a human act in a specific UI, not an API call
Proof of possession The plugin presents a PKCE-style verifier at redemption; the host stores only the SHA-256 challenge, never the verifier. requestId is unguessable but authorises nothing on its own, so it is safe in a URL
One-time, short-lived, one per plugin id, capped, rate-limited Expires on a host-advertised timer; consumed permanently on successful redemption; a second create for a plugin id with a live request is refused with 429 instead of replacing it; a global cap on pending requests; separate rate limits on creation and redemption
Nothing persists past a host restart Requests are memory-only in every state - pending, approved, rejected
Self-reported fields shown as unverified; transport origin verified Executable path, process id, SDK version and display name are shown as reported, labelled unverified, since deriving them from the loopback socket would authenticate nothing. Whether the request arrived on the public listener is verified and shown as verified
Replacement is atomic at redemption If the host holds a registration for the plugin id but the local credential is gone, the prompt offers, with a separate confirmation, to replace it. Redemption rotates the secret in place and terminates live sessions in the same step, so old and new never both authenticate. The secret is minted at redemption, never at approval: an unredeemed approval leaves an existing working credential untouched

Pairing uses the same loopback check as the rest of the plugin protocol, which has no port clause: a device reaching the loopback address through an adb reverse tunnel is indistinguishable at the socket level from a local process (ADR 0030). Pairing does not widen that existing exposure. A connection from a USB link without debugging is different: the host dials it itself and marks it, so it is never taken for a local process and cannot reach pairing (ADR 0095).

What pairing changes is what a hostile local process needs. Before, it could mint a Developer token itself through the then credential-less loopback admin. Now it needs a human to approve a specific prompt while Developer Mode is on, unless it runs as the same user and reads the desktop app’s loopback secret. That is bounded by Developer Mode being off by default, approval only over the trusted transport, a prompt that names what is approved and labels unverified fields, the one-request-per-plugin-id and rate-limit rules, and nothing being minted before proof of the verifier - a bound, not a cryptographic guarantee, as recorded in ADR 0028.

Declare a secret field in a setup flow, and read it back only when you need it:

ActionParameter.Secret("api_key", label: Strings.Setup.ApiKey(), required: true)
// Values the user never typed, such as OAuth tokens:
["access_token"] = ConfigFlowValue.Secret(token.AccessToken),
// At runtime - decrypted only on request:
var apiKey = await context.Config.GetSecretAsync(entry.Id, "api_key");
Rule Detail
Stored encrypted Secret and Password fields and ConfigFlowValue.Secret values go into SQLite through ASP.NET Core Data Protection. Plain values are stored as they are
Referenced, never inlined Configuration refers to a secret as { "$secret": "<id>" }, so no flow or action document carries plaintext
Key ring protected by the OS The Data Protection key ring on disk under the host’s data root is encrypted under a key in the OS credential store - Windows Credential Manager, macOS keychain, or Secret Service on Linux - so a copy of the data directory does not carry the key that opens it
Honest fallback With no credential store, and in a portable installation, the key ring stays readable on disk and the host reports that state rather than implying protection
Never log or show Secrets never belong in logs or user-visible errors: log the failure type, not the value

See setup flows and ADR 0047.

Check an artifact yourself, or in CI, before it reaches a host:

Terminal window
macrodeck-plugin verify ./artifacts/com.example.hello-deck-1.0.0-linux-x64.macroDeckPlugin

A .macroDeckPlugin artifact is a ZIP whose root is the version directory. Before a byte reaches its destination, the installer enforces:

Check Rejects
Path safety (zip-slip) Absolute paths, Windows drive letters, .. segments, any resolved destination outside the target
Filesystem traps Illegal filename characters, reserved Windows device names (CON, PRN, COM1, …), any symlink, fifo, socket or device node
Zip-bomb caps Entry count, per-entry uncompressed size, total uncompressed size, compressed size, compression ratio - measured live through a bounded stream, never taken from the archive’s central directory
Payload identity When the manifest declares files[]: a file whose SHA-256 or size does not match, and any extracted file not declared
Identity id or version that does not match the directory names they install under

Nothing from an artifact is ever executed during install, activate or uninstall. The manifest has no hook field; the only process a version directory ever produces is the supervisor’s own health-gated launch after activation. The execute bit is set only on the entrypoints for the current runtime identifier, and a self-contained entrypoint may not be a shell script.

Activation is atomic. current.json is written to a sibling temporary file and renamed over the real one - a single filesystem operation on every supported platform - so it always names the previous version or the new one. If the new version does not reach a healthy running state in time, the rename is undone, the failed version is removed, and the previous version is restarted if it was running. See ADR 0029.

Signing: the Creator Portal signs, and the host verifies before install and before every load

Section titled “Signing: the Creator Portal signs, and the host verifies before install and before every load”

Verification lives in MacroDeck.Signing, used by both the host and the macrodeck-plugin CLI’s verify and keygen/sign commands.

When it is checked: in a staging directory before anything is written into the plugin directory, and again on every launch of an installed plugin. The plugin directory is user-writable on every platform, so editing a plugin’s files after installation, or stripping the certificate from one installed as trusted, stops it from loading.

The host resolves a package to exactly one verdict:

Outcome Verdict Blocks install?
Signature valid, chained to the Macro Deck root directly or through an issuer certificate Trusted No
No signature declared Unsigned Only without consent
Signature block malformed Malformed Yes
Signature present but does not verify SignatureInvalid Yes
Contents do not match the signature ContentMismatch Yes
Certificate does not chain to the pinned root UntrustedRoot Yes
Certificate issued for another purpose WrongCertificatePurpose Yes
Certificate not valid at signedAt CertificateNotValidAtSignature Yes
Certificate, or the issuer that signed it, revoked by the Store registry Revoked Yes, at install and update only
Package unreadable, or algorithm unknown to this host VerificationUnavailable Yes

Unsigned is the only verdict a confirmation can admit, because nothing published today is signed yet:

Source of an unsigned plugin Installs?
A .macroDeckPlugin you select yourself, open through the file association, or upload After a deliberate confirmation, and stays marked unverified
The Store or registry, Developer Mode on Refused first; installs only if you then confirm the same warning. Developer Mode is read by the host from its own settings, so asking for the exception without it changes nothing
The Store or registry, Developer Mode off No
An update to a plugin installed as Trusted No, regardless of consent
An update Macro Deck found for you Only ever reported to you, never installed unsigned on your behalf

Icon packs and profile templates have no signature of their own and are unaffected. A signature that is present but does not verify is a failure, not a weaker kind of unsigned: no confirmation installs it. An unknown signature algorithm fails closed rather than being treated more leniently than no signature.

Signature format. Every signable format - .macroDeckPlugin, .macroDeckIconPack, and the portable .macroDeckProfile, .macroDeckFolder and .macroDeckWidget - carries its signature in its own manifest and its certificate as certificate.json and certificate.sig at the archive root, plus issuer.json and issuer.sig when an issuer certificate signed that certificate. There is no detached signature file: a signed artifact verifies on its own. The signature covers a format-specific canonical digest - the package identity and the declared file list, never the manifest’s own JSON encoding - so reformatting a manifest does not invalidate a signature, while adding a file or repointing an entrypoint does. See the manifest’s signature field and the certificate (v1, v2) and package signature schemas.

Trust anchor. MacroDeck.Signing.MacroDeckRootKey (formerly MacroDeckHost.Domain.Security.MacroDeckRootKey, which no longer exists) holds the public half of an offline Ed25519 key pair, verification-only. The private half never exists on a build machine or in CI, and is not any of the release-signing keys. --root-public on sign and verify points at a different root for testing; both commands then warn non-production-root and report a result not anchored to the Macro Deck root.

Certificate chain. A certificate is signed either by the root itself or by exactly one issuer certificate the root signed, so the root can stay offline while the Creator Portal issues certificates with the issuer’s key. The rules are strict:

  • An issuer certificate carries exactly the issuer key usage and the issuer subject kind, uses schemaVersion 2, is signed by the root, and names no issuer of its own. There is never a second intermediate level.
  • A certificate it signs uses schemaVersion 2, names the issuer’s certificateId in issuer and the same rootKeyId, carries exactly package or registry, and has a validity window inside the issuer’s.
  • An issuer certificate never signs a package or a registry manifest, and a package or registry certificate never signs another certificate.
  • Both validity windows are evaluated at the signature’s signedAt.
  • A schemaVersion 1 certificate is always signed by the root, exactly as before.

The registry publishes an issuer certificate under certificates/ beside the certificates it signed. Macro Deck versions from before this chain refuse an issuer-signed certificate. See ADR 0096.

Store artifacts are signed by the Creator Portal, not by their author. Publishing runs as Trusted Publishing: the plugin’s CI workflow authenticates to the Creator Portal with its own workload identity, the Portal verifies that the workflow is a trusted publisher and checks the run’s provenance, then signs the artifact server-side. Plugin developers and their CI workflows do not generate, receive, manage or hold signing keys, certificates or signing credentials - no signing secret in a repository or CI secret store, and no manual artifact upload in the publishing path. Certificate issuance and revocation also live in the Creator Portal. See Publishing to the Store and ADR 0042.

The CLI signs and verifies; it never issues trust. The public CLI has no root key generation, no certificate issuance and no registry signing, and keygen/sign is not the Store path - it is for artifacts distributed outside the Store and for Macro Deck’s own infrastructure. keygen produces a creator Ed25519 key pair and nothing else; sign requires a certificate only the Creator Portal can issue, validates the whole artifact against it, and embeds the signature. verify checks the certificate chain, the certificate’s validity at the signature’s own signedAt rather than at verify time, the canonical digest, and every declared file’s hash and size. See sign and verify.

Limit What it means
verify never consults revocation It says so on every run, in both output formats. A valid verdict is a fact about the signature and chain at signing time, not a live trust decision
Revocation stops new installs, not installed plugins The revoked keys come from the security.json of the signed Store registry the host has loaded. A package whose certificate, or whose certificate’s issuer, is listed there is refused at install and update. A version that is already installed keeps activating and launching, and the Store marks it Certificate revoked. See ADR 0096
Revocation needs a loaded registry A host that has not loaded a registry snapshot yet, or never can because it is offline, answers Unavailable, and that deliberately does not block: failing closed would refuse every signed plugin. See ADR 0044
A leaked signing key must be contained by other means Treat everything signed with it as untrusted. For the Store that is a Creator Portal concern, since the keys are the Portal’s; outside the Store see private-key handling
signedAt is not authenticated No canonical digest covers it, so the key holder can set any value. Checking validity at signedAt is advisory against that key holder: it only protects a package from its certificate’s later expiry. Revocation is the control that stops new installs signed with a compromised or misused key
publisher is a claim It becomes an attribution only behind a Trusted verdict. Verified publisher identity comes from the Creator Portal having checked the workflow’s provenance before signing, never from the manifest
Only plugins carry a publisher signature Store icon packs and profile templates are authenticated by the signed registry: the pinned root signs the registry’s certificate, directly or through an issuer certificate, that certificate signs the registry manifest, and the manifest carries the digest and size of every file, including the release manifest that declares each artifact’s digest. That proves the bytes are the ones the Macro Deck registry published, nothing more: they are never presented as publisher-verified, and a publisher’s revoked certificate does not apply to them

Permissions: declared, and enforced only for ADB

Section titled “Permissions: declared, and enforced only for ADB”
{ "permissions": ["host:variables", "host:adb"] }

A manifest may declare permissions from a fixed vocabulary (host:variables through device:usb), mirroring the host callback surface one for one. They are parsed, shape-validated, persisted and surfaced through the installed-plugins API so a consent surface has something real to render.

The host gates exactly one host API on a declared permission: adb, on host:adb. Every other host callback is served whether or not the calling plugin declared the permission that covers it. Plugins that predate the vocabulary declare nothing, so a default-deny posture for the rest would break every plugin already running, and a default-allow posture would not be a boundary worth the name.

host:adb is enforced because it is new: no existing plugin relied on it. A plugin reaches Macro Deck’s ADB connection only when ADB is enabled, the user has left Allow plugins to use ADB on, and the active version of the installed plugin declares host:adb. Installing a plugin that declares it while ADB or that setting is off asks the user whether to turn both on. A self-registered session, admitted through a developer token or interactive pairing, is exempt from the declaration because the user admitted it by hand. See Android devices and ADR 0092.

This is user control, not isolation. A plugin process is not sandboxed and could run its own adb without declaring anything. host:adb gives the user a visible, switchable decision about plugins using Macro Deck’s connection; it does not stop a hostile plugin from reaching a device. Local paths an adb call names are read and written with Macro Deck’s identity, which is the same user the plugin already runs as.

Treat the other declared permissions as documentation of intent, not as a constraint on what a plugin can reach.

Plugins and integrations can exchange messages through Macro Deck’s message channel. Macro Deck stamps every message with the sender’s integration id, so Sender is reliable, but it authorizes nothing: any plugin can publish on any topic, send commands and requests to any handled topic, and handle any topic nobody has claimed yet. Treat a message like any other input from another process: validate its payload, and check Sender before acting on a command where it matters who asked. host:messaging is declared, not enforced.

A video stream provider gives Macro Deck the URL of a stream, and Macro Deck relays the media to every client signed in to Macro Deck, the desktop app or any paired device. The provider’s URL never reaches a client, so credentials in it stay on the computer; a provider still must not serve anything it would not show every signed-in client, and never puts the source’s own password or API key in a URL. Macro Deck never logs a description. host:video-streams is declared, not enforced.

The relay is a deliberate, bounded fetch on behalf of a plugin:

  • The relay URL is a capability. /api/video-streams/relay/<token>/... carries an unguessable token that is bound to one session, sent only over the authenticated UI WebSocket to the connection that owns the session, and dead once the session ends. The route is anonymous by design, because an image or video element cannot send an authorization header. Anyone who holds the URL can read that one stream until it ends, which is no more than the client it was sent to could. Macro Deck redacts the token from its logs.
  • It pins the origin of the provider’s URL and nothing else. Redirects and HLS playlist URIs stay on that origin (apart from data: and skd: URIs, which are passed on untouched), and only GET and HEAD are sent. The provider chooses the origin, so a plugin can point the relay at any service on the computer or the local network, or at Macro Deck itself. A plugin could make those requests without the relay, so this adds no capability, but the relay returns the response to a client, which is why only responses that look like the session’s media are passed on.
  • Plugin bytes cannot run as a page. Content types are checked against the session’s transport, responses carry X-Content-Type-Options: nosniff and a sandboxing Content-Security-Policy, and a response that does not fit is refused, so a plugin cannot serve a script or an HTML page from Macro Deck’s own origin.
  • It is bounded. Eight concurrent relayed requests per session, 64 in all, a header timeout and an idle timeout on media.

A tree can name any provider’s stream in a macrodeck.video-stream, so any plugin’s view can make the clients that draw it open sessions on another plugin’s provider. That is no wider than what a signed-in client can already open, but it means a stream’s audience is everyone who can see any Macro Deck view, not only the views its own plugin draws.

_logger.Information("Authenticated as {User}", account.DisplayName); // not the token

The host redacts every event once, before any sink, to ***: values of sensitive keys (password, token, secret, api_key, authorization, client_secret and similar) in key=value or "key": value form, credentials in URLs (https://user:pass@), Bearer/Basic/Digest credentials, JWTs and PEM private keys. That is a safety net, not a licence: a secret in an unrecognised shape is written as-is into a file users attach to bug reports. Plugin identity on a log line comes from the authenticated session, so a plugin cannot log under another integration’s name. See logging.

Read this before deciding what a plugin should be trusted with.

Limitation Detail
A plugin runs with the user’s full privileges A managed plugin is an ordinary child process: no sandbox, container, separate account or privilege reduction. It can do anything the user can - read and write their files, open network connections, start processes. Installing one is equivalent to running any other downloaded program
Permissions are not a boundary A plugin that declares nothing can still reach every host API except adb, and even host:adb gates only Macro Deck’s own ADB connection, not a plugin’s own - see above
An unsigned plugin is still admitted on your say-so See signing. For an unsigned install the declared-digest check is corruption detection, not a boundary: whoever can rewrite the binary can rewrite the unsigned manifest. Trust rests on where you got the file
Revocation stops new installs only A plugin installed before its certificate or issuer was revoked keeps running; the Store marks it, and removing it is your decision
A process running as the same user can become admin It can read the desktop app’s loopback secret. The model defends against LAN callers, browsers and other local accounts, not a hostile process running as the same user
The plugin secret and session token cross plain HTTP on loopback The local-only rule confines that to processes already on the machine, but it is not encryption
A self-signed public certificate proves nothing about identity It encrypts the link; it does not authenticate the host to a stranger, and a DHCP change means regenerating it
Supervision contains failure, not intent Isolation is not a goal. The child’s environment is scrubbed of inherited MACRO_DECK_PLUGIN_* and ASPNETCORE_URLS; the listener port is bound by the host and handed to the child; a fresh credential is minted per launch and discarded on exit; a restart budget stops a crash loop. See ADR 0029
A hard kill is a hard kill A plugin that does not exit within its graceful timeout has its process tree killed. An SDK that does not recognise close code 4004 gets no notice and no chance to flush (“kill-only” in ADR 0029)
macOS arm64 and unsigned binaries An unsigned, freshly extracted Mach-O needs at least an ad-hoc signature on Apple silicon before the kernel runs it, and the installer does not add one. This is a known, unsolved gap.

Report privately through GitHub’s private vulnerability reporting, never a public issue, discussion or Discord. What to include, supported versions and scope are in SECURITY.md.

The flaw is in Report to
Macro Deck: host, first-party clients, built-in integrations, SDK, plugin infrastructure This repository, privately
A third-party plugin that exploits Macro Deck or bypasses a boundary Macro Deck should enforce This repository, privately
Only a third-party plugin Its author, or its repository’s security process. Store plugin with no suitable contact: report it through the Store
  • Authentication - credentials, session exchange, and what a plugin must never do with either.
  • Plugin hosting - the artifact format, the installer’s rejection rules, and what the supervisor injects.
  • Compatibility policy - what is frozen and what may change.