NVIDIA / NVIDIA/OpenShell

feat(cli): add PATH-based plugin discovery for external subcommands

Open
#1,851 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area:cli question state:stale
Dominant language
Rust
Stars
8.7k
Forks
1.3k
Avg merge
2d 7h
Merged PRs (30d)
243

Description

Problem Statement

OpenShell CLI should be extensible via third-party plugins, following the same pattern used by git, kubectl, docker, and helm. If a binary named openshell-<name> exists on $PATH, then openshell <name> [args...] should discover and execute it, forwarding all remaining arguments. This enables the ecosystem to grow beyond built-in commands without requiring changes to the core CLI.

Technical Context

The CLI binary is named openshell (defined in crates/openshell-cli/Cargo.toml). It uses clap 4.x with #[derive(Parser)] and a Commands enum with 14 subcommand variants. Today, unknown subcommands produce a clap error (unrecognized subcommand) before any custom code runs. Clap 4.x has first-class support for external subcommands via allow_external_subcommands and an #[command(external_subcommand)] enum variant, making this a well-supported pattern.

Affected Components

Component Key Files Role
CLI entry point crates/openshell-cli/src/main.rs Parser struct, Commands enum, dispatch match
Help template crates/openshell-cli/src/main.rs:197-233 Custom grouped help text
Process exec patterns crates/openshell-cli/src/ssh.rs:218-242 Existing exec_or_wait pattern to reuse
CLI tests crates/openshell-cli/src/main.rs:3124-4575 ~90 parse/completion unit tests

Technical Investigation

Architecture Overview
main()
  ├── rustls crypto provider install
  ├── CompleteEnv::with_factory(Cli::command).complete()  // shell completions
  ├── Cli::parse()                                         // clap parses args
  ├── TlsOptions setup from global flags
  ├── tracing_subscriber init from --verbose
  └── match cli.command {
        Some(Commands::Gateway { .. }) => ...
        Some(Commands::Sandbox { .. }) => ...
        ...14 variants...
        None => print root help            ◄── insertion point
      }

Key clap configuration:

  • command: Option<Commands> — subcommand is optional
  • disable_help_subcommand = true, disable_help_flag = true — help manually managed
  • allow_external_subcommands is not currently set
  • Custom HELP_TEMPLATE groups commands into "SANDBOX COMMANDS", "GATEWAY COMMANDS", "ADDITIONAL COMMANDS"
Code References
Location Description
crates/openshell-cli/Cargo.toml:14 Binary name: openshell
crates/openshell-cli/src/main.rs:356-408 Cli struct with #[derive(Parser)]
crates/openshell-cli/src/main.rs:410-580 Commands enum with #[derive(Subcommand)] — 14 variants
crates/openshell-cli/src/main.rs:197-233 HELP_TEMPLATE — custom grouped help
crates/openshell-cli/src/main.rs:1887-3106 Main dispatch: match cli.command
crates/openshell-cli/src/main.rs:3100-3102 None arm — prints help for no subcommand
crates/openshell-cli/src/ssh.rs:218-242 exec_or_wait() — Unix process replacement pattern
crates/openshell-cli/src/ssh.rs:1472-1492 launch_editor_command() — spawn with NotFound handling
crates/openshell-cli/src/main.rs:3124-4575 Test module — ~90 parse/completion tests
Current Behavior

When a user types openshell foobar:

  1. Cli::parse() calls clap's parser
  2. Clap sees foobar doesn't match any Commands variant
  3. Clap prints error: unrecognized subcommand 'foobar' with suggestions and exits code 2
  4. Custom code in main() is never reached
What Would Need to Change

1. Clap configuration (Cli struct):

  • Add allow_external_subcommands = true to the #[command(...)] attribute
  • Add an #[command(external_subcommand)] External(Vec<OsString>) variant to the Commands enum

2. Plugin dispatch (new match arm in main()):

  • Extract the subcommand name and remaining args from the External(Vec<OsString>) variant
  • Look up openshell-<subcommand> on $PATH (via which crate or Command::new + error matching)
  • Set environment variables for global CLI context (gateway name, endpoint, verbose level)
  • On Unix: use exec() to replace the process (matches existing exec_or_wait pattern)
  • On non-Unix: spawn and forward exit code

