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
62 changes: 62 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ rand = "0.10.1"
base64 = "0.22.1"
glob-match = "0.2.1"
similar = "3.1.0"
sha2 = "0.11.0"

[dev-dependencies]
reqwest = { version = "0.12", features = ["blocking"] }
38 changes: 38 additions & 0 deletions docs/safe-outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,44 @@ safe-outputs:
max: 1 # Maximum per run (default: 1)
```

### upload-build-artifact
Attaches a workspace file to an Azure DevOps build as a build attachment via the
ADO build attachments REST API
(`PUT /_apis/build/builds/{buildId}/attachments/{type}/{name}`).

**Omit `build_id` to target the current pipeline run** — the executor resolves
the build ID from the `BUILD_BUILDID` environment variable automatically. When
`build_id` is provided, the file is attached to that specific build — useful for
posthumously decorating a finished build with a generated report, screenshot, or
log bundle.

The tool stages the file during Stage 1 (MCP) by copying it into the
safe-outputs directory; Stage 3 reads the staged copy and uploads it via the REST
API.

**Agent parameters:**
- `build_id` *(optional)* - Target build ID. Omit to attach to the current pipeline run. Must be positive when specified.
- `artifact_name` - Artifact name (1–100 chars, alphanumeric / `-` / `_` / `.`, no leading `.`)
- `file_path` - Relative path to the file in the workspace (no directory traversal)

**Configuration options (front matter):**
```yaml
safe-outputs:
upload-build-artifact:
max-file-size: 52428800 # Maximum file size in bytes (default: 50 MB)
allowed-extensions: [] # Optional — restrict file types (e.g., [".png", ".pdf", ".log"])
allowed-artifact-names: [] # Optional — restrict names (suffix `*` = prefix match)
allowed-build-ids: [] # Optional — restrict target builds (skipped when targeting current build)
name-prefix: "" # Optional — prepended to the agent-supplied artifact name
attachment-type: "agent-artifact" # Optional — {type} segment in the attachments URL (default: "agent-artifact")
max: 3 # Maximum per run (default: 3)
```

**Notes:**
- Single-file only; directory uploads are not supported.
- When `build_id` is omitted and `allowed-build-ids` is configured, the allow-list check is skipped — the current build is implicitly trusted.
- The default `attachment-type` is `agent-artifact` so executor contributions are visually distinguishable from the build's own artifacts.

### cache-memory (moved to `tools:`)
Memory is now configured as a first-class tool under `tools: cache-memory:` instead of `safe-outputs: memory:`. See the [Cache Memory section](./tools.md#cache-memory-cache-memory) in `docs/tools.md` for details.

Expand Down
5 changes: 4 additions & 1 deletion src/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ use crate::safeoutputs::{
ExecutionContext, ExecutionResult, Executor, LinkWorkItemsResult, MissingDataResult,
MissingToolResult, NoopResult, QueueBuildResult, ReplyToPrCommentResult,
ReportIncompleteResult, ResolvePrThreadResult, SubmitPrReviewResult, ToolResult,
UpdatePrResult, UpdateWikiPageResult, UpdateWorkItemResult, UploadWorkitemAttachmentResult,
UpdatePrResult, UpdateWikiPageResult, UpdateWorkItemResult, UploadBuildArtifactResult,
UploadWorkitemAttachmentResult,
};

// Re-export memory types for use by main.rs
Expand Down Expand Up @@ -91,6 +92,7 @@ pub async fn execute_safe_outputs(
AddBuildTagResult,
CreateBranchResult,
UpdatePrResult,
UploadBuildArtifactResult,
UploadWorkitemAttachmentResult,
SubmitPrReviewResult,
ReplyToPrCommentResult,
Expand Down Expand Up @@ -273,6 +275,7 @@ pub async fn execute_safe_output(
"add-build-tag" => AddBuildTagResult,
"create-branch" => CreateBranchResult,
"update-pr" => UpdatePrResult,
"upload-build-artifact" => UploadBuildArtifactResult,
"upload-workitem-attachment" => UploadWorkitemAttachmentResult,
"submit-pr-review" => SubmitPrReviewResult,
"reply-to-pr-review-comment" => ReplyToPrCommentResult,
Expand Down
34 changes: 34 additions & 0 deletions src/hash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//! Cryptographic hash utilities shared across the crate.
//!
//! Used by safe-output tools to record and verify file integrity between
//! Stage 1 (MCP, in-sandbox) and Stage 3 (executor, outside sandbox).

use sha2::{Digest, Sha256};

/// Compute the SHA-256 hex digest of a byte slice.
pub(crate) fn sha256_hex(data: &[u8]) -> String {
let hash = Sha256::digest(data);
hash.iter().map(|b| format!("{:02x}", b)).collect()
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_sha256_empty() {
// SHA-256 of empty input is well-known.
assert_eq!(
sha256_hex(b""),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
}

#[test]
fn test_sha256_hello() {
assert_eq!(
sha256_hex(b"hello"),
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
);
}
}
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod ecosystem_domains;
mod engine;
mod execute;
mod fuzzy_schedule;
mod hash;
mod init;
mod logging;
mod mcp;
Expand Down
Loading
Loading