enterprise/doc/design-doc/plugin-launch-flow.md
This document describes how plugins are launched in OpenHands Saas / Enterprise, from the plugin directory through to agent execution.
Plugin Directory ──▶ Frontend /launch ──▶ App Server ──▶ Agent Server ──▶ SDK
(external) (modal) (API) (in sandbox) (plugin loading)
| Component | Responsibility |
|---|---|
| Plugin Directory | Index plugins, present to user, construct launch URLs |
| Frontend | Display confirmation modal, collect parameters, call API |
| App Server | Validate request, pass plugin specs to agent server |
| Agent Server | Run inside sandbox, delegate plugin loading to SDK |
| SDK | Fetch plugins, load contents, merge skills/hooks/MCP into agent |
The plugin directory presents users with a catalog of available plugins. For each plugin, users see:
plugin.json)When a user clicks "Launch", the plugin directory:
entry_command to know which slash command to invokeIf a plugin requires user input (API keys, configuration values, etc.), the frontend displays a form modal before starting the conversation. Parameters are passed in the launch URL and rendered as form fields based on their type:
Only primitive types are supported. Complex types (arrays, objects) are not currently supported for parameter input.
The user fills in required values, then clicks "Start Conversation" to proceed.
Plugin Directory (external) constructs a launch URL to the OpenHands app server when user clicks "Launch":
/launch?plugins=BASE64_JSON&message=/city-weather:now%20Tokyo
The plugins parameter includes any parameter definitions with default values:
[{
"source": "github:owner/repo",
"repo_path": "plugins/my-plugin",
"parameters": {"api_key": "", "timeout": 30, "debug": false}
}]
OpenHands Frontend (/launch route, PR #12699) displays modal with parameter form, collects user input
OpenHands App Server (PR #12338) receives the API call:
POST /api/v1/app-conversations
{
"plugins": [{"source": "github:owner/repo", "repo_path": "plugins/city-weather"}],
"initial_message": {"content": [{"type": "text", "text": "/city-weather:now Tokyo"}]}
}
Call stack:
AppConversationRouter receives request with PluginSpec listLiveStatusAppConversationService._finalize_conversation_request() converts PluginSpec → PluginSourceStartConversationRequest(plugins=sdk_plugins, ...) and sends to agent serverAgent Server (inside sandbox, SDK PR #1651) stores specs, defers loading:
Call stack:
ConversationService.start_conversation() receives StartConversationRequestStoredConversation with plugin specsLocalConversation(plugins=request.plugins, ...)run() or send_message()SDK fetches and loads plugins on first use:
Call stack:
LocalConversation._ensure_plugins_loaded() triggered by first messagePlugin.fetch(source, ref, repo_path) → clones/caches git repoPlugin.load(path) → parses plugin.json, loads commands/skills/hooksplugin.add_skills_to(context) → merges skills into agentplugin.add_mcp_config_to(config) → merges MCP serversAgent receives message, /city-weather:now triggers the skill
Plugins load inside the sandbox because:
The entry_command field in plugin.json allows plugin authors to declare a default command:
{
"name": "city-weather",
"entry_command": "now"
}
This flows through the system:
entry_command in plugin.json/city-weather:now in the launch URL's message parameterinitial_messageThe SDK exposes this field but does not auto-invoke it—callers control the initial message.
/launch route