mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
docs(plugins): plugin wiki + reference plugin (M7)
- Wiki pages (sync to the GitHub wiki on push to main): Plugins overview + trust model, Plugin Development (SDK, definePlugin, ctx, routes/jobs, the client bridge, testing with the mock host), Plugin Permissions reference, and Publishing (registry PR + CI gates + provenance). Linked from the sidebar. - Reference plugin plugin-sdk/examples/trip-countdown: a complete, minimal- permission widget (reads trip data through ctx, renders in the sandboxed iframe via the bridge, filled-in README). Validated in the SDK test suite so it passes the exact gate authors face.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
# Trip Countdown
|
||||
|
||||
> A dashboard widget that counts down the days to your next trip.
|
||||
|
||||

|
||||
|
||||
## What it does
|
||||
|
||||
Trip Countdown adds a small dashboard widget showing how many days remain until
|
||||
your upcoming trip starts. When a trip is under way it shows "in progress"
|
||||
instead. It's the reference plugin for the TREK plugin system — a minimal but
|
||||
complete widget that reads trip data through the sandbox and renders it in an
|
||||
isolated iframe.
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||
## Permissions
|
||||
|
||||
| Permission | Why this plugin needs it |
|
||||
|---|---|
|
||||
| `db:read:trips` | To read the current trip's start date so it can compute the countdown. The read is membership-checked by TREK, so the widget only ever sees a trip you already have access to. |
|
||||
|
||||
It requests **no** network access and stores **no** data of its own.
|
||||
|
||||
## Setup
|
||||
|
||||
None. Enable plugins on your instance (`TREK_PLUGINS_ENABLED=true`), install and
|
||||
activate Trip Countdown, and the widget appears on your dashboard.
|
||||
|
||||
## Building
|
||||
|
||||
The widget ships plain JS/HTML — no build step. `server/index.js` is the plugin
|
||||
entry; `client/index.html` is the widget iframe.
|
||||
|
||||
## License
|
||||
|
||||
MIT — see the TREK repository.
|
||||
@@ -0,0 +1,62 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Trip Countdown</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
body { margin: 0; font: 14px system-ui, sans-serif; display: grid; place-items: center; height: 100vh;
|
||||
background: transparent; color: #111827; }
|
||||
body[data-theme="dark"] { color: #e5e7eb; }
|
||||
.card { text-align: center; padding: 16px; }
|
||||
.days { font-size: 40px; font-weight: 800; letter-spacing: -0.02em; }
|
||||
.label { font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.6; margin-top: 2px; }
|
||||
.name { margin-top: 6px; opacity: 0.85; }
|
||||
.empty { opacity: 0.6; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card" id="root"><span class="empty">Loading…</span></div>
|
||||
<script>
|
||||
var ctx = null
|
||||
var reqSeq = 0
|
||||
|
||||
function send(msg) { window.parent.postMessage(msg, '*') }
|
||||
|
||||
function render(data) {
|
||||
var root = document.getElementById('root')
|
||||
if (!data || !data.trip) { root.innerHTML = '<span class="empty">No upcoming trip</span>'; return }
|
||||
var d = data.trip.days
|
||||
root.innerHTML = d == null
|
||||
? '<div class="name">' + escapeHtml(data.trip.name) + '</div><div class="label">in progress</div>'
|
||||
: '<div class="days">' + d + '</div><div class="label">days to go</div><div class="name">' + escapeHtml(data.trip.name) + '</div>'
|
||||
}
|
||||
|
||||
function escapeHtml(s) { return String(s).replace(/[&<>"]/g, function (c) {
|
||||
return ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c] }) }
|
||||
|
||||
function load() {
|
||||
var id = 'r' + (++reqSeq)
|
||||
send({ type: 'trek:invoke', requestId: id, sub: '/next?tripId=' + (ctx && ctx.tripId ? ctx.tripId : ''), method: 'GET' })
|
||||
window._pending = id
|
||||
}
|
||||
|
||||
window.addEventListener('message', function (e) {
|
||||
var m = e.data
|
||||
if (!m || typeof m !== 'object') return
|
||||
if (m.type === 'trek:context') {
|
||||
ctx = m
|
||||
document.body.dataset.theme = m.theme
|
||||
load()
|
||||
} else if (m.type === 'trek:response' && m.requestId === window._pending) {
|
||||
render(m.data)
|
||||
} else if (m.type === 'trek:error' && m.requestId === window._pending) {
|
||||
document.getElementById('root').innerHTML = '<span class="empty">Unavailable</span>'
|
||||
}
|
||||
})
|
||||
|
||||
// Ask TREK for context (theme, current trip id) — it replies with trek:context.
|
||||
send({ type: 'trek:ready' })
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,43 @@
|
||||
// Trip Countdown — reference widget plugin.
|
||||
//
|
||||
// Demonstrates the full author path: definePlugin, a permission-gated route that
|
||||
// reads trip data through ctx, and a response the client iframe renders. Runs
|
||||
// isolated; ctx is the only way to reach TREK.
|
||||
const { definePlugin } = require('trek-plugin-sdk')
|
||||
|
||||
/** Days (integer, >= 0) between now and an ISO date, or null if past/absent. */
|
||||
function daysUntil(iso) {
|
||||
if (!iso) return null
|
||||
const then = new Date(iso).getTime()
|
||||
if (Number.isNaN(then)) return null
|
||||
const diff = Math.ceil((then - Date.now()) / 86_400_000)
|
||||
return diff >= 0 ? diff : null
|
||||
}
|
||||
|
||||
module.exports = definePlugin({
|
||||
routes: [
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/next',
|
||||
auth: true,
|
||||
async handler(req, ctx) {
|
||||
// The client passes the dashboard's current trip id; the host enforces that
|
||||
// req.user actually has access to it, so a plugin can't read foreign trips.
|
||||
const tripId = Number(req.query.tripId)
|
||||
if (!tripId) return { status: 200, headers: json(), body: JSON.stringify({ trip: null }) }
|
||||
|
||||
const t = await ctx.trips.getById(tripId, req.user.id)
|
||||
const days = daysUntil(t && (t.start_date || t.startDate))
|
||||
return {
|
||||
status: 200,
|
||||
headers: json(),
|
||||
body: JSON.stringify({ trip: t ? { name: t.name, days } : null }),
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
function json() {
|
||||
return { 'content-type': 'application/json' }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"id": "trip-countdown",
|
||||
"name": "Trip Countdown",
|
||||
"version": "1.0.0",
|
||||
"apiVersion": 1,
|
||||
"author": "TREK",
|
||||
"description": "A dashboard widget counting down the days to your next trip.",
|
||||
"homepage": "https://github.com/mauriceboe/TREK",
|
||||
"license": "MIT",
|
||||
"icon": "CalendarClock",
|
||||
"type": "widget",
|
||||
"trek": ">=3.2.0 <4.0.0",
|
||||
"nativeModules": false,
|
||||
"permissions": ["db:read:trips"],
|
||||
"capabilities": {
|
||||
"widget": { "title": "Trip Countdown", "defaultSize": "small" }
|
||||
},
|
||||
"routes": [
|
||||
{ "method": "GET", "path": "/next", "auth": true }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
# Plugin Development
|
||||
|
||||
Build a plugin with the `trek-plugin-sdk` package. A plugin is a directory with a
|
||||
manifest, a built server entry, and (for page/widget plugins) a client bundle.
|
||||
|
||||
## Scaffold
|
||||
|
||||
```bash
|
||||
npx create-trek-plugin my-plugin --type integration|page|widget
|
||||
cd my-plugin
|
||||
```
|
||||
|
||||
This emits:
|
||||
|
||||
```
|
||||
my-plugin/
|
||||
trek-plugin.json # manifest
|
||||
server/index.js # your plugin code (built, plain JS)
|
||||
client/index.html # page/widget iframe (page/widget only)
|
||||
README.md # fill this in — the registry requires it
|
||||
```
|
||||
|
||||
## The three plugin types
|
||||
|
||||
- **integration** — extends an existing feature (photo provider, calendar source,
|
||||
notification target). No UI of its own.
|
||||
- **page** — adds a nav entry that opens a full-page sandboxed iframe.
|
||||
- **widget** — adds a card to the dashboard.
|
||||
|
||||
## Writing the server
|
||||
|
||||
Your `server/index.js` exports a `definePlugin(...)` object. Everything reaches
|
||||
TREK through the `ctx` argument — there is no other way out of the sandbox.
|
||||
|
||||
```js
|
||||
const { definePlugin } = require('trek-plugin-sdk')
|
||||
|
||||
module.exports = definePlugin({
|
||||
// Runs once when the plugin is activated.
|
||||
async onLoad(ctx) {
|
||||
await ctx.db.migrate('001_init', 'CREATE TABLE cache (k TEXT PRIMARY KEY, v TEXT)')
|
||||
ctx.log.info('loaded')
|
||||
},
|
||||
|
||||
// HTTP routes, mounted at /api/plugins/<id><path>.
|
||||
routes: [
|
||||
{ method: 'GET', path: '/status', auth: true, async handler(req, ctx) {
|
||||
const rows = await ctx.db.query('SELECT COUNT(*) AS n FROM cache')
|
||||
return { status: 200, headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ n: rows[0].n, user: req.user?.username }) }
|
||||
}},
|
||||
],
|
||||
|
||||
// Scheduled jobs — TREK owns the cron and calls your handler.
|
||||
jobs: [
|
||||
{ id: 'refresh', schedule: '*/15 * * * *', async handler(ctx) { /* … */ } },
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
### The `ctx` object
|
||||
|
||||
| Area | Methods | Requires |
|
||||
|---|---|---|
|
||||
| `ctx.db` | `query` / `exec` / `migrate` against your **own** SQLite file | `db:own` |
|
||||
| `ctx.trips` | `getById` / `getPlaces` / `getReservations` (membership-checked) | `db:read:trips` |
|
||||
| `ctx.users` | `getById` (public profile only — never secrets) | `db:read:users` |
|
||||
| `ctx.ws` | `broadcastToTrip` / `broadcastToUser` (events namespaced `plugin:<id>:…`) | `ws:broadcast:*` |
|
||||
| `ctx.config` | your instance settings (secrets delivered decrypted) | — |
|
||||
| `ctx.log` | `info` / `warn` / `error` → your error log | — |
|
||||
|
||||
Calling a capability your manifest didn't declare throws `PERMISSION_DENIED`.
|
||||
|
||||
### Route auth
|
||||
|
||||
Routes are authenticated by default (`req.user` is the logged-in user). Set
|
||||
`auth: false` for OAuth callbacks or webhooks that can't carry a session.
|
||||
|
||||
## Writing the client (page / widget)
|
||||
|
||||
The iframe is sandboxed at an **opaque origin** — it can't read cookies or the
|
||||
parent page. It talks to TREK only via `postMessage`:
|
||||
|
||||
```js
|
||||
window.parent.postMessage({ type: 'trek:ready' }, '*')
|
||||
|
||||
window.addEventListener('message', (e) => {
|
||||
const m = e.data
|
||||
if (m.type === 'trek:context') { /* m.theme, m.locale, m.tripId, m.userId */ }
|
||||
if (m.type === 'trek:response' && m.requestId === '1') { /* … */ }
|
||||
})
|
||||
|
||||
// Call one of your own server routes (TREK proxies it with the user's session):
|
||||
window.parent.postMessage({ type: 'trek:invoke', requestId: '1', sub: '/status', method: 'GET' }, '*')
|
||||
```
|
||||
|
||||
Bridge messages you can send: `trek:ready`, `trek:context:request`,
|
||||
`trek:navigate {to}`, `trek:notify {level,message}`, `trek:resize {height}`,
|
||||
`trek:invoke {requestId,sub,method,body}`.
|
||||
|
||||
## Settings
|
||||
|
||||
Declare settings in the manifest; TREK renders the form (you write no settings
|
||||
UI). `scope: "instance"` settings are set once by the admin; `scope: "user"`
|
||||
settings are per-user. `secret: true` fields are stored encrypted and delivered
|
||||
to your plugin decrypted through `ctx.config` (instance) — never to the iframe.
|
||||
|
||||
## Testing without a running TREK
|
||||
|
||||
`createMockHost` gives you a `ctx` that enforces the **same** permission model:
|
||||
|
||||
```js
|
||||
import { createMockHost } from 'trek-plugin-sdk/testing'
|
||||
|
||||
const { ctx, broadcasts } = createMockHost({
|
||||
grants: ['db:read:trips'],
|
||||
trips: { 1: { members: [42], data: { id: 1, name: 'Japan' } } },
|
||||
})
|
||||
await plugin.onLoad(ctx) // run your load logic
|
||||
await expect(ctx.trips.getById(1, 99)).rejects… // proves your access checks
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- **No native modules** (`.node`, `binding.gyp`, prebuilds) — they're rejected.
|
||||
- **Vendor your dependencies** — TREK never runs `npm install` on a plugin.
|
||||
- **Ship built JS** in `server/index.js` and pre-built static files in `client/`.
|
||||
- Declare every outbound host in `egress[]` when you use `http:outbound`.
|
||||
|
||||
## Validate before publishing
|
||||
|
||||
```bash
|
||||
npx trek-plugin validate .
|
||||
```
|
||||
|
||||
Runs the same checks the registry CI runs. See [[Publishing a Plugin|Plugin-Publishing]].
|
||||
@@ -0,0 +1,42 @@
|
||||
# Plugin Permissions
|
||||
|
||||
A plugin declares the permissions it needs in `trek-plugin.json`. At activation
|
||||
the admin sees the list and approves it — and because plugins run in an isolated
|
||||
process, **an unapproved capability is physically unreachable**, not just
|
||||
disallowed.
|
||||
|
||||
## Reference
|
||||
|
||||
| Permission | Grants | Notes |
|
||||
|---|---|---|
|
||||
| `db:own` | Read/write the plugin's **own** SQLite file via `ctx.db` | A separate file per plugin — never `trek.db`. `ATTACH`/`PRAGMA` are refused. |
|
||||
| `db:read:trips` | Read-only trip data via `ctx.trips` (`getById`, `getPlaces`, `getReservations`) | Every call is **membership-checked** against the acting user — a plugin can't read a trip that user can't see. |
|
||||
| `db:read:users` | Read-only public profile via `ctx.users.getById` | Returns id, username, display name, avatar only — **never** password hashes, tokens, or secrets. |
|
||||
| `ws:broadcast:trip` | Push a real-time event to a trip room via `ctx.ws.broadcastToTrip` | Event types are force-namespaced `plugin:<id>:<event>` — a plugin can't forge a core event. |
|
||||
| `ws:broadcast:user` | Push a real-time event to a user's connections | Same namespacing. |
|
||||
| `hook:photo-provider` | Register as a photo provider in Memories | Implement the `PhotoProvider` interface. |
|
||||
| `hook:calendar-source` | Register as a calendar source | Implement the `CalendarSource` interface. |
|
||||
| `http:outbound` / `http:outbound:<host>` | Make outbound network requests | **Requires** a non-empty `egress[]` list of hosts. The plugin's client CSP and the runtime egress guard are limited to those hosts. |
|
||||
|
||||
## Declaring them
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"permissions": ["db:own", "db:read:trips", "http:outbound:api.example.com"],
|
||||
"egress": ["api.example.com"] // required whenever http:outbound is declared
|
||||
}
|
||||
```
|
||||
|
||||
Rules the manifest validator enforces:
|
||||
|
||||
- Only the permissions above are accepted; an unknown string fails validation.
|
||||
- `http:outbound` (with or without a host) requires a non-empty `egress[]`.
|
||||
- `egress[]` may not contain a bare `*`.
|
||||
|
||||
## What is NOT covered
|
||||
|
||||
Isolation bounds *what* a plugin can touch, not its intent within a grant. A
|
||||
plugin you allow to read trip data **and** reach `api.example.com` could send
|
||||
that trip data there. That is the point of the consent screen — grant only what
|
||||
you'd trust the plugin to do with your data. Prefer **Reviewed** plugins and
|
||||
authors you trust.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Publishing a Plugin
|
||||
|
||||
Plugins are distributed from a static registry — the
|
||||
[TREK-Plugins](https://github.com/mauriceboe/TREK-Plugins) GitHub repo. There is
|
||||
no upload server and no account: you host your plugin in your own GitHub repo and
|
||||
list it with a pull request.
|
||||
|
||||
## 1. Host your plugin
|
||||
|
||||
Put your plugin in a **public GitHub repo** (convention: `trek-plugin-<id>`). It
|
||||
must contain a `trek-plugin.json` at the root, a built `server/index.js`, a
|
||||
filled-in `README.md`, and (page/widget) a built `client/`.
|
||||
|
||||
## 2. Tag a release
|
||||
|
||||
Tag `vX.Y.Z` where `X.Y.Z` **equals** `version` in your manifest. Either attach a
|
||||
`plugin.zip` release asset, or rely on the GitHub source archive at that commit.
|
||||
|
||||
## 3. Validate locally
|
||||
|
||||
```bash
|
||||
npx trek-plugin validate .
|
||||
```
|
||||
|
||||
This runs the exact checks the registry CI runs — a local pass predicts a CI
|
||||
pass.
|
||||
|
||||
## 4. Open a PR against the registry
|
||||
|
||||
Add one file, `registry/plugins/<id>.json`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"id": "flight-tracker",
|
||||
"name": "Flight Tracker",
|
||||
"author": "Your Name",
|
||||
"description": "Live flight status widget.",
|
||||
"repo": "you/trek-plugin-flight-tracker",
|
||||
"type": "widget",
|
||||
"versions": [{
|
||||
"version": "1.0.0",
|
||||
"gitTag": "v1.0.0",
|
||||
"commitSha": "<40-hex commit the tag points at>",
|
||||
"downloadUrl": "https://github.com/you/trek-plugin-flight-tracker/releases/download/v1.0.0/plugin.zip",
|
||||
"sha256": "<sha256 of that artifact>",
|
||||
"minTrekVersion": "3.2.0",
|
||||
"apiVersion": 1,
|
||||
"nativeModules": false
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
CI validates the entry and the artifact, and the maintainer reviews the source at
|
||||
the pinned commit before merging.
|
||||
|
||||
## What CI enforces
|
||||
|
||||
**Entry:** valid schema · `id` matches the filename and is a valid slug ·
|
||||
reserved namespaces (`trek-`, `official-`) blocked · your `id` is bound to your
|
||||
GitHub owner on first registration (nobody can repoint it later) · homoglyph
|
||||
check · the release tag exists and points at the pinned commit · manifest parity
|
||||
· **the artifact's SHA-256 matches the pin** · **no native binaries** ·
|
||||
`egress[]` present when `http:outbound` is declared.
|
||||
|
||||
**README:** must exist, have the required sections (*What it does / Screenshots /
|
||||
Permissions / Setup*), contain **at least one screenshot that actually resolves**,
|
||||
have real content (not the unfilled template), and explain every declared
|
||||
permission.
|
||||
|
||||
## Provenance & integrity
|
||||
|
||||
- `commitSha` pins the exact source the maintainer reviewed (git tags are movable).
|
||||
- `sha256` pins the exact artifact bytes TREK will run (release assets are mutable).
|
||||
|
||||
TREK verifies the downloaded bytes against `sha256` and refuses to install on a
|
||||
mismatch. A `reviewedAt` date on your entry means a maintainer looked at that
|
||||
exact commit — it is **not** an ongoing guarantee.
|
||||
|
||||
## Updating
|
||||
|
||||
Add a new version entry (new `version`/`gitTag`/`commitSha`/`sha256`) to your
|
||||
plugin file and PR it. Instances see the update on their next registry poll;
|
||||
updating is always an explicit admin action, and if a new version requests **more**
|
||||
permissions the admin must re-approve.
|
||||
@@ -0,0 +1,78 @@
|
||||
# Plugins
|
||||
|
||||
Plugins let anyone extend a self-hosted TREK instance with new features — a
|
||||
dashboard widget, a full page, a photo/calendar integration — **without touching
|
||||
TREK's source code**. They are developed by third parties, distributed from a
|
||||
public registry, and installed at the instance owner's discretion.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Plugins run arbitrary code, isolated but real.** Every plugin runs in its own
|
||||
> sandboxed child process with only the permissions you approve. TREK does not
|
||||
> maintain, audit, or take responsibility for community plugins. Grant a plugin
|
||||
> only the access you'd trust it to have with your data, and prefer plugins
|
||||
> marked **Reviewed**.
|
||||
|
||||
## Enabling plugins
|
||||
|
||||
The plugin runtime is **off by default**. To turn it on, set the environment
|
||||
variable on your instance and restart:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- TREK_PLUGINS_ENABLED=true
|
||||
```
|
||||
|
||||
When it's off, the **Admin → Plugins** tab shows a "disabled by server
|
||||
configuration" banner and nothing runs.
|
||||
|
||||
## The isolation model — what a plugin can and can't do
|
||||
|
||||
Each active plugin runs as a **separate OS process** with a deliberately empty
|
||||
environment:
|
||||
|
||||
- It has **no** access to `JWT_SECRET`, the database connection, or any TREK
|
||||
secret — those are simply not in its process.
|
||||
- It **cannot** open `trek.db`. Its own data lives in a separate SQLite file it
|
||||
reaches only through TREK.
|
||||
- It talks to TREK exclusively over an internal RPC channel, and TREK only
|
||||
answers the capabilities the plugin's manifest **declares and you approve**.
|
||||
An ungranted call is refused, not merely ignored.
|
||||
- If a plugin crashes, hangs, or runs out of memory, only *its* process dies —
|
||||
TREK keeps running and restarts or disables it.
|
||||
|
||||
This means the permission list you approve at activation is a **real boundary**,
|
||||
not a label. See [[Plugin Permissions|Plugin-Permissions]] for exactly what each
|
||||
permission grants.
|
||||
|
||||
## Installing a plugin
|
||||
|
||||
Two ways, both from **Admin → Plugins**:
|
||||
|
||||
1. **From the registry.** Click *Browse plugins*, pick one, and *Install*. TREK
|
||||
downloads the pinned version from GitHub, verifies its SHA-256 against the
|
||||
registry, safely unpacks it, and registers it — **inactive**. Nothing runs
|
||||
yet.
|
||||
2. **From disk.** Drop a plugin directory into the `/plugins` volume and click
|
||||
*Rescan* (or restart). TREK discovers it and registers it inactive.
|
||||
|
||||
## Activating a plugin
|
||||
|
||||
Activation is a separate, deliberate step. You review the plugin's requested
|
||||
permissions and outbound hosts on a consent screen, then click **Activate** —
|
||||
which grants those permissions and spawns the isolated process. A page plugin
|
||||
then appears in the top navigation; a widget appears on the dashboard.
|
||||
|
||||
## Managing plugins
|
||||
|
||||
- **Disable** stops the process immediately; the plugin keeps its data.
|
||||
- **Uninstall** removes the code and lets you keep or delete the plugin's data.
|
||||
- **View errors** shows the plugin's own error log (crashes, failed requests).
|
||||
|
||||
## Building your own
|
||||
|
||||
See [[Plugin Development|Plugin-Development]] and the `trek-plugin-sdk` package.
|
||||
|
||||
## Kill switch
|
||||
|
||||
Set `TREK_PLUGINS_ENABLED=false` to disable the entire subsystem instantly —
|
||||
installed plugins stay on disk, deactivated and harmless.
|
||||
@@ -66,6 +66,12 @@
|
||||
- [[Collections]]
|
||||
- [[Dashboard Widgets|Dashboard-Widgets]]
|
||||
|
||||
## Plugins
|
||||
- [[Plugins Overview|Plugins]]
|
||||
- [[Plugin Development|Plugin-Development]]
|
||||
- [[Plugin Permissions|Plugin-Permissions]]
|
||||
- [[Publishing a Plugin|Plugin-Publishing]]
|
||||
|
||||
## AI / MCP
|
||||
- [[MCP Overview|MCP-Overview]]
|
||||
- [[MCP Setup|MCP-Setup]]
|
||||
|
||||
Reference in New Issue
Block a user