-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathstage-base.yml
More file actions
667 lines (564 loc) · 30.5 KB
/
stage-base.yml
File metadata and controls
667 lines (564 loc) · 30.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
{{ template_parameters }}
stages:
- stage: {{ stage_prefix }}
displayName: {{ agent_display_name }}
jobs:
{{ setup_job }}
- job: {{ stage_prefix }}_Agent
displayName: "Agent"
{{ agentic_depends_on }}
{{ job_timeout }}
pool:
{{ pool }}
steps:
{{ checkout_self }}
{{ checkout_repositories }}
{{ acquire_ado_token }}
{{ engine_install_steps }}
- bash: |
set -eo pipefail
COMPILER_VERSION="{{ compiler_version }}"
DOWNLOAD_DIR="$(Pipeline.Workspace)/agentic-pipeline-compiler"
DOWNLOAD_URL="https://github.com/githubnext/ado-aw/releases/download/v${COMPILER_VERSION}/ado-aw-linux-x64"
CHECKSUM_URL="https://github.com/githubnext/ado-aw/releases/download/v${COMPILER_VERSION}/checksums.txt"
mkdir -p "$DOWNLOAD_DIR"
echo "Downloading ado-aw v${COMPILER_VERSION} from GitHub Releases..."
curl -fsSL -o "$DOWNLOAD_DIR/ado-aw-linux-x64" "$DOWNLOAD_URL"
curl -fsSL -o "$DOWNLOAD_DIR/checksums.txt" "$CHECKSUM_URL"
echo "Verifying checksum..."
cd "$DOWNLOAD_DIR" || exit 1
grep "ado-aw-linux-x64" checksums.txt | sha256sum -c -
mv ado-aw-linux-x64 ado-aw
chmod +x ado-aw
displayName: "Download agentic pipeline compiler (v{{ compiler_version }})"
{{ integrity_check }}
- bash: |
mkdir -p "$(Agent.TempDirectory)/staging"
# Generate MCPG API key early so it's available as an ADO secret variable
# for both the MCPG config and the agent's mcp-config.json
MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=')
echo "##vso[task.setvariable variable=MCP_GATEWAY_API_KEY;issecret=true]$MCP_GATEWAY_API_KEY"
# Export gateway port and domain as pipeline variables (matching gh-aw pattern).
# These duplicate the compile-time values baked into the YAML, but MCPG's
# Docker container requires MCP_GATEWAY_PORT and MCP_GATEWAY_DOMAIN env vars
# to start — the ADO variable indirection satisfies that contract.
echo "##vso[task.setvariable variable=MCP_GATEWAY_PORT]{{ mcpg_port }}"
echo "##vso[task.setvariable variable=MCP_GATEWAY_DOMAIN]{{ mcpg_domain }}"
# Write MCPG (MCP Gateway) configuration to a file
cat > "$(Agent.TempDirectory)/staging/mcpg-config.json" << 'MCPG_CONFIG_EOF'
{{ mcpg_config }}
MCPG_CONFIG_EOF
echo "MCPG config:"
cat "$(Agent.TempDirectory)/staging/mcpg-config.json"
# Validate JSON
python3 -m json.tool "$(Agent.TempDirectory)/staging/mcpg-config.json" > /dev/null && echo "JSON is valid"
displayName: "Prepare MCPG config"
- bash: |
mkdir -p /tmp/awf-tools/staging
echo "HOME: $HOME"
# Use absolute path since MCP subprocess may not inherit PATH
AGENTIC_PIPELINES_PATH="$(Pipeline.Workspace)/agentic-pipeline-compiler/ado-aw"
# Verify the binary exists and is executable
ls -la "$AGENTIC_PIPELINES_PATH"
chmod +x "$AGENTIC_PIPELINES_PATH"
$AGENTIC_PIPELINES_PATH -h
# Copy compiler binary to /tmp so it's accessible inside AWF container
cp "$AGENTIC_PIPELINES_PATH" /tmp/awf-tools/ado-aw
chmod +x /tmp/awf-tools/ado-aw
# Copy MCPG config to /tmp
cp "$(Agent.TempDirectory)/staging/mcpg-config.json" /tmp/awf-tools/staging/mcpg-config.json
displayName: "Prepare tooling"
- bash: |
# Write agent instructions to /tmp so it's accessible inside AWF container
cat > "/tmp/awf-tools/agent-prompt.md" << 'AGENT_PROMPT_EOF'
{{ agent_content }}
AGENT_PROMPT_EOF
echo "Agent prompt:"
cat "/tmp/awf-tools/agent-prompt.md"
displayName: "Prepare agent prompt"
- task: DockerInstaller@0
displayName: "Install Docker"
inputs:
dockerVersion: 26.1.4
- bash: |
set -eo pipefail
AWF_VERSION="{{ firewall_version }}"
DOWNLOAD_DIR="$(Pipeline.Workspace)/awf"
DOWNLOAD_URL="https://github.com/github/gh-aw-firewall/releases/download/v${AWF_VERSION}/awf-linux-x64"
CHECKSUM_URL="https://github.com/github/gh-aw-firewall/releases/download/v${AWF_VERSION}/checksums.txt"
mkdir -p "$DOWNLOAD_DIR"
echo "Downloading AWF v${AWF_VERSION} from GitHub Releases..."
curl -fsSL -o "$DOWNLOAD_DIR/awf-linux-x64" "$DOWNLOAD_URL"
curl -fsSL -o "$DOWNLOAD_DIR/checksums.txt" "$CHECKSUM_URL"
echo "Verifying checksum..."
cd "$DOWNLOAD_DIR" || exit 1
grep "awf-linux-x64" checksums.txt | sha256sum -c -
mv awf-linux-x64 awf
chmod +x awf
echo "##vso[task.prependpath]$(Pipeline.Workspace)/awf"
./awf --version
displayName: "Download AWF (Agentic Workflow Firewall) v{{ firewall_version }}"
- bash: |
set -eo pipefail
docker pull ghcr.io/github/gh-aw-firewall/squid:{{ firewall_version }}
docker pull ghcr.io/github/gh-aw-firewall/agent:{{ firewall_version }}
docker tag ghcr.io/github/gh-aw-firewall/squid:{{ firewall_version }} ghcr.io/github/gh-aw-firewall/squid:latest
docker tag ghcr.io/github/gh-aw-firewall/agent:{{ firewall_version }} ghcr.io/github/gh-aw-firewall/agent:latest
docker pull {{ mcpg_image }}:v{{ mcpg_version }}
displayName: "Pre-pull AWF and MCPG container images (v{{ firewall_version }})"
{{ prepare_steps }}
{{ awf_path_step }}
# Start SafeOutputs HTTP server on host (MCPG proxies to it)
- bash: |
SAFE_OUTPUTS_PORT=8100
SAFE_OUTPUTS_API_KEY=$(openssl rand -base64 45 | tr -d '/+=')
echo "##vso[task.setvariable variable=SAFE_OUTPUTS_PORT]$SAFE_OUTPUTS_PORT"
echo "##vso[task.setvariable variable=SAFE_OUTPUTS_API_KEY;issecret=true]$SAFE_OUTPUTS_API_KEY"
mkdir -p "$(Agent.TempDirectory)/staging/logs"
# Start SafeOutputs as HTTP server in the background
# NOTE: {{ enabled_tools_args }} expands to either "" or "--enabled-tools X ... "
# (with trailing space). The value MUST be newline-free; is_safe_tool_name enforces this.
# Positional args (output_directory, bounding_directory) MUST come after all named
# options — clap parses them positionally and reordering would break the command.
nohup /tmp/awf-tools/ado-aw mcp-http \
--port "$SAFE_OUTPUTS_PORT" \
--api-key "$SAFE_OUTPUTS_API_KEY" \
{{ enabled_tools_args }}"/tmp/awf-tools/staging" \
"{{ working_directory }}" \
> "$(Agent.TempDirectory)/staging/logs/safeoutputs.log" 2>&1 &
SAFE_OUTPUTS_PID=$!
echo "##vso[task.setvariable variable=SAFE_OUTPUTS_PID]$SAFE_OUTPUTS_PID"
echo "SafeOutputs HTTP server started on port $SAFE_OUTPUTS_PORT (PID: $SAFE_OUTPUTS_PID)"
# Wait for server to be ready
READY=false
# shellcheck disable=SC2034 # i is intentionally unused; wait-N-times loop
for i in $(seq 1 30); do
if curl -sf "http://localhost:$SAFE_OUTPUTS_PORT/health" > /dev/null 2>&1; then
echo "SafeOutputs HTTP server is ready"
READY=true
break
fi
sleep 1
done
if [ "$READY" != "true" ]; then
echo "##vso[task.complete result=Failed]SafeOutputs HTTP server did not become ready within 30s"
exit 1
fi
displayName: "Start SafeOutputs HTTP server"
# Start MCP Gateway (MCPG) on host
- bash: |
# Substitute runtime values into MCPG config
MCPG_CONFIG=$(sed \
-e "s|\${SAFE_OUTPUTS_PORT}|$(SAFE_OUTPUTS_PORT)|g" \
-e "s|\${SAFE_OUTPUTS_API_KEY}|$(SAFE_OUTPUTS_API_KEY)|g" \
-e "s|\${MCP_GATEWAY_API_KEY}|$(MCP_GATEWAY_API_KEY)|g" \
/tmp/awf-tools/staging/mcpg-config.json)
# Log the template config (before API key substitution) for debugging.
echo "Starting MCPG with config template:"
python3 -m json.tool < /tmp/awf-tools/staging/mcpg-config.json
# Remove any leftover container or stale output from a previous interrupted run
# (--rm only cleans up on clean exit; OOM/SIGKILL may leave it behind)
docker rm -f mcpg 2>/dev/null || true
GATEWAY_OUTPUT="/tmp/gh-aw/mcp-config/gateway-output.json"
mkdir -p "$(dirname "$GATEWAY_OUTPUT")" /tmp/gh-aw/mcp-logs
rm -f "$GATEWAY_OUTPUT"
# Start MCPG Docker container on host network.
# The Docker socket mount is required because MCPG spawns stdio-based MCP
# servers as sibling containers. This grants significant host access — acceptable
# here because the pipeline agent is already trusted and network-isolated by AWF.
#
# WORKAROUND: Override entrypoint to bypass run_containerized.sh which has a
# validate_port_mapping() bug — it calls `docker inspect .NetworkSettings.Ports`
# which is empty with --network host (by design), causing a spurious error:
# [ERROR] Port 80 is not exposed from the container
# Upstream fix: https://github.com/github/gh-aw-mcpg/issues/TBD
#
# stdout → gateway-output.json (machine-readable config, read after health check)
echo "$MCPG_CONFIG" | docker run -i --rm \
--name mcpg \
--network host \
--entrypoint /app/awmg \
-v /var/run/docker.sock:/var/run/docker.sock \
-e MCP_GATEWAY_PORT="$(MCP_GATEWAY_PORT)" \
-e MCP_GATEWAY_DOMAIN="$(MCP_GATEWAY_DOMAIN)" \
-e MCP_GATEWAY_API_KEY="$(MCP_GATEWAY_API_KEY)" \
{{ mcpg_debug_flags }}
{{ mcpg_docker_env }}
{{ mcpg_image }}:v{{ mcpg_version }} \
--routed --listen 0.0.0.0:{{ mcpg_port }} --config-stdin --log-dir /tmp/gh-aw/mcp-logs \
> "$GATEWAY_OUTPUT" 2> >(tee /tmp/gh-aw/mcp-logs/stderr.log >&2) &
MCPG_PID=$!
echo "MCPG started (PID: $MCPG_PID)"
# Wait for MCPG to be ready
READY=false
# shellcheck disable=SC2034 # i is intentionally unused; wait-N-times loop
for i in $(seq 1 30); do
if curl -sf "http://localhost:{{ mcpg_port }}/health" > /dev/null 2>&1; then
echo "MCPG is ready"
READY=true
break
fi
sleep 1
done
if [ "$READY" != "true" ]; then
echo "##vso[task.complete result=Failed]MCPG did not become ready within 30s"
exit 1
fi
# Wait for gateway output file to contain valid JSON with mcpServers.
# Health check passing doesn't guarantee stdout is flushed, so poll.
echo "Waiting for gateway output file..."
GATEWAY_READY=false
# shellcheck disable=SC2034 # i is intentionally unused; wait-N-times loop
for i in $(seq 1 15); do
if [ -s "$GATEWAY_OUTPUT" ] && jq -e '.mcpServers' "$GATEWAY_OUTPUT" > /dev/null 2>&1; then
echo "Gateway output is ready"
GATEWAY_READY=true
break
fi
sleep 1
done
if [ "$GATEWAY_READY" != "true" ]; then
echo "##vso[task.complete result=Failed]Gateway output file not ready within 15s"
echo "Gateway output content:"
cat "$GATEWAY_OUTPUT" 2>/dev/null || echo "(empty or missing)"
exit 1
fi
echo "Gateway output:"
cat "$GATEWAY_OUTPUT"
# Convert gateway output to Copilot CLI mcp-config.json.
# Mirrors gh-aw's convert_gateway_config_copilot.cjs:
# - Rewrite URLs from 127.0.0.1 to host.docker.internal (AWF container needs
# host.docker.internal to reach MCPG on the host; 127.0.0.1 is container loopback)
# - Ensure tools: ["*"] on each server entry (Copilot CLI requirement)
# - Preserve all other fields (headers, type, etc.)
jq --arg prefix "http://$(MCP_GATEWAY_DOMAIN):$(MCP_GATEWAY_PORT)" \
'.mcpServers |= (to_entries | sort_by(.key) | map(.value.url |= sub("^http://[^/]+/"; "\($prefix)/") | .value.tools = ["*"]) | from_entries)' \
"$GATEWAY_OUTPUT" > /tmp/awf-tools/mcp-config.json
chmod 600 /tmp/awf-tools/mcp-config.json
echo "Generated MCP config at: /tmp/awf-tools/mcp-config.json"
cat /tmp/awf-tools/mcp-config.json
displayName: "Start MCP Gateway (MCPG)"
{{ mcpg_step_env }}
{{ verify_mcp_backends }}
# Network isolation via AWF (Agentic Workflow Firewall)
- bash: |
set -o pipefail
AGENT_OUTPUT_FILE="$(Agent.TempDirectory)/staging/logs/agent-output.txt"
mkdir -p "$(Agent.TempDirectory)/staging/logs"
echo "=== Running AI agent with AWF network isolation ==="
echo "Allowed domains: {{ allowed_domains }}"
# AWF provides L7 domain whitelisting via Squid proxy + Docker containers.
# --enable-host-access allows the AWF container to reach host services
# (MCPG and SafeOutputs) via host.docker.internal.
# AWF auto-mounts /tmp:/tmp:rw into the container, so copilot binary,
# agent prompt, and MCP config are placed under /tmp/awf-tools/.
# Stream agent output in real-time while filtering VSO commands.
# sed -u = unbuffered (line-by-line) so output appears immediately.
# tee writes to both stdout (ADO pipeline log) and the artifact file.
# pipefail (set above) ensures AWF's exit code propagates through the pipe.
sudo -E "$(Pipeline.Workspace)/awf/awf" \
--allow-domains "{{ allowed_domains }}" \
--skip-pull \
--env-all \
--enable-host-access \
{{ awf_mounts }}
--container-workdir "{{ working_directory }}" \
--log-level info \
--proxy-logs-dir "$(Agent.TempDirectory)/staging/logs/firewall" \
-- '{{ engine_run }}' \
2>&1 \
| sed -u 's/##vso\[/[VSO-FILTERED] vso[/g; s/##\[/[VSO-FILTERED] [/g' \
| tee "$AGENT_OUTPUT_FILE" \
&& AGENT_EXIT_CODE=0 || AGENT_EXIT_CODE=$?
# Print firewall summary if available
if [ -x "$(Pipeline.Workspace)/awf/awf" ]; then
echo "=== Firewall Summary ==="
"$(Pipeline.Workspace)/awf/awf" logs summary --source "$(Agent.TempDirectory)/staging/logs/firewall" 2>/dev/null || true
fi
exit "$AGENT_EXIT_CODE"
displayName: "Run copilot (AWF network isolated)"
workingDirectory: {{ working_directory }}
env:
{{ engine_env }}
- bash: |
# Copy safe outputs from /tmp back to staging for artifact publish
mkdir -p "$(Agent.TempDirectory)/staging"
cp -r /tmp/awf-tools/staging/* "$(Agent.TempDirectory)/staging/" 2>/dev/null || true
echo "Safe outputs copied to $(Agent.TempDirectory)/staging"
ls -la "$(Agent.TempDirectory)/staging" 2>/dev/null || echo "No safe outputs found"
displayName: "Collect safe outputs from AWF container"
condition: always()
- bash: |
# Stop MCPG container
echo "Stopping MCPG..."
docker stop mcpg 2>/dev/null || true
echo "MCPG stopped"
# Stop SafeOutputs HTTP server
if [ -n "$(SAFE_OUTPUTS_PID)" ]; then
echo "Stopping SafeOutputs (PID: $(SAFE_OUTPUTS_PID))..."
kill "$(SAFE_OUTPUTS_PID)" 2>/dev/null || true
echo "SafeOutputs stopped"
fi
displayName: "Stop MCPG and SafeOutputs"
condition: always()
{{ finalize_steps }}
- bash: |
# Copy all logs to output directory for artifact upload
mkdir -p "$(Agent.TempDirectory)/staging/logs"
if [ -d "{{ engine_log_dir }}" ]; then
cp -r "{{ engine_log_dir }}"/* "$(Agent.TempDirectory)/staging/logs/" 2>/dev/null || true
fi
ADO_AW_LOG_DIR="${ADO_AW_LOG_DIR:-$HOME/.ado-aw/logs}"
if [ -d "$ADO_AW_LOG_DIR" ]; then
cp -r "$ADO_AW_LOG_DIR"/* "$(Agent.TempDirectory)/staging/logs/" 2>/dev/null || true
fi
if [ -d /tmp/gh-aw/mcp-logs ]; then
mkdir -p "$(Agent.TempDirectory)/staging/logs/mcpg"
cp -r /tmp/gh-aw/mcp-logs/* "$(Agent.TempDirectory)/staging/logs/mcpg/" 2>/dev/null || true
fi
echo "Logs copied to $(Agent.TempDirectory)/staging/logs"
ls -la "$(Agent.TempDirectory)/staging/logs" 2>/dev/null || echo "No logs found"
displayName: "Copy logs to output directory"
condition: always()
- publish: $(Agent.TempDirectory)/staging
artifact: agent_outputs_$(Build.BuildId)
condition: always()
- job: {{ stage_prefix }}_Detection
displayName: "Detection"
dependsOn: {{ stage_prefix }}_Agent
pool:
{{ pool }}
steps:
{{ checkout_self }}
{{ checkout_repositories }}
- download: current
artifact: agent_outputs_$(Build.BuildId)
{{ engine_install_steps }}
- bash: |
set -eo pipefail
COMPILER_VERSION="{{ compiler_version }}"
DOWNLOAD_DIR="$(Pipeline.Workspace)/agentic-pipeline-compiler"
DOWNLOAD_URL="https://github.com/githubnext/ado-aw/releases/download/v${COMPILER_VERSION}/ado-aw-linux-x64"
CHECKSUM_URL="https://github.com/githubnext/ado-aw/releases/download/v${COMPILER_VERSION}/checksums.txt"
mkdir -p "$DOWNLOAD_DIR"
echo "Downloading ado-aw v${COMPILER_VERSION} from GitHub Releases..."
curl -fsSL -o "$DOWNLOAD_DIR/ado-aw-linux-x64" "$DOWNLOAD_URL"
curl -fsSL -o "$DOWNLOAD_DIR/checksums.txt" "$CHECKSUM_URL"
echo "Verifying checksum..."
cd "$DOWNLOAD_DIR" || exit 1
grep "ado-aw-linux-x64" checksums.txt | sha256sum -c -
mv ado-aw-linux-x64 ado-aw
chmod +x ado-aw
displayName: "Download agentic pipeline compiler (v{{ compiler_version }})"
- task: DockerInstaller@0
displayName: "Install Docker"
inputs:
dockerVersion: 26.1.4
- bash: |
set -eo pipefail
AWF_VERSION="{{ firewall_version }}"
DOWNLOAD_DIR="$(Pipeline.Workspace)/awf"
DOWNLOAD_URL="https://github.com/github/gh-aw-firewall/releases/download/v${AWF_VERSION}/awf-linux-x64"
CHECKSUM_URL="https://github.com/github/gh-aw-firewall/releases/download/v${AWF_VERSION}/checksums.txt"
mkdir -p "$DOWNLOAD_DIR"
echo "Downloading AWF v${AWF_VERSION} from GitHub Releases..."
curl -fsSL -o "$DOWNLOAD_DIR/awf-linux-x64" "$DOWNLOAD_URL"
curl -fsSL -o "$DOWNLOAD_DIR/checksums.txt" "$CHECKSUM_URL"
echo "Verifying checksum..."
cd "$DOWNLOAD_DIR" || exit 1
grep "awf-linux-x64" checksums.txt | sha256sum -c -
mv awf-linux-x64 awf
chmod +x awf
echo "##vso[task.prependpath]$(Pipeline.Workspace)/awf"
./awf --version
displayName: "Download AWF (Agentic Workflow Firewall) v{{ firewall_version }}"
- bash: |
set -eo pipefail
docker pull ghcr.io/github/gh-aw-firewall/squid:{{ firewall_version }}
docker pull ghcr.io/github/gh-aw-firewall/agent:{{ firewall_version }}
docker tag ghcr.io/github/gh-aw-firewall/squid:{{ firewall_version }} ghcr.io/github/gh-aw-firewall/squid:latest
docker tag ghcr.io/github/gh-aw-firewall/agent:{{ firewall_version }} ghcr.io/github/gh-aw-firewall/agent:latest
displayName: "Pre-pull AWF container images (v{{ firewall_version }})"
- bash: |
mkdir -p "{{ working_directory }}/safe_outputs"
cp -a "$(Pipeline.Workspace)/agent_outputs_$(Build.BuildId)/." "{{ working_directory }}/safe_outputs"
displayName: "Prepare safe outputs for analysis"
- bash: |
# Write threat analysis prompt to /tmp (accessible inside AWF container)
cat > "/tmp/awf-tools/threat-analysis-prompt.md" << 'THREAT_ANALYSIS_EOF'
{{ threat_analysis_prompt }}
THREAT_ANALYSIS_EOF
echo "Threat analysis prompt:"
cat "/tmp/awf-tools/threat-analysis-prompt.md"
displayName: "Prepare threat analysis prompt"
- bash: |
AGENTIC_PIPELINES_PATH="$(Pipeline.Workspace)/agentic-pipeline-compiler/ado-aw"
chmod +x "$AGENTIC_PIPELINES_PATH"
displayName: "Setup agentic pipeline compiler"
- bash: |
set -o pipefail
# Run threat analysis with AWF network isolation
THREAT_OUTPUT_FILE="$(Agent.TempDirectory)/threat-analysis-output.txt"
# Stream threat analysis output in real-time with VSO command filtering
sudo -E "$(Pipeline.Workspace)/awf/awf" \
--allow-domains "{{ allowed_domains }}" \
--skip-pull \
--env-all \
--container-workdir "{{ working_directory }}" \
--log-level info \
--proxy-logs-dir "$(Agent.TempDirectory)/threat-analysis-logs/firewall" \
-- '{{ engine_run_detection }}' \
2>&1 \
| sed -u 's/##vso\[/[VSO-FILTERED] vso[/g; s/##\[/[VSO-FILTERED] [/g' \
| tee "$THREAT_OUTPUT_FILE" \
&& AGENT_EXIT_CODE=0 || AGENT_EXIT_CODE=$?
exit "$AGENT_EXIT_CODE"
displayName: "Run threat analysis (AWF network isolated)"
workingDirectory: {{ working_directory }}
env:
GITHUB_TOKEN: $(GITHUB_TOKEN)
GITHUB_READ_ONLY: 1
- bash: |
# Create analyzed outputs directory with original safe outputs and analysis
mkdir -p "$(Agent.TempDirectory)/analyzed_outputs"
# Copy original safe outputs
cp -a "$(Pipeline.Workspace)/agent_outputs_$(Build.BuildId)/." "$(Agent.TempDirectory)/analyzed_outputs/"
# Copy threat analysis output
if [ -f "$(Agent.TempDirectory)/threat-analysis-output.txt" ]; then
cp "$(Agent.TempDirectory)/threat-analysis-output.txt" "$(Agent.TempDirectory)/analyzed_outputs/"
fi
# Extract JSON from THREAT_DETECTION_RESULT line in threat analysis output
if [ -f "$(Agent.TempDirectory)/threat-analysis-output.txt" ]; then
RESULT_LINE=$(grep "THREAT_DETECTION_RESULT:" "$(Agent.TempDirectory)/threat-analysis-output.txt" | tail -1)
if [ -n "$RESULT_LINE" ]; then
# Extract JSON after the prefix
JSON_CONTENT="${RESULT_LINE##*THREAT_DETECTION_RESULT:}"
echo "$JSON_CONTENT" > "$(Agent.TempDirectory)/analyzed_outputs/threat-analysis.json"
echo "Extracted threat analysis JSON:"
cat "$(Agent.TempDirectory)/analyzed_outputs/threat-analysis.json"
else
echo "Warning: No THREAT_DETECTION_RESULT found in threat analysis output"
fi
else
echo "Warning: No threat analysis output file found"
fi
echo "Analyzed outputs directory contents:"
ls -laR "$(Agent.TempDirectory)/analyzed_outputs"
displayName: "Prepare analyzed outputs"
condition: always()
- bash: |
SAFE_TO_PROCESS="false"
JSON_FILE="$(Agent.TempDirectory)/analyzed_outputs/threat-analysis.json"
if [ -f "$JSON_FILE" ]; then
if jq -e . "$JSON_FILE" > /dev/null 2>&1; then
echo "JSON is valid"
# Check if any threat field is true
if jq -e '.prompt_injection or .secret_leak or .malicious_patch' "$JSON_FILE" > /dev/null 2>&1; then
echo "##vso[task.logissue type=warning]Threats detected - safe outputs will NOT be processed"
jq -r '.reasons[]? // empty' "$JSON_FILE" | sed 's/^/ - /'
else
echo "No threats detected - safe outputs will be processed"
SAFE_TO_PROCESS="true"
fi
else
echo "##vso[task.logissue type=warning]Invalid JSON in threat analysis - defaulting to unsafe"
fi
else
echo "##vso[task.logissue type=warning]No threat analysis JSON found - defaulting to unsafe"
fi
echo "##vso[task.setvariable variable=SafeToProcess;isOutput=true]$SAFE_TO_PROCESS"
echo "SafeToProcess set to: $SAFE_TO_PROCESS"
displayName: "Evaluate threat analysis"
name: threatAnalysis
condition: always()
- bash: |
# Copy all logs to analyzed outputs for artifact upload
mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs"
if [ -d "{{ engine_log_dir }}" ]; then
mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs/copilot"
cp -r "{{ engine_log_dir }}"/* "$(Agent.TempDirectory)/analyzed_outputs/logs/copilot/" 2>/dev/null || true
fi
ADO_AW_LOG_DIR="${ADO_AW_LOG_DIR:-$HOME/.ado-aw/logs}"
if [ -d "$ADO_AW_LOG_DIR" ]; then
mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs/ado-aw"
cp -r "$ADO_AW_LOG_DIR"/* "$(Agent.TempDirectory)/analyzed_outputs/logs/ado-aw/" 2>/dev/null || true
fi
echo "Logs copied to $(Agent.TempDirectory)/analyzed_outputs/logs"
ls -laR "$(Agent.TempDirectory)/analyzed_outputs/logs" 2>/dev/null || echo "No logs found"
displayName: "Copy logs to output directory"
condition: always()
- publish: $(Agent.TempDirectory)/analyzed_outputs
artifact: analyzed_outputs_$(Build.BuildId)
condition: always()
- job: {{ stage_prefix }}_SafeOutputs
displayName: "SafeOutputs"
dependsOn:
- {{ stage_prefix }}_Agent
- {{ stage_prefix }}_Detection
condition: and(succeeded(), eq(dependencies.{{ stage_prefix }}_Detection.outputs['threatAnalysis.SafeToProcess'], 'true'))
pool:
{{ pool }}
steps:
{{ checkout_self }}
{{ checkout_repositories }}
{{ acquire_write_token }}
- download: current
artifact: analyzed_outputs_$(Build.BuildId)
- bash: |
set -eo pipefail
COMPILER_VERSION="{{ compiler_version }}"
DOWNLOAD_DIR="$(Pipeline.Workspace)/agentic-pipeline-compiler"
DOWNLOAD_URL="https://github.com/githubnext/ado-aw/releases/download/v${COMPILER_VERSION}/ado-aw-linux-x64"
CHECKSUM_URL="https://github.com/githubnext/ado-aw/releases/download/v${COMPILER_VERSION}/checksums.txt"
mkdir -p "$DOWNLOAD_DIR"
echo "Downloading ado-aw v${COMPILER_VERSION} from GitHub Releases..."
curl -fsSL -o "$DOWNLOAD_DIR/ado-aw-linux-x64" "$DOWNLOAD_URL"
curl -fsSL -o "$DOWNLOAD_DIR/checksums.txt" "$CHECKSUM_URL"
echo "Verifying checksum..."
cd "$DOWNLOAD_DIR" || exit 1
grep "ado-aw-linux-x64" checksums.txt | sha256sum -c -
mv ado-aw-linux-x64 ado-aw
chmod +x ado-aw
displayName: "Download agentic pipeline compiler (v{{ compiler_version }})"
- bash: |
ls -la "$(Pipeline.Workspace)/agentic-pipeline-compiler"
chmod +x "$(Pipeline.Workspace)/agentic-pipeline-compiler/ado-aw"
echo "##vso[task.prependpath]$(Pipeline.Workspace)/agentic-pipeline-compiler"
displayName: Add agentic compiler to path
- bash: |
mkdir -p "$(Agent.TempDirectory)/staging"
displayName: "Prepare output directory"
- bash: |
ado-aw execute --source "{{ source_path }}" --safe-output-dir "$(Pipeline.Workspace)/analyzed_outputs_$(Build.BuildId)" --output-dir "$(Agent.TempDirectory)/staging"
EXIT_CODE=$?
if [ $EXIT_CODE -eq 2 ]; then
echo "##vso[task.complete result=SucceededWithIssues;]Executor completed with warnings"
exit 0
fi
exit $EXIT_CODE
displayName: Execute safe outputs (Stage 3)
workingDirectory: {{ working_directory }}
{{ executor_ado_env }}
- bash: |
# Copy all logs to output directory for artifact upload
mkdir -p "$(Agent.TempDirectory)/staging/logs"
# Copy agent output log from analyzed_outputs for optimisation use
cp "$(Pipeline.Workspace)/analyzed_outputs_$(Build.BuildId)/logs/agent-output.txt" \
"$(Agent.TempDirectory)/staging/logs/agent-output.txt" 2>/dev/null || true
if [ -d "{{ engine_log_dir }}" ]; then
mkdir -p "$(Agent.TempDirectory)/staging/logs/copilot"
cp -r "{{ engine_log_dir }}"/* "$(Agent.TempDirectory)/staging/logs/copilot/" 2>/dev/null || true
fi
ADO_AW_LOG_DIR="${ADO_AW_LOG_DIR:-$HOME/.ado-aw/logs}"
if [ -d "$ADO_AW_LOG_DIR" ]; then
mkdir -p "$(Agent.TempDirectory)/staging/logs/ado-aw"
cp -r "$ADO_AW_LOG_DIR"/* "$(Agent.TempDirectory)/staging/logs/ado-aw/" 2>/dev/null || true
fi
echo "Logs copied to $(Agent.TempDirectory)/staging/logs"
ls -laR "$(Agent.TempDirectory)/staging/logs" 2>/dev/null || echo "No logs found"
displayName: "Copy logs to output directory"
condition: always()
- publish: $(Agent.TempDirectory)/staging
artifact: safe_outputs
condition: always()
{{ teardown_job }}