Skip to content

Commit 58610c1

Browse files
jamesadevineCopilot
andcommitted
refactor: unify runtime/tool requirements via CompilerExtension trait
Introduce a CompilerExtension trait that runtimes and first-party tools implement to declare their compilation requirements (network hosts, bash commands, prompt supplements, prepare steps, MCPG entries). Previously, each runtime/tool required scattered special-case code across standalone.rs, common.rs, and other files. Now: - LeanExtension: declares hosts, bash cmds, install steps, prompt - AzureDevOpsExtension: declares hosts, MCPG entry, validation - CacheMemoryExtension: declares download steps, prompt supplement The compiler collects all enabled extensions via collect_extensions() and iterates over them generically -- no more bespoke if-blocks. Adding a new runtime or tool is now a 2-step process: 1. Implement CompilerExtension 2. Add to collect_extensions() MCPG types (McpgServerConfig, McpgConfig, etc.) moved from standalone.rs to extensions.rs for shared access. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 85ac3ab commit 58610c1

6 files changed

Lines changed: 946 additions & 359 deletions

File tree

AGENTS.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1447,9 +1447,27 @@ When extending the compiler:
14471447
3. **New front matter fields**: Add fields to `FrontMatter` in `src/compile/types.rs`
14481448
4. **New template markers**: Handle replacements in the target-specific compiler (e.g., `standalone.rs` or `onees.rs`)
14491449
5. **New safe-output tools**: Add to `src/safeoutputs/` — implement `ToolResult`, `Executor`, register in `mod.rs`, `mcp.rs`, `execute.rs`
1450-
6. **New first-class tools**: Add to `src/tools/` — extend `ToolsConfig` in `types.rs`, wire in compilers
1451-
7. **New runtimes**: Add to `src/runtimes/` — extend `RuntimesConfig` in `types.rs`, wire in compilers
1452-
7. **Validation**: Add compile-time validation for safe outputs and permissions
1450+
6. **New first-class tools**: Add to `src/tools/` — extend `ToolsConfig` in `types.rs`, implement `CompilerExtension` trait in `src/compile/extensions.rs`, add collection in `collect_extensions()`
1451+
7. **New runtimes**: Add to `src/runtimes/` — extend `RuntimesConfig` in `types.rs`, implement `CompilerExtension` trait in `src/compile/extensions.rs`, add collection in `collect_extensions()`
1452+
8. **Validation**: Add compile-time validation for safe outputs and permissions
1453+
1454+
#### `CompilerExtension` Trait
1455+
1456+
Runtimes and first-party tools declare their compilation requirements via the `CompilerExtension` trait (`src/compile/extensions.rs`). Instead of scattering special-case `if` blocks across the compiler, each runtime/tool implements this trait and the compiler collects requirements generically:
1457+
1458+
```rust
1459+
pub trait CompilerExtension: Send {
1460+
fn name(&self) -> &str; // Display name
1461+
fn required_hosts(&self) -> Vec<String>; // AWF network allowlist
1462+
fn required_bash_commands(&self) -> Vec<String>; // Agent bash allow-list
1463+
fn prompt_supplement(&self) -> Option<String>; // Agent prompt markdown
1464+
fn prepare_steps(&self) -> Vec<String>; // Pipeline steps (install, etc.)
1465+
fn mcpg_servers(&self, ctx) -> Result<Vec<(String, McpgServerConfig)>>; // MCPG entries
1466+
fn validate(&self, ctx) -> Result<Vec<String>>; // Compile-time warnings
1467+
}
1468+
```
1469+
1470+
To add a new runtime or tool: (1) create a struct implementing `CompilerExtension`, (2) add a collection check in `collect_extensions()`. No other files need modification.
14531471

14541472
### Security Considerations
14551473

src/compile/common.rs

Lines changed: 29 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,10 @@ const DEFAULT_BASH_COMMANDS: &[&str] = &[
470470
];
471471

