a manifest for offscale, and the rule it immediately broke

written against the real plugin rather than invented as a field list, on the
theory that an abstract one includes what nothing needs and misses what is
awkward. that paid off on the first field that mattered.

the rule here said a plugin may declare `app` and nothing else. offscale's
capability is `admin` — owner only — and should stay that way, so the rule was
wrong. the distinction is direction, not privilege: `core` means every account
and not deniable, so claiming it grants yourself to everyone; `admin` means
owner only, which is a plugin restricting itself. corrected table in the doc.
core, execution and confined stay the platform's to assign.

`publisher` is the only input to the mount prefix, through one function, so
first-party and third-party cannot drift into two code paths.

sidecar.runtime is a field because officer-pty needs node for node-pty's abi
while everything else is bun — one plugin already needs it, so not speculative.

dependsOn is informational and unenforced. code dependencies need no declaration
now that a plugin builds inside the workspace, and service dependencies already
degrade; this exists so the store can say the console section wants the terminal
plugin, rather than the section silently doing nothing.

health is marked deferred rather than open, with the reasoning, so it does not
get re-raised. migrations likewise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 19:05:15 +00:00
co-authored by Claude Opus 5
parent f6b2905cc7
commit b18601530f
+118 -2
View File
@@ -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-<appName>`, 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 `<prefix>/*` 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