Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,18 @@ jobs:
- name: Package ado-script bundle
run: |
set -euo pipefail
cd scripts
zip -r ../ado-script.zip ado-script/dist
# Flatten the ncc per-bundle layout (`dist/<name>/index.js`) into
# top-level `<name>.js` files in the zip. Pipelines extract the
# zip to `/tmp/ado-aw-scripts/` and reference each helper by name
# (e.g. `/tmp/ado-aw-scripts/gate.js`), so internal `dist/<name>/`
# nesting does not need to leak into the runtime path.
staging="$(mktemp -d)"
for bundle in scripts/ado-script/dist/*/index.js; do
name="$(basename "$(dirname "$bundle")")"
cp "$bundle" "$staging/$name.js"
done
(cd "$staging" && zip "$GITHUB_WORKSPACE/ado-script.zip" *.js)
unzip -l ado-script.zip

- name: Upload release assets
env:
Expand Down
11 changes: 8 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ Every compiled pipeline runs as three sequential jobs:
│ └── debug-ado-agentic-workflow.md # Guide for troubleshooting a failing agentic pipeline
├── scripts/ # Supporting scripts shipped as release artifacts
│ ├── ado-script/ # TypeScript workspace for bundled gate.js (and future bundles)
│ └── gate.js # Bundled gate evaluator (built from scripts/ado-script/, see docs/ado-script.md)
│ └── gate.js # Bundled gate evaluator (Setup-job step; see docs/ado-script.md)
├── tests/ # Integration tests and fixtures
├── docs/ # Per-concept reference documentation (see index below)
├── Cargo.toml # Rust dependencies
Expand Down Expand Up @@ -237,8 +237,13 @@ index to jump to the right page.
rewrite on breaking-change updates, contributor workflow for
adding codemods.
- [`docs/ado-script.md`](docs/ado-script.md) — `ado-script` workspace
(`scripts/ado-script/`): the bundled TypeScript runtime helpers (today:
`gate.js`), schemars-driven type codegen, and the A2 design decision.
(`scripts/ado-script/`): the bundled TypeScript runtime helpers
(`gate.js` for trigger gates), schemars-driven type codegen, and the
A2 design decision. Note: agent-prompt rendering is intentionally
*not* an `ado-script` bundle — it is an inline bash + awk step, see
[`docs/template-markers.md`](docs/template-markers.md) and
[`docs/front-matter.md`](docs/front-matter.md) for the
`inlined-imports` knob.
- [`docs/local-development.md`](docs/local-development.md) — local development
setup notes.

Expand Down
30 changes: 21 additions & 9 deletions docs/ado-script.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ pipeline** as runtime helpers. The first (and currently only) bundle is
> and how to wire it. See [`docs/tools.md`](tools.md) for what *is*
> user-facing.

The runtime **agent-prompt rendering** path (the default for new
agents, opt-out via `inlined-imports: true`) is *not* an `ado-script`
bundle — it is an inline `bash` + `awk` step in the compiled YAML, kept
deliberately small and transparent. See
[`docs/template-markers.md`](template-markers.md) for the
`{{ prepare_agent_prompt }}` marker and
[`docs/front-matter.md`](front-matter.md) for `inlined-imports`.

## What `gate.js` does

`gate.js` is a single-shot Node program that runs as a step in the
Expand Down Expand Up @@ -158,10 +166,12 @@ scripts/ado-script/
```

The release workflow (`.github/workflows/release.yml`) runs
`npm ci && npm run build`, then zips `scripts/ado-script/dist/` into
the `ado-script.zip` release asset. Pipelines download that asset at
runtime by URL pinned to the compiler's `CARGO_PKG_VERSION`, verify
its SHA-256 against the `checksums.txt` asset, then extract.
`npm ci && npm run build`, then **flattens** each `dist/<bundle>/index.js`
into a top-level `<bundle>.js` inside `ado-script.zip` (today just
`gate.js`). Pipelines download that asset at runtime by URL pinned to
the compiler's `CARGO_PKG_VERSION`, verify its SHA-256 against the
`checksums.txt` asset, then extract directly into `/tmp/ado-aw-scripts/`,
where each bundle is referenced by `/tmp/ado-aw-scripts/<bundle>.js`.

## Schema codegen

Expand Down Expand Up @@ -208,7 +218,7 @@ steps when any `filters:` block is active:
`CARGO_PKG_VERSION`, verifies the zip's SHA-256, then
`unzip -o /tmp/ado-aw-scripts/ado-script.zip -d /tmp/ado-aw-scripts/`.
Also capped at `timeoutInMinutes: 5`.
3. **`bash: node '/tmp/ado-aw-scripts/ado-script/dist/gate/index.js'`** —
3. **`bash: node '/tmp/ado-aw-scripts/gate.js'`** —
runs the gate with `GATE_SPEC` and the env-var contract above.

The IR-to-bash codegen that produces these steps is
Expand Down Expand Up @@ -255,10 +265,12 @@ The IR-to-bash codegen that produces these steps is
3. Add vitest tests under `src/poll/__tests__/`.
4. Wire from a new `CompilerExtension` (or extend an existing one)
that downloads `ado-script.zip` (already a release asset) and
invokes `node /tmp/ado-aw-scripts/ado-script/dist/poll/index.js`
invokes `node /tmp/ado-aw-scripts/poll.js`
as a runtime step.
5. No release-workflow change is needed — `zip -r ado-script/dist`
picks up the new bundle automatically.
5. Extend the release workflow's package step in
`.github/workflows/release.yml` — the flatten loop iterates over
every `dist/*/index.js`, so a new bundle is picked up automatically
as long as its build step writes to `dist/<name>/index.js`.

### Local development loop

Expand All @@ -269,7 +281,7 @@ npm ci # one-time
npm run codegen # regenerate types.gen.ts (compiles ado-aw first)
npm test # vitest unit tests
npm run typecheck # strict tsc --noEmit
npm run build # ncc-bundle to dist/gate/index.js
npm run build # ncc-bundle each src/<bundle>/index.ts to dist/<bundle>/index.js
npm run test:smoke # build + smoke test the bundle end-to-end
```

