Skip to content

feat(runtime-api): plugin + marketplace management over /v1/apps (Engine side) - #5842

Open
Hmbown wants to merge 1 commit into
mainfrom
feat/runtime-plugin-api-20260902
Open

feat(runtime-api): plugin + marketplace management over /v1/apps (Engine side)#5842
Hmbown wants to merge 1 commit into
mainfrom
feat/runtime-plugin-api-20260902

Conversation

@Hmbown

@Hmbown Hmbown commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Engine side of the gated local plugin system. Single commit off current tip. Gates: fmt clean, dead-code budget PASS at 425. App side follows separately. Full matrix via CI.

No-Issue: feature branch tracked in 0.9.12 milestone, no single issue.

…ine side)

Expose the Engine's plugin authority to app clients over the runtime API
the desktop shell already spawns (codewhale app-server --http → serve):

- GET /v1/apps/plugins, GET /v1/apps/plugins/{selector} with structured
  capability review payload + review token (env/header values redacted)
- POST install / update / DELETE uninstall through plugins::mutation
  (installs land disabled+untrusted; network policy enforced)
- POST trust (hash-bound token) / enable / disable / revoke through the
  registry receipt flow
- Marketplace CRUD + candidate install sharing one catalog loader
  (plugins/marketplace/document.rs) with /plugin marketplace
- RuntimeCapabilities.plugin_management advertised by /v1/runtime/info
- install:: name_conflict callbacks now &(dyn Fn + Send + Sync) so the
  install future is Send (required by async axum handlers)

Local gates: cargo clippy -p codewhale-tui --lib --tests clean;
new tests 8/8 pass (capability, full lifecycle over HTTP, 404s,
marketplace add/list/install/remove, symlink refusal); plugins + command
suites pass except two stack-overflow crashers reproduced on pristine
origin/main (kimi_plan_codes_resolve_at_render_time,
marketplace_add_list_show_remove_roundtrip) — pre-existing, not from
this change; documented in handoff.
Copilot AI lite review requested due to automatic review settings September 2, 2026 20:08
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@cursor

cursor Bot commented Sep 2, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_6d40c2b4-c5e8-4965-9edb-43ee68523b0c)

@claude

claude Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 3m 7s —— View job


Review complete

  • Gather context (diff, CLAUDE.md/AGENTS.md rules)
  • Review correctness bugs (panics, unwrap/expect, error handling, matches)
  • Review provider/model/route safety invariant
  • Review reuse/simplification/dead code
  • Review test coverage
  • Review security (secrets, exec policy, input validation)
  • Post final review

Summary

This is a clean, well-scoped diff. It follows the ponytail rule 2 (reuse) well: rather than reimplementing catalog loading and trust-token logic for the new /v1/apps/plugins and /v1/apps/marketplaces endpoints, it factors the shared bits out of the existing /plugin marketplace command into crates/tui/src/plugins/marketplace/document.rs (load_catalog_document, resolve_candidate_install) and LoadedPlugin::review_token(), and both the TUI command (marketplace.rs) and the new HTTP handlers (runtime_api/plugins.rs) now call the same code. Mutations route through the pre-existing plugins::mutation::execute controller and PluginRegistry::trust/enable/disable/revoke_trust, so nothing here bypasses the existing review-token / disabled-and-untrusted-by-default install invariant. New routes are added inside api_routes before .route_layer(require_runtime_token) (runtime_api.rs:1236), so they inherit the same auth as every other mutating endpoint — no auth gap introduced.

Correctness: no unwrap()/expect()/unreachable!() outside test code in either new file. resolve_candidate_install checks has_errors() before consulting the install plan, matching the priority the TUI command already used. CatalogInstallResolution is exhaustively matched everywhere it's consumed. The Send + Sync bound added to install/install_with_expected_content_hash/install_inner/install_remote_bytes's name_conflict: &dyn Fn (plugins/install/mod.rs) is the minimal fix needed for the new async axum handler to hold the closure across an .await — a justified one-line change, not scope creep.

Provider/model/route safety: not applicable, no model/provider selection logic is touched by this diff.

Tests: runtime_api/tests.rs adds solid end-to-end coverage — full plugin lifecycle over HTTP (install → review payload/token → wrong-token rejection → trust → enable → disable → uninstall), 404s for unknown selectors, full marketplace lifecycle (add → list with resolved relative install spec → install → 404 on unknown candidate → remove without touching the installed bundle), and a symlink-document rejection test mirroring the existing local unit test in document.rs.

