Skip to content
Open
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
7 changes: 5 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ Every compiled pipeline runs as three sequential jobs:
sandbox with a read-only ADO token. The agent produces *safe-output
proposals* (e.g. "create this PR", "comment on this work item") rather than
acting directly.
2. **Detection (Stage 2)** — a separate agent inspects Stage 1's proposals for
prompt injection, secret leaks, and other threats.
2. **Detection (Stage 2)** — by default, a separate agent inspects Stage 1's
proposals for prompt injection, secret leaks, and other threats. Authors can
configure or explicitly disable AI analysis under
`safe-outputs.threat-detection`; the Detection job remains as the pipeline
boundary even when analysis is disabled.
3. **SafeOutputs (Stage 3)** — a non-agent executor applies approved safe outputs
using a write-capable ADO token that the agent never sees.

Expand Down
2 changes: 1 addition & 1 deletion docs/audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Current top-level keys include the following. Optional sections are omitted from

| Key | Source |
| --- | --- |
| `overview` | ADO build metadata + `aw_info.json` (engine, model, agent name, source, target). |
| `overview` | ADO build metadata + `aw_info.json` (engine, model, optional threat-detection enabled/engine/model, agent name, source, target). |
| `task_domain` | Audit heuristics over the run's prompts and outputs. |
| `behavior_fingerprint` | Higher-level audit heuristics over the run's behavior. |
| `agentic_assessments` | Higher-level audit assessments emitted by the analyzers. |
Expand Down
54 changes: 50 additions & 4 deletions docs/engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,51 @@ The `timeout-minutes` field sets a wall-clock limit (in minutes) for the entire

When omitted, Azure DevOps uses its default job timeout (60 minutes). When set, the compiler emits `timeoutInMinutes: <value>` on the agentic job.

### Detection-specific engine overlay

`safe-outputs.threat-detection.engine` can override the Copilot configuration
used by the separate Detection job:

```yaml
engine:
id: copilot
model: claude-opus-4.7
version: "1.0.70"
args: [--reasoning-effort=high]
env:
SHARED_SETTING: agent

safe-outputs:
create-pull-request: {}
threat-detection:
engine:
model: gpt-5-mini
args: [] # explicitly clear inherited args
env:
SHARED_SETTING: detection
DETECTION_ONLY: "true"
```

The nested block accepts the same Copilot engine fields as top-level `engine:`.
Omitted values inherit from the top-level configuration. Scalar, command,
provider, and auth values supplied in the nested block replace inherited
values. Detection `env` merges by key with nested values winning; `args`
inherits when absent and replaces the complete inherited list when present.
When a Detection engine overlay is present, `--model` and `--api-target` are
rejected inside the effective args list; use the structured fields so metadata
and firewall hosts cannot drift. Set nested `args: []` to clear an inherited
override.

The effective Detection config drives its own binary installation, invocation,
timeout, GitHub App/provider token minting, BYOK environment and credential
isolation, API target, and AWF network hosts. It never mutates the Agent config.
Detection inherits top-level `engine.timeout-minutes`; a nested
`timeout-minutes` overrides it for Detection only.