472472
/// Generate copilot CLI params from front matter configuration
473-
pub fn generate_copilot_params(front_matter: &FrontMatter) -> Result<String> {
473+
pub fn generate_copilot_params(
474+
front_matter: &FrontMatter,
475+
extensions: &[Box<dyn super::extensions::CompilerExtension>],
476+
) -> Result<String> {
474477
let mut allowed_tools: Vec<String> = vec!["github".to_string(), "safeoutputs".to_string()];
475478

476479
// Edit tool: enabled by default, can be disabled with `edit: false`
@@ -484,7 +487,7 @@ pub fn generate_copilot_params(front_matter: &FrontMatter) -> Result<String> {
484487
}
485488

486489
// Bash tool: use configured list, or defaults if not specified
487-
let mut bash_commands: Vec<&str> =
490+
let mut bash_commands: Vec<String> =
488491
match front_matter.tools.as_ref().and_then(|t| t.bash.as_ref()) {
489492
Some(cmds) if cmds.len() == 1 && cmds[0] == ":*" => {
490493
// Unrestricted: single wildcard entry
@@ -497,50 +500,32 @@ pub fn generate_copilot_params(front_matter: &FrontMatter) -> Result<String> {
497500
}
498501
Some(cmds) => {
499502
// Explicit list of commands
500-
cmds.iter().map(|s| s.as_str()).collect()
503+
cmds.clone()
501504
}
502505
None => {
503506
// Default safe commands
504-
DEFAULT_BASH_COMMANDS.to_vec()
507+
DEFAULT_BASH_COMMANDS.iter().map(|s| (*s).to_string()).collect()
505508
}
506509
};
507510

508-
// Auto-add lean/lake/elan when runtimes.lean is enabled
509-
let has_lean = front_matter
510-
.runtimes
511-
.as_ref()
512-
.and_then(|r| r.lean.as_ref())
513-
.is_some_and(|l| l.is_enabled());
514-
511+
// Auto-add extension-declared bash commands (runtimes + first-party tools)
515512
let is_unrestricted_bash = front_matter
516513
.tools
517514
.as_ref()
518515
.and_then(|t| t.bash.as_ref())
519516
.is_some_and(|cmds| cmds.len() == 1 && cmds[0] == ":*");
520517

521-
if has_lean && !is_unrestricted_bash {
522-
let bash_disabled = front_matter
523-
.tools
524-
.as_ref()
525-
.and_then(|t| t.bash.as_ref())
526-
.is_some_and(|cmds| cmds.is_empty());
527-
528-
if bash_disabled {
529-
eprintln!(
530-
"Warning: Agent '{}' has runtimes.lean enabled but tools.bash is empty. \
531-
Lean requires bash access (lean, lake, elan commands).",
532-
front_matter.name
533-
);
534-
} else {
535-
for cmd in crate::runtimes::lean::LEAN_BASH_COMMANDS {
536-
if !bash_commands.contains(cmd) {
518+
if !is_unrestricted_bash {
519+
for ext in extensions {
520+
for cmd in ext.required_bash_commands() {
521+
if !bash_commands.contains(&cmd) {
537522
bash_commands.push(cmd);
538523
}
539524
}
540525
}
541526
}
542527

543-
for cmd in bash_commands {
528+
for cmd in &bash_commands {
544529
// Reject single quotes in bash commands — copilot_params are embedded inside
545530
// a single-quoted bash string in the AWF command.
546531
if cmd.contains('\'') {
@@ -1336,7 +1321,7 @@ mod tests {
13361321
cache_memory: None,
13371322
azure_devops: None,
13381323
});
1339-
let params = generate_copilot_params(&fm).unwrap();
1324+
let params = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm)).unwrap();
13401325
assert!(params.contains("--allow-tool \"shell(:*)\""));
13411326
}
13421327

@@ -1349,7 +1334,7 @@ mod tests {
13491334
cache_memory: None,
13501335
azure_devops: None,
13511336
});
1352-
let params = generate_copilot_params(&fm).unwrap();
1337+
let params = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm)).unwrap();
13531338
assert!(!params.contains("shell("));
13541339
}
13551340

@@ -1359,7 +1344,7 @@ mod tests {
13591344
fm.runtimes = Some(crate::compile::types::RuntimesConfig {
13601345
lean: Some(crate::runtimes::lean::LeanRuntimeConfig::Enabled(true)),
13611346
});
1362-
let params = generate_copilot_params(&fm).unwrap();
1347+
let params = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm)).unwrap();
13631348
assert!(params.contains("shell(lean)"), "lean command should be allowed");
13641349
assert!(params.contains("shell(lake)"), "lake command should be allowed");
13651350
assert!(params.contains("shell(elan)"), "elan command should be allowed");
@@ -1379,7 +1364,7 @@ mod tests {
13791364
fm.runtimes = Some(crate::compile::types::RuntimesConfig {
13801365
lean: Some(crate::runtimes::lean::LeanRuntimeConfig::Enabled(true)),
13811366
});
1382-
let params = generate_copilot_params(&fm).unwrap();
1367+
let params = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm)).unwrap();
13831368
assert!(params.contains("shell(:*)"), "unrestricted should be preserved");
13841369
// Should NOT add individual lean commands when unrestricted
13851370
assert!(!params.contains("shell(lean)"), "lean not needed with :*");
@@ -1395,7 +1380,7 @@ mod tests {
13951380
..Default::default()
13961381
}),
13971382
);
1398-
let params = generate_copilot_params(&fm).unwrap();
1383+
let params = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm)).unwrap();
13991384
assert!(!params.contains("--mcp my-tool"));
14001385
}
14011386