One point worth confirming with the author rather than a blocking bug: add_marketplace/install_plugin_api accept an absolute local filesystem path/source with no workspace confinement (same as the underlying load_catalog_document/PluginInstallSource::parse already allow for the TUI command). Exposing that over the authenticated Runtime API means any client holding a valid runtime token can now trigger a read of an arbitrary local file (bounded to 4MB, must parse as a known catalog format) or a local-path plugin install, not just an operator with shell access to the host. That may well be the intended threat model for this "gated local plugin system" (the whole Runtime API already trusts holders of the token with local session control), but it's a boundary expansion worth a one-line sanity check/ack from the author.

Nit: I could not run cargo fmt --all -- --check, cargo test -p codewhale-tui, or python3 scripts/check-dead-code-budget.py in this session — non-git Bash commands require interactive approval that wasn't available here, so I wasn't able to independently confirm the PR description's "fmt clean, dead-code budget PASS at 425" claim. Worth a maintainer re-run before merge if that hasn't happened via CI already.

Good choices to call out: the doc comment on plugins/marketplace/document.rs:1-8 explicitly states the shared-loader rationale so a future rung-2 grep finds it; resolve_spec's relative-path resolution against the catalog document's own directory (not CWD) is preserved byte-for-byte in the shared helper; the install-outcome → HTTP status mapping is correct (Install/InstallExact only ever produce Installed, so unconditional 201 Created is right, NoChange is only reachable from Update, which correctly returns 200).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new name_conflict: ...(Send + Sync) requirement in plugin install APIs breaks existing non-Sync callers (e.g., the registry-capturing closure in plugins/mutation.rs), which is a likely compile blocker.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds Engine-side plugin bundle and marketplace lifecycle management to the Runtime HTTP API under /v1/apps/plugins and /v1/apps/marketplaces, and advertises the feature via a new plugin_management runtime capability.

Changes:

  • Introduces new Runtime API handlers for plugin list/detail, install/update/uninstall, trust/enable/disable/revoke, and marketplace catalog add/list/detail/remove/install.
  • Extracts local marketplace catalog document loading + candidate install resolution into a shared plugins::marketplace::document module used by both TUI commands and the Runtime API.
  • Adds integration tests for plugin lifecycle and marketplace flows; adds LoadedPlugin::review_token() and wires capability advertisement through protocol + runtime info.
