Trust & scopes
A plugin is trusted through two layers. For the jail: an admin consents to each
capability at install (POST /api/plugins/install → /confirm, admin-gated), and
the Deno sandbox enforces exactly the granted set — see
Sandboxed packages. For the declarative seams: a
closed scope taxonomy plus a per-installation scoped token gate every HTTP
call a seam makes. There is no marketplace; the host is the trust boundary — a plugin
runs under one operator’s host, scoped to its projects. (Web install is admin-gated;
a manifest-only plugin can also be sideloaded by an operator with host access.)
This page covers the scope/scoped-token layer; capability consent is documented with
the jail. The two coexist — a package that ships a main.ts and declares seams is
gated by both.
The scoped token
Section titled “The scoped token”Each plugin installation — a (pluginId, projectPath) pair — gets its own token
(the GitHub-Apps model). It is the capability gate over every seam.
Lifecycle — minted on enable, exposed only while enabled. There is no separate
“install” step: dropping a directory into a plugins root only makes the plugin
discovered (disabled by default). The per-install token is minted the first time
the plugin is enabled for a project, and GET /api/plugins?projectPath=<abs>
returns the token only for that user’s enabled plugins (the field is omitted for
disabled ones). When a panel plugin is launched, the web client passes the returned
token into the mounted iframe as the lowkey_plugin_token query parameter (see
surfaces); the plugin’s service then presents it
on every call back to Lowkey.
Disable = revoke. The scoped-token guard re-checks enablement on every
request: a call made with the token of a now-disabled installation is denied
(plugin_disabled, 403). Disabling a plugin therefore cuts off its access without
rotating any secret.
- Format:
lkp_+ random. The service authenticates back to Lowkey by sending it asAuthorization: Bearer lkp_…orX-Lowkey-Plugin-Token: lkp_…(header). The iframe receives it as thelowkey_plugin_tokenquery param; the service sends it as a header. - Scope of authority: the token authorizes calls only for its plugin, only in its project, and only against the scopes its manifest declares.
- Isolation: a plugin cannot read another plugin’s token or a provider’s secrets. Provider-credential access is refused outright in v1.
- Per request: every scoped route checks the token, the
projectPathmatch, that the installation is registered and enabled, the manifest’s declared scope, and (where relevant) credential access — in that order.
The token is bound to the installation, not to a user session.
Scope taxonomy
Section titled “Scope taxonomy”The closed set (also in the manifest reference):
| Scope | Grants | Trust tier |
|---|---|---|
cards |
Emit closed cards into the feed. | Third-party OK. |
mcp |
Register an MCP server, injected per project. | Third-party OK. |
webhook |
Inbound trigger + turn-completion subscription. | Third-party OK. |
focus |
Mount a surface in the main pane. | Third-party OK. |
side-panel |
Mount a surface in the side panel. | Third-party OK. |
session-read |
Read a session’s transcript + lineage. | Consentable third-party. |
agent |
Contribute an AgentDef (prompt, mode, translator, runtime). |
Consent-worthy privilege. |
Not in v1 (deferred):
media— camera/mic delegated to a surface’s iframe via Permissions-Policy. When it lands it is first-party / trusted only; the highest-stakes scope.storage— a filesystem/storage-provider seam. Deferred until a storage/transport plugin is built; gated on the credential broker.
Consent
Section titled “Consent”Scopes are declared in the manifest and consented to when the plugin is enabled for a project (the point at which its scoped token is minted). Two trust distinctions matter when you design a plugin:
agentis a privilege, not just an API scope. A plugin that only contributes anAgentDefcalls no API, yet it runs on the operator’s provider credential, speaks to users, and (viaplatformPromptMode: minimal) can lower the safety floor. It is consent-worthy on its own.session-readis consentable for third parties — it reads the session’s own transcript + lineage, not platform internals — but it is still a deliberate grant a user makes knowingly.
First-party-only seams
Section titled “First-party-only seams”Some seams are reserved for first-party / trusted plugins and are not granted to third parties in v1:
mediadelegation (deferred; first-party-only when it lands).platformPromptMode: none— not exposed at all in v1; a first-party-only trapdoor if/when it returns, behind a no-tools-attached guarantee.
Everything else in the closed scope set is available to third parties, subject to consent.
Deny reasons
Section titled “Deny reasons”Scoped routes return a JSON body { "error": "unauthorized" | "forbidden", "reason": "<reason>" }:
| Reason | Status | Cause |
|---|---|---|
missing_token |
401 | No token on the request. |
invalid_token |
401 | Token not recognized. |
missing_project |
403 | No projectPath supplied. |
project_mismatch |
403 | projectPath ≠ the token’s installation project. |
plugin_not_registered |
403 | No manifest resolves for the installation. |
plugin_disabled |
403 | The installation exists but is disabled for the project (revoked access). |
plugin_mismatch |
403 | Resolved manifest id ≠ the token’s plugin id. |
undeclared_scope |
403 | The route’s scope is not in the manifest’s scopes. |
credential_forbidden |
403 | The route asked for credential access the token may not have. |
Signing
Section titled “Signing”The manifest carries signing (publicKey, signature). Designed-for but
unenforced in v1 — integrity verification lands with the catalog. Include the
fields; do not rely on enforcement yet.