every plugin permission is grantable, and there is no field to say otherwise

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>
This commit is contained in:
2026-08-15 00:42:35 +00:00
co-authored by Claude Opus 5
parent 8b6cb34ae0
commit 8cc51cfb40
5 changed files with 25 additions and 22 deletions
+9 -8
View File
@@ -22,21 +22,22 @@ export const manifest: PluginManifest = {
icon: 'Network',
color: '#818cf8',
// One permission gating the whole surface.
// One permission gating the whole surface, grantable per role at read or write like every other.
//
// `ownerOnly` because the credential behind it is a Headscale ADMIN api key that can delete every node
// on a tailnet, and there is no read-only version of it. A read grant would still be reading through
// that key; the protection is that non-owners cannot reach the routes at all.
// `[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.
//
// Read/write for members is the model recorded in docs/offscale-plugin.md and deliberately not enabled
// here yet: it needs the queries to resolve to the OWNER's rows rather than the caller's, which is a
// change inside this plugin and not a flag.
// 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',
ownerOnly: true,
// 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.