Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,28 @@ All endpoints additionally check permissions: **System Admins** (`manage_system`

## Endpoints

### Get client configuration

```
GET /config
```

Returns the client-relevant plugin configuration. Any authenticated user may call this endpoint — no additional permission checks are performed.

**Response:** `200 OK`

```json
{
"enable_ui": false
}
```

| Field | Type | Description |
| ----------- | ------- | --------------------------------------------------------------------------- |
| `enable_ui` | boolean | Whether the Channel Automation UI is enabled in the webapp product switcher |

---

### List flows

```
Expand Down
7 changes: 7 additions & 0 deletions plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@
"type": "number",
"default": 0,
"help_text": "Maximum number of flows that can target a single channel. Set to 0 or leave empty for unlimited."
},
{
"key": "EnableUI",
"display_name": "Enable UI",
"type": "bool",
"default": false,
"help_text": "When enabled, the Channel Automation product menu will be visible in the webapp. Changes require a page reload to take effect."
}
]
}
Expand Down
16 changes: 16 additions & 0 deletions server/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,27 @@ func (p *Plugin) initRouter() *mux.Router {
execAPI := execution.NewAPIHandler(p.historyStore, p.flowStore, p.API)
execAPI.RegisterRoutes(apiRouter)

apiRouter.HandleFunc("/config", p.handleGetClientConfig).Methods(http.MethodGet)
apiRouter.HandleFunc("/agents/{agent_id}/tools", p.handleGetAgentTools).Methods(http.MethodGet)

return router
}

// clientConfig is the subset of configuration returned to webapp clients.
type clientConfig struct {
EnableUI bool `json:"enable_ui"`
}

// handleGetClientConfig returns the client-relevant plugin configuration.
func (p *Plugin) handleGetClientConfig(w http.ResponseWriter, _ *http.Request) {
cfg := p.getConfiguration()

w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(clientConfig{EnableUI: cfg.EnableUI}); err != nil {
p.API.LogError("Failed to encode client config", "error", err.Error())
}
}

// handleGetAgentTools proxies a request to the AI plugin bridge to retrieve the
// tools available for a specific agent.
func (p *Plugin) handleGetAgentTools(w http.ResponseWriter, r *http.Request) {
Expand Down
1 change: 1 addition & 0 deletions server/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
type configuration struct {
MaxConcurrentFlowsLimit int
MaxFlowsPerChannelLimit int
EnableUI bool
}

// MaxFlowsPerChannel implements flow.Configuration.
Expand Down
82 changes: 82 additions & 0 deletions server/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,88 @@ func TestHandleGetAgentTools(t *testing.T) {
})
}

func TestHandleGetClientConfig(t *testing.T) {
t.Run("unauthenticated request returns 401 via real router", func(t *testing.T) {
api := &plugintest.API{}
p := &Plugin{}
p.SetAPI(api)
p.setConfiguration(&configuration{EnableUI: true})
p.router = p.initRouter()

w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/api/v1/config", nil)
// deliberately omit Mattermost-User-ID header

p.ServeHTTP(nil, w, r)

assert.Equal(t, http.StatusUnauthorized, w.Code)
})

t.Run("authenticated request succeeds via real router", func(t *testing.T) {
api := &plugintest.API{}
p := &Plugin{}
p.SetAPI(api)
p.setConfiguration(&configuration{EnableUI: true})
p.router = p.initRouter()

w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/api/v1/config", nil)
r.Header.Set("Mattermost-User-ID", mmmodel.NewId())

p.ServeHTTP(nil, w, r)

assert.Equal(t, http.StatusOK, w.Code)

var resp map[string]any
err := json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
assert.Equal(t, true, resp["enable_ui"])
})

t.Run("returns enable_ui false by default", func(t *testing.T) {
p := &Plugin{}
p.setConfiguration(&configuration{})

router := mux.NewRouter()
router.HandleFunc("/api/v1/config", p.handleGetClientConfig).Methods(http.MethodGet)

w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/api/v1/config", nil)
r.Header.Set("Mattermost-User-ID", mmmodel.NewId())

router.ServeHTTP(w, r)

assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))

var resp map[string]any
err := json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
assert.Equal(t, false, resp["enable_ui"])
})

t.Run("returns enable_ui true when configured", func(t *testing.T) {
p := &Plugin{}
p.setConfiguration(&configuration{EnableUI: true})

router := mux.NewRouter()
router.HandleFunc("/api/v1/config", p.handleGetClientConfig).Methods(http.MethodGet)

w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/api/v1/config", nil)
r.Header.Set("Mattermost-User-ID", mmmodel.NewId())

router.ServeHTTP(w, r)

assert.Equal(t, http.StatusOK, w.Code)

var resp map[string]any
err := json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
assert.Equal(t, true, resp["enable_ui"])
})
}

func TestMessageHasBeenPosted_SkipsAIGeneratedPosts(t *testing.T) {
// Plugin has nil triggerService — if the early return doesn't fire,
// we'll get a nil-pointer panic, proving the filter works.
Expand Down
8 changes: 8 additions & 0 deletions webapp/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,11 @@ export async function getAgentTools(agentId: string): Promise<AIToolInfo[]> {
const tools = await doFetch<AIToolInfo[] | null>(`${BASE_URL}/agents/${agentId}/tools`);
return tools ?? [];
}

export interface ClientConfig {
enable_ui: boolean;
}

export async function getConfig(): Promise<ClientConfig> {
return doFetch<ClientConfig>(`${BASE_URL}/config`);
}
11 changes: 11 additions & 0 deletions webapp/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {getConfig} from 'client';
import manifest from 'manifest';
import type {Store} from 'redux';

Expand All @@ -12,6 +13,16 @@ import type {PluginRegistry} from 'types/mattermost-webapp';
export default class Plugin {
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
public async initialize(registry: PluginRegistry, store: Store<GlobalState>) {
try {
const config = await getConfig();
if (!config.enable_ui) {
return;
}
} catch {
// If config fetch fails, default to not registering (safe default).
return;
}

registry.registerProduct(
`/plug/${manifest.id}`,
Icon,
Expand Down
Loading