From 19b7343af27a7d8b8e0e3fc36e592bc8cb507e91 Mon Sep 17 00:00:00 2001 From: Brady Gaster Date: Thu, 3 Sep 2026 15:02:41 -0700 Subject: [PATCH 1/2] fix(gh-aw): preserve research source URLs Allow the public Microsoft Learn and Aspire documentation domains through safe-output URL sanitization while retaining credential and sensitive-query safeguards. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f3bb47aa-1b45-4302-8ec2-5e3dc868de42 --- test/gh-aw-research-online.test.ts | 15 +++++++++++++++ workflows/squad.md | 8 +++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/test/gh-aw-research-online.test.ts b/test/gh-aw-research-online.test.ts index 4dff755a3..e014edcec 100644 --- a/test/gh-aw-research-online.test.ts +++ b/test/gh-aw-research-online.test.ts @@ -117,6 +117,21 @@ describe('gh-aw: /squad research online-documentation capability', () => { expect(researchSkill).toContain('masquerade as one that consulted a source'); }); + it('preserves full public documentation URLs without weakening general URL sanitization', () => { + const safeOutputsMatch = frontmatter.match( + /^safe-outputs:\n((?:[ \t].*\n?)*)/m + ); + expect(safeOutputsMatch, 'safe-outputs: block should exist in frontmatter').not.toBeNull(); + expect(safeOutputsMatch![1]).toMatch( + /allowed-domains:\n\s+- learn\.microsoft\.com\n\s+- aspire\.dev/ + ); + expect(researchSkill).toMatch(/Preserve\s+each public documentation URL in full/); + expect(researchSkill).toContain('do not replace the path with `/redacted`'); + expect(researchSkill).toMatch( + /Never include URL userinfo, credentials, access tokens,[\s\S]*secret-bearing query parameters/ + ); + }); + it('lists Online sources as a required labeled section of the artifact', () => { // The structural contract MUST-contain list includes Online sources. Bound // the match to the enumeration SENTENCE (up to its terminating period) so it diff --git a/workflows/squad.md b/workflows/squad.md index 44427433e..8bd4ec277 100644 --- a/workflows/squad.md +++ b/workflows/squad.md @@ -169,6 +169,9 @@ pre-agent-steps: SQUAD_CAST_VALIDATOR_RUNNER chmod 500 "$validator_runner" safe-outputs: + allowed-domains: + - learn.microsoft.com + - aspire.dev messages: append-only-comments: true run-success: "🤖 [{workflow_name}]({run_url}) finished processing. This completion message does not indicate Cast success. For Cast, only a linked Cast pull request indicates success." @@ -1464,7 +1467,10 @@ section MUST state exactly one status so a later reader or test can assert on it instead of trusting silence: - `Online sources: consulted` — followed by the list of URLs actually fetched - this run (each URL also appears as a citation in the evidence table); or + this run (each URL also appears as a citation in the evidence table). Preserve + each public documentation URL in full, including its path; do not replace the + path with `/redacted`. Never include URL userinfo, credentials, access tokens, + or secret-bearing query parameters; omit those sensitive parts instead; or - `Online sources: unavailable — ` — when no external documentation was fetched, e.g. the network policy disallowed it, no external source was needed, or a requested source-of-truth site was unreachable. From eaa209bb88ae098dd3a20883fe4c17d61ea0ac19 Mon Sep 17 00:00:00 2001 From: Brady Gaster Date: Mon, 7 Sep 2026 21:10:03 -0700 Subject: [PATCH 2/2] test(gh-aw): tighten research URL guards Assert the exact safe-output domain allowlist, tolerate prompt line wrapping, and exclude safe-output metadata from operation max validation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7abb1583-79f9-4355-882d-a9d099f6817f --- test/gh-aw-quality.test.ts | 2 +- test/gh-aw-research-online.test.ts | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/test/gh-aw-quality.test.ts b/test/gh-aw-quality.test.ts index ecf70cf15..8ce4e8744 100644 --- a/test/gh-aw-quality.test.ts +++ b/test/gh-aw-quality.test.ts @@ -326,7 +326,7 @@ describe('gh-aw: safe-output configuration', () => { it('each safe-output has a max value that is a positive integer ≤ 1000', () => { for (const [name, config] of Object.entries(safeOutputs)) { - if (name === 'data' || name === 'messages' || name === 'jobs') continue; + if (name === 'data' || name === 'messages' || name === 'jobs' || name === 'allowed-domains') continue; expect(config.max, `${name} should have a max field`).toBeDefined(); const max = config.max as number; expect(max, `${name}.max should be > 0`).toBeGreaterThan(0); diff --git a/test/gh-aw-research-online.test.ts b/test/gh-aw-research-online.test.ts index e014edcec..c845ead97 100644 --- a/test/gh-aw-research-online.test.ts +++ b/test/gh-aw-research-online.test.ts @@ -122,11 +122,18 @@ describe('gh-aw: /squad research online-documentation capability', () => { /^safe-outputs:\n((?:[ \t].*\n?)*)/m ); expect(safeOutputsMatch, 'safe-outputs: block should exist in frontmatter').not.toBeNull(); - expect(safeOutputsMatch![1]).toMatch( - /allowed-domains:\n\s+- learn\.microsoft\.com\n\s+- aspire\.dev/ + const allowedDomainsMatch = safeOutputsMatch![1].match( + /^ allowed-domains:\n((?: - .+\n?)*)/m ); + expect(allowedDomainsMatch, 'safe-outputs.allowed-domains should exist').not.toBeNull(); + const allowedDomains = allowedDomainsMatch![1] + .split('\n') + .map(line => line.trim()) + .filter(line => line.startsWith('- ')) + .map(line => line.slice(2)); + expect(allowedDomains).toEqual(['learn.microsoft.com', 'aspire.dev']); expect(researchSkill).toMatch(/Preserve\s+each public documentation URL in full/); - expect(researchSkill).toContain('do not replace the path with `/redacted`'); + expect(researchSkill).toMatch(/do not replace the\s+path with `\/redacted`/); expect(researchSkill).toMatch( /Never include URL userinfo, credentials, access tokens,[\s\S]*secret-bearing query parameters/ );