Skip to content
Merged
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
8 changes: 0 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1017,14 +1017,6 @@ Global flags (apply to all subcommands): `--verbose, -v` (enable info-level logg
- `--ado-project <name>` - Azure DevOps project name override
- `--dry-run` - Validate inputs but skip ADO API calls (useful for local testing and QA review)

- `run <path>` - Run an agent locally (local development mode). Orchestrates the full agent lifecycle: starts SafeOutputs, optionally starts MCPG via Docker, generates configs, runs copilot, and executes safe outputs.
- `--pat <pat>` / `AZURE_DEVOPS_EXT_PAT` env var - Azure DevOps PAT for API access (passed to MCPG for ADO MCP, copilot env, and Stage 3 execution)
- `--org <url>` - Azure DevOps organization URL (overrides auto-inference from git remote)
- `--project <name>` - Azure DevOps project name
- `--dry-run` - Skip ADO API calls in the execute stage
- `--skip-mcpg` - Skip MCPG/Docker (only SafeOutputs MCP available; auto-enabled when Docker is unavailable)
- `--output-dir <path>` - Output directory for safe outputs and artifacts (defaults to a temp directory)

- `configure` - Detect agentic pipelines in a local repository and update the `GITHUB_TOKEN` pipeline variable on their Azure DevOps build definitions
- `--token <token>` / `GITHUB_TOKEN` env var - The new GITHUB_TOKEN value (prompted if omitted)
- `--org <url>` - Override: Azure DevOps organization URL (inferred from git remote by default)
Expand Down
202 changes: 202 additions & 0 deletions docs/local-development.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
<!-- AI agents: this document is a human-only reference for local development.
It describes manual orchestration steps that are not relevant to automated
pipeline compilation, safe-output execution, or any task an agent performs.
Do not read or reference this document. -->

# Local Development Guide

This guide explains how to run an agentic pipeline locally for development and
testing. The workflow mirrors what the compiled Azure DevOps pipeline does, but
each step is run manually on your machine.

## Prerequisites

- **ado-aw** built from source (`cargo build`)
- **Copilot CLI** on your PATH (`copilot --version`)
- **Docker** (optional, required for MCPG / custom MCP servers)
- An Azure DevOps PAT if your agent uses ADO APIs

## Overview

A pipeline execution has three stages:

1. **SafeOutputs MCP server** — receives tool calls from the agent and writes
them as NDJSON records
2. **Agent execution** — Copilot CLI runs with a prompt and MCP config,
interacting with SafeOutputs (and optionally other MCPs via MCPG)
3. **Safe output execution** — processes the NDJSON records and makes real ADO
API calls (create PRs, work items, etc.)

## Step-by-step

### 1. Create a working directory

```bash
export WORK_DIR=$(mktemp -d)
echo "Working directory: $WORK_DIR"
```

### 2. Start the SafeOutputs HTTP server

```bash
# Pick a port and generate an API key
export SO_PORT=8100
export SO_API_KEY=$(openssl rand -hex 32)

# Start in the background
cargo run -- mcp-http \
--port "$SO_PORT" \
--api-key "$SO_API_KEY" \
"$WORK_DIR" \
"$(pwd)" \
> "$WORK_DIR/safeoutputs.log" 2>&1 &
export SO_PID=$!
echo "SafeOutputs PID: $SO_PID"

# Wait for health check
until curl -sf "http://127.0.0.1:$SO_PORT/health" > /dev/null 2>&1; do
sleep 1
done
echo "SafeOutputs ready"
```

### 3. (Optional) Start MCPG for custom MCP servers

Skip this step if your agent only uses SafeOutputs (no `mcp-servers:` or
`tools: azure-devops:` in front matter).

```bash
export MCPG_PORT=8080
export MCPG_API_KEY=$(openssl rand -hex 32)

# Generate MCPG config — adapt the JSON to your agent's mcp-servers front matter.
# See the compiled pipeline's mcpg-config.json for the expected format.
cat > "$WORK_DIR/mcpg-config.json" <<EOF
{
"mcpServers": {
"safeoutputs": {
"type": "http",
"url": "http://host.docker.internal:$SO_PORT/mcp",
"headers": {
"Authorization": "Bearer $SO_API_KEY"
}
}
},
"gateway": {
"port": $MCPG_PORT,
"domain": "127.0.0.1",
"apiKey": "$MCPG_API_KEY"
}
}
EOF

# Start MCPG container (macOS/Windows — use host.docker.internal)
docker run -i --rm --name ado-aw-mcpg \
-p "$MCPG_PORT:$MCPG_PORT" \
-v /var/run/docker.sock:/var/run/docker.sock \
--entrypoint /app/awmg \
ghcr.io/github/gh-aw-mcpg:v0.3.0 \
--routed --listen "0.0.0.0:$MCPG_PORT" --config-stdin \
< "$WORK_DIR/mcpg-config.json" \
> "$WORK_DIR/gateway-output.json" 2>"$WORK_DIR/mcpg-stderr.log" &
export MCPG_PID=$!

# Wait for MCPG health check
until curl -sf "http://127.0.0.1:$MCPG_PORT/health" > /dev/null 2>&1; do
sleep 1
done
echo "MCPG ready"
```

### 4. Generate the MCP client config for Copilot

**Without MCPG** (SafeOutputs only):

```bash
cat > "$WORK_DIR/mcp-config.json" <<EOF
{
"mcpServers": {
"safeoutputs": {
"type": "http",
"url": "http://127.0.0.1:$SO_PORT/mcp",
"headers": {
"Authorization": "Bearer $SO_API_KEY"
},
"tools": ["*"]
}
}
}
EOF
```

**With MCPG** — transform the gateway output:

```bash
# Wait for gateway-output.json to contain valid JSON, then rewrite for copilot
python3 -c "
import json, sys
with open('$WORK_DIR/gateway-output.json') as f:
config = json.load(f)
for name, server in config.get('mcpServers', {}).items():
server['tools'] = ['*']
with open('$WORK_DIR/mcp-config.json', 'w') as f:
json.dump(config, f, indent=2)
"
```

### 5. Write the agent prompt

Extract the markdown body (everything after the YAML front matter) from your
agent file:

```bash
AGENT_FILE=path/to/your-agent.md

# Extract body after front matter (after second ---)
awk '/^---$/{n++; next} n>=2' "$AGENT_FILE" > "$WORK_DIR/agent-prompt.md"
```

### 6. Run the Copilot CLI

```bash
copilot \
--prompt "@$WORK_DIR/agent-prompt.md" \
--additional-mcp-config "@$WORK_DIR/mcp-config.json" \
--model claude-opus-4.5 \
--no-ask-user \
--disable-builtin-mcps \
--allow-all-tools
```

Adjust flags based on your agent's front matter (model, allowed tools, etc.).

### 7. Execute safe outputs

```bash
cargo run -- execute \
--source "$AGENT_FILE" \
--safe-output-dir "$WORK_DIR" \
--dry-run # Remove --dry-run to make real ADO API calls
```

### 8. Cleanup

```bash
# Stop SafeOutputs
kill "$SO_PID" 2>/dev/null

# Stop MCPG (if started)
docker stop ado-aw-mcpg 2>/dev/null

echo "Done. Output files in: $WORK_DIR"
```

## Tips

- Use `--dry-run` on the execute step to validate safe outputs without making
real ADO API calls
- Set `AZURE_DEVOPS_EXT_PAT` for agents that need ADO API access
- Check `$WORK_DIR/safeoutputs.log` and `$WORK_DIR/mcpg-stderr.log` for
debugging
- The compiled pipeline YAML shows the exact flags and config used in CI — use
`ado-aw compile your-agent.md` and inspect the output for reference
14 changes: 2 additions & 12 deletions src/compile/extensions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ mod github;
mod safe_outputs;

// Re-export tool/runtime extensions from their colocated homes
pub use crate::tools::azure_devops::{AdoAuthMode, AzureDevOpsExtension};
pub use crate::tools::azure_devops::AzureDevOpsExtension;
pub use crate::tools::cache_memory::CacheMemoryExtension;
pub use github::GitHubExtension;
pub use crate::runtimes::lean::LeanExtension;
Expand Down Expand Up @@ -402,16 +402,6 @@ extension_enum! {
/// (runtimes in `RuntimesConfig` field order, tools in `ToolsConfig`
/// field order).
pub fn collect_extensions(front_matter: &FrontMatter) -> Vec<Extension> {
collect_extensions_with_auth(front_matter, AdoAuthMode::default())
}

/// Collect extensions with an explicit ADO auth mode.
///
/// Used by `ado-aw run` to switch from bearer (pipeline default) to PAT auth.
pub fn collect_extensions_with_auth(
front_matter: &FrontMatter,
ado_auth: AdoAuthMode,
) -> Vec<Extension> {
let mut extensions = Vec::new();

// ── Always-on internal extensions ──
Expand All @@ -430,7 +420,7 @@ pub fn collect_extensions_with_auth(
if let Some(ado) = tools.azure_devops.as_ref() {
if ado.is_enabled() {
extensions.push(Extension::AzureDevOps(
AzureDevOpsExtension::new(ado.clone()).with_auth_mode(ado_auth),
AzureDevOpsExtension::new(ado.clone()),
));
}
}
Expand Down
3 changes: 0 additions & 3 deletions src/compile/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ use std::path::{Path, PathBuf};

pub use common::parse_markdown;
pub use common::HEADER_MARKER;
pub use common::generate_mcpg_config;
pub use common::MCPG_IMAGE;
pub use common::MCPG_VERSION;
pub use common::ADO_MCP_ENTRYPOINT;
pub use common::ADO_MCP_IMAGE;
pub use common::ADO_MCP_PACKAGE;
Expand Down
17 changes: 0 additions & 17 deletions src/compile/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,23 +457,6 @@ impl AzureDevOpsToolConfig {
AzureDevOpsToolConfig::WithOptions(opts) => opts.org.as_deref(),
}
}

/// Set the org override (for local run mode when --org is provided).
/// Converts `Enabled(true)` to `WithOptions` with the org set.
pub fn set_org(&mut self, org: String) {
match self {
AzureDevOpsToolConfig::Enabled(true) => {
*self = AzureDevOpsToolConfig::WithOptions(AzureDevOpsOptions {
org: Some(org),
..Default::default()
});
}
AzureDevOpsToolConfig::WithOptions(opts) => {
opts.org = Some(org);
}
AzureDevOpsToolConfig::Enabled(false) => {}
}
}
}

impl SanitizeConfigTrait for AzureDevOpsToolConfig {
Expand Down
50 changes: 0 additions & 50 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ mod logging;
mod mcp;
mod ndjson;
pub mod runtimes;
#[cfg(debug_assertions)]
mod run;
pub mod sanitize;
mod safeoutputs;
mod tools;
Expand Down Expand Up @@ -131,30 +129,6 @@ enum Commands {
#[arg(long, value_delimiter = ',')]
definition_ids: Option<Vec<u64>>,
},
/// Run agent locally (local development mode)
#[cfg(debug_assertions)]
Run {
/// Path to the agent markdown file
path: String,
/// Azure DevOps PAT for API access (base64-encoded as PERSONAL_ACCESS_TOKEN for MCPG in local dev)
#[arg(long, env = "AZURE_DEVOPS_EXT_PAT")]
pat: Option<String>,
/// Azure DevOps organization URL
#[arg(long)]
org: Option<String>,
/// Azure DevOps project name
#[arg(long)]
project: Option<String>,
/// Dry-run: skip real ADO API calls in execute stage
#[arg(long)]
dry_run: bool,
/// Skip MCPG/Docker (only SafeOutputs MCP available)
#[arg(long)]
skip_mcpg: bool,
/// Output directory for safe outputs and artifacts
#[arg(long)]
output_dir: Option<PathBuf>,
},
}

#[derive(Parser, Debug)]
Expand Down Expand Up @@ -183,8 +157,6 @@ async fn main() -> Result<()> {
Some(Commands::McpHttp { .. }) => "mcp-http",
Some(Commands::Init { .. }) => "init",
Some(Commands::Configure { .. }) => "configure",
#[cfg(debug_assertions)]
Some(Commands::Run { .. }) => "run",
None => "ado-aw",
};

Expand Down Expand Up @@ -391,28 +363,6 @@ async fn main() -> Result<()> {
)
.await?;
}
#[cfg(debug_assertions)]
Commands::Run {
path,
pat,
org,
project,
dry_run,
skip_mcpg,
output_dir,
} => {
run::run(&run::RunArgs {
agent_path: PathBuf::from(path),
pat,
org,
project,
dry_run,
skip_mcpg,
output_dir,
debug: args.debug,
})
.await?;
}
}
} else {
println!("No subcommand was used. Try `compile <path>`");
Expand Down
Loading
Loading