Skip to content
109 changes: 96 additions & 13 deletions actions/setup/js/generate_safe_outputs_tools.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,18 @@ const ADD_COMMENT_DISCUSSIONS_DISABLED_NOTE =
"NOTE: Discussion comments are disabled for this workflow because discussions:write permission is not available. Set 'discussions: true' in the workflow's safe-outputs.add-comment configuration to enable discussion comments and request this permission.";
const ADD_COMMENT_REPLY_SUPPORT_SENTENCE = "Supports reply_to_id for discussion threading.";
const ADD_COMMENT_REPLY_SUPPORT_REGEX = /\s*Supports reply_to_id for discussion threading\./g;
const ISSUE_INTENT_SUFFIX = "INTENT: Include rationale (string, max 280 chars) and confidence (string, exactly one of: LOW, MEDIUM, HIGH) with each call.";
const ISSUE_INTENT_REQUIRED_SUFFIX = "INTENT REQUIRED: rationale (string, max 280 chars) and confidence (exactly one of: LOW, MEDIUM, HIGH) are required for each call.";
const ISSUE_INTENT_OPTIONAL_SUFFIX =
"INTENT ENCOURAGED: Include rationale (string, max 280 chars) and confidence (exactly one of: LOW, MEDIUM, HIGH) with each call. These fields are optional but strongly encouraged — they improve transparency and should normally be included. Use suggest: true alongside rationale and confidence to route for human review.";
const ADD_LABELS_STRICT_FIELD_DESC =
'Labels to add. Each label must be an object with required fields: name (string), rationale (string, max 280 chars), and confidence (exactly one of: LOW, MEDIUM, HIGH). Plain string label names are not permitted. Example: [{"name": "bug", "rationale": "The report describes reproducible incorrect behavior.", "confidence": "HIGH"}]. Labels must exist in the repository.';
const ADD_LABELS_OPTIONAL_FIELD_DESC =
'Labels to add. Prefer structured label objects: {"name": "bug", "rationale": "The report describes reproducible incorrect behavior.", "confidence": "HIGH", "suggest": true}. Plain strings are also accepted for compatibility. Include rationale (string, max 280 chars) and confidence (LOW, MEDIUM, or HIGH) to improve transparency; use suggest: true to route for human review. Labels must exist in the repository.';
const ASSIGN_TO_AGENT_EXAMPLE_USAGE_REGEX = /Example usage: assign_to_agent\([^)]+\)(?: or assign_to_agent\([^)]+\))?/g;
const ASSIGN_TO_AGENT_STRICT_EXAMPLE_USAGE =
'Example usage: assign_to_agent(issue_number=123, agent="copilot", rationale="Delegate this coding task to the agent.", confidence="HIGH") or assign_to_agent(pull_number=456, agent="copilot", pull_request_repo="owner/repo", rationale="The agent should implement this PR fix.", confidence="HIGH")';
const RATIONALE_REQUIRED_DESC = "Required rationale for this change (max 280 characters).";
const CONFIDENCE_REQUIRED_DESC = "Required confidence level for this change. Must be exactly one of: LOW, MEDIUM, HIGH.";
const ISSUE_INTENT_TOOL_NAMES = new Set(["set_issue_type", "set_issue_field", "add_labels", "close_issue", "assign_to_user", "assign_to_agent"]);
const ISSUE_INTENT_SCHEMA_FIELDS = ["rationale", "confidence", "suggest"];

Expand Down Expand Up @@ -94,6 +105,51 @@ function stripIssueIntentSchemaFields(tool) {
}
}

/**
* Determine whether issue-intent guidance is omitted (neither enabled nor disabled) for a tool.
* This is the default state: intent fields are present and optional, but no guidance suffix is added yet.
*
* @param {string} toolName
* @param {unknown} toolConfig
* @returns {boolean}
*/
function isIssueIntentOmittedForTool(toolName, toolConfig) {
if (!ISSUE_INTENT_TOOL_NAMES.has(toolName)) {
return false;
}
return !isIssueIntentEnabledForTool(toolName, toolConfig) && !isIssueIntentDisabledForTool(toolName, toolConfig);
}

