diff --git a/docs/offscale-plugin.md b/docs/offscale-plugin.md index 10b78b91..b0274eaf 100644 --- a/docs/offscale-plugin.md +++ b/docs/offscale-plugin.md @@ -458,6 +458,119 @@ later. Do not tidy it away as unused code. --- +## The manifest — proposal + +Written against offscale rather than invented in the abstract, on the principle that a field list designed +from nothing includes what nothing needs and misses what is awkward. The field set grows per plugin; this +is the floor, not the ceiling. + +```ts +export const manifest = { + // ── Identity ───────────────────────────────────────────────────────────── + /** THE id. Route segment, table prefix, sidecar suffix, install key. One name, everywhere. */ + appName: 'offscale', + /** Who published it. The ONLY input that decides the mount prefix — see `mountPrefix()`. */ + publisher: 'officerdev', + /** The plugin's own semver. Updates compare against this. */ + version: '1.0.0', + /** Which platforms this build is good for. Refused at install when it does not match. */ + platform: '>=1.0.0 <2.0.0', + + // ── Presentation ───────────────────────────────────────────────────────── + label: 'Offscale', + summary: 'Your tailnet — machines, users, pre-auth keys and access policy', + icon: 'Network', + color: '#818cf8', + + // ── Backend ────────────────────────────────────────────────────────────── + sidecar: { + /** PM2 process name. Always `officer-`, but written out because PM2 is told it verbatim. */ + process: 'officer-offscale', + /** `bun` or `node`. A field rather than an assumption: officer-pty needs node for node-pty's ABI. */ + runtime: 'bun', + script: 'sidecar/index.ts', + }, + + /** Permissions this plugin defines. `app` or `admin` only — see below. */ + capabilities: [ + { + key: 'offscale', + label: 'Offscale', + description: 'The tailnet: machines, routes and ACLs', + kind: 'admin', + }, + ], + + // ── Data ───────────────────────────────────────────────────────────────── + /** Drizzle schema. Every table must be prefixed `offscale_`; the installer enforces it. */ + schema: 'db/schema.ts', + + // ── Frontend ───────────────────────────────────────────────────────────── + frontend: { + /** Default export is mounted at `/*` by the generated Plugins.tsx. */ + router: 'web/Router.tsx', + /** `appRegistryMetas` — the panel apps this plugin contributes. */ + panels: 'web/panels.ts', + dock: { label: 'Offscale', to: '/offscale' }, + title: 'Offscale', + }, + + // ── Optional dependencies ──────────────────────────────────────────────── + // Informational, not enforced: both are service dependencies that already degrade. They exist so the + // store can say "the Console section needs the terminal plugin" instead of the section silently not + // working, which is the difference between a missing feature and a broken one. + dependsOn: [ + { appName: 'anthropic-proxy', optional: true, reason: 'the ACL drafting assistant' }, + { appName: 'pty', optional: true, reason: 'the SSH console section' }, + ], +} as const; +``` + +### `admin` has to be allowed, and the pilot proved it immediately + +The earlier rule here was "a plugin may declare `app`, and nothing else". **That is wrong, and offscale is +the counterexample**: its capability is `kind: 'admin'` — owner-only — and it should stay that way. + +The distinction is direction. `core` means _every account, undeniable_, so a plugin claiming it grants +itself to everyone: escalation. `admin` means _owner only_, which is a plugin **restricting** itself, and +nothing is gained by forbidding it. + +Corrected rule: + +| Kind | May a plugin declare it? | Why | +| ----------- | ------------------------ | ---------------------------------------------------------- | +| `app` | yes | the ordinary grantable surface | +| `admin` | yes | self-restriction, never an escalation | +| `core` | **no** | every account, not deniable — an ungated grant to everyone | +| `execution` | **no** | runs as the owner's OS user; the platform's to assign | +| `confined` | **no** | implies a Linux identity the platform provisions | + +### One function decides the prefix + +`publisher` is the only input, so first-party and third-party cannot become two code paths: + +```ts +const mountPrefix = (m: Manifest) => + m.publisher === 'officerdev' ? `/${m.appName}` : `/p/${m.publisher}/${m.appName}`; +``` + +Used for both `/api/...` and the frontend route. Nothing else in the codebase may branch on provenance. + +### Notes on the fields + +- **`sidecar.runtime`** exists because `officer-pty` runs under node for node-pty's native ABI while + everything else is bun. One plugin already needs it, so it is not speculative generality. +- **`platform`** is the compat range, and it presumes the platform gains a version. It has none today; + 1.0 is expected before anyone outside Officer Dev writes a plugin. +- **`dependsOn`** is deliberately not enforced. Code dependencies need no declaration — a plugin builds + inside the workspace, so `import { TerminalView }` simply resolves — and service dependencies already + degrade. This is for the human reading the store. +- **No `health`.** Deferred; process-online is what the store knows and that is enough for now. +- **No `migrations`.** Deferred; a field can be added without redesign. +- **No permission list.** A plugin calls the API with the user's token and the user's permissions. + +--- + ## The state of the app store, as found It **is** the plugin system, roughly 90% built, with one structural hole. @@ -509,8 +622,11 @@ used in addition rather than instead — they span orgs, which matters because b platform checkout, so dev-time and build-time are the same mechanism. 2. **Migrations and versioning.** A plugin needs a version and a platform-compatibility range, and something has to apply schema changes over time. Cheap now, miserable to retrofit. -3. **Health, distinct from enabled.** The store already renders amber for "installed and enabled but the - process is not online". That state needs a definition a plugin can satisfy. +3. ~~**Health, distinct from enabled.**~~ **Deferred, deliberately.** A sidecar can be online while the + thing it exists to talk to is unreachable — offscale's own `/servers/:id/health` is exactly that + question. But process-online covers the common failure, every plugin that needs more surfaces it in its + own UI, and this is a manifest field that can be added later without redesign. Revisit in a distant + future, not before. 4. ~~**No inter-plugin dependencies.**~~ **Overtaken by evidence.** That measurement was of _schemas_ and is still true there; at runtime the pilot has two — `assist` → anthropic-proxy (service) and `ConsoleView` → `TerminalView` (code). The rule became "may depend, must degrade" — see "Dependencies between