Expand Down
20 changes: 20 additions & 0 deletions docs/extending.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pub trait CompilerExtension {
fn required_awf_mounts(&self) -> Vec<AwfMount>; // AWF Docker volume mounts
fn awf_path_prepends(&self) -> Vec<String>; // Directories to add to chroot PATH
fn agent_env_vars(&self) -> Vec<(String, String)>; // Agent env vars (e.g., PIP_INDEX_URL)
fn needs_scripts_bundle(&self) -> bool; // Declare dependency on ado-script.zip
}
```

Expand All @@ -62,6 +63,25 @@ gates or checks that must complete before the agent is launched.
This guarantees runtime install steps run before tool steps that may depend
on them.

**`prompt_supplement()` delivery**: By default
(`inlined-imports: false`), supplement strings are embedded as
labelled `cat << '__ADO_AW_SUPP_<NAME>_EOF__' ... EOF` heredocs
inside the single `Render agent prompt` bash step emitted by
[`generate_prepare_agent_prompt`](../src/compile/common.rs). The
supplement text is therefore directly visible in the lock yaml and
flows through the same single-pass `awk` substitution as the body,
so an extension can parameterize its supplement using
`${{ parameters.* }}` / `$(VAR)` the same way an author parameterizes
the body. Only when `inlined-imports: true` does the compiler wrap
each supplement in a separate `cat >>` step via `wrap_prompt_append`.

**`needs_scripts_bundle()`**: Return `true` if the extension's emitted
steps invoke a bundled `ado-script.zip` helper (today: `gate.js`).
The compiler then emits the shared NodeTool@0 + checksum-verified
download pair **once** in the consuming job; the extension itself
emits only its `node /tmp/ado-aw-scripts/<bundle>.js` invocation.
See `TriggerFiltersExtension` for the reference implementation.

To add a new runtime or tool: (1) create a directory under `src/tools/` or `src/runtimes/`, (2) implement `CompilerExtension` in `extension.rs`, (3) add a variant to the `Extension` enum and a collection check in `collect_extensions()` in `src/compile/extensions/mod.rs`.

### Filter IR (`src/compile/filter_ir.rs`)
Expand Down
13 changes: 7 additions & 6 deletions docs/filter-ir.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ gate spec.
export ADO_SYSTEM_ACCESS_TOKEN="$SYSTEM_ACCESSTOKEN"

# 4. Run the bundled Node evaluator (downloaded by the Setup job)
node '/tmp/ado-aw-scripts/ado-script/dist/gate/index.js'
node '/tmp/ado-aw-scripts/gate.js'
name: prGate
displayName: "Evaluate PR filters"
env:
Expand Down Expand Up @@ -360,9 +360,9 @@ When Tier 2/3 filters are configured, the `TriggerFiltersExtension`
LTS so `gate.js` has a runtime
2. **Download step** — fetches `ado-script.zip` from the ado-aw release
artifacts, verifies its SHA256 checksum via `checksums.txt`, then
extracts `gate.js` to `/tmp/ado-aw-scripts/ado-script/dist/gate/index.js`
extracts `gate.js` to `/tmp/ado-aw-scripts/gate.js`
3. **Gate step** — calls `compile_gate_step_external()` to generate a step
that runs `node /tmp/ado-aw-scripts/ado-script/dist/gate/index.js` (no inline heredoc)
that runs `node /tmp/ado-aw-scripts/gate.js` (no inline heredoc)
4. **Validation** — runs `validate_pr_filters()` / `validate_pipeline_filters()`
during compilation via the `validate()` trait method

Expand Down Expand Up @@ -419,9 +419,10 @@ the ado-aw version:
A `checksums.txt` file is also published at the same URL base and used to
verify the SHA256 integrity of `ado-script.zip` before extraction.

The Setup-job download step pulls the zip, extracts `ado-script/dist/gate/index.js`,
and discards the rest. New per-use-site bundles follow the same pattern
(per-bundle ncc entry + per-bundle download step).
The Setup-job download step pulls the zip and extracts top-level
`gate.js` (and any sibling helpers like `prompt.js`) into
`/tmp/ado-aw-scripts/`. New per-use-site bundles follow the same pattern
(per-bundle ncc entry + the shared scripts-download step).

## Adding New Filter Types

Expand Down
33 changes: 33 additions & 0 deletions docs/front-matter.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ parameters: # optional ADO runtime parameters (surfaced in UI
displayName: "Clear agent memory"
type: boolean
default: false
inlined-imports: false # optional opt-out from runtime prompt injection (default: false)
---


Expand All @@ -154,6 +155,38 @@ parameters: # optional ADO runtime parameters (surfaced in UI
Build the project and run all tests...
```

