Add manage_own_agent and manage_others_agent permissions (#35924)

* Add PermissionCreateAgent server-side permission definition

Define PermissionCreateAgent in the model layer with system scope,
add to SystemScopedPermissionsMinusSysconsole (feeds AllPermissions),
grant to system_user in MakeDefaultRoles(), and register a permissions
migration for existing installations (system_admin + system_user).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add exhaustive tests for PermissionCreateAgent permission

Model tests: verify create_agent is in AllPermissions, has system scope,
correct i18n fields, present in system_admin and system_user default roles,
and absent from system_guest.

Migration test: verify getAddCreateAgentPermissionMigration adds create_agent
to both system_admin and system_user, and is idempotent on re-run.

Also register the migration key in testlib mock store so server initialization
skips it during test setup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add webapp permission constants and i18n for create_agent

Add CREATE_AGENT constant to permissions.ts, display strings with
defineMessages in permissions.tsx, and i18n entries in en.json so the
permission appears in System Console Permission Schemes UI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Clean up tests and minor fixups for create_agent permission

Consolidate role_test.go into table-driven tests, remove redundant comments
in permissions_migrations_test.go, add .planning/ to .gitignore, and
refresh webapp/package-lock.json.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Split create_agent into manage_own_agent and manage_others_agent

Replace PermissionCreateAgent with system-scoped own/others permissions,
update migration and defaults, and wire System Scheme UI for integrations.

Made-with: Cursor

* fixes

* Stabilize autotranslation E2E by pinning mock source language

Set LibreTranslate mock to English before the pre-enable post and Spanish
before the post-enable message so parallel tests cannot leave the mock in
a state where the new message is not translated.

Made-with: Cursor

* Revert package-lock, add more chnages

* Revert "Revert package-lock, add more chnages"

This reverts commit 7f6752c2e0.

* Drop unrelated autotranslation E2E tweak; restore package-lock

The Playwright autotranslation change was not caused by MM-65671. Revert
that test edit and restore webapp/package-lock.json after an accidental
revert of the prior package-lock update.

Made-with: Cursor

* Put package-lock back again

* fixes

* Fix migration tests for manage_own_agent on system_user role

Made-with: Cursor

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Nick Misasi
2026-04-15 14:57:46 +00:00
committed by GitHub
co-authored by Claude Opus 4.6 Mattermost Build
parent 62d0ab633f
commit d4d65c8cfb
15 changed files with 190 additions and 0 deletions
+1
View File
@@ -160,6 +160,7 @@ docker-compose.override.yaml
.notice-work/
.aider*
.env
.planning/
**/CLAUDE.local.md
**/CLAUDE.md
+2
View File
@@ -210,6 +210,7 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) {
model.PermissionDeleteCustomGroup.Id,
model.PermissionRestoreCustomGroup.Id,
model.PermissionManageCustomGroupMembers.Id,
model.PermissionManageOwnAgent.Id,
},
"system_post_all": {
model.PermissionCreatePost.Id,
@@ -278,6 +279,7 @@ func TestDoEmojisPermissionsMigration(t *testing.T) {
model.PermissionCreateEmojis.Id,
model.PermissionDeleteEmojis.Id,
model.PermissionViewMembers.Id,
model.PermissionManageOwnAgent.Id,
}
assert.ElementsMatch(t, expected3, role3.Permissions, fmt.Sprintf("'%v' did not have expected permissions", model.SystemUserRoleId))
@@ -1284,6 +1284,24 @@ func (a *App) getRestoreManageOAuthPermissionMigration() (permissionsMap, error)
}, nil
}
func (a *App) getAddManageAgentPermissionsMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: []string{
model.PermissionManageOwnAgent.Id,
model.PermissionManageOthersAgent.Id,
},
},
permissionTransformation{
On: isExactRole(model.SystemUserRoleId),
Add: []string{
model.PermissionManageOwnAgent.Id,
},
},
}, nil
}
// DoPermissionsMigrations execute all the permissions migrations need by the current version.
func (a *App) DoPermissionsMigrations() error {
return a.Srv().doPermissionsMigrations()
@@ -1343,6 +1361,7 @@ func (s *Server) doPermissionsMigrations() error {
{Key: model.MigrationKeyAddChannelAutoTranslationPermissions, Migration: a.getAddChannelAutoTranslationPermissionMigration},
{Key: model.MigrationKeyAddSharedChannelManagerPermissions, Migration: a.getAddSharedChannelManagerPermissionsMigration},
{Key: model.MigrationKeyRestoreManageOAuthPermission, Migration: a.getRestoreManageOAuthPermissionMigration},
{Key: model.MigrationKeyAddManageAgentPermissions, Migration: a.getAddManageAgentPermissionsMigration},
}
roles, err := s.Store().Role().GetAll()
@@ -65,6 +65,60 @@ func TestRestoreManageOAuthPermissionMigration(t *testing.T) {
systemStore.AssertNumberOfCalls(t, "SaveOrUpdate", 1)
}
func TestAddManageAgentPermissionsMigration(t *testing.T) {
mainHelper.Parallel(t)
th := SetupWithStoreMock(t)
migrationMap, err := th.App.getAddManageAgentPermissionsMigration()
require.NoError(t, err)
systemAdminRole := &model.Role{
Name: model.SystemAdminRoleId,
Permissions: []string{model.PermissionManageSystem.Id},
}
systemUserRole := &model.Role{
Name: model.SystemUserRoleId,
Permissions: []string{model.PermissionCreateDirectChannel.Id},
}
roles := []*model.Role{systemAdminRole, systemUserRole}
mockStore := th.App.Srv().Store().(*mocks.Store)
roleStore := mocks.RoleStore{}
systemStore := mocks.SystemStore{}
mockStore.On("Role").Return(&roleStore)
mockStore.On("System").Return(&systemStore)
systemStore.On("GetByName", model.MigrationKeyAddManageAgentPermissions).
Return(nil, model.NewAppError("test", "missing", nil, "", 404)).Once()
systemStore.On("GetByName", model.MigrationKeyAddManageAgentPermissions).
Return(&model.System{Name: model.MigrationKeyAddManageAgentPermissions, Value: "true"}, nil).Once()
systemStore.On("SaveOrUpdate", mock.MatchedBy(func(system *model.System) bool {
return system.Name == model.MigrationKeyAddManageAgentPermissions && system.Value == "true"
})).Return(nil).Once()
roleStore.On("Save", mock.AnythingOfType("*model.Role")).
Return(func(role *model.Role) *model.Role { return role }, nil).Twice()
appErr := th.App.Srv().doPermissionsMigration(model.MigrationKeyAddManageAgentPermissions, migrationMap, roles)
require.Nil(t, appErr)
assert.Contains(t, systemAdminRole.Permissions, model.PermissionManageOwnAgent.Id)
assert.Contains(t, systemAdminRole.Permissions, model.PermissionManageOthersAgent.Id)
assert.Contains(t, systemUserRole.Permissions, model.PermissionManageOwnAgent.Id)
assert.NotContains(t, systemUserRole.Permissions, model.PermissionManageOthersAgent.Id)
assert.Len(t, systemAdminRole.Permissions, 3)
assert.Len(t, systemUserRole.Permissions, 2)
appErr = th.App.Srv().doPermissionsMigration(model.MigrationKeyAddManageAgentPermissions, migrationMap, roles)
require.Nil(t, appErr)
assert.Len(t, systemAdminRole.Permissions, 3, "system_admin should still have 3 permissions after idempotent run")
assert.Len(t, systemUserRole.Permissions, 2, "system_user should still have 2 permissions after idempotent run")
roleStore.AssertNumberOfCalls(t, "Save", 2)
systemStore.AssertNumberOfCalls(t, "SaveOrUpdate", 1)
}
func TestApplyPermissionsMap(t *testing.T) {
mainHelper.Parallel(t)
tt := []struct {
+1
View File
@@ -96,6 +96,7 @@ func GetMockStoreForSetupFunctions() *mocks.Store {
systemStore.On("GetByName", model.MigrationKeyAddChannelAutoTranslationPermissions).Return(&model.System{Name: model.MigrationKeyAddChannelAutoTranslationPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyRestoreManageOAuthPermission).Return(&model.System{Name: model.MigrationKeyRestoreManageOAuthPermission, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAccessControlPolicyV0_3).Return(&model.System{Name: model.MigrationKeyAccessControlPolicyV0_3, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddManageAgentPermissions).Return(&model.System{Name: model.MigrationKeyAddManageAgentPermissions, Value: "true"}, nil)
systemStore.On("InsertIfExists", mock.AnythingOfType("*model.System")).Return(&model.System{}, nil).Once()
systemStore.On("Save", mock.AnythingOfType("*model.System")).Return(nil)
+1
View File
@@ -61,4 +61,5 @@ const (
MigrationKeyAddSharedChannelManagerPermissions = "system_shared_channel_manager_permissions"
MigrationKeyRestoreManageOAuthPermission = "restore_manage_oauth_permission"
MigrationKeyAccessControlPolicyV0_3 = "access_control_policy_v0_3_migration"
MigrationKeyAddManageAgentPermissions = "add_manage_agent_permissions"
)
+17
View File
@@ -416,6 +416,8 @@ var SysconsoleReadPermissions []*Permission
var SysconsoleWritePermissions []*Permission
var PermissionManageOutgoingOAuthConnections *Permission
var PermissionManageOwnAgent *Permission
var PermissionManageOthersAgent *Permission
var ModeratedBookmarkPermissions []*Permission
func initializePermissions() {
@@ -2328,6 +2330,19 @@ func initializePermissions() {
PermissionScopeSystem,
}
PermissionManageOwnAgent = &Permission{
"manage_own_agent",
"authentication.permissions.manage_own_agent.name",
"authentication.permissions.manage_own_agent.description",
PermissionScopeSystem,
}
PermissionManageOthersAgent = &Permission{
"manage_others_agent",
"authentication.permissions.manage_others_agent.name",
"authentication.permissions.manage_others_agent.description",
PermissionScopeSystem,
}
SysconsoleReadPermissions = []*Permission{
PermissionSysconsoleReadAboutEditionAndLicense,
PermissionSysconsoleReadBilling,
@@ -2524,6 +2539,8 @@ func initializePermissions() {
PermissionManageLicenseInformation,
PermissionCreateCustomGroup,
PermissionManageOutgoingOAuthConnections,
PermissionManageOwnAgent,
PermissionManageOthersAgent,
}
TeamScopedPermissions := []*Permission{
+1
View File
@@ -1108,6 +1108,7 @@ func MakeDefaultRoles() map[string]*Role {
PermissionDeleteCustomGroup.Id,
PermissionRestoreCustomGroup.Id,
PermissionManageCustomGroupMembers.Id,
PermissionManageOwnAgent.Id,
},
SchemeManaged: true,
BuiltIn: true,
+43
View File
@@ -341,3 +341,46 @@ func TestMakeDefaultRolesContainsNewManagerRoles(t *testing.T) {
}), "manage_oauth should not remain deprecated")
})
}
func TestManageAgentPermissionsDefinition(t *testing.T) {
assert.Equal(t, "manage_own_agent", PermissionManageOwnAgent.Id)
assert.Equal(t, "authentication.permissions.manage_own_agent.name", PermissionManageOwnAgent.Name)
assert.Equal(t, "authentication.permissions.manage_own_agent.description", PermissionManageOwnAgent.Description)
assert.Equal(t, PermissionScopeSystem, PermissionManageOwnAgent.Scope,
"manage_own_agent should have system scope")
assert.True(t, slices.ContainsFunc(AllPermissions, func(p *Permission) bool {
return p.Id == PermissionManageOwnAgent.Id
}), "manage_own_agent should be in AllPermissions")
assert.Equal(t, "manage_others_agent", PermissionManageOthersAgent.Id)
assert.Equal(t, "authentication.permissions.manage_others_agent.name", PermissionManageOthersAgent.Name)
assert.Equal(t, "authentication.permissions.manage_others_agent.description", PermissionManageOthersAgent.Description)
assert.Equal(t, PermissionScopeSystem, PermissionManageOthersAgent.Scope,
"manage_others_agent should have system scope")
assert.True(t, slices.ContainsFunc(AllPermissions, func(p *Permission) bool {
return p.Id == PermissionManageOthersAgent.Id
}), "manage_others_agent should be in AllPermissions")
}
func TestManageAgentPermissionsDefaultRoles(t *testing.T) {
roles := MakeDefaultRoles()
for _, tc := range []struct {
roleId string
expectOwn bool
expectOthers bool
}{
{SystemAdminRoleId, true, true},
{SystemUserRoleId, true, false},
{SystemGuestRoleId, false, false},
} {
t.Run(tc.roleId, func(t *testing.T) {
role, ok := roles[tc.roleId]
require.True(t, ok, "%s role should exist", tc.roleId)
assert.Equal(t, tc.expectOwn, slices.Contains(role.Permissions, PermissionManageOwnAgent.Id),
"%s manage_own_agent permission presence", tc.roleId)
assert.Equal(t, tc.expectOthers, slices.Contains(role.Permissions, PermissionManageOthersAgent.Id),
"%s manage_others_agent permission presence", tc.roleId)
})
}
}
@@ -238,6 +238,16 @@ export default class PermissionsTree extends React.PureComponent<Props, State> {
integrationsGroup.permissions.push(outgoingWebhookGroup);
}
}
const manageAgentsGroup = {
id: 'manage_agents_group',
permissions: [
Permissions.MANAGE_OWN_AGENT,
Permissions.MANAGE_OTHERS_AGENT,
],
};
if (!integrationsGroup.permissions.some((p: any) => p.id === 'manage_agents_group')) {
integrationsGroup.permissions.push(manageAgentsGroup);
}
if (config.EnableOAuthServiceProvider === 'true' && !integrationsGroup.permissions.includes(Permissions.MANAGE_OAUTH)) {
integrationsGroup.permissions.push(Permissions.MANAGE_OAUTH);
}
@@ -125,6 +125,16 @@ export const groupRolesStrings: Record<string, Record<string, MessageDescriptor>
defaultMessage: 'Manage own and others\' outgoing webhooks.',
},
}),
manage_agents_group: defineMessages({
name: {
id: 'admin.permissions.group.manage_agents.name',
defaultMessage: 'Manage AI Agents',
},
description: {
id: 'admin.permissions.group.manage_agents.description',
defaultMessage: 'Manage own and others\' AI agents.',
},
}),
manage_slash_commands_group: defineMessages({
name: {
id: 'admin.permissions.group.manage_slash_commands.name',
@@ -35,6 +35,26 @@ export const permissionRolesStrings: Record<string, Record<string, MessageDescri
defaultMessage: 'Convert private channels to public',
},
}),
manage_own_agent: defineMessages({
name: {
id: 'admin.permissions.permission.manage_own_agent.name',
defaultMessage: 'Manage own AI agents',
},
description: {
id: 'admin.permissions.permission.manage_own_agent.description',
defaultMessage: 'Create and manage your own AI agents.',
},
}),
manage_others_agent: defineMessages({
name: {
id: 'admin.permissions.permission.manage_others_agent.name',
defaultMessage: 'Manage others\' AI agents',
},
description: {
id: 'admin.permissions.permission.manage_others_agent.description',
defaultMessage: 'Create and manage AI agents for other users.',
},
}),
create_direct_channel: defineMessages({
name: {
id: 'admin.permissions.permission.create_direct_channel.name',
+6
View File
@@ -1923,6 +1923,8 @@
"admin.permissions.group.guest_use_group_mentions.name": "Group Mentions",
"admin.permissions.group.integrations.description": "Manage OAuth 2.0, slash commands, webhooks and emoji.",
"admin.permissions.group.integrations.name": "Integrations & Customizations",
"admin.permissions.group.manage_agents.description": "Manage own and others' AI agents.",
"admin.permissions.group.manage_agents.name": "Manage AI Agents",
"admin.permissions.group.manage_incoming_webhooks.description": "Manage own and others' incoming webhooks.",
"admin.permissions.group.manage_incoming_webhooks.name": "Manage Incoming Webhooks",
"admin.permissions.group.manage_oauth.description": "Manage own and others' OAuth 2.0 applications.",
@@ -2033,6 +2035,8 @@
"admin.permissions.permission.manage_jobs.name": "Manage jobs",
"admin.permissions.permission.manage_oauth.description": "Create, edit and delete OAuth 2.0 application tokens.",
"admin.permissions.permission.manage_oauth.name": "Manage OAuth Applications",
"admin.permissions.permission.manage_others_agent.description": "Create and manage AI agents for other users.",
"admin.permissions.permission.manage_others_agent.name": "Manage others' AI agents",
"admin.permissions.permission.manage_others_incoming_webhooks.description": "Create, edit, and delete incoming webhooks owned by other users.",
"admin.permissions.permission.manage_others_incoming_webhooks.name": "Manage Others'",
"admin.permissions.permission.manage_others_outgoing_webhooks.description": "Create, edit, and delete outgoing webhooks owned by other users.",
@@ -2043,6 +2047,8 @@
"admin.permissions.permission.manage_outgoing_oauth_connections.name": "Manage Outgoing OAuth Credentials",
"admin.permissions.permission.manage_outgoing_webhooks.description": "Create, edit, and delete outgoing webhooks owned by other users.",
"admin.permissions.permission.manage_outgoing_webhooks.name": "Manage Others'",
"admin.permissions.permission.manage_own_agent.description": "Create and manage your own AI agents.",
"admin.permissions.permission.manage_own_agent.name": "Manage own AI agents",
"admin.permissions.permission.manage_own_incoming_webhooks.description": "Create, edit, and delete your own incoming webhooks.",
"admin.permissions.permission.manage_own_incoming_webhooks.name": "Manage Own",
"admin.permissions.permission.manage_own_outgoing_webhooks.description": "Create, edit, and delete your own outgoing webhooks.",
@@ -267,6 +267,8 @@ const values = {
MANAGE_SECURE_CONNECTIONS: 'manage_secure_connections',
CREATE_CUSTOM_GROUP: 'create_custom_group',
MANAGE_OWN_AGENT: 'manage_own_agent',
MANAGE_OTHERS_AGENT: 'manage_others_agent',
MANAGE_CUSTOM_GROUP_MEMBERS: 'manage_custom_group_members',
EDIT_CUSTOM_GROUP: 'edit_custom_group',
DELETE_CUSTOM_GROUP: 'delete_custom_group',
+3
View File
@@ -1217,6 +1217,8 @@ export const PermissionsScope = {
[Permissions.DELETE_CUSTOM_GROUP]: 'system_scope',
[Permissions.RESTORE_CUSTOM_GROUP]: 'system_scope',
[Permissions.MANAGE_CUSTOM_GROUP_MEMBERS]: 'system_scope',
[Permissions.MANAGE_OWN_AGENT]: 'system_scope',
[Permissions.MANAGE_OTHERS_AGENT]: 'system_scope',
[Permissions.USE_SLASH_COMMANDS]: 'channel_scope',
[Permissions.ADD_BOOKMARK_PUBLIC_CHANNEL]: 'channel_scope',
[Permissions.EDIT_BOOKMARK_PUBLIC_CHANNEL]: 'channel_scope',
@@ -1280,6 +1282,7 @@ export const DefaultRolePermissions = {
Permissions.CREATE_EMOJIS,
Permissions.RUN_VIEW,
Permissions.RESTORE_CUSTOM_GROUP,
Permissions.MANAGE_OWN_AGENT,
Permissions.ADD_BOOKMARK_PUBLIC_CHANNEL,
Permissions.EDIT_BOOKMARK_PUBLIC_CHANNEL,
Permissions.DELETE_BOOKMARK_PUBLIC_CHANNEL,