3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2025-10-01 05:29:28 +00:00

update improvers

This commit is contained in:
Don Syme 2025-09-17 13:19:24 +01:00
parent 7268136bb6
commit 2364ea42ba
12 changed files with 295 additions and 228 deletions

83
.github/workflows/ask.lock.yml generated vendored
View file

@ -2,7 +2,7 @@
# To update this file, edit the corresponding .md file and run: # To update this file, edit the corresponding .md file and run:
# gh aw compile # gh aw compile
# #
# Effective stop-time: 2025-09-19 10:32:53 # Effective stop-time: 2025-09-19 12:19:14
name: "Question Answering Researcher" name: "Question Answering Researcher"
on: on:
@ -594,7 +594,7 @@ jobs:
main(); main();
- name: Setup Safe Outputs Collector MCP - name: Setup Safe Outputs Collector MCP
env: env:
GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":{\"enabled\":true}}" GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-comment\":{\"enabled\":true}}"
run: | run: |
mkdir -p /tmp/safe-outputs mkdir -p /tmp/safe-outputs
cat > /tmp/safe-outputs/mcp-server.cjs << 'EOF' cat > /tmp/safe-outputs/mcp-server.cjs << 'EOF'
@ -747,7 +747,7 @@ jobs:
}, },
}, },
{ {
name: "add-issue-comment", name: "add-comment",
description: "Add a comment to a GitHub issue or pull request", description: "Add a comment to a GitHub issue or pull request",
inputSchema: { inputSchema: {
type: "object", type: "object",
@ -854,7 +854,7 @@ jobs:
}, },
}, },
{ {
name: "add-issue-labels", name: "add-labels",
description: "Add labels to a GitHub issue or pull request", description: "Add labels to a GitHub issue or pull request",
inputSchema: { inputSchema: {
type: "object", type: "object",
@ -1028,7 +1028,7 @@ jobs:
- name: Setup MCPs - name: Setup MCPs
env: env:
GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }}
GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":{\"enabled\":true}}" GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-comment\":{\"enabled\":true}}"
run: | run: |
mkdir -p /tmp/mcp-config mkdir -p /tmp/mcp-config
cat > /tmp/mcp-config/mcp-servers.json << 'EOF' cat > /tmp/mcp-config/mcp-servers.json << 'EOF'
@ -1066,7 +1066,7 @@ jobs:
WORKFLOW_NAME="Question Answering Researcher" WORKFLOW_NAME="Question Answering Researcher"
# Check stop-time limit # Check stop-time limit
STOP_TIME="2025-09-19 10:32:53" STOP_TIME="2025-09-19 12:19:14"
echo "Checking stop-time limit: $STOP_TIME" echo "Checking stop-time limit: $STOP_TIME"
# Convert stop time to epoch seconds # Convert stop time to epoch seconds
@ -1157,7 +1157,7 @@ jobs:
**Adding a Comment to an Issue or Pull Request** **Adding a Comment to an Issue or Pull Request**
To add a comment to an issue or pull request, use the add-issue-comments tool from the safe-outputs MCP To add a comment to an issue or pull request, use the add-comments tool from the safe-outputs MCP
EOF EOF
- name: Print prompt to step summary - name: Print prompt to step summary
@ -1327,7 +1327,7 @@ jobs:
uses: actions/github-script@v8 uses: actions/github-script@v8
env: env:
GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }}
GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":{\"enabled\":true}}" GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-comment\":{\"enabled\":true}}"
with: with:
script: | script: |
async function main() { async function main() {
@ -1360,15 +1360,12 @@ jobs:
let sanitized = content; let sanitized = content;
// Neutralize @mentions to prevent unintended notifications // Neutralize @mentions to prevent unintended notifications
sanitized = neutralizeMentions(sanitized); sanitized = neutralizeMentions(sanitized);
// Remove XML comments to prevent content hiding
sanitized = removeXmlComments(sanitized);
// Remove ANSI escape sequences BEFORE removing control characters
sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
// Remove control characters (except newlines and tabs) // Remove control characters (except newlines and tabs)
sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
// XML character escaping
sanitized = sanitized
.replace(/&/g, "&amp;") // Must be first to avoid double-escaping
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
// URI filtering - replace non-https protocols with "(redacted)" // URI filtering - replace non-https protocols with "(redacted)"
sanitized = sanitizeUrlProtocols(sanitized); sanitized = sanitizeUrlProtocols(sanitized);
// Domain filtering for HTTPS URIs // Domain filtering for HTTPS URIs
@ -1388,8 +1385,7 @@ jobs:
lines.slice(0, maxLines).join("\n") + lines.slice(0, maxLines).join("\n") +
"\n[Content truncated due to line count]"; "\n[Content truncated due to line count]";
} }
// Remove ANSI escape sequences // ANSI escape sequences already removed earlier in the function
sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
// Neutralize common bot trigger phrases // Neutralize common bot trigger phrases
sanitized = neutralizeBotTriggers(sanitized); sanitized = neutralizeBotTriggers(sanitized);
// Trim excessive whitespace // Trim excessive whitespace
@ -1401,10 +1397,12 @@ jobs:
*/ */
function sanitizeUrlDomains(s) { function sanitizeUrlDomains(s) {
return s.replace( return s.replace(
/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, /\bhttps:\/\/[^\s\])}'"<>&\x00-\x1f,;]+/gi,
(match, domain) => { (match) => {
// Extract just the URL part after https://
const urlAfterProtocol = match.slice(8); // Remove 'https://'
// Extract the hostname part (before first slash, colon, or other delimiter) // Extract the hostname part (before first slash, colon, or other delimiter)
const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase(); const hostname = urlAfterProtocol.split(/[\/:\?#]/)[0].toLowerCase();
// Check if this domain or any parent domain is in the allowlist // Check if this domain or any parent domain is in the allowlist
const isAllowed = allowedDomains.some(allowedDomain => { const isAllowed = allowedDomains.some(allowedDomain => {
const normalizedAllowed = allowedDomain.toLowerCase(); const normalizedAllowed = allowedDomain.toLowerCase();
@ -1423,9 +1421,10 @@ jobs:
* @returns {string} The string with non-https protocols redacted * @returns {string} The string with non-https protocols redacted
*/ */
function sanitizeUrlProtocols(s) { function sanitizeUrlProtocols(s) {
// Match both protocol:// and protocol: patterns // Match protocol:// patterns (URLs) and standalone protocol: patterns that look like URLs
// Avoid matching command line flags like -v:10 or z3 -memory:high
return s.replace( return s.replace(
/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, /\b(\w+):\/\/[^\s\])}'"<>&\x00-\x1f]+/gi,
(match, protocol) => { (match, protocol) => {
// Allow https (case insensitive), redact everything else // Allow https (case insensitive), redact everything else
return protocol.toLowerCase() === "https" ? match : "(redacted)"; return protocol.toLowerCase() === "https" ? match : "(redacted)";
@ -1444,6 +1443,16 @@ jobs:
(_m, p1, p2) => `${p1}\`@${p2}\`` (_m, p1, p2) => `${p1}\`@${p2}\``
); );
} }
/**
* Removes XML comments to prevent content hiding
* @param {string} s - The string to process
* @returns {string} The string with XML comments removed
*/
function removeXmlComments(s) {
// Remove XML/HTML comments including malformed ones that might be used to hide content
// Matches: <!-- ... --> and <!--- ... --> and <!--- ... --!> variations
return s.replace(/<!--[\s\S]*?-->/g, "").replace(/<!--[\s\S]*?--!>/g, "");
}
/** /**
* Neutralizes bot trigger phrases by wrapping them in backticks * Neutralizes bot trigger phrases by wrapping them in backticks
* @param {string} s - The string to process * @param {string} s - The string to process
@ -1477,13 +1486,13 @@ jobs:
switch (itemType) { switch (itemType) {
case "create-issue": case "create-issue":
return 1; // Only one issue allowed return 1; // Only one issue allowed
case "add-issue-comment": case "add-comment":
return 1; // Only one comment allowed return 1; // Only one comment allowed
case "create-pull-request": case "create-pull-request":
return 1; // Only one pull request allowed return 1; // Only one pull request allowed
case "create-pull-request-review-comment": case "create-pull-request-review-comment":
return 10; // Default to 10 review comments allowed return 10; // Default to 10 review comments allowed
case "add-issue-labels": case "add-labels":
return 5; // Only one labels operation allowed return 5; // Only one labels operation allowed
case "update-issue": case "update-issue":
return 1; // Only one issue update allowed return 1; // Only one issue update allowed
@ -1698,10 +1707,10 @@ jobs:
); );
} }
break; break;
case "add-issue-comment": case "add-comment":
if (!item.body || typeof item.body !== "string") { if (!item.body || typeof item.body !== "string") {
errors.push( errors.push(
`Line ${i + 1}: add-issue-comment requires a 'body' string field` `Line ${i + 1}: add-comment requires a 'body' string field`
); );
continue; continue;
} }
@ -1736,10 +1745,10 @@ jobs:
); );
} }
break; break;
case "add-issue-labels": case "add-labels":
if (!item.labels || !Array.isArray(item.labels)) { if (!item.labels || !Array.isArray(item.labels)) {
errors.push( errors.push(
`Line ${i + 1}: add-issue-labels requires a 'labels' array field` `Line ${i + 1}: add-labels requires a 'labels' array field`
); );
continue; continue;
} }
@ -1749,7 +1758,7 @@ jobs:
) )
) { ) {
errors.push( errors.push(
`Line ${i + 1}: add-issue-labels labels array must contain only strings` `Line ${i + 1}: add-labels labels array must contain only strings`
); );
continue; continue;
} }
@ -2671,11 +2680,11 @@ jobs:
pull-requests: write pull-requests: write
timeout-minutes: 10 timeout-minutes: 10
outputs: outputs:
comment_id: ${{ steps.create_comment.outputs.comment_id }} comment_id: ${{ steps.add_comment.outputs.comment_id }}
comment_url: ${{ steps.create_comment.outputs.comment_url }} comment_url: ${{ steps.add_comment.outputs.comment_url }}
steps: steps:
- name: Add Issue Comment - name: Add Issue Comment
id: create_comment id: add_comment
uses: actions/github-script@v8 uses: actions/github-script@v8
env: env:
GITHUB_AW_AGENT_OUTPUT: ${{ needs.question-answering-researcher.outputs.output }} GITHUB_AW_AGENT_OUTPUT: ${{ needs.question-answering-researcher.outputs.output }}
@ -2709,15 +2718,15 @@ jobs:
core.info("No valid items found in agent output"); core.info("No valid items found in agent output");
return; return;
} }
// Find all add-issue-comment items // Find all add-comment items
const commentItems = validatedOutput.items.filter( const commentItems = validatedOutput.items.filter(
/** @param {any} item */ item => item.type === "add-issue-comment" /** @param {any} item */ item => item.type === "add-comment"
); );
if (commentItems.length === 0) { if (commentItems.length === 0) {
core.info("No add-issue-comment items found in agent output"); core.info("No add-comment items found in agent output");
return; return;
} }
core.info(`Found ${commentItems.length} add-issue-comment item(s)`); core.info(`Found ${commentItems.length} add-comment item(s)`);
// If in staged mode, emit step summary instead of creating comments // If in staged mode, emit step summary instead of creating comments
if (isStaged) { if (isStaged) {
let summaryContent = "## 🎭 Staged Mode: Add Comments Preview\n\n"; let summaryContent = "## 🎭 Staged Mode: Add Comments Preview\n\n";
@ -2761,7 +2770,7 @@ jobs:
for (let i = 0; i < commentItems.length; i++) { for (let i = 0; i < commentItems.length; i++) {
const commentItem = commentItems[i]; const commentItem = commentItems[i];
core.info( core.info(
`Processing add-issue-comment item ${i + 1}/${commentItems.length}: bodyLength=${commentItem.body.length}` `Processing add-comment item ${i + 1}/${commentItems.length}: bodyLength=${commentItem.body.length}`
); );
// Determine the issue/PR number and comment endpoint for this comment // Determine the issue/PR number and comment endpoint for this comment
let issueNumber; let issueNumber;

View file

@ -11,7 +11,7 @@ permissions: read-all
network: defaults network: defaults
safe-outputs: safe-outputs:
add-issue-comment: add-comment:
tools: tools:
web-fetch: web-fetch:

83
.github/workflows/ci-doctor.lock.yml generated vendored
View file

@ -2,7 +2,7 @@
# To update this file, edit the corresponding .md file and run: # To update this file, edit the corresponding .md file and run:
# gh aw compile # gh aw compile
# #
# Effective stop-time: 2025-09-19 10:32:53 # Effective stop-time: 2025-09-19 12:19:14
name: "CI Failure Doctor" name: "CI Failure Doctor"
"on": "on":
@ -75,7 +75,7 @@ jobs:
main(); main();
- name: Setup Safe Outputs Collector MCP - name: Setup Safe Outputs Collector MCP
env: env:
GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":{\"enabled\":true},\"create-issue\":true}" GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-comment\":{\"enabled\":true},\"create-issue\":true}"
run: | run: |
mkdir -p /tmp/safe-outputs mkdir -p /tmp/safe-outputs
cat > /tmp/safe-outputs/mcp-server.cjs << 'EOF' cat > /tmp/safe-outputs/mcp-server.cjs << 'EOF'
@ -228,7 +228,7 @@ jobs:
}, },
}, },
{ {
name: "add-issue-comment", name: "add-comment",
description: "Add a comment to a GitHub issue or pull request", description: "Add a comment to a GitHub issue or pull request",
inputSchema: { inputSchema: {
type: "object", type: "object",
@ -335,7 +335,7 @@ jobs:
}, },
}, },
{ {
name: "add-issue-labels", name: "add-labels",
description: "Add labels to a GitHub issue or pull request", description: "Add labels to a GitHub issue or pull request",
inputSchema: { inputSchema: {
type: "object", type: "object",
@ -509,7 +509,7 @@ jobs:
- name: Setup MCPs - name: Setup MCPs
env: env:
GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }}
GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":{\"enabled\":true},\"create-issue\":true}" GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-comment\":{\"enabled\":true},\"create-issue\":true}"
run: | run: |
mkdir -p /tmp/mcp-config mkdir -p /tmp/mcp-config
cat > /tmp/mcp-config/mcp-servers.json << 'EOF' cat > /tmp/mcp-config/mcp-servers.json << 'EOF'
@ -547,7 +547,7 @@ jobs:
WORKFLOW_NAME="CI Failure Doctor" WORKFLOW_NAME="CI Failure Doctor"
# Check stop-time limit # Check stop-time limit
STOP_TIME="2025-09-19 10:32:53" STOP_TIME="2025-09-19 12:19:14"
echo "Checking stop-time limit: $STOP_TIME" echo "Checking stop-time limit: $STOP_TIME"
# Convert stop time to epoch seconds # Convert stop time to epoch seconds
@ -770,7 +770,7 @@ jobs:
**Adding a Comment to an Issue or Pull Request** **Adding a Comment to an Issue or Pull Request**
To add a comment to an issue or pull request, use the add-issue-comments tool from the safe-outputs MCP To add a comment to an issue or pull request, use the add-comments tool from the safe-outputs MCP
**Creating an Issue** **Creating an Issue**
@ -941,7 +941,7 @@ jobs:
uses: actions/github-script@v8 uses: actions/github-script@v8
env: env:
GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }}
GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":{\"enabled\":true},\"create-issue\":true}" GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-comment\":{\"enabled\":true},\"create-issue\":true}"
with: with:
script: | script: |
async function main() { async function main() {
@ -974,15 +974,12 @@ jobs:
let sanitized = content; let sanitized = content;
// Neutralize @mentions to prevent unintended notifications // Neutralize @mentions to prevent unintended notifications
sanitized = neutralizeMentions(sanitized); sanitized = neutralizeMentions(sanitized);
// Remove XML comments to prevent content hiding
sanitized = removeXmlComments(sanitized);
// Remove ANSI escape sequences BEFORE removing control characters
sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
// Remove control characters (except newlines and tabs) // Remove control characters (except newlines and tabs)
sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
// XML character escaping
sanitized = sanitized
.replace(/&/g, "&amp;") // Must be first to avoid double-escaping
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
// URI filtering - replace non-https protocols with "(redacted)" // URI filtering - replace non-https protocols with "(redacted)"
sanitized = sanitizeUrlProtocols(sanitized); sanitized = sanitizeUrlProtocols(sanitized);
// Domain filtering for HTTPS URIs // Domain filtering for HTTPS URIs
@ -1002,8 +999,7 @@ jobs:
lines.slice(0, maxLines).join("\n") + lines.slice(0, maxLines).join("\n") +
"\n[Content truncated due to line count]"; "\n[Content truncated due to line count]";
} }
// Remove ANSI escape sequences // ANSI escape sequences already removed earlier in the function
sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
// Neutralize common bot trigger phrases // Neutralize common bot trigger phrases
sanitized = neutralizeBotTriggers(sanitized); sanitized = neutralizeBotTriggers(sanitized);
// Trim excessive whitespace // Trim excessive whitespace
@ -1015,10 +1011,12 @@ jobs:
*/ */
function sanitizeUrlDomains(s) { function sanitizeUrlDomains(s) {
return s.replace( return s.replace(
/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, /\bhttps:\/\/[^\s\])}'"<>&\x00-\x1f,;]+/gi,
(match, domain) => { (match) => {
// Extract just the URL part after https://
const urlAfterProtocol = match.slice(8); // Remove 'https://'
// Extract the hostname part (before first slash, colon, or other delimiter) // Extract the hostname part (before first slash, colon, or other delimiter)
const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase(); const hostname = urlAfterProtocol.split(/[\/:\?#]/)[0].toLowerCase();
// Check if this domain or any parent domain is in the allowlist // Check if this domain or any parent domain is in the allowlist
const isAllowed = allowedDomains.some(allowedDomain => { const isAllowed = allowedDomains.some(allowedDomain => {
const normalizedAllowed = allowedDomain.toLowerCase(); const normalizedAllowed = allowedDomain.toLowerCase();
@ -1037,9 +1035,10 @@ jobs:
* @returns {string} The string with non-https protocols redacted * @returns {string} The string with non-https protocols redacted
*/ */
function sanitizeUrlProtocols(s) { function sanitizeUrlProtocols(s) {
// Match both protocol:// and protocol: patterns // Match protocol:// patterns (URLs) and standalone protocol: patterns that look like URLs
// Avoid matching command line flags like -v:10 or z3 -memory:high
return s.replace( return s.replace(
/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, /\b(\w+):\/\/[^\s\])}'"<>&\x00-\x1f]+/gi,
(match, protocol) => { (match, protocol) => {
// Allow https (case insensitive), redact everything else // Allow https (case insensitive), redact everything else
return protocol.toLowerCase() === "https" ? match : "(redacted)"; return protocol.toLowerCase() === "https" ? match : "(redacted)";
@ -1058,6 +1057,16 @@ jobs:
(_m, p1, p2) => `${p1}\`@${p2}\`` (_m, p1, p2) => `${p1}\`@${p2}\``
); );
} }
/**
* Removes XML comments to prevent content hiding
* @param {string} s - The string to process
* @returns {string} The string with XML comments removed
*/
function removeXmlComments(s) {
// Remove XML/HTML comments including malformed ones that might be used to hide content
// Matches: <!-- ... --> and <!--- ... --> and <!--- ... --!> variations
return s.replace(/<!--[\s\S]*?-->/g, "").replace(/<!--[\s\S]*?--!>/g, "");
}
/** /**
* Neutralizes bot trigger phrases by wrapping them in backticks * Neutralizes bot trigger phrases by wrapping them in backticks
* @param {string} s - The string to process * @param {string} s - The string to process
@ -1091,13 +1100,13 @@ jobs:
switch (itemType) { switch (itemType) {
case "create-issue": case "create-issue":
return 1; // Only one issue allowed return 1; // Only one issue allowed
case "add-issue-comment": case "add-comment":
return 1; // Only one comment allowed return 1; // Only one comment allowed
case "create-pull-request": case "create-pull-request":
return 1; // Only one pull request allowed return 1; // Only one pull request allowed
case "create-pull-request-review-comment": case "create-pull-request-review-comment":
return 10; // Default to 10 review comments allowed return 10; // Default to 10 review comments allowed
case "add-issue-labels": case "add-labels":
return 5; // Only one labels operation allowed return 5; // Only one labels operation allowed
case "update-issue": case "update-issue":
return 1; // Only one issue update allowed return 1; // Only one issue update allowed
@ -1312,10 +1321,10 @@ jobs:
); );
} }
break; break;
case "add-issue-comment": case "add-comment":
if (!item.body || typeof item.body !== "string") { if (!item.body || typeof item.body !== "string") {
errors.push( errors.push(
`Line ${i + 1}: add-issue-comment requires a 'body' string field` `Line ${i + 1}: add-comment requires a 'body' string field`
); );
continue; continue;
} }
@ -1350,10 +1359,10 @@ jobs:
); );
} }
break; break;
case "add-issue-labels": case "add-labels":
if (!item.labels || !Array.isArray(item.labels)) { if (!item.labels || !Array.isArray(item.labels)) {
errors.push( errors.push(
`Line ${i + 1}: add-issue-labels requires a 'labels' array field` `Line ${i + 1}: add-labels requires a 'labels' array field`
); );
continue; continue;
} }
@ -1363,7 +1372,7 @@ jobs:
) )
) { ) {
errors.push( errors.push(
`Line ${i + 1}: add-issue-labels labels array must contain only strings` `Line ${i + 1}: add-labels labels array must contain only strings`
); );
continue; continue;
} }
@ -2480,11 +2489,11 @@ jobs:
pull-requests: write pull-requests: write
timeout-minutes: 10 timeout-minutes: 10
outputs: outputs:
comment_id: ${{ steps.create_comment.outputs.comment_id }} comment_id: ${{ steps.add_comment.outputs.comment_id }}
comment_url: ${{ steps.create_comment.outputs.comment_url }} comment_url: ${{ steps.add_comment.outputs.comment_url }}
steps: steps:
- name: Add Issue Comment - name: Add Issue Comment
id: create_comment id: add_comment
uses: actions/github-script@v8 uses: actions/github-script@v8
env: env:
GITHUB_AW_AGENT_OUTPUT: ${{ needs.ci-failure-doctor.outputs.output }} GITHUB_AW_AGENT_OUTPUT: ${{ needs.ci-failure-doctor.outputs.output }}
@ -2518,15 +2527,15 @@ jobs:
core.info("No valid items found in agent output"); core.info("No valid items found in agent output");
return; return;
} }
// Find all add-issue-comment items // Find all add-comment items
const commentItems = validatedOutput.items.filter( const commentItems = validatedOutput.items.filter(
/** @param {any} item */ item => item.type === "add-issue-comment" /** @param {any} item */ item => item.type === "add-comment"
); );
if (commentItems.length === 0) { if (commentItems.length === 0) {
core.info("No add-issue-comment items found in agent output"); core.info("No add-comment items found in agent output");
return; return;
} }
core.info(`Found ${commentItems.length} add-issue-comment item(s)`); core.info(`Found ${commentItems.length} add-comment item(s)`);
// If in staged mode, emit step summary instead of creating comments // If in staged mode, emit step summary instead of creating comments
if (isStaged) { if (isStaged) {
let summaryContent = "## 🎭 Staged Mode: Add Comments Preview\n\n"; let summaryContent = "## 🎭 Staged Mode: Add Comments Preview\n\n";
@ -2570,7 +2579,7 @@ jobs:
for (let i = 0; i < commentItems.length; i++) { for (let i = 0; i < commentItems.length; i++) {
const commentItem = commentItems[i]; const commentItem = commentItems[i];
core.info( core.info(
`Processing add-issue-comment item ${i + 1}/${commentItems.length}: bodyLength=${commentItem.body.length}` `Processing add-comment item ${i + 1}/${commentItems.length}: bodyLength=${commentItem.body.length}`
); );
// Determine the issue/PR number and comment endpoint for this comment // Determine the issue/PR number and comment endpoint for this comment
let issueNumber; let issueNumber;

View file

@ -18,7 +18,7 @@ network: defaults
safe-outputs: safe-outputs:
create-issue: create-issue:
title-prefix: "${{ github.workflow }}" title-prefix: "${{ github.workflow }}"
add-issue-comment: add-comment:
tools: tools:
web-fetch: web-fetch:

View file

@ -2,7 +2,7 @@
# To update this file, edit the corresponding .md file and run: # To update this file, edit the corresponding .md file and run:
# gh aw compile # gh aw compile
# #
# Effective stop-time: 2025-09-19 10:32:53 # Effective stop-time: 2025-09-19 12:19:14
name: "Daily Backlog Burner" name: "Daily Backlog Burner"
"on": "on":
@ -55,7 +55,7 @@ jobs:
main(); main();
- name: Setup Safe Outputs Collector MCP - name: Setup Safe Outputs Collector MCP
env: env:
GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":{\"enabled\":true,\"target\":\"*\"},\"create-issue\":true,\"create-pull-request\":true}" GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-comment\":{\"enabled\":true,\"target\":\"*\"},\"create-issue\":true,\"create-pull-request\":true}"
run: | run: |
mkdir -p /tmp/safe-outputs mkdir -p /tmp/safe-outputs
cat > /tmp/safe-outputs/mcp-server.cjs << 'EOF' cat > /tmp/safe-outputs/mcp-server.cjs << 'EOF'
@ -208,7 +208,7 @@ jobs:
}, },
}, },
{ {
name: "add-issue-comment", name: "add-comment",
description: "Add a comment to a GitHub issue or pull request", description: "Add a comment to a GitHub issue or pull request",
inputSchema: { inputSchema: {
type: "object", type: "object",
@ -315,7 +315,7 @@ jobs:
}, },
}, },
{ {
name: "add-issue-labels", name: "add-labels",
description: "Add labels to a GitHub issue or pull request", description: "Add labels to a GitHub issue or pull request",
inputSchema: { inputSchema: {
type: "object", type: "object",
@ -489,7 +489,7 @@ jobs:
- name: Setup MCPs - name: Setup MCPs
env: env:
GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }}
GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":{\"enabled\":true,\"target\":\"*\"},\"create-issue\":true,\"create-pull-request\":true}" GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-comment\":{\"enabled\":true,\"target\":\"*\"},\"create-issue\":true,\"create-pull-request\":true}"
run: | run: |
mkdir -p /tmp/mcp-config mkdir -p /tmp/mcp-config
cat > /tmp/mcp-config/mcp-servers.json << 'EOF' cat > /tmp/mcp-config/mcp-servers.json << 'EOF'
@ -527,7 +527,7 @@ jobs:
WORKFLOW_NAME="Daily Backlog Burner" WORKFLOW_NAME="Daily Backlog Burner"
# Check stop-time limit # Check stop-time limit
STOP_TIME="2025-09-19 10:32:53" STOP_TIME="2025-09-19 12:19:14"
echo "Checking stop-time limit: $STOP_TIME" echo "Checking stop-time limit: $STOP_TIME"
# Convert stop time to epoch seconds # Convert stop time to epoch seconds
@ -668,7 +668,7 @@ jobs:
**Adding a Comment to an Issue or Pull Request** **Adding a Comment to an Issue or Pull Request**
To add a comment to an issue or pull request, use the add-issue-comments tool from the safe-outputs MCP To add a comment to an issue or pull request, use the add-comments tool from the safe-outputs MCP
**Creating an Issue** **Creating an Issue**
@ -854,7 +854,7 @@ jobs:
uses: actions/github-script@v8 uses: actions/github-script@v8
env: env:
GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }}
GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":{\"enabled\":true,\"target\":\"*\"},\"create-issue\":true,\"create-pull-request\":true}" GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-comment\":{\"enabled\":true,\"target\":\"*\"},\"create-issue\":true,\"create-pull-request\":true}"
with: with:
script: | script: |
async function main() { async function main() {
@ -887,15 +887,12 @@ jobs:
let sanitized = content; let sanitized = content;
// Neutralize @mentions to prevent unintended notifications // Neutralize @mentions to prevent unintended notifications
sanitized = neutralizeMentions(sanitized); sanitized = neutralizeMentions(sanitized);
// Remove XML comments to prevent content hiding
sanitized = removeXmlComments(sanitized);
// Remove ANSI escape sequences BEFORE removing control characters
sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
// Remove control characters (except newlines and tabs) // Remove control characters (except newlines and tabs)
sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
// XML character escaping
sanitized = sanitized
.replace(/&/g, "&amp;") // Must be first to avoid double-escaping
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
// URI filtering - replace non-https protocols with "(redacted)" // URI filtering - replace non-https protocols with "(redacted)"
sanitized = sanitizeUrlProtocols(sanitized); sanitized = sanitizeUrlProtocols(sanitized);
// Domain filtering for HTTPS URIs // Domain filtering for HTTPS URIs
@ -915,8 +912,7 @@ jobs:
lines.slice(0, maxLines).join("\n") + lines.slice(0, maxLines).join("\n") +
"\n[Content truncated due to line count]"; "\n[Content truncated due to line count]";
} }
// Remove ANSI escape sequences // ANSI escape sequences already removed earlier in the function
sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
// Neutralize common bot trigger phrases // Neutralize common bot trigger phrases
sanitized = neutralizeBotTriggers(sanitized); sanitized = neutralizeBotTriggers(sanitized);
// Trim excessive whitespace // Trim excessive whitespace
@ -928,10 +924,12 @@ jobs:
*/ */
function sanitizeUrlDomains(s) { function sanitizeUrlDomains(s) {
return s.replace( return s.replace(
/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, /\bhttps:\/\/[^\s\])}'"<>&\x00-\x1f,;]+/gi,
(match, domain) => { (match) => {
// Extract just the URL part after https://
const urlAfterProtocol = match.slice(8); // Remove 'https://'
// Extract the hostname part (before first slash, colon, or other delimiter) // Extract the hostname part (before first slash, colon, or other delimiter)
const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase(); const hostname = urlAfterProtocol.split(/[\/:\?#]/)[0].toLowerCase();
// Check if this domain or any parent domain is in the allowlist // Check if this domain or any parent domain is in the allowlist
const isAllowed = allowedDomains.some(allowedDomain => { const isAllowed = allowedDomains.some(allowedDomain => {
const normalizedAllowed = allowedDomain.toLowerCase(); const normalizedAllowed = allowedDomain.toLowerCase();
@ -950,9 +948,10 @@ jobs:
* @returns {string} The string with non-https protocols redacted * @returns {string} The string with non-https protocols redacted
*/ */
function sanitizeUrlProtocols(s) { function sanitizeUrlProtocols(s) {
// Match both protocol:// and protocol: patterns // Match protocol:// patterns (URLs) and standalone protocol: patterns that look like URLs
// Avoid matching command line flags like -v:10 or z3 -memory:high
return s.replace( return s.replace(
/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, /\b(\w+):\/\/[^\s\])}'"<>&\x00-\x1f]+/gi,
(match, protocol) => { (match, protocol) => {
// Allow https (case insensitive), redact everything else // Allow https (case insensitive), redact everything else
return protocol.toLowerCase() === "https" ? match : "(redacted)"; return protocol.toLowerCase() === "https" ? match : "(redacted)";
@ -971,6 +970,16 @@ jobs:
(_m, p1, p2) => `${p1}\`@${p2}\`` (_m, p1, p2) => `${p1}\`@${p2}\``
); );
} }
/**
* Removes XML comments to prevent content hiding
* @param {string} s - The string to process
* @returns {string} The string with XML comments removed
*/
function removeXmlComments(s) {
// Remove XML/HTML comments including malformed ones that might be used to hide content
// Matches: <!-- ... --> and <!--- ... --> and <!--- ... --!> variations
return s.replace(/<!--[\s\S]*?-->/g, "").replace(/<!--[\s\S]*?--!>/g, "");
}
/** /**
* Neutralizes bot trigger phrases by wrapping them in backticks * Neutralizes bot trigger phrases by wrapping them in backticks
* @param {string} s - The string to process * @param {string} s - The string to process
@ -1004,13 +1013,13 @@ jobs:
switch (itemType) { switch (itemType) {
case "create-issue": case "create-issue":
return 1; // Only one issue allowed return 1; // Only one issue allowed
case "add-issue-comment": case "add-comment":
return 1; // Only one comment allowed return 1; // Only one comment allowed
case "create-pull-request": case "create-pull-request":
return 1; // Only one pull request allowed return 1; // Only one pull request allowed
case "create-pull-request-review-comment": case "create-pull-request-review-comment":
return 10; // Default to 10 review comments allowed return 10; // Default to 10 review comments allowed
case "add-issue-labels": case "add-labels":
return 5; // Only one labels operation allowed return 5; // Only one labels operation allowed
case "update-issue": case "update-issue":
return 1; // Only one issue update allowed return 1; // Only one issue update allowed
@ -1225,10 +1234,10 @@ jobs:
); );
} }
break; break;
case "add-issue-comment": case "add-comment":
if (!item.body || typeof item.body !== "string") { if (!item.body || typeof item.body !== "string") {
errors.push( errors.push(
`Line ${i + 1}: add-issue-comment requires a 'body' string field` `Line ${i + 1}: add-comment requires a 'body' string field`
); );
continue; continue;
} }
@ -1263,10 +1272,10 @@ jobs:
); );
} }
break; break;
case "add-issue-labels": case "add-labels":
if (!item.labels || !Array.isArray(item.labels)) { if (!item.labels || !Array.isArray(item.labels)) {
errors.push( errors.push(
`Line ${i + 1}: add-issue-labels requires a 'labels' array field` `Line ${i + 1}: add-labels requires a 'labels' array field`
); );
continue; continue;
} }
@ -1276,7 +1285,7 @@ jobs:
) )
) { ) {
errors.push( errors.push(
`Line ${i + 1}: add-issue-labels labels array must contain only strings` `Line ${i + 1}: add-labels labels array must contain only strings`
); );
continue; continue;
} }
@ -2406,6 +2415,7 @@ jobs:
GITHUB_AW_AGENT_OUTPUT: ${{ needs.daily-backlog-burner.outputs.output }} GITHUB_AW_AGENT_OUTPUT: ${{ needs.daily-backlog-burner.outputs.output }}
GITHUB_AW_ISSUE_TITLE_PREFIX: "${{ github.workflow }}" GITHUB_AW_ISSUE_TITLE_PREFIX: "${{ github.workflow }}"
with: with:
github-token: ${{ secrets.DSYME_GH_TOKEN}}
script: | script: |
async function main() { async function main() {
// Check if we're in staged mode // Check if we're in staged mode
@ -2595,16 +2605,17 @@ jobs:
pull-requests: write pull-requests: write
timeout-minutes: 10 timeout-minutes: 10
outputs: outputs:
comment_id: ${{ steps.create_comment.outputs.comment_id }} comment_id: ${{ steps.add_comment.outputs.comment_id }}
comment_url: ${{ steps.create_comment.outputs.comment_url }} comment_url: ${{ steps.add_comment.outputs.comment_url }}
steps: steps:
- name: Add Issue Comment - name: Add Issue Comment
id: create_comment id: add_comment
uses: actions/github-script@v8 uses: actions/github-script@v8
env: env:
GITHUB_AW_AGENT_OUTPUT: ${{ needs.daily-backlog-burner.outputs.output }} GITHUB_AW_AGENT_OUTPUT: ${{ needs.daily-backlog-burner.outputs.output }}
GITHUB_AW_COMMENT_TARGET: "*" GITHUB_AW_COMMENT_TARGET: "*"
with: with:
github-token: ${{ secrets.DSYME_GH_TOKEN}}
script: | script: |
async function main() { async function main() {
// Check if we're in staged mode // Check if we're in staged mode
@ -2634,15 +2645,15 @@ jobs:
core.info("No valid items found in agent output"); core.info("No valid items found in agent output");
return; return;
} }
// Find all add-issue-comment items // Find all add-comment items
const commentItems = validatedOutput.items.filter( const commentItems = validatedOutput.items.filter(
/** @param {any} item */ item => item.type === "add-issue-comment" /** @param {any} item */ item => item.type === "add-comment"
); );
if (commentItems.length === 0) { if (commentItems.length === 0) {
core.info("No add-issue-comment items found in agent output"); core.info("No add-comment items found in agent output");
return; return;
} }
core.info(`Found ${commentItems.length} add-issue-comment item(s)`); core.info(`Found ${commentItems.length} add-comment item(s)`);
// If in staged mode, emit step summary instead of creating comments // If in staged mode, emit step summary instead of creating comments
if (isStaged) { if (isStaged) {
let summaryContent = "## 🎭 Staged Mode: Add Comments Preview\n\n"; let summaryContent = "## 🎭 Staged Mode: Add Comments Preview\n\n";
@ -2686,7 +2697,7 @@ jobs:
for (let i = 0; i < commentItems.length; i++) { for (let i = 0; i < commentItems.length; i++) {
const commentItem = commentItems[i]; const commentItem = commentItems[i];
core.info( core.info(
`Processing add-issue-comment item ${i + 1}/${commentItems.length}: bodyLength=${commentItem.body.length}` `Processing add-comment item ${i + 1}/${commentItems.length}: bodyLength=${commentItem.body.length}`
); );
// Determine the issue/PR number and comment endpoint for this comment // Determine the issue/PR number and comment endpoint for this comment
let issueNumber; let issueNumber;
@ -2827,6 +2838,7 @@ jobs:
GITHUB_AW_PR_DRAFT: "true" GITHUB_AW_PR_DRAFT: "true"
GITHUB_AW_PR_IF_NO_CHANGES: "warn" GITHUB_AW_PR_IF_NO_CHANGES: "warn"
with: with:
github-token: ${{ secrets.DSYME_GH_TOKEN}}
script: | script: |
/** @type {typeof import("fs")} */ /** @type {typeof import("fs")} */
const fs = require("fs"); const fs = require("fs");

View file

@ -14,11 +14,12 @@ safe-outputs:
create-issue: create-issue:
title-prefix: "${{ github.workflow }}" title-prefix: "${{ github.workflow }}"
max: 3 max: 3
add-issue-comment: add-comment:
target: "*" # all issues and PRs target: "*" # all issues and PRs
max: 3 max: 3
create-pull-request: create-pull-request:
draft: true draft: true
github-token: ${{ secrets.DSYME_GH_TOKEN}}
tools: tools:
web-fetch: web-fetch:

View file

@ -2,7 +2,7 @@
# To update this file, edit the corresponding .md file and run: # To update this file, edit the corresponding .md file and run:
# gh aw compile # gh aw compile
# #
# Effective stop-time: 2025-09-19 10:32:53 # Effective stop-time: 2025-09-19 12:19:14
name: "Daily Perf Improver" name: "Daily Perf Improver"
"on": "on":
@ -69,7 +69,7 @@ jobs:
main(); main();
- name: Setup Safe Outputs Collector MCP - name: Setup Safe Outputs Collector MCP
env: env:
GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":{\"enabled\":true,\"target\":\"*\"},\"create-issue\":true,\"create-pull-request\":true}" GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-comment\":{\"enabled\":true,\"target\":\"*\"},\"create-issue\":true,\"create-pull-request\":true}"
run: | run: |
mkdir -p /tmp/safe-outputs mkdir -p /tmp/safe-outputs
cat > /tmp/safe-outputs/mcp-server.cjs << 'EOF' cat > /tmp/safe-outputs/mcp-server.cjs << 'EOF'
@ -222,7 +222,7 @@ jobs:
}, },
}, },
{ {
name: "add-issue-comment", name: "add-comment",
description: "Add a comment to a GitHub issue or pull request", description: "Add a comment to a GitHub issue or pull request",
inputSchema: { inputSchema: {
type: "object", type: "object",
@ -329,7 +329,7 @@ jobs:
}, },
}, },
{ {
name: "add-issue-labels", name: "add-labels",
description: "Add labels to a GitHub issue or pull request", description: "Add labels to a GitHub issue or pull request",
inputSchema: { inputSchema: {
type: "object", type: "object",
@ -503,7 +503,7 @@ jobs:
- name: Setup MCPs - name: Setup MCPs
env: env:
GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }}
GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":{\"enabled\":true,\"target\":\"*\"},\"create-issue\":true,\"create-pull-request\":true}" GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-comment\":{\"enabled\":true,\"target\":\"*\"},\"create-issue\":true,\"create-pull-request\":true}"
run: | run: |
mkdir -p /tmp/mcp-config mkdir -p /tmp/mcp-config
cat > /tmp/mcp-config/mcp-servers.json << 'EOF' cat > /tmp/mcp-config/mcp-servers.json << 'EOF'
@ -541,7 +541,7 @@ jobs:
WORKFLOW_NAME="Daily Perf Improver" WORKFLOW_NAME="Daily Perf Improver"
# Check stop-time limit # Check stop-time limit
STOP_TIME="2025-09-19 10:32:53" STOP_TIME="2025-09-19 12:19:14"
echo "Checking stop-time limit: $STOP_TIME" echo "Checking stop-time limit: $STOP_TIME"
# Convert stop time to epoch seconds # Convert stop time to epoch seconds
@ -743,7 +743,7 @@ jobs:
**Adding a Comment to an Issue or Pull Request** **Adding a Comment to an Issue or Pull Request**
To add a comment to an issue or pull request, use the add-issue-comments tool from the safe-outputs MCP To add a comment to an issue or pull request, use the add-comments tool from the safe-outputs MCP
**Creating an Issue** **Creating an Issue**
@ -929,7 +929,7 @@ jobs:
uses: actions/github-script@v8 uses: actions/github-script@v8
env: env:
GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }}
GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":{\"enabled\":true,\"target\":\"*\"},\"create-issue\":true,\"create-pull-request\":true}" GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-comment\":{\"enabled\":true,\"target\":\"*\"},\"create-issue\":true,\"create-pull-request\":true}"
with: with:
script: | script: |
async function main() { async function main() {
@ -962,15 +962,12 @@ jobs:
let sanitized = content; let sanitized = content;
// Neutralize @mentions to prevent unintended notifications // Neutralize @mentions to prevent unintended notifications
sanitized = neutralizeMentions(sanitized); sanitized = neutralizeMentions(sanitized);
// Remove XML comments to prevent content hiding
sanitized = removeXmlComments(sanitized);
// Remove ANSI escape sequences BEFORE removing control characters
sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
// Remove control characters (except newlines and tabs) // Remove control characters (except newlines and tabs)
sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
// XML character escaping
sanitized = sanitized
.replace(/&/g, "&amp;") // Must be first to avoid double-escaping
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
// URI filtering - replace non-https protocols with "(redacted)" // URI filtering - replace non-https protocols with "(redacted)"
sanitized = sanitizeUrlProtocols(sanitized); sanitized = sanitizeUrlProtocols(sanitized);
// Domain filtering for HTTPS URIs // Domain filtering for HTTPS URIs
@ -990,8 +987,7 @@ jobs:
lines.slice(0, maxLines).join("\n") + lines.slice(0, maxLines).join("\n") +
"\n[Content truncated due to line count]"; "\n[Content truncated due to line count]";
} }
// Remove ANSI escape sequences // ANSI escape sequences already removed earlier in the function
sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
// Neutralize common bot trigger phrases // Neutralize common bot trigger phrases
sanitized = neutralizeBotTriggers(sanitized); sanitized = neutralizeBotTriggers(sanitized);
// Trim excessive whitespace // Trim excessive whitespace
@ -1003,10 +999,12 @@ jobs:
*/ */
function sanitizeUrlDomains(s) { function sanitizeUrlDomains(s) {
return s.replace( return s.replace(
/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, /\bhttps:\/\/[^\s\])}'"<>&\x00-\x1f,;]+/gi,
(match, domain) => { (match) => {
// Extract just the URL part after https://
const urlAfterProtocol = match.slice(8); // Remove 'https://'
// Extract the hostname part (before first slash, colon, or other delimiter) // Extract the hostname part (before first slash, colon, or other delimiter)
const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase(); const hostname = urlAfterProtocol.split(/[\/:\?#]/)[0].toLowerCase();
// Check if this domain or any parent domain is in the allowlist // Check if this domain or any parent domain is in the allowlist
const isAllowed = allowedDomains.some(allowedDomain => { const isAllowed = allowedDomains.some(allowedDomain => {
const normalizedAllowed = allowedDomain.toLowerCase(); const normalizedAllowed = allowedDomain.toLowerCase();
@ -1025,9 +1023,10 @@ jobs:
* @returns {string} The string with non-https protocols redacted * @returns {string} The string with non-https protocols redacted
*/ */
function sanitizeUrlProtocols(s) { function sanitizeUrlProtocols(s) {
// Match both protocol:// and protocol: patterns // Match protocol:// patterns (URLs) and standalone protocol: patterns that look like URLs
// Avoid matching command line flags like -v:10 or z3 -memory:high
return s.replace( return s.replace(
/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, /\b(\w+):\/\/[^\s\])}'"<>&\x00-\x1f]+/gi,
(match, protocol) => { (match, protocol) => {
// Allow https (case insensitive), redact everything else // Allow https (case insensitive), redact everything else
return protocol.toLowerCase() === "https" ? match : "(redacted)"; return protocol.toLowerCase() === "https" ? match : "(redacted)";
@ -1046,6 +1045,16 @@ jobs:
(_m, p1, p2) => `${p1}\`@${p2}\`` (_m, p1, p2) => `${p1}\`@${p2}\``
); );
} }
/**
* Removes XML comments to prevent content hiding
* @param {string} s - The string to process
* @returns {string} The string with XML comments removed
*/
function removeXmlComments(s) {
// Remove XML/HTML comments including malformed ones that might be used to hide content
// Matches: <!-- ... --> and <!--- ... --> and <!--- ... --!> variations
return s.replace(/<!--[\s\S]*?-->/g, "").replace(/<!--[\s\S]*?--!>/g, "");
}
/** /**
* Neutralizes bot trigger phrases by wrapping them in backticks * Neutralizes bot trigger phrases by wrapping them in backticks
* @param {string} s - The string to process * @param {string} s - The string to process
@ -1079,13 +1088,13 @@ jobs:
switch (itemType) { switch (itemType) {
case "create-issue": case "create-issue":
return 1; // Only one issue allowed return 1; // Only one issue allowed
case "add-issue-comment": case "add-comment":
return 1; // Only one comment allowed return 1; // Only one comment allowed
case "create-pull-request": case "create-pull-request":
return 1; // Only one pull request allowed return 1; // Only one pull request allowed
case "create-pull-request-review-comment": case "create-pull-request-review-comment":
return 10; // Default to 10 review comments allowed return 10; // Default to 10 review comments allowed
case "add-issue-labels": case "add-labels":
return 5; // Only one labels operation allowed return 5; // Only one labels operation allowed
case "update-issue": case "update-issue":
return 1; // Only one issue update allowed return 1; // Only one issue update allowed
@ -1300,10 +1309,10 @@ jobs:
); );
} }
break; break;
case "add-issue-comment": case "add-comment":
if (!item.body || typeof item.body !== "string") { if (!item.body || typeof item.body !== "string") {
errors.push( errors.push(
`Line ${i + 1}: add-issue-comment requires a 'body' string field` `Line ${i + 1}: add-comment requires a 'body' string field`
); );
continue; continue;
} }
@ -1338,10 +1347,10 @@ jobs:
); );
} }
break; break;
case "add-issue-labels": case "add-labels":
if (!item.labels || !Array.isArray(item.labels)) { if (!item.labels || !Array.isArray(item.labels)) {
errors.push( errors.push(
`Line ${i + 1}: add-issue-labels requires a 'labels' array field` `Line ${i + 1}: add-labels requires a 'labels' array field`
); );
continue; continue;
} }
@ -1351,7 +1360,7 @@ jobs:
) )
) { ) {
errors.push( errors.push(
`Line ${i + 1}: add-issue-labels labels array must contain only strings` `Line ${i + 1}: add-labels labels array must contain only strings`
); );
continue; continue;
} }
@ -2481,6 +2490,7 @@ jobs:
GITHUB_AW_AGENT_OUTPUT: ${{ needs.daily-perf-improver.outputs.output }} GITHUB_AW_AGENT_OUTPUT: ${{ needs.daily-perf-improver.outputs.output }}
GITHUB_AW_ISSUE_TITLE_PREFIX: "${{ github.workflow }}" GITHUB_AW_ISSUE_TITLE_PREFIX: "${{ github.workflow }}"
with: with:
github-token: ${{ secrets.DSYME_GH_TOKEN}}
script: | script: |
async function main() { async function main() {
// Check if we're in staged mode // Check if we're in staged mode
@ -2670,16 +2680,17 @@ jobs:
pull-requests: write pull-requests: write
timeout-minutes: 10 timeout-minutes: 10
outputs: outputs:
comment_id: ${{ steps.create_comment.outputs.comment_id }} comment_id: ${{ steps.add_comment.outputs.comment_id }}
comment_url: ${{ steps.create_comment.outputs.comment_url }} comment_url: ${{ steps.add_comment.outputs.comment_url }}
steps: steps:
- name: Add Issue Comment - name: Add Issue Comment
id: create_comment id: add_comment
uses: actions/github-script@v8 uses: actions/github-script@v8
env: env:
GITHUB_AW_AGENT_OUTPUT: ${{ needs.daily-perf-improver.outputs.output }} GITHUB_AW_AGENT_OUTPUT: ${{ needs.daily-perf-improver.outputs.output }}
GITHUB_AW_COMMENT_TARGET: "*" GITHUB_AW_COMMENT_TARGET: "*"
with: with:
github-token: ${{ secrets.DSYME_GH_TOKEN}}
script: | script: |
async function main() { async function main() {
// Check if we're in staged mode // Check if we're in staged mode
@ -2709,15 +2720,15 @@ jobs:
core.info("No valid items found in agent output"); core.info("No valid items found in agent output");
return; return;
} }
// Find all add-issue-comment items // Find all add-comment items
const commentItems = validatedOutput.items.filter( const commentItems = validatedOutput.items.filter(
/** @param {any} item */ item => item.type === "add-issue-comment" /** @param {any} item */ item => item.type === "add-comment"
); );
if (commentItems.length === 0) { if (commentItems.length === 0) {
core.info("No add-issue-comment items found in agent output"); core.info("No add-comment items found in agent output");
return; return;
} }
core.info(`Found ${commentItems.length} add-issue-comment item(s)`); core.info(`Found ${commentItems.length} add-comment item(s)`);
// If in staged mode, emit step summary instead of creating comments // If in staged mode, emit step summary instead of creating comments
if (isStaged) { if (isStaged) {
let summaryContent = "## 🎭 Staged Mode: Add Comments Preview\n\n"; let summaryContent = "## 🎭 Staged Mode: Add Comments Preview\n\n";
@ -2761,7 +2772,7 @@ jobs:
for (let i = 0; i < commentItems.length; i++) { for (let i = 0; i < commentItems.length; i++) {
const commentItem = commentItems[i]; const commentItem = commentItems[i];
core.info( core.info(
`Processing add-issue-comment item ${i + 1}/${commentItems.length}: bodyLength=${commentItem.body.length}` `Processing add-comment item ${i + 1}/${commentItems.length}: bodyLength=${commentItem.body.length}`
); );
// Determine the issue/PR number and comment endpoint for this comment // Determine the issue/PR number and comment endpoint for this comment
let issueNumber; let issueNumber;
@ -2902,6 +2913,7 @@ jobs:
GITHUB_AW_PR_DRAFT: "true" GITHUB_AW_PR_DRAFT: "true"
GITHUB_AW_PR_IF_NO_CHANGES: "warn" GITHUB_AW_PR_IF_NO_CHANGES: "warn"
with: with:
github-token: ${{ secrets.DSYME_GH_TOKEN}}
script: | script: |
/** @type {typeof import("fs")} */ /** @type {typeof import("fs")} */
const fs = require("fs"); const fs = require("fs");

View file

@ -16,10 +16,11 @@ safe-outputs:
create-issue: create-issue:
title-prefix: "${{ github.workflow }}" title-prefix: "${{ github.workflow }}"
max: 5 max: 5
add-issue-comment: add-comment:
target: "*" # can add a comment to any one single issue or pull request target: "*" # can add a comment to any one single issue or pull request
create-pull-request: create-pull-request:
draft: true draft: true
github-token: ${{ secrets.DSYME_GH_TOKEN}}
tools: tools:
web-fetch: web-fetch:

View file

@ -2,7 +2,7 @@
# To update this file, edit the corresponding .md file and run: # To update this file, edit the corresponding .md file and run:
# gh aw compile # gh aw compile
# #
# Effective stop-time: 2025-09-19 10:32:53 # Effective stop-time: 2025-09-19 12:19:15
name: "Daily Test Coverage Improver" name: "Daily Test Coverage Improver"
"on": "on":
@ -69,7 +69,7 @@ jobs:
main(); main();
- name: Setup Safe Outputs Collector MCP - name: Setup Safe Outputs Collector MCP
env: env:
GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":{\"enabled\":true,\"target\":\"*\"},\"create-issue\":true,\"create-pull-request\":true,\"update-issue\":true}" GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-comment\":{\"enabled\":true,\"target\":\"*\"},\"create-issue\":true,\"create-pull-request\":true,\"update-issue\":true}"
run: | run: |
mkdir -p /tmp/safe-outputs mkdir -p /tmp/safe-outputs
cat > /tmp/safe-outputs/mcp-server.cjs << 'EOF' cat > /tmp/safe-outputs/mcp-server.cjs << 'EOF'
@ -222,7 +222,7 @@ jobs:
}, },
}, },
{ {
name: "add-issue-comment", name: "add-comment",
description: "Add a comment to a GitHub issue or pull request", description: "Add a comment to a GitHub issue or pull request",
inputSchema: { inputSchema: {
type: "object", type: "object",
@ -329,7 +329,7 @@ jobs:
}, },
}, },
{ {
name: "add-issue-labels", name: "add-labels",
description: "Add labels to a GitHub issue or pull request", description: "Add labels to a GitHub issue or pull request",
inputSchema: { inputSchema: {
type: "object", type: "object",
@ -503,7 +503,7 @@ jobs:
- name: Setup MCPs - name: Setup MCPs
env: env:
GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }}
GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":{\"enabled\":true,\"target\":\"*\"},\"create-issue\":true,\"create-pull-request\":true,\"update-issue\":true}" GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-comment\":{\"enabled\":true,\"target\":\"*\"},\"create-issue\":true,\"create-pull-request\":true,\"update-issue\":true}"
run: | run: |
mkdir -p /tmp/mcp-config mkdir -p /tmp/mcp-config
cat > /tmp/mcp-config/mcp-servers.json << 'EOF' cat > /tmp/mcp-config/mcp-servers.json << 'EOF'
@ -541,7 +541,7 @@ jobs:
WORKFLOW_NAME="Daily Test Coverage Improver" WORKFLOW_NAME="Daily Test Coverage Improver"
# Check stop-time limit # Check stop-time limit
STOP_TIME="2025-09-19 10:32:53" STOP_TIME="2025-09-19 12:19:15"
echo "Checking stop-time limit: $STOP_TIME" echo "Checking stop-time limit: $STOP_TIME"
# Convert stop time to epoch seconds # Convert stop time to epoch seconds
@ -714,7 +714,7 @@ jobs:
**Adding a Comment to an Issue or Pull Request** **Adding a Comment to an Issue or Pull Request**
To add a comment to an issue or pull request, use the add-issue-comments tool from the safe-outputs MCP To add a comment to an issue or pull request, use the add-comments tool from the safe-outputs MCP
**Creating an Issue** **Creating an Issue**
@ -904,7 +904,7 @@ jobs:
uses: actions/github-script@v8 uses: actions/github-script@v8
env: env:
GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }}
GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":{\"enabled\":true,\"target\":\"*\"},\"create-issue\":true,\"create-pull-request\":true,\"update-issue\":true}" GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-comment\":{\"enabled\":true,\"target\":\"*\"},\"create-issue\":true,\"create-pull-request\":true,\"update-issue\":true}"
with: with:
script: | script: |
async function main() { async function main() {
@ -937,15 +937,12 @@ jobs:
let sanitized = content; let sanitized = content;
// Neutralize @mentions to prevent unintended notifications // Neutralize @mentions to prevent unintended notifications
sanitized = neutralizeMentions(sanitized); sanitized = neutralizeMentions(sanitized);
// Remove XML comments to prevent content hiding
sanitized = removeXmlComments(sanitized);
// Remove ANSI escape sequences BEFORE removing control characters
sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
// Remove control characters (except newlines and tabs) // Remove control characters (except newlines and tabs)
sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
// XML character escaping
sanitized = sanitized
.replace(/&/g, "&amp;") // Must be first to avoid double-escaping
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
// URI filtering - replace non-https protocols with "(redacted)" // URI filtering - replace non-https protocols with "(redacted)"
sanitized = sanitizeUrlProtocols(sanitized); sanitized = sanitizeUrlProtocols(sanitized);
// Domain filtering for HTTPS URIs // Domain filtering for HTTPS URIs
@ -965,8 +962,7 @@ jobs:
lines.slice(0, maxLines).join("\n") + lines.slice(0, maxLines).join("\n") +
"\n[Content truncated due to line count]"; "\n[Content truncated due to line count]";
} }
// Remove ANSI escape sequences // ANSI escape sequences already removed earlier in the function
sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
// Neutralize common bot trigger phrases // Neutralize common bot trigger phrases
sanitized = neutralizeBotTriggers(sanitized); sanitized = neutralizeBotTriggers(sanitized);
// Trim excessive whitespace // Trim excessive whitespace
@ -978,10 +974,12 @@ jobs:
*/ */
function sanitizeUrlDomains(s) { function sanitizeUrlDomains(s) {
return s.replace( return s.replace(
/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, /\bhttps:\/\/[^\s\])}'"<>&\x00-\x1f,;]+/gi,
(match, domain) => { (match) => {
// Extract just the URL part after https://
const urlAfterProtocol = match.slice(8); // Remove 'https://'
// Extract the hostname part (before first slash, colon, or other delimiter) // Extract the hostname part (before first slash, colon, or other delimiter)
const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase(); const hostname = urlAfterProtocol.split(/[\/:\?#]/)[0].toLowerCase();
// Check if this domain or any parent domain is in the allowlist // Check if this domain or any parent domain is in the allowlist
const isAllowed = allowedDomains.some(allowedDomain => { const isAllowed = allowedDomains.some(allowedDomain => {
const normalizedAllowed = allowedDomain.toLowerCase(); const normalizedAllowed = allowedDomain.toLowerCase();
@ -1000,9 +998,10 @@ jobs:
* @returns {string} The string with non-https protocols redacted * @returns {string} The string with non-https protocols redacted
*/ */
function sanitizeUrlProtocols(s) { function sanitizeUrlProtocols(s) {
// Match both protocol:// and protocol: patterns // Match protocol:// patterns (URLs) and standalone protocol: patterns that look like URLs
// Avoid matching command line flags like -v:10 or z3 -memory:high
return s.replace( return s.replace(
/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, /\b(\w+):\/\/[^\s\])}'"<>&\x00-\x1f]+/gi,
(match, protocol) => { (match, protocol) => {
// Allow https (case insensitive), redact everything else // Allow https (case insensitive), redact everything else
return protocol.toLowerCase() === "https" ? match : "(redacted)"; return protocol.toLowerCase() === "https" ? match : "(redacted)";
@ -1021,6 +1020,16 @@ jobs:
(_m, p1, p2) => `${p1}\`@${p2}\`` (_m, p1, p2) => `${p1}\`@${p2}\``
); );
} }
/**
* Removes XML comments to prevent content hiding
* @param {string} s - The string to process
* @returns {string} The string with XML comments removed
*/
function removeXmlComments(s) {
// Remove XML/HTML comments including malformed ones that might be used to hide content
// Matches: <!-- ... --> and <!--- ... --> and <!--- ... --!> variations
return s.replace(/<!--[\s\S]*?-->/g, "").replace(/<!--[\s\S]*?--!>/g, "");
}
/** /**
* Neutralizes bot trigger phrases by wrapping them in backticks * Neutralizes bot trigger phrases by wrapping them in backticks
* @param {string} s - The string to process * @param {string} s - The string to process
@ -1054,13 +1063,13 @@ jobs:
switch (itemType) { switch (itemType) {
case "create-issue": case "create-issue":
return 1; // Only one issue allowed return 1; // Only one issue allowed
case "add-issue-comment": case "add-comment":
return 1; // Only one comment allowed return 1; // Only one comment allowed
case "create-pull-request": case "create-pull-request":
return 1; // Only one pull request allowed return 1; // Only one pull request allowed
case "create-pull-request-review-comment": case "create-pull-request-review-comment":
return 10; // Default to 10 review comments allowed return 10; // Default to 10 review comments allowed
case "add-issue-labels": case "add-labels":
return 5; // Only one labels operation allowed return 5; // Only one labels operation allowed
case "update-issue": case "update-issue":
return 1; // Only one issue update allowed return 1; // Only one issue update allowed
@ -1275,10 +1284,10 @@ jobs:
); );
} }
break; break;
case "add-issue-comment": case "add-comment":
if (!item.body || typeof item.body !== "string") { if (!item.body || typeof item.body !== "string") {
errors.push( errors.push(
`Line ${i + 1}: add-issue-comment requires a 'body' string field` `Line ${i + 1}: add-comment requires a 'body' string field`
); );
continue; continue;
} }
@ -1313,10 +1322,10 @@ jobs:
); );
} }
break; break;
case "add-issue-labels": case "add-labels":
if (!item.labels || !Array.isArray(item.labels)) { if (!item.labels || !Array.isArray(item.labels)) {
errors.push( errors.push(
`Line ${i + 1}: add-issue-labels requires a 'labels' array field` `Line ${i + 1}: add-labels requires a 'labels' array field`
); );
continue; continue;
} }
@ -1326,7 +1335,7 @@ jobs:
) )
) { ) {
errors.push( errors.push(
`Line ${i + 1}: add-issue-labels labels array must contain only strings` `Line ${i + 1}: add-labels labels array must contain only strings`
); );
continue; continue;
} }
@ -2456,6 +2465,7 @@ jobs:
GITHUB_AW_AGENT_OUTPUT: ${{ needs.daily-test-coverage-improver.outputs.output }} GITHUB_AW_AGENT_OUTPUT: ${{ needs.daily-test-coverage-improver.outputs.output }}
GITHUB_AW_ISSUE_TITLE_PREFIX: "${{ github.workflow }}" GITHUB_AW_ISSUE_TITLE_PREFIX: "${{ github.workflow }}"
with: with:
github-token: ${{ secrets.DSYME_GH_TOKEN}}
script: | script: |
async function main() { async function main() {
// Check if we're in staged mode // Check if we're in staged mode
@ -2645,16 +2655,17 @@ jobs:
pull-requests: write pull-requests: write
timeout-minutes: 10 timeout-minutes: 10
outputs: outputs:
comment_id: ${{ steps.create_comment.outputs.comment_id }} comment_id: ${{ steps.add_comment.outputs.comment_id }}
comment_url: ${{ steps.create_comment.outputs.comment_url }} comment_url: ${{ steps.add_comment.outputs.comment_url }}
steps: steps:
- name: Add Issue Comment - name: Add Issue Comment
id: create_comment id: add_comment
uses: actions/github-script@v8 uses: actions/github-script@v8
env: env:
GITHUB_AW_AGENT_OUTPUT: ${{ needs.daily-test-coverage-improver.outputs.output }} GITHUB_AW_AGENT_OUTPUT: ${{ needs.daily-test-coverage-improver.outputs.output }}
GITHUB_AW_COMMENT_TARGET: "*" GITHUB_AW_COMMENT_TARGET: "*"
with: with:
github-token: ${{ secrets.DSYME_GH_TOKEN}}
script: | script: |
async function main() { async function main() {
// Check if we're in staged mode // Check if we're in staged mode
@ -2684,15 +2695,15 @@ jobs:
core.info("No valid items found in agent output"); core.info("No valid items found in agent output");
return; return;
} }
// Find all add-issue-comment items // Find all add-comment items
const commentItems = validatedOutput.items.filter( const commentItems = validatedOutput.items.filter(
/** @param {any} item */ item => item.type === "add-issue-comment" /** @param {any} item */ item => item.type === "add-comment"
); );
if (commentItems.length === 0) { if (commentItems.length === 0) {
core.info("No add-issue-comment items found in agent output"); core.info("No add-comment items found in agent output");
return; return;
} }
core.info(`Found ${commentItems.length} add-issue-comment item(s)`); core.info(`Found ${commentItems.length} add-comment item(s)`);
// If in staged mode, emit step summary instead of creating comments // If in staged mode, emit step summary instead of creating comments
if (isStaged) { if (isStaged) {
let summaryContent = "## 🎭 Staged Mode: Add Comments Preview\n\n"; let summaryContent = "## 🎭 Staged Mode: Add Comments Preview\n\n";
@ -2736,7 +2747,7 @@ jobs:
for (let i = 0; i < commentItems.length; i++) { for (let i = 0; i < commentItems.length; i++) {
const commentItem = commentItems[i]; const commentItem = commentItems[i];
core.info( core.info(
`Processing add-issue-comment item ${i + 1}/${commentItems.length}: bodyLength=${commentItem.body.length}` `Processing add-comment item ${i + 1}/${commentItems.length}: bodyLength=${commentItem.body.length}`
); );
// Determine the issue/PR number and comment endpoint for this comment // Determine the issue/PR number and comment endpoint for this comment
let issueNumber; let issueNumber;
@ -2877,6 +2888,7 @@ jobs:
GITHUB_AW_PR_DRAFT: "true" GITHUB_AW_PR_DRAFT: "true"
GITHUB_AW_PR_IF_NO_CHANGES: "warn" GITHUB_AW_PR_IF_NO_CHANGES: "warn"
with: with:
github-token: ${{ secrets.DSYME_GH_TOKEN}}
script: | script: |
/** @type {typeof import("fs")} */ /** @type {typeof import("fs")} */
const fs = require("fs"); const fs = require("fs");
@ -3197,6 +3209,7 @@ jobs:
GITHUB_AW_UPDATE_BODY: true GITHUB_AW_UPDATE_BODY: true
GITHUB_AW_UPDATE_TARGET: "*" GITHUB_AW_UPDATE_TARGET: "*"
with: with:
github-token: ${{ secrets.DSYME_GH_TOKEN}}
script: | script: |
async function main() { async function main() {
// Check if we're in staged mode // Check if we're in staged mode

View file

@ -19,10 +19,11 @@ safe-outputs:
target: "*" # one single issue target: "*" # one single issue
body: # can update the issue title/body only body: # can update the issue title/body only
title: # can update the issue title/body only title: # can update the issue title/body only
add-issue-comment: add-comment:
target: "*" # can add a comment to any one single issue or pull request target: "*" # can add a comment to any one single issue or pull request
create-pull-request: # can create a pull request create-pull-request: # can create a pull request
draft: true draft: true
github-token: ${{ secrets.DSYME_GH_TOKEN}}
tools: tools:
web-fetch: web-fetch:

83
.github/workflows/pr-fix.lock.yml generated vendored
View file

@ -2,7 +2,7 @@
# To update this file, edit the corresponding .md file and run: # To update this file, edit the corresponding .md file and run:
# gh aw compile # gh aw compile
# #
# Effective stop-time: 2025-09-19 10:32:53 # Effective stop-time: 2025-09-19 12:19:15
name: "PR Fix" name: "PR Fix"
on: on:
@ -599,7 +599,7 @@ jobs:
main(); main();
- name: Setup Safe Outputs Collector MCP - name: Setup Safe Outputs Collector MCP
env: env:
GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":{\"enabled\":true},\"create-issue\":true,\"push-to-pr-branch\":{\"enabled\":true}}" GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-comment\":{\"enabled\":true},\"create-issue\":true,\"push-to-pr-branch\":{\"enabled\":true}}"
run: | run: |
mkdir -p /tmp/safe-outputs mkdir -p /tmp/safe-outputs
cat > /tmp/safe-outputs/mcp-server.cjs << 'EOF' cat > /tmp/safe-outputs/mcp-server.cjs << 'EOF'
@ -752,7 +752,7 @@ jobs:
}, },
}, },
{ {
name: "add-issue-comment", name: "add-comment",
description: "Add a comment to a GitHub issue or pull request", description: "Add a comment to a GitHub issue or pull request",
inputSchema: { inputSchema: {
type: "object", type: "object",
@ -859,7 +859,7 @@ jobs:
}, },
}, },
{ {
name: "add-issue-labels", name: "add-labels",
description: "Add labels to a GitHub issue or pull request", description: "Add labels to a GitHub issue or pull request",
inputSchema: { inputSchema: {
type: "object", type: "object",
@ -1033,7 +1033,7 @@ jobs:
- name: Setup MCPs - name: Setup MCPs
env: env:
GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }}
GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":{\"enabled\":true},\"create-issue\":true,\"push-to-pr-branch\":{\"enabled\":true}}" GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-comment\":{\"enabled\":true},\"create-issue\":true,\"push-to-pr-branch\":{\"enabled\":true}}"
run: | run: |
mkdir -p /tmp/mcp-config mkdir -p /tmp/mcp-config
cat > /tmp/mcp-config/mcp-servers.json << 'EOF' cat > /tmp/mcp-config/mcp-servers.json << 'EOF'
@ -1071,7 +1071,7 @@ jobs:
WORKFLOW_NAME="PR Fix" WORKFLOW_NAME="PR Fix"
# Check stop-time limit # Check stop-time limit
STOP_TIME="2025-09-19 10:32:53" STOP_TIME="2025-09-19 12:19:15"
echo "Checking stop-time limit: $STOP_TIME" echo "Checking stop-time limit: $STOP_TIME"
# Convert stop time to epoch seconds # Convert stop time to epoch seconds
@ -1174,7 +1174,7 @@ jobs:
**Adding a Comment to an Issue or Pull Request** **Adding a Comment to an Issue or Pull Request**
To add a comment to an issue or pull request, use the add-issue-comments tool from the safe-outputs MCP To add a comment to an issue or pull request, use the add-comments tool from the safe-outputs MCP
**Creating an Issue** **Creating an Issue**
@ -1358,7 +1358,7 @@ jobs:
uses: actions/github-script@v8 uses: actions/github-script@v8
env: env:
GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }}
GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":{\"enabled\":true},\"create-issue\":true,\"push-to-pr-branch\":{\"enabled\":true}}" GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-comment\":{\"enabled\":true},\"create-issue\":true,\"push-to-pr-branch\":{\"enabled\":true}}"
with: with:
script: | script: |
async function main() { async function main() {
@ -1391,15 +1391,12 @@ jobs:
let sanitized = content; let sanitized = content;
// Neutralize @mentions to prevent unintended notifications // Neutralize @mentions to prevent unintended notifications
sanitized = neutralizeMentions(sanitized); sanitized = neutralizeMentions(sanitized);
// Remove XML comments to prevent content hiding
sanitized = removeXmlComments(sanitized);
// Remove ANSI escape sequences BEFORE removing control characters
sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
// Remove control characters (except newlines and tabs) // Remove control characters (except newlines and tabs)
sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
// XML character escaping
sanitized = sanitized
.replace(/&/g, "&amp;") // Must be first to avoid double-escaping
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
// URI filtering - replace non-https protocols with "(redacted)" // URI filtering - replace non-https protocols with "(redacted)"
sanitized = sanitizeUrlProtocols(sanitized); sanitized = sanitizeUrlProtocols(sanitized);
// Domain filtering for HTTPS URIs // Domain filtering for HTTPS URIs
@ -1419,8 +1416,7 @@ jobs:
lines.slice(0, maxLines).join("\n") + lines.slice(0, maxLines).join("\n") +
"\n[Content truncated due to line count]"; "\n[Content truncated due to line count]";
} }
// Remove ANSI escape sequences // ANSI escape sequences already removed earlier in the function
sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
// Neutralize common bot trigger phrases // Neutralize common bot trigger phrases
sanitized = neutralizeBotTriggers(sanitized); sanitized = neutralizeBotTriggers(sanitized);
// Trim excessive whitespace // Trim excessive whitespace
@ -1432,10 +1428,12 @@ jobs:
*/ */
function sanitizeUrlDomains(s) { function sanitizeUrlDomains(s) {
return s.replace( return s.replace(
/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, /\bhttps:\/\/[^\s\])}'"<>&\x00-\x1f,;]+/gi,
(match, domain) => { (match) => {
// Extract just the URL part after https://
const urlAfterProtocol = match.slice(8); // Remove 'https://'
// Extract the hostname part (before first slash, colon, or other delimiter) // Extract the hostname part (before first slash, colon, or other delimiter)
const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase(); const hostname = urlAfterProtocol.split(/[\/:\?#]/)[0].toLowerCase();
// Check if this domain or any parent domain is in the allowlist // Check if this domain or any parent domain is in the allowlist
const isAllowed = allowedDomains.some(allowedDomain => { const isAllowed = allowedDomains.some(allowedDomain => {
const normalizedAllowed = allowedDomain.toLowerCase(); const normalizedAllowed = allowedDomain.toLowerCase();
@ -1454,9 +1452,10 @@ jobs:
* @returns {string} The string with non-https protocols redacted * @returns {string} The string with non-https protocols redacted
*/ */
function sanitizeUrlProtocols(s) { function sanitizeUrlProtocols(s) {
// Match both protocol:// and protocol: patterns // Match protocol:// patterns (URLs) and standalone protocol: patterns that look like URLs
// Avoid matching command line flags like -v:10 or z3 -memory:high
return s.replace( return s.replace(
/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, /\b(\w+):\/\/[^\s\])}'"<>&\x00-\x1f]+/gi,
(match, protocol) => { (match, protocol) => {
// Allow https (case insensitive), redact everything else // Allow https (case insensitive), redact everything else
return protocol.toLowerCase() === "https" ? match : "(redacted)"; return protocol.toLowerCase() === "https" ? match : "(redacted)";
@ -1475,6 +1474,16 @@ jobs:
(_m, p1, p2) => `${p1}\`@${p2}\`` (_m, p1, p2) => `${p1}\`@${p2}\``
); );
} }
/**
* Removes XML comments to prevent content hiding
* @param {string} s - The string to process
* @returns {string} The string with XML comments removed
*/
function removeXmlComments(s) {
// Remove XML/HTML comments including malformed ones that might be used to hide content
// Matches: <!-- ... --> and <!--- ... --> and <!--- ... --!> variations
return s.replace(/<!--[\s\S]*?-->/g, "").replace(/<!--[\s\S]*?--!>/g, "");
}
/** /**
* Neutralizes bot trigger phrases by wrapping them in backticks * Neutralizes bot trigger phrases by wrapping them in backticks
* @param {string} s - The string to process * @param {string} s - The string to process
@ -1508,13 +1517,13 @@ jobs:
switch (itemType) { switch (itemType) {
case "create-issue": case "create-issue":
return 1; // Only one issue allowed return 1; // Only one issue allowed
case "add-issue-comment": case "add-comment":
return 1; // Only one comment allowed return 1; // Only one comment allowed
case "create-pull-request": case "create-pull-request":
return 1; // Only one pull request allowed return 1; // Only one pull request allowed
case "create-pull-request-review-comment": case "create-pull-request-review-comment":
return 10; // Default to 10 review comments allowed return 10; // Default to 10 review comments allowed
case "add-issue-labels": case "add-labels":
return 5; // Only one labels operation allowed return 5; // Only one labels operation allowed
case "update-issue": case "update-issue":
return 1; // Only one issue update allowed return 1; // Only one issue update allowed
@ -1729,10 +1738,10 @@ jobs:
); );
} }
break; break;
case "add-issue-comment": case "add-comment":
if (!item.body || typeof item.body !== "string") { if (!item.body || typeof item.body !== "string") {
errors.push( errors.push(
`Line ${i + 1}: add-issue-comment requires a 'body' string field` `Line ${i + 1}: add-comment requires a 'body' string field`
); );
continue; continue;
} }
@ -1767,10 +1776,10 @@ jobs:
); );
} }
break; break;
case "add-issue-labels": case "add-labels":
if (!item.labels || !Array.isArray(item.labels)) { if (!item.labels || !Array.isArray(item.labels)) {
errors.push( errors.push(
`Line ${i + 1}: add-issue-labels requires a 'labels' array field` `Line ${i + 1}: add-labels requires a 'labels' array field`
); );
continue; continue;
} }
@ -1780,7 +1789,7 @@ jobs:
) )
) { ) {
errors.push( errors.push(
`Line ${i + 1}: add-issue-labels labels array must contain only strings` `Line ${i + 1}: add-labels labels array must contain only strings`
); );
continue; continue;
} }
@ -3019,11 +3028,11 @@ jobs:
pull-requests: write pull-requests: write
timeout-minutes: 10 timeout-minutes: 10
outputs: outputs:
comment_id: ${{ steps.create_comment.outputs.comment_id }} comment_id: ${{ steps.add_comment.outputs.comment_id }}
comment_url: ${{ steps.create_comment.outputs.comment_url }} comment_url: ${{ steps.add_comment.outputs.comment_url }}
steps: steps:
- name: Add Issue Comment - name: Add Issue Comment
id: create_comment id: add_comment
uses: actions/github-script@v8 uses: actions/github-script@v8
env: env:
GITHUB_AW_AGENT_OUTPUT: ${{ needs.pr-fix.outputs.output }} GITHUB_AW_AGENT_OUTPUT: ${{ needs.pr-fix.outputs.output }}
@ -3058,15 +3067,15 @@ jobs:
core.info("No valid items found in agent output"); core.info("No valid items found in agent output");
return; return;
} }
// Find all add-issue-comment items // Find all add-comment items
const commentItems = validatedOutput.items.filter( const commentItems = validatedOutput.items.filter(
/** @param {any} item */ item => item.type === "add-issue-comment" /** @param {any} item */ item => item.type === "add-comment"
); );
if (commentItems.length === 0) { if (commentItems.length === 0) {
core.info("No add-issue-comment items found in agent output"); core.info("No add-comment items found in agent output");
return; return;
} }
core.info(`Found ${commentItems.length} add-issue-comment item(s)`); core.info(`Found ${commentItems.length} add-comment item(s)`);
// If in staged mode, emit step summary instead of creating comments // If in staged mode, emit step summary instead of creating comments
if (isStaged) { if (isStaged) {
let summaryContent = "## 🎭 Staged Mode: Add Comments Preview\n\n"; let summaryContent = "## 🎭 Staged Mode: Add Comments Preview\n\n";
@ -3110,7 +3119,7 @@ jobs:
for (let i = 0; i < commentItems.length; i++) { for (let i = 0; i < commentItems.length; i++) {
const commentItem = commentItems[i]; const commentItem = commentItems[i];
core.info( core.info(
`Processing add-issue-comment item ${i + 1}/${commentItems.length}: bodyLength=${commentItem.body.length}` `Processing add-comment item ${i + 1}/${commentItems.length}: bodyLength=${commentItem.body.length}`
); );
// Determine the issue/PR number and comment endpoint for this comment // Determine the issue/PR number and comment endpoint for this comment
let issueNumber; let issueNumber;

View file

@ -14,7 +14,7 @@ safe-outputs:
push-to-pr-branch: push-to-pr-branch:
create-issue: create-issue:
title-prefix: "${{ github.workflow }}" title-prefix: "${{ github.workflow }}"
add-issue-comment: add-comment:
github-token: ${{ secrets.DSYME_GH_TOKEN}} github-token: ${{ secrets.DSYME_GH_TOKEN}}
tools: tools: