feat(compile): render proposed safe outputs to a build summary tab - #1235
Conversation
Add an always-on safe-outputs summary tab so reviewers (and observers on non-elevated runs) can see what an agent proposed without opening raw artifacts. For manual-review runs the pending-approval proposals are listed first, and the default review message points approvers at the tab. - New ado-script bundle approval-summary (scripts/ado-script/src/approval-summary/): parses safe_outputs.ndjson, renders per-tool tailored markdown (key fields + truncated body) with a generic scalar fallback, groups pending-approval proposals before automatic ones, and attaches it via task.uploadsummary. - All agent-generated content is sanitized for markdown display (escaped, code fences neutralised, control chars stripped, truncated) so a proposal cannot forge UI or break the layout. - Rendered at the END of the Agent job (not the Detection/threat-analysis stage), after collect_safe_outputs_step. Best-effort: failure is a warning, never fails the build or blocks the review gate. - Bundle delivered to the Agent job via a new safe_outputs_summary_active flag on the ado-script extension, reusing the existing install/download (no double-download). - Namespaced output base name (ado-aw-safe-outputs.md) so the ADO-derived summary-tab title never collides with a consumer/template-target tab. - New uploadSummary() vso-logger helper; reviewed-tool list passed via env (never spliced into a shell command). Tests: 22 vitest cases (parsing, grouping/ordering, per-tool detail, generic fallback, sanitization of hostile content, env handling) + 3 Rust integration tests (review / plain / no-safe-outputs placement) + bash-lint coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: looks good overall — well-structured feature with solid security fundamentals; a few minor issues worth addressing before merge. Findings🐛 Bugs / Logic Issues
|
Builds on the manual-review instruction aggregation: when multiple gated tools each carry notes, the composed ManualValidation message now also directs reviewers to the 'ado-aw-safe-outputs' summary tab (which this branch adds) to inspect the full proposed content before approving. - compose_review_instructions multi-tool branch references the summary tab - extend the multi-tool aggregation test to assert the tab pointer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good overall — well-structured feature with solid security thinking. A couple of minor issues worth addressing. Findings🐛 Bugs / Logic Issues
|
Address review feedback on #1235: - render.ts: strip control/newline chars (not just backticks) from the tool name in the heading code span — parseProposals accepts any non-empty name, so a crafted "name": "foo\nbar" could otherwise break the heading - render.ts: entity-encode & in sanitizeInline so an agent-supplied entity sequence (e.g. <) is shown literally rather than decoded by the browser - render.ts: add tailored TOOL_SPECS for the diagnostic outputs noop / report-incomplete / missing-tool / missing-data so their informative free-text (context/reason) gets a fenced body instead of a 300-char inline truncation - agentic_pipeline.rs: move partition_safe_outputs_by_approval() inside the safe-output guard so it isn't computed when no safe outputs are configured - compiler_tests.rs: anchor the job_block helper header match with a trailing newline so e.g. "Agent" can't match an "Agent_*" header - TS tests for the name-strip, & encoding, and diagnostic specs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good overall — well-considered threat model, strong test coverage, and clean design. A couple of findings worth addressing before merge. Findings
|
…obust job_block Address review feedback on #1235: - add FrontMatter::has_any_safe_output_tool() as the single source of truth for the safe-outputs-summary gate, used by BOTH collect_extensions (bundle download via safe_outputs_summary_active) and build_agent_job (step emission), so the two can no longer drift and emit a step whose bundle was never downloaded - sanitizeInline: HTML-entity-encode < and > (and & first) instead of CommonMark backslash escapes, so values stay inert regardless of whether ADO's summary renderer is CommonMark-compliant - job_block test helper: slice at the next "\n- job: " offset instead of rest[1..], removing the fixed byte-width assumption about the leading char Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good — well-designed feature with solid sanitization. Two minor issues worth addressing. Findings
|
…ence neutralization Address review feedback on #1235: - AW_REVIEWED_TOOLS is now newline-delimited (was comma): a comma can legally appear in an unrestricted YAML map key, so a comma delimiter could misparse such a key into the wrong section, whereas a newline cannot appear in a one-line map key. parseReviewed splits on newline to match. (is_safe_tool_name already rejects both via validate_safe_outputs_keys, so this is defense-in-depth.) - sanitizeBlock: expand the comment to document why U+02BC fence neutralization is deliberate — it keeps the body visually faithful while guaranteeing no real backtick run survives, which is breakout-safe regardless of how ADO's undocumented renderer tokenises fences (a zero-width-separator approach would regress if the renderer strips the separator first) - TS tests: newline split + comma-not-split coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good — well-designed feature with solid sanitization and good test coverage. One documentation inconsistency in the TypeScript I/O contract, and a minor observation on the bash step. Findings🐛 Bugs / Logic Issues
|
The module I/O contract still described AW_REVIEWED_TOOLS as comma-separated,
but it is newline-separated (parseReviewed splits on \n; the Rust side emits
reviewed.join("\n")). Correct the comment to match the code and rationale.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good overall — well-reasoned security model, strong sanitization, and clean integration. A few low-severity notes below. Findings
|
) * feat(compile): gate high-impact safe outputs behind manual review Wire the ManualValidation@1 builder into the canonical pipeline so safe outputs can require human approval via a new agentless ManualReview job. - safe-outputs.require-approval (section default) + per-tool override, accepting a bool or { approvers, notify-users, timeout-minutes, on-timeout, instructions }; reserved key filtered from tool enumeration - Pool::Server agentless IR variant emitting `pool: server` - mixed config splits Stage 3 into an automatic SafeOutputs job and a gated SafeOutputs_Reviewed job (distinct safe_outputs_reviewed artifact) - executor --only/--exclude tool filter - ManualReview only pauses when Detection cleared the run and the agent actually proposed a reviewed output (HasReviewedProposals); fail-closed - audit aggregates execution records across both safe-output artifacts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(compile): fail closed on malformed require-approval; harden review-proposal detection Address the automated PR review of #1196: - validate every require-approval value (section-level + per-tool) so a typo or unknown field surfaces as a compilation error instead of being silently dropped by the .ok() paths — previously a malformed config could let a high-impact safe output bypass the ManualReview gate - detect_reviewed_proposals_step now matches only the top-level .name of each NDJSON object via jq (with a fail-safe broad-grep fallback), avoiding a spurious pause when a tool's params contain a nested "name" key - document the single-gate instructions first-wins behaviour in safe-outputs.md - add a manual-review fixture to the bash-lint harness so the detection step is shellchecked, plus unit tests for the new validation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(compile): fail-closed approval aggregation and decouple teardown from gated job Address the second automated PR review of #1196: - aggregate_approval_config: a reviewed tool with no resolvable config now forces on-timeout=reject (fail-closed) instead of leaving all_resume=true, closing a potential approval-bypass-on-timeout regression - wire_explicit_dependencies: in the mixed split, Teardown depends only on the automatic SafeOutputs job, never on the human-gated SafeOutputs_Reviewed job (which is routinely skipped and can stay paused indefinitely) — cleanup now fires on the common no-reviewed-proposal path and never blocks on approval - filter_flags: document where the unquoted-tool-name safety invariant is enforced (validate_safe_outputs_keys before build_canonical_jobs) - add a regression test asserting Teardown skips the reviewed-job dependency Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(compile): correct on-timeout values in require-approval error message The validate_require_approval help text listed the valid on-timeout values as "(allow | reject)", but ApprovalOnTimeout serializes to "resume | reject" (kebab-case). Correct the message so a user who typos on-timeout is pointed at the actual valid values. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(compile): harden manual-review gate per review feedback Address the latest automated review of #1196: - detect_reviewed_proposals_step: when jq is present but exits non-zero (e.g. corrupt/truncated proposals), fall back to the broad raw scan (over-match, fail-safe) and log a warning, instead of silently leaving HasReviewedProposals false — closes a potential approval-gate bypass if Stage 3 ever tolerates a file jq rejects - aggregate_approval_config: debug_assert the reviewed slice is non-empty so the fail-open (resume) default for an empty slice can't be reached via future misuse - lower_pool: fold Pool::Server into a single exhaustive match, removing the early-return + unreachable! duplication so a new Pool variant is caught by exhaustiveness - add a 1ES-target integration test asserting the ManualReview server job emits pool: server and is NOT wrapped in templateContext Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(compile): defend require-approval fields and harden invariant Address the latest automated review of #1196: - reject ADO template expressions (`${{ ... }}`) in the require-approval approvers/notify-users/instructions fields — these expand at queue time before ManualValidation@1 sees them, so an author value like `${{ variables['secret-token'] }}` could leak a pipeline value into the gate. Runtime macros (`$(...)`) are intentionally still allowed since instructions documents `$(...)` interpolation. Adds a narrow contains_ado_template_expression helper. - upgrade the aggregate_approval_config empty-slice guard from debug_assert! to assert! so the fail-open (resume) default can't be reached in release builds either (security boundary; compiler is not a hot path) - document the all-reviewed edge case in safe-outputs.md: when every tool is gated, the single SafeOutputs job (incl. always-on diagnostics noop/ report_incomplete/missing-*) is deferred behind approval; leave one tool non-gated to keep diagnostics automatic Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(compile): enforce manual-review timeout on the task so on-timeout fires timeout-minutes previously only set the agentless job-level timeout, which cancels the job — ManualValidation@1's onTimeout handler never runs. For on-timeout: resume this meant a timed-out run was cancelled/failed instead of auto-approving, contradicting the documented behaviour. - add a timeout_minutes(u32) setter to the ManualValidation builder that sets the step-level timeoutInMinutes (ManualValidation@1 has no 'timeout' input; the control option is what triggers onTimeout) - set it from approval.timeout_minutes in build_manual_validation_step - keep the job-level timeout as a strictly-larger outer hard bound (timeout + 5min grace) so a job cancellation can never preempt the task's graceful onTimeout — an equal job timeout would re-introduce the bug - docs + builder unit tests + an integration test asserting the task carries timeoutInMinutes: 120 and the job carries 125 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(compile): aggregate all reviewed tools' approval instructions Previously aggregate_approval_config took only the first non-empty instructions from the sorted reviewed-tool list, so when multiple approval-gated tools each carried their own note the reviewer saw just one and the rest were silently dropped. - compose_review_instructions now lists every reviewed tool and attaches ALL author-supplied per-tool notes (grouped when identical, e.g. inherited from a section-level require-approval). A single reviewed tool with its own instructions still shows that note verbatim. - co-locate the invariant in a doc-comment on aggregate_approval_config / compose_review_instructions; update docs/safe-outputs.md (no more first-tool-wins) - integration test asserting every tool is listed and all notes appear Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(compile): render proposed safe outputs to a build summary tab (#1235) * feat(compile): render proposed safe outputs to a build summary tab Add an always-on safe-outputs summary tab so reviewers (and observers on non-elevated runs) can see what an agent proposed without opening raw artifacts. For manual-review runs the pending-approval proposals are listed first, and the default review message points approvers at the tab. - New ado-script bundle approval-summary (scripts/ado-script/src/approval-summary/): parses safe_outputs.ndjson, renders per-tool tailored markdown (key fields + truncated body) with a generic scalar fallback, groups pending-approval proposals before automatic ones, and attaches it via task.uploadsummary. - All agent-generated content is sanitized for markdown display (escaped, code fences neutralised, control chars stripped, truncated) so a proposal cannot forge UI or break the layout. - Rendered at the END of the Agent job (not the Detection/threat-analysis stage), after collect_safe_outputs_step. Best-effort: failure is a warning, never fails the build or blocks the review gate. - Bundle delivered to the Agent job via a new safe_outputs_summary_active flag on the ado-script extension, reusing the existing install/download (no double-download). - Namespaced output base name (ado-aw-safe-outputs.md) so the ADO-derived summary-tab title never collides with a consumer/template-target tab. - New uploadSummary() vso-logger helper; reviewed-tool list passed via env (never spliced into a shell command). Tests: 22 vitest cases (parsing, grouping/ordering, per-tool detail, generic fallback, sanitization of hostile content, env handling) + 3 Rust integration tests (review / plain / no-safe-outputs placement) + bash-lint coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(compile): point the aggregated approval message at the summary tab Builds on the manual-review instruction aggregation: when multiple gated tools each carry notes, the composed ManualValidation message now also directs reviewers to the 'ado-aw-safe-outputs' summary tab (which this branch adds) to inspect the full proposed content before approving. - compose_review_instructions multi-tool branch references the summary tab - extend the multi-tool aggregation test to assert the tab pointer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(compile): harden safe-outputs summary renderer per review Address review feedback on #1235: - render.ts: strip control/newline chars (not just backticks) from the tool name in the heading code span — parseProposals accepts any non-empty name, so a crafted "name": "foo\nbar" could otherwise break the heading - render.ts: entity-encode & in sanitizeInline so an agent-supplied entity sequence (e.g. <) is shown literally rather than decoded by the browser - render.ts: add tailored TOOL_SPECS for the diagnostic outputs noop / report-incomplete / missing-tool / missing-data so their informative free-text (context/reason) gets a fenced body instead of a 300-char inline truncation - agentic_pipeline.rs: move partition_safe_outputs_by_approval() inside the safe-output guard so it isn't computed when no safe outputs are configured - compiler_tests.rs: anchor the job_block helper header match with a trailing newline so e.g. "Agent" can't match an "Agent_*" header - TS tests for the name-strip, & encoding, and diagnostic specs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(compile): de-dup summary feature gate; entity-encode tags; robust job_block Address review feedback on #1235: - add FrontMatter::has_any_safe_output_tool() as the single source of truth for the safe-outputs-summary gate, used by BOTH collect_extensions (bundle download via safe_outputs_summary_active) and build_agent_job (step emission), so the two can no longer drift and emit a step whose bundle was never downloaded - sanitizeInline: HTML-entity-encode < and > (and & first) instead of CommonMark backslash escapes, so values stay inert regardless of whether ADO's summary renderer is CommonMark-compliant - job_block test helper: slice at the next "\n- job: " offset instead of rest[1..], removing the fixed byte-width assumption about the leading char Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(compile): use newline delimiter for AW_REVIEWED_TOOLS; document fence neutralization Address review feedback on #1235: - AW_REVIEWED_TOOLS is now newline-delimited (was comma): a comma can legally appear in an unrestricted YAML map key, so a comma delimiter could misparse such a key into the wrong section, whereas a newline cannot appear in a one-line map key. parseReviewed splits on newline to match. (is_safe_tool_name already rejects both via validate_safe_outputs_keys, so this is defense-in-depth.) - sanitizeBlock: expand the comment to document why U+02BC fence neutralization is deliberate — it keeps the body visually faithful while guaranteeing no real backtick run survives, which is breakout-safe regardless of how ADO's undocumented renderer tokenises fences (a zero-width-separator approach would regress if the renderer strips the separator first) - TS tests: newline split + comma-not-split coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(approval-summary): fix stale AW_REVIEWED_TOOLS delimiter comment The module I/O contract still described AW_REVIEWED_TOOLS as comma-separated, but it is newline-separated (parseReviewed splits on \n; the Rust side emits reviewed.join("\n")). Correct the comment to match the code and rationale. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Adds an always-on safe-outputs summary tab so reviewers (and observers on non-elevated runs) can see what an agent proposed without opening raw artifacts. When manual review is configured (#1196), the pending-approval proposals are listed first and the default review message points approvers at the tab.
How it works
ado-scriptbundleapproval-summary(scripts/ado-script/src/approval-summary/): parsessafe_outputs.ndjson, renders per-tool tailored markdown (key fields + truncated body, with a generic scalar fallback), groups pending-before-automatic, and attaches it via##vso[task.uploadsummary]. Consistent withgate.js/import.js/exec-context-*.js(ncc-bundled, shipped inado-script.zip).collect_safe_outputs_step) — deliberately not in the Detection/threat-analysis stage, whose only job is inspecting proposals for threats. Best-effort: a render failure is downgraded to a warning and never fails the build or blocks the review gate.uploadsummarytabs: ADO derives a summary section's title from the uploaded file's base name and does not de-duplicate, so this uses a namespaced base name (ado-aw-safe-outputs.md→ theado-aw-safe-outputssection). It is additive and build-scoped — one extra section alongside any consumer/template-target tabs, never colliding (works undertarget: job/target: stagetoo).Implementation notes
safe_outputs_summary_activeflag on the ado-script extension, reusing the existing guarded install/download (no double-download).uploadSummary()helper inshared/vso-logger.ts; the reviewed-tool list is passed via env (never spliced into a shell command, so no tool name reaches a shell word-split).default_review_instructionsnow points approvers at the tab.package.jsonbuild/clean/test:smokechains, and.gitignoreupdated for the new bundle.docs/safe-outputs.md(tab + coexistence),docs/ado-script.md,AGENTS.md.Test plan
cargo clippy --all-targets— clean.ENFORCE_BASH_LINT=1, including 3 new integration tests asserting the render step lands in the Agent job (not Detection) for the review / plain-safe-outputs / no-safe-outputs cases, plus bash-lint coverage of the new step.tsc --noEmitclean (pre-existingpicomatchtypes issue aside); 407 vitest pass incl. 22 new (NDJSON parsing, pending-first grouping/ordering, per-tool detail, generic fallback, hostile-content sanitization, env handling).<b>→\<b\>).Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com