Commit graph

9 commits

Author SHA1 Message Date
iamtoruk
6d69803817 feat(desktop): plugin manager, install flow, sharing consent center, and dormant Team tab (plugin era D1-D3)
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
2026-08-28 06:58:55 -07:00
iamtoruk
4912be1915 fix(plugins): polite one-line errors for plugin commands instead of stack traces
Convert user-facing plugin commands (info, verify, add, remove) to use
process.stderr.write() + process.exitCode pattern. Wrap internal helper calls
in try-catch at action boundary. Update tests to capture stderr and check
exitCode instead of expecting thrown errors.
2026-08-28 05:43:18 -07:00
iamtoruk
56dcc402d7 fix(plugins): reject tarball entries that escape the extraction dir before extracting
- 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
2026-08-28 02:48:09 -07:00
iamtoruk
1a1a099a73 feat(plugins): remote plugin install from the org receiver (store v1, CLI half)
- 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
2026-08-28 02:42:07 -07:00
iamtoruk
8b0e9af431 fix(plugins): sign and install plugin trees recursively; sections stay runtime-mutable
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.
2026-08-28 02:14:51 -07:00
iamtoruk
d6bdff6d2e feat(plugins): invoke declared plugin commands as child processes (10A)
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.
2026-08-28 02:10:43 -07:00
iamtoruk
67320432ca feat(plugins): ed25519 release-key verification and plugin add/remove (9b)
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
2026-08-28 00:30:43 -07:00
iamtoruk
5a72bb35c0 test(plugins): pin deny-by-default rejection of unsigned plugins without CODEBURN_PLUGIN_DEV 2026-08-28 00:16:11 -07:00
iamtoruk
52bde7c4dd feat(plugins): CB-3 plugin socket — wire guard, CLI inspector, payload seam
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.
2026-08-27 09:29:15 -07:00