@@ -1404,7 +1389,7 @@ mod tests {
14041389
let mut fm = minimal_front_matter();
14051390
fm.mcp_servers
14061391
.insert("ado".to_string(), McpConfig::Enabled(true));
1407-
let params = generate_copilot_params(&fm).unwrap();
1392+
let params = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm)).unwrap();
14081393
// Copilot CLI has no built-in MCPs — all MCPs are handled via the MCP firewall
14091394
assert!(!params.contains("--mcp ado"));
14101395
}
@@ -1415,14 +1400,14 @@ mod tests {
14151400
"---\nname: test\ndescription: test\nengine:\n model: claude-opus-4.5\n max-turns: 50\n---\n",
14161401
)
14171402
.unwrap();
1418-
let params = generate_copilot_params(&fm).unwrap();
1403+
let params = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm)).unwrap();
14191404
assert!(!params.contains("--max-turns"), "max-turns should not be emitted as a CLI arg");
14201405
}
14211406

14221407
#[test]
14231408
fn test_copilot_params_no_max_turns_when_simple_engine() {
14241409
let fm = minimal_front_matter();
1425-
let params = generate_copilot_params(&fm).unwrap();
1410+
let params = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm)).unwrap();
14261411
assert!(!params.contains("--max-turns"));
14271412
}
14281413

@@ -1432,14 +1417,14 @@ mod tests {
14321417
"---\nname: test\ndescription: test\nengine:\n model: claude-opus-4.5\n timeout-minutes: 30\n---\n",
14331418
)
14341419
.unwrap();
1435-
let params = generate_copilot_params(&fm).unwrap();
1420+
let params = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm)).unwrap();
14361421
assert!(!params.contains("--max-timeout"), "timeout-minutes should not be emitted as a CLI arg");
14371422
}
14381423

14391424
#[test]
14401425
fn test_copilot_params_no_max_timeout_when_simple_engine() {
14411426
let fm = minimal_front_matter();
1442-
let params = generate_copilot_params(&fm).unwrap();
1427+
let params = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm)).unwrap();
14431428
assert!(!params.contains("--max-timeout"));
14441429
}
14451430

@@ -1449,7 +1434,7 @@ mod tests {
14491434
"---\nname: test\ndescription: test\nengine:\n model: claude-opus-4.5\n max-turns: 0\n---\n",
14501435
)
14511436
.unwrap();
1452-
let params = generate_copilot_params(&fm).unwrap();
1437+
let params = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm)).unwrap();
14531438
assert!(!params.contains("--max-turns"), "max-turns should not be emitted as a CLI arg");
14541439
}
14551440

@@ -1459,7 +1444,7 @@ mod tests {
14591444
"---\nname: test\ndescription: test\nengine:\n model: claude-opus-4.5\n timeout-minutes: 0\n---\n",
14601445
)
14611446
.unwrap();
1462-
let params = generate_copilot_params(&fm).unwrap();
1447+
let params = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm)).unwrap();
14631448
assert!(!params.contains("--max-timeout"), "timeout-minutes should not be emitted as a CLI arg");
14641449
}
14651450

@@ -2384,7 +2369,7 @@ mod tests {
23842369
)
23852370
.unwrap();
23862371
fm.engine = crate::compile::types::EngineConfig::Simple("model' && echo pwned".to_string());
2387-
let result = generate_copilot_params(&fm);
2372+
let result = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm));
23882373
assert!(result.is_err());
23892374
assert!(result.unwrap_err().to_string().contains("invalid characters"));
23902375
}
@@ -2393,7 +2378,7 @@ mod tests {
23932378
fn test_model_name_rejects_space() {
23942379
let mut fm = minimal_front_matter();
23952380
fm.engine = crate::compile::types::EngineConfig::Simple("model && curl evil.com".to_string());
2396-
let result = generate_copilot_params(&fm);
2381+
let result = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm));
23972382
assert!(result.is_err());
23982383
}
23992384

@@ -2402,7 +2387,7 @@ mod tests {
24022387
for name in &["claude-opus-4.5", "gpt-5.2-codex", "gemini-3-pro-preview", "my_model:latest"] {
24032388
let mut fm = minimal_front_matter();
24042389
fm.engine = crate::compile::types::EngineConfig::Simple(name.to_string());
2405-
let result = generate_copilot_params(&fm);
2390+
let result = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm));
24062391
assert!(result.is_ok(), "Model name '{}' should be valid", name);
24072392
}
24082393
}
@@ -2416,7 +2401,7 @@ mod tests {
24162401
cache_memory: None,
24172402
azure_devops: None,
24182403
});
2419-
let result = generate_copilot_params(&fm);
2404+
let result = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm));
24202405
assert!(result.is_err());
24212406
assert!(result.unwrap_err().to_string().contains("single quote"));
24222407
}

0 commit comments

Comments
 (0)