/**
* Update the rationale and confidence property descriptions on a tool to say "Required"
* instead of "Optional", and add them to inputSchema.required so JSON Schema validators enforce them.
* Only affects the direct properties of inputSchema (not nested schemas).
* @param {{inputSchema?: {properties?: Record<string, {description?: string}>, required?: string[]}}} tool
*/
function makeIntentFieldDescriptionsRequired(tool) {
const schema = tool.inputSchema;
const properties = schema?.properties;
if (!properties) {
return;
}
const toRequire = [];
if (properties.rationale && typeof properties.rationale === "object") {
properties.rationale = { ...properties.rationale, description: RATIONALE_REQUIRED_DESC };
toRequire.push("rationale");
}
if (properties.confidence && typeof properties.confidence === "object") {
properties.confidence = { ...properties.confidence, description: CONFIDENCE_REQUIRED_DESC };
toRequire.push("confidence");
}
if (toRequire.length > 0 && schema) {
const required = new Set(schema.required ?? []);
for (const field of toRequire) {
required.add(field);
}
schema.required = [...required];
}
}

/**
* Update add_comment description to match runtime-safe-output permissions.
* @param {string} description
Expand Down Expand Up @@ -212,25 +268,52 @@ async function main() {
enhancedTool.description = (enhancedTool.description || "") + descSuffix;
}
if (isIssueIntentEnabledForTool(tool.name, config[tool.name])) {
enhancedTool.description = `${enhancedTool.description || ""} ${ISSUE_INTENT_SUFFIX}`.trim();
enhancedTool.description = `${enhancedTool.description || ""} ${ISSUE_INTENT_REQUIRED_SUFFIX}`.trim();
// Update top-level rationale/confidence descriptions to say "required" instead of "optional",
// and add them to inputSchema.required so JSON Schema validators enforce them.
makeIntentFieldDescriptionsRequired(enhancedTool);
// For add_labels strict mode, replace the labels items schema with an object-only
// variant that requires name, rationale, and confidence.
// variant that requires name, rationale, and confidence, and update the labels description.
if (tool.name === "add_labels") {
const labelsSchema = enhancedTool.inputSchema?.properties?.labels;
if (labelsSchema && labelsSchema.items && Array.isArray(labelsSchema.items.oneOf)) {
const objectSchema = labelsSchema.items.oneOf.find(/** @param {{type: string}} s */ s => s.type === "object");
if (objectSchema) {
labelsSchema.items = {
...objectSchema,
required: ["name", "rationale", "confidence"],
};
delete labelsSchema.items.oneOf;
if (labelsSchema) {
labelsSchema.description = ADD_LABELS_STRICT_FIELD_DESC;
if (labelsSchema.items && Array.isArray(labelsSchema.items.oneOf)) {
const objectSchema = labelsSchema.items.oneOf.find(/** @param {{type: string}} s */ s => s.type === "object");
if (objectSchema) {
const sourceProperties = objectSchema.properties ?? {};
const strictProperties = { ...sourceProperties };
if (strictProperties.rationale) {
strictProperties.rationale = { ...strictProperties.rationale, description: "Required rationale for the label (max 280 characters)." };
}
if (strictProperties.confidence) {
strictProperties.confidence = { ...strictProperties.confidence, description: "Required confidence level for the label. Must be exactly one of: LOW, MEDIUM, HIGH." };
}
labelsSchema.items = {
...objectSchema,
required: ["name", "rationale", "confidence"],
properties: strictProperties,
};
}
}
}
}
}
if (isIssueIntentDisabledForTool(tool.name, config[tool.name])) {
// Update inline example calls to include required fields for assign_to_agent
if (tool.name === "assign_to_agent") {
enhancedTool.description = enhancedTool.description.replace(ASSIGN_TO_AGENT_EXAMPLE_USAGE_REGEX, ASSIGN_TO_AGENT_STRICT_EXAMPLE_USAGE);
}
} else if (isIssueIntentDisabledForTool(tool.name, config[tool.name])) {
stripIssueIntentSchemaFields(enhancedTool);
} else if (isIssueIntentOmittedForTool(tool.name, config[tool.name])) {
enhancedTool.description = `${enhancedTool.description || ""} ${ISSUE_INTENT_OPTIONAL_SUFFIX}`.trim();
// For add_labels omitted mode, update the labels description to prefer structured objects,
// but only when the source schema actually supports plain strings via items.oneOf.
if (tool.name === "add_labels") {
const labelsSchema = enhancedTool.inputSchema?.properties?.labels;
if (labelsSchema && Array.isArray(labelsSchema.items?.oneOf)) {
labelsSchema.description = ADD_LABELS_OPTIONAL_FIELD_DESC;
}
}
}

if (tool.name === "add_comment") {
Expand Down
Loading
Loading