Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changeset/patch-enforce-add-comment-mcp.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 1 addition & 39 deletions actions/setup/js/add_comment.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,49 +15,11 @@ const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_help
const { getMissingInfoSections } = require("./missing_messages_helper.cjs");
const { getMessages } = require("./messages_core.cjs");
const { sanitizeContent } = require("./sanitize_content.cjs");
const { MAX_COMMENT_LENGTH, MAX_MENTIONS, MAX_LINKS, enforceCommentLimits } = require("./comment_limit_helpers.cjs");

/** @type {string} Safe output type handled by this module */
const HANDLER_TYPE = "add_comment";

/**
* Maximum limits for comment parameters to prevent resource exhaustion.
* These limits align with GitHub's API constraints and security best practices.
*/
/** @type {number} Maximum comment body length (GitHub's limit) */
const MAX_COMMENT_LENGTH = 65536;

/** @type {number} Maximum number of mentions allowed per comment */
const MAX_MENTIONS = 10;

/** @type {number} Maximum number of links allowed per comment */
const MAX_LINKS = 50;

/**
* Enforces maximum limits on comment parameters to prevent resource exhaustion attacks.
* Per Safe Outputs specification requirement MR3, limits must be enforced before API calls.
*
* @param {string} body - Comment body to validate
* @throws {Error} When any limit is exceeded, with error code and details
*/
function enforceCommentLimits(body) {
// Check body length - max limit exceeded check
if (body.length > MAX_COMMENT_LENGTH) {
throw new Error(`E006: Comment body exceeds maximum length of ${MAX_COMMENT_LENGTH} characters (got ${body.length})`);
}

// Count mentions (@username pattern) - max limit exceeded check
const mentions = (body.match(/@\w+/g) || []).length;
if (mentions > MAX_MENTIONS) {
throw new Error(`E007: Comment contains ${mentions} mentions, maximum is ${MAX_MENTIONS}`);
}

// Count links (http:// and https:// URLs) - max limit exceeded check
const links = (body.match(/https?:\/\/[^\s]+/g) || []).length;
if (links > MAX_LINKS) {
throw new Error(`E008: Comment contains ${links} links, maximum is ${MAX_LINKS}`);
}
}

// Copy helper functions from original file
async function minimizeComment(github, nodeId, reason = "outdated") {
const query = /* GraphQL */ `
Expand Down
62 changes: 62 additions & 0 deletions actions/setup/js/comment_limit_helpers.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// @ts-check

/**
* Comment Limit Helpers
*
* This module provides validation functions for enforcing comment constraints
* across both MCP server (Phase 4 - tool invocation) and safe output processor
* (Phase 6 - API execution) per Safe Outputs Specification MCE1 and MCE4.
*
* The limits defined here must match the constraints documented in:
* - pkg/workflow/js/safe_outputs_tools.json (add_comment tool description)
* - docs/src/content/docs/reference/safe-outputs-specification.md
*
* This ensures constraint consistency per Requirement MCE5.
*/

/**
* Maximum limits for comment parameters to prevent resource exhaustion.
* These limits align with GitHub's API constraints and security best practices.
*/

/** @type {number} Maximum comment body length (GitHub's limit) */
const MAX_COMMENT_LENGTH = 65536;

/** @type {number} Maximum number of mentions allowed per comment */
const MAX_MENTIONS = 10;

/** @type {number} Maximum number of links allowed per comment */
const MAX_LINKS = 50;

/**
* Enforces maximum limits on comment parameters to prevent resource exhaustion attacks.
* Per Safe Outputs specification requirement MR3, limits must be enforced before API calls.
*
* @param {string} body - Comment body to validate
* @throws {Error} When any limit is exceeded, with error code and details
*/
function enforceCommentLimits(body) {
// Check body length - max limit exceeded check
if (body.length > MAX_COMMENT_LENGTH) {
throw new Error(`E006: Comment body exceeds maximum length of ${MAX_COMMENT_LENGTH} characters (got ${body.length})`);
}

// Count mentions (@username pattern) - max limit exceeded check
const mentions = (body.match(/@\w+/g) || []).length;
if (mentions > MAX_MENTIONS) {
throw new Error(`E007: Comment contains ${mentions} mentions, maximum is ${MAX_MENTIONS}`);
}

// Count links (http:// and https:// URLs) - max limit exceeded check
const links = (body.match(/https?:\/\/[^\s]+/g) || []).length;
if (links > MAX_LINKS) {
throw new Error(`E008: Comment contains ${links} links, maximum is ${MAX_LINKS}`);
}
}

module.exports = {
MAX_COMMENT_LENGTH,
MAX_MENTIONS,
MAX_LINKS,
enforceCommentLimits,
};
99 changes: 99 additions & 0 deletions actions/setup/js/comment_limit_helpers.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// @ts-check
import { describe, it, expect } from "vitest";
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

The test file uses beforeEach on line 8 but it's not imported from vitest on line 2. This will cause a runtime error when the tests are executed. Add beforeEach to the import statement.

Suggested change
import { describe, it, expect } from "vitest";
import { describe, it, expect, beforeEach } from "vitest";

Copilot uses AI. Check for mistakes.

describe("comment_limit_helpers", () => {
let MAX_COMMENT_LENGTH, MAX_MENTIONS, MAX_LINKS, enforceCommentLimits;

// Import the module
beforeEach(async () => {
const module = await import("./comment_limit_helpers.cjs");
MAX_COMMENT_LENGTH = module.MAX_COMMENT_LENGTH;
MAX_MENTIONS = module.MAX_MENTIONS;
MAX_LINKS = module.MAX_LINKS;
enforceCommentLimits = module.enforceCommentLimits;
});

describe("constants", () => {
it("should export MAX_COMMENT_LENGTH constant", () => {
expect(MAX_COMMENT_LENGTH).toBe(65536);
});

it("should export MAX_MENTIONS constant", () => {
expect(MAX_MENTIONS).toBe(10);
});

it("should export MAX_LINKS constant", () => {
expect(MAX_LINKS).toBe(50);
});
});

describe("enforceCommentLimits", () => {
it("should accept valid comment body", () => {
const validBody = "This is a valid comment with reasonable length";
expect(() => enforceCommentLimits(validBody)).not.toThrow();
});

it("should accept comment at exactly maximum length", () => {
const exactBody = "a".repeat(MAX_COMMENT_LENGTH);
expect(() => enforceCommentLimits(exactBody)).not.toThrow();
});

it("should reject comment exceeding maximum length", () => {
const longBody = "a".repeat(MAX_COMMENT_LENGTH + 1);
expect(() => enforceCommentLimits(longBody)).toThrow(/E006.*maximum length/i);
});

it("should accept comment with exactly maximum mentions", () => {
const mentions = Array.from({ length: MAX_MENTIONS }, (_, i) => `@user${i}`).join(" ");
expect(() => enforceCommentLimits(mentions)).not.toThrow();
});

it("should reject comment exceeding maximum mentions", () => {
const mentions = Array.from({ length: MAX_MENTIONS + 1 }, (_, i) => `@user${i}`).join(" ");
expect(() => enforceCommentLimits(mentions)).toThrow(/E007.*mentions/i);
});

it("should accept comment with exactly maximum links", () => {
const links = Array.from({ length: MAX_LINKS }, (_, i) => `https://example.com/${i}`).join(" ");
expect(() => enforceCommentLimits(links)).not.toThrow();
});