ado-aw currently supports only engine ID `copilot`; a different nested ID fails
at compile time. See [Threat detection](safe-outputs.md#threat-detection-threat-detection)
for prompt, enable/disable, and custom-step behavior.

### GitHub App-backed Copilot engine auth

By default the Copilot engine authenticates with the `GITHUB_TOKEN` pipeline
Expand Down Expand Up @@ -240,10 +285,11 @@ sidecar active:
placeholder override.

This isolation applies to **both** the Agent stage and the Detection
(threat-analysis) stage: the detection Copilot run inherits the same
`COPILOT_PROVIDER_*` routing and api-proxy credential isolation, so it reaches
the same external provider without exposing the credential (matching gh-aw,
whose detection engine config inherits the main engine's `env`).
(threat-analysis) stage. By default Detection uses the top-level provider
routing. An explicit
`safe-outputs.threat-detection.engine.provider` can select a different provider;
its credential exclusion and required firewall host are resolved independently
from the Agent.

#### Network allowlist

Expand Down
20 changes: 16 additions & 4 deletions docs/front-matter.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,16 @@ safe-outputs: # optional per-tool configuration for safe output
artifact-link: # optional: link work item to repository branch
enabled: true
branch: main
threat-detection: # section-level Detection configuration
enabled: true # boolean only; false keeps a pass-through Detection job
prompt: | # appended to the fixed detector prompt
Focus on unsafe deserialization and authentication bypasses.
engine: # optional overlay on top-level engine:
model: gpt-5-mini # currently only the copilot engine ID is supported
steps: # trusted ADO steps before AI analysis
- bash: echo "Prepare detector"
post-steps: # trusted ADO steps after AI analysis
- bash: echo "Run additional scanner"
on: # trigger configuration (unified under on: key)
schedule: daily around 14:00 # fuzzy schedule - see docs/schedule-syntax.md
pipeline:
Expand Down Expand Up @@ -247,11 +257,13 @@ runtime — write it as clear, structured natural-language instructions.
> report on pipeline failures and surfaces diagnostic signals. See
> [`docs/conclusion.md`](conclusion.md).

## Inline step validation (`setup` / `steps` / `post-steps` / `teardown`)
## Inline step validation

Inline steps are authored as raw Azure DevOps YAML and are emitted into the
generated pipeline **verbatim** (a passthrough). For steps that invoke a
built-in ADO task the compiler also knows (e.g. `CopyFiles@2`, `Docker@2`,
Inline steps under `setup`, `steps`, `post-steps`, `teardown`,
`safe-outputs.threat-detection.steps`, and
`safe-outputs.threat-detection.post-steps` are authored as raw Azure DevOps YAML
and emitted into the generated pipeline **verbatim** (a passthrough). For steps
that invoke a built-in ADO task the compiler also knows (e.g. `CopyFiles@2`, `Docker@2`,
`DotNetCoreCLI@2`, and most other first-party tasks), `ado-aw lint` performs an
**advisory** validation of the `inputs:` mapping against the task's typed
schema — checking for missing required inputs, unknown input keys, bad
Expand Down
77 changes: 75 additions & 2 deletions docs/safe-outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,79 @@ safe-outputs:

Safe output configurations are passed to Stage 3 execution and used when processing safe outputs.

### Threat detection (`threat-detection`)

Threat Detection runs between the Agent and SafeOutputs jobs. Configuration
lives under `safe-outputs:` because it controls the gate protecting safe-output
execution, matching gh-aw.

```yaml
safe-outputs:
create-pull-request: {}
threat-detection:
enabled: true
prompt: |
Also check for:
- authentication bypasses
- unsafe deserialization
- hardcoded credentials
engine:
id: copilot
model: gpt-5-mini
args: [--reasoning-effort=high]
env:
DETECTION_MODE: strict
steps:
- bash: ./scripts/prepare-security-scanner.sh
displayName: Prepare security scanner
post-steps:
- bash: ./scripts/run-security-scanner.sh
displayName: Run security scanner
```

| Field | Type | Default | Description |
|---|---|---|---|
| `enabled` | boolean | `true` | Run AI threat analysis. Runtime expressions are not supported. |
| `prompt` | string | *(none)* | Literal additional instructions appended under `## Additional Instructions`. ADO expressions (`$(...)`, `${{ ... }}`, `$[...]`) are rejected. The fixed detector prompt and `THREAT_DETECTION_RESULT:` contract are never replaced. |
| `engine` | string/object | top-level `engine` | Detection-specific Copilot configuration overlay. Non-Copilot IDs remain unsupported. |
| `steps` | ADO step list | `[]` | Trusted host-side steps after artifact/prompt preparation and before credential minting + AI execution. |
| `post-steps` | ADO step list | `[]` | Trusted host-side steps after AI execution and token revocation, before verdict parsing. |

Boolean shorthand is also supported:

```yaml
safe-outputs:
create-pull-request: {}
threat-detection: false
```

When disabled, the Detection job remains in the pipeline as a pass-through: it
copies the Agent artifact to `analyzed_outputs`, publishes
`threatAnalysis.SafeToProcess=true`, and still detects approval-gated proposals.
Safe outputs therefore proceed without AI analysis, but manual-review gates and
the existing job/artifact graph remain intact. Custom Detection `steps` and
`post-steps` do not run while disabled.

The engine overlay inherits top-level settings unless a nested value is
supplied. Detection `env` merges by key (Detection wins); `args` inherits when
omitted and replaces the inherited list when supplied, so `args: []` clears
top-level arguments for Detection. Install version, command, auth/provider,
BYOK credential isolation, and firewall hosts are all resolved from the
effective Detection engine.

Detection inherits the top-level `engine.timeout-minutes`. Set
`safe-outputs.threat-detection.engine.timeout-minutes` to override that limit
for Detection only.

`steps` and `post-steps` run outside AWF and are trusted with the checked-out
repository and Detection artifacts. A failing custom step fails Detection and
blocks SafeOutputs. Use `pool.overrides.detection` to select a different
Detection pool; there is no duplicate `runs-on` key.

Not currently supported: runtime/expression-controlled enablement,
`continue-on-error`, `engine: false` custom-scanner-only mode, and a
Detection-specific AI-credit budget.

### Manual review (`require-approval`)

High-impact safe outputs can be gated behind a human approval step
Expand Down Expand Up @@ -120,8 +193,8 @@ apply one note to every tool.
low-impact tool (e.g. `add-pr-comment`) non-gated so the automatic split job
is created.

The Detection threat gate always runs first, so a flagged run applies nothing —
automatic or reviewed.
The Detection job always runs first. When AI threat analysis is enabled, a
flagged run applies nothing — automatic or reviewed.

> **Trust boundary note for `pool.overrides:`:** When `pool.overrides:` is used
> to move Detection, SafeOutputs, or Conclusion onto a different **self-hosted**
Expand Down
5 changes: 4 additions & 1 deletion src/audit/analyzers/otel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ mod tests {
use super::*;
use tempfile::TempDir;

const AW_INFO_JSON: &str = r#"{"schema":"ado-aw/aw_info/1","engine":"copilot","model":"claude-sonnet-4.5","agent_name":"test","target":"standalone","source":"agents/test.md","compiler_version":"0.30.0"}"#;
const AW_INFO_JSON: &str = r#"{"schema":"ado-aw/aw_info/1","engine":"copilot","model":"claude-sonnet-4.5","threat_detection_enabled":false,"detection_engine":"copilot","detection_model":"gpt-5-mini","agent_name":"test","target":"standalone","source":"agents/test.md","compiler_version":"0.30.0"}"#;
const COPILOT_OTEL_FIXTURE: &str = include_str!("../../../tests/fixtures/copilot-otel.jsonl");

async fn write_file(path: &Path, contents: &str) {
Expand Down Expand Up @@ -141,6 +141,9 @@ mod tests {
let aw_info = analysis.aw_info.expect("expected aw_info");
assert_eq!(aw_info.engine.as_deref(), Some("copilot"));
assert_eq!(aw_info.model.as_deref(), Some("claude-sonnet-4.5"));
assert_eq!(aw_info.threat_detection_enabled, Some(false));
assert_eq!(aw_info.detection_engine.as_deref(), Some("copilot"));
assert_eq!(aw_info.detection_model.as_deref(), Some("gpt-5-mini"));
assert_eq!(aw_info.compiler_version.as_deref(), Some("0.30.0"));

let engine_config = analysis.engine_config.expect("expected engine config");
Expand Down
10 changes: 10 additions & 0 deletions src/audit/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,15 @@ pub struct AwInfo {
/// Model identifier used by the agent runtime.
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
/// Whether AI threat detection was enabled for this workflow.
#[serde(skip_serializing_if = "Option::is_none")]
pub threat_detection_enabled: Option<bool>,
/// Engine identifier used by the Detection job when explicitly configured.
#[serde(skip_serializing_if = "Option::is_none")]
pub detection_engine: Option<String>,
/// Model identifier used by the Detection job when explicitly configured.
#[serde(skip_serializing_if = "Option::is_none")]
pub detection_model: Option<String>,
/// Agent name emitted by the compiled workflow metadata.
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_name: Option<String>,
Expand Down Expand Up @@ -919,6 +928,7 @@ mod tests {
source: Some(String::from("agents/security-scan.md")),
target: Some(String::from("standalone")),
compiler_version: Some(String::from("0.30.2")),
..AwInfo::default()
}),
},
task_domain: Some(TaskDomainInfo {
Expand Down
14 changes: 14 additions & 0 deletions src/audit/render/console.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,19 @@ fn render_overview_section(

push_opt_owned_row(&mut rows, "engine", engine);
push_opt_owned_row(&mut rows, "model", model);
if let Some(enabled) = aw_info.and_then(|info| info.threat_detection_enabled) {
rows.push(("threat_detection_enabled".to_string(), enabled.to_string()));
}
push_opt_owned_row(
&mut rows,
"detection_engine",
aw_info.and_then(|info| info.detection_engine.clone()),
);
push_opt_owned_row(
&mut rows,
"detection_model",
aw_info.and_then(|info| info.detection_model.clone()),
);
push_opt_owned_row(
&mut rows,
"agent",
Expand Down Expand Up @@ -1171,6 +1184,7 @@ By threat:
source: Some("agents/my-agent.md".to_string()),
target: Some("standalone".to_string()),
compiler_version: Some("0.30.2".to_string()),
..AwInfo::default()
}),
},
task_domain: Some(TaskDomainInfo {
Expand Down
1 change: 1 addition & 0 deletions src/audit/render/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ mod tests {
source: Some(String::from("agents/security-scan.md")),
target: Some(String::from("standalone")),
compiler_version: Some(String::from("0.30.2")),
..AwInfo::default()
}),
},
task_domain: Some(TaskDomainInfo {
Expand Down
Loading
Loading