Ships ed25519 signature verification for plugins with two new components:
- src/plugins/keys.ts: RELEASE_PUBLIC_KEYS map with one release keypair
- scripts/sign-plugin.mjs: keygen and sign commands for plugin developers
- verifyPlugin() in loader.ts: validates ed25519 signatures, rejects unsigned plugins unless CODEBURN_PLUGIN_DEV=1
- plugin add <path>: installs signed plugins to ~/.config/codeburn/plugins/
- plugin remove <name> --confirm: removes installed plugins
All three gates pass: tsc clean, vitest zero new failures, smoke test complete.
Design decision: public keys stored as base64-encoded PEM format rather than raw 32-byte keys. Node.js crypto.verify requires PEM/DER format or KeyObject for ed25519; raw bytes alone fail. PEM is standard and portable.
Private key written to: /tmp/codeburn-signing/codeburn-signing-key.pem
Teams issue #3 introduces the codeburn plugin socket: an opt-in escape
hatch for third parties to add sync attributes, payload sections, and CLI
commands without touching the codeburn CLI source tree. This commit ships
the first half (CB-3) — the manifest layer, the wire guard, and the user-
facing inspector. Release-key signing (the 9b seam) lands separately.
Why split now:
- The wire guard (filterPluginAttributes in sync/otlp.ts) is the load-
bearing piece. With it, no plugin code can run until the user opts
in by installing one — the default empty socket keeps every byte of
the existing telemetry wire byte-identical. That is the contract the
existing four sync test files were already pinning; this commit
re-pins it explicitly with a new test (tests/plugin-socket.test.ts).
- The inspector () makes rejections
visible. Without it, a misconfigured plugin would just vanish
silently — exactly the foot-gun the socket exists to prevent.
What ships:
- src/plugins/loader.ts: enumerates ~/.config/codeburn/plugins/<name>/,
reads + parses each codeburn-plugin.json, applies name/dir match and
cliCompat check, hands off to verifyPlugin (the 9b seam). Rejected
plugins contribute nothing to the wire.
- src/plugins/manifest.ts: parses + validates the manifest shape
(name, version, cliCompat, capabilities.{commands, syncAttributes,
payloadSections, spanKinds}). Returns {ok, reason} on any failure.
- src/plugins/cli.ts: . Read-only;
never mutates the plugins directory.
- src/sync/otlp.ts: filterPluginAttributes strips any key not in the
declared set, called from buildOtlpPayload when pluginAttributes are
supplied. With no plugin installed, the call path is a no-op.
- src/sync/push.ts, src/sync/cli.ts: thread pluginAttributeKeys
through sendBatches -> buildOtlpPayload. The wire is unchanged when
no plugin declares anything.
- src/usage-aggregator.ts, src/menubar-json.ts: add the
MenubarPayload.plugins field and populate it from pluginPayloadSections
(empty when no plugin installed).
- src/main.ts: register the plugin subcommand.
- tests/plugin-socket.test.ts: 12 tests covering the wire guard, the
byte-identical default, the loader rejection paths, and the CLI.
- scripts/smoke-plugin-socket.mjs: end-to-end smoke test against the
built CLI (13 assertions).
Byte-identical guarantee:
- With no plugin installed: loadPlugins() returns [], pluginPayloadSections
returns {}, filterPluginAttributes(empty) returns []. The OTLP payload
and the menubar payload are bit-for-bit the same as before this commit.
- The dry-run output gains one new line listing the active plugin names
(empty string when no plugin). User-facing only; not on the wire.
What's intentionally NOT here:
- Plugin command invocation: lands when at
least one community plugin publishes a command (deferred).
- : shipping this with no signed-plugin
ecosystem in place would invite supply-chain risk. Deferred.
- Release-key signing (the verifyPlugin return path). Deferred to 9b.
Refs: teams issue #3, CB-3 sync boundary spec section 9a.