Skip to content

Commit 16fc9be

Browse files
jamesadevineCopilotCopilot
authored
feat: add Lean 4 runtime support with runtimes: front matter (#208)
* feat: add Lean 4 as a first-class tool Add `tools: lean:` front matter configuration that auto-configures: - elan installation step (injected into prepare_steps before AWF sandbox) - lean/lake/elan added to bash command allow-list - Lean-specific domains (elan.lean-lang.org, leanprover.github.io, lean-lang.org) added to AWF network allowlist - Agent prompt supplement informing the agent Lean 4 is available Supports both simple (`lean: true`) and object (`lean: { toolchain: ... }`) front matter formats. Default toolchain is "stable"; lean-toolchain files in the repo override automatically via elan. Includes: - LeanToolConfig types with deserialization tests - Lean domain entries in allowed_hosts.rs - Standalone compiler wiring (prepare_steps, allowed_domains, copilot_params) - 1ES compiler: bash allow-list works; elan install via manual steps - AGENTS.md documentation - Example agent (examples/lean-verifier.md) - Unit tests for types, copilot params, allowed domains, prepare steps Related issues: - #206 (first-class tool documentation pattern) - #207 (network ecosystem identifiers) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: symlink lean tools into /tmp/awf-tools/ for AWF chroot compatibility AWF chroot mode reconstructs PATH from standard system locations, so $HOME/.elan/bin is not on PATH inside the sandbox. Symlink lean, lake, and elan into /tmp/awf-tools/ which is always mounted at /tmp inside the AWF container. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: move Lean from tools to runtimes Lean is a language runtime, not a tool (like edit, bash, memory). Refactor to align with gh-aw's `runtimes:` front matter model: - New `src/runtimes/` module alongside `src/tools/` and `src/safeoutputs/` - `src/runtimes/lean.rs`: LeanRuntimeConfig, LeanOptions, install/prompt generators - `src/runtimes/mod.rs`: module documentation - `RuntimesConfig` struct in types.rs with `runtimes` field on FrontMatter - Front matter changes from `tools: lean:` to `runtimes: lean:` - All compiler references updated (standalone.rs, common.rs) - AGENTS.md: new "Runtimes Configuration" section, separate from Tools - Example updated Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: clean up generate_lean_install string formatting Replace \x20 hex escape sequences with a readable approach: build the script body as a plain string, then indent each line by 4 spaces for the YAML block scalar. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: use lean.rs constants instead of mcp_required_hosts - Use LEAN_BASH_COMMANDS constant in common.rs instead of inline list - Move lean domains from mcp_required_hosts() to LEAN_REQUIRED_HOSTS constant in runtimes/lean.rs (lean is a runtime, not an MCP) - Update standalone.rs and allowed_hosts.rs tests accordingly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 6c73449 commit 16fc9be

9 files changed

Lines changed: 528 additions & 25 deletions

File tree

AGENTS.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ tools: # optional tool configuration
146146
# toolsets: [repos, wit]
147147
# allowed: [wit_get_work_item]
148148
# org: myorg
149+
runtimes: # optional runtime configuration (language environments)
150+
lean: true # Lean 4 theorem prover (see Runtimes section)
151+
# lean: # Alternative object format (with toolchain pinning)
152+
# toolchain: "leanprover/lean4:v4.29.1"
149153
# env: # RESERVED: workflow-level environment variables (not yet implemented)
150154
# CUSTOM_VAR: "value"
151155
mcp-servers:
@@ -460,6 +464,38 @@ When enabled, the compiler:
460464
- Auto-infers org from the git remote URL at compile time (overridable via `org:` field)
461465
- Fails compilation if org cannot be determined (no explicit override and no ADO git remote)
462466

467+
### Runtimes Configuration
468+
469+
The `runtimes` field configures language environments that are installed before the agent runs. Unlike tools (which are agent capabilities like edit, bash, memory), runtimes are execution environments that the compiler auto-installs via pipeline steps.
470+
471+
Aligned with [gh-aw's `runtimes:` front matter field](https://github.github.com/gh-aw/reference/frontmatter/#runtimes-runtimes).
472+
473+
#### Lean 4 (`lean:`)
474+
475+
Lean 4 theorem prover runtime. Auto-installs the Lean toolchain via elan, extends the bash command allow-list, adds Lean-specific domains to the network allowlist, and appends a prompt supplement informing the agent that Lean is available.
476+
477+
```yaml
478+
# Simple enablement (installs latest stable toolchain)
479+
runtimes:
480+
lean: true
481+
482+
# With options (pin specific toolchain version)
483+
runtimes:
484+
lean:
485+
toolchain: "leanprover/lean4:v4.29.1"
486+
```
487+
488+
When enabled, the compiler:
489+
- Injects an elan installation step into `{{ prepare_steps }}` (runs before AWF network isolation)
490+
- Defaults to the `stable` toolchain; if a `lean-toolchain` file exists in the repo, elan overrides to that version automatically
491+
- Auto-adds `lean`, `lake`, and `elan` to the bash command allow-list
492+
- Adds Lean-specific domains to the network allowlist: `elan.lean-lang.org`, `leanprover.github.io`, `lean-lang.org`
493+
- Symlinks lean tools into `/tmp/awf-tools/` for AWF chroot compatibility
494+
- Appends a prompt supplement informing the agent about Lean 4 availability and basic commands
495+
- Emits a compile-time warning if `tools.bash` is empty (Lean requires bash access)
496+
497+
**Note:** In the 1ES target, the bash command allow-list is updated but elan installation must be done manually via `steps:` front matter. The 1ES target handles network isolation separately.
498+
463499
### Target Platforms
464500

465501
The `target` field in the front matter determines the output format and execution environment for the compiled pipeline.
@@ -1412,6 +1448,7 @@ When extending the compiler:
14121448
4. **New template markers**: Handle replacements in the target-specific compiler (e.g., `standalone.rs` or `onees.rs`)
14131449
5. **New safe-output tools**: Add to `src/safeoutputs/` — implement `ToolResult`, `Executor`, register in `mod.rs`, `mcp.rs`, `execute.rs`
14141450
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
14151452
7. **Validation**: Add compile-time validation for safe outputs and permissions
14161453

14171454
### Security Considerations

examples/lean-verifier.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
name: "Lean Formal Verifier"
3+
description: "Analyzes code and builds formal Lean 4 proofs of critical invariants"
4+
engine: claude-opus-4.5
5+
schedule: weekly on friday around 17:00
6+
tools:
7+
cache-memory: true
8+
runtimes:
9+
lean: true
10+
safe-outputs:
11+
create-pull-request:
12+
target-branch: main
13+
create-work-item:
14+
work-item-type: Task
15+
tags:
16+
- formal-verification
17+
- lean4
18+
permissions:
19+
write: my-write-arm-connection
20+
---
21+
22+
## Formal Verification Agent
23+
24+
You are a formal verification agent. Your job is to analyze the codebase, identify critical invariants and safety properties, and build Lean 4 proofs that verify them.
25+
26+
### Workflow
27+
28+
1. **Identify invariants**: Review the source code for critical logic — data validation, state transitions, arithmetic bounds, access control checks.
29+
2. **Model in Lean**: Create `.lean` files that formalize the identified properties as Lean 4 theorems.
30+
3. **Prove correctness**: Write proofs for each theorem. Use `lake build` to verify the proofs compile.
31+
4. **Iterate on failures**: If a proof fails, analyze the error output from `lean` to understand why. Either fix the proof or report the property as unverifiable (which may indicate a bug).
32+
5. **Submit results**: Create a PR with the `.lean` proof files, or create work items for properties that could not be verified.
33+
34+
### Guidelines
35+
36+
- Start with the simplest invariants first (null checks, bounds checks, type safety).
37+
- Use `lake init` to create a new Lake project if one doesn't exist.
38+
- Check your memory for findings from previous runs to avoid re-analyzing the same code.
39+
- If a property cannot be formalized or proved, use the `create-work-item` tool to flag it for human review.

src/allowed_hosts.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,12 @@ mod tests {
139139
let hosts = mcp_required_hosts("unknown-mcp");
140140
assert!(hosts.is_empty());
141141
}
142+
143+
#[test]
144+
fn test_lean_hosts() {
145+
use crate::runtimes::lean::LEAN_REQUIRED_HOSTS;
146+
assert!(LEAN_REQUIRED_HOSTS.contains(&"elan.lean-lang.org"));
147+
assert!(LEAN_REQUIRED_HOSTS.contains(&"leanprover.github.io"));
148+
assert!(LEAN_REQUIRED_HOSTS.contains(&"lean-lang.org"));
149+
}
142150
}

src/compile/common.rs

Lines changed: 87 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -484,25 +484,62 @@ pub fn generate_copilot_params(front_matter: &FrontMatter) -> Result<String> {
484484
}
485485

486486
// Bash tool: use configured list, or defaults if not specified
487-
let bash_commands: Vec<&str> = match front_matter.tools.as_ref().and_then(|t| t.bash.as_ref()) {
488-
Some(cmds) if cmds.len() == 1 && cmds[0] == ":*" => {
489-
// Unrestricted: single wildcard entry
490-
allowed_tools.push("shell(:*)".to_string());
491-
vec![]
492-
}
493-
Some(cmds) if cmds.is_empty() => {
494-
// Explicitly disabled: no bash commands
495-
vec![]
496-
}
497-
Some(cmds) => {
498-
// Explicit list of commands
499-
cmds.iter().map(|s| s.as_str()).collect()
500-
}
501-
None => {
502-
// Default safe commands
503-
DEFAULT_BASH_COMMANDS.to_vec()
487+
let mut bash_commands: Vec<&str> =
488+
match front_matter.tools.as_ref().and_then(|t| t.bash.as_ref()) {
489+
Some(cmds) if cmds.len() == 1 && cmds[0] == ":*" => {
490+
// Unrestricted: single wildcard entry
491+
allowed_tools.push("shell(:*)".to_string());
492+
vec![]
493+
}
494+
Some(cmds) if cmds.is_empty() => {
495+
// Explicitly disabled: no bash commands
496+
vec![]
497+
}
498+
Some(cmds) => {
499+
// Explicit list of commands
500+
cmds.iter().map(|s| s.as_str()).collect()
501+
}
502+
None => {
503+
// Default safe commands
504+
DEFAULT_BASH_COMMANDS.to_vec()
505+
}
506+
};
507+
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+
515+
let is_unrestricted_bash = front_matter
516+
.tools
517+
.as_ref()
518+
.and_then(|t| t.bash.as_ref())
519+
.is_some_and(|cmds| cmds.len() == 1 && cmds[0] == ":*");
520+
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) {
537+
bash_commands.push(cmd);
538+
}
539+
}
504540
}
505-
};
541+
}
542+
506543
for cmd in bash_commands {
507544
// Reject single quotes in bash commands — copilot_params are embedded inside
508545
// a single-quoted bash string in the AWF command.
@@ -1316,6 +1353,38 @@ mod tests {
13161353
assert!(!params.contains("shell("));
13171354
}
13181355

1356+
#[test]
1357+
fn test_copilot_params_lean_adds_bash_commands() {
1358+
let mut fm = minimal_front_matter();
1359+
fm.runtimes = Some(crate::compile::types::RuntimesConfig {
1360+
lean: Some(crate::runtimes::lean::LeanRuntimeConfig::Enabled(true)),
1361+
});
1362+
let params = generate_copilot_params(&fm).unwrap();
1363+
assert!(params.contains("shell(lean)"), "lean command should be allowed");
1364+
assert!(params.contains("shell(lake)"), "lake command should be allowed");
1365+
assert!(params.contains("shell(elan)"), "elan command should be allowed");
1366+
// Default bash commands should still be present
1367+
assert!(params.contains("shell(cat)"), "default commands should remain");
1368+
}
1369+
1370+
#[test]
1371+
fn test_copilot_params_lean_with_unrestricted_bash() {
1372+
let mut fm = minimal_front_matter();
1373+
fm.tools = Some(crate::compile::types::ToolsConfig {
1374+
bash: Some(vec![":*".to_string()]),
1375+
edit: None,
1376+
cache_memory: None,
1377+
azure_devops: None,
1378+
});
1379+
fm.runtimes = Some(crate::compile::types::RuntimesConfig {
1380+
lean: Some(crate::runtimes::lean::LeanRuntimeConfig::Enabled(true)),
1381+
});
1382+
let params = generate_copilot_params(&fm).unwrap();
1383+
assert!(params.contains("shell(:*)"), "unrestricted should be preserved");
1384+
// Should NOT add individual lean commands when unrestricted
1385+
assert!(!params.contains("shell(lean)"), "lean not needed with :*");
1386+
}
1387+
13191388
#[test]
13201389
fn test_copilot_params_custom_mcp_no_mcp_flag() {
13211390
let mut fm = minimal_front_matter();

0 commit comments

Comments
 (0)