feat(safe-outputs): add upload-build-artifact safe output#373
Conversation
Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/96219dfc-20c8-4f51-a26f-5cce43192f51 Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/96219dfc-20c8-4f51-a26f-5cce43192f51 Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
|
/rust-review |
|
✅ Rust PR Reviewer completed successfully! |
🔍 Rust PR ReviewSummary: Well-implemented — the two-phase path canonicalization and defense-in-depth are solid. Two minor issues worth addressing before merge. Findings
|
* feat(safe-outputs): add unified upload-build-artifact safe output Combines the concepts from #363 (upload-artifact, current build) and #373 (upload-build-artifact, arbitrary build) into a single tool that handles both use cases via the ADO build attachments REST API. Key design: - build_id is Optional — omit to target the current pipeline run (resolved from BUILD_BUILDID in Stage 3) - REST API (PUT .../builds/{buildId}/attachments/{type}/{name}) for all cases, replacing the ##vso[artifact.upload] logging command approach - allowed-build-ids check is skipped when targeting the current build (implicitly trusted) - Full Stage 1 → Stage 3 file staging contract: validate, canonicalize, bounds-check, stage file with extension preservation, re-canonicalize and re-verify in Stage 3 Supersedes #363 and #373. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: move file read after dry-run guard and use Path::extension() for allowlist check - Move std::fs::read after the dry_run early return to avoid reading up to 50 MB into memory only to discard it in dry-run mode. - Switch extension allowlist from suffix matching on the full path to Path::extension() comparison, preventing false matches like 'catalog' matching allowed-extensions: ['log']. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: restore cache-memory heading in safe-outputs.md The heading was accidentally dropped when the upload-build-artifact section was inserted, leaving the cache-memory paragraph as a stray note under the wrong section. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: use async file read, add staged-file integrity check, clarify comments - Switch std::fs::read to tokio::fs::read().await to avoid blocking the tokio runtime for files up to 50 MB. - Add a warn!() when the live staged-file size differs from the size recorded at Stage 1, catching potential file tampering between stages. - Add comments on is_valid_version() call sites noting intentional reuse for artifact-name and attachment-type charset validation. - Add comment on staged filename explaining max-length reasoning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: make staged-file size mismatch a hard failure A size mismatch between Stage 1 and Stage 3 indicates the staged file was modified between stages — fail hard rather than warning and uploading potentially tampered content. Adds a test for the integrity check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: add attachment-type dot guard, Stage 1 size cap, executor tests - Add starts_with('.') guard to attachment-type validation, matching the artifact_name check for consistency. - Add defense-in-depth file size cap at Stage 1 (MCP) using the default 50 MB limit to prevent staging-disk exhaustion before Stage 3 enforces the operator's configured limit. - Add three missing executor tests: extension allowlist rejection, artifact-name allowlist rejection, and name-prefix application. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add SHA-256 cross-stage integrity verification Add SHA-256 hash recording at Stage 1 and verification at Stage 3 for both file-staging safe outputs: - upload-build-artifact: staged_sha256 field recorded after copying the file, verified before uploading to ADO. Catches same-size file swaps that the size-only check would miss. - create-pull-request: patch_sha256 field recorded after writing the patch, verified before applying. Field is Optional for backward compatibility with existing NDJSON records. Also adds sha256_hex() helper in upload_build_artifact module, reused by both tools and the MCP handlers. A separate issue (#381) tracks adding the full staging pattern (including SHA-256) to upload-workitem-attachment, which currently reads directly from source_directory without staging. Also fixes: name-prefix length cap (50 chars), attachment-type leading-dot guard, comment typo in MCP handler. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: make patch_sha256 a required field on CreatePrResult Pipelines are versioned with their YAML — no need for backward compatibility with old NDJSON records. Making the hash optional created a security hole where a missing hash would skip verification entirely. Now patch_sha256 is a required String and verification is unconditional. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: move sha256_hex to src/hash.rs, fix prefix slice panic - Move sha256_hex() from upload_build_artifact.rs to a dedicated src/hash.rs module, removing the coupling between create_pr, mcp.rs and the upload_build_artifact module. - Fix potential panic in name-prefix error path: &prefix[..20] can panic on multi-byte UTF-8 boundaries. Use prefix.chars().take(20).collect() instead. - Revert upload_build_artifact module visibility back to private. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: add SHA-256 mismatch tests, fix TOCTOU, extract is_valid_artifact_name - Add non-dry-run SHA-256 mismatch tests for both upload-build-artifact and create-pull-request (the mismatch path fires before any HTTP call, so no credentials are needed). - Fix TOCTOU in MCP handler: hash the source bytes before writing to the staging directory, instead of re-reading the staged copy. - Extract is_valid_artifact_name() in validate.rs to decouple artifact name validation from version string validation, preventing silent breakage if is_valid_version is ever tightened. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: import DEFAULT_MAX_FILE_SIZE in MCP handler Replace the floating STAGE1_MAX_FILE_SIZE copy with a direct import of DEFAULT_MAX_FILE_SIZE from upload_build_artifact, ensuring the Stage 1 cap stays in sync if the default is ever changed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Adds
upload-build-artifact, a safe output that lets an agent attach a workspace file to an arbitrary (typically already-finished) Azure DevOps build via the build attachments REST API. Fills the gap left byupload-artifact(#363), which can only target the current run via##vso[artifact.upload].The split mirrors
create-pull-requestandupload-artifact:build_id/artifact_name/file_path, canonicalises the file inside the agent workspace (rejecting symlink escapes and directories), and copies it intooutput_directoryunder a generated unique name. The sandbox workspace is gone by Stage 3, so the file has to be staged.working_directory, applies operator policy, thenPUTs the bytes to…/_apis/build/builds/{buildId}/attachments/{type}/{name}?api-version=7.1-preview.1.Files
src/safeoutputs/upload_build_artifact.rs— new tool:UploadBuildArtifactParams,UploadBuildArtifactResult(withbuild_id,artifact_name,file_path,staged_file,file_sizeand an explicit::new()constructor), internalUploadBuildArtifactResultFieldsto satisfy thetool_result!macro, validation reusingis_valid_version/is_safe_path_segmentfromsrc/validate.rs, and the Stage 3 REST executor (+23 unit/executor tests).src/safeoutputs/mod.rs— module, re-export,WRITE_REQUIRING_SAFE_OUTPUTS,ALL_KNOWN_SAFE_OUTPUTS, requires-write test.src/execute.rs— dispatcher entry and per-run budget registration.src/mcp.rs—upload-build-artifactMCP tool: Stage 1 validation, workspace-bounded canonicalisation, directory rejection, file copy intooutput_directory, NDJSON write.docs/safe-outputs.md— reference section.Front-matter shape
Notes / future work
attachment-typeis operator-controlled and re-validated against the artifact-name charset before being interpolated into the URL.attachment-typeisagent-artifactso the executor's contributions are visually distinguishable from the build's own pipeline-artifact list.Test plan
cargo build✅cargo test✅ (all green; 23 new tests covering name/path/build-id validation, config defaults + YAML round-trip, serialization, and Stage 3 executor behaviour against a staged file in a temporary working directory — including allow-list, oversize, and missing-file rejection paths)cargo clippy --all-targets --all-features✅ (no new warnings on the new file)