Compare commits

...

14 Commits

Author SHA1 Message Date
Argenis d9cea87fae Merge pull request #3998 from zeroclaw-labs/fix/images-2
fix(docs): absolute banner URLs + web dashboard logo update
2026-03-19 16:47:09 -04:00
argenis de la rosa 6213bcab07 fix(docs): use absolute URLs for banner in all READMEs + update web dashboard logo
- Replace relative docs/assets/zeroclaw-banner.png paths with absolute
  raw.githubusercontent.com URLs in all 31 README files so the banner
  renders correctly regardless of where the README is viewed
- Switch web dashboard favicon and logos from logo.png to zeroclaw-trans.png
- Add zeroclaw-trans.png and zeroclaw-banner.png assets
- Update build.rs to track new dashboard asset
- Fix missing autonomy_level in new test + Box::pin large future

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 16:34:32 -04:00
argenis de la rosa fe9f58f917 fix(docs): use absolute URLs for banner in all READMEs + update web dashboard logo
- Replace relative docs/assets/zeroclaw-banner.png paths with absolute
  raw.githubusercontent.com URLs in all 31 README files so the banner
  renders correctly regardless of where the README is viewed
- Switch web dashboard favicon and logos from logo.png to zeroclaw-trans.png
- Add zeroclaw-trans.png and zeroclaw-banner.png assets
- Update build.rs to track new dashboard asset

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 16:31:21 -04:00
Argenis 04c7ce4488 Merge pull request #3955 from Alix-007/issue-3952-full-autonomy-channel-prompt
fix(prompt): respect autonomy level in channel prompts
2026-03-19 16:12:13 -04:00
Argenis 5eea95ef2a Merge pull request #3899 from Alix-007/issue-3842-openrouter-timeout
fix(openrouter): respect provider_timeout_secs on slow responses
2026-03-19 16:06:57 -04:00
argenis de la rosa af1c37c2fb fix: pass autonomy_level through to prompt builder in wrapper function
build_system_prompt_with_mode was discarding the autonomy_level
parameter, passing None to build_system_prompt_with_mode_and_autonomy.
This caused full-autonomy prompts to still include "ask before acting"
instructions. Convert the level to an AutonomyConfig and pass it through.
2026-03-19 15:56:37 -04:00
argenis de la rosa e3e4aef21c fix: box-pin large future in config init test to satisfy clippy
Config::load_or_init() produces a future >16KB, triggering
clippy::large_futures. Wrap with Box::pin() as recommended.
2026-03-19 15:44:41 -04:00
argenis de la rosa a48e335be9 fix: box-pin large future in config init test to satisfy clippy
Config::load_or_init() produces a future >16KB, triggering
clippy::large_futures. Wrap with Box::pin() as recommended.
2026-03-19 15:44:19 -04:00
argenis de la rosa fba15520dc fix: add missing autonomy_level field to test ChannelRuntimeContext
The full_autonomy_prompt test was missing the autonomy_level field
added to ChannelRuntimeContext by a recently merged PR.
2026-03-19 15:32:23 -04:00
argenis de la rosa 7504da1117 fix: add missing autonomy_level arg to test after merge with master
The refresh-skills test was missing the autonomy_level parameter
added to build_system_prompt_with_mode and ChannelRuntimeContext
by a recently merged PR.
2026-03-19 15:31:18 -04:00
argenis de la rosa 6292cdfe1c Merge origin/master into issue-3952-full-autonomy-channel-prompt
Resolve conflict in src/channels/mod.rs Safety section. Keeps the
PR's AutonomyConfig-based prompt construction (build_system_prompt_with_mode_and_autonomy)
while incorporating master's granular safety rules (conditional
destructive-command and ask-before-acting lines based on autonomy level).
Also fixes missing autonomy_level arg in refresh-skills test and removes
duplicate autonomy.level args from auto-merged call sites.
2026-03-19 15:27:43 -04:00
argenis de la rosa 693661b564 Merge origin/master into issue-3842-openrouter-timeout
Resolve merge conflicts keeping the PR's changes:
- timeout_secs parameter in OpenRouterProvider::new()
- read_response_body + parse_response_body pattern
- OPENROUTER_CONNECT_TIMEOUT_SECS and DEFAULT_OPENROUTER_TIMEOUT_SECS constants
- Update master's new tests to use two-arg new() signature
2026-03-19 15:19:32 -04:00
Alix-007 2bad6678ec fix(prompt): respect autonomy level in channel prompts 2026-03-19 16:54:51 +08:00
Alix-007 e3e9db5210 fix(openrouter): respect provider timeout config 2026-03-19 01:36:57 +08:00
42 changed files with 292 additions and 96 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center" dir="rtl">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀(日本語)</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀(Русский)</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
<img src="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/docs/assets/zeroclaw-banner.png" alt="ZeroClaw" width="600" />
</p>
<h1 align="center">ZeroClaw 🦀(简体中文)</h1>
+20
View File
@@ -11,6 +11,7 @@ fn main() {
println!("cargo:rerun-if-changed=web/src");
println!("cargo:rerun-if-changed=web/public");
println!("cargo:rerun-if-changed=web/index.html");
println!("cargo:rerun-if-changed=docs/assets/zeroclaw-trans.png");
println!("cargo:rerun-if-changed=web/package.json");
println!("cargo:rerun-if-changed=web/package-lock.json");
println!("cargo:rerun-if-changed=web/tsconfig.json");
@@ -83,6 +84,7 @@ fn main() {
}
ensure_dist_dir(dist_dir);
ensure_dashboard_assets(dist_dir);
}
fn web_build_required(web_dir: &Path, dist_dir: &Path) -> bool {
@@ -136,6 +138,24 @@ fn ensure_dist_dir(dist_dir: &Path) {
}
}
fn ensure_dashboard_assets(dist_dir: &Path) {
// The Rust gateway serves `web/dist/` via rust-embed under `/_app/*`.
// Some builds may end up with missing/blank logo assets, so we ensure the
// expected image is always present in `web/dist/` at compile time.
let src = Path::new("docs/assets/zeroclaw-trans.png");
if !src.exists() {
eprintln!(
"cargo:warning=docs/assets/zeroclaw-trans.png not found; skipping dashboard asset copy"
);
return;
}
let dst = dist_dir.join("zeroclaw-trans.png");
if let Err(e) = fs::copy(src, &dst) {
eprintln!("cargo:warning=Failed to copy zeroclaw-trans.png into web/dist/: {e}");
}
}
/// Locate the `npm` binary on the system PATH.
fn which_npm() -> Result<String, ()> {
let cmd = if cfg!(target_os = "windows") {
Binary file not shown.

After

Width:  |  Height:  |  Size: 851 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

+4 -4
View File
@@ -3571,16 +3571,16 @@ pub async fn run(
None
};
let native_tools = provider.supports_native_tools();
let mut system_prompt = crate::channels::build_system_prompt_with_mode(
let mut system_prompt = crate::channels::build_system_prompt_with_mode_and_autonomy(
&config.workspace_dir,
&model_name,
&tool_descs,
&skills,
Some(&config.identity),
bootstrap_max_chars,
Some(&config.autonomy),
native_tools,
config.skills.prompt_injection_mode,
config.autonomy.level,
);
// Append structured tool-use instructions with schemas (only for non-native providers)
@@ -4228,16 +4228,16 @@ pub async fn process_message(
None
};
let native_tools = provider.supports_native_tools();
let mut system_prompt = crate::channels::build_system_prompt_with_mode(
let mut system_prompt = crate::channels::build_system_prompt_with_mode_and_autonomy(
&config.workspace_dir,
&model_name,
&tool_descs,
&skills,
Some(&config.identity),
bootstrap_max_chars,
Some(&config.autonomy),
native_tools,
config.skills.prompt_injection_mode,
config.autonomy.level,
);
if !native_tools {
system_prompt.push_str(&build_tool_instructions(&tools_registry, Some(&i18n_descs)));
+121 -7
View File
@@ -2895,6 +2895,34 @@ pub fn build_system_prompt_with_mode(
native_tools: bool,
skills_prompt_mode: crate::config::SkillsPromptInjectionMode,
autonomy_level: AutonomyLevel,
) -> String {
let autonomy_cfg = crate::config::AutonomyConfig {
level: autonomy_level,
..Default::default()
};
build_system_prompt_with_mode_and_autonomy(
workspace_dir,
model_name,
tools,
skills,
identity_config,
bootstrap_max_chars,
Some(&autonomy_cfg),
native_tools,
skills_prompt_mode,
)
}
pub fn build_system_prompt_with_mode_and_autonomy(
workspace_dir: &std::path::Path,
model_name: &str,
tools: &[(&str, &str)],
skills: &[crate::skills::Skill],
identity_config: Option<&crate::config::IdentityConfig>,
bootstrap_max_chars: Option<usize>,
autonomy_config: Option<&crate::config::AutonomyConfig>,
native_tools: bool,
skills_prompt_mode: crate::config::SkillsPromptInjectionMode,
) -> String {
use std::fmt::Write;
let mut prompt = String::with_capacity(8192);
@@ -2961,16 +2989,28 @@ pub fn build_system_prompt_with_mode(
// ── 2. Safety ───────────────────────────────────────────────
prompt.push_str("## Safety\n\n");
prompt.push_str("- Do not exfiltrate private data.\n");
if autonomy_level != AutonomyLevel::Full {
if autonomy_config.map(|cfg| cfg.level) != Some(crate::security::AutonomyLevel::Full) {
prompt.push_str(
"- Do not run destructive commands without asking.\n\
- Do not bypass oversight or approval mechanisms.\n",
);
}
prompt.push_str("- Prefer `trash` over `rm` (recoverable beats gone forever).\n");
if autonomy_level != AutonomyLevel::Full {
prompt.push_str("- When in doubt, ask before acting externally.\n");
}
prompt.push_str(match autonomy_config.map(|cfg| cfg.level) {
Some(crate::security::AutonomyLevel::Full) => {
"- Respect the runtime autonomy policy: if a tool or action is allowed, execute it directly instead of asking the user for extra approval.\n\
- If a tool or action is blocked by policy or unavailable, explain that concrete restriction instead of simulating an approval dialog.\n"
}
Some(crate::security::AutonomyLevel::ReadOnly) => {
"- Respect the runtime autonomy policy: this runtime is read-only for side effects unless a tool explicitly reports otherwise.\n\
- If a requested action is blocked by policy, explain the restriction directly instead of simulating an approval dialog.\n"
}
_ => {
"- When in doubt, ask before acting externally.\n\
- Respect the runtime autonomy policy: ask for approval only when the current runtime policy actually requires it.\n\
- If a tool or action is blocked by policy or unavailable, explain that concrete restriction instead of simulating an approval dialog.\n"
}
});
prompt.push('\n');
// ── 3. Skills (full or compact, based on config) ─────────────
@@ -3053,6 +3093,20 @@ pub fn build_system_prompt_with_mode(
prompt.push_str("## Channel Capabilities\n\n");
prompt.push_str("- You are running as a messaging bot. Your response is automatically sent back to the user's channel.\n");
prompt.push_str("- You do NOT need to ask permission to respond — just respond directly.\n");
prompt.push_str(match autonomy_config.map(|cfg| cfg.level) {
Some(crate::security::AutonomyLevel::Full) => {
"- If the runtime policy already allows a tool, use it directly; do not ask the user for extra approval.\n\
- Never pretend you are waiting for a human approval click or confirmation when the runtime policy already permits the action.\n\
- If the runtime policy blocks an action, say that directly instead of simulating an approval flow.\n"
}
Some(crate::security::AutonomyLevel::ReadOnly) => {
"- This runtime may reject write-side effects; if that happens, explain the policy restriction directly instead of simulating an approval flow.\n"
}
_ => {
"- Ask for approval only when the runtime policy actually requires it.\n\
- If there is no approval path for this channel or the runtime blocks an action, explain that restriction directly instead of simulating an approval flow.\n"
}
});
prompt.push_str("- NEVER repeat, describe, or echo credentials, tokens, API keys, or secrets in your responses.\n");
prompt.push_str("- If a tool output contains credentials, they have already been redacted — do not mention them.\n");
prompt.push_str("- When a user sends a voice note, it is automatically transcribed to text. Your text reply is automatically converted to a voice note and sent back. Do NOT attempt to generate audio yourself — TTS is handled by the channel.\n");
@@ -4133,16 +4187,16 @@ pub async fn start_channels(config: Config) -> Result<()> {
None
};
let native_tools = provider.supports_native_tools();
let mut system_prompt = build_system_prompt_with_mode(
let mut system_prompt = build_system_prompt_with_mode_and_autonomy(
&workspace,
&model,
&tool_descs,
&skills,
Some(&config.identity),
bootstrap_max_chars,
Some(&config.autonomy),
native_tools,
config.skills.prompt_injection_mode,
config.autonomy.level,
);
if !native_tools {
system_prompt.push_str(&build_tool_instructions(
@@ -6976,7 +7030,7 @@ BTC is currently around $65,000 based on latest tool output."#
let prompt = build_system_prompt(ws.path(), "model", &[], &[], None, None);
assert!(prompt.contains("Do not exfiltrate private data"));
assert!(prompt.contains("Do not run destructive commands"));
assert!(prompt.contains("Respect the runtime autonomy policy"));
assert!(prompt.contains("Prefer `trash` over `rm`"));
}
@@ -7251,6 +7305,64 @@ BTC is currently around $65,000 based on latest tool output."#
);
}
#[test]
fn full_autonomy_prompt_executes_allowed_tools_without_extra_approval() {
let ws = make_workspace();
let config = crate::config::AutonomyConfig {
level: crate::security::AutonomyLevel::Full,
..crate::config::AutonomyConfig::default()
};
let prompt = build_system_prompt_with_mode_and_autonomy(
ws.path(),
"model",
&[],
&[],
None,
None,
Some(&config),
false,
crate::config::SkillsPromptInjectionMode::Full,
);
assert!(
prompt.contains("execute it directly instead of asking the user for extra approval"),
"full autonomy should instruct direct execution for allowed tools"
);
assert!(
prompt.contains("Never pretend you are waiting for a human approval"),
"full autonomy should not simulate interactive approval flows"
);
}
#[test]
fn readonly_prompt_explains_policy_blocks_without_fake_approval() {
let ws = make_workspace();
let config = crate::config::AutonomyConfig {
level: crate::security::AutonomyLevel::ReadOnly,
..crate::config::AutonomyConfig::default()
};
let prompt = build_system_prompt_with_mode_and_autonomy(
ws.path(),
"model",
&[],
&[],
None,
None,
Some(&config),
false,
crate::config::SkillsPromptInjectionMode::Full,
);
assert!(
prompt.contains("this runtime is read-only for side effects"),
"read-only prompt should expose the runtime restriction"
);
assert!(
prompt.contains("instead of simulating an approval flow"),
"read-only prompt should explain restrictions instead of faking approval"
);
}
#[test]
fn prompt_workspace_path() {
let ws = make_workspace();
@@ -7642,6 +7754,7 @@ BTC is currently around $65,000 based on latest tool output."#
None,
false,
config.skills.prompt_injection_mode,
AutonomyLevel::default(),
);
assert!(
!initial_system_prompt.contains("refresh-test"),
@@ -7686,6 +7799,7 @@ BTC is currently around $65,000 based on latest tool output."#
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
non_cli_excluded_tools: Arc::new(Vec::new()),
autonomy_level: AutonomyLevel::default(),
tool_call_dedup_exempt: Arc::new(Vec::new()),
model_routes: Arc::new(Vec::new()),
query_classification: crate::config::QueryClassificationConfig::default(),
+1 -1
View File
@@ -10798,7 +10798,7 @@ default_model = "persisted-profile"
let dispatch = tracing::Dispatch::new(subscriber);
let guard = tracing::dispatcher::set_default(&dispatch);
let config = Config::load_or_init().await.unwrap();
let config = Box::pin(Config::load_or_init()).await.unwrap();
drop(guard);
let logs = capture.captured();
+4 -7
View File
@@ -1119,13 +1119,10 @@ fn create_provider_with_url_and_options(
)?))
}
// ── Primary providers (custom implementations) ───────
"openrouter" => {
let mut p = openrouter::OpenRouterProvider::new(key);
if let Some(t) = options.provider_timeout_secs {
p = p.with_timeout_secs(t);
}
Ok(Box::new(p))
}
"openrouter" => Ok(Box::new(openrouter::OpenRouterProvider::new(
key,
options.provider_timeout_secs,
))),
"anthropic" => Ok(Box::new(anthropic::AnthropicProvider::new(key))),
"openai" => Ok(Box::new(openai::OpenAiProvider::with_base_url(api_url, key))),
// Ollama uses api_url for custom base URL (e.g. remote Ollama instance)
+108 -43
View File
@@ -4,9 +4,9 @@ use crate::providers::traits::{
Provider, ProviderCapabilities, TokenUsage, ToolCall as ProviderToolCall,
};
use crate::tools::ToolSpec;
use anyhow::Context as _;
use async_trait::async_trait;
use reqwest::Client;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
pub struct OpenRouterProvider {
@@ -14,6 +14,9 @@ pub struct OpenRouterProvider {
timeout_secs: u64,
}
const DEFAULT_OPENROUTER_TIMEOUT_SECS: u64 = 120;
const OPENROUTER_CONNECT_TIMEOUT_SECS: u64 = 10;
#[derive(Debug, Serialize)]
struct ChatRequest {
model: String,
@@ -148,10 +151,12 @@ struct NativeResponseMessage {
}
impl OpenRouterProvider {
pub fn new(credential: Option<&str>) -> Self {
pub fn new(credential: Option<&str>, timeout_secs: Option<u64>) -> Self {
Self {
credential: credential.map(ToString::to_string),
timeout_secs: 120,
timeout_secs: timeout_secs
.filter(|secs| *secs > 0)
.unwrap_or(DEFAULT_OPENROUTER_TIMEOUT_SECS),
}
}
@@ -304,11 +309,43 @@ impl OpenRouterProvider {
}
}
fn compact_sanitized_body_snippet(body: &str) -> String {
super::sanitize_api_error(body)
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
}
async fn read_response_body(
provider_name: &str,
response: reqwest::Response,
) -> anyhow::Result<String> {
response.text().await.map_err(|error| {
let sanitized = super::sanitize_api_error(&error.to_string());
anyhow::anyhow!(
"{provider_name} transport error while reading response body: {sanitized}"
)
})
}
fn parse_response_body<T: DeserializeOwned>(
provider_name: &str,
body: &str,
kind: &str,
) -> anyhow::Result<T> {
serde_json::from_str::<T>(body).map_err(|error| {
let snippet = Self::compact_sanitized_body_snippet(body);
anyhow::anyhow!(
"{provider_name} API returned an unexpected {kind} payload: {error}; body={snippet}"
)
})
}
fn http_client(&self) -> Client {
crate::config::build_runtime_proxy_client_with_timeouts(
"provider.openrouter",
self.timeout_secs,
10,
OPENROUTER_CONNECT_TIMEOUT_SECS,
)
}
}
@@ -381,13 +418,9 @@ impl Provider for OpenRouterProvider {
return Err(super::api_error("OpenRouter", response).await);
}
let text = response.text().await?;
let chat_response: ApiChatResponse = serde_json::from_str(&text).with_context(|| {
format!(
"OpenRouter: failed to decode response body: {}",
&text[..text.len().min(500)]
)
})?;
let body = Self::read_response_body("OpenRouter", response).await?;
let chat_response =
Self::parse_response_body::<ApiChatResponse>("OpenRouter", &body, "chat-completions")?;
chat_response
.choices
@@ -434,13 +467,9 @@ impl Provider for OpenRouterProvider {
return Err(super::api_error("OpenRouter", response).await);
}
let text = response.text().await?;
let chat_response: ApiChatResponse = serde_json::from_str(&text).with_context(|| {
format!(
"OpenRouter: failed to decode response body: {}",
&text[..text.len().min(500)]
)
})?;
let body = Self::read_response_body("OpenRouter", response).await?;
let chat_response =
Self::parse_response_body::<ApiChatResponse>("OpenRouter", &body, "chat-completions")?;
chat_response
.choices
@@ -485,14 +514,9 @@ impl Provider for OpenRouterProvider {
return Err(super::api_error("OpenRouter", response).await);
}
let text = response.text().await?;
let native_response: NativeChatResponse =
serde_json::from_str(&text).with_context(|| {
format!(
"OpenRouter: failed to decode response body: {}",
&text[..text.len().min(500)]
)
})?;
let body = Self::read_response_body("OpenRouter", response).await?;
let native_response =
Self::parse_response_body::<NativeChatResponse>("OpenRouter", &body, "native chat")?;
let usage = native_response.usage.map(|u| TokenUsage {
input_tokens: u.prompt_tokens,
output_tokens: u.completion_tokens,
@@ -584,14 +608,9 @@ impl Provider for OpenRouterProvider {
return Err(super::api_error("OpenRouter", response).await);
}
let text = response.text().await?;
let native_response: NativeChatResponse =
serde_json::from_str(&text).with_context(|| {
format!(
"OpenRouter: failed to decode response body: {}",
&text[..text.len().min(500)]
)
})?;
let body = Self::read_response_body("OpenRouter", response).await?;
let native_response =
Self::parse_response_body::<NativeChatResponse>("OpenRouter", &body, "native chat")?;
let usage = native_response.usage.map(|u| TokenUsage {
input_tokens: u.prompt_tokens,
output_tokens: u.completion_tokens,
@@ -616,7 +635,7 @@ mod tests {
#[test]
fn capabilities_report_vision_support() {
let provider = OpenRouterProvider::new(Some("openrouter-test-credential"));
let provider = OpenRouterProvider::new(Some("openrouter-test-credential"), None);
let caps = <OpenRouterProvider as Provider>::capabilities(&provider);
assert!(caps.native_tool_calling);
assert!(caps.vision);
@@ -624,7 +643,7 @@ mod tests {
#[test]
fn creates_with_key() {
let provider = OpenRouterProvider::new(Some("openrouter-test-credential"));
let provider = OpenRouterProvider::new(Some("openrouter-test-credential"), None);
assert_eq!(
provider.credential.as_deref(),
Some("openrouter-test-credential")
@@ -633,20 +652,32 @@ mod tests {
#[test]
fn creates_without_key() {
let provider = OpenRouterProvider::new(None);
let provider = OpenRouterProvider::new(None, None);
assert!(provider.credential.is_none());
}
#[test]
fn uses_configured_timeout_when_provided() {
let provider = OpenRouterProvider::new(Some("openrouter-test-credential"), Some(1200));
assert_eq!(provider.timeout_secs, 1200);
}
#[test]
fn falls_back_to_default_timeout_for_zero() {
let provider = OpenRouterProvider::new(Some("openrouter-test-credential"), Some(0));
assert_eq!(provider.timeout_secs, DEFAULT_OPENROUTER_TIMEOUT_SECS);
}
#[tokio::test]
async fn warmup_without_key_is_noop() {
let provider = OpenRouterProvider::new(None);
let provider = OpenRouterProvider::new(None, None);
let result = provider.warmup().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn chat_with_system_fails_without_key() {
let provider = OpenRouterProvider::new(None);
let provider = OpenRouterProvider::new(None, None);
let result = provider
.chat_with_system(Some("system"), "hello", "openai/gpt-4o", 0.2)
.await;
@@ -657,7 +688,7 @@ mod tests {
#[tokio::test]
async fn chat_with_history_fails_without_key() {
let provider = OpenRouterProvider::new(None);
let provider = OpenRouterProvider::new(None, None);
let messages = vec![
ChatMessage {
role: "system".into(),
@@ -752,9 +783,43 @@ mod tests {
assert!(response.choices.is_empty());
}
#[test]
fn parse_chat_response_body_reports_sanitized_snippet() {
let body = r#"{"choices":"invalid","api_key":"sk-test-secret-value"}"#;
let err = OpenRouterProvider::parse_response_body::<ApiChatResponse>(
"OpenRouter",
body,
"chat-completions",
)
.expect_err("payload should fail");
let msg = err.to_string();
assert!(msg.contains("OpenRouter API returned an unexpected chat-completions payload"));
assert!(msg.contains("body="));
assert!(msg.contains("[REDACTED]"));
assert!(!msg.contains("sk-test-secret-value"));
}
#[test]
fn parse_native_response_body_reports_sanitized_snippet() {
let body = r#"{"choices":123,"api_key":"sk-another-secret"}"#;
let err = OpenRouterProvider::parse_response_body::<NativeChatResponse>(
"OpenRouter",
body,
"native chat",
)
.expect_err("payload should fail");
let msg = err.to_string();
assert!(msg.contains("OpenRouter API returned an unexpected native chat payload"));
assert!(msg.contains("body="));
assert!(msg.contains("[REDACTED]"));
assert!(!msg.contains("sk-another-secret"));
}
#[tokio::test]
async fn chat_with_tools_fails_without_key() {
let provider = OpenRouterProvider::new(None);
let provider = OpenRouterProvider::new(None, None);
let messages = vec![ChatMessage {
role: "user".into(),
content: "What is the date?".into(),
@@ -1063,13 +1128,13 @@ mod tests {
#[test]
fn default_timeout_is_120() {
let provider = OpenRouterProvider::new(Some("key"));
let provider = OpenRouterProvider::new(Some("key"), None);
assert_eq!(provider.timeout_secs, 120);
}
#[test]
fn with_timeout_secs_overrides_default() {
let provider = OpenRouterProvider::new(Some("key")).with_timeout_secs(300);
let provider = OpenRouterProvider::new(Some("key"), None).with_timeout_secs(300);
assert_eq!(provider.timeout_secs, 300);
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark" />
<link rel="icon" type="image/png" href="/_app/logo.png" />
<link rel="icon" type="image/png" href="/_app/zeroclaw-trans.png" />
<title>ZeroClaw</title>
</head>
<body>
+1 -1
View File
@@ -135,7 +135,7 @@ function PairingDialog({ onPair }: { onPair: (code: string) => Promise<void> })
<div className="text-center mb-8">
<img
src="/_app/logo.png"
src="/_app/zeroclaw-trans.png"
alt="ZeroClaw"
className="h-20 w-20 rounded-2xl object-cover mx-auto mb-4 animate-float"
style={{ boxShadow: '0 0 30px rgba(0,128,255,0.3)' }}
+1 -1
View File
@@ -35,7 +35,7 @@ export default function Sidebar() {
{/* Logo / Title */}
<div className="flex items-center gap-3 px-4 py-4 border-b border-[#1a1a3e]/50">
<img
src="/_app/logo.png"
src="/_app/zeroclaw-trans.png"
alt="ZeroClaw"
className="h-10 w-10 rounded-xl object-cover animate-pulse-glow"
/>