mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
[Build 2025] AI Plugin Bridge (#34216)
* initial commit for POC of Plugin Bridge * Updates * POC for plugin bridge * Updates from collaboration * Fixes * Refactor Plugin Bridge to use HTTP/REST instead of RPC - Remove ExecuteBridgeCall hook and Context.SourcePluginId - Implement HTTP-based bridge using existing PluginHTTP infrastructure - Add CallPlugin API method with endpoint parameter instead of method name - Update CallPluginBridge to construct HTTP POST requests - Add proper headers: Mattermost-User-Id, Mattermost-Plugin-ID - Use 'com.mattermost.server' as plugin ID for core server calls - Update ai.go to use REST endpoint /inter-plugin/v1/completion - Add comprehensive spec documentation in server/spec.md - Add MIGRATION_GUIDE.md for plugin developers - Fix 401/404 issues by setting correct headers and URL paths * Improve Plugin Bridge security and architecture - Create ServeInternalPluginRequest for internal plugin calls (core + plugin-to-plugin) - Move header-setting logic from CallPluginBridge to ServeInternalPluginRequest - Improve separation of concerns: business logic vs HTTP transport - Add security documentation explaining header protection Security Improvements: - ServeInternalPluginRequest is NOT exposed as HTTP route (internal only) - Headers (Mattermost-User-Id, Mattermost-Plugin-ID) are set by trusted server code - External requests cannot spoof these headers (stripped by servePluginRequest) - Core calls use 'com.mattermost.server' as plugin ID for authorization - Plugin-to-plugin calls use real plugin ID (enforced by server) Backward Compatibility: - Keep ServeInterPluginRequest for existing API.PluginHTTP callers (deprecated) - All tests pass Docs: - Update spec.md with security model explanation - Update MIGRATION_GUIDE.md with correct header usage examples * Space * cursor please stop creating markdown files * Fix style * Fix i18n, linter * REMOVE MARKDOWN * Remove CallPlugin method from plugin API interface Per review feedback, this method is no longer needed. Co-authored-by: Nick Misasi <[email protected]> * Remove CallPlugin method implementation from PluginAPI Co-authored-by: Nick Misasi <[email protected]> * fixes * Add AI OpenAPI spec * fix openapi spec * Use agents client (#34225) * Use agents client * Remove default agent * Fixes * fix: modify system prompts to ensure JSON is being returned * remove webapp changes * Add feature flags for rewrites and ai bridge, clean up * Remove comments that aren't helpful * Fix i18n * Remove rewrites * Fix tests * Fix i18n * adjust i18n again * Add back translations * Remove leftover mock code * remove model file * Make the real substitutions * Include a basic invokation of the client with noop to ensure build works * Remove unneeded change * Updates from review * Fixes * Use v1.5.0 of agents plugin --------- Co-authored-by: Nick Misasi <[email protected]> Co-authored-by: Christopher Speller <[email protected]> Co-authored-by: Felipe Martin <[email protected]> Co-authored-by: Mattermost Build <[email protected]> Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
committed by
GitHub
co-authored by
Hanzo Dev
parent
3c478c97a6
commit
da7a8352f2
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
agentclient "github.com/mattermost/mattermost-plugin-ai/public/bridgeclient"
|
||||
)
|
||||
|
||||
// getAIClient returns an AI client for making requests to the AI plugin
|
||||
func (a *App) getAIClient(userID string) *agentclient.Client {
|
||||
return agentclient.NewClientFromApp(a, userID)
|
||||
}
|
||||
|
||||
// Placeholder - NewClientFromApp needs to be called to initialize the AI client in order to ensure everything lines up from a build perspective, and getAIClient can't be uncalled because of linter
|
||||
// TODO: Remove once a proper feature actually uses the AI Client
|
||||
func (a *App) AIClient() error {
|
||||
aiClient := a.getAIClient("")
|
||||
if aiClient == nil {
|
||||
return errors.New("failed to get AI client")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -49,10 +49,19 @@ func (ch *Channels) ServePluginRequest(w http.ResponseWriter, r *http.Request) {
|
||||
ch.servePluginRequest(w, r, hooks.ServeHTTP)
|
||||
}
|
||||
|
||||
func (a *App) ServeInterPluginRequest(w http.ResponseWriter, r *http.Request, sourcePluginId, destinationPluginId string) {
|
||||
// ServeInternalPluginRequest handles internal requests to plugins from either core server or other plugins.
|
||||
// This is used by the Plugin Bridge to route requests with proper authentication headers.
|
||||
//
|
||||
// Parameters:
|
||||
// - userID: User ID to set in the authentication header (empty string if no user context)
|
||||
// - w: HTTP response writer
|
||||
// - r: HTTP request (should have URL path set to the endpoint, NOT including plugin ID)
|
||||
// - sourcePluginID: ID of calling plugin (empty string if from core)
|
||||
// - targetPluginID: ID of target plugin to call
|
||||
func (a *App) ServeInternalPluginRequest(userID string, w http.ResponseWriter, r *http.Request, sourcePluginID, targetPluginID string) {
|
||||
pluginsEnvironment := a.ch.GetPluginsEnvironment()
|
||||
if pluginsEnvironment == nil {
|
||||
appErr := model.NewAppError("ServeInterPluginRequest", "app.plugin.disabled.app_error", nil, "Plugin environment not found.", http.StatusNotImplemented)
|
||||
appErr := model.NewAppError("ServeInternalPluginRequest", "app.plugin.disabled.app_error", nil, "Plugin environment not found.", http.StatusNotImplemented)
|
||||
a.Log().Error(appErr.Error())
|
||||
w.WriteHeader(appErr.StatusCode)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -62,11 +71,11 @@ func (a *App) ServeInterPluginRequest(w http.ResponseWriter, r *http.Request, so
|
||||
return
|
||||
}
|
||||
|
||||
hooks, err := pluginsEnvironment.HooksForPlugin(destinationPluginId)
|
||||
hooks, err := pluginsEnvironment.HooksForPlugin(targetPluginID)
|
||||
if err != nil {
|
||||
a.Log().Error("Access to route for non-existent plugin in inter plugin request",
|
||||
mlog.String("source_plugin_id", sourcePluginId),
|
||||
mlog.String("destination_plugin_id", destinationPluginId),
|
||||
a.Log().Error("Access to route for non-existent plugin in internal plugin request",
|
||||
mlog.String("source_plugin_id", sourcePluginID),
|
||||
mlog.String("target_plugin_id", targetPluginID),
|
||||
mlog.String("url", r.URL.String()),
|
||||
mlog.Err(err),
|
||||
)
|
||||
@@ -79,11 +88,29 @@ func (a *App) ServeInterPluginRequest(w http.ResponseWriter, r *http.Request, so
|
||||
UserAgent: r.UserAgent(),
|
||||
}
|
||||
|
||||
r.Header.Set("Mattermost-Plugin-ID", sourcePluginId)
|
||||
// Set authentication headers - these are trusted because this function is internal
|
||||
// and not exposed to external HTTP routes
|
||||
r.Header.Set("Mattermost-User-Id", userID)
|
||||
|
||||
// Set plugin ID header to identify the caller
|
||||
// Use a special ID for core server calls to distinguish them from plugin-to-plugin calls
|
||||
if sourcePluginID != "" {
|
||||
r.Header.Set("Mattermost-Plugin-ID", sourcePluginID)
|
||||
} else {
|
||||
// Core server call - use special identifier
|
||||
r.Header.Set("Mattermost-Plugin-ID", "com.mattermost.server")
|
||||
}
|
||||
|
||||
hooks.ServeHTTP(context, w, r)
|
||||
}
|
||||
|
||||
// ServeInterPluginRequest handles inter-plugin HTTP requests.
|
||||
// This function does not set user authentication headers, unlike ServeInternalPluginRequest.
|
||||
func (a *App) ServeInterPluginRequest(w http.ResponseWriter, r *http.Request, sourcePluginId, destinationPluginId string) {
|
||||
// Call ServeInternalPluginRequest with empty userID since this function doesn't handle user authentication
|
||||
a.ServeInternalPluginRequest("", w, r, sourcePluginId, destinationPluginId)
|
||||
}
|
||||
|
||||
// ServePluginPublicRequest serves public plugin files
|
||||
// at the URL http(s)://$SITE_URL/plugins/$PLUGIN_ID/public/{anything}
|
||||
func (ch *Channels) ServePluginPublicRequest(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -139,6 +139,7 @@ require (
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/levigross/exp-html v0.0.0-20120902181939-8df60c69a8f5 // indirect
|
||||
github.com/mattermost/mattermost-plugin-ai v1.5.0 // indirect
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
|
||||
@@ -380,6 +380,12 @@ github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 h1:Y1Tu/swM31pVwwb
|
||||
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956/go.mod h1:SRl30Lb7/QoYyohYeVBuqYvvmXSZJxZgiV3Zf6VbxjI=
|
||||
github.com/mattermost/logr/v2 v2.0.22 h1:npFkXlkAWR9J8payh8ftPcCZvLbHSI125mAM5/r/lP4=
|
||||
github.com/mattermost/logr/v2 v2.0.22/go.mod h1:0sUKpO+XNMZApeumaid7PYaUZPBIydfuWZ0dqixXo+s=
|
||||
github.com/mattermost/mattermost-plugin-ai v1.4.1-0.20251021135930-4d1083cef23e h1:sl1hyq4YJMwefpi/AittJDxLbv5iuTZDcaqs+sgN6Ps=
|
||||
github.com/mattermost/mattermost-plugin-ai v1.4.1-0.20251021135930-4d1083cef23e/go.mod h1:sgR9+nLFCjYSE9vlqxLZxHZ+6Kz2NJw9Qko+ywVX2k0=
|
||||
github.com/mattermost/mattermost-plugin-ai v1.4.1-0.20251029144844-4c2eb7a5feb9 h1:d+ZdIU2saRH09BOnHGWXBjw9LbKOOqzcQJISQ6eOM8g=
|
||||
github.com/mattermost/mattermost-plugin-ai v1.4.1-0.20251029144844-4c2eb7a5feb9/go.mod h1:sgR9+nLFCjYSE9vlqxLZxHZ+6Kz2NJw9Qko+ywVX2k0=
|
||||
github.com/mattermost/mattermost-plugin-ai v1.5.0 h1:64P8CadbrglgiQMiYqE9kZngrvIb5Ze7Jv+iK832RbI=
|
||||
github.com/mattermost/mattermost-plugin-ai v1.5.0/go.mod h1:sgR9+nLFCjYSE9vlqxLZxHZ+6Kz2NJw9Qko+ywVX2k0=
|
||||
github.com/mattermost/mattermost/server/public v0.1.20 h1:N39ZOyYvCVZXABetpb0tVrm/08Od3q7d/D45QZjJ63s=
|
||||
github.com/mattermost/mattermost/server/public v0.1.20/go.mod h1:ZSdLCHIFYDTmX5FNCJ7LoI2CLZ1rQqZcfJ3ZF81QadY=
|
||||
github.com/mattermost/morph v1.1.0 h1:Q9vrJbeM3s2jfweGheq12EFIzdNp9a/6IovcbvOQ6Cw=
|
||||
|
||||
@@ -82,6 +82,9 @@ type FeatureFlags struct {
|
||||
// FEATURE_FLAG_REMOVAL: AutoTranslation - Remove this when MVP is to be released
|
||||
// Enable auto-translation feature for messages in channels
|
||||
AutoTranslation bool
|
||||
|
||||
// FEATURE_FLAG_REMOVAL: EnableAIPluginBridge
|
||||
EnableAIPluginBridge bool
|
||||
}
|
||||
|
||||
func (f *FeatureFlags) SetDefaults() {
|
||||
@@ -118,6 +121,9 @@ func (f *FeatureFlags) SetDefaults() {
|
||||
|
||||
// FEATURE_FLAG_REMOVAL: AutoTranslation - Remove this default when MVP is to be released
|
||||
f.AutoTranslation = false
|
||||
|
||||
// FEATURE_FLAG_REMOVAL: EnableAIPluginBridge - Remove this default when MVP is to be released
|
||||
f.EnableAIPluginBridge = false
|
||||
}
|
||||
|
||||
// ToMap returns the feature flags as a map[string]string
|
||||
|
||||
Reference in New Issue
Block a user