it("should reject comment exceeding maximum links", () => {
const links = Array.from({ length: MAX_LINKS + 1 }, (_, i) => `https://example.com/${i}`).join(" ");
expect(() => enforceCommentLimits(links)).toThrow(/E008.*links/i);
});

it("should count both http and https links", () => {
const httpsLinks = Array.from({ length: 30 }, (_, i) => `https://example.com/${i}`).join(" ");
const httpLinks = Array.from({ length: 21 }, (_, i) => `http://example.org/${i}`).join(" ");
const mixed = `${httpsLinks} ${httpLinks}`;
expect(() => enforceCommentLimits(mixed)).toThrow(/E008.*51.*links/i);
});

it("should include actual values in error messages", () => {
const longBody = "a".repeat(70000);
try {
enforceCommentLimits(longBody);
expect.fail("Should have thrown error");
} catch (error) {
expect(error.message).toContain("E006");
expect(error.message).toContain("65536"); // Max length
expect(error.message).toContain("70000"); // Actual length
}
});

it("should accept empty string", () => {
expect(() => enforceCommentLimits("")).not.toThrow();
});

it("should handle comment with no mentions", () => {
const body = "Comment without any user mentions";
expect(() => enforceCommentLimits(body)).not.toThrow();
});

it("should handle comment with no links", () => {
const body = "Comment without any links or URLs";
expect(() => enforceCommentLimits(body)).not.toThrow();
});
});
});
28 changes: 28 additions & 0 deletions actions/setup/js/safe_outputs_handlers.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const { writeLargeContentToFile } = require("./write_large_content_to_file.cjs")
const { getCurrentBranch } = require("./get_current_branch.cjs");
const { getBaseBranch } = require("./get_base_branch.cjs");
const { generateGitPatch } = require("./generate_git_patch.cjs");
const { enforceCommentLimits } = require("./comment_limit_helpers.cjs");
const { getErrorMessage } = require("./error_helpers.cjs");

/**
* Create handlers for safe output tools
Expand Down Expand Up @@ -372,12 +374,38 @@ function createHandlers(server, appendSafeOutput, config = {}) {
};
};

/**
* Handler for add_comment tool
* Per Safe Outputs Specification MCE1: Enforces constraints during tool invocation
* to provide immediate feedback to the LLM before recording to NDJSON
*/
const addCommentHandler = args => {
// Validate comment constraints before appending to safe outputs
// This provides early feedback per Requirement MCE1 (Early Validation)
try {
const body = args.body || "";
enforceCommentLimits(body);
} catch (error) {
// Return validation error with specific constraint violation details
// Per Requirement MCE3 (Actionable Error Responses)
// Use JSON-RPC error code -32602 (Invalid params) per MCP specification
throw {
code: -32602,
message: getErrorMessage(error),
};
}

// If validation passes, record the operation using default handler
return defaultHandler("add_comment")(args);
};

return {
defaultHandler,
uploadAssetHandler,
createPullRequestHandler,
pushToPullRequestBranchHandler,
createProjectHandler,
addCommentHandler,
};
}

Expand Down
Loading