diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 80dd9b1f0b7f90066c9fbd3fa63e6c80527c22b2..35823caa1695972dc29b258cb1b0231b08668c99 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -598,6 +598,10 @@ def all_catalog_resources
Ci::Catalog::Resource.where(project: all_projects)
end
+ def all_ai_catalog_items
+ ::Ai::Catalog::Item.where(project: all_projects)
+ end
+
def all_projects_except_soft_deleted
all_projects.not_aimed_for_deletion
end
diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md
index f3c6b2dacc49428797e87d824ac813bfaf4f0da3..680a38562bd300eee191a511ceb420aecc771fe0 100644
--- a/doc/api/graphql/reference/_index.md
+++ b/doc/api/graphql/reference/_index.md
@@ -336,6 +336,7 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| `itemType` | [`AiCatalogItemType`](#aicatalogitemtype) | Type of items to retrieve. |
| `search` | [`String`](#string) | Search items by name and description. |
+| `verificationLevel` | [`CiCatalogResourceVerificationLevel`](#cicatalogresourceverificationlevel) | Filter items by verification level. |
### `Query.aiChatAvailableModels`
@@ -23505,6 +23506,7 @@ An AI catalog agent.
| `public` | [`Boolean!`](#boolean) | Whether the item is publicly visible in the catalog. |
| `updatedAt` | [`Time!`](#time) | Timestamp of when the item was updated. |
| `userPermissions` | [`AiCatalogItemPermissions!`](#aicatalogitempermissions) | Permissions for the current user on the resource. |
+| `verificationLevel` | [`CiCatalogResourceVerificationLevel`](#cicatalogresourceverificationlevel) | Verification level of the item. |
| `versions` | [`AiCatalogItemVersionConnection`](#aicatalogitemversionconnection) | Versions of the item. (see [Connections](#connections)) |
#### Fields with arguments
@@ -23570,6 +23572,7 @@ An AI catalog flow.
| `public` | [`Boolean!`](#boolean) | Whether the item is publicly visible in the catalog. |
| `updatedAt` | [`Time!`](#time) | Timestamp of when the item was updated. |
| `userPermissions` | [`AiCatalogItemPermissions!`](#aicatalogitempermissions) | Permissions for the current user on the resource. |
+| `verificationLevel` | [`CiCatalogResourceVerificationLevel`](#cicatalogresourceverificationlevel) | Verification level of the item. |
| `versions` | [`AiCatalogItemVersionConnection`](#aicatalogitemversionconnection) | Versions of the item. (see [Connections](#connections)) |
#### Fields with arguments
@@ -52708,6 +52711,7 @@ Implementations:
| `public` | [`Boolean!`](#boolean) | Whether the item is publicly visible in the catalog. |
| `updatedAt` | [`Time!`](#time) | Timestamp of when the item was updated. |
| `userPermissions` | [`AiCatalogItemPermissions!`](#aicatalogitempermissions) | Permissions for the current user on the resource. |
+| `verificationLevel` | [`CiCatalogResourceVerificationLevel`](#cicatalogresourceverificationlevel) | Verification level of the item. |
| `versions` | [`AiCatalogItemVersionConnection`](#aicatalogitemversionconnection) | Versions of the item. (see [Connections](#connections)) |
##### Fields with arguments
diff --git a/ee/app/finders/ai/catalog/items_finder.rb b/ee/app/finders/ai/catalog/items_finder.rb
index 1f9de99777d53c561427f36c0a01c10f9fcc284a..54b671330117d0115844c73c846c270f132457a5 100644
--- a/ee/app/finders/ai/catalog/items_finder.rb
+++ b/ee/app/finders/ai/catalog/items_finder.rb
@@ -15,6 +15,7 @@ def execute
items = init_collection
items = by_organization(items)
items = by_item_type(items)
+ items = by_verification_level(items)
by_search(items)
end
@@ -38,6 +39,12 @@ def by_item_type(items)
items.with_item_type(params[:item_type])
end
+ def by_verification_level(items)
+ return items unless params[:verification_level]
+
+ items.for_verification_level(params[:verification_level])
+ end
+
def by_search(items)
return items if params[:search].blank?
diff --git a/ee/app/graphql/resolvers/ai/catalog/items_resolver.rb b/ee/app/graphql/resolvers/ai/catalog/items_resolver.rb
index 4ca9e8ac16de1ecc475a3c2a8776d1c441f4d937..fab67cb6a2071a7be2907617b023587baa4b2904 100644
--- a/ee/app/graphql/resolvers/ai/catalog/items_resolver.rb
+++ b/ee/app/graphql/resolvers/ai/catalog/items_resolver.rb
@@ -18,6 +18,10 @@ class ItemsResolver < BaseResolver
required: false,
description: 'Search items by name and description.'
+ argument :verification_level, ::Types::Namespaces::VerificationLevelEnum,
+ required: false,
+ description: 'Filter items by verification level.'
+
def resolve_with_lookahead(**args)
items = ::Ai::Catalog::ItemsFinder.new(
current_user,
diff --git a/ee/app/graphql/types/ai/catalog/item_interface.rb b/ee/app/graphql/types/ai/catalog/item_interface.rb
index 7ffe24acbf1cfec752ae9d5d180a8ebabb68b69e..dfcd9367c45bd7c9f252c9758be31013121591a9 100644
--- a/ee/app/graphql/types/ai/catalog/item_interface.rb
+++ b/ee/app/graphql/types/ai/catalog/item_interface.rb
@@ -41,6 +41,9 @@ module ItemInterface
argument :released, ::GraphQL::Types::Boolean, required: false,
description: 'Return the latest released version.'
end
+ field :verification_level, ::Types::Namespaces::VerificationLevelEnum,
+ null: true,
+ description: 'Verification level of the item.'
orphan_types ::Types::Ai::Catalog::AgentType
orphan_types ::Types::Ai::Catalog::FlowType
diff --git a/ee/app/services/ai/catalog/agents/create_service.rb b/ee/app/services/ai/catalog/agents/create_service.rb
index 083f96c7ba7c190feb1b1c1a458708991e6b212c..713761135828aab28e2b5b3e2404b46abbdf6094 100644
--- a/ee/app/services/ai/catalog/agents/create_service.rb
+++ b/ee/app/services/ai/catalog/agents/create_service.rb
@@ -11,7 +11,8 @@ def execute
item_params.merge!(
item_type: Ai::Catalog::Item::AGENT_TYPE,
organization_id: project.organization_id,
- project_id: project.id
+ project_id: project.id,
+ verification_level: verification_level_for_project
)
version_params = {
diff --git a/ee/app/services/ai/catalog/base_service.rb b/ee/app/services/ai/catalog/base_service.rb
index f85656f597b3167ab9085571b8432796e2f76902..2b83de9d4f7787b9ea4396a2df32af1d1a782d67 100644
--- a/ee/app/services/ai/catalog/base_service.rb
+++ b/ee/app/services/ai/catalog/base_service.rb
@@ -33,6 +33,11 @@ def track_ai_item_events(event_type, additional_properties = {})
additional_properties: additional_properties
)
end
+
+ def verification_level_for_project
+ verified_namespace = project.root_namespace.catalog_verified_namespace
+ verified_namespace&.verification_level || :unverified
+ end
end
end
end
diff --git a/ee/app/services/ai/catalog/flows/create_service.rb b/ee/app/services/ai/catalog/flows/create_service.rb
index 626e96d62118b9bb031fbc5cb67b9049b25ceebe..9bbff7471885d72c847694f02a95f9a40b0b28a5 100644
--- a/ee/app/services/ai/catalog/flows/create_service.rb
+++ b/ee/app/services/ai/catalog/flows/create_service.rb
@@ -16,7 +16,8 @@ def execute
item_params.merge!(
item_type: Ai::Catalog::Item::FLOW_TYPE,
organization_id: project.organization_id,
- project_id: project.id
+ project_id: project.id,
+ verification_level: verification_level_for_project
)
version_params = {
schema_version: ::Ai::Catalog::ItemVersion::FLOW_SCHEMA_VERSION,
diff --git a/ee/app/services/namespaces/verify_namespace_service.rb b/ee/app/services/namespaces/verify_namespace_service.rb
index 5ec80ecba4c17a4563ace700f44fb1ecc3251923..9030b3ebc8f5a36ea40aa2be4b3a13c8d6e28e2e 100644
--- a/ee/app/services/namespaces/verify_namespace_service.rb
+++ b/ee/app/services/namespaces/verify_namespace_service.rb
@@ -16,6 +16,7 @@ def execute
create_or_update_verified_namespace
update_catalog_resources
+ update_ai_catalog_items
ServiceResponse.success
end
@@ -55,5 +56,9 @@ def create_or_update_verified_namespace
def update_catalog_resources
namespace.all_catalog_resources.update_all(verification_level: verification_level)
end
+
+ def update_ai_catalog_items
+ namespace.all_ai_catalog_items.update_all(verification_level: verification_level)
+ end
end
end
diff --git a/ee/spec/finders/ai/catalog/items_finder_spec.rb b/ee/spec/finders/ai/catalog/items_finder_spec.rb
index 83cb3618b00a223d0910041c4e7513d1e0644573..8943cedc9bd20aa98bc6da3df7d86d0cde20162b 100644
--- a/ee/spec/finders/ai/catalog/items_finder_spec.rb
+++ b/ee/spec/finders/ai/catalog/items_finder_spec.rb
@@ -98,6 +98,34 @@
end
end
+ context 'when filtering by verification_level' do
+ ::Namespaces::VerifiedNamespace::VERIFICATION_LEVELS.each_key do |level|
+ context "with #{level} verification level" do
+ let(:params) { { verification_level: level } }
+
+ let!(:matching_item) { create(:ai_catalog_flow, public: true, verification_level: level) }
+
+ let!(:non_matching_item) do
+ other_levels = ::Namespaces::VerifiedNamespace::VERIFICATION_LEVELS.keys - [level]
+
+ create(:ai_catalog_flow, public: true, verification_level: other_levels.first)
+ end
+
+ it "returns only items with #{level} verification level" do
+ expect(results.map(&:verification_level).uniq).to eq([level.to_s])
+ end
+ end
+ end
+
+ context 'with invalid verification level' do
+ let(:params) { { verification_level: 'invalid_level' } }
+
+ it 'returns empty result' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
describe 'ordering' do
let_it_be(:gitlab_item_1) { create(:ai_catalog_agent, public: true) }
let_it_be(:regular_item_1) { create(:ai_catalog_agent, public: true) }
diff --git a/ee/spec/graphql/types/ai/catalog/item_interface_spec.rb b/ee/spec/graphql/types/ai/catalog/item_interface_spec.rb
index 1cb8e2d61c7ea11b5bf0899b97949d35eb9da0c0..09f7381f80c171ba7177c8dc20cbd1bfc91398d8 100644
--- a/ee/spec/graphql/types/ai/catalog/item_interface_spec.rb
+++ b/ee/spec/graphql/types/ai/catalog/item_interface_spec.rb
@@ -15,6 +15,7 @@
item_type
name
latest_version
+ verification_level
project
public
updated_at
diff --git a/ee/spec/requests/api/graphql/ai/catalog/item_spec.rb b/ee/spec/requests/api/graphql/ai/catalog/item_spec.rb
index 95f0ecf0bad36e2a3e349312e09cd06bae69410f..e3ee030e605b65415595de3953093951ce143df1 100644
--- a/ee/spec/requests/api/graphql/ai/catalog/item_spec.rb
+++ b/ee/spec/requests/api/graphql/ai/catalog/item_spec.rb
@@ -51,6 +51,7 @@
itemType
name
public
+ verificationLevel
project { id }
latestVersion {
...VersionFragment
@@ -82,6 +83,7 @@
'description' => catalog_item.description,
'itemType' => 'AGENT',
'public' => catalog_item.public,
+ 'verificationLevel' => catalog_item.verification_level.upcase,
'latestVersion' => a_graphql_entity_for(latest_version),
'versions' => hash_including(
'count' => 1,
diff --git a/ee/spec/services/ai/catalog/agents/create_service_spec.rb b/ee/spec/services/ai/catalog/agents/create_service_spec.rb
index 8dfda441ac2fd629eafdb5487ea5e6569b0d5cb9..1e755f901b3150982c4aaa88454c0cf36a2404cc 100644
--- a/ee/spec/services/ai/catalog/agents/create_service_spec.rb
+++ b/ee/spec/services/ai/catalog/agents/create_service_spec.rb
@@ -6,7 +6,7 @@
include Ai::Catalog::TestHelpers
let_it_be(:maintainer) { create(:user) }
- let_it_be(:project) { create(:project, maintainers: maintainer) }
+ let_it_be_with_reload(:project) { create(:project, maintainers: maintainer) }
let(:user) { maintainer }
let(:params) do
@@ -59,7 +59,8 @@
name: params[:name],
description: params[:description],
public: true,
- item_type: Ai::Catalog::Item::AGENT_TYPE.to_s
+ item_type: Ai::Catalog::Item::AGENT_TYPE.to_s,
+ verification_level: 'unverified'
)
expect(item.latest_version).to have_attributes(
schema_version: 1,
@@ -129,4 +130,17 @@
end
end
end
+
+ context 'when project root namespace has a verified namespace' do
+ let_it_be(:verification_level) { :gitlab_maintained }
+
+ it "assigns verification level to the created agent" do
+ create(:catalog_verified_namespace, namespace: project.root_namespace, verification_level: verification_level)
+
+ response
+
+ item = Ai::Catalog::Item.last
+ expect(item.verification_level).to eq(verification_level.to_s)
+ end
+ end
end
diff --git a/ee/spec/services/ai/catalog/flows/create_service_spec.rb b/ee/spec/services/ai/catalog/flows/create_service_spec.rb
index 33cbfc446b91fed82a9d37459b82afffa77023e8..3a82aaa8f0f2c759c2d5adb71b8ec41cf81fb3f1 100644
--- a/ee/spec/services/ai/catalog/flows/create_service_spec.rb
+++ b/ee/spec/services/ai/catalog/flows/create_service_spec.rb
@@ -6,7 +6,7 @@
include Ai::Catalog::TestHelpers
let_it_be(:maintainer) { create(:user) }
- let_it_be(:project) { create(:project, maintainers: maintainer) }
+ let_it_be_with_reload(:project) { create(:project, maintainers: maintainer) }
let_it_be(:agent) { create(:ai_catalog_agent, project: project) }
let_it_be(:v1_0) { create(:ai_catalog_agent_version, item: agent, version: '1.0.0') }
let_it_be(:v1_1) { create(:ai_catalog_agent_version, item: agent, version: '1.1.0') }
@@ -62,7 +62,8 @@
name: params[:name],
description: params[:description],
item_type: Ai::Catalog::Item::FLOW_TYPE.to_s,
- public: true
+ public: true,
+ verification_level: 'unverified'
)
expect(item.latest_version).to have_attributes(
schema_version: ::Ai::Catalog::ItemVersion::FLOW_SCHEMA_VERSION,
@@ -212,4 +213,17 @@
end
end
end
+
+ context 'when project root namespace has a verified namespace' do
+ let_it_be(:verification_level) { :gitlab_maintained }
+
+ it "assigns verification level to the created flow" do
+ create(:catalog_verified_namespace, namespace: project.root_namespace, verification_level: verification_level)
+
+ response
+
+ item = Ai::Catalog::Item.last
+ expect(item.verification_level).to eq(verification_level.to_s)
+ end
+ end
end
diff --git a/ee/spec/services/namespaces/verify_namespace_service_spec.rb b/ee/spec/services/namespaces/verify_namespace_service_spec.rb
index 9219c8d1144f83248feeb192d5fa8b03352c98c5..fc3529383db1421c269e31f6c614cfc9e335a19c 100644
--- a/ee/spec/services/namespaces/verify_namespace_service_spec.rb
+++ b/ee/spec/services/namespaces/verify_namespace_service_spec.rb
@@ -32,6 +32,10 @@
create(:ci_catalog_resource, :published, project: another_group_published_project)
end
+ let_it_be(:group_project_ai_catalog_item) { create(:ai_catalog_item, project: group_project) }
+ let_it_be(:subgroup_project_ai_catalog_item) { create(:ai_catalog_item, project: subgroup_project) }
+ let_it_be(:another_group_ai_catalog_item) { create(:ai_catalog_item, project: another_group_private_project) }
+
describe '#execute' do
context 'when namespace is not a root namespace' do
it 'returns error' do
@@ -124,6 +128,17 @@
expect(another_group_published_project_resource.reload.verification_level).to eq('unverified')
end
+
+ it 'updates the verification level for all AI catalog items under the given namespace' do
+ response = described_class.new(group, verification_level).execute
+
+ expect(response).to be_success
+
+ expect(group_project_ai_catalog_item.reload.verification_level).to eq(verification_level)
+ expect(subgroup_project_ai_catalog_item.reload.verification_level).to eq(verification_level)
+
+ expect(another_group_ai_catalog_item.reload.verification_level).to eq('unverified')
+ end
end
end
end
@@ -159,6 +174,20 @@
expect(another_group_published_project_resource.reload.verification_level).to eq('unverified')
end
+
+ it 'cascades the verification level to the AI catalog items' do
+ ::Namespaces::VerifiedNamespace.find_or_create_by!(namespace: group,
+ verification_level: 'verified_creator_self_managed')
+
+ response = described_class.new(group, verification_level).execute
+
+ expect(response).to be_success
+
+ expect(group_project_ai_catalog_item.reload.verification_level).to eq(verification_level)
+ expect(subgroup_project_ai_catalog_item.reload.verification_level).to eq(verification_level)
+
+ expect(another_group_ai_catalog_item.reload.verification_level).to eq('unverified')
+ end
end
context 'when on gitlab.com' do
@@ -201,6 +230,20 @@
expect(another_group_published_project_resource.reload.verification_level).to eq('unverified')
end
+
+ it 'cascades the verification level to the AI catalog items' do
+ ::Namespaces::VerifiedNamespace.find_or_create_by!(namespace: group,
+ verification_level: 'gitlab_maintained')
+
+ response = described_class.new(group, new_verification_level).execute
+
+ expect(response).to be_success
+
+ expect(group_project_ai_catalog_item.reload.verification_level).to eq(new_verification_level)
+ expect(subgroup_project_ai_catalog_item.reload.verification_level).to eq(new_verification_level)
+
+ expect(another_group_ai_catalog_item.reload.verification_level).to eq('unverified')
+ end
end
end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 0499f14c61d502196c0ec69be9c0b325eef7ef49..2c2404cc19875875212be6ad6db79ede709ecf4d 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -2273,6 +2273,80 @@
end
end
+ describe '#all_ai_catalog_items' do
+ shared_examples 'retrieves AI catalog items' do
+ it 'returns AI catalog items from the namespace projects' do
+ expect(namespace.all_ai_catalog_items).to match_array(expected_items)
+ end
+ end
+
+ shared_examples 'returns empty collection when no projects or items' do
+ it 'returns empty collection' do
+ expect(namespace.all_ai_catalog_items).to be_empty
+ end
+ end
+
+ context 'when namespace is a group' do
+ let_it_be(:namespace) { create(:group) }
+ let_it_be(:project1) { create(:project, namespace: namespace) }
+ let_it_be(:project2) { create(:project, namespace: namespace) }
+ let_it_be(:ai_catalog_item1) { create(:ai_catalog_item, project: project1, organization: project1.organization) }
+ let_it_be(:ai_catalog_item2) { create(:ai_catalog_item, project: project2, organization: project2.organization) }
+ let(:expected_items) { [ai_catalog_item1, ai_catalog_item2] }
+
+ include_examples 'retrieves AI catalog items'
+
+ context 'when namespace has no projects' do
+ let_it_be(:namespace) { create(:group) }
+
+ include_examples 'returns empty collection when no projects or items'
+ end
+
+ context 'when projects have no AI catalog items' do
+ let_it_be(:namespace) { create(:group) }
+ let_it_be(:project_without_items) { create(:project, namespace: namespace) }
+
+ include_examples 'returns empty collection when no projects or items'
+ end
+
+ context 'with nested groups' do
+ let_it_be(:child_group) { create(:group, parent: namespace) }
+ let_it_be(:child_project) { create(:project, namespace: child_group) }
+ let_it_be(:child_ai_catalog_item) { create(:ai_catalog_item, project: child_project, organization: child_project.organization) }
+ let(:expected_items) { [ai_catalog_item1, ai_catalog_item2, child_ai_catalog_item] }
+
+ include_examples 'retrieves AI catalog items'
+ end
+ end
+
+ context 'when namespace is a user namespace' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:namespace) { user.namespace }
+ let_it_be(:project1) { create(:project, namespace: namespace) }
+ let_it_be(:project2) { create(:project, namespace: namespace) }
+ let_it_be(:ai_catalog_item1) { create(:ai_catalog_item, project: project1, organization: project1.organization) }
+ let_it_be(:ai_catalog_item2) { create(:ai_catalog_item, project: project2, organization: project2.organization) }
+ let(:expected_items) { [ai_catalog_item1, ai_catalog_item2] }
+
+ include_examples 'retrieves AI catalog items'
+
+ context 'when namespace has no projects' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:namespace) { user.namespace }
+
+ include_examples 'returns empty collection when no projects or items'
+ end
+
+ context 'when projects have no AI catalog items' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:namespace) { user.namespace }
+ let_it_be(:project_without_items) { create(:project, namespace: namespace) }
+
+ include_examples 'returns empty collection when no projects or items'
+ end
+ end
+ end
+
describe '#share_with_group_lock with subgroups' do
context 'when creating a subgroup' do
let(:subgroup) { create(:group, parent: root_group) }