File summaries
File Description
crates/tui/src/runtime_api/tests.rs Adds Runtime API integration tests covering plugin and marketplace flows.
crates/tui/src/runtime_api/plugins.rs New Axum handlers + DTOs for plugin and marketplace management over /v1/apps/*.
crates/tui/src/runtime_api.rs Registers new routes and advertises plugin_management in default capabilities.
crates/tui/src/plugins/types.rs Adds LoadedPlugin::review_token() used by both TUI and Runtime API trust flows.
crates/tui/src/plugins/marketplace/mod.rs Exposes new shared document module and updates module docs.
crates/tui/src/plugins/marketplace/document.rs New shared loader for local catalog documents + install-spec resolution.
crates/tui/src/plugins/install/mod.rs Tightens name_conflict callback bounds to Send + Sync.
crates/tui/src/commands/groups/plugins/render.rs Delegates review-token formatting to LoadedPlugin::review_token().
crates/tui/src/commands/groups/plugins/marketplace.rs Refactors TUI marketplace command to use shared document loader + resolver.
crates/protocol/src/runtime/mod.rs Adds plugin_management to RuntimeCapabilities with serialization tests.
Review details
  • Files reviewed: 10/10 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 239 to 242
network: &NetworkPolicy,
update: bool,
name_conflict: &dyn Fn(&str) -> Option<String>,
name_conflict: &(dyn Fn(&str) -> Option<String> + Send + Sync),
) -> Result<PluginInstallOutcome> {
//! execution. Every fetch happens through the existing reviewed installer
//! when an operator explicitly installs a candidate.
//! when an operator explicitly installs a candidate. The one filesystem
//! seam — reading a local catalog document a operator pointed at — lives in
Comment on lines +10842 to +10850
let add_resp = client
.post(&base)
.json(&serde_json::json!({
"name": "team",
"path": catalog_path.display().to_string()
}))
.send()
.await?;
let add: serde_json::Value = add_resp.json().await?;
/// Resolve a user-supplied document path to an existing regular file without
/// following a final symlink (the document is untrusted input).
fn canonical_document(path: &Path) -> Result<PathBuf, String> {
let metadata = std::fs::symlink_metadata(path)
}

fn read_bounded(path: &Path) -> Result<String, String> {
let file = std::fs::File::open(path)

@codewhale-agent codewhale-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codewhale review

PR adds plugin and marketplace lifecycle endpoints to the Runtime API and extracts a shared local-catalog loader. The API shapes are generally consistent with TUI review flows, but the new run_registry_mutation has a compile-blocking move error, several API messages leak literal {name} placeholders, and the review payload may expose unredacted MCP URLs.

Findings

  • [ERROR] run_registry_mutation consumes mutation twice without Copy (crates/tui/src/runtime_api/plugins.rs:468)
    RegistryMutation does not derive Copy/Clone, but it is matched by value at lines 460-465 to compute action and then matched again at lines 468-473 to dispatch the registry mutation. The first match moves the parameter, so the second match is a use-after-move and this file will not compile. Add #[derive(Clone, Copy)] to RegistryMutation, or match by reference.
  • [WARNING] Placeholder {name} reaches API clients unformatted (crates/tui/src/runtime_api/plugins.rs:418)
    The install note at line 418, the token-mismatch error at line 455, and the enabled-untrusted note at line 492 embed literal {name} text. Since PluginMutationResponse.note and PluginActionResponse.note are Option<&'static str> and the error is not passed through format!, clients receive the literal placeholder instead of the actual plugin name. These should either use Option<String> with format! or avoid the placeholder.
  • [WARNING] MCP server review payload exposes raw URL (crates/tui/src/runtime_api/plugins.rs:273)
    mcp_server_review copies cfg.url directly into PluginMcpServerReview.url. If remote MCP URLs can contain userinfo or query-string credentials, the structured trust review leaks them despite the comment claiming secret-bearing maps are reduced to key names. Redact userinfo/sensitive query parameters or confirm this URL type cannot carry credentials.
  • [INFO] Missing coverage for several lifecycle and marketplace paths (crates/tui/src/runtime_api/tests.rs)
    Tests cover capability advertisement, install→trust→enable→disable→uninstall, and basic marketplace add/list/install/remove. There are no HTTP tests for update, revoke, install with expected_content_hash, marketplace detail/remove-not-found/add-invalid-name, or install of an unsupported candidate.

Suggestions

  • crates/tui/src/runtime_api/plugins.rs:511 — Make RegistryMutation Copy so it can safely be matched twice, or alternatively match by reference the first time. All fields are Copy.

    #[derive(Clone, Copy)]
    enum RegistryMutation<'a> {
    

Assessment

The feature is well-structured and shares security-sensitive logic with the TUI, but the compile-blocking RegistryMutation move issue must be fixed before merge. The literal placeholders and unredacted MCP URL should also be corrected, and the missing API lifecycle tests should be added to fully cover the new surface.


Advisory review by Codewhale (codewhale review --pr 5842 --post, head 6865dfb4cd68aebf6a05f4790b6d0dfaa7d01ed6). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.

};

let mut registry = (*registry).clone();
let result = match mutation {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[ERROR] run_registry_mutation consumes mutation twice without Copy

RegistryMutation does not derive Copy/Clone, but it is matched by value at lines 460-465 to compute action and then matched again at lines 468-473 to dispatch the registry mutation. The first match moves the parameter, so the second match is a use-after-move and this file will not compile. Add #[derive(Clone, Copy)] to RegistryMutation, or match by reference.

let note = match receipt.outcome {
PluginMutationOutcome::Installed => Some(
"Installed disabled and untrusted. Review the capability payload \
(GET /v1/apps/plugins/{name}), then trust and enable it.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING] Placeholder {name} reaches API clients unformatted

The install note at line 418, the token-mismatch error at line 455, and the enabled-untrusted note at line 492 embed literal {name} text. Since PluginMutationResponse.note and PluginActionResponse.note are Option<&'static str> and the error is not passed through format!, clients receive the literal placeholder instead of the actual plugin name. These should either use Option<String> with format! or avoid the placeholder.

kind: if cfg.url.is_some() { "remote" } else { "stdio" },
command: cfg.command.clone(),
args: cfg.args.clone(),
url: cfg.url.clone(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING] MCP server review payload exposes raw URL

mcp_server_review copies cfg.url directly into PluginMcpServerReview.url. If remote MCP URLs can contain userinfo or query-string credentials, the structured trust review leaks them despite the comment claiming secret-bearing maps are reduced to key names. Redact userinfo/sensitive query parameters or confirm this URL type cannot carry credentials.

})
}

enum RegistryMutation<'a> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make RegistryMutation Copy so it can safely be matched twice, or alternatively match by reference the first time. All fields are Copy.

Suggested change
enum RegistryMutation<'a> {
#[derive(Clone, Copy)]
enum RegistryMutation<'a> {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants