Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,13 +163,29 @@ pnpm lint:fix # run ESLint with auto-fix
overrides the `AUTH_INVITE_ONLY` env default; absent a stored value, the env
default applies. Never make registration read the platform DB instead.
- **`root_plugin_id` lives in `platform_settings`, seeded on first run** to
`fs.sovereign.launcher` (PLT-14/PLT-15). `/` redirects to that plugin's
`routePrefix`; the eligible set is installed + enabled + non-`adminOnly`
(validated in `runtime/src/root-plugin.ts`). Platform tables (`tenants`,
`plugin_status`, `platform_settings`) are bootstrapped with
`fs.sovereign.launcher` (PLT-14/PLT-15). The eligible set is installed +
enabled + non-`adminOnly` (validated in `runtime/src/root-plugin.ts`). `/`
**serves the root plugin in place** — the middleware rewrites `/` to the
configured plugin's `routePrefix` (URL stays `/`; the plugin remains reachable
at its own prefix too), resolving the prefix at request time via
`GET /api/admin/root-plugin` (Edge middleware can't read the DB, same fetch
pattern as `/api/admin/plugins/disabled`). `(platform)/page.tsx` keeps a
`redirect()` as a fallback for when that resolution fetch fails. Platform
tables (`tenants`, `plugin_status`, `platform_settings`) are bootstrapped with
CREATE-TABLE-IF-NOT-EXISTS + seed rows in `packages/db`'s `getPlatformDb()`
until drizzle-kit migrations land in Task 0.5.03; the DDL there must stay in
sync with the Drizzle schema.
- **Chrome plugins** (`fs.sovereign.launcher`, `fs.sovereign.account`,
`fs.sovereign.console`) are reached through the sidebar chrome (home `/`,
Console ⚙, Account avatar), never via the Launcher grid or the sidebar's
middle plugin-icon section (LCH-04, PLT-12). The canonical ID set is
`CHROME_PLUGIN_IDS` in `runtime/src/launcher-plugins.ts` — reuse it, never
re-hardcode the list.
- **Plugins that need the installed-plugin list fetch the gated `/api/plugins`**
(forwarding the session cookie), not import the registry — the SDK boundary
rule forbids plugins importing `runtime/src` or internal packages. The route
is session-gated (middleware injects `x-sovereign-user-role`) and role-filters
via `selectLauncherPlugins`; `sdk.db` replaces this fetch in Task 0.5.05.

## Design system (`packages/ui`)

Expand Down Expand Up @@ -410,9 +426,9 @@ pnpm install:plugins # clone declared sovereign/community plugins (stub until
- ✅ Task 0.4.01 — Console plugin scaffold (plugin route-composition model + middleware admin gating; platform → 0.4.0) (merged to `main`).
- ✅ Task 0.4.02 — Console: user management (user list with invited/active/deactivated status, invite flow, role change, deactivate/reactivate; `sdk.auth` + `sdk.mailer` wired) (merged to `main`).
- ✅ Task 0.4.03 — Console: plugin management (installed plugin list, enable/disable toggle, middleware 404 for disabled routes; platform DB singleton + `plugin_status` table) (merged to `main`).
- ▶️ In review: Task 0.4.04 — Console: tenant settings, system health, root plugin config (`platform_settings` + `tenants` seeded in platform DB; `sdk.platform.getConfig()` wired; invite-only toggle dual-written to auth server; `/` redirects to the configured root plugin).
- ⏳ Spec complete: Launcher platform plugin (`docs/plugins/launcher.md`) — Task 0.4.05.
- ⏳ Spec complete: Account platform plugin (`docs/plugins/account.md`) — Task 0.4.06.
- Task 0.4.04 — Console: tenant settings, system health, root plugin config (`platform_settings` + `tenants` seeded in platform DB; `sdk.platform.getConfig()` wired; invite-only toggle dual-written to auth server; `/` redirects to the configured root plugin) (merged to `main`).
- ▶️ In review: Task 0.4.05 — Launcher plugin (`plugins/launcher/` home grid; gated `/api/plugins` + `selectLauncherPlugins` helper; chrome plugins excluded from grid and sidebar middle section; `/` serves the root plugin in place via middleware rewrite — `/` and `/launcher` both render the Launcher).
- ⏳ Next: Task 0.4.06 — Account plugin (`plugins/account/` per-user profile, all authenticated users): three tabs — Profile (display name + avatar upload), Security (password change + active-session revoke), Preferences (IANA timezone + Light/Dark/System theme via `account_prefs` table + `sv-theme` cookie). Spec in `docs/plugins/account.md`. Functional dependency is `sdk.auth` (Task 0.4.02, done); branch from an up-to-date `main` once #22 merges, per the one-task-at-a-time rule. Closes the v0.4 chrome-plugin trio (Console, Launcher, Account).
- ⏳ Spec complete: Shell sidebar three-section architecture (PLT-11–PLT-15, SRS updated).
- ⏳ Spec complete: Plainwrite sovereign plugin (`docs/plugins/plainwrite.md`, v0.2 — provider + SSG adapters).

Expand Down
58 changes: 40 additions & 18 deletions docs/plugins/launcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
**Date:** June 2026\
**Author:** kasunben\
**Purpose:** Canonical specification for the Sovereign Launcher plugin — the single source of truth for its manifest, access model, functional requirements, and build plan.\
**Status:** Draft
**Status:** v0.1 implemented (Task 0.4.05)

---

Expand Down Expand Up @@ -128,8 +128,9 @@ This icon is special:

- It always appears first, regardless of install order.
- It points to `/` (the platform root), not to `/launcher` directly. The
platform resolves `/` to the configured root plugin's `routePrefix` (default:
`/launcher`; admin can change this via CON-11).
platform serves the configured root plugin in place at `/` — the runtime
rewrites `/` to that plugin's `routePrefix` (default: `/launcher`; admin can
change this via CON-11), so the URL stays `/` while the plugin renders.
- It cannot be hidden or reordered by users (v1).
- Its icon is the configured root plugin's `icon.svg` (not necessarily the
Launcher's own icon, if the admin has promoted a different plugin to root).
Expand All @@ -142,31 +143,47 @@ This icon is special:

## Directory structure

Launcher lives in the monorepo under `plugins/launcher/`.
Launcher lives in the monorepo under `plugins/launcher/`. As built (v0.1):

```
plugins/launcher/
├── manifest.json
├── icon.svg # Launcher icon — grid-of-dots or home symbol
├── app/
│ └── page.tsx # Plugin grid: regular + admin sections
└── components/
├── PluginGrid.tsx # Grid of plugin tiles
└── PluginTile.tsx # Single tile: icon, name, description
├── icon.svg # Launcher icon — four-square grid symbol
├── package.json
└── app/
├── page.tsx # Plugin grid: main + admin sections, empty state
├── launcher.module.css # Grid, tile, admin-section, empty-state styles
└── _components/
├── PluginGrid.tsx # Responsive grid of tiles
├── PluginTile.tsx # Single tile: monogram, name, description
└── monogram.ts # Two-letter monogram fallback (pure; unit-tested)
```

Components live under `app/_components/` (a private App Router folder, not a
sibling `components/` as originally sketched) because the generate script
composes only each plugin's `app/` tree into the runtime — anything outside
`app/` is not copied and would fail to resolve at runtime.

No `db/`, `migrations/`, or `lib/` directories — Launcher has no private tables
and no complex business logic. It reads the plugin registry through the platform
SDK.
and no complex business logic.

---

## Data model

Launcher has no plugin-specific database tables. It reads the platform plugin
registry (maintained by the runtime) via `sdk.db`. The registry exposes the
installed plugin list, their manifest fields (including `icon`, `name`,
`description`, `adminOnly`), and their enabled/disabled status.
registry (maintained by the runtime) — exposing the installed plugin list, their
manifest fields (`name`, `description`, `routePrefix`, `adminOnly`), and their
enabled/disabled status.

