Skip to content
Closed
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
104 changes: 104 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,110 @@ bash scripts/install-git-hooks.sh # one-time

Mixed: older crates on `2021`, newer on `2024`. Known and tracked. Don't bump in a PR that isn't explicitly about edition migration.

## Documentation standard (gold standard — applies to all crates)

**The rule:** every user-facing config section, public API, and user-facing feature must
have documentation with examples that are verified by the test suite. Docs that aren't
tested go stale silently; tested docs can't.

Channels (`docs/channels/`) were the first area to reach this standard and serve as the
reference implementation. All other config sections and crates follow the same pattern.

### What needs docs

- **Every `pub struct` in `config.rs`** — all fields documented; a TOML example that
parses against the live schema and is tested
- **Every user-facing feature** — setup guide in `docs/` covering prerequisites,
config, identity wiring (where applicable), and a verify step
- **Every public API in library crates** — at least one doctest per public function /
type showing the happy path; complex types get a full usage example

### Two testing mechanisms — use the right one for the crate type

**Library crates** (`secrets-client`, `adversary-detector`, `clashd`, `mcp-server`,
`paste-server`, `security-proxy`): use native Rust **doctests** in `///` comments.
`cargo test --doc -p <crate>` compiles and runs them. If an API changes and the
example is wrong, the build fails.

```rust
/// Fetches a secret value by name.
///
/// ```no_run
/// # use secrets_client::FnoxClient;
/// # #[tokio::main] async fn main() -> anyhow::Result<()> {
/// let client = FnoxClient::new();
/// let value = client.get("MY_API_KEY").await?;
/// # Ok(())
/// # }
/// ```
pub async fn get(&self, name: &str) -> Result<String, FnoxError> { ... }
```

**Binary crates** (`calciforge`, `host-agent`): use **`include_str!` + unit tests**
in the relevant source file's `#[cfg(test)]` block. The test loads the markdown doc,
extracts fenced TOML blocks, and parses them against the live config schema.

```rust
#[test]
fn test_agent_docs_toml_blocks_valid() {
let doc = include_str!("../../../docs/agents.md");
for block in doc_blocks_with(doc, "[[agents]]") {
parse_as_config(&block); // panics with file + block on failure
}
}
```

The channel tests in `config.rs` (`test_channel_docs_*_toml_blocks_valid`) are the
canonical example — copy that pattern for each new config section.

### Doc file locations

| What | Location | Tested by |
|---|---|---|
| Channel setup guides | `docs/channels/<channel>.md` | `config.rs` unit tests |
| Agent config | `docs/agents.md` | `config.rs` unit tests |
| Routing / identity | `docs/routing.md` | `config.rs` unit tests |
| Model gateway (alloys, cascades, dispatchers, exec models) | `docs/model-gateway.md` | `config.rs` unit tests |
| Security / proxy config | `docs/security-gateway.md` | `config.rs` unit tests |
| Library crate APIs | `///` doc comments in source | `cargo test --doc -p <crate>` |
| host-agent setup | `docs/host-agent.md` | `host-agent` unit tests |

### Structure for setup guides in `docs/`

Every setup guide covering a user-facing feature must have these sections, in order:

1. **Architecture** — ASCII diagram showing data flow end to end
2. **Prerequisites** — external accounts, tokens, running services, dependencies
3. **Config** — TOML block(s) with every required field shown; optional fields in a table
4. **Identity / routing** (where applicable) — how to wire a user to the feature
5. **Verify** — commands to confirm it's working (health check, log output, smoke test)

### Rules for every agent and contributor

**When adding a config field to any `pub struct`:**
1. Add a `///` doc comment to the field explaining what it does and its default
2. Update the TOML example in the relevant `docs/` file
3. Run `cargo test -p calciforge` — the doc-block tests will fail if the example is now
invalid, telling you exactly which file to fix
4. Commit the code change and the doc update together — never in separate PRs

**When adding a new config section:**
1. Create `docs/<section>.md` with all five structure sections above
2. Add `test_<section>_docs_toml_blocks_valid` in the relevant `#[cfg(test)]` block
3. Link from `docs/index.md`

**When adding a public function to a library crate:**
1. Write at least one `///` doctest showing the happy path
2. Run `cargo test --doc -p <crate>` to confirm it compiles and passes
3. For functions with meaningful error paths, add a second doctest for the failure case

**When renaming any field or function:**
- `cargo test` will name every broken doc example
- Fix markdown files and doctests in the same commit as the rename

Do not add a new channel, config section, or public library API without corresponding
tested documentation. A PR that adds functionality without docs should not be merged.

## When working on a specific area, also read

- `crates/host-agent/AGENTS.md` — host-agent security model (Unix-permissions enforcement, fail-closed, mTLS CN→Unix user mapping).
Expand Down
187 changes: 187 additions & 0 deletions crates/calciforge/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1542,4 +1542,191 @@ weight = 20
"error should name the missing field, got: {msg}"
);
}

// ── Channel config example tests ─────────────────────────────────────────
//
// Each test below does two things:
//
// 1. Parses the inline TOML example that appears in the corresponding
// docs/channels/*.md setup guide, verifying the example is
// syntactically valid and matches the live ChannelConfig schema.
//
// 2. Loads the markdown file via include_str! and scans every fenced
// ```toml block, wrapping each in a minimal CalciforgeConfig envelope
// and parsing it. If any code block in the doc drifts out of sync
// with the schema, cargo test catches it here.
//
// When you add or rename a ChannelConfig field, run `cargo test -p calciforge`
// and fix any failures in these tests before updating the docs.

