docs: channel setup guides with compile-tested TOML examples#82
docs: channel setup guides with compile-tested TOML examples#82bglusman wants to merge 6 commits into
Conversation
Adds per-channel setup docs (Telegram, Matrix, Signal, WhatsApp) to docs/channels/ as part of the public docs site (calciforge.org/channels/). Each guide covers: architecture diagram, prerequisites, config TOML, identity/routing wiring, and a verify step. The TOML config blocks in each doc are compile-tested: config.rs grows extract_toml_blocks() + test_channel_docs_<channel>_toml_blocks_valid tests that load each markdown via include_str!, extract every fenced [[channels]] block, and parse it against the live CalciforgeConfig schema. If a field is renamed and the doc isn't updated, cargo test fails. Also adds test_channel_config_<channel>_inline unit tests as schema-correct reference examples for each channel kind. AGENTS.md documents the standard and process: every channel needs a docs/channels/ entry, tests in config.rs, and both must be updated together when the schema changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ⓘ You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one. |
|
| Filename | Overview |
|---|---|
| crates/calciforge/src/config.rs | Adds 8 tests: 4 inline schema-correctness tests and 4 doc-block tests that load markdown via include_str! and parse every [[channels]] TOML fence. Both previously flagged gaps (unclosed-fence assertion and non-empty assertion) are addressed. |
| docs/channels/telegram.md | New setup guide: architecture diagram, prerequisites, config TOML, identity wiring, verify step. TOML block is compile-tested. |
| docs/channels/matrix.md | New setup guide with E2EE caveat, HTTP long-poll architecture, TOML config block, and identity wiring. TOML block is compile-tested. |
| docs/channels/signal.md | New setup guide for ZeroClaw-backed webhook receiver. Clearly separates Calciforge config (compile-tested) from ZeroClaw config (not tested, correctly excluded by [[channels]] filter). |
| docs/channels/whatsapp.md | New setup guide superseding WHATSAPP-SETUP.md; includes HMAC-SHA256 verification details (corrected from TODO to implemented). TOML block is compile-tested. |
| AGENTS.md | Adds the channel documentation standard and CI/test process for future contributors; clear and actionable. |
| docs/index.md | Adds links to the four new channel guides. Minor: "ZeroClaw/OpenClaw" in the Signal/WhatsApp blurbs is not referenced in the linked docs. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["cargo test -p calciforge"] --> B["test_channel_docs_*_toml_blocks_valid"]
B --> C["include_str! loads docs/channels/*.md"]
C --> D["extract_toml_blocks()"]
D --> E{"assert! unclosed fence"}
E -- fail --> F["❌ compile error: unclosed toml block"]
E -- pass --> G["channel_blocks_from_doc():\nfilter blocks containing [[channels]]"]
G --> H{"assert! non-empty"}
H -- fail --> I["❌ test failure: no [[channels]] blocks found"]
H -- pass --> J["parse_channel_block() per block\nwrap in CalciforgeConfig envelope"]
J --> K{"toml::from_str succeeds?"}
K -- no --> L["❌ panic with block + schema error"]
K -- yes --> M["assert cfg.channels[0].kind == expected"]
M --> N["✅ test passes"]
Reviews (6): Last reviewed commit: "Update docs/index.md" | Re-trigger Greptile
There was a problem hiding this comment.
Pull request overview
Adds per-channel setup documentation (Telegram, Matrix, Signal, WhatsApp) and introduces a test harness in calciforge to ensure the TOML config examples embedded in those docs continue to parse against the live CalciforgeConfig schema.
Changes:
- Added new channel setup guides under
docs/channels/and linked them fromdocs/index.md. - Added
calciforgeunit tests thatinclude_str!the markdown docs, extract fencedtomlblocks containing[[channels]], and parse them as config. - Documented the “compile-tested TOML examples” documentation process in
AGENTS.md.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/index.md | Links to new per-channel setup guides. |
| docs/channels/telegram.md | Telegram long-poll setup guide + TOML example. |
| docs/channels/matrix.md | Matrix long-poll setup guide + TOML example and E2EE caveat. |
| docs/channels/signal.md | Signal webhook setup guide + TOML example. |
| docs/channels/whatsapp.md | WhatsApp webhook setup guide + TOML example and HMAC verification notes. |
| crates/calciforge/src/config.rs | Added tests to parse inline examples + doc-extracted TOML blocks. |
| AGENTS.md | Documents the standard/process for channel docs and schema-checked examples. |
| current.push_str(line); | ||
| current.push('\n'); | ||
| } | ||
| } |
There was a problem hiding this comment.
extract_toml_blocks() silently drops an unterminated toml fence: if the markdown ends while `in_block` is still true, the current block is never pushed (and the doc tests can still pass). Consider detecting this and failing the test (e.g., `assert!(!in_block, "unterminated toml block")`) or pushing the final block so it gets parsed and fails appropriately.
| } | |
| } | |
| assert!(!in_block, "unterminated ```toml block"); |
| 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}"); | ||
| } |
There was a problem hiding this comment.
These doc tests will pass if channel_blocks_from_doc(doc) returns an empty Vec (e.g., if the [[channels]] example block is accidentally removed or extract_toml_blocks fails to detect it). Recommend capturing let blocks = channel_blocks_from_doc(doc); then assert!(!blocks.is_empty(), "no [[channels]] TOML blocks found") before iterating (same applies to the other test_channel_docs_* tests below).
- assert!(!in_block) at end of extract_toml_blocks catches unclosed ```toml fences instead of silently dropping the partial block - assert!(!blocks.is_empty()) before each test_channel_docs_* loop so the test fails visibly if the [[channels]] marker is ever accidentally removed from a doc file Addresses PR #82 review feedback. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| cfg.channels[0].homeserver.as_deref(), | ||
| Some("https://matrix.example.com") | ||
| ); | ||
| assert_eq!(cfg.channels[0].allowed_users, ["@operator:example.com"]); |
There was a problem hiding this comment.
This assertion won’t compile because ChannelConfig.allowed_users is a Vec<String> (see struct definition), but you’re comparing it to an array of &str. Compare against a Vec<String> (or a slice of String) instead.
| assert_eq!(cfg.channels[0].allowed_users, ["@operator:example.com"]); | |
| assert_eq!( | |
| cfg.channels[0].allowed_users, | |
| vec!["@operator:example.com".to_string()] | |
| ); |
| cfg.channels[0].webhook_listen.as_deref(), | ||
| Some("0.0.0.0:18796") | ||
| ); | ||
| assert_eq!(cfg.channels[0].allowed_numbers, ["+15555550001"]); |
There was a problem hiding this comment.
This assertion won’t compile because ChannelConfig.allowed_numbers is a Vec<String>, but the RHS is an array of &str. Compare against a Vec<String> (or a &[String]) instead.
| assert_eq!(cfg.channels[0].allowed_numbers, ["+15555550001"]); | |
| assert_eq!(cfg.channels[0].allowed_numbers, vec!["+15555550001".to_string()]); |
| | Field | Required | Default | Description | | ||
| |---|---|---|---| | ||
| | `zeroclaw_endpoint` | yes | — | URL of the ZeroClaw/OpenClaw gateway | | ||
| | `zeroclaw_auth_token` | yes | — | Bearer token for the gateway | | ||
| | `webhook_listen` | no | `0.0.0.0:18795` | Address Calciforge listens on for incoming WhatsApp webhooks | | ||
| | `webhook_path` | no | `/webhooks/whatsapp` | URL path for incoming webhooks | | ||
| | `webhook_secret` | no | — | HMAC-SHA256 secret; when set, Calciforge rejects requests with invalid or missing `X-Hub-Signature-256` headers | | ||
| | `allowed_numbers` | yes | `[]` | E.164 phone numbers allowed to interact | | ||
| | `scan_messages` | no | `false` | Enable inbound adversarial content scanning | | ||
|
|
There was a problem hiding this comment.
The table header starts with ||, which breaks standard Markdown table rendering in many parsers. Use a single leading | for the header/separator rows.
| | Field | Required | Default | Description | | ||
| |---|---|---|---| | ||
| | `zeroclaw_endpoint` | yes | — | URL of the ZeroClaw/OpenClaw gateway | | ||
| | `zeroclaw_auth_token` | yes | — | Bearer token for the gateway | | ||
| | `webhook_listen` | no | `0.0.0.0:18796` | Address Calciforge listens on for incoming Signal webhooks | | ||
| | `webhook_path` | no | `/webhooks/signal` | URL path for incoming webhooks | | ||
| | `webhook_secret` | no | — | HMAC-SHA256 secret; when set, Calciforge rejects unsigned requests | | ||
| | `allowed_numbers` | yes | `[]` | E.164 phone numbers allowed to interact | | ||
| | `scan_messages` | no | `false` | Enable inbound adversarial content scanning | | ||
|
|
There was a problem hiding this comment.
The table header starts with ||, which breaks standard Markdown table rendering in many parsers. Use a single leading | for the header/separator rows.
| | Field | Required | Description | | ||
| |---|---|---| | ||
| | `homeserver` | yes | Full URL of the Matrix homeserver | | ||
| | `access_token_file` | yes | Path to file containing the bot's access token | | ||
| | `room_id` | yes | Internal room ID (starts with `!`) | | ||
| | `allowed_users` | yes | Matrix user IDs permitted to send commands; empty list allows all room members (not recommended) | | ||
| | `scan_messages` | no (`false`) | Enable inbound adversarial content scanning | | ||
| | `allow_chat_secret_set` | no (`false`) | Allow `!secure set` via Matrix (not recommended) | | ||
|
|
There was a problem hiding this comment.
The table header starts with ||, which breaks standard Markdown table rendering in many parsers. Use a single leading | for the header/separator rows (and ensure the column count matches the header).
- matrix.md: correct allowed_users description — empty list errors at
startup; "* " is the wildcard for all room members
- telegram.md: fix log message grep hint to match actual tracing output
("unknown Telegram sender — dropping silently sender_id=<id>")
- config.rs: update comment to say [[channels]]-filtered blocks (not
"every fenced block"); change Vec<String> assertions to explicit
.to_string() form for clarity
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
216e14f to
9968e17
Compare
Calciforge is meant to be agent-agnostic; the prior phrasing made it sound
like ZeroClaw (or OpenClaw) was a required component of the system. The
actual contract is a webhook wire format — incoming POST to /webhooks/<channel>
and outbound POST to {gateway}/tools/invoke. Any gateway implementing those
endpoints works.
Channel docs now lead with the wire-format contract and call out ZeroClaw
as the known-working reference implementation. Future-work notes describe
embedded paths via zeroclawlabs (the Rust library) — distinct from running
ZeroClaw or OpenClaw as products.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
9968e17 to
3d545f9
Compare
| for block in blocks { | ||
| let cfg = parse_channel_block(&block); | ||
| assert_eq!( | ||
| cfg.channels[0].kind, "telegram", | ||
| "unexpected kind in block:\n{block}" | ||
| ); | ||
| } |
There was a problem hiding this comment.
These doc-validation tests index cfg.channels[0], which produces a less-informative panic if a fenced TOML block contains the string [[channels]] (e.g. in a comment) but doesn’t actually deserialize into any channel entries. Also, if a single fenced block contains multiple [[channels]] entries, only the first one is checked. Consider asserting cfg.channels.len() == 1 (or at least !cfg.channels.is_empty()) and validating the kind for all parsed channel entries; same applies to the other test_channel_docs_*_toml_blocks_valid loops.
| "entry": [{ | ||
| "changes": [{ |
There was a problem hiding this comment.
This sentence says the synthetic webhook message will be dropped unless 15555550001 is in an identity alias, but the WhatsApp channel normalizes from to E.164 with a leading + before identity lookup. Update the example text to reference +15555550001 (or otherwise clarify that aliases must include the +).
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
| 1. **Create a bot** via [@BotFather](https://t.me/BotFather): send `/newbot`, follow the | ||
| prompts, copy the token it returns (format: `1234567890:ABCDEFghijklmnopqrstuvwxyz01234567`) | ||
| 2. **Find your Telegram user ID** (numeric, not your username): |
There was a problem hiding this comment.
The Telegram token example uses a real token-shaped value (digits + ':' + long base64-ish suffix). Since this repo extends gitleaks default rules and docs/channels/** is not allowlisted, this kind of string is likely to be flagged as a leaked credential. Consider switching to an obviously-non-token placeholder (e.g., REPLACE_WITH_TELEGRAM_BOT_TOKEN) and avoid embedding a token-shaped value in both the prose and shell snippet.
| if line.trim() == "```toml" { | ||
| in_block = true; | ||
| current.clear(); | ||
| } else if line.trim() == "```" && in_block { |
There was a problem hiding this comment.
extract_toml_blocks only enters TOML mode when a line trims to exactly ```toml. This misses valid Markdown fences like ```toml with trailing spaces or extra info-string attributes (which would silently skip blocks and reduce the effectiveness of the doc/schema sync). Consider loosening the check to starts_with("```toml") (after trimming) so the extraction matches common Markdown variations.
| if line.trim() == "```toml" { | |
| in_block = true; | |
| current.clear(); | |
| } else if line.trim() == "```" && in_block { | |
| let trimmed = line.trim(); | |
| if trimmed.starts_with("```toml") { | |
| in_block = true; | |
| current.clear(); | |
| } else if trimmed == "```" && in_block { |
|
Closing as superseded. The channel setup guide/schema-test work has been carried forward on top of current main in #89, which also updates the WhatsApp docs for the embedded client and adds Text/iMessage channel docs. |
Summary
Adds per-channel setup guides to
docs/channels/(Telegram, Matrix, Signal, WhatsApp), published to calciforge.org/channels/.Each guide has: architecture diagram, prerequisites, config TOML, identity/routing wiring, verify step.
The key thing: TOML blocks are compile-tested
config.rsgrows a test suite that loads each markdown file viainclude_str!, extracts every fenced[[channels]]TOML block, and parses it against the liveCalciforgeConfigschema:If a field is renamed and the doc isn't updated,
cargo test -p calciforgefails. The docs can't silently go stale.Also adds
test_channel_config_<channel>_inlineunit tests as schema-correct reference examples.AGENTS.md
Documents the standard and process so future channels follow the same pattern automatically.
Files
docs/channels/telegram.md— newdocs/channels/matrix.md— new (includes E2EE caveat)docs/channels/signal.md— new (replaces config-example-only coverage)docs/channels/whatsapp.md— new (supersedescrates/calciforge/WHATSAPP-SETUP.md; HMAC section corrected — it is implemented, not a TODO)docs/index.md— links to channel pagesAGENTS.md— doc standard and processcrates/calciforge/src/config.rs— 8 new testsGenerated with Claude Code