Install OpenClaw channel plugin via installer#90
Conversation
There was a problem hiding this comment.
Pull request overview
Adds first-class “managed agent” installation support for the openclaw-channel adapter by bundling an OpenClaw channel plugin and wiring installer/validation/docs around required auth + reply-callback configuration.
Changes:
- Bundle and remotely install an OpenClaw channel plugin during
calciforge installforopenclaw-channeltargets, including config patching and service restart handling. - Extend installer specs/models/wizard to require inbound auth + reply webhook/auth for non-interactive OpenClaw installs, and improve install output/test coverage for OpenClaw vs ZeroClaw paths.
- Add guardrails that reject OpenClaw model IDs when using
openai-compat, plus documentation updates describing the managed-agent installer pattern and model-vs-agent separation.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/roadmap/agent-recipes-orchestrators.md | Roadmap note introducing a unified managed-agent installer pattern. |
| docs/model-gateway.md | Clarifies UX separation between model routes and agents. |
| docs/codex-openclaw-integration.md | Updates OpenClaw integration example to include reply auth + installer spec example. |
| docs/agents.md | Updates openclaw-channel requirements, adds reply auth example and installer guidance. |
| docs/agent-adapters.md | Documents OpenClaw as first-class managed agent in calciforge install. |
| crates/calciforge/src/install/wizard.rs | Adds interactive collection of managed-agent auth + reply callback fields for OpenClaw. |
| crates/calciforge/src/install/model.rs | Extends ClawTarget with auth + reply callback fields used by managed installs. |
| crates/calciforge/src/install/executor.rs | Patches OpenClaw config to add plugin entry, installs plugin files remotely, and restarts OpenClaw service as needed. |
| crates/calciforge/src/install/cli.rs | Requires auth/reply fields in --claw spec for openclaw-channel, with updated parsing/tests. |
| crates/calciforge/src/doctor.rs | Adds diagnostics rejecting OpenClaw model IDs under openai-compat. |
| crates/calciforge/src/config/validator.rs | Adds config validation rejecting OpenClaw model IDs under openai-compat. |
| crates/calciforge/src/adapters/openai_compat.rs | Rejects dispatching OpenClaw model IDs through openai-compat, with tests. |
| crates/calciforge-openclaw-channel-plugin/openclaw.plugin.json | Adds bundled OpenClaw channel plugin manifest (config schema + metadata). |
| crates/calciforge-openclaw-channel-plugin/index.js | Implements the OpenClaw channel plugin bridging /calciforge/inbound to Calciforge /hooks/reply. |
ⓘ 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. |
There was a problem hiding this comment.
Pull request overview
Adds a bundled OpenClaw “calciforge-channel” plugin and extends the managed-agent installer flow so OpenClaw installs can be fully automated (auth + reply callbacks), while tightening validation/doctoring to prevent misconfiguring OpenClaw agents as generic model routes.
Changes:
- Bundle and remotely install the OpenClaw channel plugin during
calciforge installforopenclaw-channeltargets, including service restart logic. - Require and plumb managed-agent auth + reply callback fields through installer CLI + wizard + OpenClaw config patching.
- Add guardrails preventing
openai-compatfrom using OpenClaw model IDs (validator, doctor, and adapter-level enforcement) and update docs to reflect the managed-agent pattern.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/roadmap/agent-recipes-orchestrators.md | Documents the new first-class “managed agent” installer pattern as a roadmap item. |
| docs/model-gateway.md | Clarifies UX separation between “agents” and “model/chat routes”. |
| docs/codex-openclaw-integration.md | Adds installer-managed OpenClaw token/webhook example. |
| docs/agents.md | Updates OpenClaw agent requirements + installer example and adds reply_auth_token to sample config. |
| docs/agent-adapters.md | Documents OpenClaw as an installer-managed first-class agent (and notes ZeroClaw parity follow-up). |
| crates/calciforge/src/install/wizard.rs | Collects OpenClaw managed-agent auth + reply callback fields in interactive installs. |
| crates/calciforge/src/install/model.rs | Extends ClawTarget with auth_token, reply_webhook, reply_auth_token. |
| crates/calciforge/src/install/executor.rs | Patches OpenClaw config to write the plugin entry, installs plugin files over SSH, and restarts openclaw-gateway; updates tests accordingly. |
| crates/calciforge/src/install/cli.rs | Requires OpenClaw managed-agent fields in non-interactive --claw specs and redacts secret values in parse errors. |
| crates/calciforge/src/doctor.rs | Errors when openai-compat is configured with an OpenClaw model ID. |
| crates/calciforge/src/config/validator.rs | Rejects openai-compat agents configured with OpenClaw model IDs. |
| crates/calciforge/src/adapters/openai_compat.rs | Runtime enforcement: rejects OpenClaw model IDs in openai-compat dispatch. |
| crates/calciforge-openclaw-channel-plugin/package.json | Declares module type for the bundled plugin. |
| crates/calciforge-openclaw-channel-plugin/openclaw.plugin.json | Adds plugin manifest + config schema for managed-agent tokens/webhook. |
| crates/calciforge-openclaw-channel-plugin/index.js | Implements the OpenClaw channel plugin: inbound route + auth + async run + reply webhook delivery. |
Comments suppressed due to low confidence (1)
crates/calciforge/src/install/executor.rs:825
- Writing the patched OpenClaw config via
SshClient::write_fileembeds the full file content (includingauth_token/reply_auth_token) into the remote shell command as a base64 string. That can leak secrets via process listings (ps) on both the local machine runningsshand the remote host. Prefer a write mechanism that streams content over stdin (or usesscp/SFTP) so secrets are never placed in argv, and use that for writingopenclaw.json(and any other secret-bearing files).
// Read current config.
let current = deps
.ssh
.read_file(&claw.host, key, &config_path)
.map_err(|e| anyhow::anyhow!("failed to read remote config: {}", e))?;
let patched = match &claw.adapter {
ClawKind::OpenClawChannel => patch_openclaw_config(
¤t,
&claw.name,
claw.auth_token.as_deref(),
claw.reply_webhook.as_deref(),
claw.reply_auth_token.as_deref(),
claw.policy_endpoint.as_deref(),
)
.map_err(|e| anyhow::anyhow!("failed to patch openclaw.json: {}", e))?,
ClawKind::ZeroClawNative => {
// ZeroClaw uses TOML — full patching deferred; use safe stub for now.
// TODO (follow-on): implement real TOML patching for ZeroClaw config.
patch_zeroclaw_config_stub(¤t, &claw.name)
}
_ => {
// Non-SSH adapters should never reach apply_remote_config.
return Err(anyhow::anyhow!(
"apply_remote_config called for non-SSH adapter '{}'",
claw.adapter.kind_label()
));
}
};
deps.ssh
.write_file(&claw.host, key, &config_path, &patched)
.map_err(|e| anyhow::anyhow!("failed to write patched config: {}", e))?;
| if matches!(claw.adapter, ClawKind::OpenClawChannel) { | ||
| if let Some(detail) = configure_remote_openclaw_proxy_env(claw, deps)? { | ||
| details.push(detail); | ||
| install_remote_openclaw_channel_plugin(claw, deps)?; | ||
| details.push("installed Calciforge OpenClaw channel plugin".to_string()); | ||
|
|
||
| let restarted_by_proxy = | ||
| if let Some(detail) = configure_remote_openclaw_proxy_env(claw, deps)? { | ||
| details.push(detail); | ||
| true | ||
| } else { | ||
| false | ||
| }; | ||
| if !restarted_by_proxy { | ||
| details.push(restart_remote_openclaw_service(claw, deps)?); | ||
| } | ||
| } |
| fn patch_openclaw_config( | ||
| current_content: &str, | ||
| _claw_name: &str, | ||
| _calciforge_endpoint: &str, | ||
| auth_token: Option<&str>, | ||
| reply_webhook: Option<&str>, | ||
| reply_auth_token: Option<&str>, | ||
| policy_endpoint: Option<&str>, | ||
| ) -> Result<String> { | ||
| // Parse the existing config (handles JSON5 / JSONC comments). |
| @@ -1130,6 +1312,8 @@ mod tests { | |||
| ssh.push_success(""); | |||
| // apply: read-back verify (new in S1 implementation) | |||
| ssh.push_success(r#"{"version": "2026.3.13", "hooks": {"enabled": true, "entries": {"test-claw": {"enabled": true, "url": "http://host:18789", "token": "tok"}}}}"#); | |||
| id: "calciforge-channel", | ||
| name: "Calciforge", | ||
| description: "Calciforge inbound channel", | ||
| configSchema: { type: "object", properties: {}, additionalProperties: true }, |
| } | ||
| } | ||
|
|
||
| fn is_openclaw_model_id(model: &str) -> bool { |
| /// patch_openclaw_config preserves existing hooks fields without mutation. | ||
| #[test] | ||
| fn patch_openclaw_config_preserves_existing_hooks() { | ||
| let input = | ||
| r#"{"hooks": {"enabled": false, "entries": {"calciforge": {"enabled": true}}}}"#; |
| fn collect_managed_agent_auth( | ||
| adapter_str: &str, | ||
| ) -> Result<(Option<String>, Option<String>, Option<String>)> { | ||
| if adapter_str != "openclaw-channel" { | ||
| return Ok((None, None, None)); | ||
| } | ||
|
|
||
| let auth_token: String = Password::new() | ||
| .with_prompt(" Inbound bearer token (must match Calciforge agent api_key)") | ||
| .interact() | ||
| .context("failed to read inbound token")?; | ||
| let reply_webhook: String = Input::new() | ||
| .with_prompt(" Calciforge reply webhook URL") | ||
| .interact_text() | ||
| .context("failed to read reply webhook")?; | ||
| let reply_auth_token: String = Password::new() | ||
| .with_prompt(" Reply webhook bearer token (must match reply_auth_token)") | ||
| .interact() | ||
| .context("failed to read reply token")?; | ||
|
|
||
| Ok(( | ||
| Some(auth_token.trim().to_string()), | ||
| Some(reply_webhook.trim().to_string()), | ||
| Some(reply_auth_token.trim().to_string()), | ||
| )) |
| async function readJsonBody(req) { | ||
| const chunks = []; | ||
| await new Promise((resolve, reject) => { | ||
| req.on("data", (chunk) => chunks.push(chunk)); | ||
| req.on("end", resolve); | ||
| req.on("error", reject); | ||
| }); | ||
| return JSON.parse(Buffer.concat(chunks).toString("utf8")); |
430e306 to
a08da97
Compare
Summary
openclaw-channelmanaged-agent setupapi.registerHttpRoute(...)plugin HTTP API, with the older internal route registry only as a compatibility fallback for deployed versions that lack the public APIVerification
node --check crates/calciforge-openclaw-channel-plugin/index.jsjq . crates/calciforge-openclaw-channel-plugin/openclaw.plugin.json >/dev/nullcargo test -p calciforge install:: -- --nocapturecargo test -p calciforge openclaw -- --nocapturecargo test -p calciforge agents_doc -- --nocapturegit diff --checkcargo test -p calciforgeReview Notes
api.registerHttpRoute(...)as the clean plugin route API. The plugin uses that first. The internal hashed route registry remains only as a fallback so we can test against older installed gateways without blocking the branch.ClawTargetfields and docs frame the reusable managed-agent shape, but ZeroClaw still needs the real TOML patcher and equivalent callback/auth installer path.