Skip to content

Commit 7497cd6

Browse files
feat: enable real-time agent output streaming with VSO filtering (#159)
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>
1 parent f43b22e commit 7497cd6

2 files changed

Lines changed: 25 additions & 20 deletions

File tree

templates/1es-base.yml

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -228,15 +228,16 @@ extends:
228228
displayName: "Start network proxy"
229229
230230
- bash: |
231-
THREAT_OUTPUT_FILE="$(Agent.TempDirectory)/threat-analysis-output.txt"
231+
set -o pipefail
232232
233-
# Use $(cat file) like gh-aw does - the command is executed directly, not via a variable
234-
copilot --prompt "$(cat $(Agent.TempDirectory)/threat-analysis-prompt.md)" {{ copilot_params }} > "$THREAT_OUTPUT_FILE" 2>&1
235-
AGENT_EXIT_CODE=$?
233+
THREAT_OUTPUT_FILE="$(Agent.TempDirectory)/threat-analysis-output.txt"
236234
237-
echo "=== Threat Analysis Output (sanitized) ==="
238-
sed 's/##vso\[/## [SANITIZED] vso[/g' "$THREAT_OUTPUT_FILE"
239-
echo "=== End Threat Analysis Output ==="
235+
# Stream threat analysis output in real-time with VSO command filtering
236+
copilot --prompt "$(cat $(Agent.TempDirectory)/threat-analysis-prompt.md)" {{ copilot_params }} \
237+
2>&1 \
238+
| sed -u 's/##vso\[/[VSO-FILTERED] vso[/g; s/##\[/[VSO-FILTERED] [/g' \
239+
| tee "$THREAT_OUTPUT_FILE" \
240+
&& AGENT_EXIT_CODE=0 || AGENT_EXIT_CODE=$?
240241
241242
exit $AGENT_EXIT_CODE
242243
displayName: "Run threat analysis"

templates/base.yml

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,10 @@ jobs:
303303
# (MCPG and SafeOutputs) via host.docker.internal.
304304
# AWF auto-mounts /tmp:/tmp:rw into the container, so copilot binary,
305305
# agent prompt, and MCP config are placed under /tmp/awf-tools/.
306+
# Stream agent output in real-time while filtering VSO commands.
307+
# sed -u = unbuffered (line-by-line) so output appears immediately.
308+
# tee writes to both stdout (ADO pipeline log) and the artifact file.
309+
# pipefail (set above) ensures AWF's exit code propagates through the pipe.
306310
sudo -E "$(Pipeline.Workspace)/awf/awf" \
307311
--allow-domains {{ allowed_domains }} \
308312
--skip-pull \
@@ -312,14 +316,11 @@ jobs:
312316
--log-level info \
313317
--proxy-logs-dir "$(Agent.TempDirectory)/staging/logs/firewall" \
314318
-- '/tmp/awf-tools/copilot --prompt "$(cat /tmp/awf-tools/agent-prompt.md)" --additional-mcp-config @/tmp/awf-tools/mcp-config.json {{ copilot_params }}' \
315-
> "$AGENT_OUTPUT_FILE" 2>&1 \
319+
2>&1 \
320+
| sed -u 's/##vso\[/[VSO-FILTERED] vso[/g; s/##\[/[VSO-FILTERED] [/g' \
321+
| tee "$AGENT_OUTPUT_FILE" \
316322
&& AGENT_EXIT_CODE=0 || AGENT_EXIT_CODE=$?
317323
318-
# Display sanitized output
319-
echo "=== Agent Output (sanitized) ==="
320-
sed 's/##vso\[/[SANITIZED] vso[/g' "$AGENT_OUTPUT_FILE"
321-
echo "=== End Agent Output ==="
322-
323324
# Print firewall summary if available
324325
if [ -x "$(Pipeline.Workspace)/awf/awf" ]; then
325326
echo "=== Firewall Summary ==="
@@ -485,9 +486,12 @@ jobs:
485486
displayName: "Setup agentic pipeline compiler"
486487
487488
- bash: |
489+
set -o pipefail
490+
488491
# Run threat analysis with AWF network isolation
489492
THREAT_OUTPUT_FILE="$(Agent.TempDirectory)/threat-analysis-output.txt"
490493
494+
# Stream threat analysis output in real-time with VSO command filtering
491495
sudo -E "$(Pipeline.Workspace)/awf/awf" \
492496
--allow-domains {{ allowed_domains }} \
493497
--skip-pull \
@@ -496,13 +500,10 @@ jobs:
496500
--log-level info \
497501
--proxy-logs-dir "$(Agent.TempDirectory)/threat-analysis-logs/firewall" \
498502
-- '/tmp/awf-tools/copilot --prompt "$(cat /tmp/awf-tools/threat-analysis-prompt.md)" {{ copilot_params }}' \
499-
> "$THREAT_OUTPUT_FILE" 2>&1
500-
AGENT_EXIT_CODE=$?
501-
502-
# Display sanitized output
503-
echo "=== Threat Analysis Output (sanitized) ==="
504-
sed 's/##vso\[/## [SANITIZED] vso[/g' "$THREAT_OUTPUT_FILE"
505-
echo "=== End Threat Analysis Output ==="
503+
2>&1 \
504+
| sed -u 's/##vso\[/[VSO-FILTERED] vso[/g; s/##\[/[VSO-FILTERED] [/g' \
505+
| tee "$THREAT_OUTPUT_FILE" \
506+
&& AGENT_EXIT_CODE=0 || AGENT_EXIT_CODE=$?
506507
507508
exit $AGENT_EXIT_CODE
508509
displayName: "Run threat analysis (AWF network isolated)"
@@ -647,6 +648,9 @@ jobs:
647648
- bash: |
648649
# Copy all logs to output directory for artifact upload
649650
mkdir -p "$(Agent.TempDirectory)/staging/logs"
651+
# Copy agent output log from analyzed_outputs for optimisation use
652+
cp "$(Pipeline.Workspace)/analyzed_outputs_$(Build.BuildId)/logs/agent-output.txt" \
653+
"$(Agent.TempDirectory)/staging/logs/agent-output.txt" 2>/dev/null || true
650654
if [ -d ~/.copilot/logs ]; then
651655
mkdir -p "$(Agent.TempDirectory)/staging/logs/copilot"
652656
cp -r ~/.copilot/logs/* "$(Agent.TempDirectory)/staging/logs/copilot/" 2>/dev/null || true

0 commit comments

Comments
 (0)