offscale was missing from the permissions page because it declared ownerOnly, which mapped to kind admin, and admin capabilities are never offered for granting. correct by the old rule and wrong by the standard: every plugin follows the same platform-level permission model, appearing on the same page with the same read/write/none per role. so the field is gone rather than flipped. a plugin has no way to say owner-only, which makes the standard structural instead of remembered — the same move as the workspace rule. core, execution, confined and admin stay the platform's to assign and a plugin cannot name any of them, so the escalation question is removed rather than answered. this is the second draft of this decision to be deleted: first a full CapabilityKind with three of five values forbidden, then an ownerOnly boolean, now nothing. the doc records all three so the reasoning is visible rather than just the conclusion. finer visibility stays the plugin's job. offscale is the worked example of the gap that leaves and its manifest says so: it is grantable now, and its queries still scope by the caller, so a granted member would see their own empty server list rather than the owner's. closing that is a change inside the plugin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
48 lines
2.4 KiB
TypeScript
48 lines
2.4 KiB
TypeScript
import type { PluginManifest } from '@@/plugins/manifest';
|
|
|
|
// Offscale — Headscale, plus the Companion that ships beside it.
|
|
//
|
|
// Not a rename of Headscale and not a fork: the server underneath is stock, and the Companion adds what
|
|
// Headscale itself does not do — the invite flow being the first of them. The distinct name marks a
|
|
// distinct product rather than a badge on someone else's.
|
|
//
|
|
// The first real plugin, extracted from the platform on 2026-08-15. Everything it needs is here:
|
|
//
|
|
// api/router.ts a thin auth-gated proxy — no Headscale knowledge, and it must never grow any
|
|
// sidecar/ the whole Headscale contract, holding the admin API keys
|
|
// db/ offscale_servers, and the only table this plugin owns
|
|
// web/ panels and a layout; the shell renders the Workspace
|
|
export const manifest: PluginManifest = {
|
|
publisher: 'officerdev',
|
|
version: '1.0.0',
|
|
platform: '>=1.0.0',
|
|
|
|
label: 'Offscale',
|
|
summary: 'Your tailnet — machines, users, pre-auth keys, access policy and device invites',
|
|
icon: 'Network',
|
|
color: '#818cf8',
|
|
|
|
// One permission gating the whole surface, grantable per role at read or write like every other.
|
|
//
|
|
// `[open]` What a member's grant MEANS here is this plugin's own job and is not finished. The queries
|
|
// still scope by the caller (`listHeadscaleServers(userId)`), so a granted member would see their own
|
|
// empty server list rather than the owner's, and could register a Headscale of their own. The model in
|
|
// docs/offscale-plugin.md is one shared resource: read sees what the owner sees, write can change it.
|
|
// That is a change inside these queries, not a flag on the manifest.
|
|
//
|
|
// Worth knowing while it is unfinished: the stored credential is a Headscale ADMIN api key that can
|
|
// delete every node on a tailnet, and there is no read-only version of it — so `write` here is close to
|
|
// full control of the tailnet, which is the owner's decision to make deliberately.
|
|
permissions: [
|
|
{
|
|
key: 'offscale',
|
|
label: 'Offscale',
|
|
description: 'The tailnet: machines, routes, keys and ACLs',
|
|
// Two POSTs that are really reads — a reachability probe and a policy DRAFT that never saves.
|
|
// Without declaring them a read-level account meets a broken feature where a withheld permission
|
|
// should be. Inert while ownerOnly, and correct the moment that changes.
|
|
readOnlyWrites: ['/ssh-test', '/policy/assist'],
|
|
},
|
|
],
|
|
};
|