**As built (v0.1):** because the SDK boundary rule forbids a plugin importing the
runtime registry or internal packages, and `sdk.db` does not land until Task
0.5.05, the Launcher reads this list from a **session-gated** runtime route,
`GET /api/plugins`, forwarding the caller's session cookie. The route applies
all access filtering server-side via `selectLauncherPlugins()` (excludes chrome
plugins and disabled plugins; hides `adminOnly` plugins from non-admins) and
returns each plugin's `adminOnly` flag so the page can section the tiles. This
fetch is replaced by `sdk.db` when 0.5.05 lands.

---

Expand All @@ -177,6 +194,10 @@ installed plugin list, their manifest fields (including `icon`, `name`,
| `sdk.auth` | Current user session; role check for the admin section | Task 0.4.02 |
| `sdk.db` | Read plugin registry (installed plugins + their manifest metadata) | Task 0.5.05 |

In v0.1, the registry read is served by the runtime route `GET /api/plugins`
(see [Data model](#data-model)) until `sdk.db` is wired. `sdk.auth.getSession()`
supplies the role used to decide whether the Admin section renders.

---

## UI
Expand Down Expand Up @@ -245,6 +266,7 @@ Multi-project tile expansion, search, notification badges.

## Changelog

| Version | Date | Change |
| ------- | -------- | ------------------------------------------ |
| 0.1 | Jun 2026 | Initial draft — platform home screen spec. |
| Version | Date | Change |
| ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0.1 | Jun 2026 | Initial draft — platform home screen spec. |
| 0.1 | Jun 2026 | v0.1 implemented (Task 0.4.05). Deviations from this spec: components live under `app/_components/` (not a sibling `components/`) since composition copies only `app/`; registry is read via the gated `/api/plugins` route (forwarding the session cookie) rather than `sdk.db`, which lands in 0.5.05; tiles use monograms pending an icon-serving pipeline (Q3). |
2 changes: 2 additions & 0 deletions docs/sovereign-implementation-tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,8 @@ consistent info/success/warn/error formatting. CLI is monorepo-internal in v1

---

_Version 1.7 — June 2026. Changes from v1.6 (Task 0.4.05, Launcher plugin): the first non-Console platform plugin ships in `plugins/launcher/` — a home grid (LCH-01–05) that serves `/launcher` and, as the default `root_plugin_id`, now backs `/` (so the 0.4.04 root redirect resolves to a real plugin for the first time). Because the SDK boundary rule forbids a plugin importing the registry, the Launcher reads its tile list from a new **session-gated** `runtime/app/api/plugins/route.ts` (not under the `/api/admin` exclusion — middleware injects `x-sovereign-user-role`), forwarding the caller's cookie; the route role-filters via a pure `selectLauncherPlugins(plugins, disabledIds, role)` in `runtime/src/launcher-plugins.ts` (excludes the three **chrome** plugins `fs.sovereign.{launcher,account,console}` via the canonical `CHROME_PLUGIN_IDS` set, excludes disabled plugins, and hides `adminOnly` plugins from non-admins). The Launcher page renders a main grid plus an admin-only "Admin" section and an empty state (Console link for admins, contact-admin note otherwise). The sidebar middle section now also excludes chrome plugins (PLT-12); full root-plugin-first ordering (PLT-11–15) remains separate. Plugin components live under `app/_components/` (a private App Router folder) because the generate script composes only each plugin's `app/` tree. Tiles use a two-letter monogram — no icon-serving pipeline yet (launcher.md Q3). `/` now **serves the root plugin in place** rather than redirecting (revises the 0.4.04 redirect): the middleware rewrites `/` to the configured plugin's `routePrefix` (URL stays `/`, plugin still reachable at its prefix), resolving the prefix at request time via a new `GET /api/admin/root-plugin` route (Edge middleware can't read the DB — same fetch pattern as disabled-plugins); `resolveRootRoutePrefix()` in `runtime/src/root-plugin.ts` is unit-tested, and `(platform)/page.tsx` keeps its `redirect()` as a fallback. Verified live with a real session: `/` → 200 serving the Launcher in place, `/launcher` → 200 directly, `/api/plugins` → `{plugins:[]}` (only chrome installed), unauthenticated gating (307). `selectLauncherPlugins` has 7 unit tests, `monogram` 5, `resolveRootRoutePrefix` 4. `runtime` → 0.3.0; new `plugins/launcher` at 0.1.0. Earlier notes retained._

_Version 1.6 — June 2026. Changes from v1.5 (Task 0.4.04, Console settings + health + root plugin): `packages/db` gained a `platform_settings` table (composite PK `key`+`tenant_id`, PLT-15) and a `tenants` table, both bootstrapped and seeded by a new `getPlatformDb()` singleton that moved from `runtime/src/db.ts` into `packages/db/src/platform-db.ts` (the runtime now re-exports it). First run seeds the default tenant (`default`, name "Sovereign") and `root_plugin_id = fs.sovereign.launcher`. `sdk.platform.getConfig()` is now wired (PLT-06) — reads tenant name + `invite_only` from the platform DB and the platform version from the workspace-root `package.json`; the SDK took a `@sovereignfs/db` dependency for this. Console `/console/settings` exposes tenant name (CON-08), an invite-only toggle (CON-10), and a root-plugin selector (CON-11, eligible = installed + enabled + non-`adminOnly`, validated in `runtime/src/root-plugin.ts`). Invite-only is **dual-written**: the runtime PATCH writes the platform-DB copy and proxies to a new `apps/auth` `/api/admin/settings` route that writes the auth server's own `auth_settings` table — registration enforcement reads only the auth copy (a stored value overrides the `AUTH_INVITE_ONLY` env default; the create-hook resolves this per registration, no restart). `/console/health` (CON-09) renders runtime version, DB dialect + connection status + SQLite file size, auth-server reachability, and uptime from a new `runtime/api/admin/health` route; `apps/auth` gained an unauthenticated `/api/health` liveness probe. `runtime/app/(platform)/page.tsx` now redirects `/` to the configured root plugin's `routePrefix` (PLT-14), falling back to a placeholder while the Launcher is uninstalled. Verified live: settings GET/PATCH incl. validation rejections (admin-only/not-installed/empty-name), health report, invite-only dual-write reaching the auth server, admin-key gating. `packages/db` → 0.3.0, `packages/sdk` → 0.3.0, `apps/auth` → 0.3.0, `plugins/console` → 0.4.0, `runtime` → 0.2.0. Earlier notes retained._

_Version 1.5 — June 2026. Changes from v1.4 (Task 0.4.03, Console plugin management): the platform database is now actually opened by the runtime — `packages/db` gained a `plugin_status` table (plugin_id PK, tenant_id, enabled, updated_at; absence of a row = enabled; bootstrapped via CREATE TABLE IF NOT EXISTS in `runtime/src/db.ts` until drizzle-kit migrations land in 0.5.03). Console `/console/plugins` lists installed plugins with an enable/disable toggle. The middleware returns 404 for routes under a disabled plugin's `routePrefix`; because middleware runs on the Edge runtime (no SQLite access), it fetches disabled IDs from the Node-runtime route `/api/admin/plugins/disabled` — the same round-trip pattern as `/api/verify` — and fails open. Internal admin routes (`/api/admin/*`) are gated by `SOVEREIGN_ADMIN_KEY` and excluded from the middleware matcher; self-fetches to the runtime's own API use `http://localhost:3000` (the server always pins :3000), never the public URL. **New convention:** relative SQLite `file:` paths resolve against the workspace root (nearest `pnpm-workspace.yaml`), not the process cwd — all SQLite files land in the single root-level `data/` directory in native dev and Docker alike (this also fixed Docker persistence: DBs previously landed outside the `./data:/app/data` mount). `better-sqlite3` is a direct runtime dependency (pnpm strict node_modules requires it for webpack externalization) and is in `serverExternalPackages`. Both compose files now pass `SOVEREIGN_ADMIN_KEY` and `NEXT_PUBLIC_RUNTIME_URL` to the containers. `packages/db` → 0.2.0, `plugins/console` → 0.3.0. Earlier notes retained._
Expand Down
Loading