Draft: Add compliance control: duo_agentic_flow_governance_oversight_configured

What does this MR do and why?

Adds the duo_agentic_flow_governance_oversight_configured compliance control (control #18 (closed)) to the compliance requirements controls framework.

The EU AI Act (Art. 14, 17) and NIST GV-4.3/MS-2.5 require that human oversight measures exist for high-risk AI systems. This control verifies that Duo Agent Platform flows have those measures in place.

Implementation

The control is implemented as a project field in the compliance requirements framework:

  • New field method duo_agentic_flow_governance_oversight_configured? in ComplianceManagement::ComplianceRequirements::ProjectFields
  • Control registered in ee/config/compliance_management/compliance_controls.yml (boolean, enum_value: 67) and ee/config/compliance_management/requirement_controls.json
  • Field added to the control expression JSON schema
  • Frontend statuses_info.js entry (title, description, remediation guidance) for the standards adherence details drawer
  • Documentation entry in the compliance frameworks controls table

What the field checks (configuration-based, not event history)

  1. Flow trigger configured: At least one active Ai::FlowTrigger applies to the project (direct model check, replacing the flow_trigger_created 90-day audit event look-back). Uses Ai::FlowTrigger.applicable_to_project, the same scope as the firing path: active project triggers plus inherited root-group triggers (inherited only while ai_catalog_group_inherited_consumers is enabled). Inactive triggers count neither for presence nor for broad-scope detection.
  2. MCP explicitly configured: The root ancestor's AI settings have the duo_workflow_mcp_enabled key in configured_at (per-setting provenance). Groups::UpdateService writes this key only when the setting is explicitly submitted, even if the submitted value equals the default. Record presence alone is not a signal. See !256692 (merged).
  3. No ungated broad-scope trigger: A trigger is broad-scope when it is autonomous (scheduled, per Ai::FlowTrigger#autonomous_only?: no human initiator, not branch-scoped), or when it is a pipeline_hooks trigger whose filter does not restrict the ref. Ref scoping follows Gitlab::FilterEvaluator semantics: an all group restricts if any child restricts, an any group only if every child restricts, and a leaf restricts only for object_attributes.ref with eq/in, or object_attributes.default_branch eq true. ne, not_in, contains, and not_contains do not count. If any broad-scope trigger exists, the project must have at least one linked, enforced (non-warn-mode) approval policy with a rule covering all protected branches (branches: [] or branch_type: protected). Approval policies gate merge requests by target branch while the trigger fires on any push, so there is no per-trigger match; requiring approval on every protected branch is the strongest gate expressible with current policy data. Linked policies are already enabled and scope-applicable to the project (Security::PolicyProjectLink is only created under those conditions).

Short-circuits:

Why configuration-based (not event history)

The original spec required querying historical audit events. This was dropped because:

  • Checking historical event occurrences measures product usage, not compliance configuration
  • A correctly configured but low-traffic project would fail purely from inactivity
  • Neither EU AI Act Art. 14/17 nor NIST GV-4.3/MS-2.5 impose time-based activity requirements

This follows the same pattern as the sibling control duo_agentic_flow_audit_events_streaming (!252332 (merged)).

Database

No schema changes. New read queries introduced by duo_agentic_flow_governance_oversight_configured? (all executed once per project evaluation; flow triggers are loaded once and reused):

1. Active flow triggers applicable to the project

Ai::FlowTrigger.applicable_to_project(project), the same query Ai::FlowTriggers::EventTriggerService runs on the firing path (without the triggered_on condition). Still one ai_flow_triggers query. When ai_catalog_group_inherited_consumers is enabled for the root group:

SELECT "ai_flow_triggers".* FROM (
  (SELECT "ai_flow_triggers".* FROM "ai_flow_triggers"
   WHERE "ai_flow_triggers"."project_id" = 86434914 AND "ai_flow_triggers"."active" = TRUE)
  UNION ALL
  (SELECT "ai_flow_triggers".* FROM "ai_flow_triggers"
   INNER JOIN "ai_catalog_item_consumers" ON "ai_catalog_item_consumers"."id" = "ai_flow_triggers"."ai_catalog_item_consumer_id"
   WHERE "ai_catalog_item_consumers"."group_id" = 9970 AND "ai_catalog_item_consumers"."kind" = 1
     AND "ai_flow_triggers"."active" = TRUE)
) ai_flow_triggers

When the flag is disabled, only the first branch runs:

SELECT "ai_flow_triggers".* FROM "ai_flow_triggers"
WHERE "ai_flow_triggers"."project_id" = 86434914 AND "ai_flow_triggers"."active" = TRUE

The SQL above was derived from the scope definitions and not captured from a running instance (the development environment had no database). New plans for the union are pending. Previous plan for the project-only query (before the active condition): https://console.postgres.ai/gitlab/gitlab-production-main/sessions/57132/commands/496770 (Index Scan on index_ai_flow_triggers_on_project_id, 3 rows, 2.2 ms). Also for gitlab-org/gitlab (278964, 26 rows, 60 ms cold): https://console.postgres.ai/gitlab/gitlab-production-main/sessions/57132/commands/496755

2. Root ancestor AI settings

SELECT "namespace_ai_settings".* FROM "namespace_ai_settings" WHERE "namespace_ai_settings"."namespace_id" = 9970 LIMIT 1

Plan: https://console.postgres.ai/gitlab/gitlab-production-main/sessions/57132/commands/496779 (Index Scan on index_namespace_ai_settings_on_namespace_id, 4.6 ms)

3. Enforced approval policies linked to the project

Only runs when a broad-scope trigger (autonomous, or pipeline_hooks without ref scoping) exists.

SELECT "security_policies".* FROM "security_policies"
INNER JOIN "security_policy_project_links" ON "security_policies"."id" = "security_policy_project_links"."security_policy_id"
WHERE "security_policy_project_links"."project_id" = 86434914
  AND "security_policies"."type" = 0
  AND (content->>'enforcement_type' IS DISTINCT FROM 'warn')

Plan: https://console.postgres.ai/gitlab/gitlab-production-main/sessions/57132/commands/496772 (Nested Loop over idx_security_policy_project_links_on_project_id_and_id + security_policies_pkey, 3 rows, 3.7 ms)

4. Undeleted rules for those policies (preload)

SELECT "approval_policy_rules".* FROM "approval_policy_rules"
WHERE (rule_index >= 0) AND "approval_policy_rules"."security_policy_id" IN (2480423, 2480422, 2480424)

Plan: https://console.postgres.ai/gitlab/gitlab-production-main/sessions/57132/commands/496777 (Index Scan on idx_approval_policy_rules_security_policy_id_id, 3 rows, 10 ms)

References

Screenshots or screen recordings

N/A - no user-facing UI changes beyond the new control's entry in the standards adherence details drawer.

How to set up and validate locally

  1. Create a group with a project
  2. Enable the Duo Agent Platform for the group
  3. Create an Ai::FlowTrigger for the project, then record MCP provenance on the root group: Groups::UpdateService.new(group, owner, ai_settings_attributes: { duo_workflow_mcp_enabled: true }).execute
  4. Evaluate the field in a Rails console:
project = Project.find(<project_id>)
ComplianceManagement::ComplianceRequirements::ProjectFields.map_field(
  project, 'duo_agentic_flow_governance_oversight_configured'
)
# => true (compliant) / false (non-compliant)

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.

Edited by Duo Developer

Merge request reports

Loading
Loading