Let a flow trigger another flow through a mention

What does this MR do and why?

When a flow posts a note that mentions another flow's service account, the note author is the first flow's service account. This is because notes made through a composite identity are attributed to the service account. Ai::FlowTriggers::RunService rejects such triggers with :non_human_trigger_not_permitted. This blocks the Software Factory handover, where Duo Code Review mentions Duo Developer.

This MR lets a flow trigger another flow through a mention, within the chain limits from !258724 (merged). The change is behind the ai_flow_trigger_chaining feature flag, which is off by default.

Changes:

  • EE::Notes::PostProcessService#process_ai_flow_triggers finds the workflow that created the note. It uses the Ai::DuoWorkflows::WorkflowNote link with link_type: :created.
    • The lookup only happens when the note author is a composite-identity service account (composite_identity_enforced?). A person's Duo Chat session also writes a created link for the notes it posts, because Workhorse sends the X-Gitlab-Duo-Workflow-Id header and the person owns the session. Without this check, a mention a person typed through Chat would count as a chained run. With the check, it runs as it does today. This matches how the assign and cloud event paths detect a flow (!259133 (merged)).
    • EE::Notes::CreateService writes that link when the note is created with the X-Gitlab-Duo-Workflow-Id header, and only after it checks :update_duo_workflow.
    • The workflow is passed to RunService as parent_workflow, on both the plain path and the messaging adapter path.
    • The existing guard that stops a service account from triggering itself stays.
  • RunService accepts parent_workflow:. When the flag is enabled for the project's top-level namespace and a parent is given, the run is a chained run:
    • current_user becomes the human who started the chain (CallerContext#root_human). Everything that used the triggering user now uses that human: the composite identity link (link_composite_identity!), the OAuth token (composite_identity_token), the workflow creator, the execute_ai_catalog_item checks in the catalog services, the Git committer, and the gateway token.
    • Each hop runs as its own service account plus the root human. It never runs as a service account plus another service account. That would be a nested composite identity, which Gitlab::Auth::Identity#valid? rejects.
    • validation_error replaces the human? check with chain checks:
      • :trigger_depth_exceeded when the chain is too deep.
      • :chain_root_not_human when there is no root human. This covers an autonomous root, a non-human root, or a chain whose ancestors were deleted.
      • :chain_root_not_permitted when the root human does not have trigger_ai_flow on the project.
    • The parent is passed to CreateWorkflowService (non-catalog path) and Ai::Catalog::Flows::ExecuteService (catalog path), so the breadth and namespace checks from !258724 (merged) also apply.
    • New public method RunService#acting_user returns the user the run acts for.
  • Without a parent, or with the flag disabled, behavior does not change. A non-human trigger is still rejected with :non_human_trigger_not_permitted. So a note written by a flow without a created link is rejected. This fails closed.
  • Messaging adapter (Ai::Messaging::Adapters::GitlabDuoNote), used for foundational flows such as Duo Developer:
    • It posts the final reply later from CallbackWorker, and links the reply's service account to the mention author. In a chained run the mention author is the parent flow's service account, so the reply could not be posted.
    • The adapter now accepts an optional scoped_user_id, stores it in the callback context, and links the reply to that user.
    • PostProcessService builds RunService before the adapter. It sets scoped_user_id only when acting_user differs from the note author, so normal mentions do not change.
  • Specs:
    • ee/spec/services/ai/flow_triggers/run_service_spec.rb: chained run uses the root human; composite identity is linked to the root human; token is scoped to the root human; depth limit; autonomous root; root human without trigger_ai_flow; catalog path passes the parent and the root human; flag disabled keeps the old rejection.
    • ee/spec/services/ee/notes/post_process_service_spec.rb: parent is passed when the note has a created link, and not for a triggered link; parent is passed when a composite-identity service account wrote the note, and not when a person wrote it through Duo Chat; the adapter gets scoped_user_id.
    • ee/spec/services/ai/messaging/adapters/gitlab_duo_note_spec.rb: scoped user is serialized and used for the reply link.
    • The existing spec "when a flow is triggered by a service account" covers the case with the flag on and no parent.
  • No changelog, because the change is behind a default-off flag.

Known limitations

  • There is a race. NewNoteWorker is queued when the note's transaction commits. EE::Notes::CreateService writes the created link later, at the end of when_saved, after todos, suggestions, usage counters, tracking, and, for a new diff thread, capturing the diff position. The adapter also writes the link after it posts its reply. If the worker runs first, it finds no parent and rejects the mention. This fails closed, but chained mentions may miss often. It must be fixed before the flag is enabled, and is tracked in https://gitlab.com/gitlab-org/gitlab/-/work_items/631754.
  • As with the API path, the created link proves that the note's author could update the workflow. It does not prove that the workflow wrote the note.
  • This MR alone does not unblock the Duo Code Review to Duo Developer handover. Duo Code Review on master posts its review comments as the internal Duo Code Review bot, which never gets a created link, so a mention in them is still refused. The Software Factory proof of concept (gitlab-org/modelops/applied-ml/code-suggestions/ai-assist!7145) changes the review tool to also post a plain note that mentions Duo Developer and to assign the merge request to it, both with the flow's own token. That note is covered by this MR. The assignment is covered by !259133 (merged).
  • The ai_flow_trigger_chaining flag is for internal testing for now. Enabling it more widely needs an AppSec review first, because the plan https://gitlab.com/gitlab-org/gitlab/-/work_items/630476 still needs a threat model.
  • Not covered in this MR:
    • Assign, assign reviewer, and cloud event paths (MR 5 of the plan, !259133 (merged)).
    • Logging and internal events (MR 6 of the plan).

References

Screenshots or screen recordings

Not applicable. This is a backend-only change.

How to set up and validate locally

  1. In the Rails console, enable the flag:

    Feature.enable(:ai_flow_trigger_chaining)
  2. Set up two mention flow triggers on a project, for example Duo Code Review and Duo Developer.

  3. From a running flow, post a note that mentions the other flow's service account. The note must be created with the flow's composite token and the X-Gitlab-Duo-Workflow-Id header.

  4. Check that:

    • The mentioned flow starts.
    • Its workflow has parent_workflow_id set to the first workflow, and trigger_source set to flow.
    • Its user is the human who started the first flow.
  5. Run the specs:

    bin/rspec ee/spec/services/ai/flow_triggers/run_service_spec.rb ee/spec/services/ee/notes/post_process_service_spec.rb ee/spec/services/ai/messaging/adapters/gitlab_duo_note_spec.rb

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist.

Edited by Igor Drozdov

Merge request reports

Loading
Loading