@@ -20,7 +20,7 @@ const { getErrorMessage } = require("./error_helpers.cjs");
2020const { ERR_CONFIG , ERR_PARSE , ERR_SYSTEM , ERR_VALIDATION } = require ( "./error_codes.cjs" ) ;
2121const { findRepoCheckout } = require ( "./find_repo_checkout.cjs" ) ;
2222const { resolveTargetRepoConfig, resolveAndValidateRepo } = require ( "./repo_helpers.cjs" ) ;
23- const { getOrGenerateTemporaryId } = require ( "./temporary_id.cjs" ) ;
23+ const { generateTemporaryId , getOrGenerateTemporaryId } = require ( "./temporary_id.cjs" ) ;
2424const { parseAllowedExtensionsEnv } = require ( "./allowed_extensions_helpers.cjs" ) ;
2525const { getStagedPatchDiffSizeBytes } = require ( "./git_patch_utils.cjs" ) ;
2626const { sanitizeTitle, applyTitlePrefix } = require ( "./sanitize_title.cjs" ) ;
@@ -101,7 +101,7 @@ function readJSONFile(filePath) {
101101
102102const safeOutputsTools = readJSONFile ( path . join ( __dirname , "safe_outputs_tools.json" ) ) ;
103103
104- const safeOutputsToolMap = new Map ( safeOutputsTools . map ( tool => [ tool . name , tool ] ) ) ;
104+ const safeOutputsToolMap = new Map ( safeOutputsTools . map ( tool => [ tool . name . replace ( / - / g , "_" ) , tool ] ) ) ;
105105
106106/**
107107 * @param {string } error
@@ -2063,6 +2063,89 @@ function createHandlers(server, appendSafeOutput, config = {}) {
20632063 ] ,
20642064 isError : true ,
20652065 } ;
2066+
2067+ const createWorkItemHandler = args => {
2068+ const temporaryId = `#${ generateTemporaryId ( ) } ` ;
2069+ const entry = { ...( args || { } ) , type : "create_work_item" , temporary_id : temporaryId } ;
2070+ appendSafeOutputCounted ( entry ) ;
2071+ const output = { result : "success" , temporary_id : temporaryId } ;
2072+ return {
2073+ content : [ { type : "text" , text : JSON . stringify ( output ) } ] ,
2074+ structuredContent : output ,
2075+ } ;
2076+ } ;
2077+
2078+ const createAzureDevOpsWorkItemHandler = type => args => {
2079+ const entry = { ...( args || { } ) , type } ;
2080+ appendSafeOutputCounted ( entry ) ;
2081+ return {
2082+ content : [ { type : "text" , text : JSON . stringify ( { result : "success" } ) } ] ,
2083+ } ;
2084+ } ;
2085+
2086+ const uploadWorkItemAttachmentHandler = args => {
2087+ const entry = { ...( args || { } ) , type : "upload_workitem_attachment" } ;
2088+ const rawPath = typeof entry . file_path === "string" ? entry . file_path . trim ( ) : "" ;
2089+ if ( ! rawPath || path . isAbsolute ( rawPath ) || rawPath . includes ( ":" ) ) {
2090+ return buildIntentErrorResponse ( "upload-workitem-attachment file_path must be a workspace-relative path without ':'" ) ;
2091+ }
2092+
2093+ const segments = rawPath . split ( / [ \\ / ] + / ) ;
2094+ if ( segments . some ( segment => ! segment || segment === "." || segment === ".." ) ) {
2095+ return buildIntentErrorResponse ( "upload-workitem-attachment file_path must not contain empty, '.' or '..' path segments" ) ;
2096+ }
2097+
2098+ const workspace = path . resolve ( process . env . GITHUB_WORKSPACE || process . cwd ( ) ) ;
2099+ const sourcePath = path . resolve ( workspace , ...segments ) ;
2100+ if ( sourcePath !== workspace && ! sourcePath . startsWith ( workspace + path . sep ) ) {
2101+ return buildIntentErrorResponse ( "upload-workitem-attachment file_path resolves outside the workspace" ) ;
2102+ }
2103+
2104+ let current = workspace ;
2105+ let sourceStat ;
2106+ try {
2107+ for ( const segment of segments ) {
2108+ current = path . join ( current , segment ) ;
2109+ sourceStat = lstatGuard ( current ) ;
2110+ if ( ! sourceStat ) {
2111+ return buildIntentErrorResponse ( "upload-workitem-attachment does not accept symbolic links" ) ;
2112+ }
2113+ }
2114+ } catch ( error ) {
2115+ return buildIntentErrorResponse ( `upload-workitem-attachment could not read file_path: ${ getErrorMessage ( error ) } ` ) ;
2116+ }
2117+ if ( ! sourceStat ?. isFile ( ) ) {
2118+ return buildIntentErrorResponse ( "upload-workitem-attachment file_path must identify one regular file" ) ;
2119+ }
2120+
2121+ const attachmentConfig = getSafeOutputsToolConfig ( config , "upload_workitem_attachment" ) ;
2122+ const maxFileSize = Number ( attachmentConfig . max_file_size || 5 * 1024 * 1024 ) ;
2123+ if ( ! Number . isSafeInteger ( maxFileSize ) || maxFileSize < 1 || sourceStat . size > maxFileSize ) {
2124+ return buildIntentErrorResponse ( `upload-workitem-attachment file exceeds the configured max-file-size of ${ maxFileSize } bytes` ) ;
2125+ }
2126+ const allowedExtensions = Array . isArray ( attachmentConfig . allowed_extensions ) ? attachmentConfig . allowed_extensions : [ ] ;
2127+ if ( allowedExtensions . length > 0 && ! allowedExtensions . some ( extension => rawPath . toLowerCase ( ) . endsWith ( String ( extension ) . toLowerCase ( ) ) ) ) {
2128+ return buildIntentErrorResponse ( "upload-workitem-attachment file extension is not allowed by the workflow configuration" ) ;
2129+ }
2130+
2131+ try {
2132+ const stagingRoot = path . join ( process . env . RUNNER_TEMP || "/tmp" , "gh-aw" , "safeoutputs" , "upload-artifacts" ) ;
2133+ const stagingDirectory = path . join ( stagingRoot , "azure-devops-work-items" ) ;
2134+ fs . mkdirSync ( stagingDirectory , { recursive : true , mode : 0o700 } ) ;
2135+ const stagedName = `${ crypto . randomUUID ( ) } -${ path . basename ( rawPath ) } ` ;
2136+ const stagedPath = path . join ( stagingDirectory , stagedName ) ;
2137+ fs . copyFileSync ( sourcePath , stagedPath , fs . constants . COPYFILE_EXCL ) ;
2138+ fs . chmodSync ( stagedPath , 0o600 ) ;
2139+ entry . staged_file = path . posix . join ( "azure-devops-work-items" , stagedName ) ;
2140+ } catch ( error ) {
2141+ throw new Error ( `${ ERR_SYSTEM } : Failed to stage Azure DevOps work-item attachment: ${ getErrorMessage ( error ) } ` , { cause : error } ) ;
2142+ }
2143+
2144+ appendSafeOutputCounted ( entry ) ;
2145+ return {
2146+ content : [ { type : "text" , text : JSON . stringify ( { result : "success" , file_path : rawPath } ) } ] ,
2147+ } ;
2148+ } ;
20662149 }
20672150 const resolvedRepo = repoResult . repo ;
20682151
@@ -3117,6 +3200,12 @@ function createHandlers(server, appendSafeOutput, config = {}) {
31173200 pushToPullRequestBranchHandler,
31183201 pushRepoMemoryHandler,
31193202 createIssueHandler,
3203+ createWorkItemHandler,
3204+ updateWorkItemHandler : createAzureDevOpsWorkItemHandler ( "update_work_item" ) ,
3205+ commentOnWorkItemHandler : createAzureDevOpsWorkItemHandler ( "comment_on_work_item" ) ,
3206+ assignWorkItemHandler : createAzureDevOpsWorkItemHandler ( "assign_work_item" ) ,
3207+ linkWorkItemsHandler : createAzureDevOpsWorkItemHandler ( "link_work_items" ) ,
3208+ uploadWorkItemAttachmentHandler,
31203209 createProjectHandler,
31213210 addCommentHandler,
31223211 createPullRequestReviewCommentHandler,
0 commit comments