Skip to content

Surface DIFC-filtered items in tool responses to prevent targeted dispatch drift#2175

Merged
lpcox merged 1 commit intomainfrom
copilot/fix-issue-21784
Mar 19, 2026
Merged

Surface DIFC-filtered items in tool responses to prevent targeted dispatch drift#2175
lpcox merged 1 commit intomainfrom
copilot/fix-issue-21784

Conversation

Copy link
Contributor

Copilot AI commented Mar 19, 2026

Summary

Fixes the core gateway-side issue described in gh-aw#21784.

When DIFC integrity policy removes items from a tool response (e.g. list_issues) in filter/propagate mode, the agent previously received only the accessible items with no indication that filtering occurred. An empty result looked identical to a genuine "no items" response, causing targeted-dispatch workflows to silently fall back to scheduled/backlog-scan mode.

Root cause

In callBackendTool (Phase 5 of the DIFC reference-monitor pipeline), after FilterCollection removes low-integrity items, ToResult() returns only the accessible items as a plain array. The FilteredCollectionLabeledData struct tracked filtered details internally (for audit logging) but never exposed them to the caller.

Fix

After converting the filtered result to an SDK CallToolResult, append an additional TextContent block if any items were removed:

[DIFC] 1 item(s) in this response were removed by integrity policy and are not shown: issue:org/repo#14 (integrity too low for agent context).

For up to 5 filtered items the notice includes per-item description + reason. For larger sets only the count is reported.

This applies only in filter/propagate enforcement modes. Strict mode already returns an explicit error blocking the entire response.

Changes

File Change
internal/server/difc_log.go buildDIFCFilteredNotice helper + maxFilteredItemsInNotice constant
internal/server/unified.go Track difcFiltered in callBackendTool; append notice after ConvertToCallToolResult
internal/server/difc_log_test.go 6 new unit tests: nil input, empty, single item, within limit, exceeds limit, no description

Testing

  • All existing unit and integration tests pass (make agent-finished)
  • 6 new tests for buildDIFCFilteredNotice
  • CodeQL: 0 alerts

Security Summary

No new security vulnerabilities introduced. The notice text contains only the count of filtered items and their already-logged resource descriptions and denial reasons — no data from the filtered items' payload is exposed.

…patch drift

Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Copilot AI requested a review from lpcox March 19, 2026 18:16
@lpcox lpcox marked this pull request as ready for review March 19, 2026 18:20
Copilot AI review requested due to automatic review settings March 19, 2026 18:20
@lpcox lpcox merged commit 9322ba8 into main Mar 19, 2026
20 checks passed
@lpcox lpcox deleted the copilot/fix-issue-21784 branch March 19, 2026 18:20
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR surfaces a DIFC integrity-filtering indicator to agents by appending a human-readable notice to tool responses when items are removed in filter/propagate modes, preventing “empty == no items” ambiguity that can cause targeted-dispatch drift.

Changes:

  • Track filtered-out DIFC items during Phase 5 filtering and append a notice to the returned CallToolResult.
  • Add buildDIFCFilteredNotice helper (with an item limit) to generate concise agent-facing notices.
  • Add unit tests covering notice construction across nil/empty/single/multi/limit/no-description scenarios.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
internal/server/unified.go Captures filtered response metadata and appends a DIFC-filtering notice to tool results.
internal/server/difc_log.go Adds the helper/constant to format an agent-facing notice about filtered items.
internal/server/difc_log_test.go Adds unit tests for the notice formatting helper.
Comments suppressed due to low confidence (1)

internal/server/difc_log.go:146

  • The notice uses n := filtered.GetFilteredCount() for the headline count, but the per-item parts list can be shorter because items with both empty description and empty reason are skipped. This can produce a message like “3 item(s)…: <only 2 entries>”, which is misleading. Consider either (a) not skipping items when n <= maxFilteredItemsInNotice (use a placeholder like "(no description)") or (b) switching to count-only when you can’t produce n useful entries (or adding “and X more”).
	if n <= maxFilteredItemsInNotice {
		parts := make([]string, 0, n)
		for _, detail := range filtered.Filtered {
			desc := ""
			if detail.Item.Labels != nil {
				desc = detail.Item.Labels.Description
			}
			// Skip items that carry no useful identifying information.
			if desc == "" && detail.Reason == "" {
				continue
			}
			if desc != "" && detail.Reason != "" {
				parts = append(parts, fmt.Sprintf("%s (%s)", desc, detail.Reason))
			} else if desc != "" {
				parts = append(parts, desc)
			} else {
				parts = append(parts, detail.Reason)
			}
		}
		if len(parts) > 0 {
			return fmt.Sprintf(
				"[DIFC] %d item(s) in this response were removed by integrity policy and are not shown: %s.",
				n, strings.Join(parts, "; "),
			)
		}
	}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +1033 to +1041
// If items were filtered by DIFC policy in filter/propagate mode, append a notice so
// the agent knows items exist but were withheld. Without this, an agent receiving an
// empty (or partial) list has no way to distinguish "no items" from "items filtered",
// which can cause targeted-dispatch workflows to silently fall back to scheduled mode.
if difcFiltered != nil {
if notice := buildDIFCFilteredNotice(difcFiltered); notice != "" {
callResult.Content = append(callResult.Content, &sdk.TextContent{Text: notice})
}
}
Comment on lines +121 to +144
if n <= maxFilteredItemsInNotice {
parts := make([]string, 0, n)
for _, detail := range filtered.Filtered {
desc := ""
if detail.Item.Labels != nil {
desc = detail.Item.Labels.Description
}
// Skip items that carry no useful identifying information.
if desc == "" && detail.Reason == "" {
continue
}
if desc != "" && detail.Reason != "" {
parts = append(parts, fmt.Sprintf("%s (%s)", desc, detail.Reason))
} else if desc != "" {
parts = append(parts, desc)
} else {
parts = append(parts, detail.Reason)
}
}
if len(parts) > 0 {
return fmt.Sprintf(
"[DIFC] %d item(s) in this response were removed by integrity policy and are not shown: %s.",
n, strings.Join(parts, "; "),
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants