docs: split CLAUDE.md into an always-loaded core plus dev-docs/
Nightly Build / build (push) Successful in 10s
Nightly Build / build (push) Successful in 10s
CLAUDE.md had grown to 152 KB (~21k words, ~40k tokens) and is loaded into
every coding-agent session. The cost is not the cache read, it is attention:
the rules that are genuinely invariant were drowning in the mechanics of
subsystems that most tasks never touch.
The split criterion is blast radius, not importance. A rule a change anywhere
could violate stays in CLAUDE.md — the commit rule, the production/schema
constraint, domain neutrality, the event-bus rule, the crate boundaries, and
the module map. The mechanism of one subsystem moves to dev-docs/, opened on
entry to that subsystem via a routing table at the top of CLAUDE.md.
Nothing was rewritten: every section was moved verbatim by line range and
verified line-by-line against the original. The only edits are cross-reference
repairs ("see the DB section" -> a link), the promotion of headings in the
extracted files, and a condensed "Current state" whose full text now lives in
dev-docs/users-auth-and-boot.md.
CLAUDE.md: 152 KB -> 31 KB. Twelve subsystem files plus an index under
dev-docs/, which now carries the same standing rule as docs/ and CHANGELOG.md:
a change to a subsystem updates its dev-doc in the same change.
No CHANGELOG entry: this is documentation for coding agents with no observable
effect on the application.
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
*Skald dev-docs — architectural reference for coding agents. Index: [README.md](README.md) · Entry point: [../CLAUDE.md](../CLAUDE.md)*
|
||||
|
||||
**Read this when:** you touch the plugin system: visibility, per-user config, HTTP routers or plugin-contributed web pages.
|
||||
|
||||
---
|
||||
|
||||
# Plugins
|
||||
|
||||
**Plugin visibility & per-user config.** The admin surface is `#plugins` (`plugin-catalog.js`), a status board — one card per plugin with an enable toggle + health dot + a Configure button — plus `#plugin-detail?id=<id>` (`plugin-detail.js`), which holds the instance-config form for one plugin (the plugin counterpart of `connector-detail.js`). **Granting is user-side, exactly like a connector grant**: the checkboxes live in the **Plugins** section of `#users/{id}` (`users-page.js`), right below that person's connectors, and the plugin's own page keeps only a read-only roster of who holds it, linking there. The question an admin asks is "what may this person use", and answering it plugin-by-plugin meant opening every plugin in turn; one write path also means the two surfaces cannot disagree. Unlike an MCP grant — which gates a runtime snapshotted at login and so needs a synchronous revoke — a plugin grant is re-read from `plugin_access` on every request that depends on it (sidebar pages, `/plugins/mine`, and each inbound channel message: Telegram checks it per message), so a revoke lands with no push and nothing on the bus. Binding-managed plugins (`Plugin::manages_own_access`, e.g. mobile-connector) are absent from the user-side list and rejected by its writer — a box that controls nothing is worse than no box. There is **no generic per-user plugin page**: a plugin with per-user settings (Telegram's pairing, Honcho's opt-in) hosts them in its own sidebar page via `Plugin::web_pages()`, like mobile-connector. Enable/disable + instance config + access grants are gated by the `plugin.manage` capability (admin-only by construction). Visibility is a row in `plugin_access(plugin_id, user_id)`, which grants a user sight of an enabled plugin (`plugin_id` is bare TEXT, never a FK — a `plugins` row exists only after the first toggle); the table is deny-by-default but the rows are **written for you at install time** — see [default-access.md](default-access.md). Per-user values are stored in `plugin_user_configs` (**admin-readable system.db — never secrets**) and applied through the `Plugin::update_user_config` hook, whose default just stores the blob via the `PluginUserConfigApi` on `PluginContext.user_config`. Telegram is the reference impl: its pairing page (a `web_pages()` fragment with no backend of its own) reads the `{linked, chat_id}` status blob from `GET /api/plugins/mine` and submits the code through `PUT /api/plugins/{id}/my-config`; the override turns it into a `chat_id → user_id` binding (same write path as the `telegram_pairing` tool). Endpoints: admin `GET/PUT /api/plugins[/{id}]`, `GET /api/plugins/{id}/access` (read-only roster) + **`GET/PUT /api/users/{id}/plugins`** (the grant write path, the twin of `/api/users/{id}/connectors`); user `GET /api/plugins/mine` + `PUT /api/plugins/{id}/my-config`.
|
||||
|
||||
**Plugin HTTP routes & web pages.** Every plugin's `http_router()` mounts at boot under `/api/plugin/<id>/` — **enabled or not**: two shared gates wrap each router (`require_auth`, then `guard::plugin_enabled_gate`, which re-checks the DB flag per request and answers 404 while disabled), so enable/disable serves/stops routes immediately with no restart, and plugin responses carry `Cache-Control: no-cache`. The router contract: cheap and safe to build pre-start, handlers tolerant of the not-running state (resolve runtime state per request through a shared cell, as mobile-connector does). A plugin may also contribute **frontend pages** via `Plugin::web_pages()` (`PluginPage { page_id, title, icon, entry, admin_only, priority }`): `GET /api/plugins/pages` returns the caller's visible pages (admin: all; others: non-`admin_only` pages of granted, enabled plugins) with `entry_url` resolved, and the sidebar renders them as menu entries routed `#plugin/<plugin_id>/<page_id>`. A single `<plugin-page-host>` (`web/components/plugin-page-host.js`) dynamic-imports the fragment ES module the plugin serves from its own router, registers its default-exported HTMLElement class, and mounts it with the `plugin-id` attribute — the fragment talks to its backend only through `/api/plugin/<id>/…` and runs with full session privileges (plugins are trusted: they ship in the binary). The frontend knows nothing about plugin page contents or behavior.
|
||||
Reference in New Issue
Block a user