Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9cea87fae | |||
| 6213bcab07 | |||
| fe9f58f917 | |||
| 04c7ce4488 | |||
| 5eea95ef2a | |||
| af1c37c2fb | |||
| e3e4aef21c | |||
| a48e335be9 | |||
| fba15520dc | |||
| 7504da1117 | |||
| 6292cdfe1c | |||
| 693661b564 | |||
| 2bad6678ec | |||
| e3e9db5210 |
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)' }}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user