feat(security): scope the plugin runner to a capability-limited bearer token
The scripting runner used to hold the console's full-admin mgmt-token — a plugin defect could rewrite hooks.json (arbitrary command execution) or administer pairing. The host now mints a second persisted secret, plugin-token (PUNKTFUNK_PLUGIN_TOKEN), and require_auth grows a third lane for it: loopback-confined like the admin token, but plugin_may_access carves out /hooks (read AND write), everything under /pair, /native/pair, /native/pending, client unpair DELETEs, and other plugins' ui-credential. Everything a plugin legitimately does (status/library/events/sessions, its own UI lease) is untouched. The SDK's zero-config connect() now prefers PUNKTFUNK_PLUGIN_TOKEN / plugin-token over mgmt-token, so plugins pick the scoped credential up automatically; a script that genuinely needs the admin surface sets PUNKTFUNK_MGMT_TOKEN explicitly. Old hosts without a plugin-token fall back to mgmt-token unchanged. No OpenAPI change: the lane is auth-layer only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+16
-5
@@ -2,9 +2,17 @@
|
||||
// identity cert, from the environment with file fallbacks — so `connect()` on the host machine
|
||||
// needs zero configuration.
|
||||
//
|
||||
// PUNKTFUNK_MGMT_URL (default https://127.0.0.1:47990)
|
||||
// PUNKTFUNK_MGMT_TOKEN (else <config_dir>/mgmt-token)
|
||||
// PUNKTFUNK_MGMT_CA (path; else <config_dir>/cert.pem when present)
|
||||
// PUNKTFUNK_MGMT_URL (default https://127.0.0.1:47990)
|
||||
// PUNKTFUNK_MGMT_TOKEN (admin override), else PUNKTFUNK_PLUGIN_TOKEN,
|
||||
// else <config_dir>/plugin-token, else <config_dir>/mgmt-token
|
||||
// PUNKTFUNK_MGMT_CA (path; else <config_dir>/cert.pem when present)
|
||||
//
|
||||
// Token precedence is deliberate: the host mints a capability-limited `plugin-token` for the
|
||||
// scripting runner (it cannot register hooks or administer pairing), and that is what a plugin's
|
||||
// zero-config connect() should hold — the full-admin `mgmt-token` is only a fallback for hosts
|
||||
// that predate the plugin token (and on Windows the runner's LocalService principal can't read it
|
||||
// at all). A script that legitimately needs the admin surface sets PUNKTFUNK_MGMT_TOKEN or passes
|
||||
// { token } explicitly.
|
||||
//
|
||||
// The CA is the host's own identity certificate — trusting exactly it (not the system roots)
|
||||
// IS the pin for the loopback hop. Per-runtime plumbing differs: Bun takes `tls.ca` on fetch,
|
||||
@@ -73,11 +81,14 @@ export const resolveConfig = async (
|
||||
const token =
|
||||
options?.token ??
|
||||
process.env.PUNKTFUNK_MGMT_TOKEN ??
|
||||
process.env.PUNKTFUNK_PLUGIN_TOKEN ??
|
||||
parseTokenFile(readIfExists(path.join(configDir(), "plugin-token")) ?? "") ??
|
||||
parseTokenFile(readIfExists(path.join(configDir(), "mgmt-token")) ?? "");
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
"no management token: set PUNKTFUNK_MGMT_TOKEN, pass { token }, or run where " +
|
||||
`the host's token file exists (${path.join(configDir(), "mgmt-token")})`,
|
||||
"no management token: set PUNKTFUNK_PLUGIN_TOKEN (or PUNKTFUNK_MGMT_TOKEN), pass " +
|
||||
"{ token }, or run where the host's token files exist " +
|
||||
`(${path.join(configDir(), "plugin-token")})`,
|
||||
);
|
||||
}
|
||||
const caPath = process.env.PUNKTFUNK_MGMT_CA;
|
||||
|
||||
Reference in New Issue
Block a user