Gate AI audit event storage on the cascading setting
What does this MR do and why?
Makes ai_audit_events_storage_enabled a working cascading setting (instance -> group -> project), has the audit pipeline honor it, and exposes it via GraphQL. Second and final backend MR for https://gitlab.com/gitlab-org/gitlab/-/work_items/603892, stacked on the columns MR.
Storage is disabled by default: customers must explicitly opt in before AI audit events (which can carry prompt content) are persisted. Streaming and logging are unaffected.
Model and services
- Declares
cascading_attr :ai_audit_events_storage_enabledonNamespaceSettingandProjectSetting, with read delegates onNamespaceandProject. - Write-time cascade per the current recommendation: the setting is registered in
Ai::CascadeDuoSettingsServiceand in the group and application-settings update services, so a change enqueues the existing cascade workers and writes the value down the hierarchy. - Changing the setting requires the new
update_storage_ai_audit_eventspermission, granted to Security Manager (Owner inherits it). This matches the rest of the AI-audit-events family (read_compliance_dashboardfor the report,admin_external_audit_eventsfor streaming destinations) and is stricter than the Maintainer-leveladmin_projectthe other project settings use - a maintainer should not be able to enable storing data they cannot view. Enforced inEE::NamespaceSettings::AssignAttributesService(group params), theprojectSettingsUpdatemutation (hard deny), and anEE::Projects::UpdateServiceparam strip mirroring the SAST carve-out. - New projects inherit the group value at creation (
reset_ai_audit_events_storage_to_inherit_from_namespace, mirroringreset_duo_features_to_inherit_from_namespace), closing the gap between project creation and the next cascade run. - Changing the group value is audited (
ai_audit_events_storage_enabled_updated).
Storage gate
Ai::DuoWorkflows::ProcessAuditEventsWorker skips persistence (Postgres and ClickHouse paths) for events whose resolved setting is off. The gate fails closed: if the event's project/namespace cannot be resolved, the event is not stored. Streaming and audit_json logging still happen either way.
GraphQL
Group.aiAuditEventsStorageEnabled+Group.lockAiAuditEventsStorageEnabled(read), settable viagroupUpdate.ProjectSetting.aiAuditEventsStorageEnabled(read), settable viaprojectSettingsUpdate.- All fields/args are
experiment: { milestone: '19.2' }. The project value follows theProjectSetting-nested pattern (toolApprovalForSessionEnabledprecedent), not the older flat-field-on-Project duplication.
Setting semantics (value + lock)
The value is the default for descendants; the lock is the mandate. Same model as duo_features_enabled:
| Group value | Group lock | Result for projects beneath |
|---|---|---|
| Disabled | Unlocked | Projects default to off, may opt in |
| Disabled | Locked | Off everywhere, projects cannot enable |
| Enabled | Unlocked | Projects default to on, may opt out |
| Enabled | Locked | On everywhere, projects cannot disable |
The same relationship applies instance -> group. Group value writes cascade to all descendants (overwriting prior project choices, like the other Duo settings); lock enforcement is read-time and immediate; new projects copy the group value at creation. A frontend can present fewer of these states than the API supports.
Feature flag
Everything is behind enforce_ai_audit_events_storage_setting (default off): with the flag off, the worker stores as before and the group/project write paths strip the params, so production behavior does not change until rollout.
Database queries
New SQL introduced by this MR:
-
Project creation sync (
reset_ai_audit_events_storage_to_inherit_from_namespace) - single-row update by unique-indexed FK, only when the group value istrue:UPDATE "project_settings" SET "ai_audit_events_storage_enabled" = TRUE WHERE "project_settings"."project_id" = 123 -
Worker gate lookups - primary-key
Project.find_by_id/Namespace.find_by_id, memoized once per[project_id, namespace_id]scope per batch.
The cascade introduces no new query shapes: it adds one column to the SET clause of the existing Ai::CascadeDuoSettingsService batched updates (NamespaceSetting.for_namespaces(ids).update_all / ProjectSetting.for_projects(ids).update_all, batches of 100 via NamespaceEachBatch), which were introduced and database-reviewed with the Duo settings cascade. The group and application-settings update services only strip params when the feature flag is off (no SQL).
References
- Model: https://gitlab.com/gitlab-org/gitlab/-/work_items/604014
- Worker gate: https://gitlab.com/gitlab-org/gitlab/-/work_items/604015
- Parent: https://gitlab.com/gitlab-org/gitlab/-/work_items/603892
- Feature flag rollout: https://gitlab.com/gitlab-org/gitlab/-/work_items/604269
How to set up and validate locally
1. Enable the feature flag
# rails console
Feature.enable(:enforce_ai_audit_events_storage_setting)
# or scope it to a single actor:
Feature.enable(:enforce_ai_audit_events_storage_setting, Project.find_by_full_path('<group>/<project>'))2. Set the setting through the API
With a PAT (api scope), as a group/project owner or security manager (update_storage_ai_audit_events). Group value + lock:
curl -s "http://gdk.test:3000/api/graphql" \
-H "PRIVATE-TOKEN: $TOKEN" -H "Content-Type: application/json" \
-d '{"query":"mutation { groupUpdate(input: { fullPath: \"<group-path>\", aiAuditEventsStorageEnabled: true, lockAiAuditEventsStorageEnabled: true }) { group { aiAuditEventsStorageEnabled lockAiAuditEventsStorageEnabled } errors } }"}'Project:
curl -s "http://gdk.test:3000/api/graphql" \
-H "PRIVATE-TOKEN: $TOKEN" -H "Content-Type: application/json" \
-d '{"query":"mutation { projectSettingsUpdate(input: { fullPath: \"<group-path>/<project-path>\", aiAuditEventsStorageEnabled: true }) { projectSettings { aiAuditEventsStorageEnabled } errors } }"}'Read the group value back with { group(fullPath: "<group-path>") { aiAuditEventsStorageEnabled lockAiAuditEventsStorageEnabled } }. The project value is readable via console (project.ai_audit_events_storage_enabled) — there is no query field on Project by design, matching the toolApprovalForSessionEnabled precedent.
With the flag disabled, both mutations strip the argument: the group mutation leaves the setting unchanged, and the project mutation returns Must provide at least one argument when it was the only argument.
3. Verify events are not stored (PostgreSQL and ClickHouse)
Simulate what the ingest pipeline enqueues, with the setting at its default (off):
# rails console
user = User.find_by(username: 'root')
project = Project.find_by_full_path('<group>/<project>')
workflow = Ai::DuoWorkflows::Workflow.create!(user: user, project: project)
event = {
'cloud_event_id' => SecureRandom.uuid,
'event_name' => 'ai_llm_input_sent',
'created_at' => Time.current.iso8601,
'author_id' => user.id,
'project_id' => project.id,
'namespace_id' => nil,
'ip_address' => nil,
'workflow_id' => workflow.id,
'details' => { 'model' => 'claude' }
}
project.ai_audit_events_storage_enabled # => false (disabled by default)
Ai::DuoWorkflows::ProcessAuditEventsWorker.new.perform([event])
# PostgreSQL - no row:
AuditEvents::AiAuditEvent.exists?(cloud_event_id: event['cloud_event_id']) # => false
# ClickHouse (when use_clickhouse_for_analytics is enabled) - no row:
ClickHouse::DumpWriteBufferWorker.new.perform('ai_audit_events')
ClickHouse::Client.select(
"SELECT count() AS c FROM ai_audit_events WHERE id = '#{event['cloud_event_id']}'", :main
) # => [{"c"=>0}]The worker writes to ClickHouse when Gitlab::ClickHouse.globally_enabled_for_analytics? is true, otherwise to PostgreSQL — check whichever applies to your GDK.
Then opt in (mutation from step 2, or project.project_setting.update!(ai_audit_events_storage_enabled: true)), rerun the worker with a fresh cloud_event_id, and the same checks return the row (exists? => true on the PostgreSQL path, [{"c"=>1}] on the ClickHouse path). Opting back out gates new events again. Streaming and log/audit_json.log receive every event in all cases — only storage is gated.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.