3. Internal binary blocklist:

  • The workspace ships binaries like openshell-gateway, openshell-sandbox, openshell-driver-kubernetes, etc.
  • These are on $PATH in deployment scenarios but are NOT plugins
  • Need a blocklist to prevent openshell driver-kubernetes from exec'ing the driver binary

4. Help template update:

  • Add a "PLUGINS" section to HELP_TEMPLATE explaining that openshell-<name> binaries on PATH are available as openshell <name>
Alternative Approaches Considered
Approach Pros Cons Verdict
A. allow_external_subcommands Built into clap, minimal code, established pattern Changes error messages for genuine typos Recommended
B. Manual pre-parse No clap changes, preserves error messages Must handle global flags manually, fragile Not recommended
C. try_parse + fallback Preserves clap error messages for typos More complex; must re-handle parse failures Good fallback if typo UX matters
Patterns to Follow
  • Process replacement: Reuse the exec_or_wait pattern from ssh.rs:218-242 — Unix exec() for clean signal handling, Windows spawn+wait fallback
  • Error handling: Reuse the NotFound matching pattern from launch_editor_command in ssh.rs:1472-1492
  • Env var conventions: Follow existing OPENSHELL_* env var naming (gateway, gateway_endpoint, gateway_insecure)

Proposed Approach

Use clap's built-in allow_external_subcommands + #[command(external_subcommand)] enum variant. When an unknown subcommand is encountered, look up openshell-<name> on $PATH, propagate global CLI state as environment variables, and exec() the plugin on Unix (spawn+wait on Windows). Add a blocklist for internal binaries (openshell-gateway, openshell-sandbox, etc.) that are not plugins. Update the help template with a "PLUGINS" section.

Scope Assessment

  • Complexity: Low
  • Confidence: High — clear path, well-understood pattern, clap has first-class support
  • Estimated files to change: 2-3 (main.rs, Cargo.toml for which dep, possibly a new test file)
  • Issue type: feat

Risks & Open Questions

  • Internal binary collision: Binaries like openshell-gateway and openshell-sandbox are on PATH in deployments. A blocklist is needed, but the exact list should be reviewed — should it be hardcoded or derived from workspace Cargo.toml?
  • Auth token forwarding: Should plugins receive auth tokens via environment variables? This widens the trust boundary. Recommendation: do NOT pass tokens; let plugins resolve auth via openshell-bootstrap APIs. Needs human decision.
  • Shell completions: External subcommands won't appear in shell completions automatically. The clap_complete dynamic engine won't suggest plugin names. Acceptable for v1, but worth noting as a follow-up.
  • Error UX for typos: With allow_external_subcommands, clap no longer shows "did you mean?" suggestions for typos. The plugin-not-found error should include available built-in commands or suggest --help. Alternatively, use the try_parse fallback approach (Option C).
  • Security (accepted risk): Running arbitrary binaries from PATH is the same threat model as git/kubectl plugins — the user controls their PATH. No shell interpolation is used (Command::new is safe).

Test Considerations

  • Unit tests: Add parse tests verifying (1) external subcommands are captured in the External variant, (2) built-in commands take precedence, (3) global flags work with external subcommands
  • Integration test: Create a temporary openshell-test-plugin script on PATH, run openshell test-plugin --check, verify exit code and environment variables are forwarded
  • Existing tests: The ~90 existing parse/completion tests in main.rs::tests must continue to pass. The completions_engine_returns_candidates test should verify external subcommands don't leak into normal completion
  • Blocklist test: Verify that openshell driver-kubernetes (internal binary name) produces an error, not a plugin exec

Created by spike investigation. Use build-from-issue to plan and implement.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in crates/openshell-cli/src/main.rs, reviewing the Cli and Commands definitions, main dispatch, HELP_TEMPLATE, and existing parse/completion tests. Compare process handling with ssh.rs:218-242 and ssh.rs:1472-1492. Done means external subcommands are safely discovered and dispatched with the required blocklist, help text, environment behavior, and tests while existing tests continue to pass.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
cli
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.