You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(compile): inject conditional Azure CLI advisory into the agent prompt
When the always-on AzureCli extension detects azure-cli on the host
(AW_AZ_MOUNTS non-empty), append an Azure CLI advisory section to
the agent prompt so the agent knows az is on PATH inside the
sandbox, what it's good for, and the auth model. Skip the append
when az is missing so the agent never tries to call az on a runner
that doesn't have it.
Design
======
AzureCliExtension::prepare_steps() now returns TWO YAML steps:
1. Detection (existing) — sets AW_AZ_MOUNTS to the two --mount
args or empty string.
2. NEW: "Append Azure CLI prompt" — a single-quoted heredoc that
cats an Azure CLI advisory into /tmp/awf-tools/agent-prompt.md,
gated by `condition: ne(variables['AW_AZ_MOUNTS'], '')`.
The CompilerExtension trait API is unchanged. wrap_prompt_append
is unchanged. The single call site in common.rs:2311 is unchanged.
prompt_supplement() on AzureCli stays None. The conditional
injection is entirely self-contained inside the extension's own
prepare_steps Vec.
Why not extend the trait. The existing prompt_supplement() hook
doesn't carry a step-level condition. Adding one would require a
new trait method, a new wrap_prompt_append signature, an enum-macro
arm update, and a call-site change in common.rs — disproportionate
for a 15-line advisory that only one extension wants gated.
Advisory content
================
The advisory assumes az IS available (no "may be" hedging — the
step only runs when it is) and covers:
- az devops family — autoauthed via $AZURE_DEVOPS_EXT_PAT when
permissions: read: is declared
- Azure Resource Manager — separate identity required, not
provisioned by ado-aw
- Microsoft Graph — same caveat as ARM
- Fallback — file a missing-tool safe output naming azure-cli
Heredoc terminator is SINGLE-QUOTED ('AZURE_CLI_PROMPT_EOF') so
$AZURE_DEVOPS_EXT_PAT and similar literals are appended verbatim
rather than being shell-expanded to the runner's PAT value. Locked
in by test_azure_cli_prompt_append_uses_single_quoted_heredoc.
Tests
=====
Five new unit tests in src/compile/extensions/azure_cli.rs:
- test_azure_cli_prompt_append_step_is_conditional
- test_azure_cli_prompt_append_step_targets_agent_prompt_file
- test_azure_cli_prompt_append_step_has_advisory_anchors
- test_azure_cli_prompt_append_uses_single_quoted_heredoc
- test_azure_cli_prompt_append_displayname_matches_lint_list
Existing test_azure_cli_prepare_steps_detects_az_before_setting_var
updated for the new vec length (2 instead of 1).
Integration test test_default_pipeline_mounts_az_and_allows_azure_hosts
extended to assert the displayName, the condition expression in the
same step (proximity check), and the advisory anchor strings.
tests/bash_lint_tests.rs REQUIRED_STEP_DISPLAY_NAMES gains "Append
Azure CLI prompt" so shellcheck exercises the new heredoc.
tests/safe-outputs/azure-cli.lock.yml regenerated.
Docs
====
docs/network.md and docs/tools.md "Always-on Azure CLI" sections
gain a paragraph describing the conditional advisory injection. The
same edits also correct a small carryover inaccuracy from commit
7fe562f: the previous text said "leaves AW_AZ_MOUNTS unset" — the
graceful-degradation fix actually sets it to the empty string. Now
documented correctly with the rationale.
Validation
==========
- cargo build: clean
- cargo test: 1753 unit + 119 compiler + all integration pass
- cargo clippy --all-targets --all-features -- -D warnings: clean
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@@ -136,8 +157,50 @@ impl CompilerExtension for AzureCliExtension {
136
157
echo "##vso[task.logissue type=warning]Azure CLI not detected on this runner (missing /usr/bin/az or /opt/az). The az command will not be available inside the agent sandbox. Install azure-cli on the runner image to enable it."
137
158
fi
138
159
displayName: "Detect Azure CLI on host (for AWF mount)"
139
-
"###;
140
-
vec![step.to_string()]
160
+
"###
161
+
.to_string()
162
+
}
163
+
164
+
/// Conditional `cat >>` step that appends an Azure CLI advisory
165
+
/// section to the agent prompt file at pipeline time, only when
166
+
/// the detection step above set `AW_AZ_MOUNTS` to non-empty.
167
+
///
168
+
/// Uses a SINGLE-QUOTED heredoc delimiter (`<< 'AZURE_CLI_PROMPT_EOF'`)
169
+
/// so `$AZURE_DEVOPS_EXT_PAT` and any other dollar references inside
170
+
/// the prompt body are appended literally rather than expanded by
171
+
/// bash. The closing delimiter is indented to match the bash block
172
+
/// scalar style used by `wrap_prompt_append`.
173
+
///
174
+
/// The `condition:` clause uses an ADO runtime expression. ADO
175
+
/// evaluates it at step start against the variables visible at
176
+
/// that moment — the detection step above has already run by
177
+
/// then (steps execute sequentially within a job), so the
178
+
/// expression sees the value just written by `setvariable`.
179
+
///
180
+
/// displayName must stay in sync with the entry in
The Azure CLI is available inside this sandbox at `/usr/bin/az`. Prefer it over hand-rolled curl calls when it covers what you need:
191
+
192
+
- **Azure DevOps management** — `az devops`, `az pipelines`, `az repos`, `az boards`. These are authenticated automatically from `$AZURE_DEVOPS_EXT_PAT` when the pipeline declares `permissions: read:`. List/inspect operations Just Work; write operations honour the PAT's scopes.
193
+
- **Azure Resource Manager** — `az resource`, `az account`, `az group`. These require a separate Azure identity that ado-aw does not provision out of the box; sign in with `az login` using credentials supplied by another mechanism (e.g. a service connection writing them into your sandbox env) before invoking them.
194
+
- **Microsoft Graph** — `az ad`, `az rest`. Same caveat as ARM.
195
+
196
+
If a command you need isn't covered above, file a `missing-tool` safe output naming `azure-cli` so the operator can extend coverage rather than blocking on it silently.
197
+
AZURE_CLI_PROMPT_EOF
198
+
199
+
echo "Azure CLI prompt appended"
200
+
displayName: "Append Azure CLI prompt"
201
+
condition: ne(variables['AW_AZ_MOUNTS'], '')
202
+
"#
203
+
.to_string()
141
204
}
142
205
}
143
206
@@ -189,22 +252,27 @@ mod tests {
189
252
let fm = fm();
190
253
let ctx = CompileContext::for_test(&fm);
191
254
let steps = ext.prepare_steps(&ctx);
255
+
// Two prepare steps: [0] detection (always runs), [1] conditional
256
+
// prompt-append (skipped when AW_AZ_MOUNTS is empty). The
257
+
// detection step MUST stay at index 0 — it is what sets the
258
+
// pipeline variable that the prompt-append step's
259
+
// `condition:` reads.
192
260
assert_eq!(
193
261
steps.len(),
194
-
1,
195
-
"expected exactly one prepare step (the az detection step), got: {steps:?}"
262
+
2,
263
+
"expected two prepare steps (detection, conditional prompt-append), got: {steps:?}"
196
264
);
197
265
let step = &steps[0];
198
266
// Detection must check both the launcher shim and the venv
199
267
// directory — mounting only one would leave az partially
200
268
// available and produce confusing errors inside the sandbox.
201
269
assert!(
202
270
step.contains("[ -f /usr/bin/az ]"),
203
-
"prepare step must test for /usr/bin/az launcher: {step}"
271
+
"first prepare step (detection) must test for /usr/bin/az launcher: {step}"
204
272
);
205
273
assert!(
206
274
step.contains("[ -d /opt/az ]"),
207
-
"prepare step must test for /opt/az venv directory: {step}"
275
+
"first prepare step (detection) must test for /opt/az venv directory: {step}"
208
276
);
209
277
}
210
278
@@ -316,6 +384,108 @@ mod tests {
316
384
);
317
385
}
318
386
387
+
// ── Conditional prompt-append step (step index 1) ──────────────────────
0 commit comments