fn extract_toml_blocks(markdown: &str) -> Vec<String> {
let mut blocks = Vec::new();
let mut in_block = false;
let mut current = String::new();
for line in markdown.lines() {
if line.trim() == "```toml" {
in_block = true;
current.clear();
} else if line.trim() == "```" && in_block {
in_block = false;
blocks.push(current.clone());
} else if in_block {
current.push_str(line);
current.push('\n');
}
}
blocks
}

fn channel_blocks_from_doc(markdown: &str) -> Vec<String> {
extract_toml_blocks(markdown)
.into_iter()
.filter(|b| b.contains("[[channels]]"))
.collect()
}

fn parse_channel_block(block: &str) -> CalciforgeConfig {
let wrapped = format!("[calciforge]\nversion = 2\n\n{block}");
toml::from_str(&wrapped).unwrap_or_else(|e| {
panic!("channel config block failed to parse:\n{block}\nerror: {e}")
})
}

#[test]
fn test_channel_config_telegram_inline() {
let raw = r#"
[calciforge]
version = 2

[[channels]]
kind = "telegram"
enabled = true
bot_token_file = "~/.calciforge/secrets/telegram-token"
"#;
let cfg: CalciforgeConfig = toml::from_str(raw).expect("telegram channel config");
assert_eq!(cfg.channels[0].kind, "telegram");
assert!(cfg.channels[0].enabled);
assert_eq!(
cfg.channels[0].bot_token_file.as_deref(),
Some("~/.calciforge/secrets/telegram-token")
);
}

#[test]
fn test_channel_config_matrix_inline() {
let raw = r#"
[calciforge]
version = 2

[[channels]]
kind = "matrix"
enabled = true
homeserver = "https://matrix.example.com"
access_token_file = "~/.calciforge/secrets/matrix-token"
room_id = "!abc123def456:example.com"
allowed_users = ["@operator:example.com"]
"#;
let cfg: CalciforgeConfig = toml::from_str(raw).expect("matrix channel config");
assert_eq!(cfg.channels[0].kind, "matrix");
assert_eq!(
cfg.channels[0].homeserver.as_deref(),
Some("https://matrix.example.com")
);
assert_eq!(cfg.channels[0].allowed_users, ["@operator:example.com"]);
}

#[test]
fn test_channel_config_signal_inline() {
let raw = r#"
[calciforge]
version = 2

[[channels]]
kind = "signal"
enabled = true
zeroclaw_endpoint = "http://127.0.0.1:18789"
zeroclaw_auth_token = "REPLACE_WITH_AUTH_TOKEN"
webhook_listen = "0.0.0.0:18796"
webhook_path = "/webhooks/signal"
allowed_numbers = ["+15555550001"]
"#;
let cfg: CalciforgeConfig = toml::from_str(raw).expect("signal channel config");
assert_eq!(cfg.channels[0].kind, "signal");
assert_eq!(
cfg.channels[0].webhook_listen.as_deref(),
Some("0.0.0.0:18796")
);
assert_eq!(cfg.channels[0].allowed_numbers, ["+15555550001"]);
}

#[test]
fn test_channel_config_whatsapp_inline() {
let raw = r#"
[calciforge]
version = 2

[[channels]]
kind = "whatsapp"
enabled = true
zeroclaw_endpoint = "http://127.0.0.1:18789"
zeroclaw_auth_token = "REPLACE_WITH_AUTH_TOKEN"
webhook_listen = "0.0.0.0:18795"
webhook_path = "/webhooks/whatsapp"
allowed_numbers = ["+15555550001"]
"#;
let cfg: CalciforgeConfig = toml::from_str(raw).expect("whatsapp channel config");
assert_eq!(cfg.channels[0].kind, "whatsapp");
assert_eq!(
cfg.channels[0].webhook_listen.as_deref(),
Some("0.0.0.0:18795")
);
}

#[test]
fn test_channel_docs_telegram_toml_blocks_valid() {
let doc = include_str!("../../../docs/channels/telegram.md");
for block in channel_blocks_from_doc(doc) {
let cfg = parse_channel_block(&block);
assert_eq!(
cfg.channels[0].kind, "telegram",
"unexpected kind in block:\n{block}"
);
}
}

#[test]
fn test_channel_docs_matrix_toml_blocks_valid() {
let doc = include_str!("../../../docs/channels/matrix.md");
for block in channel_blocks_from_doc(doc) {
let cfg = parse_channel_block(&block);
assert_eq!(
cfg.channels[0].kind, "matrix",
"unexpected kind in block:\n{block}"
);
}
}

#[test]
fn test_channel_docs_signal_toml_blocks_valid() {
let doc = include_str!("../../../docs/channels/signal.md");
for block in channel_blocks_from_doc(doc) {
let cfg = parse_channel_block(&block);
assert_eq!(
cfg.channels[0].kind, "signal",
"unexpected kind in block:\n{block}"
);
}
}

#[test]
fn test_channel_docs_whatsapp_toml_blocks_valid() {
let doc = include_str!("../../../docs/channels/whatsapp.md");
for block in channel_blocks_from_doc(doc) {
let cfg = parse_channel_block(&block);
assert_eq!(
cfg.channels[0].kind, "whatsapp",
"unexpected kind in block:\n{block}"
);
}
}
}
Loading
Loading