Skip to content

Commit 21e5674

Browse files
Copilotjamesadevine
andcommitted
feat: add update-work-item safe output
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
1 parent 87d6527 commit 21e5674

5 files changed

Lines changed: 938 additions & 2 deletions

File tree

src/compile/common.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,7 @@ pub fn generate_executor_ado_env(write_service_connection: Option<&str>) -> Stri
553553
}
554554

555555
/// Safe-output names that require write access to ADO.
556-
const WRITE_REQUIRING_SAFE_OUTPUTS: &[&str] = &["create-pull-request", "create-work-item", "create-wiki-page", "edit-wiki-page"];
556+
const WRITE_REQUIRING_SAFE_OUTPUTS: &[&str] = &["create-pull-request", "create-work-item", "update-work-item", "create-wiki-page", "edit-wiki-page"];
557557

558558
/// Validate that write-requiring safe-outputs have a write service connection configured.
559559
pub fn validate_write_permissions(front_matter: &FrontMatter) -> Result<()> {

src/execute.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use std::path::Path;
1111
use crate::ndjson::{self, SAFE_OUTPUT_FILENAME};
1212
use crate::tools::{
1313
CreatePrResult, CreateWikiPageResult, CreateWorkItemResult, EditWikiPageResult,
14-
ExecutionContext, ExecutionResult, Executor,
14+
ExecutionContext, ExecutionResult, Executor, UpdateWorkItemConfig, UpdateWorkItemResult,
1515
};
1616

1717
// Re-export memory types for use by main.rs
@@ -80,6 +80,24 @@ pub async fn execute_safe_outputs(
8080
info!("Found {} safe output(s) to execute", entries.len());
8181
println!("Found {} safe output(s) to execute", entries.len());
8282

83+
// Pre-validate the update-work-item max constraint before executing anything
84+
let update_wi_count = entries
85+
.iter()
86+
.filter(|e| e.get("name").and_then(|n| n.as_str()) == Some("update-work-item"))
87+
.count();
88+
if update_wi_count > 0 {
89+
let update_config: UpdateWorkItemConfig = ctx.get_tool_config("update-work-item");
90+
if update_wi_count > update_config.max as usize {
91+
return Err(anyhow::anyhow!(
92+
"Too many update-work-item safe outputs: {} found, but max is {}. \
93+
Reduce the number of work item updates or increase 'max' in the \
94+
safe-outputs.update-work-item configuration.",
95+
update_wi_count,
96+
update_config.max
97+
));
98+
}
99+
}
100+
83101
// Log summary of what we're about to execute
84102
for (i, entry) in entries.iter().enumerate() {
85103
if let Some(name) = entry.get("name").and_then(|n| n.as_str()) {
@@ -172,6 +190,13 @@ pub async fn execute_safe_output(
172190
);
173191
output.execute_sanitized(ctx).await?
174192
}
193+
"update-work-item" => {
194+
debug!("Parsing update-work-item payload");
195+
let mut output: UpdateWorkItemResult = serde_json::from_value(entry.clone())
196+
.map_err(|e| anyhow::anyhow!("Failed to parse update-work-item: {}", e))?;
197+
debug!("update-work-item: id={}", output.id);
198+
output.execute_sanitized(ctx).await?
199+
}
175200
"create-pull-request" => {
176201
debug!("Parsing create-pull-request payload");
177202
let mut output: CreatePrResult = serde_json::from_value(entry.clone())

src/mcp.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::tools::{
1515
CreateWorkItemParams, CreateWorkItemResult,
1616
EditWikiPageParams, EditWikiPageResult, MissingDataParams, MissingDataResult,
1717
MissingToolParams, MissingToolResult, NoopParams, NoopResult, ToolResult,
18+
UpdateWorkItemParams, UpdateWorkItemResult,
1819
anyhow_to_mcp_error,
1920
};
2021

@@ -335,6 +336,40 @@ impl SafeOutputs {
335336
Ok(CallToolResult::success(vec![]))
336337
}
337338

339+
#[tool(
340+
name = "update-work-item",
341+
description = "Update an existing Azure DevOps work item. Only fields explicitly enabled \
342+
in the pipeline configuration (safe-outputs.update-work-item) may be changed. Updates may be \
343+
further restricted by target (only a specific work item ID) or title-prefix (only work items \
344+
whose current title starts with a configured prefix). Provide the work item ID and only the \
345+
fields you want to update."
346+
)]
347+
async fn update_work_item(
348+
&self,
349+
params: Parameters<UpdateWorkItemParams>,
350+
) -> Result<CallToolResult, McpError> {
351+
info!("Tool called: update-work-item - id={}", params.0.id);
352+
// Sanitize untrusted agent-provided text fields (IS-01)
353+
let mut sanitized = params.0;
354+
sanitized.title = sanitized.title.map(|t| sanitize_text(&t));
355+
sanitized.body = sanitized.body.map(|b| sanitize_text(&b));
356+
sanitized.state = sanitized.state.map(|s| sanitize_text(&s));
357+
sanitized.work_item_type = sanitized.work_item_type.map(|t| sanitize_text(&t));
358+
sanitized.area_path = sanitized.area_path.map(|p| sanitize_text(&p));
359+
sanitized.iteration_path = sanitized.iteration_path.map(|p| sanitize_text(&p));
360+
sanitized.assignee = sanitized.assignee.map(|a| sanitize_text(&a));
361+
sanitized.tags = sanitized
362+
.tags
363+
.map(|ts| ts.into_iter().map(|t| sanitize_text(&t)).collect());
364+
let result: UpdateWorkItemResult = sanitized.try_into()?;
365+
let _ = self.write_safe_output_file(&result).await;
366+
info!("Work item update queued for #{}", result.id);
367+
Ok(CallToolResult::success(vec![Content::text(format!(
368+
"Work item #{} update queued. Changes will be applied during safe output processing.",
369+
result.id
370+
))]))
371+
}
372+
338373
#[tool(
339374
name = "create-pull-request",
340375
description = "Create a new pull request to propose code changes. Use this after making file edits to submit them for review and merging. The PR will be created from the current branch with your committed changes. Use 'self' for the pipeline's own repository, or a repository alias from the checkout list."

src/tools/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ mod missing_data;
1818
mod missing_tool;
1919
mod noop;
2020
mod result;
21+
mod update_work_item;
2122

2223
pub use create_pr::*;
2324
pub use create_wiki_page::*;
@@ -29,3 +30,4 @@ pub use noop::*;
2930
pub use result::{
3031
ExecutionContext, ExecutionResult, Executor, ToolResult, Validate, anyhow_to_mcp_error,
3132
};
33+
pub use update_work_item::*;

0 commit comments

Comments
 (0)