Part 1: Add --json output flags to three CLI commands for machine-readable output
- codeburn plugin list --json: array of plugin objects with status and capabilities
- codeburn plugin info <name> --json: manifest with dir and onDiskSections
- codeburn sync auto status --json: status object with configured, accepted, killed, receipts
Part 2: Implement Electron bridge with read and mutation handlers
- Read handlers: pluginList, pluginInfo, syncAutoStatus (return parsed JSON)
- Mutation handlers: pluginAdd, pluginRemove, pluginVerify, syncAutoEnable, syncAutoDisable
- All handlers follow existing Envelope pattern with proper error handling
- Special case: syncAutoEnable captures disclosure text when accept=false
Part 3: Renderer UI components (minimal implementation D1-D3 shell)
- Add Plugins section to nav rail at bottom (quiet entry, plugin icon)
- Implement PluginsSection component with list rendering for loaded/rejected plugins
- Update Section type and routing to include plugins navigation
- Add test coverage for Plugins panel row states
- Implemented as dormant Team tab registry placeholder for future plugin sections
Tests: All CLI tests (3839 passed), app tests (613 passed), E2E gates all passing
- CLI: tsc clean, vitest passes with 26 pre-existing jsdom errors
- App: typecheck clean, npm test green, npm run build succeeds
- Validate tarball entries with tar -tzf BEFORE extraction to prevent path traversal
- Reject entries that: start with /, contain .., start with ~, or contain \
- Prevents malicious/compromised servers from writing outside temp dir
- Test: create malicious gzipped tar with ../evil.txt entry and verify rejection
- Extraction only proceeds after successful validation
- Dispatch plugin add <source> to local or remote flow based on path-like vs plugin-name pattern
- Remote flow: readSyncConfig, refresh OIDC token, fetch manifest with sha256, download tarball with integrity verification (50MB limit)
- Extract via system tar to temp dir, detect single top-level dir or files at root
- Verify manifest name matches requested name, hand to shared verify+install path
- Refactor verify+install into reusable helper for both local and remote flows
- Add tests for happy path, sha256 mismatch, manifest 404, no sync config
- Secure token refresh, sha256 verification on both manifest and download header
ponytail: no retries or resume; failed download just reruns
Closes security hole: plugin commands (executable .mjs files under commands/) are now part of the signed digest, preventing tampering. getPluginFilesList and sign-plugin.mjs getFilesList now walk directories recursively with relative POSIX paths, hashing all regular files while excluding codeburn-plugin.sig (the signature itself) and sections/ (runtime-mutable plugin output). plugin add copies the full tree recursively, preserving directory structure including commands/ and commands/subdir/*.mjs. Installed plugins verify correctly without CODEBURN_PLUGIN_DEV. checkForSymlinks now recurses to catch symlinks anywhere in the tree. Sections may change at runtime without breaking verification because they are excluded from the canonical digest.
Loaded, signature-verified plugins declaring capabilities.commands now register as top-level CLI commands that spawn the plugin's command entry file as a child process. New function registerLoadedPluginCommands(program, loads?) in src/plugins/cli.ts handles collision detection (built-ins win), missing entry file detection with clean error reporting, and exit code propagation. Tests cover all five scenarios: successful invocation, child exit code propagation, missing file handling, collision detection, and rejection filtering. Integrated into src/main.ts else branch to wire plugin commands into the CLI. E2E test (non-dev environment with real signing key) confirms end-to-end plugin invocation works.
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.