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_triggersfinds the workflow that created the note. It uses theAi::DuoWorkflows::WorkflowNotelink withlink_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 acreatedlink for the notes it posts, because Workhorse sends theX-Gitlab-Duo-Workflow-Idheader 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::CreateServicewrites that link when the note is created with theX-Gitlab-Duo-Workflow-Idheader, and only after it checks:update_duo_workflow.- The workflow is passed to
RunServiceasparent_workflow, on both the plain path and the messaging adapter path. - The existing guard that stops a service account from triggering itself stays.
- The lookup only happens when the note author is a composite-identity service account (
RunServiceacceptsparent_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_userbecomes 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, theexecute_ai_catalog_itemchecks 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_errorreplaces thehuman?check with chain checks::trigger_depth_exceededwhen the chain is too deep.:chain_root_not_humanwhen there is no root human. This covers an autonomous root, a non-human root, or a chain whose ancestors were deleted.:chain_root_not_permittedwhen the root human does not havetrigger_ai_flowon the project.
- The parent is passed to
CreateWorkflowService(non-catalog path) andAi::Catalog::Flows::ExecuteService(catalog path), so the breadth and namespace checks from !258724 (merged) also apply. - New public method
RunService#acting_userreturns 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 acreatedlink 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. PostProcessServicebuildsRunServicebefore the adapter. It setsscoped_user_idonly whenacting_userdiffers from the note author, so normal mentions do not change.
- It posts the final reply later from
- 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 withouttrigger_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 acreatedlink, and not for atriggeredlink; parent is passed when a composite-identity service account wrote the note, and not when a person wrote it through Duo Chat; the adapter getsscoped_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.
NewNoteWorkeris queued when the note's transaction commits.EE::Notes::CreateServicewrites thecreatedlink later, at the end ofwhen_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
createdlink 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
createdlink, 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_chainingflag 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
- Plan: https://gitlab.com/gitlab-org/gitlab/-/work_items/630476
- MR 0, cloud events caused by a flow skip flow triggers: !258734 (merged)
- MR 1,
parent_workflow_idcolumn andflowtrigger source: !258704 (merged) - MR 2,
Ai::FlowTriggers::CallerContextand chain limits inCreateWorkflowService: !258724 (merged)
Screenshots or screen recordings
Not applicable. This is a backend-only change.
How to set up and validate locally
-
In the Rails console, enable the flag:
Feature.enable(:ai_flow_trigger_chaining) -
Set up two mention flow triggers on a project, for example Duo Code Review and Duo Developer.
-
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-Idheader. -
Check that:
- The mentioned flow starts.
- Its workflow has
parent_workflow_idset to the first workflow, andtrigger_sourceset toflow. - Its
useris the human who started the first flow.
-
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.