Skip to content

feat: enable real-time agent output streaming with VSO filtering#159

Merged
jamesadevine merged 1 commit intomainfrom
feat/improve-realtime-output
Apr 13, 2026
Merged

feat: enable real-time agent output streaming with VSO filtering#159
jamesadevine merged 1 commit intomainfrom
feat/improve-realtime-output

Conversation

@jamesadevine
Copy link
Copy Markdown
Collaborator

Summary

Enables real-time agent output streaming in ADO pipeline logs while filtering VSO commands to prevent injection.

Problem

All agent output was previously hidden until the run completed. The AWF step redirected stdout/stderr to a file (> file 2>&1), then replayed it post-hoc via sed. Operators watching the pipeline saw nothing during what can be a long-running agent execution.

Solution

Replace file-redirect-then-replay with piped streaming:

AWF (agent) → sed -u (filter ##vso[ and ##[) → tee (display + save to file)
  • sed -u (GNU unbuffered mode) processes output line-by-line for immediate display
  • tee splits output to both stdout (ADO pipeline log) and the artifact file
  • set -o pipefail ensures AWF exit codes propagate correctly through the pipe
  • Filters both ##vso[ and ##[ shorthand forms (consistent with sanitize.rs)

Changes

  • templates/base.yml
    • Agent step: piped streaming with VSO filtering
    • Threat analysis step: same treatment
    • Added dedicated agent_log_$(Build.BuildId) artifact for easy access
  • templates/1es-base.yml
    • Threat analysis step: same piped streaming treatment

Security

  • VSO commands (##vso[ and ##[) are filtered before reaching ADO log interpreter
  • Artifact file contains filtered output (no raw VSO commands in artifacts)
  • Pattern is consistent with neutralize_pipeline_commands() in sanitize.rs

@jamesadevine jamesadevine force-pushed the feat/improve-realtime-output branch from d04ee33 to 6262e5e Compare April 13, 2026 11:27
@github-actions
Copy link
Copy Markdown
Contributor

🔍 Rust PR Review

Summary: Has one pipeline-breaking bug that will fail the ProcessSafeOutputs job.

Findings

🐛 Bugs / Logic Issues

  • templates/base.yml:611 — Orphaned agent_log_$(Build.BuildId) download

    The ProcessSafeOutputs job downloads an artifact named agent_log_$(Build.BuildId):

    - download: current
      artifact: agent_log_$(Build.BuildId)

    But no publish step for this artifact exists anywhere in the template. The three published artifacts are agent_outputs_$(Build.BuildId), analyzed_outputs_$(Build.BuildId), and safe_outputs — none is named agent_log_$(Build.BuildId). A failed download: step aborts the job, so any pipeline run that reaches ProcessSafeOutputs will fail.

    The PR description says "Added dedicated agent_log_$(Build.BuildId) artifact for easy access" — the corresponding - publish: step was never added to PerformAgenticTask.

    Fix options:

    1. Add a - publish: step in PerformAgenticTask for the log file (e.g. publish just $(Agent.TempDirectory)/staging/logs/agent-output.txt)
    2. Remove the download step and rely on the agent log already being present in analyzed_outputs_$(Build.BuildId)/logs/agent-output.txt (which is already being copied on line 651 — and this path is valid since AnalyzeSafeOutputs does a cp -a of the full agent_outputs artifact into analyzed_outputs)
  • templates/base.yml:651cp source path is inconsistent with the download step it depends on

    The copy at line 651 sources from analyzed_outputs_$(Build.BuildId)/logs/agent-output.txt. This path is valid (because AnalyzeSafeOutputs copies the entire agent_outputs artifact into analyzed_outputs, including logs/agent-output.txt). The || true guards it. But this makes the agent_log_$(Build.BuildId) download step above completely unused — the log is sourced from analyzed_outputs, not from agent_log_. The two were clearly meant to go together but the publish side was dropped.

⚠️ Suggestions

  • templates/base.yml:316, templates/1es-base.yml — The sed filter runs on the saved artifact file too (because tee copies the already-filtered stdout stream). This means THREAT_OUTPUT_FILE / AGENT_OUTPUT_FILE contain the [VSO-FILTERED]-replaced text, not the raw output. The downstream grep "THREAT_DETECTION_RESULT:" step (line 525) reads this filtered file — unlikely to matter in practice since agent-emitted JSON shouldn't contain ##vso[, but worth documenting as intentional.

✅ What Looks Good

  • The set -o pipefail + && AGENT_EXIT_CODE=0 || AGENT_EXIT_CODE=$? pattern is correct: with pipefail the pipeline's exit status propagates to $? before the || branch assigns it. The exit $AGENT_EXIT_CODE below correctly honours it.
  • sed -u (unbuffered) is the right choice for line-by-line streaming on GNU systems (ADO Linux agents).
  • Filtering both ##vso[ and ##[ is consistent with neutralize_pipeline_commands() in sanitize.rs.
  • The 2>&1 | sed | tee ordering correctly merges stderr into stdout before filtering so no unfiltered stream reaches the log.

Generated by Rust PR Reviewer for issue #159 · ● 583.7K ·

Replace file-redirect-then-replay pattern with piped streaming:
- AWF output piped through sed -u (unbuffered) for real-time VSO command
  filtering, then through tee for simultaneous display and file capture
- Filters both ##vso[ and ##[ shorthand forms to prevent ADO command
  injection while streaming
- Add dedicated agent_log artifact for easy access to agent output
- Apply same streaming pattern to threat analysis steps in both
  standalone and 1ES templates

Previously all agent output was hidden until the run completed, then
replayed with post-hoc sed sanitization. Now output appears line-by-line
in the ADO pipeline UI as the agent runs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jamesadevine jamesadevine force-pushed the feat/improve-realtime-output branch from 6262e5e to b1a76bf Compare April 13, 2026 11:36
@github-actions
Copy link
Copy Markdown
Contributor

🔍 Rust PR Review

Summary: Looks good — streaming implementation is correct; one PR description inaccuracy and one minor operator observation worth noting.

Findings

✅ What Looks Good

  • Exit code propagation is correct. The set -o pipefail + && AGENT_EXIT_CODE=0 || AGENT_EXIT_CODE=$? pattern is sound. With pipefail, the pipeline exit code reflects AWF's exit code when sed/tee succeed. In the || branch, $? is the exit code of the failed (pipeline && AGENT_EXIT_CODE=0) compound — which equals the pipeline's exit code. Assignment can't fail, so the anti-pattern risk doesn't apply here.

  • ##[ shorthand filter is a genuine security improvement. The previous agent step only filtered ##vso[, leaving ##[warning], ##[error], ##[group] etc. exploitable via shorthand form. The new two-clause sed expression is consistent with neutralize_pipeline_commands() in sanitize.rs (lines 104–109), which also handles both forms.

  • THREAT_DETECTION_RESULT: extraction is not broken. The grep at base.yml:525 searches for the substring anywhere on the line. Even if the line were prefixed with ##[...] and transformed to [VSO-FILTERED] [...] THREAT_DETECTION_RESULT: {...}, the grep still matches. In practice the agent emits this as plain text, so filtering is a no-op for it.

  • Artifact path in ProcessSafeOutputs is correct. AnalyzeSafeOutputs does a full cp -a from agent_outputs_$(Build.BuildId) into analyzed_outputs (line 516), so logs/agent-output.txt is present in analyzed_outputs_$(Build.BuildId). The guarded 2>/dev/null || true handles any absent-file edge cases.

⚠️ Suggestions

  • base.yml:648 comment typo: "optimisation use" — likely meant "optimization" or just "processing". Cosmetic but appears in generated pipelines.

  • PR description inaccuracy: The description states "Added dedicated agent_log_$(Build.BuildId) artifact for easy access" but no such artifact is published in the diff. The change copies agent-output.txt into the existing safe_outputs artifact. The description should be corrected to avoid confusion when bisecting future issues.

  • sed -u is GNU-specific: On the declared Ubuntu 22.04 agents this is fine, but it's worth a comment that -u (unbuffered) is not POSIX. If the agent OS ever changes this would silently regress to batched output. Already have comments explaining the flag, so this is low priority.

Generated by Rust PR Reviewer for issue #159 · ● 595.6K ·

@jamesadevine jamesadevine merged commit 7497cd6 into main Apr 13, 2026
7 checks passed
@jamesadevine jamesadevine deleted the feat/improve-realtime-output branch April 13, 2026 11:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant