Skip to content

Commit a88b1be

Browse files
jamesadevineCopilot
andcommitted
feat(compile): runtime prompt loading via {{#runtime-import}} markers
Adopts gh-aw's {{#runtime-import path}} marker model and the inlined-imports front-matter toggle. Agent prompt bodies (and the Stage-2 threat-analysis prompt) are now loaded at pipeline runtime by default; body edits no longer require ado-aw compile. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f63a1a6 commit a88b1be

47 files changed

Lines changed: 3020 additions & 945 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ado-script.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,13 @@ jobs:
6969
working-directory: scripts/ado-script
7070
run: npm run typecheck
7171

72-
- name: Build bundle (gate.js)
72+
- name: Build bundles
7373
working-directory: scripts/ado-script
7474
run: npm run build
7575

76-
- name: Smoke-test bundle
76+
- name: Smoke-test bundles
7777
working-directory: scripts/ado-script
78-
run: npx vitest run -c vitest.config.smoke.ts
78+
run: npm run test:smoke
7979

8080
- name: E2E gate test
8181
run: cargo test --test gate_e2e -- --ignored --nocapture

AGENTS.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ Every compiled pipeline runs as three sequential jobs:
6161
│ │ │ ├── mod.rs # Trait, Extension enum, collect_extensions(), re-exports
6262
│ │ │ ├── github.rs # Always-on GitHub MCP extension
6363
│ │ │ ├── safe_outputs.rs # Always-on SafeOutputs MCP extension
64-
│ │ │ ├── trigger_filters.rs # Trigger filter extension (gate evaluator delivery)
64+
│ │ │ ├── ado_script.rs # Always-on ado-script extension (gate evaluator + runtime-import resolver, per-job downloads)
6565
│ │ │ └── tests.rs # Extension integration tests
6666
│ │ ├── codemods/ # Front-matter codemods (one file per transformation)
6767
│ │ │ ├── mod.rs # Codemod struct, CODEMODS registry, runner
@@ -156,7 +156,9 @@ Every compiled pipeline runs as three sequential jobs:
156156
│ ├── update-ado-agentic-workflow.md # Guide for modifying an existing agentic pipeline
157157
│ └── debug-ado-agentic-workflow.md # Guide for troubleshooting a failing agentic pipeline
158158
├── scripts/ # Supporting scripts shipped as release artifacts
159-
│ ├── ado-script/ # TypeScript workspace for bundled gate.js (and future bundles)
159+
│ ├── ado-script/ # TypeScript workspace for bundled gate.js, import.js, and future bundles
160+
│ │ └── src/
161+
│ │ └── import/ # Runtime prompt resolver bundle
160162
│ └── gate.js # Bundled gate evaluator (built from scripts/ado-script/, see docs/ado-script.md)
161163
├── tests/ # Integration tests and fixtures
162164
├── docs/ # Per-concept reference documentation (see index below)
@@ -169,7 +171,7 @@ Every compiled pipeline runs as three sequential jobs:
169171
- **Language**: Rust (2024 edition) - Note: Rust 2024 edition exists and is the edition used by this project
170172
- **CLI Framework**: clap v4 with derive macros
171173
- **Error Handling**: anyhow for ergonomic error propagation
172-
- **Bundled scripts**: TypeScript + ncc (`scripts/ado-script/`) — compiled gate evaluator and future internal helpers; see [`docs/ado-script.md`](docs/ado-script.md).
174+
- **Bundled scripts**: TypeScript + ncc (`scripts/ado-script/`) — compiled gate evaluator, runtime import resolver, and future internal helpers; see [`docs/ado-script.md`](docs/ado-script.md).
173175
- **Async Runtime**: tokio with full features
174176
- **YAML Parsing**: serde_yaml
175177
- **MCP Server**: rmcp with server and transport-io features
@@ -193,6 +195,8 @@ index to jump to the right page.
193195

194196
- [`docs/front-matter.md`](docs/front-matter.md) — full agent file format
195197
(markdown body + YAML front matter grammar) with every supported field.
198+
- [`docs/runtime-imports.md`](docs/runtime-imports.md) — runtime prompt import
199+
markers, path resolution, and `inlined-imports:` behavior.
196200
- [`docs/schedule-syntax.md`](docs/schedule-syntax.md) — fuzzy schedule time
197201
syntax (`daily around 14:00`, `weekly on monday`, timezones, scattering).
198202
- [`docs/engine.md`](docs/engine.md)`engine:` configuration (model,
@@ -239,7 +243,7 @@ index to jump to the right page.
239243
adding codemods.
240244
- [`docs/ado-script.md`](docs/ado-script.md)`ado-script` workspace
241245
(`scripts/ado-script/`): the bundled TypeScript runtime helpers (today:
242-
`gate.js`), schemars-driven type codegen, and the A2 design decision.
246+
`gate.js` and `import.js`), schemars-driven type codegen, and the A2 design decision.
243247
- [`docs/local-development.md`](docs/local-development.md) — local development
244248
setup notes.
245249

docs/ado-script.md

Lines changed: 89 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
`ado-script` is the umbrella name for the TypeScript workspace at
44
[`scripts/ado-script/`](../scripts/ado-script/). It produces small,
55
ncc-bundled Node programs that the **compiler injects into every emitted
6-
pipeline** as runtime helpers. The first (and currently only) bundle is
7-
`gate.js`, the trigger-filter gate evaluator.
6+
pipeline** as runtime helpers. Today it produces `gate.js`, the
7+
trigger-filter gate evaluator, and `import.js`, the runtime prompt
8+
resolver described in [`runtime-imports.md`](runtime-imports.md).
89

910
> **Internal-only.** `ado-script` is not a user-facing front-matter
1011
> feature. Authors never write an `ado-script:` block in their agent
@@ -33,6 +34,33 @@ discriminated union. There is no `eval`, no `Function`, no `vm` — a
3334
compromised compiler cannot use the spec to run arbitrary code on the
3435
pipeline runner.
3536

37+
## What `import.js` does
38+
39+
`import.js` is a single-shot Node program. It reads the prompt file path
40+
from `argv[2]` and resolves `{{#runtime-import path}}` markers in place.
41+
The compiler runs it as a post-prepare-prompt step when
42+
[`inlined-imports: false`](front-matter.md#inlined-imports). See
43+
[`runtime-imports.md`](runtime-imports.md) for the author-facing marker
44+
syntax.
45+
46+
### Env-var contract
47+
48+
| Env var | Required | Purpose |
49+
|---|---|---|
50+
| `ADO_AW_IMPORT_BASE` | No | Base directory for relative imports. Defaults to `dirname(argv[2])`. |
51+
52+
The bundle lives at `dist/import/index.js` and ships in the same
53+
`ado-script.zip` release asset as `gate.js`, so pipelines download it
54+
through the same Setup-job asset flow. `import.js` uses only the Node
55+
standard library, so the ncc bundle is small (~1.5 KB) and carries no
56+
SDK dependency.
57+
58+
The Stage-2 threat-analysis prompt is **not** runtime-imported.
59+
`src/data/threat-analysis.md` is `include_str!`'d into the `ado-aw`
60+
binary and inlined into the emitted YAML at compile time, matching
61+
gh-aw's pattern (their `threat_detection.md` ships with the setup
62+
action and is read directly from disk — no marker, no resolver).
63+
3664
## End-to-end data flow
3765

3866
```
@@ -147,14 +175,19 @@ scripts/ado-script/
147175
│ │ ├── env-facts.ts # Pipeline-variable readers + ENV_BY_FACT + BRANCH_FACTS + ref-prefix stripping
148176
│ │ ├── policy.ts # PolicyTracker state machine
149177
│ │ └── vso-logger.ts # ##vso[…] emitters with property/message escaping; complete() is idempotent
150-
│ └── gate/ # gate.js entry point + per-concern modules
151-
│ ├── index.ts # main(): decode → preflight → bypass → facts → eval → emit
152-
│ ├── bypass.ts # build-reason auto-pass
153-
│ ├── facts.ts # fact acquisition (env + REST)
154-
│ ├── predicates.ts # 11 predicate evaluators + validatePredicateTree + glob ReDoS hardening
155-
│ └── selfcancel.ts # best-effort build cancellation
178+
│ ├── gate/ # gate.js entry point + per-concern modules
179+
│ │ ├── index.ts # main(): decode → preflight → bypass → facts → eval → emit
180+
│ │ ├── bypass.ts # build-reason auto-pass
181+
│ │ ├── facts.ts # fact acquisition (env + REST)
182+
│ │ ├── predicates.ts # 11 predicate evaluators + validatePredicateTree + glob ReDoS hardening
183+
│ │ └── selfcancel.ts # best-effort build cancellation
184+
│ └── import/ # import.js entry point + runtime prompt resolver
185+
│ ├── index.ts # main(): expand runtime-import markers in place
186+
│ └── __tests__/ # marker, path-resolution, and single-pass coverage
156187
├── test/ # End-to-end smoke tests
157-
└── dist/gate/index.js # ncc bundle output (gitignored)
188+
└── dist/ # ncc bundle output (gitignored)
189+
├── gate/index.js
190+
└── import/index.js
158191
```
159192

160193
The release workflow (`.github/workflows/release.yml`) runs
@@ -195,11 +228,18 @@ The Rust subcommand that emits the schema is intentionally hidden:
195228
cargo run -- export-gate-schema --output schema/gate-spec.schema.json
196229
```
197230

198-
## How the gate bundle is wired into emitted pipelines
231+
## How the bundles are wired into emitted pipelines
232+
233+
`AdoScriptExtension`
234+
(`src/compile/extensions/ado_script.rs`) is the always-on single
235+
extension that owns all `ado-script` wiring. It has two independent
236+
features, each emitted **into the job that actually consumes the
237+
bundle**:
199238

200-
`TriggerFiltersExtension`
201-
(`src/compile/extensions/trigger_filters.rs`) injects three Setup-job
202-
steps when any `filters:` block is active:
239+
### Setup job (gate evaluator)
240+
241+
When `filters:` lowers to non-empty checks, `setup_steps()` returns
242+
three step strings into the Setup job:
203243

204244
1. **`NodeTool@0`** — installs Node 20.x LTS, capped at
205245
`timeoutInMinutes: 5`.
@@ -209,9 +249,42 @@ steps when any `filters:` block is active:
209249
`unzip -o /tmp/ado-aw-scripts/ado-script.zip -d /tmp/ado-aw-scripts/`.
210250
Also capped at `timeoutInMinutes: 5`.
211251
3. **`bash: node '/tmp/ado-aw-scripts/ado-script/dist/gate/index.js'`**
212-
runs the gate with `GATE_SPEC` and the env-var contract above.
213-
214-
The IR-to-bash codegen that produces these steps is
252+
runs the gate with `GATE_SPEC` and the env-var contract documented
253+
above.
254+
255+
### Agent job (runtime-import resolver)
256+
257+
When `inlined-imports: false` (the default), `prepare_steps()` returns
258+
the same install + download pair plus the resolver invocation, into
259+
the Agent job's existing `{{ prepare_steps }}` block:
260+
261+
1. **`NodeTool@0`** — same shape as above.
262+
2. **`curl` download + verify + extract** — same artefact, same
263+
verification.
264+
3. **`bash: node '/tmp/ado-aw-scripts/ado-script/dist/import/index.js'`**
265+
expands `{{#runtime-import …}}` markers in
266+
`/tmp/awf-tools/agent-prompt.md` in place. See
267+
[`runtime-imports.md`](runtime-imports.md) for marker syntax.
268+
269+
### Per-job download (NOT a duplication bug)
270+
271+
ADO jobs use **isolated VMs**`/tmp` is not shared between jobs.
272+
The `ado-script.zip` bundle therefore has to be downloaded once per
273+
job that consumes it. When both features are active (a pipeline with
274+
both `filters:` and `inlined-imports: false`), install + download
275+
steps appear in **both** Setup and Agent. That's correct architecture
276+
given ADO's topology, not waste.
277+
278+
### What gets emitted, by case
279+
280+
| `filters:` | `inlined-imports` | Setup-job steps | Agent-job extra steps |
281+
|---|---|---|---|
282+
| inactive | `true` | (none) | (none) |
283+
| inactive | `false` | (no Setup job) | install + download + resolver |
284+
| active | `true` | install + download + gate | (none) |
285+
| active | `false` | install + download + gate | install + download + resolver |
286+
287+
The IR-to-bash codegen that produces the gate step is
215288
`compile_gate_step_external` in `src/compile/filter_ir.rs`.
216289

217290
## Modifying `ado-script`

docs/filter-ir.md

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -350,24 +350,30 @@ The bash shim exports only the ADO macros needed by the spec's facts:
350350

351351
## Integration Points
352352

353-
### TriggerFiltersExtension
353+
### AdoScriptExtension
354354

355-
When Tier 2/3 filters are configured, the `TriggerFiltersExtension`
356-
(`src/compile/extensions/trigger_filters.rs`) activates via
357-
`collect_extensions()`. It implements `CompilerExtension` and controls:
355+
When `filters:` is configured (and lowers to non-empty checks), the
356+
always-on `AdoScriptExtension`
357+
(`src/compile/extensions/ado_script.rs`) emits the gate-side steps via
358+
the `setup_steps()` trait hook. The extension also owns the unrelated
359+
runtime-import resolver — see [`runtime-imports.md`](runtime-imports.md).
360+
361+
For the gate path it controls:
358362

359363
1. **Node install step** — emits a `NodeTool@0` step pinned to Node 20.x
360-
LTS so `gate.js` has a runtime
364+
LTS so `gate.js` has a runtime.
361365
2. **Download step** — fetches `ado-script.zip` from the ado-aw release
362366
artifacts, verifies its SHA256 checksum via `checksums.txt`, then
363-
extracts `gate.js` to `/tmp/ado-aw-scripts/ado-script/dist/gate/index.js`
367+
extracts `gate.js` to `/tmp/ado-aw-scripts/ado-script/dist/gate/index.js`.
364368
3. **Gate step** — calls `compile_gate_step_external()` to generate a step
365-
that runs `node /tmp/ado-aw-scripts/ado-script/dist/gate/index.js` (no inline heredoc)
369+
that runs `node /tmp/ado-aw-scripts/ado-script/dist/gate/index.js` (no inline heredoc).
366370
4. **Validation** — runs `validate_pr_filters()` / `validate_pipeline_filters()`
367-
during compilation via the `validate()` trait method
371+
during compilation via the `validate()` trait method.
368372

369-
The extension uses the `setup_steps()` trait method (not `prepare_steps()`)
370-
because the gate must run in the **Setup job** (before the Agent job).
373+
The gate-side steps use `setup_steps()` (not `prepare_steps()`)
374+
because the gate must run in the **Setup job**, before the Agent job.
375+
Runtime-import resolver steps for the agent body use `prepare_steps()` and
376+
land in the Agent job — see [`runtime-imports.md`](runtime-imports.md).
371377

372378
### Tier 1 Inline Path
373379

@@ -379,7 +385,7 @@ no Node evaluator and no download step.
379385
### Gate Step Injection
380386

381387
Gate steps are injected into the Setup job by `generate_setup_job()` in
382-
`common.rs`. When the `TriggerFiltersExtension` is active, its
388+
`common.rs`. When the `AdoScriptExtension` is active, its
383389
`setup_steps()` are collected and injected first (download + gate). When
384390
only Tier 1 filters are present, the inline gate step is injected directly.
385391

docs/front-matter.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,27 @@ becomes a `repos:` entry, with `checkout: false` added for entries
234234
that weren't listed under `checkout:`. Mixing the legacy fields with
235235
an existing `repos:` block is rejected; pick one shape.
236236

237+
## Inlined Imports
238+
239+
The `inlined-imports:` field controls when `{{#runtime-import ...}}`
240+
markers in the markdown body are resolved. It defaults to `false`.
241+
See [`runtime-imports.md`](runtime-imports.md) for the full marker
242+
syntax, path resolution rules, and runtime behavior.
243+
244+
When `inlined-imports: false`, the compiler leaves runtime-import
245+
markers to be resolved on the pipeline runner. This is the default
246+
behavior, and it means prompt-body edits do not require recompiling the
247+
generated YAML.
248+
249+
When `inlined-imports: true`, the compiler resolves all runtime-import
250+
markers at compile time, including the implicit top-level marker that
251+
normally reloads the body itself. The emitted YAML contains the fully
252+
expanded prompt body, so the pipeline file is self-contained.
253+
254+
The trade-off is that the generated YAML is larger, and prompt-body
255+
edits require `ado-aw compile` plus committing the updated pipeline
256+
file.
257+
237258
## Filter Validation
238259

239260
The compiler validates filter configurations at compile time and will emit

docs/runtime-imports.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Runtime Imports
2+
3+
_Part of the [ado-aw documentation](../AGENTS.md)._
4+
5+
Runtime imports let agent prompts pull in snippet files with gh-aw-compatible
6+
`{{#runtime-import ...}}` markers. They are available in the markdown body and
7+
are controlled by the [`inlined-imports:` field](front-matter.md#inlined-imports).
8+
The runtime bundle that expands them is documented in [`ado-script.md`](ado-script.md).
9+
10+
## Marker syntax
11+
12+
Use `{{#runtime-import path}}` for a required import. If the target file is
13+
missing, resolution fails.
14+
15+
```markdown
16+
## Repository policy
17+
{{#runtime-import docs/policy.md}}
18+
```
19+
20+
Use `{{#runtime-import? path}}` for an optional import. If the target file is
21+
missing, the marker is replaced with an empty string.
22+
23+
```markdown
24+
## Local notes
25+
{{#runtime-import? docs/local-notes.md}}
26+
```
27+
28+
## Where markers can appear
29+
30+
Authors can place runtime-import markers anywhere in the agent markdown body.
31+
When `inlined-imports: false` (the default), the compiler also injects an
32+
implicit top-level runtime-import marker that reloads the body itself at
33+
pipeline runtime instead of embedding it into the generated YAML. When
34+
`inlined-imports: true`, that implicit body marker is resolved at compile time
35+
along with any author-written markers.
36+
37+
## Path resolution
38+
39+
- **Absolute paths** are used as-is.
40+
- **Relative paths at runtime** are resolved against the `ADO_AW_IMPORT_BASE`
41+
environment variable. For user-facing imports, the compiler sets this to
42+
`{{ trigger_repo_directory }}`.
43+
- **Relative paths at compile time** (`inlined-imports: true`) are resolved
44+
against the source `.md` file's directory.
45+
46+
## Single-pass behavior
47+
48+
Runtime imports are expanded in a single pass. Imported snippets are inserted
49+
verbatim, and any nested `{{#runtime-import ...}}` or
50+
`{{#runtime-import? ...}}` markers inside those snippets are **not** expanded.
51+
This matches gh-aw's runtime-import behavior.
52+
53+
## Resolver ordering
54+
55+
The runtime-import resolver runs first. Any extension supplements that are
56+
appended later with `cat >>` — including SafeOutputs guidance, GitHub MCP
57+
guidance, runtime guidance, and cache-memory guidance — are added after import
58+
resolution and are left untouched.
59+
60+
## Failure modes
61+
62+
| Marker kind | Missing file behavior |
63+
|---|---|
64+
| `{{#runtime-import path}}` | Resolver exits with status 1 and the pipeline fails. |
65+
| `{{#runtime-import? path}}` | Marker is silently replaced with an empty string. |
66+
67+
When `inlined-imports: true`, the same required/optional rules are applied at
68+
compile time instead of on the pipeline runner.
69+
70+
## Implementation notes
71+
72+
- **Runtime**: `dist/import/index.js` is ncc-bundled into `ado-script.zip`.
73+
The always-on `AdoScriptExtension`'s `prepare_steps()` injects three
74+
steps into the Agent job's existing `{{ prepare_steps }}` block:
75+
`NodeTool@0` install, the `ado-script.zip` download/verify/extract,
76+
and the `node import.js` resolver invocation. All three run on the
77+
same VM as the agent — ADO jobs are VM-isolated, so the bundle must
78+
be downloaded inside whichever job consumes it.
79+
- **Compile time**: `resolve_imports_inline()` in
80+
`src/compile/extensions/ado_script.rs` performs the inline expansion
81+
when `inlined-imports: true`.
82+

docs/template-markers.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,8 @@ resources:
385385

386386
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.
387387

388+
When `inlined-imports: false` (the default), the compiler emits a top-level `{{#runtime-import ...}}` marker here so the prompt body is reloaded from the source markdown at pipeline runtime. When `inlined-imports: true`, any `{{#runtime-import ...}}` markers in the markdown body are resolved at compile time and the emitted YAML contains the expanded content directly.
389+
388390
## {{ mcpg_config }}
389391

390392
Should be replaced with the MCP Gateway (MCPG) configuration JSON generated from the `mcp-servers:` front matter. This configuration defines the MCPG server entries and gateway settings.
@@ -487,6 +489,8 @@ Tool names are validated at compile time:
487489

488490
Should be replaced with the embedded threat detection analysis prompt from `src/data/threat-analysis.md`. This prompt template includes markers for `{{ source_path }}`, `{{ agent_name }}`, `{{ agent_description }}`, and `{{ working_directory }}` which are replaced during compilation.
489491

492+
When `inlined-imports: false`, the compiler emits a top-level `{{#runtime-import ...}}` marker pointing at the bundled threat-analysis prompt that ships in `ado-script.zip`; when `inlined-imports: true`, the expanded prompt body is embedded directly into the YAML.
493+
490494
The threat analysis prompt instructs the security analysis agent to check for:
491495
- Prompt injection attempts
492496
- Secret leaks

0 commit comments

Comments
 (0)