Surface DIFC-filtered items in tool responses to prevent targeted dispatch drift#2175
Merged
Surface DIFC-filtered items in tool responses to prevent targeted dispatch drift#2175
Conversation
…patch drift Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Copilot created this pull request from a session on behalf of
lpcox
March 19, 2026 18:16
View session
Contributor
There was a problem hiding this comment.
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
buildDIFCFilteredNoticehelper (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-itempartslist 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 whenn <= maxFilteredItemsInNotice(use a placeholder like "(no description)") or (b) switching to count-only when you can’t producenuseful 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, "; "), | ||
| ) |
This was referenced Mar 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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), afterFilterCollectionremoves low-integrity items,ToResult()returns only the accessible items as a plain array. TheFilteredCollectionLabeledDatastruct 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 additionalTextContentblock if any items were removed: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
internal/server/difc_log.gobuildDIFCFilteredNoticehelper +maxFilteredItemsInNoticeconstantinternal/server/unified.godifcFilteredincallBackendTool; append notice afterConvertToCallToolResultinternal/server/difc_log_test.goTesting
make agent-finished)buildDIFCFilteredNoticeSecurity 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.