Scope BLSA merge request scans to changed files
Targets master. Its base, !257092 (merged), merged on 2026-09-25. !257519 (merged) is stacked on this MR and merges after it.
What does this MR do and why?
Problem: without this MR every merge request scan reviews the whole repository — slower, costlier, and it reports findings unrelated to the change.
- When the goal is a merge request IID and the run checks out a branch other than the default branch, the Business Logic Security Scan now scans only the files the merge request adds or modifies.
- It refuses a merge request that only deletes files, since nothing is left to scan.
- It adds two runtime dials from project CI/CD variables:
BL_TARGET_FILESandBL_SCAN_EFFORT(low,standardorhigh; anything else is logged and sent aslow). They are sent in a new BLSA-only additional-context category,agent_platform_bl_security_context(fieldsscan_effortandtarget_files), not in the shared standard context. All of it sits behind thebl_security_analyzerflag (default off).
Part of the BLSA split of !246889. Tracker: https://gitlab.com/gitlab-org/gitlab/-/work_items/630266. The engine counterpart is ai-assist gitlab-org/modelops/applied-ml/code-suggestions/ai-assist!7077 (merged), which reads the new agent_platform_bl_security_context category; it has not merged yet. Both sides are behind flags: this MR behind bl_security_analyzer, and the engine flow's fan-out stages behind the dap_for_each beta flag.
Behavior
| Case | Without this MR | With this MR |
|---|---|---|
| MR pipeline with a small diff | Whole repository | Only the files the MR's latest diff adds or modifies. Renamed files count under their new path. Deleted files are not scanned |
| MR IID run that checks out the default branch (API or Duo Chat, no source branch) | Whole repository | Whole repository. BL_TARGET_FILES and the MR diff apply only when the run checks out a branch other than the default branch, so default-branch runs are never partial |
| More than 400 added or modified files | Whole repository | Whole repository. target_files is left out and the reason is logged |
| Diff overflowed (or not collected) | Whole repository | Whole repository. target_files is left out and the reason is logged. The scan is never partial |
| A changed path contains whitespace, a comma or a semicolon (the engine splits the list on those) | Whole repository | Whole repository. target_files is left out, reason: unsupported_path is logged |
| MR that only deletes files | Runs a whole-repository scan | Refused before a workflow exists, reason: :no_files_to_scan, including when BL_TARGET_FILES is set or the run checks out the default branch |
| Deletion-only MR whose diff overflowed | Whole repository | Whole repository: its paths are unknown, so it can't be recognized as deletion-only |
BL_TARGET_FILES set |
Ignored | Used as target_files when the run checks out a branch other than the default branch; the MR diff is then not read. Ignored on default-branch runs. A deletion-only MR is still refused |
BL_SCAN_EFFORT unset or blank |
No scan_effort sent |
scan_effort: low |
BL_SCAN_EFFORT set to low, standard or high (any case, surrounding spaces allowed) |
No scan_effort sent |
Sent as scan_effort, stripped and lowercased |
BL_SCAN_EFFORT set to anything else |
No scan_effort sent |
scan_effort: low, reason: invalid_scan_effort is logged |
A goal that is not an MR IID (a pipeline URL, used for a fork's MR) gets no target_files and logs nothing. Most such runs are refused; a goal that resolves to no pipeline or MR runs a whole-repository scan.
The diff is used only when the checked-out branch is the MR's own source branch and the MR isn't from a fork. Otherwise target_files is left out and a reason is logged.
How to review
BlSecurity::TargetFiles.for_goal: reads the paths frommerge_request_diff_files, skipping deleted files. It plucks at most 401 rows.new_pathfalls back toold_pathwhen stored blank. A path with whitespace, a comma or a semicolon returns:unsupported_path(whole-repository scan).BlSecurityContextBuilder.build: returnsnil(no category) unless the flow is this one and the flag is on. It reads only the two variables, unprotected and not file-typed, in the*environment scope. It validates and normalizesscan_effortagainst the engine's tiers. It derivestarget_filesfrom the MR diff only when the goal is an MR IID and the run checks out a branch other than the default branch.BuildAdditionalContextService#with_bl_security_context: appends the category when the builder returns fields. A caller-supplied envelope with this category is always dropped, like the other Rails-controlled categories.- The
no_files_to_scanrefusal in the goal validator. It runs after the flag check and the fork and no-MR refusals.
Backward compatibility / impact on existing flows
| Shared code | Change | Risk | Mitigation |
|---|---|---|---|
StandardContextBuilder and agent_platform_standard_context/1.1.0.json |
None: both are byte-identical to the base | None | The dials live in their own category |
BuildAdditionalContextService |
New optional workflow_definition: and goal: kwargs; appends agent_platform_bl_security_context for this flow with the flag on |
An extra query or a new envelope for other flows | Both kwargs default to nil. The builder returns nil before any query unless the flow is this one and the flag is on; specs assert the context is identical to one built without the flow for other flows and with the flag off |
StartWorkflowService |
Passes the workflow definition and goal to BuildAdditionalContextService |
None beyond the above | Other flows get the same context as before |
agent_platform_bl_security_context/1.0.0.json (new schema) |
scan_effort (required, low/standard/high) and target_files (optional string), additionalProperties: false, like the other per-flow category schemas |
An engine that does not know the category skips it with a warning | The engine counterpart (ai-assist !7077) reads it |
Database queries
Plans from Database Lab (postgresai joe explain, production clones, 2026-09-25).
1. Changed paths of the MR's latest diff (TargetFiles.for_goal)
SELECT "merge_request_diff_files"."new_path", "merge_request_diff_files"."old_path"
FROM "merge_request_diff_files"
WHERE "merge_request_diff_files"."merge_request_diff_id" = $1
AND "merge_request_diff_files"."deleted_file" = FALSE
ORDER BY "merge_request_diff_files"."merge_request_diff_id" ASC,
"merge_request_diff_files"."relative_order" ASC
LIMIT 401- Index:
merge_request_diff_files_pkey, the primary key(merge_request_diff_id, relative_order). The table is range-partitioned onmerge_request_diff_id, so the query hits one partition. TheORDER BYcomes from thehas_many :merge_request_diff_filesscope and matches the key.deleted_fileis a filter on at most one diff's rows, capped byLIMIT 401. - Plan (
gitlab-production-main, a 91-file diff):Limit -> Index Scan using index_6ed6e79676 on merge_request_diff_files_2000000001 (Index Cond: merge_request_diff_id = …, Filter: NOT deleted_file). 9.8 ms total (planning 1.5 ms, execution 8.3 ms), 91 rows. - How often: once per business logic security scan run with
bl_security_analyzeron, when the goal is a merge request IID. The goal validator andBlSecurityContextBuilderboth ask, and the result is memoized inGitlab::SafeRequestStore, so it runs once per request.
2. Scan dials from project CI/CD variables (BlSecurityContextBuilder.build)
SELECT "ci_variables".*
FROM "ci_variables"
WHERE "ci_variables"."project_id" = $1
AND "ci_variables"."protected" = FALSE
AND "ci_variables"."variable_type" = 1
AND "ci_variables"."environment_scope" = '*'
AND "ci_variables"."key" IN ('BL_SCAN_EFFORT', 'BL_TARGET_FILES')- Index:
index_ci_variables_on_project_id_and_key_and_environment_scope, the unique index on(project_id, key, environment_scope). Returns at most 2 rows. - Plan (
gitlab-production-ci, project 278964):Index Scan using index_ci_variables_on_project_id_and_key_and_environment_scope (Index Cond: project_id, key = ANY(…), environment_scope = '*'). 5.4 ms total (planning 1.5 ms, execution 3.8 ms). The addedprotected/variable_typefilters apply on the same index scan (at most 2 rows). - How often: once per business logic security scan workflow start with
bl_security_analyzeron. Other flows, and runs with the flag off, skip it.
Design decisions
- The fallback is a whole-repository scan, never a partial one. A partial scan would look complete. A large MR costs a full scan.
BL_TARGET_FILESwins over the MR diff. It is an explicit project override.- Only added and modified files are scanned. A deletion can still break a check that lives in another file; that file is scanned only if the MR also changes it.
- A deletion-only MR is refused, even with
BL_TARGET_FILESset or on a default-branch run. There is no change to review, so no scan starts. BL_TARGET_FILESand the MR diff apply only when the run checks out a branch other than the default branch; default-branch runs are always whole-repository scans. A default-branch run is ingested as a full scan, so it must not be partial.- MR scans use the MR's latest diff. The workload checks out the branch head at start, not the pipeline SHA, so the file list and the checkout match. A later push gets its own pipeline and scan.
scan_effortdefaults tolow. It keeps the automatic scan cheap. A project sets another tier withBL_SCAN_EFFORT.- Only the
*environment scope is read. A workflow has no environment when it starts. - An unknown
scan_effortbecomeslow. Matches the engine's tiers (it strips and downcases); logged once so a typo is visible. - The dials live in a BLSA-only category,
agent_platform_bl_security_context, built at workflow start (inBuildAdditionalContextService, called fromStartWorkflowService), not in the shared standard context and not through the flow'sadditional_context_resolver. That hook runs only on the trigger path, not on API, Duo Chat, restart or resume starts, which would then lose the dials. A follow-up could make that hook run on all paths, and move this category onto it.
Not in this MR (later in the stack)
- API and Duo Chat runs with an MR IID check out the source branch: !257519 (merged). Until it merges, those runs check out the default branch and stay a whole-repository scan.
File inventory (12 files)
- Scope:
ee/app/models/ai/catalog/foundational_flow/bl_security/target_files.rb(new) - Refusal:
ee/app/models/ai/catalog/foundational_flow/bl_security/definition.rb - Comment:
ee/app/models/ai/catalog/goal_templates/bl_security.rb - BLSA context category:
ee/app/services/ai/duo_workflows/additional_context/bl_security_context_builder.rb(new),app/validators/json_schemas/agent_platform/agent_platform_bl_security_context/1.0.0.json(new),ee/app/services/ai/duo_workflows/build_additional_context_service.rb,ee/app/services/ai/duo_workflows/start_workflow_service.rb - Specs:
target_files_spec.rb(new),bl_security_context_builder_spec.rb(new),foundational_flow_spec.rb,build_additional_context_service_spec.rb,start_workflow_service_spec.rb
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.