-
Notifications
You must be signed in to change notification settings - Fork 418
Improve tool-denial failure report formatting for last denied request #38101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2482,6 +2482,10 @@ describe("handle_agent_failure", () => { | |
| expect(result).toContain("guard.tool_denials_exceeded"); | ||
| expect(result).toContain("daily-spdd-spec-planner"); | ||
| expect(result).toContain("> [!WARNING]"); | ||
| expect(result).toContain("<details>"); | ||
| expect(result).toContain("<summary><strong>Last denied request</strong></summary>"); | ||
| expect(result).toContain("```text"); | ||
| expect(result).toContain("\nread\n"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The primary behavioral change (fallback removal) has zero test coverage. These new assertions verify the happy-path template rendering, but there is no test for what happens when 💡 Suggested fixAdd a test that stubs the file system to throw and asserts the function still produces output rather than propagating the exception — or, if the new intent is to let it throw, document that explicitly with a test asserting the exception type so the caller contract is locked: it("throws when the template file is missing", () => {
// If the design intent is fail-fast, this test documents the contract.
const tmpEnv = process.env.GH_AW_PROMPTS_DIR;
delete process.env.GH_AW_PROMPTS_DIR;
delete process.env.RUNNER_TEMP;
try {
expect(() =>
buildToolDenialsExceededContext([{ denialCount: 1, threshold: 5, reason: "permission denied: read" }], "wf")
).toThrow();
} finally {
if (tmpEnv !== undefined) process.env.GH_AW_PROMPTS_DIR = tmpEnv;
}
});Without a test in either direction, this edge case is invisible to future refactors. |
||
| }); | ||
|
|
||
| it("normalizes Python 3 heredoc reason to a single-line summary", () => { | ||
|
|
@@ -2492,6 +2496,7 @@ describe("handle_agent_failure", () => { | |
| const python3Reason = 'permission denied: shell(python3 << \'EOF\'\nimport re\n\nfiles = [("foo.go", "/path/foo.go")]\nfor f, p in files:\n print(f)\nEOF)'; | ||
| const result = buildToolDenialsExceededContext([{ denialCount: 5, threshold: 5, reason: python3Reason }], "daily-compiler-quality"); | ||
| expect(result).toContain("shell(python3 ...)"); | ||
| expect(result).not.toContain("`shell(python3 ...)`"); | ||
| // The full multi-line program body should not appear in the output | ||
| expect(result).not.toContain("import re"); | ||
| expect(result).not.toContain("for f, p in files"); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,8 +2,14 @@ | |
| > [!WARNING] | ||
| > **Excessive Tool Denials**: The Copilot SDK hit the max tool denial guardrail and stopped the session early (`{denial_count}/{threshold}`). | ||
|
|
||
| **Last denied request:** | ||
| <details> | ||
| <summary><strong>Last denied request</strong></summary> | ||
|
|
||
| ```text | ||
| {reason} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Raw substitution of
💡 Suggested fixSanitize the reason before substitution in the caller: reason: normalizedReason.replace(/`{3,}/g, "``"),Or add a dedicated escaping step in |
||
| ``` | ||
|
|
||
| </details> | ||
|
|
||
| This is a structured guardrail event (`guard.tool_denials_exceeded`) captured in `events.jsonl`. | ||
|
|
||
|
|
@@ -16,7 +22,8 @@ Update the workflow prompt and/or permissions so required actions are permitted: | |
|
|
||
| ``` | ||
| The workflow {workflow_id} stopped because the Copilot SDK exceeded its tool denial threshold ({denial_count}/{threshold}). | ||
| Last denied request: {reason} | ||
| Last denied request: | ||
| {reason} | ||
|
|
||
| Please update the workflow so the prompt only uses tools permitted by the workflow tool policy. | ||
| ``` | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing the fallback silently kills the entire failure report when this template is missing.
The old
try/catchensured that iftool_denials_exceeded_context.mdwas unreadable,buildToolDenialsExceededContextstill returned a useful inline string and the caller could proceed to create the failure issue/comment. Now, if this template is absent orgetPromptPaththrows (e.g. neitherGH_AW_PROMPTS_DIRnorRUNNER_TEMPis set), the exception propagates out of this helper and is caught by the outertry/catchat line 2625 — which only emits acore.warningand returns, so no failure issue or comment is created at all.This is strictly worse than the previous behaviour: a single missing optional template now suppresses the entire
guard.tool_denials_exceededfailure report, leaving users with no feedback.💡 Suggested fix
The main issue/comment templates (
agent_failure_comment.md,agent_failure_issue.md) are read inside the same outertryand have no local fallback — if those fail the report is unavoidably lost. But this helper was specifically architected to degrade gracefully; restoring that property is cheap and meaningfully improves resilience.