## Prompt rendering: `inlined-imports`

By default (`inlined-imports: false`, or unset), the agent body is
**not embedded** in the compiled YAML. Instead, the compiled pipeline
emits a small `bash` + `awk` step that, at pipeline runtime, `cat`s
the agent `.md` from the workspace, appends any extension supplements
(emitted as labelled heredocs in the same step — visible directly in
the lock yaml), strips the front matter, runs a single-pass `awk`
substitution for `${{ parameters.* }}` and `$(VAR)` patterns, and
writes the rendered prompt for the AWF sandbox. The entire pipeline
is transparent in the compiled YAML and requires no Node bundle.

Body-only edits to the `.md` therefore no longer require an
`ado-aw compile` rebuild — only front-matter changes do.

Set `inlined-imports: true` in front matter to opt out and restore the
legacy compile-time behaviour: the body is embedded verbatim in a
heredoc step at compile time, and extension supplements are emitted as
per-extension `cat >>` steps. Use this when:

- The agent `.md` source path will not be resolvable inside
`$(Build.SourcesDirectory)` at runtime (e.g., compile happens
outside the trigger repo).
- The Agent pool cannot reach `github.com` for the release-asset
download.
- You need a fully self-contained compiled YAML for offline review or
archival.

The field name matches gh-aw's equivalent knob exactly so authors
familiar with that ecosystem can reuse the same mental model.


## Workspace Defaults

The `workspace:` field controls which directory the agent runs in. When it is
Expand Down
64 changes: 61 additions & 3 deletions docs/template-markers.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,9 +381,67 @@ resources:
- release/*
```

## {{ agent_content }}

Should be replaced with the markdown body (agent instructions) extracted from the source markdown file, excluding the YAML front matter. This content provides the agent with its task description and guidelines.
## {{ prepare_agent_prompt }}

Replaces the entire "Prepare agent prompt" step in the Agent job.

The compiler expands this to one of two shapes depending on the
`inlined-imports:` front-matter field:

- **`inlined-imports: false` (default — runtime rendering).** Expands
to a single inline `bash` step (no JS bundle, no Node install) that:
1. Composes the prompt with a brace-grouped command list:
`cat "$(Build.SourcesDirectory)/<path>/agent.md"` for the body
followed by one heredoc per extension supplement
(`cat << '__ADO_AW_SUPP_<NAME>_EOF__' ... __ADO_AW_SUPP_<NAME>_EOF__`).
Every fragment is visible directly in the lock yaml.
2. Strips the agent's YAML front matter with an `awk` block.
3. Runs a single-pass `awk` substitution program over the body that
recognises four token shapes and resolves them in priority
order:

| Token | Resolved via |
|------------------------------|--------------------------------------------------------|
| `\$(VAR)` / `\$(VAR.SUB)` | escape — strip backslash, leave `$(VAR)` literal |
| `${{ parameters.NAME }}` | env `ADO_AW_PARAM_<NAME upper, hyphen→underscore>` |
| `$(VAR)` / `$(VAR.SUB)` | env `<NAME upper, dot→underscore>` (process env) |
| `$[ ... ]` | left verbatim with a one-shot warning |

The walk uses substring slicing rather than `gsub()` so the
replacement text is **never re-scanned**. This blocks the
"queue-with-malicious-parameter-value" chaining attack: a
parameter value that contains `$(...)` stays literal in the
rendered prompt instead of expanding against a secret env var
on a follow-up pass.

Each declared pipeline parameter gets a separate
`ADO_AW_PARAM_<UPPER>: ${{ parameters.<name> }}` env line on
the step, so the value reaches the awk script via `ENVIRON`
without being baked into the compiled YAML.
4. Fails closed on empty output, then writes the rendered prompt
to `/tmp/awf-tools/agent-prompt.md` for the AWF sandbox.

- **`inlined-imports: true` (opt-out).** Expands to the legacy
heredoc step that writes the markdown body verbatim into
`/tmp/awf-tools/agent-prompt.md`. Extension supplements are emitted
as separate `cat >>` steps via `wrap_prompt_append` (handled in
`{{ prepare_steps }}`).

This marker replaces the older `{{ agent_content }}` placeholder. The
compiler resolves `{{ trigger_repo_directory }}` inside the body
path **before** emitting the step, so the `cat "$(Build.SourcesDirectory)/..."`
reference uses a fully resolved path at runtime. If the source path
cannot be expressed relative to the trigger repo (e.g., compile
invoked from outside the repo), the compiler bails with an actionable
error message pointing at `inlined-imports: true` as the escape
hatch.

The runtime branch deliberately ships **no JS bundle**: the compose +
strip + substitute steps are pure bash + awk, both present on every
ADO image, and the entire transformation is human-readable in the
compiled YAML. See [`docs/ado-script.md`](ado-script.md) for the
contrasting `gate.js` design where a Node bundle is justified by the
much larger amount of logic involved.

## {{ mcpg_config }}

Expand Down
2 changes: 1 addition & 1 deletion scripts/ado-script/src/gate/predicates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export function evaluatePredicate(p: PredicateSpec, facts: Map<string, unknown>)
const unknownType = (p as { type?: unknown }).type;
logWarning(
`Unknown predicate type '${String(unknownType)}'; failing closed. ` +
"Update scripts/ado-script/dist/gate/index.js (or the bundled ado-script.zip) to a " +
"Update the bundled ado-script.zip (which provides gate.js) to a " +
"release that supports this predicate.",
);
return false;
Expand Down
2 changes: 1 addition & 1 deletion scripts/ado-script/src/shared/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export type PredicateSpec =

/**
* Serializable gate specification — the JSON document consumed by the
* Node gate evaluator (`scripts/ado-script/dist/gate/index.js`) at pipeline runtime.
* Node gate evaluator (bundled as `gate.js`) at pipeline runtime.
*/
export interface GateSpec {
checks: CheckSpec[];
Expand Down
Loading
Loading